Sfoglia il codice sorgente

update knowledge manager

guantao 1 giorno fa
parent
commit
5f60578355

+ 129 - 0
agent/tools/builtin/knowledge.py

@@ -16,6 +16,7 @@
 """
 """
 
 
 import os
 import os
+import json
 import logging
 import logging
 import subprocess
 import subprocess
 import httpx
 import httpx
@@ -623,3 +624,131 @@ async def resource_get(
             error=str(e)
             error=str(e)
         )
         )
 
 
+
+# ==================== Tool 表查询工具 ====================
+
+@tool(hidden_params=["context"])
+async def tool_search(
+    query: str,
+    top_k: int = 5,
+    status: Optional[str] = None,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """向量检索工具 (Tool)
+    Args:
+        query: 检索词
+        top_k: 返回数量
+        status: 过滤状态 (如 '未接入', '已封装')
+    """
+    try:
+        params = {"q": query, "top_k": top_k}
+        if status: params["status"] = status
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            res = await client.get(f"{KNOWHUB_API}/api/tool/search", params=params)
+            res.raise_for_status()
+            data = res.json()
+        return ToolResult(title="✅ 工具检索成功", output=json.dumps(data, ensure_ascii=False, indent=2))
+    except Exception as e:
+        return ToolResult(title="❌ 工具检索失败", output=str(e), error=str(e))
+
+
+@tool(hidden_params=["context"])
+async def tool_list(
+    limit: int = 20,
+    offset: int = 0,
+    status: Optional[str] = None,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """列出工具列表 (Tool)"""
+    try:
+        params = {"limit": limit, "offset": offset}
+        if status: params["status"] = status
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            res = await client.get(f"{KNOWHUB_API}/api/tool", params=params)
+            res.raise_for_status()
+            data = res.json()
+        return ToolResult(title="✅ 工具列表获取成功", output=json.dumps(data, ensure_ascii=False, indent=2))
+    except Exception as e:
+        return ToolResult(title="❌ 工具列表失败", output=str(e), error=str(e))
+
+
+# ==================== Capability (原子能力) 表查询工具 ====================
+
+@tool(hidden_params=["context"])
+async def capability_search(
+    query: str,
+    top_k: int = 5,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """向量检索原子能力 (Capability)
+    Args:
+        query: 检索词
+        top_k: 返回数量
+    """
+    try:
+        params = {"q": query, "top_k": top_k}
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            res = await client.get(f"{KNOWHUB_API}/api/capability/search", params=params)
+            res.raise_for_status()
+            data = res.json()
+        return ToolResult(title="✅ 原子能力检索成功", output=json.dumps(data, ensure_ascii=False, indent=2))
+    except Exception as e:
+        return ToolResult(title="❌ 原子能力检索失败", output=str(e), error=str(e))
+
+
+@tool(hidden_params=["context"])
+async def capability_list(
+    limit: int = 20,
+    offset: int = 0,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """列出原子能力列表 (Capability)"""
+    try:
+        params = {"limit": limit, "offset": offset}
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            res = await client.get(f"{KNOWHUB_API}/api/capability", params=params)
+            res.raise_for_status()
+            data = res.json()
+        return ToolResult(title="✅ 原子能力列表获取成功", output=json.dumps(data, ensure_ascii=False, indent=2))
+    except Exception as e:
+        return ToolResult(title="❌ 原子能力列表失败", output=str(e), error=str(e))
+
+
+# ==================== Requirement (需求) 表查询工具 ====================
+
+@tool(hidden_params=["context"])
+async def requirement_search(
+    query: str,
+    top_k: int = 5,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """向量检索需求 (Requirement)"""
+    try:
+        params = {"q": query, "top_k": top_k}
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            res = await client.get(f"{KNOWHUB_API}/api/requirement/search", params=params)
+            res.raise_for_status()
+            data = res.json()
+        return ToolResult(title="✅ 需求检索成功", output=json.dumps(data, ensure_ascii=False, indent=2))
+    except Exception as e:
+        return ToolResult(title="❌ 需求检索失败", output=str(e), error=str(e))
+
+
+@tool(hidden_params=["context"])
+async def requirement_list(
+    limit: int = 20,
+    offset: int = 0,
+    status: Optional[str] = None,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """列出需求列表 (Requirement)"""
+    try:
+        params = {"limit": limit, "offset": offset}
+        if status: params["status"] = status
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            res = await client.get(f"{KNOWHUB_API}/api/requirement", params=params)
+            res.raise_for_status()
+            data = res.json()
+        return ToolResult(title="✅ 需求列表获取成功", output=json.dumps(data, ensure_ascii=False, indent=2))
+    except Exception as e:
+        return ToolResult(title="❌ 需求列表失败", output=str(e), error=str(e))

+ 13 - 3
agent/tools/builtin/knowledge_manager.py

@@ -128,18 +128,20 @@ async def ask_knowledge(
 )
 )
 async def upload_knowledge(
 async def upload_knowledge(
     data: Dict[str, Any],
     data: Dict[str, Any],
+    source_type: str = "research",
     finalize: bool = False,
     finalize: bool = False,
     contact_id: str = "agent_research",
     contact_id: str = "agent_research",
     context: Optional[ToolContext] = None,
     context: Optional[ToolContext] = None,
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
-    上传调研结果到知识库(异步,立即返回)
+    上传调研结果或执行经验到知识库(异步,立即返回)
 
 
     Args:
     Args:
-        data: 调研结果,包含:
+        data: 结构化数据,包含:
             - tools: 工具列表
             - tools: 工具列表
             - resources: 资源列表
             - resources: 资源列表
             - knowledge: 知识列表
             - knowledge: 知识列表
+        source_type: 数据来源分类。调研结果填 "research",执行任务时产生的经验请填 "execution"。
         finalize: 是否最终提交(True=入库,False=仅缓冲)
         finalize: 是否最终提交(True=入库,False=仅缓冲)
         contact_id: 当前 Agent 的 IM contact_id
         contact_id: 当前 Agent 的 IM contact_id
         context: 工具上下文
         context: 工具上下文
@@ -156,12 +158,20 @@ async def upload_knowledge(
                 error="im_not_initialized"
                 error="im_not_initialized"
             )
             )
 
 
+        # 将来源类型标记灌入每条 knowledge 的 source 字段
+        if "knowledge" in data and isinstance(data["knowledge"], list):
+            for k in data["knowledge"]:
+                if "source" not in k:
+                    k["source"] = {}
+                if "category" not in k["source"]:
+                    k["source"]["category"] = source_type
+
         # 构造消息(带类型标记)
         # 构造消息(带类型标记)
         if finalize:
         if finalize:
             action = "最终提交"
             action = "最终提交"
             message = f"[UPLOAD:FINALIZE] {json.dumps(data, ensure_ascii=False)}"
             message = f"[UPLOAD:FINALIZE] {json.dumps(data, ensure_ascii=False)}"
         else:
         else:
-            action = "增量上传"
+            action = f"增量上传({source_type})"
             message = f"[UPLOAD] {json.dumps(data, ensure_ascii=False)}"
             message = f"[UPLOAD] {json.dumps(data, ensure_ascii=False)}"
 
 
         # 发送(不等待回复)
         # 发送(不等待回复)

+ 1 - 1
examples/tool_research/config.py

@@ -66,4 +66,4 @@ IM_NOTIFY_INTERVAL = 10.0                  # 新消息检查间隔(秒)
 # ===== Knowledge Manager 配置 =====
 # ===== Knowledge Manager 配置 =====
 KNOWLEDGE_MANAGER_ENABLED = True           # 是否启动 Knowledge Manager(作为后台 IM Client)
 KNOWLEDGE_MANAGER_ENABLED = True           # 是否启动 Knowledge Manager(作为后台 IM Client)
 KNOWLEDGE_MANAGER_CONTACT_ID = "knowledge_manager"  # Knowledge Manager 的 IM 身份 ID
 KNOWLEDGE_MANAGER_CONTACT_ID = "knowledge_manager"  # Knowledge Manager 的 IM 身份 ID
-KNOWLEDGE_MANAGER_ENABLE_DB_COMMIT = False # 是否允许入库(False=只缓存,True=可入库)
+KNOWLEDGE_MANAGER_ENABLE_DB_COMMIT = True # 是否允许入库(False=只缓存,True=可入库)

+ 3 - 4
examples/tool_research/tool_research.prompt

@@ -62,10 +62,9 @@ ask_knowledge("查询关于 [工具名] 的所有信息(工具、资源、知
 
 
 **执行策略**:
 **执行策略**:
 - 按顺序逐个调用子 agent,每次只分配一个明确的渠道/目标
 - 按顺序逐个调用子 agent,每次只分配一个明确的渠道/目标
-- 每个子 agent 返回后,评估信息质量:
-  - 是否找到了目标渠道的信息?
-  - 信息是否足够详细和可信?
-  - 是否需要追问补充?
+- **每个子 agent 返回后,立即做两件事**:
+  1. 评估信息质量(是否找到了目标渠道的信息?信息是否足够详细和可信?是否需要追问补充?)
+  2. **增量上传**:将本轮收获的有价值信息立即通过 `upload_knowledge` 发送给 Knowledge Manager,不要等到最后。这样 Knowledge Manager 可以边收边整理图谱关系。
 - 如果某个渠道信息不足,使用 `continue_from` 追问同一个子 agent
 - 如果某个渠道信息不足,使用 `continue_from` 追问同一个子 agent
 - 所有渠道调研完成后,进入下一步
 - 所有渠道调研完成后,进入下一步
 
 

+ 104 - 135
knowhub/agents/knowledge_manager.prompt

@@ -14,58 +14,76 @@ $system$
 - 处理完毕后自动休眠,等待下一条消息
 - 处理完毕后自动休眠,等待下一条消息
 - 每条消息都是独立的请求,快速响应是第一优先级
 - 每条消息都是独立的请求,快速响应是第一优先级
 
 
-## 数据库表结构
+## 数据库核心实体(分层关联架构)
 
 
-### 1. knowledge 表(核心知识条目)
-存储所有类型的知识,通过 `types` 字段区分:
-
-**知识分类(types 字段)**:
-- `["tool"]`:工具知识 - 单个工具的功能、用法、限制
-- `["strategy"]`:工序知识 - 工作流、方案、多步骤流程
-- `["case"]`:用例知识 - 真实案例、应用场景
-- `["experience"]`:执行经验 - Agent 执行任务时的反思和总结
+本系统采用了自顶向下的关联架构,核心表由四个实体构成:需求(Requirement)、原子能力(Capability)、工具(Tool)、通用知识(Knowledge)。
 
 
+### 1. Requirements 表(业务需求层)
+定义最终要达成的业务需求目标,它是自上而下分析的起点。
 **关键字段**:
 **关键字段**:
-- `task`: 产生该知识时的任务描述
-- `content`: 知识正文
-- `types`: 知识类型(上述分类)
-- `tags`: 标签键值对(如 `{"intent": "调研", "domain": "image_gen"}`)
-- `resource_ids`: 关联的 resource ID 列表
-- `eval.score`: 评分 1-5
-
-### 2. tool_table 表(工具表)
-存储工具的元信息和关联知识:
-
+- `id`: 需求唯一ID(如 req_xxx)
+- `description`: 需求描述内容(如:爬取竞品网站动态数据)
+- `atomics`: 关联的**原子能力(Capability) ID 列表**。一个需求通常会被拆解为多个必须达成的原子能力。
+- `status`: 状态("未满足", "部分满足", "已满足" 等)
+- `match_result`: 针对现有能力的分析匹配结果结论
+
+### 2. Capabilities 表(原子能力层)
+连接业务需求与具体工具的桥梁,定义一种平台/模型能够完成的独立抽象能力。
+**关键字段**:
+- `id`: 能力唯一ID(如 cap_xxx)
+- `name`: 原子能力名称(如:动态网页渲染提取器)
+- `criterion`: 衡量标准 / 前置条件
+- `description`: 能力描述
+- `requirements`: [上游引用] 被哪些 需求 ID(requirement_id) 需要
+- `tools`: [下游引用] 能实现该能力的具体 工具 ID(tool_id) 列表
+
+### 3. tool_table 表(具体工具层)
+系统接入的具体软件、脚本或第三方API,是原子能力的具体实现。
 **关键字段**:
 **关键字段**:
 - `id`: 工具 ID,格式 `tools/{category}/{name}`
 - `id`: 工具 ID,格式 `tools/{category}/{name}`
 - `name`: 工具名称
 - `name`: 工具名称
 - `introduction`: 简介
 - `introduction`: 简介
 - `tutorial`: 使用教程
 - `tutorial`: 使用教程
 - `input`/`output`: 输入输出规格
 - `input`/`output`: 输入输出规格
-- `knowledge`: 关联的通用知识 ID 列表(工具知识)
-- `case_knowledge`: 关联的用例知识 ID 列表
-- `process_knowledge`: 关联的工序知识 ID 列表
+- `capabilities`: [上游引用] 该工具实现了哪些 原子能力 ID(capability_id)
+- `tool_knowledge`/`case_knowledge`/`process_knowledge`: 补充关联的经验/用例等 通用知识(Knowledge) ID
+
+### 4. knowledge 表(通用经验/技巧提取表)
+存储在使用工具/落实能力过程中产生的细碎经验。
+通过 `types` 字段区分:
+- `["tool"]`:工具知识 - 单个工具的功能、用法、限制
+- `["strategy"]`:工序知识 - 工作流、多步骤流程
+- `["case"]`:用例知识 - 真实案例场景
+- `["experience"]`:执行经验 - 反思与教训
+**部分关键字段**:
+- `task`: 产生该知识的上下文描述
+- `content`: 知识正文
+- `tools`: 涉及的 tool_id 列表
+- `support_capability`: 支持的 capability_id 列表
+- `resource_ids`: 关联的其他实体文档资源
 
 
-### 3. resources 表(资源表)
-存储文档、代码、凭证等资源:
+### 5. resources 表(文件内容表)
+存储一切具体的网页文档、代码、凭证。
+- `content_type`: text/code/credential/cookie
 
 
-**关键字段**:
-- `id`: 资源 ID,路径格式(如 `code/crawler/baidu`)
-- `title`: 标题
-- `body`: 公开内容
-- `secure_body`: 私有/加密内容
-- `content_type`: 类型(text/code/credential/cookie)
-- `metadata.knowledge_ids`: 关联的知识 ID 列表
+💡 总结关系链:
+【业务需求(Requirement)】 👉 拆解为多个【原子能力(Capability)】 👉 对应多个可选【工具(Tool)】 👉 工具运用过程中沉淀【通用知识(Knowledge)】
 
 
 ## 可用工具
 ## 可用工具
 
 
-### 知识库工具
-- `knowledge_search(query, top_k, types, owner)`: 搜索知识库
-- `knowledge_save(task, content, types, tags, resource_ids, score)`: 保存知识
-- `knowledge_list(limit, types, scopes)`: 列出知识
-- `knowledge_update(knowledge_id, ...)`: 更新知识
-- `resource_save(resource_id, title, body, content_type, metadata)`: 保存资源
-- `resource_get(resource_id)`: 获取资源详情
+### 全局跨表检索工具
+你可以通过以下内置工具查询整个关系链条,请结合这些工具去主动发掘和梳理数据库中的关联性。
+- `requirement_search(query, top_k)`: 语义检索业务需求
+- `requirement_list(limit, offset, status)`: 列表分页查看所有需求
+- `capability_search(query, top_k)`: 语义检索原子能力
+- `capability_list(limit, offset)`: 列表分页查看所有原子能力
+- `tool_search(query, top_k, status)`: 语义检索工具
+- `tool_list(limit, offset, status)`: 列表分页获取所有工具
+- `knowledge_search(query, top_k, types, owner)`: 搜索通用知识片段
+- `knowledge_list(limit, types, scopes)`: 列表分页通用知识
+- `knowledge_save(...)` / `knowledge_update(...)`: 保存或更新知识
+- `resource_get(resource_id)`: 获取详细长文本资源内容
+- `resource_save(...)`: 保存纯文本文件/凭据或代码文档资源
 
 
 ### 本地缓存工具(优先使用)
 ### 本地缓存工具(优先使用)
 - `cache_research_data(data, source)`: 缓存调研数据到本地(不入库)
 - `cache_research_data(data, source)`: 缓存调研数据到本地(不入库)
@@ -92,16 +110,16 @@ $system$
 调研 Agent 想了解知识库中已有什么信息。
 调研 Agent 想了解知识库中已有什么信息。
 
 
 **处理方式**:
 **处理方式**:
-1. 调用 `knowledge_search` 搜索相关知识,按 types 分类查询
-   - `types=["tool"]` 查工具知识
-   - `types=["strategy"]` 查工序知识
-   - `types=["case"]` 查用例知识
-   - `types=["experience"]` 查执行经验
-2. 分析搜索结果,整理出
-   - 已有信息:知识库中已覆盖的方面(按类型分组)
-   - 缺失信息:知识库中缺少的关键信息
-   - 调研建议:明确的调研方向和优先级
-3. 回复结构化报告
+1. 识别用户的查询意图(想问顶层需求?还是具体的工具?还是使用步骤?)
+   - 找目标/痛点?调用 `requirement_search`。
+   - 找能力/模块?调用 `capability_search`。
+   - 找软件/脚本?调用 `tool_search`。
+   - 找技巧/反思/经验?调用 `knowledge_search`。
+2. 挖掘关联情况进行结构化报告
+   比如如果用户询问具体需求,你可以调用 `requirement_search` 之后顺藤摸瓜拿到里面的 `atomics` 列表,再去调用 `capability_search` 查对应的工具,这样能直接给用户展示“该需求需要哪些原子能力,由哪些工具解决”这套完整链条。
+3. 请自由组合使用查询列表工具返回详细分析!
+
+**要求**:如果发现查询的层级中有缺失(如找到需求,但下面没挂接原子技能;或有技能但没有可用工具),要在报告中重点突出这些缺失部分并提出进一步调研建议!
 
 
 **回复格式**:
 **回复格式**:
 ```
 ```
@@ -130,113 +148,64 @@ $system$
 
 
 **要求**:简洁直接,每个要点 1-2 句话。重点突出缺失部分。
 **要求**:简洁直接,每个要点 1-2 句话。重点突出缺失部分。
 
 
-### 2. 上传请求
-调研 Agent 发送调研结果,格式为 JSON,包含 tools/resources/knowledge。
+### 2. 上传请求与图谱预处理编排 (预整理阶段)
+调研 Agent 发送调研结果,格式为 JSON,包含 tools/resources/knowledge。由于收到的经验通常是碎片化的,你需要充当**图谱整理员**,构建严格的底层库格式实体,并把它们写入草稿池维持全貌。
 
 
 **消息类型**:
 **消息类型**:
-- `[UPLOAD] {...}`: 增量上传,缓存到本地
-- `[UPLOAD:BATCH] ...`: 批量上传(多条合并),一起处理
-
-**处理方式(默认只缓存,不入库)**:
-1. 解析消息内容(批量消息需要解析多个 JSON)
-2. **调用 `cache_research_data(data, source)`** 缓存到本地
-   - 直接传 JSON 字符串或字典,工具会自动处理
-   - 不要用 `write_file`,缓存工具会自动生成文件名和统计
-3. 如果是 BATCH,调用 `organize_cached_data(merge=True)` 整理
-4. 回复缓存确认 + 统计
-5. **不要自动入库**:只缓存和整理,等待用户明确要求"提交到数据库"
-
-**批量处理优势**:
-- 多条 upload 消息会自动合并成一条 BATCH 消息
-- 可以一次性去重、整理所有数据,全局观更好
-- 如果正在处理时收到新消息,通过 `get_current_context` 看到队列状态,可以快速结束当前轮次
+- `[UPLOAD:BATCH] ...`: 批量上传(多条合并),需要你进行结构化图谱组装。
+
+**图谱排版处理方式(不入库,只写本地草稿)**:
+1. **读取或初始化草稿池**:使用 `read_file(".cache/.knowledge/pre_upload_list.json")` 获取目前本地堆积的图谱草稿(如果没有则初始化为含有四大数组 {"requirements":[], "capabilities":[], "tools":[], "knowledge":[]} 的长 JSON 字符串结构)。
+2. **现网检索关联补全(极其重要!)**:
+   对于传来的经验,你必须思考:
+   - 如果是一条**工具**,调用 `tool_search` 确认是否存在,调用 `capability_search` 寻找它到底隶属于哪一条**原子能力**,并记录下来。
+   - 如果这个衍生出了**新的原子能力**,那你必须补充构造一个新的 Capability 实体。
+   - 如果发现某个 Capability,继续思考它解决什么**业务需求**(调 `requirement_search`)。要是没需求承载它,你可以自行脑补构造一个对应的 Requirement 实体。
+3. **格式严格转化 (Format Conversion)**:
+   无论如何,都要把你补全的故事链转化为**远端后端约定的精美 JSON 格式实体**,放进刚刚的草稿对应数组中去:
+   - **RequirementIn**: `{"id": "req_...", "description": "...", "atomics": ["cap_id_1"], "status": "未满足"}`
+   - **CapabilityIn**: `{"id": "cap_...", "name": "...", "description": "...", "requirements": ["req_id_..."], "tools": ["tools/..."]}`
+   - **ToolIn**: `{"id": "tools/...", "name": "...", "introduction": "...", "capabilities": ["cap_id_..."], "tool_knowledge": ["knowledge-..."]}`
+   - **KnowledgeIn**: `{"task": "...", "content": "...", "types": [...], "score": 3, "tools": ["tools/..."], "support_capability": ["cap_id_..."], "source": {"category": "..."}}`
+4. **回写草稿**:去重和补充处理完毕后,将拼接好的四大数组大 JSON,用 `write_file(".cache/.knowledge/pre_upload_list.json", content_json)` 完整覆写保存。
+5. **汇报整理概要**:告诉用户你刚才将什么关联到了什么,新增了什么节点,暂存草稿池完成。
 
 
 **回复格式**:
 **回复格式**:
 ```
 ```
-✅ 已缓存到本地(批量处理 N 条)
+✅ 增量经验已整理并存入本地临时草稿池!
 
 
-**数据统计**:
-- 工具: X 个
-- 资源: Y 个
-- 知识: Z 个(工具知识 A / 工序知识 B / 用例知识 C / 执行经验 D)
+**本次处理推断的图谱关系**:
+- 🔍 发现工具:`xxx`,为其寻找并**间接关联**到了已有原子能力 `cap_yyy`
+- 🆕 缺少相关需求:已在草稿中临时构造需求 `req_zzz` (爬取XXX),并将其指向能力 `cap_yyy`
+- 📝 经验内容已被标准格式化注入 Knowledge 组。
 
 
-**去重检查**:
-- 新增: N 条
-- 重复跳过: M 条
-
-**缓存位置**:`.cache/.knowledge/buffer/{source}-{timestamp}.json`
-
-💡 数据已保存到本地缓存,尚未入库。如需提交到数据库,请回复"提交到数据库"或"入库"。
+💡 以上改动已记入 `.cache/.knowledge/pre_upload_list.json`。如确认处理完善并需要实装,请回复"提交到数据库"或"入库"。
 ```
 ```
 
 
-### 3. 整理请求
-用户或调研 Agent 要求整理缓存数据。
-
-**处理方式**:
-1. 调用 `organize_cached_data(merge=True)` 整理所有缓存文件
-2. 去重、合并,保存到 `.cache/.knowledge/organized/`
-3. **按知识类型分组统计**
-4. 回复整理统计
-
-**回复格式**:
-```
-已整理完成
-
-**按类型统计**:
-- 工具知识: X → Y (去重 Z)
-- 工序知识: A → B (去重 C)
-- 用例知识: D → E (去重 F)
-- 执行经验: G → H (去重 I)
-
-**资源统计**:
-- 资源: J → K (去重 L)
-
-**工具统计**:
-- 工具: M → N (去重 O)
-```
+### 3. 查看草稿请求
+用户询问当前草稿里有什么,或是要求查看整理清单。
+**处理方式**:直接 `read_file(".cache/.knowledge/pre_upload_list.json")` 然后格式化打印出来给用户肉眼 Review。
 
 
 ### 4. 提交到数据库请求
 ### 4. 提交到数据库请求
-用户明确要求将缓存数据提交到数据库(关键词:"提交到数据库"、"入库"、"保存到数据库")。
-
-**前置检查**:
-- 如果 `commit_to_database` 工具不可用,回复:"当前配置为仅缓存模式,不支持入库。如需入库,请联系管理员修改配置。"
+用户明确要求将排版好的草稿提交到数据库(关键词:"提交到数据库"、"入库"、"保存到数据库")。
 
 
 **处理方式**:
 **处理方式**:
-1. 如果 buffer 中还有未整理的数据,先调用 `organize_cached_data()`
-2. 调用 `commit_to_database()` 将整理后的数据提交到数据库
-3. **建立关联关系**:
-   - 工具 → tool_table,关联 knowledge/case_knowledge/process_knowledge
-   - 资源 → resources,在 metadata.knowledge_ids 中关联知识
-   - 知识 → knowledge,在 resource_ids 中关联资源
-4. 回复提交统计
+1. 确认无误后,直接调用 `commit_to_database()`,它会自动去读取 `.cache/.knowledge/pre_upload_list.json`,并执行远端 POST 写入 Requirement、Capability、Tool 以及 Knowledge 表。
+2. 回复提交成功的节点统计,并告知用户草稿池已由脚本清理。
 
 
 **回复格式**:
 **回复格式**:
 ```
 ```
-✅ 已提交到数据库
-
-**knowledge 表**:
-- 工具知识: X 条
-- 工序知识: Y 条
-- 用例知识: Z 条
-- 执行经验: W 条
+✅ 打包流水线入库成功!
 
 
-**tool_table 表**:
-- 工具: A 个(已关联知识)
+**本次真实刷入数据库的节点**:
+- 需求 (Requirement): A 个
+- 原子能力 (Capability): B 个
+- 具体工具 (Tool): C 个
+- 通用知识 (Knowledge): D 条
 
 
-**resources 表**:
-- 资源: B 个(已关联知识)
-
-**关联关系**:
-- 工具 ↔ 知识: C 条
-- 资源 ↔ 知识: D 条
+🎉 暂存草稿已清空。
 ```
 ```
 
 
-**重要**:
-- 只有在用户明确要求"提交到数据库"、"入库"、"保存到数据库"时才尝试入库
-- 如果 `commit_to_database` 工具不可用,说明当前为"仅缓存模式",告知用户无法入库
-- 默认情况下,只缓存和整理,不入库
-- 如果用户没有明确要求入库,不要主动提示或询问是否入库
-
 ## 响应原则
 ## 响应原则
 
 
 1. **快速响应**:尽快回复,不要做不必要的操作
 1. **快速响应**:尽快回复,不要做不必要的操作

+ 12 - 61
knowhub/agents/knowledge_manager.py

@@ -33,17 +33,19 @@ ENABLE_DATABASE_COMMIT = False  # 是否允许入库(False=只缓存,True=
 def get_knowledge_manager_config(enable_db_commit: bool = ENABLE_DATABASE_COMMIT) -> RunConfig:
 def get_knowledge_manager_config(enable_db_commit: bool = ENABLE_DATABASE_COMMIT) -> RunConfig:
     """获取 Knowledge Manager 配置(根据是否允许入库动态调整工具列表)"""
     """获取 Knowledge Manager 配置(根据是否允许入库动态调整工具列表)"""
     tools = [
     tools = [
+        # 只读查询工具(用于跨表检索和关联分析)
         "knowledge_search",
         "knowledge_search",
-        "knowledge_save",
         "knowledge_list",
         "knowledge_list",
-        "knowledge_update",
-        "resource_save",
-        "resource_get",
+        "tool_search",
+        "tool_list",
+        "capability_search",
+        "capability_list",
+        "requirement_search",
+        "requirement_list",
+        # 文件工具(用于维护 pre_upload_list.json 草稿)
         "read_file",
         "read_file",
         "write_file",
         "write_file",
         # 本地缓存工具
         # 本地缓存工具
-        "cache_research_data",
-        "organize_cached_data",
         "list_cache_status",
         "list_cache_status",
     ]
     ]
 
 
@@ -273,57 +275,7 @@ async def start_knowledge_manager(
             "content": merged_content
             "content": merged_content
         })
         })
 
 
-    # --- 快速处理 ask 查询(不经过队列)---
-    async def handle_ask_query(sender: str, content: str):
-        """快速响应 ask 查询,不阻塞 upload 处理"""
-        try:
-            logger.info(f"[KM] <- 快速查询: {sender}")
-
-            # 1. 查询数据库
-            from agent.tools.builtin.knowledge import knowledge_search
-            db_result = await knowledge_search(query=content, top_k=5, min_score=3)
-
-            # 2. 读取缓存(正在处理的 upload 数据)
-            from knowhub.internal_tools.cache_manager import list_cache_status
-            cache_status = await list_cache_status()
-
-            # 3. 组合回复
-            response_parts = []
-
-            if db_result.output and db_result.output != "未找到相关知识":
-                response_parts.append("## 数据库中的知识\n\n" + db_result.output)
-
-            if cache_status.metadata and cache_status.metadata.get("files"):
-                cache_files = cache_status.metadata["files"]
-                if cache_files:
-                    response_parts.append(
-                        f"## 缓存中的数据\n\n"
-                        f"正在处理 {len(cache_files)} 个缓存文件,包含最新调研数据(尚未入库)。\n"
-                        f"如需查看详情,请稍后再次查询。"
-                    )
-
-            if not response_parts:
-                response_parts.append("暂无相关知识,数据库和缓存均为空。")
-
-            response_text = "\n\n".join(response_parts)
-
-            # 4. 立即回复
-            client.send_message(
-                chat_id=chat_id,
-                receiver=sender,
-                content=response_text
-            )
-            logger.info(f"[KM] -> 快速回复: {sender} ({len(response_text)} 字符)")
-
-        except Exception as e:
-            logger.error(f"[KM] 快速查询失败: {e}", exc_info=True)
-            client.send_message(
-                chat_id=chat_id,
-                receiver=sender,
-                content=f"查询失败: {e}"
-            )
-
-    # --- 消息回调(ask 快速通道,upload 批处理)---
+    # --- 消息回调(所有消息统一经过 AgentRunner 处理)---
     async def on_message(msg: dict):
     async def on_message(msg: dict):
         nonlocal upload_timer
         nonlocal upload_timer
 
 
@@ -336,10 +288,9 @@ async def start_knowledge_manager(
 
 
         # 判断消息类型(根据前缀标记)
         # 判断消息类型(根据前缀标记)
         if content.startswith("[ASK]"):
         if content.startswith("[ASK]"):
-            # 快速通道:立即处理,不入队
-            query = content[5:].strip()  # 去掉 [ASK] 前缀
-            logger.info(f"[KM] <- 收到查询消息: {sender} (快速通道)")
-            asyncio.create_task(handle_ask_query(sender, query))
+            # ASK 查询:入队,由 AgentRunner 驱动(带 Trace、可跨表推理)
+            logger.info(f"[KM] <- 收到查询消息: {sender} (入队)")
+            await message_queue.put(msg)
 
 
         elif content.startswith("[UPLOAD"):
         elif content.startswith("[UPLOAD"):
             # 批处理:加入缓冲区,延迟处理
             # 批处理:加入缓冲区,延迟处理

+ 0 - 279
knowhub/agents/knowledge_manager_v2.py

@@ -1,279 +0,0 @@
-"""
-Knowledge Manager V2 - 轻量级缓存 + 按需 Agent
-
-架构:
-1. 收到 upload 消息 → 直接调用 cache_research_data,不启动 Agent
-2. 收到 query/organize 消息 → 启动 Agent 处理
-3. 消息队列保证不丢消息
-
-优势:
-- upload 操作轻量快速
-- 只在需要时才启动 Agent(查询、整理、入库)
-"""
-
-import asyncio
-import json
-import logging
-import sys
-from pathlib import Path
-from typing import Optional
-
-# 确保项目路径可用
-sys.path.insert(0, str(Path(__file__).parent.parent.parent))
-
-from agent.core.runner import AgentRunner, RunConfig
-from agent.trace import FileSystemTraceStore, Trace, Message
-from agent.llm import create_qwen_llm_call
-from agent.llm.prompts import SimplePrompt
-from agent.tools.builtin.knowledge import KnowledgeConfig
-
-# 导入缓存工具
-from knowhub.internal_tools.cache_manager import cache_research_data
-
-logger = logging.getLogger("agents.knowledge_manager_v2")
-
-# Knowledge Manager Agent 配置
-KNOWLEDGE_MANAGER_CONFIG = RunConfig(
-    model="qwen3.5-plus",
-    temperature=0.2,
-    max_iterations=50,
-    agent_type="knowledge_manager",
-    name="知识库管理",
-    goal_compression="none",
-    knowledge=KnowledgeConfig(
-        enable_extraction=False,
-        enable_completion_extraction=False,
-        enable_injection=False,
-    ),
-    tools=[
-        "knowledge_search",
-        "knowledge_save",
-        "knowledge_list",
-        "knowledge_update",
-        "resource_save",
-        "resource_get",
-        "read_file",
-        "write_file",
-        "cache_research_data",
-        "organize_cached_data",
-        "commit_to_database",
-        "list_cache_status",
-    ],
-)
-
-
-async def start_knowledge_manager_v2(
-    contact_id: str = "knowledge_manager",
-    server_url: str = "ws://43.106.118.91:8105",
-    chat_id: str = "main"
-):
-    """
-    启动 Knowledge Manager V2(轻量级缓存 + 按需 Agent)
-
-    消息类型:
-    1. upload 消息 → 直接缓存,不启动 Agent
-    2. query/organize/commit 消息 → 启动 Agent 处理
-    """
-    logger.info(f"正在启动 Knowledge Manager V2...")
-    logger.info(f"  - Contact ID: {contact_id}")
-    logger.info(f"  - Server: {server_url}")
-
-    # 导入 IM Client
-    try:
-        sys.path.insert(0, str(Path(__file__).parent.parent.parent / "im-client"))
-        from client import IMClient
-    except ImportError as e:
-        logger.error(f"无法导入 IM Client: {e}")
-        return
-
-    # --- 初始化 AgentRunner ---
-    store = FileSystemTraceStore(base_path=".trace")
-    llm_call = create_qwen_llm_call(model=KNOWLEDGE_MANAGER_CONFIG.model)
-
-    runner = AgentRunner(
-        trace_store=store,
-        llm_call=llm_call,
-        debug=True
-    )
-
-    # 加载 system prompt
-    prompt_path = Path(__file__).parent / "knowledge_manager.prompt"
-    prompt = SimplePrompt(prompt_path)
-    system_messages = prompt.build_messages()
-
-    # Trace 状态
-    current_trace_id = None
-    message_queue = asyncio.Queue()
-
-    # --- 初始化 IM Client ---
-    client = IMClient(
-        contact_id=contact_id,
-        server_url=server_url,
-        data_dir=str(Path.home() / ".knowhub" / "im_data")
-    )
-    client.open_window(chat_id=chat_id)
-
-    # --- 消息分类处理器 ---
-    async def message_processor():
-        nonlocal current_trace_id
-
-        while True:
-            msg = await message_queue.get()
-
-            sender = msg.get("sender")
-            content = msg.get("content", "")
-
-            logger.info(f"[KM] <- 收到消息: {sender}")
-            logger.info(f"[KM]    内容: {content[:120]}{'...' if len(content) > 120 else ''}")
-
-            try:
-                # 判断消息类型
-                is_upload = "增量上传" in content or "最终提交" in content
-
-                if is_upload:
-                    # 直接缓存,不启动 Agent
-                    await handle_upload_message(content, sender, client, chat_id)
-                else:
-                    # 启动 Agent 处理
-                    await handle_agent_message(
-                        content, sender, client, chat_id,
-                        runner, system_messages, current_trace_id
-                    )
-
-            except Exception as e:
-                logger.error(f"[KM] 处理失败: {e}", exc_info=True)
-                # 回复错误
-                client.send_message(
-                    chat_id=chat_id,
-                    receiver=sender,
-                    content=f"处理失败: {str(e)}"
-                )
-
-            message_queue.task_done()
-
-    async def handle_upload_message(content: str, sender: str, client, chat_id: str):
-        """处理 upload 消息:直接缓存,不启动 Agent"""
-        logger.info(f"[KM] 处理 upload 消息(轻量级)")
-
-        # 提取 JSON 数据
-        try:
-            # 找到 JSON 部分
-            json_start = content.find("{")
-            if json_start == -1:
-                raise ValueError("消息中没有 JSON 数据")
-
-            json_str = content[json_start:]
-            data = json.loads(json_str)
-
-            # 直接调用缓存工具
-            result = await cache_research_data(data=data, source=sender)
-
-            # 回复
-            response = result.output if hasattr(result, 'output') else str(result)
-            client.send_message(
-                chat_id=chat_id,
-                receiver=sender,
-                content=response
-            )
-            logger.info(f"[KM] -> 已缓存并回复: {sender}")
-
-        except Exception as e:
-            logger.error(f"[KM] 缓存失败: {e}")
-            client.send_message(
-                chat_id=chat_id,
-                receiver=sender,
-                content=f"缓存失败: {str(e)}"
-            )
-
-    async def handle_agent_message(
-        content: str, sender: str, client, chat_id: str,
-        runner, system_messages, trace_id
-    ):
-        """处理需要 Agent 的消息:查询、整理、入库"""
-        logger.info(f"[KM] 处理 Agent 消息(重量级)")
-
-        nonlocal current_trace_id
-
-        # 构造 user message
-        if current_trace_id is None:
-            messages = system_messages + [{"role": "user", "content": content}]
-        else:
-            messages = [{"role": "user", "content": content}]
-
-        config = RunConfig(
-            model=KNOWLEDGE_MANAGER_CONFIG.model,
-            temperature=KNOWLEDGE_MANAGER_CONFIG.temperature,
-            max_iterations=KNOWLEDGE_MANAGER_CONFIG.max_iterations,
-            agent_type=KNOWLEDGE_MANAGER_CONFIG.agent_type,
-            name=KNOWLEDGE_MANAGER_CONFIG.name,
-            goal_compression=KNOWLEDGE_MANAGER_CONFIG.goal_compression,
-            tools=KNOWLEDGE_MANAGER_CONFIG.tools,
-            knowledge=KNOWLEDGE_MANAGER_CONFIG.knowledge,
-            trace_id=current_trace_id,
-        )
-
-        # 执行 AgentRunner
-        response_text = ""
-        async for item in runner.run(messages=messages, config=config):
-            if isinstance(item, Trace):
-                current_trace_id = item.trace_id
-                if item.status == "running":
-                    logger.info(f"[KM] Trace: {item.trace_id[:8]}...")
-                elif item.status == "completed":
-                    logger.info(f"[KM] Trace 完成")
-                elif item.status == "failed":
-                    logger.error(f"[KM] Trace 失败: {item.error_message}")
-
-            elif isinstance(item, Message):
-                if item.role == "assistant":
-                    msg_content = item.content
-                    if isinstance(msg_content, dict):
-                        text = msg_content.get("text", "")
-                        tool_calls = msg_content.get("tool_calls")
-                        if text and not tool_calls:
-                            response_text = text
-
-                elif item.role == "tool":
-                    tool_content = item.content
-                    tool_name = tool_content.get("tool_name", "?") if isinstance(tool_content, dict) else "?"
-                    logger.info(f"[KM] 工具: {tool_name}")
-
-        # 回复
-        if response_text:
-            client.send_message(
-                chat_id=chat_id,
-                receiver=sender,
-                content=response_text
-            )
-            logger.info(f"[KM] -> 已回复: {sender}")
-        else:
-            logger.warning(f"[KM] AgentRunner 没有生成回复")
-
-    # --- 消息回调(只入队)---
-    async def on_message(msg: dict):
-        sender = msg.get("sender")
-        if sender == contact_id:
-            return
-
-        logger.info(f"[KM] <- 入队: {sender} (队列长度: {message_queue.qsize() + 1})")
-        await message_queue.put(msg)
-
-    client.on_message(on_message, chat_id="*")
-
-    # 启动消息处理器
-    processor_task = asyncio.create_task(message_processor())
-
-    # 启动 IM Client
-    logger.info("✅ Knowledge Manager V2 已启动(轻量级缓存 + 按需 Agent)")
-    try:
-        await client.run()
-    finally:
-        processor_task.cancel()
-        try:
-            await processor_task
-        except asyncio.CancelledError:
-            pass
-
-
-if __name__ == "__main__":
-    asyncio.run(start_knowledge_manager_v2())

+ 108 - 353
knowhub/internal_tools/cache_manager.py

@@ -1,14 +1,15 @@
 """
 """
-Knowledge Manager 本地缓存工具
+Knowledge Manager 本地缓存与整理工具
 
 
 负责:
 负责:
-1. 接收调研数据,存入本地缓存
-2. 整理缓存数据(去重、合并、关联)
-3. 可选提交到数据库或仅保存本地
+1. 维护本地的 pre_upload_list.json 图谱草稿
+2. 提供 commit_to_database 将草稿分发入库(Requirements, Capabilities, Tools, Knowledge)
 """
 """
 
 
+import os
 import json
 import json
 import logging
 import logging
+import httpx
 from pathlib import Path
 from pathlib import Path
 from typing import Dict, Any, List, Optional
 from typing import Dict, Any, List, Optional
 from datetime import datetime
 from datetime import datetime
@@ -18,328 +19,119 @@ logger = logging.getLogger(__name__)
 
 
 # 缓存目录
 # 缓存目录
 CACHE_DIR = Path(".cache/.knowledge")
 CACHE_DIR = Path(".cache/.knowledge")
-BUFFER_DIR = CACHE_DIR / "buffer"
-ORGANIZED_DIR = CACHE_DIR / "organized"
+PRE_UPLOAD_FILE = CACHE_DIR / "pre_upload_list.json"
 
 
 
 
 def _ensure_dirs():
 def _ensure_dirs():
     """确保缓存目录存在"""
     """确保缓存目录存在"""
-    BUFFER_DIR.mkdir(parents=True, exist_ok=True)
-    ORGANIZED_DIR.mkdir(parents=True, exist_ok=True)
+    CACHE_DIR.mkdir(parents=True, exist_ok=True)
 
 
 
 
+@tool()
+async def organize_cached_data(merge: bool = True) -> ToolResult:
+    """为了兼容旧指令保留。现在实际上不再需要独立调用。"""
+    return ToolResult(title="ℹ️ 提示", output="请直接使用 read_file 和 write_file 编辑 pre_upload_list.json。")
+
+@tool()
+async def cache_research_data(data: str | Dict[str, Any], source: str = "unknown") -> ToolResult:
+    """为了兼容旧指令保留。现在实际上不再需要独立调用。"""
+    return ToolResult(title="ℹ️ 提示", output="请直接使用 read_file 和 write_file 编辑 pre_upload_list.json。")
+
 @tool(
 @tool(
     description=(
     description=(
-        "缓存调研数据到本地(不入库)。"
-        "接受 JSON 字符串或字典,自动解析。"
-        "适用于增量上传场景,先缓存后整理。"
+        "将本地维护好的 pre_upload_list.json 预上传图谱草稿,"
+        "分发提交到远端真实的 Requirements、Capabilities、Tools、Knowledge 数据表中。"
     )
     )
 )
 )
-async def cache_research_data(
-    data: str | Dict[str, Any],
-    source: str = "unknown",
-) -> ToolResult:
-    """
-    缓存调研数据到本地(不入库)
-
-    Args:
-        data: 调研结果(JSON 字符串或字典),包含 tools/resources/knowledge
-        source: 数据来源标识(如 agent_id)
-
-    Returns:
-        缓存确认和统计
-
-    Examples:
-        # 方式 1:直接传字典
-        cache_research_data(data={"knowledge": [...]}, source="agent_research")
-
-        # 方式 2:传 JSON 字符串
-        cache_research_data(data='{"knowledge": [...]}', source="agent_research")
-    """
-    try:
-        _ensure_dirs()
-
-        # 自动解析 JSON 字符串
-        if isinstance(data, str):
-            try:
-                data = json.loads(data)
-            except json.JSONDecodeError as e:
-                return ToolResult(
-                    title="❌ JSON 解析失败",
-                    output=f"无法解析 JSON 字符串: {e}",
-                    error=str(e)
-                )
-
-        # 生成文件名
-        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
-        filename = f"{source}_{timestamp}.json"
-        filepath = BUFFER_DIR / filename
-
-        # 写入缓存
-        with open(filepath, "w", encoding="utf-8") as f:
-            json.dump(data, f, ensure_ascii=False, indent=2)
-
-        # 统计
-        stats = []
-        if data.get("tools"):
-            stats.append(f"工具: {len(data['tools'])} 个")
-        if data.get("resources"):
-            stats.append(f"资源: {len(data['resources'])} 个")
-        if data.get("knowledge"):
-            stats.append(f"知识: {len(data['knowledge'])} 个")
-
-        return ToolResult(
-            title="✅ 已缓存到本地",
-            output=f"文件: {filename}\n\n" + "\n".join(f"- {s}" for s in stats),
-            metadata={"filepath": str(filepath), "stats": stats}
-        )
-
-    except Exception as e:
-        logger.error(f"缓存失败: {e}")
-        return ToolResult(
-            title="❌ 缓存失败",
-            output=f"错误: {str(e)}",
-            error=str(e)
-        )
-
-
-@tool()
-async def organize_cached_data(
-    merge: bool = True,
-) -> ToolResult:
-    """
-    整理缓存数据(去重、合并)
-
-    Args:
-        merge: 是否合并所有缓存文件
-
-    Returns:
-        整理后的数据统计
-    """
-    try:
-        _ensure_dirs()
-
-        # 读取所有缓存文件
-        buffer_files = list(BUFFER_DIR.glob("*.json"))
-        if not buffer_files:
-            return ToolResult(
-                title="ℹ️ 无缓存数据",
-                output="buffer 目录为空"
-            )
-
-        all_tools = []
-        all_resources = []
-        all_knowledge = []
-
-        for filepath in buffer_files:
-            with open(filepath, "r", encoding="utf-8") as f:
-                data = json.load(f)
-                all_tools.extend(data.get("tools", []))
-                all_resources.extend(data.get("resources", []))
-                all_knowledge.extend(data.get("knowledge", []))
-
-        # 去重(基于名称/标题)
-        def dedupe_by_key(items: List[Dict], key: str) -> List[Dict]:
-            seen = set()
-            result = []
-            for item in items:
-                identifier = item.get(key)
-                if identifier and identifier not in seen:
-                    seen.add(identifier)
-                    result.append(item)
-            return result
-
-        tools_deduped = dedupe_by_key(all_tools, "名称")
-        resources_deduped = dedupe_by_key(all_resources, "标题")
-        knowledge_deduped = dedupe_by_key(all_knowledge, "内容")
-
-        # 保存整理后的数据
-        organized_data = {
-            "tools": tools_deduped,
-            "resources": resources_deduped,
-            "knowledge": knowledge_deduped,
-            "organized_at": datetime.now().isoformat(),
-            "source_files": [f.name for f in buffer_files]
-        }
-
-        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
-        organized_file = ORGANIZED_DIR / f"organized_{timestamp}.json"
-
-        with open(organized_file, "w", encoding="utf-8") as f:
-            json.dump(organized_data, f, ensure_ascii=False, indent=2)
-
-        # 清空 buffer(可选)
-        if merge:
-            for filepath in buffer_files:
-                filepath.unlink()
-
-        stats = [
-            f"工具: {len(all_tools)} → {len(tools_deduped)} (去重 {len(all_tools) - len(tools_deduped)})",
-            f"资源: {len(all_resources)} → {len(resources_deduped)} (去重 {len(all_resources) - len(resources_deduped)})",
-            f"知识: {len(all_knowledge)} → {len(knowledge_deduped)} (去重 {len(all_knowledge) - len(knowledge_deduped)})",
-        ]
-
-        return ToolResult(
-            title="✅ 整理完成",
-            output=f"文件: {organized_file.name}\n\n" + "\n".join(f"- {s}" for s in stats),
-            metadata={"filepath": str(organized_file), "stats": organized_data}
-        )
-
-    except Exception as e:
-        logger.error(f"整理失败: {e}")
-        return ToolResult(
-            title="❌ 整理失败",
-            output=f"错误: {str(e)}",
-            error=str(e)
-        )
-
-
-@tool()
-async def commit_to_database(
-    organized_file: Optional[str] = None,
-) -> ToolResult:
+async def commit_to_database() -> ToolResult:
     """
     """
-    将整理后的数据提交到数据库,建立完整的关联关系
-
-    Args:
-        organized_file: 指定要提交的文件(不指定则提交最新的)
-
+    提交预处理好的知识图谱到数据库(涵盖 Requirements, Capabilities, Tools, Knowledge)。
+    从 .cache/.knowledge/pre_upload_list.json 读取。
+    
     Returns:
     Returns:
-        提交结果统计(按知识类型分组)
+        提交结果和统计信息
     """
     """
     try:
     try:
-        _ensure_dirs()
-
-        # 找到要提交的文件
-        if organized_file:
-            filepath = ORGANIZED_DIR / organized_file
-        else:
-            organized_files = sorted(ORGANIZED_DIR.glob("organized_*.json"))
-            if not organized_files:
-                return ToolResult(
-                    title="ℹ️ 无整理数据",
-                    output="organized 目录为空,请先调用 organize_cached_data"
-                )
-            filepath = organized_files[-1]
-
-        if not filepath.exists():
+        if not PRE_UPLOAD_FILE.exists():
             return ToolResult(
             return ToolResult(
-                title="❌ 文件不存在",
-                output=f"文件: {filepath.name}"
+                title="⚠️ 无草稿可提交",
+                output=f"未找到预处理草稿文件:{PRE_UPLOAD_FILE}"
             )
             )
 
 
-        # 读取数据
-        with open(filepath, "r", encoding="utf-8") as f:
+        with open(PRE_UPLOAD_FILE, "r", encoding="utf-8") as f:
             data = json.load(f)
             data = json.load(f)
 
 
-        # 导入数据库工具
-        from agent.tools.builtin.knowledge import resource_save, knowledge_save
-
-        # 统计变量
-        saved_tools = 0
-        saved_resources = 0
-        knowledge_by_type = {"tool": 0, "strategy": 0, "case": 0, "experience": 0}
-        errors = []
-
-        # 映射:resource_id -> knowledge_ids(用于反向关联)
-        resource_to_knowledge = {}
-
-        # 第一步:保存资源
-        for resource in data.get("resources", []):
-            try:
-                resource_id = resource.get("id", f"resource_{saved_resources}")
-                await resource_save(
-                    resource_id=resource_id,
-                    title=resource.get("标题", ""),
-                    body=resource.get("内容", ""),
-                    content_type=resource.get("类型", "text"),
-                    metadata=resource.get("元数据", {})
-                )
-                saved_resources += 1
-                resource_to_knowledge[resource_id] = []
-            except Exception as e:
-                errors.append(f"资源 {resource.get('标题')}: {e}")
-
-        # 第二步:保存知识,建立 resource_ids 关联
-        for knowledge in data.get("knowledge", []):
-            try:
-                # 提取关联的 resource_ids
-                resource_ids = knowledge.get("resource_ids", [])
-
-                # 保存知识
-                result = await knowledge_save(
-                    task=knowledge.get("主题", ""),
-                    content=knowledge.get("内容", ""),
-                    types=knowledge.get("类型", []),
-                    tags=knowledge.get("标签", {}),
-                    resource_ids=resource_ids,
-                )
-
-                # 统计知识类型
-                types = knowledge.get("类型", [])
-                for t in types:
-                    if t in knowledge_by_type:
-                        knowledge_by_type[t] += 1
+        reqs = data.get("requirements", [])
+        caps = data.get("capabilities", [])
+        tools_list = data.get("tools", [])
+        knowledges = data.get("knowledge", [])
 
 
-                # 提取 knowledge_id,用于反向关联
-                knowledge_id = result.metadata.get("knowledge_id")
-                if knowledge_id:
-                    for rid in resource_ids:
-                        if rid in resource_to_knowledge:
-                            resource_to_knowledge[rid].append(knowledge_id)
+        api_base = os.environ.get("KNOWHUB_API", "http://localhost:9999")
 
 
-            except Exception as e:
-                errors.append(f"知识 {knowledge.get('主题')}: {e}")
+        saved_reqs, saved_caps, saved_tools, saved_knows = 0, 0, 0, 0
+        errors = []
 
 
-        # 第三步:保存工具(作为 resource,类型为 tool)
-        for tool in data.get("tools", []):
+        async with httpx.AsyncClient(timeout=30.0) as client:
+            # 1. 提交 Requirements
+            for r in reqs:
+                try:
+                    res = await client.post(f"{api_base}/api/requirement", json=r)
+                    res.raise_for_status()
+                    saved_reqs += 1
+                except Exception as e:
+                    errors.append(f"提交需求失败 {r.get('id', '')}: {e}")
+
+            # 2. 提交 Capabilities
+            for c in caps:
+                try:
+                    res = await client.post(f"{api_base}/api/capability", json=c)
+                    res.raise_for_status()
+                    saved_caps += 1
+                except Exception as e:
+                    errors.append(f"提交能力失败 {c.get('id', '')}: {e}")
+
+            # 3. 提交 Tools
+            for t in tools_list:
+                try:
+                    res = await client.post(f"{api_base}/api/tool", json=t)
+                    res.raise_for_status()
+                    saved_tools += 1
+                except Exception as e:
+                    errors.append(f"提交工具失败 {t.get('id', '')}: {e}")
+
+        # 4. 提交 Knowledge
+        from agent.tools.builtin.knowledge import knowledge_save
+        for k in knowledges:
             try:
             try:
-                tool_id = f"tools/{tool.get('分类', 'misc')}/{tool.get('名称', 'unknown')}"
-                await resource_save(
-                    resource_id=tool_id,
-                    title=tool.get("名称", ""),
-                    body=json.dumps(tool, ensure_ascii=False, indent=2),
-                    content_type="tool",
-                    metadata={
-                        "category": tool.get("分类", ""),
-                        "introduction": tool.get("简介", ""),
-                        **tool.get("工具信息", {})
-                    }
+                await knowledge_save(
+                    task=k.get("task", "补充知识"),
+                    content=k.get("content", ""),
+                    types=k.get("types", []),
+                    score=k.get("score", 3),
+                    tools=k.get("tools", []),
+                    support_capability=k.get("support_capability", []),
+                    source_category=k.get("source", {}).get("category", "exp")
                 )
                 )
-                saved_tools += 1
+                saved_knows += 1
             except Exception as e:
             except Exception as e:
-                errors.append(f"工具 {tool.get('名称')}: {e}")
+                errors.append(f"提交知识失败: {e}")
 
 
-        # 构建统计输出
-        stats_lines = [
-            "**knowledge 表**:",
-            f"- 工具知识: {knowledge_by_type['tool']} 条",
-            f"- 工序知识: {knowledge_by_type['strategy']} 条",
-            f"- 用例知识: {knowledge_by_type['case']} 条",
-            f"- 执行经验: {knowledge_by_type['experience']} 条",
-            "",
-            "**resources 表**:",
-            f"- 资源: {saved_resources} 个",
-            f"- 工具: {saved_tools} 个",
-        ]
+        # 若完全没有错误,清空草稿
+        if not errors:
+            PRE_UPLOAD_FILE.unlink(missing_ok=True)
 
 
-        if errors:
-            stats_lines.append(f"\n**错误**: {len(errors)} 个")
+        output = (f"已成功将图谱发往数据库流水线。\n"
+                  f"- 写入 Requirement: {saved_reqs} 个\n"
+                  f"- 写入 Capability: {saved_caps} 个\n"
+                  f"- 写入 Tool: {saved_tools} 个\n"
+                  f"- 写入 Knowledge: {saved_knows} 条\n")
 
 
-        output = "已提交到数据库\n\n" + "\n".join(stats_lines)
         if errors:
         if errors:
-            output += "\n\n错误详情:\n" + "\n".join(f"- {e}" for e in errors[:5])
+            output += "\n伴随部分异常:\n" + "\n".join(f"- {e}" for e in errors[:5])
 
 
         return ToolResult(
         return ToolResult(
-            title="✅ 提交到数据库完成",
-            output=output,
-            metadata={
-                "saved": {
-                    "tools": saved_tools,
-                    "resources": saved_resources,
-                    "knowledge": knowledge_by_type
-                }
-            }
+            title="✅ 提交入库完成",
+            output=output
         )
         )
 
 
     except Exception as e:
     except Exception as e:
@@ -352,67 +144,30 @@ async def commit_to_database(
 
 
 
 
 @tool(
 @tool(
-    description=(
-        "查看缓存状态,包括 buffer 和 organized 中的文件列表、统计信息。"
-        "用于了解当前有多少数据在缓存中等待处理。"
-    )
+    description="查看当前预整理草稿的统计信息。"
 )
 )
 async def list_cache_status() -> ToolResult:
 async def list_cache_status() -> ToolResult:
     """
     """
-    查看缓存状态(buffer 和 organized 中的文件)
-
-    Returns:
-        缓存目录中的文件列表和统计
+    查看草稿状态(pre_upload_list.json)
     """
     """
     _ensure_dirs()
     _ensure_dirs()
+    if not PRE_UPLOAD_FILE.exists():
+        return ToolResult(title="ℹ️ 暂无草稿", output="pre_upload_list.json 不存在。")
 
 
-    buffer_files = sorted(BUFFER_DIR.glob("*.json"))
-    organized_files = sorted(ORGANIZED_DIR.glob("*.json"))
-
-    lines = [f"**Buffer** ({len(buffer_files)} 个文件):"]
-
-    total_tools = 0
-    total_resources = 0
-    total_knowledge = 0
-
-    for f in buffer_files:
-        size = f.stat().st_size
-        # 读取文件统计
-        try:
-            with open(f, "r", encoding="utf-8") as file:
-                data = json.load(file)
-                tools = len(data.get("tools", []))
-                resources = len(data.get("resources", []))
-                knowledge = len(data.get("knowledge", []))
-                total_tools += tools
-                total_resources += resources
-                total_knowledge += knowledge
-                lines.append(
-                    f"  - {f.name} ({size // 1024}KB): "
-                    f"工具 {tools}, 资源 {resources}, 知识 {knowledge}"
-                )
-        except Exception:
-            lines.append(f"  - {f.name} ({size // 1024}KB)")
-
-    if buffer_files:
-        lines.append(
-            f"\n  **合计**: 工具 {total_tools}, 资源 {total_resources}, 知识 {total_knowledge}"
-        )
-
-    lines.append(f"\n**Organized** ({len(organized_files)} 个文件):")
-    for f in organized_files:
-        size = f.stat().st_size
-        lines.append(f"  - {f.name} ({size // 1024}KB)")
-
-    return ToolResult(
-        title="📁 缓存状态",
-        output="\n".join(lines),
-        metadata={
-            "files": [f.name for f in buffer_files],
-            "total": {
-                "tools": total_tools,
-                "resources": total_resources,
-                "knowledge": total_knowledge
-            }
-        }
-    )
+    try:
+        with open(PRE_UPLOAD_FILE, "r", encoding="utf-8") as f:
+            data = json.load(f)
+        
+        reqs = len(data.get("requirements", []))
+        caps = len(data.get("capabilities", []))
+        tools = len(data.get("tools", []))
+        knows = len(data.get("knowledge", []))
+
+        output = (f"当前草稿数据({PRE_UPLOAD_FILE.name}):\n"
+                  f" - 需求: {reqs}\n"
+                  f" - 能力: {caps}\n"
+                  f" - 工具: {tools}\n"
+                  f" - 知识: {knows}\n")
+        return ToolResult(title="📁 草稿状态", output=output)
+    except Exception as e:
+        return ToolResult(title="❌ 读取状态失败", output=str(e), error=str(e))

+ 0 - 746
knowhub/static/app.js

@@ -1,746 +0,0 @@
-// 全局变量
-let allKnowledge = [];
-let availableTags = [];
-let currentPage = 1;
-let pageSize = 200;
-let totalPages = 1;
-let totalCount = 0;
-let isSearchMode = false;
-let selectedIds = new Set();
-let _allTools = [];
-let _activeCategory = 'all';
-
-// 加载 Tags
-async function loadTags() {
-    const res = await fetch('/api/knowledge/meta/tags');
-    const data = await res.json();
-    availableTags = data.tags;
-    renderTagsFilter();
-}
-
-function renderTagsFilter() {
-    const container = document.getElementById('tagsFilterContainer');
-    if (availableTags.length === 0) {
-        container.innerHTML = '<p class="text-sm text-gray-500">暂无 tags</p>';
-        return;
-    }
-    container.innerHTML = availableTags.map(tag =>
-        `<label class="flex items-center"><input type="checkbox" value="${escapeHtml(tag)}" class="mr-2 tag-filter"> ${escapeHtml(tag)}</label>`
-    ).join('');
-}
-
-// 加载知识列表
-async function loadKnowledge(page = 1) {
-    const params = new URLSearchParams();
-    params.append('page', page);
-    params.append('page_size', pageSize);
-
-    const selectedTypes = Array.from(document.querySelectorAll('.type-filter:checked')).map(el => el.value);
-    if (selectedTypes.length > 0) {
-        params.append('types', selectedTypes.join(','));
-    }
-
-    const selectedTags = Array.from(document.querySelectorAll('.tag-filter:checked')).map(el => el.value);
-    if (selectedTags.length > 0) {
-        params.append('tags', selectedTags.join(','));
-    }
-
-    const ownerFilter = document.getElementById('ownerFilter').value.trim();
-    if (ownerFilter) {
-        params.append('owner', ownerFilter);
-    }
-
-    const scopesFilter = document.getElementById('scopesFilter').value.trim();
-    if (scopesFilter) {
-        params.append('scopes', scopesFilter);
-    }
-
-    const selectedStatus = Array.from(document.querySelectorAll('.status-filter:checked')).map(el => el.value);
-    if (selectedStatus.length > 0) {
-        params.append('status', selectedStatus.join(','));
-    }
-
-    try {
-        const res = await fetch(`/api/knowledge?${params.toString()}`);
-        if (!res.ok) {
-            console.error('加载失败:', res.status, res.statusText);
-            document.getElementById('knowledgeList').innerHTML = '<p class="text-red-500 text-center py-8">加载失败,请刷新页面重试</p>';
-            return;
-        }
-        const data = await res.json();
-        allKnowledge = data.results || [];
-        currentPage = data.pagination.page;
-        totalPages = data.pagination.total_pages;
-        totalCount = data.pagination.total;
-        renderKnowledge(allKnowledge);
-        updatePagination();
-    } catch (error) {
-        console.error('加载错误:', error);
-        document.getElementById('knowledgeList').innerHTML = '<p class="text-red-500 text-center py-8">加载错误: ' + error.message + '</p>';
-    }
-}
-
-function applyFilters() {
-    currentPage = 1;
-    loadKnowledge(currentPage);
-}
-
-function goToPage(page) {
-    if (page < 1 || page > totalPages) return;
-    loadKnowledge(page);
-}
-
-function updatePagination() {
-    const paginationDiv = document.getElementById('pagination');
-    const pageInfo = document.getElementById('pageInfo');
-    const prevBtn = document.getElementById('prevBtn');
-    const nextBtn = document.getElementById('nextBtn');
-
-    if (totalPages <= 1) {
-        paginationDiv.classList.add('hidden');
-    } else {
-        paginationDiv.classList.remove('hidden');
-        pageInfo.textContent = `第 ${currentPage} / ${totalPages} 页 (共 ${totalCount} 条)`;
-        prevBtn.disabled = currentPage === 1;
-        nextBtn.disabled = currentPage === totalPages;
-    }
-}
-
-// 搜索功能
-async function performSearch() {
-    const query = document.getElementById('searchInput').value.trim();
-    if (!query) {
-        alert('请输入搜索内容');
-        return;
-    }
-
-    isSearchMode = true;
-    const statusDiv = document.getElementById('searchStatus');
-    statusDiv.textContent = '搜索中...';
-    statusDiv.classList.remove('hidden');
-
-    try {
-        const params = new URLSearchParams();
-        params.append('q', query);
-        params.append('top_k', '20');
-        params.append('min_score', '1');
-
-        const selectedTypes = Array.from(document.querySelectorAll('.type-filter:checked')).map(el => el.value);
-        if (selectedTypes.length > 0) {
-            params.append('types', selectedTypes.join(','));
-        }
-
-        const ownerFilter = document.getElementById('ownerFilter').value.trim();
-        if (ownerFilter) {
-            params.append('owner', ownerFilter);
-        }
-
-        const res = await fetch(`/api/knowledge/search?${params.toString()}`);
-        if (!res.ok) {
-            throw new Error(`搜索失败: ${res.status}`);
-        }
-
-        const data = await res.json();
-        allKnowledge = data.results || [];
-
-        statusDiv.textContent = `找到 ${allKnowledge.length} 条相关知识${data.reranked ? ' (已智能排序)' : ''}`;
-        renderKnowledge(allKnowledge);
-
-        document.getElementById('pagination').classList.add('hidden');
-    } catch (error) {
-        console.error('搜索错误:', error);
-        statusDiv.textContent = '搜索失败: ' + error.message;
-        statusDiv.classList.add('text-red-500');
-    }
-}
-
-function clearSearch() {
-    document.getElementById('searchInput').value = '';
-    document.getElementById('searchStatus').classList.add('hidden');
-    document.getElementById('searchStatus').classList.remove('text-red-500');
-    isSearchMode = false;
-    currentPage = 1;
-    loadKnowledge(currentPage);
-}
-
-// 渲染知识列表
-function renderKnowledge(list) {
-    const container = document.getElementById('knowledgeList');
-    if (list.length === 0) {
-        container.innerHTML = '<p class="text-gray-500 text-center py-8">暂无知识</p>';
-        return;
-    }
-
-    container.innerHTML = list.map(k => {
-        let types = [];
-        if (Array.isArray(k.types)) {
-            types = k.types;
-        } else if (typeof k.types === 'string') {
-            if (k.types.startsWith('[')) {
-                try {
-                    types = JSON.parse(k.types);
-                } catch (e) {
-                    console.error('解析types失败:', k.types, e);
-                    types = [k.types];
-                }
-            } else {
-                types = [k.types];
-            }
-        }
-        const eval_data = k.eval || {};
-        const isChecked = selectedIds.has(k.id);
-        const statusColor = {
-            'approved': 'bg-green-100 text-green-800',
-            'checked':  'bg-blue-100 text-blue-800',
-            'rejected': 'bg-red-100 text-red-800',
-            'pending':  'bg-yellow-100 text-yellow-800',
-            'processing': 'bg-orange-100 text-orange-800',
-        };
-        const statusClass = statusColor[k.status] || 'bg-gray-100 text-gray-800';
-        const statusLabel = k.status || 'approved';
-
-        const toolIds = (k.resource_ids || []).filter(id => id.startsWith('tools/'));
-        const toolTagsHtml = toolIds.length > 0
-            ? `<div class="flex gap-1 flex-wrap mt-2">
-                   ${toolIds.map(tid => {
-                       const name = tid.split('/').pop();
-                       return `<span onclick="event.stopPropagation(); openToolTableModal('${tid}')"
-                                     class="text-[11px] px-2 py-0.5 bg-indigo-50 text-indigo-700 border border-indigo-200 rounded-full cursor-pointer hover:bg-indigo-100 transition">
-                                   🔧 ${name}
-                               </span>`;
-                   }).join('')}
-               </div>`
-            : '';
-
-        return `
-        <div class="bg-white rounded-lg shadow p-6 hover:shadow-lg transition relative">
-            <div class="absolute top-4 left-4">
-                <input type="checkbox" class="knowledge-checkbox w-5 h-5 cursor-pointer"
-                       data-id="${k.id}" ${isChecked ? 'checked' : ''}
-                       onclick="event.stopPropagation(); toggleSelect('${k.id}')">
-            </div>
-            <div class="ml-10 cursor-pointer" onclick="openEditModal('${k.id}')">
-                <div class="flex justify-between items-start mb-2">
-                    <div class="flex gap-2 flex-wrap">
-                        ${types.map(t => `<span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">${t}</span>`).join('')}
-                    </div>
-                    <div class="flex items-center gap-2">
-                        <span class="text-xs px-2 py-1 rounded ${statusClass}">${statusLabel}</span>
-                        <span class="text-sm text-gray-500">${eval_data.score || 3}/5</span>
-                    </div>
-                </div>
-                <h3 class="text-lg font-semibold text-gray-800 mb-2">${escapeHtml(k.task)}</h3>
-                <p class="text-sm text-gray-600 mb-2">${escapeHtml(k.content.substring(0, 150))}${k.content.length > 150 ? '...' : ''}</p>
-                <div class="flex justify-between text-xs text-gray-500">
-                    <span>Owner: ${k.owner || 'N/A'}</span>
-                    <span>${new Date(k.created_at).toLocaleDateString()}</span>
-                </div>
-                ${toolTagsHtml}
-            </div>
-            <div class="flex gap-2 mt-3 pt-3 border-t border-gray-100">
-                <button onclick="event.stopPropagation(); verifyKnowledge('${k.id}', 'approve', this)"
-                        class="${k.status === 'checked' ? 'bg-gray-300 hover:bg-gray-400 text-gray-700' : 'bg-green-400 hover:bg-green-500 text-white'} text-xs px-3 py-1 rounded transition-colors">
-                    ${k.status === 'checked' ? '取消验证' : '✓ 验证通过'}
-                </button>
-                <button onclick="event.stopPropagation(); verifyKnowledge('${k.id}', 'reject', this)"
-                        class="bg-red-400 hover:bg-red-500 text-white text-xs px-3 py-1 rounded transition-colors">
-                    ✗ 拒绝
-                </button>
-            </div>
-        </div>
-    `;
-    }).join('');
-}
-
-// 选择相关函数
-function toggleSelect(id) {
-    if (selectedIds.has(id)) {
-        selectedIds.delete(id);
-    } else {
-        selectedIds.add(id);
-    }
-    updateBatchDeleteButton();
-}
-
-function toggleSelectAll() {
-    if (selectedIds.size === allKnowledge.length) {
-        selectedIds.clear();
-    } else {
-        selectedIds.clear();
-        allKnowledge.forEach(k => selectedIds.add(k.id));
-    }
-    renderKnowledge(allKnowledge);
-    updateBatchDeleteButton();
-}
-
-function updateBatchDeleteButton() {
-    const count = selectedIds.size;
-    document.getElementById('selectedCount').textContent = count;
-    document.getElementById('verifyCount').textContent = count;
-    document.getElementById('batchDeleteBtn').disabled = count === 0;
-    document.getElementById('batchVerifyBtn').disabled = count === 0;
-    document.getElementById('selectAllBtn').textContent =
-        selectedIds.size === allKnowledge.length ? '取消全选' : '全选';
-}
-
-// 批量删除
-async function batchDelete() {
-    if (selectedIds.size === 0) return;
-    if (!confirm(`确定要删除选中的 ${selectedIds.size} 条知识吗?此操作不可恢复!`)) return;
-
-    try {
-        const ids = Array.from(selectedIds);
-        const res = await fetch('/api/knowledge/batch_delete', {
-            method: 'POST',
-            headers: {'Content-Type': 'application/json'},
-            body: JSON.stringify(ids)
-        });
-
-        if (!res.ok) throw new Error(`删除失败: ${res.status}`);
-
-        const data = await res.json();
-        alert(`成功删除 ${data.deleted_count} 条知识`);
-
-        selectedIds.clear();
-        updateBatchDeleteButton();
-
-        if (isSearchMode) {
-            clearSearch();
-        } else {
-            loadKnowledge(currentPage);
-        }
-    } catch (error) {
-        console.error('批量删除错误:', error);
-        alert('删除失败: ' + error.message);
-    }
-}
-
-// 批量验证
-async function batchVerify() {
-    if (selectedIds.size === 0) return;
-    if (!confirm(`确定要批量验证通过选中的 ${selectedIds.size} 条知识吗?`)) return;
-    const btn = document.getElementById('batchVerifyBtn');
-    if (btn) { btn.disabled = true; btn.textContent = `处理中...`; }
-    try {
-        const ids = Array.from(selectedIds);
-        const res = await fetch('/api/knowledge/batch_verify', {
-            method: 'POST',
-            headers: {'Content-Type': 'application/json'},
-            body: JSON.stringify({ knowledge_ids: ids, action: 'approve', verified_by: 'user' })
-        });
-        if (!res.ok) throw new Error('请求失败: ' + res.status);
-        selectedIds.clear();
-        updateBatchDeleteButton();
-        if (isSearchMode) {
-            clearSearch();
-        } else {
-            loadKnowledge(currentPage);
-        }
-    } catch (error) {
-        console.error('批量验证错误:', error);
-        alert('验证失败: ' + error.message);
-        if (btn) { btn.disabled = false; updateBatchDeleteButton(); }
-    }
-}
-
-// 验证单个知识
-async function verifyKnowledge(id, action, btn) {
-    if (btn) {
-        btn.disabled = true;
-        btn._origText = btn.textContent;
-        btn.textContent = '处理中...';
-    }
-    try {
-        const res = await fetch('/api/knowledge/' + id + '/verify', {
-            method: 'POST',
-            headers: {'Content-Type': 'application/json'},
-            body: JSON.stringify({ action })
-        });
-        if (!res.ok) throw new Error('请求失败: ' + res.status);
-        if (isSearchMode) {
-            clearSearch();
-        } else {
-            loadKnowledge(currentPage);
-        }
-    } catch (error) {
-        console.error('验证错误:', error);
-        alert('操作失败: ' + error.message);
-        if (btn) {
-            btn.disabled = false;
-            btn.textContent = btn._origText;
-        }
-    }
-}
-
-// 模态框操作
-function openAddModal() {
-    document.getElementById('modalTitle').textContent = '新增知识';
-    document.getElementById('knowledgeForm').reset();
-    document.getElementById('editId').value = '';
-    document.querySelectorAll('.type-checkbox').forEach(el => el.checked = false);
-    document.getElementById('modal').classList.remove('hidden');
-}
-
-async function openEditModal(id) {
-    let k = allKnowledge.find(item => item.id === id);
-    if (!k) {
-        try {
-            const res = await fetch('/api/knowledge/' + encodeURIComponent(id));
-            if (!res.ok) { alert('知识未找到: ' + id); return; }
-            k = await res.json();
-        } catch (e) { alert('获取知识失败: ' + e.message); return; }
-    }
-
-    document.getElementById('modalTitle').textContent = '编辑知识';
-    document.getElementById('editId').value = k.id;
-    document.getElementById('taskInput').value = k.task || '';
-    document.getElementById('contentInput').value = k.content || '';
-    document.getElementById('tagsInput').value = JSON.stringify(k.tags || {});
-
-    const scopes = Array.isArray(k.scopes) ? k.scopes : [];
-    document.getElementById('scopesInput').value = scopes.join(', ');
-    document.getElementById('ownerInput').value = k.owner || '';
-
-    const types = Array.isArray(k.types) ? k.types : [];
-    document.querySelectorAll('.type-checkbox').forEach(el => {
-        el.checked = types.includes(el.value);
-    });
-
-    let rels = [];
-    if (Array.isArray(k.relationships)) {
-        rels = k.relationships;
-    } else if (typeof k.relationships === 'string' && k.relationships.startsWith('[')) {
-        try { rels = JSON.parse(k.relationships); } catch(e) {}
-    }
-    const section = document.getElementById('relationshipsSection');
-    if (rels.length > 0) {
-        const typeColor = {
-            superset: 'text-green-700', subset: 'text-orange-600',
-            conflict: 'text-red-600', complement: 'text-blue-600',
-            duplicate: 'text-gray-500'
-        };
-        document.getElementById('relationshipsList').innerHTML = rels.map(r =>
-            `<div class="flex gap-2 items-center">
-                <span class="font-medium ${typeColor[r.type] || 'text-gray-700'}">[${r.type}]</span>
-                <span class="font-mono text-xs text-gray-500 cursor-pointer hover:underline"
-                      onclick="openEditModal('${r.target}')">${r.target}</span>
-            </div>`
-        ).join('');
-        section.classList.remove('hidden');
-    } else {
-        section.classList.add('hidden');
-    }
-
-    document.getElementById('modal').classList.remove('hidden');
-}
-
-function closeModal() {
-    document.getElementById('modal').classList.add('hidden');
-}
-
-// 表单提交处理
-document.addEventListener('DOMContentLoaded', function() {
-    document.getElementById('knowledgeForm').addEventListener('submit', async (e) => {
-        e.preventDefault();
-
-        const editId = document.getElementById('editId').value;
-        const task = document.getElementById('taskInput').value;
-        const content = document.getElementById('contentInput').value;
-        const types = Array.from(document.querySelectorAll('.type-checkbox:checked')).map(el => el.value);
-        const tagsText = document.getElementById('tagsInput').value.trim();
-        const scopesText = document.getElementById('scopesInput').value.trim();
-        const owner = document.getElementById('ownerInput').value.trim();
-
-        let tags = {};
-        if (tagsText) {
-            try {
-                tags = JSON.parse(tagsText);
-            } catch (e) {
-                alert('Tags JSON 格式错误');
-                return;
-            }
-        }
-
-        const scopes = scopesText ? scopesText.split(',').map(s => s.trim()).filter(s => s) : ['org:cybertogether'];
-
-        if (editId) {
-            const res = await fetch(`/api/knowledge/${editId}`, {
-                method: 'PATCH',
-                headers: {'Content-Type': 'application/json'},
-                body: JSON.stringify({task, content, types, tags, scopes, owner})
-            });
-            if (!res.ok) {
-                alert('更新失败');
-                return;
-            }
-        } else {
-            const res = await fetch('/api/knowledge', {
-                method: 'POST',
-                headers: {'Content-Type': 'application/json'},
-                body: JSON.stringify({task, content, types, tags, scopes, owner})
-            });
-            if (!res.ok) {
-                alert('新增失败');
-                return;
-            }
-        }
-
-        closeModal();
-        await loadKnowledge();
-    });
-
-    // 初始化加载
-    loadTags();
-    loadKnowledge();
-});
-
-// 工具表相关函数
-async function openToolTableModal(targetToolId = null) {
-    document.getElementById('toolTableModal').classList.remove('hidden');
-    if (_allTools.length === 0) {
-        await loadToolList();
-    } else {
-        renderCategoryTabs();
-        renderToolList('all');
-    }
-    if (targetToolId) {
-        const targetTool = _allTools.find(t => t.id === targetToolId);
-        if (targetTool) {
-            const cat = targetTool.metadata && targetTool.metadata.category ? targetTool.metadata.category : 'other';
-            renderToolList(cat);
-        }
-        loadToolDetail(targetToolId);
-        setTimeout(() => {
-            const el = document.querySelector(`.tool-item[data-id="${targetToolId}"]`);
-            if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
-        }, 100);
-    }
-}
-
-function closeToolTableModal() {
-    document.getElementById('toolTableModal').classList.add('hidden');
-}
-
-async function loadToolList() {
-    try {
-        const res = await fetch('/api/resource?limit=1000');
-        const data = await res.json();
-        _allTools = (data.results || []).filter(r => r.id.startsWith('tools/'));
-        renderCategoryTabs();
-        renderToolList('all');
-    } catch (err) {
-        console.error('加载工具列表失败', err);
-        document.getElementById('toolList').innerHTML = '<p class="text-red-500 text-sm text-center">加载失败</p>';
-    }
-}
-
-function renderCategoryTabs() {
-    const cats = ['all', ...new Set(_allTools.map(t => t.metadata && t.metadata.category ? t.metadata.category : 'other'))];
-    document.getElementById('toolCategoryTabs').innerHTML = cats.map(cat => {
-        const isActive = cat === _activeCategory;
-        const activeClass = 'bg-indigo-600 text-white border-indigo-600';
-        const inactiveClass = 'bg-white text-gray-600 border-gray-300 hover:border-indigo-400';
-        return `<button onclick="renderToolList('${cat}')"
-                        id="tab_${cat}"
-                        class="tool-cat-tab px-4 py-1.5 rounded-full text-sm font-medium border transition ${isActive ? activeClass : inactiveClass}">
-                    ${cat === 'all' ? '全部' : cat}
-                </button>`;
-    }).join('');
-}
-
-function renderToolList(category) {
-    _activeCategory = category;
-    document.querySelectorAll('.tool-cat-tab').forEach(btn => {
-        const isCurrent = btn.id === `tab_${category}`;
-        btn.className = `tool-cat-tab px-4 py-1.5 rounded-full text-sm font-medium border transition ${
-            isCurrent ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-gray-600 border-gray-300 hover:border-indigo-400'
-        }`;
-    });
-    const filtered = category === 'all'
-        ? _allTools
-        : _allTools.filter(t => (t.metadata && t.metadata.category ? t.metadata.category : 'other') === category);
-    const listHtml = filtered.length === 0
-        ? '<p class="text-sm text-gray-400 text-center mt-4">该分类下暂无工具</p>'
-        : filtered.map(t => `
-            <div onclick="loadToolDetail('${t.id}')"
-                 class="tool-item p-3 rounded-lg border border-gray-200 cursor-pointer hover:border-indigo-400 hover:shadow-sm bg-white transition"
-                 data-id="${t.id}">
-                <div class="font-bold text-gray-800 text-sm truncate" title="${escapeHtml(t.title || t.id)}">${escapeHtml(t.title || t.id.split('/').pop())}</div>
-                <div class="mt-1 flex items-center justify-between">
-                    <span class="text-xs px-2 py-0.5 rounded truncate max-w-[100px] ${(t.metadata && t.metadata.status === '已接入') ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'}">${escapeHtml(t.metadata && t.metadata.status ? t.metadata.status : '未设置')}</span>
-                    <span class="text-[10px] text-gray-400">${escapeHtml(t.content_type || '')}</span>
-                </div>
-            </div>`).join('');
-    document.getElementById('toolList').innerHTML = listHtml;
-}
-
-async function loadToolDetail(id) {
-    document.querySelectorAll('.tool-item').forEach(el => {
-        if (el.dataset.id === id) {
-            el.classList.add('border-indigo-500', 'ring-1', 'ring-indigo-500');
-            el.classList.remove('border-gray-200');
-        } else {
-            el.classList.remove('border-indigo-500', 'ring-1', 'ring-indigo-500');
-            el.classList.add('border-gray-200');
-        }
-    });
-    const detailEl = document.getElementById('toolDetail');
-    detailEl.innerHTML = '<div class="flex h-full items-center justify-center"><p class="text-gray-400 animate-pulse">加载详情中...</p></div>';
-    try {
-        const res = await fetch('/api/resource/' + id);
-        const tool = await res.json();
-        const knowledgeIds = (tool.metadata && tool.metadata.knowledge_ids) ? tool.metadata.knowledge_ids : [];
-        const knowledgeHtml = knowledgeIds.length === 0
-            ? '<span class="text-gray-400 text-xs">暂无</span>'
-            : knowledgeIds.map(kid => `
-                <span onclick="openKnowledgeDetailModal('${kid}')"
-                      class="text-xs px-2 py-1 bg-gray-100 hover:bg-gray-200 border rounded cursor-pointer text-gray-700 font-mono transition">
-                    ${kid.length > 24 ? kid.slice(0, 24) + '...' : kid}
-                </span>`).join('');
-
-        const toolhubItems = (tool.metadata && tool.metadata.toolhub_items) ? tool.metadata.toolhub_items : [];
-        const toolhubHtml = toolhubItems.length === 0
-            ? '<span class="text-gray-400 text-xs">暂无</span>'
-            : toolhubItems.map(item => {
-                const [id, desc] = Object.entries(item)[0];
-                return `<span class="text-xs px-2 py-1 bg-blue-50 hover:bg-blue-100 border border-blue-200 rounded text-blue-700 font-mono transition">
-                    ${escapeHtml(id)}: ${escapeHtml(desc)}
-                </span>`;
-            }).join('');
-
-        const meta = tool.metadata || {};
-        const scenariosMd = Array.isArray(meta.scenarios) && meta.scenarios.length > 0
-            ? meta.scenarios.map(s => `<li>${escapeHtml(s)}</li>`).join('')
-            : '<li class="text-gray-400">暂无</li>';
-
-        detailEl.innerHTML = `
-            <div class="mb-6 border-b pb-4">
-                <h2 class="text-3xl font-black text-gray-900 mb-3">${escapeHtml(tool.title || id)}</h2>
-                <div class="flex gap-2 flex-wrap text-sm mb-3">
-                    <span class="px-2.5 py-1 bg-indigo-50 text-indigo-700 rounded-md border border-indigo-100">
-                        📁 分类: ${escapeHtml(meta.category || '–')}
-                    </span>
-                    <span class="px-2.5 py-1 rounded-md border ${(meta.status === '已接入') ? 'bg-green-50 text-green-700 border-green-200' : 'bg-gray-50 text-gray-700 border-gray-200'}">
-                        🏷️ 状态: ${escapeHtml(meta.status || '–')}
-                    </span>
-                    <span class="px-2.5 py-1 bg-blue-50 text-blue-700 rounded-md border border-blue-100">
-                        📌 Slug: ${escapeHtml(meta.tool_slug || '–')}
-                    </span>
-                </div>
-                <div class="flex gap-1 flex-wrap items-center">
-                    <span class="text-xs text-gray-500 mr-1">🔗 关联知识:</span>
-                    ${knowledgeHtml}
-                </div>
-                <div class="flex gap-1 flex-wrap items-center mt-2">
-                    <span class="text-xs text-gray-500 mr-1">🔧 工具项:</span>
-                    ${toolhubHtml}
-                </div>
-            </div>
-
-            <div class="text-gray-800 leading-relaxed max-w-none space-y-6">
-                <div>
-                    <h3 class="text-lg font-bold border-b pb-2 mb-3">基础概览</h3>
-                    <div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
-                        <div><span class="text-gray-500 font-semibold">工具版本:</span> ${escapeHtml(meta.version || '–')}</div>
-                    </div>
-                    <div class="mt-3">
-                        <span class="text-gray-500 font-semibold text-sm block mb-1">功能介绍:</span>
-                        <div class="bg-gray-50 p-3 rounded-md text-sm border whitespace-pre-wrap">${escapeHtml(meta.description || '暂无')}</div>
-                    </div>
-                </div>
-
-                <div>
-                    <h3 class="text-lg font-bold border-b pb-2 mb-3">使用指南</h3>
-                    <div class="mb-4">
-                        <span class="text-gray-500 font-semibold text-sm block mb-1">用法:</span>
-                        <div class="bg-gray-50 p-3 rounded-md text-sm border whitespace-pre-wrap">${escapeHtml(meta.usage || '暂无')}</div>
-                    </div>
-                    <div>
-                        <span class="text-gray-500 font-semibold text-sm block mb-1">应用场景:</span>
-                        <ul class="list-disc pl-5 space-y-1 text-sm">${scenariosMd}</ul>
-                    </div>
-                </div>
-
-                <div>
-                    <h3 class="text-lg font-bold border-b pb-2 mb-3">技术规格</h3>
-                    <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
-                        <div class="flex flex-col">
-                            <span class="text-gray-500 font-semibold text-sm block mb-1">输入:</span>
-                            <div class="bg-gray-50 p-3 rounded-md text-sm border flex-1 whitespace-pre-wrap break-all">${escapeHtml(meta.input || '暂无')}</div>
-                        </div>
-                        <div class="flex flex-col">
-                            <span class="text-gray-500 font-semibold text-sm block mb-1">输出:</span>
-                            <div class="bg-gray-50 p-3 rounded-md text-sm border flex-1 whitespace-pre-wrap break-all">${escapeHtml(meta.output || '暂无')}</div>
-                        </div>
-                    </div>
-                </div>
-
-                ${meta.source ? `
-                <div>
-                    <h3 class="text-lg font-bold border-b pb-2 mb-3">消息信源</h3>
-                    <div class="text-sm overflow-hidden break-words text-blue-600 hover:underline">
-                        ${escapeHtml(meta.source)}
-                    </div>
-                </div>` : ''}
-
-                ${tool.body ? `
-                <div class="pt-4 mt-6 border-t border-dashed">
-                    <h3 class="text-lg font-bold mb-3 text-gray-500">补充说明 (文档内容)</h3>
-                    <div class="markdown-body bg-gray-50 p-4 rounded-lg border text-sm">
-                        ${typeof marked !== 'undefined' ? marked.parse(tool.body) : escapeHtml(tool.body)}
-                    </div>
-                </div>` : ''}
-            </div>`;
-    } catch (err) {
-        detailEl.innerHTML = '<div class="text-red-500 flex h-full items-center justify-center">加载详情失败,请检查网络或日志</div>';
-        console.error(err);
-    }
-}
-
-async function openKnowledgeDetailModal(id) {
-    document.getElementById('knowledgeDetailModal').classList.remove('hidden');
-    const contentEl = document.getElementById('knowledgeDetailContent');
-    contentEl.innerHTML = '<p class="text-gray-400 text-center animate-pulse">加载中...</p>';
-    try {
-        const res = await fetch(`/api/knowledge/${encodeURIComponent(id)}`);
-        if (!res.ok) { contentEl.innerHTML = '<p class="text-red-500 text-center">知识未找到</p>'; return; }
-        const k = await res.json();
-        const statusColor = {
-            'approved': 'bg-green-100 text-green-800',
-            'checked':  'bg-blue-100 text-blue-800',
-            'rejected': 'bg-red-100 text-red-800',
-            'pending':  'bg-yellow-100 text-yellow-800',
-        };
-        const types = Array.isArray(k.types) ? k.types : [];
-        const tags = k.tags || {};
-        const tagKeys = Object.keys(tags);
-        contentEl.innerHTML = `
-            <div class="flex gap-2 flex-wrap mb-4">
-                ${types.map(t => `<span class="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs">${escapeHtml(t)}</span>`).join('')}
-                <span class="px-2 py-0.5 rounded text-xs ${statusColor[k.status] || 'bg-gray-100 text-gray-700'}">${escapeHtml(k.status || '–')}</span>
-            </div>
-            <h3 class="text-lg font-bold text-gray-900 mb-3">${escapeHtml(k.task || '')}</h3>
-            <div class="text-sm text-gray-700 bg-gray-50 rounded-lg p-4 mb-4 whitespace-pre-wrap leading-relaxed">
-                ${escapeHtml(k.content || '')}
-            </div>
-            <div class="text-xs text-gray-500 space-y-1 border-t pt-3">
-                <div>📌 ID:<span class="font-mono">${escapeHtml(k.id || '')}</span></div>
-                <div>👤 Owner:${escapeHtml(k.owner || '–')}</div>
-                <div>🕐 创建:${k.created_at ? new Date(k.created_at * 1000).toLocaleString() : '–'}</div>
-                ${tagKeys.length > 0 ? `<div>🏷️ Tags:${tagKeys.map(t => `<span class="bg-gray-100 px-1 rounded">${escapeHtml(t)}</span>`).join(' ')}</div>` : ''}
-            </div>`;
-    } catch (err) {
-        contentEl.innerHTML = '<p class="text-red-500 text-center">加载失败</p>';
-        console.error(err);
-    }
-}
-
-function closeKnowledgeDetailModal() {
-    document.getElementById('knowledgeDetailModal').classList.add('hidden');
-}
-
-function escapeHtml(text) {
-    const div = document.createElement('div');
-    div.textContent = text;
-    return div.innerHTML;
-}

+ 0 - 225
knowhub/static/index.html

@@ -1,225 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>KnowHub 管理</title>
-    <script src="https://cdn.tailwindcss.com"></script>
-    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
-</head>
-
-<body class="bg-gray-50">
-    <div class="container mx-auto px-4 py-8 max-w-7xl">
-        <div class="flex justify-between items-center mb-8">
-            <h1 class="text-3xl font-bold text-gray-800">KnowHub 全局知识库</h1>
-            <div class="flex gap-3">
-                <button onclick="toggleSelectAll()" id="selectAllBtn"
-                    class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg">
-                    全选
-                </button>
-                <button onclick="batchDelete()" id="batchDeleteBtn"
-                    class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
-                    disabled>
-                    删除选中 (<span id="selectedCount">0</span>)
-                </button>
-                <button onclick="batchVerify()" id="batchVerifyBtn"
-                    class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
-                    disabled>
-                    ✓ 批量验证通过 (<span id="verifyCount">0</span>)
-                </button>
-                <button onclick="openToolTableModal()"
-                    class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg">
-                    🔧 工具表
-                </button>
-                <button onclick="openAddModal()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg">
-                    + 新增知识
-                </button>
-            </div>
-        </div>
-
-        <!-- 搜索栏 -->
-        <div class="bg-white rounded-lg shadow p-6 mb-6">
-            <div class="flex gap-4">
-                <input type="text" id="searchInput" placeholder="输入任务描述进行语义搜索..."
-                    class="flex-1 border rounded px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
-                    onkeypress="if(event.key==='Enter') performSearch()">
-                <button onclick="performSearch()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded">
-                    搜索
-                </button>
-                <button onclick="clearSearch()" class="bg-gray-500 hover:bg-gray-600 text-white px-6 py-2 rounded">
-                    清除
-                </button>
-            </div>
-            <div id="searchStatus" class="mt-2 text-sm text-gray-600 hidden"></div>
-        </div>
-
-        <!-- 筛选栏 -->
-        <div class="bg-white rounded-lg shadow p-6 mb-6">
-            <div class="grid grid-cols-1 md:grid-cols-5 gap-4">
-                <div>
-                    <label class="block text-sm font-medium text-gray-700 mb-2">类型 (Types)</label>
-                    <div class="space-y-2">
-                        <label class="flex items-center"><input type="checkbox" value="strategy"
-                                class="mr-2 type-filter"> Strategy</label>
-                        <label class="flex items-center"><input type="checkbox" value="tool" class="mr-2 type-filter">
-                            Tool</label>
-                        <label class="flex items-center"><input type="checkbox" value="user_profile"
-                                class="mr-2 type-filter"> User Profile</label>
-                        <label class="flex items-center"><input type="checkbox" value="usecase"
-                                class="mr-2 type-filter"> Usecase</label>
-                        <label class="flex items-center"><input type="checkbox" value="definition"
-                                class="mr-2 type-filter"> Definition</label>
-                        <label class="flex items-center"><input type="checkbox" value="plan" class="mr-2 type-filter">
-                            Plan</label>
-                    </div>
-                </div>
-                <div>
-                    <label class="block text-sm font-medium text-gray-700 mb-2">Tags</label>
-                    <div id="tagsFilterContainer" class="space-y-2 max-h-40 overflow-y-auto">
-                        <p class="text-sm text-gray-500">加载中...</p>
-                    </div>
-                </div>
-                <div>
-                    <label class="block text-sm font-medium text-gray-700 mb-2">Owner</label>
-                    <input type="text" id="ownerFilter" placeholder="输入 owner" class="w-full border rounded px-3 py-2">
-                </div>
-                <div>
-                    <label class="block text-sm font-medium text-gray-700 mb-2">Scopes</label>
-                    <input type="text" id="scopesFilter" placeholder="输入 scope" class="w-full border rounded px-3 py-2">
-                </div>
-                <div>
-                    <label class="block text-sm font-medium text-gray-700 mb-2">Status</label>
-                    <div class="space-y-2">
-                        <label class="flex items-center"><input type="checkbox" value="approved"
-                                class="mr-2 status-filter" checked> Approved</label>
-                        <label class="flex items-center"><input type="checkbox" value="checked"
-                                class="mr-2 status-filter" checked> Checked</label>
-                        <label class="flex items-center"><input type="checkbox" value="rejected"
-                                class="mr-2 status-filter"> Rejected</label>
-                        <label class="flex items-center"><input type="checkbox" value="pending"
-                                class="mr-2 status-filter"> Pending</label>
-                    </div>
-                </div>
-            </div>
-            <button onclick="applyFilters()" class="mt-4 bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded">
-                应用筛选
-            </button>
-        </div>
-
-        <!-- 知识列表 -->
-        <div id="knowledgeList" class="space-y-4"></div>
-
-        <!-- 分页控件 -->
-        <div id="pagination" class="flex justify-center items-center gap-4 mt-6 hidden">
-            <button onclick="goToPage(currentPage - 1)" id="prevBtn"
-                class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded disabled:opacity-50 disabled:cursor-not-allowed">
-                上一页
-            </button>
-            <span id="pageInfo" class="text-gray-700"></span>
-            <button onclick="goToPage(currentPage + 1)" id="nextBtn"
-                class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded disabled:opacity-50 disabled:cursor-not-allowed">
-                下一页
-            </button>
-        </div>
-    </div>
-
-    <!-- 新增/编辑 Modal -->
-    <div id="modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
-        <div class="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto p-6">
-            <h2 id="modalTitle" class="text-2xl font-bold mb-4">新增知识</h2>
-            <form id="knowledgeForm" class="space-y-4">
-                <input type="hidden" id="editId">
-                <div>
-                    <label class="block text-sm font-medium mb-1">Task *</label>
-                    <input type="text" id="taskInput" required class="w-full border rounded px-3 py-2">
-                </div>
-                <div>
-                    <label class="block text-sm font-medium mb-1">Content *</label>
-                    <textarea id="contentInput" required rows="6" class="w-full border rounded px-3 py-2"></textarea>
-                </div>
-                <div>
-                    <label class="block text-sm font-medium mb-1">Types (多选)</label>
-                    <div class="space-y-1">
-                        <label class="flex items-center"><input type="checkbox" value="strategy"
-                                class="mr-2 type-checkbox"> Strategy</label>
-                        <label class="flex items-center"><input type="checkbox" value="tool" class="mr-2 type-checkbox">
-                            Tool</label>
-                        <label class="flex items-center"><input type="checkbox" value="user_profile"
-                                class="mr-2 type-checkbox"> User Profile</label>
-                        <label class="flex items-center"><input type="checkbox" value="usecase"
-                                class="mr-2 type-checkbox"> Usecase</label>
-                        <label class="flex items-center"><input type="checkbox" value="definition"
-                                class="mr-2 type-checkbox"> Definition</label>
-                        <label class="flex items-center"><input type="checkbox" value="plan" class="mr-2 type-checkbox">
-                            Plan</label>
-                    </div>
-                </div>
-                <div>
-                    <label class="block text-sm font-medium mb-1">Tags (JSON)</label>
-                    <textarea id="tagsInput" rows="2" placeholder='{"key": "value"}'
-                        class="w-full border rounded px-3 py-2"></textarea>
-                </div>
-                <div>
-                    <label class="block text-sm font-medium mb-1">Scopes (逗号分隔)</label>
-                    <input type="text" id="scopesInput" placeholder="org:cybertogether"
-                        class="w-full border rounded px-3 py-2">
-                </div>
-                <div>
-                    <label class="block text-sm font-medium mb-1">Owner</label>
-                    <input type="text" id="ownerInput" class="w-full border rounded px-3 py-2">
-                </div>
-                <div id="relationshipsSection" class="hidden">
-                    <label class="block text-sm font-medium text-gray-700 mb-2">关联知识</label>
-                    <div id="relationshipsList" class="space-y-1 text-sm bg-gray-50 rounded p-3"></div>
-                </div>
-                <div class="flex gap-2 pt-4">
-                    <button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded">保存</button>
-                    <button type="button" onclick="closeModal()"
-                        class="bg-gray-300 hover:bg-gray-400 px-6 py-2 rounded">取消</button>
-                </div>
-            </form>
-        </div>
-    </div>
-
-    <!-- 工具表 Modal -->
-    <div id="toolTableModal"
-        class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
-        <div class="bg-white rounded-xl shadow-xl w-full max-w-5xl max-h-[90vh] flex flex-col">
-            <div class="flex justify-between items-center p-6 border-b">
-                <h2 class="text-2xl font-bold">🔧 工具表</h2>
-                <button onclick="closeToolTableModal()" class="text-gray-400 hover:text-gray-600 text-2xl">✕</button>
-            </div>
-            <div id="toolCategoryTabs" class="flex gap-2 px-6 pt-4 flex-wrap border-b pb-4"></div>
-            <div class="flex flex-1 overflow-hidden">
-                <div id="toolList" class="w-[250px] border-r overflow-y-auto p-4 space-y-2 bg-gray-50 flex-shrink-0">
-                    <p class="text-sm text-gray-500 text-center mt-4">加载中...</p>
-                </div>
-                <div id="toolDetail" class="flex-1 overflow-y-auto p-6 bg-white">
-                    <div class="flex h-full items-center justify-center text-gray-400">
-                        ← 请在左侧选择要查看的工具
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <!-- 知识详情弹窗(只读)-->
-    <div id="knowledgeDetailModal"
-        class="hidden fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center p-4 z-[60]">
-        <div class="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[85vh] flex flex-col">
-            <div class="flex justify-between items-center p-6 border-b">
-                <h2 class="text-xl font-bold text-gray-800">知识详情</h2>
-                <button onclick="closeKnowledgeDetailModal()"
-                    class="text-gray-400 hover:text-gray-600 text-2xl">✕</button>
-            </div>
-            <div id="knowledgeDetailContent" class="flex-1 overflow-y-auto p-6">
-                <p class="text-gray-400 text-center animate-pulse">加载中...</p>
-            </div>
-        </div>
-    </div>
-
-    <script src="/static/app.js"></script>
-</body>
-
-</html>