Files
kefu/deploy/knowledge-v2-edit.py
T
2026-09-21 10:34:06 +08:00

179 lines
12 KiB
Python

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<?")):
if options.get(key):
where.append(condition)
args.append(options[key])
if options.get("conversation_id"):
where.append("m.conversation_id=?")
args.append(options["conversation_id"])
return where, args
def job_options(values):
options = dict(values)
start = day_boundary(options.get("date_from", ""))
end = day_boundary(options.get("date_to", ""), end=True)
if start and end and start >= 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.')