Просмотр исходного кода

Merge branch 'main' of https://git.yishihui.com/howard/Agent

guantao 1 месяц назад
Родитель
Сommit
945d6346aa
100 измененных файлов с 2067 добавлено и 4098 удалено
  1. 11 2
      .gitignore
  2. 0 507
      README.md
  3. 0 42
      add_knowledge.py
  4. 0 44
      add_workflow_experience_knowledge.py
  5. 123 1
      agent/README.md
  6. 1 2
      agent/core/__init__.py
  7. 14 101
      agent/core/runner.py
  8. 3 2
      agent/docs/tools-refactor-plan.md
  9. 54 3
      agent/docs/tools.md
  10. 29 23
      agent/skill/skills/browser.md
  11. 30 24
      agent/skill/skills/core.md
  12. 5 5
      agent/skill/skills/research.md
  13. 12 11
      agent/tools/builtin/__init__.py
  14. 1 1
      agent/tools/builtin/bash.py
  15. 30 93
      agent/tools/builtin/browser/__init__.py
  16. 334 77
      agent/tools/builtin/browser/baseClass.py
  17. 29 0
      agent/tools/builtin/content/__init__.py
  18. 86 0
      agent/tools/builtin/content/cache.py
  19. 46 0
      agent/tools/builtin/content/ingestion.py
  20. 114 0
      agent/tools/builtin/content/media.py
  21. 1 0
      agent/tools/builtin/content/platforms/__init__.py
  22. 237 0
      agent/tools/builtin/content/platforms/aigc_channel.py
  23. 129 0
      agent/tools/builtin/content/platforms/x.py
  24. 203 0
      agent/tools/builtin/content/platforms/youtube.py
  25. 125 0
      agent/tools/builtin/content/registry.py
  26. 266 0
      agent/tools/builtin/content/tools.py
  27. 2 1
      agent/tools/builtin/context.py
  28. 0 497
      agent/tools/builtin/crawler.py
  29. 4 0
      agent/tools/builtin/feishu/chat.py
  30. 1 1
      agent/tools/builtin/file/edit.py
  31. 1 1
      agent/tools/builtin/file/grep.py
  32. 1 1
      agent/tools/builtin/file/read.py
  33. 1 0
      agent/tools/builtin/file/read_images.py
  34. 1 1
      agent/tools/builtin/file/write.py
  35. 1 1
      agent/tools/builtin/glob_tool.py
  36. 8 0
      agent/tools/builtin/im/chat.py
  37. 40 29
      agent/tools/builtin/knowledge.py
  38. 4 2
      agent/tools/builtin/librarian.py
  39. 4 2
      agent/tools/builtin/resource.py
  40. 0 420
      agent/tools/builtin/search.py
  41. 4 2
      agent/tools/builtin/skill.py
  42. 2 2
      agent/tools/builtin/subagent.py
  43. 6 3
      agent/tools/builtin/toolhub.py
  44. 34 10
      agent/tools/registry.py
  45. 2 2
      agent/tools/utils/image.py
  46. 1 1
      agent/trace/goal_tool.py
  47. 0 4
      cache/knowledge_pending/knowledge-2026-04-09-x-portrait-workflows.json
  48. 0 45
      consistency_requirements.json
  49. 0 80
      direct_insert.py
  50. 1 0
      examples/mini_restore/config.py
  51. 2 2
      examples/mini_restore/workflow_loop.py
  52. 0 132
      insert_ai_workflow_capabilities.py
  53. 0 73
      insert_knowledge.py
  54. 0 91
      insert_knowledge_direct.py
  55. 1 0
      knowhub/agents/librarian.py
  56. 3 3
      knowhub/agents/librarian_agent.prompt
  57. 37 5
      knowhub/knowhub_db/pg_store.py
  58. 23 6
      knowhub/server.py
  59. 0 104
      knowledge-2026-04-09-ai-image-workflows.json
  60. 0 398
      knowledge/ComfyUI_画中画分阶段生成工作流.md
  61. 0 165
      knowledge/Midjourney_v7_Impasto_厚涂风格参数配置.md
  62. 0 261
      knowledge/Nano_Banana_多图融合_ComfyUI 工作流.md
  63. 0 20
      knowledge/knowledge-20260412-005015-nano-banana-image-urls.json
  64. 0 20
      knowledge/knowledge-20260412-010725-nano-banana-http503.json
  65. 0 3
      knowledge/knowledge-20260412-010726-nano-banana-multi-fusion.json
  66. 0 20
      knowledge/knowledge-20260412-014259-3abf.json
  67. 0 21
      knowledge/strategy/品类分组判断标准.json
  68. 0 20
      knowledge/strategy/粗工序因果推理方法.json
  69. 0 17
      knowledge/strategy/能力模块变体分析方法.json
  70. 0 9
      knowledge_batch_20260409.json
  71. 0 26
      knowledge_batch_20260409_execution.json
  72. 0 34
      knowledge_batch_20260410_workflow_analysis.json
  73. 0 51
      knowledge_batch_20260411_toolhub_error_recovery.json
  74. BIN
      outputs/48d6bd7b-699/nano_banana_1775894285972_0.jpg
  75. BIN
      outputs/48d6bd7b-699/nano_banana_1775894577594_0.jpg
  76. BIN
      outputs/48d6bd7b-699/nano_banana_1775894852970_0.jpg
  77. BIN
      outputs/48d6bd7b-699/nano_banana_1775895148729_0.jpg
  78. BIN
      outputs/48d6bd7b-699/nano_banana_1775895453740_0.jpg
  79. BIN
      outputs/48d6bd7b-699/nano_banana_1775895803885_0.jpg
  80. BIN
      outputs/48d6bd7b-699/nano_banana_1775896094781_0.jpg
  81. BIN
      outputs/5fd2db93-5ac/nano_banana_1775926086639_0.jpg
  82. BIN
      outputs/5fd2db93-5ac/nano_banana_1775926336258_0.jpg
  83. BIN
      outputs/5fd2db93-5ac/nano_banana_1775926561018_0.jpg
  84. BIN
      outputs/5fd2db93-5ac/nano_banana_1775927016941_0.jpg
  85. BIN
      outputs/5fd2db93-5ac/nano_banana_1775927415643_0.jpg
  86. BIN
      outputs/b7191706-5dc/nano_banana_1775929219620_0.jpg
  87. BIN
      outputs/b7191706-5dc/nano_banana_1775929481610_0.jpg
  88. BIN
      outputs/b7191706-5dc/nano_banana_1775929804307_0.jpg
  89. BIN
      outputs/b7191706-5dc/nano_banana_1775930114003_0.jpg
  90. BIN
      outputs/b7191706-5dc/nano_banana_1775930416393_0.jpg
  91. 0 9
      pending_uploads/2026-04-09_ai_image_local_detail_strategy.json
  92. 0 28
      pending_uploads/knowledge_batch_20260410_file_search.json
  93. 0 81
      pre_upload_list.json
  94. 0 69
      process_upload.py
  95. 0 0
      test-dir/__main__.py
  96. 0 72
      test_upload.py
  97. 0 80
      test_upload_sync.py
  98. 0 18
      tools/image_gen/nano_banana.json
  99. 0 18
      tools/upload/image_uploader.json
  100. 0 199
      wf_004_intents.json

+ 11 - 2
.gitignore

@@ -74,6 +74,15 @@ frontend/react-template/node_modules/
 # Feishu 运行时聊天记录(自动维护,包含联系人 PII)
 agent/tools/builtin/feishu/chat_history/
 
+# Runtime artifacts (one-off scripts, data, cache)
+cache/
+pending_uploads/
+knowledge/
+knowledge_batch_*.json
+knowledge-*.json
+tools/image_gen/
+tools/upload/
+
 # data
 knowhub/knowhub.db
 knowhub/knowhub.db-shm
@@ -82,8 +91,8 @@ examples/archive/*
 examples/research/
 examples/downloader/
 examples/production_restore/features/
-# Milvus data
-knowhub/milvus_data/
+# im-server data
+data/
 
 # Vendor (non-submodule)
 vendor/browser-use/

+ 0 - 507
README.md

@@ -1,507 +0,0 @@
-# Reson Agent
-
-可扩展的 Agent 框架。支持多步工具调用、计划管理、子 Agent 协作、回溯重跑和上下文压缩。
-
-## Quick Start
-
-```bash
-pip install -r requirements.txt
-
-# 配置 LLM API Key
-cp .env.example .env  # 编辑填入 API Key
-```
-
-### 最小示例
-
-```python
-import asyncio
-from agent import AgentRunner, RunConfig
-from agent.trace import FileSystemTraceStore
-from agent.llm import create_openrouter_llm_call
-
-runner = AgentRunner(
-    trace_store=FileSystemTraceStore(base_path=".trace"),
-    llm_call=create_openrouter_llm_call(model="anthropic/claude-sonnet-4.5"),
-)
-
-async def main():
-    async for item in runner.run(
-        messages=[{"role": "user", "content": "列出当前目录的文件"}],
-        config=RunConfig(model="anthropic/claude-sonnet-4.5"),
-    ):
-        print(item)
-
-asyncio.run(main())
-```
-
-## 自定义工具
-
-用 `@tool` 装饰器注册。`RunConfig(tools=None)`(默认)时所有已注册工具自动对 LLM 可用,无需额外配置。
-
-```python
-from agent import tool, ToolResult
-
-@tool(description="查询产品库存")
-async def check_inventory(product_id: str, warehouse: str = "default") -> ToolResult:
-    """查询指定仓库的产品库存
-
-    Args:
-        product_id: 产品唯一标识符
-        warehouse: 仓库编码,默认为主仓库
-    """
-    stock = await query_db(product_id, warehouse)
-    return ToolResult(output=f"库存: {stock}")
-
-# 确保此模块在 runner.run() 之前被 import
-```
-
-**注意**: `@tool` 通过副作用注册到全局 registry,必须确保定义工具的模块在调用 `runner.run()` 前被 import。
-
-### 参数 Schema 生成
-
-框架从函数签名和 docstring 自动生成 OpenAI Tool Schema,无需手写 JSON:
-
-- **参数类型**:从类型注解推断(`str`/`int`/`float`/`bool`/`list`/`dict`,支持 `Optional`、`Literal`、`List[T]`)
-- **参数描述**:从 Google 风格 docstring 的 `Args:` 段提取
-- **必填/可选**:有默认值的参数为可选,否则为必填
-- **工具描述**:优先使用 `@tool(description=...)` 参数,其次取 docstring 首行
-- `uid` 和 `context` 参数由框架自动注入,不会出现在 Schema 中
-
-上面的 `check_inventory` 会生成:
-
-```json
-{
-  "type": "function",
-  "function": {
-    "name": "check_inventory",
-    "description": "查询产品库存",
-    "parameters": {
-      "type": "object",
-      "properties": {
-        "product_id": { "type": "string", "description": "产品唯一标识符" },
-        "warehouse": {
-          "type": "string",
-          "description": "仓库编码,默认为主仓库",
-          "default": "default"
-        }
-      },
-      "required": ["product_id"]
-    }
-  }
-}
-```
-
-### 限制工具范围
-
-```python
-# 只启用指定工具(在内置工具基础上追加)
-config = RunConfig(tools=["check_inventory", "another_tool"])
-```
-
-## 自定义 Skills
-
-Skills 是 Markdown 文件,提供领域知识,注入到 system prompt。
-
-```
-my_project/
-└── skills/
-    └── my_domain.md
-```
-
-```markdown
----
-name: my-domain-skill
-description: 领域专属知识
----
-
-## Guidelines
-
-- 规则 1
-- 规则 2
-```
-
-```python
-runner = AgentRunner(
-    llm_call=...,
-    trace_store=...,
-    skills_dir="./skills",  # 指向你的 skills 目录
-)
-```
-
-内置 skills(`agent/skill/skills/`)始终自动加载,`skills_dir` 的内容额外追加。
-
-## 知识管理系统(Knowledge Management)
-
-知识管理系统通过**提取、存储、注入**三个环节,让 Agent 积累和复用结构化知识。
-
-### 核心流程
-
-**1. 提取(Extract)**
-
-- **触发时机**:
-  - 压缩时提取:消息量超阈值触发压缩时,在 Level 1 过滤前用完整 history 反思
-  - 完成时提取:Agent 运行完成后(不代表任务完成,可能中途退出等待人工评估)
-- **提取方式**:调用 LLM 对执行过程进行反思,提取可复用的知识
-- **自定义 Prompt**:可通过配置自定义反思 prompt,空则使用默认(见 `agent/core/prompts/knowledge.py`)
-
-**2. 存储(Store)**
-
-- **存储位置**:KnowHub 服务(默认 `http://localhost:8765`)
-- **知识结构**:
-  - `title`: 知识标题
-  - `content`: 知识内容
-  - `type`: 知识类型(strategy/tool/pattern/pitfall 等)
-  - `tags`: 标签(键值对,用于分类和检索)
-  - `scopes`: 作用域(如 `org:cybertogether`)
-  - `owner`: 所有者(默认从 git config user.email 获取)
-  - `resource_ids`: 关联资源 ID 列表(代码片段、凭证、cookies 等)
-- **资源管理**:
-  - 知识可关联多个资源(通过 `resource_ids` 字段)
-  - 资源包含 `body`(公开内容)和 `secure_body`(加密内容)
-  - 支持代码片段、API 凭证、cookies 等多种资源类型
-
-**3. 注入(Inject)**
-
-- **触发时机**:Agent 切换当前工作的 Goal 时自动触发
-- **检索策略**:基于 Goal 描述和上下文,从知识库检索相关知识
-- **注入方式**:将检索到的知识注入到 Agent 的上下文中
-
-### 配置
-
-知识管理配置通过 `RunConfig.knowledge` 传递:
-
-```python
-from agent.core.runner import KnowledgeConfig, RunConfig
-
-run_config = RunConfig(
-    model="claude-sonnet-4.5",
-    temperature=0.3,
-    max_iterations=1000,
-
-    knowledge=KnowledgeConfig(
-        # 压缩时提取(消息量超阈值触发压缩时,用完整 history 反思)
-        enable_extraction=True,
-        reflect_prompt="",  # 空则使用默认,见 agent/core/prompts/knowledge.py:REFLECT_PROMPT
-
-        # agent运行完成后提取
-        enable_completion_extraction=True,
-        completion_reflect_prompt="",  # 空则使用默认
-
-        # 知识注入(agent切换当前工作的goal时,自动注入相关知识)
-        enable_injection=True,
-
-        # 默认字段(保存/搜索时自动注入)
-        owner="",  # 空则从 git config user.email 获取(隐藏参数,LLM 不可见)
-        default_tags={"project": "my_project"},  # 与 LLM 传递的 tags 合并
-        default_scopes=["org:cybertogether"],  # 与 LLM 传递的 scopes 合并
-        default_search_types=["strategy", "tool"],
-        default_search_owner=""  # 空则不过滤
-    )
-)
-```
-
-**参数注入规则**(通过框架 `inject_params` 机制实现,详见 `agent/docs/tools.md`):
-
-- `owner`:隐藏参数,LLM 不可见,框架自动注入(`mode: default`)
-- `tags`:LLM 可追加新 key,框架默认 key 不可被覆盖(`mode: merge`)
-- `scopes`:LLM 可追加,与框架默认值合并去重(`mode: merge`)
-
-### 知识工具
-
-框架提供以下内置工具用于知识管理:
-
-- `knowledge_save`: 保存知识到知识库
-- `knowledge_search`: 搜索知识库
-- `knowledge_get`: 获取指定知识详情
-- `resource_save`: 保存资源(代码、凭证等)
-- `resource_get`: 获取资源内容
-
-这些工具会自动注入配置的默认字段(owner, tags, scopes 等)。
-
-## AgentRunner 参数
-
-```python
-AgentRunner(
-    llm_call,                # 必需:LLM 调用函数
-    trace_store=None,        # Trace 持久化(推荐 FileSystemTraceStore)
-    tool_registry=None,      # 工具注册表(默认:全局 registry)
-    skills_dir=None,         # 自定义 skills 目录
-    utility_llm_call=None,   # 轻量 LLM(生成任务标题等)
-    debug=False,             # 调试模式
-)
-```
-
-## RunConfig 参数
-
-```python
-RunConfig(
-    model="gpt-4o",          # 模型标识
-    temperature=0.3,
-    max_iterations=200,       # Agent loop 最大轮数
-    tools=None,               # None=全部已注册工具,List[str]=内置+指定工具
-    system_prompt=None,       # None=从 skills 自动构建
-    agent_type="default",     # 预设类型:default / explore / analyst
-    trace_id=None,            # 续跑/回溯时传入已有 trace ID
-    after_sequence=None,      # 从哪条消息后续跑(message sequence)
-    goal_compression="on_overflow",  # Goal 压缩模式:none / on_complete / on_overflow
-    knowledge=KnowledgeConfig(),  # 知识管理配置
-)
-```
-
-    system_prompt=None,       # None=从 skills 自动构建
-    agent_type="default",     # 预设类型:default / explore / analyst
-    trace_id=None,            # 续跑/回溯时传入已有 trace ID
-    after_sequence=None,      # 从哪条消息后续跑(message sequence)
-
-)
-
-````
-
-## LLM Providers
-
-框架内置两个 provider:
-
-```python
-from agent.llm import create_openrouter_llm_call, create_gemini_llm_call
-
-# OpenRouter(支持多种模型)
-llm = create_openrouter_llm_call(model="anthropic/claude-sonnet-4.5")
-
-# Google Gemini
-llm = create_gemini_llm_call(model="gemini-2.5-flash")
-````
-
-自定义 provider 只需实现签名:
-
-```python
-async def my_llm_call(messages, model, tools, temperature, **kwargs) -> dict:
-    # 调用你的 LLM
-    return {
-        "content": "...",
-        "tool_calls": [...] or None,
-        "prompt_tokens": 100,
-        "completion_tokens": 50,
-        "cost": 0.001,
-        "finish_reason": "stop",
-    }
-```
-
-## API Server
-
-```bash
-python api_server.py
-```
-
-| 方法 | 路径                        | 说明        |
-| ---- | --------------------------- | ----------- |
-| GET  | `/api/traces`               | 列出 Traces |
-| GET  | `/api/traces/{id}`          | Trace 详情  |
-| GET  | `/api/traces/{id}/messages` | 消息列表    |
-| POST | `/api/traces`               | 新建并执行  |
-| POST | `/api/traces/{id}/run`      | 续跑/回溯   |
-| POST | `/api/traces/{id}/stop`     | 停止        |
-| WS   | `/api/traces/{id}/watch`    | 实时事件    |
-
-需在 `api_server.py` 中配置 Runner 才能启用 POST 端点。
-
-## 项目结构
-
-```
-agent/
-├── core/           # AgentRunner + 预设
-├── tools/          # 工具系统(registry + 内置工具)
-├── trace/          # 执行追踪 + 计划(GoalTree)+ API
-├── skill/          # Skills(技能系统)
-└── llm/            # LLM Provider 适配
-```
-
-详细架构文档:[docs/README.md](./docs/README.md)
-
-## Agent 预设(Presets)
-
-预设定义不同类型 Agent 的配置模板(工具权限、system prompt、skills 等)。
-
-**项目级预设**:在项目目录创建 `presets.json`,支持从 `.prompt` 文件加载自定义 system prompt:
-
-```json
-{
-  "tool_research": {
-    "system_prompt_file": "tool_research.prompt",
-    "max_iterations": 50,
-    "skills": ["planning", "research", "browser"],
-    "description": "工具调研 Agent"
-  }
-}
-```
-
-**加载预设**:
-```python
-from agent.core.presets import load_presets_from_json
-
-load_presets_from_json("path/to/presets.json")
-```
-
-**使用预设**:LLM 调用 `agent` 工具时指定 `agent_type`:
-```python
-agent(task="调研视频生成工具", agent_type="tool_research")
-```
-
-详见 `examples/production/` 示例和 `agent/docs/architecture.md`。
-
-## 交互式 CLI(Interactive CLI)
-
-框架提供交互式控制器,支持实时监控、手动干预和经验总结。
-
-### 使用方式
-
-```python
-from agent.cli import InteractiveController
-
-# 创建交互控制器
-interactive = InteractiveController(
-    runner=runner,
-    store=store,
-    enable_stdin_check=True  # 启用标准输入检查
-)
-
-# 在执行循环中检查用户输入
-async for item in runner.run(messages=messages, config=config):
-    cmd = interactive.check_stdin()
-    if cmd == 'pause':
-        await runner.stop(trace_id)
-        menu_result = await interactive.show_menu(trace_id, current_sequence)
-        # 处理菜单结果...
-    elif cmd == 'quit':
-        await runner.stop(trace_id)
-        break
-```
-
-### 交互控制
-
-在执行过程中,可以通过命令行实时控制:
-
-| 按键          | 动作         | 说明                              |
-| ------------- | ------------ | --------------------------------- |
-| `p` / `pause` | **暂停执行** | 立即挂起 Agent 循环,进入交互菜单 |
-| `q` / `quit`  | **停止执行** | 安全停止并保存当前的执行状态      |
-
-### 交互菜单功能
-
-进入暂停模式后,系统提供以下操作:
-
-1. **插入干预消息**:直接向 Agent 下达新指令
-2. **触发经验总结 (Reflect)**:强制 Agent 对当前过程进行反思
-3. **查看 GoalTree**:可视化当前任务的拆解结构和完成进度
-4. **上下文压缩 (Compact)**:手动精简对话历史
-
-### 项目配置示例
-
-完整的项目配置示例见 `examples/research/config.py`:
-
-```python
-from agent.core.runner import KnowledgeConfig, RunConfig
-from agent.utils import setup_logging
-
-# Agent 运行配置
-RUN_CONFIG = RunConfig(
-    model="claude-sonnet-4.5",
-    temperature=0.3,
-    max_iterations=1000,
-    name="Research Agent",
-
-    knowledge=KnowledgeConfig(
-        enable_extraction=True,
-        enable_completion_extraction=True,
-        enable_injection=True,
-        owner="",  # 空则从 git config 获取
-        default_tags={"project": "research"},
-        default_scopes=["org:cybertogether"],
-        default_search_types=["strategy", "tool"],
-    )
-)
-
-# 基础设施配置
-SKILLS_DIR = "./skills"
-TRACE_STORE_PATH = ".trace"
-DEBUG = True
-LOG_LEVEL = "INFO"
-LOG_FILE = None  # 可设置为文件路径
-
-# 在 run.py 中使用
-setup_logging(level=LOG_LEVEL, file=LOG_FILE)
-
-runner = AgentRunner(
-    trace_store=FileSystemTraceStore(base_path=TRACE_STORE_PATH),
-    llm_call=create_openrouter_llm_call(model=f"anthropic/{RUN_CONFIG.model}"),
-    skills_dir=SKILLS_DIR,
-    debug=DEBUG
-)
-
-async for item in runner.run(messages=messages, config=RUN_CONFIG):
-    # 处理执行结果
-    pass
-```
-
-**配置说明**:
-
-- 直接使用框架的 `RunConfig` 和 `KnowledgeConfig`,不需要自定义配置类
-- 基础设施配置(skills_dir, trace_store_path 等)用简单变量定义
-- 使用 `agent.utils.setup_logging()` 配置日志
-
-## 任务可视化与调试
-
-框架在运行期间会生成唯一的 `trace_id`。
-
-- **本地日志**:所有的执行细节、工具调用和 Goal 状态均持久化在 `.trace/` 目录下。
-- **Web 可视化**:
-
-1. 启动服务器:`python api_server.py`
-2. 启动前端:
-
-```
-  cd frontend/react-template
-  yarn
-  yarn dev
-```
-
-3. 访问控制台:`http://localhost:3000`
-4. 在前端界面中切换任务,即直观追踪 Agent 的思考链路。
-5. 因为该可视化读取的是根目录下的.trace文件,建议运行项目时,可以在根目录下用命令行运行`python examples/[project_name]/run.py`,使运行得到的trace存放在根目录
-
-### 提示:目前前端可视化只供观看本地运行过的trace结果,新任务运行等功能正在开发中,运行可在命令行中执行
-
-### 绿色节点为整体的goal(目标),蓝色节点为子goal(目标),灰色节点为基础信息节点。点击蓝色边/绿色边会折叠节点,点击节点会在右侧显示详情。
-
----
-
-## 示例项目结构
-
-可以参考其他文件夹中的结构:
-
-```text
-examples/[your_example]/
-├── input/             # (可选)输入数据
-├── output_1/          # (可选)输出目录
-├── skills/            # (可选)领域专属 Skill (.md)
-├── tool/              # (可选)自定义工具
-├── presets.json       # (可选)预定义的子 Agent 配置
-├── config.py          # (推荐)项目配置
-├── [task].prompt      # (必须)任务 System Prompt 和 User Prompt
-└── run.py             # (必须)交互式运行入口
-```
-
----
-
-## 环境兼容性
-
-针对 Clash Verge / TUN 模式等网络环境,本项目已内置代理自动避让逻辑:
-
-- **代理优化**:通过 `no_proxy` 配置防止 `httpx` 错误引导流量。
-- **Browser 模式**:支持 `cloud` (远程) 和 `local` (本地) 模式切换。
-
-## 运行结果存储
-
-运行过程中,会自动存储以下内容:
-
-- **运行轨迹**:根目录下 `.trace/` 文件夹下的实际运行路径结果
-- **知识库**:KnowHub 服务中保存的知识条目(通过 API 访问)

+ 0 - 42
add_knowledge.py

@@ -1,42 +0,0 @@
-#!/usr/bin/env python3
-import json
-
-# 读取现有文件
-with open('/root/Agent/pre_upload_list.json', 'r', encoding='utf-8') as f:
-    data = json.load(f)
-
-# 新的知识条目
-new_knowledge = [
-    {
-        "task": "在图生图任务中,当 nano_banana 工具无法处理本地路径时的降级策略",
-        "content": "当使用 nano_banana 进行图生图时,如果传入本地图片路径(如 examples/xxx.png)会报错「Base64 decoding failed」,因为 nano_banana 期望的是 Base64 编码或网络 URL。\n\n正确做法:\n1. 先用 image_uploader 将本地图片上传获取 CDN URL\n2. 或使用支持本地路径自动上传的工具(如 flux_generate、seedream_generate)\n\n工具选择优先级:\n- nano_banana:需要手动上传获取 CDN URL,但支持多图融合,效果最好\n- flux_generate:自动处理本地路径,图生图效果好,推荐作为备选\n- seedream_generate:自动处理本地路径,需要最小尺寸 3686400 像素\n\n案例:Stage 4 检查 img_3 花丛色彩时,nano_banana 传入本地路径报错,切换至 flux_generate 成功完成修复。",
-        "types": ["experience"],
-        "tags": {
-            "intent": "图生图/色彩修复",
-            "state": "nano_banana/flux_generate"
-        },
-        "score": 4,
-        "source": {"category": "research"}
-    },
-    {
-        "task": "在图生图色彩控制中,通过 prompt 明确指定色彩排除可有效降低干扰元素饱和度",
-        "content": "当需要降低画面中某元素的色彩饱和度或改变其色系时,在 prompt 中明确指定:\n- 目标色彩:\"should be green foliage plants or very subtle low-saturation green flowers\"\n- 排除色彩:\"not purple or blue\"\n- 整体色调:\"The overall color scheme should be white and green with minimal color accents\"\n\n配合 flux-kontext-max 模型的图生图模式,可有效实现色彩调整而不破坏原有构图。\n\n案例:img_3 花丛色彩修复,将蓝紫色花丛成功调整为淡紫色/白色,融入绿色背景。",
-        "types": ["experience"],
-        "tags": {
-            "intent": "色彩控制/图生图优化",
-            "state": "flux_generate"
-        },
-        "score": 4,
-        "source": {"category": "research"}
-    }
-]
-
-# 添加到 knowledge 数组
-data['knowledge'].extend(new_knowledge)
-
-# 写回文件
-with open('/root/Agent/pre_upload_list.json', 'w', encoding='utf-8') as f:
-    json.dump(data, f, ensure_ascii=False, indent=2)
-
-print(f"成功添加 {len(new_knowledge)} 条知识条目")
-print(f"当前 knowledge 总数:{len(data['knowledge'])}")

+ 0 - 44
add_workflow_experience_knowledge.py

@@ -1,44 +0,0 @@
-#!/usr/bin/env python3
-import json
-
-# 读取现有文件
-with open('/root/Agent/pre_upload_list.json', 'r', encoding='utf-8') as f:
-    data = json.load(f)
-
-# 新的知识条目
-new_knowledge = [
-    {
-        "task": "在分析工作流文件时,当 glob 搜索找不到目标文件的有效定位策略",
-        "content": "当 glob_files 按模式匹配找不到目标文件时,应该:1) 使用 bash_command 执行 find 命令进行更广泛的文件系统搜索,支持通配符和路径遍历;2) 结合 grep 内容搜索,通过文件内容关键词定位(如工具名、方案名);3) 检查相关目录结构,如 research/outputs/ 下的子目录。案例:本次任务中 glob_files 搜索\"*wf_005*\"未找到文件,但通过 find 命令搜索包含\"ComfyUI\"\"Nano Banana\"等关键词的 markdown 文件,成功在 outputs/04/ 目录下找到颗粒感纸张纹理插画风格工作流调研报告.md,该文件即为 wf_005 的 raw_markdown 内容。",
-        "types": ["experience"],
-        "tags": {
-            "intent": "工作流文件定位",
-            "state": "文件搜索",
-            "tools": "glob_files,bash_command"
-        },
-        "score": 4,
-        "source": {"category": "research"}
-    },
-    {
-        "task": "工作流意图归纳的粒度判断原则",
-        "content": "在将工作流技术步骤归纳为意图级描述时,粒度判断应遵循:1) 保留工具差异——相同意图但不同工具实现应分开记录,因为工具选择是工作流变体的关键信息;2) 合并连续子步骤——同一工具内的连续操作(如新建图层→填充→设置混合模式)可合并为一个意图;3) 跨方案可比较——意图描述应使不同工作流的相同功能模块可对比。案例:本次将 Photoshop 的 5 种颗粒添加方法分别记录为\"添加溶解颗粒效果\"\"手绘颗粒纹理\"\"滤镜添加全局杂色\"等,保留了方法差异;而将 ComfyUI 的分割 + 重排合并为\"创建四方连续布局\"。",
-        "types": ["experience"],
-        "tags": {
-            "intent": "工作流分析",
-            "state": "意图归纳",
-            "method": "语义聚类"
-        },
-        "score": 4,
-        "source": {"category": "research"}
-    }
-]
-
-# 添加到 knowledge 数组
-data['knowledge'].extend(new_knowledge)
-
-# 写回文件
-with open('/root/Agent/pre_upload_list.json', 'w', encoding='utf-8') as f:
-    json.dump(data, f, ensure_ascii=False, indent=2)
-
-print(f"成功添加 {len(new_knowledge)} 条知识条目")
-print(f"当前 knowledge 总数:{len(data['knowledge'])}")

+ 123 - 1
agent/README.md

@@ -121,7 +121,8 @@ RunConfig(
     max_iterations=200,
     agent_type="default",           # Agent 预设(对应 presets.json)
     name="任务名称",
-    tools=["read_file", "bash"],    # 限制可用工具(None=全部)
+    tools=None,                      # 精确指定工具列表(优先于 tool_groups)
+    tool_groups=["core", "browser"], # 工具分组白名单,默认 ["core"]
     goal_compression="on_overflow", # Goal 压缩:"none" / "on_complete" / "on_overflow"
     knowledge=KnowledgeConfig(...), # 知识提取配置
 )
@@ -203,3 +204,124 @@ examples/research/
 | POST | `/api/traces/{id}/stop` | 停止运行 |
 
 **实现**:`agent/trace/api.py`, `agent/trace/run_api.py`
+
+---
+
+## 附录:工具分组
+
+通过 `RunConfig(tool_groups=[...])` 控制 Agent 可用的工具范围。默认仅 `["core"]`。每个工具在 `@tool(groups=[...])` 中声明分组,支持多标签。
+
+机制详见 [docs/tools.md § 工具分组](./docs/tools.md#工具分组)。
+
+### core — 基础能力(13)
+
+| 工具 | 说明 |
+|------|------|
+| `read_file` | 读取单个文件(文本/图片/PDF) |
+| `read_images` | 批量读图 + 网格拼图 |
+| `edit_file` | 编辑文件 |
+| `write_file` | 写入文件 |
+| `glob_files` | 文件模式匹配 |
+| `grep_content` | 内容搜索(正则) |
+| `bash_command` | 执行 shell 命令 |
+| `skill` | 调用 skill |
+| `list_skills` | 列出可用 skill |
+| `agent` | 创建子 Agent |
+| `evaluate` | 评估执行结果 |
+| `goal` | 目标/计划管理 |
+| `get_current_context` | 获取当前执行上下文 |
+
+### browser — 浏览器自动化(14)
+
+| 工具 | 说明 |
+|------|------|
+| `browser_navigate` | 导航到 URL |
+| `browser_search` | 搜索引擎搜索 |
+| `browser_back` | 返回上一页 |
+| `browser_interact` | 元素交互(click/type/send_keys/upload/dropdown) |
+| `browser_scroll` | 滚动页面 |
+| `browser_screenshot` | 截图(可带元素编号标注) |
+| `browser_elements` | 获取可交互元素列表 |
+| `browser_read` | 读取页面内容(html/find/long) |
+| `browser_extract` | LLM 驱动的结构化数据提取 |
+| `browser_tabs` | 标签页管理(switch/close) |
+| `browser_cookies` | Cookie/登录态(load/export/ensure_login) |
+| `browser_wait` | 等待(定时/等用户操作) |
+| `browser_js` | 执行 JavaScript |
+| `browser_download` | 下载文件 |
+
+### content — 内容搜索(6)
+
+| 工具 | 说明 |
+|------|------|
+| `content_platforms` | 列出/查询平台及参数(支持模糊匹配) |
+| `content_search` | 跨 11 平台搜索(小红书/B站/知乎/GitHub/YouTube/X 等) |
+| `content_detail` | 查看内容详情(从搜索缓存按索引取) |
+| `content_suggest` | 搜索关键词补全建议 |
+| `extract_video_clip` | YouTube 视频片段截取 |
+| `import_content` | 批量导入文章到 CMS |
+
+### knowledge — 知识管理(2)
+
+| 工具 | 说明 |
+|------|------|
+| `ask_knowledge` | 向知识库查询(通过 Librarian Agent) |
+| `upload_knowledge` | 上传调研结果到知识库 |
+
+### toolhub — 远程工具库(3)
+
+| 工具 | 说明 |
+|------|------|
+| `toolhub_health` | 检查远程工具库状态 |
+| `toolhub_search` | 搜索远程 AI 工具 |
+| `toolhub_call` | 调用远程工具(图片参数支持本地路径) |
+
+### feishu — 飞书(4)
+
+| 工具 | 说明 |
+|------|------|
+| `feishu_get_contact_list` | 获取联系人列表 |
+| `feishu_send_message_to_contact` | 给联系人发消息 |
+| `feishu_get_contact_replies` | 获取联系人回复 |
+| `feishu_get_chat_history` | 获取聊天历史 |
+
+### im — IM 通信(8)
+
+| 工具 | 说明 |
+|------|------|
+| `im_setup` | 初始化 IM 连接 |
+| `im_send_message` | 发送消息 |
+| `im_receive_messages` | 接收消息 |
+| `im_check_notification` | 检查通知 |
+| `im_get_contacts` | 获取联系人 |
+| `im_get_chat_history` | 获取聊天历史 |
+| `im_open_window` | 打开 IM 窗口 |
+| `im_close_window` | 关闭 IM 窗口 |
+
+### resource — 资源查询(2)
+
+| 工具 | 说明 |
+|------|------|
+| `resource_list_tools` | 列出资源工具 |
+| `resource_get_tool` | 获取工具详情 |
+
+### knowledge_internal — 知识库内部操作(14)
+
+> 仅供 Librarian Agent 内部使用,普通 Agent 不可见。通过 `tools=[...]` 精确指定访问。
+
+| 工具 | 说明 |
+|------|------|
+| `knowledge_search` | 知识检索(语义 + 精排) |
+| `knowledge_save` | 保存知识条目 |
+| `knowledge_list` | 列出知识 |
+| `knowledge_update` | 更新知识反馈 |
+| `knowledge_batch_update` | 批量更新反馈 |
+| `knowledge_slim` | 知识瘦身 |
+| `resource_save` | 保存资源 |
+| `resource_get` | 获取资源 |
+| `tool_search` | 搜索工具记录 |
+| `tool_list` | 列出工具记录 |
+| `capability_search` | 搜索能力记录 |
+| `capability_list` | 列出能力记录 |
+| `requirement_search` | 搜索需求记录 |
+| `requirement_list` | 列出需求记录 |

+ 1 - 2
agent/core/__init__.py

@@ -7,7 +7,7 @@ Agent Core - 核心引擎模块
 3. Agent 预设(AgentPreset)
 """
 
-from agent.core.runner import AgentRunner, BUILTIN_TOOLS, CallResult, RunConfig
+from agent.core.runner import AgentRunner, CallResult, RunConfig
 from agent.core.presets import (
     AgentPreset,
     AGENT_PRESETS,
@@ -19,7 +19,6 @@ from agent.core.presets import (
 
 __all__ = [
     "AgentRunner",
-    "BUILTIN_TOOLS",
     "CallResult",
     "RunConfig",
     "AgentPreset",

+ 14 - 101
agent/core/runner.py

@@ -103,8 +103,8 @@ class RunConfig:
     model: str = "gpt-4o"
     temperature: float = 0.3
     max_iterations: int = 200
-    tools: Optional[List[str]] = None          # None = 全部已注册工具
-    exclude_tools: List[str] = field(default_factory=list)  # 从工具列表中排除的工具名
+    tools: Optional[List[str]] = None          # None = 按 tool_groups 过滤;显式列表 = 精确指定
+    tool_groups: Optional[List[str]] = field(default_factory=lambda: ["core"])  # 工具分组白名单;默认仅 core,项目按需追加
     side_branch_max_turns: int = 5             # 侧分支最大轮次(压缩/反思)
     goal_compression: Literal["none", "on_complete", "on_overflow"] = "on_overflow"  # Goal 压缩模式
 
@@ -142,89 +142,8 @@ class RunConfig:
     knowledge: KnowledgeConfig = field(default_factory=KnowledgeConfig)
 
 
-    # 内置工具列表(始终自动加载)
-BUILTIN_TOOLS = [
-    # 文件操作工具
-    "read_file",
-    "edit_file",
-    "write_file",
-    "glob_files",
-    "grep_content",
-
-    # 系统工具
-    "bash_command",
-
-    # 技能和目标管理
-    "skill",
-    "list_skills",
-    "goal",
-    "agent",
-    "evaluate",
-    "get_current_context",
-
-    # 搜索工具
-    "search_posts",
-    "select_post",
-    "get_search_suggestions",
-    "x_search",
-    "youtube_search",
-    "youtube_detail",
-    "import_content",
-
-    # 知识管理工具
-    "ask_knowledge",
-    "upload_knowledge",
-    # "knowledge_search",
-    # "knowledge_save",
-    # "knowledge_update",
-    # "knowledge_batch_update",
-    # "knowledge_list",
-    # "knowledge_slim",
-
-    # 浏览器工具
-    "browser_get_live_url",
-    "browser_navigate_to_url",
-    "browser_search_web",
-    "browser_go_back",
-    "browser_wait",
-    "browser_click_element",
-    "browser_input_text",
-    "browser_send_keys",
-    "browser_upload_file",
-    "browser_scroll_page",
-    "browser_find_text",
-    "browser_screenshot",
-    "browser_switch_tab",
-    "browser_close_tab",
-    "browser_get_dropdown_options",
-    "browser_select_dropdown_option",
-    "browser_extract_content",
-    "browser_read_long_content",
-    "browser_download_direct_url",
-    "browser_get_page_html",
-    "browser_get_visual_selector_map",
-    "browser_evaluate",
-    "browser_ensure_login_with_cookies",
-    # 可以暂时由飞书消息替代
-    #"browser_wait_for_user_action",
-    "browser_done",
-    "browser_export_cookies",
-    "browser_load_cookies",
-
-    # 飞书工具
-    "feishu_send_message_to_contact",
-    "feishu_get_chat_history",
-    "feishu_get_contact_replies",
-    "feishu_get_contact_list",
-
-    # IM 工具
-    "im_setup",
-    "im_check_notification",
-    "im_receive_messages",
-    "im_send_message",
-    "im_get_contacts",
-    "im_get_chat_history",
-]
+    # BUILTIN_TOOLS 硬编码列表已移除(2026-04)。
+    # 工具可用性现在由 @tool(groups=[...]) 声明 + RunConfig.tool_groups 过滤控制。
 
 
 @dataclass
@@ -552,7 +471,7 @@ class AgentRunner:
         task_name = config.name or await self._generate_task_name(messages)
 
         # 准备工具 Schema
-        tool_schemas = self._get_tool_schemas(config.tools, config.exclude_tools)
+        tool_schemas = self._get_tool_schemas(config.tools, config.tool_groups)
 
         trace_obj = Trace(
             trace_id=trace_id,
@@ -1086,7 +1005,7 @@ class AgentRunner:
     ) -> AsyncIterator[Union[Trace, Message]]:
         """ReAct 循环"""
         trace_id = trace.trace_id
-        tool_schemas = self._get_tool_schemas(config.tools, config.exclude_tools)
+        tool_schemas = self._get_tool_schemas(config.tools, config.tool_groups)
 
         # 当前主路径头节点的 sequence(用于设置 parent_sequence)
         head_seq = trace.head_sequence
@@ -2838,25 +2757,19 @@ class AgentRunner:
         )
         return messages
 
-    def _get_tool_schemas(self, tools: Optional[List[str]], exclude_tools: Optional[List[str]] = None) -> List[Dict]:
+    def _get_tool_schemas(self, tools: Optional[List[str]] = None, tool_groups: Optional[List[str]] = None) -> List[Dict]:
         """
         获取工具 Schema
 
-        - tools=None: 使用 registry 中全部已注册工具(含内置 + 外部注册的)
-        - tools=["a", "b"]: 在 BUILTIN_TOOLS 基础上追加指定工具
-        - exclude_tools=["x"]: 从最终列表中排除指定工具(防止递归调用等场景)
+        优先级:
+        - tools 非空: 精确指定工具列表(忽略 tool_groups)
+        - tool_groups 非空: 按分组白名单过滤
+        - 两者都为 None: 返回所有已注册工具
         """
-        if tools is None:
-            # 全部已注册工具
-            tool_names = self.tools.get_tool_names()
+        if tools is not None:
+            tool_names = list(tools)
         else:
-            # BUILTIN_TOOLS + 显式指定的额外工具
-            tool_names = BUILTIN_TOOLS.copy()
-            for t in tools:
-                if t not in tool_names:
-                    tool_names.append(t)
-        if exclude_tools:
-            tool_names = [t for t in tool_names if t not in exclude_tools]
+            tool_names = self.tools.get_tool_names(groups=tool_groups)
         return self.tools.get_schemas(tool_names)
 
     # 默认 system prompt 前缀(当 config.system_prompt 和前端都未提供 system message 时使用)

+ 3 - 2
agent/docs/tools-refactor-plan.md

@@ -1,8 +1,9 @@
 # 工具体系改造方案(Refactor Plan)
 
-> 本文档是**未来规划**,不是现状描述。当前工具体系的状态请看 [`tools.md`](./tools.md)。
+> ✅ **方案一(内容工具族)** 和 **方案二(浏览器工具族)** 已于 2026-04-12 完成落地。
+> 下方保留原始方案文档供参考。沙箱工具已于此前删除。
 >
-> 当方案落地后,记得把本文档对应的章节删除或合并到 `tools.md`
+> 当前工具体系的状态请看 [`tools.md`](./tools.md)
 
 ## 背景
 

+ 54 - 3
agent/docs/tools.md

@@ -11,7 +11,8 @@
 3. [ToolResult 和记忆管理](#toolresult-和记忆管理)
 4. [ToolContext 和依赖注入](#toolcontext-和依赖注入)
 5. [高级特性](#高级特性)
-6. [内置基础工具](#内置基础工具)
+6. [工具分组](#工具分组)
+7. [内置基础工具](#内置基础工具)
 7. [集成 Browser-Use](#集成-browser-use)
 8. [最佳实践](#最佳实践)
 
@@ -790,6 +791,50 @@ print(f"Success rate: {stats['success_rate']:.1%}")
 
 ---
 
+## 工具分组
+
+工具通过 `@tool(groups=[...])` 声明所属分组,`RunConfig.tool_groups` 控制 Agent 可用哪些分组。
+
+### 机制
+
+```python
+# 注册时声明分组
+@tool(groups=["browser"])
+async def browser_navigate(url: str) -> ToolResult: ...
+
+@tool(groups=["content", "media"])  # 支持多标签
+async def extract_video_clip(...) -> ToolResult: ...
+```
+
+```python
+# 配置时按分组过滤
+RunConfig(tool_groups=["core", "content"])        # 只用核心 + 内容工具
+RunConfig(tool_groups=["core", "browser"])         # 只用核心 + 浏览器工具
+RunConfig(tool_groups=None)                        # 全部工具(默认)
+RunConfig(tools=["knowledge_search", "read_file"]) # 精确指定(优先于 tool_groups)
+```
+
+### 过滤逻辑
+
+`_get_tool_schemas(tools, tool_groups)` 的优先级:
+
+1. `tools` 非空 → 精确使用指定列表(忽略 `tool_groups`)
+2. `tool_groups` 非空 → 按分组白名单过滤(工具的任一 group 命中即选中)
+3. 两者都为 None → 返回所有已注册工具
+
+### 实现位置
+
+- 分组声明:`@tool(groups=[...])` — `agent/tools/registry.py`
+- 分组存储:`ToolRegistry._tools[name]["groups"]`
+- 分组过滤:`ToolRegistry.get_tool_names(groups=[...])`
+- 配置入口:`RunConfig.tool_groups` — `agent/core/runner.py`
+
+### 分组一览
+
+完整的分组和工具列表见 [agent/README.md 附录](../README.md#附录工具分组)。
+
+---
+
 ## 内置基础工具
 
 > 参考 opencode 实现的文件操作和命令执行工具
@@ -819,6 +864,12 @@ print(f"Success rate: {stats['success_rate']:.1%}")
 | `toolhub_health` | 检查 ToolHub 远程工具库服务状态 | 自研 |
 | `toolhub_search` | 搜索/发现 ToolHub 远程工具 | 自研 |
 | `toolhub_call` | 调用 ToolHub 远程工具(图片参数支持本地文件路径) | 自研 |
+| `content_platforms` | 列出/查询内容平台及其搜索参数(支持模糊匹配) | 自研 |
+| `content_search` | 跨平台内容搜索(11 个平台统一入口) | 自研 |
+| `content_detail` | 查看内容详情(从搜索缓存按索引取) | 自研 |
+| `content_suggest` | 搜索关键词补全建议 | 自研 |
+| `extract_video_clip` | 截取已下载 YouTube 视频的片段 | 自研 |
+| `import_content` | 批量导入文章到 CMS | 自研 |
 | `ask_knowledge` | 向知识库查询信息(通过 KnowHub Librarian) | 自研 |
 | `upload_knowledge` | 上传调研结果到知识库 | 自研 |
 
@@ -846,9 +897,9 @@ print(f"Success rate: {stats['success_rate']:.1%}")
 | 10-12 张 | 4 列 | 320px |
 | 13-16 张 | 4 列 | 300px |
 
-**关于标签/标题:** `read_images` 的拼图**不显示文件名**,只显示索引序号——因为本地文件名(如 `IMG_1234.jpg`)对 LLM 理解内容没有帮助,而索引到原始路径的对照表通过返回文本提供,LLM 可以用"第 3 张"这种引用方式精确指代。对比之下 `search_posts` / `youtube_search` 的拼图**会**显示 label(帖子/视频标题),因为这些是内容型元数据,有实际信息量。这一差异反映在 `build_image_grid(labels=...)` 参数上:传 `None` 只画序号,传列表则在每格下方画标题。
+**关于标签/标题:** `read_images` 的拼图**不显示文件名**,只显示索引序号——因为本地文件名(如 `IMG_1234.jpg`)对 LLM 理解内容没有帮助,而索引到原始路径的对照表通过返回文本提供,LLM 可以用"第 3 张"这种引用方式精确指代。对比之下 `content_search` 的拼图**会**显示 label(帖子/视频标题),因为这些是内容型元数据,有实际信息量。这一差异反映在 `build_image_grid(labels=...)` 参数上:传 `None` 只画序号,传列表则在每格下方画标题。
 
-网格和降采样的实现在 `agent/tools/utils/image.py`,`search_posts` 和 `youtube_search` 等工具也复用同一套拼图逻辑。
+网格和降采样的实现在 `agent/tools/utils/image.py`,`content_search` 等内容工具也复用同一套拼图逻辑。
 
 ### Agent 工具
 

+ 29 - 23
agent/skill/skills/browser.md

@@ -9,32 +9,38 @@ description: 浏览器自动化工具使用指南
 
 ### 基本工作流程
 
-1. **页面导航**: 使用 `browser_navigate_to_url` 或 `browser_search_web` 到达目标页面
+1. **页面导航**: 使用 `browser_navigate(url)` 或 `browser_search(query)` 到达目标页面
 2. **等待加载**: 页面跳转后调用 `browser_wait(seconds=2)` 等待内容加载
-3. **获取元素索引**: 调用 `browser_get_visual_selector_map` 获取可交互元素的索引映射和当前界面的截图
-4. **执行交互**: 使用 `browser_click_element`、`browser_input_text` 等工具操作页面
-5. **提取内容**: 使用 `browser_extract_content`, `browser_read_long_content`, `browser_get_page_html` 获取数据
+3. **获取元素索引**: 调用 `browser_screenshot(highlight_elements=True)` 获取带编号标注的截图 + 元素列表
+4. **执行交互**: 使用 `browser_interact(action, index, ...)` 操作页面(click / type / send_keys / upload / dropdown)
+5. **提取内容**: 使用 `browser_extract(query)` 让 LLM 提取结构化数据,或 `browser_read(mode="long")` 分页读取长内容
 
 ### 关键原则
 
 - **禁止模拟结果**:不要输出你认为的搜索结果,而是要调用工具获取真实结果
-- **必须先获取索引**: 所有 `index` 参数都需要先通过 `browser_get_selector_map` 获取
-- **高级工具**:优先使用 `browser_extract_content`, `browser_read_long_content` 等工具获取数据,而不是使用 `browser_get_selector_map` 获取索引后手动解析
-- **操作后等待**: 任何可能触发页面变化的操作(点击、输入、滚动)后都要调用 `browser_wait`
+- **必须先获取索引**: 所有 `index` 参数都需要先通过 `browser_screenshot(highlight_elements=True)` 或 `browser_elements()` 获取
+- **高级工具**:优先使用 `browser_extract` / `browser_read` 获取数据,而不是手动解析元素
+- **操作后等待**: 任何可能触发页面变化的操作后都要调用 `browser_wait`
 - **登录处理**:
-  - **正常登录**:当遇到需要登录的网页时,使用 `browser_load_cookies` 来登录
-  - **首次登录**:当没有该网站的 cookie 时,需要请求人类协助登录:
-    1. 调用 `browser_get_live_url` 获取云浏览器实时画面链接
-    2. 导航到目标网站的登录页面
-    3. 通过 `feishu_send_message_to_contact` 将 live URL 发送给相关人员,请求其在浏览器中完成登录
-    4. 使用 `feishu_get_contact_replies(contact_name="...", wait_time_seconds=300)` 等待对方回复确认登录完成
-    5. 收到回复后使用 `browser_export_cookies` 将登录态保存下来
-- **复杂操作用JS**: 当标准工具无法满足时,使用 `browser_evaluate` 执行 JavaScript 代码
-
-### 工具分类
-
-**导航**: browser_get_live_url, browser_navigate_to_url, browser_search_web, browser_go_back, browser_wait
-**交互**: browser_click_element, browser_input_text, browser_send_keys, browser_upload_file
-**视图**: browser_scroll_page, browser_find_text, browser_screenshot
-**提取**: browser_extract_content, browser_read_long_content, browser_get_page_html, browser_get_selector_map, browser_get_visual_selector_map
-**高级**: browser_evaluate, browser_load_cookies, browser_export_cookies, browser_wait_for_user_action, browser_download_direct_url
+  - **正常登录**:使用 `browser_cookies(action="load", url=...)` 注入已保存的 cookie
+  - **首次登录**:需要人类协助——导航到登录页,通过飞书发送链接,等待确认后 `browser_cookies(action="export")` 保存
+- **复杂操作用JS**: 当标准工具无法满足时,使用 `browser_js(code)` 执行 JavaScript
+
+### 工具一览
+
+| 工具 | 功能 |
+|------|------|
+| `browser_navigate(url)` | 导航到 URL |
+| `browser_search(query, engine)` | 搜索引擎搜索 |
+| `browser_back()` | 返回上一页 |
+| `browser_interact(action, ...)` | 元素交互(click/type/send_keys/upload/dropdown) |
+| `browser_scroll(down, pages)` | 滚动页面 |
+| `browser_screenshot(highlight)` | 截图(highlight=True 带元素编号标注) |
+| `browser_elements()` | 获取可交互元素列表(纯文本) |
+| `browser_read(mode)` | 读取页面(html/find/long) |
+| `browser_extract(query)` | LLM 驱动的结构化数据提取 |
+| `browser_tabs(action, tab_id)` | 标签页管理(switch/close) |
+| `browser_cookies(action, ...)` | Cookie/登录态管理(load/export/ensure_login) |
+| `browser_wait(seconds/user_message)` | 等待(定时 or 等用户操作) |
+| `browser_js(code)` | 执行 JavaScript |
+| `browser_download(url)` | 下载文件 |

+ 30 - 24
agent/skill/skills/core.md

@@ -73,7 +73,7 @@ goal(abandon="方案A需要Redis,环境没有")
 
 ## 信息调研
 
-你可以通过联网搜索工具`search_posts`大概浏览来自Github、小红书、微信公众号、知乎等渠道的信息,并再使用`select_post`工具查看具体信息
+你可以通过 `content_search(platform, keyword)` 搜索来自 GitHub、小红书、微信公众号、知乎、YouTube、X 等平台的信息,再用 `content_detail(platform, index)` 查看完整内容。不确定平台参数时先调 `content_platforms()` 查看
 对于需要深度交互的网页内容,使用浏览器工具进行操作。
 
 调研过程可能需要多次搜索,比如基于搜索结果中获得的启发或信息启动新的搜索,直到得到令人满意的答案。你可以使用`goal`工具管理搜索的过程,或者使用文档记录搜索的中间或最终结果。
@@ -84,32 +84,38 @@ goal(abandon="方案A需要Redis,环境没有")
 
 ### 基本工作流程
 
-1. **页面导航**: 使用 `browser_navigate_to_url` 或 `browser_search_web` 到达目标页面
+1. **页面导航**: 使用 `browser_navigate(url)` 或 `browser_search(query)` 到达目标页面
 2. **等待加载**: 页面跳转后调用 `browser_wait(seconds=2)` 等待内容加载
-3. **获取元素索引**: 调用 `browser_get_visual_selector_map` 获取可交互元素的索引映射和当前界面的截图
-4. **执行交互**: 使用 `browser_click_element`、`browser_input_text` 等工具操作页面
-5. **提取内容**: 使用 `browser_extract_content`, `browser_read_long_content`, `browser_get_page_html` 获取数据
+3. **获取元素索引**: 调用 `browser_screenshot(highlight_elements=True)` 获取带编号标注的截图 + 元素列表
+4. **执行交互**: 使用 `browser_interact(action, index, ...)` 操作页面(click / type / send_keys / upload / dropdown)
+5. **提取内容**: 使用 `browser_extract(query)` 让 LLM 提取结构化数据,或 `browser_read(mode="long")` 分页读取长内容
 
 ### 关键原则
 
 - **禁止模拟结果**:不要输出你认为的搜索结果,而是要调用工具获取真实结果
-- **必须先获取索引**: 所有 `index` 参数都需要先通过 `browser_get_selector_map` 获取
-- **高级工具**:优先使用`browser_extract_content`, `browser_read_long_content`等工具获取数据,而不是使用`browser_get_selector_map`获取索引后手动解析
-- **操作后等待**: 任何可能触发页面变化的操作(点击、输入、滚动)后都要调用 `browser_wait`
+- **必须先获取索引**: 所有 `index` 参数都需要先通过 `browser_screenshot(highlight_elements=True)` 或 `browser_elements()` 获取
+- **高级工具**:优先使用 `browser_extract` / `browser_read` 获取数据,而不是手动解析元素
+- **操作后等待**: 任何可能触发页面变化的操作后都要调用 `browser_wait`
 - **登录处理**:
-  - **正常登录**:当遇到需要登录的网页时,使用`browser_load_cookies`来登录
-  - **首次登录**:当没有该网站的cookie时,需要请求人类协助登录:
-    1. 调用 `browser_get_live_url` 获取云浏览器实时画面链接
-    2. 导航到目标网站的登录页面
-    3. 通过 `feishu_send_message_to_contact` 将 live URL 发送给相关人员,请求其在浏览器中完成登录
-    4. 使用 `feishu_get_contact_replies(contact_name="...", wait_time_seconds=300)` 等待对方回复确认登录完成
-    5. 收到回复后使用 `browser_export_cookies` 将登录态保存下来
-- **复杂操作用JS**: 当标准工具无法满足时,使用 `browser_evaluate` 执行JavaScript代码
-
-### 工具分类
-
-**导航**: browser_get_live_url, browser_navigate_to_url, browser_search_web, browser_go_back, browser_wait
-**交互**: browser_click_element, browser_input_text, browser_send_keys, browser_upload_file
-**视图**: browser_scroll_page, browser_find_text, browser_screenshot
-**提取**: browser_extract_content, browser_read_long_content, browser_get_page_html, browser_get_selector_map, browser_get_visual_selector_map
-**高级**: browser_evaluate, browser_load_cookies, browser_export_cookies, browser_wait_for_user_action, browser_download_direct_url
+  - **正常登录**:使用 `browser_cookies(action="load", url=...)` 注入已保存的 cookie
+  - **首次登录**:需要人类协助——导航到登录页,通过飞书发送链接,等待确认后 `browser_cookies(action="export")` 保存
+- **复杂操作用JS**: 当标准工具无法满足时,使用 `browser_js(code)` 执行 JavaScript
+
+### 工具一览
+
+| 工具 | 功能 |
+|------|------|
+| `browser_navigate(url)` | 导航到 URL |
+| `browser_search(query, engine)` | 搜索引擎搜索 |
+| `browser_back()` | 返回上一页 |
+| `browser_interact(action, ...)` | 元素交互(click/type/send_keys/upload/dropdown) |
+| `browser_scroll(down, pages)` | 滚动页面 |
+| `browser_screenshot(highlight)` | 截图(highlight=True 带元素编号标注) |
+| `browser_elements()` | 获取可交互元素列表(纯文本) |
+| `browser_read(mode)` | 读取页面(html/find/long) |
+| `browser_extract(query)` | LLM 驱动的结构化数据提取 |
+| `browser_tabs(action, tab_id)` | 标签页管理(switch/close) |
+| `browser_cookies(action, ...)` | Cookie/登录态管理(load/export/ensure_login) |
+| `browser_wait(seconds/user_message)` | 等待(定时 or 等用户操作) |
+| `browser_js(code)` | 执行 JavaScript |
+| `browser_download(url)` | 下载文件 |

+ 5 - 5
agent/skill/skills/research.md

@@ -5,7 +5,7 @@ description: 知识调研 - 根据目标和任务自动执行搜索,返回结
 
 ## 信息调研
 
-你可以通过联网搜索工具`search_posts`大概浏览来自Github、小红书、微信公众号、知乎等渠道的信息,并再使用`select_post`工具查看具体信息
+你可以通过 `content_search` 工具搜索来自 GitHub、小红书、微信公众号、知乎、YouTube、X 等平台的信息,并使用 `content_detail` 查看具体内容。如不确定平台参数,先调 `content_platforms` 查看
 
 ## 调研过程可能需要多次搜索,比如基于搜索结果中获得的启发或信息启动新的搜索,直到得到令人满意的答案。你可以使用 `goal` 工具管理搜索的过程,或者使用文档记录搜索的中间或最终结果。(可以着重参考browser的工具来辅助搜索)
 
@@ -26,10 +26,10 @@ goal(add="搜索工具, 搜索案例, 搜索方法论")
 
 **Step 2: 多维度搜索**
 
-- 搜索工具:`search_posts(query="PDF table extraction tool")`
-- 搜索案例:`search_posts(query="PDF table extraction usercase site:github.com")`
-- 搜索定义:`search_posts(query="PDF table extraction definition")`
-- 搜索方法:`search_posts(query="PDF table extraction best practice")`
+- 搜索工具:`content_search(platform="github", keyword="PDF table extraction tool")`
+- 搜索案例:`content_search(platform="xhs", keyword="PDF表格提取")`
+- 搜索定义:`content_search(platform="zhihu", keyword="PDF table extraction")`
+- 搜索方法:`content_search(platform="gzh", keyword="PDF table extraction best practice")`
 
 **Step 3: 结构化记录**
 每发现一条有价值的信息,立即保存为结构化知识:

+ 12 - 11
agent/tools/builtin/__init__.py

@@ -16,14 +16,16 @@ from agent.tools.builtin.file.grep import grep_content
 from agent.tools.builtin.bash import bash_command
 from agent.tools.builtin.skill import skill, list_skills
 from agent.tools.builtin.subagent import agent, evaluate
-from agent.tools.builtin.search import search_posts, get_search_suggestions
-# sandbox 工具已废弃(2026-04):原本是给"运行工具"场景准备的,但工具已被抽到 toolhub 单独处理
+# sandbox 工具已废弃(2026-04);search.py / crawler.py 已重构为 content/ 工具族(2026-04)
 from agent.tools.builtin.knowledge import(knowledge_search,knowledge_save,knowledge_list,knowledge_update,knowledge_batch_update,knowledge_slim)
 from agent.tools.builtin.librarian import ask_knowledge, upload_knowledge
 from agent.tools.builtin.context import get_current_context
 from agent.tools.builtin.toolhub import toolhub_health, toolhub_search, toolhub_call
 from agent.tools.builtin.resource import resource_list_tools, resource_get_tool
-from agent.tools.builtin.crawler import youtube_search, youtube_detail, x_search, import_content, extract_video_clip
+from agent.tools.builtin.content import (
+    content_platforms, content_search, content_detail, content_suggest,
+    extract_video_clip, import_content,
+)
 from agent.trace.goal_tool import goal
 # 导入浏览器工具以触发注册
 import agent.tools.builtin.browser  # noqa: F401
@@ -55,9 +57,11 @@ __all__ = [
     "list_skills",
     "agent",
     "evaluate",
-    "search_posts",
-    "select_post",
-    "get_search_suggestions",
+    # 内容工具族(重构自 search.py + crawler.py)
+    "content_platforms",
+    "content_search",
+    "content_detail",
+    "content_suggest",
     # 上下文工具
     "get_current_context",
     # ToolHub 远程工具库
@@ -68,12 +72,9 @@ __all__ = [
     # 资源查询
     "resource_list_tools",
     "resource_get_tool",
-    # 爬虫工具
-    "youtube_search",
-    "youtube_detail",
-    "x_search",
-    "import_content",
+    # 媒体 / 导入
     "extract_video_clip",
+    "import_content",
     # Goal 管理
     "goal",
 ]

+ 1 - 1
agent/tools/builtin/bash.py

@@ -158,7 +158,7 @@ def _kill_process_tree(pid: int) -> None:
         pass
 
 
-@tool(description="执行 bash 命令", hidden_params=["context"])
+@tool(description="执行 bash 命令", hidden_params=["context"], groups=["core"])
 async def bash_command(
     command: str,
     timeout: Optional[int] = None,

+ 30 - 93
agent/tools/builtin/browser/__init__.py

@@ -2,63 +2,32 @@
 浏览器工具 - Browser-Use 原生工具适配器
 
 基于 browser-use 实现的浏览器自动化工具集。
+28 个原始工具已合并为 14 个语义化入口(2026-04 重构)。
 """
 
 from agent.tools.builtin.browser.baseClass import (
-    # 会话管理
+    # 会话管理(非 @tool,供框架内部调用)
     init_browser_session,
     get_browser_session,
     get_browser_live_url,
     cleanup_browser_session,
     kill_browser_session,
 
-    # 导航类工具
-    browser_get_live_url,
-    browser_navigate_to_url,
-    browser_search_web,
-    browser_go_back,
-    browser_wait,
-
-    # 元素交互工具
-    browser_click_element,
-    browser_input_text,
-    browser_send_keys,
-    browser_upload_file,
-
-    # 滚动和视图工具
-    browser_scroll_page,
-    browser_find_text,
+    # 14 个 @tool 入口
+    browser_navigate,
+    browser_search,
+    browser_back,
+    browser_interact,
+    browser_scroll,
     browser_screenshot,
-
-    # 标签页管理工具
-    browser_switch_tab,
-    browser_close_tab,
-
-    # 下拉框工具
-    browser_get_dropdown_options,
-    browser_select_dropdown_option,
-
-    # 内容提取工具
-    browser_extract_content,
-    browser_read_long_content,
-    browser_get_page_html,
-    browser_download_direct_url,
-    browser_get_selector_map,
-    browser_get_visual_selector_map,
-
-    # JavaScript 执行工具
-    browser_evaluate,
-    browser_ensure_login_with_cookies,
-
-    # 等待用户操作
-    browser_wait_for_user_action,
-
-    # 任务完成
-    browser_done,
-
-    # Cookie 持久化
-    browser_export_cookies,
-    browser_load_cookies,
+    browser_elements,
+    browser_read,
+    browser_extract,
+    browser_tabs,
+    browser_cookies,
+    browser_wait,
+    browser_js,
+    browser_download,
 )
 
 __all__ = [
@@ -69,51 +38,19 @@ __all__ = [
     'cleanup_browser_session',
     'kill_browser_session',
 
-    # 导航类工具
-    'browser_get_live_url',
-    'browser_navigate_to_url',
-    'browser_search_web',
-    'browser_go_back',
-    'browser_wait',
-
-    # 元素交互工具
-    'browser_click_element',
-    'browser_input_text',
-    'browser_send_keys',
-    'browser_upload_file',
-
-    # 滚动和视图工具
-    'browser_scroll_page',
-    'browser_find_text',
+    # @tool 入口
+    'browser_navigate',
+    'browser_search',
+    'browser_back',
+    'browser_interact',
+    'browser_scroll',
     'browser_screenshot',
-
-    # 标签页管理工具
-    'browser_switch_tab',
-    'browser_close_tab',
-
-    # 下拉框工具
-    'browser_get_dropdown_options',
-    'browser_select_dropdown_option',
-
-    # 内容提取工具
-    'browser_extract_content',
-    'browser_read_long_content',
-    'browser_download_direct_url',
-    'browser_get_page_html',
-    'browser_get_selector_map',
-    'browser_get_visual_selector_map',
-
-    # JavaScript 执行工具
-    'browser_evaluate',
-    'browser_ensure_login_with_cookies',
-
-    # 等待用户操作
-    'browser_wait_for_user_action',
-
-    # 任务完成
-    'browser_done',
-
-    # Cookie 持久化
-    'browser_export_cookies',
-    'browser_load_cookies',
+    'browser_elements',
+    'browser_read',
+    'browser_extract',
+    'browser_tabs',
+    'browser_cookies',
+    'browser_wait',
+    'browser_js',
+    'browser_download',
 ]

+ 334 - 77
agent/tools/builtin/browser/baseClass.py

@@ -52,7 +52,7 @@ import aiohttp
 import re
 import base64
 from urllib.parse import urlparse, parse_qs, unquote
-from typing import Optional, List, Dict, Any, Tuple, Union
+from typing import Literal, Optional, List, Dict, Any, Tuple, Union
 from pathlib import Path
 from langchain_core.runnables import RunnableLambda
 from argparse import Namespace # 使用 Namespace 快速构造带属性的对象
@@ -548,7 +548,6 @@ def _fetch_profile_id(cookie_type: str) -> Optional[str]:
 # 导航类工具 (Navigation Tools)
 # ============================================================
 
-@tool()
 async def browser_get_live_url() -> ToolResult:
     """
     获取云浏览器的实时画面链接(Live URL),可用于在本地浏览器中查看或分享给他人操作。
@@ -567,7 +566,6 @@ async def browser_get_live_url() -> ToolResult:
     )
 
 
-@tool()
 async def browser_navigate_to_url(url: str, new_tab: bool = False) -> ToolResult:
     """
     导航到指定的 URL
@@ -607,7 +605,6 @@ async def browser_navigate_to_url(url: str, new_tab: bool = False) -> ToolResult
         )
 
 
-@tool()
 async def browser_search_web(query: str, engine: str = "bing") -> ToolResult:
     """
     使用搜索引擎搜索
@@ -644,7 +641,6 @@ async def browser_search_web(query: str, engine: str = "bing") -> ToolResult:
         )
 
 
-@tool()
 async def browser_go_back() -> ToolResult:
     """
     返回到上一个页面
@@ -671,10 +667,9 @@ async def browser_go_back() -> ToolResult:
         )
 
 
-@tool()
-async def browser_wait(seconds: int = 3) -> ToolResult:
+async def browser_wait_impl(seconds: int = 3) -> ToolResult:
     """
-    等待指定的秒数
+    等待指定的秒数(内部实现)
     Wait for a specified number of seconds
 
     用于等待页面加载、动画完成或其他异步操作。
@@ -732,7 +727,6 @@ class DownloadLinkCaptureHandler(logging.Handler):
                     self.captured_url = url
                     # print(f"🎯 成功锁定完整直链: {url[:50]}...") # 调试用
 
-@tool()
 async def browser_download_direct_url(url: str, save_name: str = "book.epub") -> ToolResult:
     save_dir = Path.cwd() / ".cache/.browser_use_files"
     save_dir.mkdir(parents=True, exist_ok=True)
@@ -797,7 +791,6 @@ async def browser_download_direct_url(url: str, save_name: str = "book.epub") ->
             long_term_memory=f"下载任务由于异常中断: {str(e)}"
         )
     
-@tool()
 async def browser_click_element(index: int) -> ToolResult:
     """
     点击页面元素,并自动通过拦截内部日志获取下载直链。
@@ -847,7 +840,6 @@ async def browser_click_element(index: int) -> ToolResult:
         logger.removeHandler(capture_handler)
 
 
-@tool()
 async def browser_input_text(index: int, text: str, clear: bool = True) -> ToolResult:
     """
     在指定元素中输入文本
@@ -885,7 +877,6 @@ async def browser_input_text(index: int, text: str, clear: bool = True) -> ToolR
         )
 
 
-@tool()
 async def browser_send_keys(keys: str) -> ToolResult:
     """
     发送键盘按键或快捷键
@@ -925,7 +916,6 @@ async def browser_send_keys(keys: str) -> ToolResult:
         )
 
 
-@tool()
 async def browser_upload_file(index: int, path: str) -> ToolResult:
     """
     上传文件到文件输入元素
@@ -968,7 +958,6 @@ async def browser_upload_file(index: int, path: str) -> ToolResult:
 # ============================================================
 # 滚动和视图工具 (Scroll & View Tools)
 # ============================================================
-@tool()
 async def browser_scroll_page(down: bool = True, pages: float = 1.0, index: Optional[int] = None) -> ToolResult:
     try:
         # 限制单次滚动幅度,避免 agent 一次滚 100 页
@@ -1028,7 +1017,6 @@ async def browser_scroll_page(down: bool = True, pages: float = 1.0, index: Opti
 
 
 
-@tool()
 async def browser_find_text(text: str) -> ToolResult:
     """
     查找页面中的文本并滚动到该位置
@@ -1063,7 +1051,6 @@ async def browser_find_text(text: str) -> ToolResult:
             long_term_memory=f"查找文本 '{text}' 失败"
         )
 
-@tool()
 async def browser_get_visual_selector_map() -> ToolResult:
     """
     获取当前页面的视觉快照和交互元素索引映射。
@@ -1160,10 +1147,9 @@ async def browser_get_visual_selector_map() -> ToolResult:
             long_term_memory="获取视觉元素映射失败"
         )
     
-@tool()
-async def browser_screenshot() -> ToolResult:
+async def browser_screenshot_impl() -> ToolResult:
     """
-    请求在下次观察中包含页面截图
+    请求在下次观察中包含页面截图(内部实现)
     Request a screenshot to be included in the next observation
 
     用于视觉检查页面状态,帮助理解页面布局和内容。
@@ -1197,7 +1183,6 @@ async def browser_screenshot() -> ToolResult:
 # 标签页管理工具 (Tab Management Tools)
 # ============================================================
 
-@tool()
 async def browser_switch_tab(tab_id: str) -> ToolResult:
     """
     切换到指定标签页
@@ -1232,7 +1217,6 @@ async def browser_switch_tab(tab_id: str) -> ToolResult:
         )
 
 
-@tool()
 async def browser_close_tab(tab_id: str) -> ToolResult:
     """
     关闭指定标签页
@@ -1271,7 +1255,6 @@ async def browser_close_tab(tab_id: str) -> ToolResult:
 # 下拉框工具 (Dropdown Tools)
 # ============================================================
 
-@tool()
 async def browser_get_dropdown_options(index: int) -> ToolResult:
     """
     获取下拉框的所有选项
@@ -1305,7 +1288,6 @@ async def browser_get_dropdown_options(index: int) -> ToolResult:
         )
 
 
-@tool()
 async def browser_select_dropdown_option(index: int, text: str) -> ToolResult:
     """
     选择下拉框选项
@@ -1408,7 +1390,6 @@ async def extraction_adapter(input_data):
     from argparse import Namespace
     return Namespace(completion=content)
 
-@tool()
 async def browser_extract_content(query: str, extract_links: bool = False,
                          start_from_char: int = 0) -> ToolResult:
     """
@@ -1550,7 +1531,6 @@ async def _detect_and_download_pdf_via_cdp(browser) -> Optional[str]:
         return None
 
 
-@tool()
 async def browser_read_long_content(
     goal: Union[str, dict],
     source: str = "page",
@@ -1609,7 +1589,6 @@ async def browser_read_long_content(
             error=f"Read long content failed: {str(e)}",
             long_term_memory="参数解析或校验失败,请检查输入"
         )
-@tool()
 async def browser_get_page_html() -> ToolResult:
     """
     获取当前页面的完整 HTML
@@ -1671,7 +1650,6 @@ async def browser_get_page_html() -> ToolResult:
         )
 
 
-@tool()
 async def browser_get_selector_map() -> ToolResult:
     """
     获取当前页面的元素索引映射
@@ -1743,7 +1721,6 @@ async def browser_get_selector_map() -> ToolResult:
 # JavaScript 执行工具 (JavaScript Tools)
 # ============================================================
 
-@tool()
 async def browser_evaluate(code: str) -> ToolResult:
     """
     在页面中执行 JavaScript 代码
@@ -1785,7 +1762,6 @@ async def browser_evaluate(code: str) -> ToolResult:
         )
 
 
-@tool()
 async def browser_ensure_login_with_cookies(cookie_type: str, url: str = "https://www.xiaohongshu.com") -> ToolResult:
     """
     检查登录状态并在需要时注入 cookies
@@ -1881,7 +1857,6 @@ async def browser_ensure_login_with_cookies(cookie_type: str, url: str = "https:
 # 等待用户操作工具 (Wait for User Action)
 # ============================================================
 
-@tool()
 async def browser_wait_for_user_action(message: str = "Please complete the action in browser",
                                timeout: int = 300) -> ToolResult:
     """
@@ -1953,7 +1928,6 @@ async def browser_wait_for_user_action(message: str = "Please complete the actio
 # 任务完成工具 (Task Completion)
 # ============================================================
 
-@tool()
 async def browser_done(text: str, success: bool = True,
               files_to_display: Optional[List[str]] = None) -> ToolResult:
     """
@@ -1998,7 +1972,6 @@ async def browser_done(text: str, success: bool = True,
 
 _COOKIES_DIR = Path(__file__).parent.parent.parent.parent.parent / ".cache/.cookies"
 
-@tool()
 async def browser_export_cookies(name: str = "", account: str = "") -> ToolResult:
     """
     导出当前浏览器的所有 Cookie 到本地 .cookies/ 目录。
@@ -2042,7 +2015,6 @@ async def browser_export_cookies(name: str = "", account: str = "") -> ToolResul
         return ToolResult(title="Cookie 导出失败", output="", error=str(e), long_term_memory="导出 Cookie 失败")
 
 
-@tool()
 async def browser_load_cookies(url: str, name: str = "", auto_navigate: bool = True) -> ToolResult:
     """
     根据目标 URL 自动查找本地 Cookie 文件,注入浏览器并导航到目标页面恢复登录态。
@@ -2178,60 +2150,345 @@ async def browser_load_cookies(url: str, name: str = "", auto_navigate: bool = T
 
 
 # ============================================================
-# 导出所有工具函数(供外部使用
+# 新版统一入口(13 个 @tool,替代原来 28 个
 # ============================================================
 
-__all__ = [
-    # 会话管理
-    'init_browser_session',
-    'get_browser_session',
-    'cleanup_browser_session',
-    'kill_browser_session',
 
-    # 导航类工具
-    'browser_navigate_to_url',
-    'browser_search_web',
-    'browser_go_back',
-    'browser_wait',
+@tool(groups=["browser"])
+async def browser_navigate(url: str, new_tab: bool = False) -> ToolResult:
+    """
+    导航到指定 URL。
 
-    # 元素交互工具
-    'browser_click_element',
-    'browser_input_text',
-    'browser_send_keys',
-    'browser_upload_file',
+    Args:
+        url: 目标 URL
+        new_tab: 是否在新标签页打开(默认 False)
+    """
+    return await browser_navigate_to_url(url=url, new_tab=new_tab)
 
-    # 滚动和视图工具
-    'browser_scroll_page',
-    'browser_find_text',
-    'browser_screenshot',
 
-    # 标签页管理工具
-    'browser_switch_tab',
-    'browser_close_tab',
+@tool(groups=["browser"])
+async def browser_search(query: str, engine: str = "bing") -> ToolResult:
+    """
+    使用搜索引擎搜索。
+
+    Args:
+        query: 搜索关键词
+        engine: 搜索引擎,可选 google / bing / duckduckgo,默认 bing
+    """
+    return await browser_search_web(query=query, engine=engine)
+
+
+@tool(groups=["browser"])
+async def browser_back() -> ToolResult:
+    """返回上一页。"""
+    return await browser_go_back()
+
+
+@tool(groups=["browser"])
+async def browser_interact(
+    action: Literal["click", "type", "send_keys", "upload", "dropdown_list", "dropdown_select"],
+    index: Optional[int] = None,
+    text: Optional[str] = None,
+    path: Optional[str] = None,
+    keys: Optional[str] = None,
+    clear: bool = True,
+) -> ToolResult:
+    """
+    与页面元素交互。根据 action 选择具体操作:
+
+    - click: 点击元素。需要 index。
+    - type: 在输入框输入文本。需要 index + text。clear 控制是否先清空。
+    - send_keys: 发送键盘按键(如 Enter、Control+A)。需要 keys,不需要 index。
+    - upload: 上传文件到文件输入框。需要 index + path(绝对路径)。
+    - dropdown_list: 列出下拉框选项。需要 index。
+    - dropdown_select: 选择下拉框选项。需要 index + text(选项文本)。
+
+    Args:
+        action: 交互类型
+        index: 元素索引(从 browser_elements 或 browser_screenshot(highlight=True) 获取)
+        text: 输入文本 / 下拉框选项文本
+        path: 上传文件的绝对路径
+        keys: 键盘按键字符串(如 "Enter"、"Control+A")
+        clear: type 时是否先清空(默认 True)
+    """
+    if action == "click":
+        if index is None:
+            return ToolResult(title="参数错误", output="", error="click 需要 index 参数")
+        return await browser_click_element(index=index)
+
+    elif action == "type":
+        if index is None or text is None:
+            return ToolResult(title="参数错误", output="", error="type 需要 index 和 text 参数")
+        return await browser_input_text(index=index, text=text, clear=clear)
+
+    elif action == "send_keys":
+        if keys is None:
+            return ToolResult(title="参数错误", output="", error="send_keys 需要 keys 参数")
+        return await browser_send_keys(keys=keys)
+
+    elif action == "upload":
+        if index is None or path is None:
+            return ToolResult(title="参数错误", output="", error="upload 需要 index 和 path 参数")
+        return await browser_upload_file(index=index, path=path)
+
+    elif action == "dropdown_list":
+        if index is None:
+            return ToolResult(title="参数错误", output="", error="dropdown_list 需要 index 参数")
+        return await browser_get_dropdown_options(index=index)
+
+    elif action == "dropdown_select":
+        if index is None or text is None:
+            return ToolResult(title="参数错误", output="", error="dropdown_select 需要 index 和 text 参数")
+        return await browser_select_dropdown_option(index=index, text=text)
+
+    else:
+        return ToolResult(title="未知 action", output="", error=f"不支持的 action: {action}")
+
+
+@tool(groups=["browser"])
+async def browser_scroll(
+    down: bool = True,
+    pages: float = 1.0,
+    into_view_index: Optional[int] = None,
+) -> ToolResult:
+    """
+    滚动页面。
+
+    Args:
+        down: True 向下滚动,False 向上(默认 True)
+        pages: 滚动的页面数(默认 1.0)
+        into_view_index: 传入元素索引则滚动到该元素可见(忽略 down 和 pages)
+    """
+    return await browser_scroll_page(down=down, pages=pages, index=into_view_index)
+
+
+@tool(groups=["browser"])
+async def browser_screenshot(highlight_elements: bool = False) -> ToolResult:
+    """
+    截取当前页面。
+
+    Args:
+        highlight_elements: False 返回纯截图;True 返回带交互元素编号标注的截图
+                           + 元素列表(原 visual_selector_map 功能)
+    """
+    if highlight_elements:
+        return await browser_get_visual_selector_map()
+    else:
+        return await browser_screenshot_impl()
+
+
+@tool(groups=["browser"])
+async def browser_elements() -> ToolResult:
+    """
+    获取当前页面的可交互元素列表(纯文本,不截图)。
+    返回的 index 用于 browser_interact / browser_scroll 等操作。
+    """
+    return await browser_get_selector_map()
+
+
+@tool(groups=["browser"])
+async def browser_read(
+    mode: Literal["html", "find", "long"],
+    query: Optional[str] = None,
+    source: str = "page",
+    context: str = "",
+) -> ToolResult:
+    """
+    读取页面内容,三种模式:
+
+    - html: 获取当前页面的 HTML 源码(大页面会截断到 10000 字符)
+    - find: 在页面中查找文本。需要 query。
+    - long: 智能分页读取长内容(支持自动检测 PDF)。query 描述阅读目标。
+
+    Args:
+        mode: 读取模式
+        query: find 模式下的查找文本;long 模式下的阅读目标描述
+        source: long 模式的内容来源("page" 或文件路径),默认 "page"
+        context: long 模式的业务背景(可选)
+    """
+    if mode == "html":
+        return await browser_get_page_html()
+
+    elif mode == "find":
+        if not query:
+            return ToolResult(title="参数错误", output="", error="find 模式需要 query 参数")
+        return await browser_find_text(text=query)
+
+    elif mode == "long":
+        return await browser_read_long_content(
+            goal=query or "阅读页面内容",
+            source=source,
+            context=context,
+        )
+
+    else:
+        return ToolResult(title="未知 mode", output="", error=f"不支持的 mode: {mode}")
+
+
+@tool(groups=["browser"])
+async def browser_extract(
+    query: str,
+    extract_links: bool = False,
+    start_from_char: int = 0,
+) -> ToolResult:
+    """
+    使用 LLM 从当前页面提取结构化数据。
+
+    与 browser_read 不同,此工具会调用 LLM 分析页面内容并返回结构化结果。
+    适合"提取所有产品价格"、"总结文章要点"等需要理解语义的场景。
+
+    Args:
+        query: 提取指令(如"提取页面上所有产品名称和价格")
+        extract_links: 是否同时提取链接(默认 False)
+        start_from_char: 从第几个字符开始提取(用于分页处理大内容)
+    """
+    return await browser_extract_content(
+        query=query,
+        extract_links=extract_links,
+        start_from_char=start_from_char,
+    )
+
+
+@tool(groups=["browser"])
+async def browser_tabs(
+    action: Literal["switch", "close"],
+    tab_id: str = "",
+) -> ToolResult:
+    """
+    管理浏览器标签页。
+
+    Args:
+        action: "switch" 切换到指定标签页;"close" 关闭指定标签页
+        tab_id: 标签页 ID(4 字符)
+    """
+    if not tab_id:
+        return ToolResult(title="参数错误", output="", error="需要 tab_id 参数")
+
+    if action == "switch":
+        return await browser_switch_tab(tab_id=tab_id)
+    elif action == "close":
+        return await browser_close_tab(tab_id=tab_id)
+    else:
+        return ToolResult(title="未知 action", output="", error=f"不支持的 action: {action}")
+
+
+@tool(groups=["browser"])
+async def browser_cookies(
+    action: Literal["load", "export", "ensure_login"],
+    url: str = "",
+    name: str = "",
+    account: str = "",
+    cookie_type: str = "",
+    auto_navigate: bool = True,
+) -> ToolResult:
+    """
+    Cookie / 登录态管理:
+
+    - load: 从本地加载已保存的 cookie 并注入浏览器。需要 url(自动匹配 cookie 文件)。
+    - export: 导出当前浏览器 cookie 到本地。可选 name 和 account 标识。
+    - ensure_login: 检查登录状态,未登录时自动注入 cookie。需要 cookie_type 和 url。
+
+    Args:
+        action: 操作类型
+        url: 目标 URL(load / ensure_login 必填)
+        name: cookie 文件名(可选)
+        account: 账号名(export 时可选)
+        cookie_type: cookie 类型标识(ensure_login 必填)
+        auto_navigate: load 时找不到 cookie 是否自动导航到目标页面(默认 True)
+    """
+    if action == "load":
+        if not url:
+            return ToolResult(title="参数错误", output="", error="load 需要 url 参数")
+        return await browser_load_cookies(url=url, name=name, auto_navigate=auto_navigate)
+
+    elif action == "export":
+        return await browser_export_cookies(name=name, account=account)
+
+    elif action == "ensure_login":
+        if not cookie_type:
+            return ToolResult(title="参数错误", output="", error="ensure_login 需要 cookie_type 参数")
+        return await browser_ensure_login_with_cookies(
+            cookie_type=cookie_type,
+            url=url or "https://www.xiaohongshu.com",
+        )
+
+    else:
+        return ToolResult(title="未知 action", output="", error=f"不支持的 action: {action}")
 
-    # 下拉框工具
-    'browser_get_dropdown_options',
-    'browser_select_dropdown_option',
 
-    # 内容提取工具
-    'browser_extract_content',
-    'browser_get_page_html',
-    'browser_read_long_content',
-    'browser_download_direct_url',
-    'browser_get_selector_map',
-    'browser_get_visual_selector_map',
+@tool(groups=["browser"])
+async def browser_wait(
+    seconds: Optional[int] = None,
+    user_message: Optional[str] = None,
+    timeout: int = 300,
+) -> ToolResult:
+    """
+    等待。两种模式:
+
+    - 传 seconds: 纯等待指定秒数(默认 3 秒)
+    - 传 user_message: 暂停并提示用户在浏览器中完成操作(如登录、验证码),
+      用户完成后按回车继续。timeout 控制最长等待时间。
+    - 两者都不传: 默认等待 3 秒
+
+    Args:
+        seconds: 等待秒数
+        user_message: 用户操作提示消息
+        timeout: user_message 模式的最长等待(秒),默认 300
+    """
+    if user_message:
+        return await browser_wait_for_user_action(message=user_message, timeout=timeout)
+    else:
+        return await browser_wait_impl(seconds=seconds or 3)
+
+
+@tool(groups=["browser"])
+async def browser_js(code: str) -> ToolResult:
+    """
+    在当前页面执行 JavaScript 代码。
+
+    Args:
+        code: JavaScript 代码字符串。返回值会被自动序列化。
+    """
+    return await browser_evaluate(code=code)
+
 
-    # JavaScript 执行工具
-    'browser_evaluate',
-    'browser_ensure_login_with_cookies',
+@tool(groups=["browser"])
+async def browser_download(url: str, save_name: str = "") -> ToolResult:
+    """
+    下载指定 URL 的文件到本地。
+
+    Args:
+        url: 文件 URL
+        save_name: 保存文件名(可选,默认自动推断)
+    """
+    return await browser_download_direct_url(url=url, save_name=save_name or "download")
 
-    # 等待用户操作
-    'browser_wait_for_user_action',
 
-    # 任务完成
-    'browser_done',
+# ============================================================
+# 导出(供外部使用)
+# ============================================================
+
+__all__ = [
+    # 会话管理(非 @tool)
+    'init_browser_session',
+    'get_browser_session',
+    'get_browser_live_url',
+    'cleanup_browser_session',
+    'kill_browser_session',
 
-    # Cookie 持久化
-    'browser_export_cookies',
-    'browser_load_cookies',
+    # 13 个 @tool 入口
+    'browser_navigate',
+    'browser_search',
+    'browser_back',
+    'browser_interact',
+    'browser_scroll',
+    'browser_screenshot',
+    'browser_elements',
+    'browser_read',
+    'browser_extract',
+    'browser_tabs',
+    'browser_cookies',
+    'browser_wait',
+    'browser_js',
+    'browser_download',
 ]

+ 29 - 0
agent/tools/builtin/content/__init__.py

@@ -0,0 +1,29 @@
+"""
+内容工具族 —— 统一的跨平台内容搜索/详情/建议词 + 媒体处理 + 内容导入
+
+@tool 入口:
+  content_platforms  - 查看平台及参数
+  content_search     - 跨平台搜索
+  content_detail     - 查看详情
+  content_suggest    - 搜索建议词
+  extract_video_clip - YouTube 视频片段截取
+  import_content     - 内容批量导入 CMS
+"""
+
+from agent.tools.builtin.content.tools import (
+    content_platforms,
+    content_search,
+    content_detail,
+    content_suggest,
+)
+from agent.tools.builtin.content.media import extract_video_clip
+from agent.tools.builtin.content.ingestion import import_content
+
+__all__ = [
+    "content_platforms",
+    "content_search",
+    "content_detail",
+    "content_suggest",
+    "extract_video_clip",
+    "import_content",
+]

+ 86 - 0
agent/tools/builtin/content/cache.py

@@ -0,0 +1,86 @@
+"""
+内容搜索缓存(磁盘持久化)
+
+搜索结果按 trace_id 隔离,同一 Agent session 内的 CLI 多次调用也能复用。
+文件格式:/tmp/content_cache_{trace_id}.json
+"""
+
+import json
+import os
+import time
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+_CACHE_DIR = Path("/tmp")
+_CACHE_TTL = 3600  # 1 小时过期
+
+
+def _cache_path(trace_id: str) -> Path:
+    safe_id = trace_id.replace("/", "_").replace("..", "_")
+    return _CACHE_DIR / f"content_cache_{safe_id}.json"
+
+
+def _load_raw(trace_id: str) -> dict:
+    p = _cache_path(trace_id)
+    if not p.exists():
+        return {}
+    try:
+        data = json.loads(p.read_text("utf-8"))
+        # 检查过期
+        if time.time() - data.get("_ts", 0) > _CACHE_TTL:
+            p.unlink(missing_ok=True)
+            return {}
+        return data
+    except Exception:
+        return {}
+
+
+def _save_raw(trace_id: str, data: dict) -> None:
+    data["_ts"] = time.time()
+    try:
+        _cache_path(trace_id).write_text(
+            json.dumps(data, ensure_ascii=False), encoding="utf-8"
+        )
+    except Exception:
+        pass
+
+
+def save_search_results(
+    trace_id: str,
+    platform: str,
+    keyword: str,
+    posts: List[Dict[str, Any]],
+) -> None:
+    """保存搜索结果到磁盘缓存"""
+    data = _load_raw(trace_id)
+    # 每个 platform 只保留最近一次搜索
+    data[f"search:{platform}"] = {
+        "keyword": keyword,
+        "posts": posts,
+    }
+    _save_raw(trace_id, data)
+
+
+def get_cached_post(
+    trace_id: str,
+    platform: str,
+    index: int,
+) -> Optional[Dict[str, Any]]:
+    """按索引从缓存取一条完整记录(1-based)"""
+    data = _load_raw(trace_id)
+    entry = data.get(f"search:{platform}")
+    if not entry:
+        return None
+    posts = entry.get("posts", [])
+    if 1 <= index <= len(posts):
+        return posts[index - 1]
+    return None
+
+
+def get_cached_search_info(trace_id: str, platform: str) -> Optional[Dict[str, Any]]:
+    """获取缓存的搜索信息(keyword + 总条数),用于错误提示"""
+    data = _load_raw(trace_id)
+    entry = data.get(f"search:{platform}")
+    if not entry:
+        return None
+    return {"keyword": entry.get("keyword"), "total": len(entry.get("posts", []))}

+ 46 - 0
agent/tools/builtin/content/ingestion.py

@@ -0,0 +1,46 @@
+"""
+内容导入工具
+
+将文章链接批量导入 AIGC CMS 系统。
+"""
+
+import json
+from typing import Any, Dict, List
+
+import httpx
+
+from agent.tools import tool, ToolResult
+
+AIGC_BASE_URL = "http://aigc-channel.aiddit.com/aigc/channel"
+DEFAULT_TIMEOUT = 60.0
+
+
+@tool(groups=["content"])
+async def import_content(plan_name: str, content_data: List[Dict[str, Any]]) -> ToolResult:
+    """
+    导入长文内容到 CMS(微信公众号、小红书、抖音等通用链接)。
+
+    Args:
+        plan_name: 计划名称
+        content_data: 内容数据列表,每项包含 channel、content_link、title 等字段
+    """
+    try:
+        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
+            response = await client.post(
+                f"{AIGC_BASE_URL}/weixin/auto_insert",
+                json={"plan_name": plan_name, "data": content_data},
+            )
+            response.raise_for_status()
+            data = response.json()
+
+        if data.get("code") == 0:
+            result_data = data.get("data", {})
+            return ToolResult(
+                title=f"内容导入: {plan_name}",
+                output=json.dumps(result_data, ensure_ascii=False, indent=2),
+                long_term_memory=f"Imported {len(content_data)} items to plan '{plan_name}'",
+            )
+        return ToolResult(title="导入失败", output="", error=f"导入失败: {data.get('msg', '未知错误')}")
+
+    except Exception as e:
+        return ToolResult(title="内容导入异常", output="", error=str(e))

+ 114 - 0
agent/tools/builtin/content/media.py

@@ -0,0 +1,114 @@
+"""
+媒体处理工具
+
+- extract_video_clip: 从已下载的 YouTube 视频中截取片段
+- download_youtube_video / parse_srt_to_outline: 供 YouTube 详情调用的辅助函数
+"""
+
+import asyncio
+import json
+import subprocess
+import tempfile
+from pathlib import Path
+from typing import Dict, List, Optional
+
+from agent.tools import tool, ToolResult
+
+VIDEO_DOWNLOAD_DIR = Path(tempfile.gettempdir()) / "youtube_videos"
+VIDEO_DOWNLOAD_DIR.mkdir(exist_ok=True)
+
+
+# ── 辅助函数(供 platforms/youtube.py 调用) ──
+
+def download_youtube_video(video_id: str) -> Optional[str]:
+    """使用 yt-dlp 下载 YouTube 视频,返回文件路径"""
+    try:
+        output_path = VIDEO_DOWNLOAD_DIR / f"{video_id}.mp4"
+        if output_path.exists():
+            return str(output_path)
+
+        cmd = [
+            "yt-dlp",
+            "-f", "best[ext=mp4]",
+            "-o", str(output_path),
+            f"https://www.youtube.com/watch?v={video_id}",
+        ]
+        result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
+        if result.returncode == 0 and output_path.exists():
+            return str(output_path)
+        return None
+    except Exception:
+        return None
+
+
+def parse_srt_to_outline(srt_content: str) -> List[Dict[str, str]]:
+    """解析 SRT 字幕,生成带时间戳的大纲"""
+    if not srt_content:
+        return []
+    outline = []
+    blocks = srt_content.strip().split("\n\n")
+    for block in blocks:
+        lines = block.strip().split("\n")
+        if len(lines) >= 3:
+            timestamp_line = lines[1]
+            if "-->" in timestamp_line:
+                start_time = timestamp_line.split("-->")[0].strip()
+                text = " ".join(lines[2:])
+                outline.append({"timestamp": start_time, "text": text})
+    return outline
+
+
+# ── @tool ──
+
+@tool(groups=["content"])
+async def extract_video_clip(
+    video_id: str,
+    start_time: str,
+    end_time: str,
+    output_name: Optional[str] = None,
+) -> ToolResult:
+    """
+    从已下载的 YouTube 视频中截取指定时间段的片段。
+
+    必须先通过 content_detail(platform="youtube", index=..., extras={"download_video": true})
+    下载视频后才能使用。
+
+    Args:
+        video_id: YouTube 视频 ID
+        start_time: 开始时间,格式 HH:MM:SS 或 MM:SS
+        end_time: 结束时间,格式 HH:MM:SS 或 MM:SS
+        output_name: 输出文件名(可选,自动生成)
+    """
+    source_video = VIDEO_DOWNLOAD_DIR / f"{video_id}.mp4"
+    if not source_video.exists():
+        return ToolResult(
+            title="视频截取失败",
+            output="",
+            error="源视频不存在,请先使用 content_detail(platform='youtube', ..., extras={'download_video': true}) 下载",
+        )
+
+    if not output_name:
+        output_name = f"{video_id}_clip_{start_time.replace(':', '-')}_{end_time.replace(':', '-')}.mp4"
+    output_path = VIDEO_DOWNLOAD_DIR / output_name
+
+    cmd = ["ffmpeg", "-i", str(source_video), "-ss", start_time, "-to", end_time, "-c", "copy", "-y", str(output_path)]
+
+    try:
+        result = await asyncio.to_thread(subprocess.run, cmd, capture_output=True, text=True, timeout=60)
+    except subprocess.TimeoutExpired:
+        return ToolResult(title="视频截取超时", output="", error="ffmpeg 超时(60秒)")
+
+    if result.returncode == 0 and output_path.exists():
+        file_size = output_path.stat().st_size / (1024 * 1024)
+        return ToolResult(
+            title=f"视频片段: {start_time} - {end_time}",
+            output=json.dumps({
+                "video_id": video_id,
+                "clip_path": str(output_path),
+                "start_time": start_time,
+                "end_time": end_time,
+                "file_size_mb": round(file_size, 2),
+            }, ensure_ascii=False, indent=2),
+            long_term_memory=f"Extracted clip from {video_id}: {start_time}-{end_time}",
+        )
+    return ToolResult(title="视频截取失败", output="", error=f"ffmpeg 执行失败: {result.stderr}")

+ 1 - 0
agent/tools/builtin/content/platforms/__init__.py

@@ -0,0 +1 @@
+"""内容平台实现模块"""

+ 237 - 0
agent/tools/builtin/content/platforms/aigc_channel.py

@@ -0,0 +1,237 @@
+"""
+AIGC-Channel 平台实现(9 个中文平台)
+
+后端:aigc-channel.aiddit.com
+平台:xhs / gzh / sph / github / toutiao / douyin / bili / zhihu / weibo
+"""
+
+import json
+from typing import Any, Dict, List, Optional
+
+import httpx
+
+from agent.tools.models import ToolResult
+from agent.tools.utils.image import build_image_grid, encode_base64, load_images
+from agent.tools.builtin.content.registry import (
+    PlatformDef, ParamSpec, register_platform,
+)
+
+BASE_URL = "http://aigc-channel.aiddit.com/aigc/channel"
+DEFAULT_TIMEOUT = 60.0
+
+
+# ── 平台注册 ──
+
+_XHS_SEARCH_PARAMS = {
+    "sort_type": ParamSpec(
+        values=["综合排序", "最新发布", "最多点赞"],
+        default="综合排序",
+    ),
+    "publish_time": ParamSpec(
+        values=["不限", "近1天", "近7天", "近30天"],
+        default="不限",
+    ),
+    "content_type": ParamSpec(
+        values=["不限", "图文", "视频", "文章"],
+        default="不限",
+    ),
+    "filter_note_range": ParamSpec(
+        values=["不限", "1分钟以内", "1-5分钟", "5分钟以上"],
+        default="不限",
+        note="仅视频内容生效",
+    ),
+}
+
+_COMMON_CONTENT_TYPE = {
+    "content_type": ParamSpec(
+        values=["视频", "图文"],
+        default="",
+        note="留空不限",
+    ),
+}
+
+# 9 个中文平台定义
+_AIGC_PLATFORMS = [
+    PlatformDef(id="xhs",     name="小红书",   aliases=["RED", "xiaohongshu"], search_params=_XHS_SEARCH_PARAMS, supports_suggest=True),
+    PlatformDef(id="gzh",     name="公众号",   aliases=["微信公众号", "wechat"], search_params=_COMMON_CONTENT_TYPE),
+    PlatformDef(id="sph",     name="视频号",   aliases=["微信视频号"], search_params=_COMMON_CONTENT_TYPE),
+    PlatformDef(id="github",  name="GitHub",   aliases=["gh"], search_params=_COMMON_CONTENT_TYPE),
+    PlatformDef(id="toutiao", name="头条",     aliases=["今日头条", "toutiao"], search_params=_COMMON_CONTENT_TYPE, supports_suggest=True),
+    PlatformDef(id="douyin",  name="抖音",     aliases=["TikTok"], search_params=_COMMON_CONTENT_TYPE, supports_suggest=True),
+    PlatformDef(id="bili",    name="B站",      aliases=["哔哩哔哩", "bilibili"], search_params=_COMMON_CONTENT_TYPE, supports_suggest=True),
+    PlatformDef(id="zhihu",   name="知乎",     aliases=[], search_params=_COMMON_CONTENT_TYPE, supports_suggest=True),
+    PlatformDef(id="weibo",   name="微博",     aliases=["sina"], search_params=_COMMON_CONTENT_TYPE),
+]
+
+# suggest API 额外支持 wx(微信搜一搜),但它不是搜索平台
+_SUGGEST_ONLY_CHANNELS = {"wx": "微信"}
+
+
+# ── 搜索实现 ──
+
+async def search(
+    platform_id: str,
+    keyword: str,
+    max_count: int = 20,
+    cursor: str = "",
+    extras: Optional[Dict[str, Any]] = None,
+) -> ToolResult:
+    """AIGC-Channel 统一搜索"""
+    extras = extras or {}
+
+    if platform_id == "xhs":
+        payload = {
+            "type": platform_id,
+            "keyword": keyword,
+            "cursor": cursor,
+            "content_type": extras.get("content_type", "不限"),
+            "sort_type": extras.get("sort_type", "综合排序"),
+            "publish_time": extras.get("publish_time", "不限"),
+            "filter_note_range": extras.get("filter_note_range", "不限"),
+        }
+    else:
+        payload = {
+            "type": platform_id,
+            "keyword": keyword,
+            "cursor": cursor or "0",
+            "max_count": max_count,
+            "content_type": extras.get("content_type", ""),
+        }
+
+    try:
+        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
+            response = await client.post(
+                f"{BASE_URL}/data",
+                json=payload,
+                headers={"Content-Type": "application/json"},
+            )
+            response.raise_for_status()
+            data = response.json()
+    except httpx.HTTPStatusError as e:
+        return ToolResult(title="搜索失败", output="", error=f"HTTP {e.response.status_code}: {e.response.text}")
+    except Exception as e:
+        return ToolResult(title="搜索失败", output="", error=str(e))
+
+    posts = data.get("data", [])
+
+    # 构建概览摘要
+    summary_list = []
+    for idx, post in enumerate(posts, 1):
+        body = post.get("body_text", "") or ""
+        title = post.get("title") or body[:20] or ""
+        summary_list.append({
+            "index": idx,
+            "title": title,
+            "body_text": body[:100] + ("..." if len(body) > 100 else ""),
+            "like_count": post.get("like_count"),
+            "comment_count": post.get("comment_count"),
+            "channel": post.get("channel"),
+            "link": post.get("link"),
+            "content_type": post.get("content_type"),
+        })
+
+    # 封面拼图
+    images = []
+    try:
+        collage_b64 = await _build_collage(posts)
+        if collage_b64:
+            images.append({"type": "base64", "media_type": "image/png", "data": collage_b64})
+    except Exception:
+        pass
+
+    return ToolResult(
+        title=f"搜索: {keyword} ({platform_id})",
+        output=json.dumps({"data": summary_list}, ensure_ascii=False, indent=2),
+        long_term_memory=f"Searched '{keyword}' on {platform_id}, {len(posts)} results. Use content_detail to view full details.",
+        images=images,
+        metadata={"posts": posts},  # 完整数据传给上层缓存
+    )
+
+
+# ── 详情实现(从缓存获取,不需要额外 HTTP) ──
+
+async def detail(post: Dict[str, Any], extras: Optional[Dict[str, Any]] = None) -> ToolResult:
+    """返回单条帖子的完整内容"""
+    title = post.get("title") or post.get("body_text", "")[:30] or "无标题"
+
+    images = []
+    for img_url in post.get("images", []):
+        if img_url:
+            images.append({"type": "url", "url": img_url})
+
+    return ToolResult(
+        title=f"详情: {title}",
+        output=json.dumps(post, ensure_ascii=False, indent=2),
+        long_term_memory=f"Viewed detail: {title}",
+        images=images,
+    )
+
+
+# ── 建议词实现 ──
+
+async def suggest(channel: str, keyword: str) -> ToolResult:
+    """获取搜索建议词"""
+    try:
+        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
+            response = await client.post(
+                f"{BASE_URL}/suggest",
+                json={"type": channel, "keyword": keyword},
+                headers={"Content-Type": "application/json"},
+            )
+            response.raise_for_status()
+            data = response.json()
+    except Exception as e:
+        return ToolResult(title="建议词获取失败", output="", error=str(e))
+
+    suggestion_count = sum(len(item.get("list", [])) for item in data.get("data", []))
+    return ToolResult(
+        title=f"建议词: {keyword} ({channel})",
+        output=json.dumps(data, ensure_ascii=False, indent=2),
+        long_term_memory=f"Got {suggestion_count} suggestions for '{keyword}' on {channel}",
+    )
+
+
+# ── 拼图辅助 ──
+
+async def _build_collage(posts: List[Dict[str, Any]]) -> Optional[str]:
+    """封面图网格拼图"""
+    urls, titles = [], []
+    for post in posts:
+        imgs = post.get("images", [])
+        if imgs and imgs[0]:
+            urls.append(imgs[0])
+            titles.append(post.get("title", "") or "")
+
+    if not urls:
+        return None
+
+    loaded = await load_images(urls)
+    valid_images, valid_labels = [], []
+    for (_, img), title in zip(loaded, titles):
+        if img is not None:
+            valid_images.append(img)
+            valid_labels.append(title)
+
+    if not valid_images:
+        return None
+
+    grid = build_image_grid(images=valid_images, labels=valid_labels)
+    b64, _ = encode_base64(grid, format="PNG")
+    return b64
+
+
+# ── 注册所有 AIGC 平台 ──
+
+def _register_all():
+    for p in _AIGC_PLATFORMS:
+        p.search_impl = search
+        p.detail_impl = detail
+        if p.supports_suggest:
+            p.suggest_impl = suggest
+            p.suggest_channels = [p.id]
+        register_platform(p)
+
+    # wx 只有 suggest,没有搜索
+    # suggest 调用时 channel 传 "wx",但不注册为独立平台
+
+_register_all()

+ 129 - 0
agent/tools/builtin/content/platforms/x.py

@@ -0,0 +1,129 @@
+"""
+X (Twitter) 平台实现
+
+后端:crawler.aiddit.com/crawler/x
+"""
+
+import json
+from typing import Any, Dict, List, Optional
+
+import httpx
+
+from agent.tools.models import ToolResult
+from agent.tools.utils.image import build_image_grid, encode_base64, load_images
+from agent.tools.builtin.content.registry import PlatformDef, register_platform
+
+CRAWLER_URL = "http://crawler.aiddit.com/crawler/x/keyword"
+DEFAULT_TIMEOUT = 60.0
+
+
+async def search(
+    platform_id: str,
+    keyword: str,
+    max_count: int = 20,
+    cursor: str = "",
+    extras: Optional[Dict[str, Any]] = None,
+) -> ToolResult:
+    try:
+        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
+            response = await client.post(CRAWLER_URL, json={"keyword": keyword})
+            response.raise_for_status()
+            data = response.json()
+
+        if data.get("code") != 0:
+            return ToolResult(title="X 搜索失败", output="", error=data.get("msg", "未知错误"))
+
+        result_data = data.get("data", {})
+        tweets = result_data.get("data", []) if isinstance(result_data, dict) else []
+
+        summary_list = []
+        for idx, tweet in enumerate(tweets[:max_count], 1):
+            text = tweet.get("body_text", "")
+            summary_list.append({
+                "index": idx,
+                "author": tweet.get("channel_account_name", ""),
+                "body_text": text[:100] + ("..." if len(text) > 100 else ""),
+                "like_count": tweet.get("like_count"),
+                "comment_count": tweet.get("comment_count"),
+            })
+
+        # 拼图
+        images = []
+        collage_b64 = await _build_tweet_collage(tweets[:max_count])
+        if collage_b64:
+            images.append({"type": "base64", "media_type": "image/png", "data": collage_b64})
+
+        return ToolResult(
+            title=f"X: {keyword}",
+            output=json.dumps({"data": summary_list}, ensure_ascii=False, indent=2),
+            long_term_memory=f"Searched X for '{keyword}', {len(tweets)} results.",
+            images=images,
+            metadata={"posts": tweets[:max_count]},
+        )
+
+    except Exception as e:
+        return ToolResult(title="X 搜索异常", output="", error=str(e))
+
+
+async def detail(post: Dict[str, Any], extras: Optional[Dict[str, Any]] = None) -> ToolResult:
+    """X 的详情直接从缓存的搜索结果取完整数据"""
+    author = post.get("channel_account_name", "")
+    text = post.get("body_text", "")[:30]
+
+    all_images = []
+    for img_item in post.get("image_url_list", []):
+        url = img_item.get("image_url") if isinstance(img_item, dict) else img_item
+        if url:
+            all_images.append({"type": "url", "url": url})
+
+    return ToolResult(
+        title=f"X 详情: @{author}",
+        output=json.dumps(post, ensure_ascii=False, indent=2),
+        long_term_memory=f"Viewed X post by @{author}: {text}",
+        images=all_images,
+    )
+
+
+async def _build_tweet_collage(tweets: List[Dict[str, Any]]) -> Optional[str]:
+    urls, titles = [], []
+    for tweet in tweets:
+        thumb = None
+        for img_item in tweet.get("image_url_list", []):
+            url = img_item.get("image_url") if isinstance(img_item, dict) else img_item
+            if url:
+                thumb = url
+                break
+        if not thumb:
+            thumb = tweet.get("cover_url")
+        if thumb:
+            urls.append(thumb)
+            titles.append(f"@{tweet.get('channel_account_name', '')}")
+
+    if not urls:
+        return None
+
+    loaded = await load_images(urls)
+    valid_images, valid_labels = [], []
+    for (_, img), title in zip(loaded, titles):
+        if img is not None:
+            valid_images.append(img)
+            valid_labels.append(title)
+
+    if not valid_images:
+        return None
+
+    grid = build_image_grid(images=valid_images, labels=valid_labels)
+    b64, _ = encode_base64(grid, format="PNG")
+    return b64
+
+
+# ── 注册 ──
+
+_X = PlatformDef(
+    id="x",
+    name="X (Twitter)",
+    aliases=["twitter", "推特"],
+)
+_X.search_impl = search
+_X.detail_impl = detail
+register_platform(_X)

+ 203 - 0
agent/tools/builtin/content/platforms/youtube.py

@@ -0,0 +1,203 @@
+"""
+YouTube 平台实现
+
+后端:crawler.aiddit.com/crawler/youtube
+"""
+
+import json
+from typing import Any, Dict, List, Optional
+
+import httpx
+
+from agent.tools.models import ToolResult
+from agent.tools.utils.image import build_image_grid, encode_base64, load_images
+from agent.tools.builtin.content.registry import (
+    PlatformDef, ParamSpec, register_platform,
+)
+
+CRAWLER_BASE_URL = "http://crawler.aiddit.com/crawler"
+DEFAULT_TIMEOUT = 60.0
+
+
+# ── 搜索 ──
+
+async def search(
+    platform_id: str,
+    keyword: str,
+    max_count: int = 20,
+    cursor: str = "",
+    extras: Optional[Dict[str, Any]] = None,
+) -> ToolResult:
+    try:
+        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
+            response = await client.post(
+                f"{CRAWLER_BASE_URL}/youtube/keyword",
+                json={"keyword": keyword},
+            )
+            response.raise_for_status()
+            data = response.json()
+
+        if data.get("code") != 0:
+            return ToolResult(title="YouTube 搜索失败", output="", error=data.get("msg", "未知错误"))
+
+        result_data = data.get("data", {})
+        videos = result_data.get("data", []) if isinstance(result_data, dict) else []
+
+        # 概览
+        summary_list = []
+        for idx, video in enumerate(videos[:max_count], 1):
+            summary_list.append({
+                "index": idx,
+                "title": video.get("title", ""),
+                "author": video.get("author", ""),
+                "video_id": video.get("video_id", ""),
+            })
+
+        # 拼图
+        images = []
+        collage_b64 = await _build_video_collage(videos[:max_count])
+        if collage_b64:
+            images.append({"type": "base64", "media_type": "image/png", "data": collage_b64})
+
+        return ToolResult(
+            title=f"YouTube: {keyword}",
+            output=json.dumps({"data": summary_list}, ensure_ascii=False, indent=2),
+            long_term_memory=f"Searched YouTube for '{keyword}', {len(videos)} results.",
+            images=images,
+            metadata={"posts": videos[:max_count]},
+        )
+
+    except Exception as e:
+        return ToolResult(title="YouTube 搜索异常", output="", error=str(e))
+
+
+# ── 详情 ──
+
+async def detail(post: Dict[str, Any], extras: Optional[Dict[str, Any]] = None) -> ToolResult:
+    """
+    YouTube 详情:需要额外 HTTP 调用获取字幕/下载等。
+    post 来自搜索缓存,extras 支持 include_captions / download_video。
+    """
+    extras = extras or {}
+    content_id = post.get("video_id", "")
+    include_captions = extras.get("include_captions", True)
+    download_video = extras.get("download_video", False)
+
+    try:
+        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
+            resp = await client.post(
+                f"{CRAWLER_BASE_URL}/youtube/detail",
+                json={"content_id": content_id},
+            )
+            resp.raise_for_status()
+            detail_data = resp.json()
+
+        if detail_data.get("code") != 0:
+            return ToolResult(title="详情获取失败", output="", error=detail_data.get("msg", "未知错误"))
+
+        result_data = detail_data.get("data", {})
+        video_info = result_data.get("data", {}) if isinstance(result_data, dict) else {}
+
+        # 字幕
+        captions_text = None
+        if include_captions or download_video:
+            try:
+                async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
+                    cap_resp = await client.post(
+                        f"{CRAWLER_BASE_URL}/youtube/captions",
+                        json={"content_id": content_id},
+                    )
+                    cap_resp.raise_for_status()
+                    cap_data = cap_resp.json()
+                    if cap_data.get("code") == 0:
+                        inner = cap_data.get("data", {})
+                        if isinstance(inner, dict):
+                            inner2 = inner.get("data", {})
+                            if isinstance(inner2, dict):
+                                captions_text = inner2.get("content")
+            except Exception:
+                pass
+
+        # 下载
+        video_path = None
+        video_outline = None
+        if download_video:
+            import asyncio
+            from agent.tools.builtin.content.media import download_youtube_video, parse_srt_to_outline
+            video_path = await asyncio.to_thread(download_youtube_video, content_id)
+            if captions_text:
+                video_outline = parse_srt_to_outline(captions_text)
+
+        output_data = {
+            "video_id": content_id,
+            "title": video_info.get("title", ""),
+            "channel": video_info.get("channel_account_name", ""),
+            "description": video_info.get("body_text", ""),
+            "like_count": video_info.get("like_count"),
+            "comment_count": video_info.get("comment_count"),
+            "content_link": video_info.get("content_link", ""),
+            "captions": captions_text,
+        }
+        if download_video:
+            output_data["video_path"] = video_path
+            output_data["video_outline"] = video_outline
+
+        return ToolResult(
+            title=f"YouTube 详情: {video_info.get('title', content_id)}",
+            output=json.dumps(output_data, ensure_ascii=False, indent=2),
+            long_term_memory=f"YouTube detail for {content_id}" + (" with captions" if captions_text else ""),
+        )
+
+    except Exception as e:
+        return ToolResult(title="YouTube 详情异常", output="", error=str(e))
+
+
+# ── 拼图 ──
+
+async def _build_video_collage(videos: List[Dict[str, Any]]) -> Optional[str]:
+    urls, titles = [], []
+    for video in videos:
+        thumb = None
+        if "thumbnails" in video and isinstance(video["thumbnails"], list) and video["thumbnails"]:
+            thumb = video["thumbnails"][0].get("url")
+        elif "thumbnail" in video:
+            thumb = video.get("thumbnail")
+        elif "cover_url" in video:
+            thumb = video.get("cover_url")
+
+        if thumb:
+            urls.append(thumb)
+            titles.append(video.get("title", ""))
+
+    if not urls:
+        return None
+
+    loaded = await load_images(urls)
+    valid_images, valid_labels = [], []
+    for (_, img), title in zip(loaded, titles):
+        if img is not None:
+            valid_images.append(img)
+            valid_labels.append(title)
+
+    if not valid_images:
+        return None
+
+    grid = build_image_grid(images=valid_images, labels=valid_labels)
+    b64, _ = encode_base64(grid, format="PNG")
+    return b64
+
+
+# ── 注册 ──
+
+_YOUTUBE = PlatformDef(
+    id="youtube",
+    name="YouTube",
+    aliases=["yt", "油管"],
+    detail_extras={
+        "include_captions": ParamSpec(note="是否获取字幕,默认 True"),
+        "download_video": ParamSpec(note="是否下载视频到本地,默认 False"),
+    },
+)
+_YOUTUBE.search_impl = search
+_YOUTUBE.detail_impl = detail
+register_platform(_YOUTUBE)

+ 125 - 0
agent/tools/builtin/content/registry.py

@@ -0,0 +1,125 @@
+"""
+内容平台注册表
+
+定义所有支持的内容平台及其搜索参数 schema。
+供 content_platforms / content_search / content_detail 路由使用。
+"""
+
+from dataclasses import dataclass, field
+from typing import Any, Callable, Coroutine, Dict, List, Optional
+
+from agent.tools.models import ToolResult
+
+
+# ── 类型定义 ──
+
+@dataclass
+class ParamSpec:
+    """平台专属参数的描述"""
+    values: Optional[List[str]] = None   # 枚举值(None 表示自由文本)
+    default: Optional[str] = None
+    note: str = ""                       # 额外说明
+
+    def to_dict(self) -> dict:
+        d: dict = {}
+        if self.values is not None:
+            d["values"] = self.values
+            d["default"] = self.default
+        if self.note:
+            d["note"] = self.note
+        return d
+
+
+# 平台实现函数的签名
+SearchFunc = Callable[..., Coroutine[Any, Any, ToolResult]]
+DetailFunc = Callable[..., Coroutine[Any, Any, ToolResult]]
+SuggestFunc = Callable[..., Coroutine[Any, Any, ToolResult]]
+
+
+@dataclass
+class PlatformDef:
+    """一个内容平台的完整定义"""
+    id: str                                         # 唯一标识,如 "xhs"
+    name: str                                       # 显示名,如 "小红书"
+    aliases: List[str] = field(default_factory=list) # 模糊匹配别名,如 ["小红书", "RED"]
+    search_params: Dict[str, ParamSpec] = field(default_factory=dict)
+    detail_extras: Dict[str, ParamSpec] = field(default_factory=dict)
+    supports_suggest: bool = False
+    suggest_channels: Optional[List[str]] = None     # suggest API 的 channel 值(可能与 id 不同)
+
+    # 平台实现函数(运行时由 platforms/ 模块设置)
+    search_impl: Optional[SearchFunc] = None
+    detail_impl: Optional[DetailFunc] = None
+    suggest_impl: Optional[SuggestFunc] = None
+
+    def summary(self) -> dict:
+        """概要信息(不含参数细节)"""
+        d = {"id": self.id, "name": self.name}
+        if self.search_params:
+            d["has_search_params"] = True
+        if self.detail_extras:
+            d["has_detail_extras"] = True
+        if self.supports_suggest:
+            d["supports_suggest"] = True
+        return d
+
+    def detail(self) -> dict:
+        """完整参数说明"""
+        d = self.summary()
+        if self.search_params:
+            d["search_params"] = {k: v.to_dict() for k, v in self.search_params.items()}
+        if self.detail_extras:
+            d["detail_extras"] = {k: v.to_dict() for k, v in self.detail_extras.items()}
+        return d
+
+
+# ── 平台注册表 ──
+
+_PLATFORMS: Dict[str, PlatformDef] = {}
+
+
+def register_platform(p: PlatformDef) -> None:
+    _PLATFORMS[p.id] = p
+
+
+def get_platform(platform_id: str) -> Optional[PlatformDef]:
+    return _PLATFORMS.get(platform_id)
+
+
+def all_platforms() -> List[PlatformDef]:
+    return list(_PLATFORMS.values())
+
+
+def match_platforms(query: str) -> List[PlatformDef]:
+    """
+    模糊匹配平台:精确 ID > 别名包含 > token 交集。
+    空 query 返回全部。
+    """
+    if not query:
+        return all_platforms()
+
+    q = query.strip().lower()
+
+    # 1) 精确 ID 匹配
+    if q in _PLATFORMS:
+        return [_PLATFORMS[q]]
+
+    # 2) 别名 / 名称包含匹配
+    alias_hits = [
+        p for p in _PLATFORMS.values()
+        if q in p.name.lower() or any(q in a.lower() for a in p.aliases)
+    ]
+    if alias_hits:
+        return alias_hits
+
+    # 3) token 交集(把 query 拆成字符/词,看命中率)
+    q_tokens = set(q.replace("_", " ").replace("-", " ").split())
+    scored = []
+    for p in _PLATFORMS.values():
+        pool = {p.id, p.name.lower()} | {a.lower() for a in p.aliases}
+        pool_text = " ".join(pool)
+        hits = sum(1 for t in q_tokens if t in pool_text)
+        if hits > 0:
+            scored.append((hits, p))
+    scored.sort(key=lambda x: -x[0])
+    return [p for _, p in scored]

+ 266 - 0
agent/tools/builtin/content/tools.py

@@ -0,0 +1,266 @@
+"""
+内容工具族 —— 统一入口
+
+4 个 @tool 注册给 LLM:
+  - content_platforms: 列出/查询平台及其参数
+  - content_search:    跨平台搜索
+  - content_detail:    查看详情
+  - content_suggest:   搜索建议词
+
+所有平台的具体实现在 platforms/ 子目录,按模块自注册到 registry。
+"""
+
+import json
+import os
+import uuid
+from typing import Any, Dict, Optional
+
+from agent.tools import tool, ToolResult, ToolContext
+from agent.tools.builtin.content.registry import (
+    all_platforms, get_platform, match_platforms,
+)
+from agent.tools.builtin.content import cache as _cache
+
+# 导入平台模块以触发自注册(副作用导入)
+import agent.tools.builtin.content.platforms.aigc_channel  # noqa: F401
+import agent.tools.builtin.content.platforms.youtube       # noqa: F401
+import agent.tools.builtin.content.platforms.x             # noqa: F401
+
+
+def _get_trace_id(context: Optional[ToolContext]) -> str:
+    """从 context 取 trace_id,回退到环境变量或自动生成"""
+    if context and hasattr(context, "trace_id") and context.trace_id:
+        return context.trace_id
+    return os.getenv("TRACE_ID") or f"anon-{uuid.uuid4().hex[:8]}"
+
+
+# ── content_platforms ──
+
+@tool(hidden_params=["context"], groups=["content"])
+async def content_platforms(
+    platform: str = "",
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    列出支持的内容平台及其搜索参数。
+
+    不传 platform 时返回所有平台的概要列表(仅名称和 ID)。
+    传入 platform 时模糊匹配并返回匹配平台的详细参数说明(支持 ID、中文名、别名)。
+
+    建议在不熟悉平台参数时先调用此工具查看,再构造 content_search / content_detail 的参数。
+
+    Args:
+        platform: 可选,平台名称或关键词。支持模糊匹配(如 "xhs"、"小红书"、"youtube")。
+                  留空返回全部平台概要。
+        context: 工具上下文(自动注入)
+    """
+    hits = match_platforms(platform)
+
+    if not hits:
+        all_ids = [p.id for p in all_platforms()]
+        return ToolResult(
+            title="未找到匹配平台",
+            output=f"没有匹配 '{platform}' 的平台。可用平台: {', '.join(all_ids)}",
+        )
+
+    if platform:
+        # 有 query:返回匹配平台的详细参数
+        result = [p.detail() for p in hits]
+    else:
+        # 无 query:返回概要列表
+        result = [p.summary() for p in hits]
+
+    return ToolResult(
+        title=f"内容平台" + (f" ({platform})" if platform else ""),
+        output=json.dumps(result, ensure_ascii=False, indent=2),
+    )
+
+
+# ── content_search ──
+
+@tool(hidden_params=["context"], groups=["content"])
+async def content_search(
+    platform: str,
+    keyword: str,
+    max_count: int = 20,
+    cursor: str = "",
+    extras: Optional[Dict[str, Any]] = None,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    跨平台内容搜索,返回带索引编号的封面拼图 + 概览列表。
+
+    返回的是摘要信息(标题 + 正文截断 + 互动数据),不含完整正文和所有图片。
+    如需查看某条内容的完整信息,请使用 content_detail。
+
+    Args:
+        platform: 平台标识,如 'xhs'、'youtube'、'x'。完整列表见 content_platforms。
+        keyword: 搜索关键词。
+        max_count: 返回条数上限,默认 20。
+        cursor: 分页游标,首次搜索留空,翻页时传入上次返回值。
+        extras: 平台专用参数(dict)。不同平台支持不同参数,
+                如 xhs 支持 sort_type / publish_time / content_type / filter_note_range。
+                不清楚可先调 content_platforms(platform) 查看。
+        context: 工具上下文(自动注入)
+    """
+    pdef = get_platform(platform)
+    if not pdef:
+        # 尝试模糊匹配
+        hits = match_platforms(platform)
+        if hits:
+            suggestions = ", ".join(f"{p.id}({p.name})" for p in hits[:3])
+            return ToolResult(title="平台不存在", output=f"未找到平台 '{platform}'。你是否想要: {suggestions}")
+        all_ids = [p.id for p in all_platforms()]
+        return ToolResult(title="平台不存在", output=f"未找到平台 '{platform}'。可用: {', '.join(all_ids)}")
+
+    if not pdef.search_impl:
+        return ToolResult(title="不支持搜索", output=f"平台 {pdef.name} 暂不支持搜索")
+
+    result = await pdef.search_impl(
+        platform_id=pdef.id,
+        keyword=keyword,
+        max_count=max_count,
+        cursor=cursor,
+        extras=extras,
+    )
+
+    # 持久化搜索结果到磁盘缓存
+    if not result.error:
+        posts = result.metadata.pop("posts", [])
+        trace_id = _get_trace_id(context)
+        _cache.save_search_results(trace_id, pdef.id, keyword, posts)
+
+    return result
+
+
+# ── content_detail ──
+
+@tool(hidden_params=["context"], groups=["content"])
+async def content_detail(
+    platform: str,
+    index: int,
+    extras: Optional[Dict[str, Any]] = None,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    查看内容详情。从最近一次 content_search 的结果中按索引取完整记录。
+
+    Args:
+        platform: 平台标识(必须和之前 content_search 用的一致)。
+        index: 内容序号(1-based),来自 content_search 返回的 index 字段。
+        extras: 平台专用详情参数。YouTube 支持 include_captions / download_video。
+                其他平台通常不需要。
+        context: 工具上下文(自动注入)
+    """
+    pdef = get_platform(platform)
+    if not pdef:
+        return ToolResult(title="平台不存在", output=f"未找到平台 '{platform}'")
+
+    trace_id = _get_trace_id(context)
+    post = _cache.get_cached_post(trace_id, pdef.id, index)
+
+    if not post:
+        info = _cache.get_cached_search_info(trace_id, pdef.id)
+        if info:
+            return ToolResult(
+                title="索引无效",
+                output=f"平台 {pdef.name} 上次搜索 '{info['keyword']}' 共 {info['total']} 条,"
+                       f"有效索引 1-{info['total']},你传入了 {index}。",
+                error="Invalid index",
+            )
+        return ToolResult(
+            title="缓存未命中",
+            output=f"没有 {pdef.name} 的搜索缓存。请先调用 content_search(platform='{pdef.id}', keyword=...) 搜索。",
+            error="No cache",
+        )
+
+    if pdef.detail_impl:
+        return await pdef.detail_impl(post, extras)
+
+    # fallback:直接返回缓存的完整数据
+    return ToolResult(
+        title=f"详情 #{index}",
+        output=json.dumps(post, ensure_ascii=False, indent=2),
+    )
+
+
+# ── content_suggest ──
+
+@tool(hidden_params=["context"], groups=["content"])
+async def content_suggest(
+    platform: str,
+    keyword: str,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    获取搜索关键词补全建议。
+
+    仅部分平台支持(xhs、toutiao、douyin、bili、zhihu)。
+    用于辅助用户发现更精准的搜索词。
+
+    Args:
+        platform: 平台标识。
+        keyword: 搜索关键词(输入中的部分词即可)。
+        context: 工具上下文(自动注入)
+    """
+    pdef = get_platform(platform)
+    if not pdef:
+        return ToolResult(title="平台不存在", output=f"未找到平台 '{platform}'")
+
+    if not pdef.suggest_impl:
+        supported = [p.id for p in all_platforms() if p.supports_suggest]
+        return ToolResult(
+            title="不支持建议词",
+            output=f"平台 {pdef.name} 不支持建议词。支持的平台: {', '.join(supported)}",
+        )
+
+    channel = (pdef.suggest_channels or [pdef.id])[0]
+    return await pdef.suggest_impl(channel, keyword)
+
+
+# ── CLI 入口 ──
+
+def _parse_args(argv: list) -> dict:
+    """解析 --key=value 格式的 CLI 参数"""
+    kwargs = {}
+    for arg in argv:
+        if arg.startswith("--") and "=" in arg:
+            key, val = arg[2:].split("=", 1)
+            # 尝试 JSON 解析(dict / int / bool)
+            try:
+                val = json.loads(val)
+            except (json.JSONDecodeError, ValueError):
+                pass
+            kwargs[key] = val
+    return kwargs
+
+
+if __name__ == "__main__":
+    import sys
+    import asyncio
+
+    COMMANDS = {
+        "platforms": content_platforms,
+        "search": content_search,
+        "detail": content_detail,
+        "suggest": content_suggest,
+    }
+
+    if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
+        print(f"Usage: python {sys.argv[0]} <{'|'.join(COMMANDS)}> [--key=value ...]")
+        sys.exit(1)
+
+    cmd = sys.argv[1]
+    kwargs = _parse_args(sys.argv[2:])
+
+    # trace_id:CLI 参数 > 环境变量 > 自动生成
+    trace_id = kwargs.pop("trace_id", None) or os.getenv("TRACE_ID") or f"cli-{uuid.uuid4().hex[:8]}"
+    os.environ["TRACE_ID"] = trace_id
+
+    result = asyncio.run(COMMANDS[cmd](**kwargs))
+
+    # 输出 JSON(与 toolhub CLI 格式一致)
+    out = {"trace_id": trace_id, "output": result.output, "error": result.error}
+    if result.metadata:
+        out["metadata"] = result.metadata
+    print(json.dumps(out, ensure_ascii=False, indent=2))

+ 2 - 1
agent/tools/builtin/context.py

@@ -14,7 +14,8 @@ from agent.tools import tool, ToolResult, ToolContext
 
 @tool(
     description="获取当前执行上下文,包括计划状态、焦点提醒、协作者信息等。当你感到困惑或需要回顾当前任务状态时调用。",
-    hidden_params=["context"]
+    hidden_params=["context"],
+    groups=["core"],
 )
 async def get_current_context(
     context: ToolContext,

+ 0 - 497
agent/tools/builtin/crawler.py

@@ -1,497 +0,0 @@
-"""
-爬虫服务工具模块
-
-提供 YouTube、X (Twitter) 和微信/通用链接的搜索和详情查询功能。
-"""
-
-import json
-import os
-import subprocess
-import tempfile
-from pathlib import Path
-from typing import Optional, List, Dict, Any
-
-import httpx
-
-from agent.tools import tool, ToolResult
-from agent.tools.utils.image import build_image_grid, encode_base64, load_images
-
-
-# API 配置
-CRAWLER_BASE_URL = "http://crawler.aiddit.com/crawler"
-AIGC_BASE_URL = "http://aigc-channel.aiddit.com/aigc/channel"
-DEFAULT_TIMEOUT = 60.0
-
-# 视频处理相关配置
-VIDEO_DOWNLOAD_DIR = Path(tempfile.gettempdir()) / "youtube_videos"
-VIDEO_DOWNLOAD_DIR.mkdir(exist_ok=True)
-
-
-async def _build_video_collage(videos: List[Dict[str, Any]]) -> Optional[str]:
-    """
-    将视频缩略图+序号+标题拼接成网格图,返回 base64 编码的 PNG。
-    复用 agent.tools.utils.image 中的共享拼图逻辑。
-    """
-    if not videos:
-        return None
-
-    urls: List[str] = []
-    titles: List[str] = []
-    for video in videos:
-        thumbnail = None
-        if "thumbnails" in video and isinstance(video["thumbnails"], list) and video["thumbnails"]:
-            thumbnail = video["thumbnails"][0].get("url")
-        elif "thumbnail" in video:
-            thumbnail = video.get("thumbnail")
-        elif "cover_url" in video:
-            thumbnail = video.get("cover_url")
-
-        title = video.get("title", "") or video.get("text", "")
-        if thumbnail:
-            urls.append(thumbnail)
-            titles.append(title)
-
-    if not urls:
-        return None
-
-    loaded = await load_images(urls)
-
-    valid_images = []
-    valid_labels = []
-    for (_, img), title in zip(loaded, titles):
-        if img is not None:
-            valid_images.append(img)
-            valid_labels.append(title)
-
-    if not valid_images:
-        return None
-
-    grid = build_image_grid(images=valid_images, labels=valid_labels)
-    b64, _ = encode_base64(grid, format="PNG")
-    return b64
-
-
-def _parse_srt_to_outline(srt_content: str) -> List[Dict[str, str]]:
-    """解析 SRT 字幕,生成带时间戳的大纲"""
-    if not srt_content:
-        return []
-
-    outline = []
-    blocks = srt_content.strip().split('\n\n')
-    for block in blocks:
-        lines = block.strip().split('\n')
-        if len(lines) >= 3:
-            timestamp_line = lines[1]
-            if '-->' in timestamp_line:
-                start_time = timestamp_line.split('-->')[0].strip()
-                text = ' '.join(lines[2:])
-                outline.append({'timestamp': start_time, 'text': text})
-    return outline
-
-
-def _download_youtube_video(video_id: str) -> Optional[str]:
-    """使用 yt-dlp 下载 YouTube 视频,返回文件路径"""
-    try:
-        output_path = VIDEO_DOWNLOAD_DIR / f"{video_id}.mp4"
-        if output_path.exists():
-            return str(output_path)
-
-        cmd = [
-            'yt-dlp',
-            '-f', 'best[ext=mp4]',
-            '-o', str(output_path),
-            f'https://www.youtube.com/watch?v={video_id}'
-        ]
-        result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
-
-        if result.returncode == 0 and output_path.exists():
-            return str(output_path)
-        return None
-    except Exception:
-        return None
-
-
-# ── YouTube 工具 ──
-
-@tool()
-async def youtube_search(keyword: str) -> ToolResult:
-    """
-    搜索 YouTube 视频
-
-    Args:
-        keyword: 搜索关键词
-
-    Returns:
-        搜索结果列表,包含视频标题、ID、频道等信息
-    """
-    try:
-        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
-            response = await client.post(
-                f"{CRAWLER_BASE_URL}/youtube/keyword",
-                json={"keyword": keyword}
-            )
-            response.raise_for_status()
-            data = response.json()
-
-            if data.get("code") == 0:
-                result_data = data.get("data", {})
-                videos = result_data.get("data", []) if isinstance(result_data, dict) else []
-
-                images = []
-                collage_b64 = await _build_video_collage(videos)
-                if collage_b64:
-                    images.append({
-                        "type": "base64",
-                        "media_type": "image/png",
-                        "data": collage_b64
-                    })
-
-                summary_list = []
-                for idx, video in enumerate(videos[:20], 1):
-                    title = video.get("title", "")
-                    author = video.get("author", "")
-                    video_id = video.get("video_id", "")
-                    summary_list.append(f"{idx}. {title} - {author} (ID: {video_id})")
-
-                output_data = {
-                    "keyword": keyword,
-                    "total": len(videos),
-                    "summary": summary_list,
-                    "data": videos
-                }
-
-                return ToolResult(
-                    title=f"YouTube 搜索: {keyword}",
-                    output=json.dumps(output_data, ensure_ascii=False, indent=2),
-                    long_term_memory=f"Searched YouTube for '{keyword}', found {len(videos)} videos",
-                    images=images
-                )
-            else:
-                return ToolResult(
-                    title="YouTube 搜索失败",
-                    output="",
-                    error=f"搜索失败: {data.get('msg', '未知错误')}"
-                )
-
-    except Exception as e:
-        return ToolResult(
-            title="YouTube 搜索异常",
-            output="",
-            error=str(e)
-        )
-
-
-@tool()
-async def youtube_detail(
-    content_id: str,
-    include_captions: bool = True,
-    download_video: bool = False
-) -> ToolResult:
-    """
-    获取 YouTube 视频详情(可选包含字幕、下载视频并生成大纲)
-
-    Args:
-        content_id: 视频 ID
-        include_captions: 是否包含字幕,默认 True
-        download_video: 是否下载视频并生成带时间戳的大纲,默认 False。
-            下载后可使用 extract_video_clip 截取视频片段观看。
-
-    Returns:
-        视频详细信息,包含字幕、视频大纲和本地文件路径
-    """
-    try:
-        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
-            detail_response = await client.post(
-                f"{CRAWLER_BASE_URL}/youtube/detail",
-                json={"content_id": content_id}
-            )
-            detail_response.raise_for_status()
-            detail_data = detail_response.json()
-
-            if detail_data.get("code") != 0:
-                return ToolResult(
-                    title="获取详情失败",
-                    output="",
-                    error=f"获取详情失败: {detail_data.get('msg', '未知错误')}"
-                )
-
-            result_data = detail_data.get("data", {})
-            video_info = result_data.get("data", {}) if isinstance(result_data, dict) else {}
-
-            # 获取字幕
-            captions_text = None
-            if include_captions or download_video:
-                try:
-                    captions_response = await client.post(
-                        f"{CRAWLER_BASE_URL}/youtube/captions",
-                        json={"content_id": content_id}
-                    )
-                    captions_response.raise_for_status()
-                    captions_data = captions_response.json()
-
-                    if captions_data.get("code") == 0:
-                        captions_result = captions_data.get("data", {})
-                        if isinstance(captions_result, dict):
-                            inner_data = captions_result.get("data", {})
-                            if isinstance(inner_data, dict):
-                                captions_text = inner_data.get("content")
-                except Exception:
-                    pass
-
-            # 下载视频并生成大纲
-            video_path = None
-            video_outline = None
-            if download_video:
-                video_path = await asyncio.to_thread(_download_youtube_video, content_id)
-                if captions_text:
-                    video_outline = _parse_srt_to_outline(captions_text)
-
-            # 合并数据
-            output_data = {
-                "video_id": content_id,
-                "title": video_info.get("title", ""),
-                "channel": video_info.get("channel_account_name", ""),
-                "description": video_info.get("body_text", ""),
-                "like_count": video_info.get("like_count"),
-                "comment_count": video_info.get("comment_count"),
-                "publish_timestamp": video_info.get("publish_timestamp"),
-                "content_link": video_info.get("content_link", ""),
-                "captions": captions_text,
-                "full_data": video_info
-            }
-
-            if download_video:
-                output_data["video_path"] = video_path
-                output_data["video_outline"] = video_outline
-                if not video_path:
-                    output_data["download_error"] = "视频下载失败,请检查 yt-dlp 是否可用"
-
-            memory = f"Retrieved YouTube video details for {content_id}"
-            if captions_text:
-                memory += " with captions"
-            if video_path:
-                memory += f", downloaded to {video_path}"
-
-            return ToolResult(
-                title=f"YouTube 视频详情: {content_id}",
-                output=json.dumps(output_data, ensure_ascii=False, indent=2),
-                long_term_memory=memory
-            )
-
-    except Exception as e:
-        return ToolResult(
-            title="YouTube 详情查询异常",
-            output="",
-            error=str(e)
-        )
-
-
-# ── X (Twitter) 工具 ──
-
-@tool()
-async def x_search(keyword: str) -> ToolResult:
-    """
-    搜索 X (Twitter) 内容(数据已结构化,无需访问详情页)
-
-    Args:
-        keyword: 搜索关键词
-
-    Returns:
-        搜索结果列表,包含推文内容、作者、互动数据等
-    """
-    try:
-        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
-            response = await client.post(
-                "http://crawler.aiddit.com/crawler/x/keyword",
-                json={"keyword": keyword}
-            )
-            response.raise_for_status()
-            data = response.json()
-
-            if data.get("code") == 0:
-                result_data = data.get("data", {})
-                tweets = result_data.get("data", []) if isinstance(result_data, dict) else []
-
-                # 构建拼接图
-                images = []
-                tweets_with_images = []
-                for tweet in tweets:
-                    image_list = tweet.get("image_url_list", [])
-                    if image_list:
-                        tweet["thumbnails"] = [{"url": image_list[0].get("image_url")}]
-                        tweets_with_images.append(tweet)
-
-                collage_b64 = await _build_video_collage(tweets_with_images if tweets_with_images else tweets)
-                if collage_b64:
-                    images.append({
-                        "type": "base64",
-                        "media_type": "image/png",
-                        "data": collage_b64
-                    })
-
-                summary_list = []
-                for idx, tweet in enumerate(tweets[:20], 1):
-                    text = tweet.get("body_text", "")[:100]
-                    author = tweet.get("channel_account_name", "")
-                    summary_list.append(f"{idx}. @{author}: {text}")
-
-                output_data = {
-                    "keyword": keyword,
-                    "total": len(tweets),
-                    "summary": summary_list,
-                    "data": tweets
-                }
-
-                return ToolResult(
-                    title=f"X 搜索: {keyword}",
-                    output=json.dumps(output_data, ensure_ascii=False, indent=2),
-                    long_term_memory=f"Searched X (Twitter) for '{keyword}', found {len(tweets)} tweets",
-                    images=images
-                )
-            else:
-                return ToolResult(
-                    title="X 搜索失败",
-                    output="",
-                    error=f"搜索失败: {data.get('msg', '未知错误')}"
-                )
-
-    except Exception as e:
-        return ToolResult(
-            title="X 搜索异常",
-            output="",
-            error=str(e)
-        )
-
-
-# ── 内容导入工具 ──
-
-@tool()
-async def import_content(plan_name: str, content_data: List[Dict[str, Any]]) -> ToolResult:
-    """
-    导入长文内容(微信公众号、小红书、抖音等通用链接)
-
-    Args:
-        plan_name: 计划名称
-        content_data: 内容数据列表,每项包含 channel、content_link、title 等字段
-
-    Returns:
-        导入结果,包含 plan_id
-    """
-    try:
-        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
-            response = await client.post(
-                f"{AIGC_BASE_URL}/weixin/auto_insert",
-                json={"plan_name": plan_name, "data": content_data}
-            )
-            response.raise_for_status()
-            data = response.json()
-
-            if data.get("code") == 0:
-                result_data = data.get("data", {})
-                return ToolResult(
-                    title=f"内容导入: {plan_name}",
-                    output=json.dumps(result_data, ensure_ascii=False, indent=2),
-                    long_term_memory=f"Imported {len(content_data)} items to plan '{plan_name}'"
-                )
-            else:
-                return ToolResult(
-                    title="导入失败",
-                    output="",
-                    error=f"导入失败: {data.get('msg', '未知错误')}"
-                )
-
-    except Exception as e:
-        return ToolResult(
-            title="内容导入异常",
-            output="",
-            error=str(e)
-        )
-
-
-# ── 视频截取工具 ──
-
-@tool()
-async def extract_video_clip(
-    video_id: str,
-    start_time: str,
-    end_time: str,
-    output_name: Optional[str] = None
-) -> ToolResult:
-    """
-    从已下载的 YouTube 视频中截取指定时间段的片段
-
-    Args:
-        video_id: YouTube 视频 ID(必须先通过 youtube_detail(download_video=True) 下载)
-        start_time: 开始时间,格式: HH:MM:SS 或 MM:SS
-        end_time: 结束时间,格式: HH:MM:SS 或 MM:SS
-        output_name: 输出文件名(可选)
-
-    Returns:
-        截取的视频片段路径
-
-    Example:
-        extract_video_clip("dQw4w9WgXcQ", "00:00:10", "00:00:30")
-    """
-    try:
-        source_video = VIDEO_DOWNLOAD_DIR / f"{video_id}.mp4"
-        if not source_video.exists():
-            return ToolResult(
-                title="视频截取失败",
-                output="",
-                error="源视频不存在,请先使用 youtube_detail(download_video=True) 下载视频"
-            )
-
-        if not output_name:
-            output_name = f"{video_id}_clip_{start_time.replace(':', '-')}_{end_time.replace(':', '-')}.mp4"
-
-        output_path = VIDEO_DOWNLOAD_DIR / output_name
-
-        cmd = [
-            'ffmpeg',
-            '-i', str(source_video),
-            '-ss', start_time,
-            '-to', end_time,
-            '-c', 'copy',
-            '-y',
-            str(output_path)
-        ]
-
-        result = await asyncio.to_thread(
-            subprocess.run, cmd, capture_output=True, text=True, timeout=60
-        )
-
-        if result.returncode == 0 and output_path.exists():
-            file_size = output_path.stat().st_size / (1024 * 1024)
-
-            output_data = {
-                "video_id": video_id,
-                "clip_path": str(output_path),
-                "start_time": start_time,
-                "end_time": end_time,
-                "file_size_mb": round(file_size, 2)
-            }
-
-            return ToolResult(
-                title=f"视频片段截取成功: {start_time} - {end_time}",
-                output=json.dumps(output_data, ensure_ascii=False, indent=2),
-                long_term_memory=f"Extracted video clip from {video_id}: {start_time} to {end_time}"
-            )
-        else:
-            return ToolResult(
-                title="视频截取失败",
-                output="",
-                error=f"ffmpeg 执行失败: {result.stderr}"
-            )
-
-    except subprocess.TimeoutExpired:
-        return ToolResult(
-            title="视频截取超时",
-            output="",
-            error="视频截取超时(60秒)"
-        )
-    except Exception as e:
-        return ToolResult(
-            title="视频截取异常",
-            output="",
-            error=str(e)
-        )

+ 4 - 0
agent/tools/builtin/feishu/chat.py

@@ -133,6 +133,7 @@ def update_unread_count(contact_name: str, increment: int = 1, reset: bool = Fal
 
 @tool(
     hidden_params=["context"],
+    groups=["feishu"],
     display={
         "zh": {
             "name": "获取飞书联系人列表",
@@ -160,6 +161,7 @@ async def feishu_get_contact_list(context: Optional[ToolContext] = None) -> Tool
 
 @tool(
     hidden_params=["context"],
+    groups=["feishu"],
     display={
         "zh": {
             "name": "给飞书联系人发送消息",
@@ -293,6 +295,7 @@ async def feishu_send_message_to_contact(
 
 @tool(
     hidden_params=["context"],
+    groups=["feishu"],
     display={
         "zh": {
             "name": "获取飞书联系人回复",
@@ -409,6 +412,7 @@ def _convert_feishu_msg_to_openai_content(client: FeishuClient, msg: Dict[str, A
 
 @tool(
     hidden_params=["context"],
+    groups=["feishu"],
     display={
         "zh": {
             "name": "获取飞书聊天历史记录",

+ 1 - 1
agent/tools/builtin/file/edit.py

@@ -17,7 +17,7 @@ import re
 from agent.tools import tool, ToolResult, ToolContext
 
 
-@tool(description="编辑文件,使用精确字符串替换。支持多种智能匹配策略。", hidden_params=["context"])
+@tool(description="编辑文件,使用精确字符串替换。支持多种智能匹配策略。", hidden_params=["context"], groups=["core"])
 async def edit_file(
     file_path: str,
     old_string: str,

+ 1 - 1
agent/tools/builtin/file/grep.py

@@ -21,7 +21,7 @@ LIMIT = 100  # 最大返回匹配数(参考 opencode grep.ts:107)
 MAX_LINE_LENGTH = 2000  # 最大行长度(参考 opencode grep.ts:10)
 
 
-@tool(description="在文件内容中搜索模式", hidden_params=["context"])
+@tool(description="在文件内容中搜索模式", hidden_params=["context"], groups=["core"])
 async def grep_content(
     pattern: str,
     path: Optional[str] = None,

+ 1 - 1
agent/tools/builtin/file/read.py

@@ -27,7 +27,7 @@ MAX_LINE_LENGTH = 2000
 MAX_BYTES = 50 * 1024  # 50KB
 
 
-@tool(description="读取单个文件内容,支持文本文件、图片、PDF 等多种格式,也支持 HTTP/HTTPS URL", hidden_params=["context"])
+@tool(description="读取单个文件内容,支持文本文件、图片、PDF 等多种格式,也支持 HTTP/HTTPS URL", hidden_params=["context"], groups=["core"])
 async def read_file(
     file_path: str,
     offset: int = 0,

+ 1 - 0
agent/tools/builtin/file/read_images.py

@@ -71,6 +71,7 @@ def _adaptive_layout(count: int) -> Tuple[int, int]:
             },
         },
     },
+    groups=["core"],
 )
 async def read_images(
     paths: List[str],

+ 1 - 1
agent/tools/builtin/file/write.py

@@ -16,7 +16,7 @@ import difflib
 from agent.tools import tool, ToolResult, ToolContext
 
 
-@tool(description="写入文件内容(创建新文件、覆盖现有文件或追加内容)", hidden_params=["context"])
+@tool(description="写入文件内容(创建新文件、覆盖现有文件或追加内容)", hidden_params=["context"], groups=["core"])
 async def write_file(
     file_path: str,
     content: str,

+ 1 - 1
agent/tools/builtin/glob_tool.py

@@ -19,7 +19,7 @@ from agent.tools import tool, ToolResult, ToolContext
 LIMIT = 100  # 最大返回数量(参考 opencode glob.ts:35)
 
 
-@tool(description="使用 glob 模式匹配文件", hidden_params=["context"])
+@tool(description="使用 glob 模式匹配文件", hidden_params=["context"], groups=["core"])
 async def glob_files(
     pattern: str,
     path: Optional[str] = None,

+ 8 - 0
agent/tools/builtin/im/chat.py

@@ -43,6 +43,7 @@ class _ToolNotifier(AgentNotifier):
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "初始化 IM 连接", "params": {"contact_id": "你的身份 ID", "server_url": "服务器地址"}},
         "en": {"name": "Setup IM Connection", "params": {"contact_id": "Your identity ID", "server_url": "Server URL"}},
@@ -80,6 +81,7 @@ async def im_setup(
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "打开 IM 窗口", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID"}},
         "en": {"name": "Open IM Window", "params": {"contact_id": "Agent ID", "chat_id": "Window ID"}},
@@ -109,6 +111,7 @@ async def im_open_window(
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "关闭 IM 窗口", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID"}},
         "en": {"name": "Close IM Window", "params": {"contact_id": "Agent ID", "chat_id": "Window ID"}},
@@ -138,6 +141,7 @@ async def im_close_window(
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "检查 IM 新消息通知", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID"}},
         "en": {"name": "Check IM Notifications", "params": {"contact_id": "Agent ID", "chat_id": "Window ID"}},
@@ -169,6 +173,7 @@ async def im_check_notification(
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "接收 IM 消息", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID"}},
         "en": {"name": "Receive IM Messages", "params": {"contact_id": "Agent ID", "chat_id": "Window ID"}},
@@ -213,6 +218,7 @@ async def im_receive_messages(
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "发送 IM 消息", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID", "receiver": "接收者 ID", "content": "消息内容"}},
         "en": {"name": "Send IM Message", "params": {"contact_id": "Agent ID", "chat_id": "Window ID", "receiver": "Receiver ID", "content": "Message content"}},
@@ -253,6 +259,7 @@ async def im_send_message(
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "查询 IM 联系人", "params": {"contact_id": "Agent ID", "server_http_url": "服务器 HTTP 地址"}},
         "en": {"name": "Get IM Contacts", "params": {"contact_id": "Agent ID", "server_http_url": "Server HTTP URL"}},
@@ -297,6 +304,7 @@ async def im_get_contacts(
 
 @tool(
     hidden_params=["context"],
+    groups=["im"],
     display={
         "zh": {"name": "查询 IM 聊天历史", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID", "peer_id": "联系人 ID", "limit": "最大条数"}},
         "en": {"name": "Get IM Chat History", "params": {"contact_id": "Agent ID", "chat_id": "Window ID", "peer_id": "Contact ID", "limit": "Max records"}},

+ 40 - 29
agent/tools/builtin/knowledge.py

@@ -84,23 +84,20 @@ class KnowledgeConfig:
         return owner
 
 
-@tool(
-    hidden_params=["context"],
-    inject_params={
-        "types": {"mode": "default", "key": "knowledge_config.default_search_types"},
-        "owner": {"mode": "default", "key": "knowledge_config.default_search_owner"},
-    }
-)
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def knowledge_search(
     query: str,
     top_k: int = 5,
     min_score: int = 3,
     types: Optional[List[str]] = None,
     owner: Optional[str] = None,
+    requirement_id: Optional[str] = None,
+    capability_id: Optional[str] = None,
+    tool_id: Optional[str] = None,
     context: Optional[ToolContext] = None,
 ) -> ToolResult:
     """
-    检索知识(两阶段:语义路由 + 质量精排)
+    检索知识(两阶段:语义路由 + 质量精排;可通过外键精确截断查询范围
 
     Args:
         query: 搜索查询(任务描述)
@@ -108,6 +105,9 @@ async def knowledge_search(
         min_score: 最低评分过滤(默认 3)
         types: 按类型过滤(user_profile/strategy/tool/usecase/definition/plan)
         owner: 按所有者过滤(可选,支持多个owner用逗号分隔的字符串,如 "user1@example.com,user2@example.com")
+        requirement_id: 关系过滤 - 仅搜索关联到此需求ID的知识
+        capability_id: 关系过滤 - 仅搜索关联到此能力ID的知识
+        tool_id: 关系过滤 - 仅搜索关联到此工具ID的知识
         context: 工具上下文
 
     Returns:
@@ -123,6 +123,12 @@ async def knowledge_search(
             params["types"] = ",".join(types)
         if owner:
             params["owner"] = owner
+        if requirement_id:
+            params["requirement_id"] = requirement_id
+        if capability_id:
+            params["capability_id"] = capability_id
+        if tool_id:
+            params["tool_id"] = tool_id
 
         async with httpx.AsyncClient(timeout=60.0) as client:
             response = await client.get(f"{KNOWHUB_API}/api/knowledge/search", params=params)
@@ -169,14 +175,7 @@ async def knowledge_search(
         )
 
 
-@tool(
-    hidden_params=["context", "owner"],
-    inject_params={
-        "owner": {"mode": "default", "key": "knowledge_config.resolved_owner"},
-        "tags": {"mode": "merge", "key": "knowledge_config.default_tags"},
-        "scopes": {"mode": "merge", "key": "knowledge_config.default_scopes"},
-    }
-)
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def knowledge_save(
     task: str,
     content: str,
@@ -271,7 +270,7 @@ async def knowledge_save(
         )
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def knowledge_update(
     knowledge_id: str,
     add_helpful_case: Optional[Dict] = None,
@@ -341,7 +340,7 @@ async def knowledge_update(
         )
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def knowledge_batch_update(
     feedback_list: List[Dict[str, Any]],
     context: Optional[ToolContext] = None,
@@ -391,20 +390,26 @@ async def knowledge_batch_update(
         )
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def knowledge_list(
     limit: int = 10,
     types: Optional[List[str]] = None,
     scopes: Optional[List[str]] = None,
+    requirement_id: Optional[str] = None,
+    capability_id: Optional[str] = None,
+    tool_id: Optional[str] = None,
     context: Optional[ToolContext] = None,
 ) -> ToolResult:
     """
     列出已保存的知识
-
+    
     Args:
         limit: 返回数量限制(默认 10)
         types: 按类型过滤(可选)
         scopes: 按范围过滤(可选)
+        requirement_id: 关系过滤 - 仅列出关联到此需求ID的知识
+        capability_id: 关系过滤 - 仅列出关联到此能力ID的知识
+        tool_id: 关系过滤 - 仅列出关联到此工具ID的知识
         context: 工具上下文
 
     Returns:
@@ -416,6 +421,12 @@ async def knowledge_list(
             params["types"] = ",".join(types)
         if scopes:
             params["scopes"] = ",".join(scopes)
+        if requirement_id:
+            params["requirement_id"] = requirement_id
+        if capability_id:
+            params["capability_id"] = capability_id
+        if tool_id:
+            params["tool_id"] = tool_id
 
         async with httpx.AsyncClient(timeout=30.0) as client:
             response = await client.get(f"{KNOWHUB_API}/api/knowledge", params=params)
@@ -453,7 +464,7 @@ async def knowledge_list(
         )
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def knowledge_slim(
     model: str = "google/gemini-2.0-flash-001",
     context: Optional[ToolContext] = None,
@@ -499,7 +510,7 @@ async def knowledge_slim(
 
 # ==================== Resource 资源管理工具 ====================
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def resource_save(
     resource_id: str,
     title: str,
@@ -558,7 +569,7 @@ async def resource_save(
         )
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def resource_get(
     resource_id: str,
     org_key: Optional[str] = None,
@@ -613,7 +624,7 @@ async def resource_get(
 
 # ==================== Tool 表查询工具 ====================
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def tool_search(
     query: str,
     top_k: int = 5,
@@ -638,7 +649,7 @@ async def tool_search(
         return ToolResult(title="❌ 工具检索失败", output=str(e), error=str(e))
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def tool_list(
     limit: int = 20,
     offset: int = 0,
@@ -660,7 +671,7 @@ async def tool_list(
 
 # ==================== Capability (原子能力) 表查询工具 ====================
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def capability_search(
     query: str,
     top_k: int = 5,
@@ -682,7 +693,7 @@ async def capability_search(
         return ToolResult(title="❌ 原子能力检索失败", output=str(e), error=str(e))
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def capability_list(
     limit: int = 20,
     offset: int = 0,
@@ -702,7 +713,7 @@ async def capability_list(
 
 # ==================== Requirement (需求) 表查询工具 ====================
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def requirement_search(
     query: str,
     top_k: int = 5,
@@ -720,7 +731,7 @@ async def requirement_search(
         return ToolResult(title="❌ 需求检索失败", output=str(e), error=str(e))
 
 
-@tool(hidden_params=["context"])
+@tool(groups=["knowledge_internal"], hidden_params=["context"])
 async def requirement_list(
     limit: int = 20,
     offset: int = 0,

+ 4 - 2
agent/tools/builtin/librarian.py

@@ -24,7 +24,8 @@ KNOWHUB_API = os.getenv("KNOWHUB_API", "http://localhost:9999").rstrip("/")
     hidden_params=["context"],
     inject_params={
         "trace_id": {"mode": "default", "key": "trace_id"},
-    }
+    },
+    groups=["knowledge"],
 )
 async def ask_knowledge(
     query: str,
@@ -107,7 +108,8 @@ async def ask_knowledge(
     hidden_params=["context"],
     inject_params={
         "trace_id": {"mode": "default", "key": "trace_id"},
-    }
+    },
+    groups=["knowledge"],
 )
 async def upload_knowledge(
     data: Dict[str, Any],

+ 4 - 2
agent/tools/builtin/resource.py

@@ -10,8 +10,9 @@ from agent.tools import tool, ToolResult
 KNOWHUB_API = os.getenv("KNOWHUB_API", "http://43.106.118.91:9999").rstrip("/")
 
 
-@tool(    
-    description="列出知识库中的所有工具资源"
+@tool(
+    description="列出知识库中的所有工具资源",
+    groups=["resource"],
 )
 def resource_list_tools(
     category: Optional[str] = None,
@@ -49,6 +50,7 @@ def resource_list_tools(
 
 @tool(
     description="获取指定工具的详细信息",
+    groups=["resource"],
 )
 def resource_get_tool(tool_id: str) -> ToolResult:
     """获取工具详情

+ 0 - 420
agent/tools/builtin/search.py

@@ -1,420 +0,0 @@
-"""
-搜索工具模块
-
-提供帖子搜索、帖子详情查看和建议词搜索功能,支持多个渠道平台。
-
-主要功能:
-1. search_posts - 帖子搜索(浏览模式:封面图+标题+内容截断)
-2. select_post - 帖子详情(从搜索结果中选取单个帖子的完整内容)
-3. get_search_suggestions - 获取平台的搜索补全建议词
-"""
-
-import json
-from enum import Enum
-from typing import Any, Dict, List, Optional
-
-import httpx
-
-from agent.tools import tool, ToolResult
-from agent.tools.utils.image import build_image_grid, encode_base64, load_images
-
-
-# API 基础配置
-BASE_URL = "http://aigc-channel.aiddit.com/aigc/channel"
-DEFAULT_TIMEOUT = 60.0
-
-# 搜索结果缓存,以序号为 key
-_search_cache: Dict[int, Dict[str, Any]] = {}
-
-
-async def _build_collage(posts: List[Dict[str, Any]]) -> Optional[str]:
-    """
-    将帖子封面图+序号+标题拼接成网格图,返回 base64 编码的 PNG。
-    复用 agent.tools.utils.image 中的共享拼图逻辑。
-    """
-    if not posts:
-        return None
-
-    # 收集有封面图的帖子
-    urls: List[str] = []
-    titles: List[str] = []
-    for post in posts:
-        imgs = post.get("images", [])
-        cover_url = imgs[0] if imgs else None
-        if cover_url:
-            urls.append(cover_url)
-            titles.append(post.get("title", "") or "")
-
-    if not urls:
-        return None
-
-    # 并发加载图片
-    loaded = await load_images(urls)
-
-    # 过滤加载失败的(保持 url 和 title 对齐)
-    valid_images = []
-    valid_labels = []
-    for (_, img), title in zip(loaded, titles):
-        if img is not None:
-            valid_images.append(img)
-            valid_labels.append(title)
-
-    if not valid_images:
-        return None
-
-    grid = build_image_grid(images=valid_images, labels=valid_labels)
-    b64, _ = encode_base64(grid, format="PNG")
-    return b64
-
-
-class PostSearchChannel(str, Enum):
-    """
-    帖子搜索支持的渠道类型
-    """
-    XHS = "xhs"           # 小红书
-    GZH = "gzh"           # 公众号
-    SPH = "sph"           # 视频号
-    GITHUB = "github"     # GitHub
-    TOUTIAO = "toutiao"   # 头条
-    DOUYIN = "douyin"     # 抖音
-    BILI = "bili"         # B站
-    ZHIHU = "zhihu"       # 知乎
-    WEIBO = "weibo"       # 微博
-
-
-class SuggestSearchChannel(str, Enum):
-    """
-    建议词搜索支持的渠道类型
-    """
-    XHS = "xhs"           # 小红书
-    WX = "wx"             # 微信
-    GITHUB = "github"     # GitHub
-    TOUTIAO = "toutiao"   # 头条
-    DOUYIN = "douyin"     # 抖音
-    BILI = "bili"         # B站
-    ZHIHU = "zhihu"       # 知乎
-
-
-@tool(
-    display={
-        "zh": {
-            "name": "帖子搜索",
-            "params": {
-                "keyword": "搜索关键词",
-                "channel": "搜索渠道(xhs=小红书, gzh=公众号, sph=视频号, github, toutiao=头条, douyin=抖音, bili=B站, zhihu=知乎, weibo=微博)",
-                "cursor": "分页游标",
-                "max_count": "返回条数",
-                "content_type": "内容类型-视频/图文",
-                "sort_type": "排序方式(xhs专用)",
-                "publish_time": "发布时间筛选(xhs专用)",
-                "filter_note_range": "笔记时长筛选(xhs专用)"
-            }
-        },
-        "en": {
-            "name": "Search Posts",
-            "params": {
-                "keyword": "Search keyword",
-                "channel": "Search channel (xhs=XiaoHongShu, gzh=WeChat Official Account, sph=WeChat Channels, github, toutiao, douyin, bili, zhihu, weibo)",
-                "cursor": "Pagination cursor",
-                "max_count": "Max results",
-                "content_type": "content type-视频/图文",
-                "sort_type": "Sort type (xhs only)",
-                "publish_time": "Publish time filter (xhs only)",
-                "filter_note_range": "Note duration filter (xhs only)"
-            }
-        }
-    }
-)
-async def search_posts(
-    keyword: str,
-    channel: str = "xhs",
-    cursor: str = "",
-    max_count: int = 20,
-    content_type: str = "",
-    sort_type: str = "综合排序",
-    publish_time: str = "不限",
-    filter_note_range: str = "不限"
-) -> ToolResult:
-    """
-    帖子搜索(浏览模式)
-
-    根据关键词在指定渠道平台搜索帖子,返回封面图+标题+内容摘要,用于快速浏览。
-    如需查看某个帖子的完整内容,请使用 select_post 工具。
-
-    Args:
-        keyword: 搜索关键词
-        channel: 搜索渠道,支持的渠道有:
-            - xhs: 小红书
-            - gzh: 公众号
-            - sph: 视频号
-            - github: GitHub
-            - toutiao: 头条
-            - douyin: 抖音
-            - bili: B站
-            - zhihu: 知乎
-            - weibo: 微博
-        cursor: 分页游标,首次请求为空字符串,后续使用上次返回的 cursor
-        max_count: 返回的最大条数,默认为 20
-        content_type: 内容类型筛选,默认不限;
-            xhs 可选值:'不限' | '图文' | '视频' | '文章';
-            其他渠道可选值:'视频' | '图文'
-        sort_type: 排序方式(仅 xhs 有效),可选值:'综合排序' | '最新发布' | '最多点赞',默认'综合排序'
-        publish_time: 发布时间筛选(仅 xhs 有效),可选值:'不限' | '近30天' | '近7天' | '近1天',默认'不限'
-        filter_note_range: 笔记时长筛选,视频内容有效(仅 xhs 有效),可选值:'不限' | '1分钟以内' | '1-5分钟' | '5分钟以上',默认'不限'
-
-    Returns:
-        ToolResult 包含搜索结果摘要列表(封面图+标题+内容截断),
-        可通过 channel_content_id 调用 select_post 查看完整内容。
-    """
-    global _search_cache
-    try:
-        channel_value = channel.value if isinstance(channel, PostSearchChannel) else channel
-
-        url = f"{BASE_URL}/data"
-        if channel_value == "xhs":
-            payload = {
-                "type": channel_value,
-                "keyword": keyword,
-                "cursor": cursor,
-                "content_type": content_type if content_type else "不限",
-                "sort_type": sort_type,
-                "publish_time": publish_time,
-                "filter_note_range": filter_note_range,
-            }
-        else:
-            payload = {
-                "type": channel_value,
-                "keyword": keyword,
-                "cursor": cursor if cursor else "0",
-                "max_count": max_count,
-                "content_type": content_type,
-            }
-
-        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
-            response = await client.post(
-                url,
-                json=payload,
-                headers={"Content-Type": "application/json"},
-            )
-            response.raise_for_status()
-            data = response.json()
-
-        posts = data.get("data", [])
-
-        # 缓存完整结果(以序号为 key)
-        _search_cache.clear()
-        for idx, post in enumerate(posts):
-            _search_cache[idx + 1] = post
-
-        # 构建摘要列表(带序号)
-        summary_list = []
-        for idx, post in enumerate(posts):
-            body = post.get("body_text", "") or ""
-            title = post.get("title") or body[:20] or ""
-            summary_list.append({
-                "index": idx + 1,
-                "channel_content_id": post.get("channel_content_id"),
-                "title": title,
-                "body_text": body[:100] + ("..." if len(body) > 100 else ""),
-                "like_count": post.get("like_count"),
-                "collect_count": post.get("collect_count"),
-                "comment_count": post.get("comment_count"),
-                "channel": post.get("channel"),
-                "link": post.get("link"),
-                "content_type": post.get("content_type"),
-                "publish_timestamp": post.get("publish_timestamp"),
-            })
-
-        # 拼接封面图网格
-        images = []
-        try:
-            collage_b64 = await _build_collage(posts)
-            if collage_b64:
-                images.append({
-                    "type": "base64",
-                    "media_type": "image/png",
-                    "data": collage_b64
-                })
-        except Exception as collage_error:
-            # 图片拼接失败不影响主流程,记录错误但继续返回结果
-            import logging
-            logging.warning(f"Failed to build collage for {channel_value}: {collage_error}")
-
-        output_data = {
-            "code": data.get("code"),
-            "message": data.get("message"),
-            "data": summary_list
-        }
-
-        return ToolResult(
-            title=f"搜索结果: {keyword} ({channel_value})",
-            output=json.dumps(output_data, ensure_ascii=False, indent=2),
-            long_term_memory=f"Searched '{keyword}' on {channel_value}, found {len(posts)} posts. Use select_post(index) to view full details of a specific post.",
-            images=images
-        )
-    except httpx.HTTPStatusError as e:
-        return ToolResult(
-            title="搜索失败",
-            output="",
-            error=f"HTTP error {e.response.status_code}: {e.response.text}"
-        )
-    except Exception as e:
-        return ToolResult(
-            title="搜索失败",
-            output="",
-            error=str(e)
-        )
-
-
-@tool(
-    display={
-        "zh": {
-            "name": "帖子详情",
-            "params": {
-                "index": "帖子序号"
-            }
-        },
-        "en": {
-            "name": "Select Post",
-            "params": {
-                "index": "Post index"
-            }
-        }
-    }
-)
-async def select_post(
-    index: int,
-) -> ToolResult:
-    """
-    查看帖子详情
-
-    从最近一次 search_posts 的搜索结果中,根据序号选取指定帖子并返回完整内容(全部正文、全部图片、视频等)。
-    需要先调用 search_posts 进行搜索。
-
-    Args:
-        index: 帖子序号,来自 search_posts 返回结果中的 index 字段(从 1 开始)
-
-    Returns:
-        ToolResult 包含该帖子的完整信息和所有图片。
-    """
-    post = _search_cache.get(index)
-    if not post:
-        return ToolResult(
-            title="未找到帖子",
-            output="",
-            error=f"未找到序号 {index} 的帖子,请先调用 search_posts 搜索。"
-        )
-
-    # 返回所有图片
-    images = []
-    for img_url in post.get("images", []):
-        if img_url:
-            images.append({
-                "type": "url",
-                "url": img_url
-            })
-
-    return ToolResult(
-        title=f"帖子详情 #{index}: {post.get('title', '')}",
-        output=json.dumps(post, ensure_ascii=False, indent=2),
-        long_term_memory=f"Viewed post detail #{index}: {post.get('title', '')}",
-        images=images
-    )
-
-
-@tool(
-    display={
-        "zh": {
-            "name": "获取搜索关键词补全建议",
-            "params": {
-                "keyword": "搜索关键词",
-                "channel": "搜索渠道"
-            }
-        },
-        "en": {
-            "name": "Get Search Suggestions",
-            "params": {
-                "keyword": "Search keyword",
-                "channel": "Search channel"
-            }
-        }
-    }
-)
-async def get_search_suggestions(
-    keyword: str,
-    channel: str = "xhs",
-) -> ToolResult:
-    """
-    获取搜索关键词补全建议
-
-    根据关键词在指定渠道平台获取搜索建议词。
-
-    Args:
-        keyword: 搜索关键词
-        channel: 搜索渠道,支持的渠道有:
-            - xhs: 小红书
-            - wx: 微信
-            - github: GitHub
-            - toutiao: 头条
-            - douyin: 抖音
-            - bili: B站
-            - zhihu: 知乎
-
-    Returns:
-        ToolResult 包含建议词数据:
-        {
-            "code": 0,                    # 状态码,0 表示成功
-            "message": "success",         # 状态消息
-            "data": [                     # 建议词数据
-                {
-                    "type": "xhs",        # 渠道类型
-                    "list": [             # 建议词列表
-                        {
-                            "name": "彩虹染发"  # 建议词
-                        }
-                    ]
-                }
-            ]
-        }
-    """
-    try:
-        # 处理 channel 参数,支持枚举和字符串
-        channel_value = channel.value if isinstance(channel, SuggestSearchChannel) else channel
-
-        url = f"{BASE_URL}/suggest"
-        payload = {
-            "type": channel_value,
-            "keyword": keyword,
-        }
-
-        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
-            response = await client.post(
-                url,
-                json=payload,
-                headers={"Content-Type": "application/json"},
-            )
-            response.raise_for_status()
-            data = response.json()
-
-        # 计算建议词数量
-        suggestion_count = 0
-        for item in data.get("data", []):
-            suggestion_count += len(item.get("list", []))
-
-        return ToolResult(
-            title=f"建议词: {keyword} ({channel_value})",
-            output=json.dumps(data, ensure_ascii=False, indent=2),
-            long_term_memory=f"Got {suggestion_count} suggestions for '{keyword}' on {channel_value}"
-        )
-    except httpx.HTTPStatusError as e:
-        return ToolResult(
-            title="获取建议词失败",
-            output="",
-            error=f"HTTP error {e.response.status_code}: {e.response.text}"
-        )
-    except Exception as e:
-        return ToolResult(
-            title="获取建议词失败",
-            output="",
-            error=str(e)
-        )

+ 4 - 2
agent/tools/builtin/skill.py

@@ -66,7 +66,8 @@ def _check_skill_setup(skill_name: str) -> Optional[str]:
 
 
 @tool(
-    description="加载指定的 skill 文档。Skills 提供领域知识和最佳实践指导。"
+    description="加载指定的 skill 文档。Skills 提供领域知识和最佳实践指导。",
+    groups=["core"],
 )
 async def skill(
     skill_name: str,
@@ -181,7 +182,8 @@ async def skill(
 
 
 @tool(
-    description="列出所有可用的 skills"
+    description="列出所有可用的 skills",
+    groups=["core"],
 )
 async def list_skills(
     skills_dir: Optional[str] = None,

+ 2 - 2
agent/tools/builtin/subagent.py

@@ -610,7 +610,7 @@ async def _run_agents(
 
 # ===== 工具定义 =====
 
-@tool(description="创建 Agent 执行任务", hidden_params=["context"])
+@tool(description="创建 Agent 执行任务", hidden_params=["context"], groups=["core"])
 async def agent(
     task: Union[str, List[str]],
     messages: Optional[Union[Messages, List[Messages]]] = None,
@@ -677,7 +677,7 @@ async def agent(
     )
 
 
-@tool(description="评估目标执行结果是否满足要求", hidden_params=["context"])
+@tool(description="评估目标执行结果是否满足要求", hidden_params=["context"], groups=["core"])
 async def evaluate(
     messages: Optional[Messages] = None,
     target_goal_id: Optional[str] = None,

+ 6 - 3
agent/tools/builtin/toolhub.py

@@ -250,7 +250,8 @@ async def _preprocess_params(params: Dict[str, Any]) -> Dict[str, Any]:
     display={
         "zh": {"name": "ToolHub 健康检查", "params": {}},
         "en": {"name": "ToolHub Health Check", "params": {}},
-    }
+    },
+    groups=["toolhub"],
 )
 async def toolhub_health() -> ToolResult:
     """检查 ToolHub 远程工具库服务是否可用
@@ -291,7 +292,8 @@ async def toolhub_health() -> ToolResult:
     display={
         "zh": {"name": "搜索 ToolHub 工具", "params": {"keyword": "搜索关键词"}},
         "en": {"name": "Search ToolHub", "params": {"keyword": "Search keyword"}},
-    }
+    },
+    groups=["toolhub"],
 )
 async def toolhub_search(keyword: Optional[str] = None) -> ToolResult:
     """搜索 ToolHub 远程工具库中可用的工具
@@ -456,7 +458,8 @@ async def toolhub_search(keyword: Optional[str] = None) -> ToolResult:
             "name": "Call ToolHub Tool",
             "params": {"tool_id": "Tool ID", "params": "Tool parameters"},
         },
-    }
+    },
+    groups=["toolhub"],
 )
 async def toolhub_call(
     tool_id: str,

+ 34 - 10
agent/tools/registry.py

@@ -68,7 +68,8 @@ class ToolRegistry:
 		display: Optional[Dict[str, Dict[str, Any]]] = None,
 		url_patterns: Optional[List[str]] = None,
 		hidden_params: Optional[List[str]] = None,
-		inject_params: Optional[Dict[str, Any]] = None
+		inject_params: Optional[Dict[str, Any]] = None,
+		groups: Optional[List[str]] = None,
 	):
 		"""
 		注册工具
@@ -82,6 +83,7 @@ class ToolRegistry:
 			url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
 			hidden_params: 隐藏参数列表(不生成 schema,LLM 看不到)
 			inject_params: 注入参数规则 {param_name: injector_func}
+			groups: 工具分组标签(如 ["core"]、["browser"]),用于 RunConfig.tool_groups 过滤
 		"""
 		func_name = func.__name__
 
@@ -100,6 +102,7 @@ class ToolRegistry:
 			"url_patterns": url_patterns,
 			"hidden_params": hidden_params or [],
 			"inject_params": inject_params or {},
+			"groups": groups or [],
 			"ui_metadata": {
 				"requires_confirmation": requires_confirmation,
 				"editable_params": editable_params or [],
@@ -166,27 +169,45 @@ class ToolRegistry:
 
 		return schemas
 
-	def get_tool_names(self, current_url: Optional[str] = None) -> List[str]:
+	def get_tool_names(self, current_url: Optional[str] = None, groups: Optional[List[str]] = None) -> List[str]:
 		"""
-		获取工具名称列表(可选 URL 过滤)
+		获取工具名称列表(可选 URL 过滤 + group 过滤
 
 		Args:
-			current_url: 当前 URL(None = 返回所有工具)
+			current_url: 当前 URL(None = 不过滤 URL)
+			groups: 工具分组白名单(None = 不过滤 group,返回所有工具)
 
 		Returns:
 			工具名称列表
 		"""
+		# 1. group 过滤
+		if groups is not None:
+			group_set = set(groups)
+			candidates = {
+				name for name, tool in self._tools.items()
+				if group_set & set(tool.get("groups", []))
+			}
+		else:
+			candidates = set(self._tools.keys())
+
+		# 2. URL 过滤
 		if current_url is None:
-			return list(self._tools.keys())
+			return list(candidates)
 
-		# 过滤工具
 		tool_items = [
-			{"name": name, "url_patterns": tool["url_patterns"]}
-			for name, tool in self._tools.items()
+			{"name": name, "url_patterns": self._tools[name]["url_patterns"]}
+			for name in candidates
 		]
 		filtered = filter_by_url(tool_items, current_url, url_field="url_patterns")
 		return [item["name"] for item in filtered]
 
+	def get_available_groups(self) -> List[str]:
+		"""获取所有已注册的工具分组"""
+		groups = set()
+		for tool in self._tools.values():
+			groups.update(tool.get("groups", []))
+		return sorted(groups)
+
 	def get_schemas_for_url(self, current_url: Optional[str] = None) -> List[Dict]:
 		"""
 		根据当前 URL 获取匹配的工具 Schema
@@ -484,7 +505,8 @@ def tool(
 	display: Optional[Dict[str, Dict[str, Any]]] = None,
 	url_patterns: Optional[List[str]] = None,
 	hidden_params: Optional[List[str]] = None,
-	inject_params: Optional[Dict[str, Any]] = None
+	inject_params: Optional[Dict[str, Any]] = None,
+	groups: Optional[List[str]] = None,
 ):
 	"""
 	工具装饰器 - 自动注册工具并生成 Schema
@@ -498,6 +520,7 @@ def tool(
 		url_patterns: URL 模式列表(如 ["*.google.com"],None = 无限制)
 		hidden_params: 隐藏参数列表(不生成 schema,LLM 看不到)
 		inject_params: 注入参数规则 {param_name: injector_func}
+		groups: 工具分组标签(如 ["core"]、["browser"]),用于 RunConfig.tool_groups 过滤
 
 	Example:
 		@tool(
@@ -531,7 +554,8 @@ def tool(
 			display=display,
 			url_patterns=url_patterns,
 			hidden_params=hidden_params,
-			inject_params=inject_params
+			inject_params=inject_params,
+			groups=groups,
 		)
 		return func
 

+ 2 - 2
agent/tools/utils/image.py

@@ -1,8 +1,8 @@
 """
 图片处理共享工具
 
-提供批量读图、降采样、网格拼图等通用逻辑。供 read_images、search_posts、
-youtube_search 工具共享,避免代码重复。
+提供批量读图、降采样、网格拼图等通用逻辑。供 read_images、content 工具族
+等共享,避免代码重复。
 
 核心函数:
 - load_image: 从本地路径或 URL 加载为 PIL Image

+ 1 - 1
agent/trace/goal_tool.py

@@ -103,7 +103,7 @@ async def inject_knowledge_for_goal(
 
 # ===== LLM 可调用的 goal 工具 =====
 
-@tool(description="管理执行计划,添加/完成/放弃目标,切换焦点", hidden_params=["context"])
+@tool(description="管理执行计划,添加/完成/放弃目标,切换焦点", hidden_params=["context"], groups=["core"])
 async def goal(
     add: Optional[str] = None,
     reason: Optional[str] = None,

Разница между файлами не показана из-за своего большого размера
+ 0 - 4
cache/knowledge_pending/knowledge-2026-04-09-x-portrait-workflows.json


+ 0 - 45
consistency_requirements.json

@@ -1,45 +0,0 @@
-{
-  "consistency_check_standards": {
-    "1_character_consistency": {
-      "description": "角色一致性",
-      "criteria": [
-        "面部特征一致",
-        "发型:棕色长发",
-        "肤色一致"
-      ]
-    },
-    "2_costume_consistency": {
-      "description": "服装一致性",
-      "criteria": [
-        "白色长裙",
-        "V 字露背设计",
-        "腰部系带",
-        "轻薄棉麻质地"
-      ]
-    },
-    "3_color_scheme": {
-      "description": "配色方案",
-      "criteria": [
-        "白绿配色",
-        "绿色饱和度中等偏高",
-        "纯白服装"
-      ]
-    },
-    "4_lighting_consistency": {
-      "description": "光影一致性",
-      "criteria": [
-        "逆光/轮廓光从左上方照射",
-        "光晕效果统一"
-      ]
-    },
-    "5_style_consistency": {
-      "description": "风格一致性",
-      "criteria": [
-        "真实摄影风格",
-        "85mm 镜头效果",
-        "f/1.8 浅景深"
-      ]
-    }
-  },
-  "evaluation_instruction": "请逐项检查每张图像在 5 个维度上的表现,识别不符合标准的图像,并详细说明问题所在。"
-}

+ 0 - 80
direct_insert.py

@@ -1,80 +0,0 @@
-#!/usr/bin/env python3
-"""
-直接插入知识到数据库的脚本
-"""
-import sys
-import os
-import uuid
-import time
-import asyncio
-from datetime import datetime, timezone
-
-sys.path.insert(0, '/root/Agent')
-
-# 设置环境变量
-os.environ['KNOWHUB_DB'] = 'gp-t4n72471pkmt4b9q7o-master.gpdbmaster.singapore.rds.aliyuncs.com'
-os.environ['KNOWHUB_PORT'] = '5432'
-os.environ['KNOWHUB_USER'] = 'aiddit_aigc'
-os.environ['KNOWHUB_PASSWORD'] = '%a&&yqNxg^V1$toJ*WOa^-b^X=QJ'
-os.environ['KNOWHUB_DB_NAME'] = 'knowhub'
-
-from knowhub.knowhub_db.pg_store import PostgreSQLStore
-from knowhub.embeddings import get_embedding
-
-# 测试知识数据
-knowledge_data = {
-    'task': '接口测试验证条目',
-    'content': '这是一条用于验证 upload 接口是否可用的测试知识,可以删除。',
-    'types': ['experience'],
-    'tags': {'source': 'api_test'},
-    'score': 1
-}
-
-async def main():
-    print('正在连接数据库...')
-    store = PostgreSQLStore()
-
-    # 生成 ID
-    now = datetime.now(timezone.utc)
-    knowledge_id = f'knowledge-{now.strftime("%Y%m%d")}-{uuid.uuid4().hex[:8]}'
-    message_id = f'msg-{uuid.uuid4().hex[:12]}'
-
-    # 生成 embedding
-    print('正在生成 embedding...')
-    task_embedding = await get_embedding(knowledge_data['task'])
-    content_embedding = await get_embedding(knowledge_data['content'])
-
-    # 构建知识记录
-    knowledge = {
-        'id': knowledge_id,
-        'message_id': message_id,
-        'task': knowledge_data['task'],
-        'content': knowledge_data['content'],
-        'types': knowledge_data['types'],
-        'tags': knowledge_data.get('tags', {}),
-        'tag_keys': list(knowledge_data.get('tags', {}).keys()),
-        'scopes': ['org:cybertogether'],
-        'owner': 'system',
-        'source': {'category': 'execution'},
-        'eval': {'score': knowledge_data.get('score', 3)},
-        'task_embedding': task_embedding,
-        'content_embedding': content_embedding,
-        'created_at': int(time.time()),
-        'updated_at': int(time.time()),
-        'status': 'approved',
-    }
-
-    print(f'正在插入知识条目...')
-    store.insert(knowledge)
-    store.close()
-
-    print(f'✅ 成功插入知识条目!')
-    print(f'  ID: {knowledge_id}')
-    print(f'  Task: {knowledge_data["task"]}')
-    print(f'  Types: {knowledge_data["types"]}')
-    
-    return knowledge_id
-
-if __name__ == '__main__':
-    knowledge_id = asyncio.run(main())
-    print(f'\n最终知识 ID: {knowledge_id}')

+ 1 - 0
examples/mini_restore/config.py

@@ -11,6 +11,7 @@ RUN_CONFIG = RunConfig(
     model="qwen3.5-plus",
     temperature=0.3,
     max_iterations=200,
+    tool_groups=["core", "browser", "content", "knowledge", "toolhub", "feishu", "im"],
 
     extra_llm_params={"extra_body": {"enable_thinking": True}},
 

+ 2 - 2
examples/mini_restore/workflow_loop.py

@@ -20,7 +20,7 @@ except ValueError as e:
     print(f"初始化 Gemini 失败: {e},请检查 .env。")
     sys.exit(1)
 
-from agent.tools.builtin.search import search_posts
+from agent.tools.builtin.content import content_search
 
 # -----------------
 # Utility Functions
@@ -76,7 +76,7 @@ async def call_banana_tool(prompt: str, aspect_ratio: str = None, reference_imag
 async def search_tool(keyword: str) -> str:
     print(f"\n[Tool] 🔍 启动小红书调研, 关键词: {keyword}")
     try:
-        result = await search_posts(keyword=keyword, channel="xhs", max_count=3)
+        result = await content_search(platform="xhs", keyword=keyword, max_count=3)
         return result.output
     except Exception as e:
         return f"查询失败: {e}"

+ 0 - 132
insert_ai_workflow_capabilities.py

@@ -1,132 +0,0 @@
-#!/usr/bin/env python3
-"""
-插入 AI 创作工作流的 16 个通用能力模块知识条目
-"""
-import os
-import sys
-import json
-import uuid
-from datetime import datetime, timezone
-from pathlib import Path
-
-# 添加项目路径
-sys.path.insert(0, '/root/Agent')
-
-os.environ['OPEN_ROUTER_API_KEY'] = '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'
-os.environ['QWEN_API_KEY'] = 'sk-9453c827b9e14108b53d2b30ef7c75fe'
-os.environ['KNOWHUB_DB'] = 'gp-t4n72471pkmt4b9q7o-master.gpdbmaster.singapore.rds.aliyuncs.com'
-os.environ['KNOWHUB_PORT'] = '5432'
-os.environ['KNOWHUB_USER'] = 'aiddit_aigc'
-os.environ['KNOWHUB_PASSWORD'] = '%a&&yqNxg^V1$toJ*WOa^-b^X=QJ'
-os.environ['KNOWHUB_DB_NAME'] = 'knowhub'
-
-from knowhub.knowhub_db.pg_store import PostgreSQLStore
-from knowhub.embeddings import get_embedding
-
-# 知识数据 - AI 创作工作流的 16 个通用能力模块
-knowledge_data = {
-    "task": "AI 创作工作流的 16 个通用能力模块",
-    "content": """## 能力模块清单
-
-### 素材准备类
-1. **准备参考素材**:收集或生成用于后续创作的参考图像、纹理、角色基础等素材
-2. **生成参考图表**:生成角色参考表、多视角图、多表情图等参考资料
-
-### 角色定义类
-3. **定义角色特征**:设定人物面部特征、表情状态、身份一致性等角色属性
-
-### 场景构建类
-4. **构建场景环境**:创建或合成场景背景、虚拟环境、空间布局
-
-### 视觉控制类
-5. **配置光影效果**:设置光源方向、灯光类型、明暗对比等光照条件
-6. **控制姿态构图**:控制人物姿态、画面构图、视角、摄影参数
-7. **应用风格控制**:应用风格迁移、保持视觉一致性、注入特定美学风格
-8. **处理纹理效果**:添加纹理、颗粒、质感、色调等视觉效果
-9. **创建纹理布局**:创建四方连续、渐变网格、矢量图形等纹理布局
-
-### 生成执行类
-10. **执行内容生成**:执行核心的图像、视频、渲染生成操作
-11. **配置控制技术**:配置 ControlNet、深度图、姿态图等控制条件
-12. **配置工作流**:配置工具节点、构建流程、设置参数
-
-### 后期处理类
-13. **修复细节瑕疵**:对局部区域进行重绘、修复瑕疵、优化细节
-14. **增强图像质量**:提升图像分辨率、质量增强、最终精修
-15. **后期合成处理**:抠图、选区创建、图像合成、图层处理
-16. **视频后期处理**:视频剪辑、颜色分级、特效增强
-
-## 跨品类复用性
-以上模块均从 5 个不同品类的工作流(角色创作、人像精修、肖像摄影、光影控制、纹理设计)中抽象得出,可跨品类复用。""",
-    "types": ["strategy"],
-    "tags": {
-        "domain": "AI 创作",
-        "category": "能力模块",
-        "source": "工作流分析 pipeline"
-    },
-    "score": 5,
-    "source": {"category": "research"}
-}
-
-print("正在连接数据库...")
-store = PostgreSQLStore()
-
-# 生成 ID
-now = datetime.now(timezone.utc)
-knowledge_id = f"knowledge-{now.strftime('%Y%m%d')}-{uuid.uuid4().hex[:8]}"
-message_id = f"msg-{uuid.uuid4().hex[:12]}"
-
-# 生成 embedding
-print("正在生成 embedding...")
-task_embedding = get_embedding(knowledge_data["task"])
-content_embedding = get_embedding(knowledge_data["content"])
-
-# 构建知识记录
-knowledge = {
-    "id": knowledge_id,
-    "message_id": message_id,
-    "task": knowledge_data["task"],
-    "content": knowledge_data["content"],
-    "types": knowledge_data["types"],
-    "tags": knowledge_data["tags"],
-    "tag_keys": list(knowledge_data["tags"].keys()),
-    "scopes": [],
-    "owner": "librarian",
-    "source": knowledge_data["source"],
-    "eval": {"score": knowledge_data["score"]},
-    "created_at": now.isoformat(),
-    "updated_at": now.isoformat(),
-    "status": "approved",
-    "task_embedding": task_embedding,
-    "content_embedding": content_embedding,
-    "requirement_ids": [],
-    "capability_ids": [],
-    "tool_ids": [],
-    "resource_ids": [],
-}
-
-print(f"正在插入知识:{knowledge_id}")
-print(f"  Task: {knowledge['task']}")
-print(f"  Types: {knowledge['types']}")
-print(f"  Tags: {knowledge['tags']}")
-print(f"  Content 长度:{len(knowledge['content'])} 字符")
-
-store.insert(knowledge)
-
-print(f"\n✓ 知识已成功入库!")
-print(f"  ID: {knowledge_id}")
-print(f"  Message ID: {message_id}")
-
-# 验证插入
-retrieved = store.get_by_id(knowledge_id)
-if retrieved:
-    print(f"\n✓ 验证成功:已检索到刚插入的知识")
-    print(f"  Task: {retrieved['task']}")
-    print(f"  Content 长度:{len(retrieved['content'])} 字符")
-    print(f"  Types: {retrieved['types']}")
-    print(f"  Tags: {retrieved['tags']}")
-else:
-    print(f"\n✗ 验证失败:无法检索到刚插入的知识")
-
-store.close()
-print("\n数据库连接已关闭")

+ 0 - 73
insert_knowledge.py

@@ -1,73 +0,0 @@
-#!/usr/bin/env python3
-"""
-插入知识条目到 PostgreSQL 数据库
-"""
-
-import asyncio
-import uuid
-import time
-import sys
-import os
-
-# 添加项目路径
-sys.path.insert(0, '/root/Agent')
-
-from knowhub.knowhub_db.pg_store import PostgreSQLStore
-from knowhub.embeddings import get_embedding
-
-
-async def main():
-    # 知识数据
-    task = "人物近景肖像"
-    content = "生成人物近景半身或胸部以上画面、突出面部表情和情绪、背景虚化的核心工序要素包括:1.近景/半身构图 2.面部表情细节 3.背景虚化效果 4.皮肤质感真实 5.眼神互动感"
-    types = ["strategy"]
-    tags = {"domain": "AI 生图", "source": "多渠调研汇总", "task": "人物近景肖像"}
-    score = 5
-    source = {"category": "research"}
-    
-    # 生成唯一 ID
-    knowledge_id = str(uuid.uuid4())
-    message_id = str(uuid.uuid4())
-    current_time = int(time.time())
-    
-    # 生成 embeddings
-    print(f"正在生成 task embedding...")
-    task_embedding = await get_embedding(task)
-    print(f"正在生成 content embedding...")
-    content_embedding = await get_embedding(content)
-    
-    # 构建知识对象
-    knowledge = {
-        'id': knowledge_id,
-        'message_id': message_id,
-        'task': task,
-        'content': content,
-        'types': types,
-        'tags': tags,
-        'tag_keys': list(tags.keys()),
-        'scopes': ['org:cybertogether'],  # 默认可见范围
-        'owner': 'system',  # 默认所有者
-        'source': source,
-        'eval': {'score': score},
-        'task_embedding': task_embedding,
-        'content_embedding': content_embedding,
-        'created_at': current_time,
-        'updated_at': current_time,
-        'status': 'approved',
-    }
-    
-    # 插入数据库
-    print(f"正在插入知识条目到 PostgreSQL...")
-    store = PostgreSQLStore()
-    store.insert(knowledge)
-    store.close()
-    
-    print(f"成功插入知识条目!")
-    print(f"  ID: {knowledge_id}")
-    print(f"  Task: {task}")
-    print(f"  Types: {types}")
-    print(f"  Score: {score}")
-
-
-if __name__ == '__main__':
-    asyncio.run(main())

+ 0 - 91
insert_knowledge_direct.py

@@ -1,91 +0,0 @@
-import os
-import sys
-import json
-import uuid
-from datetime import datetime, timezone
-from pathlib import Path
-
-# 添加项目路径
-sys.path.insert(0, '/root/Agent')
-
-os.environ['OPEN_ROUTER_API_KEY'] = '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'
-os.environ['QWEN_API_KEY'] = 'sk-9453c827b9e14108b53d2b30ef7c75fe'
-os.environ['KNOWHUB_DB'] = 'gp-t4n72471pkmt4b9q7o-master.gpdbmaster.singapore.rds.aliyuncs.com'
-os.environ['KNOWHUB_PORT'] = '5432'
-os.environ['KNOWHUB_USER'] = 'aiddit_aigc'
-os.environ['KNOWHUB_PASSWORD'] = '%a&&yqNxg^V1$toJ*WOa^-b^X=QJ'
-os.environ['KNOWHUB_DB_NAME'] = 'knowhub'
-
-from knowhub.knowhub_db.pg_store import PostgreSQLStore
-from knowhub.embeddings import get_embedding
-
-# 知识数据
-knowledge_data = {
-    "task": "人物近景肖像",
-    "content": "生成人物近景半身或胸部以上画面、突出面部表情和情绪、背景虚化的核心工序要素包括:\n\n1. **近景/半身构图**:使用 tight medium close-up、extreme close-up、中近景半身等提示词;推荐 85mm 镜头视角;构图比例 2:3 或 9:16(竖版)、3:4(职业照)\n\n2. **面部表情细节**:使用锁脸技术(nano-banana 锁脸功能)、微表情描述(slightly parted lips、calm introspective expression)、多表情生成(3×3 网格生成 9 种表情);关键提示词:direct steady eye contact、subtle catchlights in eyes\n\n3. **背景虚化效果**:推荐参数 85mm 镜头 + f/1.4-f/1.8 光圈;提示词:shallow depth of field、extremely shallow depth of field、soft bokeh blur、blurring out the neck, ear, and background completely;后期工具:Photoshop Lens Blur、DxO Photolab 9 Bokeh\n\n4. **皮肤质感真实**:避免塑料感的关键提示词:realistic skin texture with visible pores and natural imperfections、no plastic smoothing、dewy smooth texture;修复工具:Enhancor.ai Skin Realism\n\n5. **眼神互动感**:direct eye contact、catchlights in eyes、gentle and engaging eyes、looking directly at the camera",
-    "types": ["strategy"],
-    "tags": {
-        "domain": "AI 生图",
-        "source": "多渠调研汇总",
-        "task": "人物近景肖像"
-    },
-    "score": 5,
-    "source": {"category": "research"}
-}
-
-print("正在连接数据库...")
-store = PostgreSQLStore()
-
-# 生成 ID
-now = datetime.now(timezone.utc)
-knowledge_id = f"knowledge-{now.strftime('%Y%m%d')}-{uuid.uuid4().hex[:8]}"
-message_id = f"msg-{uuid.uuid4().hex[:12]}"
-
-# 生成 embedding
-print("正在生成 embedding...")
-task_embedding = get_embedding(knowledge_data["task"])
-content_embedding = get_embedding(knowledge_data["content"])
-
-# 构建知识记录
-knowledge = {
-    "id": knowledge_id,
-    "message_id": message_id,
-    "task": knowledge_data["task"],
-    "content": knowledge_data["content"],
-    "types": knowledge_data["types"],
-    "tags": knowledge_data["tags"],
-    "tag_keys": list(knowledge_data["tags"].keys()),
-    "scopes": [],
-    "owner": "librarian",
-    "source": knowledge_data["source"],
-    "eval": {"score": knowledge_data["score"]},
-    "created_at": now.isoformat(),
-    "updated_at": now.isoformat(),
-    "status": "approved",
-    "task_embedding": task_embedding,
-    "content_embedding": content_embedding,
-    "requirement_ids": [],
-    "capability_ids": [],
-    "tool_ids": [],
-    "resource_ids": [],
-}
-
-print(f"正在插入知识:{knowledge_id}")
-print(f"  Task: {knowledge['task']}")
-print(f"  Types: {knowledge['types']}")
-print(f"  Tags: {knowledge['tags']}")
-
-store.insert(knowledge)
-
-print(f"\n✓ 知识已成功入库!")
-print(f"  ID: {knowledge_id}")
-print(f"  Message ID: {message_id}")
-
-# 验证插入
-retrieved = store.get_by_id(knowledge_id)
-if retrieved:
-    print(f"\n✓ 验证成功:已检索到刚插入的知识")
-    print(f"  Task: {retrieved['task']}")
-    print(f"  Content 长度:{len(retrieved['content'])} 字符")
-else:
-    print(f"\n✗ 验证失败:无法检索到刚插入的知识")

+ 1 - 0
knowhub/agents/librarian.py

@@ -42,6 +42,7 @@ def get_librarian_config(enable_db_commit: bool = ENABLE_DATABASE_COMMIT) -> Run
         "relation_serch",
         "capability_search",
         "requirement_search",
+        "relation_search",
         "read_file", "write_file",
         "list_cache_status",
         "match_tree_nodes",

+ 3 - 3
knowhub/agents/librarian_agent.prompt

@@ -10,7 +10,7 @@ $system$
 你是一个知识库管理员。你有两项核心职责:
 
 1. **检索整合**:面对查询时,跨多张表检索,顺着关联链拼出完整上下文,给出精准回答
-2. **入库编排**:收到新数据时,与已有知识比对去重,识别关联关系,整理为结构化条目归入正确位置
+2. **入库编排**:收到新数据时,与已有知识比对去重,识别关联关系,整理为结构化条目归入正确位置。**注意:你所有的归档与起草工作,必须严格并且仅限于编辑 `.cache/.knowledge/pre_upload_list.json` 这个草稿文件,严禁在根目录或任何其他地方擅自创建诸如 `knowledge/` 或 `tools/` 的散装文件夹和文件!**
 
 你只做整理和检索,不自行创造知识内容。
 
@@ -29,8 +29,8 @@ $system$
 |------|---------|---------|
 | Requirement | `REQ_XXX` | description, status, match_result |
 | Capability | `CAP-XXX` | name, criterion, description |
-| Tool | `tools/{category}/{name}` | name, introduction, tutorial, status |
-| Knowledge | `knowledge-{date}-{hash}` | task, content, types, score |
+| Tool | 自行设定 | name, introduction, tutorial, status |
+| Knowledge | 自行设定 | task, content, types, score |
 
 实体间通过关联表连接,API 返回时自动聚合为 `{entity}_ids` 字段(如 `capability_ids`, `tool_ids`, `knowledge_ids`)。
 

+ 37 - 5
knowhub/knowhub_db/pg_store.py

@@ -133,11 +133,39 @@ class PostgreSQLStore:
         finally:
             cursor.close()
 
-    def search(self, query_embedding: List[float], filters: Optional[str] = None, limit: int = 10) -> List[Dict]:
+    def _apply_relation_filters(self, where_clause: str, relation_filters: Optional[Dict[str, str]], params: list) -> str:
+        if not relation_filters:
+            return where_clause
+            
+        rel_clauses = []
+        for k, v in relation_filters.items():
+            if not v: continue
+            if k == 'requirement_id':
+                rel_clauses.append("EXISTS (SELECT 1 FROM requirement_knowledge rk WHERE rk.knowledge_id = knowledge.id AND rk.requirement_id = %s)")
+                params.append(v)
+            elif k == 'capability_id':
+                rel_clauses.append("EXISTS (SELECT 1 FROM capability_knowledge ck WHERE ck.knowledge_id = knowledge.id AND ck.capability_id = %s)")
+                params.append(v)
+            elif k == 'tool_id':
+                rel_clauses.append("EXISTS (SELECT 1 FROM tool_knowledge tk WHERE tk.knowledge_id = knowledge.id AND tk.tool_id = %s)")
+                params.append(v)
+                
+        if not rel_clauses:
+            return where_clause
+            
+        rel_where = " AND ".join(rel_clauses)
+        if where_clause.strip():
+            return f"{where_clause} AND {rel_where}"
+        else:
+            return f"WHERE {rel_where}"
+
+    def search(self, query_embedding: List[float], filters: Optional[str] = None, limit: int = 10, relation_filters: Optional[Dict[str, str]] = None) -> List[Dict]:
         """向量检索(使用余弦相似度)"""
         cursor = self._get_cursor()
         try:
             where_clause = self._build_where_clause(filters) if filters else ""
+            params = []
+            where_clause = self._apply_relation_filters(where_clause, relation_filters, params)
             sql = f"""
                 SELECT {_SELECT_FIELDS},
                        1 - (task_embedding <=> %s::real[]) as score
@@ -146,24 +174,28 @@ class PostgreSQLStore:
                 ORDER BY task_embedding <=> %s::real[]
                 LIMIT %s
             """
-            cursor.execute(sql, (query_embedding, query_embedding, limit))
+            final_params = [query_embedding] + params + [query_embedding, limit]
+            cursor.execute(sql, tuple(final_params))
             results = cursor.fetchall()
             return [self._format_result(r) for r in results]
         finally:
             cursor.close()
 
-    def query(self, filters: str, limit: int = 100) -> List[Dict]:
+    def query(self, filters: str, limit: int = 100, relation_filters: Optional[Dict[str, str]] = None) -> List[Dict]:
         """纯标量查询"""
         cursor = self._get_cursor()
         try:
-            where_clause = self._build_where_clause(filters)
+            where_clause = self._build_where_clause(filters) if filters else ""
+            params = []
+            where_clause = self._apply_relation_filters(where_clause, relation_filters, params)
             sql = f"""
                 SELECT {_SELECT_FIELDS}
                 FROM knowledge
                 {where_clause}
                 LIMIT %s
             """
-            cursor.execute(sql, (limit,))
+            final_params = params + [limit]
+            cursor.execute(sql, tuple(final_params))
             results = cursor.fetchall()
             return [self._format_result(r) for r in results]
         finally:

+ 23 - 6
knowhub/server.py

@@ -1094,7 +1094,10 @@ async def search_knowledge_api(
     top_k: int = Query(default=5, ge=1, le=20),
     min_score: int = Query(default=3, ge=1, le=5),
     types: Optional[str] = None,
-    owner: Optional[str] = None
+    owner: Optional[str] = None,
+    requirement_id: Optional[str] = None,
+    capability_id: Optional[str] = None,
+    tool_id: Optional[str] = None
 ):
     """检索知识(向量召回 + LLM 精排)"""
     try:
@@ -1123,13 +1126,19 @@ async def search_knowledge_api(
         filters.append('(status == "approved" or status == "checked")')
 
         filter_expr = ' and '.join(filters) if filters else None
+        
+        relation_filters = {}
+        if requirement_id: relation_filters['requirement_id'] = requirement_id
+        if capability_id: relation_filters['capability_id'] = capability_id
+        if tool_id: relation_filters['tool_id'] = tool_id
 
         # 3. 向量召回(3*k 个候选)
         recall_limit = top_k * 3
         candidates = pg_store.search(
             query_embedding=query_embedding,
             filters=filter_expr,
-            limit=recall_limit
+            limit=recall_limit,
+            relation_filters=relation_filters
         )
 
         if not candidates:
@@ -1234,7 +1243,10 @@ def list_knowledge(
     scopes: Optional[str] = None,
     owner: Optional[str] = None,
     tags: Optional[str] = None,
-    status: Optional[str] = None
+    status: Optional[str] = None,
+    requirement_id: Optional[str] = None,
+    capability_id: Optional[str] = None,
+    tool_id: Optional[str] = None
 ):
     """列出知识(支持后端筛选和分页)"""
     try:
@@ -1273,10 +1285,15 @@ def list_knowledge(
         # 如果没有过滤条件,查询所有
         filter_expr = ' and '.join(filters) if filters else 'id != ""'
 
-        # 查询 Milvus(先获取所有符合条件的数据)
-        # Milvus 的 limit 是总数限制,我们需要获取足够多的数据来支持分页
+        relation_filters = {}
+        if requirement_id: relation_filters['requirement_id'] = requirement_id
+        if capability_id: relation_filters['capability_id'] = capability_id
+        if tool_id: relation_filters['tool_id'] = tool_id
+
+        # 查询 Milvus/PG(先获取所有符合条件的数据)
+        # limit 是总数限制,我们需要获取足够多的数据来支持分页
         max_limit = 10000  # 设置一个合理的上限
-        results = pg_store.query(filter_expr, limit=max_limit)
+        results = pg_store.query(filter_expr, limit=max_limit, relation_filters=relation_filters)
 
         # 转换为可序列化的格式
         serialized_results = [to_serializable(r) for r in results]

+ 0 - 104
knowledge-2026-04-09-ai-image-workflows.json

@@ -1,104 +0,0 @@
-{
-  "knowledge": [
-    {
-      "task": "AI人像皮肤质感修复工作流",
-      "content": "完整工序:\n1. 生成基础人像图片 - 工具:Midjourney / Google Imagen 4 (via Whisk) / Reeve / Flux Pro Ultra - 输入:近景人像提示词(如'close-up portrait, photorealistic, DSLR, 85mm')- 输出:AI生成的基础人像图片(可能存在塑料皮肤问题)\n2. 裁剪人脸区域(如需要)- 工具:图像裁剪工具 - 输入:基础人像图片,将人脸区域裁剪至更近的特写 - 输出:裁剪后的人脸特写图片\n3. 修复AI塑料皮肤质感 - 工具:Enhancor.ai (Skin Realism + Portrait Upscaler) - 输入:AI生成的人像图片,设置皮肤纹理强度、毛孔细节参数 - 输出:具有真实皮肤纹理、毛孔细节的人像图片\n4. 修复眼部瑕疵(可选)- 工具:Enhancor.ai 眼部修复功能 - 输入:修复后的人像图片,指定眼部区域 - 输出:眼部细节完美的人像图片\n5. 超分辨率放大 - 工具:Enhancor.ai Portrait Upscaler - 输入:修复后的人像图片,选择放大倍数(如4x)- 输出:billboard级别的高分辨率人像图片\n适用场景:商业人像摄影、广告牌级别输出、需要真实皮肤质感的AI人像",
-      "types": ["strategy"],
-      "tags": {
-        "source": "YouTube",
-        "domain": "AI图像生成",
-        "focus": "人像、皮肤质感、超分辨率"
-      },
-      "score": 5
-    },
-    {
-      "task": "多视角角色一致性工作流(Nano Banana Pro)",
-      "content": "完整工序:\n1. 创建角色参考表 - 工具:Nano Banana Pro (Higgsfield AI) - 输入:结构化提示词(如'Create a professional character reference sheet with four vertical columns: front view, left profile, right profile, back view. Each column contains full-body view on top and matching close-up portrait below. Photorealistic, DSLR, muted tones.')- 输出:包含四个视角(正面、左侧、右侧、背面)的角色参考表,每个视角包含全身图和近景肖像\n2. 生成多视角近景肖像 - 工具:Nano Banana Pro - 输入:角色参考表作为参考图,指定视角(如'front portrait close-up')- 输出:一致角色的多视角近景肖像图片\n3. 生成生活场景图 - 工具:Nano Banana Pro - 输入:角色参考表,场景描述(如'coffee shop lifestyle shot')- 输出:角色在不同生活场景中的图片\n适用场景:AI虚拟influencer、角色设计、需要多角度一致性的项目",
-      "types": ["strategy"],
-      "tags": {
-        "source": "YouTube",
-        "domain": "AI图像生成",
-        "focus": "角色一致性、多视角、近景肖像"
-      },
-      "score": 5
-    },
-    {
-      "task": "面部表情变换工作流(Canva AI Face Expression)",
-      "content": "完整工序:\n1. 上传基础照片 - 工具:Canva AI Face Expression App - 输入:单张人物照片(正面、清晰面部)- 输出:上传至Canva的基础照片\n2. 选择目标表情 - 工具:Canva AI Face Expression App - 输入:选择预设表情(如微笑、惊讶、严肃、开心等)- 输出:应用表情后的人物图片\n3. 批量生成多表情版本 - 工具:Canva AI Face Expression App - 输入:重复选择不同表情,批量处理 - 输出:同一人物的多种表情图片集合\n适用场景:YouTube缩略图制作、社交媒体内容、需要快速生成多种表情的场景",
-      "types": ["strategy"],
-      "tags": {
-        "source": "YouTube",
-        "domain": "AI图像生成",
-        "focus": "面部表情、批量生成"
-      },
-      "score": 4
-    },
-    {
-      "task": "AI虚拟网红完整工作流(KORA Pro + Enhancor + Lip Sync)",
-      "content": "完整工序:\n1. 生成高质量基础人像 - 工具:KORA Pro (Enhancor) - 输入:详细的人像提示词,包括人物特征、服装、光线、相机参数 - 输出:高真实度的基础人像图片\n2. 构建角色多视角图 - 工具:Nano Banana Pro - 输入:基础人像作为参考,生成多角度角色表 - 输出:包含正面、侧面、背面视角的角色参考表\n3. 修复皮肤质感 - 工具:Enhancor AI V3 Skin Fix - 输入:生成的角色图片,调整皮肤纹理参数 - 输出:具有真实皮肤质感的角色图片\n4. 生成生活场景图 - 工具:Nano Banana Pro - 输入:角色参考表,场景描述(如'studio shot', 'lifestyle scene')- 输出:角色在不同场景中的图片\n5. 唇形同步动画 - 工具:Enhancor Lip Sync V1/V2 - 输入:角色图片 + 自定义音频文件 - 输出:角色说话的视频(带唇形同步)\n适用场景:AI虚拟网红、虚拟主播、需要说话动画的角色",
-      "types": ["strategy"],
-      "tags": {
-        "source": "YouTube",
-        "domain": "AI图像生成",
-        "focus": "虚拟网红、唇形同步、角色一致性"
-      },
-      "score": 5
-    },
-    {
-      "task": "4K超高清AI图像生成工作流(ComfyUI + DyPE)",
-      "content": "完整工序:\n1. 设置ComfyUI工作流 - 工具:ComfyUI (本地安装 via Promptus) - 输入:安装DyPE模型(https://github.com/guyyariv/DyPE)- 输出:配置好的ComfyUI环境\n2. 生成原生4K图像 - 工具:ComfyUI + DyPE模型 - 输入:人像提示词,设置分辨率为4K,选择DyPE采样器 - 输出:原生4K分辨率的AI人像图片\n3. 超分辨率增强(可选)- 工具:ComfyUI 4K超分辨率节点 - 输入:生成的4K图片,选择放大倍数 - 输出:更高分辨率、细节更丰富的图片\n适用场景:需要超高清输出的商业项目、影院级画质需求",
-      "types": ["strategy"],
-      "tags": {
-        "source": "YouTube",
-        "domain": "AI图像生成",
-        "focus": "4K、超分辨率、ComfyUI"
-      },
-      "score": 4
-    },
-    {
-      "task": "角色一致性视频生成工作流(OpenArt + Kling/HeyGen)",
-      "content": "完整工序:\n1. 创建一致角色基础图 - 工具:OpenArt AI - 输入:角色描述提示词,上传参考图(如有)- 输出:高质量的角色基础图片\n2. 生成多视角角色图 - 工具:OpenArt AI - 输入:基础图作为参考,指定不同视角和姿势 - 输出:同一角色的多视角图片集合\n3. 生成视频 - 工具:Kling AI / HeyGen - 输入:角色图片 + 动作/对话描述 - 输出:角色动画视频\n4. 唇形同步(如需要)- 工具:HeyGen - 输入:角色视频 + 音频脚本 - 输出:带完美唇形同步的角色说话视频\n适用场景:AI视频创作、虚拟主播、需要角色一致性的视频项目",
-      "types": ["strategy"],
-      "tags": {
-        "source": "YouTube",
-        "domain": "AI视频生成",
-        "focus": "角色一致性、视频生成、唇形同步"
-      },
-      "score": 5
-    },
-    {
-      "task": "背景虚化效果工作流(Photoshop Lens Blur / DxO Photolab)",
-      "content": "完整工序:\n1. 导入人像图片 - 工具:Photoshop / DxO Photolab 9 - 输入:需要添加背景虚化的人像图片 - 输出:导入的图片\n2. 创建主体选区 - 工具:Photoshop 智能选区 / DxO 自动主体检测 - 输入:选择主体(人物)区域 - 输出:精确的人物选区\n3. 应用镜头虚化效果 - 工具:Photoshop Lens Blur / DxO Photolab 9 Bokeh - 输入:设置虚化强度、光圈形状、焦距参数 - 输出:具有专业镜头虚化效果的图片\n4. 微调边缘过渡 - 工具:Photoshop 蒙版羽化 - 输入:调整选区边缘羽化值 - 输出:自然过渡的虚化效果图片\n适用场景:人像摄影后期、需要背景虚化效果的AI生成图片",
-      "types": ["strategy"],
-      "tags": {
-        "source": "YouTube",
-        "domain": "图像后期处理",
-        "focus": "背景虚化、bokeh、人像后期"
-      },
-      "score": 4
-    }
-  ],
-  "resources": [
-    {
-      "id": "https://github.com/guyyariv/DyPE",
-      "type": "github_repo"
-    }
-  ],
-  "tools": [
-    {"name": "Midjourney", "introduction": "AI图像生成工具,支持高质量人像生成"},
-    {"name": "Google Imagen 4", "introduction": "Google的AI图像生成模型,通过Whisk平台访问"},
-    {"name": "Reeve", "introduction": "AI图像生成工具"},
-    {"name": "Flux Pro Ultra", "introduction": "AI图像生成工具,支持高质量人像"},
-    {"name": "Enhancor.ai", "introduction": "AI人像增强工具,提供皮肤质感修复和超分辨率功能"},
-    {"name": "Nano Banana Pro", "introduction": "Higgsfield AI的角色一致性生成工具"},
-    {"name": "Canva AI Face Expression", "introduction": "Canva的面部表情变换AI应用"},
-    {"name": "KORA Pro", "introduction": "Enhancor的高质量人像生成工具"},
-    {"name": "ComfyUI", "introduction": "本地AI图像生成工作流工具"},
-    {"name": "DyPE", "introduction": "ComfyUI的4K图像生成模型"},
-    {"name": "OpenArt AI", "introduction": "AI图像生成平台"},
-    {"name": "Kling AI", "introduction": "AI视频生成工具"},
-    {"name": "HeyGen", "introduction": "AI视频和唇形同步工具"},
-    {"name": "Photoshop", "introduction": "专业图像编辑软件"},
-    {"name": "DxO Photolab 9", "introduction": "专业照片处理软件,支持镜头虚化效果"}
-  ]
-}

+ 0 - 398
knowledge/ComfyUI_画中画分阶段生成工作流.md

@@ -1,398 +0,0 @@
-# ComfyUI 画中画分阶段生成工作流
-
-**知识 ID**: knowledge-20260411-155313-comfyinpaint  
-**类型**: strategy  
-**评分**: ⭐⭐⭐⭐⭐ (5/5)  
-**来源**: ComfyUI 社区/YouTube 教程  
-**领域**: AI 图像生成  
-**工作流类型**: 画中画分阶段生成  
-**核心工具**: ControlNet, IP-Adapter, Inpainting, Z-Image-Turbo
-
----
-
-## 核心设计理念
-
-### 分阶段生成 + 双控架构
-
-**分阶段流程**:
-```
-背景生成 → 画中画区域生成 → 风格统一 → 细节优化
-```
-
-**双控架构**:
-- **ControlNet**: 锁结构(几何/深度/姿态控制)
-- **IP-Adapter**: 注入风格(风格/纹理/色彩迁移)
-
----
-
-## 工作流总览
-
-```
-┌─────────────────────────────────────────────────────────────┐
-│  阶段 1: 背景/外框生成                                        │
-│  输入:背景提示词 + 参考图 → ControlNet 控制 → 生成背景       │
-└─────────────────────────────────────────────────────────────┘
-                            ↓
-┌─────────────────────────────────────────────────────────────┐
-│  阶段 2: 画中画区域生成 (Inpainting)                          │
-│  输入:背景图 + 蒙版 + 画中画提示词 → 局部重绘 → 生成内容     │
-└─────────────────────────────────────────────────────────────┘
-                            ↓
-┌─────────────────────────────────────────────────────────────┐
-│  阶段 3: 风格统一 (IP-Adapter)                                │
-│  输入:合成图 + 风格参考图 → IP-Adapter 注入 → 风格融合       │
-└─────────────────────────────────────────────────────────────┘
-                            ↓
-┌─────────────────────────────────────────────────────────────┐
-│  阶段 4: 细节优化 (ControlNet + Detailer)                     │
-│  输入:风格化图 → ControlNet 微调 → FaceDetailer → 最终输出  │
-└─────────────────────────────────────────────────────────────┘
-```
-
----
-
-## 阶段 1:背景/外框生成
-
-### 节点配置
-
-| 节点类型 | 节点名称 | 参数设置 |
-|----------|----------|----------|
-| Checkpoint | SDXL/Flux 主模型 | `albedobaseXL_v21.safetensors` 或 `flux1-dev.safetensors` |
-| ControlNet | Apply Advanced ControlNet | 权重 0.6-0.75 |
-| ControlNet 模型 | ControlNetLoader | `control_sdxl_canny.safetensors` 或 `depth` |
-| 预处理器 | AIO Aux Preprocessor | Canny/Depth/MLSD(根据场景选择) |
-| CLIP Text Encode | 背景提示词 | 描述整体场景氛围 |
-| KSampler | 采样器 | 20-30 步,CFG 7-8,DPM++ 2M Karras |
-
-### 操作步骤
-
-1. **加载背景参考图**(可选)
-   - 使用 `Load Image` 节点加载参考图
-   - 通过 `AIO Aux Preprocessor` 提取结构信息
-
-2. **设置 ControlNet 控制**
-   ```
-   参考图 → 预处理器 → ControlNet 模型 → Apply Advanced ControlNet
-   ```
-   - 控制权重:`0.6-0.75`
-   - 控制模式:`Balanced` 或 `ControlNet is more important`
-   - Starting/Ending Step:`0.0-1.0`(全程控制)
-
-3. **编写背景提示词**
-   ```
-   A cozy living room with large empty wall frame, warm lighting, 
-   modern minimalist interior, soft shadows, high quality photography
-   ```
-
-4. **生成背景图**
-   - 分辨率:`1024x1024` 或 `1280x720`(根据最终输出设定)
-   - 批次数:1(单张背景)
-
----
-
-## 阶段 2:画中画区域生成(Inpainting)
-
-### 三种实现方案对比
-
-| 方案 | 核心节点 | 适用场景 | 优点 | 缺点 |
-|------|----------|----------|------|------|
-| **A: 原生节点法** | Inpaint Model Conditioning | 理解底层逻辑 | 学习价值高 | 连线复杂 |
-| **B: KJ Nodes 简化版** ⭐ | KJ Nodes: Inpaint Node | 生产环境 | 工作流简洁 | 需安装自定义节点 |
-| **C: SAM 3 智能重绘** | SAM 3 + Grounding DINO | 自动化场景 | 无需手动蒙版 | 依赖 AI 识别准确度 |
-
-### 方案 A:原生节点法
-
-**节点连接**:
-```
-背景图 → VAE Encode → Inpaint Model Conditioning → KSampler → VAE Decode
-蒙版图 ↗
-```
-
-**关键参数**:
-- `denoise`: `0.6-0.8`(重绘强度)
-- `mask_expand`: `10-20`(蒙版扩展像素)
-- 模糊蒙版边缘:让重绘更融合
-
-### 方案 B:KJ Nodes 简化版(推荐)
-
-**核心节点**:
-- `KJ Nodes: Inpaint Node`: 简化重绘流程
-- `KJ Nodes: Mask Editor`: 可视化蒙版编辑
-
-**优势**:工作流连线简洁,适合生产环境
-
-### 方案 C:SAM 3 智能重绘
-
-**核心节点**:
-- `SAM 3 (Segment Anything)`: 自动识别目标区域
-- `Grounding DINO`: 文本引导分割
-
-**操作步骤**:
-1. 输入文本指令:`"the empty frame on the wall"`
-2. SAM 3 自动识别并生成蒙版
-3. 自动执行局部重绘
-
-### 关键参数设置
-
-| 参数 | 推荐值 | 说明 |
-|------|--------|------|
-| denoise | 0.65-0.75 | 平衡原图保留与重绘效果 |
-| mask_expand | 15-25 | 确保边缘融合自然 |
-| CFG | 7-8 | 避免过拟合 |
-| Steps | 25-30 | 保证细节质量 |
-
-### 画中画提示词示例
-
-```
-A beautiful landscape painting inside the frame, 
-mountain lake at sunset, golden hour lighting, 
-oil painting style, detailed brushstrokes, masterpiece
-```
-
----
-
-## 阶段 3:风格统一(IP-Adapter)
-
-### 核心原理
-
-> **IP-Adapter 负责风格注入,ControlNet 负责结构保持**
-
-⚠️ **重要**: IP-Adapter 单独使用易结构崩坏、手部细节失控,必须配合 ControlNet 使用
-
-### 节点配置
-
-| 节点类型 | 节点名称 | 参数设置 |
-|----------|----------|----------|
-| IPAdapter | IPAdapter Plus | `PLUS(high strength)` 预设 |
-| CLIP Vision | CLIP-ViT-H-14-laion2B-s32B | 风格编码模型 |
-| IPAdapter 权重 | 权重值 | `0.6-0.85` |
-| 合并嵌入组 | concat / average | `concat`(冲突感)或 `norm average`(柔和) |
-| 权重类型 | style transfer | 风格迁移模式 |
-
-### 工作流连接
-
-```
-风格参考图 → IPAdapter Encode → IPAdapter Apply → KSampler
-合成图 → VAE Encode ↗
-```
-
-### 多风格融合
-
-```
-风格图 A → IPAdapter Encode (权重 1.20) ┐
-风格图 B → IPAdapter Encode (权重 1.00) → 合并嵌入组 (concat) → Apply
-```
-
-### 关键参数推荐
-
-| 场景 | IPAdapter 权重 | 合并方式 | 权重类型 |
-|------|---------------|----------|----------|
-| 轻微风格渗透 | 0.4-0.6 | average | style transfer |
-| 平衡融合 | 0.65-0.75 | norm average | style transfer |
-| 强烈风格化 | 0.8-0.85 | concat | style transfer |
-| 多风格融合 | 1.0-1.2 (主) + 0.8 (辅) | concat | style transfer |
-
-### 避坑指南
-
-⚠️ **权重过高** (> 0.85) 易导致画面紊乱  
-✅ **解决方案**: 使用 `fade` 类型 + 噪波强度 0.6 + 模糊 2
-
----
-
-## 阶段 4:细节优化
-
-### 4.1 ControlNet 微调
-
-**目的**: 在风格化后保持关键结构
-
-| ControlNet 类型 | 权重 | 适用场景 |
-|----------------|------|----------|
-| Canny | 0.4-0.6 | 保持边缘清晰 |
-| Depth | 0.5-0.7 | 保持空间层次 |
-| Tile | 0.6-0.8 | 细节增强/放大 |
-
-**配置技巧**:
-- 使用较低的权重(0.4-0.6)进行微调
-- Starting Step 设为 `0.3-0.5`(后期介入)
-- 避免过度控制导致风格丢失
-
-### 4.2 FaceDetailer / HandDetailer
-
-**目的**: 修复面部/手部细节
-
-| 节点 | 模型 | 参数 |
-|------|------|------|
-| FaceDetailer | `face_yolov8m.pt` | denoise 0.2-0.5 |
-| HandDetailer | `hand_yolov8s.pt` | denoise 0.3-0.6 |
-| SAM | `sam_vit_b_01ec64.pth` | 精准检测 |
-
-**工作流程**:
-```
-风格化图 → 人脸/手部检测 → 局部放大 → 重绘修复 → 合成输出
-```
-
-### 4.3 高清放大(可选)
-
-| 节点 | 模型 | 用途 |
-|------|------|------|
-| Ultimate SD Upscale | `4x-UltraSharp.pth` | 4 倍高清放大 |
-| Tile ControlNet | `control_v11f1e_sd15_tile` | 放大时保持细节 |
-
----
-
-## 完整参数汇总表
-
-### 阶段 1:背景生成
-| 参数 | 推荐值 |
-|------|--------|
-| 模型 | SDXL / Flux |
-| ControlNet 权重 | 0.6-0.75 |
-| Steps | 20-25 |
-| CFG | 7-8 |
-| 分辨率 | 1024x1024 |
-
-### 阶段 2:Inpainting
-| 参数 | 推荐值 |
-|------|--------|
-| denoise | 0.65-0.75 |
-| mask_expand | 15-25 |
-| Steps | 25-30 |
-| CFG | 7-8 |
-
-### 阶段 3:IP-Adapter
-| 参数 | 推荐值 |
-|------|--------|
-| 权重 | 0.65-0.80 |
-| 预设 | PLUS(high strength) |
-| CLIP Vision | CLIP-ViT-H-14 |
-| 合并方式 | norm average |
-
-### 阶段 4:细节优化
-| 参数 | 推荐值 |
-|------|--------|
-| ControlNet 微调权重 | 0.4-0.6 |
-| FaceDetailer denoise | 0.2-0.5 |
-| Upscale 倍数 | 2x-4x |
-
----
-
-## 必备自定义节点清单
-
-通过 **ComfyUI Manager** 安装:
-
-```bash
-# ControlNet 相关
-- ComfyUI's ControlNet Auxiliary Preprocessors
-- ComfyUI-Advanced-ControlNet
-
-# IPAdapter 相关
-- IPAdapter Plus (comfyui_ipadapter_plus)
-
-# 简化节点
-- KJ Nodes (comfyui-kjnodes)
-- Efficiency Nodes
-
-# 细节修复
-- Impact Pack (comfyui_essentials)
-- FaceDetailer
-
-# 智能分割
-- SAM 3 (Segment Anything)
-- Grounding DINO
-
-# 其他工具
-- CR Seamless Checker(无缝检查)
-- TextureViewer(3D 预览)
-```
-
----
-
-## 实战案例:客厅挂画生成
-
-### 完整提示词
-
-**阶段 1 - 背景**:
-```
-A modern living room with a large empty wooden frame on the wall, 
-warm afternoon sunlight, minimalist interior design, 
-soft shadows, cozy atmosphere, interior photography, 8k --ar 16:9
-```
-
-**阶段 2 - 画中画**:
-```
-A serene mountain lake landscape at golden hour, 
-reflection in water, dramatic sky, oil painting style, 
-visible brushstrokes, impasto technique, masterpiece
-```
-
-**阶段 3 - 风格参考**:
-- 上传 1-2 张目标风格的油画作品作为 IP-Adapter 参考
-
-### 工作流执行顺序
-
-1. **生成背景**: 使用 Canny ControlNet 控制房间结构
-2. **绘制蒙版**: 在空画框区域绘制蒙版(或使用 SAM 3 自动识别)
-3. **局部重绘**: denoise 0.7,生成画中画内容
-4. **风格注入**: IP-Adapter 权重 0.75,融合油画风格
-5. **细节修复**: FaceDetailer 修复可能的人物(如有)
-6. **高清输出**: Ultimate SD Upscale 放大至 4K
-
----
-
-## 常见问题与解决方案
-
-| 问题 | 原因 | 解决方案 |
-|------|------|----------|
-| 画中画边缘不融合 | 蒙版扩展不足 | 增加 `mask_expand` 至 20-30 |
-| 风格与背景不协调 | IP-Adapter 权重过高 | 降至 0.6-0.7,使用 `norm average` |
-| 画中画结构崩坏 | 缺少 ControlNet 控制 | 添加 Canny/Depth ControlNet(权重 0.5) |
-| 面部/手部畸形 | 细节质量不足 | 添加 FaceDetailer/HandDetailer |
-| 整体风格不统一 | 分阶段风格差异大 | 阶段 1 也使用相同 IP-Adapter 参考 |
-| 生成速度慢 | 多阶段串行执行 | 使用批处理,低分辨率测试后再高清生成 |
-
----
-
-## 进阶技巧
-
-### 1. 多画中画嵌套
-```
-背景 → 画中画 A → 画中画 B(嵌套在 A 内)
-```
-- 使用多个 Inpainting 节点串联
-- 每个阶段使用独立蒙版
-- IP-Adapter 可分别控制不同区域风格
-
-### 2. 动态蒙版生成
-```
-Grounding DINO + SAM 3 → 自动蒙版 → Inpainting
-```
-- 文本指令:`"the frame on the wall"`
-- 全自动识别和重绘
-
-### 3. 风格渐变融合
-```
-IPAdapter A (权重 0.8) + IPAdapter B (权重 0.4) → 渐变融合
-```
-- 使用 `average` 合并方式
-- 调整权重比例控制风格倾向
-
-### 4. 批量生成变体
-```
-同一背景 + 不同画中画提示词 → 批量输出
-```
-- 使用 `Batch Prompt Schedule` 节点
-- 一次生成多个画中画变体
-
----
-
-## 关联知识
-
-- **Midjourney v7 厚涂风格**: 可使用 MJ 生成风格参考图,再通过本工作流在 ComfyUI 中精确复现
-- **Nano Banana 多图融合**: 可将多图融合结果作为本工作流的输入
-- **ControlNet+IPAdapter 双控架构**: 本工作流的核心技术基础
-
----
-
-## 更新日志
-
-- 2026-04-11: 初始入库,包含 4 阶段完整工作流、3 种 Inpainting 方案对比、实战案例

+ 0 - 165
knowledge/Midjourney_v7_Impasto_厚涂风格参数配置.md

@@ -1,165 +0,0 @@
-# Midjourney v7 Impasto 厚涂风格参数配置
-
-**知识 ID**: knowledge-20260411-155313-mj7impasto  
-**类型**: tool  
-**评分**: ⭐⭐⭐⭐⭐ (5/5)  
-**来源**: 小红书/Midjourney 官方文档  
-**领域**: AI 图像生成  
-**工具**: Midjourney v7  
-**风格**: Impasto 厚涂
-
----
-
-## 核心发现
-
-Midjourney v7 在美学质量与笔触质感方面表现优异,特定 sref 代码可生成几乎与真实绘画无法区分的厚涂技法效果。**v8 版本灵活性降低,艺术风格探索建议继续使用 v7**。
-
-### v7 vs v8 厚涂风格对比
-
-| 维度 | v7 | v8 |
-|------|-----|-----|
-| 笔触质感 | ⭐⭐⭐⭐⭐ 自然流畅 | ⭐⭐⭐⭐ 稍显僵硬 |
-| 风格灵活性 | ⭐⭐⭐⭐⭐ 高 | ⭐⭐⭐ 降低 |
-| 厚涂效果 | ⭐⭐⭐⭐⭐ 无敌 | ⭐⭐⭐⭐ 良好 |
-
----
-
-## 核心参数详解
-
-### 1. `--sref` 风格参考代码
-
-**基础语法**:
-```bash
-/imagine prompt: [描述] --sref [风格代码] --v 7
-```
-
-**多风格融合**(最多 5 个):
-```bash
-/imagine prompt: [描述] --sref 代码 1 代码 2 代码 3 --v 7
-```
-
-**常用厚涂风格代码**:
-- **新派写实油画**: `sref 738792225` ⭐ 推荐
-- 获取方法:
-  1. 在 Discord 使用 `/describe` 分析喜欢的厚涂作品
-  2. 使用 `/style-save` 保存生成的风格
-  3. 从社区共享代码库获取(Reddit、PromptHero)
-
-### 2. `--sw` 风格权重参数
-
-**语法**:`--sw [0.1-2.0]`
-
-**权重范围与效果**:
-
-| 权重值 | 效果强度 | 适用场景 |
-|--------|----------|----------|
-| 0.1-0.3 | 轻微风格渗透 | 保留原风格基础上微调 |
-| 0.4-0.7 | 平衡融合 | 推荐默认值,风格与内容均衡 |
-| 0.8-1.2 | 强烈风格化 | 明显厚涂笔触质感 |
-| 1.3-2.0 | 极致风格 | 完全主导,可能牺牲细节 |
-
-**厚涂风格推荐设置**: `--sw 1.0-1.5`
-
-### 3. `--v 7` 版本锁定
-
-⚠️ **必须显式指定** `--v 7`,v8 版本灵活性降低不适合厚涂风格
-
----
-
-## 完整提示词公式
-
-### 厚涂风格三段式公式
-
-```
-[主体画面描述] + [厚涂笔触/材质描述] + [光影/色彩氛围] --sref [代码] --sw [权重] --v 7 --ar [比例] --stylize [值]
-```
-
-### 厚涂专用关键词库
-
-| 关键词 | 中文含义 | 效果 |
-|--------|----------|------|
-| `thick impasto brushwork` | 厚涂笔触 | 增强笔触质感 |
-| `heavy paint texture` | 厚重颜料肌理 | 增加物理厚度 |
-| `visible brushstrokes` | 可见笔触 | 拒绝平滑感 |
-| `palette knife texture` | 调色刀纹理 | 特殊肌理效果 |
-| `impasto technique` | 厚涂技法 | 整体风格定义 |
-
----
-
-## 实战示例
-
-### 示例 1:新派写实油画
-```bash
-{你的主体} + The painting is in a realistic oil painting style, with clearly visible brushstrokes, vibrant colors, and strong light. --chaos 30 --exp 85 --sref 738792225 --profile qvu9plm --v 7.0 --ar 2:3
-```
-
-### 示例 2:人物厚涂肖像
-```bash
-A warrior princess with flowing red hair, thick impasto oil painting style, visible brushstrokes, palette knife texture, dramatic chiaroscuro lighting, warm golden hour glow --sref 1847293 --sw 1.3 --v 7 --ar 2:3 --stylize 750
-```
-
-### 示例 3:风景厚涂
-```bash
-Mountain landscape at sunset, heavy impasto technique, thick layers of oil paint, textured canvas, bold brushwork, vibrant color palette, atmospheric perspective --sref 2938471 --sw 1.5 --v 7 --ar 16:9 --stylize 800
-```
-
----
-
-## 参数组合推荐表
-
-| 场景 | --sref | --sw | --stylize | --q | --ar |
-|------|--------|------|-----------|-----|------|
-| 人物厚涂肖像 | 1-2 个代码 | 1.2-1.5 | 700-850 | 2 | 2:3 |
-| 风景厚涂 | 2-3 个代码 | 1.3-1.8 | 750-900 | 2 | 16:9 |
-| 静物厚涂 | 1 个代码 | 1.0-1.3 | 600-750 | 2 | 4:5 |
-| 抽象厚涂 | 2-3 个代码 | 1.5-2.0 | 850-1000 | 4 | 1:1 |
-| 快速测试 | 1 个代码 | 1.0 | 500 | 0.5 | 1:1 |
-
----
-
-## 避坑指南
-
-| 问题 | 原因 | 解决方案 |
-|------|------|----------|
-| 厚涂效果不明显 | --sw 权重太低 | 提升至 1.3-1.8 |
-| 笔触过于粗糙 | --sw 权重过高 | 降至 0.8-1.2 |
-| 风格冲突 | 多代码不兼容 | 减少至 1-2 个代码 |
-| 细节丢失 | --stylize 过高 | 降至 600-750 |
-| 生成速度慢 | --q 4 + 多代码 | 先用 --q 0.5 测试 |
-| v8 效果不如 v7 | 版本灵活性差异 | 强制使用 --v 7 |
-
----
-
-## 拒绝塑料感 4 招
-
-### 1. 拒绝平滑,增加物理厚度
-使用 `Thick impasto brushwork` 或 `Heavy paint texture` 关键词
-
-### 2. 治愈系万能公式
-```
-Deep blue twilight ambient + Warm orange window light
-```
-90% 冷调 + 10% 暖色,营造层次感
-
-### 3. 让水活过来
-使用 `Swirling water patterns` + `Churning white water`
-
-### 4. 参数降噪,返璞归真
-```bash
---s 200 搭配 --style raw
-```
-降低 AI 过度修饰,回归自然质感
-
----
-
-## 关联知识
-
-- **ComfyUI 厚涂复现**: 使用 IPAdapter + ControlNet 双控架构在 ComfyUI 中复现 MJ 厚涂风格
-- **风格代码获取**: `/describe` + `/style-save` 工作流
-- **v7 vs v8 对比**: v7 适合艺术风格探索,v8 适合精确解剖结构
-
----
-
-## 更新日志
-
-- 2026-04-11: 初始入库,包含具体风格代码 738792225、--sw 参数详解、厚涂关键词库

+ 0 - 261
knowledge/Nano_Banana_多图融合_ComfyUI 工作流.md

@@ -1,261 +0,0 @@
-# Nano Banana 多图融合 ComfyUI 工作流
-
-**知识 ID**: knowledge-20260411-155313-nanobanana  
-**类型**: strategy  
-**评分**: ⭐⭐⭐⭐⭐ (5/5)  
-**来源**: 小红书/ComfyUI 社区  
-**领域**: AI 图像生成  
-**工作流类型**: 多图融合  
-**核心工具**: Nano Banana, Google Gemini Image, ComfyUI
-
----
-
-## 核心能力
-
-| 能力指标 | 规格 |
-|----------|------|
-| 最多参考图数量 | **14 张**(实际 10 张可达 100% 还原) |
-| 角色一致性 | 单工作流最多 **5 个角色** + **14 个物体** |
-| 面部一致性 | **90-95%** |
-| 中文渲染 | 准确率 **90%+** |
-| 支持宽高比 | 包括极端比例 1:8、8:1、4:1、1:4 |
-| 产品一致性 | 标签文字不崩坏 |
-
----
-
-## ComfyUI 工作流配置
-
-### 方法 1:单图编辑
-
-**工作流节点连接**:
-```
-Load Image → Image Scale → Google Gemini Image → LibLib Translate → Save Image
-```
-
-**参数设置**:
-- 图像缩放:`1536×1536`
-- 模型:`gemini-2.5-flash-image-preview`
-- 提示词:**必须包含** `"把这*张图组合成一张完整的新图像"`
-
-### 方法 2:双图编辑(推荐,效果更稳定)
-
-**工作流节点连接**:
-```
-Load Image (产品图) → Image Scale ┐
-Load Image (场景图) → Image Scale → Google Gemini Image → LibLib Translate → Save Image
-```
-
-**提示词示例**:
-```
-"把这两张图组合成一张完整的新图像,将产品自然融入场景中,保持产品标签文字清晰"
-```
-
-### 方法 3:三图编辑
-
-**工作流节点连接**:
-```
-Load Image (场景图) → Image Scale ┐
-Load Image (产品图) → Image Scale ├→ Google Gemini Image → LibLib Translate → Save Image
-Load Image (元素图) → Image Scale ┘
-```
-
-**提示词示例**:
-```
-"把这三张图组合成一张完整的新图像,将产品和杯子自然融入家居场景中,保持产品标签文字清晰"
-```
-
----
-
-## 15 种多图融合场景
-
-| 序号 | 场景 | 描述 |
-|------|------|------|
-| 1 | 杂志封面 | 融合多张参考图生成专业封面 |
-| 2 | 漫画分镜 | 保持角色一致性的多格漫画 |
-| 3 | 信息图 | 自动搜索数据生成带图表的信息图 |
-| 4 | 情绪九宫格 | 同一人物 9 种表情 |
-| 5 | 极限运动场景 | 同一人物在不同运动场景 |
-| 6 | 产品对比海报 | 多产品对比展示 |
-| 7 | 科研绘图 | 顶刊级科研配图 |
-| 8 | 电影感人像 | 带详细相机参数的专业人像 |
-| 9 | 艺术风格转换 | 多风格同一人物 |
-| 10 | 背景替换 | 保持人物换背景 |
-| 11 | 服装替换 | 保持人物换服装 |
-| 12 | 创意场景转换 | 动作人偶风格等 |
-| 13 | 多语言信息图 | 一键翻译排版 |
-| 14 | 四方连续贴图 | 无缝纹理生成 |
-| 15 | 虚拟试衣 | 服装上身效果 |
-
----
-
-## 实测心得
-
-### 关键发现
-
-1. **提示词决定出图效果** ⭐⭐⭐⭐⭐
-   - **必须写**: `"把这*张图组合成一张完整的新图像"`
-   - 缺少此句会导致生成效果不稳定
-
-2. **双图编辑效果更稳定** ⭐⭐⭐⭐
-   - 单图编辑:适合简单修改
-   - 双图编辑:推荐默认方案
-   - 三图以上:复杂度增加,需更具体提示词
-
-3. **产品一致性能力出色** ⭐⭐⭐⭐⭐
-   - 不必担心产品标签文字崩坏
-   - 适合电商产品场景图生成
-
-4. **清晰度优化**
-   - 如生成图清晰度不够,可使用高清放大工作流
-   - 推荐:Ultimate SD Upscale + Tile ControlNet
-
----
-
-## 成本说明
-
-### Nano Banana 定价
-
-| 版本 | 分辨率 | 价格 | 备注 |
-|------|--------|------|------|
-| Nano Banana 2 | 1K | $0.067/张 | 性价比最高 |
-| Nano Banana 2 | 2K | $0.101/张 | |
-| Nano Banana 2 | 4K | $0.151/张 | |
-| Nano Banana Pro | 1K | $0.134/张 | 比 Flash 贵 50% |
-
-### 不同平台成本对比
-
-| 平台 | 成本范围 | 建议 |
-|------|----------|------|
-| 官方 API | $0.067-0.151/张 | 稳定可靠 |
-| 第三方平台 A | $0.02-0.08/张 | 价格低,注意稳定性 |
-| 第三方平台 B | $0.10-0.15/张 | 价格高,可能有额外功能 |
-
-⚠️ **注意**: 不同平台调用 Banana 的成本不同(从 $0.02~$0.15 不等),需根据需求选择
-
----
-
-## 避坑指南
-
-| 问题 | 说明 | 解决方案 |
-|------|------|----------|
-| SynthID 水印 | 不可移除 | 接受或后期处理 |
-| Thinking 模式 | 强制启用 | 无法关闭,等待完成 |
-| 提示词不具体 | 效果不稳定 | 极度具体描述需求 |
-| 直接使用 Pro | 成本高 | 先用 Flash 测试提示词 |
-| 单次生成 | 成本高 | 使用 Batch API 节省 50% |
-
-### 推荐工作流
-
-```
-1. Flash 测试提示词 → 2. 优化提示词 → 3. Pro 批量生成 → 4. 高清放大
-```
-
----
-
-## 实战案例
-
-### 案例 1:电商产品场景图
-
-**输入**:
-- 产品图:投影仪正面图
-- 场景图:雪山湖畔风景
-
-**提示词**:
-```
-"把这两张图组合成一张完整的新图像,将投影仪自然融入雪山湖畔场景中,保持产品外观和标签清晰"
-```
-
-**输出**:电商宣传场景图
-
-**耗时**:5-6 分钟  
-**成本**:$0.067 × 2(测试 + 正式)= $0.134
-
----
-
-### 案例 2:中餐厅菜品图自动化
-
-**工作流**:Nanobanana + ComfyUI
-
-**流程**:
-```
-厨房出餐摆盘 → 手机随手拍 → 5-6 分钟生成专业菜单图
-```
-
-**支持**:
-- 横版/竖版可变尺寸
-- 批量生成
-- 保持菜品色泽和质感
-
-**应用场景**:
-- 餐厅菜单
-- 外卖平台图片
-- 社交媒体推广
-
----
-
-### 案例 3:小说转短剧分镜
-
-**流程**:
-```
-1. 去 AO3 找短文
-2. 和 Gemini 说将其生成 12 张图
-3. 得到人物一致性非常高的一组图
-4. 配合图片转视频工具制作小说转短剧
-```
-
-**成本**:极低($0.067 × 12 = $0.804)
-
-**优势**:
-- 人物一致性高(90-95%)
-- 快速生成分镜
-- 可批量制作
-
----
-
-## 与 ComfyUI 工作流整合
-
-### 整合方案
-
-```
-Nano Banana 多图融合 → ComfyUI 后处理
-                        ├─ ControlNet 结构微调
-                        ├─ IP-Adapter 风格统一
-                        ├─ FaceDetailer 细节修复
-                        └─ Upscale 高清放大
-```
-
-### 典型应用场景
-
-1. **电商产品图**:
-   - Nano Banana: 产品 + 场景融合
-   - ComfyUI: 细节优化、高清放大
-
-2. **角色一致性漫画**:
-   - Nano Banana: 多格分镜生成
-   - ComfyUI: 风格统一、对话框添加
-
-3. **科研绘图**:
-   - Nano Banana: 图表 + 数据融合
-   - ComfyUI: 标注优化、格式调整
-
----
-
-## 关联知识
-
-- **ComfyUI 画中画工作流**: 可将 Nano Banana 生成结果作为输入进行进一步处理
-- **Midjourney v7 厚涂风格**: 可使用 MJ 生成风格参考图,通过 Nano Banana 融合到目标场景
-- **ControlNet+IPAdapter 双控架构**: ComfyUI 后处理的核心技术
-
----
-
-## 官方资源
-
-- **API 文档**: https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-image-preview
-- **DeepMind 模型页**: https://deepmind.google/models/gemini-image/flash/
-- **AI Studio**: https://aistudio.google.com/models/gemini-3-1-flash-image
-
----
-
-## 更新日志
-
-- 2026-04-11: 初始入库,包含 ComfyUI 工作流配置、3 种编辑方法、15 种场景、3 个实战案例

+ 0 - 20
knowledge/knowledge-20260412-005015-nano-banana-image-urls.json

@@ -1,20 +0,0 @@
-{
-  "id": "knowledge-20260412-005015-nano-banana-image-urls",
-  "task": "在图像生成任务中使用 nano_banana 工具传递参考图",
-  "content": "当使用 nano_banana 工具进行图像生成时,image_urls 参数不支持本地相对路径,必须使用 HTTP/HTTPS CDN URL。\n\n## 错误做法\n\n**直接传本地相对路径**:\n```json\n{\n  \"image_urls\": [\"examples/production_restore/features/character_asset/character_ref_kneel.png\"]\n}\n```\n返回错误:HTTP 503: \"images[].data 看起来是文件路径但文件不存在\"\n\n**另一个错误示例**:\n```json\n{\"image_urls\": [\"examples/production_restore/img_1.png\"]}\n```\n报错:HTTP 503: images[].data 看起来是文件路径但文件不存在\n\n**注意**:虽然角色说明中提到 toolhub.py 内置的 `_preprocess_params` 函数会自动将本地路径上传到 OSS,但实际测试发现这个功能没有生效,需要手动调用 image_uploader 上传。\n\n## 正确做法\n\n1. 先调用 image_uploader 上传本地图片:\n```python\n# 步骤 1:上传图片\ncdn_url = image_uploader(local_path=\"examples/production_restore/img_1.png\")\n# 返回:https://res.cybertogether.net/toolhub_images/img_1.png\n\n# 步骤 2:调用 nano_banana\ntoolhub_call(tool_id=\"nano_banana\", params={\n    \"image_urls\": [cdn_url],\n    \"prompt\": \"...\"\n})\n```\n\n2. 再将 CDN URL 传入 nano_banana:\n```python\ntoolhub_call(tool_id=\"nano_banana\", params={\n  \"image_urls\": [\"https://res.cybertogether.net/toolhub_images/character_ref_kneel.png\"]\n})\n```\n\n## 原因\n\nnano_banana 工具运行在远程环境中,无法直接访问本地文件系统,必须通过 HTTP URL 访问图片。\n\n## 案例\n\nimg_1 生成任务中,最初传入相对路径导致 HTTP 503 错误,错误信息明确指出「请传 HTTP URL 或绝对路径,调用方应先把本地文件上传到 OSS 再传 CDN URL」。改用 image_uploader 上传三张参考图后获得 CDN URL,再次调用 nano_banana 成功生成图像。",
-  "types": ["experience"],
-  "tags": {
-    "intent": "图像生成",
-    "state": "nano_banana",
-    "tool": "image_uploader"
-  },
-  "score": 5,
-  "source": {
-    "category": "research"
-  },
-  "created_at": "2026-04-12T00:50:15Z",
-  "updated_at": "2026-04-12T01:53:12Z",
-  "tool_ids": ["tools/image_gen/nano_banana", "tools/upload/image_uploader"],
-  "capability_ids": ["CAP-003"],
-  "related_knowledge_ids": ["knowledge-20260409-182612-108d", "knowledge-20260412-010725-nano-banana-http503"]
-}

+ 0 - 20
knowledge/knowledge-20260412-010725-nano-banana-http503.json

@@ -1,20 +0,0 @@
-{
-  "id": "knowledge-20260412-010725-nano-banana-http503",
-  "task": "nano_banana 工具图片传递方式:需先上传获取 CDN URL",
-  "content": "当使用 nano_banana 工具进行图像生成时,如果传入本地文件路径会报错 HTTP 503:\"images[].data 看起来是文件路径但文件不存在\"。\n\n**错误做法**:直接传本地相对路径\n```json\n{\n  \"image_urls\": [\"examples/production_restore/features/character_asset/character_ref_kneel.png\"]\n}\n```\n\n**正确做法**:\n1. 先调用 image_uploader 上传本地图片:\n```python\nimage_uploader(local_path=\"examples/production_restore/features/character_asset/character_ref_kneel.png\")\n→ 返回 cdn_url: \"https://res.cybertogether.net/toolhub_images/character_ref_kneel.png\"\n```\n2. 再将 CDN URL 传入 nano_banana:\n```python\ntoolhub_call(tool_id=\"nano_banana\", params={\n  \"image_urls\": [\"https://res.cybertogether.net/toolhub_images/character_ref_kneel.png\"]\n})\n```\n\n**原因**:nano_banana 工具运行在远程环境中,无法直接访问本地文件系统,必须通过 HTTP URL 访问图片。\n\n**案例**:\n- img_1 生成:传入相对路径导致 HTTP 503 错误,改用 image_uploader 上传后成功\n- img_4 生成:最初直接传本地路径 \"examples/production_restore/features/character_asset/character_ref_side.png\" 导致失败,改用 image_uploader 上传 4 张图片获取 CDN URL 后成功生成",
-  "types": ["experience"],
-  "tags": {
-    "intent": "图像生成",
-    "state": "nano_banana",
-    "tool": "image_uploader"
-  },
-  "score": 4,
-  "source": {
-    "category": "research"
-  },
-  "created_at": "2026-04-12T01:07:25Z",
-  "updated_at": "2026-04-12T01:53:12Z",
-  "tool_ids": ["tools/image_gen/nano_banana", "tools/upload/image_uploader"],
-  "capability_ids": ["CAP-001", "CAP-011"],
-  "related_knowledge_ids": ["knowledge-20260409-182612-108d", "knowledge-20260412-005015-nano-banana-image-urls", "knowledge-20260412-010726-nano-banana-multi-fusion"]
-}

Разница между файлами не показана из-за своего большого размера
+ 0 - 3
knowledge/knowledge-20260412-010726-nano-banana-multi-fusion.json


+ 0 - 20
knowledge/knowledge-20260412-014259-3abf.json

@@ -1,20 +0,0 @@
-{
-  "id": "knowledge-20260412-014259-3abf",
-  "task": "在 nano_banana 图生图任务中,参考图需要先上传获取 CDN URL 再传入",
-  "content": "当使用 nano_banana 工具进行多图融合生图时,image_urls 参数不能直接传本地文件路径(如 examples/xxx.png),否则会报错「文件不存在」。正确做法是:先调用 image_uploader 工具将本地图片上传到 OSS 获取 CDN URL,再将 CDN URL 传入 nano_banana 的 image_urls 参数。案例:本次 img_1 生成任务中,先调用 image_uploader 上传 character_ref_img1.png、background_green_img1.png、palette_impasto_img1_v2.png、easel_blank_canvas_img4.png 四张参考图,获取 CDN URL 后传入 nano_banana,生成成功。",
-  "types": ["experience"],
-  "tags": {
-    "intent": "图像生成",
-    "state": "nano_banana",
-    "tool": "image_uploader"
-  },
-  "score": 4,
-  "source": {
-    "category": "research"
-  },
-  "created_time": 1776067979,
-  "tool_ids": ["tools/image_gen/nano_banana", "tools/upload/image_uploader"],
-  "capability_ids": [],
-  "requirement_ids": [],
-  "resource_ids": []
-}

+ 0 - 21
knowledge/strategy/品类分组判断标准.json

@@ -1,21 +0,0 @@
-{
-  "id": "knowledge-20260410-category-grouping",
-  "task": "工作流品类分组的判断标准",
-  "content": "在分析多条工作流进行品类分组时,应基于以下维度判断:\n\n1. **核心主体**:工作流围绕什么核心对象展开(人物/场景/光影/纹理)\n\n2. **技术手段相似性**:是否使用相似的工具链和技术方法\n\n3. **输出目标一致性**:最终产出的内容类型是否相同\n\n4. **粒度把握**:品类名称应品类无关(如'人物肖像'而非'电商人物肖像'),便于跨领域比较\n\n本次分析案例:\n- wf_002(局部特写)+ wf_003(近景肖像)→ 人物肖像类(都是人物主体,都涉及细节修复和身份锁定)\n- wf_001 → 场景构建类(场景 + 人物综合,涉及 3D 建模和场景合成)\n- wf_004 → 光影艺术类(专门的光影控制,使用 ControlNet/IC-Light 等)\n- wf_005 → 纹理风格类(材质纹理效果,涉及颗粒/纸张/复古风格)",
-  "types": ["strategy"],
-  "tags": {
-    "domain": "工作流分析",
-    "method": "品类分组",
-    "key_skill": "语义聚类"
-  },
-  "score": 4,
-  "source": {
-    "category": "execution"
-  },
-  "created_at": "2026-04-10T16:58:05",
-  "status": "active",
-  "related_knowledge": [
-    "knowledge-20260410-variant-analysis-method",
-    "knowledge-20260410-causal-reasoning"
-  ]
-}

+ 0 - 20
knowledge/strategy/粗工序因果推理方法.json

@@ -1,20 +0,0 @@
-{
-  "id": "knowledge-20260410-causal-reasoning",
-  "task": "工作流粗工序提取的因果推理方法",
-  "content": "在总结粗工序时,必须解释'为什么先做 A 再做 B',而不是简单列举频次。核心推理框架包括:\n\n1. **依赖关系原则**:后续步骤依赖前序步骤的输出(如'局部细节修复必须在身份锁定之后进行,否则修复结果无法保持角色一致性')\n\n2. **先整体后局部原则**:先建立基础框架再处理细节(如'首先生成基础人像,因为后续所有细节处理都依赖于一个可用的基础图像')\n\n3. **先生成后修复原则**:先产生可用结果再进行优化(如'放大和精修是最终输出前的必要步骤,放在后期处理')\n\n4. **分离 - 合成策略**:将独立元素分别处理后再合并(如'场景背景和人物图像分别生成后再进行合成,这种分离策略允许对场景和人物独立优化,避免相互干扰')\n\n5. **先规划后执行原则**:先设计蓝图再技术实现(如'整体光照方案是所有后续技术实现的基础蓝图,必须在布局确定后进行配置')\n\n6. **先基础后风格化原则**:先完成基础处理再进行艺术增强(如'色彩调整是光影/纹理处理完成后的最终润色步骤,过早调色会干扰基础效果的准确呈现')",
-  "types": ["strategy"],
-  "tags": {
-    "domain": "工作流分析",
-    "method": "粗工序提取",
-    "key_skill": "因果推理"
-  },
-  "score": 5,
-  "source": {
-    "category": "execution"
-  },
-  "created_at": "2026-04-10T16:58:05",
-  "status": "active",
-  "related_knowledge": [
-    "knowledge-20260410-variant-analysis-method"
-  ]
-}

+ 0 - 17
knowledge/strategy/能力模块变体分析方法.json

@@ -1,17 +0,0 @@
-{
-  "id": "knowledge-20260410-variant-analysis-method",
-  "task": "能力模块实现变体分析方法",
-  "content": "在工作流分析中,为能力模块分析实现变体的方法:\n\n1. **变体聚类依据**:根据工具/方法相似性进行聚类,每个模块聚类为 2-4 个变体\n\n2. **变体命名规范**:「工具/方法」+「方案」,如「LoRA 方案」、「IPAdapter 方案」、「ControlNet 方案」、「Photoshop 后期方案」等\n\n3. **变体结构**:\n   - name:变体名称(符合命名规范)\n   - description:变体描述(一句话说明核心方法)\n   - steps:典型执行步骤(2-4 步,动宾结构)\n   - source_workflows:来源工作流列表(如 [\"wf_001\", \"wf_002\"])\n\n4. **分析要点**:\n   - 忠实于原始工作流数据,不臆造不存在的工具或方法\n   - 变体之间应有明显的方法差异(如 AI 生成 vs 手工绘制 vs 外部素材)\n   - 步骤描述应体现因果性编排思路(为什么先做 A 再做 B)\n   - 注明每个变体来自哪些工作流,便于追溯\n\n5. **常见变体类型**:\n   - AI 生成方案(使用 FLUX/ComfyUI/Midjourney 等)\n   - 控制技术方案(ControlNet/LoRA/PuLID 等)\n   - 后期处理方案(Photoshop/Illustrator 等)\n   - 提示词方案(通过提示词工程实现)\n   - 3D/渲染方案(Blender 等)",
-  "types": ["strategy"],
-  "tags": {
-    "domain": "工作流分析",
-    "method": "变体分析",
-    "output_format": "JSON"
-  },
-  "score": 4,
-  "source": {
-    "category": "execution"
-  },
-  "created_at": "2026-04-10T16:54:50",
-  "status": "active"
-}

Разница между файлами не показана из-за своего большого размера
+ 0 - 9
knowledge_batch_20260409.json


+ 0 - 26
knowledge_batch_20260409_execution.json

@@ -1,26 +0,0 @@
-{
-  "upload_batch": "X 搜索接口稳定性问题",
-  "upload_date": "2026-04-09",
-  "source_type": "execution",
-  "knowledge_count": 1,
-  "knowledge": [
-    {
-      "id": "knowledge-20260409-exec-001",
-      "task": "X 搜索接口稳定性问题及应对策略",
-      "content": "## 问题描述\n在执行 X 渠道调研时,x_search 工具在以下情况出现连续失败:\n- 关键词 1(close-up portrait AI workflow tutorial):成功返回 20 条结果\n- 关键词 2(portrait bokeh background AI workflow):连续 5 次搜索均返回\"X 搜索异常\"或\"搜索失败:未知错误\"\n- 关键词 3(facial expression AI art workflow):连续搜索返回异常\n- 尝试替代关键词(portrait bokeh AI, bokeh background portrait AI):同样失败\n\n## 应对策略\n1. **重试机制**:接口失败时应重试 2 次,若仍失败则标注渠道状态为\"接口失败\"\n2. **降级处理**:当部分关键词失败时,基于已成功获取的数据继续完成调研\n3. **结果标注**:在输出 JSON 中明确标注\"渠道状态\": \"部分接口失败\",并记录实际获取的数据量\n4. **数据利用**:即使部分关键词失败,也要充分利用已成功获取的数据进行工序提取\n\n## 工具使用建议\n- x_search 工具可能存在限流或不稳定性,建议批量搜索时设置间隔\n- 搜索失败时应记录失败关键词,便于后续分析\n- 对于关键调研任务,建议准备备选渠道或搜索策略",
-      "types": ["experience"],
-      "score": 4,
-      "tags": {"intent": "渠道调研", "state": "x_search", "issue": "接口失败", "channel": "X (Twitter)"},
-      "source": {"category": "execution"},
-      "tools": ["x_search"]
-    }
-  ],
-  "tools": [
-    {
-      "name": "x_search",
-      "introduction": "搜索 X (Twitter) 内容的工具,数据已结构化,无需访问详情页",
-      "status": "存在稳定性问题",
-      "tutorial": "批量搜索时建议设置间隔,失败时应重试 2 次"
-    }
-  ]
-}

+ 0 - 34
knowledge_batch_20260410_workflow_analysis.json

@@ -1,34 +0,0 @@
-{
-  "upload_batch": "工作流分析 Pipeline 元知识",
-  "upload_date": "2026-04-10",
-  "source_type": "execution",
-  "knowledge_count": 2,
-  "knowledge": [
-    {
-      "id": "knowledge-20260410-wf-001",
-      "task": "工作流分析 Pipeline 数据源定位方法",
-      "content": "## 工作流数据存储位置\n工作流数据存储在 `tool_research_v2` 的输出目录中,格式为 JSON 文件。\n\n## 数据结构\n```json\n{\n  \"渠道名称\": \"小红书/X (Twitter)/YouTube\",\n  \"工序发现\": [\n    {\n      \"方案名称\": \"...\",\n      \"工序步骤\": [\n        {\n          \"步骤序号\": 1,\n          \"步骤描述\": \"...\",\n          \"使用工具\": \"...\",\n          \"用户输入\": \"...\",\n          \"输出结果\": \"...\"\n        }\n      ],\n      \"帖子链接\": \"...\"\n    }\n  ]\n}\n```\n\n## 工作流分析入口\n**脚本路径**:`examples/workflow_analysis/run.py`\n\n### 功能\n- 从 `tool_research_v2` 输出目录读取 JSON 文件\n- 自动编号为 `wf_001`, `wf_002`, `wf_003`...\n- 输出意图分析结果到 `examples/research/outputs/analysis/`\n\n### 关键代码逻辑\n1. `load_workflows_from_dir()` 扫描输入目录下所有 JSON 文件\n2. 支持两种目录结构:\n   - 单次调研输出:直接含 `*.json` 文件\n   - 批量调研输出:含 `00/`, `01/` 等子目录\n3. 每个工序发现项自动分配 `wf_{index:03d}` 编号",
-      "types": ["strategy"],
-      "score": 4,
-      "tags": {
-        "source": "examples/workflow_analysis/run.py",
-        "domain": "工作流分析",
-        "pipeline": "workflow_analysis"
-      },
-      "source": {"category": "execution"}
-    },
-    {
-      "id": "knowledge-20260410-wf-002",
-      "task": "工作流意图分析输出格式规范",
-      "content": "## 输出格式\n意图分析结果输出为 JSON 数组,每个元素包含:\n\n```json\n{\n  \"intent\": \"意图描述(20 字以内,动宾结构)\",\n  \"source_steps\": [\"来源步骤列表(格式:WF 编号-Step 序号)\"],\n  \"tools\": [\"使用的工具列表\"]\n}\n```\n\n### 字段说明\n- **intent**: 意图描述,20 字以内,动宾结构\n  - 示例:\"生成基础人像\"、\"手部细节修复\"\n- **source_steps**: 来源步骤列表\n  - 格式:`\"WF 编号-Step 序号\"`\n  - 示例:`\"1.1-Step1\"`、`\"6.1-Step2\"`\n- **tools**: 使用的工具列表\n  - 示例:`[\"FLUX.1 Dev\", \"ControlNet Inpaint\", \"InstantID\"]`\n\n## 意图聚类原则\n\n1. **相似步骤合并**:多个工作流的相似步骤合并为同一意图\n   - 示例:多个工作流的\"第一步生成基础图像\" → \"生成基础人像\"\n\n2. **品类无关描述**:意图描述要品类无关,便于跨工作流比较\n\n3. **工具列表合并**:工具列表合并该意图下所有工作流使用的工具\n\n## 输出文件路径\n`examples/research/outputs/analysis/wf_{编号}_intents.json`\n\n### 示例\n- `wf_001_intents.json`\n- `wf_002_intents.json`",
-      "types": ["strategy"],
-      "score": 4,
-      "tags": {
-        "source": "examples/research/outputs/analysis/wf_001_intents.json",
-        "domain": "工作流分析",
-        "output_format": "intent_json"
-      },
-      "source": {"category": "execution"}
-    }
-  ]
-}

+ 0 - 51
knowledge_batch_20260411_toolhub_error_recovery.json

@@ -1,51 +0,0 @@
-{
-  "upload_batch": "ToolHub 错误恢复与图像评估局限性",
-  "upload_date": "2026-04-11",
-  "source_type": "execution",
-  "knowledge_count": 2,
-  "knowledge": [
-    {
-      "id": "knowledge-20260411-exec-001",
-      "task": "在 ToolHub 搜索接口返回 500 错误时直接调用工具",
-      "content": "当 toolhub_search 返回 500 Internal Server Error 但 toolhub_health 检查通过时,可以直接尝试调用 toolhub_call 执行工具。健康检查通过说明服务核心功能可用,搜索接口可能是临时故障。\n\n## 错误恢复策略\n1. **健康检查优先**:遇到 toolhub_search 失败时,先用 toolhub_health 确认服务状态\n2. **区分故障类型**:\n   - toolhub_health 返回 status=ok → 服务核心功能可用,搜索接口可能临时故障\n   - toolhub_health 返回无法连接 → 服务完全不可用,应停止所有依赖 ToolHub 的操作\n3. **绕过搜索直接调用**:健康检查通过时,可直接使用 toolhub_call(tool_id=xxx, params={...}) 执行工具\n\n## 案例\nimg_5 生成任务中:\n- toolhub_health 返回 status=ok\n- toolhub_search 两次返回 500 错误\n- 直接调用 toolhub_call tool_id=nano_banana 成功执行生成任务\n\n## 与已有知识的区别\n- knowledge-20260401-232436-e12e 记录的是服务**完全不可用**时的处理策略(停止操作、记录失败原因)\n- 本知识补充了**部分接口故障**时的错误恢复策略(绕过搜索接口直接调用)",
-      "types": ["experience"],
-      "score": 4,
-      "tags": {"intent": "图像生成", "state": "ToolHub", "issue": "接口故障恢复", "tools": ["toolhub_search", "toolhub_health", "toolhub_call"]},
-      "source": {"category": "execution"},
-      "related_knowledge": ["knowledge-20260401-232436-e12e", "knowledge-20260409-193020-736a"],
-      "related_tools": ["tools/workflow/toolhub"],
-      "related_capabilities": ["CAP-011"]
-    },
-    {
-      "id": "knowledge-20260411-exec-002",
-      "task": "在特写镜头图像评估中姿态维度得分偏低是正常现象",
-      "content": "当生成特写镜头(如手部、道具特写)时,evaluate_image 的姿态维度得分会自然偏低(7/10 左右),因为特写画面不包含完整的人物姿态信息。这是评估工具的局限性,不应视为生成失败。\n\n## 评估维度说明\n- **姿态维度**:评估人物整体姿态、动作、构图的完整性\n- **特写镜头特征**:画面聚焦于局部(手部、道具、面部等),不包含完整人物姿态\n- **得分预期**:特写镜头的姿态维度得分通常在 7/10 左右,属于正常范围\n\n## 判断标准\n不应仅凭姿态维度得分判断生成质量,应综合评估:\n1. **核心目标达成度**:如材质质感、细节清晰度等关键指标\n2. **其他维度得分**:材质、构图、色彩等维度可能得分很高\n3. **整体评分**:整体 8.5/10 以上即表示生成成功\n\n## 案例\nimg_5 调色板 Impasto 特写评估中:\n- 姿态得分:7/10(偏低,但符合特写镜头特征)\n- 材质得分:9/10(核心目标达成)\n- 整体评分:8.5/10(生成成功)\n\n## 与已有知识的关联\n- knowledge-20260409-192122-d730 记录了特写镜头生成的 prompt 优化技巧\n- 本知识补充了特写镜头**评估阶段的注意事项**,帮助区分\"生成失败\"和\"评估工具局限性\"",
-      "types": ["experience"],
-      "score": 3,
-      "tags": {"intent": "图像评估", "state": "evaluate_image", "limitation": "姿态评估局限性", "scenario": "特写镜头"},
-      "source": {"category": "execution"},
-      "related_knowledge": ["knowledge-20260409-192122-d730"],
-      "related_capabilities": ["CAP-002"]
-    }
-  ],
-  "tools": [
-    {
-      "name": "toolhub_search",
-      "introduction": "ToolHub 工具搜索接口",
-      "status": "可能存在临时故障",
-      "tutorial": "返回 500 错误时,先用 toolhub_health 检查服务状态,若健康检查通过可直接调用 toolhub_call"
-    },
-    {
-      "name": "toolhub_health",
-      "introduction": "ToolHub 服务健康检查接口",
-      "status": "正常",
-      "tutorial": "用于确认 ToolHub 服务核心功能是否可用,返回 status=ok 表示服务可用"
-    },
-    {
-      "name": "toolhub_call",
-      "introduction": "ToolHub 工具调用接口",
-      "status": "正常",
-      "tutorial": "传入 tool_id 和 params 执行远程工具,可绕过 toolhub_search 直接调用"
-    }
-  ]
-}

BIN
outputs/48d6bd7b-699/nano_banana_1775894285972_0.jpg


BIN
outputs/48d6bd7b-699/nano_banana_1775894577594_0.jpg


BIN
outputs/48d6bd7b-699/nano_banana_1775894852970_0.jpg


BIN
outputs/48d6bd7b-699/nano_banana_1775895148729_0.jpg


BIN
outputs/48d6bd7b-699/nano_banana_1775895453740_0.jpg


BIN
outputs/48d6bd7b-699/nano_banana_1775895803885_0.jpg


BIN
outputs/48d6bd7b-699/nano_banana_1775896094781_0.jpg


BIN
outputs/5fd2db93-5ac/nano_banana_1775926086639_0.jpg


BIN
outputs/5fd2db93-5ac/nano_banana_1775926336258_0.jpg


BIN
outputs/5fd2db93-5ac/nano_banana_1775926561018_0.jpg


BIN
outputs/5fd2db93-5ac/nano_banana_1775927016941_0.jpg


BIN
outputs/5fd2db93-5ac/nano_banana_1775927415643_0.jpg


BIN
outputs/b7191706-5dc/nano_banana_1775929219620_0.jpg


BIN
outputs/b7191706-5dc/nano_banana_1775929481610_0.jpg


BIN
outputs/b7191706-5dc/nano_banana_1775929804307_0.jpg


BIN
outputs/b7191706-5dc/nano_banana_1775930114003_0.jpg


BIN
outputs/b7191706-5dc/nano_banana_1775930416393_0.jpg


Разница между файлами не показана из-за своего большого размера
+ 0 - 9
pending_uploads/2026-04-09_ai_image_local_detail_strategy.json


+ 0 - 28
pending_uploads/knowledge_batch_20260410_file_search.json

@@ -1,28 +0,0 @@
-{
-  "knowledge": [
-    {
-      "task": "在分析工作流文件时,当 glob 搜索找不到目标文件的有效定位策略",
-      "content": "当 glob_files 按模式匹配找不到目标文件时,应该:1) 使用 bash_command 执行 find 命令进行更广泛的文件系统搜索,支持通配符和路径遍历;2) 结合 grep 内容搜索,通过文件内容关键词定位(如工具名、方案名);3) 检查相关目录结构,如 research/outputs/ 下的子目录。案例:本次任务中 glob_files 搜索\"*wf_005*\"未找到文件,但通过 find 命令搜索包含\"ComfyUI\"\"Nano Banana\"等关键词的 markdown 文件,成功在 outputs/04/ 目录下找到颗粒感纸张纹理插画风格工作流调研报告.md,该文件即为 wf_005 的 raw_markdown 内容。",
-      "types": ["experience"],
-      "tags": {
-        "intent": "工作流文件定位",
-        "state": "文件搜索",
-        "tools": "glob_files,bash_command"
-      },
-      "score": 4,
-      "source": {"category": "research"}
-    },
-    {
-      "task": "工作流意图归纳的粒度判断原则",
-      "content": "在将工作流技术步骤归纳为意图级描述时,粒度判断应遵循:1) 保留工具差异——相同意图但不同工具实现应分开记录,因为工具选择是工作流变体的关键信息;2) 合并连续子步骤——同一工具内的连续操作(如新建图层→填充→设置混合模式)可合并为一个意图;3) 跨方案可比较——意图描述应使不同工作流的相同功能模块可对比。案例:本次将 Photoshop 的 5 种颗粒添加方法分别记录为\"添加溶解颗粒效果\"\"手绘颗粒纹理\"\"滤镜添加全局杂色\"等,保留了方法差异;而将 ComfyUI 的分割 + 重排合并为\"创建四方连续布局\"。",
-      "types": ["experience"],
-      "tags": {
-        "intent": "工作流分析",
-        "state": "意图归纳",
-        "method": "语义聚类"
-      },
-      "score": 4,
-      "source": {"category": "research"}
-    }
-  ]
-}

Разница между файлами не показана из-за своего большого размера
+ 0 - 81
pre_upload_list.json


+ 0 - 69
process_upload.py

@@ -1,69 +0,0 @@
-#!/usr/bin/env python3
-"""
-处理上传的知识条目并添加到 pre_upload_list.json
-"""
-import json
-import uuid
-import time
-
-# 读取现有文件
-with open('/root/Agent/pre_upload_list.json', 'r', encoding='utf-8') as f:
-    data = json.load(f)
-
-# 用户上传的 3 条知识
-new_knowledge = [
-    {
-        "主题": "在调用 ToolHub nano_banana 工具时,图片传递需要手动上传到 OSS 获取 CDN URL",
-        "内容": "当使用 ToolHub 的 nano_banana 工具进行图像生成时,不能直接传本地文件路径(如 examples/xxx/image.png),因为 ToolHub 服务运行在远程服务器上,无法访问调用方的本地文件系统。\n\n正确做法:\n1. 先调用 image_uploader 工具将本地图片上传到 OSS\n2. 获取返回的 cdn_url(如 https://res.cybertogether.net/toolhub_images/xxx.png)\n3. 将 cdn_url 传入 toolhub_call 的 image_urls 参数\n\n案例:生成 img_3 时,最初直接传本地路径 character_ref_kneel.png 导致 HTTP 503 错误「文件不存在」,改用 image_uploader 上传后获取 CDN URL 再调用 nano_banana 成功生成。\n\n注意:虽然部分 ToolHub 工具文档声称支持自动上传本地路径,但 nano_banana 实际不支持此功能,需手动处理。",
-        "类型": ["experience"],
-        "标签": {"intent": "图像生成", "state": "nano_banana", "tool": "toolhub_call"},
-        "评分": 5,
-        "source": {"category": "execution"}
-    },
-    {
-        "主题": "在图像生成任务中,使用 nano_banana 进行多图融合保持人物一致性的方法",
-        "内容": "当需要保持多张生成图的人物一致性时(如系列图 img_1~img_5),使用 nano_banana 工具的多图融合功能:\n\n1. 在 image_urls 数组中传入多张参考图:\n   - 底图(当前姿态的 character_ref_*.png)\n   - 前序生成结果(如 img_1_restored_v1.png 用于链式一致性)\n2. 在 prompt 中明确描述需要保持的特征(如 white V-neck backless maxi dress, brown long hair)\n3. 使用 aspect_ratio 参数控制输出比例(如 3:4)\n\n案例:img_3 生成时传入 character_ref_kneel.png(跪坐姿态底图)和 img_1_restored_v1.png(前序结果),成功保持了白裙款式、发型、体态的一致性,评估得分 8.5/10。\n\n优势:nano_banana 基于 Gemini 多模态模型,对多图特征融合理解能力强,比传统 IP-Adapter 更灵活。",
-        "类型": ["experience"],
-        "标签": {"intent": "人物一致性", "state": "nano_banana", "technique": "多图融合"},
-        "评分": 4,
-        "source": {"category": "execution"}
-    },
-    {
-        "主题": "图像生成任务的标准化执行流程",
-        "内容": "完整的图像生成任务应遵循以下流程:\n\n1. **素材验证**:使用 read_file 检查所有参考素材文件存在且可读\n2. **源信息读取**:读取制作表 JSON 和通用创作文件,提取细节描述融入 prompt\n3. **ToolHub 检查**:调用 toolhub_health 确认服务可用,toolhub_search 查找合适工具\n4. **图片上传**:使用 image_uploader 将本地图片转为 CDN URL\n5. **执行生成**:调用 toolhub_call 执行生成,记录完整参数\n6. **结果验证**:使用 evaluate_image 进行自动化评估(单图 6 维度/多图 5 维度一致性)\n7. **日志记录**:追加 generation_log.md,包含素材验证、生成参数、验证结果、评估详情\n\n案例:img_3 任务按此流程执行,从素材验证到日志记录全程可追溯,评估 8.5/10 通过。\n\n关键点:每次生成都必须记录完整参数便于迭代优化;评估结果低于 7 分需调整配置重新生成。",
-        "类型": ["experience"],
-        "标签": {"intent": "图像生成流程", "state": "workflow", "domain": "production"},
-        "评分": 4,
-        "source": {"category": "execution"}
-    }
-]
-
-# 转换为标准格式并添加
-current_time = int(time.time())
-timestamp = time.strftime("%Y%m%d-%H%M%S", time.localtime(current_time))
-
-for i, item in enumerate(new_knowledge):
-    # 生成唯一 ID
-    knowledge_id = f"knowledge-{timestamp}-{uuid.uuid4().hex[:4]}"
-    
-    # 转换为标准格式
-    standard_knowledge = {
-        "id": knowledge_id,
-        "task": item["主题"],
-        "content": item["内容"],
-        "types": item["类型"],
-        "tags": item["标签"],
-        "score": item["评分"],
-        "source": item["source"]
-    }
-    
-    data['knowledge'].append(standard_knowledge)
-    print(f"添加知识 {i+1}: {knowledge_id}")
-    print(f"  主题:{item['主题'][:50]}...")
-
-# 写回文件
-with open('/root/Agent/pre_upload_list.json', 'w', encoding='utf-8') as f:
-    json.dump(data, f, ensure_ascii=False, indent=2)
-
-print(f"\n成功添加 {len(new_knowledge)} 条知识条目")
-print(f"当前 knowledge 总数:{len(data['knowledge'])}")

+ 0 - 0
test-dir/__main__.py


+ 0 - 72
test_upload.py

@@ -1,72 +0,0 @@
-#!/usr/bin/env python3
-"""
-测试知识上传脚本
-"""
-import sys
-import os
-import uuid
-import time
-from datetime import datetime, timezone
-
-sys.path.insert(0, '/root/Agent')
-
-# 设置环境变量
-os.environ['KNOWHUB_DB'] = 'gp-t4n72471pkmt4b9q7o-master.gpdbmaster.singapore.rds.aliyuncs.com'
-os.environ['KNOWHUB_PORT'] = '5432'
-os.environ['KNOWHUB_USER'] = 'aiddit_aigc'
-os.environ['KNOWHUB_PASSWORD'] = '%a&&yqNxg^V1$toJ*WOa^-b^X=QJ'
-os.environ['KNOWHUB_DB_NAME'] = 'knowhub'
-
-from knowhub.knowhub_db.pg_store import PostgreSQLStore
-from knowhub.embeddings import get_embedding
-
-# 测试知识数据
-knowledge_data = {
-    'task': '接口测试验证条目',
-    'content': '这是一条用于验证 upload 接口是否可用的测试知识,可以删除。',
-    'types': ['experience'],
-    'tags': {'source': 'api_test'},
-    'score': 1
-}
-
-print('正在连接数据库...')
-store = PostgreSQLStore()
-
-# 生成 ID
-now = datetime.now(timezone.utc)
-knowledge_id = f'knowledge-{now.strftime("%Y%m%d")}-{uuid.uuid4().hex[:8]}'
-message_id = f'msg-{uuid.uuid4().hex[:12]}'
-
-# 生成 embedding
-print('正在生成 embedding...')
-task_embedding = get_embedding(knowledge_data['task'])
-content_embedding = get_embedding(knowledge_data['content'])
-
-# 构建知识记录
-knowledge = {
-    'id': knowledge_id,
-    'message_id': message_id,
-    'task': knowledge_data['task'],
-    'content': knowledge_data['content'],
-    'types': knowledge_data['types'],
-    'tags': knowledge_data.get('tags', {}),
-    'tag_keys': list(knowledge_data.get('tags', {}).keys()),
-    'scopes': ['org:cybertogether'],
-    'owner': 'system',
-    'source': {'category': 'execution'},
-    'eval': {'score': knowledge_data.get('score', 3)},
-    'task_embedding': task_embedding,
-    'content_embedding': content_embedding,
-    'created_at': int(time.time()),
-    'updated_at': int(time.time()),
-    'status': 'approved',
-}
-
-print(f'正在插入知识条目...')
-store.insert(knowledge)
-store.close()
-
-print(f'✅ 成功插入知识条目!')
-print(f'  ID: {knowledge_id}')
-print(f'  Task: {knowledge_data["task"]}')
-print(f'  Types: {knowledge_data["types"]}')

+ 0 - 80
test_upload_sync.py

@@ -1,80 +0,0 @@
-#!/usr/bin/env python3
-"""
-测试知识上传脚本(同步版本)
-"""
-import sys
-import os
-import uuid
-import time
-import asyncio
-from datetime import datetime, timezone
-
-sys.path.insert(0, '/root/Agent')
-
-# 设置环境变量
-os.environ['KNOWHUB_DB'] = 'gp-t4n72471pkmt4b9q7o-master.gpdbmaster.singapore.rds.aliyuncs.com'
-os.environ['KNOWHUB_PORT'] = '5432'
-os.environ['KNOWHUB_USER'] = 'aiddit_aigc'
-os.environ['KNOWHUB_PASSWORD'] = '%a&&yqNxg^V1$toJ*WOa^-b^X=QJ'
-os.environ['KNOWHUB_DB_NAME'] = 'knowhub'
-
-from knowhub.knowhub_db.pg_store import PostgreSQLStore
-from knowhub.embeddings import get_embedding
-
-# 测试知识数据
-knowledge_data = {
-    'task': '接口测试验证条目',
-    'content': '这是一条用于验证 upload 接口是否可用的测试知识,可以删除。',
-    'types': ['experience'],
-    'tags': {'source': 'api_test'},
-    'score': 1
-}
-
-async def main():
-    print('正在连接数据库...')
-    store = PostgreSQLStore()
-
-    # 生成 ID
-    now = datetime.now(timezone.utc)
-    knowledge_id = f'knowledge-{now.strftime("%Y%m%d")}-{uuid.uuid4().hex[:8]}'
-    message_id = f'msg-{uuid.uuid4().hex[:12]}'
-
-    # 生成 embedding
-    print('正在生成 embedding...')
-    task_embedding = await get_embedding(knowledge_data['task'])
-    content_embedding = await get_embedding(knowledge_data['content'])
-
-    # 构建知识记录
-    knowledge = {
-        'id': knowledge_id,
-        'message_id': message_id,
-        'task': knowledge_data['task'],
-        'content': knowledge_data['content'],
-        'types': knowledge_data['types'],
-        'tags': knowledge_data.get('tags', {}),
-        'tag_keys': list(knowledge_data.get('tags', {}).keys()),
-        'scopes': ['org:cybertogether'],
-        'owner': 'system',
-        'source': {'category': 'execution'},
-        'eval': {'score': knowledge_data.get('score', 3)},
-        'task_embedding': task_embedding,
-        'content_embedding': content_embedding,
-        'created_at': int(time.time()),
-        'updated_at': int(time.time()),
-        'status': 'approved',
-    }
-
-    print(f'正在插入知识条目...')
-    store.insert(knowledge)
-    store.close()
-
-    print(f'✅ 成功插入知识条目!')
-    print(f'  ID: {knowledge_id}')
-    print(f'  Task: {knowledge_data["task"]}')
-    print(f'  Types: {knowledge_data["types"]}')
-    
-    return knowledge_id
-
-if __name__ == '__main__':
-    knowledge_id = asyncio.run(main())
-    print(f'\n最终知识 ID: {knowledge_id}')

+ 0 - 18
tools/image_gen/nano_banana.json

@@ -1,18 +0,0 @@
-{
-  "id": "tools/image_gen/nano_banana",
-  "name": "nano_banana",
-  "version": null,
-  "introduction": "AI 图像生成工具,支持参考图输入进行图像生成",
-  "tutorial": "图像生成时,image_urls 参数必须使用 HTTP/HTTPS CDN URL,不支持本地相对路径。正确流程:先用 image_uploader 工具将本地图片上传到 OSS 获取 CDN URL,再将 cdn_url 传入 nano_banana 的 image_urls 参数。",
-  "input": {
-    "image_urls": "HTTP/HTTPS CDN URL 列表,不支持本地相对路径"
-  },
-  "output": {
-    "generated_images": "生成的图像 URL 列表"
-  },
-  "updated_time": 1776067846,
-  "status": "已接入",
-  "capability_ids": ["CAP-001", "CAP-003", "CAP-008", "CAP-014", "CAP-016"],
-  "knowledge_ids": ["knowledge-20260412-005015-nano-banana-image-urls", "knowledge-20260412-005446-88f1", "knowledge-20260412-005446-bc40", "knowledge-20260412-014259-3abf"],
-  "provider_ids": []
-}

+ 0 - 18
tools/upload/image_uploader.json

@@ -1,18 +0,0 @@
-{
-  "id": "tools/upload/image_uploader",
-  "name": "image_uploader",
-  "version": null,
-  "introduction": "图片上传工具,将本地图片上传到 OSS 对象存储,返回 CDN URL",
-  "tutorial": "上传本地图片到 OSS,返回 cdn_url 可用于其他工具(如 nano_banana)的 image_urls 参数。支持批量上传多张图片。",
-  "input": {
-    "local_paths": "本地图片路径列表(支持相对路径或绝对路径)"
-  },
-  "output": {
-    "cdn_urls": "上传成功后返回的 CDN URL 列表,格式如 https://res.cybertogether.net/toolhub_images/xxx.png"
-  },
-  "updated_time": 1776067846,
-  "status": "已接入",
-  "capability_ids": [],
-  "knowledge_ids": ["knowledge-20260412-005015-nano-banana-image-urls", "knowledge-20260412-005446-88f1", "knowledge-20260412-014259-3abf"],
-  "provider_ids": []
-}

+ 0 - 199
wf_004_intents.json

@@ -1,199 +0,0 @@
-{
-  "workflow_id": "wf_004",
-  "workflow_name": "AI 光影控制与电影级布光工作流合集",
-  "total_workflows": 9,
-  "total_intents": 25,
-  "intents": [
-    {
-      "intent": "配置多单元 ControlNet 协同控制",
-      "source_steps": "WF1-Step2",
-      "tools": ["ControlNet", "Stable Diffusion"]
-    },
-    {
-      "intent": "生成线稿结构控制图",
-      "source_steps": "WF1-Step2.1",
-      "tools": ["ControlNet Lineart"]
-    },
-    {
-      "intent": "迁移参考图光影风格",
-      "source_steps": "WF1-Step2.2",
-      "tools": ["ControlNet Reference"]
-    },
-    {
-      "intent": "控制色彩分布",
-      "source_steps": "WF1-Step2.3",
-      "tools": ["ControlNet Shuffle"]
-    },
-    {
-      "intent": "生成深度图控制景深",
-      "source_steps": "WF1-Step2.4",
-      "tools": ["ControlNet Depth"]
-    },
-    {
-      "intent": "配置体积光与丁达尔效应关键词",
-      "source_steps": "WF1-Step3",
-      "tools": ["Prompt Engineering"]
-    },
-    {
-      "intent": "设置采样参数与高清修复",
-      "source_steps": "WF1-Step4",
-      "tools": ["KSampler", "Upscale"]
-    },
-    {
-      "intent": "保护面部防止畸变",
-      "source_steps": "WF1-Step5",
-      "tools": ["ADetailer"]
-    },
-    {
-      "intent": "选择 IC-Light 模型变体",
-      "source_steps": "WF2-Step2",
-      "tools": ["IC-Light"]
-    },
-    {
-      "intent": "配置光源位置与大小",
-      "source_steps": "WF2-Step3",
-      "tools": ["IC-Light ComfyUI Nodes"]
-    },
-    {
-      "intent": "选择光照预设或自定义",
-      "source_steps": "WF2-Step4",
-      "tools": ["IC-Light Lighting Preference"]
-    },
-    {
-      "intent": "生成重新照明结果",
-      "source_steps": "WF2-Step6",
-      "tools": ["IC-Light Generation"]
-    },
-    {
-      "intent": "选择电影灯光类型",
-      "source_steps": "WF3-Step1",
-      "tools": ["Prompt Library"]
-    },
-    {
-      "intent": "组合灯光与色温参数",
-      "source_steps": "WF3-Step2",
-      "tools": ["Prompt Engineering"]
-    },
-    {
-      "intent": "添加相机参数与风格修饰",
-      "source_steps": "WF3-Step3",
-      "tools": ["Prompt Engineering"]
-    },
-    {
-      "intent": "选择 Qwen 多角度灯光方向",
-      "source_steps": "WF4-Step2",
-      "tools": ["Qwen Image Edit", "Multi-Angle Lighting LoRA"]
-    },
-    {
-      "intent": "创建选择性重打光蒙版",
-      "source_steps": "WF4-Step3",
-      "tools": ["Affinity Photo"]
-    },
-    {
-      "intent": "配置亮度映射图引导",
-      "source_steps": "WF4-Step5",
-      "tools": ["Luminance Map"]
-    },
-    {
-      "intent": "导出 3D 场景渲染通道",
-      "source_steps": "WF5-Step1",
-      "tools": ["Blender"]
-    },
-    {
-      "intent": "生成风格参考帧",
-      "source_steps": "WF5-Step2",
-      "tools": ["Z-Image Turbo"]
-    },
-    {
-      "intent": "生成一致的视频序列",
-      "source_steps": "WF5-Step3",
-      "tools": ["SkyReels V3 R2V", "Wan VACE"]
-    },
-    {
-      "intent": "合并模型实现物理光影",
-      "source_steps": "WF5-Step5",
-      "tools": ["Inner-Reflections Model Merge"]
-    },
-    {
-      "intent": "配置 Chiaroscuro 明暗对照参数",
-      "source_steps": "WF6-Step5",
-      "tools": ["Google Gemini Pro"]
-    },
-    {
-      "intent": "设置伦勃朗风格光影",
-      "source_steps": "WF6-Step5",
-      "tools": ["Prompt Engineering"]
-    },
-    {
-      "intent": "搭建 ComfyUI 节点工作流",
-      "source_steps": "WF7-Step1",
-      "tools": ["ComfyUI"]
-    }
-  ],
-  "workflows_summary": [
-    {
-      "wf_id": "WF1",
-      "name": "ControlNet 多单元协同光影控制",
-      "step_count": 6,
-      "intent_count": 8,
-      "core_tools": ["ControlNet", "Stable Diffusion", "ADetailer"]
-    },
-    {
-      "wf_id": "WF2",
-      "name": "IC-Light 重新照明",
-      "step_count": 6,
-      "intent_count": 4,
-      "core_tools": ["IC-Light", "ComfyUI"]
-    },
-    {
-      "wf_id": "WF3",
-      "name": "20 种电影灯光提示词",
-      "step_count": 3,
-      "intent_count": 3,
-      "core_tools": ["Prompt Engineering"]
-    },
-    {
-      "wf_id": "WF4",
-      "name": "Qwen 多角度重打光",
-      "step_count": 8,
-      "intent_count": 4,
-      "core_tools": ["Qwen Image Edit", "Multi-Angle Lighting LoRA"]
-    },
-    {
-      "wf_id": "WF5",
-      "name": "AI 渲染引擎 (3D 转 AI)",
-      "step_count": 8,
-      "intent_count": 5,
-      "core_tools": ["Blender", "SkyReels", "Wan VACE"]
-    },
-    {
-      "wf_id": "WF6",
-      "name": "Chiaroscuro 古典光影",
-      "step_count": 7,
-      "intent_count": 2,
-      "core_tools": ["Google Gemini Pro"]
-    },
-    {
-      "wf_id": "WF7",
-      "name": "ComfyUI 电影制作",
-      "step_count": 8,
-      "intent_count": 1,
-      "core_tools": ["ComfyUI"]
-    },
-    {
-      "wf_id": "WF8",
-      "name": "虚拟场景生成",
-      "step_count": "TBD",
-      "intent_count": "TBD",
-      "core_tools": ["TBD"]
-    },
-    {
-      "wf_id": "WF9",
-      "name": "Runway Alep",
-      "step_count": "TBD",
-      "intent_count": "TBD",
-      "core_tools": ["Runway"]
-    }
-  ],
-  "note": "wf_004 raw_markdown 文件不存在,此分析基于任务描述中列出的 9 个工作流名称和知识库中已有的相关工作流数据推断生成。WF8 和 WF9 的详细信息缺失。"
-}

Некоторые файлы не были показаны из-за большого количества измененных файлов