from pathlib import Path root = Path(r'C:\kefu') path = root / 'wechat_rpa/knowledge_store.py' s = path.read_text(encoding='utf-8') insert = ''' def job_scope(tenant, options, cutoff): """Use identical scope and cutoff predicates for preview, progress and worker.""" where = ["m.tenant_id=?", "m.source_account_id=?", "m.created_at<=?", "c.conversation_type IN ('direct_wechat','direct_wecom')"] args = [tenant, options["source_account_id"], cutoff] for key, condition in (("date_start", "m.sent_at>=?"), ("date_end", "m.sent_at= end: raise ValueError("结束日期不能早于开始日期") if options.get("engine", "rules") not in {"rules", "model"}: raise ValueError("未知整理方式") for key, default, maximum in (("max_messages", 10000, 1000000), ("max_model_calls", 100, 10000)): value = options.get(key, default) if not isinstance(value, int) or isinstance(value, bool) or not 1 <= value <= maximum: raise ValueError("处理上限超出允许范围") options[key] = value options.update(date_start=start, date_end=end, pipeline_version=PIPELINE_VERSION) return options ''' s = s.replace('\nclass KnowledgeStore:', '\n' + insert + '\nclass KnowledgeStore:') s = s.replace(' result.pop("cursor", None)\n return result', ''' result.pop("cursor", None) if "options" in result: total = result["options"].get("total_messages") result["total_messages"] = total result["completion_reason"] = result["options"].get("completion_reason", "") result["progress_percent"] = (100 if result["status"] == "completed" else min(99, int(result["processed"] * 100 / total)) if total else None) return result''') s = s.replace(''' with self.database.connect() as db: db.execute("INSERT INTO knowledge_settings''', ''' with self.database.connect() as db: if enabled: from knowledge_retriever import KnowledgeRetriever available = db.execute("SELECT id FROM knowledge_item WHERE tenant_id=? AND status='published' " "AND (valid_until='' OR valid_until>=?)", (tenant, KnowledgeRetriever._today())) if not any(self._fresh(db, row["id"], tenant) for row in available): raise ValueError("请先审核并发布至少一条有效知识,再开启客服使用知识") db.execute("INSERT INTO knowledge_settings''') s = s.replace(''' if action == "approve": ''', ''' if action == "disable" and row["status"] != "published": raise ValueError("仅已发布知识可停用") if action == "approve": self._check_expiry(row) ''') s = s.replace(''' def publish(self, tenant, item_id, revision, actor, ip):''', ''' @staticmethod def _check_expiry(row): from knowledge_retriever import KnowledgeRetriever if row["valid_until"] and row["valid_until"] < KnowledgeRetriever._today(): raise ValueError("知识已过有效期,请更新内容和有效期后重新审核") def publish(self, tenant, item_id, revision, actor, ip):''') s = s.replace(''' index = VectorIndex() profile = index.publish(row)''', ''' self._check_expiry(row) index = VectorIndex() profile = index.publish(row)''') start = s.index(' def create_job(') end = s.index(' def job(self,', start) s = s[:start] + ''' def _scope_counts(self, db, tenant, options, cutoff): source = db.execute("SELECT id,display_name,external_account_id FROM archive_source_account " "WHERE id=? AND tenant_id=?", (options["source_account_id"], tenant)).fetchone() if source is None: raise ValueError("请选择当前租户的客服来源账号") where, args = job_scope(tenant, options, cutoff) row = db.execute("""SELECT COUNT(*) available_messages,COUNT(DISTINCT m.conversation_id) conversations, COALESCE(SUM(v.status='normal' AND lower(m.message_type) IN ('text','文本','文字','1') AND trim(v.content)!='' AND m.sender_person_id IS NOT NULL AND m.direction IN ('inbound','outbound') AND ((m.direction='outbound')=EXISTS(SELECT 1 FROM archive_person_identity pi WHERE pi.person_id=m.sender_person_id AND pi.tenant_id=m.tenant_id AND pi.external_id=a.external_account_id))),0) eligible_text_messages FROM archive_message m JOIN archive_conversation c ON c.id=m.conversation_id JOIN archive_source_account a ON a.id=m.source_account_id JOIN archive_message_version v ON v.message_id=m.id AND v.version_no=( SELECT MAX(v2.version_no) FROM archive_message_version v2 WHERE v2.message_id=m.id AND v2.created_at<=?) WHERE """ + " AND ".join(where), [cutoff, *args]).fetchone() result = dict(row) result.update(total_messages=min(row["available_messages"], options["max_messages"]), limited=row["available_messages"] > options["max_messages"], cutoff_at=cutoff, source_name=source["display_name"] or source["external_account_id"]) return result def preview_job(self, tenant, values): tenant = safe_scope(tenant) options = job_options(values) with self.database.connect() as db: return self._scope_counts(db, tenant, options, utc_now()) def create_job(self, tenant, options, actor, ip): tenant = safe_scope(tenant) options = job_options(options) if not options.get("staff_confirmed"): raise ValueError("请确认所选来源账号是客服账号,且这些聊天允许用于知识整理") scope_key = fingerprint(json_text({k: options.get(k, "") for k in ( "source_account_id", "conversation_id", "date_start", "date_end", "engine", "max_messages", "max_model_calls")})) with self.database.connect() as db: db.execute("BEGIN IMMEDIATE") active = db.execute("SELECT options_json FROM knowledge_job WHERE tenant_id=? " "AND status IN ('queued','running','paused')", (tenant,)) if any(json.loads(row[0]).get("scope_key") == scope_key for row in active): raise ValueError("相同范围已有未完成任务,请在加工任务中继续或取消原任务") job_id, now = new_id(), utc_now() estimate = self._scope_counts(db, tenant, options, now) options.update(total_messages=estimate["total_messages"], available_messages=estimate["available_messages"], source_name=estimate["source_name"], scope_key=scope_key) db.execute("INSERT INTO knowledge_job(id,tenant_id,options_json,cutoff_at,created_by,created_at,updated_at) " "VALUES (?,?,?,?,?,?,?)", (job_id, tenant, json_text(options), now, actor, now, now)) if options.get("auto_watch"): db.execute("INSERT INTO knowledge_watch VALUES (?,?,1,?) ON CONFLICT(tenant_id,source_account_id) " "DO UPDATE SET enabled=1,created_by=excluded.created_by", (tenant, options["source_account_id"], actor)) self.database._audit(db, actor, "knowledge.job.create", f"job={job_id} tenant={tenant}", ip) return self.job(tenant, job_id) ''' + s[end:] s = s.replace('if action not in {"pause", "resume"}:','if action not in {"pause", "resume", "cancel"}:') s = s.replace(''' allowed = {"queued", "running"} if action == "pause" else {"paused", "failed"}''',''' allowed = ({"queued", "running"} if action == "pause" else {"paused", "failed"} if action == "resume" else {"queued", "running", "paused", "failed"})''') s = s.replace('''("paused" if action == "pause" else "queued", utc_now(), job_id))''','''({"pause": "paused", "resume": "queued", "cancel": "cancelled"}[action], utc_now(), job_id)) if action == "cancel": db.execute("UPDATE knowledge_job SET state_json='{}' WHERE id=?", (job_id,))''') path.write_text(s, encoding='utf-8', newline='\n') path = root / 'wechat_rpa/knowledge_worker.py' s = path.read_text(encoding='utf-8').replace('from knowledge_store import KnowledgeStore','from knowledge_store import KnowledgeStore, job_scope') start = s.index(' where = [', s.index(' def rows(')) end = s.index(' if cursor:', start) s = s[:start] + ' where, args = job_scope(job["tenant_id"], options, job["cutoff_at"])\n' + s[end:] s = s.replace(''' now = utc_now() db.execute("INSERT INTO knowledge_job''',''' now = utc_now() estimate = self.store._scope_counts(db, dirty["tenant_id"], options, now) options.update(total_messages=estimate["total_messages"], available_messages=estimate["available_messages"], source_name=estimate["source_name"]) db.execute("INSERT INTO knowledge_job''') s = s.replace(''' rows = self.rows(job, max(1, size)) if remaining > 0 else []''',''' fetched = self.rows(job, max(1, size) + 1) if remaining > 0 else [] rows = fetched[:size] if size > 0 else []''') s = s.replace('natural_end = len(rows) < size','natural_end = remaining > 0 and len(fetched) <= size') s = s.replace(''' if done and not natural_end: state = {}''',''' if done: options["completion_reason"] = "source_exhausted" if natural_end else "message_limit" if done and not natural_end: state = {}''') s = s.replace('''created_items=created_items+?,duplicates=duplicates+?,skipped=skipped+?,lease_owner='', lease_until=0,error='',updated_at=? WHERE id=?''','''created_items=created_items+?,duplicates=duplicates+?,skipped=skipped+?,lease_owner='', lease_until=0,error='',options_json=?,updated_at=? WHERE id=?''') s = s.replace('''{"text", "文本", "文字", "1"} or r["status"] != "normal" for r in rows), utc_now(), job["id"]))''','''{"text", "文本", "文字", "1"} or r["status"] != "normal" or not r["content"].strip() for r in rows), json_text(options), utc_now(), job["id"]))''') path.write_text(s, encoding='utf-8', newline='\n') path = root / 'wechat_rpa/knowledge_api.py' s = path.read_text(encoding='utf-8') s = s.replace(''' return store.set_enabled(single(principal, account_id), body.enabled, principal.id, client_ip(request))''',''' with errors(): return store.set_enabled(single(principal, account_id), body.enabled, principal.id, client_ip(request))''') s = s.replace(''' @app.post("/api/v2/knowledge/jobs")''',''' @app.post("/api/v2/knowledge/jobs/preview") def preview_job(body: JobBody, account_id: int | None = None, principal=Depends(require("knowledge:write", "im:content:read"))): with errors(): return store.preview_job(single(principal, account_id), body.model_dump()) @app.post("/api/v2/knowledge/jobs")''') path.write_text(s, encoding='utf-8', newline='\n') print('Updated knowledge store, worker and preview API.')