494 lines
21 KiB
Python
494 lines
21 KiB
Python
import json
|
||
import re
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from wiki_engine import service
|
||
|
||
|
||
CONFIRM_WORDS = ("确认", "执行", "开始", "确定", "同意", "dry_run=false", "不是预览")
|
||
CANCEL_WORDS = ("取消", "先不", "不要执行", "作废")
|
||
|
||
|
||
async def run_wiki_admin_workflow(
|
||
extracted_text: str,
|
||
combined_query: str,
|
||
history_message: str = "",
|
||
route_flag: str = "",
|
||
chat_id: str = "",
|
||
message_id: str = "",
|
||
**_: Any,
|
||
) -> Dict[str, Any]:
|
||
query = (combined_query or extracted_text or "").strip()
|
||
if not query:
|
||
return _response("请告诉我要管理哪个知识库或文件,例如:检查知识库 163舰 的 Wiki 同步情况。")
|
||
|
||
try:
|
||
result = await _dispatch(query=query, chat_id=chat_id)
|
||
return _response(_format_result(result), result=result)
|
||
except Exception as exc:
|
||
return _response(f"Wiki 管理操作失败:{exc}", error=str(exc))
|
||
|
||
|
||
async def _dispatch(query: str, chat_id: str = "") -> Dict[str, Any]:
|
||
pending_plan = await service.get_pending_admin_plan(chat_id) if chat_id else None
|
||
confirmed = _is_confirmed(query)
|
||
selected_indexes = _extract_selection_indexes(query)
|
||
job_id = _extract_job_id(query)
|
||
|
||
if job_id and any(word in query for word in ("进度", "状态", "查看", "job", "任务", "同步")):
|
||
job = await service.get_sync_job(job_id)
|
||
if not job:
|
||
return {"operation": "job_progress", "data": {"success": False, "job_id": job_id, "message": "未找到这个同步任务。"}}
|
||
return {"operation": "job_progress", "data": {"success": True, "job": job}}
|
||
|
||
if pending_plan and _is_cancel(query):
|
||
cancelled = await service.cancel_admin_plan(str(pending_plan.get("id") or ""))
|
||
return {"operation": "plan_cancelled", "data": {"success": True, "plan": cancelled or pending_plan}}
|
||
|
||
if pending_plan and confirmed and not _contains_new_target(query):
|
||
data = await service.execute_admin_plan(pending_plan, selected_indexes=selected_indexes)
|
||
data["plan"] = pending_plan
|
||
return {"operation": "plan_executed", "data": data}
|
||
|
||
kb_id = _extract_kb_id(query)
|
||
file_id = _extract_id(query, ("文件", "file", "file_id"))
|
||
include_children = any(word in query for word in ("子知识库", "子库", "包含子", "包括子"))
|
||
use_llm = any(word.lower() in query.lower() for word in ("大模型", "llm", "模型摘要", "智能摘要"))
|
||
limit = _extract_limit(query) or 20
|
||
|
||
if any(word in query for word in ("统计", "状态", "概览", "数量")) and not kb_id and not file_id:
|
||
return {"operation": "stats", "data": {"success": True, "stats": await service.get_stats()}}
|
||
|
||
if any(word in query for word in ("检查", "对比", "差异", "巡检", "同步情况")):
|
||
target = await _resolve_kb_target(query, kb_id)
|
||
if not target.get("success"):
|
||
return {"operation": "need_kb_name", "data": target}
|
||
diff = await service.diff_kb(kb_id=target["kb_id"], include_children=include_children, limit=limit)
|
||
diff.update({"kb_name": target.get("kb_name", ""), "kb_path": target.get("kb_path", "")})
|
||
actions = _sync_actions_from_diff(diff, use_llm=use_llm)
|
||
plan = await _save_plan(
|
||
chat_id=chat_id,
|
||
intent="sync_after_check",
|
||
scope="kb",
|
||
target=target,
|
||
summary=diff.get("summary", {}),
|
||
actions=actions,
|
||
metadata={"source_operation": "diff_kb", "include_children": include_children, "limit": limit},
|
||
)
|
||
diff["plan"] = plan
|
||
diff["planned_actions"] = actions
|
||
return {"operation": "diff_kb", "data": diff}
|
||
|
||
if any(word in query for word in ("清理", "修复", "对齐", "同步当前", "reconcile")):
|
||
target = await _resolve_kb_target(query, kb_id)
|
||
if not target.get("success"):
|
||
return {"operation": "need_kb_name", "data": target}
|
||
delete_orphan = any(word in query for word in ("删除孤儿", "删除脏数据", "清理脏数据", "已删除", "不存在", "孤儿"))
|
||
preview = await service.reconcile_kb(
|
||
kb_id=target["kb_id"],
|
||
include_children=include_children,
|
||
limit=limit,
|
||
sync_missing=True,
|
||
rebuild_stale=True,
|
||
delete_orphan=delete_orphan,
|
||
use_llm=use_llm,
|
||
dry_run=True,
|
||
)
|
||
preview.update({"kb_name": target.get("kb_name", ""), "kb_path": target.get("kb_path", "")})
|
||
return await _plan_or_execute(
|
||
chat_id=chat_id,
|
||
confirmed=confirmed,
|
||
intent="reconcile_kb",
|
||
scope="kb",
|
||
target=target,
|
||
summary=preview.get("summary", {}),
|
||
actions=preview.get("planned_actions", []),
|
||
metadata={"delete_orphan": delete_orphan, "include_children": include_children, "limit": limit},
|
||
preview=preview,
|
||
)
|
||
|
||
if any(word in query for word in ("删除", "移除")):
|
||
if file_id:
|
||
action = {"action": "delete_wiki_file", "file_id": file_id}
|
||
return await _plan_or_execute(
|
||
chat_id=chat_id,
|
||
confirmed=confirmed,
|
||
intent="delete_wiki_file",
|
||
scope="file",
|
||
target={"kb_id": kb_id or "", "kb_name": "", "kb_path": ""},
|
||
summary={"delete_files": 1},
|
||
actions=[action],
|
||
metadata={"destructive": True},
|
||
preview={"success": True, "planned_actions": [action]},
|
||
)
|
||
target = await _resolve_kb_target(query, kb_id)
|
||
if target.get("success"):
|
||
action = {"action": "delete_wiki_kb", "kb_id": target["kb_id"], "kb_name": target.get("kb_name", "")}
|
||
return await _plan_or_execute(
|
||
chat_id=chat_id,
|
||
confirmed=confirmed,
|
||
intent="delete_wiki_kb",
|
||
scope="kb",
|
||
target=target,
|
||
summary={"delete_kb": 1},
|
||
actions=[action],
|
||
metadata={"destructive": True},
|
||
preview={"success": True, "planned_actions": [action], **target},
|
||
)
|
||
return {"operation": "need_target", "data": {"success": False, "message": "删除 Wiki 数据需要提供文件 id,或说清楚知识库名称。", **target}}
|
||
|
||
if any(word in query for word in ("同步", "构建", "建立", "重建", "生成")):
|
||
if file_id:
|
||
action = {"action": "sync_file", "file_id": file_id, "kb_id": kb_id or "", "use_llm": use_llm}
|
||
return await _plan_or_execute(
|
||
chat_id=chat_id,
|
||
confirmed=confirmed,
|
||
intent="sync_file",
|
||
scope="file",
|
||
target={"kb_id": kb_id or "", "kb_name": "", "kb_path": ""},
|
||
summary={"sync_files": 1},
|
||
actions=[action],
|
||
metadata={"use_llm": use_llm},
|
||
preview={"success": True, "planned_actions": [action]},
|
||
)
|
||
target = await _resolve_kb_target(query, kb_id)
|
||
if target.get("success"):
|
||
preview = await service.reconcile_kb(
|
||
kb_id=target["kb_id"],
|
||
include_children=include_children,
|
||
limit=limit,
|
||
sync_missing=True,
|
||
rebuild_stale=True,
|
||
delete_orphan=False,
|
||
use_llm=use_llm,
|
||
dry_run=True,
|
||
)
|
||
preview.update({"kb_name": target.get("kb_name", ""), "kb_path": target.get("kb_path", "")})
|
||
return await _plan_or_execute(
|
||
chat_id=chat_id,
|
||
confirmed=confirmed,
|
||
intent="sync_kb",
|
||
scope="kb",
|
||
target=target,
|
||
summary=preview.get("summary", {}),
|
||
actions=preview.get("planned_actions", []),
|
||
metadata={"include_children": include_children, "limit": limit, "use_llm": use_llm},
|
||
preview=preview,
|
||
)
|
||
return {"operation": "need_target", "data": {"success": False, "message": "同步 Wiki 需要提供文件 id,或说清楚知识库名称。", **target}}
|
||
|
||
if pending_plan:
|
||
return {"operation": "pending_plan", "data": {"success": True, "plan": pending_plan}}
|
||
|
||
return {
|
||
"operation": "help",
|
||
"data": {
|
||
"success": True,
|
||
"examples": [
|
||
"检查知识库 163舰 的 Wiki 同步情况",
|
||
"同步知识库 163舰 的前 50 个文件",
|
||
"清理知识库 163舰 中 11000 已删除但 Wiki 还存在的数据",
|
||
"确认执行",
|
||
"只执行第 1、3 个",
|
||
],
|
||
},
|
||
}
|
||
|
||
|
||
async def _plan_or_execute(
|
||
chat_id: str,
|
||
confirmed: bool,
|
||
intent: str,
|
||
scope: str,
|
||
target: Dict[str, Any],
|
||
summary: Dict[str, Any],
|
||
actions: List[Dict[str, Any]],
|
||
metadata: Dict[str, Any],
|
||
preview: Dict[str, Any],
|
||
) -> Dict[str, Any]:
|
||
plan = await _save_plan(chat_id, intent, scope, target, summary, actions, metadata)
|
||
preview["plan"] = plan
|
||
preview["planned_actions"] = actions
|
||
if confirmed and actions:
|
||
if not plan:
|
||
return {"operation": "plan_preview", "data": preview}
|
||
executed = await service.execute_admin_plan(plan)
|
||
executed["plan"] = plan
|
||
return {"operation": "plan_executed", "data": executed}
|
||
return {"operation": "plan_preview", "data": preview}
|
||
|
||
|
||
async def _save_plan(
|
||
chat_id: str,
|
||
intent: str,
|
||
scope: str,
|
||
target: Dict[str, Any],
|
||
summary: Dict[str, Any],
|
||
actions: List[Dict[str, Any]],
|
||
metadata: Dict[str, Any],
|
||
) -> Dict[str, Any]:
|
||
if not chat_id or not actions:
|
||
return {}
|
||
return await service.create_admin_plan(
|
||
chat_id=chat_id,
|
||
intent=intent,
|
||
scope=scope,
|
||
kb_id=str(target.get("kb_id") or ""),
|
||
kb_name=str(target.get("kb_name") or target.get("kb_path") or ""),
|
||
summary=summary,
|
||
actions=actions,
|
||
metadata=metadata,
|
||
)
|
||
|
||
|
||
def _sync_actions_from_diff(diff: Dict[str, Any], use_llm: bool = False) -> List[Dict[str, Any]]:
|
||
actions: List[Dict[str, Any]] = []
|
||
for item in diff.get("missing_in_wiki", []):
|
||
actions.append({
|
||
"action": "sync_file",
|
||
"file_id": item.get("file_id", ""),
|
||
"kb_id": item.get("kb_id") or diff.get("kb_id", ""),
|
||
"filename": item.get("filename", ""),
|
||
"use_llm": use_llm,
|
||
})
|
||
for item in diff.get("stale_in_wiki", []):
|
||
actions.append({
|
||
"action": "rebuild_file",
|
||
"file_id": item.get("file_id", ""),
|
||
"kb_id": item.get("kb_id") or diff.get("kb_id", ""),
|
||
"filename": item.get("filename", ""),
|
||
"use_llm": use_llm,
|
||
})
|
||
return actions
|
||
|
||
|
||
def _format_result(result: Dict[str, Any]) -> str:
|
||
op = result.get("operation", "")
|
||
data = result.get("data", {})
|
||
if op == "help":
|
||
return "我是 Wiki 管理助手,可以检查、生成计划、同步、清理和删除 Wiki 指针数据。\n\n示例:\n" + "\n".join(f"- {x}" for x in data.get("examples", []))
|
||
if op in {"need_kb_id", "need_kb_name", "need_target"}:
|
||
matches = data.get("matches") or data.get("candidates") or []
|
||
if matches:
|
||
options = "\n".join(
|
||
f"- {item.get('path') or item.get('name')}(id: {item.get('kb_id')})"
|
||
for item in matches[:5]
|
||
)
|
||
return f"{data.get('message') or data.get('error') or '请说清楚知识库名称。'}\n可选知识库:\n{options}"
|
||
return data.get("message", "请补充必要参数。")
|
||
if op == "stats":
|
||
stats = data.get("stats", {})
|
||
return "当前 Wiki 数据统计:\n" + "\n".join(f"- {k}: {v}" for k, v in stats.items())
|
||
if op == "job_progress":
|
||
if not data.get("success"):
|
||
return data.get("message") or "未找到同步任务。"
|
||
return _format_job_progress(data.get("job") or {})
|
||
if op == "diff_kb":
|
||
summary = data.get("summary", {})
|
||
target = data.get("kb_path") or data.get("kb_name") or data.get("kb_id")
|
||
lines = [
|
||
f"知识库 {target} 的 Wiki 同步检查完成。",
|
||
f"- 11000 可用文件数:{summary.get('htknow_files', 0)}",
|
||
f"- Wiki 已同步文件数:{summary.get('wiki_files', 0)}",
|
||
f"- 11000 有但 Wiki 缺失:{summary.get('missing_in_wiki', 0)}",
|
||
f"- Wiki 已过期需重建:{summary.get('stale_in_wiki', 0)}",
|
||
f"- Wiki 有但 11000 不存在:{summary.get('orphan_in_wiki', 0)}",
|
||
f"- 跳过未解析文件:{summary.get('skipped_unparsed', 0)}",
|
||
]
|
||
lines.extend(_format_plan_tail(data.get("plan") or {}, data.get("planned_actions") or []))
|
||
return "\n".join(lines)
|
||
if op in {"plan_preview", "pending_plan"}:
|
||
plan = data.get("plan") or {}
|
||
actions = data.get("planned_actions") or plan.get("actions") or []
|
||
target = data.get("kb_path") or data.get("kb_name") or plan.get("kb_name") or plan.get("kb_id") or "当前目标"
|
||
lines = [f"已生成 {target} 的操作计划。", f"- 计划编号:{plan.get('id') or '未保存'}", f"- 待执行动作:{len(actions)}"]
|
||
lines.extend(_format_actions(actions))
|
||
if plan:
|
||
lines.append("确认无误后,请回复“确认执行”;也可以说“只执行第 1、3 个”或“取消”。")
|
||
else:
|
||
lines.append("当前请求没有 chat_id,计划未落库;需要前端带 chat_id 才能多轮确认执行。")
|
||
return "\n".join(lines)
|
||
if op == "plan_executed":
|
||
plan = data.get("plan") or {}
|
||
lines = [
|
||
f"已提交 Wiki 同步计划 {plan.get('id') or ''}。",
|
||
f"- 已提交任务:{data.get('executed_count', 0)}",
|
||
f"- 提交失败:{data.get('failed_count', 0)}",
|
||
]
|
||
jobs = _extract_executed_jobs(data.get("executed") or [])
|
||
if jobs:
|
||
lines.append("同步正在后台执行,可继续使用系统。当前任务:")
|
||
for job in jobs[:8]:
|
||
lines.append(f"- job_id: {job.get('id') or job.get('job_id')},进度:{_job_progress_text(job)}")
|
||
if len(jobs) > 8:
|
||
lines.append(f"- 还有 {len(jobs) - 8} 个后台任务")
|
||
lines.append("想看进度时,可以问:查看同步进度 <job_id>")
|
||
return "\n".join(lines)
|
||
if op == "plan_cancelled":
|
||
plan = data.get("plan") or {}
|
||
return f"已取消待执行计划 {plan.get('id') or ''}。"
|
||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||
|
||
|
||
def _format_plan_tail(plan: Dict[str, Any], actions: List[Dict[str, Any]]) -> List[str]:
|
||
if not actions:
|
||
return ["当前没有需要同步或重建的 Wiki 动作。"]
|
||
lines = [f"已为缺失/过期数据生成待确认计划:{plan.get('id') or '未保存'}", f"- 待同步/重建动作:{len(actions)}"]
|
||
lines.extend(_format_actions(actions))
|
||
if plan:
|
||
lines.append("如果要执行,请回复“确认执行”;如果要清理孤儿数据,请明确说“清理孤儿数据”。")
|
||
return lines
|
||
|
||
|
||
def _format_actions(actions: List[Dict[str, Any]], max_items: int = 8) -> List[str]:
|
||
lines = []
|
||
for idx, action in enumerate(actions[:max_items], start=1):
|
||
lines.append(f" {idx}. {_action_label(action)}")
|
||
if len(actions) > max_items:
|
||
lines.append(f" ... 还有 {len(actions) - max_items} 个动作")
|
||
return lines
|
||
|
||
|
||
def _action_label(action: Dict[str, Any]) -> str:
|
||
action_type = action.get("action", "")
|
||
if action_type == "sync_file":
|
||
return f"同步文件 {action.get('filename') or action.get('file_id')}"
|
||
if action_type == "rebuild_file":
|
||
return f"重建文件 {action.get('filename') or action.get('file_id')}"
|
||
if action_type == "delete_wiki_file":
|
||
return f"删除 Wiki 中的文件数据 {action.get('file_id')}"
|
||
if action_type == "delete_wiki_kb":
|
||
return f"删除 Wiki 中的知识库数据 {action.get('kb_name') or action.get('kb_id')}"
|
||
return json.dumps(action, ensure_ascii=False)
|
||
|
||
|
||
def _extract_executed_jobs(executed: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||
jobs: List[Dict[str, Any]] = []
|
||
for item in executed:
|
||
result = item.get("result") or {}
|
||
job = result.get("job") if isinstance(result.get("job"), dict) else {}
|
||
if job:
|
||
jobs.append(job)
|
||
elif result.get("job_id"):
|
||
jobs.append({"id": result.get("job_id"), "status": result.get("status") or "running"})
|
||
return jobs
|
||
|
||
|
||
def _format_job_progress(job: Dict[str, Any]) -> str:
|
||
current = str(job.get("current_filename") or job.get("current_file_id") or "").strip()
|
||
lines = [
|
||
f"同步任务 {job.get('id') or ''}",
|
||
f"- 状态:{job.get('status') or ''}",
|
||
f"- 目标:{job.get('target_type') or ''} {job.get('target_id') or ''}",
|
||
f"- 当前阶段:{job.get('stage') or '处理中'}",
|
||
]
|
||
if current:
|
||
lines.append(f"- 当前文件:{current}")
|
||
lines.extend([
|
||
f"- 进度:{_job_progress_text(job)}",
|
||
f"- 已处理文件:{job.get('processed_files', 0)} / {job.get('total_files', 0)}",
|
||
f"- 已跳过未变化文件:{job.get('skipped_files', 0)}",
|
||
f"- 已同步切片:{job.get('total_slices', 0)}",
|
||
f"- 已生成/更新页面:{job.get('page_count', 0)}",
|
||
f"- 更新时间:{job.get('updated_at') or ''}",
|
||
f"- 错误:{job.get('error') or '无'}",
|
||
])
|
||
return "\n".join(lines)
|
||
|
||
|
||
def _job_progress_text(job: Dict[str, Any]) -> str:
|
||
total = int(job.get("total_files") or 0)
|
||
processed = int(job.get("processed_files") or 0)
|
||
status = str(job.get("status") or "")
|
||
if total > 0:
|
||
percent = min(100.0, max(0.0, processed * 100.0 / total))
|
||
return f"{processed}/{total}({percent:.0f}%),状态:{status}"
|
||
return f"等待统计文件数,状态:{status}"
|
||
|
||
|
||
def _response(text: str, result: Optional[Dict[str, Any]] = None, error: str = "") -> Dict[str, Any]:
|
||
return {
|
||
"response": text,
|
||
"actions": [],
|
||
"result_tag": "wiki_admin",
|
||
"suggestedReplies": [
|
||
{"title": "检查同步情况", "content": "检查知识库 163舰 的 Wiki 同步情况"},
|
||
{"title": "确认执行", "content": "确认执行"},
|
||
],
|
||
"route_flag": "wiki_admin",
|
||
"wiki_admin_result": result or {},
|
||
"error_message": error,
|
||
}
|
||
|
||
|
||
def _is_confirmed(query: str) -> bool:
|
||
return any(word in query for word in CONFIRM_WORDS)
|
||
|
||
|
||
def _is_cancel(query: str) -> bool:
|
||
return any(word in query for word in CANCEL_WORDS)
|
||
|
||
|
||
def _contains_new_target(query: str) -> bool:
|
||
return any(word in query for word in ("知识库", "文件", "file_id", "kb_id"))
|
||
|
||
|
||
def _extract_selection_indexes(query: str) -> Optional[List[int]]:
|
||
if not any(word in query for word in ("第", "只", "选择", "操作", "执行")):
|
||
return None
|
||
nums = [int(x) for x in re.findall(r"\d+", query)]
|
||
return nums or None
|
||
|
||
|
||
def _extract_job_id(query: str) -> Optional[str]:
|
||
match = re.search(r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b", query or "")
|
||
return match.group(0) if match else None
|
||
|
||
|
||
def _extract_id(query: str, labels: tuple[str, ...]) -> Optional[str]:
|
||
for label in labels:
|
||
patterns = [
|
||
rf"{re.escape(label)}\s*[::]?\s*([A-Za-z0-9_\-]+)",
|
||
rf"([A-Za-z0-9_\-]+)\s*号?{re.escape(label)}",
|
||
]
|
||
for pattern in patterns:
|
||
match = re.search(pattern, query, flags=re.IGNORECASE)
|
||
if match:
|
||
return match.group(1)
|
||
return None
|
||
|
||
|
||
def _extract_kb_id(query: str) -> Optional[str]:
|
||
patterns = [
|
||
r"(?:kb_id|kb|知识库\s*(?:id|ID|编号))\s*[::=]?\s*([A-Za-z0-9_\-]+)",
|
||
r"知识库\s*[::]?\s*(\d+)(?![A-Za-z0-9_\-])",
|
||
r"(\d+)\s*号?\s*知识库",
|
||
]
|
||
for pattern in patterns:
|
||
match = re.search(pattern, query, flags=re.IGNORECASE)
|
||
if match:
|
||
return match.group(1)
|
||
return None
|
||
|
||
|
||
async def _resolve_kb_target(query: str, kb_id: Optional[str]) -> Dict[str, Any]:
|
||
if kb_id:
|
||
return {"success": True, "kb_id": kb_id, "kb_name": "", "kb_path": ""}
|
||
resolved = await service.resolve_kb_reference(query)
|
||
if resolved.get("success"):
|
||
return resolved
|
||
return {
|
||
"success": False,
|
||
"message": resolved.get("error") or "请说清楚知识库名称。",
|
||
"matches": resolved.get("matches") or resolved.get("candidates") or [],
|
||
}
|
||
|
||
|
||
def _extract_limit(query: str) -> Optional[int]:
|
||
patterns = [r"前\s*(\d+)\s*个", r"limit\s*[:=]\s*(\d+)", r"最多\s*(\d+)\s*个"]
|
||
for pattern in patterns:
|
||
match = re.search(pattern, query, flags=re.IGNORECASE)
|
||
if match:
|
||
return max(1, min(int(match.group(1)), 1000))
|
||
return None
|