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("想看进度时,可以问:查看同步进度 ") 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