guantao 1 день назад
Родитель
Сommit
5f60578355

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

@@ -16,6 +16,7 @@
 """
 
 import os
+import json
 import logging
 import subprocess
 import httpx
@@ -623,3 +624,131 @@ async def resource_get(
             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(
     data: Dict[str, Any],
+    source_type: str = "research",
     finalize: bool = False,
     contact_id: str = "agent_research",
     context: Optional[ToolContext] = None,
 ) -> ToolResult:
     """
-    上传调研结果到知识库(异步,立即返回)
+    上传调研结果或执行经验到知识库(异步,立即返回)
 
     Args:
-        data: 调研结果,包含:
+        data: 结构化数据,包含:
             - tools: 工具列表
             - resources: 资源列表
             - knowledge: 知识列表
+        source_type: 数据来源分类。调研结果填 "research",执行任务时产生的经验请填 "execution"。
         finalize: 是否最终提交(True=入库,False=仅缓冲)
         contact_id: 当前 Agent 的 IM contact_id
         context: 工具上下文
@@ -156,12 +158,20 @@ async def upload_knowledge(
                 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:
             action = "最终提交"
             message = f"[UPLOAD:FINALIZE] {json.dumps(data, ensure_ascii=False)}"
         else:
-            action = "增量上传"
+            action = f"增量上传({source_type})"
             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_ENABLED = True           # 是否启动 Knowledge Manager(作为后台 IM Client)
 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 返回后,立即做两件事**:
+  1. 评估信息质量(是否找到了目标渠道的信息?信息是否足够详细和可信?是否需要追问补充?)
+  2. **增量上传**:将本轮收获的有价值信息立即通过 `upload_knowledge` 发送给 Knowledge Manager,不要等到最后。这样 Knowledge Manager 可以边收边整理图谱关系。
 - 如果某个渠道信息不足,使用 `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}`
 - `name`: 工具名称
 - `introduction`: 简介
 - `tutorial`: 使用教程
 - `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)`: 缓存调研数据到本地(不入库)
@@ -92,16 +110,16 @@ $system$
 调研 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 句话。重点突出缺失部分。
 
-### 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. 提交到数据库请求
-用户明确要求将缓存数据提交到数据库(关键词:"提交到数据库"、"入库"、"保存到数据库")。
-
-**前置检查**:
-- 如果 `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. **快速响应**:尽快回复,不要做不必要的操作

+ 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:
     """获取 Knowledge Manager 配置(根据是否允许入库动态调整工具列表)"""
     tools = [
+        # 只读查询工具(用于跨表检索和关联分析)
         "knowledge_search",
-        "knowledge_save",
         "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",
         "write_file",
         # 本地缓存工具
-        "cache_research_data",
-        "organize_cached_data",
         "list_cache_status",
     ]
 
@@ -273,57 +275,7 @@ async def start_knowledge_manager(
             "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):
         nonlocal upload_timer
 
@@ -336,10 +288,9 @@ async def start_knowledge_manager(
 
         # 判断消息类型(根据前缀标记)
         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"):
             # 批处理:加入缓冲区,延迟处理

+ 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 logging
+import httpx
 from pathlib import Path
 from typing import Dict, Any, List, Optional
 from datetime import datetime
@@ -18,328 +19,119 @@ logger = logging.getLogger(__name__)
 
 # 缓存目录
 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():
     """确保缓存目录存在"""
-    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(
     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:
-        提交结果统计(按知识类型分组)
+        提交结果和统计信息
     """
     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(
-                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)
 
-        # 导入数据库工具
-        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:
-                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:
-                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:
-            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(
-            title="✅ 提交到数据库完成",
-            output=output,
-            metadata={
-                "saved": {
-                    "tools": saved_tools,
-                    "resources": saved_resources,
-                    "knowledge": knowledge_by_type
-                }
-            }
+            title="✅ 提交入库完成",
+            output=output
         )
 
     except Exception as e:
@@ -352,67 +144,30 @@ async def commit_to_database(
 
 
 @tool(
-    description=(
-        "查看缓存状态,包括 buffer 和 organized 中的文件列表、统计信息。"
-        "用于了解当前有多少数据在缓存中等待处理。"
-    )
+    description="查看当前预整理草稿的统计信息。"
 )
 async def list_cache_status() -> ToolResult:
     """
-    查看缓存状态(buffer 和 organized 中的文件)
-
-    Returns:
-        缓存目录中的文件列表和统计
+    查看草稿状态(pre_upload_list.json)
     """
     _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>