wx-agent/workflows/workflow_wiki_admin.py

494 lines
21 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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