180 lines
8.8 KiB
Diff
180 lines
8.8 KiB
Diff
--- production/wechat_rpa/admin_api.py
|
|
+++ release/wechat_rpa/admin_api.py
|
|
@@ -42,6 +42,7 @@
|
|
import admin_backend as backend
|
|
import app_version
|
|
import archive_api
|
|
+import knowledge_api
|
|
import zyt_auth
|
|
|
|
# 账号下拉框里「全部」的取值。用 -1 而不是 0:0 早就有含义了(旧版未归属数据),
|
|
@@ -1277,12 +1278,19 @@
|
|
) -> dict:
|
|
# 这一份的结构由 `admin_backend.desktop_config_payload` 统一给出。
|
|
# 两处各写一份的话迟早漂移,表现成"某台客户端少了个字段",极难查。
|
|
- return backend.desktop_config_payload(
|
|
+ payload = backend.desktop_config_payload(
|
|
database,
|
|
scheme=request.headers.get("x-forwarded-proto", request.url.scheme),
|
|
host=request.headers.get("host", ""),
|
|
desktop_account=account,
|
|
)
|
|
+ knowledge = app.state.knowledge_store
|
|
+ tenant = str(account["tenant_id"])
|
|
+ counts = await asyncio.to_thread(knowledge.stats, tenant)
|
|
+ settings = await asyncio.to_thread(knowledge.settings, tenant)
|
|
+ payload["knowledge"] = {"published": counts.get("published", 0),
|
|
+ "enabled": bool(settings["enabled"]), "updated_at": settings["updated_at"]}
|
|
+ return payload
|
|
|
|
# 桌面端回流一次编排调用。补上这条之前,客户端改指 8766 会丢掉全部调用留痕
|
|
# ——配置照常同步、回复照常发,唯独调用记录一条不进库,而那恰恰是出事后用来
|
|
@@ -1325,6 +1333,10 @@
|
|
resolve_admin_tenant=resolve_admin_tenant,
|
|
zyt_api_url=lambda: desktop_login_api_url()[0],
|
|
zyt_patient_searcher=zyt_patient_searcher,
|
|
+ )
|
|
+
|
|
+ knowledge_api.register_knowledge_routes(
|
|
+ app, database, require, client_ip, resolve_admin_tenant, current_desktop,
|
|
)
|
|
|
|
# ── 前端静态资源 ─────────────────────────────────────────────────────
|
|
|
|
--- production/wechat_rpa/model_gateway.py
|
|
+++ release/wechat_rpa/model_gateway.py
|
|
@@ -44,6 +44,8 @@
|
|
|
|
import model_protocol
|
|
import admin_backend
|
|
+from knowledge_store import KnowledgeStore
|
|
+from knowledge_retriever import KnowledgeRetriever, inject_references
|
|
|
|
# ── 超时分层 ─────────────────────────────────────────────────────────────────
|
|
# 整体必须小于桌面端的超时,否则桌面端先超时重发,网关还在跑,双倍扣费。
|
|
@@ -477,6 +479,9 @@
|
|
app = FastAPI(title="模型统一网关", version="1.0")
|
|
database = admin_backend.Database(db_path)
|
|
database.migrate()
|
|
+ knowledge_store = KnowledgeStore(database)
|
|
+ knowledge_store.initialize()
|
|
+ knowledge_retriever = KnowledgeRetriever(knowledge_store)
|
|
catalog = Catalog(db_path)
|
|
idempotency: dict[str, tuple[float, dict]] = {}
|
|
log_queue: asyncio.Queue = asyncio.Queue(maxsize=2000)
|
|
@@ -522,6 +527,9 @@
|
|
record = await log_queue.get()
|
|
try:
|
|
await asyncio.to_thread(_write_log, db_path, record)
|
|
+ if record.get("knowledge_retrieval") is not None:
|
|
+ await asyncio.to_thread(knowledge_retriever.record, record["tenant_id"],
|
|
+ record["task_id"], record["knowledge_retrieval"])
|
|
except Exception:
|
|
pass
|
|
finally:
|
|
@@ -584,7 +592,12 @@
|
|
)
|
|
if idempotency_key and idempotency_key in idempotency:
|
|
# 桌面端重发(超时后重试)不该重复扣费
|
|
- return JSONResponse(idempotency[idempotency_key][1])
|
|
+ cached = idempotency[idempotency_key][1]
|
|
+ cached_hits = (cached.get("knowledge") or {}).get("hits", [])
|
|
+ if not cached_hits or await asyncio.to_thread(
|
|
+ knowledge_retriever.references_current, str(account["tenant_id"]), cached_hits):
|
|
+ return JSONResponse(cached)
|
|
+ idempotency.pop(idempotency_key, None)
|
|
|
|
body = await request.json()
|
|
answers, judge, mode, version = catalog.plan()
|
|
@@ -606,6 +619,32 @@
|
|
tools = body.get("tools") or None
|
|
|
|
started = time.monotonic()
|
|
+ knowledge_result = None
|
|
+ knowledge_trace = {"enabled": False, "hits": [], "mode": "disabled", "elapsed_ms": 0}
|
|
+ tenant = str(account["tenant_id"])
|
|
+ if str(body.get("purpose") or "chat") == "chat":
|
|
+ settings = await asyncio.to_thread(knowledge_store.settings, tenant)
|
|
+ if settings["enabled"]:
|
|
+ from knowledge_processing import latest_query
|
|
+ query = str(body.get("customer_text") or "")
|
|
+ if not query:
|
|
+ for turn in reversed(messages):
|
|
+ if turn.get("role") == "user" and isinstance(turn.get("content"), str):
|
|
+ query = turn["content"]
|
|
+ break
|
|
+ query = latest_query(query) or query
|
|
+ try:
|
|
+ knowledge_result = await asyncio.wait_for(
|
|
+ asyncio.to_thread(knowledge_retriever.search, tenant, query), timeout=4.0)
|
|
+ knowledge_trace = {"enabled": True, "mode": knowledge_result["mode"],
|
|
+ "elapsed_ms": knowledge_result["elapsed_ms"], "hits": [
|
|
+ {k: h[k] for k in ("id", "revision", "score")} for h in knowledge_result["hits"]]}
|
|
+ messages = inject_references(messages, knowledge_result["hits"])
|
|
+ except Exception:
|
|
+ knowledge_trace = {"enabled": True, "hits": [], "mode": "unavailable", "elapsed_ms": 4000}
|
|
+ from knowledge_processing import redact
|
|
+ knowledge_result = {**knowledge_trace, "query": redact(query)[:1000]}
|
|
+ messages = inject_references(messages, [])
|
|
|
|
if tools:
|
|
# 工具轮:只问主出口,不并发也不评审。
|
|
@@ -634,12 +673,24 @@
|
|
"judge": dict(_EMPTY_VERDICT),
|
|
"judge_mode": mode,
|
|
"roles_version": version,
|
|
+ "knowledge": knowledge_trace,
|
|
"total_ms": int((time.monotonic() - started) * 1000),
|
|
}
|
|
if single.get("error") and not payload["reply"] and not payload["tool_calls"]:
|
|
return JSONResponse(payload, status_code=502)
|
|
- if x_idempotency_key:
|
|
- idempotency[x_idempotency_key] = (now, payload)
|
|
+ if idempotency_key:
|
|
+ idempotency[idempotency_key] = (now, payload)
|
|
+ try:
|
|
+ log_queue.put_nowait({
|
|
+ "desktop_account_id": int(account["id"]), "tenant_id": tenant,
|
|
+ "device_id": x_device_id, "task_id": str(body.get("task_id") or ""),
|
|
+ "roles_version": version, "judge_mode": mode, "chosen": payload["chosen"],
|
|
+ "judge": payload["judge"], "candidates": [single], "total_ms": payload["total_ms"],
|
|
+ "customer_text": str(body.get("customer_text") or ""), "reply_text": payload["reply"],
|
|
+ "purpose": str(body.get("purpose") or "chat"), "knowledge_retrieval": knowledge_result,
|
|
+ })
|
|
+ except asyncio.QueueFull:
|
|
+ pass
|
|
return JSONResponse(payload)
|
|
|
|
results = await asyncio.gather(
|
|
@@ -683,6 +734,7 @@
|
|
|
|
payload = {
|
|
"reply": chosen["text"],
|
|
+ "knowledge": knowledge_trace,
|
|
"tool_calls": [],
|
|
"chosen": chosen["provider"],
|
|
"candidates": results,
|
|
@@ -708,6 +760,7 @@
|
|
"customer_text": str(body.get("customer_text") or ""),
|
|
"reply_text": payload["reply"],
|
|
"purpose": str(body.get("purpose") or "chat"),
|
|
+ "knowledge_retrieval": knowledge_result,
|
|
})
|
|
except asyncio.QueueFull:
|
|
pass # 观测数据丢一条,不能因此拖慢回复
|
|
|
|
--- production/wechat_rpa/rbac.py
|
|
+++ release/wechat_rpa/rbac.py
|
|
@@ -27,6 +27,11 @@
|
|
("model:write", "增删改模型与编排", "模型"),
|
|
("agent:read", "查看智能体", "模型"),
|
|
("agent:write", "增删改智能体与协作方案", "模型"),
|
|
+ ("knowledge:read", "查看知识中心", "知识库"),
|
|
+ ("knowledge:write", "加工和编辑知识草稿", "知识库"),
|
|
+ ("knowledge:review", "审核知识", "知识库"),
|
|
+ ("knowledge:publish", "发布和停用知识", "知识库"),
|
|
+ ("knowledge:export", "导出已发布知识", "知识库"),
|
|
("review:read", "查看待审队列", "审核"),
|
|
("review:write", "通过/驳回待审回复", "审核"),
|
|
("stats:read", "查看调用统计", "运营"),
|