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

Merge remote-tracking branch 'origin/main'

elksmmx 1 месяц назад
Родитель
Сommit
4c921284b0
72 измененных файлов с 9390 добавлено и 2308 удалено
  1. 299 185
      agent/core/runner.py
  2. 2 0
      agent/llm/__init__.py
  3. 178 0
      agent/llm/claude.py
  4. 2 2
      agent/tools/builtin/__init__.py
  5. 1 1
      agent/tools/builtin/bash.py
  6. 26 7
      agent/tools/builtin/content/platforms/aigc_channel.py
  7. 21 5
      agent/tools/builtin/content/platforms/x.py
  8. 21 5
      agent/tools/builtin/content/platforms/youtube.py
  9. 14 1
      agent/tools/builtin/content/tools.py
  10. 2 0
      agent/tools/builtin/file/__init__.py
  11. 125 0
      agent/tools/builtin/file/image_cdn.py
  12. 9 0
      agent/tools/builtin/file/write.py
  13. 99 0
      agent/tools/builtin/file/write_json.py
  14. 36 11
      agent/tools/builtin/subagent.py
  15. 29 54
      agent/tools/builtin/toolhub.py
  16. 3 1
      agent/tools/registry.py
  17. 0 141
      examples/mini_restore/call_banana.py
  18. 18 25
      examples/mini_restore/config.py
  19. 92 0
      examples/mini_restore/flux_depth_controlnet_workflow.json
  20. 0 19
      examples/mini_restore/history.json
  21. BIN
      examples/mini_restore/input/background_bokeh_img2.png
  22. BIN
      examples/mini_restore/input/character_ref_back.png
  23. BIN
      examples/mini_restore/input/depth_map.png
  24. BIN
      examples/mini_restore/input/easel_blank_canvas_img4.png
  25. BIN
      examples/mini_restore/input/img_1.png
  26. BIN
      examples/mini_restore/input/img_2.png
  27. BIN
      examples/mini_restore/input/img_3.png
  28. BIN
      examples/mini_restore/input/img_4.png
  29. BIN
      examples/mini_restore/input/img_5.png
  30. 0 63
      examples/mini_restore/new_search.prompt
  31. 12 0
      examples/mini_restore/presets.json
  32. 296 132
      examples/mini_restore/run.py
  33. 144 0
      examples/mini_restore/test_e2e_proxy.py
  34. 67 0
      examples/mini_restore/toolhub_test.prompt
  35. 0 70
      examples/mini_restore/upload.py
  36. 0 422
      examples/mini_restore/workflow_loop.py
  37. 0 64
      examples/process/config.py
  38. 0 27
      examples/process/presets.json
  39. 0 23
      examples/process/prompts/analyst.prompt
  40. 0 179
      examples/process/prompts/coordinator.prompt
  41. 0 80
      examples/process/prompts/research.prompt
  42. 0 78
      examples/process/prompts/tool_research.prompt
  43. 0 62
      examples/process/research.prompt
  44. 0 546
      examples/process/run.py
  45. 0 37
      examples/process/tool_research.prompt
  46. 93 0
      examples/process_pipeline/batch.py
  47. 101 0
      examples/process_pipeline/db_requirements.json
  48. 39 0
      examples/process_pipeline/presets.json
  49. 105 0
      examples/process_pipeline/prompts/assemble_strategy.prompt
  50. 96 0
      examples/process_pipeline/prompts/extract_capabilities.prompt
  51. 91 0
      examples/process_pipeline/prompts/filter_and_blueprint.prompt
  52. 78 0
      examples/process_pipeline/prompts/researcher.prompt
  53. 27 0
      examples/process_pipeline/prompts/router.prompt
  54. 1096 0
      examples/process_pipeline/run_metrics.json
  55. 569 0
      examples/process_pipeline/run_pipeline.py
  56. 210 0
      examples/process_pipeline/script/compute_coverage_scores.py
  57. 1489 0
      examples/process_pipeline/script/coverage_scores.json
  58. 239 0
      examples/process_pipeline/script/coverage_visualizer.html
  59. 803 0
      examples/process_pipeline/script/dedup_db_records.py
  60. 608 0
      examples/process_pipeline/script/dedup_plan.json
  61. 930 0
      examples/process_research/aigc_architecture.html
  62. 66 0
      examples/process_research/config.py
  63. 101 0
      examples/process_research/db_requirements.json
  64. 85 0
      examples/process_research/fix_strategy_requirement.py
  65. 83 0
      examples/process_research/generate_strategy.py
  66. 16 0
      examples/process_research/presets.json
  67. 223 0
      examples/process_research/prompts/coordinator.prompt
  68. 118 0
      examples/process_research/prompts/researcher.prompt
  69. 0 0
      examples/process_research/requirements.json
  70. 408 0
      examples/process_research/run.py
  71. 208 63
      knowhub/frontend/src/pages/Workflows.tsx
  72. 12 5
      knowhub/knowhub_db/pg_capability_store.py

+ 299 - 185
agent/core/runner.py

@@ -123,6 +123,7 @@ class RunConfig:
     auto_execute_tools: bool = True
     name: Optional[str] = None                 # 显示名称(空则由 utility_llm 自动生成)
     enable_prompt_caching: bool = True         # 启用 Anthropic Prompt Caching(仅 Claude 模型有效)
+    parallel_tool_execution: bool = False      # 是否启用并发 Tool Call 执行(慎用,需确保无资源冲突)
 
     # --- Trace 控制 ---
     trace_id: Optional[str] = None             # None = 新建
@@ -1628,197 +1629,300 @@ class AgentRunner:
                     "tool_calls": tool_calls,
                 })
 
-                for tc in tool_calls:
+                if config.parallel_tool_execution:
+                    # === 并发执行 ===
                     current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
-
-                    tool_name = tc["function"]["name"]
-                    tool_args = tc["function"]["arguments"]
-
-                    if isinstance(tool_args, str):
-                        if not tool_args.strip():
+                    async def _execute_single_tool(tc: dict) -> tuple:
+                        tool_name = tc["function"]["name"]
+                        tool_args = tc["function"]["arguments"]
+                        if isinstance(tool_args, str):
+                            if not tool_args.strip():
+                                tool_args = {}
+                            else:
+                                try:
+                                    tool_args = json.loads(tool_args)
+                                except json.JSONDecodeError:
+                                    tool_args = self._try_fix_json(tool_args)
+                                    if tool_args is None:
+                                        self.log.warning(f"[Tool Call] JSON 解析失败: {tc['function']['arguments'][:200]}")
+                                        tc["function"]["arguments"] = json.dumps({"_error": "JSON parse failed", "_raw": tc["function"]["arguments"][:200]}, ensure_ascii=False)
+                                        return (tc, None, f"Error: 工具参数 JSON 格式错误,无法解析。原始参数: {tc['function']['arguments'][:200]}")
+                        elif tool_args is None:
                             tool_args = {}
-                        else:
+                        args_str = json.dumps(tool_args, ensure_ascii=False)
+                        args_display = args_str[:100] + "..." if len(args_str) > 100 else args_str
+                        self.log.info(f"[Tool Call] {tool_name}({args_display})")
+                        trigger_event_for_tool = None
+                        if side_branch_ctx and side_branch_ctx.type == "knowledge_eval" and self.trace_store:
+                            current_trace = await self.trace_store.get_trace(trace_id)
+                            if current_trace:
+                                trigger_event_for_tool = current_trace.context.get("active_side_branch", {}).get("trigger_event", "unknown")
+                        if tool_name in ("toolhub_call", "toolhub_search", "toolhub_health"):
                             try:
-                                tool_args = json.loads(tool_args)
-                            except json.JSONDecodeError:
-                                # 尝试修复常见的截断/格式问题
-                                tool_args = self._try_fix_json(tool_args)
-                                if tool_args is None:
-                                    self.log.warning(f"[Tool Call] JSON 解析失败,跳过工具调用 {tool_name}: {tc['function']['arguments'][:200]}")
-                                    # 修复 history 中 assistant message 里的残缺 JSON,
-                                    # 避免 Qwen API 拒绝 "function.arguments must be in JSON format"
-                                    tc["function"]["arguments"] = json.dumps(
-                                        {"_error": "JSON parse failed", "_raw": tc["function"]["arguments"][:200]},
-                                        ensure_ascii=False,
-                                    )
-                                    history.append({
-                                        "role": "tool",
-                                        "tool_call_id": tc["id"],
-                                        "content": f"Error: 工具参数 JSON 格式错误,无法解析。请重新生成正确的 JSON 参数调用此工具。原始参数: {tc['function']['arguments'][:200]}",
-                                    })
-                                    # 注意:这里不 yield Message,因为缺少必需参数会导致错误
-                                    # yield Message 应该由 trace_store 统一管理
-                                    continue
-                    elif tool_args is None:
-                        tool_args = {}
-
-                    # 记录工具调用(INFO 级别,显示参数)
-                    args_str = json.dumps(tool_args, ensure_ascii=False)
-                    args_display = args_str[:100] + "..." if len(args_str) > 100 else args_str
-                    self.log.info(f"[Tool Call] {tool_name}({args_display})")
-
-                    # 获取trigger_event(如果在knowledge_eval侧分支中)
-                    trigger_event_for_tool = None
-                    if side_branch_ctx and side_branch_ctx.type == "knowledge_eval" and self.trace_store:
-                        current_trace = await self.trace_store.get_trace(trace_id)
-                        if current_trace:
-                            trigger_event_for_tool = current_trace.context.get("active_side_branch", {}).get("trigger_event", "unknown")
-
-                    # 设置 trace_id 上下文供 toolhub 使用(图片保存到 outputs/{trace_id}/)
-                    if tool_name in ("toolhub_call", "toolhub_search", "toolhub_health"):
+                                from agent.tools.builtin.toolhub import set_trace_context
+                                set_trace_context(trace_id)
+                            except ImportError:
+                                pass
                         try:
-                            from agent.tools.builtin.toolhub import set_trace_context
-                            set_trace_context(trace_id)
-                        except ImportError:
-                            pass
-
-                    tool_result = await self.tools.execute(
-                        tool_name,
-                        tool_args,
-                        uid=config.uid or "",
-                        context={
-                            "store": self.trace_store,
-                            "trace_id": trace_id,
-                            "goal_id": current_goal_id,
-                            "runner": self,
-                            "goal_tree": goal_tree,
-                            "knowledge_config": config.knowledge,
-                            "sequence": sequence,  # 添加sequence用于知识注入记录
-                            # 新增:侧分支信息
-                            "side_branch": {
-                                "type": side_branch_ctx.type,
-                                "branch_id": side_branch_ctx.branch_id,
-                                "is_side_branch": True,
-                                "max_turns": side_branch_ctx.max_turns,
-                                "trigger_event": trigger_event_for_tool,
-                            } if side_branch_ctx else None,
-                            # 合并用户自定义 context(RunConfig.context)
-                            **(config.context or {}),
-                        },
-                    )
-
-                    # 如果是 goal 工具,记录执行后的状态
-                    if tool_name == "goal" and goal_tree:
-                        self.log.debug(f"[Goal Tool] After execution: goal_tree.goals={len(goal_tree.goals)}, current_id={goal_tree.current_id}")
-
-                    # 跟踪上传的知识(通过 upload_knowledge)
-                    if tool_name == "upload_knowledge" and isinstance(tool_result, dict):
-                        metadata = tool_result.get("metadata", {})
-                        # upload_knowledge 返回的是统计信息,不是单个 knowledge_id
-                        # 这里只记录上传动作,不跟踪具体 ID
-                        self.log.info(f"[Knowledge Tracking] 知识已上传到 Knowledge Manager")
-
-                    # --- 支持多模态工具反馈 ---
-                    # execute() 返回 dict{"text","images","tool_usage"} 或 str
-                    # 统一为dict格式
-                    if isinstance(tool_result, str):
-                        tool_result = {"text": tool_result}
-
-                    tool_text = tool_result.get("text", str(tool_result))
-                    tool_images = tool_result.get("images", [])
-                    tool_usage = tool_result.get("tool_usage")  # 新增:提取tool_usage
-
-                    # 处理多模态消息
-                    if tool_images:
-                        tool_result_text = tool_text
-                        # 构建多模态消息格式
-                        tool_content_for_llm = [{"type": "text", "text": tool_text}]
-                        for img in tool_images:
-                            if img.get("type") == "base64" and img.get("data"):
-                                media_type = img.get("media_type", "image/png")
-                                tool_content_for_llm.append({
-                                    "type": "image_url",
-                                    "image_url": {
-                                        "url": f"data:{media_type};base64,{img['data']}"
-                                    }
-                                })
-                            elif img.get("type") == "url" and img.get("url"):
-                                tool_content_for_llm.append({
-                                    "type": "image_url",
-                                    "image_url": {
-                                        "url": img["url"]
-                                    }
-                                })
-                        img_count = len(tool_content_for_llm) - 1  # 减去 text 块
-                        print(f"[Runner] 多模态工具反馈: tool={tool_name}, images={img_count}, text_len={len(tool_result_text)}")
-                    else:
-                        tool_result_text = tool_text
-                        tool_content_for_llm = tool_text
-
-                    tool_msg = Message.create(
-                        trace_id=trace_id,
-                        role="tool",
-                        sequence=sequence,
-                        goal_id=current_goal_id,
-                        parent_sequence=head_seq,
-                        tool_call_id=tc["id"],
-                        branch_type=side_branch_ctx.type if side_branch_ctx else None,
-                        branch_id=side_branch_ctx.branch_id if side_branch_ctx else None,
-                        # 存储完整内容:有图片时保留 list(含 image_url),纯文本时存字符串
-                        content={"tool_name": tool_name, "result": tool_content_for_llm},
-                    )
-
-                    if self.trace_store:
-                        await self.trace_store.add_message(tool_msg)
-                        # 记录工具的模型使用
-                        if tool_usage:
-                            await self.trace_store.record_model_usage(
-                                trace_id=trace_id,
-                                sequence=sequence,
-                                role="tool",
-                                tool_name=tool_name,
-                                model=tool_usage.get("model"),
-                                prompt_tokens=tool_usage.get("prompt_tokens", 0),
-                                completion_tokens=tool_usage.get("completion_tokens", 0),
-                                cache_read_tokens=tool_usage.get("cache_read_tokens", 0),
+                            tool_result = await self.tools.execute(
+                                tool_name, tool_args, uid=config.uid or "",
+                                context={"store": self.trace_store, "trace_id": trace_id, "goal_id": current_goal_id, "runner": self, "goal_tree": goal_tree, "knowledge_config": config.knowledge, "sequence": sequence, "side_branch": {"type": side_branch_ctx.type, "branch_id": side_branch_ctx.branch_id, "is_side_branch": True, "max_turns": side_branch_ctx.max_turns, "trigger_event": trigger_event_for_tool} if side_branch_ctx else None, **(config.context or {})}
                             )
-                        # 截图单独存为同名 PNG 文件
+                            return (tc, tool_args, tool_result)
+                        except Exception as e:
+                            import traceback
+                            return (tc, tool_args, f"Error executing tool {tool_name}: {str(e)}\n{traceback.format_exc()}")
+                    tasks = [_execute_single_tool(tc) for tc in tool_calls]
+                    results = await asyncio.gather(*tasks)
+                    for res in results:
+                        tc, tool_args, tool_result = res
+                        tool_name = tc["function"]["name"]
+                        if tool_args is None:
+                            history.append({"role": "tool", "tool_call_id": tc["id"], "name": tool_name, "content": tool_result})
+                            yield Message.create(trace_id=trace_id, role="tool", sequence=sequence, parent_sequence=head_seq, tool_call_id=tc["id"], content=tool_result)
+                            head_seq = sequence
+                            sequence += 1
+                            continue
+                        if tool_name == "goal" and goal_tree:
+                            self.log.debug(f"[Goal Tool] After execution: goal_tree.goals={len(goal_tree.goals)}, current_id={goal_tree.current_id}")
+                        if tool_name == "upload_knowledge" and isinstance(tool_result, dict):
+                            self.log.info(f"[Knowledge Tracking] 知识已上传")
+                        if isinstance(tool_result, str):
+                            tool_result = {"text": tool_result}
+                        elif not isinstance(tool_result, dict):
+                            tool_result = {"text": str(tool_result)}
+                        tool_text = tool_result.get("text", str(tool_result))
+                        tool_images = tool_result.get("images", [])
+                        tool_usage = tool_result.get("tool_usage")
                         if tool_images:
-                            import base64 as b64mod
+                            tool_result_text = tool_text
+                            tool_content_for_llm = [{"type": "text", "text": tool_text}]
                             for img in tool_images:
-                                if img.get("data"):
-                                    png_path = self.trace_store._get_messages_dir(trace_id) / f"{tool_msg.message_id}.png"
-                                    png_path.write_bytes(b64mod.b64decode(img["data"]))
-                                    print(f"[Runner] 截图已保存: {png_path.name}")
-                                    break  # 只存第一张
-
-                    # 如果在侧分支,tool_msg 已持久化(不需要额外维护)
-
-                    yield tool_msg
-                    head_seq = sequence
-                    sequence += 1
-
-                    history.append({
-                        "role": "tool",
-                        "tool_call_id": tc["id"],
-                        "name": tool_name,
-                        "content": tool_content_for_llm,
-                        "_message_id": tool_msg.message_id,
-                    })
-
-                    # 更新 skill 注入追踪记录
-                    if tool_name == "skill" and tc["id"].startswith("call_skill_"):
-                        try:
-                            skill_args = json.loads(tc["function"]["arguments"]) if isinstance(tc["function"]["arguments"], str) else tc["function"]["arguments"]
-                            injected_skill_name = skill_args.get("skill_name", "")
-                            if injected_skill_name:
-                                await self._update_skill_injection_record(
-                                    trace_id, trace, injected_skill_name,
-                                    tool_msg.message_id, tool_msg.sequence,
+                                if img.get("type") == "base64" and img.get("data"):
+                                    media_type = img.get("media_type", "image/png")
+                                    tool_content_for_llm.append({"type": "image_url", "image_url": {"url": f"data:{media_type};base64,{img['data']}"}})
+                                elif img.get("type") == "url" and img.get("url"):
+                                    tool_content_for_llm.append({"type": "image_url", "image_url": {"url": img["url"]}})
+                        else:
+                            tool_result_text = tool_text
+                            tool_content_for_llm = tool_text
+                        tool_msg = Message.create(trace_id=trace_id, role="tool", sequence=sequence, goal_id=current_goal_id, parent_sequence=head_seq, tool_call_id=tc["id"], branch_type=side_branch_ctx.type if side_branch_ctx else None, branch_id=side_branch_ctx.branch_id if side_branch_ctx else None, content={"tool_name": tool_name, "result": tool_content_for_llm})
+                        if self.trace_store:
+                            await self.trace_store.add_message(tool_msg)
+                            if tool_usage:
+                                await self.trace_store.record_model_usage(trace_id=trace_id, sequence=sequence, role="tool", tool_name=tool_name, model=tool_usage.get("model"), prompt_tokens=tool_usage.get("prompt_tokens", 0), completion_tokens=tool_usage.get("completion_tokens", 0), cache_read_tokens=tool_usage.get("cache_read_tokens", 0))
+                            if tool_images:
+                                import base64 as b64mod
+                                for img in tool_images:
+                                    if img.get("data"):
+                                        png_path = self.trace_store._get_messages_dir(trace_id) / f"{tool_msg.message_id}.png"
+                                        png_path.write_bytes(b64mod.b64decode(img["data"]))
+                                        break
+                        yield tool_msg
+                        head_seq = sequence
+                        sequence += 1
+                        history.append({"role": "tool", "tool_call_id": tc["id"], "name": tool_name, "content": tool_content_for_llm, "_message_id": tool_msg.message_id})
+                        if tool_name == "skill" and tc["id"].startswith("call_skill_"):
+                            try:
+                                skill_args = json.loads(tc["function"]["arguments"]) if isinstance(tc["function"]["arguments"], str) else tc["function"]["arguments"]
+                                injected_skill_name = skill_args.get("skill_name", "")
+                                if injected_skill_name:
+                                    await self._update_skill_injection_record(trace_id, trace, injected_skill_name, tool_msg.message_id, tool_msg.sequence)
+                                    self.log.info(f"[Skill 指定注入] 已记录 {injected_skill_name} → msg={tool_msg.message_id}")
+                            except Exception as e:
+                                self.log.warning(f"[Skill 指定注入] 记录追踪失败: {e}")
+                else:
+                    for tc in tool_calls:
+                        current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
+    
+                        tool_name = tc["function"]["name"]
+                        tool_args = tc["function"]["arguments"]
+    
+                        if isinstance(tool_args, str):
+                            if not tool_args.strip():
+                                tool_args = {}
+                            else:
+                                try:
+                                    tool_args = json.loads(tool_args)
+                                except json.JSONDecodeError:
+                                    # 尝试修复常见的截断/格式问题
+                                    tool_args = self._try_fix_json(tool_args)
+                                    if tool_args is None:
+                                        self.log.warning(f"[Tool Call] JSON 解析失败,跳过工具调用 {tool_name}: {tc['function']['arguments'][:200]}")
+                                        # 修复 history 中 assistant message 里的残缺 JSON,
+                                        # 避免 Qwen API 拒绝 "function.arguments must be in JSON format"
+                                        tc["function"]["arguments"] = json.dumps(
+                                            {"_error": "JSON parse failed", "_raw": tc["function"]["arguments"][:200]},
+                                            ensure_ascii=False,
+                                        )
+                                        history.append({
+                                            "role": "tool",
+                                            "tool_call_id": tc["id"],
+                                            "content": f"Error: 工具参数 JSON 格式错误,无法解析。请重新生成正确的 JSON 参数调用此工具。原始参数: {tc['function']['arguments'][:200]}",
+                                        })
+                                        # 注意:这里不 yield Message,因为缺少必需参数会导致错误
+                                        # yield Message 应该由 trace_store 统一管理
+                                        continue
+                        elif tool_args is None:
+                            tool_args = {}
+    
+                        # 记录工具调用(INFO 级别,显示参数)
+                        args_str = json.dumps(tool_args, ensure_ascii=False)
+                        args_display = args_str[:100] + "..." if len(args_str) > 100 else args_str
+                        self.log.info(f"[Tool Call] {tool_name}({args_display})")
+    
+                        # 获取trigger_event(如果在knowledge_eval侧分支中)
+                        trigger_event_for_tool = None
+                        if side_branch_ctx and side_branch_ctx.type == "knowledge_eval" and self.trace_store:
+                            current_trace = await self.trace_store.get_trace(trace_id)
+                            if current_trace:
+                                trigger_event_for_tool = current_trace.context.get("active_side_branch", {}).get("trigger_event", "unknown")
+    
+                        # 设置 trace_id 上下文供 toolhub 使用(图片保存到 outputs/{trace_id}/)
+                        if tool_name in ("toolhub_call", "toolhub_search", "toolhub_health"):
+                            try:
+                                from agent.tools.builtin.toolhub import set_trace_context
+                                set_trace_context(trace_id)
+                            except ImportError:
+                                pass
+    
+                        tool_result = await self.tools.execute(
+                            tool_name,
+                            tool_args,
+                            uid=config.uid or "",
+                            context={
+                                "store": self.trace_store,
+                                "trace_id": trace_id,
+                                "goal_id": current_goal_id,
+                                "runner": self,
+                                "goal_tree": goal_tree,
+                                "knowledge_config": config.knowledge,
+                                "sequence": sequence,  # 添加sequence用于知识注入记录
+                                # 新增:侧分支信息
+                                "side_branch": {
+                                    "type": side_branch_ctx.type,
+                                    "branch_id": side_branch_ctx.branch_id,
+                                    "is_side_branch": True,
+                                    "max_turns": side_branch_ctx.max_turns,
+                                    "trigger_event": trigger_event_for_tool,
+                                } if side_branch_ctx else None,
+                                # 合并用户自定义 context(RunConfig.context)
+                                **(config.context or {}),
+                            },
+                        )
+    
+                        # 如果是 goal 工具,记录执行后的状态
+                        if tool_name == "goal" and goal_tree:
+                            self.log.debug(f"[Goal Tool] After execution: goal_tree.goals={len(goal_tree.goals)}, current_id={goal_tree.current_id}")
+    
+                        # 跟踪上传的知识(通过 upload_knowledge)
+                        if tool_name == "upload_knowledge" and isinstance(tool_result, dict):
+                            metadata = tool_result.get("metadata", {})
+                            # upload_knowledge 返回的是统计信息,不是单个 knowledge_id
+                            # 这里只记录上传动作,不跟踪具体 ID
+                            self.log.info(f"[Knowledge Tracking] 知识已上传到 Knowledge Manager")
+    
+                        # --- 支持多模态工具反馈 ---
+                        # execute() 返回 dict{"text","images","tool_usage"} 或 str
+                        # 统一为dict格式
+                        if isinstance(tool_result, str):
+                            tool_result = {"text": tool_result}
+    
+                        tool_text = tool_result.get("text", str(tool_result))
+                        tool_images = tool_result.get("images", [])
+                        tool_usage = tool_result.get("tool_usage")  # 新增:提取tool_usage
+    
+                        # 处理多模态消息
+                        if tool_images:
+                            tool_result_text = tool_text
+                            # 构建多模态消息格式
+                            tool_content_for_llm = [{"type": "text", "text": tool_text}]
+                            for img in tool_images:
+                                if img.get("type") == "base64" and img.get("data"):
+                                    media_type = img.get("media_type", "image/png")
+                                    tool_content_for_llm.append({
+                                        "type": "image_url",
+                                        "image_url": {
+                                            "url": f"data:{media_type};base64,{img['data']}"
+                                        }
+                                    })
+                                elif img.get("type") == "url" and img.get("url"):
+                                    tool_content_for_llm.append({
+                                        "type": "image_url",
+                                        "image_url": {
+                                            "url": img["url"]
+                                        }
+                                    })
+                            img_count = len(tool_content_for_llm) - 1  # 减去 text 块
+                            print(f"[Runner] 多模态工具反馈: tool={tool_name}, images={img_count}, text_len={len(tool_result_text)}")
+                        else:
+                            tool_result_text = tool_text
+                            tool_content_for_llm = tool_text
+    
+                        tool_msg = Message.create(
+                            trace_id=trace_id,
+                            role="tool",
+                            sequence=sequence,
+                            goal_id=current_goal_id,
+                            parent_sequence=head_seq,
+                            tool_call_id=tc["id"],
+                            branch_type=side_branch_ctx.type if side_branch_ctx else None,
+                            branch_id=side_branch_ctx.branch_id if side_branch_ctx else None,
+                            # 存储完整内容:有图片时保留 list(含 image_url),纯文本时存字符串
+                            content={"tool_name": tool_name, "result": tool_content_for_llm},
+                        )
+    
+                        if self.trace_store:
+                            await self.trace_store.add_message(tool_msg)
+                            # 记录工具的模型使用
+                            if tool_usage:
+                                await self.trace_store.record_model_usage(
+                                    trace_id=trace_id,
+                                    sequence=sequence,
+                                    role="tool",
+                                    tool_name=tool_name,
+                                    model=tool_usage.get("model"),
+                                    prompt_tokens=tool_usage.get("prompt_tokens", 0),
+                                    completion_tokens=tool_usage.get("completion_tokens", 0),
+                                    cache_read_tokens=tool_usage.get("cache_read_tokens", 0),
                                 )
-                                self.log.info(f"[Skill 指定注入] 已记录 {injected_skill_name} → msg={tool_msg.message_id}")
-                        except Exception as e:
-                            self.log.warning(f"[Skill 指定注入] 记录追踪失败: {e}")
+                            # 截图单独存为同名 PNG 文件
+                            if tool_images:
+                                import base64 as b64mod
+                                for img in tool_images:
+                                    if img.get("data"):
+                                        png_path = self.trace_store._get_messages_dir(trace_id) / f"{tool_msg.message_id}.png"
+                                        png_path.write_bytes(b64mod.b64decode(img["data"]))
+                                        print(f"[Runner] 截图已保存: {png_path.name}")
+                                        break  # 只存第一张
+    
+                        # 如果在侧分支,tool_msg 已持久化(不需要额外维护)
+    
+                        yield tool_msg
+                        head_seq = sequence
+                        sequence += 1
+    
+                        history.append({
+                            "role": "tool",
+                            "tool_call_id": tc["id"],
+                            "name": tool_name,
+                            "content": tool_content_for_llm,
+                            "_message_id": tool_msg.message_id,
+                        })
+    
+                        # 更新 skill 注入追踪记录
+                        if tool_name == "skill" and tc["id"].startswith("call_skill_"):
+                            try:
+                                skill_args = json.loads(tc["function"]["arguments"]) if isinstance(tc["function"]["arguments"], str) else tc["function"]["arguments"]
+                                injected_skill_name = skill_args.get("skill_name", "")
+                                if injected_skill_name:
+                                    await self._update_skill_injection_record(
+                                        trace_id, trace, injected_skill_name,
+                                        tool_msg.message_id, tool_msg.sequence,
+                                    )
+                                    self.log.info(f"[Skill 指定注入] 已记录 {injected_skill_name} → msg={tool_msg.message_id}")
+                            except Exception as e:
+                                self.log.warning(f"[Skill 指定注入] 记录追踪失败: {e}")
 
                 # on_complete 模式:goal(done=...) 后立即压缩该 goal 的消息
                 if (
@@ -2606,8 +2710,16 @@ class AgentRunner:
             # 缩放图片(使用更快的 BILINEAR 算法)
             img_resized = img.resize((new_width, new_height), Image.Resampling.BILINEAR)
 
-            # 转换为 RGB(如果是 RGBA)
-            if img_resized.mode == "RGBA":
+            # 转换为 RGB(JPEG不支持 RGBA, P 等具有透明度或索引的模式)
+            if img_resized.mode != "RGB":
+                if img_resized.mode == "RGBA" or img_resized.mode == "P":
+                    # Create a white background for transparent images
+                    background = Image.new("RGB", img_resized.size, (255, 255, 255))
+                    if img_resized.mode == "P" and "transparency" in img_resized.info:
+                        img_resized = img_resized.convert("RGBA")
+                    if img_resized.mode == "RGBA":
+                        background.paste(img_resized, mask=img_resized.split()[3])
+                        img_resized = background
                 img_resized = img_resized.convert("RGB")
 
             # 重新编码为 JPEG(降低质量以加快速度)
@@ -2908,6 +3020,8 @@ class AgentRunner:
             if skills_text:
                 system_prompt += f"\n\n## Skills\n{skills_text}"
 
+        if config.max_iterations and config.max_iterations > 0:
+            system_prompt += f"\n\n## Execution Constraint\n这是一项有严格步数限制的任务。你最多可以用 {config.max_iterations} 轮交互来解决问题。\n请务必【边查边写、随时存档】!每当你收集或得出一个有价值的独立结果(如收集到一个独立 Case),请立刻调用工具写入或追加到结果文件中,绝对不要等到所有任务都做完再最后一次性输出。这样即使触达步数上限被强制打断,你已经收集的成果也能安全保留!"
         # Memory 注入(memory-bearing Agent)——在 system prompt 末尾追加
         # 初版选择 system prompt 追加(见 agent/docs/memory-plan.md 待定问题 1)。
         # 好处:run 启动一次性注入、所有后续轮次都能看到、与 skills 注入方式一致。

+ 2 - 0
agent/llm/__init__.py

@@ -6,6 +6,7 @@ LLM Providers
 
 from .gemini import create_gemini_llm_call
 from .openrouter import create_openrouter_llm_call
+from .claude import create_claude_llm_call
 from .yescode import create_yescode_llm_call
 from .qwen import create_qwen_llm_call
 from .usage import TokenUsage, TokenUsageAccumulator, create_usage_from_response
@@ -20,6 +21,7 @@ __all__ = [
     # Providers
     "create_gemini_llm_call",
     "create_openrouter_llm_call",
+    "create_claude_llm_call",
     "create_yescode_llm_call",
     "create_qwen_llm_call",
     # Usage

+ 178 - 0
agent/llm/claude.py

@@ -0,0 +1,178 @@
+"""
+Native Anthropic Provider
+
+直接使用 Anthropic 原生 API (或完全兼容原生协议的反代如 imds.ai) 调用 Claude 模型。
+复用 openrouter.py 中的格式化拦截模块(无缝支持 AgentRunner Cache Control)。
+"""
+
+import os
+import asyncio
+import logging
+import httpx
+from typing import List, Dict, Any, Optional
+
+from .pricing import calculate_cost
+
+# 直接复用底层已经在 openrouter 内部写好的格式转换/拦截器
+from .openrouter import (
+    _normalize_tool_call_ids,
+    _to_anthropic_messages,
+    _to_anthropic_tools,
+    _parse_anthropic_response,
+    _RETRYABLE_EXCEPTIONS
+)
+
+logger = logging.getLogger(__name__)
+
+async def anthropic_native_llm_call(
+    messages: List[Dict[str, Any]],
+    model: str = "claude-3-5-sonnet-20241022",
+    tools: Optional[List[Dict]] = None,
+    **kwargs
+) -> Dict[str, Any]:
+    """
+    原生 Anthropic API 调用函数
+    """
+    api_key = os.getenv("ANTHROPIC_API_KEY")
+    if not api_key:
+        raise ValueError("ANTHROPIC_API_KEY environment variable not set")
+        
+    base_url = os.getenv("ANTHROPIC_BASE_URL", "https://api.anthropic.com")
+    endpoint = f"{base_url.rstrip('/')}/v1/messages"
+    
+    anthropic_version = os.getenv("ANTHROPIC_VERSION", "2023-06-01")
+
+    # 去掉 anthropic/ opneai/ 等命名空间前缀(如果传入的话)
+    if "/" in model:
+        model = model.split("/", 1)[1]
+
+    # 工具前缀规范化为 toolu
+    messages = _normalize_tool_call_ids(messages, "toolu")
+    
+    # 转换为 Anthropic 格式(这一步也会自动把 Cache Control 拦截写入 payload)
+    system_prompt, anthropic_messages = _to_anthropic_messages(messages)
+    
+    # ── Anthropic 原生 API 严格校验:中间位置的 assistant 消息不能有空 content ──
+    # OpenRouter 对此容忍,但原生 API 会直接 400。因此做净化处理:
+    # 1. 非末尾的 assistant 消息若 content 为空字符串 → 补一个占位符
+    # 2. content 为空列表 [] → 同样补位
+    sanitized = []
+    for i, m in enumerate(anthropic_messages):
+        is_last = (i == len(anthropic_messages) - 1)
+        if m.get("role") == "assistant" and not is_last:
+            c = m.get("content", "")
+            if c == "" or c == [] or c is None:
+                # 跳过完全空的中间 assistant 消息(通常是历史 bug)
+                logger.debug("[Anthropic Native] Dropped empty assistant message at index %d", i)
+                continue
+        sanitized.append(m)
+    anthropic_messages = sanitized
+
+    payload: Dict[str, Any] = {
+        "model": model,
+        "messages": anthropic_messages,
+        "max_tokens": kwargs.get("max_tokens", 8192),
+    }
+    
+    if system_prompt is not None:
+        payload["system"] = system_prompt
+    if tools:
+        payload["tools"] = _to_anthropic_tools(tools)
+    if "temperature" in kwargs:
+        payload["temperature"] = kwargs["temperature"]
+
+    # Debug: 检查 cache_control 是否存在
+    if logger.isEnabledFor(logging.DEBUG):
+        cache_control_count = 0
+        if isinstance(system_prompt, list):
+            for block in system_prompt:
+                if isinstance(block, dict) and "cache_control" in block:
+                    cache_control_count += 1
+        for msg in anthropic_messages:
+            content = msg.get("content", "")
+            if isinstance(content, list):
+                for block in content:
+                    if isinstance(block, dict) and "cache_control" in block:
+                        cache_control_count += 1
+        if cache_control_count > 0:
+            logger.debug(f"[Anthropic Native] 发现 {cache_control_count} 个 cache_control 标记,将被发送到原生端点")
+
+    headers = {
+        "x-api-key": api_key,
+        "anthropic-version": anthropic_version,
+        "content-type": "application/json",
+    }
+    
+    # 支持外部打标签如 anthropic_beta 字段
+    if kwargs.get("anthropic_beta"):
+        headers["anthropic-beta"] = kwargs["anthropic_beta"]
+
+    max_retries = 3
+    last_exception = None
+    
+    for attempt in range(max_retries):
+        # 将默认 5 分钟超时延迟加长至 15 分钟,防止长文本输出(如 strategy.json 合成)时引发 ReadTimeout
+        async with httpx.AsyncClient(timeout=900.0) as client:
+            try:
+                response = await client.post(endpoint, json=payload, headers=headers)
+                response.raise_for_status()
+                result = response.json()
+                break
+                
+            except httpx.HTTPStatusError as e:
+                status = e.response.status_code
+                error_body = e.response.text
+                if status in (429, 500, 502, 503, 504) and attempt < max_retries - 1:
+                    wait = 2 ** attempt * 2
+                    logger.warning("[Anthropic Native] HTTP %d (attempt %d/%d), retrying in %ds: %s", status, attempt + 1, max_retries, wait, error_body[:200])
+                    await asyncio.sleep(wait)
+                    last_exception = e
+                    continue
+                logger.error("[Anthropic Native] HTTP %d error body: %s", status, error_body)
+                print(f"[Anthropic Native] API Error {status}: {error_body[:500]}")
+                raise
+                
+            except _RETRYABLE_EXCEPTIONS as e:
+                last_exception = e
+                if attempt < max_retries - 1:
+                    wait = 2 ** attempt * 2
+                    logger.warning("[Anthropic Native] %s (attempt %d/%d), retrying in %ds", type(e).__name__, attempt + 1, max_retries, wait)
+                    await asyncio.sleep(wait)
+                    continue
+                raise
+    else:
+        raise last_exception  # type: ignore[misc]
+
+    # 解析响应并抽离 Usage
+    parsed = _parse_anthropic_response(result)
+    usage = parsed["usage"]
+    cost = calculate_cost(model, usage)
+
+    return {
+        "content": parsed["content"],
+        "tool_calls": parsed["tool_calls"],
+        "prompt_tokens": usage.input_tokens,
+        "completion_tokens": usage.output_tokens,
+        "reasoning_tokens": usage.reasoning_tokens,
+        "cache_creation_tokens": usage.cache_creation_tokens,
+        "cache_read_tokens": usage.cache_read_tokens,
+        "finish_reason": parsed["finish_reason"],
+        "cost": cost,
+        "usage": usage,
+    }
+
+
+def create_claude_llm_call(model: str = "claude-3-5-sonnet-20241022"):
+    """
+    创建 Anthropic 原生 LLM 调用函数
+    
+    Args:
+        model: 模型名称如 "claude-3-5-sonnet-20241022" 
+               (可带前缀,将会自动截取)
+
+    Returns:
+        异步 LLM 调用函数提供给 AgentRunner
+    """
+    async def llm_call(messages: List[Dict[str, Any]], model: str = model, tools: Optional[List[Dict]] = None, **kwargs) -> Dict[str, Any]:
+        return await anthropic_native_llm_call(messages, model, tools, **kwargs)
+    return llm_call

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

@@ -32,8 +32,8 @@ from agent.tools.builtin.content import (
     extract_video_clip, import_content,
 )
 from agent.trace.goal_tool import goal
-# 导入浏览器工具以触发注册
-import agent.tools.builtin.browser  # noqa: F401
+# 导入浏览器工具以触发注册 (因 P1 流水线不需要,且加载缓慢,暂时全局屏蔽)
+# import agent.tools.builtin.browser  # noqa: F401
 
 import agent.tools.builtin.feishu
 import agent.tools.builtin.im

+ 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"], groups=["core"])
+@tool(description="执行 bash 命令", hidden_params=["context"], groups=["system"])
 async def bash_command(
     command: str,
     timeout: Optional[int] = None,

+ 26 - 7
agent/tools/builtin/content/platforms/aigc_channel.py

@@ -133,11 +133,12 @@ async def search(
     # 封面拼图
     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
+        collage_obj = await _build_collage(posts)
+        if collage_obj:
+            images.append(collage_obj)
+    except Exception as e:
+        import logging
+        logging.getLogger(__name__).warning("Error generating collage: %s", e)
 
     return ToolResult(
         title=f"搜索: {keyword} ({platform_id})",
@@ -216,8 +217,26 @@ async def _build_collage(posts: List[Dict[str, Any]]) -> Optional[str]:
         return None
 
     grid = build_image_grid(images=valid_images, labels=valid_labels)
-    b64, _ = encode_base64(grid, format="PNG")
-    return b64
+    import io
+    buf = io.BytesIO()
+    grid.save(buf, format="PNG")
+    img_bytes = buf.getvalue()
+    
+    # 尝试上传到 CDN,替换冗长的 base64
+    try:
+        from agent.tools.builtin.file.image_cdn import _upload_bytes_to_oss
+        import hashlib
+        
+        md5_hash = hashlib.md5(img_bytes).hexdigest()[:12]
+        filename = f"collage_search_{md5_hash}.png"
+        cdn_url = await _upload_bytes_to_oss(img_bytes, filename)
+        return {"type": "url", "url": cdn_url}
+    except Exception as e:
+        import logging
+        logging.getLogger(__name__).warning("Failed to upload collage to CDN: %s", e)
+        # 降级:还是用 base64 但可能会超长
+        b64, _ = encode_base64(grid, format="PNG")
+        return {"type": "base64", "media_type": "image/png", "data": b64}
 
 
 # ── 注册所有 AIGC 平台 ──

+ 21 - 5
agent/tools/builtin/content/platforms/x.py

@@ -49,9 +49,9 @@ async def search(
 
         # 拼图
         images = []
-        collage_b64 = await _build_tweet_collage(tweets[:max_count])
-        if collage_b64:
-            images.append({"type": "base64", "media_type": "image/png", "data": collage_b64})
+        collage_obj = await _build_tweet_collage(tweets[:max_count])
+        if collage_obj:
+            images.append(collage_obj)
 
         return ToolResult(
             title=f"X: {keyword}",
@@ -113,8 +113,24 @@ async def _build_tweet_collage(tweets: List[Dict[str, Any]]) -> Optional[str]:
         return None
 
     grid = build_image_grid(images=valid_images, labels=valid_labels)
-    b64, _ = encode_base64(grid, format="PNG")
-    return b64
+    import io
+    buf = io.BytesIO()
+    grid.save(buf, format="PNG")
+    img_bytes = buf.getvalue()
+    
+    try:
+        from agent.tools.builtin.file.image_cdn import _upload_bytes_to_oss
+        import hashlib
+        
+        md5_hash = hashlib.md5(img_bytes).hexdigest()[:12]
+        filename = f"x_collage_{md5_hash}.png"
+        cdn_url = await _upload_bytes_to_oss(img_bytes, filename)
+        return {"type": "url", "url": cdn_url}
+    except Exception as e:
+        import logging
+        logging.getLogger(__name__).warning("Failed to upload x collage to CDN: %s", e)
+        b64, _ = encode_base64(grid, format="PNG")
+        return {"type": "base64", "media_type": "image/png", "data": b64}
 
 
 # ── 注册 ──

+ 21 - 5
agent/tools/builtin/content/platforms/youtube.py

@@ -55,9 +55,9 @@ async def search(
 
         # 拼图
         images = []
-        collage_b64 = await _build_video_collage(videos[:max_count])
-        if collage_b64:
-            images.append({"type": "base64", "media_type": "image/png", "data": collage_b64})
+        collage_obj = await _build_video_collage(videos[:max_count])
+        if collage_obj:
+            images.append(collage_obj)
 
         return ToolResult(
             title=f"YouTube: {keyword}",
@@ -183,8 +183,24 @@ async def _build_video_collage(videos: List[Dict[str, Any]]) -> Optional[str]:
         return None
 
     grid = build_image_grid(images=valid_images, labels=valid_labels)
-    b64, _ = encode_base64(grid, format="PNG")
-    return b64
+    import io
+    buf = io.BytesIO()
+    grid.save(buf, format="PNG")
+    img_bytes = buf.getvalue()
+    
+    try:
+        from agent.tools.builtin.file.image_cdn import _upload_bytes_to_oss
+        import hashlib
+        
+        md5_hash = hashlib.md5(img_bytes).hexdigest()[:12]
+        filename = f"youtube_collage_{md5_hash}.png"
+        cdn_url = await _upload_bytes_to_oss(img_bytes, filename)
+        return {"type": "url", "url": cdn_url}
+    except Exception as e:
+        import logging
+        logging.getLogger(__name__).warning("Failed to upload youtube collage to CDN: %s", e)
+        b64, _ = encode_base64(grid, format="PNG")
+        return {"type": "base64", "media_type": "image/png", "data": b64}
 
 
 # ── 注册 ──

+ 14 - 1
agent/tools/builtin/content/tools.py

@@ -27,10 +27,13 @@ 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:
+def _get_trace_id(context: Optional[Dict[str, Any]]) -> str:
     """从 context 取 trace_id,回退到环境变量或自动生成"""
+    if isinstance(context, dict) and context.get("trace_id"):
+        return 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]}"
 
 
@@ -116,6 +119,11 @@ async def content_search(
     if not pdef.search_impl:
         return ToolResult(title="不支持搜索", output=f"平台 {pdef.name} 暂不支持搜索")
 
+    try:
+        max_count = int(max_count)
+    except (ValueError, TypeError):
+        max_count = 20
+
     result = await pdef.search_impl(
         platform_id=pdef.id,
         keyword=keyword,
@@ -156,6 +164,11 @@ async def content_detail(
     if not pdef:
         return ToolResult(title="平台不存在", output=f"未找到平台 '{platform}'")
 
+    try:
+        index = int(index)
+    except (ValueError, TypeError):
+        return ToolResult(title="参数错误", output="参数 index 必须是整数", error="Invalid index parameter")
+
     trace_id = _get_trace_id(context)
     post = _cache.get_cached_post(trace_id, pdef.id, index)
 

+ 2 - 0
agent/tools/builtin/file/__init__.py

@@ -6,6 +6,7 @@ File tools - 文件操作工具
 
 from .read import read_file
 from .write import write_file
+from .write_json import write_json
 from .edit import edit_file
 from .glob import glob_files
 from .grep import grep_content
@@ -13,6 +14,7 @@ from .grep import grep_content
 __all__ = [
     "read_file",
     "write_file",
+    "write_json",
     "edit_file",
     "glob_files",
     "grep_content",

+ 125 - 0
agent/tools/builtin/file/image_cdn.py

@@ -0,0 +1,125 @@
+"""
+image_cdn.py - 将内容中的外站图片 URL 转换为自有 CDN 链接
+
+使用场景:在 write_json / write_file 落盘前调用,自动把
+  小红书、B站、知乎、微博等平台的图片链接替换为 res.cybertogether.net CDN 链接,
+  防止外站图片过期/防盗链导致后续流程无法访问。
+"""
+
+import hashlib
+import json
+import logging
+import re
+from typing import Any, Union
+import httpx
+
+logger = logging.getLogger(__name__)
+
+# ── 匹配需要转存的外站图片 URL(只抓图片后缀或明显的图床域名)──────────────────
+_IMG_URL_RE = re.compile(
+    r'https?://(?!'  # 排除自有 CDN,不重复上传
+    r'res\.cybertogether\.net'
+    r')'
+    r'[^\s"\'<>]+'  # URL 主体
+    r'(?:'
+        r'\.(?:jpg|jpeg|png|gif|webp|avif|bmp|svg)'  # 明确图片后缀
+        r'|'
+        r'(?:xhscdn\.com|bdimg\.com|hdslb\.com|zhihu\.com/p/[^/]+\.(?:jpg|png)'
+        r'|sinaimg\.cn|wx\d+\.sinaimg\.cn|mmbiz\.qpic\.cn'  # 微博/微信图床
+        r'|imagev[12]\.meitudata\.com'  # 美图
+        r'|p[0-9]\.douyinpic\.com'  # 抖音封面
+        r'|i0\.hdslb\.com|article\.biliimg\.com'  # B站
+        r')'
+    r')',
+    re.IGNORECASE,
+)
+
+BUCKET_NAME = "aigc-admin"
+BUCKET_PATH = "crawler/image"
+CDN_BASE = "https://res.cybertogether.net"
+
+
+async def _upload_bytes_to_oss(data: bytes, filename: str) -> str:
+    """上传 bytes 到 OSS 并返回 CDN URL,失败时抛出异常"""
+    from cyber_sdk.ali_oss import _upload_v2
+    result = await _upload_v2(
+        file_name=filename,
+        file_content=data,
+        bucket_path=BUCKET_PATH,
+        bucket_name=BUCKET_NAME,
+    )
+    oss_key = result.get("oss_object_key")
+    if not oss_key:
+        raise ValueError(f"OSS response missing oss_object_key: {result}")
+    return f"{CDN_BASE}/{oss_key}"
+
+
+async def _download_image(url: str) -> bytes:
+    """下载图片 bytes,失败抛出异常"""
+    async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
+        resp = await client.get(url, headers={"Referer": url, "User-Agent": "Mozilla/5.0"})
+        resp.raise_for_status()
+        return resp.content
+
+
+def _ext_from_url(url: str) -> str:
+    """从 URL 猜测文件扩展名,默认 jpg"""
+    path = url.split("?")[0].lower()
+    for ext in ("png", "gif", "webp", "avif", "bmp", "svg", "jpg", "jpeg"):
+        if path.endswith(f".{ext}"):
+            return ext
+    return "jpg"
+
+
+async def replace_image_urls(text: str) -> str:
+    """
+    扫描 text 中所有外站图片 URL,下载并上传到自有 OSS,原地替换为 CDN 链接。
+
+    - 已是 res.cybertogether.net 的 URL 直接跳过
+    - 下载/上传失败的 URL 保留原值,只打 WARNING 日志
+    - 同一 URL 使用 MD5 去重(同一次调用内)
+    """
+    urls = list(dict.fromkeys(_IMG_URL_RE.findall(text)))  # 去重但保留顺序
+    if not urls:
+        return text
+
+    import asyncio
+
+    async def _process_single_url(url: str) -> tuple[str, str]:
+        url_hash = hashlib.md5(url.encode()).hexdigest()[:12]
+        ext = _ext_from_url(url)
+        filename = f"{url_hash}.{ext}"
+        try:
+            data = await _download_image(url)
+            cdn_url = await _upload_bytes_to_oss(data, filename)
+            logger.info("[ImageCDN] %s → %s", url[:60], cdn_url)
+            return url, cdn_url
+        except Exception as e:
+            logger.warning("[ImageCDN] Failed to mirror %s: %s (%s)", url[:60], str(e) or repr(e), type(e).__name__)
+            return url, url
+
+    tasks = [_process_single_url(u) for u in urls]
+    results = await asyncio.gather(*tasks)
+    url_map: dict[str, str] = dict(results)
+
+    for orig, cdn in url_map.items():
+        if orig != cdn:
+            text = text.replace(orig, cdn)
+
+    replaced = sum(1 for o, c in url_map.items() if o != c)
+    if replaced:
+        logger.info("[ImageCDN] Replaced %d/%d image URLs with CDN links", replaced, len(urls))
+
+    return text
+
+
+async def replace_image_urls_in_obj(obj: Any) -> Any:
+    """
+    递归扫描 dict/list 中所有字符串值,替换其中的外站图片 URL。
+    先整体 JSON 序列化再做替换,再反序列化,效率高且避免遗漏嵌套字段。
+    """
+    raw = json.dumps(obj, ensure_ascii=False)
+    replaced = await replace_image_urls(raw)
+    if replaced == raw:
+        return obj
+    return json.loads(replaced)

+ 9 - 0
agent/tools/builtin/file/write.py

@@ -75,6 +75,15 @@ async def write_file(
     # 确保父目录存在
     path.parent.mkdir(parents=True, exist_ok=True)
 
+    # 落盘前自动将内容中的外站图片 URL 替换为自有 CDN 链接(仅处理文本文件)
+    if not append:  # 追加模式不做替换,避免重复处理
+        try:
+            from agent.tools.builtin.file.image_cdn import replace_image_urls
+            new_content = await replace_image_urls(new_content)
+        except Exception as cdn_err:
+            import logging
+            logging.getLogger(__name__).warning("[write_file] CDN mirror step failed, writing original: %s", cdn_err)
+
     # 写入文件
     try:
         with open(path, 'w', encoding='utf-8') as f:

+ 99 - 0
agent/tools/builtin/file/write_json.py

@@ -0,0 +1,99 @@
+"""
+Write JSON Tool - 用于直接安全写入结构化 JSON 数据,避免大模型生成超长转义文本导致的截断和报错
+"""
+
+import json
+from pathlib import Path
+from typing import Optional, Dict, Any
+
+from agent.tools import tool, ToolResult, ToolContext
+
+
+@tool(description="专门且唯一安全的 JSON 数据文件写入工具。传入 Python Dict/Object,自动为你生成格式化和转义无误的 JSON 文件。严禁使用普通的 write_file 写 JSON。参数 file_path 是文件绝对路径字符串,json_data 是要写入的原生 JSON 对象(直接传 dict,无需提前序列化)", hidden_params=["context"], groups=["core"])
+async def write_json(
+    file_path: str = "",
+    json_data: dict = None,
+    context: Optional[ToolContext] = None
+) -> ToolResult:
+    # 参数防空保护 - 给出明确错误而非空报错
+    if not file_path:
+        return ToolResult(
+            title="参数错误",
+            output="请提供 file_path 参数:file_path 必须是一个有效的绝对文件路径字符串,例如 '/path/to/output.json'",
+            error="Missing required argument: file_path"
+        )
+    if json_data is None:
+        return ToolResult(
+            title="参数错误",
+            output="请提供 json_data 参数:json_data 必须是一个原生的 Python dict 对象,请将要写入的 JSON 结构直接作为对象传入,无需序列化为字符串",
+            error="Missing required argument: json_data"
+        )
+    """
+    专门安全地写入结构化 JSON 数据到文件。
+
+    Args:
+        file_path: 要写入的绝对或相对文件路径。
+        json_data: 要写入的数据对象 (object/dict)。
+        context: 工具上下文
+
+    Returns:
+        ToolResult: 写入操作结果
+    """
+    path = Path(file_path)
+    if not path.is_absolute():
+        path = Path.cwd() / path
+
+    if path.exists() and path.is_dir():
+        return ToolResult(
+            title="路径错误",
+            output=f"路径是目录,不是文件: {file_path}",
+            error="Path is a directory"
+        )
+
+    path.parent.mkdir(parents=True, exist_ok=True)
+
+    # 自动检测并修正类型:如果模型传进来的是 JSON 字符串,先反序列化再写入
+    # 这解决了模型把 dict 当成 JSON 字符串传入导致的双重序列化问题
+    if isinstance(json_data, str):
+        try:
+            json_data = json.loads(json_data)
+        except json.JSONDecodeError as e:
+            return ToolResult(
+                title="JSON 解析失败",
+                output=f"json_data 参数作为 JSON 字符串解析失败,错误在这附近:{e}\n\n"
+                       f"[系统强制警告] 你的 JSON 生成存在语法错误(很可能是 description 中的未转义双引号、换行符,或者缺少逗号)。\n"
+                       f"⚠️ 严禁使用 write_file 回退硬写!用 write_file 强行写入坏 JSON 会导致整个流水线后续崩溃死锁!\n"
+                       f"你必须立即检查并修复字符串内的转义问题,然后重新调用 write_json!",
+                error=str(e)
+            )
+
+    try:
+        # 落盘前自动将 JSON 数据中的外站图片 URL 替换为自有 CDN 链接
+        try:
+            from agent.tools.builtin.file.image_cdn import replace_image_urls_in_obj
+            json_data = await replace_image_urls_in_obj(json_data)
+        except Exception as cdn_err:
+            import logging
+            logging.getLogger(__name__).warning("[write_json] CDN mirror step failed, writing original: %s", cdn_err)
+
+        with open(path, 'w', encoding='utf-8') as f:
+            json.dump(json_data, f, ensure_ascii=False, indent=2)
+            
+        json_str = json.dumps(json_data, ensure_ascii=False, indent=2)
+        lines = len(json_str.split('\n'))
+
+        return ToolResult(
+            title=path.name,
+            output=f"JSON 数据已安全且完美地格式化写入: {path.name}",
+            metadata={
+                "lines": lines,
+                "existed": path.exists()
+            },
+            long_term_memory=f"安全覆盖写入了结构化 JSON 文件 {path.name}"
+        )
+    except Exception as e:
+        return ToolResult(
+            title="JSON 写入失败",
+            output=f"无法写入 JSON 到文件: {str(e)}",
+            error=str(e)
+        )

+ 36 - 11
agent/tools/builtin/subagent.py

@@ -193,13 +193,10 @@ def _aggregate_stats(results: List[Dict[str, Any]]) -> Dict[str, Any]:
 
 
 def _get_allowed_tools(single: bool, context: dict) -> Optional[List[str]]:
-    """获取允许工具列表。single=True: 全部(去掉 agent/evaluate); single=False: 只读"""
-    if not single:
-        return ["read_file", "grep_content", "glob_files", "goal"]
-    # single (delegate): 获取所有工具,排除 agent 和 evaluate
+    """获取允许工具列表。获取所有工具,排除 agent 和 evaluate"""
     runner = context.get("runner")
-    if runner and hasattr(runner, "tools") and hasattr(runner.tools, "registry"):
-        all_tools = list(runner.tools.registry.keys())
+    if runner and hasattr(runner, "tools") and hasattr(runner.tools, "_tools"):
+        all_tools = list(runner.tools._tools.keys())
         return [t for t in all_tools if t not in ("agent", "evaluate")]
     return None
 
@@ -501,6 +498,7 @@ async def _run_agents(
         debug = getattr(runner, 'debug', False)
         agent_label = (agent_type or ("delegate" if single else f"explore-{i+1}"))
         debug_printer = _make_event_printer(agent_label) if debug else None
+        # 为了彻底封死逃逸风险,禁用由于缺省 tool_groups 注入进来的 agent 和 bash_command
         on_event = _make_interactive_handler(
             runner, cur_stid, trace_id, debug_printer=debug_printer
         )
@@ -510,9 +508,12 @@ async def _run_agents(
             config=_make_run_config(
                 trace_id=cur_stid,
                 agent_type=agent_type or ("delegate" if single else "explore"),
+                max_iterations=50 if single else 50,
                 model=parent_trace.model if parent_trace else "gpt-4o",
                 uid=parent_trace.uid if parent_trace else None,
                 tools=allowed_tools,
+                tool_groups=[], # 清空默认的 "core" 组,防止把 agent 和 bash_command 偷渡进来
+                exclude_tools=["agent", "evaluate", "bash_command"], # 严格锁死危险元工具
                 name=task_item[:50],
                 skills=skills,
                 knowledge=context.get("knowledge_config"),
@@ -580,11 +581,24 @@ async def _run_agents(
                 "sub_trace_id": stid,
             }
     else:
-        # 多任务并行执行
-        raw_results = await asyncio.gather(
-            *(coro for _, _, _, coro in coros),
-            return_exceptions=True,
-        )
+        # 检查父 Agent 是否开启了并发配置
+        is_parallel = getattr(runner.config, "parallel_tool_execution", True) if runner and hasattr(runner, "config") else True
+        
+        if is_parallel:
+            # 多任务并行执行
+            raw_results = await asyncio.gather(
+                *(coro for _, _, _, coro in coros),
+                return_exceptions=True,
+            )
+        else:
+            # 多任务串行执行(为了省显存/内存)
+            raw_results = []
+            for _, _, _, coro in coros:
+                try:
+                    res = await coro
+                    raw_results.append(res)
+                except Exception as e:
+                    raw_results.append(e)
 
         processed_results = []
         for idx, raw in enumerate(raw_results):
@@ -755,6 +769,17 @@ async def agent(
         return {"status": "failed", "error": f"Missing required context: {', '.join(missing)}"}
 
     # 归一化 task → list
+    if isinstance(task, str):
+        task_str = task.strip()
+        if task_str.startswith("[") and task_str.endswith("]"):
+            try:
+                import json
+                parsed_task = json.loads(task_str)
+                if isinstance(parsed_task, list):
+                    task = parsed_task
+            except:
+                pass
+
     single = isinstance(task, str)
     tasks = [task] if single else task
 

+ 29 - 54
agent/tools/builtin/toolhub.py

@@ -41,7 +41,7 @@ logger = logging.getLogger(__name__)
 
 # ── 配置 ─────────────────────────────────────────────
 
-TOOLHUB_BASE_URL = "http://43.106.118.91:8001"
+TOOLHUB_BASE_URL = os.environ.get("TOOLHUB_BASE_URL", "http://43.106.118.91:8001")
 DEFAULT_TIMEOUT = 30.0
 CALL_TIMEOUT = 600.0   # 图像生成类工具耗时较长,云端机器启动可能需要数分钟
 
@@ -169,79 +169,54 @@ async def _process_images(raw_images: List[str], tool_id: str) -> tuple:
     return images_for_llm, cdn_urls, saved_paths
 
 
-_SINGLE_IMAGE_PARAMS = ("image", "image_url", "mask_image", "pose_image", "reference_image")
-_ARRAY_IMAGE_PARAMS = ("images", "image_urls", "reference_images")
-
-
 async def _maybe_upload_local(val: str) -> Optional[str]:
     """如果 val 是存在的本地文件路径,上传 OSS 并返回 CDN URL;否则返回 None。"""
     if not isinstance(val, str):
         return None
-    if val.startswith(("http://", "https://", "data:")):
+    # 忽略明显不是路径的字符串(URL、Base64、JSON串、多行文本或超长文本)
+    if val.startswith(("http://", "https://", "data:", "{", "[")) or "\n" in val or len(val) > 1024:
         return None
     try:
         p = Path(val)
         if p.exists() and p.is_file():
             return await _upload_to_oss(str(p.resolve()))
-    except Exception as e:
-        logger.warning(f"[ToolHub] 本地路径处理失败 {val}: {e}")
+    except Exception:
+        # Windows 下无效路径可能抛出 OSError/ValueError,直接忽略
+        pass
     return None
 
 
 async def _preprocess_params(params: Dict[str, Any]) -> Dict[str, Any]:
     """
-    预处理工具参数:检测本地文件路径,自动上传到 OSS 并替换为 CDN URL。
-
-    支持的单值参数:image, image_url, mask_image, pose_image, reference_image
-    支持的数组参数:images, image_urls, reference_images
+    预处理工具参数:递归检测参数中的本地文件路径,只要是存在的本地文件且像是图片等素材,
+    就自动上传到 OSS 并替换为 CDN URL。
 
-    设计要点:远程工具服务的 cwd 和调用方不一样,相对路径在服务器上会找不到文件。
-    所以必须在客户端就把本地路径转成 CDN URL,不能期望服务器侧有 fallback。
+    设计要点:远程工具服务的 cwd 和调用方不一样,必须在客户端就转成 CDN URL。支持深层嵌套字典。
     """
     if not params:
         return params
 
-    processed = params.copy()
-
-    # 单值图片参数
-    for key in _SINGLE_IMAGE_PARAMS:
-        if key in processed and isinstance(processed[key], str):
-            val = processed[key]
-            if val.startswith(("http://", "https://", "data:")):
-                continue
-            cdn_url = await _maybe_upload_local(val)
+    async def _walk(obj: Any) -> Any:
+        if isinstance(obj, dict):
+            new_dict = {}
+            for k, v in obj.items():
+                new_dict[k] = await _walk(v)
+            return new_dict
+        elif isinstance(obj, list):
+            new_list = []
+            for item in obj:
+                new_list.append(await _walk(item))
+            return new_list
+        elif isinstance(obj, str):
+            cdn_url = await _maybe_upload_local(obj)
             if cdn_url:
-                processed[key] = cdn_url
-                logger.info(f"[ToolHub] {key} 本地路径已替换为 CDN: {cdn_url}")
-            elif not os.path.isfile(val):
-                # 既不是远程 URL 也不是已存在的本地文件,直接报错比让远程服务抛神秘的 base64 错误强
-                logger.warning(f"[ToolHub] {key}={val!r} 既不是 URL 也不是存在的本地文件")
-
-    # 数组型图片参数
-    for array_key in _ARRAY_IMAGE_PARAMS:
-        if array_key not in processed or not isinstance(processed[array_key], list):
-            continue
-        new_list = []
-        for idx, item in enumerate(processed[array_key]):
-            if not isinstance(item, str):
-                new_list.append(item)
-                continue
-            if item.startswith(("http://", "https://", "data:")):
-                new_list.append(item)
-                continue
-            cdn_url = await _maybe_upload_local(item)
-            if cdn_url:
-                new_list.append(cdn_url)
-                logger.info(f"[ToolHub] {array_key}[{idx}] 本地路径已替换为 CDN: {cdn_url}")
-            else:
-                new_list.append(item)
-                if not os.path.isfile(item):
-                    logger.warning(
-                        f"[ToolHub] {array_key}[{idx}]={item!r} 既不是 URL 也不是存在的本地文件"
-                    )
-        processed[array_key] = new_list
-
-    return processed
+                logger.info(f"[ToolHub] 检测到本地文件路径,已替换: {obj} -> {cdn_url}")
+                return cdn_url
+            return obj
+        else:
+            return obj
+
+    return await _walk(params)
 
 
 # ── 工具实现 ──────────────────────────────────────────

+ 3 - 1
agent/tools/registry.py

@@ -264,8 +264,10 @@ class ToolRegistry:
 				arguments = replace_sensitive_data(arguments, sensitive_data, current_url)
 
 			# 准备参数:只注入函数需要的参数
-			kwargs = {**arguments}
 			sig = inspect.signature(func)
+			# 过滤掉函数签名中不存在的参数(如 Claude SDK 发送的 {"_": true} 占位符)
+			valid_params = set(sig.parameters.keys())
+			kwargs = {k: v for k, v in arguments.items() if k in valid_params}
 
 			# 注入隐藏参数(hidden_params)
 			hidden_params = tool_info.get("hidden_params", [])

+ 0 - 141
examples/mini_restore/call_banana.py

@@ -1,141 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-"""
-🍌 Nano Banana (千层套路·多模态生成) 外部调用脚本
-
-【工具定位】:它使用的是 Google 最新的多模态 Imagen 3 引擎(gemini-3.1-flash-image-preview)。
-【独门绝技 - 跨模态多图推理】:
-与传统的 SD / Flux (图生图) 的本质区别在于,你可以给它丢【任意张完全不同的图片】,
-然后在 prompt 里随心所欲地让它“融梗”交汇。
-比如传入图片 [猫.jpg] 和 [未来城.jpg],让它“把这只猫生成在这座未来城里”。
-
-【参数模式支持】:
-1. 纯文生图:只传 prompt
-2. 单图生图/重绘:传 1 张图 + prompt
-3. 多路意象融合:传 N 张图 + prompt
-
-【本地文件自动上传机制】:
-大模型 API 往往更喜欢吃稳定的 CDN 外链。
-此脚本在启动前会自动检测你传入的图片:如果是本地硬盘文件(比如 examples/cat.png),
-脚本会自动静默调用内部的 OSS 工具,把它秒传为 https://res.cybertogether.net/.. 干净外链,然后投喂给大脑!
-"""
-
-import asyncio
-import os
-import argparse
-import sys
-import httpx
-import json
-
-# 动态引入我们系统现成的 CDN 上传脚本
-sys.path.append(os.path.dirname(os.path.abspath(__file__)))
-sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'examples', 'production_restore'))
-try:
-    from upload import upload_image
-except Exception as e:
-    import traceback
-    print(f"错误: 导入 upload.py 及其依赖链失败: {e}\n{traceback.format_exc()}")
-    sys.exit(1)
-
-ROUTER_URL = "http://43.106.118.91:8001/run_tool"
-
-async def process_images(images_list: list[str]) -> list[str]:
-    """处理图片数组,外链直接过,本地文件传 OSS"""
-    final_urls = []
-    for item in images_list:
-        if item.startswith("http://") or item.startswith("https://"):
-            print(f"✅ 检测到公网外链,无需转存: {item}")
-            final_urls.append(item)
-        elif os.path.exists(item):
-            print(f"📦 正在极速倒卖本地图片到 CDN: {item}")
-            try:
-                uploaded_url = await upload_image(item, 'aigc-admin', 'crawler/image')
-                if uploaded_url:
-                    print(f"🚀 上传成功: {uploaded_url}")
-                    final_urls.append(uploaded_url)
-                else:
-                    print(f"❌ 上传失败: {item}")
-            except Exception as e:
-                print(f"❌ 上传报错: {e}")
-        else:
-            print(f"⚠️ 跳过找不到的本地文件或无法识别的格式: {item}")
-    return final_urls
-
-async def run_nano_banana(prompt: str, images: list[str] = None, model: str = None, aspect_ratio: str = None):
-    print(f"\n=======================")
-    print(f"🍌 Nano Banana 启动中...")
-    print(f"=======================")
-
-    # 1. 整理图片
-    final_image_urls = []
-    if images and len(images) > 0:
-        print(f"🔍 检查到传了 {len(images)} 张神秘原图,准备过安检...")
-        final_image_urls = await process_images(images)
-    
-    # 2. 组装发给大模型中枢 Router 的参数
-    params = {
-        "prompt": prompt,
-        "image_urls": final_image_urls if final_image_urls else None
-    }
-    
-    if model:
-        params["model"] = model
-        
-    if aspect_ratio:
-        params["aspect_ratio"] = aspect_ratio
-
-    payload = {
-        "tool_id": "nano_banana",
-        "params": params
-    }
-
-    # 3. 轰入 API
-    print("\n⚡ 正在呼叫总后台路由节点打怪...")
-    try:
-        async with httpx.AsyncClient(timeout=300.0) as client:
-            resp = await client.post(ROUTER_URL, json=payload)
-            resp.raise_for_status()
-            
-            result = resp.json()
-            if result.get("status") == "success":
-                gen_data = result.get("result", {})
-                
-                print("\n🎉 === 生成成功! ===")
-                # 打印文本回复
-                if gen_data.get("text"):
-                    print(f"\n💬 模型回话:\n{gen_data.get('text')}")
-                
-                # 打印图片输出
-                images_out = gen_data.get("images", [])
-                if images_out:
-                    print("\n🖼️ 吐出的神图 (Base64 数据流已转为你本地文件):")
-                    for idx, img_b64 in enumerate(images_out):
-                        # 处理前缀 data:image/jpeg;base64,
-                        if img_b64.startswith("data:"):
-                            mime_split = img_b64.split(";base64,")
-                            if len(mime_split) == 2:
-                                ext = mime_split[0].split("/")[-1]
-                                raw_data = mime_split[1]
-                                
-                                import base64
-                                save_path = f"banana_output_{idx}.{ext}"
-                                with open(save_path, "wb") as f:
-                                    f.write(base64.b64decode(raw_data))
-                                print(f" 💾 已保存到本地 -> {save_path}")
-            else:
-                print(f"❌ 大模型傲娇了: {result.get('error')}")
-                
-    except Exception as e:
-        print(f"💥 网络大爆炸报错: {e}")
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="调用 Nano Banana 进行任意风格的多段融合魔法")
-    parser.add_argument("-p", "--prompt", type=str, required=True, help="你想对 AI 喊瞎什么 (比如:用图1的赛博风画一只图2里的猫)")
-    parser.add_argument("-i", "--images", type=str, nargs="+", help="无限追加的垫图清单(可以是现成的 http 链接,也可以是你电脑里的硬盘文件如 example.png)")
-    parser.add_argument("-m", "--model", type=str, default=None, help="覆盖模型 (默认后台会走 gemini-3.1-flash-image-preview)")
-    parser.add_argument("-a", "--aspect_ratio", type=str, default=None, help="图片比例,例如 3:4, 16:9, 1:1 等")
-    
-    args = parser.parse_args()
-    
-    asyncio.run(run_nano_banana(prompt=args.prompt, images=args.images, model=args.model, aspect_ratio=args.aspect_ratio))

+ 18 - 25
examples/mini_restore/config.py

@@ -1,5 +1,8 @@
 """
-项目配置
+mini_restore 配置 — ToolHub 工具测试
+
+精简版配置,仅用于测试 ToolHub 的搜索和调用流程。
+不启用浏览器、知识管理等复杂功能。
 """
 
 from agent.core.runner import KnowledgeConfig, RunConfig
@@ -8,31 +11,36 @@ from agent.core.runner import KnowledgeConfig, RunConfig
 # ===== Agent 运行配置 =====
 
 RUN_CONFIG = RunConfig(
+    # 模型配置
     model="qwen3.5-plus",
     temperature=0.3,
-    max_iterations=200,
-    tool_groups=["core", "browser", "content", "knowledge", "toolhub", "feishu", "im"],
+    max_iterations=50,
 
+    # 启用 thinking 模式
     extra_llm_params={"extra_body": {"enable_thinking": True}},
 
+    # Agent 预设(对应 presets.json 中的 "main")
     agent_type="main",
 
-    name="工具调研测试(KM 通信)",
+    # 工具:仅 toolhub 相关
+    tools=None,
+    tool_groups=["core", "toolhub"],
+
+    # 任务名称
+    name="ToolHub 工具测试",
 
+    # 知识管理配置
     knowledge=KnowledgeConfig(
-        enable_extraction=False,
-        enable_completion_extraction=False,
+        # 知识注入(关闭以避免 405 错误)
         enable_injection=False,
-        owner="sunlit.howard@gmail.com",
-        default_tags={"project": "new_search"},
-        default_scopes=["org:cybertogether"],
     )
 )
 
 
 # ===== 任务配置 =====
 
-OUTPUT_DIR = "examples/new_search/outputs"
+INPUT_DIR = "examples/mini_restore/input"
+OUTPUT_DIR = "examples/mini_restore/output"
 
 
 # ===== 基础设施配置 =====
@@ -42,18 +50,3 @@ TRACE_STORE_PATH = ".trace"
 DEBUG = True
 LOG_LEVEL = "INFO"
 LOG_FILE = None
-
-# ===== 浏览器配置 =====
-BROWSER_TYPE = "local"
-HEADLESS = False
-
-# ===== IM 配置 =====
-IM_ENABLED = True
-IM_CONTACT_ID = "agent_research"
-IM_SERVER_URL = "ws://43.106.118.91:8105"
-IM_WINDOW_MODE = True
-IM_NOTIFY_INTERVAL = 10.0
-
-# ===== Knowledge Manager 配置 =====
-KNOWLEDGE_MANAGER_ENABLED = True
-KNOWLEDGE_MANAGER_CONTACT_ID = "knowledge_manager"

+ 92 - 0
examples/mini_restore/flux_depth_controlnet_workflow.json

@@ -0,0 +1,92 @@
+{
+  "3": {
+    "inputs": {
+      "seed": 370146334065324,
+      "steps": 20,
+      "cfg": 1.0,
+      "sampler_name": "euler",
+      "scheduler": "normal",
+      "denoise": 1.0,
+      "model": ["20", 0],
+      "positive": ["14", 0],
+      "negative": ["14", 1],
+      "latent_image": ["28", 0]
+    },
+    "class_type": "KSampler"
+  },
+  "7": {
+    "inputs": {
+      "text": "",
+      "clip": ["20", 1]
+    },
+    "class_type": "CLIPTextEncode"
+  },
+  "8": {
+    "inputs": {
+      "samples": ["3", 0],
+      "vae": ["20", 2]
+    },
+    "class_type": "VAEDecode"
+  },
+  "9": {
+    "inputs": {
+      "filename_prefix": "mini_restore_output",
+      "images": ["8", 0]
+    },
+    "class_type": "SaveImage"
+  },
+  "14": {
+    "inputs": {
+      "strength": 0.6,
+      "start_percent": 0.0,
+      "end_percent": 1.0,
+      "positive": ["26", 0],
+      "negative": ["7", 0],
+      "control_net": ["15", 0],
+      "vae": ["20", 2],
+      "image": ["17", 0]
+    },
+    "class_type": "ControlNetApplySD3"
+  },
+  "15": {
+    "inputs": {
+      "control_net_name": "flux/flux-depth-controlnet.safetensors"
+    },
+    "class_type": "ControlNetLoader"
+  },
+  "17": {
+    "inputs": {
+      "image": "depth_map.png",
+      "upload": "image"
+    },
+    "class_type": "LoadImage"
+  },
+  "20": {
+    "inputs": {
+      "ckpt_name": "flux1-dev-fp8.safetensors"
+    },
+    "class_type": "CheckpointLoaderSimple"
+  },
+  "23": {
+    "inputs": {
+      "text": "a beautiful landscape with mountains and lake, highly detailed, professional photography",
+      "clip": ["20", 1]
+    },
+    "class_type": "CLIPTextEncode"
+  },
+  "26": {
+    "inputs": {
+      "guidance": 3.5,
+      "conditioning": ["23", 0]
+    },
+    "class_type": "FluxGuidance"
+  },
+  "28": {
+    "inputs": {
+      "width": 1024,
+      "height": 1024,
+      "batch_size": 1
+    },
+    "class_type": "EmptySD3LatentImage"
+  }
+}

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


BIN
examples/mini_restore/input/background_bokeh_img2.png


BIN
examples/mini_restore/input/character_ref_back.png


BIN
examples/mini_restore/input/depth_map.png


BIN
examples/mini_restore/input/easel_blank_canvas_img4.png


BIN
examples/mini_restore/input/img_1.png


BIN
examples/mini_restore/input/img_2.png


BIN
examples/mini_restore/input/img_3.png


BIN
examples/mini_restore/input/img_4.png


BIN
examples/mini_restore/input/img_5.png


+ 0 - 63
examples/mini_restore/new_search.prompt

@@ -1,63 +0,0 @@
----
-model: qwen3.5-plus
-temperature: 0.3
----
-
-$system$
-
-## 角色
-你是一个工具调研专家,能够独立搜索和整理工具信息,并与 Knowledge Manager 协作管理知识。
-
-## 可用工具
-- `ask_knowledge`: 向 Knowledge Manager 查询知识库中已有的信息(同步等待回复)
-- `upload_knowledge`: 上传调研结果到 Knowledge Manager(异步,立即返回)
-- `search_posts`: 搜索帖子(小红书、知乎、B站等)
-- `x_search`: 搜索推文
-- `youtube_search`: 搜索 YouTube 视频
-- `web_search`: 网页搜索
-- `im_send_message`: 发送 IM 消息
-- `im_receive_messages`: 接收 IM 消息
-
-## 工作流程
-
-### 第一步:查询已有知识(必须先做!)
-**在做任何搜索之前**,必须先调用 `ask_knowledge` 查询知识库:
-```
-ask_knowledge("查询关于 [工具名] 的所有信息")
-```
-这一步是强制的,不能跳过。根据返回结果决定后续调研重点。
-
-### 第二步:搜索调研
-使用搜索工具直接调研目标工具:
-- 搜索官方信息和文档
-- 搜索用户案例和评测
-- 搜索使用教程
-
-**每搜到一批有价值的信息,就用 `upload_knowledge` 发送给 Knowledge Manager:**
-```
-upload_knowledge({
-  "tools": [
-    {"name": "工具名", "slug": "tool_slug", "category": "分类", "description": "简介", "source_url": "链接"}
-  ],
-  "resources": [
-    {"title": "文档标题", "body": "文档内容", "content_type": "documentation", "source_url": "链接"}
-  ],
-  "knowledge": [
-    {"task": "使用场景", "content": "具体知识", "types": ["tool"], "score": 4}
-  ]
-})
-```
-
-### 第三步:最终提交
-调研完成后,用 `finalize=True` 触发入库:
-```
-upload_knowledge({...最后一批数据...}, finalize=True)
-```
-
-## 注意事项
-- 边搜边传:每搜到有价值的信息就 upload,不要攒到最后
-- 分类清晰:工具元信息放 tools,文档/教程放 resources,经验/技巧放 knowledge
-- 最后 finalize:确保最后一次 upload 设置 finalize=True
-
-$user$
-请调研 ControlNet 工具,了解它的功能、使用方法和应用场景,并将结果同步到知识库。

+ 12 - 0
examples/mini_restore/presets.json

@@ -0,0 +1,12 @@
+{
+  "main": {
+    "system_prompt_file": "toolhub_test.prompt",
+    "max_iterations": 50,
+    "skills": ["planning"],
+    "prompt_vars": {
+      "input_dir": "examples/mini_restore/input",
+      "output_dir": "examples/mini_restore/output"
+    },
+    "description": "ToolHub 测试 Agent - 搜索工具 → 调用工具 → 完成任务"
+  }
+}

+ 296 - 132
examples/mini_restore/run.py

@@ -1,5 +1,13 @@
 """
-新搜索测试 - 测试 Research Agent 与 Knowledge Manager 的 IM 通信
+ToolHub 工具测试流程 — mini_restore
+
+精简版运行脚本,仅测试 ToolHub 搜索和调用。
+不启用浏览器、知识管理等复杂功能。
+
+功能:
+1. 使用框架提供的 InteractiveController
+2. 支持命令行交互('p' 暂停,'q' 退出)
+3. 支持通过 --trace <ID> 恢复已有 Trace 继续执行
 """
 
 import argparse
@@ -8,8 +16,13 @@ import sys
 import asyncio
 from pathlib import Path
 
+# Clash Verge TUN 模式兼容:禁止 httpx/urllib 自动检测系统 HTTP 代理
 os.environ.setdefault("no_proxy", "*")
 
+# ToolHub 指向本地服务(tool_agent)
+os.environ.setdefault("TOOLHUB_BASE_URL", "http://localhost:8001")
+
+# 添加项目根目录到 Python 路径
 sys.path.insert(0, str(Path(__file__).parent.parent.parent))
 
 from dotenv import load_dotenv
@@ -17,189 +30,340 @@ load_dotenv()
 
 from agent.llm.prompts import SimplePrompt
 from agent.core.runner import AgentRunner, RunConfig
-from agent.trace import FileSystemTraceStore, Trace, Message
+from agent.trace import (
+    FileSystemTraceStore,
+    Trace,
+    Message,
+)
 from agent.llm import create_qwen_llm_call
 from agent.cli import InteractiveController
 from agent.utils import setup_logging
-from agent.tools.builtin.browser.baseClass import init_browser_session, kill_browser_session
 
-from config import (
-    RUN_CONFIG, SKILLS_DIR, TRACE_STORE_PATH, DEBUG, LOG_LEVEL, LOG_FILE,
-    BROWSER_TYPE, HEADLESS, OUTPUT_DIR,
-    IM_ENABLED, IM_CONTACT_ID, IM_SERVER_URL, IM_WINDOW_MODE, IM_NOTIFY_INTERVAL,
-    KNOWLEDGE_MANAGER_ENABLED, KNOWLEDGE_MANAGER_CONTACT_ID,
-)
+# 导入 ToolHub 工具(触发 @tool 注册)
+from agent.tools.builtin.toolhub import toolhub_health, toolhub_search, toolhub_call  # noqa: F401
+
+# 导入项目配置
+from config import RUN_CONFIG, SKILLS_DIR, TRACE_STORE_PATH, DEBUG, LOG_LEVEL, LOG_FILE, INPUT_DIR, OUTPUT_DIR
 
 
 async def main():
-    parser = argparse.ArgumentParser(description="新搜索测试(KM 通信)")
-    parser.add_argument("--trace", type=str, default=None, help="恢复 Trace ID")
+    # 解析命令行参数
+    parser = argparse.ArgumentParser(description="ToolHub 工具测试 (mini_restore)")
+    parser.add_argument(
+        "--trace", type=str, default=None,
+        help="已有的 Trace ID,用于恢复继续执行(不指定则新建)",
+    )
+    parser.add_argument(
+        "--task", type=str, default=None,
+        help="自定义任务描述(覆盖 prompt 中的默认任务)",
+    )
     args = parser.parse_args()
 
+    # 路径配置
     base_dir = Path(__file__).parent
     project_root = base_dir.parent.parent
-    prompt_path = base_dir / "new_search.prompt"
+    prompt_path = base_dir / "toolhub_test.prompt"
     output_dir = project_root / OUTPUT_DIR
     output_dir.mkdir(parents=True, exist_ok=True)
 
-    # 1. 日志
+    # 1. 配置日志
     setup_logging(level=LOG_LEVEL, file=LOG_FILE)
 
-    # 2. Prompt
-    print("1. 加载 prompt...")
+    # 2. 加载项目级 presets
+    print("2. 加载 presets...")
+    presets_path = base_dir / "presets.json"
+    if presets_path.exists():
+        from agent.core.presets import load_presets_from_json
+        load_presets_from_json(str(presets_path))
+        print(f"   - 已加载项目 presets")
+    else:
+        print(f"   - 未找到 presets.json,跳过")
+
+    # 3. 加载 prompt
+    print("3. 加载 prompt...")
     prompt = SimplePrompt(prompt_path)
-    messages = prompt.build_messages(output_dir=str(output_dir))
-
-    # 3. 浏览器
-    print("2. 初始化浏览器...")
-    await init_browser_session(browser_type=BROWSER_TYPE, headless=HEADLESS, url="https://www.google.com/", profile_name="")
-    print("   ✅ 浏览器就绪\n")
-
-    # 4. IM Client + Knowledge Manager
-    km_task = None
-    if IM_ENABLED:
-        from agent.tools.builtin.im.chat import im_setup, im_open_window
-        print("3. 初始化 IM Client...")
-        print(f"   - 身份: {IM_CONTACT_ID}, 服务器: {IM_SERVER_URL}")
-        result = await im_setup(
-            contact_id=IM_CONTACT_ID,
-            server_url=IM_SERVER_URL,
-            notify_interval=IM_NOTIFY_INTERVAL,
-        )
-        print(f"   ✅ {result.output}")
-
-        if IM_WINDOW_MODE:
-            window_result = await im_open_window(contact_id=IM_CONTACT_ID)
-            print(f"   ✅ {window_result.output}\n")
-
-        if KNOWLEDGE_MANAGER_ENABLED:
-            print("4. 启动 Knowledge Manager...")
-            print(f"   - Contact ID: {KNOWLEDGE_MANAGER_CONTACT_ID}")
-            try:
-                sys.path.insert(0, str(Path(__file__).parent.parent.parent / "knowhub"))
-                from agents.knowledge_manager import start_knowledge_manager
-
-                km_task = asyncio.create_task(start_knowledge_manager(
-                    contact_id=KNOWLEDGE_MANAGER_CONTACT_ID,
-                    server_url=IM_SERVER_URL,
-                    chat_id="main"
-                ))
-                # 等待一下让 KM 连接完成
-                await asyncio.sleep(2)
-                print(f"   ✅ Knowledge Manager 已启动\n")
-            except Exception as e:
-                print(f"   ⚠️ 启动失败: {e}\n")
 
-    # 5. Agent Runner
+    # 4. 构建任务消息
+    print("4. 构建任务消息...")
+    print(f"   - 输入目录: {INPUT_DIR}")
+    print(f"   - 输出目录: {OUTPUT_DIR}")
+
+    if args.task:
+        # 使用命令行自定义任务
+        messages = prompt.build_messages(input_dir=INPUT_DIR, output_dir=OUTPUT_DIR)
+        # 替换最后一条 user 消息为自定义任务
+        for i in range(len(messages) - 1, -1, -1):
+            if messages[i].get("role") == "user":
+                messages[i]["content"] = args.task
+                break
+        print(f"   - 自定义任务: {args.task[:80]}...")
+    else:
+        messages = prompt.build_messages(input_dir=INPUT_DIR, output_dir=OUTPUT_DIR)
+
+    # 5. 创建 Agent Runner(无浏览器)
     print("5. 创建 Agent Runner...")
+    print(f"   - Skills 目录: {SKILLS_DIR}")
+
+    # 从 prompt 的 frontmatter 中提取模型配置(优先于 config.py)
     prompt_model = prompt.config.get("model", None)
-    model_for_llm = prompt_model or RUN_CONFIG.model
-    print(f"   - 模型: {model_for_llm}")
+    if prompt_model:
+        model_for_llm = prompt_model
+        print(f"   - 模型 (from prompt): {model_for_llm}")
+    else:
+        model_for_llm = RUN_CONFIG.model
+        print(f"   - 模型 (from config): {model_for_llm}")
 
     store = FileSystemTraceStore(base_path=TRACE_STORE_PATH)
     runner = AgentRunner(
         trace_store=store,
         llm_call=create_qwen_llm_call(model=model_for_llm),
         skills_dir=SKILLS_DIR,
-        debug=DEBUG,
-        logger_name="agents.research_agent"
+        debug=DEBUG
     )
 
-    interactive = InteractiveController(runner=runner, store=store, enable_stdin_check=True)
+    # 6. 创建交互控制器
+    interactive = InteractiveController(
+        runner=runner,
+        store=store,
+        enable_stdin_check=True
+    )
     runner.stdin_check = interactive.check_stdin
 
-    # 6. 执行
+    # 7. 任务信息
     task_name = RUN_CONFIG.name or base_dir.name
     print("=" * 60)
     print(f"{task_name}")
     print("=" * 60)
-    print("💡 输入 'p' 暂停,'q' 退出")
+    print("💡 交互提示:")
+    print("   - 执行过程中输入 'p' 或 'pause' 暂停并进入交互模式")
+    print("   - 执行过程中输入 'q' 或 'quit' 停止执行")
     print("=" * 60)
     print()
 
-    run_config = RUN_CONFIG
-    current_trace_id = args.trace
-    current_sequence = 0
+    # 8. 判断是新建还是恢复
+    resume_trace_id = args.trace
+    if resume_trace_id:
+        existing_trace = await store.get_trace(resume_trace_id)
+        if not existing_trace:
+            print(f"\n错误: Trace 不存在: {resume_trace_id}")
+            sys.exit(1)
+        print(f"恢复已有 Trace: {resume_trace_id[:8]}...")
+        print(f"   - 状态: {existing_trace.status}")
+        print(f"   - 消息数: {existing_trace.total_messages}")
+    else:
+        print(f"启动新 Agent...")
 
-    # 注入 IM 配置到 context(用于周期性通知检查)
-    if IM_ENABLED:
-        run_config.context["im_config"] = {
-            "contact_id": IM_CONTACT_ID,
-            "chat_id": "main"
-        }
+    print()
 
-    if current_trace_id:
-        run_config.trace_id = current_trace_id
-        initial_messages = None
-    else:
-        initial_messages = messages
+    final_response = ""
+    current_trace_id = resume_trace_id
+    current_sequence = 0
+    should_exit = False
 
     try:
-        async for item in runner.run(messages=initial_messages, config=run_config):
-            cmd = interactive.check_stdin()
-            if cmd == 'quit':
-                print("\n🛑 停止...")
-                if current_trace_id:
-                    await runner.stop(current_trace_id)
-                break
-            elif cmd == 'pause':
-                print("\n⏸️ 暂停...")
-                if current_trace_id:
-                    await runner.stop(current_trace_id)
+        # 配置
+        run_config = RUN_CONFIG
+        if resume_trace_id:
+            initial_messages = None
+            run_config.trace_id = resume_trace_id
+        else:
+            initial_messages = messages
+            run_config.name = f"{task_name}:测试任务"
+
+        while not should_exit:
+            if current_trace_id:
+                run_config.trace_id = current_trace_id
+
+            final_response = ""
+
+            # 如果是恢复 trace,进入交互菜单
+            if current_trace_id and initial_messages is None:
+                check_trace = await store.get_trace(current_trace_id)
+                if check_trace:
+                    if check_trace.status == "completed":
+                        print(f"\n[Trace] ✅ 已完成")
+                        print(f"  - Total messages: {check_trace.total_messages}")
+                        print(f"  - Total cost: ${check_trace.total_cost:.4f}")
+                    elif check_trace.status == "failed":
+                        print(f"\n[Trace] ❌ 已失败: {check_trace.error_message}")
+                    elif check_trace.status == "stopped":
+                        print(f"\n[Trace] ⏸️ 已停止")
+                        print(f"  - Total messages: {check_trace.total_messages}")
+                    else:
+                        print(f"\n[Trace] 📊 状态: {check_trace.status}")
+                        print(f"  - Total messages: {check_trace.total_messages}")
+
+                    current_sequence = check_trace.head_sequence
+
+                    menu_result = await interactive.show_menu(current_trace_id, current_sequence)
+
+                    if menu_result["action"] == "stop":
+                        break
+                    elif menu_result["action"] == "continue":
+                        new_messages = menu_result.get("messages", [])
+                        if new_messages:
+                            initial_messages = new_messages
+                            run_config.after_sequence = menu_result.get("after_sequence")
+                        else:
+                            initial_messages = []
+                            run_config.after_sequence = None
+                        continue
+                    break
+
+            if initial_messages is None:
+                initial_messages = []
+
+            print(f"{'▶️ 开始执行...' if not current_trace_id else '▶️ 继续执行...'}")
+
+            # 执行 Agent
+            paused = False
+            try:
+                async for item in runner.run(messages=initial_messages, config=run_config):
+                    # 检查用户中断
+                    cmd = interactive.check_stdin()
+                    if cmd == 'pause':
+                        print("\n⏸️ 正在暂停执行...")
+                        if current_trace_id:
+                            await runner.stop(current_trace_id)
+                        await asyncio.sleep(0.5)
+
+                        menu_result = await interactive.show_menu(current_trace_id, current_sequence)
+
+                        if menu_result["action"] == "stop":
+                            should_exit = True
+                            paused = True
+                            break
+                        elif menu_result["action"] == "continue":
+                            new_messages = menu_result.get("messages", [])
+                            if new_messages:
+                                initial_messages = new_messages
+                                after_seq = menu_result.get("after_sequence")
+                                if after_seq is not None:
+                                    run_config.after_sequence = after_seq
+                                paused = True
+                                break
+                            else:
+                                initial_messages = []
+                                run_config.after_sequence = None
+                                paused = True
+                                break
+
+                    elif cmd == 'quit':
+                        print("\n🛑 用户请求停止...")
+                        if current_trace_id:
+                            await runner.stop(current_trace_id)
+                        should_exit = True
+                        break
+
+                    # 处理 Trace 对象
+                    if isinstance(item, Trace):
+                        current_trace_id = item.trace_id
+                        if item.status == "running":
+                            print(f"[Trace] 开始: {item.trace_id[:8]}...")
+                        elif item.status == "completed":
+                            print(f"\n[Trace] ✅ 完成")
+                            print(f"  - Total messages: {item.total_messages}")
+                            print(f"  - Total cost: ${item.total_cost:.4f}")
+                        elif item.status == "failed":
+                            print(f"\n[Trace] ❌ 失败: {item.error_message}")
+                        elif item.status == "stopped":
+                            print(f"\n[Trace] ⏸️ 已停止")
+
+                    # 处理 Message 对象
+                    elif isinstance(item, Message):
+                        current_sequence = item.sequence
+
+                        if item.role == "assistant":
+                            content = item.content
+                            if isinstance(content, dict):
+                                text = content.get("text", "")
+                                tool_calls = content.get("tool_calls")
+
+                                if text and not tool_calls:
+                                    final_response = text
+                                    print(f"\n[Response] Agent 回复:")
+                                    print(text)
+                                elif text:
+                                    preview = text[:150] + "..." if len(text) > 150 else text
+                                    print(f"[Assistant] {preview}")
+
+                        elif item.role == "tool":
+                            content = item.content
+                            tool_name = "unknown"
+                            if isinstance(content, dict):
+                                tool_name = content.get("tool_name", "unknown")
+
+                            if item.description and item.description != tool_name:
+                                desc = item.description[:80] if len(item.description) > 80 else item.description
+                                print(f"[Tool Result] ✅ {tool_name}: {desc}...")
+                            else:
+                                print(f"[Tool Result] ✅ {tool_name}")
+
+            except Exception as e:
+                print(f"\n执行出错: {e}")
+                import traceback
+                traceback.print_exc()
+
+            if paused:
+                if should_exit:
+                    break
+                continue
+
+            if should_exit:
                 break
 
-            if isinstance(item, Trace):
-                current_trace_id = item.trace_id
-                if item.status == "running":
-                    print(f"[Trace] 开始: {item.trace_id[:8]}...")
-                elif item.status == "completed":
-                    print(f"\n[Trace] ✅ 完成 (消息: {item.total_messages}, 费用: ${item.total_cost:.4f})")
-                elif item.status == "failed":
-                    print(f"\n[Trace] ❌ 失败: {item.error_message}")
-
-            elif isinstance(item, Message):
-                current_sequence = item.sequence
-                if item.role == "assistant":
-                    content = item.content
-                    if isinstance(content, dict):
-                        text = content.get("text", "")
-                        tool_calls = content.get("tool_calls")
-                        if text and not tool_calls:
-                            print(f"\n[Response] {text}")
-                        elif text:
-                            preview = text[:150] + "..." if len(text) > 150 else text
-                            print(f"[Assistant] {preview}")
-
-                elif item.role == "tool":
-                    content = item.content
-                    tool_name = content.get("tool_name", "unknown") if isinstance(content, dict) else "unknown"
-                    desc = item.description or ""
-                    if desc and desc != tool_name:
-                        desc = desc[:80]
-                        print(f"[Tool] ✅ {tool_name}: {desc}...")
+            # Runner 退出后显示交互菜单
+            if current_trace_id:
+                menu_result = await interactive.show_menu(current_trace_id, current_sequence)
+
+                if menu_result["action"] == "stop":
+                    break
+                elif menu_result["action"] == "continue":
+                    new_messages = menu_result.get("messages", [])
+                    if new_messages:
+                        initial_messages = new_messages
+                        run_config.after_sequence = menu_result.get("after_sequence")
                     else:
-                        print(f"[Tool] ✅ {tool_name}")
+                        initial_messages = []
+                        run_config.after_sequence = None
+                    continue
+            break
 
     except KeyboardInterrupt:
         print("\n\n用户中断 (Ctrl+C)")
         if current_trace_id:
             await runner.stop(current_trace_id)
-    finally:
-        if km_task and not km_task.done():
-            print("正在关闭 Knowledge Manager...")
-            km_task.cancel()
-            try:
-                await km_task
-            except asyncio.CancelledError:
-                pass
 
-        try:
-            await kill_browser_session()
-        except Exception:
-            pass
+    # 输出结果
+    if final_response:
+        print()
+        print("=" * 60)
+        print("Agent 响应:")
+        print("=" * 60)
+        print(final_response)
+        print("=" * 60)
+        print()
+
+        output_file = output_dir / "result.txt"
+        with open(output_file, 'w', encoding='utf-8') as f:
+            f.write(final_response)
+
+        print(f"✓ 结果已保存到: {output_file}")
+        print()
 
+    # 可视化提示
     if current_trace_id:
-        print(f"\nTrace ID: {current_trace_id}")
+        print("=" * 60)
+        print("可视化 Step Tree:")
+        print("=" * 60)
+        print("1. 启动 API Server:")
+        print("   python3 api_server.py")
+        print()
+        print("2. 浏览器访问:")
+        print("   http://localhost:8000/api/traces")
+        print()
+        print(f"3. Trace ID: {current_trace_id}")
+        print("=" * 60)
 
 
 if __name__ == "__main__":

+ 144 - 0
examples/mini_restore/test_e2e_proxy.py

@@ -0,0 +1,144 @@
+import os
+import sys
+import json
+import time
+import requests
+import asyncio
+
+# 添加项目根目录到 Python 路径
+sys.path.insert(0, str(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
+from agent.tools.builtin.toolhub import _preprocess_params
+
+ROUTER_URL = "http://127.0.0.1:8001/run_tool"
+
+def call_tool(tool_id, params):
+    print(f"\n[{tool_id}] Calling...")
+    # 就像真正的 agent 流程一样,先预处理参数(将本地文件转化为 CDN 链接)
+    processed_params = asyncio.run(_preprocess_params(params))
+    
+    resp = requests.post(ROUTER_URL, json={"tool_id": tool_id, "params": processed_params})
+    resp.raise_for_status()
+    res = resp.json()
+    if res.get("status") == "error":
+        raise Exception(f"Tool error: {res.get('error')}")
+    print(f"[{tool_id}] Success")
+    return res.get("result", {})
+
+def test_full_workflow():
+    print("=== RunComfy Workflow E2E Test ===")
+    
+    # 0. Check existing machines
+    print("\n0. Checking for existing ComfyUI Instances...")
+    status_res = call_tool("runcomfy_check_status", {})
+    servers = status_res.get("servers", [])
+    
+    server_id = None
+    for s in servers:
+        if s.get("current_status") == "Ready":
+            server_id = s.get("server_id")
+            print(f"Found Ready machine: {server_id}")
+            break
+        elif s.get("current_status") in ("Starting", "Creating") and not server_id:
+            server_id = s.get("server_id")
+            print(f"Found Starting machine: {server_id}. Note: You might need to wait for it to be Ready.")
+
+    if not server_id:
+        raise Exception("\nNo active machine found. Expecting an already running machine. Aborting.")
+    
+    print("\n1. Proceeding with existing machine...")
+
+    try:
+        print("\n2. Loading custom workflow from JSON...")
+        import os
+        workflow_path = os.path.join(os.path.dirname(__file__), "flux_depth_controlnet_workflow.json")
+        with open(workflow_path, "r", encoding="utf-8") as f:
+            workflow_api = json.load(f)
+
+        print("\n3. Injecting correct prompt to fix ControlNet mismatch...")
+        for node_id, node in workflow_api.items():
+            if node.get("class_type") == "CLIPTextEncode":
+                current_text = node["inputs"].get("text", "")
+                if "landscape" in current_text:
+                    new_prompt = (
+                        "A back-view of a person standing in front of a wooden painting easel, "
+                        "looking out at a beautiful landscape with majestic mountains and a serene cyan lake. "
+                        "Highly detailed, masterpiece, realistic photography, cinematic lighting, 8k resolution"
+                    )
+                    node["inputs"]["text"] = new_prompt
+                    print(f"  -> Successfully updated prompt in node {node_id}")
+
+        print("\n4. Executing workflow with ControlNet inputs...")
+        
+        # 收集输入图片文件列表传递给 executor
+        # 我们在这里填入包含本地路径的 url
+        # call_tool 会使用 ToolHub 的 _preprocess_params 自动把这些 url 的本地路径替换为真正的 CDN 链接
+        input_dir = os.path.join(os.path.dirname(__file__), "input")
+        input_files = [
+            {
+                "filename": "depth_map.png",
+                "type": "images",
+                "url": os.path.join(input_dir, "depth_map.png")
+            },
+            {
+                "filename": "background_bokeh_img2.png",
+                "type": "images",
+                "url": os.path.join(input_dir, "background_bokeh_img2.png")
+            },
+            {
+                "filename": "character_ref_back.png",
+                "type": "images",
+                "url": os.path.join(input_dir, "character_ref_back.png")
+            },
+            {
+                "filename": "easel_blank_canvas_img4.png",
+                "type": "images",
+                "url": os.path.join(input_dir, "easel_blank_canvas_img4.png")
+            }
+        ]
+
+        exec_res = call_tool("runcomfy_workflow_executor", {
+            "server_id": server_id,
+            "workflow_api": workflow_api,
+            "input_files": input_files 
+        })
+        
+        print("Execution finished! Prompt ID:", exec_res.get("prompt_id"))
+        
+        images = exec_res.get("images", [])
+        print("Generated images count:", len(images))
+        if images:
+            # 拿到的是 base64 或者是 CDN url
+            img_data = images[0]
+            if isinstance(img_data, dict):
+                print("First image info:", img_data.get("url") or (img_data.get("data", "")[:50] + "..."))
+            else:
+                print("First image info (raw):", img_data[:50] + "...")
+                
+                output_dir = os.path.join(os.path.dirname(__file__), "output")
+                os.makedirs(output_dir, exist_ok=True)
+                output_path = os.path.join(output_dir, "test_output.png")
+                
+                try:
+                    if img_data.startswith("http"):
+                        import requests
+                        img_resp = requests.get(img_data)
+                        img_resp.raise_for_status()
+                        with open(output_path, "wb") as fh:
+                            fh.write(img_resp.content)
+                    else:
+                        import base64
+                        with open(output_path, "wb") as fh:
+                            fh.write(base64.b64decode(img_data))
+                    print(f"🎉 成功!生成的图片已保存至: {output_path}")
+                except Exception as e:
+                    print(f"Failed to save image: {e}")
+        
+    finally:
+        # 4. Cleanup
+        print("\n4. Keep machine alive (Skip cleanup so you can check runcomfy)")
+        # 暂时跳过 stop 方便排查
+        # call_tool("runcomfy_stop_env", {"server_id": server_id})
+
+if __name__ == "__main__":
+    test_full_workflow()
+    os._exit(0)

+ 67 - 0
examples/mini_restore/toolhub_test.prompt

@@ -0,0 +1,67 @@
+---
+model: qwen3.5-plus
+temperature: 0.3
+---
+
+$system$
+
+## 角色
+你是一个 ToolHub 工具测试 Agent。你的职责是通过 ToolHub 远程工具库发现并调度合适的工具(特别是 ComfyUI 相关工具)来完成复杂的图文生成任务。
+
+## 可用工具 (ToolHub)
+
+| 工具 | 用途 |
+|------|------|
+| `toolhub_search(keyword=...)` | 搜索/发现可用工具。返回 tool_id、参数、以及**生命周期分组(Group)和调用顺序**。 |
+| `toolhub_call(tool_id=..., params={...})` | 调用指定工具。图片参数传本地路径。 |
+
+## 工作流程
+
+### 第一步:环境检查
+1. 调用 `toolhub_search(keyword="runcomfy")` 或 `toolhub_search(keyword="builder")` 寻找 ComfyUI 相关工具。
+2. 仔细阅读搜索结果,识别是否有 `runcomfy_launch` -> `runcomfy_executor` -> `runcomfy_stop` 这样的序列,或 `runcomfy_workflow_builder` 这样的辅助工具。
+
+### 第二步:素材确认
+使用 `list_dir` 和 `read_file` 确认 `%input_dir%` 中的素材:
+- `character_ref_back.png`: 人物背面参考
+- `background_bokeh_img2.png`: 背景参考
+- `depth_map.png`: 深度图(适用于 ControlNet)
+- `easel_blank_canvas_img4.png`: 道具/构图参考
+
+### 第三步:设计并执行交互
+针对 `runcomfy` 任务:
+1. **构建 Workflow**: 
+   - 如果有 `runcomfy_workflow_builder` 工具,优先调用它来生成包含 ControlNet 的 workflow JSON。
+   - 必须在 workflow 中集成 ControlNet (Depth),使用 `depth_map.png` 路径。
+   - 融入人物和背景素材的描述。
+3. **执行生命周期**:
+   - `launch_comfy_env`: 启动远程 ComfyUI 实例。记得使用 `check_comfy_env_status` 直到机器状态为 Ready 才能拿到最终的 `server_id`。
+   - `runcomfy_workflow_executor`: 传入构建好的 workflow JSON,执行图片生成任务。
+     **重要规范 - 输入图片参数格式:** 如果流程包含本地参考图/深度图输入,严禁传入随意格式,必须在 `input_files` 参数中构造成对象数组格式,例如:
+     ```json
+     "input_files": [
+         { "filename": "depth_map.png", "type": "images", "url": "%input_dir%/depth_map.png" },
+         { "filename": "character.png", "type": "images", "url": "%input_dir%/character_ref_back.png" }
+     ]
+     ```
+     (只要 `url` 中提供的是本地路径,底层代理就会自动帮我们上传并替换成远端 CDN 链接给 Executor)
+   - `runcomfy_stop_env`: 任务完成后(或失败后)务必释放实例资源。
+
+### 第四步:报告结果
+输出结构化的结果报告,包含每个步骤的状态、使用的工具、生成的图片路径。
+
+## 路径约定
+- 输入素材:`%input_dir%`
+- 输出结果:`%output_dir%`
+
+## 注意事项
+1. **严禁硬编码**: 必须根据 `toolhub_search` 的实时返回结果来决定工具参数。
+2. **异常释放**: 即便 executor 报错,也应尝试调用 `stop` (如分组定义了 stop 步骤)。
+3. **ControlNet 必选**: 任务明确要求带 ControlNet,必须在 workflow 中体现。
+
+$user$
+请执行以下任务:
+1. 检查 `%input_dir%` 下的素材文件。
+2. 使用 `runcomfy` 相关工具(可能涉及 builder 构造 workflow,以及 launch/executor/stop 生命周期),通过 ComfyUI 执行一次带 ControlNet (Depth) 的图像生成。
+3. 将 `%input_dir%/depth_map.png` 用作 ControlNet 深度引导。
+4. 将生成结果保存到 `%output_dir%` 目录,并给出详细的执行报告。

+ 0 - 70
examples/mini_restore/upload.py

@@ -1,70 +0,0 @@
-import asyncio
-import os
-import sys
-import httpx
-from cyber_sdk.ali_oss import upload_localfile
-
-async def upload_image(local_file_path: str, bucket_name: str = 'aigc-admin', bucket_path: str = 'template') -> str:
-    """Uploads a local image to OSS and returns the CDN URL."""
-    print(f"Uploading {local_file_path} to {bucket_name}/{bucket_path}...")
-    # Use forward slashes so cyber_sdk's .split('/') can correctly extract filename on Windows
-    safe_path = os.path.abspath(local_file_path).replace("\\", "/")
-    result = await upload_localfile(
-        file_path=safe_path, 
-        bucket_path=bucket_path, 
-        bucket_name=bucket_name
-    )
-    print("Upload SDK Response:", result)
-    
-    oss_object_key = result.get('oss_object_key')
-    if oss_object_key:
-        cdn_url = f"https://res.cybertogether.net/{oss_object_key}"
-        return cdn_url
-    return None
-
-async def download_image(url: str, save_path: str):
-    """Downloads an image from an HTTP link and saves it locally."""
-    print(f"Downloading from {url} to {save_path}...")
-    async with httpx.AsyncClient(timeout=30.0) as client:
-        resp = await client.get(url)
-        resp.raise_for_status()
-        with open(save_path, 'wb') as f:
-            f.write(resp.content)
-    print(f"Download completed: {save_path}")
-
-async def main():
-    if len(sys.argv) < 2:
-        print("Usage:")
-        print("  Upload: python upload.py <file_path>")
-        print("  Download: python upload.py download <url> <save_path>")
-        
-        # Self-test block if no param is given
-        print("\n--- Running Self Test ---")
-        test_file = 'img_1_gen.png'
-        if not os.path.exists(test_file):
-            print(f"Creating a dummy 1x1 PNG at {test_file} for testing.")
-            with open(test_file, 'wb') as f:
-                f.write(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82')
-        
-        url = await upload_image(test_file, 'aigc-admin', 'crawler/image')
-        print(f"\nExtracted URL: {url}")
-        
-        if url:
-            # Download it back
-            download_path = "downloaded_dummy.png"
-            await download_image(url, download_path)
-            
-    elif sys.argv[1] == 'download':
-        if len(sys.argv) >= 4:
-            await download_image(sys.argv[2], sys.argv[3])
-        else:
-            print("Error: Missing parameters for download.")
-            print("Usage: python upload.py download <url> <save_path>")
-    else:
-        # Upload context
-        file_path = sys.argv[1]
-        url = await upload_image(file_path, 'aigc-admin', 'crawler/image')
-        print(f"\nFinal CDN URL: {url}")
-
-if __name__ == '__main__':
-    asyncio.run(main())

+ 0 - 422
examples/mini_restore/workflow_loop.py

@@ -1,422 +0,0 @@
-import sys
-import os
-import asyncio
-import json
-import base64
-import re
-
-# 将项目根目录加入,方便导入内部包
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
-
-from agent.tools.builtin.toolhub import toolhub_call
-from agent.llm.gemini import create_gemini_llm_call
-
-from dotenv import load_dotenv
-load_dotenv()
-
-try:
-    gemini_llm_call = create_gemini_llm_call()
-except ValueError as e:
-    print(f"初始化 Gemini 失败: {e},请检查 .env。")
-    sys.exit(1)
-
-from agent.tools.builtin.content import content_search
-
-# -----------------
-# Utility Functions
-# -----------------
-def encode_image(image_path: str) -> str:
-    with open(image_path, "rb") as image_file:
-        return base64.b64encode(image_file.read()).decode('utf-8')
-
-def get_base64_url(image_path: str) -> str:
-    b64_data = encode_image(image_path)
-    ext = image_path.split('.')[-1].lower()
-    if ext == 'jpg': ext = 'jpeg'
-    return f"data:image/{ext};base64,{b64_data}"
-
-# -----------------
-# Tools definitions
-# -----------------
-async def call_banana_tool(prompt: str, aspect_ratio: str = None, reference_image: str = None, is_final: bool = True) -> str:
-    """包装 call_banana.py 生成图片,返回一张图的路径"""
-    print(f"\n[Tool] ✨ 正在调用 call_banana 生成图片 (is_final={is_final}), Prompt: {prompt[:50]}...")
-    script_path = os.path.join(os.path.dirname(__file__), "call_banana.py")
-    
-    env = os.environ.copy()
-    env["PYTHONIOENCODING"] = "utf-8"
-    
-    cmd_args = [sys.executable, script_path, "-p", prompt]
-    if aspect_ratio:
-        cmd_args.extend(["-a", aspect_ratio])
-    if reference_image:
-        cmd_args.extend(["-i", reference_image])
-        
-    process = await asyncio.create_subprocess_exec(
-        *cmd_args,
-        stdout=asyncio.subprocess.PIPE,
-        stderr=asyncio.subprocess.PIPE,
-        env=env
-    )
-    stdout, stderr = await process.communicate()
-    output = stdout.decode('utf-8', errors='replace')
-    err_output = stderr.decode('utf-8', errors='replace')
-    if err_output:
-        output += "\n" + err_output
-    
-    match = re.search(r"已保存到本地 -> (.+)", output)
-    if match:
-        path = match.group(1).strip()
-        print(f"[Tool] ✅ call_banana 返回图片路径: {path}")
-        return path
-    else:
-        print(f"[Tool] ❌ call_banana 执行失败:\n{output}")
-        return f"Tool Execution Failed. output:\n{output}"
-
-async def search_tool(keyword: str) -> str:
-    print(f"\n[Tool] 🔍 启动小红书调研, 关键词: {keyword}")
-    try:
-        result = await content_search(platform="xhs", keyword=keyword, max_count=3)
-        return result.output
-    except Exception as e:
-        return f"查询失败: {e}"
-
-def get_agent_tools():
-    return [
-        {
-            "type": "function",
-            "function": {
-                "name": "search_tool",
-                "description": "如果需要了解某个风格如何写 Prompt(例如“写实风格提示词”),调用此工具进行小红书全网搜索,返回总结经验以更新你的参数。",
-                "parameters": {
-                    "type": "object",
-                    "properties": {
-                        "keyword": {
-                            "type": "string",
-                            "description": "搜索关键词"
-                        }
-                    },
-                    "required": ["keyword"]
-                }
-            }
-        },
-        {
-            "type": "function",
-            "function": {
-                "name": "call_banana_tool",
-                "description": "使用此工具通过给定的详细提示词生成图片。工具将返回生成图片的本地保存路径。",
-                "parameters": {
-                    "type": "object",
-                    "properties": {
-                        "prompt": {
-                            "type": "string",
-                            "description": "英语或中文详细的生图提示词"
-                        },
-                        "aspect_ratio": {
-                            "type": "string",
-                            "description": "(可选)你期望生成的图片宽高比,例如 3:4, 16:9, 1:1,请根据目标参考图的比例传入该参数"
-                        },
-                        "reference_image": {
-                            "type": "string",
-                            "description": "(动作控制底图)如果你在这一步设 is_final=true,请将你在上一阶段生成的【辅助骨架素材(is_final=false)】产生的本地路径填入此处。绝对禁止传入原始目标照片!"
-                        },
-                        "is_final": {
-                            "type": "boolean",
-                            "description": "指示本次生成是否是本轮次的最终产物。如果你需要先生成一张『白底火柴人/3D骨架』作为辅助垫图素材,请设为 false;拿到素材后,你必须继续将它的本地路径填给 `reference_image` 并使用最终 Prompt 和 is_final=true 完成最后合成。"
-                        }
-                    },
-                    "required": ["prompt"]
-                }
-            }
-        }
-    ]
-
-
-
-# -----------------
-# Main Workflow Loop
-# -----------------
-
-def get_base64_url(image_path: str) -> str:
-    with open(image_path, "rb") as image_file:
-        b64_data = base64.b64encode(image_file.read()).decode('utf-8')
-    ext = image_path.split('.')[-1].lower()
-    if ext == 'jpg': ext = 'jpeg'
-    return f"data:image/{ext};base64,{b64_data}"
-
-async def main():
-    import argparse
-    import os
-    import json
-    
-    default_target = os.path.join(os.path.dirname(os.path.abspath(__file__)), "input", "img_1.png")
-    parser = argparse.ArgumentParser(description="多智能体画图自动优化 Workflow")
-    parser.add_argument("-t", "--target", default=default_target, help="你想逼近的目标参考图本地路径")
-    parser.add_argument("-p", "--pose", default=None, help="你提供的姿势参考图(如果有的话,给 Agent 用来走捷径垫底)")
-    parser.add_argument("-m", "--max_loops", type=int, default=15, help="优化的最大迭代论调")
-    parser.add_argument("-r", "--resume", action="store_true", help="是否从上次的 history.json 继续运行")
-    args = parser.parse_args()
-    
-    target_image = args.target
-    pose_image = args.pose
-
-    print("\n" + "="*50)
-    print("🤖 启动双 Agent 生图闭环工作流 (纯 Vision-Language 架构)")
-    print("="*50)
-    
-    if not os.path.exists(target_image):
-        print(f"⚠️ 找不到目标图片: {target_image}")
-        print("提示: 系统依然会运行寻找文件,但 Agent 2 将无法给出评估。可随便放一个图片来模拟。")
-    
-    sys_content = f"你是一个高度自治的闭环生图优化 AI 架构师。你的目标是:生成一张与【目标参考图】在主角姿势、整体结构上无限接近的图片。\n你拥有极强的视觉反思能力和 Prompt 编写能力。\n\n【核心工作流与防坑指南】:\n- 你会看到你的【目标参考图】和你的【往期历史尝试与生成结果】。\n- 请你先利用你的**多模态火眼金睛**,无情地对自己上一轮生成的图片进行找茬。绝不允许说客套话!重点对比人物骨架、姿势和构图的偏离程度。\n- 紧接着,请在反思的基础上,直接重构或调整你的 Prompt,并在一次回复中调用 `call_banana_tool` 下发生图指令!\n- 【防作弊铁律】:你**绝对禁止**直接将【目标参考图】的路径传进 `reference_image` 来作弊!如果你想用图生图垫出完美动作,必须使用【中间素材战法】亲手画一张骨架出来垫。\n- 【中间素材战法】:如果原图姿态过于刁钻复杂,**要求你必须**分两步走:\n   第一步:设置 `is_final=false` 并写一段专门用于抽出单一维度的动作骨架/白模 Prompt(如: \"a generic white 3d mannequin jumping in mid-air, clean white background, high contrast skeleton\"),专门用于抽出干净的辅助骨架。\n   第二步:拿到这只纯净骨架的本地路径后,在同回合的下一次调用中,把这只骨架当做 `reference_image` 垫进去,配合你华丽的最终描述(如: \"a neon cyberpunk assassin jumping\"),设置 `is_final=true` 完成高阶对齐兼防污染! \n\n"
-    
-    if pose_image and os.path.exists(pose_image):
-        sys_content += f"【🔥终极开挂特权】:\n天啊!用户居然为你额外提供了一张极致完美的【姿势参考图】!既然有了这张现成的动作骨架底图,你**立刻抛弃**两步走去抽骨架的方法。你应当直接使用特权,将这张姿势参考图的绝对物理路径 `{os.path.abspath(pose_image)}` 作为 `reference_image` 无脑传给引擎,配合你的终极词汇,并在第一回合内设置 `is_final=true` 完成终极绝杀生成!\n\n"
-
-    sys_content += "流程要求:\n1. 仔细分析差异,在你的纯文本回复段落写出【犀利的反思和执行步骤】。\n2. 反思结束后,使用工具发号施令。\n3. 当调用 `is_final=true` 时,视为你的本轮彻底结束。"
-
-    system_msg = {
-        "role": "system",
-        "content": sys_content
-    }
-
-    max_loops = args.max_loops
-    current_generation_loop_count = 0
-    last_gen_info = None
-    prompt_history = [] # 记录完整的历史 Prompt 轨迹,防止反复抽卡
-    
-    history_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "history.json")
-    if args.resume and os.path.exists(history_file):
-        try:
-            with open(history_file, "r", encoding="utf-8") as f:
-                prompt_history = json.load(f)
-            if prompt_history:
-                current_generation_loop_count = len(prompt_history)
-                last_gen_info = prompt_history[-1]
-                print(f"✅ [状态恢复] 已成功从 history.json 加载 {current_generation_loop_count} 轮历史,即将开始第 {current_generation_loop_count + 1} 轮...")
-        except Exception as e:
-            print(f"⚠️ [状态恢复失败] 读取历史记录报错: {e},将重新开始第一轮。")
-            prompt_history = []
-
-    while current_generation_loop_count < max_loops:
-        print(f"\n" + "="*40)
-        print(f"🔄 优化循环: 第 {current_generation_loop_count + 1}/{max_loops} 轮")
-        print("="*40)
-        
-        # 每轮重置上下文,只保留 system message 和含有"上次结果"的 initial user message
-        messages = [system_msg]
-        
-        if last_gen_info is None:
-            try:
-                target_b64_url = get_base64_url(target_image)
-                content_list = [
-                    {"type": "text", "text": "【首轮启动】\n这是你需要逼近的【目标参考图】。现在请你仔细观察它,提炼出一份初步生图 Prompt。\n因为是第一轮,请直接凭借直觉观察,并使用 call_banana_tool 生成原型。"},
-                    {"type": "image_url", "image_url": {"url": target_b64_url}}
-                ]
-                
-                if pose_image and os.path.exists(pose_image):
-                    content_list.append({"type": "text", "text": "并且,下面是用户良心为你提供的【开挂级·姿势参考图】!你可以直接在接下来的提示词工具调用中将此图拿去垫图!"})
-                    content_list.append({"type": "image_url", "image_url": {"url": get_base64_url(pose_image)}})
-                    
-                messages.append({
-                    "role": "user",
-                    "content": content_list
-                })
-            except Exception as e:
-                messages.append({
-                    "role": "user",
-                    "content": f"目标图片读取失败({e}),请盲猜一个初始 Prompt 用 call_banana_tool 生成。"
-                })
-        else:
-            try:
-                target_b64_url = get_base64_url(target_image)
-                user_content = [
-                    {"type": "text", "text": "【持续干预闭环】\n这是不可动摇的【目标参考图】,它是一切评判的唯一基准:"},
-                    {"type": "image_url", "image_url": {"url": target_b64_url}}
-                ]
-                
-                if pose_image and os.path.exists(pose_image):
-                    user_content.append({"type": "text", "text": "【外挂辅助】\n这是不可动摇的【姿势参考图】,请毫不犹豫地拿它去填进 reference_image 控制动作:"})
-                    user_content.append({"type": "image_url", "image_url": {"url": get_base64_url(pose_image)}})
-                    
-                user_content.append({"type": "text", "text": "\n==== 【你的历史试错轨迹】 ====\n为了防止你在这场试错过程中来回打转(所谓的废卡反复抽卡),我为你列出了你*从古至今*所有的失败作品和对应的提示词!请认真观察下面每一张你过去的废片:\n"})
-                
-                for i, record in enumerate(prompt_history):
-                    user_content.append({"type": "text", "text": f"-- 第 {i+1} 轮 --\n[上次使用的 Prompt]:\n{record['prompt']}\n[此轮的废片结果]:"})
-                    
-                    try: 
-                        img_path = record.get("image_paths", [record.get("image_path")])[0]
-                        # 节约上下文 Token 和视觉注意力:只渲染第一张(由于打底盲测)和最近一次的历史原图,中间的全部折叠仅保留反思文本
-                        if i == 0 or i == len(prompt_history) - 1:
-                            user_content.append({"type": "image_url", "image_url": {"url": get_base64_url(img_path)}})
-                        else:
-                            user_content.append({"type": "text", "text": "*(由于历史过于久远,中间轮次图片已省去展示,请聚焦于下面你对它的纯文本反思)*"})
-                    except:
-                        pass
-                        
-                    if record.get("feedback"):
-                        user_content.append({"type": "text", "text": f"[你在本轮结束后的反思]:\n{record['feedback']}\n"})
-                
-                user_content.append({"type": "text", "text": "====================\n\n现在,结合上述轨迹与那张【目标参考图】,请在回复中写出最新的【极度苛刻自我反思】,然后立马调用工具生成这轮新的 Prompt!"})
-                
-                messages.append({"role": "user", "content": user_content})
-                
-            except Exception as e:
-                messages.append({"role": "user", "content": f"上下文读取失败 ({e})。请重试用 call_banana_tool 生成。"})
-
-        # Agent 1 内部工具调研微循环 (Agent 1 minor logic loop)
-        agent1_finished_generation = False
-        consecutive_empty = 0
-        
-        while not agent1_finished_generation:
-            print(f"---\n💬 正在请求 Agent 1 (Prompt 师)...")
-            # 这里 Agent 1 也换成 qwen-vl-max,这样它才能看到传给它的上一轮图片
-            response = await gemini_llm_call(
-                messages=messages,
-                model="gemini-3.1-pro-preview",
-                tools=get_agent_tools()
-            )
-            
-            content = response.get("content", "")
-            tool_calls = response.get("tool_calls")
-            
-            if content:
-                print(f"\n[Agent 1 思考]:\n{content}")
-                
-            if not tool_calls and not content:
-                consecutive_empty += 1
-                if consecutive_empty >= 3:
-                    print("Agent 连续多次无有意义输出,强制跳出本轮。")
-                    break
-            else:
-                consecutive_empty = 0
-
-            # 保持上下文
-            assistant_reply = {"role": "assistant"}
-            if content: assistant_reply["content"] = content
-            if tool_calls: assistant_reply["tool_calls"] = tool_calls
-            if "raw_gemini_parts" in response: assistant_reply["raw_gemini_parts"] = response["raw_gemini_parts"]
-            messages.append(assistant_reply)
-
-            if tool_calls:
-                for tc in tool_calls:
-                    func_name = tc["function"]["name"]
-                    args_dict = json.loads(tc["function"]["arguments"])
-                    tc_id = tc["id"]
-                    
-                    if func_name == "search_tool":
-                        res = await search_tool(**args_dict)
-                        messages.append({
-                            "role": "tool",
-                            "tool_call_id": tc_id,
-                            "content": str(res)
-                        })
-                    
-                    elif func_name == "call_banana_tool":
-                        is_final = args_dict.get("is_final", True)
-                        print(f"\n⚙️ 节点发起了生图请求 (是否为终极图: {is_final})!")
-                        gen_path = await call_banana_tool(**args_dict)
-                        
-                        if os.path.exists(gen_path):
-                            ext = gen_path.split('.')[-1]
-                            import shutil
-                            if is_final:
-                                new_gen_path = f"gen_loop_{current_generation_loop_count + 1}.{ext}"
-                            else:
-                                import uuid
-                                new_gen_path = f"gen_loop_{current_generation_loop_count + 1}_material_{str(uuid.uuid4())[:8]}.{ext}"
-                            shutil.move(gen_path, new_gen_path)
-                            gen_path = new_gen_path
-                            print(f"[文件管理] 生图结果已重命名并保存为: {new_gen_path}")
-                        
-                        prompt_used = args_dict.get("prompt", "")
-                        
-                        messages.append({
-                            "role": "tool",
-                            "tool_call_id": tc_id,
-                            "content": f"已成功生成,图片路径: {os.path.abspath(gen_path)}"
-                        })
-                        
-                        if is_final:
-                            agent1_finished_generation = True
-                            current_generation_loop_count += 1
-                            
-                            last_gen_info = {
-                                "prompt": prompt_used,
-                                "image_path": gen_path,
-                                "feedback": content if content else "无反思内容"
-                            }
-                            
-                            prompt_history.append(last_gen_info)
-                            try:
-                                with open(history_file, "w", encoding="utf-8") as f:
-                                    json.dump(prompt_history, f, ensure_ascii=False, indent=2)
-                            except Exception as e:
-                                print(f"[警告] 历史记录保存失败: {e}")
-                            break # 跳出 tool_calls for loop 并进入下一大轮
-                        else:
-                            print(f"[战术回馈] 这是辅助素材,已将路径返回给 Agent1 继续思考。")
-            else:
-                # 没调工具
-                print("\n[控制中心] Agent 1 没有继续使用任何工具。结束其周期。")
-                agent1_finished_generation = True
-                break
-                
-    print("\n🎉 工作流闭环成功完成或达到了最大迭代次数。")
-    
-    # 最后由评估专家出具一份最完善的多维度最终报告
-    if len(prompt_history) > 0 and os.path.exists(target_image):
-        print("\n" + "="*50)
-        print("🏆 正在生成【专家最终多维度反馈报告】...")
-        print("="*50)
-        
-        first_gen_record = prompt_history[0]
-        last_gen_record = prompt_history[-1]
-        
-        # 兼容旧版本的单图记录和新版本的多图记录
-        first_gen = first_gen_record.get("image_paths", [first_gen_record.get("image_path")])[0]
-        last_gen = last_gen_record.get("image_paths", [last_gen_record.get("image_path")])[0]
-        
-        if first_gen and last_gen and os.path.exists(first_gen) and os.path.exists(last_gen):
-            try:
-                target_b64 = encode_image(target_image)
-                first_b64 = encode_image(first_gen)
-                last_b64 = encode_image(last_gen)
-                target_ext = target_image.split('.')[-1].lower()
-                first_ext = first_gen.split('.')[-1].lower()
-                last_ext = last_gen.split('.')[-1].lower()
-                
-                # 构建供最终分析的文字轨迹
-                full_history_text = "【历次 Prompt 与专家反馈的演进轨迹】\n"
-                for i, record in enumerate(prompt_history):
-                    full_history_text += f"-- 第 {i+1} 轮 --\n[Prompt]: {record['prompt']}\n[反馈]: {record['feedback']}\n\n"
-
-                final_messages = [
-                    {
-                        "role": "system",
-                        "content": "你是首席AI打样架构师。目前的生图迭代优化工作流已拉下帷幕。你不需要拘泥于打分,而是要通过回顾整个演进历程,总结出‘最好用的 Prompt 模板’和‘最精准的评估反馈维度模板’。"
-                    },
-                    {
-                        "role": "user",
-                        "content": [
-                            {"type": "text", "text": "【目标参考图(原图)】:"},
-                            {"type": "image_url", "image_url": {"url": f"data:image/{target_ext if target_ext != 'jpg' else 'jpeg'};base64,{target_b64}"}},
-                            {"type": "text", "text": "这是最初第1轮盲试的生成图:"},
-                            {"type": "image_url", "image_url": {"url": f"data:image/{first_ext if first_ext != 'jpg' else 'jpeg'};base64,{first_b64}"}},
-                            {"type": "text", "text": f"这是经过迭代后的【最终生成图】:"},
-                            {"type": "image_url", "image_url": {"url": f"data:image/{last_ext if last_ext != 'jpg' else 'jpeg'};base64,{last_b64}"}},
-                            {"type": "text", "text": f"下面是 {len(prompt_history)} 轮迭代中,Prompt 和专家反馈的完整变迁记录:\n\n{full_history_text}\n\n请结合首尾图片的巨大差异以及中间的踩坑过程,深度复盘:\n1. 在构建生图 Prompt 时,哪些描述方式、句型或结构最能有效命中模型?请提炼出一个【最终版高转化率 Prompt 语法模板】。\n2. 在进行视觉反馈时,哪些维度的批评和建议对 Prompt 师是最具指导意义的?请提炼出一个【最终版高维度视觉评估反馈模板】。\n这两个模版需要具备极强的通用性和实战复用价值!"}
-                        ]
-                    }
-                ]
-                
-                response = await gemini_llm_call(
-                    messages=final_messages,
-                    model="gemini-3.1-pro-preview"
-                )
-                print(f"\n[Agent 2] 📋 【最终多维度评估报告】:\n{response['content']}\n")
-            except Exception as e:
-                print(f"最终报告生成失败: {e}")
-
-if __name__ == "__main__":
-    asyncio.run(main())

+ 0 - 64
examples/process/config.py

@@ -1,64 +0,0 @@
-"""
-项目配置
-"""
-
-from agent.core.runner import KnowledgeConfig, RunConfig
-
-
-# ===== Stage 1:调研配置 =====
-
-RESEARCH_RUN_CONFIG = RunConfig(
-    model="qwen3.5-plus",
-    temperature=0.3,
-    max_iterations=1000,
-    extra_llm_params={"extra_body": {"enable_thinking": True}},
-    agent_type="main",
-    name="工具调研与文档生成",
-    knowledge=KnowledgeConfig(
-        enable_extraction=False,
-        enable_completion_extraction=True,
-        enable_injection=False,
-        owner="sunlit.howard@gmail.com",
-        default_tags={"project": "tool_research", "domain": "ai_agent"},
-        default_scopes=["org:cybertogether"],
-        default_search_types=["tool", "guide"],
-        default_search_owner="sunlit.howard@gmail.com"
-    )
-)
-
-# ===== Stage 2:分析配置 =====
-
-ANALYSIS_RUN_CONFIG = RunConfig(
-    model="anthropic/claude-sonnet-4-6",
-    temperature=0.3,
-    max_iterations=500,
-    agent_type="coordinator",
-    name="工作流分析 Pipeline",
-)
-
-ANALYSIS_MODEL = "anthropic/claude-sonnet-4-6"
-
-# ===== 输出目录 =====
-
-RESEARCH_OUTPUT_DIR = "examples/tool_research_v2/output"            # Stage 1 输出根目录
-ANALYSIS_OUTPUT_DIR = "examples/tool_research_v2/output/analysis"   # Stage 2 输出目录
-
-
-# ===== 基础设施配置 =====
-
-SKILLS_DIR = "./skills"
-TRACE_STORE_PATH = ".trace"
-DEBUG = True
-LOG_LEVEL = "INFO"
-LOG_FILE = None
-
-# ===== 浏览器配置 =====
-BROWSER_TYPE = "local"
-HEADLESS = False
-
-# ===== IM 配置 =====
-IM_ENABLED = True
-IM_CONTACT_ID = "agent_research"
-IM_SERVER_URL = "ws://43.106.118.91:8105"
-IM_WINDOW_MODE = True
-IM_NOTIFY_INTERVAL = 10.0

+ 0 - 27
examples/process/presets.json

@@ -1,27 +0,0 @@
-{
-  "main": {
-    "max_iterations": 1000,
-    "skills": ["planning"],
-    "description": "主 Agent - 调研任务管理与协调"
-  },
-  "research": {
-    "system_prompt_file": "prompts/research.prompt",
-    "max_iterations": 200,
-    "temperature": 0.3,
-    "skills": ["planning", "research", "browser"],
-    "description": "调研 Agent - 根据指令搜索策略、工具、方法论等信息"
-  },
-  "coordinator": {
-    "system_prompt_file": "prompts/coordinator.prompt",
-    "max_iterations": 500,
-    "skills": ["planning"],
-    "description": "工作流分析协调器:编排4步分析流程"
-  },
-  "analyst": {
-    "system_prompt_file": "prompts/analyst.prompt",
-    "max_iterations": 200,
-    "temperature": 0.3,
-    "skills": [],
-    "description": "分析子 Agent:执行单步分析任务(意图归纳、聚类、变体发现、粗工序提取)"
-  }
-}

+ 0 - 23
examples/process/prompts/analyst.prompt

@@ -1,23 +0,0 @@
----
-temperature: 0.3
----
-
-$system$
-
-## 角色
-你是一个专注的工作流分析专家。你负责执行工作流分析 Pipeline 中的单步分析任务,包括:
-- 意图归纳:将一条工作流的技术步骤归纳为意图级描述
-- 语义聚类:对跨工作流的意图列表做语义聚类,发现能力模块
-- 变体发现:在一个能力模块内发现不同实现变体
-- 粗工序总结:对一个品类的工作流总结因果性编排思路
-
-## 核心原则
-1. **忠实于数据**:不臆造不存在于原始步骤中的意图或工具
-2. **意图粒度判断**:归纳意图时,由你根据上下文判断合并粒度,目标是让不同工作流可比较
-3. **结构化输出**:始终返回符合任务要求的 JSON 格式
-4. **因果推理**:总结粗工序时,必须解释"为什么先做 A 再做 B",而不是简单列举频次
-
-## 输出规范
-- 意图描述:20 字以内,动宾结构(如"锁定产品物理结构")
-- 能力模块名称:4-8 字,品类无关(如"定义风格"而非"电商风格定义")
-- 所有输出必须是合法 JSON,不要包含 markdown 代码块标记

+ 0 - 179
examples/process/prompts/coordinator.prompt

@@ -1,179 +0,0 @@
----
-temperature: 0.3
----
-
-$system$
-
-## 角色
-你是一个工作流分析协调器。你的任务是对已采集的 AI 创作工作流数据进行结构化分析,提炼出可复用的工序体系(细工序 + 粗工序),并输出完整的结构化 JSON 报告。
-
-## 输入数据格式
-你将收到一个 JSON 对象,包含:
-- `workflows`:工作流列表,每条工作流有 `id`(如 `wf_001`)、`name`、`category`(可能为空)、`source_channel`、`steps`(步骤数组,每步有 `步骤描述`、`使用工具`、`用户输入`、`输出结果`)
-
-## 可用工具
-- `agent`:调用 analyst 子 agent 执行分析任务
-- `write_file`:写中间结果和最终输出
-- `read_file`:读取中间结果
-
-## 原子能力表
-在 `%output_dir%/atomic_capabilities.json` 中已预先准备好原子能力表,格式为:
-```json
-{
-  "atomic_capabilities": [
-    {
-      "id": "能力ID",
-      "name": "能力名称",
-      "description": "能力描述",
-      "criterion": "判断标准"
-    }
-  ]
-}
-```
-在 Step 2b 匹配时,使用 `read_file` 读取该文件。
-
-## 执行流程
-
-### Step 1:意图归纳(粒度统一化)
-
-对**每一条**工作流,调用一个 analyst 子 agent,任务如下:
-```
-读取工作流 {wf_id} 的完整步骤,将步骤重新归纳为「意图级描述」。
-规则:
-- 允许把多个连续步骤合并为一个意图(如9个技术步骤 → 3个意图)
-- 允许一个步骤就对应一个意图
-- 意图粒度由你根据上下文判断,目标是让不同工作流的步骤在意图层面可比较
-- 每个意图记录:意图描述(20字以内)、涉及的原始步骤序号、核心工具名称
-输出为 JSON 数组。
-```
-
-将所有工作流的意图归纳结果合并,写入 `%output_dir%/step1_intents.json`,格式:
-```json
-{
-  "wf_001": [
-    {"intent": "意图描述", "source_steps": [步骤序号], "tools": ["工具名称"]},
-    ...
-  ]
-}
-```
-
-**注意**:Step 1 可以并行调用多个 analyst(每条工作流一个),提高效率。
-
----
-
-### Step 2:能力模块聚类(细工序发现)
-
-读取 `step1_intents.json`,收集所有工作流的所有意图描述(约 N×3 个)。
-
-**Step 2a:LLM 聚类**
-
-调用一个 analyst 子 agent,任务如下:
-```
-对以下意图列表做跨工作流语义聚类,发现品类无关的「能力模块」(细工序)。
-规则:
-- 语义相近的意图归为一个能力模块
-- 能力模块应品类无关,可跨品类复用
-- 目标:10-20 个能力模块
-每个模块输出:模块ID(cm_001起)、模块名称、描述、包含的意图列表(带来源工作流ID)
-```
-
-**Step 2b:原子能力匹配**
-
-用 `read_file` 读取 `%output_dir%/atomic_capabilities.json`,对每个能力模块在原子能力表中查找语义最接近的原子能力:
-- 如果找到语义相近的原子能力,记录其 `id` 和 `name`,并估算相似度分数(0-1)
-- 如果没有语义相近的原子能力,则标记 `atomic_capability_id: null`,表示这是新发现的能力模块
-
-将最终结果写入 `%output_dir%/step2_modules.json`,每个模块包含以下字段:
-- 匹配成功:`atomic_capability_id`(原子能力ID)、`atomic_capability_name`(原子能力名称)、`atomic_match_score`(相似度分数)
-- 未匹配:三个字段均为 `null`
-
----
-
-### Step 3:变体发现
-
-读取 `step2_modules.json` 和原始工作流数据。
-
-对**每个能力模块**,调用一个 analyst 子 agent,任务如下:
-```
-分析能力模块「{module_name}」下的所有原始步骤(来自不同工作流),发现实现变体。
-规则:
-- 按工具/方法的相似性聚类
-- 对每个变体,聚合其典型执行步骤(按步骤功能去重合并,保留最具代表性的描述)
-- 注明每个变体来自哪些工作流
-```
-
-将所有模块的变体结果合并,更新 `step2_modules.json` 中对应模块的 `variants` 字段。
-
----
-
-### Step 4:粗工序提取
-
-读取原始工作流数据和 `step2_modules.json`。
-
-**4a 品类分组**:调用一个 analyst 子 agent,按内容品类对所有工作流分组。
-
-**4b 粗工序总结**:对**每个品类**,调用一个 analyst 子 agent,任务如下:
-```
-读取品类「{category}」下所有工作流的能力模块编排顺序,总结:
-1. 做这类内容的典型思路(能力模块的编排顺序)
-2. 为什么先做 A 再做 B(因果推理,不是统计频次)
-输出:粗工序名称、步骤列表(能力模块引用)、因果推理说明、来源工作流列表
-```
-
----
-
-### 最终输出
-
-将所有结果整合,写入 `%output_path%`,格式见 `输出格式规范`。
-
-## 输出格式规范
-
-```json
-{
-  "capability_modules": [
-    {
-      "id": "模块ID",
-      "name": "模块名称",
-      "description": "模块描述",
-      "atomic_capability_id": "原子能力ID或null",
-      "atomic_capability_name": "原子能力名称或null",
-      "atomic_match_score": "相似度分数或null",
-      "variants": [
-        {
-          "name": "变体名称",
-          "typical_steps": [
-            {"step": "步骤名称", "detail": "步骤详情"}
-          ],
-          "source_workflows": ["工作流ID"]
-        }
-      ]
-    }
-  ],
-  "coarse_workflows": [
-    {
-      "id": "粗工序ID",
-      "category": "品类名称",
-      "rationale": "因果推理说明",
-      "steps": [
-        {"module_id": "能力模块ID", "module_name": "能力模块名称", "note": "备注"}
-      ],
-      "source_workflows": ["工作流ID"]
-    }
-  ],
-  "provenance": {
-    "工作流ID": {
-      "source_channel": "来源渠道",
-      "source_name": "来源名称",
-      "category": "品类"
-    }
-  }
-}
-```
-
-$user$
-请开始分析以下工作流数据:
-
-%workflows_json%
-
-输出目录(中间文件):%output_dir%
-最终输出路径:%output_path%

+ 0 - 80
examples/process/prompts/research.prompt

@@ -1,80 +0,0 @@
----
-temperature: 0.3
----
-
-$system$
-## 角色
-你是一个专注的渠道调研专家。你负责在指定的单个渠道(如小红书、X、youtube)进行完整的广度调研,包括多关键词搜索、适度查看内容,并输出结构化的【工序(工作流)】调研结果。
-
-## 核心原则
-1. **渠道专注**:你只负责一个渠道的完整调研,绝不跨渠道。
-2. **唯工序论**:本调研**只关注能解决问题的多步工序(Workflow/组合方案)**,不搜集单一零散工具。
-3. **结构化提取**:必须将工序严格拆解为按序执行的步骤,并明确每个步骤的工具、输入与输出。
-4. **相关性过滤**:只记录与调研目标相关的**图片生成**工序(排除视频、音频及非AI桌面软件如PS)。
-
-## 可用工具
-### 内容搜索工具
-- `search_posts(keyword, channel, cursor="0", max_count=20)`: 搜索帖子
-  - **channel 参数**:xhs(小红书), gzh(公众号), zhihu(知乎), bili(B站), douyin(抖音), toutiao(头条), weibo(微博)
-  - 示例:`search_posts("AI 生图工作流", channel="xhs", max_count=20)`
-- `select_post(index)`: 查看帖子详情(需先调用 search_posts)
-  - 示例:`select_post(index=1)`
-- `youtube_search(keyword)`: 搜索 YouTube 视频
-  - 示例:`youtube_search("AI image workflow tutorial")`
-- `youtube_detail(content_id, include_captions=True)`: 获取 YouTube 视频详情和字幕
-  - 示例:`youtube_detail("视频ID", include_captions=True)`
-- `x_search(keyword)`: 搜索 X (Twitter) 内容
-  - 示例:`x_search("AI workflow comfyui")`
-- `browser-use`: 浏览器搜索(search_posts 不好用时使用)
-
-### 接口失败处理策略
-如果搜索接口失败或返回为空:检查参数 -> 重试2次 -> 若仍失败,在JSON中标注 `"渠道状态": "接口失败"` 并返回空数据。
-
-## 执行流程
-
-### 第一步:广度扫描(发现工序)
-1. **初始搜索**:使用任务提供的关键词,每个关键词搜索 20 条结果。
-2. **适度查看内容**:对点赞数高或标题符合业务需求的帖子查看详情(可看图片)。
-3. **提取工序信息**:
-   - 重点识别:这是一套怎样的完整工作流?
-   - **严格拆解步骤**:必须提取每一步的 `工具`、`用户输入(Prompt/参数/参考图)`、`输出结果(阶段性产物)`。
-
-### 第二步:反思与补充搜索
-每完成一轮关键词搜索后,主动评估:
-- 已发现的工序是否覆盖了主要方案?
-- 是否有明显遗漏的渠道热点或关键词变体?
-- 如有不足,调整关键词后补充搜索 1-2 轮,直到结果开始重复为止。
-
-## 输出格式(必须使用 `write_file` 写入指定路径)
-```jsonschema
-{
-  "渠道名称": "string - 小红书/X/YouTube等",
-  "初始关键词": ["string"],
-  "采集时间": "string - ISO 8601",
-  "渠道状态": "string - 正常/接口失败",
-  "工序发现": [
-    {
-      "方案名称": "string - 如 Midjourney + ControlNet + 局部重绘工作流",
-      "最新提及时间": "string",
-      "工序步骤": [
-        {
-          "步骤序号": "number - 如 1, 2, 3",
-          "步骤描述": "string - 该步骤的核心目的",
-          "使用工具": "string - 该步骤使用的具体工具/模型/节点",
-          "用户输入": "string - 用户的真实输入(如:特定风格的Prompt、垫图、Lora权重等)",
-          "输出结果": "string - 该步骤生成的阶段性产物或最终图片效果"
-        }
-      ],
-      "帖子链接": ["string"]
-    }
-  ],
-  "调研轨迹": {
-    "搜索次数": "number",
-    "查看详情数": "number",
-    "发现工序数": "number"
-  }
-}
-```
-
-$user$
-%task%

+ 0 - 78
examples/process/prompts/tool_research.prompt

@@ -1,78 +0,0 @@
----
-temperature: 0.3
----
-
-$system$
-
-## 角色
-你是一个工序调研协调器。你负责制定调研策略、分发广度采集任务、交叉验证数据,并输出结构化的工序(Workflow)调研报告。
-
-**核心目标**:
-寻找并汇总能解决特定生图需求的**多步工序(Workflow)**。必须结构化记录每个工序的执行步骤(Step、输入、输出、工具)。
-
-## 可用工具
-- `agent`: 调用 research 子 agent 执行渠道采集
-- `write_file`: 输出调研报告和结构化数据
-
-## 工作流程:广度扫描与汇总
-
-### 第一步:理解需求并构造搜索关键词
-- 将用户的业务能力需求,转换为**AI生图工序/工作流**的搜索词。
-- 关键词应偏向寻找"教程"、"工作流"、"全套方案",而非单一软件名。
-- 准备 3-5 个关键词。
-
-### 第二步:并行调用渠道调研 agent
-默认使用小红书、X、youtube 这三个渠道进行调研。为每个渠道启动一个独立的 research 子 agent。
-
-**任务描述规范示例**:
-```
-[小红书调研] 使用以下关键词进行广度扫描:[关键词1, 关键词2, 关键词3]。
-要求:
-- 每个关键词搜索 20 条结果。
-- 只提取多步工序(Workflow),必须严格拆解每个工序的步骤(包含输入、输出、工具)。
-- 输出路径:%output_dir%/<渠道名>.json
-```
-
-每个子 agent 会将自己的采集结果写入对应的渠道 JSON 文件。
-
-### 第三步:汇总并输出结构化数据
-
-等所有渠道子 agent 完成后,将三个渠道的工序发现合并,执行以下两个输出:
-
-**输出 1:可读报告**(Markdown 格式)
-用 `write_file` 写入 `%output_dir%/report.md`,内容包括:
-- 调研概况表格(渠道、搜索词数、获取结果数、提取工序数)
-- 每个工序的详细步骤拆解(表格形式)
-- 核心工具汇总
-
-**输出 2:结构化工序数据**(JSON 格式,供后续分析使用)
-用 `write_file` 写入 `%output_dir%/workflows.json`,格式如下:
-
-```json
-{
-  "需求": "用户的原始需求描述",
-  "工序发现": [
-    {
-      "方案名称": "方案的简短名称,如 ComfyUI + ControlNet 工作流",
-      "来源渠道": "小红书/X/YouTube",
-      "工序步骤": [
-        {
-          "步骤序号": 1,
-          "步骤描述": "该步骤的核心目的",
-          "使用工具": "该步骤使用的具体工具/模型/节点",
-          "用户输入": "用户的真实输入(如:特定风格的Prompt、垫图、LoRA权重等)",
-          "输出结果": "该步骤生成的阶段性产物或最终图片效果"
-        }
-      ],
-      "帖子链接": ["https://..."]
-    }
-  ]
-}
-```
-
-**重要**:`workflows.json` 是必须输出的,不可省略。即使某些工序步骤信息不完整,也要尽量填充已知字段。
-
-$user$
-请开始工作:调研 %requirement%
-
-输出目录:%output_dir%/

+ 0 - 62
examples/process/research.prompt

@@ -1,62 +0,0 @@
----
-model: qwen3.5-plus
-temperature: 0.3
----
-
-$system$
-## 角色
-你是一个专注的渠道调研专家。你负责在指定的单个渠道(如小红书、X、youtube)进行完整的广度调研,包括多关键词搜索、适度查看内容,并输出结构化的【工序(工作流)】调研结果。
-
-## 核心原则
-1. **渠道专注**:你只负责一个渠道的完整调研,绝不跨渠道。
-2. **唯工序论**:本调研**只关注能解决问题的多步工序(Workflow/组合方案)**,不搜集单一零散工具。
-3. **结构化提取**:必须将工序严格拆解为按序执行的步骤,并明确每个步骤的工具、输入与输出。
-4. **相关性过滤**:只记录与调研目标相关的**图片生成**工序(排除视频、音频及非AI桌面软件如PS)。
-
-## 可用工具
-### 内容搜索工具
-- `search_posts(keyword, channel, cursor="0", max_count=20)`: 搜索帖子(channel参数必须用双引号小写,如 `"xhs"`)
-- `select_post(index)`: 查看帖子详情
-- `youtube_search(keyword)` / `youtube_detail(content_id)`: 搜索/查看 YouTube
-- `x_search(keyword)`: 搜索 X (Twitter) 内容
-
-### 接口失败处理策略
-如果搜索接口失败或返回为空:检查参数 -> 重试2次 -> 若仍失败,在JSON中标注 `"渠道状态": "接口失败"` 并返回空数据。
-
-## 执行流程:广度扫描(发现工序)
-1. **初始搜索**:使用任务提供的关键词,每个关键词搜索 20 条结果。
-2. **适度查看内容**:对点赞数高或标题符合业务需求的帖子查看详情(可看图片)。
-3. **提取工序信息**:
-   - 重点识别:这是一套怎样的完整工作流?
-   - **严格拆解步骤**:必须提取每一步的 `工具`、`用户输入(Prompt/参数/参考图)`、`输出结果(阶段性产物)`。
-
-第二部结构化输出:
-
-## 输出格式(必须使用 `write_file` 写入指定路径)
-```jsonschema
-{
-  "渠道名称": "string - 小红书/X/公众号等",
-  "初始关键词": ["string"],
-  "采集时间": "string - ISO 8601",
-  "渠道状态": "string - 正常/接口失败",
-  "工序发现": [
-    {
-      "方案名称": "string - 如 Midjourney + ControlNet + 局部重绘工作流",
-      "最新提及时间": "string",
-      "工序步骤": [
-        {
-          "步骤序号": "number - 如 1, 2, 3",
-          "步骤描述": "string - 该步骤的核心目的",
-          "使用工具": "string - 该步骤使用的具体工具/模型/节点",
-          "用户输入": "string - 用户的真实输入(如:特定风格的Prompt、垫图、Lora权重等)",
-          "输出结果": "string - 该步骤生成的阶段性产物或最终图片效果"
-        }
-      ],
-      "帖子链接": {"string"}
-  ]
-  "调研轨迹": {
-    "搜索次数": "number",
-    "查看详情数": "number",
-    "发现工序数": "number"
-  }
-}

+ 0 - 546
examples/process/run.py

@@ -1,546 +0,0 @@
-"""
-两阶段 Pipeline:工具调研 + 工作流分析
-
-Stage 1:批量调研(qwen3.5-plus),每个需求输出到 output/research/NN/
-Stage 2:工作流分析(claude-sonnet),读取 Stage 1 输出,生成 output/analysis/result.json
-
-用法:
-  python run.py                        # 完整两阶段(默认)
-  python run.py --stage research       # 只跑调研
-  python run.py --stage analysis       # 只跑分析(用已有调研结果)
-  python run.py --stage research --from 2  # 从第3个需求续跑
-"""
-
-import argparse
-import json
-import os
-import sys
-import asyncio
-from pathlib import Path
-
-
-sys.path.insert(0, str(Path(__file__).parent.parent.parent))
-
-from dotenv import load_dotenv
-load_dotenv()
-
-from agent.llm.prompts import SimplePrompt
-from agent.core.runner import AgentRunner, RunConfig
-from agent.trace import FileSystemTraceStore, Trace, Message
-from agent.llm import create_qwen_llm_call, create_openrouter_llm_call
-from agent.cli import InteractiveController
-from agent.utils import setup_logging
-
-from config import (
-    RESEARCH_RUN_CONFIG, ANALYSIS_RUN_CONFIG,
-    RESEARCH_OUTPUT_DIR, ANALYSIS_OUTPUT_DIR,
-    SKILLS_DIR, TRACE_STORE_PATH, DEBUG, LOG_LEVEL, LOG_FILE,
-    IM_ENABLED, IM_CONTACT_ID, IM_SERVER_URL, IM_WINDOW_MODE, IM_NOTIFY_INTERVAL,
-)
-
-
-# ─────────────────────────────────────────────
-# Stage 1 helpers
-# ─────────────────────────────────────────────
-
-async def run_single(
-    runner: AgentRunner,
-    interactive: InteractiveController,
-    store: FileSystemTraceStore,
-    prompt: SimplePrompt,
-    requirement: str,
-    output_dir: Path,
-    task_name: str,
-    req_index: int,
-) -> tuple[str, bool]:
-    """执行单个需求的完整调研流程,返回 (最终响应文本, 是否应退出)。"""
-
-    output_dir.mkdir(parents=True, exist_ok=True)
-
-    messages = prompt.build_messages(
-        requirement=requirement,
-        output_dir=str(output_dir),
-    )
-
-    prompt_model = prompt.config.get("model", None)
-    run_config = RunConfig(
-        model=prompt_model or RESEARCH_RUN_CONFIG.model,
-        temperature=RESEARCH_RUN_CONFIG.temperature,
-        max_iterations=RESEARCH_RUN_CONFIG.max_iterations,
-        extra_llm_params=RESEARCH_RUN_CONFIG.extra_llm_params,
-        agent_type=RESEARCH_RUN_CONFIG.agent_type,
-        name=f"{task_name}:需求{req_index:02d}",
-        knowledge=RESEARCH_RUN_CONFIG.knowledge,
-    )
-
-    print(f"\n{'=' * 60}")
-    print(f"[{req_index:02d}] 开始调研")
-    print(f"需求:{requirement[:80]}{'...' if len(requirement) > 80 else ''}")
-    print(f"输出:{output_dir}")
-    print(f"{'=' * 60}")
-
-    current_trace_id = None
-    current_sequence = 0
-    final_response = ""
-    should_exit = False
-
-    try:
-        async for item in runner.run(messages=messages, config=run_config):
-            cmd = interactive.check_stdin()
-            if cmd == 'pause':
-                print("\n⏸️ 正在暂停执行...")
-                if current_trace_id:
-                    await runner.stop(current_trace_id)
-                await asyncio.sleep(0.5)
-                menu_result = await interactive.show_menu(current_trace_id, current_sequence)
-                if menu_result["action"] == "stop":
-                    should_exit = True
-                    break
-                elif menu_result["action"] == "continue":
-                    new_messages = menu_result.get("messages", [])
-                    run_config.after_sequence = menu_result.get("after_sequence")
-                    if new_messages:
-                        messages = new_messages
-                    break
-            elif cmd == 'quit':
-                print("\n🛑 用户请求停止...")
-                if current_trace_id:
-                    await runner.stop(current_trace_id)
-                should_exit = True
-                break
-
-            if isinstance(item, Trace):
-                current_trace_id = item.trace_id
-                if item.status == "running":
-                    print(f"[Trace] 开始: {item.trace_id[:8]}...")
-                elif item.status == "completed":
-                    print(f"\n[Trace] ✅ 完成  messages={item.total_messages}  cost=${item.total_cost:.4f}")
-                elif item.status == "failed":
-                    print(f"\n[Trace] ❌ 失败: {item.error_message}")
-                elif item.status == "stopped":
-                    print(f"\n[Trace] ⏸️ 已停止")
-
-            elif isinstance(item, Message):
-                current_sequence = item.sequence
-                if item.role == "assistant":
-                    content = item.content
-                    if isinstance(content, dict):
-                        text = content.get("text", "")
-                        tool_calls = content.get("tool_calls")
-                        if text and not tool_calls:
-                            final_response = text
-                            print(f"\n[Response] Agent 回复:")
-                            print(text)
-                        elif text:
-                            preview = text[:150] + "..." if len(text) > 150 else text
-                            print(f"[Assistant] {preview}")
-                elif item.role == "tool":
-                    content = item.content
-                    tool_name = "unknown"
-                    if isinstance(content, dict):
-                        tool_name = content.get("tool_name", "unknown")
-                    if item.description and item.description != tool_name:
-                        desc = item.description[:80] if len(item.description) > 80 else item.description
-                        print(f"[Tool Result] ✅ {tool_name}: {desc}...")
-                    else:
-                        print(f"[Tool Result] ✅ {tool_name}")
-
-    except Exception as e:
-        print(f"\n执行出错: {e}")
-        import traceback
-        traceback.print_exc()
-
-    if final_response:
-        output_file = output_dir / "result.txt"
-        with open(output_file, 'w', encoding='utf-8') as f:
-            f.write(final_response)
-        print(f"\n✓ 结果已保存到: {output_file}")
-
-    if current_trace_id:
-        print(f"  Trace ID: {current_trace_id}")
-
-    return final_response, should_exit
-
-
-# ─────────────────────────────────────────────
-# Stage 2 helpers
-# ─────────────────────────────────────────────
-
-def load_workflows_from_dir(research_dir: Path) -> list[dict]:
-    """
-    扫描 research_dir 下所有子目录(00/, 01/ ...),合并工序发现列表。
-
-    优先读取 workflows.json(Stage 1 新格式);
-    若不存在则把目录内 *.md 文件内容作为文本传给 coordinator(兜底)。
-    """
-    workflows = []
-    wf_index = 1
-
-    subdirs = sorted(
-        [d for d in research_dir.iterdir() if d.is_dir()],
-        key=lambda d: d.name,
-    )
-
-    if not subdirs:
-        # 单次调研输出(直接含 JSON 文件)
-        subdirs = [research_dir]
-
-    for subdir in subdirs:
-        workflows_json_path = subdir / "workflows.json"
-
-        # ── 优先:读取 workflows.json ──
-        if workflows_json_path.exists():
-            try:
-                with open(workflows_json_path, encoding='utf-8') as f:
-                    data = json.load(f)
-                discovered = data.get("工序发现", [])
-                for item in discovered:
-                    wf_id = f"wf_{wf_index:03d}"
-                    wf_index += 1
-                    workflows.append({
-                        "id": wf_id,
-                        "name": item.get("方案名称", "未命名工序"),
-                        "category": "",
-                        "source_channel": item.get("来源渠道", "未知"),
-                        "source_file": str(workflows_json_path.relative_to(research_dir)),
-                        "steps": item.get("工序步骤", []),
-                        "post_links": list(item.get("帖子链接", [])),
-                    })
-                    print(f"   + {wf_id}: {item.get('方案名称', '未命名')[:50]}")
-                continue
-            except (json.JSONDecodeError, IOError) as e:
-                print(f"   [警告] workflows.json 解析失败: {subdir.name} ({e}),尝试 Markdown 兜底")
-
-        # ── 兜底:读取 *.md 文件内容 ──
-        md_files = sorted(subdir.glob("*.md"))
-        if md_files:
-            for md_file in md_files:
-                try:
-                    content = md_file.read_text(encoding='utf-8')
-                    wf_id = f"wf_{wf_index:03d}"
-                    wf_index += 1
-                    workflows.append({
-                        "id": wf_id,
-                        "name": md_file.stem,
-                        "category": "",
-                        "source_channel": "Markdown报告",
-                        "source_file": str(md_file.relative_to(research_dir)),
-                        "steps": [],
-                        "raw_markdown": content,  # coordinator 可直接阅读
-                    })
-                    print(f"   + {wf_id}: [MD兜底] {md_file.name}")
-                except IOError as e:
-                    print(f"   [警告] 无法读取 {md_file.name}: {e}")
-        else:
-            print(f"   [跳过] {subdir.name}:无 workflows.json 也无 .md 文件")
-
-    return workflows
-
-
-async def fetch_atomic_capabilities() -> list[dict]:
-    """从 knowhub API 获取全量原子能力表。"""
-    import urllib.request
-    knowhub_api = os.getenv("KNOWHUB_API", "http://43.106.118.91:9999")
-    url = f"{knowhub_api}/api/capability?limit=500"
-    try:
-        with urllib.request.urlopen(url, timeout=10) as resp:
-            data = json.loads(resp.read().decode())
-        capabilities = data.get("results", [])
-        print(f"   已获取原子能力表:{len(capabilities)} 条")
-        return capabilities
-    except Exception as e:
-        print(f"   [警告] 获取原子能力表失败:{e},将跳过匹配")
-        return []
-
-
-async def run_analysis(
-    research_dir: Path,
-    analysis_dir: Path,
-    store: FileSystemTraceStore,
-    prompt_path: Path,
-) -> bool:
-    """执行 Stage 2 分析,返回是否成功。"""
-
-    print(f"\n{'=' * 60}")
-    print("Stage 2:工作流分析")
-    print(f"输入:{research_dir}")
-    print(f"输出:{analysis_dir}")
-    print(f"{'=' * 60}")
-
-    # 扫描工作流数据
-    print("扫描调研结果...")
-    workflows = load_workflows_from_dir(research_dir)
-    if not workflows:
-        print("   错误: 未找到任何工序数据,请先运行 Stage 1")
-        return False
-    print(f"   共加载 {len(workflows)} 条工作流")
-
-    analysis_dir.mkdir(parents=True, exist_ok=True)
-
-    # 获取原子能力表并写入文件
-    print("获取原子能力表...")
-    atomic_capabilities = await fetch_atomic_capabilities()
-    atomic_capabilities_path = analysis_dir / "atomic_capabilities.json"
-    atomic_capabilities_path.write_text(
-        json.dumps({"atomic_capabilities": atomic_capabilities}, ensure_ascii=False, indent=2),
-        encoding='utf-8'
-    )
-    print(f"   已写入:{atomic_capabilities_path}")
-    output_path = analysis_dir / "result.json"
-
-    # 加载 coordinator prompt
-    prompt = SimplePrompt(prompt_path)
-    workflows_json = json.dumps({"workflows": workflows}, ensure_ascii=False, indent=2)
-    messages = prompt.build_messages(
-        workflows_json=workflows_json,
-        output_dir=str(analysis_dir),
-        output_path=str(output_path),
-    )
-
-    # 创建 Runner(OpenRouter / Claude)
-    prompt_model = prompt.config.get("model", None) or ANALYSIS_RUN_CONFIG.model
-    print(f"   模型: {prompt_model}")
-
-    runner = AgentRunner(
-        trace_store=store,
-        llm_call=create_openrouter_llm_call(model=prompt_model),
-        skills_dir=SKILLS_DIR,
-        debug=DEBUG,
-    )
-    interactive = InteractiveController(runner=runner, store=store, enable_stdin_check=True)
-    runner.stdin_check = interactive.check_stdin
-
-    run_config = RunConfig(
-        model=prompt_model,
-        temperature=ANALYSIS_RUN_CONFIG.temperature,
-        max_iterations=ANALYSIS_RUN_CONFIG.max_iterations,
-        agent_type=ANALYSIS_RUN_CONFIG.agent_type,
-        name=f"工作流分析:{len(workflows)} 条工作流",
-    )
-
-    current_trace_id = None
-    current_sequence = 0
-
-    try:
-        async for item in runner.run(messages=messages, config=run_config):
-            cmd = interactive.check_stdin()
-            if cmd == 'pause':
-                print("\n⏸️ 正在暂停...")
-                if current_trace_id:
-                    await runner.stop(current_trace_id)
-                await asyncio.sleep(0.5)
-                menu_result = await interactive.show_menu(current_trace_id, current_sequence)
-                if menu_result["action"] == "stop":
-                    break
-                elif menu_result["action"] == "continue":
-                    new_messages = menu_result.get("messages", [])
-                    run_config.after_sequence = menu_result.get("after_sequence")
-                    if new_messages:
-                        messages = new_messages
-                    break
-            elif cmd == 'quit':
-                print("\n🛑 停止执行...")
-                if current_trace_id:
-                    await runner.stop(current_trace_id)
-                break
-
-            if isinstance(item, Trace):
-                current_trace_id = item.trace_id
-                if item.status == "running":
-                    print(f"[Trace] 开始: {item.trace_id[:8]}...")
-                elif item.status == "completed":
-                    print(f"\n[Trace] ✅ 完成  messages={item.total_messages}  cost=${item.total_cost:.4f}")
-                elif item.status == "failed":
-                    print(f"\n[Trace] ❌ 失败: {item.error_message}")
-                elif item.status == "stopped":
-                    print(f"\n[Trace] ⏸️ 已停止")
-
-            elif isinstance(item, Message):
-                current_sequence = item.sequence
-                if item.role == "assistant":
-                    content = item.content
-                    if isinstance(content, dict):
-                        text = content.get("text", "")
-                        tool_calls = content.get("tool_calls")
-                        if text and not tool_calls:
-                            print(f"\n[Response]\n{text}")
-                        elif text:
-                            preview = text[:150] + "..." if len(text) > 150 else text
-                            print(f"[Assistant] {preview}")
-                elif item.role == "tool":
-                    content = item.content
-                    tool_name = "unknown"
-                    if isinstance(content, dict):
-                        tool_name = content.get("tool_name", "unknown")
-                    if item.description and item.description != tool_name:
-                        desc = item.description[:80] if len(item.description) > 80 else item.description
-                        print(f"[Tool] ✅ {tool_name}: {desc}...")
-                    else:
-                        print(f"[Tool] ✅ {tool_name}")
-
-    except Exception as e:
-        print(f"\n执行出错: {e}")
-        import traceback
-        traceback.print_exc()
-    except KeyboardInterrupt:
-        print("\n\n用户中断 (Ctrl+C)")
-        if current_trace_id:
-            await runner.stop(current_trace_id)
-
-    # 结果摘要
-    print()
-    print("=" * 60)
-    if output_path.exists():
-        print(f"✅ 分析完成,结果已写入:{output_path}")
-        try:
-            with open(output_path, encoding='utf-8') as f:
-                result = json.load(f)
-            n_modules = len(result.get("capability_modules", []))
-            n_coarse = len(result.get("coarse_workflows", []))
-            print(f"   - 能力模块(细工序):{n_modules} 个")
-            print(f"   - 粗工序:{n_coarse} 个品类")
-        except Exception:
-            pass
-        return True
-    else:
-        print("⚠️  未检测到最终输出文件,分析可能未完成")
-        print(f"   期望路径:{output_path}")
-        return False
-
-
-# ─────────────────────────────────────────────
-# Main
-# ─────────────────────────────────────────────
-
-async def main():
-    parser = argparse.ArgumentParser(description="两阶段 Pipeline:工具调研 + 工作流分析")
-    parser.add_argument(
-        "--stage", choices=["research", "analysis", "all"], default="all",
-        help="执行阶段:research=只调研, analysis=只分析, all=完整流程(默认)",
-    )
-    parser.add_argument(
-        "--from", dest="from_index", type=int, default=0,
-        help="从第几个需求开始(0-based,仅 stage=research/all 时有效)",
-    )
-    parser.add_argument(
-        "--requirements", type=str, default=None,
-        help="需求列表 JSON 文件路径(默认 requirements.json)",
-    )
-    args = parser.parse_args()
-
-    base_dir = Path(__file__).parent
-    project_root = base_dir.parent.parent
-    research_output_dir = project_root / RESEARCH_OUTPUT_DIR
-    analysis_output_dir = project_root / ANALYSIS_OUTPUT_DIR
-
-    setup_logging(level=LOG_LEVEL, file=LOG_FILE)
-
-    # 加载 presets
-    presets_path = base_dir / "presets.json"
-    if presets_path.exists():
-        from agent.core.presets import load_presets_from_json
-        load_presets_from_json(str(presets_path))
-        print("已加载 presets")
-
-    store = FileSystemTraceStore(base_path=TRACE_STORE_PATH)
-
-    # ── Stage 1: Research ──
-    if args.stage in ("all", "research"):
-        req_path = Path(args.requirements) if args.requirements else base_dir / "requirements.json"
-        if not req_path.exists():
-            print(f"错误: 需求文件不存在: {req_path}")
-            sys.exit(1)
-        with open(req_path, encoding='utf-8') as f:
-            requirements = json.load(f)
-        if not isinstance(requirements, list) or len(requirements) == 0:
-            print("错误: 需求文件必须是非空 JSON 数组")
-            sys.exit(1)
-
-        research_output_dir.mkdir(parents=True, exist_ok=True)
-        prompt_path = base_dir / "prompts" / "tool_research.prompt"
-        prompt = SimplePrompt(prompt_path)
-
-        # IM 初始化(可选)
-        if IM_ENABLED:
-            from agent.tools.builtin.im.chat import im_setup, im_open_window
-            result = await im_setup(
-                contact_id=IM_CONTACT_ID,
-                server_url=IM_SERVER_URL,
-                notify_interval=IM_NOTIFY_INTERVAL,
-            )
-            print(f"IM: {result.output}")
-            if IM_WINDOW_MODE:
-                window_result = await im_open_window(contact_id=IM_CONTACT_ID)
-                print(f"IM: {window_result.output}")
-
-        prompt_model = prompt.config.get("model", None) or RESEARCH_RUN_CONFIG.model
-        runner = AgentRunner(
-            trace_store=store,
-            llm_call=create_qwen_llm_call(model=prompt_model),
-            skills_dir=SKILLS_DIR,
-            debug=DEBUG,
-        )
-        interactive = InteractiveController(runner=runner, store=store, enable_stdin_check=True)
-        runner.stdin_check = interactive.check_stdin
-
-        task_name = RESEARCH_RUN_CONFIG.name or base_dir.name
-        total = len(requirements)
-        start = args.from_index
-
-        print("=" * 60)
-        print(f"Stage 1:{task_name}")
-        print(f"共 {total} 个需求,从第 {start} 个开始")
-        print("=" * 60)
-        print("💡 输入 'p' 暂停,'q' 退出")
-        print("=" * 60)
-
-        completed = 0
-        try:
-            for i, requirement in enumerate(requirements):
-                if i < start:
-                    continue
-                req_output_dir = research_output_dir / f"{i:02d}"
-                _, should_exit = await run_single(
-                    runner=runner,
-                    interactive=interactive,
-                    store=store,
-                    prompt=prompt,
-                    requirement=requirement,
-                    output_dir=req_output_dir,
-                    task_name=task_name,
-                    req_index=i,
-                )
-                completed += 1
-                if should_exit:
-                    print(f"\n🛑 用户中止,已完成 {completed}/{total - start} 个需求")
-                    break
-        except KeyboardInterrupt:
-            print(f"\n\n用户中断 (Ctrl+C),已完成 {completed}/{total - start} 个需求")
-
-        print()
-        print("=" * 60)
-        print(f"Stage 1 完成:{completed}/{total - start} 个需求")
-        print(f"输出根目录:{research_output_dir}")
-        print("=" * 60)
-
-        if args.stage == "all":
-            # 统计已采集工作流数量(粗略)
-            wf_count = sum(
-                1 for d in research_output_dir.iterdir()
-                if d.is_dir() and (d / "workflows.json").exists()
-            )
-            print(f"\n[Stage 1 完成] 共 {wf_count} 个目录含 workflows.json,自动进入 Stage 2 分析...")
-
-    # ── Stage 2: Analysis ──
-    if args.stage in ("all", "analysis"):
-        coordinator_prompt_path = base_dir / "prompts" / "coordinator.prompt"
-        await run_analysis(
-            research_dir=research_output_dir,
-            analysis_dir=analysis_output_dir,
-            store=store,
-            prompt_path=coordinator_prompt_path,
-        )
-
-
-if __name__ == "__main__":
-    asyncio.run(main())

+ 0 - 37
examples/process/tool_research.prompt

@@ -1,37 +0,0 @@
----
-temperature: 0.3
----
-
-$system$
-
-## 角色
-你是一个工序调研协调器。你负责制定调研策略、分发广度采集任务、交叉验证数据,并输出结构化的工序(Workflow)调研报告。
-
-**核心目标**:
-寻找并汇总能解决特定生图需求的**多步工序(Workflow)**。必须结构化记录每个工序的执行步骤(Step、输入、输出、工具)。
-
-## 可用工具
-- `agent`: 调用 research 子 agent 执行渠道采集
-- `write_file`: 输出调研报告
-
-## 工作流程:广度扫描与汇总
-
-### 第一步:理解需求并构造搜索关键词
-- 将用户的业务能力需求,转换为**AI生图工序/工作流**的搜索词。
-- 关键词应偏向寻找“教程”、“工作流”、“全套方案”,而非单一软件名。
-- 准备 3-5 个关键词。
-
-### 第二步:并行调用渠道调研 agent
-默认使用小红书、X、youtube 这三个渠道进行调研。为每个渠道启动一个独立的 research 子 agent。
-
-**任务描述规范示例**:
-```text
-[小红书调研] 使用以下关键词进行广度扫描:[关键词1, 关键词2, 关键词3]。
-要求:
-- 每个关键词搜索 20 条结果。
-- 只提取多步工序(Workflow),必须严格拆解每个工序的步骤(包含输入、输出、工具)。
-
-$user$
-请开始工作:调研 %requirement%
-
-输出目录:%output_dir%/

+ 93 - 0
examples/process_pipeline/batch.py

@@ -0,0 +1,93 @@
+import argparse
+import subprocess
+import os
+from pathlib import Path
+
+def main():
+    parser = argparse.ArgumentParser(description="Spawn multi-window batch runner")
+    parser.add_argument("--start", type=int, required=True, help="Start requirement (1-based, inclusive, e.g. 31)")
+    parser.add_argument("--end", type=int, required=True, help="End requirement (1-based, inclusive, e.g. 50)")
+    parser.add_argument("--workers", type=int, default=7, help="Number of physical CMD windows to spawn")
+    parser.add_argument("--skip-research", action="store_true", help="Skip Phase 1 and use existing raw cases")
+    parser.add_argument("--research-only", action="store_true", help="Only run research phases, skip Phase 2 and 3")
+    parser.add_argument("--use-claude-sdk", action="store_true", help="Use pure Anthropic SDK for Phase 2/3")
+    parser.add_argument("--platforms", type=str, default="", help="Comma-separated list of platforms to search")
+    args = parser.parse_args()
+
+    start_idx = args.start - 1
+    end_idx = args.end - 1
+    total_tasks = end_idx - start_idx + 1
+    
+    if total_tasks <= 0:
+        print("Invalid range")
+        return
+
+    # Assign indices to workers using round-robin
+    worker_tasks = {i: [] for i in range(args.workers)}
+    for i, req_idx in enumerate(range(start_idx, end_idx + 1)):
+        worker_id = i % args.workers
+        worker_tasks[worker_id].append(req_idx)
+        
+    base_dir = Path(__file__).parent
+    
+    print(f"Spawning {args.workers} actual CMD windows to process {total_tasks} requirements...")
+
+    for worker_id, indices in worker_tasks.items():
+        if not indices:
+            continue
+            
+        # Create a temporary batch file for this worker window
+        bat_path = base_dir / f"worker_{worker_id}.bat"
+        with open(bat_path, "w", encoding="utf-8") as f:
+            f.write("@echo off\n")
+            f.write("cd /d %~dp0\n")
+            f.write("if exist ..\\..\\.venv\\Scripts\\activate.bat call ..\\..\\.venv\\Scripts\\activate.bat\n")
+            f.write("echo Worker Group %d starting...\n" % worker_id)
+            for idx in indices:
+                f.write(f"echo =======================================\n")
+                f.write(f"echo Running Requirement Index: {idx}\n")
+                f.write(f"echo =======================================\n")
+                
+                cmd = f"python run_pipeline.py --index {idx}"
+                if args.skip_research:
+                    cmd += " --skip-research"
+                if args.research_only:
+                    cmd += " --research-only"
+                if args.use_claude_sdk:
+                    cmd += " --use-claude-sdk"
+                if args.platforms:
+                    cmd += f" --platforms {args.platforms}"
+                
+                # 第 1 遍执行
+                f.write(f"echo [Attempt 1] {cmd}\n")
+                f.write(f"{cmd}\n")
+                
+                # 第 2 遍执行(检查重试)
+                if not args.research_only:
+                    target_file = f"output\\{(idx+1):03d}\\strategy.json"
+                    f.write(f"if not exist \"{target_file}\" (\n")
+                    f.write(f"    echo =======================================\n")
+                    f.write(f"    echo [Retry] target.json missing, retrying once...\n")
+                    f.write(f"    echo =======================================\n")
+                    f.write(f"    {cmd}\n")
+                    f.write(f") else (\n")
+                    f.write(f"    echo [Verified] {target_file} generated successfully.\n")
+                    f.write(f")\n")
+                else:
+                    # 如果是 resaerch-only 则较难在 batch 中校验几个文件,默认让它安全重试一遍
+                    f.write(f"echo [Retry For Research] Automatically trying once more to ensure completeness...\n")
+                    f.write(f"{cmd}\n")
+                
+                f.write("timeout /t 2 > NUL\n")
+            f.write("echo Worker Group %d finished!\n" % worker_id)
+            f.write("pause\n")
+            
+        # Open a new CMD window executing this batch file
+        subprocess.Popen(
+            ["cmd.exe", "/c", "start", f"Worker_{worker_id}", "cmd.exe", "/c", str(bat_path.absolute())],
+            cwd=str(base_dir)
+        )
+        print(f"Launched window for Worker {worker_id} -> Indices: {indices}")
+
+if __name__ == "__main__":
+    main()

+ 101 - 0
examples/process_pipeline/db_requirements.json

@@ -0,0 +1,101 @@
+[
+  "生成人物在不同场景下呈现丰富面部表情的图片,例如夸张的痛苦、无奈、开心、困倦等神态,表情要生动传神、情绪感强烈",
+  "生成人物与道具、环境或其他角色发生互动的画面,例如人物摆弄物品、与道具合影、在特定场景中做出配合动作等,画面要体现人物和周围元素之间的关联感",
+  "生成将动物(如猫咪)拟人化扮演特定角色或情境的图片,赋予其人类的表情、姿态和道具,用来传达幽默或情感共鸣的视觉效果",
+  "生成带有特定道具装扮的人物场景图,道具需与人物自然融合,例如猫咪戴假发穿衣服手持书本、人物手持购物篮抱着玩偶玩具等,道具细节清晰可辨",
+  "生成婚礼或节日庆典场景,背景需包含大量花卉装饰、定制发光字牌、喜字等布景元素,整体氛围感强烈,道具与场景协调统一",
+  "生成精致室内空间场景,画面中需呈现陶瓷器皿、绿植、家具等道具摆件,光线自然柔和,营造出温馨生活感或高颜值家居氛围",
+  "生成真实人物在户外或特定场景中的生活记录照片,画面自然真实,包含儿童在公园、农场等户外环境中玩耍的多角度抓拍效果,光线自然,氛围温馨",
+  "制作将真实人物照片合成到趣味场景中的创意图片,例如把人物缩小放入超市肉类托盘包装内、或与冰雕翅膀等道具结合形成视觉错位的幽默效果",
+  "生成真实场景的多图拼贴展示图,将同一地点或主题的多张实拍照片拼合为一张图文并茂的内容图,适合用于地点打卡、产品展示或生活记录类帖子",
+  "制作多格宫格式信息图,将同类内容(如多种食材搭配方案)拆分为统一风格的小卡片,每格包含标题、食材图片和文字说明,整体排列整齐、色块鲜明,适合一图展示多个并列条目",
+  "在图片上叠加标注元素,如用红色圆点、箭头或emoji符号指向图中特定位置,配合说明文字,实现在真实照片上直观标记关键信息的视觉效果",
+  "制作图文混排的长图文内容,将大段文字与人物照片、数据表格、流程图等多种视觉元素组合排布在同一版面中,形成类似杂志或报告的专业排版风格",
+  "生成以暖黄/米棕色为背景底色的图文排版内容,整体画面呈现温暖、复古的暖色调氛围,适合健康养生、生活方式类主题",
+  "生成以深色(黑色/深蓝/深紫)为背景底色的海报,搭配霓虹感彩色光效(橙、紫、青等),营造出科技感强烈的冷暖对比配色效果",
+  "生成整体色调偏粉紫、薄荷绿、浅蓝等低饱和度冷色系的插画或场景图,画面呈现出梦幻、静谧的冷色调氛围,颜色搭配柔和克制",
+  "生成手部持握或展示物品的特写画面,手势自然,物品清晰呈现,如用手托举饺子、手持卡片、手握手机等,突出手与物品的互动关系",
+  "生成多人或多只动物同框的协同姿态画面,如两只猫并排躺着穿睡衣、两人一起摆造型抱花束,呈现出同步、对称或互动的视觉效果",
+  "生成创意性的嘴部动作特写,如用嘴唇衔住花朵茎部形成'嘴唇花'的视觉效果,将身体部位与物品结合产生趣味创意画面",
+  "将人物照片与中国传统吉祥符号(如双喜字、红玫瑰、金色祝福文字)融合,生成具有强烈喜庆氛围的定制化图案,人物面孔清晰嵌入红色喜庆背景中",
+  "制作统一模板风格的系列信息卡片,每张卡片包含固定的图标符号(如皇冠等级图标)、彩色标题文字和配图,整体视觉风格一致、可批量复用",
+  "生成黑色科技感背景的人物宣传海报,背景带有流光线条或霓虹光效,人物照片与品牌Logo、活动标识、二维码等视觉元素整齐排布,形成高辨识度的系列展示图",
+  "生成宠物或动物穿戴人类服饰配件(如帽子、围巾)的画面,让动物看起来像在过节或扮演某种角色,整体效果可爱又有趣",
+  "生成将普通服装或日常物品以夸张搞怪方式穿戴的人物画面,比如把超大号短裤当长袍穿、用篮球短裤模仿古希腊长袍,产生强烈的视觉反差和幽默效果",
+  "生成具有超现实风格的创意合成画面,将人物头部替换为宇宙星云、太极图、粒子爆炸等抽象元素,配合深蓝红色调背景,营造出哲学感或科幻感的视觉冲击",
+  "生成穿着完整冬季搭配的人物形象,展示黑色羽绒服、红色围巾、红色手套、宽腿裤等单品的组合穿搭效果,呈现从全身到局部细节的多角度造型展示",
+  "生成宠物穿着服装的可爱造型图,展示猫咪穿上印花连体衣的整体穿着效果,需要清晰呈现服装的图案、版型与宠物身体的贴合细节",
+  "生成创意合成图,将人物穿搭形象嵌入特定场景容器中(如超市生鲜托盘),使人物服装与场景产生趣味性视觉对比,同时保留服装细节的清晰可见",
+  "将猫咪表情包图片与各种场景素材(办公室、食物、产品、背景环境等)合成拼贴在一起,让猫咪看起来自然地处于这些场景中,形成多格并排的拼贴版式",
+  "把猫咪图片与各类装扮道具(帽子、眼镜、服装、假发等)或其他卡通/玩具素材叠加合成,让不同来源的素材无缝融合成一张完整的搞笑图",
+  "在同一张图中将多只猫咪或同一只猫咪的不同姿态照片拼接组合,配合文字标注形成对话或对比效果的多格拼图",
+  "将真实照片中的人物与卡通/奇幻元素合成,例如给人物添加蟑螂的触角和腿,使人物看起来像变成了一只蟑螂,整体画面自然融合不突兀",
+  "给普通猫咪照片套上不同职业的服装和场景(如医生、上班族、老板等),并保持猫咪面部表情清晰可辨,制作出系列表情包拼贴图",
+  "在真实物体照片上叠加手绘风格的简笔画元素,例如在猕猴桃切片上添加卡通五官和小触角,让照片呈现出实物与手绘结合的趣味效果",
+  "生成真实户外场景中的人物活动照片,画面要呈现自然光线下的街道、公园、游乐场等具体地点环境,人物动作自然生动,背景环境细节丰富真实",
+  "生成多人聚集的活动现场图,如会议、展览、户外聚会等场景,画面中需要呈现多个人物同框、有组织的群体互动氛围,背景有明显的活动标识或场地特征",
+  "生成真实物品的特写或陈列展示图,物品摆放清晰、细节可辨,适合用于产品展示或场景道具呈现,画面构图干净突出主体",
+  "生成同一人物在同一场景中多角度、多姿态的即时抓拍效果图,画面呈现自然随意的动态感,如行走、转身、低头、仰望等非摆拍状态,整体风格真实生活化",
+  "生成动物(如马)在运动瞬间被捕捉的高动态画面,鬃毛飞扬、肢体伸展,呈现出强烈的瞬间张力和动感,背景简洁以突出主体动态",
+  "生成人物在真实日常场景(街头、公园、机场等)中被随手拍下的多张图片拼贴效果,画面构图不刻意、视角多变(含俯拍脚部、镜中自拍、远景抓拍等),整体呈现出碎片化的生活记录感",
+  "制作图文卡片时,需要让插图与文字在语义上高度呼应——比如用可爱小驴的不同表情和动作配合对应的幽默文字,每张小卡片中图在上、文字在下,插图内容直接反映文字含义,形成一眼就能看懂的图文配合效果",
+  "制作多图拼贴帖子时,需要将多张照片或截图按照叙事顺序排列在一张大图中,并在关键图片上叠加说明性文字标注,让图片和文字共同讲述一个完整故事,文字起到补充说明和情感点评的作用",
+  "制作信息图文海报时,需要将大标题、分类小标题与正文段落按照清晰的层级排布在版面上,标题用大字醒目展示,正文紧跟其下,整体版面分区明确、图文对应,让读者能快速扫读获取信息",
+  "在图片上叠加标题文字,文字大小、粗细、颜色各异,形成层次感强的排版效果——例如大标题用粗体醒目字体,副标题用细体小字,整体风格统一(如深色系商务风或简约设计风)",
+  "在AI生成的卡通角色图片上叠加幽默吐槽文案,文字直接覆盖在图片上方,字体简洁白色,与画面情绪呼应,形成图文结合的表情包风格内容",
+  "制作多宫格拼图帖子,每格图片配有对应的标题文字或字幕说明,文字风格统一,整体排列整齐,适合用于周记、日历、流程说明等系列内容展示",
+  "制作图文卡片式科普内容:每张卡片包含统一的标题样式、编号序列、配套插图(卡通/示意图风格)和说明文字,多张卡片拼成一组,整体风格统一、排版清晰,适合健康养生、步骤教程类内容展示",
+  "制作数据报告类图文内容:包含柱状图、饼图、折线图、词云图、环形图等多种数据可视化图表,配合标题、要点文字说明,整体呈现专业研究报告的视觉风格,色彩搭配统一(如蓝紫色系或橙色系)",
+  "制作流程图/架构示意图:用箭头、方框、层级结构或立体堆叠图形展示系统架构、业务流程或概念层级关系,配合文字标注说明各模块功能,视觉上清晰呈现逻辑关系",
+  "将整个帖子内容拆分为多个独立小格子并排列成网格或矩阵布局,每个格子承载一个独立的场景或信息单元,格子之间有明显的分隔边界,整体看起来像一张由多张小图拼合而成的大图",
+  "在同一张图中混合使用多种内容载体形式,例如将真实照片、插画角色、产品图、文字说明、图表等不同类型的视觉元素组合排布在同一个版面内,形成图文混排的丰富视觉层次",
+  "以统一的视觉主角(如同一个卡通角色、同一个人物、同一主题场景)贯穿多个分格画面,每个格子呈现该主角在不同场景或状态下的样子,配合文字说明形成系列感强的多格叙事版式",
+  "将多张图片按网格或分区方式拼贴成一张图,每个区域展示不同角度或不同场景,整体画面有清晰的分割感和节奏感",
+  "在同一画面中合理安排主体与背景的空间关系,让主体(人物、动物、物品)在画面中有明确的视觉焦点,背景简洁或有层次地衬托主体",
+  "生成包含多个独立小格子的图文排版画面,每个格子内有图片和文字说明,格子之间疏密有致、整齐排列,整体呈现信息图表或内容合集的视觉效果",
+  "生成人物局部特写画面,如放大呈现嘴巴咬食物、手持物品、耳朵佩戴饰品、鼻子、指甲等身体局部细节,画面填充感强,细节清晰可见",
+  "生成人物近景半身或胸部以上的画面,突出人物面部表情和情绪,背景适当虚化,让观看者能清楚看到人物的神态与互动感",
+  "生成产品或物品的极近距离特写图,如食物截面、商品细节、小物件放大展示,画面主体占满画幅,质感和纹理清晰突出",
+  "生成具有强烈透视纵深感的室内空间图,画面中窗框、拱门、地板线条等建筑元素形成明显的空间层次,光线从远处窗口射入,营造出由近到远的视觉延伸效果",
+  "生成采用夸张变形构图的图片,例如鱼眼镜头效果将人物或场景扭曲成球形全景、仰拍使近处物体极度放大而远处极度缩小,或通过搞怪角度让画面产生强烈的视觉冲击感",
+  "生成画面中存在嵌套框架效果的图片,如在沙漠场景中用一个悬空的矩形框将主体框住形成画中画,或利用水面倒影与实景上下对称形成嵌套镜像构图,制造超现实的空间突破感",
+  "生成具有强烈色彩对比的艺术插画,整体画面以高饱和度的红色与蓝色为主色调,两种颜色形成鲜明的冷暖对撞,背景大面积纯色铺底,视觉冲击力极强",
+  "在以暗色或单色为主的画面中,用局部的高饱和亮色(如红色心脏、橙色暖光窗口、金黄色星光)作为点睛之笔,让视线自然聚焦到这个色彩亮点上,形成强烈的视觉引导",
+  "生成整体色调统一、饱和度偏高的场景图,例如全画面笼罩在深蓝色夜光氛围或浓郁的赤红土地色调中,让单一主色调主导整个画面,营造出沉浸式的强烈色彩氛围感",
+  "生成具有强烈氛围感的插画风场景图,整体画面以深蓝色调为主,室内外场景都笼罩在宁静的夜色中,窗户透出暖黄色灯光形成冷暖对比,画面质感接近油画或数字绘画风格,传达出静谧、沉思、略带忧郁的情绪氛围",
+  "制作色彩鲜艳、视觉冲击力强的宣传海报,背景使用渐变色块(蓝紫、橙红等高饱和度色彩),搭配几何抽象图形装饰,文字排版醒目大气,整体呈现出热烈、充满活力的欢庆氛围",
+  "生成暖色调的室内空间效果图,以米白、浅棕、焦糖色为主色调,光线柔和自然,空间布置温馨舒适,整体画面传达出放松、治愈、生活化的温暖氛围",
+  "生成具有强烈视觉冲击力的超现实主义风格图像:画面以大地色系(深红、赭石、深蓝)为主调,将白马、牛仔等元素置于极简的荒漠/盐湖场景中,营造出孤寂、神秘、如油画般的电影感氛围",
+  "生成3D卡通风格的拟人化动物角色,角色具有毛绒质感和丰富的表情神态,能够在不同生活场景(办公室、卧室、户外)中呈现出喜怒哀乐等情绪状态,整体风格类似皮克斯动画",
+  "制作融合插画风格的信息图文海报:以卡通机器人/科技感插图作为视觉主体,搭配醒目的彩色标题文字和数据图表,整体呈现出活泼又专业的视觉效果",
+  "生成一组多格拼贴图,每格展示同一人物在不同场景/状态下的夸张表情和肢体动作,配合幽默文字标注,整体呈现出戏剧化的情绪起伏效果(如一周心情变化、苦情崩溃、搞笑反应等)",
+  "将动物(如猫咪)与各种食物、道具进行创意合成,给动物添加配饰(帽子、假发、领带等)并嵌入食物场景中,搭配谐音梗或双关文字,制作出拟人化角色扮演的趣味表情包图片",
+  "给同一张猫咪照片批量添加不同职业的帽子、道具和配件(如厨师帽、安全帽、眼镜、画板等),让猫咪看起来像在扮演各种职业角色",
+  "将真实照片转换成具有统一色调风格的插画效果,整体呈现蓝紫色调的复古油画或动画风格,让风景场景看起来像艺术插图",
+  "制作图文排版信息图,将多张食材产品图片抠出后整齐排列在统一背景上,配合文字说明组合成内容丰富的科普海报",
+  "生成具有强烈光影对比的场景图,画面中光源明显(如阳光折射、水面反光、彩虹色光晕),暗部极深、亮部极亮,整体呈现出戏剧性的明暗反差和光线质感",
+  "生成带有明显颗粒感或纸张纹理的插画风格图片,画面整体像是印刷在粗糙介质上,物体表面有细腻的颗粒噪点或手工绘制的笔触肌理",
+  "生成室内场景时,能真实还原不同材质的质感细节,如木地板的纹路光泽、布艺沙发的绒毛感、大理石茶几的光滑反射、藤编家具的编织纹理等",
+  "在图片上叠加对话气泡或多行说明文字,用于讲述故事背景或补充情节说明,文字带有描边或阴影效果以确保在复杂背景上清晰可读",
+  "制作图文并茂的科普说明卡片,每张卡片包含标题、编号、插图和详细文字说明,整体排版整齐统一,适合分步骤展示教程或知识点",
+  "在多图拼贴海报上为每个区域叠加带图标的标签(如勾选符号+地点名称),并在整体画面上方添加大标题和副标题文字,形成图文结合的内容合集展示效果",
+  "为多人物展示海报中的每个人物添加姓名和职位标签,并在画面顶部叠加活动主题、专场名称等层级分明的标题文字,整体风格统一、信息密度高",
+  "对同一场景或主体生成多个不同距离和景别的画面,包括远景展示整体环境、中景呈现主体与环境关系、近景突出细节,形成一组视角丰富的图片集合",
+  "生成采用非常规拍摄角度的图片,如从低角度仰拍、从高处俯视、或模拟第一人称主观视角看向场景,让画面产生独特的视觉冲击感",
+  "生成能展示宽广空间感的室内或室外全景图,画面中包含完整的环境纵深,让观看者感受到场景的整体规模和空间层次",
+  "生成具有统一色调风格的插画场景,整体画面使用高度协调的单一色系(如全蓝紫色调的火车风景、全粉紫色调的奇幻海洋),让画面中所有元素的颜色都偏向同一个色相,营造出梦幻沉浸的视觉氛围",
+  "生成色彩鲜艳、多色并置的视觉冲击画面,画面中同时出现多种高饱和度的颜色搭配(如复古拼贴风格中的粉色、蓝色、橙色并置,或彩色条纹波浪地形),让整体色彩浓烈饱满、视觉张力强烈",
+  "生成低饱和度或去色风格的极简画面,整体色彩纯度降低,呈现出克制、安静的视觉质感(如黑白灰调的海洋孤舟场景,或接近无彩色的素雅插画),与高饱和度画面形成鲜明对比",
+  "生成超现实浪漫场景图:将人物置于不可能存在的宏大环境中,如站在地球边缘俯瞰星空、坐在云端长椅上漂浮、在星海上骑行,画面充满梦幻感和史诗级视觉冲击力",
+  "制作科技感强烈的活动宣传海报:使用深色背景配合橙色、蓝色等高对比度霓虹色调,融合未来感城市或科技场景插图,搭配大号粗体标题文字,整体呈现出硬核、前沿的视觉气质",
+  "生成融合东方传统与现代简约的室内空间效果图:以米白、暖棕为主色调,加入拱形门洞、藤编元素、中式花卉装饰画等传统细节,整体呈现温润雅致的新中式生活美学氛围",
+  "生成拟人化动物角色表情包:用AI生成具有丰富表情和情绪的卡通动物形象(如毛茸茸的红色马、灰色驴),能够呈现出沮丧、无奈、委屈等多种情绪状态,配合不同场景背景(办公室、草地、室内),整体风格介于3D皮克斯动画和水彩插画之间,适合搭配幽默文案使用",
+  "制作图文混排的知识科普长图:以深青色/蓝绿色为底色背景,将心理学等知识内容拆分为多个板块,每个板块搭配风格统一的插画小图(奇幻风格人物、动物等),文字与插图穿插排布,整体呈现出版式清晰、视觉层次丰富的杂志风格科普图文效果",
+  "生成室内空间效果图:用AI渲染出具有温暖奶油色调的室内场景,包含拱形门洞、藤编家具、自然光影等元素,整体呈现出地中海或法式复古风格的高质感室内设计效果,光线柔和、色调统一,适合作为家居内容的视觉展示",
+  "生成具有强烈戏剧性光影对比的户外场景图,画面中光源方向明确(如侧光或逆光),亮部与暗部之间形成鲜明反差,阴影轮廓清晰,整体呈现出电影感或艺术摄影风格的视觉张力",
+  "生成室内暖光氛围图,画面中多个光源(吊灯、筒灯、窗外自然光)共同营造出温暖柔和的米色调空间,光线从不同方向照射,形成层次丰富的软阴影,整体氛围温馨舒适",
+  "生成充满魔幻或超现实感的彩色光效场景,画面中有多种颜色的光线(如橙、蓝、紫等)交织流动,光源本身成为视觉焦点,整体营造出梦幻、神秘或节日感的强烈氛围",
+  "制作大字号标题搭配正文内容的图文排版,标题文字极大且颜色鲜艳(红色、黄色等高饱和色),与正文小字形成强烈的大小对比,整体版面信息密度高、视觉冲击力强",
+  "在图片上叠加大字幕文字,字体粗大醒目,常带有描边或阴影效果,文字直接覆盖在照片或场景图上,起到强调说明或搞笑点评的作用",
+  "制作多宫格拼贴式内容图,每个格子内有大标题文字突出显示核心信息(如价格、品类名),配合产品图或场景图,标题字号远大于说明文字,形成层次分明的视觉结构"
+]

+ 39 - 0
examples/process_pipeline/presets.json

@@ -0,0 +1,39 @@
+{
+  "researcher": {
+    "system_prompt_file": "prompts/researcher.prompt",
+    "max_iterations": 50,
+    "temperature": 0.3,
+    "skills": [
+      "core",
+      "research"
+    ],
+    "description": "Phase 1: 纯粹的平台爬虫与原文采集者"
+  },
+  "filter_and_blueprint": {
+    "system_prompt_file": "prompts/filter_and_blueprint.prompt",
+    "max_iterations": 50,
+    "temperature": 0.2,
+    "skills": [
+      "core"
+    ],
+    "description": "Phase 2-A: 过滤劣质 Case,抽象工序 Blueprint (只有本地文件读写权限)"
+  },
+  "extract_capabilities": {
+    "system_prompt_file": "prompts/extract_capabilities.prompt",
+    "max_iterations": 100,
+    "temperature": 0.1,
+    "skills": [
+      "core"
+    ],
+    "description": "Phase 2-B: 原子能力映射图库分析 (无浏览器权限,只挂载基础和指定检索工具)"
+  },
+  "assemble_strategy": {
+    "system_prompt_file": "prompts/assemble_strategy.prompt",
+    "max_iterations": 50,
+    "temperature": 0.1,
+    "skills": [
+      "core"
+    ],
+    "description": "Phase 3: 最终策略组装者 (只有本地文件读写权限)"
+  }
+}

+ 105 - 0
examples/process_pipeline/prompts/assemble_strategy.prompt

@@ -0,0 +1,105 @@
+---
+temperature: 0.1
+---
+
+$system$
+
+你是一个完全不懂编程、没有任何代码开发能力的 AIGC 业务流派整合产品经理(Reduce 阶段)。
+**不管你以前拥有多么高深的技术背景,此刻你必须认清自己的最新身份:你只是一个连 Python 都没听说过、绝对不会去开发任何脚本的业务统筹者。你的任务只靠文本文档处理来完成!**
+此时,前方的探子们已经为你清洗好了一切数据并写在文件中:
+1. `blueprint_file`:包含提纯后的纯 AIGC 极品 cases,以及初步推演的不带技术细节的自动化架构流(Blueprints)。
+2. `capabilities_file`:包含前方能力分析师对比了工具库之后提取出的“可用原子能力列表”。
+
+你的任务:
+**仅仅是缝合。** 将初步架构流(Blueprints)里的各个自动化阶段,与 AIGC 原子能力列表里的标准化模块“无缝嵌合”,拼接成最终能直接在生产环境被执行的标准化 `strategy.json`。
+最终的策略必须且只能是全自动化 AIGC 节点操作流。任何残留的手工作业术语(如 PS、橡皮擦、手工抠图)若穿透到此阶段,你必须将其所属的阶段极其无情地直接丢弃、整段截断!宁可策略精简,也绝不在输出中保留丝毫传统人工操作建议。
+> 🚨 **【系统红线】**:严禁尝试编写、输出任何 Python/JSON 分析处理脚本串行工具!你所在的系统是一个纯文本逻辑闭环,不支持运行代码,你必须直接在响应中给出最终推演好的文本与结构数据。
+> 🚨 **【输出文件红线】**:你的最终及唯一输出文件必须是且只能是 `%output_file%`!严禁你在当前目录下私自输出任何临时阅读稿或文件夹(绝不要新建类似 `analysis_temp.txt` 或 `knowledge/` 这类辅助目录),这会严重污染存储库。
+
+---
+
+## 可用工具
+- `read_file(path)` — 读取文件
+- `write_json(file_path, json_data)` — 专门安全地将原生大结构 dict 数据写入为 JSON 文件(不要试图用普通字符串写)
+
+---
+
+## 执行流程
+
+### 第一步:加载前置文件
+1. `read_file("%blueprint_file%")` 获取架构蓝图和精华用例。
+2. `read_file("%capabilities_file%")` 获取经过鉴定的原子能力池。
+
+### 第二步:拼接与决断 (Assembly)
+- 不要凭空捏造新能力,**必须只从 `capabilities_file` 里挑选能力** 填补进蓝图的阶段中。
+- 把粗糙的蓝图变成体系化的流派架构。
+- 从这些蓝图中,**拍板挑选一条并且只挑选一条作为主线(is_selected: true)**,其余路线退居二线(is_selected: false)。
+- 你不需要重新分析好坏,直接把你在 `blueprint` 中看到的优势/劣势,填进 `highlight_coverage` 和 `why_not` 等字段中。
+
+### 第三步:写入终端
+全部拼接好后,写入到 `%output_file%`。
+
+---
+
+## 输出 JSON 格式
+
+```json
+{
+  "requirement": "需求描述",
+  "strategies": [
+    {
+      "is_selected": true,
+      "name": "首推策略命名 (从 Blueprint 提炼)",
+      "source": "标明采纳了哪几个精华 case",
+      "workflow_outline": [
+        {
+          "phase": "阶段1:...",
+          "description": "...",
+          "capabilities": [
+            {
+              "id": "从 capabilities_file抄过来的id",
+              "name": "抄过来的能力名",
+              "is_new": false,
+              "implements": {},
+              "case_references": [],
+              "effects": ["直接从 capabilities_file 对应能力的 effects 字段抄过来"]
+            }
+          ]
+        },
+        {
+          "phase": "阶段2:高光特效处理",
+          "description": "(注意:此处所有阶段必须完整,绝不可省略)",
+          "capabilities": [ ... ]
+        }
+      ],
+      "highlight_coverage": [ "高光覆盖说明..." ],
+      "baseline_coverage": [ "底线覆盖说明..." ],
+      "reasoning": "为什么拍板这套作为 is_selected: true",
+      "why_not": null,
+      "could_switch_if": null
+    },
+    {
+      "is_selected": false,
+      "name": "备选流派一",
+      "source": "...",
+      "workflow_outline": [ ... 同样完整的 phases ... ],
+      "highlight_coverage": [],
+      "baseline_coverage": [],
+      "reasoning": null,
+      "why_not": "为什么被淘汰",
+      "could_switch_if": "切换条件"
+    }
+  ],
+  "uncovered_requirements": [
+    "目前技术局限导致的短板"
+  ]
+}
+```
+
+$user$
+
+【开始执行】
+原始需求:%requirement%
+蓝图与精用例文件:%blueprint_file%
+提取出的能力文件:%capabilities_file%
+最终结果存放路径:%output_file%

+ 96 - 0
examples/process_pipeline/prompts/extract_capabilities.prompt

@@ -0,0 +1,96 @@
+---
+temperature: 0.1
+---
+
+$system$
+
+你是一个完全不懂编程、没有任何代码开发能力的 AIGC 业务提炼策划专家。你会收到其他环节整理好的所有 Workflow Case 源文件。你的专属任务是:
+**请永远认清自己的身份:你只是一个只会用文档和文字做整理的非技术人员,你绝对不懂如何编写任何 Python 解析工具!你的本职是从文本层逻辑去理解生成式的组合。**
+只关注"生成式 AI 的自动化能力和节点工具"。对每一个提到的专业技术组合,去"工具字典/原子能力库"里寻找对应。
+任何包含"人工介入"(如 Photoshop 仿制图章、美图秀秀、手动橡皮擦、手工抠皮)的传统图像处理术语,均不属于你的提取范畴,如果在源文件中看到,一律直接无视剔除且绝不要提取为系统能力。
+> 🚨 **【系统红线】**:在处理原始 JSON 用例时,你必须直接在内心中阅读提炼并输出最终结果,严禁输出任何 Python 代码或期望通过代码来帮你解析数据!系统完全不具备代码执行能力。
+> 🚨 **【知识库调用红线】**:你仅且只被允许调用 **查询类** 工具(如 `capability_search`, `tool_search`)来从字典里寻找对应。**严禁**调用 `knowledge_save`、`knowledge_update` 等任何会引发"上传/更新"行为的工具!同时**严禁**调用任何类似于 `ask_user` 或者 `ask` 的询问工具。绝不允许你外溢保存垃圾或打扰人类!
+> 🚨 **【输出文件红线】**:你仅被允许使用 `write_json` 把最终结构直接写入到指定的 `%output_file%` 文件中。严禁私自创建或写入任何其它临时文件或知识夹!
+
+---
+
+## 什么是"正确的 AIGC 原子能力"?
+
+原子能力是一种**面向需求的、跨工具的高维能力**。它不是具体的底层工作流节点或执行步骤,而是一种**独立完整的、不可分割的特征维度的需求实现方案**。
+
+**核心特征:**
+- **不可分割**:拆分后将无法独立满足任何需求(如「制作电商产品图」太宽泛,可被拆分,而「保持角色面部一致性」则是不可分割的原子)。
+- **面向需求**:直接对应用户的某一类具体的创作要求(如文字渲染、空间透视约束)。
+- **跨工具组合**:同一个能力可以由不同的前沿引擎/节点以不同方式实现(例如面部继承既能用特定 LoRA 也能用相关适配器节点)。
+
+---
+
+## 可用工具
+- `glob_files(pattern)` — 查找匹配的调研用例文件
+- `read_file(path)` — 读取文件
+- `write_json(file_path, json_data)` — 专门安全地将原生大结构 dict 数据写入为 JSON 文件(不要试图用普通字符串写)
+- `capability_search(query, top_k=6)` — 检索现有原子能力库(非常重要!必须使用!)
+- `capability_list(limit=50)` — 列出系统内的所有原子能力总表
+
+---
+
+## 执行流程
+
+### 第一步:读取用例
+通过 `glob_files("%raw_files_glob%")` 找到原始的用例,全部读取进上下文,重点关注 `workflow_process` 与 `input/output`。
+
+### 第二步:扫描与匹配 (Map to Capabilities)
+对于文中浮现的每一种特定的 AI 控制手法:
+- 提炼它的"高维动作意图"(例如:案例说用了 IPAdapter Plus 垫图,它的意图是`特征风格迁移与一致性保持`)。
+- **必须调用 `capability_search("特征风格迁移与一致性保持")`**,看看工具库字典里存不存在类似的标准化预设。
+  - 如果搜到了,就记录下它的 `ID` 并且标记 `is_new: false`。
+  - 如果库里确实没有,或者搜出来的效果牛头不对马嘴,那就**暂定发明一个新能力名称**,并标记 `is_new: true`。
+
+### 第三步:输出结果
+请将你提取出来的这几十个分散的"原子能力"(包括旧的或暂定的新的)统一列出,写入 `%output_file%`。
+
+> 📌 **【effects 字段要求】**:每个能力都必须包含 `effects` 字段——从 case 里提炼出这个能力在真实场景里被使用后,用户/平台/结果侧实际观察到了什么效果(如"互动量提升 X%"、"批量生成 N 张无需手动干预"、"视觉风格统一无需反复调整"等)。这是最直接体现该能力价值的字段,必须填写,不能为空列表。
+
+---
+
+## 输出 JSON 格式
+
+```json
+{
+  "requirement": "需求描述",
+  "extracted_capabilities": [
+    {
+      "id": "CAP-001 (如果是库里搜到的,填真实的 ID)",
+      "name": "搜到的真实名称",
+      "is_new": false,
+      "description": "搜到的描述(原样保留)",
+      "criterion": "搜到的检验标准(原样保留)",
+      "implements": {
+        "原样保留的工具1": "搜到的具体实现说明"
+      },
+      "case_references": ["case_001 中的垫图操作"],
+      "effects": ["在同一场景中保持人物面部高度相似,无抽卡现象", "生成的批量角色图在服装、发型上基本统一"]
+    },
+    {
+      "id": null,
+      "name": "目前库里没有的新能力名称",
+      "is_new": true,
+      "description": "为其草拟一段正式的能力描述(1-2句话)",
+      "criterion": "为其草拟一条明确的效果检验标准",
+      "implements": {
+        "推荐使用的工具名称1": "具体如何通过该工具实现此能力的实施细节",
+        "推荐使用的工具名称2": "具体的底层节点或算法组合"
+      },
+      "case_references": ["case_002 提到需要用到最新的这个插件"],
+      "effects": ["实测可在 30 秒内完成背景去除,透明底 PNG 可直接叠加到模板上"]
+    }
+  ]
+}
+```
+
+$user$
+
+【开始执行】
+原始需求:%requirement%
+读取原始用例搜索路径:%raw_files_glob%
+输出能力集写回到文件:%output_file%

+ 91 - 0
examples/process_pipeline/prompts/filter_and_blueprint.prompt

@@ -0,0 +1,91 @@
+---
+temperature: 0.2
+---
+
+$system$
+
+你是一个完全不懂编程、没有任何代码开发能力的 AIGC 业务流派策划专家。你会收到一批原始的调研用例文件。
+**请务必认清自己的身份:你只是一个纯粹的业务分析师,只懂如何用业务概念去理解 AIGC 的功能组合,你绝对不会写任何 Python、HTML 或各种自动化脚本!一旦试图写代码来解析文件,你就违背了你的人设。**
+你的核心职责纯粹是在“不碰触任何编程活动”的前提下,直接用内心的理解力阅读文本,从海量杂音中提纯出可被执行的思路蓝图。
+
+你的专属任务是:
+1. 从这些文件里淘尽沙子,只留下质量最高、最有启发性的纯 AIGC 案例。
+2. 严苛洗炼数据:从根本上拒绝任何包含“人工干预”(如 Photoshop 仿制图章、美图秀秀手工修图、手动抠皮/橡皮擦、手工调色)的传统手工作业案例。如果案例主轴是手工修图,直接视为无效并丢弃。
+3. 依据筛选出的纯正 AIGC 精华案例,推提出对应的全自动化节点执行雏形(Blueprint)。
+4. > 🚨 **【系统红线】**:严禁尝试编写、输出任何 Python/JSON 分析处理脚本来“分包”你的工作!系统不会执行你的代码。你必须利用自身优秀的理解力,直接在内部上下文中阅读并输出最终总结。
+5. > 🚨 **【输出文件红线】**:你仅被允许使用 `write_json` 把最终结果直接写入到为你指定的 `%output_file%` 文件中。严禁私自创建或写入任何其它临时文件或附属文件夹(绝不要新建类似 `analysis_temp.txt` 或 `knowledge` 之类的辅助目录)!
+
+---
+
+## 可用工具
+- `glob_files(pattern)` — 查找匹配的文件
+- `read_file(path)` — 读取文件
+- `write_json(file_path, json_data)` — 专门安全地将原生大结构 dict 数据写入为 JSON 文件(不要试图用普通字符串写)
+
+---
+
+## 执行流程
+
+### 第一步:读取数据
+- 通过 `glob_files("%raw_files_glob%")` 找到原始的调研文件。
+- 全部读取进入你的上下文。
+
+### 第二步:过滤与合并 (Distill)
+并不是所有搜到的 case 都值得用。请使用极其严苛的眼光过滤:
+- 【高相关性过滤(最重要)】:严禁生搬硬套!挑选出的 case 必须在生成主题、业务场景和解决痛点上高度贴近本次的原始需求(%requirement%),如果偏离核心需求重心,必须果断剔除。
+- 【非 AI 过滤】:剔除任何涉及人工 PS 操作、传统抠图工具、需手动绘画等非自动化管线的传统 case。
+- 【时效性过滤】:剔除那些工具名明显过时(几年前的旧模型)的 case。
+- 【低价值过滤】:剔除那些只是吹嘘效果,没有提供任何节点、工作流、AI 参数参考价值的 case。
+- 【劣口碑过滤】:剔除评论区全都是“生成失败/拉胯”的灾难 case。
+最终筛选出 2~5 个最具参考价值的极品 AIGC Case,梳理为标准的规范数组。
+
+### 第三步:推演 Blueprint 雏形
+依据筛选出的好 Case 库,抽象出 1 到 2 套初步的执行工序(Blueprint)。
+不要查什么原子能力,也不要管具体的工具参数!只要写出:
+【第一步该干什么 ➔ 第二步该干什么 ➔ 第三步该干什么】
+比如:“阶段1 搭建底图结构;阶段2 进行脸部一致性替换;阶段3 进行局部光影补偿”。
+
+### 第四步:输出
+请将所有内容打包,一次性写入指定的 `%output_file%` 中。
+
+---
+
+## 输出 JSON 格式
+
+```json
+{
+  "requirement": "本次的需求描述",
+  "distilled_cases": [
+    {
+      "id": "case_001",
+      "title": "...",
+      "source_url": "...",
+      "user_feedback": "...",
+      "workflow_process": "..."
+    }
+  ],
+  "blueprints": [
+    {
+      "name": "高水准路线蓝图(来自 case_001 等)",
+      "phases": [
+        {
+          "phase": "阶段1:搭建底图结构",
+          "description": "需要用到姿态控制、线稿约束等原理来达成..."
+        },
+        {
+          "phase": "阶段2:高光特效处理",
+          "description": "..."
+        }
+      ],
+      "reasoning": "为什么这条蓝图路线行得通"
+    }
+  ]
+}
+```
+
+$user$
+
+【开始执行】
+原始需求:%requirement%
+读取原始用例搜索路径:%raw_files_glob%
+写入提纯蓝图文件路径:%output_file%

+ 78 - 0
examples/process_pipeline/prompts/researcher.prompt

@@ -0,0 +1,78 @@
+---
+temperature: 0.3
+---
+
+$system$
+
+你是一个工序调研统筹子 Agent。你会收到一个**具体的调研渠道和方向**,你需要为你接到的方向广泛搜索相关工序案例,并统一将结果结构化写入到指定的 JSON 文件中。
+
+**你的边界**:完全独立搜索,查案子、抓原文、存入文件即可。不负责策略归纳。
+
+---
+
+## 可用工具
+
+### 搜索工具
+- `content_platforms(platform="")` — 列出/查询平台详细搜索参数
+- `content_search(platform, keyword, max_count=20)` — 跨平台搜索案例(返回结果和序列号)
+  - platform 常用值: `xhs`(小红书), `youtube`, `x`(Twitter), `bili`, `gzh`, `zhihu`
+- `content_detail(platform, index)` — 根据 content_search 结果的序号查看详细内容和全文
+- `content_suggest(platform, keyword)` — 获取搜索相关建议词
+
+### 文件工具
+- `read_file(path)` — 读取文件(追加前必须先读)
+- `write_file(path, content)` — 写入/覆盖文件
+
+---
+
+## 执行流程
+
+### 第一步:理解定向任务并进行广泛搜索
+1. 阅读任务指令,明确专属平台和切入点。
+2. > 🚨 **【工具调用最高红线】**:**必须**优先调用 `content_search` 工具搜索真实信息。严禁在对话中输出任何形式的 Python 脚本或编程代码,系统不支持你运行代码!
+3. > 🚨 **【搜索词红线】**:搜索词严禁夹带具体的 AI 软件名或技术框架名。
+4. 搜索并用 `content_detail` 仔细阅读高赞方案。尝试更换关键词,重试 2 次找不到则结束。
+
+### 第二步:萃取原汁原味的案例信息
+从文章中重点提取:
+1. **输入与输出效果(Input & Output)**:作者给了什么提示词、图?最终产出什么?必须保留效果图的 URL!
+2. **操作过程记录(Raw Workflow)**:关键节点名、调参术语(原汁原味)。
+3. **来源信源与反馈评估**:三连数据,以及精选 1~3 条核心评论(报错、吐槽、优缺点)。
+
+### 第三步:存储结果文件
+🚨 **绝对不能更改任务规定的 `output_file` 路径名**!
+每收集到 2~3 个 case,应立即持久化一次:
+```
+read_file("{output_file}")   → 若文件不存在则初始化 {"requirement": "总体需求", "cases": []}
+将新 case 追加进 cases 数组
+write_file("{output_file}", 更新后的完整 JSON)
+```
+
+---
+
+## 输出格式
+写入到 `%output_file%`:
+```json
+{
+  "requirement": "本次的需求描述",
+  "searched_at": "ISO时间",
+  "cases": [
+    {
+      "id": "case_001",
+      "title": "案例标题",
+      "platform": "xhs",
+      "source_url": "https://...",
+      "metrics": { "likes": 100, "comments": 20 },
+      "user_feedback": "核心评论反馈摘抄",
+      "images": ["https://链接"],
+      "input_details": "输入配置",
+      "output_details": "输出效果评价",
+      "workflow_process": "大白话流程和作者的术语原话"
+    }
+  ]
+}
+```
+
+$user$
+
+%task%

+ 27 - 0
examples/process_pipeline/prompts/router.prompt

@@ -0,0 +1,27 @@
+---
+temperature: 0.1
+max_tokens: 300
+model: qwen-plus
+---
+
+$system$
+
+你是一个高效且冷酷的路由决策机。你的唯一任务是根据输入的业务需求,选出最适合进行 AIGC 工序案例调研的平台。
+目前本地系统中已有的渠道缓存为:[%existing_platforms%]
+你需要**排除上述已有渠道**,再额外补充挑选出精准的 %needed_count% 个未探索渠道。
+可选平台库:[xhs, youtube, bili, x, zhihu, gzh]
+
+各平台最佳适用性:
+- `youtube`:适合需搜索复杂完整工作流大课及英文前沿技术。
+- `xhs`:适合小红书参数面板、避坑指南、美学设计排版。
+- `x`:适合最新 AI 工具发布、前沿生成模型尝鲜。
+- `bili`:适合 B 站中文长视频教程、二次元或游戏美术工序。
+- `zhihu` / `gzh`:适合深度原理剖析。
+
+**输出要求**:
+根据用户需求,直接输出逗号分隔的 %needed_count% 个新鲜平台代码(例如补充 `xhs,youtube`),绝不要包含任何 [%existing_platforms%] 里的元素。
+不要任何分析、标点符号、解释性语言或多余文字。
+
+$user$
+
+需求:%requirement%

+ 1096 - 0
examples/process_pipeline/run_metrics.json

@@ -0,0 +1,1096 @@
+[
+  {
+    "index": 9,
+    "requirement": "制作多格宫格式信息图,将同类内容(如多种食材搭配方案)拆分为统一风格的小卡片,每格包含标题、食材图片和文字说明,整体排列整齐、色块鲜明,适合一图展示多个并列条目...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:15:29.896650"
+  },
+  {
+    "index": 8,
+    "requirement": "生成真实场景的多图拼贴展示图,将同一地点或主题的多张实拍照片拼合为一张图文并茂的内容图,适合用于地点打卡、产品展示或生活记录类帖子...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:15:31.188161"
+  },
+  {
+    "index": 6,
+    "requirement": "生成真实人物在户外或特定场景中的生活记录照片,画面自然真实,包含儿童在公园、农场等户外环境中玩耍的多角度抓拍效果,光线自然,氛围温馨...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:15:31.242730"
+  },
+  {
+    "index": 7,
+    "requirement": "制作将真实人物照片合成到趣味场景中的创意图片,例如把人物缩小放入超市肉类托盘包装内、或与冰雕翅膀等道具结合形成视觉错位的幽默效果...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:15:31.270036"
+  },
+  {
+    "index": 14,
+    "requirement": "生成整体色调偏粉紫、薄荷绿、浅蓝等低饱和度冷色系的插画或场景图,画面呈现出梦幻、静谧的冷色调氛围,颜色搭配柔和克制...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:15:43.041197"
+  },
+  {
+    "index": 10,
+    "requirement": "在图片上叠加标注元素,如用红色圆点、箭头或emoji符号指向图中特定位置,配合说明文字,实现在真实照片上直观标记关键信息的视觉效果...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:15:45.555339"
+  },
+  {
+    "index": 12,
+    "requirement": "生成以暖黄/米棕色为背景底色的图文排版内容,整体画面呈现温暖、复古的暖色调氛围,适合健康养生、生活方式类主题...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:15:45.588916"
+  },
+  {
+    "index": 13,
+    "requirement": "生成以深色(黑色/深蓝/深紫)为背景底色的海报,搭配霓虹感彩色光效(橙、紫、青等),营造出科技感强烈的冷暖对比配色效果...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:15:45.643514"
+  },
+  {
+    "index": 19,
+    "requirement": "制作统一模板风格的系列信息卡片,每张卡片包含固定的图标符号(如皇冠等级图标)、彩色标题文字和配图,整体视觉风格一致、可批量复用...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:15:57.458785"
+  },
+  {
+    "index": 18,
+    "requirement": "将人物照片与中国传统吉祥符号(如双喜字、红玫瑰、金色祝福文字)融合,生成具有强烈喜庆氛围的定制化图案,人物面孔清晰嵌入红色喜庆背景中...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:15:59.316482"
+  },
+  {
+    "index": 15,
+    "requirement": "生成手部持握或展示物品的特写画面,手势自然,物品清晰呈现,如用手托举饺子、手持卡片、手握手机等,突出手与物品的互动关系...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:15:59.353969"
+  },
+  {
+    "index": 24,
+    "requirement": "生成穿着完整冬季搭配的人物形象,展示黑色羽绒服、红色围巾、红色手套、宽腿裤等单品的组合穿搭效果,呈现从全身到局部细节的多角度造型展示...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:16:11.587806"
+  },
+  {
+    "index": 23,
+    "requirement": "生成具有超现实风格的创意合成画面,将人物头部替换为宇宙星云、太极图、粒子爆炸等抽象元素,配合深蓝红色调背景,营造出哲学感或科幻感的视觉冲击...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:16:13.499411"
+  },
+  {
+    "index": 29,
+    "requirement": "在同一张图中将多只猫咪或同一只猫咪的不同姿态照片拼接组合,配合文字标注形成对话或对比效果的多格拼图...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:16:25.435465"
+  },
+  {
+    "index": 28,
+    "requirement": "把猫咪图片与各类装扮道具(帽子、眼镜、服装、假发等)或其他卡通/玩具素材叠加合成,让不同来源的素材无缝融合成一张完整的搞笑图...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:16:26.936993"
+  },
+  {
+    "index": 34,
+    "requirement": "生成多人聚集的活动现场图,如会议、展览、户外聚会等场景,画面中需要呈现多个人物同框、有组织的群体互动氛围,背景有明显的活动标识或场地特征...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:16:38.492493"
+  },
+  {
+    "index": 33,
+    "requirement": "生成真实户外场景中的人物活动照片,画面要呈现自然光线下的街道、公园、游乐场等具体地点环境,人物动作自然生动,背景环境细节丰富真实...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:16:39.537907"
+  },
+  {
+    "index": 38,
+    "requirement": "生成人物在真实日常场景(街头、公园、机场等)中被随手拍下的多张图片拼贴效果,画面构图不刻意、视角多变(含俯拍脚部、镜中自拍、远景抓拍等),整体呈现出碎片化的生活...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:16:53.010281"
+  },
+  {
+    "index": 43,
+    "requirement": "在AI生成的卡通角色图片上叠加幽默吐槽文案,文字直接覆盖在图片上方,字体简洁白色,与画面情绪呼应,形成图文结合的表情包风格内容...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:17:05.220228"
+  },
+  {
+    "index": 48,
+    "requirement": "将整个帖子内容拆分为多个独立小格子并排列成网格或矩阵布局,每个格子承载一个独立的场景或信息单元,格子之间有明显的分隔边界,整体看起来像一张由多张小图拼合而成的大...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:17:17.311331"
+  },
+  {
+    "index": 53,
+    "requirement": "生成包含多个独立小格子的图文排版画面,每个格子内有图片和文字说明,格子之间疏密有致、整齐排列,整体呈现信息图表或内容合集的视觉效果...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:17:29.613783"
+  },
+  {
+    "index": 11,
+    "requirement": "制作图文混排的长图文内容,将大段文字与人物照片、数据表格、流程图等多种视觉元素组合排布在同一版面中,形成类似杂志或报告的专业排版风格...",
+    "duration_seconds": 597.41,
+    "total_cost_usd": 0.8892,
+    "costs_breakdown": {
+      "P3_Assembler": 0.8892
+    },
+    "errors": [],
+    "timestamp": "2026-04-19T23:25:42.987738"
+  },
+  {
+    "index": 16,
+    "requirement": "生成多人或多只动物同框的协同姿态画面,如两只猫并排躺着穿睡衣、两人一起摆造型抱花束,呈现出同步、对称或互动的视觉效果...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:25:55.607805"
+  },
+  {
+    "index": 21,
+    "requirement": "生成宠物或动物穿戴人类服饰配件(如帽子、围巾)的画面,让动物看起来像在过节或扮演某种角色,整体效果可爱又有趣...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:26:07.484490"
+  },
+  {
+    "index": 20,
+    "requirement": "生成黑色科技感背景的人物宣传海报,背景带有流光线条或霓虹光效,人物照片与品牌Logo、活动标识、二维码等视觉元素整齐排布,形成高辨识度的系列展示图...",
+    "duration_seconds": 593.96,
+    "total_cost_usd": 0.6884,
+    "costs_breakdown": {
+      "P3_Assembler": 0.6884
+    },
+    "errors": [],
+    "timestamp": "2026-04-19T23:26:07.501019"
+  },
+  {
+    "index": 26,
+    "requirement": "生成创意合成图,将人物穿搭形象嵌入特定场景容器中(如超市生鲜托盘),使人物服装与场景产生趣味性视觉对比,同时保留服装细节的清晰可见...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:26:20.319815"
+  },
+  {
+    "index": 25,
+    "requirement": "生成宠物穿着服装的可爱造型图,展示猫咪穿上印花连体衣的整体穿着效果,需要清晰呈现服装的图案、版型与宠物身体的贴合细节...",
+    "duration_seconds": 0.01,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:26:20.359250"
+  },
+  {
+    "index": 30,
+    "requirement": "将真实照片中的人物与卡通/奇幻元素合成,例如给人物添加蟑螂的触角和腿,使人物看起来像变成了一只蟑螂,整体画面自然融合不突兀...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:26:33.728124"
+  },
+  {
+    "index": 17,
+    "requirement": "生成创意性的嘴部动作特写,如用嘴唇衔住花朵茎部形成'嘴唇花'的视觉效果,将身体部位与物品结合产生趣味创意画面...",
+    "duration_seconds": 644.96,
+    "total_cost_usd": 0.79,
+    "costs_breakdown": {
+      "P3_Assembler": 0.79
+    },
+    "errors": [],
+    "timestamp": "2026-04-19T23:26:44.272246"
+  },
+  {
+    "index": 35,
+    "requirement": "生成真实物品的特写或陈列展示图,物品摆放清晰、细节可辨,适合用于产品展示或场景道具呈现,画面构图干净突出主体...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:26:46.199165"
+  },
+  {
+    "index": 22,
+    "requirement": "生成将普通服装或日常物品以夸张搞怪方式穿戴的人物画面,比如把超大号短裤当长袍穿、用篮球短裤模仿古希腊长袍,产生强烈的视觉反差和幽默效果...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:26:57.954750"
+  },
+  {
+    "index": 39,
+    "requirement": "制作图文卡片时,需要让插图与文字在语义上高度呼应——比如用可爱小驴的不同表情和动作配合对应的幽默文字,每张小卡片中图在上、文字在下,插图内容直接反映文字含义,形...",
+    "duration_seconds": 611.54,
+    "total_cost_usd": 0.7025,
+    "costs_breakdown": {
+      "P3_Assembler": 0.7025
+    },
+    "errors": [],
+    "timestamp": "2026-04-19T23:27:03.659098"
+  },
+  {
+    "index": 27,
+    "requirement": "将猫咪表情包图片与各种场景素材(办公室、食物、产品、背景环境等)合成拼贴在一起,让猫咪看起来自然地处于这些场景中,形成多格并排的拼贴版式...",
+    "duration_seconds": 0.01,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:27:10.434900"
+  },
+  {
+    "index": 44,
+    "requirement": "制作多宫格拼图帖子,每格图片配有对应的标题文字或字幕说明,文字风格统一,整体排列整齐,适合用于周记、日历、流程说明等系列内容展示...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:27:16.196832"
+  },
+  {
+    "index": 32,
+    "requirement": "在真实物体照片上叠加手绘风格的简笔画元素,例如在猕猴桃切片上添加卡通五官和小触角,让照片呈现出实物与手绘结合的趣味效果...",
+    "duration_seconds": 0.01,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:27:23.450714"
+  },
+  {
+    "index": 49,
+    "requirement": "在同一张图中混合使用多种内容载体形式,例如将真实照片、插画角色、产品图、文字说明、图表等不同类型的视觉元素组合排布在同一个版面内,形成图文混排的丰富视觉层次...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:27:29.669170"
+  },
+  {
+    "index": 54,
+    "requirement": "生成人物局部特写画面,如放大呈现嘴巴咬食物、手持物品、耳朵佩戴饰品、鼻子、指甲等身体局部细节,画面填充感强,细节清晰可见...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:27:42.575218"
+  },
+  {
+    "index": 59,
+    "requirement": "生成画面中存在嵌套框架效果的图片,如在沙漠场景中用一个悬空的矩形框将主体框住形成画中画,或利用水面倒影与实景上下对称形成嵌套镜像构图,制造超现实的空间突破感...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:27:55.105064"
+  },
+  {
+    "index": 58,
+    "requirement": "生成采用夸张变形构图的图片,例如鱼眼镜头效果将人物或场景扭曲成球形全景、仰拍使近处物体极度放大而远处极度缩小,或通过搞怪角度让画面产生强烈的视觉冲击感...",
+    "duration_seconds": 618.5,
+    "total_cost_usd": 0.7384,
+    "costs_breakdown": {
+      "P3_Assembler": 0.7384
+    },
+    "errors": [],
+    "timestamp": "2026-04-19T23:28:00.126280"
+  },
+  {
+    "index": 64,
+    "requirement": "制作色彩鲜艳、视觉冲击力强的宣传海报,背景使用渐变色块(蓝紫、橙红等高饱和度色彩),搭配几何抽象图形装饰,文字排版醒目大气,整体呈现出热烈、充满活力的欢庆氛围...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:28:08.629904"
+  },
+  {
+    "index": 69,
+    "requirement": "生成一组多格拼贴图,每格展示同一人物在不同场景/状态下的夸张表情和肢体动作,配合幽默文字标注,整体呈现出戏剧化的情绪起伏效果(如一周心情变化、苦情崩溃、搞笑反应...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:28:21.153513"
+  },
+  {
+    "index": 40,
+    "requirement": "制作多图拼贴帖子时,需要将多张照片或截图按照叙事顺序排列在一张大图中,并在关键图片上叠加说明性文字标注,让图片和文字共同讲述一个完整故事,文字起到补充说明和情感...",
+    "duration_seconds": 776.37,
+    "total_cost_usd": 1.2366,
+    "costs_breakdown": {
+      "P3_Assembler": 1.2366
+    },
+    "errors": [],
+    "timestamp": "2026-04-19T23:39:56.276033"
+  },
+  {
+    "index": 45,
+    "requirement": "制作图文卡片式科普内容:每张卡片包含统一的标题样式、编号序列、配套插图(卡通/示意图风格)和说明文字,多张卡片拼成一组,整体风格统一、排版清晰,适合健康养生、步...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:40:12.343727"
+  },
+  {
+    "index": 37,
+    "requirement": "生成动物(如马)在运动瞬间被捕捉的高动态画面,鬃毛飞扬、肢体伸展,呈现出强烈的瞬间张力和动感,背景简洁以突出主体动态...",
+    "duration_seconds": 965.11,
+    "total_cost_usd": 2.3653,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 1.7198,
+      "P3_Assembler": 0.6455
+    },
+    "errors": [],
+    "timestamp": "2026-04-19T23:43:42.056388"
+  },
+  {
+    "index": 42,
+    "requirement": "在图片上叠加标题文字,文字大小、粗细、颜色各异,形成层次感强的排版效果——例如大标题用粗体醒目字体,副标题用细体小字,整体风格统一(如深色系商务风或简约设计风)...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:43:56.599559"
+  },
+  {
+    "index": 47,
+    "requirement": "制作流程图/架构示意图:用箭头、方框、层级结构或立体堆叠图形展示系统架构、业务流程或概念层级关系,配合文字标注说明各模块功能,视觉上清晰呈现逻辑关系...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:44:10.038791"
+  },
+  {
+    "index": 50,
+    "requirement": "以统一的视觉主角(如同一个卡通角色、同一个人物、同一主题场景)贯穿多个分格画面,每个格子呈现该主角在不同场景或状态下的样子,配合文字说明形成系列感强的多格叙事版...",
+    "duration_seconds": 235.74,
+    "total_cost_usd": 0.6122,
+    "costs_breakdown": {
+      "P3_Assembler": 0.6122
+    },
+    "errors": [],
+    "timestamp": "2026-04-19T23:44:22.019065"
+  },
+  {
+    "index": 52,
+    "requirement": "在同一画面中合理安排主体与背景的空间关系,让主体(人物、动物、物品)在画面中有明确的视觉焦点,背景简洁或有层次地衬托主体...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:44:24.286119"
+  },
+  {
+    "index": 74,
+    "requirement": "生成具有强烈光影对比的场景图,画面中光源明显(如阳光折射、水面反光、彩虹色光晕),暗部极深、亮部极亮,整体呈现出戏剧性的明暗反差和光线质感...",
+    "duration_seconds": 1609.27,
+    "total_cost_usd": 2.9491,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 1.7028,
+      "P3_Assembler": 1.2463
+    },
+    "errors": [],
+    "timestamp": "2026-04-19T23:55:23.184364"
+  },
+  {
+    "index": 55,
+    "requirement": "生成人物近景半身或胸部以上的画面,突出人物面部表情和情绪,背景适当虚化,让观看者能清楚看到人物的神态与互动感...",
+    "duration_seconds": 647.04,
+    "total_cost_usd": 2.6124,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 1.8646,
+      "P3_Assembler": 0.7478
+    },
+    "errors": [],
+    "timestamp": "2026-04-19T23:55:23.935722"
+  },
+  {
+    "index": 79,
+    "requirement": "在多图拼贴海报上为每个区域叠加带图标的标签(如勾选符号+地点名称),并在整体画面上方添加大标题和副标题文字,形成图文结合的内容合集展示效果...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:55:36.480549"
+  },
+  {
+    "index": 84,
+    "requirement": "生成具有统一色调风格的插画场景,整体画面使用高度协调的单一色系(如全蓝紫色调的火车风景、全粉紫色调的奇幻海洋),让画面中所有元素的颜色都偏向同一个色相,营造出梦...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-19T23:55:49.058460"
+  },
+  {
+    "index": 63,
+    "requirement": "生成具有强烈氛围感的插画风场景图,整体画面以深蓝色调为主,室内外场景都笼罩在宁静的夜色中,窗户透出暖黄色灯光形成冷暖对比,画面质感接近油画或数字绘画风格,传达出...",
+    "duration_seconds": 1907.74,
+    "total_cost_usd": 3.0503,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 2.0063,
+      "P3_Assembler": 1.0439
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T00:00:01.184892"
+  },
+  {
+    "index": 68,
+    "requirement": "制作融合插画风格的信息图文海报:以卡通机器人/科技感插图作为视觉主体,搭配醒目的彩色标题文字和数据图表,整体呈现出活泼又专业的视觉效果...",
+    "duration_seconds": 0.01,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:00:13.735875"
+  },
+  {
+    "index": 57,
+    "requirement": "生成具有强烈透视纵深感的室内空间图,画面中窗框、拱门、地板线条等建筑元素形成明显的空间层次,光线从远处窗口射入,营造出由近到远的视觉延伸效果...",
+    "duration_seconds": 1034.05,
+    "total_cost_usd": 2.7907,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 2.1551,
+      "P3_Assembler": 0.6356
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T00:01:52.838362"
+  },
+  {
+    "index": 62,
+    "requirement": "生成整体色调统一、饱和度偏高的场景图,例如全画面笼罩在深蓝色夜光氛围或浓郁的赤红土地色调中,让单一主色调主导整个画面,营造出沉浸式的强烈色彩氛围感...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:02:04.693859"
+  },
+  {
+    "index": 60,
+    "requirement": "生成具有强烈色彩对比的艺术插画,整体画面以高饱和度的红色与蓝色为主色调,两种颜色形成鲜明的冷暖对撞,背景大面积纯色铺底,视觉冲击力极强...",
+    "duration_seconds": 745.99,
+    "total_cost_usd": 0.8759,
+    "costs_breakdown": {
+      "P3_Assembler": 0.8759
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T00:08:02.457791"
+  },
+  {
+    "index": 65,
+    "requirement": "生成暖色调的室内空间效果图,以米白、浅棕、焦糖色为主色调,光线柔和自然,空间布置温馨舒适,整体画面传达出放松、治愈、生活化的温暖氛围...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:08:15.068429"
+  },
+  {
+    "index": 73,
+    "requirement": "制作图文排版信息图,将多张食材产品图片抠出后整齐排列在统一背景上,配合文字说明组合成内容丰富的科普海报...",
+    "duration_seconds": 747.52,
+    "total_cost_usd": 0.9626,
+    "costs_breakdown": {
+      "P3_Assembler": 0.9626
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T00:12:53.620765"
+  },
+  {
+    "index": 78,
+    "requirement": "制作图文并茂的科普说明卡片,每张卡片包含标题、编号、插图和详细文字说明,整体排版整齐统一,适合分步骤展示教程或知识点...",
+    "duration_seconds": 0.01,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:13:06.026875"
+  },
+  {
+    "index": 83,
+    "requirement": "生成能展示宽广空间感的室内或室外全景图,画面中包含完整的环境纵深,让观看者感受到场景的整体规模和空间层次...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:13:17.823353"
+  },
+  {
+    "index": 70,
+    "requirement": "将动物(如猫咪)与各种食物、道具进行创意合成,给动物添加配饰(帽子、假发、领带等)并嵌入食物场景中,搭配谐音梗或双关文字,制作出拟人化角色扮演的趣味表情包图片...",
+    "duration_seconds": 592.71,
+    "total_cost_usd": 0.7538,
+    "costs_breakdown": {
+      "P3_Assembler": 0.7538
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T00:18:19.694807"
+  },
+  {
+    "index": 89,
+    "requirement": "生成融合东方传统与现代简约的室内空间效果图:以米白、暖棕为主色调,加入拱形门洞、藤编元素、中式花卉装饰画等传统细节,整体呈现温润雅致的新中式生活美学氛围...",
+    "duration_seconds": 1395.81,
+    "total_cost_usd": 3.8001,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 2.5269,
+      "P3_Assembler": 1.2732
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T00:19:17.007559"
+  },
+  {
+    "index": 94,
+    "requirement": "生成室内暖光氛围图,画面中多个光源(吊灯、筒灯、窗外自然光)共同营造出温暖柔和的米色调空间,光线从不同方向照射,形成层次丰富的软阴影,整体氛围温馨舒适...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:19:28.703944"
+  },
+  {
+    "index": 75,
+    "requirement": "生成带有明显颗粒感或纸张纹理的插画风格图片,画面整体像是印刷在粗糙介质上,物体表面有细腻的颗粒噪点或手工绘制的笔触肌理...",
+    "duration_seconds": 218.06,
+    "total_cost_usd": 0.5543,
+    "costs_breakdown": {
+      "P3_Assembler": 0.5543
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T00:22:10.044854"
+  },
+  {
+    "index": 67,
+    "requirement": "生成3D卡通风格的拟人化动物角色,角色具有毛绒质感和丰富的表情神态,能够在不同生活场景(办公室、卧室、户外)中呈现出喜怒哀乐等情绪状态,整体风格类似皮克斯动画...",
+    "duration_seconds": 1470.8,
+    "total_cost_usd": 4.0586,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 3.405,
+      "P3_Assembler": 0.6536
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T00:26:47.583506"
+  },
+  {
+    "index": 80,
+    "requirement": "为多人物展示海报中的每个人物添加姓名和职位标签,并在画面顶部叠加活动主题、专场名称等层级分明的标题文字,整体风格统一、信息密度高...",
+    "duration_seconds": 283.92,
+    "total_cost_usd": 0.6949,
+    "costs_breakdown": {
+      "P3_Assembler": 0.6949
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T00:27:05.698868"
+  },
+  {
+    "index": 88,
+    "requirement": "制作科技感强烈的活动宣传海报:使用深色背景配合橙色、蓝色等高对比度霓虹色调,融合未来感城市或科技场景插图,搭配大号粗体标题文字,整体呈现出硬核、前沿的视觉气质...",
+    "duration_seconds": 1022.93,
+    "total_cost_usd": 0.9975,
+    "costs_breakdown": {
+      "P3_Assembler": 0.9975
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T00:30:32.632520"
+  },
+  {
+    "index": 85,
+    "requirement": "生成色彩鲜艳、多色并置的视觉冲击画面,画面中同时出现多种高饱和度的颜色搭配(如复古拼贴风格中的粉色、蓝色、橙色并置,或彩色条纹波浪地形),让整体色彩浓烈饱满、视...",
+    "duration_seconds": 629.99,
+    "total_cost_usd": 0.8028,
+    "costs_breakdown": {
+      "P3_Assembler": 0.8028
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T00:37:47.894960"
+  },
+  {
+    "index": 90,
+    "requirement": "生成拟人化动物角色表情包:用AI生成具有丰富表情和情绪的卡通动物形象(如毛茸茸的红色马、灰色驴),能够呈现出沮丧、无奈、委屈等多种情绪状态,配合不同场景背景(办...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:38:00.120293"
+  },
+  {
+    "index": 72,
+    "requirement": "将真实照片转换成具有统一色调风格的插画效果,整体呈现蓝紫色调的复古油画或动画风格,让风景场景看起来像艺术插图...",
+    "duration_seconds": 807.81,
+    "total_cost_usd": 2.067,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 1.5767,
+      "P3_Assembler": 0.4903
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T00:40:27.634511"
+  },
+  {
+    "index": 31,
+    "requirement": "给普通猫咪照片套上不同职业的服装和场景(如医生、上班族、老板等),并保持猫咪面部表情清晰可辨,制作出系列表情包拼贴图...",
+    "duration_seconds": 4443.65,
+    "total_cost_usd": 8.8153,
+    "costs_breakdown": {
+      "P3_Assembler": 8.8153
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T00:40:37.379387"
+  },
+  {
+    "index": 77,
+    "requirement": "在图片上叠加对话气泡或多行说明文字,用于讲述故事背景或补充情节说明,文字带有描边或阴影效果以确保在复杂背景上清晰可读...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:40:39.891530"
+  },
+  {
+    "index": 36,
+    "requirement": "生成同一人物在同一场景中多角度、多姿态的即时抓拍效果图,画面呈现自然随意的动态感,如行走、转身、低头、仰望等非摆拍状态,整体风格真实生活化...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:40:50.537093"
+  },
+  {
+    "index": 41,
+    "requirement": "制作信息图文海报时,需要将大标题、分类小标题与正文段落按照清晰的层级排布在版面上,标题用大字醒目展示,正文紧跟其下,整体版面分区明确、图文对应,让读者能快速扫读...",
+    "duration_seconds": 0.01,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:41:02.965722"
+  },
+  {
+    "index": 46,
+    "requirement": "制作数据报告类图文内容:包含柱状图、饼图、折线图、词云图、环形图等多种数据可视化图表,配合标题、要点文字说明,整体呈现专业研究报告的视觉风格,色彩搭配统一(如蓝...",
+    "duration_seconds": 277.25,
+    "total_cost_usd": 0.7766,
+    "costs_breakdown": {
+      "P3_Assembler": 0.7766
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T00:45:52.168163"
+  },
+  {
+    "index": 51,
+    "requirement": "将多张图片按网格或分区方式拼贴成一张图,每个区域展示不同角度或不同场景,整体画面有清晰的分割感和节奏感...",
+    "duration_seconds": 0.01,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:46:04.715701"
+  },
+  {
+    "index": 56,
+    "requirement": "生成产品或物品的极近距离特写图,如食物截面、商品细节、小物件放大展示,画面主体占满画幅,质感和纹理清晰突出...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:46:16.677016"
+  },
+  {
+    "index": 61,
+    "requirement": "在以暗色或单色为主的画面中,用局部的高饱和亮色(如红色心脏、橙色暖光窗口、金黄色星光)作为点睛之笔,让视线自然聚焦到这个色彩亮点上,形成强烈的视觉引导...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:46:28.556416"
+  },
+  {
+    "index": 93,
+    "requirement": "生成具有强烈戏剧性光影对比的户外场景图,画面中光源方向明确(如侧光或逆光),亮部与暗部之间形成鲜明反差,阴影轮廓清晰,整体呈现出电影感或艺术摄影风格的视觉张力...",
+    "duration_seconds": 1232.33,
+    "total_cost_usd": 2.0039,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 2.0039,
+      "P3_Assembler": 0.0
+    },
+    "errors": [
+      "P3_Assembler Failed: Client error '402 Payment Required' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402",
+      "P3_Assembler crashed: HTTPStatusError: Client error '402 Payment Required' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402"
+    ],
+    "timestamp": "2026-04-20T00:51:16.949336"
+  },
+  {
+    "index": 95,
+    "requirement": "生成充满魔幻或超现实感的彩色光效场景,画面中有多种颜色的光线(如橙、蓝、紫等)交织流动,光源本身成为视觉焦点,整体营造出梦幻、神秘或节日感的强烈氛围...",
+    "duration_seconds": 789.26,
+    "total_cost_usd": 1.8366,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 1.8366,
+      "P3_Assembler": 0.0
+    },
+    "errors": [
+      "P3_Assembler Failed: Client error '402 Payment Required' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402",
+      "P3_Assembler crashed: HTTPStatusError: Client error '402 Payment Required' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402"
+    ],
+    "timestamp": "2026-04-20T00:51:22.257660"
+  },
+  {
+    "index": 98,
+    "requirement": "制作多宫格拼贴式内容图,每个格子内有大标题文字突出显示核心信息(如价格、品类名),配合产品图或场景图,标题字号远大于说明文字,形成层次分明的视觉结构...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:51:28.832955"
+  },
+  {
+    "index": 66,
+    "requirement": "生成具有强烈视觉冲击力的超现实主义风格图像:画面以大地色系(深红、赭石、深蓝)为主调,将白马、牛仔等元素置于极简的荒漠/盐湖场景中,营造出孤寂、神秘、如油画般的...",
+    "duration_seconds": 335.16,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 0.0,
+      "P3_Assembler": 0.0
+    },
+    "errors": [
+      "P2_ExtractCaps Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P2_ExtractCaps crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P2_ExtractCaps Recovery Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P2_ExtractCaps recovery crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P3_Assembler Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P3_Assembler crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403"
+    ],
+    "timestamp": "2026-04-20T00:52:15.682630"
+  },
+  {
+    "index": 82,
+    "requirement": "生成采用非常规拍摄角度的图片,如从低角度仰拍、从高处俯视、或模拟第一人称主观视角看向场景,让画面产生独特的视觉冲击感...",
+    "duration_seconds": 687.45,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {
+      "P3_Assembler": 0.0
+    },
+    "errors": [
+      "P3_Assembler Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P3_Assembler crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P3_Assembler Recovery Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P3_Assembler recovery crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403"
+    ],
+    "timestamp": "2026-04-20T00:52:19.932967"
+  },
+  {
+    "index": 71,
+    "requirement": "给同一张猫咪照片批量添加不同职业的帽子、道具和配件(如厨师帽、安全帽、眼镜、画板等),让猫咪看起来像在扮演各种职业角色...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:52:28.515467"
+  },
+  {
+    "index": 82,
+    "requirement": "生成采用非常规拍摄角度的图片,如从低角度仰拍、从高处俯视、或模拟第一人称主观视角看向场景,让画面产生独特的视觉冲击感...",
+    "duration_seconds": 3.25,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {
+      "P3_Assembler": 0.0
+    },
+    "errors": [
+      "P3_Assembler Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P3_Assembler crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P3_Assembler Recovery Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P3_Assembler recovery crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403"
+    ],
+    "timestamp": "2026-04-20T00:52:34.372996"
+  },
+  {
+    "index": 76,
+    "requirement": "生成室内场景时,能真实还原不同材质的质感细节,如木地板的纹路光泽、布艺沙发的绒毛感、大理石茶几的光滑反射、藤编家具的编织纹理等...",
+    "duration_seconds": 5.24,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 0.0,
+      "P3_Assembler": 0.0
+    },
+    "errors": [
+      "P2_ExtractCaps Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P2_ExtractCaps crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P2_ExtractCaps Recovery Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P2_ExtractCaps recovery crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P3_Assembler Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P3_Assembler crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403"
+    ],
+    "timestamp": "2026-04-20T00:52:46.323049"
+  },
+  {
+    "index": 87,
+    "requirement": "生成超现实浪漫场景图:将人物置于不可能存在的宏大环境中,如站在地球边缘俯瞰星空、坐在云端长椅上漂浮、在星海上骑行,画面充满梦幻感和史诗级视觉冲击力...",
+    "duration_seconds": 4.72,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 0.0,
+      "P3_Assembler": 0.0
+    },
+    "errors": [
+      "P2_ExtractCaps Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P2_ExtractCaps crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P2_ExtractCaps Recovery Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P2_ExtractCaps recovery crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P3_Assembler Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P3_Assembler crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403"
+    ],
+    "timestamp": "2026-04-20T00:52:52.015331"
+  },
+  {
+    "index": 81,
+    "requirement": "对同一场景或主体生成多个不同距离和景别的画面,包括远景展示整体环境、中景呈现主体与环境关系、近景突出细节,形成一组视角丰富的图片集合...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:52:59.111531"
+  },
+  {
+    "index": 92,
+    "requirement": "生成室内空间效果图:用AI渲染出具有温暖奶油色调的室内场景,包含拱形门洞、藤编家具、自然光影等元素,整体呈现出地中海或法式复古风格的高质感室内设计效果,光线柔和...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:53:05.127079"
+  },
+  {
+    "index": 86,
+    "requirement": "生成低饱和度或去色风格的极简画面,整体色彩纯度降低,呈现出克制、安静的视觉质感(如黑白灰调的海洋孤舟场景,或接近无彩色的素雅插画),与高饱和度画面形成鲜明对比...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:53:12.479377"
+  },
+  {
+    "index": 97,
+    "requirement": "在图片上叠加大字幕文字,字体粗大醒目,常带有描边或阴影效果,文字直接覆盖在照片或场景图上,起到强调说明或搞笑点评的作用...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:53:18.172908"
+  },
+  {
+    "index": 91,
+    "requirement": "制作图文混排的知识科普长图:以深青色/蓝绿色为底色背景,将心理学等知识内容拆分为多个板块,每个板块搭配风格统一的插画小图(奇幻风格人物、动物等),文字与插图穿插...",
+    "duration_seconds": 0.0,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {},
+    "errors": [],
+    "timestamp": "2026-04-20T00:53:24.730483"
+  },
+  {
+    "index": 96,
+    "requirement": "制作大字号标题搭配正文内容的图文排版,标题文字极大且颜色鲜艳(红色、黄色等高饱和色),与正文小字形成强烈的大小对比,整体版面信息密度高、视觉冲击力强...",
+    "duration_seconds": 4.96,
+    "total_cost_usd": 0.0,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 0.0,
+      "P3_Assembler": 0.0
+    },
+    "errors": [
+      "P2_ExtractCaps Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P2_ExtractCaps crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P2_ExtractCaps Recovery Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P2_ExtractCaps recovery crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P3_Assembler Failed: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
+      "P3_Assembler crashed: HTTPStatusError: Client error '403 Forbidden' for url 'https://openrouter.ai/api/v1/messages'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403"
+    ],
+    "timestamp": "2026-04-20T00:53:41.584201"
+  },
+  {
+    "index": 87,
+    "requirement": "生成超现实浪漫场景图:将人物置于不可能存在的宏大环境中,如站在地球边缘俯瞰星空、坐在云端长椅上漂浮、在星海上骑行,画面充满梦幻感和史诗级视觉冲击力...",
+    "duration_seconds": 989.29,
+    "total_cost_usd": 3.1837,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 2.4937,
+      "P3_Assembler": 0.6899
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T08:19:27.343152"
+  },
+  {
+    "index": 66,
+    "requirement": "生成具有强烈视觉冲击力的超现实主义风格图像:画面以大地色系(深红、赭石、深蓝)为主调,将白马、牛仔等元素置于极简的荒漠/盐湖场景中,营造出孤寂、神秘、如油画般的...",
+    "duration_seconds": 1211.44,
+    "total_cost_usd": 2.7678,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 2.0586,
+      "P3_Assembler": 0.7091
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T11:31:11.819849"
+  },
+  {
+    "index": 76,
+    "requirement": "生成室内场景时,能真实还原不同材质的质感细节,如木地板的纹路光泽、布艺沙发的绒毛感、大理石茶几的光滑反射、藤编家具的编织纹理等...",
+    "duration_seconds": 1597.66,
+    "total_cost_usd": 2.9276,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 2.0551,
+      "P3_Assembler": 0.8725
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T11:58:01.549057"
+  },
+  {
+    "index": 82,
+    "requirement": "生成采用非常规拍摄角度的图片,如从低角度仰拍、从高处俯视、或模拟第一人称主观视角看向场景,让画面产生独特的视觉冲击感...",
+    "duration_seconds": 1310.55,
+    "total_cost_usd": 1.5389,
+    "costs_breakdown": {
+      "P3_Assembler": 1.5389
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T12:20:03.529782"
+  },
+  {
+    "index": 96,
+    "requirement": "制作大字号标题搭配正文内容的图文排版,标题文字极大且颜色鲜艳(红色、黄色等高饱和色),与正文小字形成强烈的大小对比,整体版面信息密度高、视觉冲击力强...",
+    "duration_seconds": 2229.75,
+    "total_cost_usd": 3.2993,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 2.128,
+      "P3_Assembler": 1.1713
+    },
+    "errors": [],
+    "timestamp": "2026-04-20T12:57:24.642731"
+  },
+  {
+    "index": 15,
+    "requirement": "生成手部持握或展示物品的特写画面,手势自然,物品清晰呈现,如用手托举饺子、手持卡片、手握手机等,突出手与物品的互动关系...",
+    "duration_seconds": 705.24,
+    "total_cost_usd": 0.8887,
+    "costs_breakdown": {
+      "P3_Assembler": 0.8887
+    },
+    "errors": [],
+    "timestamp": "2026-04-21T10:54:46.797482"
+  },
+  {
+    "index": 33,
+    "requirement": "生成真实户外场景中的人物活动照片,画面要呈现自然光线下的街道、公园、游乐场等具体地点环境,人物动作自然生动,背景环境细节丰富真实...",
+    "duration_seconds": 732.69,
+    "total_cost_usd": 0.8328,
+    "costs_breakdown": {
+      "P3_Assembler": 0.8328
+    },
+    "errors": [],
+    "timestamp": "2026-04-21T10:55:23.908823"
+  },
+  {
+    "index": 98,
+    "requirement": "制作多宫格拼贴式内容图,每个格子内有大标题文字突出显示核心信息(如价格、品类名),配合产品图或场景图,标题字号远大于说明文字,形成层次分明的视觉结构...",
+    "duration_seconds": 946.26,
+    "total_cost_usd": 1.0342,
+    "costs_breakdown": {
+      "P3_Assembler": 1.0342
+    },
+    "errors": [],
+    "timestamp": "2026-04-21T10:59:20.860781"
+  },
+  {
+    "index": 53,
+    "requirement": "生成包含多个独立小格子的图文排版画面,每个格子内有图片和文字说明,格子之间疏密有致、整齐排列,整体呈现信息图表或内容合集的视觉效果...",
+    "duration_seconds": 975.14,
+    "total_cost_usd": 0.6321,
+    "costs_breakdown": {
+      "P3_Assembler": 0.6321
+    },
+    "errors": [],
+    "timestamp": "2026-04-21T10:59:34.305244"
+  },
+  {
+    "index": 54,
+    "requirement": "生成人物局部特写画面,如放大呈现嘴巴咬食物、手持物品、耳朵佩戴饰品、鼻子、指甲等身体局部细节,画面填充感强,细节清晰可见...",
+    "duration_seconds": 998.61,
+    "total_cost_usd": 0.8328,
+    "costs_breakdown": {
+      "P3_Assembler": 0.8328
+    },
+    "errors": [],
+    "timestamp": "2026-04-21T11:00:05.337261"
+  },
+  {
+    "index": 13,
+    "requirement": "生成以深色(黑色/深蓝/深紫)为背景底色的海报,搭配霓虹感彩色光效(橙、紫、青等),营造出科技感强烈的冷暖对比配色效果...",
+    "duration_seconds": 1646.63,
+    "total_cost_usd": 2.8809,
+    "costs_breakdown": {
+      "P2_ExtractCaps": 1.8683,
+      "P3_Assembler": 1.0126
+    },
+    "errors": [],
+    "timestamp": "2026-04-21T11:08:42.407784"
+  }
+]

+ 569 - 0
examples/process_pipeline/run_pipeline.py

@@ -0,0 +1,569 @@
+"""
+V4 Pipeline: Hardcoded Map-Reduce Orchestration for AIGC Process Research
+"""
+import argparse
+import asyncio
+import json
+import sys
+import time
+from datetime import datetime
+from pathlib import Path
+
+# Add project root to path
+sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+from dotenv import load_dotenv
+load_dotenv()
+
+from agent.llm.prompts import SimplePrompt
+from agent.core.runner import AgentRunner, RunConfig
+from agent.tools.builtin.knowledge import KnowledgeConfig
+from agent.trace import FileSystemTraceStore, Trace, Message
+from agent.llm import create_qwen_llm_call
+from agent.llm.openrouter import create_openrouter_llm_call
+from agent.llm.claude import create_claude_llm_call
+
+# config from existing setup
+from examples.process_research.config import (
+    OUTPUT_DIR, TRACE_STORE_PATH, SKILLS_DIR, LOG_LEVEL, LOG_FILE,
+    BROWSER_TYPE, HEADLESS, COORDINATOR_RUN_CONFIG
+)
+from agent.utils import setup_logging
+
+async def run_agent_task(runner: AgentRunner, prompt_name: str, kwargs: dict, task_name: str, model_name: str):
+    base_dir = Path(__file__).parent
+    prompt_path = base_dir / "prompts" / f"{prompt_name}.prompt"
+    prompt = SimplePrompt(prompt_path)
+    
+    messages = prompt.build_messages(**kwargs)
+    target_tools = []
+    if prompt_name == "extract_capabilities":
+        target_tools = ["capability_search", "capability_list", "tool_search"]
+
+    # 按 agent 类型配置工具组权限
+    tool_groups_map = {
+        "researcher": ["core", "content"],           # 搜索+文件,无浏览器
+        "filter_and_blueprint": ["core"],             # 只需文件读写
+        "extract_capabilities": ["core"],             # 只需文件读写(额外工具由 target_tools 补充)
+        "assemble_strategy": ["core"],                # 只需文件读写
+    }
+
+    run_config = RunConfig(
+        model=prompt.config.get("model") or model_name,
+        temperature=prompt.config.get("temperature") or 0.3,
+        name=task_name,
+        agent_type=prompt_name,
+        tools=target_tools,
+        tool_groups=tool_groups_map.get(prompt_name, ["core"]),
+        knowledge=KnowledgeConfig(enable_completion_extraction=False, enable_extraction=False, enable_injection=False)
+    )
+    
+    task_cost = 0.0
+    task_errors = []
+    last_trace_id = None
+    
+    print(f"🚀 [Launch] {task_name}")
+    try:
+        async for item in runner.run(messages=messages, config=run_config):
+            if isinstance(item, Trace):
+                last_trace_id = item.trace_id
+                if item.status == "completed":
+                    print(f"✅ [Done] {task_name} (Cost: ${item.total_cost:.4f})")
+                    task_cost = item.total_cost
+                elif item.status == "failed":
+                    print(f"❌ [Fail] {task_name}: {item.error_message}")
+                    task_errors.append(f"{task_name} Failed: {item.error_message}")
+            if isinstance(item, Message):
+                if item.role == "tool":
+                    content = item.content if isinstance(item.content, dict) else {}
+                    t_name = content.get("tool_name", "unknown")
+                    if t_name in ("write_file", "write_json"):
+                        print(f"   💾 [File Written by {task_name}]")
+    except Exception as e:
+        err_msg = f"{type(e).__name__}: {e}"
+        print(f"❌ [Exception] {task_name} crashed: {err_msg}")
+        task_errors.append(f"{task_name} crashed: {err_msg}")
+                    
+    # Verification & Recovery block
+    out_file = kwargs.get("output_file")
+    if out_file and not Path(out_file).exists() and last_trace_id:
+        print(f"⚠️ [Recovery] {task_name} missing output file. Triggering forced wrap-up continuation...")
+        recovery_messages = [{
+            "role": "user", 
+            "content": f"【系统强制指令】你的任务阶段已终止,但尚未将结果写入文件。请立刻调用 write_json 工具,将你目前已经搜集或处理到的原生结构化内容直接作为 json_data 参数对象写入到绝对路径 `{out_file}`,如果搜集失败也请写入空的总结对象。必须立刻执行写入!"
+        }]
+        rec_config = RunConfig(
+            model=model_name,
+            temperature=0.1,
+            name=task_name + "_Rec",
+            agent_type=prompt_name,
+            trace_id=last_trace_id,
+            knowledge=KnowledgeConfig(enable_completion_extraction=False, enable_extraction=False, enable_injection=False)
+        )
+        try:
+            async for r_item in runner.run(messages=recovery_messages, config=rec_config):
+                if isinstance(r_item, Trace):
+                    if r_item.status == "completed":
+                        task_cost += r_item.total_cost
+                    elif r_item.status == "failed":
+                        task_errors.append(f"{task_name} Recovery Failed: {r_item.error_message}")
+                if isinstance(r_item, Message) and r_item.role == "tool":
+                    content = r_item.content if isinstance(r_item.content, dict) else {}
+                    if content.get("tool_name") in ("write_file", "write_json"):
+                        print(f"   💾 [Recovery File Written by {task_name}]")
+        except Exception as e:
+            err_msg = f"{type(e).__name__}: {e}"
+            print(f"❌ [Exception Recovery] {task_name} crashed: {err_msg}")
+            task_errors.append(f"{task_name} recovery crashed: {err_msg}")
+                    
+    return task_cost, task_errors
+
+async def run_anthropic_sdk_task(prompt_name: str, kwargs: dict, task_name: str, model_name: str):
+    """
+    备用:使用纯净的官方 Anthropic SDK 驱动。
+    跳过内部大架构的 trace 追踪,但保留对原有 Python 工具库(如 write_file/glob_files)的无缝调用。
+    """
+    from anthropic import AsyncAnthropic
+    from agent.tools.registry import get_tool_registry
+    
+    base_dir = Path(__file__).parent
+    prompt_path = base_dir / "prompts" / f"{prompt_name}.prompt"
+    prompt = SimplePrompt(prompt_path)
+    
+    # 1. 组装输入 Message
+    raw_messages = prompt.build_messages(**kwargs)
+    system_prompt = ""
+    messages = []
+    for msg in raw_messages:
+        if msg["role"] == "system":
+            system_prompt += msg["content"] + "\n\n"
+        else:
+            messages.append({"role": msg["role"], "content": msg["content"]})
+            
+    # 2. 映射目标工具
+    target_tools = ["write_file", "write_json", "read_file", "glob_files"]
+    if prompt_name == "extract_capabilities":
+        target_tools.extend(["capability_search", "capability_list", "tool_search"])
+        
+    registry = get_tool_registry()
+    schemas = registry.get_schemas(target_tools)
+    anthropic_tools = []
+    for s in schemas:
+        anthropic_tools.append({
+            "name": s["function"]["name"],
+            "description": s["function"].get("description", ""),
+            "input_schema": s["function"]["parameters"]
+        })
+        
+    # 3. 初始化并开启 Loop
+    # 提示:你需要在你的终端中配置好 ANTHROPIC_API_KEY 环境变量
+    client = AsyncAnthropic()  
+    task_cost = 0.0
+    task_errors = []
+    print(f"🚀 [Launch Anthropic SDK] {task_name}")
+    
+    max_loops = 50
+    for loop_idx in range(max_loops):
+        try:
+            # 去除前缀(兼容比如 openrouter 传入的名字)
+            clean_model = model_name.split("/")[-1] if "/" in model_name else model_name
+            
+            # 这里专门将实际请求映射到其可用的特殊别名 claude-sonnet-4-5 
+            target_model = "claude-sonnet-4-5" if "claude" in clean_model else clean_model
+
+            response = await client.messages.create(
+                model=target_model,
+                max_tokens=4096,
+                temperature=0.2,
+                system=system_prompt,
+                messages=messages,
+                tools=anthropic_tools
+            )
+            
+            # (简略预估,不代表真实官方开销)
+            if hasattr(response, 'usage'):
+                step_cost = (response.usage.input_tokens / 1e6 * 3.0) + (response.usage.output_tokens / 1e6 * 15.0)
+                task_cost += step_cost
+            
+            # 加入助手回复
+            assistant_content = []
+            tool_uses = []
+            
+            for content_block in response.content:
+                if content_block.type == "text":
+                    text_val = content_block.text
+                    if text_val:
+                        assistant_content.append({"type": "text", "text": text_val})
+                        print(f"\n🤖 [{task_name} Output]:\n{text_val}\n")
+                elif content_block.type == "tool_use":
+                    assistant_content.append({
+                        "type": "tool_use",
+                        "id": content_block.id,
+                        "name": content_block.name,
+                        "input": content_block.input
+                    })
+                    tool_uses.append(content_block)
+            
+            if not assistant_content:
+                assistant_content.append({"type": "text", "text": "(Thinking completed but no output)"})
+                
+            messages.append({"role": "assistant", "content": assistant_content})
+            
+            # 出口:没有调用工具说明任务结束
+            if not tool_uses:
+                print(f"✅ [Done Anthropic SDK] {task_name} (Cost: ${task_cost:.4f})")
+                break
+                
+            # 工具执行与回传
+            tool_results = []
+            for tu in tool_uses:
+                if tu.name in ("write_file", "write_json"):
+                    print(f"   💾 [File Written by SDK] {task_name}")
+                
+                print(f"   🛠️ [Tool Exec Debug] name_is={tu.name}, input_is={tu.input}, type_is={type(tu.input)}")
+                # 执行本地环境的函数
+                result_str = await registry.execute(tu.name, tu.input)
+                
+                tool_results.append({
+                    "type": "tool_result",
+                    "tool_use_id": tu.id,
+                    "content": result_str
+                })
+                
+            messages.append({"role": "user", "content": tool_results})
+            
+        except Exception as e:
+            err_msg = str(e)
+            print(f"❌ [Fail SDK Core] {task_name}: {err_msg}")
+            task_errors.append(err_msg)
+            break
+            
+    # Verification & Recovery block for SDK (porting from AgentRunner)
+    out_file = kwargs.get("output_file")
+    if out_file and not Path(out_file).exists():
+        print(f"⚠️ [Recovery SDK] {task_name} missing output file. Triggering forced wrap-up continuation...")
+        messages.append({
+            "role": "user", 
+            "content": f"【系统强制指令】你的任务阶段已完成分析,但尚未将最终结果写入目标文件。请立刻调用 write_json (或 write_file) 工具,将你的成果数据直接写入到绝对路径 `{out_file}`,务必立刻执行写入动作!"
+        })
+        try:
+            target_model = "claude-sonnet-4-5" if "claude" in clean_model else clean_model
+            rec_response = await client.messages.create(
+                model=target_model,
+                max_tokens=4096,
+                temperature=0.1,
+                system=system_prompt,
+                messages=messages,
+                tools=anthropic_tools,
+                tool_choice={"type": "any"}
+            )
+            for content_block in rec_response.content:
+                if content_block.type == "tool_use":
+                    if content_block.name in ("write_file", "write_json"):
+                        print(f"   💾 [Recovery File Written by SDK] {task_name}")
+                    await registry.execute(content_block.name, content_block.input)
+        except Exception as e:
+            print(f"❌ [Fail SDK Recovery] {task_name}: {e}")
+            task_errors.append(str(e))
+            
+    return task_cost, task_errors
+
+async def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--index", type=int, required=True, help="Index of requirement in db_requirements.json")
+    parser.add_argument("--skip-research", action="store_true", help="Skip Phase 1 and use existing raw cases")
+    parser.add_argument("--research-only", action="store_true", help="Only run research phases, skip Phase 2 and 3")
+    parser.add_argument("--platforms", type=str, default="xhs,youtube,bili,x", help="Comma-separated list of platforms to search")
+    parser.add_argument("--use-claude-sdk", action="store_true", help="Use pure Anthropic SDK (run_anthropic_sdk_task) instead of internal AgentRunner for Phase 2/3")
+    args = parser.parse_args()
+
+    base_dir = Path(__file__).parent
+    # Load requirements locally
+    req_path = base_dir / "db_requirements.json"
+    
+    with open(req_path, encoding='utf-8') as f:
+        reqs = json.load(f)
+    
+    if args.index < 0 or args.index >= len(reqs):
+        print("Index out of bounds")
+        sys.exit(1)
+        
+    requirement = reqs[args.index]
+    
+    # 0. Setup directories
+    output_dir = base_dir / "output" / f"{(args.index+1):03d}"
+    output_dir.mkdir(parents=True, exist_ok=True)
+    raw_cases_dir = output_dir / "raw_cases"
+    raw_cases_dir.mkdir(parents=True, exist_ok=True)
+    
+    setup_logging(level=LOG_LEVEL, file=LOG_FILE)
+    
+    print("=" * 60)
+    print(f"V4 Hardcoded Pipeline | Demand: [{args.index+1:03d}] {requirement[:40]}...")
+    print("=" * 60)
+
+    # Load presets
+    presets_path = base_dir / "presets.json"
+    if presets_path.exists():
+        from agent.core.presets import load_presets_from_json
+        load_presets_from_json(str(presets_path))
+        print("✅ Configured Agent Presets (Skills Boundaries)")
+
+    # Browser initialization removed to save resources
+    
+    store = FileSystemTraceStore(base_path=TRACE_STORE_PATH)
+
+    # Instantiate two distinct LLM orchestrators
+    qwen_model = "qwen3.5-plus"  # maps to qwen3.5-plus via Qwen interface
+    
+    # 用户指示使用 OpenRouter 代理的 Claude 4.5
+    claude_model = "anthropic/claude-4.5-sonnet"
+    args.use_claude_sdk = False  # 禁用纯 Native SDK 模式,走内部通用 AgentRunner (即可对接 OpenRouter)
+    from agent.llm.openrouter import create_openrouter_llm_call
+    claude_llm_call = create_openrouter_llm_call(model=claude_model)
+    
+    runner_qwen = AgentRunner(
+        trace_store=store,
+        llm_call=create_qwen_llm_call(model=qwen_model),
+        skills_dir=SKILLS_DIR
+    )
+    
+    runner_claude = AgentRunner(
+        trace_store=store,
+        llm_call=claude_llm_call,
+        skills_dir=SKILLS_DIR
+    )
+
+    try:
+        start_time = time.time()
+        total_cost = 0.0
+        costs_breakdown = {}
+        global_errors = []
+        strategy_file = None
+        
+        existing_platforms = []
+        if raw_cases_dir.exists():
+            for f in raw_cases_dir.glob("case_*.json"):
+                plat = f.stem.replace("case_", "")
+                if plat in ["xhs", "youtube", "bili", "x", "zhihu", "gzh"]:
+                    existing_platforms.append(plat)
+                    
+        platforms = [p.strip() for p in args.platforms.split(",") if p.strip()]
+        needed_count = max(0, 4 - len(existing_platforms))
+        
+        # Phase 0: Dynamic Routing
+        if not args.skip_research:
+            if needed_count == 0:
+                print(f"\n--- Phase 0: Skipping Routing (Already have {len(existing_platforms)} existing cases: {existing_platforms}) ---")
+                platforms = []
+            else:
+                print(f"\n--- Phase 0: Dynamic Platform Routing ({qwen_model}) ---")
+                print(f"📡 Found existing cases: {existing_platforms}. Requesting {needed_count} new platforms...")
+                try:
+                    router_prompt = SimplePrompt(base_dir / "prompts" / "router.prompt")
+                    rmessages = router_prompt.build_messages(
+                        requirement=requirement,
+                        existing_platforms=",".join(existing_platforms) if existing_platforms else "无",
+                        needed_count=str(needed_count)
+                    )
+                    rconfig = RunConfig(
+                        model=qwen_model,
+                        temperature=0.1,
+                        name="P0_Router",
+                        agent_type="router",
+                        knowledge=KnowledgeConfig(enable_completion_extraction=False, enable_extraction=False, enable_injection=False)
+                    )
+                    print(f"🚀 [Launch] P0_Router calculating optimal platforms...")
+                    
+                    router_response = ""
+                    async for item in runner_qwen.run(messages=rmessages, config=rconfig):
+                        if isinstance(item, Message) and item.role == "assistant" and isinstance(item.content, dict):
+                            text = item.content.get("text", "")
+                            if text and not item.content.get("tool_calls"):
+                                router_response = text
+                        if isinstance(item, Trace) and item.status == "completed":
+                            total_cost += item.total_cost
+                            costs_breakdown["P0_Router"] = round(item.total_cost, 4)
+                            
+                    if router_response:
+                        import re
+                        # Extract all alphabetic words from the response to handle extra text or markdown
+                        words = set(re.findall(r'\b[a-z]+\b', router_response.lower()))
+                        valid_platforms = ["xhs", "youtube", "bili", "x", "zhihu", "gzh"]
+                        # Intersect words with valid platforms (direct exact word matching, exclude existing)
+                        final_platforms = [p for p in valid_platforms if p in words and p not in existing_platforms]
+                        
+                        if final_platforms:
+                            platforms = final_platforms[:needed_count]
+                            print(f"🎯 [Router Decision] Selected {len(platforms)} new platforms: {platforms}")
+                        else:
+                            platforms = [p for p in platforms if p not in existing_platforms][:needed_count]
+                            print(f"⚠️ [Router Fallback] Invalid output '{router_response}'. Using delta default: {platforms}")
+                except Exception as e:
+                    platforms = [p for p in platforms if p not in existing_platforms][:needed_count]
+                    print(f"⚠️ [Router Logic Failed] Using delta default platforms ({platforms}). Error: {e}")
+
+        # Phase 1: MAP (Parallel Search) uses Qwen
+        if not args.skip_research:
+            print(f"\n--- Phase 1: Distributed Research Map ({qwen_model}) ---")
+            phase1_tasks = []
+            for p in platforms:
+                task_desc = f"渠道:{p.upper()}。核心需求:{requirement}"
+                out_file = str(raw_cases_dir / f"case_{p}.json")
+                kwargs = {
+                    "task": task_desc,
+                    "output_file": out_file
+                }
+                phase1_tasks.append(run_agent_task(runner_qwen, "researcher", kwargs, f"P1_Research_{p}", qwen_model))
+                
+            phase1_results = await asyncio.gather(*phase1_tasks)
+            for (task_cost, task_errors), p in zip(phase1_results, platforms):
+                total_cost += task_cost
+                costs_breakdown[f"P1_Research_{p}"] = round(task_cost, 4)
+                global_errors.extend(task_errors)
+                
+                # Check if cases actually got written
+                expected_file = Path(raw_cases_dir / f"case_{p}.json")
+                if not expected_file.exists():
+                    err_msg = f"Missing case file for {p}! Agent likely hit max_iterations without saving."
+                    print(f"⚠️ [Warning] {err_msg}")
+                    global_errors.append(err_msg)
+        else:
+            print("\n⏭️  [Skip] Phase 1 Skipped via --skip-research. Using existing cases...")
+
+        # Phase 2: REDUCE 1 (Parallel Distillation) uses Claude
+        if not args.research_only:
+            # Phase 2: REDUCE 1 (Parallel Distillation) uses Claude
+            print(f"\n--- Phase 2: Parallel Distillation ({claude_model}) ---")
+            blueprint_file = str(output_dir / "blueprint.json")
+            capabilities_file = str(output_dir / "capabilities_extracted.json")
+            raw_glob = str(raw_cases_dir / "case_*.json").replace("\\", "/")
+            
+            task_a = None
+            task_b = None
+            force_strategy_rerun = False
+            
+            if Path(blueprint_file).exists():
+                print(f"⏭️  [Skip P2] blueprint.json already exists. Skipping P2_FilterBlueprint.")
+            else:
+                force_strategy_rerun = True
+                if args.use_claude_sdk:
+                    print("   > Using [Anthropic SDK Core] for P2_FilterBlueprint")
+                    task_a = run_anthropic_sdk_task("filter_and_blueprint", {
+                        "requirement": requirement,
+                        "raw_files_glob": raw_glob,
+                        "output_file": blueprint_file
+                    }, "P2_FilterBlueprint", claude_model)
+                else:
+                    print("   > Using [AgentRunner Core] for P2_FilterBlueprint")
+                    task_a = run_agent_task(runner_claude, "filter_and_blueprint", {
+                        "requirement": requirement,
+                        "raw_files_glob": raw_glob,
+                        "output_file": blueprint_file
+                    }, "P2_FilterBlueprint", claude_model)
+                    
+            if Path(capabilities_file).exists():
+                print(f"⏭️  [Skip P2] capabilities_extracted.json already exists. Skipping P2_ExtractCaps.")
+            else:
+                force_strategy_rerun = True
+                if args.use_claude_sdk:
+                    print("   > Using [Anthropic SDK Core] for P2_ExtractCaps")
+                    task_b = run_anthropic_sdk_task("extract_capabilities", {
+                        "requirement": requirement,
+                        "raw_files_glob": raw_glob,
+                        "output_file": capabilities_file
+                    }, "P2_ExtractCaps", claude_model)
+                else:
+                    print("   > Using [AgentRunner Core] for P2_ExtractCaps")
+                    task_b = run_agent_task(runner_claude, "extract_capabilities", {
+                        "requirement": requirement,
+                        "raw_files_glob": raw_glob,
+                        "output_file": capabilities_file
+                    }, "P2_ExtractCaps", claude_model)
+            
+            to_await = []
+            names_await = []
+            if task_a:
+                to_await.append(task_a)
+                names_await.append("P2_FilterBlueprint")
+            if task_b:
+                to_await.append(task_b)
+                names_await.append("P2_ExtractCaps")
+                
+            if to_await:
+                phase2_results = await asyncio.gather(*to_await)
+                for (cost, errs), t_name in zip(phase2_results, names_await):
+                    total_cost += cost
+                    costs_breakdown[t_name] = round(cost, 4)
+                    global_errors.extend(errs)
+
+            # Phase 3: REDUCE 2 (Final Assembly) uses Claude
+            print(f"\n--- Phase 3: Final Strategy Assembly ({claude_model}) ---")
+            strategy_file = str(output_dir / "strategy.json")
+            
+            if Path(strategy_file).exists() and not force_strategy_rerun:
+                print(f"⏭️  [Skip P3] strategy.json already exists. Skipping P3_Assembler.")
+            else:
+                if Path(strategy_file).exists():
+                    print(f"⚠️  [Force P3] Upstream dependencies were regenerated. Forcing re-run of P3_Assembler...")
+                
+                if args.use_claude_sdk:
+                    print("   > Using [Anthropic SDK Core]")
+                    phase3_cost, phase3_errs = await run_anthropic_sdk_task("assemble_strategy", {
+                        "requirement": requirement,
+                        "blueprint_file": blueprint_file,
+                        "capabilities_file": capabilities_file,
+                        "output_file": strategy_file
+                    }, "P3_Assembler", claude_model)
+                else:
+                    print("   > Using [AgentRunner Core]")
+                    phase3_cost, phase3_errs = await run_agent_task(runner_claude, "assemble_strategy", {
+                        "requirement": requirement,
+                        "blueprint_file": blueprint_file,
+                        "capabilities_file": capabilities_file,
+                        "output_file": strategy_file
+                    }, "P3_Assembler", claude_model)
+                total_cost += phase3_cost
+                costs_breakdown["P3_Assembler"] = round(phase3_cost, 4)
+                global_errors.extend(phase3_errs)
+        else:
+            print("\n--- [Research Only] Stopping early. Skipping Phase 2 and Phase 3 ---")
+        
+        end_time = time.time()
+        elapsed_sec = end_time - start_time
+        
+        # Save Metrics
+        metrics_file = base_dir / "run_metrics.json"
+        metrics_data = []
+        if metrics_file.exists():
+            with open(metrics_file, "r", encoding="utf-8") as f:
+                try:
+                    metrics_data = json.load(f)
+                except json.JSONDecodeError:
+                    pass
+                    
+        metrics_data.append({
+            "index": args.index,
+            "requirement": requirement[:80] + "...",
+            "duration_seconds": round(elapsed_sec, 2),
+            "total_cost_usd": round(total_cost, 4),
+            "costs_breakdown": costs_breakdown,
+            "errors": global_errors,
+            "timestamp": datetime.now().isoformat()
+        })
+        
+        with open(metrics_file, "w", encoding="utf-8") as f:
+            json.dump(metrics_data, f, indent=2, ensure_ascii=False)
+            
+        print(f"\n📊 [Metrics] Pipeline completed in {elapsed_sec:.1f}s. Total Cost: ${total_cost:.4f}")
+        
+    finally:
+        pass
+        
+    print("✅ Pipeline run finished.")
+    if strategy_file:
+        print("✅ Strategy saved to:", strategy_file)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 210 - 0
examples/process_pipeline/script/compute_coverage_scores.py

@@ -0,0 +1,210 @@
+import json
+import sys
+import asyncio
+from pathlib import Path
+repo_root = str(Path(__file__).parent.parent.parent.parent)
+if repo_root not in sys.path:
+    sys.path.insert(0, repo_root)
+
+from dotenv import load_dotenv
+load_dotenv()
+
+from knowhub.knowhub_db.pg_strategy_store import PostgreSQLStrategyStore
+from knowhub.knowhub_db.pg_requirement_store import PostgreSQLRequirementStore
+from agent.llm.openrouter import openrouter_llm_call
+
+OUTPUT_JSON = Path("examples/process_pipeline/script/coverage_scores.json")
+
+EVAL_PROMPT = """
+You are an expert system architecture and pipeline evaluator.
+You will be provided with a User Requirement and multiple alternative Proposed Pipeline Workflows created to resolve that requirement.
+Your task is to evaluate how well each workflow semantically covers and resolves the user's needs.
+
+User Requirement:
+{req_desc}
+
+Proposed Workflows:
+{workflows_json}
+
+For each workflow, assign a `coverage_score` between 0.00 and 1.00 (1.00 = completely and deeply resolves the core requirement).
+Return your result STRICTLY as a JSON array of objects, one for each workflow evaluated, containing:
+[
+  {{
+    "strategy_id": "<exact strategy_id from the input>",
+    "coverage_score": 0.85,
+    "explanation": "<1-2 sentence justification on what it covers well and what it might be missing>"
+  }}
+]
+DO NOT output any thinking, markdown wrapping (```json), or conversational text. Output ONLY the raw JSON array.
+"""
+
+async def process_requirement(req_desc: str, group_strats: list) -> dict:
+    # Prepare payload to send to LLM
+    workflows_payload = []
+    for s in group_strats:
+        body_data = s.get("body") or {}
+        if isinstance(body_data, str):
+            try:
+                body_data = json.loads(body_data)
+            except:
+                body_data = {}
+                
+        workflows_payload.append({
+            "strategy_id": s["id"],
+            "workflow": body_data.get("workflow", [])
+        })
+        
+    prompt = EVAL_PROMPT.format(
+        req_desc=req_desc,
+        workflows_json=json.dumps(workflows_payload, ensure_ascii=False, indent=2)
+    )
+    
+    try:
+        resp = await openrouter_llm_call(
+            messages=[{"role": "user", "content": prompt}],
+            model="anthropic/claude-sonnet-4.5",  # OpenRouter uses this to route to latest 3.5 Sonnet
+            max_tokens=4096,
+            temperature=0.1
+        )
+        content = resp["content"].strip()
+        if content.startswith("```json"):
+            content = content.replace("```json", "").replace("```", "").strip()
+        elif content.startswith("```"):
+            content = content.replace("```", "").strip()
+            
+        return json.loads(content)
+    except Exception as e:
+        print(f"  [Error] LLM Call failed for a requirement: {e}")
+        return []
+
+async def main(dry_run: bool = False, force: bool = False):
+    print("Connecting to DB...")
+    strat_store = PostgreSQLStrategyStore()
+    req_store = PostgreSQLRequirementStore()
+    
+    requirements = req_store.list_all(limit=10000)
+    strategies = strat_store.list_all(limit=10000)
+    
+    strat_map = {s["id"]: s for s in strategies}
+    
+    output_data = {}
+    if OUTPUT_JSON.exists() and not force:
+        try:
+            with open(OUTPUT_JSON, "r", encoding="utf-8") as f:
+                output_data = json.load(f)
+            print(f"Loaded existing coverage scores for {len(output_data)} requirements. Resuming...")
+        except:
+            print("Failed to load existing JSON, starting fresh.")
+    elif force:
+        print("Force run enabled. Discarding existing records and starting completely fresh.")
+            
+    processed_req_ids = set(output_data.keys())
+    
+    total_reqs = len(output_data)
+
+    # Filter out already processed requirements
+    pending_requirements = [r for r in requirements if r["id"] not in processed_req_ids]
+    
+    print(f"Starting LLM coverage semantic evaluation using Sonnet 4.5 via OpenRouter...")
+    print(f"Total Requirements remaining to evaluate: {len(pending_requirements)} (out of {len(requirements)})")
+    
+    # Process in batches of 10 concurrent requests
+    batch_size = 10
+    
+    for i in range(0, len(pending_requirements), batch_size):
+        batch_reqs = pending_requirements[i:i+batch_size]
+        tasks = []
+        
+        print(f"Evaluating Batch {i//batch_size + 1} (Reqs {i+1} to min({i+batch_size}, {len(pending_requirements)}))")
+        
+        for req in batch_reqs:
+            req_id = req["id"]
+            req_desc = req.get("description", "Unknown Description")
+            req_strat_ids = req.get("strategy_ids") or []
+            
+            group_strats = [strat_map[sid] for sid in req_strat_ids if sid in strat_map]
+            if not group_strats:
+                continue
+                
+            tasks.append((req_id, req_desc, process_requirement(req_desc, group_strats)))
+            
+        if not tasks:
+            continue
+            
+        # Execute batch concurrently
+        results = await asyncio.gather(*(t[2] for t in tasks))
+        
+        # Map results back
+        for idx, (req_id, req_desc, _) in enumerate(tasks):
+            evaluations = results[idx]
+            if not evaluations:
+                continue
+                
+            strat_results = []
+            for ev in evaluations:
+                sid = ev.get("strategy_id")
+                if sid in strat_map:
+                    strat_info = strat_map[sid]
+                    is_selected = (strat_info.get("status") == "published")
+                    strat_results.append({
+                        "strategy_id": sid,
+                        "strategy_name": strat_info.get("name", ""),
+                        "is_selected": is_selected,
+                        "coverage_score": ev.get("coverage_score", 0),
+                        "explanation": ev.get("explanation", "")
+                    })
+            
+            if strat_results:
+                strat_results.sort(key=lambda x: x["coverage_score"], reverse=True)
+                output_data[req_id] = {
+                    "requirement_desc": req_desc,
+                    "strategies": strat_results
+                }
+                total_reqs += 1
+                
+                # Write the calculated score and explanation directly back to the database body
+                updated_count = 0
+                for ev in strat_results:
+                    sid = ev["strategy_id"]
+                    if sid in strat_map:
+                        strat_info = strat_map[sid]
+                        body_data = strat_info.get("body") or {}
+                        if isinstance(body_data, str):
+                            try:
+                                body_data = json.loads(body_data)
+                            except:
+                                body_data = {}
+                        
+                        body_data.setdefault("coverage_evaluations", {})
+                        body_data["coverage_evaluations"][req_id] = {
+                            "score": ev["coverage_score"],
+                            "explanation": ev["explanation"]
+                        }
+                        
+                        if not dry_run:
+                            try:
+                                strat_store.update(sid, {"body": json.dumps(body_data, ensure_ascii=False)})
+                                strat_map[sid]["body"] = body_data  # Update local map cache
+                                updated_count += 1
+                            except Exception as e:
+                                print(f"  [Error] Failed to update body for strategy {sid}: {e}")
+                        else:
+                            updated_count += 1
+                            
+                tag_word = "[DRY-RUN] Simulated updating" if dry_run else "Updated"
+                print(f"  -> Processed requirement {req_id}: {tag_word} DB body for {updated_count} strategies.")
+                
+        # Save incrementally after every batch to prevent data loss
+        with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
+            json.dump(output_data, f, ensure_ascii=False, indent=2)
+
+    print(f"Evaluated {total_reqs} requirements overall.")
+    print(f"Results {"simulated (DB untouched)" if dry_run else "and DB updates"} successfully saved to: {OUTPUT_JSON}")
+
+if __name__ == "__main__":
+    import argparse
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--dry-run", action="store_true", help="Calculate scores and save to JSON only, do not write to DB")
+    parser.add_argument("--force", action="store_true", help="Discard existing JSON and rerun all requirements from scratch")
+    args = parser.parse_args()
+    asyncio.run(main(args.dry_run, args.force))

+ 1489 - 0
examples/process_pipeline/script/coverage_scores.json

@@ -0,0 +1,1489 @@
+{
+  "REQ_001": {
+    "requirement_desc": "生成人物在不同场景下呈现丰富面部表情的图片,例如夸张的痛苦、无奈、开心、困倦等神态,表情要生动传神、情绪感强烈",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-2c12f52d",
+        "strategy_name": "3D 卡通风情绪矩阵路线(皮克斯/九宫格/表情包)",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流全面覆盖了用户需求,通过四个阶段系统性地实现了角色生成、多情绪表情批量生成、场景融合和风格精调,特别强调了情绪标注驱动机制和场景道具设计,能够生成夸张痛苦、无奈、开心、困倦等丰富表情。唯一轻微不足是流程较复杂,可能增加执行时间。"
+      },
+      {
+        "strategy_id": "STRAT-0902410f",
+        "strategy_name": "锁主体身份 + 情绪 Prompt 矩阵批量路线",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流通过三个阶段有效实现了角色生成、六种表情一致性生成和场景融合,核心能力覆盖了用户对丰富面部表情的需求(开心、悲伤、愤怒、惊讶、委屈、无奈)。相比第一个方案,缺少了情绪标注驱动和道具设计等增强情绪表现力的机制,但流程更简洁高效。"
+      }
+    ]
+  },
+  "REQ_002": {
+    "requirement_desc": "生成人物与道具、环境或其他角色发生互动的画面,例如人物摆弄物品、与道具合影、在特定场景中做出配合动作等,画面要体现人物和周围元素之间的关联感",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-96e19b67",
+        "strategy_name": "人物×场景深度融合路线",
+        "is_selected": false,
+        "coverage_score": 0.92,
+        "explanation": "该工作流深度覆盖了用户需求的核心要素:通过多主体场景合成、人景融合提示词公式和光影匹配技术,系统性地实现了人物与道具/环境/角色的互动画面生成,强调关联感(光影交互、物理接触、视线呼应)和故事叙事感。唯一轻微不足是在单纯的'人物摆弄物品'这一具体动作细节控制上,可以进一步补充手部姿态精准控制的能力模块。"
+      },
+      {
+        "strategy_id": "STRAT-f84e9426",
+        "strategy_name": "MJ Omni Reference 锁主体 + Prompt 批量变体路线",
+        "is_selected": false,
+        "coverage_score": 0.38,
+        "explanation": "该工作流专注于近景人像肖像生成,核心能力集中在人脸一致性、浅景深背景虚化、表情控制和光影氛围渲染,适合生成'人物与道具合影'中的特写肖像部分。但缺乏对'人物与道具/环境互动'的核心场景合成能力,无法体现人物摆弄物品、在特定场景中做出配合动作等关联感需求,仅能满足静态合影类场景的部分需求。"
+      }
+    ]
+  },
+  "REQ_003": {
+    "requirement_desc": "生成将动物(如猫咪)拟人化扮演特定角色或情境的图片,赋予其人类的表情、姿态和道具,用来传达幽默或情感共鸣的视觉效果",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-38fbb4e1",
+        "strategy_name": "精品单图拟人化角色生成流派",
+        "is_selected": false,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完整覆盖了拟人化动物图像生成的核心需求,从角色人设构建、分层提示词工程、AI生成与一致性保持到高分辨率输出,系统性地解决了赋予动物人类表情、姿态、道具以传达幽默或情感共鸣的全流程。唯一轻微不足是未涉及动态化或视频化扩展。"
+      },
+      {
+        "strategy_id": "STRAT-4914537c",
+        "strategy_name": "文生图底帧 + 图生视频动态化路线(首尾帧/多镜头/Hailuo-Seedance-Kling)",
+        "is_selected": true,
+        "coverage_score": 0.25,
+        "explanation": "该工作流专注于非常规摄像机视角(虫眼仰拍、鸟瞰俯视、POV等)和镜头运动的生成,核心能力在于视角控制和图生视频,但完全未涉及用户需求的核心要素:动物拟人化、角色扮演、人类表情姿态赋予、道具配置等,语义覆盖度极低。"
+      }
+    ]
+  },
+  "REQ_005": {
+    "requirement_desc": "生成婚礼或节日庆典场景,背景需包含大量花卉装饰、定制发光字牌、喜字等布景元素,整体氛围感强烈,道具与场景协调统一",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-e4e136f7",
+        "strategy_name": "沉浸式花卉穹顶婚礼场景——三层立体空间叙事流派",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了婚礼场景的核心需求:通过三阶段流程系统性地生成花卉装饰底图、强化多层次花卉密度、精确植入定制发光字牌和喜字元素,并通过全局光效增强确保氛围感强烈和道具场景协调统一。唯一轻微不足是对节日庆典(如春节等中式场景)的覆盖相对较弱。"
+      },
+      {
+        "strategy_id": "STRAT-b775c852",
+        "strategy_name": "中式节日庆典喜庆场景——红金配色书法字牌流派",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流专注于中式节日庆典场景,完整覆盖了红金色调、喜字福字、花艺装饰和灯笼等传统元素的生成与植入,氛围感强烈且道具协调。但对婚礼场景(特别是西式浪漫婚礼)的覆盖不如第一个工作流全面,且缺少对多色系花卉穹顶等现代婚礼元素的深度处理。"
+      }
+    ]
+  },
+  "REQ_007": {
+    "requirement_desc": "生成真实人物在户外或特定场景中的生活记录照片,画面自然真实,包含儿童在公园、农场等户外环境中玩耍的多角度抓拍效果,光线自然,氛围温馨",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-6b8ca29b",
+        "strategy_name": "专属 LoRA 精细化控制生图路线(人像/产品)",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流精准覆盖用户需求的所有核心要素:使用写实模型生成真实儿童人物、明确户外场景(公园/农场)与自然光线设定、通过去油腻感处理实现自然真实效果、最终批量生成多角度抓拍组图。唯一微小不足是未明确提及温馨氛围的情感化描述,但整体技术路径完整且高度匹配需求。"
+      },
+      {
+        "strategy_id": "STRAT-f84e9426",
+        "strategy_name": "MJ Omni Reference 锁主体 + Prompt 批量变体路线",
+        "is_selected": false,
+        "coverage_score": 0.45,
+        "explanation": "该工作流专注于近景人像特写、面部表情控制和浅景深背景虚化,技术能力强大但与需求核心偏离:用户需要儿童在户外场景中玩耍的多角度生活记录照片,而该方案主要解决商业肖像、面部特写和情绪表达,缺少户外场景构建、儿童动态抓拍、多角度全身/半身构图等关键要素。"
+      }
+    ]
+  },
+  "REQ_008": {
+    "requirement_desc": "制作将真实人物照片合成到趣味场景中的创意图片,例如把人物缩小放入超市肉类托盘包装内、或与冰雕翅膀等道具结合形成视觉错位的幽默效果",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-1c15a66b",
+        "strategy_name": "纯 AI 一键合成路线:人物照片 → 微缩场景融合",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流全面覆盖了人物特征保留、场景构建、比例缩放、视觉错位效果生成和细节优化的完整流程,特别强调了面部特征锁定和多工具协同(Gemini + nano banana pro),并提供了具体的提示词参数指导。唯一不足是缺少多版本生成对比机制,可能需要多次迭代才能获得最优效果。"
+      },
+      {
+        "strategy_id": "STRAT-7008f9b3",
+        "strategy_name": "场景定制增强路线:人物照片 → 指定趣味场景精准合成",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流通过模板化设计和多版本生成机制提供了系统化的创意图片制作方案,特别强调了氛围增强和特效叠加(如保鲜膜反光、星空背景、飘雪特效)以增强视觉冲击力。相比STRAT-1c15a66b,该方案在技术实现细节(如比例缩放参数、材质质感控制)的描述上略显简略,但在创意场景多样性和氛围营造上更具优势。"
+      }
+    ]
+  },
+  "REQ_009": {
+    "requirement_desc": "生成真实场景的多图拼贴展示图,将同一地点或主题的多张实拍照片拼合为一张图文并茂的内容图,适合用于地点打卡、产品展示或生活记录类帖子",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-d8340a6a",
+        "strategy_name": "实拍照片 + 模板工具拼贴叙事路线(Canva/醒图/ProCCD/CapCut)",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完整覆盖了真实场景多图拼贴的核心需求,包括批量导入实拍照片、智能抠图、叙事排序、布局排版、文字标注、装饰元素等全流程,特别适合地点打卡、产品展示和生活记录类帖子。唯一轻微不足是对AI自动化程度的描述较少,更多依赖用户手动操作现有工具。"
+      },
+      {
+        "strategy_id": "STRAT-425a984a",
+        "strategy_name": "结构化 Prompt 一次直出多格/网格路线",
+        "is_selected": true,
+        "coverage_score": 0.35,
+        "explanation": "该工作流主要聚焦于AI模型一次性生成网格布局图像,适用于虚拟角色分镜、多姿态展示等AI生成场景,但不符合用户需求中'将同一地点或主题的多张实拍照片拼合'这一核心要求。该方案缺少对真实照片导入、预处理、叙事排序、图文叠加等关键环节的支持。"
+      }
+    ]
+  },
+  "REQ_010": {
+    "requirement_desc": "制作多格宫格式信息图,将同类内容(如多种食材搭配方案)拆分为统一风格的小卡片,每格包含标题、食材图片和文字说明,整体排列整齐、色块鲜明,适合一图展示多个并列条目",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-494cc29c",
+        "strategy_name": "Coze 工作流端到端批量生成路线(宫格/长图/信息可视化)",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完整覆盖了多格宫格信息图的核心需求:从内容拆解、批量图像生成、文字渲染到网格排版和切割输出,每个阶段都针对'统一风格小卡片'和'整齐排列'的要求提供了专门工具和流程。唯一微小不足是阶段1的AI脚本生成更偏向叙事分镜,对于纯信息展示类(如食材搭配)的结构化模板支持描述不够明确。"
+      },
+      {
+        "strategy_id": "STRAT-425a984a",
+        "strategy_name": "结构化 Prompt 一次直出多格/网格路线",
+        "is_selected": true,
+        "coverage_score": 0.78,
+        "explanation": "该工作流通过AI模型一次性直出完整网格大图的方式实现了多宫格布局,并提供了网格切割和局部精修能力,基本满足整齐排列和统一风格的需求。但缺少针对'每格包含标题、食材图片和文字说明'这一核心要素的专门文字渲染阶段,且更侧重于分镜叙事场景而非信息图卡片的结构化内容组织。"
+      }
+    ]
+  },
+  "REQ_011": {
+    "requirement_desc": "在图片上叠加标注元素,如用红色圆点、箭头或emoji符号指向图中特定位置,配合说明文字,实现在真实照片上直观标记关键信息的视觉效果",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-805daf9b",
+        "strategy_name": "多模态AI指令驱动路线",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流完整覆盖了用户需求的所有核心要素:通过自然语言指令驱动图像编辑实现标注需求解析,利用AI矢量图形生成能力精确创建箭头、圆点等标注元素,并通过图像内文字渲染实现说明文字叠加。智能标注规划阶段特别考虑了布局优化和避免遮挡,完全符合'直观标记关键信息'的视觉效果要求。"
+      },
+      {
+        "strategy_id": "STRAT-b7aa114e",
+        "strategy_name": "AI视觉理解 + 自动标注生成路线",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流同样覆盖了用户需求的主要功能点:图像理解与定位阶段实现了对特定位置的识别,标注元素生成阶段支持圆点、箭头、emoji等多种标注类型,图层合成输出阶段确保标注清晰可见。相比STRAT-805daf9b,该方案在智能布局规划和批量处理能力方面描述较少,但整体流程逻辑清晰且实用。"
+      }
+    ]
+  },
+  "REQ_012": {
+    "requirement_desc": "制作图文混排的长图文内容,将大段文字与人物照片、数据表格、流程图等多种视觉元素组合排布在同一版面中,形成类似杂志或报告的专业排版风格",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-6f1bf38a",
+        "strategy_name": "杂志报告风格图文混排全流程(通用四阶段主线)",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了图文混排长图的核心需求,包括多类型视觉元素生成(人物照片、数据表格、流程图)、文字精准渲染、风格统一、信息层级控制和版面自动布局合成,形成完整的端到端解决方案。唯一轻微不足是对实际杂志/报告排版中的网格系统和模块化设计规范的显式支持描述较少。"
+      },
+      {
+        "strategy_id": "STRAT-48f04a9c",
+        "strategy_name": "产品展示型长图生成(电商场景优化备选)",
+        "is_selected": false,
+        "coverage_score": 0.45,
+        "explanation": "该工作流主要聚焦于电商产品展示场景,强调产品多角度视图生成和背景替换,但缺少用户需求中核心的数据表格、流程图等多种视觉元素的生成能力,也未涉及大段文字与多元素的复杂版面布局合成,不完全匹配杂志或报告式的专业图文混排需求。"
+      }
+    ]
+  },
+  "REQ_013": {
+    "requirement_desc": "生成以暖黄/米棕色为背景底色的图文排版内容,整体画面呈现温暖、复古的暖色调氛围,适合健康养生、生活方式类主题",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-dda4228c",
+        "strategy_name": "AI图文生成+色调控制路线",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流完整覆盖了暖黄/米棕色背景底色生成、图文排版、色调统一等核心需求,系统性地实现了温暖复古氛围的营造。唯一略显不足的是在主题元素和装饰风格上相对通用化,对健康养生、生活方式类主题的针对性表达稍弱。"
+      },
+      {
+        "strategy_id": "STRAT-0006cf28",
+        "strategy_name": "AI插画生成+排版合成路线",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流通过手绘插画、书法字体、东方美学等元素深度契合健康养生主题,复古氛围渲染到位。但工作流程从插画生成开始而非背景底色构建,在强调'背景底色'这一核心要求的优先级和系统性上略有偏差。"
+      }
+    ]
+  },
+  "REQ_014": {
+    "requirement_desc": "生成以深色(黑色/深蓝/深紫)为背景底色的海报,搭配霓虹感彩色光效(橙、紫、青等),营造出科技感强烈的冷暖对比配色效果",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-e5fde1dc",
+        "strategy_name": "分层精控路线",
+        "is_selected": false,
+        "coverage_score": 0.94,
+        "explanation": "该工作流通过四阶段精细化流程,将深色背景与霓虹光效分层生成并独立迭代,确保冷暖对比达到最佳视觉张力,完全契合用户对科技感强烈配色效果的需求。色彩系统搭建阶段提供了精确的配色规范和比例控制,覆盖深度和执行细节均优于另一方案。"
+      },
+      {
+        "strategy_id": "STRAT-d073b214",
+        "strategy_name": "提示词直出路线",
+        "is_selected": true,
+        "coverage_score": 0.82,
+        "explanation": "该工作流完整覆盖了深色背景、霓虹光效、冷暖对比配色的核心需求,并通过三阶段流程实现从配色方案到最终海报的完整输出。但在光效层的精细控制和迭代优化方面略显简化,可能影响冷暖对比的最佳视觉张力。"
+      }
+    ]
+  },
+  "REQ_015": {
+    "requirement_desc": "生成整体色调偏粉紫、薄荷绿、浅蓝等低饱和度冷色系的插画或场景图,画面呈现出梦幻、静谧的冷色调氛围,颜色搭配柔和克制",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-33acc2f0",
+        "strategy_name": "多阶段精准控制流派:构图生成 → 色调锁定 → 氛围增强",
+        "is_selected": true,
+        "coverage_score": 0.94,
+        "explanation": "该方案采用三阶段渐进式流程,从基础构图、精准色调锁定到梦幻氛围增强,全面覆盖了用户需求的所有维度:低饱和度冷色系(粉紫/薄荷绿/浅蓝)、梦幻静谧氛围、柔和克制的色彩搭配。通过多种能力组合(风格控制、全向参考、光照转换、柔光效果、装饰元素)实现了对色调、氛围、质感的精细化控制,能够稳定输出高质量的目标风格插画。"
+      },
+      {
+        "strategy_id": "STRAT-91477a0a",
+        "strategy_name": "单阶段精准提示词文生图路线",
+        "is_selected": false,
+        "coverage_score": 0.72,
+        "explanation": "该方案通过精准色彩语义提示词和个人化风格持久化能力,能够直接生成符合粉紫、薄荷绿、浅蓝等低饱和度冷色系的插画,并保持风格一致性。但缺少对梦幻静谧氛围的深度控制手段(如柔光效果、装饰元素等),仅依赖提示词可能在氛围营造的精准度和层次感上有所不足。"
+      }
+    ]
+  },
+  "REQ_016": {
+    "requirement_desc": "生成手部持握或展示物品的特写画面,手势自然,物品清晰呈现,如用手托举饺子、手持卡片、手握手机等,突出手与物品的互动关系",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-3b0052f8",
+        "strategy_name": "结构化提示词直接生成路线",
+        "is_selected": false,
+        "coverage_score": 0.0,
+        "explanation": "The workflow is empty and contains no steps or components to address the requirement of generating close-up images of hands holding or displaying objects with natural gestures and clear item presentation."
+      },
+      {
+        "strategy_id": "STRAT-6ada0f7a",
+        "strategy_name": "双图垫图融合路线(推荐首选)",
+        "is_selected": false,
+        "coverage_score": 0.0,
+        "explanation": "The workflow is empty and provides no pipeline or processing steps to fulfill the user's need for generating hand-object interaction imagery with emphasis on natural gestures and clear object visibility."
+      }
+    ]
+  },
+  "REQ_017": {
+    "requirement_desc": "生成多人或多只动物同框的协同姿态画面,如两只猫并排躺着穿睡衣、两人一起摆造型抱花束,呈现出同步、对称或互动的视觉效果",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-76111aad",
+        "strategy_name": "OpenPose + ControlNet 姿态迁移路线(多人/服装)",
+        "is_selected": true,
+        "coverage_score": 0.15,
+        "explanation": "该工作流专注于将单个人物嵌入场景容器(如托盘)的创意合成,强调人物、服装、场景三要素融合及光影一致性,但完全未涉及用户核心需求中的'多人或多只动物同框'、'协同姿态'、'同步/对称/互动效果'等多主体协同生成能力,语义覆盖严重不足。"
+      }
+    ]
+  },
+  "REQ_018": {
+    "requirement_desc": "生成创意性的嘴部动作特写,如用嘴唇衔住花朵茎部形成'嘴唇花'的视觉效果,将身体部位与物品结合产生趣味创意画面",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-0ea1c8bc",
+        "strategy_name": "精准嘴唇衔花特写生成流派(参数锚定 × 接触关系精控 × 氛围精修)",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完整覆盖了用户需求的核心要素:通过全向参考和主体一致性锁定花朵与嘴唇的视觉目标,通过结构控制和新增的'人体与物品接触关系精准生成'能力精确实现嘴唇衔花的物理接触效果,通过局部特写构图能力聚焦嘴部动作,最后通过局部重绘、细节增强、光照转换和风格控制完成氛围优化。唯一微小不足是对表情情绪层面的控制描述相对较少。"
+      },
+      {
+        "strategy_id": "STRAT-f84e9426",
+        "strategy_name": "MJ Omni Reference 锁主体 + Prompt 批量变体路线",
+        "is_selected": false,
+        "coverage_score": 0.42,
+        "explanation": "该工作流聚焦于人像面部表情、构图、光影和胶片质感的精准控制,适合生成高质量近景人像肖像,但完全未涉及用户需求的核心创意元素——'嘴唇衔住花朵茎部'这一身体部位与物品结合的趣味创意画面,缺少对物品道具(花朵)及其与嘴部接触关系的生成能力支持。"
+      }
+    ]
+  },
+  "REQ_019": {
+    "requirement_desc": "将人物照片与中国传统吉祥符号(如双喜字、红玫瑰、金色祝福文字)融合,生成具有强烈喜庆氛围的定制化图案,人物面孔清晰嵌入红色喜庆背景中",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-505c2718",
+        "strategy_name": "直接融合生成路线",
+        "is_selected": true,
+        "coverage_score": 0.72,
+        "explanation": "该工作流覆盖了提示词构建、AI生成融合图像和细节优化三个核心阶段,能够实现人物与喜庆元素的融合生成。但缺少对用户提供的真实人物照片的直接处理能力(如人脸提取、特征保持),主要依赖文本描述而非真实照片输入,可能导致生成人物与原照片相似度不足。"
+      }
+    ]
+  },
+  "REQ_020": {
+    "requirement_desc": "制作统一模板风格的系列信息卡片,每张卡片包含固定的图标符号(如皇冠等级图标)、彩色标题文字和配图,整体视觉风格一致、可批量复用",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-441008e2",
+        "strategy_name": "LLM + Canva 模板驱动批量合成路线",
+        "is_selected": true,
+        "coverage_score": 0.72,
+        "explanation": "该工作流覆盖了批量生成、统一视觉风格、图标符号叠加、彩色标题文字等核心需求,特别是通过Canva Bulk Create实现了模板复用和批量产出。但工作流过于复杂,包含了多图拼贴、信息分区等用户需求中未明确要求的功能,且聚焦于复杂海报制作而非简洁的系列信息卡片,部分阶段(如多图网格拼贴)偏离了'固定图标+彩色标题+配图'的简单卡片结构核心诉求。"
+      }
+    ]
+  },
+  "REQ_022": {
+    "requirement_desc": "生成宠物或动物穿戴人类服饰配件(如帽子、围巾)的画面,让动物看起来像在过节或扮演某种角色,整体效果可爱又有趣",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-e1903c78",
+        "strategy_name": "IP-Adapter 主体/风格迁移路线(动画/换装/风格统一)",
+        "is_selected": false,
+        "coverage_score": 0.92,
+        "explanation": "该工作流完整覆盖了用户需求的核心要素:通过阶段1的角色扮演场景生成能力直接实现动物穿戴服饰配件的可爱有趣效果,阶段2的姿态控制确保服饰配件与动物身体自然契合,阶段3的动画生成提供了超出基础需求的增强价值。唯一轻微不足是阶段3的动画能力虽然增值明显但对于静态画面生成的核心需求来说属于可选扩展而非必需。"
+      }
+    ]
+  },
+  "REQ_023": {
+    "requirement_desc": "生成将普通服装或日常物品以夸张搞怪方式穿戴的人物画面,比如把超大号短裤当长袍穿、用篮球短裤模仿古希腊长袍,产生强烈的视觉反差和幽默效果",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-f921299a",
+        "strategy_name": "纯图像超现实错位穿戴路线",
+        "is_selected": false,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完全覆盖用户需求的核心要素:通过精准提示词构建'日常物品错位穿戴'的荒诞场景(阶段1),使用文生图直接生成夸张穿搭人物画面(阶段2),并通过光影色彩强化视觉反差和幽默效果(阶段3)。工作流直接针对'超大号短裤当长袍'等具体场景设计,能够产生强烈视觉反差,唯一微小不足是未涉及动态视频生成。"
+      },
+      {
+        "strategy_id": "STRAT-4914537c",
+        "strategy_name": "文生图底帧 + 图生视频动态化路线(首尾帧/多镜头/Hailuo-Seedance-Kling)",
+        "is_selected": true,
+        "coverage_score": 0.25,
+        "explanation": "该工作流专注于非常规摄像机角度和POV视角生成(虫眼仰拍、鸟瞰俯视、第一人称视角等),虽然技术能力强大,但与用户需求'夸张搞怪方式穿戴日常物品产生幽默效果'的核心诉求严重不匹配。工作流重点在于视角转换而非服装穿戴的创意错位,仅在极少数情况下可能间接辅助呈现夸张穿搭效果。"
+      }
+    ]
+  },
+  "REQ_024": {
+    "requirement_desc": "生成具有超现实风格的创意合成画面,将人物头部替换为宇宙星云、太极图、粒子爆炸等抽象元素,配合深蓝红色调背景,营造出哲学感或科幻感的视觉冲击",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-d41e55b3",
+        "strategy_name": "分步合成路线",
+        "is_selected": false,
+        "coverage_score": 0.92,
+        "explanation": "该方案采用分阶段流程,通过独立生成人物身体、抽象元素素材库,再使用局部重绘精准替换头部,最后统一色调和氛围渲染,完整覆盖用户需求的所有关键要素。分步控制使头部替换更精准、融合更自然,且能灵活调整各抽象元素,唯一不足是流程相对复杂需要多步操作。"
+      },
+      {
+        "strategy_id": "STRAT-5a38e3fc",
+        "strategy_name": "直接生成路线",
+        "is_selected": true,
+        "coverage_score": 0.75,
+        "explanation": "该方案通过单次精细提示词生成完整画面,能够直接实现头部抽象元素替换、深蓝红色调背景和超现实风格,效率高且风格统一。但依赖AI模型一次性理解复杂提示词,对头部替换的精准控制和边缘融合效果可能不如分阶段处理稳定。"
+      }
+    ]
+  },
+  "REQ_025": {
+    "requirement_desc": "生成穿着完整冬季搭配的人物形象,展示黑色羽绒服、红色围巾、红色手套、宽腿裤等单品的组合穿搭效果,呈现从全身到局部细节的多角度造型展示",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-795a1faa",
+        "strategy_name": "提示词工程直接生成路线",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了用户需求的所有核心要素:通过结构化提示词精准生成黑色羽绒服、红色围巾、红色手套、宽腿裤等单品组合,材质物理属性模拟确保羽绒服蓬松度和围巾纹理真实呈现,冬季场景氛围构建提供完整视觉语境,多景别构图生成实现从全身到局部细节的15张多角度展示。唯一微小不足是未明确提及换装流程的灵活性控制。"
+      },
+      {
+        "strategy_id": "STRAT-e3232f37",
+        "strategy_name": "人物固定+多角度换装路线",
+        "is_selected": false,
+        "coverage_score": 0.82,
+        "explanation": "该工作流采用分阶段生成策略,通过基础人物模板+服装层替换+多角度扩展的流程实现需求,能够生成目标冬季搭配并保持人物一致性,最终输出15张多角度展示图。但相比第一个方案,在材质物理属性模拟(羽绒服蓬松度、围巾纹理)和冬季氛围场景构建方面的技术深度和系统性描述略显不足,可能影响冬季服装质感的最终呈现效果。"
+      }
+    ]
+  },
+  "REQ_026": {
+    "requirement_desc": "生成宠物穿着服装的可爱造型图,展示猫咪穿上印花连体衣的整体穿着效果,需要清晰呈现服装的图案、版型与宠物身体的贴合细节",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-f5c616fe",
+        "strategy_name": "AI文生图路线(从零生成)",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流直接针对宠物穿着服装效果图生成,明确包含服装印花图案精准还原能力和版型贴合控制,通过精准提示词构建和姿态控制实现清晰呈现服装细节的核心需求;唯一不足是缺少对连体衣特定版型(如袖口、裤腿贴合)的专项优化说明。"
+      },
+      {
+        "strategy_id": "STRAT-86d1256a",
+        "strategy_name": "主体一致性 + Inpaint 局部换装/换景路线",
+        "is_selected": true,
+        "coverage_score": 0.65,
+        "explanation": "该工作流专注于多职业造型批量生成,通过局部重绘和道具叠加实现多样化输出,但核心需求是单一印花连体衣的穿着效果展示,而非多职业场景;工作流在服装图案精准还原和版型贴合细节方面缺乏针对性能力支撑。"
+      }
+    ]
+  },
+  "REQ_027": {
+    "requirement_desc": "生成创意合成图,将人物穿搭形象嵌入特定场景容器中(如超市生鲜托盘),使人物服装与场景产生趣味性视觉对比,同时保留服装细节的清晰可见",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-76111aad",
+        "strategy_name": "OpenPose + ControlNet 姿态迁移路线(多人/服装)",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了用户需求的核心要素:通过三要素分层准备实现人物、服装、场景容器的精确控制;使用ControlNet OpenPose确保姿态和透视关系正确;通过FLORA AI和ComfyUI Redux实现高质量融合合成;特别强调了服装细节保留和光影一致性处理。唯一微小不足是未明确提及最终输出的趣味性视觉对比效果的验证机制。"
+      }
+    ]
+  },
+  "REQ_028": {
+    "requirement_desc": "将猫咪表情包图片与各种场景素材(办公室、食物、产品、背景环境等)合成拼贴在一起,让猫咪看起来自然地处于这些场景中,形成多格并排的拼贴版式",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-e9ec0dad",
+        "strategy_name": "AI人宠合照专业模型路线(高质量型)",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流更贴近用户需求的核心场景,通过双素材准备(猫咪+场景背景图)直接进行AI合成,流程更直观高效。覆盖了抠图、透视光影校正、批量迭代和多格拼贴的完整链路,且强调了场景素材的多样性准备,与用户要求的'与各种场景素材合成'高度契合。"
+      },
+      {
+        "strategy_id": "STRAT-7fd1acde",
+        "strategy_name": "AI图生图场景融合路线(通用型)",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流完整覆盖了从猫咪抠图、场景提示词构建、AI批量合成到多格拼贴的全流程,特别强调了批量生成和自动化处理。略微不足在于阶段2构建提示词矩阵的方式相对间接,需要用户自行准备场景描述而非直接使用现成场景素材。"
+      }
+    ]
+  },
+  "REQ_029": {
+    "requirement_desc": "把猫咪图片与各类装扮道具(帽子、眼镜、服装、假发等)或其他卡通/玩具素材叠加合成,让不同来源的素材无缝融合成一张完整的搞笑图",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-86d1256a",
+        "strategy_name": "主体一致性 + Inpaint 局部换装/换景路线",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该方案完整覆盖了用户需求的核心:保持原始猫咪图片不变,通过局部重绘技术在指定区域精准添加帽子、眼镜、服装等道具,实现真正的素材叠加合成。支持批量处理和多道具分步叠加,完美契合'不同来源素材无缝融合'的要求。"
+      },
+      {
+        "strategy_id": "STRAT-e1903c78",
+        "strategy_name": "IP-Adapter 主体/风格迁移路线(动画/换装/风格统一)",
+        "is_selected": false,
+        "coverage_score": 0.65,
+        "explanation": "该方案侧重于AI生成完整的角色扮演场景,通过提示词和模型生成猫咪穿戴装扮的整体图像,但不是真正的素材叠加合成。缺少对用户提供的原始猫咪图片与外部道具素材进行无缝融合的核心能力,更多是重新生成而非合成。"
+      }
+    ]
+  },
+  "REQ_030": {
+    "requirement_desc": "在同一张图中将多只猫咪或同一只猫咪的不同姿态照片拼接组合,配合文字标注形成对话或对比效果的多格拼图",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-425a984a",
+        "strategy_name": "结构化 Prompt 一次直出多格/网格路线",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流完整覆盖了多猫咪拼图的核心需求:通过AI模型一次性生成网格大图,支持角色一致性保持(同一只猫的不同姿态),提供网格切割和高清输出。唯一不足是文字标注功能未明确提及专门的文字渲染工具,可能需要在提示词中实现或后期添加。"
+      },
+      {
+        "strategy_id": "STRAT-494cc29c",
+        "strategy_name": "Coze 工作流端到端批量生成路线(宫格/长图/信息可视化)",
+        "is_selected": true,
+        "coverage_score": 0.88,
+        "explanation": "该工作流提供了完整的端到端解决方案,特别强调了文字标注(阶段3专门的图像内文字渲染)和网格布局排版,完全满足对话或对比效果需求。但其侧重于AI脚本生成和批量处理流程,对于用户已有猫咪照片的拼接场景(非AI生成)的支持不如STRAT-425a984a直接。"
+      }
+    ]
+  },
+  "REQ_033": {
+    "requirement_desc": "在真实物体照片上叠加手绘风格的简笔画元素,例如在猕猴桃切片上添加卡通五官和小触角,让照片呈现出实物与手绘结合的趣味效果",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-10a9da66",
+        "strategy_name": "双工具协作精细化策略(Nano Banana + Adobe Firefly)",
+        "is_selected": false,
+        "coverage_score": 0.92,
+        "explanation": "该工作流完整覆盖了用户需求的核心要素:通过图像局部重绘保留真实物体照片底图,通过手绘笔触生成和卡通拟人化元素生成实现简笔画叠加(五官、触角等),并通过风格控制确保手绘与照片融合的趣味效果。唯一轻微不足是阶段1的语义分析描述较为抽象,实际实现路径可更具体化。"
+      }
+    ]
+  },
+  "REQ_034": {
+    "requirement_desc": "生成真实户外场景中的人物活动照片,画面要呈现自然光线下的街道、公园、游乐场等具体地点环境,人物动作自然生动,背景环境细节丰富真实",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-6881b31c",
+        "strategy_name": "混合素材增强路线(有实景素材)",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流通过实景素材采集、光照分析、物理真实性关键词注入和运动关键帧设置,能够生成高度真实的户外场景人物活动内容,背景环境细节来自真实素材保证了丰富性和真实性,但流程较复杂且依赖实景素材的前期准备。"
+      },
+      {
+        "strategy_id": "STRAT-a5b959a8",
+        "strategy_name": "纯提示词驱动路线(从零生成)",
+        "is_selected": true,
+        "coverage_score": 0.82,
+        "explanation": "该工作流通过构建包含7大核心要素的精准提示词框架,系统性地覆盖了地点、光线、环境细节、人物动作等所有需求维度,能够直接生成高质量户外人物活动照片,但完全依赖提示词工程可能在极端真实感和物理准确性上略逊于混合实景素材的方案。"
+      },
+      {
+        "strategy_id": "STRAT-59a75d98",
+        "strategy_name": "参考图复刻路线(有参考素材)",
+        "is_selected": false,
+        "coverage_score": 0.72,
+        "explanation": "该工作流通过参考图复刻和详细提示词工程能够生成具有自然光线和丰富环境细节的户外人物照片,但其核心依赖参考图上传,限制了从零开始创作多样化场景的灵活性,且缺少对真实物理光照和景深的深度控制。"
+      }
+    ]
+  },
+  "REQ_035": {
+    "requirement_desc": "生成多人聚集的活动现场图,如会议、展览、户外聚会等场景,画面中需要呈现多个人物同框、有组织的群体互动氛围,背景有明显的活动标识或场地特征",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-ce4ef3f5",
+        "strategy_name": "草图布局驱动路线:ControlNet + 分区控制精准生成流",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流从草图布局设计开始,通过姿态与分区控制、细节生成与风格统一,全面覆盖了多人位置关系、群体互动氛围、活动标识和场地特征的所有核心需求,并提供可选的动态化与合成能力,语义覆盖度最高且流程更加系统化。"
+      },
+      {
+        "strategy_id": "STRAT-5fe35cac",
+        "strategy_name": "参数公式直出路线:模型优选 + 可视化分区 + 批量筛选流",
+        "is_selected": false,
+        "coverage_score": 0.82,
+        "explanation": "该工作流通过模型选择、可视化分区固定和批量生成筛选三个阶段,完整覆盖了多人同框、群体互动氛围和人脸修复的核心需求,但对活动标识和场地特征的生成控制较为隐含,缺少明确的背景细节控制步骤。"
+      }
+    ]
+  },
+  "REQ_036": {
+    "requirement_desc": "生成真实物品的特写或陈列展示图,物品摆放清晰、细节可辨,适合用于产品展示或场景道具呈现,画面构图干净突出主体",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-744c86a3",
+        "strategy_name": "摄影级产品特写路线(微距/材质/工作室布光)",
+        "is_selected": false,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了用户需求的核心要素:通过三阶段流程系统性地实现了产品特写生成(材质定义、构图控制)、光影质感强化(精准光照、无景深全画面清晰)、细节层次叠加(8K输出、微观纹理),特别强调了'细节可辨'和'构图干净突出主体'的要求。唯一微小不足是部分能力(如爆炸分解图)超出了基础产品陈列展示的范畴,但不影响核心需求的完整实现。"
+      },
+      {
+        "strategy_id": "STRAT-d37d2be3",
+        "strategy_name": "高端产品爆炸图信息图流派",
+        "is_selected": false,
+        "coverage_score": 0.72,
+        "explanation": "该工作流能够生成高质量的产品特写图(阶段1覆盖了8K分辨率、超写实质感、清晰边缘等核心要求),但后续阶段(爆炸图、动态效果)偏离了用户'真实物品陈列展示'的静态特写需求。工作流重点放在了产品分解和动态展示上,而非单纯的'摆放清晰、细节可辨'的静态陈列,导致整体语义覆盖度降低。"
+      }
+    ]
+  },
+  "REQ_037": {
+    "requirement_desc": "生成同一人物在同一场景中多角度、多姿态的即时抓拍效果图,画面呈现自然随意的动态感,如行走、转身、低头、仰望等非摆拍状态,整体风格真实生活化",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-3c6df37a",
+        "strategy_name": "Nano Banana Pro 参数化多视角/多姿态批量路线",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流全面覆盖了用户需求的核心要素:通过JSON结构化提示词确保人物一致性,系统化设计多姿态(行走、转身、低头、仰望等非摆拍状态)和多角度方案,并通过拼贴生成实现同场景多视角输出,最后进行人脸修复保证真实生活化效果。唯一可能的改进空间在于场景一致性的显式控制机制未单独强调,但整体方案已通过背景参数锁定解决。"
+      }
+    ]
+  },
+  "REQ_038": {
+    "requirement_desc": "生成动物(如马)在运动瞬间被捕捉的高动态画面,鬃毛飞扬、肢体伸展,呈现出强烈的瞬间张力和动感,背景简洁以突出主体动态",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-9b4cae79",
+        "strategy_name": "静态高动态图像流派:专业摄影级瞬间张力捕捉",
+        "is_selected": true,
+        "coverage_score": 0.98,
+        "explanation": "该工作流全面覆盖了用户需求的所有核心要素:通过阶段1-2精确构建高动态运动瞬间(鬃毛飞扬、肢体伸展)和专业摄影参数模拟实现瞬间张力捕捉,阶段3的光线氛围定调和阶段4的毛发动态及粒子特效强化完美呈现动感,阶段1明确的简洁背景策略突出主体,阶段5的高分辨率输出和全向参考确保最终质量。唯一微小不足是未明确提及后期微调迭代机制,但整体语义覆盖极其完整且具备可执行性。"
+      }
+    ]
+  },
+  "REQ_039": {
+    "requirement_desc": "生成人物在真实日常场景(街头、公园、机场等)中被随手拍下的多张图片拼贴效果,画面构图不刻意、视角多变(含俯拍脚部、镜中自拍、远景抓拍等),整体呈现出碎片化的生活记录感",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-07cdf8b9",
+        "strategy_name": "多视角生活拼贴自动化流派",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了用户需求的核心要素:真实日常场景库(街头、公园、机场等)、多变视角(俯拍脚部、镜中自拍、远景抓拍)、非刻意构图(candid/unposed)、碎片化拼贴布局(不对称、尺寸随机)以及胶片质感增强。唯一微小不足是未明确提及镜中自拍等特定视角的技术实现细节,但整体语义覆盖极为完整且具备可执行性。"
+      }
+    ]
+  },
+  "REQ_040": {
+    "requirement_desc": "制作图文卡片时,需要让插图与文字在语义上高度呼应——比如用可爱小驴的不同表情和动作配合对应的幽默文字,每张小卡片中图在上、文字在下,插图内容直接反映文字含义,形成一眼就能看懂的图文配合效果",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-23d50376",
+        "strategy_name": "全自动批量语义驱动图文卡片流水线(Lovart + Nano Banana)",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完整覆盖了用户需求的所有核心要素:通过元提示词规范确保风格一致性,批量生成多情绪小驴表情集建立可复用资产,利用Lovart的语义驱动能力实现文案与插图的精准呼应,并通过AI辅助排版自动完成'图上文下'布局合成与批量导出。唯一微小不足是阶段划分较细,可能增加初学者理解成本。"
+      },
+      {
+        "strategy_id": "STRAT-fd507b56",
+        "strategy_name": "精品单卡语义匹配路线(Midjourney 图集 + 逐张情绪匹配)",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流同样覆盖了核心需求:建立小驴情绪图集、实现图文语义匹配、完成AI辅助排版合成。但相比STRAT-23d50376,缺少了阶段1的元提示词规范设计环节,在风格一致性保障和批量自动化流程的系统性描述上略显不足,更依赖人工标注情绪标签进行匹配。"
+      }
+    ]
+  },
+  "REQ_041": {
+    "requirement_desc": "制作多图拼贴帖子时,需要将多张照片或截图按照叙事顺序排列在一张大图中,并在关键图片上叠加说明性文字标注,让图片和文字共同讲述一个完整故事,文字起到补充说明和情感点评的作用",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-d8340a6a",
+        "strategy_name": "实拍照片 + 模板工具拼贴叙事路线(Canva/醒图/ProCCD/CapCut)",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完整覆盖了用户需求的核心要素:多图按叙事顺序排列(阶段2的叙事排序规划和布局排版)、关键图片上叠加说明性文字标注(阶段3的文字叠加与情感标注)、文字起到补充说明和情感点评作用(碎碎念式标注)。工作流还额外提供了抠图、色调调整、装饰元素等增强功能,使最终输出更专业。唯一微小不足是阶段1的抠图预处理对于某些简单拼贴场景可能是可选而非必需的。"
+      }
+    ]
+  },
+  "REQ_042": {
+    "requirement_desc": "制作信息图文海报时,需要将大标题、分类小标题与正文段落按照清晰的层级排布在版面上,标题用大字醒目展示,正文紧跟其下,整体版面分区明确、图文对应,让读者能快速扫读获取信息",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-c5ac801a",
+        "strategy_name": "参考图引导生成路线(版式迁移·稳定可控)",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流全面覆盖了信息图文海报制作的核心需求,包括层级排布、标题醒目展示、版面分区明确和图文对应等关键要素,并通过五个阶段系统性地实现从生成到精修的完整流程。唯一轻微不足是对快速扫读的视觉引导路径设计可以更明确地体现在具体执行步骤中。"
+      }
+    ]
+  },
+  "REQ_043": {
+    "requirement_desc": "在图片上叠加标题文字,文字大小、粗细、颜色各异,形成层次感强的排版效果——例如大标题用粗体醒目字体,副标题用细体小字,整体风格统一(如深色系商务风或简约设计风)",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-941dc269",
+        "strategy_name": "蓝图A:纯AI自动生成路线",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流完整覆盖了用户需求的核心要素:通过AI自动化实现多层次文字排版(大标题粗体、副标题细体)、自动匹配字体颜色大小形成层次感、并支持统一风格(商务风/简约风)。唯一轻微不足是对手动精细调整参数(如具体字距、行距数值)的控制粒度描述较少。"
+      },
+      {
+        "strategy_id": "STRAT-6d8e64bb",
+        "strategy_name": "蓝图B:智能修复增强路线",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流详细覆盖了文字叠加的参数化实现(字体选择、字重、字距、行距等具体数值),并通过阴影、透明度等技术强化层次感,满足统一风格需求。相比第一个方案,其依赖更多手动操作而非AI自动化,在智能化程度上略逊一筹,但在精细控制方面更具优势。"
+      }
+    ]
+  },
+  "REQ_044": {
+    "requirement_desc": "在AI生成的卡通角色图片上叠加幽默吐槽文案,文字直接覆盖在图片上方,字体简洁白色,与画面情绪呼应,形成图文结合的表情包风格内容",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-ff13853b",
+        "strategy_name": "路线A:提示词驱动的图文一体化生成(推荐)",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流通过精准提示词工程实现AI一次性生成图文结合内容,完整覆盖了卡通角色生成、幽默文案创作、文字叠加和情绪匹配等核心需求,且强调文字与画面的视觉互动关系。唯一的潜在风险是依赖AI模型的文字渲染能力,可能在中文字体精准控制上存在局限性。"
+      },
+      {
+        "strategy_id": "STRAT-ff97a911",
+        "strategy_name": "路线B:AI智能后处理叠加",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流采用分阶段方法,先生成角色底图,再通过多模态AI识别情绪并生成文案,最后使用专业工具叠加文字,流程清晰且可控性强。相比一次性生成方案,该方法在文字渲染质量和位置精准度上更有保障,但增加了工作流复杂度和人工介入环节。"
+      }
+    ]
+  },
+  "REQ_045": {
+    "requirement_desc": "制作多宫格拼图帖子,每格图片配有对应的标题文字或字幕说明,文字风格统一,整体排列整齐,适合用于周记、日历、流程说明等系列内容展示",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-494cc29c",
+        "strategy_name": "Coze 工作流端到端批量生成路线(宫格/长图/信息可视化)",
+        "is_selected": true,
+        "coverage_score": 0.98,
+        "explanation": "该工作流完整覆盖了多宫格拼图帖子制作的所有核心需求:从主题拆解、批量图像生成、统一风格控制、文字渲染嵌入、网格布局排版到最终切割输出,形成完整闭环,特别适合周记、日历、流程说明等系列内容展示。唯一微小不足是未明确提及用户自定义文字样式的灵活性调整接口。"
+      }
+    ]
+  },
+  "REQ_047": {
+    "requirement_desc": "制作数据报告类图文内容:包含柱状图、饼图、折线图、词云图、环形图等多种数据可视化图表,配合标题、要点文字说明,整体呈现专业研究报告的视觉风格,色彩搭配统一(如蓝紫色系或橙色系)",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-ce0c37dd",
+        "strategy_name": "数据驱动 AI 图表/报告生成路线(多图表 + 词云 + 美化重构)",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流全面覆盖了用户需求的所有核心要素:多种图表类型生成(柱状图、饼图、折线图、词云图、环形图)、统一色彩配色方案(蓝紫色系/橙色系)、标题与要点文字说明、以及专业研究报告的整体视觉风格排版。工作流结构清晰,从数据分析、图表生成、文字配套到最终排版输出形成完整闭环,且提供了多种工具实现路径(DeepSeek/ChatGPT、Nano Banana、镝数图表等),具备很强的可操作性。唯一微小不足是在环形图的具体生成细节上着墨较少,但整体能力已充分满足需求。"
+      }
+    ]
+  },
+  "REQ_048": {
+    "requirement_desc": "制作流程图/架构示意图:用箭头、方框、层级结构或立体堆叠图形展示系统架构、业务流程或概念层级关系,配合文字标注说明各模块功能,视觉上清晰呈现逻辑关系",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-360e072f",
+        "strategy_name": "参考图逆向复刻流派:草图/截图→AI识别→可编辑数字图表",
+        "is_selected": false,
+        "coverage_score": 0.92,
+        "explanation": "This workflow directly addresses the core requirement of creating flowcharts/architecture diagrams with arrows, boxes, hierarchical structures through AI-powered diagram recognition and iterative editing. It excels at converting sketches to editable diagrams and enabling conversational refinement, though it focuses more on diagram editing than initial creation from scratch."
+      },
+      {
+        "strategy_id": "STRAT-cde98036",
+        "strategy_name": "LLM 生成代码 → 渲染为图路线(HTML/SVG/Code Interpreter)",
+        "is_selected": true,
+        "coverage_score": 0.45,
+        "explanation": "This workflow is designed for creating magazine-style infographics with fantasy illustrations and long-form content layouts, which diverges significantly from the requirement of creating technical flowcharts/architecture diagrams with arrows, boxes, and hierarchical structures. While it produces visual content with text annotations, it does not address system architecture visualization or business process flowcharts."
+      }
+    ]
+  },
+  "REQ_049": {
+    "requirement_desc": "将整个帖子内容拆分为多个独立小格子并排列成网格或矩阵布局,每个格子承载一个独立的场景或信息单元,格子之间有明显的分隔边界,整体看起来像一张由多张小图拼合而成的大图",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-425a984a",
+        "strategy_name": "结构化 Prompt 一次直出多格/网格路线",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了用户需求的核心要素:通过阶段1明确网格布局和内容拆分逻辑,阶段2使用AI模型一次性生成完整网格大图(多个独立场景单元、清晰分隔边界),阶段3进行质检和局部精修,阶段4支持高清输出和可选的网格拆解。唯一轻微不足是对于非AI直出场景(如手动拼接多张独立图片)的覆盖较少,但整体已深度满足'多个独立小格子排列成网格、格子间有明显分隔边界、像多张小图拼合成大图'的核心诉求。"
+      }
+    ]
+  },
+  "REQ_050": {
+    "requirement_desc": "在同一张图中混合使用多种内容载体形式,例如将真实照片、插画角色、产品图、文字说明、图表等不同类型的视觉元素组合排布在同一个版面内,形成图文混排的丰富视觉层次",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-6aeb80a1",
+        "strategy_name": "QA Agent 闭环信息图/海报自动化路线",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了多种内容载体混排的核心需求,从版面信息架构规划、多类型视觉元素分区生成、品牌化约束注入到质量校验与批量输出,形成完整的端到端流程。特别是通过AI版面自动排布、多主体场景合成、图像内文字渲染等能力,能够在单一版面内有效整合真实照片、插画、产品图、文字说明、图表等多种视觉元素,并通过QA代理机制确保输出质量。唯一微小不足是对手工精修环节的描述相对较少。"
+      },
+      {
+        "strategy_id": "STRAT-1b504a8e",
+        "strategy_name": "AI提示词驱动混合媒体拼贴创意流派",
+        "is_selected": false,
+        "coverage_score": 0.82,
+        "explanation": "该工作流通过内容载体清单化、多载体混排核心提示词构建、风格参考锚定和局部元素精修四个阶段,有效实现了多种视觉元素的混合排布。特别强调了混合媒体拼贴美学和分层叙事构图生成能力,能够在单次生成中融合照片、插画、几何图形、文字等多种载体。相比第一个工作流,该方案更侧重提示词工程和风格控制,但在自动化版面规划、品牌化约束注入和系统性质量校验方面的覆盖深度略显不足。"
+      }
+    ]
+  },
+  "REQ_051": {
+    "requirement_desc": "以统一的视觉主角(如同一个卡通角色、同一个人物、同一主题场景)贯穿多个分格画面,每个格子呈现该主角在不同场景或状态下的样子,配合文字说明形成系列感强的多格叙事版式",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-1d3bf349",
+        "strategy_name": "工业化路线:角色资产先行 + 分镜批量生成",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完整覆盖了用户需求的所有核心要素:通过角色设定卡生成统一视觉主角,多格叙事分镜版式自动编排实现系列感强的叙事结构,批量生成保持角色一致性,并配合文字说明和对话气泡形成完整多格叙事版式。六个阶段从角色锁定、分镜规划、批量生成、场景连贯、文字排版到质检修复形成完整闭环,深度解决了统一主角贯穿多格的技术难点。"
+      },
+      {
+        "strategy_id": "STRAT-007e744f",
+        "strategy_name": "轻量直出路线:单Prompt多格叙事一次成型",
+        "is_selected": false,
+        "coverage_score": 0.78,
+        "explanation": "该工作流覆盖了核心需求的主要环节:通过结构化prompt实现单次多格生成,保持角色一致性和文字叙事排版。但相比第一个方案,缺少专门的角色设定卡生成阶段和跨格场景连贯性约束机制,且没有批量自动化工作流支持,在处理复杂多格叙事(如25宫格)时的系统性和可扩展性较弱。"
+      }
+    ]
+  },
+  "REQ_052": {
+    "requirement_desc": "将多张图片按网格或分区方式拼贴成一张图,每个区域展示不同角度或不同场景,整体画面有清晰的分割感和节奏感",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-13eb963a",
+        "strategy_name": "叙事性场景拼贴流派(Bento-grid 不规则布局)",
+        "is_selected": false,
+        "coverage_score": 0.92,
+        "explanation": "该方案深度覆盖了用户需求的核心要素:通过bento-grid不规则布局实现清晰分割感,通过Hero Cell和Feature Cell的大小差异化创造视觉节奏,通过色调统一和风格一致性保证整体协调。唯一轻微不足是对传统规则网格(如3x3)的支持描述相对较少,更侧重不规则布局。"
+      },
+      {
+        "strategy_id": "STRAT-425a984a",
+        "strategy_name": "结构化 Prompt 一次直出多格/网格路线",
+        "is_selected": true,
+        "coverage_score": 0.88,
+        "explanation": "该方案完整覆盖了网格拼贴的技术实现路径,特别强调了3x3/5x5等规则网格的一次性生成能力和角色一致性保持,符合用户对多角度/多场景展示的需求。但在'清晰分割感和节奏感'的设计层面(如不规则布局、视觉层级强化)的阐述不如STRAT-13eb963a深入。"
+      }
+    ]
+  },
+  "REQ_054": {
+    "requirement_desc": "生成包含多个独立小格子的图文排版画面,每个格子内有图片和文字说明,格子之间疏密有致、整齐排列,整体呈现信息图表或内容合集的视觉效果",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-cde98036",
+        "strategy_name": "LLM 生成代码 → 渲染为图路线(HTML/SVG/Code Interpreter)",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流完整覆盖了用户需求的核心要素:通过LLM进行内容结构化拆解为多个独立板块,AI生成HTML实现多格子布局和疏密排列,奇幻风格插画生成确保每个格子内有图片和文字说明,QA评分保证视觉质量,最终导出PNG长图呈现信息图表效果。唯一小缺陷是案例更侧重心理学科普长图,但技术能力完全适用于通用信息图表和内容合集场景。"
+      },
+      {
+        "strategy_id": "STRAT-74369213",
+        "strategy_name": "AI分镜逻辑驱动的多格子视频生成路线",
+        "is_selected": false,
+        "coverage_score": 0.45,
+        "explanation": "该工作流主要针对视频分镜和动态场景生成,使用9宫格/25宫格布局实现视觉统一,但核心是AI视频生成而非静态图文排版。虽然提到格子布局和场景说明,但缺少文字排版、信息图表设计、静态图文混排等用户需求的核心要素,更适合短片制作而非信息图表生成。"
+      }
+    ]
+  },
+  "REQ_055": {
+    "requirement_desc": "生成人物局部特写画面,如放大呈现嘴巴咬食物、手持物品、耳朵佩戴饰品、鼻子、指甲等身体局部细节,画面填充感强,细节清晰可见",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-6b8ca29b",
+        "strategy_name": "专属 LoRA 精细化控制生图路线(人像/产品)",
+        "is_selected": true,
+        "coverage_score": 0.15,
+        "explanation": "该工作流专注于生成儿童户外全身/半身照片,强调自然光线、场景氛围和多角度抓拍,但完全未涉及用户核心需求——身体局部特写(嘴巴、手部、耳朵、鼻子、指甲等)的放大呈现和细节填充,语义方向偏离严重。"
+      },
+      {
+        "strategy_id": "STRAT-c2bf1271",
+        "strategy_name": "提示词驱动极端特写路线",
+        "is_selected": false,
+        "coverage_score": 0.0,
+        "explanation": "该工作流为空,未提供任何阶段或工具配置,完全无法满足用户需求。"
+      }
+    ]
+  },
+  "REQ_056": {
+    "requirement_desc": "生成人物近景半身或胸部以上的画面,突出人物面部表情和情绪,背景适当虚化,让观看者能清楚看到人物的神态与互动感",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-6b3e3450",
+        "strategy_name": "情绪驱动型近景肖像策略",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流语义覆盖度最高,完美匹配用户需求:阶段1明确情绪基调与近景构图框架,阶段2构建光线与背景虚化参数直接对应'背景适当虚化'需求,阶段3注入情绪细节与皮肤质感深度实现'突出面部表情和情绪、清楚看到神态',阶段4质检兜底确保商业级输出。流程逻辑清晰且每个阶段都紧扣用户核心诉求。"
+      },
+      {
+        "strategy_id": "STRAT-f84e9426",
+        "strategy_name": "MJ Omni Reference 锁主体 + Prompt 批量变体路线",
+        "is_selected": false,
+        "coverage_score": 0.92,
+        "explanation": "该工作流全面覆盖了用户需求的所有核心要素:通过阶段1锁定人脸一致性,阶段2精准控制近景构图和背景虚化,阶段3细化面部表情和光影氛围,完整实现了'突出人物面部表情和情绪、背景适当虚化、清楚看到人物神态与互动感'的要求。唯一轻微不足是阶段1的人脸一致性保持对于单次生成场景可能是过度设计。"
+      }
+    ]
+  },
+  "REQ_057": {
+    "requirement_desc": "生成产品或物品的极近距离特写图,如食物截面、商品细节、小物件放大展示,画面主体占满画幅,质感和纹理清晰突出",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-744c86a3",
+        "strategy_name": "摄影级产品特写路线(微距/材质/工作室布光)",
+        "is_selected": false,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了用户需求的核心要素:通过三阶段流程实现了产品特写的材质质感控制、微距构图与画幅填充、精准光影强化,以及食物截面、商品细节、小物件放大等具体场景的生成能力,并明确支持8K高分辨率输出。唯一微小不足是对某些极端材质(如液体、透明物体)的特殊处理描述相对较少。"
+      }
+    ]
+  },
+  "REQ_058": {
+    "requirement_desc": "生成具有强烈透视纵深感的室内空间图,画面中窗框、拱门、地板线条等建筑元素形成明显的空间层次,光线从远处窗口射入,营造出由近到远的视觉延伸效果",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-c61ec3cc",
+        "strategy_name": "结构化Prompt五维度直出路线",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完整覆盖了用户需求的所有核心要素:通过一点透视精确控制建筑元素(窗框、拱门、地板线条)形成空间层次,远处窗口光源注入配合体积光效营造由近到远的视觉延伸,分层空间元素叠加强化透视纵深感,并提供高分辨率输出。唯一微小不足是未明确提及对输入图像结构的锁定能力。"
+      },
+      {
+        "strategy_id": "STRAT-728a2e72",
+        "strategy_name": "Depth/白模 ControlNet 结构锁定 + 风格迁移室内渲染路线",
+        "is_selected": true,
+        "coverage_score": 0.72,
+        "explanation": "该工作流通过ControlNet Depth锁定空间结构、IPAdapter Plus风格迁移和光影优化,能够生成具有透视感的室内空间图,但其核心聚焦于风格迁移(奶油色调、地中海/法式风格)而非透视纵深感的精确控制。对于窗框、拱门、地板线条形成明显空间层次和光线从远处射入的视觉延伸效果的描述不够明确和系统化。"
+      }
+    ]
+  },
+  "REQ_059": {
+    "requirement_desc": "生成采用夸张变形构图的图片,例如鱼眼镜头效果将人物或场景扭曲成球形全景、仰拍使近处物体极度放大而远处极度缩小,或通过搞怪角度让画面产生强烈的视觉冲击感",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-0919d3e6",
+        "strategy_name": "极端视角 Prompt 路线(鱼眼/广角/球形/仰拍)",
+        "is_selected": true,
+        "coverage_score": 0.98,
+        "explanation": "该工作流全面覆盖了用户需求的三大核心场景:鱼眼镜头球形畸变(阶段1)、仰拍/俯拍透视变形(阶段2)、搞怪角度视觉冲击(阶段3荷兰角),并提供了详细的提示词实现方案和多平台工具支持。唯一微小不足是对某些极端创意角度(如360度环绕畸变)的探索深度可以进一步扩展,但已充分满足实际应用需求。"
+      }
+    ]
+  },
+  "REQ_060": {
+    "requirement_desc": "生成画面中存在嵌套框架效果的图片,如在沙漠场景中用一个悬空的矩形框将主体框住形成画中画,或利用水面倒影与实景上下对称形成嵌套镜像构图,制造超现实的空间突破感",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-fafdbef0",
+        "strategy_name": "悬空矩形框画中画流派",
+        "is_selected": false,
+        "coverage_score": 0.92,
+        "explanation": "该工作流全面覆盖了用户需求的两个核心场景:阶段2专门针对'沙漠场景悬空矩形框'的画中画效果,阶段1和3提供了完整的场景构建和细节强化流程。虽然对水面倒影镜像构图的描述相对简略,但通过'画中画嵌套框架构图生成'能力和多个相关案例充分证明了对核心需求的深度理解和实现路径。"
+      },
+      {
+        "strategy_id": "STRAT-e06aec89",
+        "strategy_name": "水面倒影嵌套镜像流派",
+        "is_selected": true,
+        "coverage_score": 0.75,
+        "explanation": "该工作流深度覆盖了水面倒影嵌套镜像构图的生成,提供了详细的prompt工程和专项工具(Midjourney Editor水面反射功能),但对用户需求中明确提到的'沙漠场景中悬空矩形框'这类画中画框架构图覆盖不足,主要聚焦于对称镜像而非框架嵌套。"
+      }
+    ]
+  },
+  "REQ_061": {
+    "requirement_desc": "生成具有强烈色彩对比的艺术插画,整体画面以高饱和度的红色与蓝色为主色调,两种颜色形成鲜明的冷暖对撞,背景大面积纯色铺底,视觉冲击力极强",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-0de66146",
+        "strategy_name": "风格锁定直出路线:--sref 参数锚定 × 双色调构图精控 × 批量优选",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "全面覆盖用户需求的所有核心要素:红蓝高饱和度主色调、冷暖对撞、大面积纯色铺底、强烈视觉冲击力。通过双色调构图控制、风格参考码锁定、饱和度精准控制、波普纹理与光效增强等多层次能力,系统性地实现了从构图到色彩到视觉增强的完整流程,且提供批量生成与风格持久化能力支持系列化产出。"
+      },
+      {
+        "strategy_id": "STRAT-fdfe1caa",
+        "strategy_name": "多风格分层融合路线:风格基底选择 × 双色分层结构 × 饱和度校验微调",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "有效覆盖核心需求,通过风格基底选择、红蓝双色分层结构、饱和度校验等阶段实现红蓝冷暖对撞与高饱和度效果。相比第一个方案,缺少风格参考码锁定和波普纹理/光效增强等高级视觉冲击力强化手段,但通过双重曝光与剪影融合提供了独特的叙事深度维度。"
+      }
+    ]
+  },
+  "REQ_062": {
+    "requirement_desc": "在以暗色或单色为主的画面中,用局部的高饱和亮色(如红色心脏、橙色暖光窗口、金黄色星光)作为点睛之笔,让视线自然聚焦到这个色彩亮点上,形成强烈的视觉引导",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-94f4e98d",
+        "strategy_name": "Prompt 驱动全自动暗调点睛生成流(文生图/AI视频一体化路线)",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完整覆盖了用户需求的三个核心要素:暗色基调建立、高饱和亮色点睛、视觉引导强化。通过系统化的三阶段流程,结合暗调长调配色、选择性色彩保留、暖光点睛、发光效果等多种能力,全面实现了从暗色底图到局部亮色焦点的视觉引导效果,且提供了丰富的风格扩展选项(赛博朋克、金色粒子等)。"
+      },
+      {
+        "strategy_id": "STRAT-a856d593",
+        "strategy_name": "ComfyUI 节点化工作流路线(精准控制版)",
+        "is_selected": false,
+        "coverage_score": 0.55,
+        "explanation": "该工作流覆盖了暗色底图生成和发光效果增强两个环节,但关键的阶段2(局部色彩注入)被完全截断丢弃,导致缺失了实现'局部高饱和亮色点睛'这一核心需求的关键步骤。虽然阶段1和阶段3的能力本身有效,但工作流的完整性和可执行性存在重大缺陷。"
+      }
+    ]
+  },
+  "REQ_063": {
+    "requirement_desc": "生成整体色调统一、饱和度偏高的场景图,例如全画面笼罩在深蓝色夜光氛围或浓郁的赤红土地色调中,让单一主色调主导整个画面,营造出沉浸式的强烈色彩氛围感",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-2b07a94f",
+        "strategy_name": "主色调锚定 + 色彩脚本分层 + 胶片氛围强化流派",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流完整覆盖了用户需求的核心要素:通过主色调锚定直接生成单一色调主导的画面,配合色彩脚本分区域控制实现深度层次,再通过光照氛围和胶片效果强化沉浸式色彩氛围感。四个阶段层层递进,从色调统一、高饱和度到沉浸式氛围营造,完整解决了'让单一主色调主导整个画面'的核心诉求,且提供了丰富的案例支撑(深蓝夜光、赤红土地等典型场景)。"
+      },
+      {
+        "strategy_id": "STRAT-f13bb636",
+        "strategy_name": "HEX 色板精准定调 + LUT 风格模板跨模型复用流派",
+        "is_selected": false,
+        "coverage_score": 0.78,
+        "explanation": "该工作流通过HEX色板提取、LUT风格提示词和风格参考代码三个阶段,提供了精准的色调控制方法,能够实现色调统一和高饱和度效果。但其侧重于色彩提取和风格复现的技术流程,对于如何直接生成单一主色调主导整个画面的沉浸式氛围感描述相对间接,需要依赖参考图或预设风格,缺少从零开始直接生成目标色调场景的能力。"
+      }
+    ]
+  },
+  "REQ_064": {
+    "requirement_desc": "生成具有强烈氛围感的插画风场景图,整体画面以深蓝色调为主,室内外场景都笼罩在宁静的夜色中,窗户透出暖黄色灯光形成冷暖对比,画面质感接近油画或数字绘画风格,传达出静谧、沉思、略带忧郁的情绪氛围",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-000f9272",
+        "strategy_name": "高水准氛围插画路线:色调建立→光源植入→质感收敛",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完整覆盖了用户需求的所有核心要素:深蓝色调夜景底图构建、冷暖对比光源设计(暖黄窗灯)、油画质感强化与情绪氛围后处理,三阶段逻辑清晰且每个阶段都有精确的技术实现路径和案例支撑。唯一微小不足是批量变体生成环节未明确提及,但整体语义覆盖度极高。"
+      },
+      {
+        "strategy_id": "STRAT-61e0f069",
+        "strategy_name": "快速全自动管线路线:提示词模板→ComfyUI节点管线→批量优选",
+        "is_selected": false,
+        "coverage_score": 0.82,
+        "explanation": "该工作流通过预设模板+ComfyUI自动化管线+批量变体生成,实现了用户需求的核心视觉元素(深蓝夜景、暖黄窗灯、油画风格、静谧情绪),且强调自动化效率。但在油画质感强化的具体后处理步骤描述上不如STRAT-000f9272详细,缺少渐变映射等精细调色环节的明确说明。"
+      }
+    ]
+  },
+  "REQ_065": {
+    "requirement_desc": "制作色彩鲜艳、视觉冲击力强的宣传海报,背景使用渐变色块(蓝紫、橙红等高饱和度色彩),搭配几何抽象图形装饰,文字排版醒目大气,整体呈现出热烈、充满活力的欢庆氛围",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-419e9ab1",
+        "strategy_name": "极速直出路线:精准配色锚点 × AI一键生成 × 多轮收敛",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了用户需求的所有核心要素:通过精准配色库选定高饱和度蓝紫/橙红渐变色组,使用几何抽象图形装饰,配备电影级光效和8K商业级渲染,并通过多轮迭代优化文字排版和视觉冲击力。唯一微小不足是文字排版的具体实现细节相对较少,但整体方案完整且有大量案例验证支撑。"
+      },
+      {
+        "strategy_id": "STRAT-fa2fe44f",
+        "strategy_name": "参考图驱动路线:GPT反向读图 × AI主图生成 × 版式合成",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流通过参考图反向解析和GPT关键词提取实现了精准的风格控制,覆盖了渐变背景、几何装饰和色彩饱和度控制等核心需求,并在阶段3专门处理文字排版与版式合成。相比第一个方案,其在色彩方案的预设库和多轮迭代优化方面略显不足,但参考图驱动的方式在风格还原度上有独特优势。"
+      }
+    ]
+  },
+  "REQ_067": {
+    "requirement_desc": "生成具有强烈视觉冲击力的超现实主义风格图像:画面以大地色系(深红、赭石、深蓝)为主调,将白马、牛仔等元素置于极简的荒漠/盐湖场景中,营造出孤寂、神秘、如油画般的电影感氛围",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-4914537c",
+        "strategy_name": "文生图底帧 + 图生视频动态化路线(首尾帧/多镜头/Hailuo-Seedance-Kling)",
+        "is_selected": true,
+        "coverage_score": 0.35,
+        "explanation": "该工作流专注于非常规摄像机角度(虫眼仰拍、鸟瞰俯视、POV等)和视角转换技术,虽然可以提供视觉冲击力和电影感,但用户需求的核心是超现实主义风格、大地色系、荒漠场景和特定元素(白马、牛仔),而非摄像机角度实验。工作流缺少色调风格化、场景氛围营造和超现实主义美学控制能力。"
+      },
+      {
+        "strategy_id": "STRAT-f84e9426",
+        "strategy_name": "MJ Omni Reference 锁主体 + Prompt 批量变体路线",
+        "is_selected": false,
+        "coverage_score": 0.25,
+        "explanation": "该工作流专注于人像面部特写、表情控制、浅景深和胶片质感,但用户需求是超现实主义风格的荒漠/盐湖场景中的白马、牛仔等元素,两者在主题、构图和场景类型上完全不匹配。工作流缺少场景构建、色调控制(大地色系)和超现实主义风格渲染能力。"
+      }
+    ]
+  },
+  "REQ_068": {
+    "requirement_desc": "生成3D卡通风格的拟人化动物角色,角色具有毛绒质感和丰富的表情神态,能够在不同生活场景(办公室、卧室、户外)中呈现出喜怒哀乐等情绪状态,整体风格类似皮克斯动画",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-071a27d4",
+        "strategy_name": "快速批量出图流派(场景×情绪矩阵变量公式)",
+        "is_selected": false,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了用户需求的所有核心要素:通过结构化提示词实现3D卡通风格和毛绒质感,系统化生成多场景多情绪组合,并通过角色一致性机制和统一渲染参数确保皮克斯风格的整体性。唯一微小不足是未明确提及动画或动态表情的可能性,但对于静态角色生成需求已完全满足。"
+      }
+    ]
+  },
+  "REQ_071": {
+    "requirement_desc": "将动物(如猫咪)与各种食物、道具进行创意合成,给动物添加配饰(帽子、假发、领带等)并嵌入食物场景中,搭配谐音梗或双关文字,制作出拟人化角色扮演的趣味表情包图片",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-46ca5644",
+        "strategy_name": "统一IP系列化批量生产流派",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流全面覆盖了用户需求的所有核心要素:动物配饰穿戴、食物场景合成、谐音梗文案生成、批量系列化输出,并通过IP一致性保持实现系列化品牌效果。唯一轻微不足是流程分为三个阶段,操作步骤相对复杂,但这也带来了更高的质量控制和系列化能力。"
+      },
+      {
+        "strategy_id": "STRAT-d6cb776f",
+        "strategy_name": "单图精品直出流派",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流通过精细prompt工程一步直出完整合成图,覆盖了动物配饰、食物场景、拟人化和谐音梗文字的核心需求,流程更简洁高效。相比第一个方案,缺少了IP角色一致性保持和批量生成能力,更适合单图精品路线而非系列化生产,在系列化表情包制作方面略显不足。"
+      }
+    ]
+  },
+  "REQ_072": {
+    "requirement_desc": "给同一张猫咪照片批量添加不同职业的帽子、道具和配件(如厨师帽、安全帽、眼镜、画板等),让猫咪看起来像在扮演各种职业角色",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-86d1256a",
+        "strategy_name": "主体一致性 + Inpaint 局部换装/换景路线",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流通过IP-Adapter保持猫咪主体一致性,使用Flux-kontext局部重绘精准添加不同职业道具,支持多道具叠加和批量处理,完整覆盖了用户需求的核心要素(同一张猫咪照片+批量添加不同职业道具)。唯一不足是流程相对复杂,需要手动构建遮罩和分步执行。"
+      },
+      {
+        "strategy_id": "STRAT-a0341ba4",
+        "strategy_name": "轻量级文生图批量生成路线",
+        "is_selected": false,
+        "coverage_score": 0.68,
+        "explanation": "该工作流通过AI文生图工具(豆包AI/小云雀AI)批量生成职业场景,操作简便且支持批量处理,但核心缺陷是无法保证每次生成使用的是'同一张猫咪照片'——AI每次会重新生成猫咪形象,导致不同职业图中的猫咪外观可能存在差异,不完全符合用户'同一张照片'的严格要求。"
+      }
+    ]
+  },
+  "REQ_073": {
+    "requirement_desc": "将真实照片转换成具有统一色调风格的插画效果,整体呈现蓝紫色调的复古油画或动画风格,让风景场景看起来像艺术插图",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-581b4a18",
+        "strategy_name": "高精度全自动路线:ComfyUI Flux深度控制 × 蓝紫色调插画转换",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流通过深度图提取保留场景结构,结合Flux Redux风格迁移和专门的蓝紫色调LoRA/提示词,系统性地实现了照片到统一蓝紫色调插画的转换,并包含光照氛围强化和超分辨率输出。技术路径更完整,对结构保留、色调统一、批量处理和高清输出的控制更精确,全面覆盖用户需求。"
+      },
+      {
+        "strategy_id": "STRAT-8d447fff",
+        "strategy_name": "轻量快速路线:Midjourney提示词驱动蓝紫色调油画转换",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流通过Midjourney niji 6模型和详细的蓝紫色调提示词体系(Blue Hour/Nordic Noir/Very light purple等)直接实现照片到插画的风格转换,完整覆盖了蓝紫色调、复古油画/动画风格、批量统一处理等核心需求。略微不足在于依赖Midjourney平台,对结构保留的精确控制相对较弱。"
+      }
+    ]
+  },
+  "REQ_074": {
+    "requirement_desc": "制作图文排版信息图,将多张食材产品图片抠出后整齐排列在统一背景上,配合文字说明组合成内容丰富的科普海报",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-494cc29c",
+        "strategy_name": "Coze 工作流端到端批量生成路线(宫格/长图/信息可视化)",
+        "is_selected": true,
+        "coverage_score": 0.45,
+        "explanation": "该工作流专注于多宫格分镜图文内容生成和排版,但用户需求核心是「食材产品图片抠图后排列+文字说明」,该流程缺少关键的图像抠图/背景移除能力,且从文本生成图像而非处理已有产品图片,语义匹配度较低。"
+      }
+    ]
+  },
+  "REQ_075": {
+    "requirement_desc": "生成具有强烈光影对比的场景图,画面中光源明显(如阳光折射、水面反光、彩虹色光晕),暗部极深、亮部极亮,整体呈现出戏剧性的明暗反差和光线质感",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-62b34455",
+        "strategy_name": "戏剧性体积光四阶段流派(自然场景主线)",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了用户需求的所有核心要素:通过阶段1建立明确光源(阳光折射、体积光),阶段2实现水面反光和彩虹色光晕(焦散、色散),阶段3强化极致明暗对比(Chiaroscuro、负补光),阶段4精修光线质感。四阶段递进式结构系统性地解决了光源明显、光影对比、彩虹光晕和戏剧性质感的全部要求,实现路径清晰且技术覆盖完整。"
+      },
+      {
+        "strategy_id": "STRAT-c2977647",
+        "strategy_name": "冷暖双极对立光晕流派(城市/人造场景备选)",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流通过冷暖双极光源设定(阶段1)有效建立戏剧性明暗反差,阶段2的光晕与折射特效叠加精准实现了阳光折射、彩虹色光晕需求,阶段3整合场景氛围强化了整体光线质感。相比STRAT-62b34455,该方案在体积光/丁达尔效应的系统性处理上略显不足,且缺少对光源架构和介质注入的独立阶段规划,但核心需求覆盖度仍然很高。"
+      }
+    ]
+  },
+  "REQ_076": {
+    "requirement_desc": "生成带有明显颗粒感或纸张纹理的插画风格图片,画面整体像是印刷在粗糙介质上,物体表面有细腻的颗粒噪点或手工绘制的笔触肌理",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-9c64803f",
+        "strategy_name": "路线A:MidJourney 颗粒质感直接生成法 + 纸张底图双层叠加",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了用户需求的所有核心要素:通过CAP-GRAIN实现明显颗粒感和噪点纹理,通过CAP-STIPPLE模拟手工绘制的点画笔触肌理,通过CAP-WATERCOLOR和CAP-PAPER生成纸张纹理和粗糙介质效果,并提供了多种实现路径(Flux/MidJourney/ComfyUI)和丰富的案例支撑。唯一微小不足是部分能力(如个人化风格持久化)与核心需求关联度较弱。"
+      },
+      {
+        "strategy_id": "STRAT-5e02cd3f",
+        "strategy_name": "路线B:Risograph 孔版印刷风格模拟法",
+        "is_selected": false,
+        "coverage_score": 0.82,
+        "explanation": "该工作流聚焦于Risograph孔版印刷风格,通过CAP-HALFTONE和CAP-GRAIN有效实现了颗粒感和粗糙介质质感,特别适合复古印刷品风格。但相比需求中强调的'细腻的颗粒噪点或手工绘制的笔触肌理',该路线更侧重半色调网点和色彩叠印效果,对水彩笔触、点彩等手绘肌理的覆盖相对较弱,且缺少纸张纹理素材生成等能力。"
+      }
+    ]
+  },
+  "REQ_078": {
+    "requirement_desc": "在图片上叠加对话气泡或多行说明文字,用于讲述故事背景或补充情节说明,文字带有描边或阴影效果以确保在复杂背景上清晰可读",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-54878602",
+        "strategy_name": "在线工具自动化文字叠加路线(Canva/CapCut)",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了用户需求的核心要素:对话气泡生成与定位、多行文字添加、描边/阴影效果实现、复杂背景可读性保证,并提供了Canva/CapCut/ComfyUI等多种实现路径和丰富案例支撑。唯一轻微不足是部分高级特效(如文字与主体深度融合)超出了基础需求范畴,但整体语义覆盖度极高。"
+      }
+    ]
+  },
+  "REQ_079": {
+    "requirement_desc": "制作图文并茂的科普说明卡片,每张卡片包含标题、编号、插图和详细文字说明,整体排版整齐统一,适合分步骤展示教程或知识点",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-c44e2daa",
+        "strategy_name": "DeepSeek + MD2Card 文本生成排版流派",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完整覆盖了用户需求的所有核心要素:通过DeepSeek生成结构化内容,MD2Card自动转换为图文并茂的卡片并支持自动编号和拆分,提供多种统一风格主题选择,最终导出标准格式。唯一的小缺陷是插图部分依赖MD2Card的自动配图,可能在插图定制化方面略有限制。"
+      },
+      {
+        "strategy_id": "STRAT-4d502039",
+        "strategy_name": "豆包Seedream提示词生图流派",
+        "is_selected": false,
+        "coverage_score": 0.82,
+        "explanation": "该工作流通过AI图像生成直接产出图文并茂的知识卡片,支持多种风格和版式布局,能够满足标题、插图、文字说明等基本需求。但在编号的自动化管理、系列卡片的批量统一排版方面不如专门的卡片工具,且需要多次抽卡选优增加了不确定性和时间成本。"
+      }
+    ]
+  },
+  "REQ_080": {
+    "requirement_desc": "在多图拼贴海报上为每个区域叠加带图标的标签(如勾选符号+地点名称),并在整体画面上方添加大标题和副标题文字,形成图文结合的内容合集展示效果",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-441008e2",
+        "strategy_name": "LLM + Canva 模板驱动批量合成路线",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完整覆盖了用户需求的所有核心要素:多图拼贴排版(阶段1)、区域图标标签叠加(阶段2)、大标题和副标题添加(阶段4),并通过信息层级分区(阶段3)实现图文结合的内容合集展示效果。额外提供的AI智能填充(阶段5)和批量生成(阶段6)能力进一步增强了实用性和效率,唯一微小不足是部分实现细节依赖特定工具(Canva)可能限制通用性。"
+      }
+    ]
+  },
+  "REQ_081": {
+    "requirement_desc": "为多人物展示海报中的每个人物添加姓名和职位标签,并在画面顶部叠加活动主题、专场名称等层级分明的标题文字,整体风格统一、信息密度高",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-4a3f7b3e",
+        "strategy_name": "AI全自动图层合成流派(信息结构→底图→布局→文字叠加→批量输出)",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流从信息结构预处理开始,系统性覆盖了底图生成、人物布局排版、文字层级叠加、标签渲染和批量输出全链路,完整解决用户需求中的所有关键点(多人物、姓名职位标签、顶部标题、风格统一、高信息密度)。流程逻辑清晰、层次分明,且在阶段4专门强调了文字层级叠加与标签渲染的精细化处理,覆盖度极高。"
+      },
+      {
+        "strategy_id": "STRAT-17d5d5e5",
+        "strategy_name": "模板驱动批量生成流派(预设框架→素材注入→标签渲染→风格统一输出)",
+        "is_selected": false,
+        "coverage_score": 0.92,
+        "explanation": "该工作流完整覆盖了模板框架设计、人物素材注入、姓名职位标签批量渲染、顶部标题层叠加和风格统一输出全流程,直接对应用户需求的所有核心要素(多人物展示、姓名职位标签、顶部标题文字、风格统一、信息密度高)。唯一轻微不足是阶段划分偏向模板预设,对动态人数适配的灵活性描述略少。"
+      }
+    ]
+  },
+  "REQ_082": {
+    "requirement_desc": "对同一场景或主体生成多个不同距离和景别的画面,包括远景展示整体环境、中景呈现主体与环境关系、近景突出细节,形成一组视角丰富的图片集合",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-04934b67",
+        "strategy_name": "结构化提示词路线(专业分镜术语驱动)",
+        "is_selected": false,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完整覆盖了用户需求的核心要素:通过结构化镜头列表精确控制远景/中景/近景等多景别生成,批量生成功能实现多距离画面集合,景别递进组合形成视角丰富的图片序列。唯一微小不足是对同一主体一致性保持的技术细节描述相对较少。"
+      },
+      {
+        "strategy_id": "STRAT-3c6df37a",
+        "strategy_name": "Nano Banana Pro 参数化多视角/多姿态批量路线",
+        "is_selected": true,
+        "coverage_score": 0.72,
+        "explanation": "该工作流重点聚焦于人物主体一致性保持和多姿态多角度生成,能够实现不同距离和角度的画面集合。但对用户需求中明确提到的'远景展示整体环境、中景呈现主体与环境关系、近景突出细节'等景别层次化控制缺乏专门设计,更侧重于人物姿态变化而非景别距离的系统化覆盖。"
+      }
+    ]
+  },
+  "REQ_083": {
+    "requirement_desc": "生成采用非常规拍摄角度的图片,如从低角度仰拍、从高处俯视、或模拟第一人称主观视角看向场景,让画面产生独特的视觉冲击感",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-4914537c",
+        "strategy_name": "文生图底帧 + 图生视频动态化路线(首尾帧/多镜头/Hailuo-Seedance-Kling)",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖用户需求,从专业摄像机角度术语库(42种词汇)构建提示词,到主流模型生成非常规视角底图,再到专项POV沉浸式视角生成和图生视频动态化,完整实现低角度仰拍、高处俯视、第一人称主观视角等所有要求的非常规拍摄角度,并通过鱼眼/广角畸变效果进一步增强视觉冲击感。唯一微小不足是流程较长,对初学者可能存在一定学习曲线。"
+      },
+      {
+        "strategy_id": "STRAT-94288ec5",
+        "strategy_name": "LoRA精确角度控制路线:坐标系量化控制 × 96角度批量生成 × 可选动态输出",
+        "is_selected": false,
+        "coverage_score": 0.92,
+        "explanation": "该工作流通过准备高质量基础锚点图像,结合Qwen Multi-Angle LoRA坐标系统精确控制96种角度组合(-30°仰拍到60°俯视),高效批量生成非常规视角并保持主体一致性,完全满足用户对低角度仰拍、高处俯视、POV视角的需求。相比STRAT-4914537c,该方案更侧重图像转换而非从零生成,在POV沉浸式视角的专项处理上略显不足,但在批量生成效率和精确控制方面更具优势。"
+      }
+    ]
+  },
+  "REQ_084": {
+    "requirement_desc": "生成能展示宽广空间感的室内或室外全景图,画面中包含完整的环境纵深,让观看者感受到场景的整体规模和空间层次",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-108fb2c7",
+        "strategy_name": "文本直驱全景生成路线",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流通过DiT360/混元3D等专用全景生成模型直接输出360°等距矩形全景图,完整覆盖了宽广空间感、环境纵深和整体规模的核心需求,并通过Gaussian Splat技术进一步增强沉浸式体验。唯一不足是对室内外场景的差异化处理策略描述较少。"
+      },
+      {
+        "strategy_id": "STRAT-aa34c436",
+        "strategy_name": "超宽画幅空间营造路线",
+        "is_selected": false,
+        "coverage_score": 0.78,
+        "explanation": "该工作流通过超宽画幅、精确的透视控制和体积大气效果有效展现空间感和纵深层次,特别在Prompt工程和光照控制方面有深入设计。但相比真正的360°全景图,21:9画幅在展示完整环境和整体规模方面存在视角局限性。"
+      }
+    ]
+  },
+  "REQ_086": {
+    "requirement_desc": "生成色彩鲜艳、多色并置的视觉冲击画面,画面中同时出现多种高饱和度的颜色搭配(如复古拼贴风格中的粉色、蓝色、橙色并置,或彩色条纹波浪地形),让整体色彩浓烈饱满、视觉张力强烈",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-cdc95b7e",
+        "strategy_name": "风格化拼贴构图路线",
+        "is_selected": false,
+        "coverage_score": 0.92,
+        "explanation": "该工作流全面覆盖了用户需求的核心要素:通过阶段1选定复古拼贴风格,阶段2生成多色并置的各色彩区块(包含彩色条纹地形等特定图案),阶段3实现多色并置合成,阶段4强化复古质感与视觉张力。完整覆盖了'色彩鲜艳、多色并置、高饱和度、复古拼贴风格、视觉冲击'等所有关键需求点,且提供了具体的风格选择(多巴胺拼贴、孟菲斯、丝网印刷)和质感强化手段(复古颗粒、半色调、叠印效果)。"
+      },
+      {
+        "strategy_id": "STRAT-0e87b6a6",
+        "strategy_name": "AI提示词驱动的多色并置视觉冲击生成路线",
+        "is_selected": true,
+        "coverage_score": 0.85,
+        "explanation": "该工作流有效覆盖了高饱和多色并置和视觉张力的核心需求,通过阶段1确定色彩方案、阶段2构建参数化提示词、阶段3生成多版本、阶段4强化色彩对比。但相比STRAT-cdc95b7e,该流程更侧重迷幻艺术风格而非用户明确要求的'复古拼贴风格',且缺少对'彩色条纹波浪地形'等具体图案类型的针对性生成步骤,对复古质感(丝网印刷、半色调)的处理也不够深入。"
+      }
+    ]
+  },
+  "REQ_087": {
+    "requirement_desc": "生成低饱和度或去色风格的极简画面,整体色彩纯度降低,呈现出克制、安静的视觉质感(如黑白灰调的海洋孤舟场景,或接近无彩色的素雅插画),与高饱和度画面形成鲜明对比",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-737a0111",
+        "strategy_name": "禅意极简水墨路线(主线)",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流全面覆盖了用户需求的核心要素:通过三阶段系统性地实现极简构图、低饱和度色彩控制和留白质感强化,提供了完整的提示词策略(水墨风、莫兰迪色、禅意)和参数控制方法(--s、--sref),能够生成黑白灰调海洋孤舟等典型场景。唯一轻微不足是缺少后期色彩微调的备选方案。"
+      },
+      {
+        "strategy_id": "STRAT-96f6e387",
+        "strategy_name": "莫兰迪素雅插画路线(备选)",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流有效覆盖了低饱和度生成的核心路径:通过风格参数锁定莫兰迪色系基调,结合极简构图生成克制画面,并创新性地加入高饱和度对比验证环节以确保效果达标。相比STRAT-737a0111,该方案在极简场景语义构建和留白强化方面的描述略显简略,但对比验证机制是独特优势。"
+      }
+    ]
+  },
+  "REQ_088": {
+    "requirement_desc": "生成超现实浪漫场景图:将人物置于不可能存在的宏大环境中,如站在地球边缘俯瞰星空、坐在云端长椅上漂浮、在星海上骑行,画面充满梦幻感和史诗级视觉冲击力",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-4e74f78f",
+        "strategy_name": "诗意提示词驱动·单图超现实人物场景流派",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流完整覆盖了超现实浪漫场景生成的核心需求:通过专业提示词工程构建不可能存在的宏大环境(地球边缘/云端/星海),使用Midjourney V7生成史诗级视觉冲击力的底图,并通过光影精修强化梦幻感和尺度对比。唯一轻微不足是未明确涉及动态视频化环节,但静态图像生成部分已完全满足需求。"
+      },
+      {
+        "strategy_id": "STRAT-4914537c",
+        "strategy_name": "文生图底帧 + 图生视频动态化路线(首尾帧/多镜头/Hailuo-Seedance-Kling)",
+        "is_selected": true,
+        "coverage_score": 0.62,
+        "explanation": "该工作流聚焦于非常规摄像机角度(虫眼仰拍/鸟瞰俯视/POV)的技术实现,虽然可以生成宏大环境的视角效果,但核心侧重点在于视角转换技术而非超现实浪漫场景的艺术表现。缺少对梦幻感、史诗级氛围、超现实元素(如站在地球边缘、坐在云端、星海骑行)的专项设计和光影氛围营造,与用户需求的艺术方向匹配度较低。"
+      }
+    ]
+  },
+  "REQ_089": {
+    "requirement_desc": "制作科技感强烈的活动宣传海报:使用深色背景配合橙色、蓝色等高对比度霓虹色调,融合未来感城市或科技场景插图,搭配大号粗体标题文字,整体呈现出硬核、前沿的视觉气质",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-00faa3e6",
+        "strategy_name": "文字主导型科技活动海报流派(GPT-Image 2文字场景深度融合路线)",
+        "is_selected": false,
+        "coverage_score": 0.92,
+        "explanation": "该工作流深度覆盖了用户需求的所有核心要素:深色背景、橙蓝霓虹色调、未来科技场景、大号粗体标题,并特别强调了3D透视文字与场景表面融合的高级效果,以及电影级色彩分级。唯一轻微不足是阶段划分较为技术导向,可能在实际执行中需要更多工具间的协调。"
+      },
+      {
+        "strategy_id": "STRAT-f72fe65d",
+        "strategy_name": "场景主导型科技活动海报全自动化流派(城市场景驱动·霓虹光效强化·版式智能收尾)",
+        "is_selected": true,
+        "coverage_score": 0.88,
+        "explanation": "该工作流完整覆盖了深色背景、霓虹色调、未来城市场景、科技元素、文字排版和高分辨率输出等所有关键需求,并增加了批量生成能力以提供多方案选择。相比第一个方案,文字处理采用传统叠加方式而非深度融合,在视觉冲击力上略逊一筹,但整体流程更加清晰易执行。"
+      }
+    ]
+  },
+  "REQ_090": {
+    "requirement_desc": "生成融合东方传统与现代简约的室内空间效果图:以米白、暖棕为主色调,加入拱形门洞、藤编元素、中式花卉装饰画等传统细节,整体呈现温润雅致的新中式生活美学氛围",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-74adb2a2",
+        "strategy_name": "新中式 LoRA 室内效果图路线",
+        "is_selected": true,
+        "coverage_score": 0.98,
+        "explanation": "该工作流全面覆盖用户需求的所有核心要素:米白暖棕主色调通过阶段1的配色方案智能生成精准实现,拱形门洞通过阶段2的传统建筑造型元素精准生成完整呈现,藤编元素通过阶段3的多材质纹理精准生成清晰可辨,中式花卉装饰画通过阶段4的中式装饰艺术画生成定制化输出,整体温润雅致氛围通过阶段5的灯光氛围与细节收尾完美营造。工作流逻辑清晰、能力匹配精准、案例支撑充分,几乎完全解决用户需求,仅在实际执行的技术细节调优上可能需要微调。"
+      }
+    ]
+  },
+  "REQ_091": {
+    "requirement_desc": "生成拟人化动物角色表情包:用AI生成具有丰富表情和情绪的卡通动物形象(如毛茸茸的红色马、灰色驴),能够呈现出沮丧、无奈、委屈等多种情绪状态,配合不同场景背景(办公室、草地、室内),整体风格介于3D皮克斯动画和水彩插画之间,适合搭配幽默文案使用",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-2c12f52d",
+        "strategy_name": "3D 卡通风情绪矩阵路线(皮克斯/九宫格/表情包)",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流不仅全面覆盖角色生成、毛茸茸质感、多情绪表情、场景适配和风格控制等核心需求,还特别强调了情绪标注驱动机制和场景道具设计,使表情包更适合搭配幽默文案使用,并在阶段4明确包含图像内文字渲染能力实现图文一体。整体语义覆盖更深入且流程更完整。"
+      },
+      {
+        "strategy_id": "STRAT-0902410f",
+        "strategy_name": "锁主体身份 + 情绪 Prompt 矩阵批量路线",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流完整覆盖了拟人化动物角色生成、毛茸茸质感渲染、多情绪表情批量生成、场景背景融合和3D皮克斯/水彩风格切换等核心需求,流程清晰且工具实现具体。略微不足在于缺少文案融合环节,未明确提及如何将幽默文案直接渲染进表情包图像。"
+      }
+    ]
+  },
+  "REQ_092": {
+    "requirement_desc": "制作图文混排的知识科普长图:以深青色/蓝绿色为底色背景,将心理学等知识内容拆分为多个板块,每个板块搭配风格统一的插画小图(奇幻风格人物、动物等),文字与插图穿插排布,整体呈现出版式清晰、视觉层次丰富的杂志风格科普图文效果",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-cde98036",
+        "strategy_name": "LLM 生成代码 → 渲染为图路线(HTML/SVG/Code Interpreter)",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了用户需求的所有核心要素:深青/蓝绿色底色背景、心理学知识板块拆分、奇幻风格插画生成与嵌入、图文穿插排布、杂志风格排版,并通过品牌规则文件和QA代理确保风格统一和质量达标。唯一微小不足是流程较为复杂,对于简单场景可能存在一定的学习成本。"
+      },
+      {
+        "strategy_id": "STRAT-052f6ab9",
+        "strategy_name": "科普长图模板化生成路线",
+        "is_selected": false,
+        "coverage_score": 0.82,
+        "explanation": "该工作流覆盖了用户需求的主要元素:深青/蓝绿色背景、内容板块拆分、奇幻插画生成、视觉层次优化和导出。但在风格一致性保障机制、自动化质量检查、HTML模板化生成等方面不如第一个方案完善,缺少品牌规则文件和QA代理等确保批量输出质量稳定的机制。"
+      }
+    ]
+  },
+  "REQ_093": {
+    "requirement_desc": "生成室内空间效果图:用AI渲染出具有温暖奶油色调的室内场景,包含拱形门洞、藤编家具、自然光影等元素,整体呈现出地中海或法式复古风格的高质感室内设计效果,光线柔和、色调统一,适合作为家居内容的视觉展示",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-728a2e72",
+        "strategy_name": "Depth/白模 ControlNet 结构锁定 + 风格迁移室内渲染路线",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "该工作流全面覆盖了用户需求的核心要素:通过ControlNet Depth锁定拱形门洞等空间结构,IPAdapter Plus实现地中海/法式复古风格和奶油色调迁移,Color Grading统一色调,Lighting Modification模拟柔和自然光影,Material Swap强化藤编家具纹理细节,Atmospheric Effects提升照片级质感。工作流逻辑清晰、技术栈完整,能够高质量实现温暖奶油色调、拱形门洞、藤编家具、自然光影等所有关键元素,适合家居视觉展示。唯一微小不足是未明确提及如何确保最终输出分辨率满足商业展示需求。"
+      }
+    ]
+  },
+  "REQ_094": {
+    "requirement_desc": "生成具有强烈戏剧性光影对比的户外场景图,画面中光源方向明确(如侧光或逆光),亮部与暗部之间形成鲜明反差,阴影轮廓清晰,整体呈现出电影感或艺术摄影风格的视觉张力",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-5cfc91aa",
+        "strategy_name": "提示词精细化驱动路线:逐层叠加光影控制词",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流全面覆盖了用户需求的所有核心要素:通过阶段1-2精准构建户外场景和光影控制词(侧光/逆光、硬光、高对比度、清晰阴影轮廓),阶段3叠加电影感/艺术摄影风格标签,阶段4确保高清输出。提供了完整的提示词工程方法论(万能公式、光影词库、设备参数锚定)和多模型实现路径(Midjourney/Gemini/Nanobanana),案例参考丰富且具体。唯一微小不足是未涉及ControlNet等空间约束工具,对光影方向的可控性略低于方案二。"
+      },
+      {
+        "strategy_id": "STRAT-092a4373",
+        "strategy_name": "ControlNet精准光影约束路线:参考图驱动空间光影锁定",
+        "is_selected": false,
+        "coverage_score": 0.78,
+        "explanation": "该工作流通过ControlNet光影控制模型提供了对光源方向和阴影分布的精确空间约束能力,解决了纯提示词方案中光影随机性的痛点,特别适合需要高度可复现的批量生产场景。但工作流仅包含3个阶段,在场景构建、风格化美学标签叠加、摄影参数锚定等方面的描述不如方案一详尽,且依赖用户预先准备高质量光影参考图,整体覆盖深度和易用性略逊一筹。"
+      }
+    ]
+  },
+  "REQ_095": {
+    "requirement_desc": "生成室内暖光氛围图,画面中多个光源(吊灯、筒灯、窗外自然光)共同营造出温暖柔和的米色调空间,光线从不同方向照射,形成层次丰富的软阴影,整体氛围温馨舒适",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-1c278c59",
+        "strategy_name": "多光源分层打光策略(三阶段闭环)",
+        "is_selected": true,
+        "coverage_score": 0.95,
+        "explanation": "This workflow comprehensively addresses all core requirements: multi-source lighting (pendant lights, spotlights, natural light), warm beige tones, soft shadows from multiple directions, and cozy atmosphere. It provides detailed three-phase approach covering space foundation, layered lighting setup with specific color temperature control (3000K), and shadow refinement with material reflections, directly matching the user's need for rich layered soft shadows and warm comfortable ambiance."
+      },
+      {
+        "strategy_id": "STRAT-ec8393b8",
+        "strategy_name": "AI提示词直出策略(快速迭代)",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "This workflow effectively covers the main requirements through structured prompt assembly, tool selection, and atmosphere enhancement phases. It addresses multi-source lighting, beige tones, soft shadows, and warm atmosphere with specific prompt engineering techniques. However, it is slightly less detailed than STRAT-1c278c59 in terms of explicit color temperature parameterization and the systematic breakdown of lighting layer strategies."
+      }
+    ]
+  },
+  "REQ_096": {
+    "requirement_desc": "生成充满魔幻或超现实感的彩色光效场景,画面中有多种颜色的光线(如橙、蓝、紫等)交织流动,光源本身成为视觉焦点,整体营造出梦幻、神秘或节日感的强烈氛围",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-2e74394c",
+        "strategy_name": "霓虹粒子风暴+生物发光路线(超现实奇幻)",
+        "is_selected": false,
+        "coverage_score": 0.92,
+        "explanation": "该工作流深度契合用户需求,通过暗黑背景对比、霓虹粒子流动系统、生物发光和棱镜折射等手段,完整实现了多色光线交织流动、光源视觉焦点化和强烈梦幻神秘氛围。特别是粒子河流/瀑布和魔法生物发光的设计强化了超现实感,覆盖度极高。"
+      },
+      {
+        "strategy_id": "STRAT-25ecbf37",
+        "strategy_name": "体积光+霓虹双色对比路线(电影级灯光)",
+        "is_selected": true,
+        "coverage_score": 0.88,
+        "explanation": "该工作流全面覆盖了多彩光效交织、光源成为视觉焦点、梦幻神秘氛围等核心需求,通过体积光、霓虹双色对比和发光粒子系统实现了完整的魔幻光效场景。略微不足在于对超现实感和节日感的营造相对隐含,未明确强调这两个特定氛围维度。"
+      }
+    ]
+  },
+  "REQ_098": {
+    "requirement_desc": "在图片上叠加大字幕文字,字体粗大醒目,常带有描边或阴影效果,文字直接覆盖在照片或场景图上,起到强调说明或搞笑点评的作用",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-2bf80b8e",
+        "strategy_name": "纯AI图文一体生成路线",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流通过AI图文一体化生成直接输出带有粗大醒目文字、描边/阴影效果的图像,完全覆盖用户需求的核心场景(表情包、海报、宣传图),并提供质量校验与批量生成能力。唯一不足是缺少对已有照片进行文字叠加的明确处理路径。"
+      },
+      {
+        "strategy_id": "STRAT-72f1244e",
+        "strategy_name": "AI底图生成 + 专项文字渲染路线",
+        "is_selected": false,
+        "coverage_score": 0.88,
+        "explanation": "该工作流提供了完整的底图生成、文字样式规划、程序化渲染和格式适配流程,特别强调描边阴影等视觉强化效果和透视变形等高级特性。相比第一个方案更注重传统图像合成路径,但在AI一体化生成的便捷性上略逊一筹。"
+      }
+    ]
+  },
+  "REQ_099": {
+    "requirement_desc": "制作多宫格拼贴式内容图,每个格子内有大标题文字突出显示核心信息(如价格、品类名),配合产品图或场景图,标题字号远大于说明文字,形成层次分明的视觉结构",
+    "strategies": [
+      {
+        "strategy_id": "STRAT-6aeb80a1",
+        "strategy_name": "QA Agent 闭环信息图/海报自动化路线",
+        "is_selected": true,
+        "coverage_score": 0.92,
+        "explanation": "该工作流全面覆盖了多宫格拼贴式内容图的核心需求,特别是通过AI版面自动排布、多类型视觉元素分区生成、图像内文字渲染等能力实现了大标题文字突出显示、产品图/场景图配合、层次分明的视觉结构。唯一轻微不足是其更侧重信息图和混合媒体拼贴的复杂场景,对于简单的多宫格网格布局可能存在一定的流程冗余。"
+      },
+      {
+        "strategy_id": "STRAT-425a984a",
+        "strategy_name": "结构化 Prompt 一次直出多格/网格路线",
+        "is_selected": true,
+        "coverage_score": 0.68,
+        "explanation": "该工作流专注于AI模型一次性直出完整网格大图,能够实现多宫格的基础布局和角色一致性,但在核心需求'大标题文字突出显示核心信息(如价格、品类名)'和'标题字号远大于说明文字,形成层次分明的视觉结构'方面缺乏明确的文字渲染和排版层次控制能力,更适合分镜叙事和多姿态展示场景。"
+      }
+    ]
+  }
+}

Разница между файлами не показана из-за своего большого размера
+ 239 - 0
examples/process_pipeline/script/coverage_visualizer.html


+ 803 - 0
examples/process_pipeline/script/dedup_db_records.py

@@ -0,0 +1,803 @@
+"""
+dedup_db_records.py
+语义去重:原子能力 / 制作策略 / 案例素材
+
+用法:
+  --dry-run   分析 + 生成 dedup_plan.json,不写库
+  --execute   读取 dedup_plan.json 并应用(不重新调用 LLM)
+  (无参数)    分析 + 直接执行(不保存 plan)
+
+去重逻辑:
+  Pass 1 - Case:       difflib title 相似度 > 80% → 合并
+  Pass 2 - Capability: difflib name 预筛 (>60%) → Qwen3.5-plus 精判
+  Pass 3 - Strategy:   对 capability_id 集合的 Jaccard 相似度 > 70% → 合并
+            (Strategy 去重必须在 Capability 去重之后,因依赖其 ID)
+"""
+
+import os, sys, json, asyncio, argparse, difflib
+from pathlib import Path
+
+repo_root = str(Path(__file__).parent.parent.parent.parent)
+if repo_root not in sys.path:
+    sys.path.insert(0, repo_root)
+
+from dotenv import load_dotenv
+load_dotenv()
+
+from knowhub.knowhub_db.pg_requirement_store import PostgreSQLRequirementStore
+from knowhub.knowhub_db.pg_capability_store import PostgreSQLCapabilityStore
+from knowhub.knowhub_db.pg_strategy_store import PostgreSQLStrategyStore
+from knowhub.knowhub_db.pg_resource_store import PostgreSQLResourceStore
+from knowhub.knowhub_db.pg_tool_store import PostgreSQLToolStore
+from agent.llm.openrouter import openrouter_llm_call
+
+PLAN_FILE = Path(__file__).parent / "dedup_plan.json"
+
+# ─── helpers ─────────────────────────────────────────────────────────────────
+
+def sim(a: str, b: str) -> float:
+    return difflib.SequenceMatcher(None, a.lower(), b.lower()).ratio()
+
+def apply_merges(store, entity_table: str, junction_tables: list, merges: list, dry_run: bool):
+    """junction_tables: [(table_name, foreign_key_col), ...]
+    Supports both flat {master_id, duplicate_ids[]} and enriched {master:{id}, duplicates:[{id}]} formats.
+    """
+    if not merges:
+        return
+    cur = store._get_cursor()
+    try:
+        for merge in merges:
+            # Support both flat and enriched plan formats
+            if "master_id" in merge:
+                master_id = merge["master_id"]
+                dup_ids = merge.get("duplicate_ids", [])
+            else:
+                master_id = merge["master"]["id"]
+                dup_ids = [d["id"] for d in merge.get("duplicates", [])]
+            if not merge.get("enabled", True):
+                print("    [SKIP disabled] master=%s" % master_id)
+                continue
+            for dup_id in dup_ids:
+                print("    Merge %s -> %s" % (dup_id, master_id))
+                if dry_run:
+                    continue
+                for (jtable, fkcol) in junction_tables:
+                    # INSERT master rows (skip existing)
+                    cur.execute("SELECT * FROM " + jtable + " WHERE " + fkcol + " = %s", (dup_id,))
+                    rows = cur.fetchall()
+                    for row in rows:
+                        try:
+                            cols = [desc[0] for desc in cur.description]
+                            new_vals = []
+                            for c, v in zip(cols, row):
+                                new_vals.append(master_id if c == fkcol else v)
+                            placeholders = ",".join(["%s"] * len(cols))
+                            cur.execute(
+                                "INSERT INTO " + jtable + " (" + ",".join(cols) + ") VALUES (" + placeholders + ") ON CONFLICT DO NOTHING",
+                                new_vals
+                            )
+                        except Exception:
+                            store.conn.rollback()
+                    cur.execute("DELETE FROM " + jtable + " WHERE " + fkcol + " = %s", (dup_id,))
+                
+                # Instead of deleting, mark them as deprecated based on user request
+                if entity_table == "resource":
+                    cur.execute("UPDATE " + entity_table + " SET title = COALESCE(title, '') || '-已废弃' WHERE id = %s AND COALESCE(title, '') NOT LIKE '%%-已废弃'", (dup_id,))
+                else:
+                    cur.execute("UPDATE " + entity_table + " SET name = COALESCE(name, '') || '-已废弃' WHERE id = %s AND COALESCE(name, '') NOT LIKE '%%-已废弃'", (dup_id,))
+        if not dry_run:
+            store.conn.commit()
+    finally:
+        cur.close()
+
+# ─── Pass 0: Tools ────────────────────────────────────────────────────────────────
+
+async def plan_tool_dedup(tool_store, model="anthropic/claude-sonnet-4.5") -> list:
+    """
+    Deduplicate tools using LLM to catch aliases (e.g. "PS" vs "Photoshop").
+    """
+    print("\n[Pass 0] Tool dedup (LLM semantic batching)...")
+    tools = tool_store.list_all(limit=10000)
+    print("  Total tools: %d" % len(tools))
+
+    payload = []
+    # Deduplicate exact name matching locally first to reduce LLM load
+    name_map = {}
+    exact_merges = []
+    for t in tools:
+        t_name = t.get("name") or t["id"]
+        t_lower = t_name.strip().lower()
+        
+        full_tool = {
+            "id": t["id"], 
+            "name": t_name,
+            "introduction": t.get("introduction", ""),
+            "tutorial": t.get("tutorial", "")
+        }
+        
+        if t_lower in name_map:
+            exact_merges.append((name_map[t_lower], t))
+        else:
+            name_map[t_lower] = t
+            payload.append(full_tool)
+
+    print("  Unique entities to send to LLM: %d" % len(payload))
+
+    tools_schema = [{
+        "type": "function",
+        "function": {
+            "name": "submit_tool_merges",
+            "description": "Submit semantically identical tool merge groups along with their synthesized complete field states",
+            "parameters": {
+                "type": "object",
+                "properties": {
+                    "merges": {
+                        "type": "array",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "master_id": {"type": "string", "description": "ID of the tool chosen to be the master"},
+                                "duplicate_ids": {"type": "array", "items": {"type": "string"}, "description": "IDs of the duplicate tools"},
+                                "merged_state": {
+                                    "type": "object",
+                                    "description": "The ultimate merged state of this tool, combining the best information from all duplicates.",
+                                    "properties": {
+                                        "name": {"type": "string", "description": "The standardized, optimal industry-consensus name"},
+                                        "introduction": {"type": "string", "description": "Combined and normalized introduction text"},
+                                        "tutorial": {"type": "string", "description": "Combined tutorial text if any"}
+                                    },
+                                    "required": ["name"]
+                                }
+                            },
+                            "required": ["master_id", "duplicate_ids", "merged_state"]
+                        }
+                    }
+                },
+                "required": ["merges"]
+            }
+        }
+    }]
+
+    prompt = (
+        "你是一个 AI 工具注册表维护专家。下面是一个软件/工具实体的列表。\n"
+        "你的任务是找出所有因为别名、缩写或微小拼写差异(例如 'PS' 和 'Photoshop')造成的重复工具条目。\n"
+        "对于每一组极其肯定为重复的项,请指定一个 master_id(保留的主干),列出 duplicate_ids(待废弃的重复项),并合成一个强大的 `merged_state` 对象。\n"
+        "这个 `merged_state` 必须智能地吸收并结合它们各自的描述与教程信息,并敲定最官方权威的工具名称 `name`。\n\n"
+        "工具列表:\n" + json.dumps(payload, ensure_ascii=False, indent=2)
+    )
+
+    print("  Calling %s..." % model)
+    try:
+        resp = await openrouter_llm_call(
+            messages=[{"role": "user", "content": prompt}],
+            model=model,
+            tools=tools_schema,
+            tool_choice="required",
+            max_tokens=4096
+        )
+        tool_calls = resp.get("tool_calls", [])
+    except Exception as e:
+        print("  LLM call failed: %s" % e)
+        tool_calls = []
+
+    merges = []
+    
+    # Add the local exact matches first
+    exact_groups = {}
+    for master, dup in exact_merges:
+        exact_groups.setdefault(master["id"], {"master": master, "dups": []})["dups"].append(dup)
+        
+    for m_id, grp in exact_groups.items():
+        merges.append({
+            "enabled": True,
+            "master": {"id": m_id, "name": grp["master"]["name"]},
+            "duplicates": [{"id": d["id"], "name": d["name"], "match_reason": "exact_name_local"} for d in grp["dups"]],
+            "merged_result": "Keep master, redirect relations"
+        })
+
+    # Add the LLM matches
+    if tool_calls:
+        try:
+            args = json.loads(tool_calls[0]["function"]["arguments"])
+            llm_merges = args.get("merges", [])
+            print("  LLM returned %d merge groups." % len(llm_merges))
+            
+            # map back to names
+            id_name_map = {t["id"]: t["name"] for t in payload}
+            
+            for m in llm_merges:
+                m_id = m["master_id"]
+                d_ids = m["duplicate_ids"]
+                m_state = m.get("merged_state", {})
+                if m_id not in id_name_map: continue
+                
+                dups = []
+                for d_id in d_ids:
+                    if d_id in id_name_map and d_id != m_id:
+                        dups.append({"id": d_id, "name": id_name_map[d_id], "match_reason": "llm_semantic"})
+                
+                if dups:
+                    merges.append({
+                        "enabled": True,
+                        "master": {"id": m_id, "name": id_name_map[m_id], "merged_state": m_state},
+                        "duplicates": dups,
+                        "merged_result": "Keep master, update fields to merged_state, redirect relations"
+                    })
+        except Exception as e:
+            print("  Error parsing LLM response:", e)
+
+    print("  Total Tool clusters to merge: %d" % len(merges))
+    return merges
+
+# ─── Pass 1: Cases ────────────────────────────────────────────────────────────
+
+def plan_case_dedup(res_store) -> list:
+    print("\n[Pass 1] Case dedup (source_url exact + title fuzzy >80%%)...")
+    cases = res_store.list_resources(content_type="case", limit=10000)
+    print("  Total cases: %d" % len(cases))
+
+    def get_url(c):
+        import json as _json
+        meta = c.get("metadata") or {}
+        if isinstance(meta, str):
+            try: meta = _json.loads(meta)
+            except Exception: meta = {}
+        return (meta.get("source_url") or "").strip()
+
+    processed = set()
+    merges = []
+
+    # Step 1: Exact URL match
+    url_groups = {}
+    for c in cases:
+        url = get_url(c)
+        if url:
+            url_groups.setdefault(url, []).append(c)
+
+    for url, group in url_groups.items():
+        if len(group) < 2:
+            continue
+        master = group[0]
+        dups = []
+        for b in group[1:]:
+            if b["id"] in processed or master["id"] in processed:
+                continue
+            dups.append({"id": b["id"], "title": b.get("title", ""),
+                         "match_reason": "exact_url", "similarity": 1.0, "source_url": url})
+            processed.add(b["id"])
+        if dups:
+            processed.add(master["id"])
+            merges.append({
+                "enabled": True,
+                "master": {"id": master["id"], "title": master.get("title", ""), "source_url": url},
+                "duplicates": dups,
+                "merged_result": "Keep master, redirect all relations to master id"
+            })
+
+    # Step 2: Title fuzzy match fallback
+    for i, a in enumerate(cases):
+        if a["id"] in processed:
+            continue
+        dups = []
+        for j in range(i + 1, len(cases)):
+            b = cases[j]
+            if b["id"] in processed:
+                continue
+            score = sim(a.get("title", ""), b.get("title", ""))
+            if score > 0.80:
+                dups.append({"id": b["id"], "title": b.get("title", ""),
+                             "match_reason": "title_fuzzy", "similarity": round(score, 3)})
+                processed.add(b["id"])
+        if dups:
+            processed.add(a["id"])
+            merges.append({
+                "enabled": True,
+                "master": {"id": a["id"], "title": a.get("title", ""), "source_url": get_url(a)},
+                "duplicates": dups,
+                "merged_result": "Keep master, redirect all relations to master id"
+            })
+
+    url_m = sum(1 for m in merges if any(d.get("match_reason") == "exact_url" for d in m["duplicates"]))
+    print("  URL exact: %d groups, Title fuzzy: %d groups" % (url_m, len(merges) - url_m))
+    return merges
+
+# ─── Pass 2: Capabilities ─────────────────────────────────────────────────────
+
+async def plan_cap_dedup(cap_store, model="anthropic/claude-sonnet-4.5") -> list:
+    print("\n[Pass 2] Capability dedup (LLM Global Clustering + LLM Detailed Merging)...")
+    caps = cap_store.list_all(limit=10000)
+    print("  Total capabilities: %d" % len(caps))
+
+    # Phase 1: Global LLM Clustering
+    print("  Running Phase 1: Global capability clustering via LLM...")
+    minimal_caps = [{"id": c["id"], "name": c.get("name", "")} for c in caps]
+    
+    cluster_schema = [{
+        "type": "function",
+        "function": {
+            "name": "generate_candidate_clusters",
+            "description": "Group identical or highly similar capability IDs together.",
+            "parameters": {
+                "type": "object",
+                "properties": {
+                    "clusters": {
+                        "type": "array",
+                        "items": {
+                            "type": "array",
+                            "items": {"type": "string", "description": "capability ID"}
+                        }
+                    }
+                },
+                "required": ["clusters"]
+            }
+        }
+    }]
+    
+    cluster_prompt = (
+        "你是一个顶级的全景归类AI。下面是数据库中现存的所有「原子能力」极简清单。\n"
+        "你的任务是仔细阅读它们的名字,找出所有**在业务意义上完全相同或可以合并**的能力,哪怕它们的字面长短不一、用词差异极大。\n"
+        "将那些应当被认为是完全重复或高度同质化的原子能力ID打包在一起,形成聚类数组。\n"
+        "注意:只有包含 2 个或以上名字的能力圈子才需要返回。你必须尽可能不遗漏任何有合并价值的核心节点。\n\n"
+        "能力清单:\n" + json.dumps(minimal_caps, ensure_ascii=False)
+    )
+    
+    try:
+        resp = await openrouter_llm_call(
+            messages=[{"role": "user", "content": cluster_prompt}], 
+            model=model, 
+            tools=cluster_schema, 
+            tool_choice="required", 
+            max_tokens=8192
+        )
+        t_calls = resp.get("tool_calls")
+        if not t_calls:
+            print("  Phase 1 returned no tool calls/clusters.")
+            return []
+        args = json.loads(t_calls[0]["function"]["arguments"])
+        cluster_ids = args.get("clusters", [])
+    except Exception as e:
+        print("  [Error] Phase 1 Global Clustering failed: %s" % e)
+        return []
+        
+    print("  Phase 1 raw LLM clustering returned %d clusters." % len(cluster_ids))
+    
+    # Map IDs back to detailed capability objects
+    caps_map = {c["id"]: c for c in caps}
+    candidate_clusters = []
+    for id_list in cluster_ids:
+        real_objs = [caps_map[cid] for cid in id_list if cid in caps_map]
+        if len(real_objs) > 1:
+            candidate_clusters.append(real_objs)
+
+    print("  Valid candidate clusters hydrated: %d (total items: %d)" % (
+        len(candidate_clusters), sum(len(c) for c in candidate_clusters)))
+
+    if not candidate_clusters:
+        return []
+
+    print("  Running Phase 2: Detailed merging of candidate clusters...")
+
+    # Build payload for Qwen - candidate clusters equipped with details
+    payload = []
+    for cluster in candidate_clusters:
+        grp = []
+        for c in cluster:
+            effects_str = c.get("effects") or "[]"
+            try:
+                if isinstance(effects_str, str): effects_obj = json.loads(effects_str)
+                else: effects_obj = effects_str
+            except: effects_obj = []
+            
+            implements_str = c.get("implements") or "{}"
+            try:
+                if isinstance(implements_str, str): imps_obj = json.loads(implements_str)
+                else: imps_obj = implements_str
+            except: imps_obj = {}
+            
+            criterion_str = c.get("criterion") or "[]"
+            try:
+                if isinstance(criterion_str, str): crit_obj = json.loads(criterion_str)
+                else: crit_obj = criterion_str
+            except: crit_obj = []
+            
+            grp.append({
+                "id": c["id"],
+                "name": c.get("name", ""),
+                "description": c.get("description", ""),
+                "effects": effects_obj,
+                "implements": imps_obj,
+                "criterion": crit_obj
+            })
+        payload.append(grp)
+
+    tools_schema = [{
+        "type": "function",
+        "function": {
+            "name": "submit_capability_merges",
+            "description": "Submit semantically IDENTICAL capability merge groups, complete with synthesized fields",
+            "parameters": {
+                "type": "object",
+                "properties": {
+                    "merges": {
+                        "type": "array",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "master_id": {"type": "string"},
+                                "duplicate_ids": {"type": "array", "items": {"type": "string"}},
+                                "merged_description": {"type": "string", "description": "A comprehensive, combined description integrating details from all merged capabilities."},
+                                "synthesized_effects": {
+                                    "type": "array", 
+                                    "items": {"type": "object"},
+                                    "description": "A deduplicated, logically combined array of all effects from the master and duplicates."
+                                },
+                                "synthesized_implements": {
+                                    "type": "object",
+                                    "description": "A deduplicated, logically combined dictionary map of all implements/parameters from the master and duplicates."
+                                },
+                                "synthesized_criterion": {
+                                    "type": "array",
+                                    "items": {"type": "object"},
+                                    "description": "A deduplicated, logically combined array of evaluation criteria from the master and duplicates."
+                                }
+                            },
+                            "required": ["master_id", "duplicate_ids", "merged_description", "synthesized_effects", "synthesized_implements", "synthesized_criterion"]
+                        }
+                    }
+                },
+                "required": ["merges"]
+            }
+        }
+    }]
+
+    prompt_template = (
+        "你是一个高级 AI 知识图谱工程师。下面是若干组经过粗筛的候选「原子能力」簇。\n"
+        "请仔细分析每个簇中的能力,判断它们是否属于语义和操作本质上完全相同的原子动作。\n"
+        "如果它们的核心本质和底层操作逻辑根本就是一回事(即使举例或关联对象有微小差异),你也**必须**将它们合并!\n"
+        "对于每决定好的一组合并:选择一个最佳的 master_id,将其他的塞进 duplicate_ids。\n"
+        "核心任务:你必须亲自操刀,合成一份大一统综合描述 `merged_description`;\n"
+        "生成一个去重后的影响力效果数组 `synthesized_effects`;\n"
+        "生成一个整合了所有实现级参数与独立用法的字典 `synthesized_implements`;\n"
+        "最后,生成一个保留全部评判标准的去重评估数组 `synthesized_criterion`。\n\n"
+        "候选原子能力簇:\n{payload_json}"
+    )
+
+    BATCH_SIZE = 5
+    batches = [payload[i:i + BATCH_SIZE] for i in range(0, len(payload), BATCH_SIZE)]
+    print("  Calling %s in %d batches..." % (model, len(batches)))
+
+    async def process_batch(batch):
+        prompt = prompt_template.format(payload_json=json.dumps(batch, ensure_ascii=False, indent=2))
+        try:
+            resp = await openrouter_llm_call(messages=[{"role": "user", "content": prompt}], model=model, tools=tools_schema, tool_choice="required", max_tokens=4096)
+            t_calls = resp.get("tool_calls")
+            if not t_calls: return []
+            args = json.loads(t_calls[0]["function"]["arguments"])
+            return args.get("merges", [])
+        except Exception as e:
+            print("  [Error] LLM batch failed: %s" % e)
+            return []
+
+    results = await asyncio.gather(*(process_batch(b) for b in batches))
+    
+    final_merges = []
+    total_merges = 0
+    for merges in results:
+        total_merges += len(merges)
+        for m in merges:
+            final_merges.append({
+                "enabled": True,
+                "master": {
+                    "id": m["master_id"],
+                    "merged_description": m.get("merged_description", ""),
+                    "synthesized_effects": m.get("synthesized_effects", []),
+                    "synthesized_implements": m.get("synthesized_implements", {}),
+                    "synthesized_criterion": m.get("synthesized_criterion", [])
+                },
+                "duplicates": [{"id": d_id} for d_id in m["duplicate_ids"]],
+                "merged_result": "Keep master, update fields, redirect relations"
+            })
+            
+    print("  LLM suggested %d total merge groups across all batches." % total_merges)
+    return final_merges
+
+# ─── Pass 3: Strategies (Jaccard on capability_ids) ───────────────────────────
+
+async def plan_strategy_dedup(strat_store, model="anthropic/claude-sonnet-4.5", threshold=0.45) -> list:
+    print("\n[Pass 3] Strategy dedup (Jaccard > %.0f%% + LLM workflow refinement)..." % (threshold * 100))
+    strats = strat_store.list_all(limit=10000)
+    print("  Total strategies: %d" % len(strats))
+
+    cap_sets = {}
+    for s in strats:
+        ids = set(s.get("capability_ids") or [])
+        if ids: cap_sets[s["id"]] = ids
+
+    processed = set()
+    candidate_clusters = []
+    strat_list = [s for s in strats if s["id"] in cap_sets]
+
+    # Pre-filter clusters
+    for i, a in enumerate(strat_list):
+        if a["id"] in processed: continue
+        cluster = [a]
+        sa = cap_sets[a["id"]]
+        for j in range(i + 1, len(strat_list)):
+            b = strat_list[j]
+            if b["id"] in processed: continue
+            sb = cap_sets[b["id"]]
+            if not sa and not sb: continue
+            jaccard = len(sa & sb) / len(sa | sb)
+            if jaccard >= threshold:
+                cluster.append(b)
+                processed.add(b["id"])
+        if len(cluster) > 1:
+            processed.add(a["id"])
+            candidate_clusters.append(cluster)
+
+    print("  Candidate strategy clusters (Jaccard > %.0f%%): %d" % (threshold * 100, len(candidate_clusters)))
+    if not candidate_clusters: return []
+
+    payload = []
+    for cluster in candidate_clusters:
+        grp = []
+        for s in cluster:
+            body_str = s.get("body") or "{}"
+            try:
+                if isinstance(body_str, str): b_obj = json.loads(body_str)
+                else: b_obj = body_str
+            except: b_obj = {"_raw": body_str}
+            
+            # Extract just the steps to save tokens (user requested mainly description)
+            workflow = b_obj.get("workflow", [])
+            condensed_workflow = [{"phase": w.get("phase", ""), "description": w.get("description", "")} for w in workflow]
+            
+            grp.append({
+                "id": s["id"],
+                "name": s.get("name", "Unnamed Strategy"),
+                "workflow_summary": condensed_workflow
+            })
+        payload.append(grp)
+
+    strat_schema = [{
+        "type": "function",
+        "function": {
+            "name": "submit_strategy_merges",
+            "description": "Submit semantically identical strategy workflows, consolidating their step instructions.",
+            "parameters": {
+                "type": "object",
+                "properties": {
+                    "merges": {
+                        "type": "array",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "master_id": {"type": "string"},
+                                "duplicate_ids": {"type": "array", "items": {"type": "string"}},
+                                "merged_workflow": {
+                                    "type": "array",
+                                    "items": {"type": "object"},
+                                    "description": "A logically synthesized, unified pipeline array combining the best steps from the merged strategies."
+                                }
+                            },
+                            "required": ["master_id", "duplicate_ids", "merged_workflow"]
+                        }
+                    }
+                },
+                "required": ["merges"]
+            }
+        }
+    }]
+
+    prompt_template = (
+        "你是一个资深人工智能工作流引擎架构师。下面是一批疑似重合的候选「执行工序(Strategy)」簇。\n"
+        "每个簇里包含多个工作流定义,请极其苛刻地审视它们的步骤顺序与逻辑意图。\n"
+        "如果多个工作流指挥的是一段**从根本上完全一样的核心处理链路**(即使修饰词、举例、参数或个别无关紧要的废话步骤有别),你也**应该**将它们合并。\n"
+        "注意:如果同一个簇里的工序代表了截然不同的概念流派分支,你可以分别输出多组合并(或直接决绝合并,不返回内容)。\n"
+        "对于同意合并的组:指定一个 master_id、列出 duplicate_ids,并提炼一个 `merged_workflow`(合并后的工作流数组)。\n"
+        "在这个 `merged_workflow` 里,请存放经过你清洗、压缩、取长补短后所形成的最干货、最权威的完整操作指令步骤。\n\n"
+        "候选工序簇:\n{payload_json}"
+    )
+
+    BATCH_SIZE = 8
+    batches = [payload[i:i + BATCH_SIZE] for i in range(0, len(payload), BATCH_SIZE)]
+    print("  Calling %s in %d batches..." % (model, len(batches)))
+
+    async def process_batch(batch):
+        prompt = prompt_template.format(payload_json=json.dumps(batch, ensure_ascii=False, indent=2))
+        try:
+            resp = await openrouter_llm_call(messages=[{"role": "user", "content": prompt}], model=model, tools=strat_schema, tool_choice="required", max_tokens=4096)
+            t_calls = resp.get("tool_calls")
+            if not t_calls: return []
+            args = json.loads(t_calls[0]["function"]["arguments"])
+            return args.get("merges", [])
+        except Exception as e:
+            print("  [Error] LLM batch failed: %s" % e)
+            return []
+
+    results = await asyncio.gather(*(process_batch(b) for b in batches))
+
+    merges = []
+    total_merges = 0
+    for llm_merges in results:
+        total_merges += len(llm_merges)
+        for m in llm_merges:
+            merges.append({
+                "enabled": True,
+                "master": {
+                    "id": m["master_id"],
+                    "merged_workflow": m.get("merged_workflow", [])
+                },
+                "duplicates": [{"id": d_id} for d_id in m["duplicate_ids"]],
+                "merged_result": "Keep master, update workflow body, reparent links"
+            })
+            
+    print("  LLM suggested %d total merge groups across all batches." % total_merges)
+    return merges
+
+
+# ─── Main ─────────────────────────────────────────────────────────────────────
+
+async def run_analysis(res_store, cap_store, strat_store, req_store, tool_store, caps_only=False):
+    plan = {}
+    plan["tools"] = []
+    plan["cases"] = []
+    plan["capabilities"] = await plan_cap_dedup(cap_store)
+    if not caps_only:
+        plan["strategies"] = await plan_strategy_dedup(strat_store)
+    else:
+        plan["strategies"] = []
+    return plan
+
+def execute_plan(plan, res_store, cap_store, strat_store, req_store, tool_store, dry_run: bool):
+    tag = "[DRY-RUN] " if dry_run else ""
+
+    # 先跳过 tools 和 cases
+    plan["tools"] = []
+    plan["cases"] = []
+
+    print("\n%s=== Applying Tool Merges & Synthesized Fields ===" % tag)
+    tool_merges = plan.get("tools", [])
+    if tool_merges:
+        cur = tool_store._get_cursor()
+        try:
+            for m in tool_merges:
+                if not m.get("enabled", True): continue
+                st = m["master"].get("merged_state")
+                if st:
+                    print("    Update fields for %s -> %s" % (m["master"]["id"], st.get("name")))
+                    if not dry_run:
+                        cur.execute(
+                            "UPDATE tool SET name = COALESCE(%s, name), introduction = COALESCE(%s, introduction), tutorial = COALESCE(%s, tutorial) WHERE id = %s",
+                            (st.get("name"), st.get("introduction"), st.get("tutorial"), m["master"]["id"])
+                        )
+            if not dry_run:
+                tool_store.conn.commit()
+        finally:
+            cur.close()
+
+    apply_merges(tool_store, "tool", [
+        ("capability_tool", "tool_id"),
+        ("tool_knowledge",  "tool_id"),
+        ("tool_provider",   "tool_id"),
+    ], tool_merges, dry_run)
+
+    print("\n%s=== Applying Case Merges ===" % tag)
+    apply_merges(res_store, "resource", [
+        ("requirement_resource", "resource_id"),
+        ("strategy_resource",    "resource_id"),
+        ("capability_resource",  "resource_id"),
+    ], plan.get("cases", []), dry_run)
+
+    print("\n%s=== Applying Capability Merges & Synthesized Fields ===" % tag)
+    cap_merges = plan.get("capabilities", [])
+    if cap_merges:
+        cur = cap_store._get_cursor()
+        try:
+            for m in cap_merges:
+                if not m.get("enabled", True): continue
+                m_desc = m["master"].get("merged_description")
+                s_effs = m["master"].get("synthesized_effects")
+                s_imps = m["master"].get("synthesized_implements")
+                s_crit = m["master"].get("synthesized_criterion")
+                if m_desc or s_effs or s_crit:
+                    print("    Update %s -> new fields" % m["master"]["id"])
+                    if not dry_run:
+                        cur.execute("UPDATE capability SET description = COALESCE(%s, description), effects = COALESCE(%s::jsonb, effects), criterion = COALESCE(%s, criterion) WHERE id = %s",
+                                    (m_desc, json.dumps(s_effs, ensure_ascii=False) if s_effs else None, json.dumps(s_crit, ensure_ascii=False) if s_crit else None, m["master"]["id"]))
+                        
+                        # implements 是关联表 capability_tool 中的 description
+                        if s_imps:
+                            for t_id, desc in s_imps.items():
+                                cur.execute("UPDATE capability_tool SET description = %s WHERE tool_id = %s AND capability_id IN %s",
+                                            (desc, t_id, tuple([m["master"]["id"]] + [d["id"] for d in m.get("duplicates", [])])))
+            if not dry_run:
+                cap_store.conn.commit()
+        finally:
+            cur.close()
+
+    apply_merges(cap_store, "capability", [
+        ("requirement_capability", "capability_id"),
+        ("strategy_capability",    "capability_id"),
+        ("capability_tool",        "capability_id"),
+        ("capability_knowledge",   "capability_id"),
+        ("capability_resource",    "capability_id"),
+    ], plan.get("capabilities", []), dry_run)
+
+    print("\n%s=== Applying Strategy Merges & Workflows ===" % tag)
+    strat_merges = plan.get("strategies", [])
+    if strat_merges:
+        cur = strat_store._get_cursor()
+        try:
+            for m in strat_merges:
+                if not m.get("enabled", True): continue
+                m_wf = m["master"].get("merged_workflow")
+                if m_wf:
+                    print("    Update %s -> new workflow array" % m["master"]["id"])
+                    if not dry_run:
+                        # Strategy body is a jsonb. We update body -> 'workflow'
+                        cur.execute("""
+                            UPDATE strategy
+                            SET body = jsonb_set(body, '{workflow}', %s::jsonb)
+                            WHERE id = %s
+                        """, (json.dumps(m_wf, ensure_ascii=False), m["master"]["id"]))
+            if not dry_run:
+                strat_store.conn.commit()
+        finally:
+            cur.close()
+
+    apply_merges(strat_store, "strategy", [
+        ("requirement_strategy", "strategy_id"),
+        ("strategy_capability",  "strategy_id"),
+        ("strategy_knowledge",   "strategy_id"),
+        ("strategy_resource",    "strategy_id"),
+    ], plan.get("strategies", []), dry_run)
+
+async def main():
+    parser = argparse.ArgumentParser(description="Semantic dedup for KnowHub DB")
+    parser.add_argument("--dry-run",  action="store_true", help="Analyze only, save plan, do not write DB")
+    parser.add_argument("--execute",  action="store_true", help="Execute saved dedup_plan.json without re-analyzing")
+    parser.add_argument("--caps-only", action="store_true", help="Only run capability deduplication and skip strategies")
+    args = parser.parse_args()
+
+    print("Connecting to DB...")
+    res_store   = PostgreSQLResourceStore()
+    cap_store   = PostgreSQLCapabilityStore()
+    strat_store = PostgreSQLStrategyStore()
+    req_store   = PostgreSQLRequirementStore()
+    tool_store  = PostgreSQLToolStore()
+
+    try:
+        if args.execute:
+            if not PLAN_FILE.exists():
+                print("ERROR: dedup_plan.json not found. Run --dry-run first.")
+                return
+            print("Loading plan from %s ..." % PLAN_FILE)
+            plan = json.loads(PLAN_FILE.read_text(encoding="utf-8"))
+            execute_plan(plan, res_store, cap_store, strat_store, req_store, tool_store, dry_run=False)
+            print("\nAll merges applied.")
+
+        else:
+            plan = await run_analysis(res_store, cap_store, strat_store, req_store, tool_store, caps_only=args.caps_only)
+
+            PLAN_FILE.write_text(json.dumps(plan, ensure_ascii=False, indent=2), encoding="utf-8")
+            print("\nPlan saved to: %s" % PLAN_FILE)
+            total_merges = sum(len(plan.get(k, [])) for k in ("tools", "cases", "capabilities", "strategies"))
+            print("Total merge groups planned: %d" % total_merges)
+
+            if args.dry_run:
+                print("\n[DRY-RUN] No DB changes made. Review dedup_plan.json then run --execute.")
+                execute_plan(plan, res_store, cap_store, strat_store, req_store, tool_store, dry_run=True)
+            else:
+                execute_plan(plan, res_store, cap_store, strat_store, req_store, tool_store, dry_run=False)
+                print("\nAll merges applied.")
+    finally:
+        res_store.close()
+        cap_store.close()
+        strat_store.close()
+        req_store.close()
+        tool_store.close()
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 608 - 0
examples/process_pipeline/script/dedup_plan.json

@@ -0,0 +1,608 @@
+{
+  "tools": [],
+  "cases": [],
+  "capabilities": [
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-009",
+        "merged_description": "基于文本描述或参考图像,生成具有时间连贯性的动画帧序列,使内容产生动态运动效果。支持将静态图像转换为动态视频,可实现超现实动态效果(如颜色渐变、光效流动等),适用于魔幻视觉、音乐视频、冥想/ASMR场景等多种内容类型。可在无专业特效团队的情况下独立完成多镜头奇幻短片制作。",
+        "synthesized_effects": [
+          "MidJourney+Kling制作的奇幻音乐视频在YouTube获得4886点赞、399条评论,高互动率证明魔幻光效动态视频的强吸引力",
+          "AI生成的动态光效视频可实现篝火颜色缓慢渐变为彩虹色等超现实效果,适合冥想/ASMR场景",
+          "多镜头奇幻短片可在无专业特效团队的情况下独立完成,大幅降低魔幻视觉内容的制作门槛"
+        ],
+        "synthesized_implements": {
+          "tools/workflow/comfyui": "AnimateDiff 自定义节点 + ControlNet(保持角色一致性)+ KSampler + 视频合成节点工作流",
+          "midjourney_submit_job": "结合Kling等视频生成工具,将MidJourney静态图转换为动态视频"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-523c8623"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-007",
+        "merged_description": "将黑白或低饱和度的图像自动上色,生成色彩自然、符合语义的彩色图像。支持跨图像色彩统一处理,可将多张图像的色调调整为一致风格,显著提升合成场景的视觉真实感和连贯性。",
+        "synthesized_effects": [
+          "5只毛色各异的猫经过色彩统一处理后,视觉上呈现为同一只猫的不同角度,大幅提升合成真实感"
+        ],
+        "synthesized_implements": {
+          "tools/workflow/comfyui": "DeOldify 模型节点或类似上色模型节点 + 图像输入 + VAEDecode 工作流;结合 Hue/Saturation 调整层实现跨图像色彩统一"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-75ba0eac"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-006",
+        "merged_description": "对已生成的图像进行分辨率提升和细节增强,在放大的同时补充高频细节(后处理路径,区别于生成阶段直接高清输出的 CAP-016)。支持将图像放大至4K分辨率,使光效细节(如粒子光点、光线边缘)、纹理细节(如金饰、飘带、发丝)在大尺寸下依然精致锐利,适合大屏展示或商业用途。",
+        "synthesized_effects": [
+          "4K upscale后光效细节(如粒子光点、光线边缘)更加清晰锐利,适合大屏展示或商业用途",
+          "高清放大使金色粒子流光特效的细节纹理(金饰、飘带、发丝)在大尺寸下依然精致"
+        ],
+        "synthesized_implements": {
+          "tools/workflow/comfyui": "Ultimate SD Upscale 自定义节点 + Tile ControlNet 模型 + VAEDecode 工作流;大图使用 Tiled VAE 节点避免显存溢出",
+          "Runway Aleph": "内置4K upscale功能,可将生成的光效视频片段放大到4K分辨率"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-767e7848"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-067edd94",
+        "merged_description": "在图文排版生成中,通过多种技术手段构建清晰的信息层级结构(标题→副标题→正文→注释),引导读者视线按预设路径流动,提升内容的可读性和信息传达效率。技术实现包括:(1)传统排版元素差异化处理:字号大小、字重、留白、分段、图标等视觉元素的差异化应用;(2)AI生成阶段权重控制:通过注意力掩码(Attention Mask)或区域提示词权重控制,在AI生成阶段即对不同信息区域施加差异化的生成权重,使主要信息区域(标题、核心数据)获得更高的视觉突出度,次要信息区域(正文、背景)保持适当的视觉弱化。两种技术路径均可实现版面信息层级的清晰表达,无需后期手工调整。",
+        "synthesized_effects": [
+          "养生类长文排版的阅读完成率提升,用户不因信息密度过高而中途放弃阅读",
+          "清晰的信息层级使健康知识要点一目了然,用户收藏和转发意愿增强",
+          "标准化的层级排版规范可复用于批量内容生产,保证多篇内容的视觉一致性",
+          "版面信息层级清晰,用户视线可自然从标题引导至核心数据再到正文,阅读体验流畅",
+          "主要信息区域视觉突出度显著提升,核心数据和标题在版面中一目了然",
+          "无需后期手工调整各区域对比度,AI 生成阶段即完成信息层级的视觉化表达"
+        ],
+        "synthesized_implements": {
+          "Canva AI设计系统": "通过模板预设字号层级(主标题/副标题/说明文字),AI自动应用到批量生成的内容中,保持视觉层次一致性",
+          "Claude + HTML模板工作流": "在CLAUDE.md品牌规则文件中定义字号层级规范,Claude自动按规则构建HTML排版,QA Agent检查视觉层次是否达标(15+设计检查项)",
+          "提示词工程 + 后期图层分离": "在提示词中详细描述层级需求(如'大标题用72号粗体黑色、小标题用36号中等灰色、正文用18号浅灰色'),生成后通过图层分离工具拆分并调整",
+          "ComfyUI + 自定义排版节点": "通过自定义节点接收结构化文本输入,结合预设的排版规则(字号比例、颜色方案、间距参数),自动生成分层排版的图像",
+          "midjourney_submit_job": "通过提示词指定'bold typography'、'large headline'、'visual hierarchy'等参数,生成具有明确字号层次的图文内容",
+          "Morpholio Trace(Studio Text)": "创建多个 Studio Text Layer 分别承载不同层级的文字内容,通过图层顺序和样式差异实现信息分层",
+          "Canva AI(模板 + 文字层级)": "使用 Canva 预设模板的分区结构,通过调整文字大小、色块背景、图标 bullet 实现信息层级分区,支持 STEP 步骤结构、编号列表、条列式等多种层级表达方式",
+          "AI 海报生成工具(如 Canva AI、Seedream)": "通过提示词描述信息层级结构(如 'large title at top, subtitle below, icon bullet list in middle section'),AI 直接生成符合层级布局的海报",
+          "AI信息图生成工具(如Canva AI、美图设计室AI等)": "输入结构化文本内容和层级标记,AI自动应用版式模板,生成具有层级关系的信息图",
+          "flux_generate": "在提示词中描述排版层级结构(如 clear typographic hierarchy, title in large bold text, body text in smaller regular weight, generous whitespace between sections),引导模型生成具有层级感的图文排版",
+          "tools/workflow/comfyui": "Regional Conditioning 节点(区域条件控制)+ Attention Mask 输入 + CLIP 区域权重调节 + KSampler 工作流,实现版面不同区域的差异化生成权重控制"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-94c648d6"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-6af51b66",
+        "merged_description": "将角色、人物或主体元素与指定的场景背景进行自动合成,生成主体置于目标场景中的完整图像。支持多种背景来源:现成的场景图片、纯色/渐变/材质背景、或AI生成的创意场景(如雪山、梦幻环境、超现实场景)。可处理白底图、透明底图或实拍主体,实现低成本、高创意的场景置景效果,无需手动抠图或复杂合成操作。",
+        "synthesized_effects": [
+          {
+            "description": "白底角色图秒变多场景效果图,一键完成,无需手动合成"
+          },
+          {
+            "description": "同一角色快速适配办公室/草地/室内等多种场景,表情包使用场景大幅扩展"
+          },
+          {
+            "description": "批量场景替换后可快速生成完整表情包系列,上架效率提升数倍"
+          },
+          {
+            "description": "低成本实现梦幻场景,无需外景拍摄"
+          },
+          {
+            "description": "创意天马行空,不受现实场景限制"
+          },
+          {
+            "description": "Great tutorial评价,光照分析部分particularly valuable"
+          },
+          {
+            "description": "统一色调背景使拼贴整体视觉更和谐,无需反复调整"
+          },
+          {
+            "description": "材质背景增加手工质感,用户反馈作品更有温度"
+          },
+          {
+            "description": "局限性:脚周边素材需保持原素材,光线无法映射影子,人物不能乱动"
+          }
+        ],
+        "synthesized_implements": {
+          "flux_generate": "使用场景描述prompt配合角色参考图,生成角色+场景合成图",
+          "bili_015工具流": "AI一键将白底图转换为办公室/草地/室内等场景效果图",
+          "即梦AI / Dreamina": "上传角色图,描述目标场景,AI自动生成角色置于场景中的合成图",
+          "Midjourney + Topaz Photo AI": "在MidJourney生成场景,用Topaz Photo AI放大,分析光照后在工作室重现光效,最后在Photoshop合成",
+          "AI视频合成工具": "自由更换视频背景,但存在光线映射、人物动作等局限性",
+          "Midjourney + Photoshop": "先用Midjourney生成场景图,拍摄时模拟AI场景的光效,在Photoshop里抠图合成",
+          "醒图": "选择色调一致的图片做背景图,不抠图直接导入",
+          "ProCCD 跨屏拼图": "点击跨屏拼图新建画布,选择纯色/渐变色/材质背景(推荐星星卡纸)",
+          "黄油相机": "背景搜索'星星卡纸',根据照片色调选择合适背景"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-411da039"
+        },
+        {
+          "id": "CAP-66cd9fbf"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-ed4b506e",
+        "merged_description": "通过在AI生成提示词中使用专业摄影布光术语和精确的光照描述,驱动AI精准复现各种经典摄影布光模式和特定光效,包括但不限于:伦勃朗光(Rembrandt Lighting)、蝴蝶光(Butterfly Lighting)、侧逆光/轮廓光(Rim Lighting)、二分光(Split Lighting)、逆光发丝光(Backlit Hair Light)等。使生成图像呈现出专业摄影棚级别的光影结构、人物立体感和情绪氛围,特别擅长生成逆光场景中主体轮廓被光线勾勒的视觉效果,使人物或物体的边缘呈现出发光的金色/白色轮廓,营造神圣感或电影感氛围。",
+        "synthesized_effects": [
+          {
+            "description": "伦勃朗光提示词使AI生成的人像立即呈现出专业摄影棚级别的面部立体感,无需实体灯光设备"
+          },
+          {
+            "description": "侧逆光关键词可精准勾勒发丝轮廓,生成具有强烈情绪感的人像,用户反馈'极具情绪感'"
+          },
+          {
+            "description": "10个神仙光影词的系统化应用使AI摄影图像从普通生成跃升为'单反级质感',获97点赞"
+          },
+          {
+            "description": "逆光发丝光效果案例获得 6319 赞(小红书),用户学习逆光拍摄技巧,证明该效果具有极高的视觉吸引力和传播价值"
+          },
+          {
+            "description": "B站教程反馈发丝光效果让人物剥离背景,皮肤通透,是最好用的人像布光方式,说明该能力可显著提升人像的专业感和层次感"
+          },
+          {
+            "description": "AI 生成逆光效果可替代需要特定时段(日落)和专业设备(反光板)的实拍条件,大幅降低创作门槛"
+          }
+        ],
+        "synthesized_implements": {
+          "Flux/Nano Banana": "使用 'high-contrast cinematic lighting from the side'、'warm spotlight halo'、'strong natural sunlight through window blinds casting striped shadows'、'sunlight from behind'、'translucent hair'、'glowing edges' 等具体描述实现精准布光效果和逆光轮廓效果",
+          "tools/image_gen/midjourney": "使用 'Rembrandt lighting'、'butterfly lighting'、'rim lighting'、'split lighting'、'underlighting'、'cameo lighting'、'three-point lighting'、'backlit' 等专业布光术语直接触发对应光影模式",
+          "tools/workflow/comfyui": "CheckpointLoader + CLIPTextEncode(含 rim light、backlit、Rembrandt lighting 等关键词)+ KSampler + VAEDecode;可配合 ControlNet Depth 图约束主体位置,确保光源在主体后方或特定角度"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-47151d87"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-d92ffc99",
+        "merged_description": "以参考服装图像为输入,利用扩散模型(如 IDM-VTON、OOTDiffusion、CatVTON、Klein 等)将指定服装自动迁移并穿戴到目标主体(人物模特或宠物)上,保持服装的颜色、纹理、款式特征不变,同时与主体体型、姿态自然融合,实现无需实物拍摄的虚拟换装效果。适用于电商服装展示、宠物表情包制作、时尚营销素材生成等场景。",
+        "synthesized_effects": [
+          "无需实物拍摄即可将任意服装穿戴到模特身上,大幅降低服装展示成本",
+          "可将超大号短裤、篮球短裤等日常物品以夸张方式穿戴到人物上,快速生成视觉反差效果图",
+          "批量生成不同服装组合的模特图,无需多次拍摄",
+          "3分钟内完成从普通主体照片到职业装换装的全流程,无需任何手工操作",
+          "同一主体可批量换遍医生服、西装、厨师服等所有职业服装,效率极高",
+          "换装效果自然贴合,服装与主体体型协调,可直接用于表情包制作或商业展示无需二次修图",
+          "CatVTON 等模型显卡要求低,普通消费级显卡即可运行,降低创作门槛"
+        ],
+        "synthesized_implements": {
+          "nano_banana (Gemini AI)": "使用 'Keep the same character sheet layout. Change the outfit to [outfit description]' 提示词模板更换服装",
+          "GPT-4o": "使用'保持角色风格不变,替换服装为XXX'的指令进行换装",
+          "Stable Diffusion + rolescene-blend LoRA": "使用 rolescene-blend LoRA 模型进行换装换脸万物迁移,支持任何角度姿势,效果超自然",
+          "Stable Diffusion Inpainting": "使用局部重绘(Inpaint)节点,蒙版遮住服装区域后重新生成指定服装",
+          "midjourney_submit_job": "使用 --oref 服装参考图 + 'Clothing Match & Recreating Your OREF' 技巧实现服装匹配",
+          "FLORA AI": "系统化工作流,输入服装单品图(上衣、下装)+ 模特参考 + 环境参考,自动合成完整时尚营销素材",
+          "FashionModelAI": "基于Replit Agent 4构建的专用工具,上传flat lay服装图片后自动生成模特上身效果",
+          "tools/workflow/comfyui": "IDM-VTON 或 OOTDiffusion 或 CatVTON 等虚拟试衣节点 + 服装分割蒙版 + KSampler 工作流,实现精准服装区域替换,支持人物和宠物主体",
+          "OpenAI Image Model": "上传多张服装参考图,使用简单提示词如'street style shot of a woman in this outfit'自动生成模特穿着照片",
+          "即梦AI / Nano Banana 2": "在角色描述中锁定外观特征,仅修改服装相关描述词",
+          "即梦图片 4.5 模型": "上传人物参考图和服装参考图,使用详细提示词描述服装细节,保持服装细节不变",
+          "Klein 模型 + ComfyUI 工作流": "上传人物图片+服装图片,使用 Klein 模型进行虚拟试衣,一键生成换装效果",
+          "Higgsfield AI Canvas": "使用 Canvas + Flux Kontext 在现有角色图上替换服装元素",
+          "LTX-2.3 换装术(RunningHub)": "在 RunningHub 平台调用 LTX-2.3 换装术工作流 → 上传主体照片 → 输入英文提示词描述目标服装 → 3分钟内生成换装结果",
+          "ComfyUI + CatVTON": "安装 CatVTON 插件(中山大学开源项目)→ 输入主体原图 + 职业服装参考图 → 扩散模型推理换装 → 输出换装结果;支持 T恤、套装、裙子等多种服装类型,显卡要求低,适用于宠物换装"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-bcdfba3c"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-c5beae45",
+        "merged_description": "在AI图像生成阶段,通过提示词中的极简主义构图语义描述(如留白、负空间、极简、单一元素、稀疏元素、禅意美学等)引导模型生成大面积空白、元素极少的极简构图画面。该能力适用于多种场景,包括东方禅意艺术创作、现代极简美学表达、产品展示图生成等,使画面呈现'以少胜多'的高级感,避免信息密集的复杂构图。核心特征是干净的单色或简洁背景、居中对称或经典三分构图、最少的视觉干扰元素、突出主体的空间留白。",
+        "synthesized_effects": [
+          {
+            "description": "通过极简构图提示词,AI可直接生成大面积留白的禅意画面,用户评价'黑白之间藏禅意,静谧无声胜万语'",
+            "source": "CAP-c5beae45"
+          },
+          {
+            "description": "极简水墨风格AI生成案例获得346-495赞,证明该构图风格具有稳定的用户审美共鸣",
+            "source": "CAP-c5beae45"
+          },
+          {
+            "description": "极简构图生成可快速产出适合社交媒体的高级感配图,降低内容创作门槛",
+            "source": "CAP-c5beae45"
+          },
+          {
+            "description": "产品主体突出,视觉焦点清晰,符合现代电商和品牌展示的审美趋势",
+            "source": "CAP-e92b8d89"
+          },
+          {
+            "description": "适合产品详情页、广告投放、品牌视觉等商业场景",
+            "source": "CAP-e92b8d89"
+          }
+        ],
+        "synthesized_implements": {
+          "tools/workflow/comfyui": "CLIPTextEncode注入极简构图提示词('minimalist composition', 'large negative space', 'single subject', 'zen aesthetic', 'sparse elements', 'vast emptiness'等)+ 低CFG值(5-7)减少细节密度 + KSampler工作流生成极简画面",
+          "midjourney_submit_job": "提示词中加入极简构图语义关键词:'minimalist'、'negative space'、'zen composition'、'sparse elements'、'vast emptiness'、'clean modern composition'等;配合--ar参数控制画幅比例(如3:4竖构图强化留白感),实现禅意或产品展示的极简效果",
+          "flux_generate": "在提示词中使用极简构图描述:'seamless pastel backdrop', 'clean modern composition', 'minimalist aesthetic'等,配合产品或主体描述生成极简风格图像"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-e92b8d89"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-cd789496",
+        "merged_description": "在单张画布上将画面划分为多个独立的潜空间区域,每个区域绑定独立的提示词和/或LoRA模型,从而在同一画面内实现不同角色、不同场景、不同风格的并存和精确控制。该能力解决了多人物/多角色生成时特征相互污染、外观混乱的核心问题,使各区域之间有清晰的分割感和独立性。通过可视化分区界面或编程式区域定义,用户可直观配置每个区域的内容,生成成功率从随机抽卡提升至稳定可控,适合商业AI影视制作、活动宣传图、批量内容生产等场景。",
+        "synthesized_effects": [
+          {
+            "description": "实现双角色或多角色同台画面,各角色外观独立控制,无相互干扰",
+            "source": "CAP-cd789496"
+          },
+          {
+            "description": "多人场景中各角色服装、发型、外观特征不再相互污染,生成成功率从随机抽卡提升至稳定可控",
+            "source": "CAP-59491141"
+          },
+          {
+            "description": "不挑模型版本,通用性强,适合商业AI影视制作和批量内容生产",
+            "source": "CAP-cd789496"
+          },
+          {
+            "description": "相比传统多图合成,分区控制在单次生成中完成,减少后期合成工序",
+            "source": "CAP-cd789496"
+          },
+          {
+            "description": "可视化分区界面使非技术用户也能直观配置多角色布局,操作门槛大幅降低",
+            "source": "CAP-59491141"
+          },
+          {
+            "description": "配合ControlNet使用后,多人画面中角色位置固定、特征清晰,可直接用于活动宣传图制作",
+            "source": "CAP-59491141"
+          }
+        ],
+        "synthesized_implements": {
+          "tools/workflow/comfyui": "ComfyUI分区LoRA控制工作流:使用提示词遮罩(Prompt Mask)节点或Latent Couple节点为每个区域绑定独立提示词,配合LoRA Loader为不同区域加载不同角色LoRA,通过easy use的lora工作流模板或自定义节点组合实现多角色同台分区绘制(case_bili_011/012/013)",
+          "Regional Prompter(SD WebUI/Forge插件)": "SD WebUI/Forge平台通过Regional Prompter插件为画面不同区域指定独立提示词,实现多角色分区绘制,每个区域可独立配置正向/负向提示词(case_bili_012)",
+          "Latent Couple(ComfyUI/SD插件)": "将潜空间划分为多个矩形区域,每个区域独立绑定正向提示词,实现分区独立生成后融合输出,避免角色特征混淆",
+          "Composable LoRA(SD插件)": "配合Latent Couple使用,使每个分区可独立加载对应角色的LoRA模型,实现多角色特征隔离和精确控制",
+          "Davemane42 Custom Node(ComfyUI可视化分区插件)": "提供可视化的分区界面,用户可直接拖拽划定各角色的画面区域,并为每个区域配置独立的提示词和LoRA参数,降低技术门槛"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-59491141"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-cee93f12",
+        "merged_description": "根据内容结构、叙事逻辑和设计规范,自动将AI生成的文字内容与配套插画/图像按照预设的版式规则(分栏、色块区分、标题层级、图文穿插位置、对话框位置、说明区布局等)排布为完整的多页版式或长图作品,输出可直接发布或商业化的图文混排成品。该能力适用于多种场景:品牌信息图、产品宣传杂志、儿童故事书、情感叙事图文、教育内容等。通过模板化和自动化流程,使复杂图文排版门槛大幅降低,设计小白也能快速产出专业级效果,制作时间从数小时压缩至分钟级。支持文字位置自适应画面构图,并可格式化为KDP/Etsy等平台的出版格式。",
+        "synthesized_effects": [
+          {
+            "description": "case_x_001实现9步全自动生成品牌信息图,无需设计技能即可产出专业级版式",
+            "source": "CAP-cee93f12"
+          },
+          {
+            "description": "case_x_002高收藏量(1361),用户认为该模板非常适合创建产品宣传杂志",
+            "source": "CAP-cee93f12"
+          },
+          {
+            "description": "秀米固定布局使复杂图文排版门槛大幅降低,设计小白也能快速出效果",
+            "source": "CAP-cee93f12"
+          },
+          {
+            "description": "自动化版式生成将单张长图制作时间从数小时压缩至分钟级",
+            "source": "CAP-cee93f12"
+          },
+          {
+            "description": "完整情感故事通过多张图片+对话文字呈现,阅读流畅,故事性强(case_xhs_005,点赞3471)",
+            "source": "CAP-fbb3586e"
+          },
+          {
+            "description": "适合教师、家长、创意故事讲述者,无需昂贵软件即可制作有趣的卡通风格动画故事(case_youtube_006,点赞7452)",
+            "source": "CAP-fbb3586e"
+          },
+          {
+            "description": "儿童故事书可直接格式化为KDP/Etsy上架销售格式,实现商业变现(case_youtube_007,点赞3253)",
+            "source": "CAP-fbb3586e"
+          }
+        ],
+        "synthesized_implements": {
+          "Canva模板 + AI内容填充": "选择适配的Canva模板(信息图模板、故事书页面模板等),通过AI自动替换文字和图片内容,批量生成多板块版式或叙事性图文页面。支持上方插画+下方文字说明区的经典故事书布局,支持动画场景添加,导出数字友好格式(参考case_youtube_003/006/007)",
+          "Claude + HTML/CSS代码生成": "通过LLM(如Claude)根据品牌规则和内容结构自动生成HTML版式代码,配合Puppeteer截图导出PNG,实现编程式的自动化图文排版(参考case_x_001的9步工作流)",
+          "秀米/飞书固定布局模块化排版": "使用秀米固定布局功能或飞书文档分栏,通过预设模块化组件实现复杂图文混排,支持多知识板块的色块区分和层级化内容组织(参考case_bili_003、case_bili_002)",
+          "ChatGPT + Canva组合流程": "ChatGPT生成故事文案/信息图内容 → Canva设计精美页面 → 为KDP/Etsy格式化故事书或导出社交媒体长图(case_youtube_007)",
+          "ComfyUI批量排版节点": "通过批量图像生成节点+文字渲染节点组合,自动将插画序列与对应文字合成为统一格式的多页图文作品"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-fbb3586e"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-1c71b52e",
+        "merged_description": "在多帧图像、视频或多格分镜画面生成过程中,通过AI模型和工作流技术保持角色外观、场景风格、光照色调、视觉元素在不同生成单元间的高度一致性和连贯性。适用于绘本故事创作、视频分镜制作、故事板设计等需要跨帧视觉统一的场景,确保从单个角色到整体画面风格都保持连贯,避免风格割裂。",
+        "synthesized_effects": [
+          {
+            "description": "轻松解决人物风格不一致问题,从故事创作到图生视频全程保持角色一致性"
+          },
+          {
+            "description": "AI短片制作效率提升10倍,多格分镜画面视觉统一,无需逐帧手动调整"
+          },
+          {
+            "description": "操作简单高效,适合绘本、故事类创作,提供分镜画面提示+提示词+脚本分镜模板+实战疑问解答"
+          },
+          {
+            "description": "9宫格/25宫格分镜画面中各格子场景、角色、光照、色调连贯统一,适合AI视频一致性控制和故事板制作"
+          },
+          {
+            "description": "生成的多格子分镜可直接用于AI视频生成的参考帧,大幅降低视频制作门槛"
+          }
+        ],
+        "synthesized_implements": {
+          "即梦AI故事创作功能": "输入故事脚本和角色设定,使用故事创作功能生成图生视频,全程保持角色一致性",
+          "即梦AI一致性人物场景图片": "配合分镜画面提示词、脚本分镜模板,支持单人和多人场景生成",
+          "midjourney_submit_job": "使用 --cref(character reference)和 --sref(style reference)参数统一多格的风格参考,配合 --stylize 参数控制风格强度;在 prompt 中明确 consistent styling across all frames",
+          "Canva品牌配色系统": "使用Canva的品牌套件功能预设配色方案,一键将所有格子的颜色切换为指定主题色,支持粉色系/黑白系/蓝色系/黄色系等多种预设",
+          "Notebook LM + 导演级Prompt": "将场景资产描述(背景/光照/色调/道具清单)喂入Notebook LM生成结构化导演级Prompt,在每格生成时统一引用该Prompt约束场景一致性",
+          "ComfyUI FLUX-Klein": "分镜流中通过统一的场景ControlNet参考图约束各格背景结构,配合局部修复节点修正场景崩坏区域",
+          "tools/workflow/comfyui": "FLUX-Klein 分镜工作流(ComfyUI)+ IP-Adapter 角色一致性节点 + ControlNet 姿态控制 + KSampler,生成具有超强一致性的九宫格分镜",
+          "PPT/Figma主题色批量替换": "在PPT或Figma中设置全局主题色,修改主题色后所有格子的背景、文字、装饰元素自动同步更新",
+          "AI提示词配色控制(Midjourney/Flux)": "在提示词中指定配色主题(如'pastel pink palette'/'monochrome black and white'),AI生成的多格子画面自动遵循指定配色方案",
+          "ChatGPT代码解释器批量生成": "通过代码解释器一次性生成多格子画面,在提示词中统一指定风格参数(色调/光照/画风),确保所有格子遵循相同的视觉规则",
+          "Gemini + 即梦Seedance 2.0工作流": "使用Gemini构建分镜逻辑和视觉统一规则,通过9宫格/25宫格实现多画面视觉统一,配合即梦Seedance 2.0的单帧镜头混合实现电影感成片"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-e9b763d2"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-4e89bb57",
+        "merged_description": "通过预先定义品牌规则文件或全局配色方案(包含配色、字体、Logo、布局约束、风格指南等视觉规范),在批量生成信息图、数据报告、图表等多个视觉组件时,AI自动遵循统一的视觉规范,确保所有输出元素(图表、标题、文字、背景等)在配色、风格、布局上高度一致,符合品牌视觉体系或指定设计主题,无需每次在提示词中重复描述规范要求。",
+        "synthesized_effects": [
+          {
+            "description": "生成与品牌匹配的美丽信息图和专业数据报告,避免杂乱无章和通用剪贴画,输出专业级品牌化视觉内容"
+          },
+          {
+            "description": "不同主题的信息图、图表在同一品牌规则或配色方案下视觉风格统一,无需每次重复描述品牌要求或手动调整单个元素颜色"
+          },
+          {
+            "description": "麦肯锡/奢侈品级别的报告质感,逻辑清晰、图表专业、设计高级,用户反馈'专业度直接拉满'"
+          },
+          {
+            "description": "5分钟内生成专业信息图或完整数据报告,无需设计经验,效率大幅提升"
+          },
+          {
+            "description": "统一配色后整份年度数据报告或系列信息图视觉风格高度一致,科技风/商务风等主题配色获得'炫酷''专业感满满'的用户评价"
+          }
+        ],
+        "synthesized_implements": {
+          "Claude (claude.ai)": "创建 CLAUDE.md 品牌规则文件 + references 参考图文件夹 + skill 文件定义布局约束,Claude 从模板构建 HTML 信息图并自动遵循品牌规范",
+          "Venngage AI": "Brand Kit 功能:上传 logo、设定颜色和字体,AI 在生成信息图时自动应用品牌套件",
+          "DeepSeek / ChatGPT": "通过 Prompt 指定色彩主题和报告风格,AI 生成包含统一配色方案的完整报告版式代码(HTML/CSS)或设计规范,所有图表自动应用同一色系",
+          "FineBI / 镝数图表": "在平台内设置全局配色主题,所有图表自动继承同一色系(如生长色系渐变、同色系配色),保证指标颜色一致性",
+          "Nano Banana + Gemini": "上传参考版式图片,AI 提取配色方案并应用到新生成的图表和仪表板中,实现风格迁移与统一"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-45a51b6d"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-e80e4194",
+        "merged_description": "以视觉叙事或展示逻辑为驱动,通过在提示词中系统性地指定不同景别/构图尺度描述词(远景/全景/中景/近景/特写,或 full body shot / waist-up shot / close-up / detail shot),批量生成同一主体或场景从宏观到微观的多尺度图像序列。该能力形成具有完整视觉叙事张力或全方位展示效果的图片集合,可应用于短片分镜脚本、漫画叙事、电商详情页多角度展示、商业影视创作等场景。核心是通过景别参数的系统化控制,实现从大范围环境到局部细节的连续视觉过渡。",
+        "synthesized_effects": [
+          "以'万能公式'景别叙事结构一键生成5个景别系列图,用户可直接用于短视频开场、产品展示、旅行打卡内容创作",
+          "连续生成几十个分镜并全部保持一致性,彻底替代传统需反复抽卡/手动修改的多轮操作",
+          "一套冬季穿搭生成15张涵盖全身到局部细节的多角度图,满足电商详情页从整体造型到单品细节的完整展示需求",
+          "多景别图像序列无需实拍,0成本完成从全身造型到围巾、手套等配饰特写的完整展示,获赞2051",
+          "xhs/case_003景别教程获2328赞,印证了景别叙事序列化方法论的强烈用户需求"
+        ],
+        "synthesized_implements": {
+          "kling_ai": "可灵3.0自定义分镜功能,配合场景设定多角度切换工作流,结合5000+分镜画面提示词库和300个AI视频脚本模板,实现从远景到特写的叙事序列生成",
+          "comfyui_runninghub": "在 RunningHub 平台运行专用工作流,同一场景一次性生成16个不同镜头,支持普通版和PLUS版,无废图",
+          "tools/image_gen/midjourney": "在提示词中分别指定景别描述词(full body shot / waist-up shot / close-up of scarf detail / detail shot of gloves / extreme wide shot / establishing shot / medium shot / close-up)生成对应构图,配合--ar参数控制画幅比例",
+          "Stable Diffusion + ControlNet": "使用OpenPose骨架控制保持人物姿态一致性,通过Depth预处理器控制景深,分别生成全身和局部特写图;结合CLIPTextEncode节点为每个景别编写专用提示词"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-1da1bf7a"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-0e3d61ca",
+        "merged_description": "在 AI 图像生成或转换过程中施加鱼眼镜头(Fisheye Lens)或超广角镜头的球形畸变效果,产生桶形畸变、透视压缩和极端透视夸张感,使画面边缘产生弯曲变形,强化视觉冲击力和空间纵深感。该能力支持两种输入模式:(1)文本生成图像(text2img):通过提示词直接生成具有鱼眼效果的新图像;(2)图像转换(img2img):将用户上传的普通照片转换为鱼眼视角版本,在保留原始特征的同时施加光学畸变效果。常与虫眼视角、GoPro/Action Cam 风格、大头娃娃效果结合使用,适用于商业摄影、运动视频、社交媒体内容创作等场景。",
+        "synthesized_effects": [
+          "鱼眼视角商业摄影提示词模板(杯子/盒子内部仰拍)获得高收藏量,证明该效果在商业内容创作中具有实用价值",
+          "fisheye + worm's eye 组合可生成极具视觉冲击力的超现实商业摄影效果,无需实体鱼眼镜头设备",
+          "GoPro/Action Cam 风格提示词可快速生成运动感强烈的第一人称视角内容,适合短视频平台",
+          "用户上传自拍照即可一键转换为鱼眼大头娃娃效果,无需专业摄影设备或技术",
+          "X 平台相关教程帖获得 186 赞、131 收藏、5915 浏览,是该需求下互动量最高的案例,证明该能力具有强烈的用户需求",
+          "转换后图像头部显著放大、身体压缩,产生独特的时尚编辑风格视觉效果,适合社交媒体内容创作"
+        ],
+        "synthesized_implements": {
+          "tools/image_gen/stable_diffusion_comfyui": "【文本生成模式】在正向提示词中加入 'fisheye lens, barrel distortion, ultra-wide 180 degree, vignette' 等关键词;可配合专用鱼眼透视 LoRA(如 Civitai 模型 494107,训练自画师 gaku)增强效果;使用 CheckpointLoader + CLIPTextEncode + KSampler 标准工作流。【图像转换模式】使用 img2img 工作流(VAE Encode + KSampler + VAEDecode)+ 提示词注入 'transform into ultra-wide 10mm fisheye portrait, strong perspective distortion';配合 IP-Adapter 节点保留人物外观特征",
+          "tools/image_gen/midjourney": "【文本生成模式】在提示词末尾加入 'fisheye lens effect, extreme wide angle distortion, circular fisheye frame' 等描述词;配合 --ar 1:1 宽高比强化圆形鱼眼视野感。【图像转换模式】使用 --cref(角色参考)配合鱼眼效果提示词,或使用 Omni Reference 功能同时保留人物特征并施加鱼眼变形",
+          "Flux / ComfyUI / Grok Imagine": "【文本生成模式】提示词关键词:极端广角镜头 / 鱼眼透视 / fisheye distortion / 低角度仰拍 / 从产品底部向上拍摄 / F1.2大光圈浅景深 / 14mm超广角",
+          "Flux / nano_banana": "【文本生成模式】在提示词中描述 '10mm ultra-wide fisheye portrait, strong perspective distortion, visible fisheye curvature, subtle dark vignette around edges' 实现超广角鱼眼人像效果。【图像转换模式】在提示词中使用 'Transform the uploaded photo into a 10mm ultra-wide fisheye portrait with dramatic top-down angle' 等指令式描述,配合图像输入实现照片转鱼眼效果",
+          "ComfyUI(后处理节点)": "在生成后通过图像变换节点应用桶形畸变滤镜,或在提示词中结合 ControlNet 深度图约束实现透视夸张效果",
+          "Gemini AI": "在视角重构提示词中指定 'Fisheye View' 作为目标摄像机角度,模型自动应用桶形畸变效果",
+          "Midjourney / Flux / Seedream": "在提示词中加入 'fisheye lens, barrel distortion, ultra-wide 180 degree, vignette' 等关键词,可叠加 'GoPro footage, action camera, body cam, wide angle' 模拟运动相机风格",
+          "垫图反推": "上传具有透视感的参考图,通过图生文功能反推透视视角描述词(虫瞰视角、鱼眼畸变、微距大特写)再用于生成"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-db22d1ad"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-3e353fc5",
+        "merged_description": "在图片或拼贴画布的指定区域上叠加信息标注元素,包括纯文字内容(说明性文字、情感点评、地点标注、碎碎念)或图标与文字组合而成的标签组件(定位图标+地点名称、勾选符号+任务描述、时钟图标+时间标注、价格标签等)。支持字体样式、背景色、透明度、图标选择等多维度调整,实现信息模块化标注和视觉统一化管理。",
+        "synthesized_effects": [
+          {
+            "description": "文字与图片共同叙事,读者无需额外说明即可理解图片背后的故事"
+          },
+          {
+            "description": "情感点评文字使帖子互动率提升,用户反馈更有共鸣感"
+          },
+          {
+            "description": "地点标注文字使旅行拼贴内容信息量更丰富"
+          },
+          {
+            "description": "每个地点/物品区域自动标注名称和图标,信息一目了然,无需反复手动添加"
+          },
+          {
+            "description": "勾选框+任务描述的标签形式使活动规则清晰可读,用户参与意愿提升"
+          },
+          {
+            "description": "批量为多个区域叠加不同标签内容,整体视觉统一且信息密度高"
+          }
+        ],
+        "synthesized_implements": {
+          "醒图/ProCCD/黄油相机": "添加文字后点击样式-背景调整颜色透明度,支持多种字体",
+          "Photoshop": "文字图层叠加,支持精确位置控制",
+          "CapCut": "文字高亮效果,支持trending stylish text设计",
+          "Canva": "文字工具支持多种字体和样式,适合标题和说明文字;在 Elements 面板搜索并添加定位图标、勾选框等图标,配合 Text Box 组合为标签组件,通过 Bulk Create 数据连接批量为每个区域填充不同标签内容",
+          "Canva AI(图标+文字模板)": "使用预设的标签条模板(定位图标+文字框、勾选框+任务描述),通过数据连接批量替换每个标签的文字内容",
+          "Morpholio Trace(Studio Text)": "在图层上批量添加多个文本框,选择 Select Mode 批量编辑对齐、颜色、字体、大小,实现标注标签的统一样式管理"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-154a84fd"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    },
+    {
+      "enabled": true,
+      "master": {
+        "id": "CAP-1b3e966f",
+        "merged_description": "以等距矩形投影(equirectangular)格式直接生成具备全局空间一致性的 360 度球面全景图,输出图像具有无缝环绕边缘,确保场景在各个方向上的几何关系、光照和风格保持连贯。生成的全景图可被 360 度查看器、VR 设备和游戏引擎直接识别和使用,无需后期拼接,支持自由漫游探索和 mesh 资产导出用于二次建模。",
+        "synthesized_effects": [
+          {
+            "description": "无需昂贵相机设备或实地拍摄,纯 AI 生成即可获得可交互的 360 度全景图"
+          },
+          {
+            "description": "生成的全景图可直接用于 VR 内容、游戏环境、虚拟制作背景等场景"
+          },
+          {
+            "description": "批量生成不同风格的室内全景图,快速探索多种设计方向"
+          },
+          {
+            "description": "单张图片或一句话即可生成可自由漫游的完整 360° 空间世界"
+          },
+          {
+            "description": "导出的 mesh 资产可在 SketchUp、Rhino 等工具中进行二次建模和编辑"
+          },
+          {
+            "description": "空间一致性保证使生成的全景世界可直接用于 VR 仿真和游戏场景"
+          }
+        ],
+        "synthesized_implements": {
+          "ChatGPT / 自定义 Chatbot": "使用特定提示词(如 'Transforms this photo into a 360° panoramic image... equirectangular viewing')并指定 2:1 或 3000x1500 分辨率生成球面全景图",
+          "tools/image_gen/midjourney": "使用 equirectangular photograph of + 场景描述的提示词公式,配合 2:1 宽高比参数生成等距矩形投影全景图",
+          "ComfyUI 工作流": "配置专用 360 全景工作流节点,输入室内设计提示词,一键生成 360° 室内家装全景图",
+          "Marble World AI": "专门支持360度全景室内图生成的AI平台,配合Gemini提示词生成适配VR展示的全景图",
+          "DiT360 AI": "专用 360 度全景生成模型,通过文本描述直接输出具有无缝环绕边缘和空间一致性的等距矩形全景图,可在 HuggingFace Spaces 在线使用",
+          "Gemini AI": "在提示词中指定'ultra-realistic 360-degree panoramic photo (equirectangular projection, 2:1 aspect ratio)'、'as if captured by a professional 360 camera'、'horizontal seamless continuity, left and right edges perfectly aligned'",
+          "腾讯混元 3D 世界模型": "输入一句话提示词或单张图片,生成具备空间一致性的 360° 全景世界,支持自由漫游探索和 mesh 资产导出二次建模"
+        },
+        "synthesized_criterion": []
+      },
+      "duplicates": [
+        {
+          "id": "CAP-ee5ffa0b"
+        }
+      ],
+      "merged_result": "Keep master, update fields, redirect relations"
+    }
+  ],
+  "strategies": []
+}

+ 930 - 0
examples/process_research/aigc_architecture.html

@@ -0,0 +1,930 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>AIGC 自动化工作流与知识库闭环架构</title>
+    <link
+        href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=Noto+Sans+SC:wght@300;400;500;700&display=swap"
+        rel="stylesheet">
+    <style>
+        :root {
+            --bg: #09090b;
+            --card-bg: rgba(255, 255, 255, 0.03);
+            --card-border: rgba(255, 255, 255, 0.1);
+            --text-primary: #f4f4f5;
+            --text-secondary: #a1a1aa;
+
+            --accent-cyan: #06b6d4;
+            --accent-purple: #8b5cf6;
+            --accent-pink: #ec4899;
+            --accent-green: #10b981;
+            --accent-amber: #f59e0b;
+            --accent-red: #ef4444;
+            --accent-blue: #3b82f6;
+
+            --glow: 0 0 20px rgba(6, 182, 212, 0.3);
+        }
+
+        * {
+            box-sizing: border-box;
+            margin: 0;
+            padding: 0;
+        }
+
+        body {
+            font-family: 'Outfit', 'Noto Sans SC', sans-serif;
+            background-color: var(--bg);
+            color: var(--text-primary);
+            min-height: 100vh;
+            overflow-x: hidden;
+            background-image:
+                radial-gradient(circle at 15% 50%, rgba(139, 92, 246, 0.08) 0%, transparent 50%),
+                radial-gradient(circle at 85% 30%, rgba(6, 182, 212, 0.08) 0%, transparent 50%);
+        }
+
+        .container {
+            max-width: 1200px;
+            margin: 0 auto;
+            padding: 40px 20px;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+        }
+
+        .header {
+            text-align: center;
+            margin-bottom: 60px;
+            animation: fadeInDown 1s ease-out;
+        }
+
+        .header h1 {
+            font-size: 2.5rem;
+            font-weight: 800;
+            background: linear-gradient(135deg, #fff, #a1a1aa);
+            -webkit-background-clip: text;
+            -webkit-text-fill-color: transparent;
+            margin-bottom: 10px;
+            letter-spacing: 1px;
+        }
+
+        .header p {
+            color: var(--text-secondary);
+            font-size: 1.1rem;
+            max-width: 600px;
+            margin: 0 auto;
+        }
+
+        /* Flowchart Flow */
+        .flow-container {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            position: relative;
+            width: 100%;
+        }
+
+        /* Connecting vertical line */
+        .flow-container::before {
+            content: '';
+            position: absolute;
+            top: 20px;
+            bottom: 50px;
+            left: 50%;
+            transform: translateX(-50%);
+            width: 2px;
+            background: linear-gradient(to bottom, var(--card-border) 40%, var(--card-border) 60%, transparent 100%);
+            background-size: 100% 20px;
+            z-index: 0;
+            box-shadow: var(--glow);
+        }
+
+        .layer {
+            position: relative;
+            width: 100%;
+            display: flex;
+            justify-content: center;
+            margin-bottom: 60px;
+            /* reduced from 80px to fit more steps */
+            z-index: 1;
+            opacity: 0;
+            transform: translateY(30px);
+            animation: fadeInUp 0.8s forwards;
+        }
+
+        .layer:nth-child(2) {
+            animation-delay: 0.15s;
+        }
+
+        .layer:nth-child(3) {
+            animation-delay: 0.3s;
+        }
+
+        .layer:nth-child(4) {
+            animation-delay: 0.45s;
+        }
+
+        .layer:nth-child(5) {
+            animation-delay: 0.6s;
+        }
+
+        .layer:nth-child(6) {
+            animation-delay: 0.75s;
+        }
+
+        .node {
+            background: var(--card-bg);
+            border: 1px solid var(--card-border);
+            border-radius: 16px;
+            padding: 24px 32px;
+            backdrop-filter: blur(12px);
+            -webkit-backdrop-filter: blur(12px);
+            text-align: center;
+            position: relative;
+            transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+            min-width: 320px;
+            max-width: 480px;
+        }
+
+        .node:hover {
+            transform: translateY(-5px) scale(1.02);
+            background: rgba(255, 255, 255, 0.05);
+            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
+        }
+
+        /* Accent glows for different node types */
+        .node.node-req {
+            border-top: 3px solid var(--accent-cyan);
+        }
+
+        .node.node-search {
+            border-top: 3px solid var(--accent-blue);
+        }
+
+        .node.node-process {
+            border-top: 3px solid var(--accent-purple);
+        }
+
+        .node.node-pipeline {
+            border-top: 3px solid var(--accent-pink);
+        }
+
+        .node.node-eval {
+            border-top: 3px solid var(--accent-amber);
+        }
+
+        .node:hover.node-req {
+            box-shadow: 0 0 20px rgba(6, 182, 212, 0.2);
+        }
+
+        .node:hover.node-search {
+            box-shadow: 0 0 20px rgba(59, 130, 246, 0.2);
+        }
+
+        .node:hover.node-process {
+            box-shadow: 0 0 20px rgba(139, 92, 246, 0.2);
+        }
+
+        .node:hover.node-pipeline {
+            box-shadow: 0 0 20px rgba(236, 72, 153, 0.2);
+        }
+
+        .node:hover.node-eval {
+            box-shadow: 0 0 20px rgba(245, 158, 11, 0.2);
+        }
+
+        .node-title {
+            font-size: 1.25rem;
+            font-weight: 700;
+            margin-bottom: 12px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: 8px;
+        }
+
+        .node-desc {
+            font-size: 0.9rem;
+            color: var(--text-secondary);
+            line-height: 1.6;
+        }
+
+        /* Side Branches / Split Layouts */
+        .split-col {
+            display: flex;
+            gap: 40px;
+            align-items: stretch;
+            justify-content: center;
+            width: 100%;
+        }
+
+        .sub-node-box {
+            background: rgba(0, 0, 0, 0.2);
+            border: 1px dashed var(--card-border);
+            border-radius: 12px;
+            padding: 16px;
+            margin-top: 20px;
+            text-align: left;
+        }
+
+        .sub-list {
+            list-style: none;
+            margin-top: 10px;
+        }
+
+        .sub-list li {
+            font-size: 0.85rem;
+            color: var(--text-secondary);
+            margin-bottom: 8px;
+            display: flex;
+            align-items: center;
+            gap: 6px;
+        }
+
+        .icon-emoji {
+            font-size: 1.2rem;
+            filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.2));
+        }
+
+        /* Abstract structural capabilities styling */
+        .ability-circles {
+            display: flex;
+            justify-content: center;
+            gap: 12px;
+            margin: 15px 0;
+        }
+
+        .ability-circle {
+            width: 32px;
+            height: 32px;
+            border-radius: 50%;
+            border: 2px solid var(--accent-purple);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 0.8rem;
+            color: #fff;
+            background: rgba(139, 92, 246, 0.1);
+        }
+
+        .ability-circle.solid {
+            background: var(--accent-purple);
+            border-style: solid;
+            box-shadow: 0 0 10px rgba(139, 92, 246, 0.4);
+        }
+
+        .ability-circle.outline {
+            background: transparent;
+            border-style: dashed;
+            border-color: var(--accent-cyan);
+            color: var(--accent-cyan);
+        }
+
+        .ability-circle.half {
+            background: linear-gradient(135deg, var(--accent-purple) 50%, transparent 50%);
+        }
+
+        .case-square {
+            width: 28px;
+            height: 28px;
+            border-radius: 4px;
+            border: 1px solid var(--accent-purple);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 0.75rem;
+            color: #fff;
+            background: rgba(139, 92, 246, 0.2);
+            transition: all 0.2s;
+        }
+
+        .case-square:hover {
+            transform: translateY(-2px);
+            background: rgba(139, 92, 246, 0.4);
+        }
+
+        .process-pipeline {
+            display: flex;
+            align-items: flex-start;
+            justify-content: center;
+            gap: 12px;
+            margin-top: 25px;
+            padding: 15px;
+            background: rgba(0, 0, 0, 0.2);
+            border: 1px dashed var(--card-border);
+            border-radius: 12px;
+            position: relative;
+        }
+
+        .pipeline-step {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            gap: 6px;
+        }
+
+        .pipeline-arrow {
+            margin-top: 8px;
+            /* align with abilities */
+            color: var(--accent-purple);
+            font-weight: bold;
+        }
+
+        .case-cluster {
+            display: flex;
+            gap: 4px;
+            flex-wrap: wrap;
+            justify-content: center;
+            width: 60px;
+        }
+
+        .v-line {
+            width: 1px;
+            height: 15px;
+            background-color: var(--card-border);
+            position: relative;
+        }
+
+        .v-line.extract {
+            background-color: var(--accent-cyan);
+            border-left: 1px dashed var(--accent-cyan);
+            width: 0;
+        }
+
+        .v-line-label {
+            font-size: 0.65rem;
+            color: var(--text-secondary);
+            margin-bottom: -4px;
+        }
+
+        .v-line-label.extract {
+            color: var(--accent-cyan);
+        }
+
+        .pipeline-grid {
+            display: grid;
+            grid-template-columns: auto 20px 40px 30px 1fr;
+            align-items: center;
+            row-gap: 5px;
+            margin-top: 15px;
+            text-align: left;
+        }
+
+        .pipeline-grid .cases {
+            justify-self: end;
+            display: flex;
+            gap: 4px;
+        }
+
+        .pipeline-grid .dash {
+            justify-self: center;
+            color: var(--text-secondary);
+            font-size: 0.8rem;
+        }
+
+        .pipeline-grid .ability {
+            justify-self: center;
+        }
+
+        .pipeline-grid .impl {
+            justify-self: center;
+            padding: 8px 16px;
+            border-radius: 8px;
+            font-size: 0.85rem;
+            background: rgba(236, 72, 153, 0.15);
+            border: 1px solid var(--accent-pink);
+            color: #fff;
+            min-width: 180px;
+            text-align: center;
+        }
+
+        .pipeline-grid .impl.dashed {
+            background: rgba(236, 72, 153, 0.05);
+            border: 1px dashed var(--accent-pink);
+            color: var(--text-secondary);
+        }
+
+        .pipeline-grid .v-link {
+            justify-self: center;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            height: 25px;
+            color: var(--text-secondary);
+            font-size: 10px;
+        }
+
+        .pipeline-grid .v-link::before {
+            content: '';
+            width: 2px;
+            flex: 1;
+            background: var(--text-secondary);
+            margin-bottom: 2px;
+        }
+
+        .pipeline-grid .v-link.impl-side {
+            align-items: center;
+            justify-self: center;
+            margin-left: 0;
+        }
+
+        /* specific connecting arrows */
+        .connector-label {
+            position: absolute;
+            font-size: 0.95rem;
+            font-weight: 500;
+            color: var(--accent-cyan);
+            background: var(--bg);
+            padding: 4px 12px;
+            border-radius: 6px;
+            border: 1px solid var(--accent-cyan);
+            box-shadow: 0 0 15px rgba(6, 182, 212, 0.3);
+            white-space: nowrap;
+            z-index: 2;
+        }
+
+        /* Specific placements for the eval loop */
+        .eval-success {
+            color: var(--accent-green) !important;
+            border-color: var(--accent-green) !important;
+        }
+
+        .eval-fail {
+            color: var(--accent-red) !important;
+            border-color: var(--accent-red) !important;
+        }
+
+        @keyframes fadeInUp {
+            to {
+                opacity: 1;
+                transform: translateY(0);
+            }
+        }
+
+        @keyframes fadeInDown {
+            from {
+                opacity: 0;
+                transform: translateY(-30px);
+            }
+
+            to {
+                opacity: 1;
+                transform: translateY(0);
+            }
+        }
+
+        /* Pulse for the execution block */
+        .pulse {
+            animation: pulse-border 2s infinite;
+        }
+
+        @keyframes pulse-border {
+            0% {
+                box-shadow: 0 0 0 0 rgba(6, 182, 212, 0.4);
+            }
+
+            70% {
+                box-shadow: 0 0 0 15px rgba(6, 182, 212, 0);
+            }
+
+            100% {
+                box-shadow: 0 0 0 0 rgba(6, 182, 212, 0);
+            }
+        }
+    </style>
+</head>
+
+<body>
+
+    <div class="container">
+        <div class="header">
+            <h1>系统架构图流转拓扑</h1>
+            <p>AIGC 自动化任务拆解与知识库演进全景图</p>
+        </div>
+
+        <div class="flow-container">
+
+            <!-- Step 1: Requirements Extraction & DB Match -->
+            <div class="layer">
+                <div style="display: flex; justify-content: center; width: 100%; gap: 60px; position: relative;">
+                    
+                    <div class="node node-req" style="flex: 1; max-width: 400px;">
+                        <div class="node-title">
+                            <span class="icon-emoji">📥</span> 提取需求 (Requirements)
+                        </div>
+                        <div class="node-desc" style="line-height: 1.6;">
+                            从原始的帖子、还原任务或用户指令中剖析意图。<br>
+                            <span style="color:var(--text-secondary); font-size: 0.85rem;">(明确用户的核心诉求与待解任务)</span>
+                        </div>
+                    </div>
+
+                    <div class="node node-eval" style="flex: 1; max-width: 350px; border-top-color: var(--accent-green); display: flex; flex-direction: column; justify-content: center; position: relative;">
+                        <!-- Connector arrow pointing left and right -->
+                        <div class="connector-label" style="left: -30px; top: 50%; transform: translate(-50%, -50%); z-index: 10;">⟷ 对齐 / 拦截</div>
+                        
+                        <div class="node-title">
+                            <span class="icon-emoji">📂</span> 已有需求库 (Req DB)
+                        </div>
+                        <div class="node-desc">匹配 db_requirements.json 树状分布</div>
+                        <div style="margin-top: 12px; font-size: 0.8rem; background: rgba(16, 185, 129, 0.1); padding: 8px; border-radius: 6px; border: 1px dashed rgba(16, 185, 129, 0.5);">
+                            <span style="color: var(--accent-green); font-weight: bold; display: flex; align-items: center; gap: 6px;">
+                                <span style="font-size: 1.1rem;">⚡</span>
+                                <span>若已实现: 跳出循环,直接复用底层 Pipeline</span>
+                            </span>
+                        </div>
+                    </div>
+
+                </div>
+            </div>
+
+            <!-- Step 2: Research and Case Collection -->
+            <div class="layer">
+                <div class="node node-search" style="margin-top: 20px;">
+                    <div class="connector-label" style="top: -34px; left: 50%; transform: translateX(-50%);">根据新需求/未满足需求 ⬇</div>
+                    <div class="node-title">
+                        <span class="icon-emoji">🔍</span> 调研核心 Case
+                    </div>
+                    <div class="node-desc">
+                        面向全网或本地库,定向并发检索大量真实场景参考。<br>
+                        获取高度相关的图文案例资料 (<b style="color:var(--accent-blue)">拿到 Case</b>)。
+                    </div>
+                    
+                    <div style="font-size: 0.75rem; color: var(--text-secondary); width: 100%; text-align: center; margin-top: 15px; margin-bottom: 5px;">
+                        <span style="color: var(--accent-blue); font-weight: bold;">■ 网络搜索</span> (外网新知) &nbsp;&nbsp;|&nbsp;&nbsp; 
+                        <span style="color: var(--accent-purple); font-weight: bold;">■ 库内召回</span> (自有知识)
+                    </div>
+                    <div style="display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; padding: 15px; background: rgba(0,0,0,0.2); border-radius: 12px; border: 1px dashed rgba(255, 255, 255, 0.1);">
+                        <!-- Network Cases -->
+                        <div class="case-square" title="网络用例 01" style="background: rgba(59, 130, 246, 0.2); border-color: var(--accent-blue);">C1</div>
+                        <div class="case-square" title="网络用例 02" style="background: rgba(59, 130, 246, 0.2); border-color: var(--accent-blue);">C2</div>
+                        <div class="case-square" title="网络用例 03" style="background: rgba(59, 130, 246, 0.2); border-color: var(--accent-blue);">C3</div>
+                        <div class="case-square" title="网络用例 04" style="background: rgba(59, 130, 246, 0.2); border-color: var(--accent-blue);">C4</div>
+                        
+                        <!-- Internal Cases -->
+                        <div class="case-square" title="库内用例 05" style="background: rgba(139, 92, 246, 0.2); border-color: var(--accent-purple);">C5</div>
+                        <div class="case-square" title="库内用例 06" style="background: rgba(139, 92, 246, 0.2); border-color: var(--accent-purple);">C6</div>
+                        <div class="case-square" title="库内用例 07" style="background: rgba(139, 92, 246, 0.2); border-color: var(--accent-purple);">C7</div>
+                        <div style="color: var(--text-secondary); display: flex; align-items: center; justify-content: center; margin-left: 5px; font-weight: bold;">...</div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- Step 3: Process Design & Ability Extraction -->
+            <div class="layer">
+                <div style="display: flex; flex-direction: column; align-items: center; width: 100%; position: relative;">
+                    <!-- New Flow Label -->
+                    <div class="connector-label" style="top: -65px; left: 50%; transform: translateX(-50%);">解析业务场景特征 ⬇</div>
+
+                    <!-- Top Row: Two Pools -->
+                    <div style="display: flex; gap: 40px; justify-content: center; width: 100%; margin-bottom: 20px;">
+
+                        <!-- Left: Ability Pool -->
+                        <div class="node node-process"
+                            style="flex: 1; max-width: 350px; border-top-color: var(--accent-cyan); display: flex; flex-direction: column;">
+                            <div class="node-title">
+                                <span class="icon-emoji">💡</span> 现有能力池 (Ability Pool)
+                            </div>
+                            <div class="node-desc" style="margin-bottom: 8px;">全局积累的标准原子能力库</div>
+                            
+                            <!-- Legend for Ability Pool -->
+                            <div style="font-size: 0.75rem; color: var(--text-secondary); width: 100%; text-align: center; margin-bottom: 12px;">
+                                <span style="display:inline-flex; align-items:center; gap:6px; margin-right:12px;">
+                                    <div class="ability-circle solid" style="width:16px; height:16px; font-size:0; min-width:16px;"></div> <span style="color:var(--text-primary);">已实现能力</span>
+                                </span>
+                                <span style="display:inline-flex; align-items:center; gap:6px;">
+                                    <div class="ability-circle outline" style="width:16px; height:16px; font-size:0; min-width:16px; border-color:var(--accent-cyan);"></div> <span style="color:var(--accent-cyan);">暂定新能力</span>
+                                </span>
+                            </div>
+
+                            <div
+                                style="display:flex; gap: 8px; flex-wrap:wrap; justify-content:center; padding: 15px; border-radius: 12px; background: rgba(0,0,0,0.3); border: 1px solid var(--card-border);">
+                                <div class="ability-circle solid" title="已实现能力">A</div>
+                                <div class="ability-circle solid" title="已实现能力">C</div>
+                                <div class="ability-circle solid" title="已实现能力">E</div>
+                                <div class="ability-circle solid" title="已实现能力">F</div>
+                                <div class="ability-circle outline" title="暂定新能力" style="border-color: var(--accent-cyan); color: var(--accent-cyan);">B</div>
+                                <div class="ability-circle solid" title="已实现能力">G</div>
+                                <div class="ability-circle solid" title="已实现能力">H</div>
+                                <div class="ability-circle solid" title="已实现能力">I</div>
+                                <div class="ability-circle outline" title="暂定新能力" style="border-color: var(--accent-cyan); color: var(--accent-cyan);">D</div>
+                            </div>
+                        </div>
+
+                        <!-- Right: Process Pool -->
+                        <div class="node node-process"
+                            style="flex: 1; max-width: 350px; border-top-color: var(--accent-amber); display: flex; flex-direction: column;">
+                            <div class="node-title" style="color: var(--text-primary);">
+                                <span class="icon-emoji">📚</span> 结构化工序池
+                            </div>
+                            <div class="node-desc" style="margin-bottom: 15px;">已验证的高分复用工序</div>
+
+                            <div style="display: flex; flex-direction: column; gap: 10px; text-align: left;">
+                                <!-- Process 1 -->
+                                <div
+                                    style="background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.5); padding: 10px; border-radius: 8px;">
+                                    <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
+                                        <span
+                                            style="font-weight: bold; font-size: 0.85rem; color: #fff;">人物视觉合成工序</span>
+                                        <span
+                                            style="background: var(--accent-amber); color: #000; font-size: 0.7rem; padding: 2px 6px; border-radius: 10px; font-weight: bold;">验证分数:
+                                            8</span>
+                                    </div>
+                                    <div
+                                        style="font-size: 0.75rem; color: var(--text-secondary); display: flex; align-items: center; gap: 6px; margin-top: 5px;">
+                                        包含能力:
+                                        <div
+                                            style="display: flex; align-items: center; gap: 4px; transform: scale(0.8); transform-origin: left center;">
+                                            <div class="ability-circle solid">A</div>
+                                            <div style="color: var(--text-secondary); font-size: 1rem;">➔</div>
+                                            <div class="ability-circle solid">C</div>
+                                            <div style="color: var(--text-secondary); font-size: 1rem;">➔</div>
+                                            <div class="ability-circle solid">E</div>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <!-- Process 2 (New) -->
+                                <div
+                                    style="background: rgba(245, 158, 11, 0.05); border: 1px dashed rgba(245, 158, 11, 0.3); padding: 10px; border-radius: 8px;">
+                                    <div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
+                                        <span
+                                            style="font-weight: bold; font-size: 0.85rem; color: var(--text-secondary);">(NEW)
+                                            特效错位新工序</span>
+                                        <span
+                                            style="background: rgba(255, 255, 255, 0.1); color: var(--text-secondary); font-size: 0.7rem; padding: 2px 6px; border-radius: 10px;">初始入库:
+                                            0</span>
+                                    </div>
+                                    <div
+                                        style="font-size: 0.75rem; color: var(--text-secondary); display: flex; align-items: center; gap: 6px; margin-top: 5px;">
+                                        包含能力:
+                                        <div
+                                            style="display: flex; align-items: center; gap: 4px; transform: scale(0.8); transform-origin: left center;">
+                                            <div class="ability-circle solid">A</div>
+                                            <div style="color: var(--text-secondary); font-size: 1rem;">➔</div>
+                                            <div class="ability-circle outline"
+                                                style="border-color: var(--accent-cyan); color: var(--accent-cyan);">B
+                                            </div>
+                                            <div style="color: var(--text-secondary); font-size: 1rem;">➔</div>
+                                            <div class="ability-circle solid">C</div>
+                                            <div style="color: var(--text-secondary); font-size: 1rem;">➔</div>
+                                            <div class="ability-circle outline"
+                                                style="border-color: var(--accent-cyan); color: var(--accent-cyan);">D
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+
+                    <!-- Funnel Arrows -->
+                    <div style="display: flex; justify-content: center; gap: 80px; margin-bottom: -15px; z-index: 10; position: relative;">
+                        <div class="connector-label" style="position: static; font-size: 0.9rem; color: var(--accent-purple); border-color: var(--accent-purple); box-shadow: 0 0 10px rgba(139, 92, 246, 0.2);">
+                            ↘ 提取引用能力
+                        </div>
+                        <div class="connector-label" style="position: static; font-size: 0.9rem; color: var(--accent-amber); border-color: var(--accent-amber); box-shadow: 0 0 10px rgba(245, 158, 11, 0.2);">
+                            复用现成工序 ↙
+                        </div>
+                    </div>
+
+                    <!-- Center Base: Structured Process Builder -->
+                    <div class="node node-process" style="width: 100%; max-width: 500px;">
+                        <div class="node-title">
+                            <span class="icon-emoji">🧩</span> 设计/复用结构化工序
+                        </div>
+                        <div class="node-desc">
+                            参考原有的工序沉淀,结合新 Case 抽取出结构流向。
+                        </div>
+
+                        <div class="process-pipeline">
+                            <div class="connector-label" style="top: -12px; left: 10px; font-weight: normal;">执行链条
+                                (Structured Process)</div>
+
+                            <!-- Step A -->
+                            <div class="pipeline-step">
+                                <div class="ability-circle solid" title="已实现的标准能力 (Implemented)">A</div>
+                                <div class="v-line-label">支撑</div>
+                                <div class="v-line"></div>
+                                <div class="case-cluster">
+                                    <div class="case-square" title="Case 01">C1</div>
+                                    <div class="case-square" title="Case 02">C2</div>
+                                </div>
+                            </div>
+
+                            <div class="pipeline-arrow">➔</div>
+
+                            <!-- Step B -->
+                            <div class="pipeline-step">
+                                <div class="ability-circle outline" title="暂定的新能力 (Tentative)">B</div>
+                                <div class="v-line-label extract">提炼</div>
+                                <div class="v-line extract"></div>
+                                <div class="case-cluster">
+                                    <div class="case-square" title="Case 03">C3</div>
+                                    <div class="case-square" title="Case 04">C4</div>
+                                </div>
+                            </div>
+
+                            <div class="pipeline-arrow">➔</div>
+
+                            <!-- Step C -->
+                            <div class="pipeline-step">
+                                <div class="ability-circle solid" title="已实现的标准能力 (Implemented)">C</div>
+                                <div class="v-line-label">支撑</div>
+                                <div class="v-line"></div>
+                                <div class="case-cluster">
+                                    <div class="case-square" title="Case 05">C5</div>
+                                </div>
+                            </div>
+
+                            <div class="pipeline-arrow">➔</div>
+
+                            <!-- Step D -->
+                            <div class="pipeline-step">
+                                <div class="ability-circle outline" title="暂定的新能力 (Tentative)">D</div>
+                                <div class="v-line-label extract">提炼</div>
+                                <div class="v-line extract"></div>
+                                <div class="case-cluster">
+                                    <div class="case-square" title="Case 06">C6</div>
+                                    <div class="case-square" title="Case 07">C7</div>
+                                </div>
+                            </div>
+                        </div>
+                        <p style="font-size: 0.75rem; text-align:center; color:var(--text-secondary); margin-top:10px;">
+                            (沉淀:由本轮提炼的新能力和新工序将反哺至上方池中)
+                        </p>
+                    </div>
+                </div>
+            </div>
+
+            <!-- Step 4: Pipeline Assembly -->
+            <div class="layer">
+                <div class="node node-pipeline" style="min-width: 500px; margin-top: 20px;">
+                    <div class="connector-label" style="top: -34px; left: 50%; transform: translateX(-50%);">下发执行规范 (JSON) ⬇</div>
+                    <div class="node-title">
+                        <span class="icon-emoji">⚙️</span> 设计可执行 Pipeline
+                    </div>
+                    <div class="node-desc" style="margin-bottom: 20px;">
+                        将抽象能力序列下沉为具体的底层节点调用组
+                    </div>
+
+                    <div
+                        style="background: rgba(0,0,0,0.3); padding: 20px; border-radius: 12px; border: 1px dashed var(--accent-pink); position: relative;">
+                        <!-- Header -->
+                        <div
+                            style="display: flex; justify-content: space-between; font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 5px; padding-bottom: 8px; border-bottom: 1px solid var(--card-border);">
+                            <div style="flex: 1; text-align: left;">能力序列 (with Cases)</div>
+                            <div style="flex: 1; text-align: right;">真实工具表 (素材/要素)</div>
+                        </div>
+
+                        <div class="pipeline-grid" style="position: relative;">
+                            <!-- Divider Line -->
+                            <div
+                                style="position: absolute; left: 50%; top: -15px; bottom: 0; width: 1px; background: linear-gradient(to bottom, transparent, var(--card-border) 10%, transparent); transform: translateX(-50%); z-index: 0;">
+                            </div>
+
+                            <!-- Row 1 -->
+                            <div class="cases" style="z-index: 1;">
+                                <div class="case-square" title="Case 01">C1</div>
+                                <div class="case-square" title="Case 02">C2</div>
+                            </div>
+                            <div class="dash" style="z-index: 1;">一</div>
+                            <div class="ability" style="z-index: 1;">
+                                <div class="ability-circle solid" title="已实现的标准能力 (Implemented)">A</div>
+                            </div>
+                            <div class="dash" style="z-index: 1;">一</div>
+                            <div class="impl" style="z-index: 1;">implements (确定工具)</div>
+
+                            <!-- V-Link Row 1 -->
+                            <div></div>
+                            <div></div>
+                            <div class="v-link">⬇</div>
+                            <div></div>
+                            <div class="v-link impl-side">⬇</div>
+
+                            <!-- Row 2 -->
+                            <div class="cases" style="z-index: 1;">
+                                <div class="case-square" title="Case 03">C3</div>
+                                <div class="case-square" title="Case 04">C4</div>
+                            </div>
+                            <div class="dash" style="z-index: 1;">一</div>
+                            <div class="ability" style="z-index: 1;">
+                                <div class="ability-circle outline" title="暂定的新能力 (Tentative)">B</div>
+                            </div>
+                            <div class="dash" style="z-index: 1;">一</div>
+                            <div class="impl dashed" style="z-index: 1;">implements_candidates (已有工具替代)</div>
+
+                            <!-- V-Link Row 2 -->
+                            <div></div>
+                            <div></div>
+                            <div class="v-link">⬇</div>
+                            <div></div>
+                            <div class="v-link impl-side">⬇</div>
+
+                            <!-- Row 3 -->
+                            <div class="cases" style="z-index: 1;">
+                                <div class="case-square" title="Case 05">C5</div>
+                            </div>
+                            <div class="dash" style="z-index: 1;">一</div>
+                            <div class="ability" style="z-index: 1;">
+                                <div class="ability-circle solid" title="已实现的标准能力 (Implemented)">C</div>
+                            </div>
+                            <div class="dash" style="z-index: 1;">一</div>
+                            <div class="impl" style="z-index: 1;">im...</div>
+
+                            <!-- V-Link dots -->
+                            <div></div>
+                            <div></div>
+                            <div class="v-link" style="font-size: 14px; margin-top: -5px; color: var(--card-border);">⋮
+                            </div>
+                            <div></div>
+                            <div class="v-link impl-side"
+                                style="font-size: 14px; margin-top: -5px; color: var(--card-border);">⋮</div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- Step 5: Execution & Evaluate -->
+            <div class="layer">
+                <div class="split-col" style="align-items: flex-start; position: relative;">
+                    <!-- New Flow Label -->
+                    <div class="connector-label" style="top: -65px; left: 50%; transform: translateX(-50%);">投递至底层 Agent 引擎 ⬇</div>
+
+                    <!-- Left Branch: Failure / Refactoring -->
+                    <div class="node node-eval" style="border-top-color: var(--accent-red);">
+                        <div class="connector-label eval-fail" style="top: -30px; right: 0;">⬅ 失败反馈 (Fail)</div>
+                        <div class="node-title" style="color: var(--accent-red)">
+                            <span class="icon-emoji">🔧</span> 工序重构
+                        </div>
+                        <div class="node-desc">
+                            <strong>重试优先级机制</strong>
+                        </div>
+                        <ul class="sub-list" style="margin-top:15px; text-align: left;">
+                            <li><span style="color:var(--accent-red)">[1] 替换出错底层工具</span><br>
+                                <span style="font-size: 0.75rem; color: var(--text-secondary); margin-left: 18px;">⤤ 短路回退 <b>Step 4</b> (更换 API / 代码逻辑)</span>
+                            </li>
+                            <li style="margin-top: 8px;"><span style="color:var(--accent-red)">[2] 微调能力序列节点</span><br>
+                                <span style="font-size: 0.75rem; color: var(--text-secondary); margin-left: 18px;">⤤ 中度回退 <b>Step 3</b> (删减 / 新增组装原子)</span>
+                            </li>
+                            <li style="margin-top: 8px;"><span style="color:var(--accent-red)">[3] 彻底打翻工序结构</span><br>
+                                <span style="font-size: 0.75rem; color: var(--text-secondary); margin-left: 18px;">⤤ 深度回退 <b>Step 2</b> (重新提取用例与新能力)</span>
+                            </li>
+                        </ul>
+                    </div>
+
+                    <!-- Center Core: Execution Action -->
+                    <div class="node pulse" style="border:2px solid var(--accent-cyan); min-width: 200px; background: rgba(6, 182, 212, 0.05);">
+                        <div class="node-title" style="color: var(--accent-cyan);">
+                            <span class="icon-emoji">🚀</span> 实际执行 Pipeline
+                        </div>
+                        <div class="node-desc">
+                            在底层投递执行并产出最终成果<br><br>
+                            ⬇<br>
+                            <b style="font-size: 1.2rem; color: #fff; text-shadow: 0 0 10px rgba(255,255,255,0.5);">Result (核心产出结果)</b>
+                        </div>
+                        <br>
+                        <div
+                            style="background:var(--accent-amber); color:#000; padding:8px 0; border-radius:8px; font-weight:bold; width:80%; margin: 0 auto; box-shadow: 0 0 15px rgba(245, 158, 11, 0.4);">
+                            双向评估判定 (Evaluate)
+                        </div>
+                    </div>
+
+                    <!-- Right Branch: Success / Accumulation -->
+                    <div class="node node-eval" style="border-top-color: var(--accent-green);">
+                        <div class="connector-label eval-success" style="top: -30px; left: 0;">成功通过 (Success) ➡</div>
+                        <div class="node-title" style="color: var(--accent-green)">
+                            <span class="icon-emoji">✨</span> 系统底座升维
+                        </div>
+                        <div class="node-desc">自动沉淀知识体系资产</div>
+                        <div class="sub-node-box"
+                            style="border-color: rgba(16, 185, 129, 0.3); background: rgba(16, 185, 129, 0.05); text-align: center;">
+                            <div style="margin-bottom: 12px; font-size: 0.95rem;">✅ <b>新能力转为 Implemented</b></div>
+                            <div style="font-size: 0.95rem; line-height: 1.5;">✅ <b>完整执行流沉淀为自有 Case 入库</b><br>
+                                <span style="font-size: 0.75rem; color: #10B981; font-weight: normal; opacity: 0.9;">(反哺给 Step 1 实现拦截复用)</span>
+                            </div>
+                        </div>
+                        <div
+                            style="margin-top: 16px; font-weight: bold; background: linear-gradient(135deg, var(--accent-green), var(--accent-cyan)); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
+                            🚀 结构化工序 (Pipeline) 效能评分 +1
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+        </div>
+    </div>
+
+    <script>
+        document.querySelectorAll('.node').forEach((el, index) => {
+            el.addEventListener('mouseenter', () => {
+                el.style.transform = 'translateY(-5px) scale(1.02)';
+            });
+            el.addEventListener('mouseleave', () => {
+                el.style.transform = 'translateY(0) scale(1)';
+            });
+        });
+    </script>
+</body>
+
+</html>

+ 66 - 0
examples/process_research/config.py

@@ -0,0 +1,66 @@
+"""
+项目配置
+"""
+
+from agent.core.runner import KnowledgeConfig, RunConfig
+
+
+# ===== 主 Agent 配置(Coordinator,负责分发调研方向 + 汇总策略)=====
+
+# 是否开启子 Agent 真并发调研(极大榨取网络 IO 速度)
+PARALLEL_TOOL_EXECUTION = True
+
+COORDINATOR_RUN_CONFIG = RunConfig(
+    model="qwen-plus",
+    temperature=0.3,
+    max_iterations=800,
+    parallel_tool_execution=PARALLEL_TOOL_EXECUTION,
+    extra_llm_params={},
+    agent_type="coordinator",
+    name="工序调研协调器",
+    knowledge=KnowledgeConfig(
+        enable_extraction=False,
+        enable_completion_extraction=False,
+        enable_injection=False,
+        owner="sunlit.howard@gmail.com",
+    )
+)
+
+
+# ===== 输出目录 =====
+
+OUTPUT_DIR = "examples/process_research/output"   # 每个需求输出到 output/{N}/
+
+
+
+# ===== 基础设施配置 =====
+
+SKILLS_DIR = "./skills"
+TRACE_STORE_PATH = ".trace"
+DEBUG = True
+LOG_LEVEL = "INFO"
+LOG_FILE = None
+
+# ===== 浏览器配置 =====
+# 当 PARALLEL_TOOL_EXECUTION=True 时,强烈建议使用 cloud(云浏览器)或 headless,否则本地会出现抢鼠标冲突!
+BROWSER_TYPE = "local" if not PARALLEL_TOOL_EXECUTION else "cloud"
+HEADLESS = False
+
+# ===== IM 配置 =====
+IM_ENABLED = True
+IM_CONTACT_ID = "agent_research"
+IM_SERVER_URL = "ws://43.106.118.91:8105"
+IM_WINDOW_MODE = True
+IM_NOTIFY_INTERVAL = 10.0
+
+# ===== 远端 Research Agent 配置 =====
+# 此 pipeline 的 researcher 子 Agent 需要写文件到本地 output_dir,
+# 因此必须使用本地模式(USE_REMOTE_RESEARCH = False)。
+# 远端模式(True)仅适用于不需要写本地文件的单体 Agent 场景。
+USE_REMOTE_RESEARCH = False
+
+# 线上 KnowHub 地址(也可通过 .env 的 KNOWHUB_API 变量覆盖)
+# subagent._run_remote_agent 运行时会读取 KNOWHUB_API 环境变量
+# 这里显式设置以便 config 层可见
+import os as _os
+_os.environ.setdefault("KNOWHUB_API", "http://43.106.118.91:9999")

+ 101 - 0
examples/process_research/db_requirements.json

@@ -0,0 +1,101 @@
+[
+  "生成人物在不同场景下呈现丰富面部表情的图片,例如夸张的痛苦、无奈、开心、困倦等神态,表情要生动传神、情绪感强烈",
+  "生成人物与道具、环境或其他角色发生互动的画面,例如人物摆弄物品、与道具合影、在特定场景中做出配合动作等,画面要体现人物和周围元素之间的关联感",
+  "生成将动物(如猫咪)拟人化扮演特定角色或情境的图片,赋予其人类的表情、姿态和道具,用来传达幽默或情感共鸣的视觉效果",
+  "生成带有特定道具装扮的人物场景图,道具需与人物自然融合,例如猫咪戴假发穿衣服手持书本、人物手持购物篮抱着玩偶玩具等,道具细节清晰可辨",
+  "生成婚礼或节日庆典场景,背景需包含大量花卉装饰、定制发光字牌、喜字等布景元素,整体氛围感强烈,道具与场景协调统一",
+  "生成精致室内空间场景,画面中需呈现陶瓷器皿、绿植、家具等道具摆件,光线自然柔和,营造出温馨生活感或高颜值家居氛围",
+  "生成真实人物在户外或特定场景中的生活记录照片,画面自然真实,包含儿童在公园、农场等户外环境中玩耍的多角度抓拍效果,光线自然,氛围温馨",
+  "制作将真实人物照片合成到趣味场景中的创意图片,例如把人物缩小放入超市肉类托盘包装内、或与冰雕翅膀等道具结合形成视觉错位的幽默效果",
+  "生成真实场景的多图拼贴展示图,将同一地点或主题的多张实拍照片拼合为一张图文并茂的内容图,适合用于地点打卡、产品展示或生活记录类帖子",
+  "制作多格宫格式信息图,将同类内容(如多种食材搭配方案)拆分为统一风格的小卡片,每格包含标题、食材图片和文字说明,整体排列整齐、色块鲜明,适合一图展示多个并列条目",
+  "在图片上叠加标注元素,如用红色圆点、箭头或emoji符号指向图中特定位置,配合说明文字,实现在真实照片上直观标记关键信息的视觉效果",
+  "制作图文混排的长图文内容,将大段文字与人物照片、数据表格、流程图等多种视觉元素组合排布在同一版面中,形成类似杂志或报告的专业排版风格",
+  "生成以暖黄/米棕色为背景底色的图文排版内容,整体画面呈现温暖、复古的暖色调氛围,适合健康养生、生活方式类主题",
+  "生成以深色(黑色/深蓝/深紫)为背景底色的海报,搭配霓虹感彩色光效(橙、紫、青等),营造出科技感强烈的冷暖对比配色效果",
+  "生成整体色调偏粉紫、薄荷绿、浅蓝等低饱和度冷色系的插画或场景图,画面呈现出梦幻、静谧的冷色调氛围,颜色搭配柔和克制",
+  "生成手部持握或展示物品的特写画面,手势自然,物品清晰呈现,如用手托举饺子、手持卡片、手握手机等,突出手与物品的互动关系",
+  "生成多人或多只动物同框的协同姿态画面,如两只猫并排躺着穿睡衣、两人一起摆造型抱花束,呈现出同步、对称或互动的视觉效果",
+  "生成创意性的嘴部动作特写,如用嘴唇衔住花朵茎部形成'嘴唇花'的视觉效果,将身体部位与物品结合产生趣味创意画面",
+  "将人物照片与中国传统吉祥符号(如双喜字、红玫瑰、金色祝福文字)融合,生成具有强烈喜庆氛围的定制化图案,人物面孔清晰嵌入红色喜庆背景中",
+  "制作统一模板风格的系列信息卡片,每张卡片包含固定的图标符号(如皇冠等级图标)、彩色标题文字和配图,整体视觉风格一致、可批量复用",
+  "生成黑色科技感背景的人物宣传海报,背景带有流光线条或霓虹光效,人物照片与品牌Logo、活动标识、二维码等视觉元素整齐排布,形成高辨识度的系列展示图",
+  "生成宠物或动物穿戴人类服饰配件(如帽子、围巾)的画面,让动物看起来像在过节或扮演某种角色,整体效果可爱又有趣",
+  "生成将普通服装或日常物品以夸张搞怪方式穿戴的人物画面,比如把超大号短裤当长袍穿、用篮球短裤模仿古希腊长袍,产生强烈的视觉反差和幽默效果",
+  "生成具有超现实风格的创意合成画面,将人物头部替换为宇宙星云、太极图、粒子爆炸等抽象元素,配合深蓝红色调背景,营造出哲学感或科幻感的视觉冲击",
+  "生成穿着完整冬季搭配的人物形象,展示黑色羽绒服、红色围巾、红色手套、宽腿裤等单品的组合穿搭效果,呈现从全身到局部细节的多角度造型展示",
+  "生成宠物穿着服装的可爱造型图,展示猫咪穿上印花连体衣的整体穿着效果,需要清晰呈现服装的图案、版型与宠物身体的贴合细节",
+  "生成创意合成图,将人物穿搭形象嵌入特定场景容器中(如超市生鲜托盘),使人物服装与场景产生趣味性视觉对比,同时保留服装细节的清晰可见",
+  "将猫咪表情包图片与各种场景素材(办公室、食物、产品、背景环境等)合成拼贴在一起,让猫咪看起来自然地处于这些场景中,形成多格并排的拼贴版式",
+  "把猫咪图片与各类装扮道具(帽子、眼镜、服装、假发等)或其他卡通/玩具素材叠加合成,让不同来源的素材无缝融合成一张完整的搞笑图",
+  "在同一张图中将多只猫咪或同一只猫咪的不同姿态照片拼接组合,配合文字标注形成对话或对比效果的多格拼图",
+  "将真实照片中的人物与卡通/奇幻元素合成,例如给人物添加蟑螂的触角和腿,使人物看起来像变成了一只蟑螂,整体画面自然融合不突兀",
+  "给普通猫咪照片套上不同职业的服装和场景(如医生、上班族、老板等),并保持猫咪面部表情清晰可辨,制作出系列表情包拼贴图",
+  "在真实物体照片上叠加手绘风格的简笔画元素,例如在猕猴桃切片上添加卡通五官和小触角,让照片呈现出实物与手绘结合的趣味效果",
+  "生成真实户外场景中的人物活动照片,画面要呈现自然光线下的街道、公园、游乐场等具体地点环境,人物动作自然生动,背景环境细节丰富真实",
+  "生成多人聚集的活动现场图,如会议、展览、户外聚会等场景,画面中需要呈现多个人物同框、有组织的群体互动氛围,背景有明显的活动标识或场地特征",
+  "生成真实物品的特写或陈列展示图,物品摆放清晰、细节可辨,适合用于产品展示或场景道具呈现,画面构图干净突出主体",
+  "生成同一人物在同一场景中多角度、多姿态的即时抓拍效果图,画面呈现自然随意的动态感,如行走、转身、低头、仰望等非摆拍状态,整体风格真实生活化",
+  "生成动物(如马)在运动瞬间被捕捉的高动态画面,鬃毛飞扬、肢体伸展,呈现出强烈的瞬间张力和动感,背景简洁以突出主体动态",
+  "生成人物在真实日常场景(街头、公园、机场等)中被随手拍下的多张图片拼贴效果,画面构图不刻意、视角多变(含俯拍脚部、镜中自拍、远景抓拍等),整体呈现出碎片化的生活记录感",
+  "制作图文卡片时,需要让插图与文字在语义上高度呼应——比如用可爱小驴的不同表情和动作配合对应的幽默文字,每张小卡片中图在上、文字在下,插图内容直接反映文字含义,形成一眼就能看懂的图文配合效果",
+  "制作多图拼贴帖子时,需要将多张照片或截图按照叙事顺序排列在一张大图中,并在关键图片上叠加说明性文字标注,让图片和文字共同讲述一个完整故事,文字起到补充说明和情感点评的作用",
+  "制作信息图文海报时,需要将大标题、分类小标题与正文段落按照清晰的层级排布在版面上,标题用大字醒目展示,正文紧跟其下,整体版面分区明确、图文对应,让读者能快速扫读获取信息",
+  "在图片上叠加标题文字,文字大小、粗细、颜色各异,形成层次感强的排版效果——例如大标题用粗体醒目字体,副标题用细体小字,整体风格统一(如深色系商务风或简约设计风)",
+  "在AI生成的卡通角色图片上叠加幽默吐槽文案,文字直接覆盖在图片上方,字体简洁白色,与画面情绪呼应,形成图文结合的表情包风格内容",
+  "制作多宫格拼图帖子,每格图片配有对应的标题文字或字幕说明,文字风格统一,整体排列整齐,适合用于周记、日历、流程说明等系列内容展示",
+  "制作图文卡片式科普内容:每张卡片包含统一的标题样式、编号序列、配套插图(卡通/示意图风格)和说明文字,多张卡片拼成一组,整体风格统一、排版清晰,适合健康养生、步骤教程类内容展示",
+  "制作数据报告类图文内容:包含柱状图、饼图、折线图、词云图、环形图等多种数据可视化图表,配合标题、要点文字说明,整体呈现专业研究报告的视觉风格,色彩搭配统一(如蓝紫色系或橙色系)",
+  "制作流程图/架构示意图:用箭头、方框、层级结构或立体堆叠图形展示系统架构、业务流程或概念层级关系,配合文字标注说明各模块功能,视觉上清晰呈现逻辑关系",
+  "将整个帖子内容拆分为多个独立小格子并排列成网格或矩阵布局,每个格子承载一个独立的场景或信息单元,格子之间有明显的分隔边界,整体看起来像一张由多张小图拼合而成的大图",
+  "在同一张图中混合使用多种内容载体形式,例如将真实照片、插画角色、产品图、文字说明、图表等不同类型的视觉元素组合排布在同一个版面内,形成图文混排的丰富视觉层次",
+  "以统一的视觉主角(如同一个卡通角色、同一个人物、同一主题场景)贯穿多个分格画面,每个格子呈现该主角在不同场景或状态下的样子,配合文字说明形成系列感强的多格叙事版式",
+  "将多张图片按网格或分区方式拼贴成一张图,每个区域展示不同角度或不同场景,整体画面有清晰的分割感和节奏感",
+  "在同一画面中合理安排主体与背景的空间关系,让主体(人物、动物、物品)在画面中有明确的视觉焦点,背景简洁或有层次地衬托主体",
+  "生成包含多个独立小格子的图文排版画面,每个格子内有图片和文字说明,格子之间疏密有致、整齐排列,整体呈现信息图表或内容合集的视觉效果",
+  "生成人物局部特写画面,如放大呈现嘴巴咬食物、手持物品、耳朵佩戴饰品、鼻子、指甲等身体局部细节,画面填充感强,细节清晰可见",
+  "生成人物近景半身或胸部以上的画面,突出人物面部表情和情绪,背景适当虚化,让观看者能清楚看到人物的神态与互动感",
+  "生成产品或物品的极近距离特写图,如食物截面、商品细节、小物件放大展示,画面主体占满画幅,质感和纹理清晰突出",
+  "生成具有强烈透视纵深感的室内空间图,画面中窗框、拱门、地板线条等建筑元素形成明显的空间层次,光线从远处窗口射入,营造出由近到远的视觉延伸效果",
+  "生成采用夸张变形构图的图片,例如鱼眼镜头效果将人物或场景扭曲成球形全景、仰拍使近处物体极度放大而远处极度缩小,或通过搞怪角度让画面产生强烈的视觉冲击感",
+  "生成画面中存在嵌套框架效果的图片,如在沙漠场景中用一个悬空的矩形框将主体框住形成画中画,或利用水面倒影与实景上下对称形成嵌套镜像构图,制造超现实的空间突破感",
+  "生成具有强烈色彩对比的艺术插画,整体画面以高饱和度的红色与蓝色为主色调,两种颜色形成鲜明的冷暖对撞,背景大面积纯色铺底,视觉冲击力极强",
+  "在以暗色或单色为主的画面中,用局部的高饱和亮色(如红色心脏、橙色暖光窗口、金黄色星光)作为点睛之笔,让视线自然聚焦到这个色彩亮点上,形成强烈的视觉引导",
+  "生成整体色调统一、饱和度偏高的场景图,例如全画面笼罩在深蓝色夜光氛围或浓郁的赤红土地色调中,让单一主色调主导整个画面,营造出沉浸式的强烈色彩氛围感",
+  "生成具有强烈氛围感的插画风场景图,整体画面以深蓝色调为主,室内外场景都笼罩在宁静的夜色中,窗户透出暖黄色灯光形成冷暖对比,画面质感接近油画或数字绘画风格,传达出静谧、沉思、略带忧郁的情绪氛围",
+  "制作色彩鲜艳、视觉冲击力强的宣传海报,背景使用渐变色块(蓝紫、橙红等高饱和度色彩),搭配几何抽象图形装饰,文字排版醒目大气,整体呈现出热烈、充满活力的欢庆氛围",
+  "生成暖色调的室内空间效果图,以米白、浅棕、焦糖色为主色调,光线柔和自然,空间布置温馨舒适,整体画面传达出放松、治愈、生活化的温暖氛围",
+  "生成具有强烈视觉冲击力的超现实主义风格图像:画面以大地色系(深红、赭石、深蓝)为主调,将白马、牛仔等元素置于极简的荒漠/盐湖场景中,营造出孤寂、神秘、如油画般的电影感氛围",
+  "生成3D卡通风格的拟人化动物角色,角色具有毛绒质感和丰富的表情神态,能够在不同生活场景(办公室、卧室、户外)中呈现出喜怒哀乐等情绪状态,整体风格类似皮克斯动画",
+  "制作融合插画风格的信息图文海报:以卡通机器人/科技感插图作为视觉主体,搭配醒目的彩色标题文字和数据图表,整体呈现出活泼又专业的视觉效果",
+  "生成一组多格拼贴图,每格展示同一人物在不同场景/状态下的夸张表情和肢体动作,配合幽默文字标注,整体呈现出戏剧化的情绪起伏效果(如一周心情变化、苦情崩溃、搞笑反应等)",
+  "将动物(如猫咪)与各种食物、道具进行创意合成,给动物添加配饰(帽子、假发、领带等)并嵌入食物场景中,搭配谐音梗或双关文字,制作出拟人化角色扮演的趣味表情包图片",
+  "给同一张猫咪照片批量添加不同职业的帽子、道具和配件(如厨师帽、安全帽、眼镜、画板等),让猫咪看起来像在扮演各种职业角色",
+  "将真实照片转换成具有统一色调风格的插画效果,整体呈现蓝紫色调的复古油画或动画风格,让风景场景看起来像艺术插图",
+  "制作图文排版信息图,将多张食材产品图片抠出后整齐排列在统一背景上,配合文字说明组合成内容丰富的科普海报",
+  "生成具有强烈光影对比的场景图,画面中光源明显(如阳光折射、水面反光、彩虹色光晕),暗部极深、亮部极亮,整体呈现出戏剧性的明暗反差和光线质感",
+  "生成带有明显颗粒感或纸张纹理的插画风格图片,画面整体像是印刷在粗糙介质上,物体表面有细腻的颗粒噪点或手工绘制的笔触肌理",
+  "生成室内场景时,能真实还原不同材质的质感细节,如木地板的纹路光泽、布艺沙发的绒毛感、大理石茶几的光滑反射、藤编家具的编织纹理等",
+  "在图片上叠加对话气泡或多行说明文字,用于讲述故事背景或补充情节说明,文字带有描边或阴影效果以确保在复杂背景上清晰可读",
+  "制作图文并茂的科普说明卡片,每张卡片包含标题、编号、插图和详细文字说明,整体排版整齐统一,适合分步骤展示教程或知识点",
+  "在多图拼贴海报上为每个区域叠加带图标的标签(如勾选符号+地点名称),并在整体画面上方添加大标题和副标题文字,形成图文结合的内容合集展示效果",
+  "为多人物展示海报中的每个人物添加姓名和职位标签,并在画面顶部叠加活动主题、专场名称等层级分明的标题文字,整体风格统一、信息密度高",
+  "对同一场景或主体生成多个不同距离和景别的画面,包括远景展示整体环境、中景呈现主体与环境关系、近景突出细节,形成一组视角丰富的图片集合",
+  "生成采用非常规拍摄角度的图片,如从低角度仰拍、从高处俯视、或模拟第一人称主观视角看向场景,让画面产生独特的视觉冲击感",
+  "生成能展示宽广空间感的室内或室外全景图,画面中包含完整的环境纵深,让观看者感受到场景的整体规模和空间层次",
+  "生成具有统一色调风格的插画场景,整体画面使用高度协调的单一色系(如全蓝紫色调的火车风景、全粉紫色调的奇幻海洋),让画面中所有元素的颜色都偏向同一个色相,营造出梦幻沉浸的视觉氛围",
+  "生成色彩鲜艳、多色并置的视觉冲击画面,画面中同时出现多种高饱和度的颜色搭配(如复古拼贴风格中的粉色、蓝色、橙色并置,或彩色条纹波浪地形),让整体色彩浓烈饱满、视觉张力强烈",
+  "生成低饱和度或去色风格的极简画面,整体色彩纯度降低,呈现出克制、安静的视觉质感(如黑白灰调的海洋孤舟场景,或接近无彩色的素雅插画),与高饱和度画面形成鲜明对比",
+  "生成超现实浪漫场景图:将人物置于不可能存在的宏大环境中,如站在地球边缘俯瞰星空、坐在云端长椅上漂浮、在星海上骑行,画面充满梦幻感和史诗级视觉冲击力",
+  "制作科技感强烈的活动宣传海报:使用深色背景配合橙色、蓝色等高对比度霓虹色调,融合未来感城市或科技场景插图,搭配大号粗体标题文字,整体呈现出硬核、前沿的视觉气质",
+  "生成融合东方传统与现代简约的室内空间效果图:以米白、暖棕为主色调,加入拱形门洞、藤编元素、中式花卉装饰画等传统细节,整体呈现温润雅致的新中式生活美学氛围",
+  "生成拟人化动物角色表情包:用AI生成具有丰富表情和情绪的卡通动物形象(如毛茸茸的红色马、灰色驴),能够呈现出沮丧、无奈、委屈等多种情绪状态,配合不同场景背景(办公室、草地、室内),整体风格介于3D皮克斯动画和水彩插画之间,适合搭配幽默文案使用",
+  "制作图文混排的知识科普长图:以深青色/蓝绿色为底色背景,将心理学等知识内容拆分为多个板块,每个板块搭配风格统一的插画小图(奇幻风格人物、动物等),文字与插图穿插排布,整体呈现出版式清晰、视觉层次丰富的杂志风格科普图文效果",
+  "生成室内空间效果图:用AI渲染出具有温暖奶油色调的室内场景,包含拱形门洞、藤编家具、自然光影等元素,整体呈现出地中海或法式复古风格的高质感室内设计效果,光线柔和、色调统一,适合作为家居内容的视觉展示",
+  "生成具有强烈戏剧性光影对比的户外场景图,画面中光源方向明确(如侧光或逆光),亮部与暗部之间形成鲜明反差,阴影轮廓清晰,整体呈现出电影感或艺术摄影风格的视觉张力",
+  "生成室内暖光氛围图,画面中多个光源(吊灯、筒灯、窗外自然光)共同营造出温暖柔和的米色调空间,光线从不同方向照射,形成层次丰富的软阴影,整体氛围温馨舒适",
+  "生成充满魔幻或超现实感的彩色光效场景,画面中有多种颜色的光线(如橙、蓝、紫等)交织流动,光源本身成为视觉焦点,整体营造出梦幻、神秘或节日感的强烈氛围",
+  "制作大字号标题搭配正文内容的图文排版,标题文字极大且颜色鲜艳(红色、黄色等高饱和色),与正文小字形成强烈的大小对比,整体版面信息密度高、视觉冲击力强",
+  "在图片上叠加大字幕文字,字体粗大醒目,常带有描边或阴影效果,文字直接覆盖在照片或场景图上,起到强调说明或搞笑点评的作用",
+  "制作多宫格拼贴式内容图,每个格子内有大标题文字突出显示核心信息(如价格、品类名),配合产品图或场景图,标题字号远大于说明文字,形成层次分明的视觉结构"
+]

+ 85 - 0
examples/process_research/fix_strategy_requirement.py

@@ -0,0 +1,85 @@
+import os
+import json
+import glob
+
+def main():
+    base_dir = r"c:\Users\11304\gitlab\cybertogether\Agent\examples\process_research\output"
+    
+    # 遍历 output 目录下的所有子文件夹
+    if not os.path.exists(base_dir):
+        print(f"Directory not found: {base_dir}")
+        return
+
+    fixed_count = 0
+    skip_count = 0
+
+    for item in os.listdir(base_dir):
+        sub_dir = os.path.join(base_dir, item)
+        if not os.path.isdir(sub_dir):
+            continue
+            
+        strategy_path = os.path.join(sub_dir, "strategy.json")
+        if not os.path.exists(strategy_path):
+            continue
+            
+        # 读取 strategy.json
+        try:
+            with open(strategy_path, "r", encoding="utf-8") as f:
+                strategy_data = json.load(f)
+        except Exception as e:
+            print(f"Error reading {strategy_path}: {e}")
+            continue
+
+        # 如果已经有了,跳过
+        if "requirement" in strategy_data:
+            print(f"[{item}] skip: already has requirement")
+            skip_count += 1
+            continue
+            
+        # 寻找对应的 case.json 提取 requirement
+        requirement_text = None
+        
+        # 1. 尝试直接找 case.json
+        case_path = os.path.join(sub_dir, "case.json")
+        if os.path.exists(case_path):
+            try:
+                with open(case_path, "r", encoding="utf-8") as f:
+                    case_data = json.load(f)
+                    requirement_text = case_data.get("requirement")
+            except Exception:
+                pass
+                
+        # 2. 如果没找到或 case.json 不存在,并且存在分散的 case_*.json
+        if not requirement_text:
+            case_files = glob.glob(os.path.join(sub_dir, "case_*.json"))
+            for cf in case_files:
+                try:
+                    with open(cf, "r", encoding="utf-8") as f:
+                        cf_data = json.load(f)
+                        if "requirement" in cf_data:
+                            requirement_text = cf_data["requirement"]
+                            break
+                except Exception:
+                    continue
+                    
+        if not requirement_text:
+            print(f"[{item}] ERROR: could not find 'requirement' in any case files")
+            continue
+            
+        # 更新 strategy.json
+        # 为了保证 requirement 在最开头,创建一个新的 dict
+        new_strategy_data = {"requirement": requirement_text}
+        new_strategy_data.update(strategy_data)
+        
+        try:
+            with open(strategy_path, "w", encoding="utf-8") as f:
+                json.dump(new_strategy_data, f, ensure_ascii=False, indent=4)
+            print(f"[{item}] fixed: added requirement successfully")
+            fixed_count += 1
+        except Exception as e:
+            print(f"[{item}] ERROR writing back strategy data: {e}")
+
+    print(f"\nSummary: Fixed {fixed_count} files, Skipped {skip_count} files.")
+
+if __name__ == "__main__":
+    main()

+ 83 - 0
examples/process_research/generate_strategy.py

@@ -0,0 +1,83 @@
+import asyncio
+import json
+import sys
+import os
+
+# 将 Agent 根目录加入 sys.path
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
+
+from agent.llm.qwen import qwen_llm_call
+
+async def main():
+    case_path = os.path.join(os.path.dirname(__file__), 'output', '04_parallel', 'case.json')
+    out_path = os.path.join(os.path.dirname(__file__), 'output', '04_parallel', 'strategy.json')
+    
+    if not os.path.exists(case_path):
+        print(f"Error: {case_path} does not exist.")
+        return
+        
+    print("Loading case.json...")
+    with open(case_path, 'r', encoding='utf-8') as f:
+        case_data = json.load(f)
+        
+    # 提取精简的提示词
+    prompt = f"""
+你是一个工序调研协调器。你面前已经有了 54 个子 Agent 刚刚从各大平台收集回来的调研案例(见下方 JSON)。
+这里的核心需求是:生成带有明显颗粒感或纸张纹理的插画风格图片。
+
+请分析这些案例并总结、提取、设计制作最终的打法策略,按照下方强制的 JSON 格式输出,不要包含 Markdown 代码块(如 ```json 等),纯粹返回合法的 JSON 字符串。
+
+强制输出格式:
+{{
+  "selected_strategy": {{
+    "name": "端到端策略命名(如:分层生成 + 颗粒噪点叠加 + FLUX局部精修)",
+    "source": "指明主要受到这批采集回来的哪些 case 的启发",
+    "workflow_outline": [
+      {{
+        "phase": "阶段1:底层结构与材质生成",
+        "description": "这个阶段要完成的子目标...",
+        "capabilities": [
+          {{
+            "capability_id": null,
+            "capability_name": "原子能力名称",
+            "is_new": true,
+            "suggested_tools": ["提取自Case的建议工具"],
+            "case_references": ["来源于哪个case的反馈"]
+          }}
+        ]
+      }}
+    ]
+  }}
+}}
+
+调研案例 JSON 内容如下:
+{json.dumps(case_data, ensure_ascii=False)}
+"""
+
+    print("Submitting to Qwen-Plus for strategy generation...")
+    messages = [{"role": "user", "content": prompt}]
+    
+    try:
+        response = await qwen_llm_call(messages, model="qwen-plus", temperature=0.3)
+        content = response.get("content", "").strip()
+        
+        # 清理多余的 Markdown 标记
+        if content.startswith("```json"):
+            content = content[7:]
+        elif content.startswith("```"):
+            content = content[3:]
+        if content.endswith("```"):
+            content = content[:-3]
+            
+        content = content.strip()
+        
+        with open(out_path, 'w', encoding='utf-8') as f:
+            f.write(content)
+            
+        print(f"\\n✅ Strategy successfully saved to: {out_path}")
+        
+    except Exception as e:
+        print(f"\\n❌ Generation failed: {str(e)}")
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 16 - 0
examples/process_research/presets.json

@@ -0,0 +1,16 @@
+{
+  "coordinator": {
+    "system_prompt_file": "prompts/coordinator.prompt",
+    "max_iterations": 800,
+    "temperature": 0.3,
+    "skills": ["planning", "research", "browser", "knowledge"],
+    "description": "主 Agent:需求分析 + 分发调研方向 + 汇总策略"
+  },
+  "researcher": {
+    "system_prompt_file": "prompts/researcher.prompt",
+    "max_iterations": 300,
+    "temperature": 0.3,
+    "skills": ["planning", "research", "browser"],
+    "description": "调研子 Agent:单方向搜索 + 步骤级能力提取"
+  }
+}

+ 223 - 0
examples/process_research/prompts/coordinator.prompt

@@ -0,0 +1,223 @@
+---
+temperature: 0.3
+---
+
+$system$
+
+## 角色
+
+你是一个工序调研协调器。你负责分析需求、规划调研方向、逐一委托调研子 Agent 执行搜索,最后汇总所有调研结果设计制作策略。
+
+---
+
+## 可用工具
+
+- `agent(task, agent_type)` — 委托调研子 Agent(单任务 = delegate 模式,子 Agent 有完整工具权限)
+- `read_file(path)` — 读取文件
+- `write_file(path, content)` — 写入文件
+- `capability_search(query, top_k=5)` — 检索现有原子能力(Phase 2 使用)
+- `capability_list(limit=50)` — 列出所有原子能力(Phase 2 使用)
+
+---
+
+## 执行流程
+
+### Phase 1:规划 + 分发调研任务
+
+**第一步:确定渠道搜索策略**
+
+你的目标是针对原始需求,全方位搜索实战工序案例。**不要把原始需求强行拆分成碎片化的子问题**(例如不要把“生成户外人物”拆成“光影”、“肤质”),需求始终是同一个完整的中心主题。
+
+你需要从以下可用渠道库中,仔细评估并挑选 **2~4 个最适合当前业务需求的渠道** 作为探索策略:
+- `youtube`:侧重英文、中文的长线大课、详细的工作流搭建视频。
+- `xhs` (小红书):侧重中文高质量的参数面板分享、避坑图文攻略。
+- `x` (Twitter):侧重海外最前沿的技术发布、大 V 的新工具尝鲜案例。
+- `bili` (B站):侧重中文详细长视频教程、二次元与游戏美术案例。
+- `zhihu` (知乎) / `gzh` (公众号):侧重深度的中文原理剖析与长图文拆解。
+
+**第二步:并行委托针对各垂直渠道的子 Agent**
+
+因为我们底层的并发引擎已经开启,你**必须在同一次回答中,使用数组发起并发调用**。请基于你挑选的渠道,组织你的 `agent` 工具调用,将各个渠道的独立任务存入 `task` 数组中,发起一次**并发分发**:
+
+```json
+{
+  "name": "agent",
+  "arguments": {
+    "task": [
+      "[RESEARCH TASK]\n\n渠道:YouTube\n目标:寻找长线完整工作流视频\n建议关键词:...\n...",
+      "[RESEARCH TASK]\n\n渠道:小红书\n目标:寻找参数面板、避坑图文攻略\n建议关键词:...\n...",
+      "[RESEARCH TASK]\n\n渠道:B站\n目标:寻找中文详细长视频教程\n建议关键词:...\n..."
+    ],
+    "agent_type": "researcher"
+  }
+}
+```
+
+**任务描述规范**(数组中每个 `[RESEARCH TASK]` 元素的负载要求):
+- 明确指出这个**相同的唯一核心需求**。
+- 按自己负责的专属渠道列出建议的英文/中文关键词。
+  > 🚨 **【工具约束红线】**:搜索词严禁出现任何具体的 AI 工具、平台或插件名(如 ControlNet...)。
+- 明确指出需要排除的干扰项。
+- 给出**独立隔离**的输出追加路径:`{output_dir}/case_渠道名.json`(例如 `case_youtube.json`、`case_xhs.json`。绝不允许所有任务指向同一个 `case.json` 以防并发写入冲突!)
+
+**重点声明**:请仅调用 **一次** `agent` 工具,将多条任务合并进同一个 `task` 数组(`List[str]`)。收到指令后,底层协程会自动创建相互隔离的并发容器处理每个请求,不仅避免了本地资源争抢,还能最大化检索效率。
+
+
+---
+
+### Phase 2:读取结果与设计制作策略
+
+子 Agent 完成任务并返回后:
+
+**第三步:聚总并整理各个 case 文件**
+
+由于我们开启了并发隔离写入,子 Agent 们将案例分布存在了各自独立的 JSON 文件中(如 `case_xhs.json`)。你必须:
+```
+1. glob_files("{output_dir}/case_*.json") 查出所有产出的结果文件。
+2. 挨个使用 read_file("文件路径") 读取并加载进你的上下文。
+3. 如果所有文件内容完整,你可用 write_file 将它们手动合并输出到全局统一的 {output_dir}/case.json 留存:
+{
+  "requirement": "原始需求",
+  "generated_at": "ISO 8601",
+  "cases": [ ...所有渠道的合并集合... ]
+}
+```
+
+**第四步:设计最终交付级生产策略**
+
+你现在需要作为总架构师,基于收集到的这几十个成功与失败的真实案例,拍板决定一条**唯一的最优端到端制作流派(Strategy)**。
+你不需要平行列举多个等价的策略,你需要给出**最终推荐的生产级解决方案**(可以糅合不同案例的亮点,比如前置生成用A方案,精修用B方案)。
+
+策略提炼必须包含以下深度思考(写入 strategy.json):
+1. **统一标准策略阵列(strategies)**:
+   不要区分 selected_strategy 和 vs_alternatives 对象!请将所有可行的路线(首推流派与所有备选流派)以统一的 JSON 数据结构放入一个 `strategies` 数组中。
+   对于数组中的每一套流派方案,你必须:
+   - **标识优先级**:通过 `is_selected: true` 标记其中**唯一一条**你最推荐的、作为主线落地的最优流水线。其余作为备用的方案标记为 `is_selected: false`。
+   - **全流程拆解**:**每一套**方案(无论是首推还是备选)都必须是完整的端到端结构化工序!必须详细写满从头到尾的 `workflow_outline`(阶段1、阶段2... 直至交付的最终阶段),🚨 **严禁在备选方案里偷工减料只写一个阶段!**
+   - **原子能力要求**:每一个阶段必须明确依托于一个或多个**原子能力**。用 `capability_search(意图描述)` 匹配现有能力库;若库中没有(即 `is_new: true`),请**暂定该新能力的名称**,并从 case 中总结出**建议使用的工具/节点组合(suggested_tools)**。
+   - **保障性分析**:不管是主线还是备线,都要写明它的 `highlight_coverage` 和 `baseline_coverage`。
+   - **拍板/淘汰原因**:如果是首选方案,请写明拍板原因(`reasoning`);如果是备选方案,请写明未被选为主推的短板(`why_not`)及未来的降级切换条件(`could_switch_if`)。
+2. **能力盲区说明(uncovered_requirements)**:
+   - 当前 AI 工具链确实无法做到的短板。
+
+能力抽取准则:
+请严格遵循上方“什么是正确的原子能力?”的规范,拒绝把低级参数当做能力。即使是暂定的新能力,也必须足够高维、面向泛化需求。
+
+**第五步:输出最终策略**
+
+```
+write_file("{output_dir}/strategy.json", 最终拍板的打法策略)
+```
+
+---
+
+## 输出文件格式
+
+### `case.json`(合并后示例)
+```json
+{
+  "requirement": "原始需求",
+  "generated_at": "ISO 8601",
+  "cases": [
+    {
+      "id": "case_001",
+      "title": "案例标题",
+      "platform": "xhs",
+      "source_url": "https://...",
+      "metrics": {"likes": 100, "comments": 20},
+      "user_feedback": "用户评论如是说...",
+      "images": ["https://img_url"],
+      "input_details": "输入图片与提示词",
+      "output_details": "输出效果图评价",
+      "workflow_process": "详实的步骤记录与原理参数说明"
+    }
+  ]
+}
+```
+
+### `strategy.json`
+```json
+{
+  "requirement": "原始需求的完整描述(原样复制即可)",
+  "strategies": [
+    {
+      "is_selected": true,
+      "name": "首推策略命名(如:分层生成 + MJ底层主力 + FLUX局部精修)",
+      "source": "指明主要受到这批采集回来的哪些 case (如 case_001 / case_005)或者反馈的直接启发",
+      "workflow_outline": [
+        {
+          "phase": "阶段1:底层结构生成",
+          "description": "这个阶段要完成的子目标...",
+          "capabilities": [
+            {
+              "capability_id": "库中已有ID(若无则填 null)",
+              "capability_name": "原子能力名称(如:保持角色特征一致性)",
+              "is_new": true,
+              "suggested_tools": ["提取自Case的建议工具,如:IP-Adapter + 深度图"],
+              "case_references": ["来源于哪个case的反馈,比如:case_001 作者提到加上深度图能防崩"]
+            }
+          ]
+        },
+        {
+          "phase": "阶段2:高光特效处理",
+          "description": "(注意:每套方案都必须写满完整的从头到尾的各个阶段,绝不可省略)",
+          "capabilities": [
+            ...
+          ]
+        }
+      ],
+      "highlight_coverage": [
+        "原始需求中的高光特色1 — 靠由于阶段X的某个操作保障"
+      ],
+      "baseline_coverage": [
+        "基本的人体比例防崩 — 靠阶段Y保障"
+      ],
+      "reasoning": "评判综合为什么最终拍板了这套打法。比如结合了 A 路线的可控性强和 B 路线的高水准特征...",
+      "why_not": null,
+      "could_switch_if": null
+    },
+    {
+      "is_selected": false,
+      "name": "备选流派一(如:全程依赖纯重度连线的 ComfyUI 工作流)",
+      "source": "指明主要受到哪些 case 启发",
+      "workflow_outline": [
+        {
+          "phase": "阶段1:...",
+          ...同首选方案的高度全流程详实度...
+        },
+        {
+          "phase": "阶段2:...",
+          ...同首选方案的高度全流程详实度...
+        }
+      ],
+      "highlight_coverage": [],
+      "baseline_coverage": [],
+      "reasoning": null,
+      "why_not": "指出为什么不作为主推:比如查到的案例中评论区反映太吃显存或容易崩溃",
+      "could_switch_if": "在什么情况下需要切换:比如,如果硬件算力突破限制或客户要求纯本地执行,可切换此路线"
+    }
+  ],
+  "uncovered_requirements": [
+    "目前技术局限导致的短板:比如物理级别的光影反射当前所有案例均需后期纯手绘补充"
+  ]
+}
+```
+
+---
+
+## 重要约束
+
+1. Phase 1 的子 Agent 任务必须合并在**同一次 `agent` 调用中**,通过 `task` 数组(Array)一次性并发提交。
+2. 在 `strategy.json` 中,不可为了拼凑阶段而凭空发明不可靠的操作,每个关键阶段都需能从 Case 依据或者你匹配查询到的原子库能力中获得有效支撑。
+
+$user$
+
+请开始工作。
+
+**原始需求**:%requirement%
+
+**输出根目录**:%output_dir%
+
+最终输出:
+- `%output_dir%/case.json`(子 Agent 原汁原味沉淀的全量实战案例与带溯源图片的素材库)
+- `%output_dir%/strategy.json`(基于高维原子能力的终局打法矩阵)

+ 118 - 0
examples/process_research/prompts/researcher.prompt

@@ -0,0 +1,118 @@
+---
+temperature: 0.3
+---
+
+$system$
+
+你是一个工序调研统筹子 Agent。你会收到**多个调研方向**的清单,你需要**自主设定 goal(每个方向一个 goal)**,为各个方向覆盖适当的渠道,广泛搜索相关工序案例,并统一将结果结构化追加到指定的同一个文件中。
+
+**你的边界**:全权负责本需求下的所有资料搜集,按要求存入 JSON 文件。不负责最终的策略设计。
+
+---
+
+## 可用工具
+
+### 搜索工具
+- `content_platforms(platform="")` — 列出/查询平台详细搜索参数
+- `content_search(platform, keyword, max_count=20)` — 跨平台搜索案例(返回结果和序列号)
+  - platform 常用值: `xhs`(小红书), `youtube`, `x`(Twitter), `bili`, `gzh`, `zhihu`
+- `content_detail(platform, index)` — 根据 content_search 结果的序号查看详细内容和全文
+- `content_suggest(platform, keyword)` — 获取搜索相关建议词
+- `browser-use` — 真实浏览器交互(仅用于处理 content_*. 系列工具无法覆盖的特殊场景)
+
+### 文件工具
+- `read_file(path)` — 读取文件(追加前必须先读)
+- `write_file(path, content)` — 写入/覆盖文件
+
+---
+
+## 执行流程
+
+### 第一步:理解需求与方向拆解
+
+你会从任务中收到多个方向清单(通常包含侧重渠道和推荐词)。
+**强烈建议**:为你接到的每一个“方向”都调用一次 `goal` 工具设立独立的调研目标任务,这样你能更有条理地进行多渠道的深度挖掘,防止混乱。
+
+### 第二步:逐一方向广泛搜索
+
+对每一个设定的目标方向:
+1. 分析它推荐的平台(如小红书、YouTube等)
+2. > 🚨 **【工具调用最高红线】**:**严禁使用 `bash_command` 或自己编写任何 Python 脚本/爬虫去抓取网页!**你必须、且只能使用系统提供的 `content_search` 和 `content_detail` 这个现成的 API 工具去完成搜索!如果你尝试自己写脚本,任务将直接判定失败。
+3. > 🚨 **【搜索词红线警告】**:在调用 `content_search` 时,**严禁夹带任何具体的 AI 工具名或插件名**(如 ControlNet...)。你只能使用朴素业务语言。
+4. 通过 `content_search` 进行搜索,并通过 `content_detail` 仔细阅读高赞的方案。
+
+接口失败处理:尝试更换关键词 → 重试 2 次 → 放弃。
+
+
+
+### 第三步:萃取原汁原味的案例信息
+
+对每个有价值的工序 case,请放弃生硬的字段拆解,**最大程度为您保留它原始的、有血有肉的内容与上下文**。
+重点围绕以下几个版块提取信息:
+
+1. **输入与输出效果(Input & Output)**:
+   - 作者到底投喂了什么(何种参考图、提示词模板、特定的尺寸/控制参数)?
+   - 最终产出了什么?
+   - **核心要求**:如果有效果图的 URL 或节点连线图的链接,**必须保存原始的图片链接**(以 URL 形式或 Markdown `![截图](URL)` 存入)。
+
+2. **操作过程记录(Raw Workflow)**:
+   - 详实记录作者在原文中所表述的工具链搭配、核心节点名称、关键参数调优(尽量保留作者的原始术语和经验总结)。
+
+3. **来源信源与反馈评估(Source Evaluation)**:
+   - 记录点赞数、评论数、收藏数。
+   - **非常重要**:摘录 1~3 条核心的用户评论反馈(如果有)。比如“跑出来发灰”、“这个节点早就淘汰了”、“太耗显存,推荐平替版”,这最能反映该方案的真实可用性。
+
+### 第四步:独立存储结果文件
+
+🚨 **【并发写文件安全红线】**:你有多个分身正在其他进程中同时执行任务!
+因此你**绝对不能向一个公共的 JSON 文件写入!** 
+你必须根据接受任务时 Coordinator 给你分配的专属 `output_file`(形如 `{output_dir}/case_xhs.json` 或 `case_youtube.json`),将查到的 case 独立持久化。
+
+**每次收集到 2~3 个 case 后,应立即持久化一次**:
+```
+read_file("{output_file}")   → 若文件不存在则初始化 {"requirement": "总体需求描述", "cases": []}
+将新 case 追加进 cases 数组
+write_file("{output_file}", 更新后的完整 JSON)
+```
+
+---
+
+## 输出格式
+
+写入到任务中指定的 `{output_file}`:
+
+```json
+{
+  "requirement": "本次的需求名称/描述",
+  "searched_at": "ISO 8601 时间戳",
+  "cases": [
+    {
+      "id": "调研时自动编号,如 case_001",
+      "title": "案例标题",
+      "platform": "xhs | youtube | x | zhihu | bili | gzh",
+      "source_url": "https://...",
+      "metrics": {
+        "likes": 12400,
+        "comments": 480
+      },
+      "user_feedback": "(如有)摘录关键的用户评论反馈,如'效果不错但是很吃显存','提示词太长跑不出来'",
+      "images": ["https://图片链接1", "https://图片链接2"],
+      "input_details": "详细描述作者输入的参考图类型、Prompt 等",
+      "output_details": "详细描述最终呈现的效果,以及作者对效果的评价",
+      "workflow_process": "用一段结构化的文字或短数组,保留原汁原味的关键步骤梳理、核心控制节点及参数(不需强制格式化为复杂 json 对象,只需清晰可读)"
+    }
+  ]
+}
+```
+
+---
+
+## 重要约束
+
+1. **原汁原味**:重点抓取用户的输入输出配置和原文中的图片链接,不要弄丢上下文!
+2. **重反馈**:发掘评论区里的避坑指南和赞誉,作为判断策略价值的锚点。
+3. **按时持久化**:每 3~5 个 case 写一次文件,防止中途丢数据。
+
+$user$
+
+%task%

+ 0 - 0
examples/process/requirements.json → examples/process_research/requirements.json


+ 408 - 0
examples/process_research/run.py

@@ -0,0 +1,408 @@
+"""
+Process Research Pipeline v3:单 Agent 完成搜索 + 能力提取 + 策略归纳
+
+每个需求输出到 output/{N}/:
+  - case.json     工序案例(含步骤级能力详情)
+  - strategy.json 策略 × case 关联索引
+  - process.json  策略 × 能力流水线详情
+
+用法:
+  python run.py                        # 跑全部需求
+  python run.py --from 2               # 从第3个需求续跑
+  python run.py --requirements req.json  # 指定需求文件
+  python run.py --remote               # 强制远端模式(覆盖 config.USE_REMOTE_RESEARCH)
+  python run.py --local                # 强制本地模式
+
+环境变量:
+  KNOWHUB_API  线上 KnowHub 地址(默认 http://localhost:9999)
+"""
+
+import argparse
+import json
+import os
+import sys
+import asyncio
+from datetime import datetime
+from pathlib import Path
+
+
+sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+from dotenv import load_dotenv
+load_dotenv()
+
+from agent.llm.prompts import SimplePrompt
+from agent.core.runner import AgentRunner, RunConfig
+from agent.trace import FileSystemTraceStore, Trace, Message
+from agent.llm import create_qwen_llm_call
+from agent.cli import InteractiveController
+from agent.utils import setup_logging
+from agent.tools.builtin.subagent import _run_remote_agent
+from agent.tools.builtin.browser.baseClass import init_browser_session, kill_browser_session
+
+from config import (
+    COORDINATOR_RUN_CONFIG,
+    OUTPUT_DIR,
+    SKILLS_DIR, TRACE_STORE_PATH, DEBUG, LOG_LEVEL, LOG_FILE,
+    BROWSER_TYPE, HEADLESS,
+    IM_ENABLED, IM_CONTACT_ID, IM_SERVER_URL, IM_WINDOW_MODE, IM_NOTIFY_INTERVAL,
+    USE_REMOTE_RESEARCH,
+)
+
+
+
+# ─────────────────────────────────────────────
+# 本地模式:单需求执行
+# ─────────────────────────────────────────────
+
+async def run_single_local(
+    runner: AgentRunner,
+    interactive: InteractiveController,
+    store: FileSystemTraceStore,
+    prompt: SimplePrompt,
+    requirement: str,
+    output_dir: Path,
+    req_index: int,
+) -> tuple[str, bool]:
+    """本地 AgentRunner 模式执行单个需求,返回 (最终回复, 是否应退出)。"""
+
+    output_dir.mkdir(parents=True, exist_ok=True)
+
+    messages = prompt.build_messages(
+        requirement=requirement,
+        output_dir=str(output_dir),
+    )
+
+    prompt_model = prompt.config.get("model", None)
+    run_config = RunConfig(
+        model=prompt_model or COORDINATOR_RUN_CONFIG.model,
+        temperature=COORDINATOR_RUN_CONFIG.temperature,
+        max_iterations=COORDINATOR_RUN_CONFIG.max_iterations,
+        extra_llm_params=COORDINATOR_RUN_CONFIG.extra_llm_params,
+        agent_type=COORDINATOR_RUN_CONFIG.agent_type,
+        name=f"工序调研:需求{req_index+1:03d}",
+        knowledge=COORDINATOR_RUN_CONFIG.knowledge,
+    )
+
+    print(f"\n{'=' * 60}")
+    print(f"[{req_index+1:03d}] 开始调研 【本地模式】")
+    print(f"需求:{requirement[:80]}{'...' if len(requirement) > 80 else ''}")
+    print(f"输出:{output_dir}")
+    print(f"{'=' * 60}")
+
+    current_trace_id = None
+    current_sequence = 0
+    final_response = ""
+    should_exit = False
+
+    try:
+        async for item in runner.run(messages=messages, config=run_config):
+            cmd = interactive.check_stdin()
+            if cmd == 'pause':
+                print("\n⏸️ 正在暂停执行...")
+                if current_trace_id:
+                    await runner.stop(current_trace_id)
+                await asyncio.sleep(0.5)
+                menu_result = await interactive.show_menu(current_trace_id, current_sequence)
+                if menu_result["action"] == "stop":
+                    should_exit = True
+                    break
+                elif menu_result["action"] == "continue":
+                    new_messages = menu_result.get("messages", [])
+                    run_config.after_sequence = menu_result.get("after_sequence")
+                    if new_messages:
+                        messages = new_messages
+                    break
+            elif cmd == 'quit':
+                print("\n🛑 用户请求停止...")
+                if current_trace_id:
+                    await runner.stop(current_trace_id)
+                should_exit = True
+                break
+
+            if isinstance(item, Trace):
+                current_trace_id = item.trace_id
+                if item.status == "running":
+                    print(f"[Trace] 开始: {item.trace_id[:8]}...")
+                elif item.status == "completed":
+                    print(f"\n[Trace] ✅ 完成  messages={item.total_messages}  cost=${item.total_cost:.4f}")
+                elif item.status == "failed":
+                    print(f"\n[Trace] ❌ 失败: {item.error_message}")
+                elif item.status == "stopped":
+                    print(f"\n[Trace] ⏸️ 已停止")
+
+            elif isinstance(item, Message):
+                current_sequence = item.sequence
+                if item.role == "assistant":
+                    content = item.content
+                    if isinstance(content, dict):
+                        text = content.get("text", "")
+                        tool_calls = content.get("tool_calls")
+                        if text and not tool_calls:
+                            final_response = text
+                            print(f"\n[Response] Agent 回复:")
+                            print(text)
+                        elif text:
+                            preview = text[:150] + "..." if len(text) > 150 else text
+                            print(f"[Assistant] {preview}")
+                elif item.role == "tool":
+                    content = item.content
+                    tool_name = "unknown"
+                    if isinstance(content, dict):
+                        tool_name = content.get("tool_name", "unknown")
+                    if item.description and item.description != tool_name:
+                        desc = item.description[:80]
+                        print(f"[Tool] ✅ {tool_name}: {desc}...")
+                    else:
+                        print(f"[Tool] ✅ {tool_name}")
+
+    except Exception as e:
+        print(f"\n执行出错: {e}")
+        import traceback
+        traceback.print_exc()
+
+    if current_trace_id:
+        print(f"  Trace ID: {current_trace_id}")
+
+    return final_response, should_exit
+
+
+# ─────────────────────────────────────────────
+# 远端模式:单需求执行
+# ─────────────────────────────────────────────
+
+async def run_single_remote(
+    requirement: str,
+    output_dir: Path,
+    req_index: int,
+) -> tuple[str, bool]:
+    """HTTP 调用线上 KnowHub remote_research agent,返回 (摘要, False)。"""
+
+    output_dir.mkdir(parents=True, exist_ok=True)
+
+    print(f"\n{'=' * 60}")
+    print(f"[{req_index+1:03d}] 开始调研 【远端模式 → KnowHub remote_research】")
+    print(f"需求:{requirement[:80]}{'...' if len(requirement) > 80 else ''}")
+    print(f"输出:{output_dir}")
+    print(f"{'=' * 60}")
+
+    result = await _run_remote_agent(
+        agent_type="remote_research",
+        task=requirement,
+        messages=None,
+        continue_from=None,
+        skills=None,
+    )
+
+    status = result.get("status", "unknown")
+    summary = result.get("summary", "")
+    error = result.get("error")
+    stats = result.get("stats", {})
+
+    if status == "completed":
+        print(f"\n[Remote] ✅ 完成  tokens={stats.get('total_tokens', 0)}  cost=${stats.get('total_cost', 0.0):.4f}")
+    else:
+        print(f"\n[Remote] ❌ 失败: {error}")
+
+    if result.get("sub_trace_id"):
+        print(f"  Remote Trace ID: {result['sub_trace_id']}")
+
+    return summary or "", False
+
+
+# ─────────────────────────────────────────────
+# Main
+# ─────────────────────────────────────────────
+
+async def main():
+    parser = argparse.ArgumentParser(description="Process Research Pipeline v3")
+    parser.add_argument(
+        "--from", dest="from_index", type=int, default=0,
+        help="从第几个需求开始(0-based)",
+    )
+    parser.add_argument(
+        "--only", dest="only_index", type=int, default=None,
+        help="只执行指定的第几个需求(0-based),这会覆盖 --from",
+    )
+    parser.add_argument(
+        "--requirements", type=str, default=None,
+        help="需求列表 JSON 文件路径(默认 db_requirements.json)",
+    )
+    parser.add_argument(
+        "--remote", action="store_true",
+        help="强制使用远端模式(覆盖 config.USE_REMOTE_RESEARCH)",
+    )
+    parser.add_argument(
+        "--local", action="store_true",
+        help="强制使用本地模式(覆盖 config.USE_REMOTE_RESEARCH)",
+    )
+    parser.add_argument(
+        "--parallel", action="store_true",
+        help="强制开启并发多浏览器机制(覆盖 config.PARALLEL_TOOL_EXECUTION)",
+    )
+    parser.add_argument(
+        "--sequential", action="store_true",
+        help="强制开启单步串行机制(覆盖 config.PARALLEL_TOOL_EXECUTION)",
+    )
+    args = parser.parse_args()
+
+    # 决定是否用远端
+    use_remote = USE_REMOTE_RESEARCH
+    if args.remote:
+        use_remote = True
+    if args.local:
+        use_remote = False
+
+    # 决定并发模式
+    is_parallel = COORDINATOR_RUN_CONFIG.parallel_tool_execution
+    if args.parallel:
+        is_parallel = True
+    if args.sequential:
+        is_parallel = False
+    COORDINATOR_RUN_CONFIG.parallel_tool_execution = is_parallel
+    
+    # 根据并发模式覆盖浏览器类型(并发必须用云浏览器防止冲突)
+    browser_type = BROWSER_TYPE
+    if is_parallel:
+        browser_type = "cloud"
+    elif args.sequential:
+        browser_type = "local" # 串行可以选择回本地
+
+    base_dir = Path(__file__).parent
+    project_root = base_dir.parent.parent
+    output_root = project_root / OUTPUT_DIR
+
+    setup_logging(level=LOG_LEVEL, file=LOG_FILE)
+
+    # 加载 presets
+    presets_path = base_dir / "presets.json"
+    if presets_path.exists():
+        from agent.core.presets import load_presets_from_json
+        load_presets_from_json(str(presets_path))
+        print("已加载 presets")
+
+    # 读取需求
+    req_path = Path(args.requirements) if args.requirements else base_dir / "db_requirements.json"
+    if not req_path.exists():
+        print(f"错误: 需求文件不存在: {req_path}")
+        sys.exit(1)
+    with open(req_path, encoding='utf-8') as f:
+        requirements = json.load(f)
+    if not isinstance(requirements, list) or len(requirements) == 0:
+        print("错误: 需求文件必须是非空 JSON 数组")
+        sys.exit(1)
+
+    output_root.mkdir(parents=True, exist_ok=True)
+    store = FileSystemTraceStore(base_path=TRACE_STORE_PATH)
+    total = len(requirements)
+    start = args.from_index
+
+    print("=" * 60)
+    print(f"Process Research Pipeline v3")
+    print(f"执行引擎:{'并发多云并发 (Parallel)' if is_parallel else '单步串行序列 (Sequential)'}")
+    print(f"模式:{'远端 KnowHub' if use_remote else '本地'}")
+    if args.only_index is not None:
+        print(f"模式:仅执行第 {args.only_index} 个需求")
+    else:
+        print(f"共 {total} 个需求,从第 {start} 个开始")
+    print("=" * 60)
+
+    # IM 初始化(可选)
+    if IM_ENABLED and not use_remote:
+        from agent.tools.builtin.im.chat import im_setup, im_open_window
+        result = await im_setup(
+            contact_id=IM_CONTACT_ID,
+            server_url=IM_SERVER_URL,
+            notify_interval=IM_NOTIFY_INTERVAL,
+        )
+        print(f"IM: {result.output}")
+        if IM_WINDOW_MODE:
+            window_result = await im_open_window(contact_id=IM_CONTACT_ID)
+            print(f"IM: {window_result.output}")
+
+    # 初始化本地浏览器
+    if not use_remote:
+        print(f"正在初始化浏览器环境 ({browser_type})...")
+        await init_browser_session(
+            browser_type=browser_type,
+            headless=HEADLESS,
+            url="https://www.google.com/",
+            profile_name=""
+        )
+
+    # 本地模式:初始化 runner
+    runner = None
+    interactive = None
+    prompt = None
+    if not use_remote:
+        prompt_path = base_dir / "prompts" / "coordinator.prompt"
+        prompt = SimplePrompt(prompt_path)
+        prompt_model = prompt.config.get("model", None) or COORDINATOR_RUN_CONFIG.model
+        runner = AgentRunner(
+            trace_store=store,
+            llm_call=create_qwen_llm_call(model=prompt_model),
+            skills_dir=SKILLS_DIR,
+            debug=DEBUG,
+        )
+        interactive = InteractiveController(runner=runner, store=store, enable_stdin_check=True)
+        runner.stdin_check = interactive.check_stdin
+        print("💡 输入 'p' 暂停,'q' 退出")
+        print("=" * 60)
+
+    completed = 0
+    try:
+        for i, requirement in enumerate(requirements):
+            if args.only_index is not None:
+                if i != args.only_index:
+                    continue
+            else:
+                if i < start:
+                    continue
+
+            # 为防止两个模式同时跑起冲突,动态附加模式后缀
+            mode_suffix = "_parallel" if is_parallel else "_sequential"
+            req_output_dir = output_root / f"{(i+1):03d}{mode_suffix}"
+
+            if use_remote:
+                _, _ = await run_single_remote(
+                    requirement=requirement,
+                    output_dir=req_output_dir,
+                    req_index=i,
+                )
+            else:
+                _, should_exit = await run_single_local(
+                    runner=runner,
+                    interactive=interactive,
+                    store=store,
+                    prompt=prompt,
+                    requirement=requirement,
+                    output_dir=req_output_dir,
+                    req_index=i,
+                )
+                if should_exit:
+                    print(f"\n🛑 用户中止,已完成 {completed}/{total - start} 个需求")
+                    break
+
+            completed += 1
+            print(f"\n✓ [{(i+1):03d}] 完成,输出:{req_output_dir}")
+            print(f"  - case.json")
+            print(f"  - strategy.json")
+
+    except KeyboardInterrupt:
+        print(f"\n\n用户中断 (Ctrl+C),已完成 {completed}/{total - start} 个需求")
+    finally:
+        if not use_remote:
+            try:
+                await kill_browser_session()
+            except Exception:
+                pass
+
+    print()
+    print("=" * 60)
+    print(f"完成:{completed}/{total - start} 个需求")
+    print(f"输出根目录:{output_root}")
+    print("=" * 60)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 208 - 63
knowhub/frontend/src/pages/Workflows.tsx

@@ -1,13 +1,15 @@
 import { useState, useEffect } from 'react';
 import type { FormEvent } from 'react';
-import { Layers, Cpu, CheckCircle2, Target, Search, Activity } from 'lucide-react';
-import { getStrategies, getCapabilities } from '../services/api';
+import { Layers, Cpu, CheckCircle2, Target, Search, Activity, ChevronDown, ChevronRight } from 'lucide-react';
+import { getStrategies, getCapabilities, getTools, getRequirements } from '../services/api';
 import { StatCard } from '../components/common/StatCard';
 import { StatusBadge } from '../components/common/EntityTag';
 
 interface WorkflowStep {
   title?: string;
   description: string;
+  input?: string;
+  tools?: string[];
   capabilities: any[];
 }
 
@@ -44,14 +46,27 @@ function parseSteps(body: string | any[] | object): WorkflowStep[] {
       const parsedCaps = capsArray.map((c: any) => typeof c === 'string' ? { id: c } : c).filter((c: any) => c && (c.capability_id || c.id || c.name || c.capability_name));
       
       let title = phaseObj.phase || phaseObj.name || phaseObj.title || phaseObj.step || phaseObj.module_label || '';
-      let description = phaseObj.description || phaseObj.desc || phaseObj.details || (typeof phase === 'string' ? phase : JSON.stringify(phase));
+      let description = phaseObj.description || phaseObj.desc || phaseObj.details || '';
+      let inputVal = phaseObj.input || phaseObj.inputs || phaseObj.output || '';
+      
+      // If phase is string and title is not set, meaning fallback
+      if (typeof phase === 'string' && !title && !description) {
+         description = phase;
+      }
+      // If we don't have a separated description but we do have a title (phase), that's fine.
+      
+      let toolsRaw = phaseObj.tools || phaseObj.tool || [];
+      const parsedTools = Array.isArray(toolsRaw) ? toolsRaw : (typeof toolsRaw === 'string' ? [toolsRaw] : []);
 
       if (typeof title === 'object') title = JSON.stringify(title);
       if (typeof description === 'object') description = JSON.stringify(description);
+      if (typeof inputVal === 'object') inputVal = JSON.stringify(inputVal);
 
       return {
         title: String(title),
         description: String(description),
+        input: String(inputVal),
+        tools: parsedTools,
         capabilities: parsedCaps
       };
     });
@@ -81,6 +96,8 @@ export function Workflows() {
   
   const [strategies, setStrategies] = useState<any[]>(() => getCache()?.strategies || []);
   const [capabilities, setCapabilities] = useState<Record<string, any>>(() => getCache()?.capabilities || {});
+  const [allToolsMap, setAllToolsMap] = useState<Record<string, any>>(() => getCache()?.allToolsMap || {});
+  const [allReqsMap, setAllReqsMap] = useState<Record<string, any>>(() => getCache()?.allReqsMap || {});
   const [isLoading, setIsLoading] = useState(() => !(getCache()?.strategies?.length > 0));
   const [isLoadingMore, setIsLoadingMore] = useState(false);
   const [offset, setOffset] = useState(0);
@@ -88,6 +105,8 @@ export function Workflows() {
   const [totalCount, setTotalCount] = useState<number>(() => getCache()?.totalCount || 0);
   const [activeCount, setActiveCount] = useState<number>(() => getCache()?.activeCount || 0);
   const [searchQuery, setSearchQuery] = useState("");
+  const [expandedReqs, setExpandedReqs] = useState<Record<string, boolean>>({});
+  const [expandedSteps, setExpandedSteps] = useState<Record<string, boolean>>({});
   const LIMIT = 50;
   
   const loadStrategies = (currentOffset: number, isInit = false) => {
@@ -121,13 +140,26 @@ export function Workflows() {
 
   useEffect(() => {
     setIsLoading(true);
-    getCapabilities(1000).then(capRes => {
+    Promise.all([getCapabilities(1000), getTools(1000), getRequirements(1000)]).then(([capRes, toolRes, reqRes]) => {
       const capMap: Record<string, any> = {};
       (capRes.capabilities || capRes.results || (Array.isArray(capRes) ? capRes : [])).forEach((c: any) => {
         capMap[c.capability_id || c.id || c.capability_name || c.name] = c;
       });
       setCapabilities(capMap);
-      saveCache({ capabilities: capMap });
+
+      const tMap: Record<string, any> = {};
+      (toolRes.tools || toolRes.results || (Array.isArray(toolRes) ? toolRes : [])).forEach((t: any) => {
+        tMap[t.id] = t;
+      });
+      setAllToolsMap(tMap);
+
+      const rMap: Record<string, any> = {};
+      (reqRes.requirements || reqRes.results || (Array.isArray(reqRes) ? reqRes : [])).forEach((r: any) => {
+        rMap[r.req_id || r.id] = r;
+      });
+      setAllReqsMap(rMap);
+
+      saveCache({ capabilities: capMap, allToolsMap: tMap, allReqsMap: rMap });
       loadStrategies(0, true);
       
       // Calculate active count without downloading all bodies
@@ -212,72 +244,185 @@ export function Workflows() {
             const stratCaps = strategy.capability_ids || [];
 
           return (
-            <div key={strategy.id} className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden hover:border-indigo-300 transition-colors duration-300">
-              <div className="p-6 border-b border-slate-100 flex flex-col md:flex-row justify-between items-start gap-4 bg-slate-50/50">
-                <div>
-                   <div className="flex items-center gap-3 mb-2">
-                     <h2 className="text-lg font-black text-slate-900">{strategy.name || '未命名工序'}</h2>
-                     <span className="text-[11px] font-mono text-slate-400 bg-white px-2 py-0.5 rounded border border-slate-100">ID: {strategy.id}</span>
+            <div key={strategy.id} className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden mb-6">
+              <div className="px-5 py-4 border-b border-slate-100 flex flex-col gap-3 bg-slate-50">
+                 <div className="flex items-center justify-between">
+                   <div className="flex items-center gap-3">
+                     <span className="text-sm font-bold text-slate-500 font-mono select-all">{strategy.id}</span>
+                     <h2 className="text-[16px] font-bold text-slate-900">{strategy.name || '未命名工序'}</h2>
                    </div>
-                   <p className="text-sm text-slate-600 leading-relaxed max-w-3xl">
-                     {strategy.description || '无详细描述'}
-                   </p>
-                </div>
-              </div>
+                 </div>
+                 
+                 {strategy.description && (
+                   <p className="text-sm text-slate-600 leading-relaxed max-w-4xl">{strategy.description}</p>
+                 )}
+                     
+                     {(() => {
+                       let bodyObj: any = {};
+                       try {
+                         bodyObj = typeof strategy.body === 'string' ? JSON.parse(strategy.body || '{}') : (strategy.body || {});
+                       } catch { /* ignore */ }
+                       
+                       const evals = bodyObj.coverage_evaluations;
+                       const hasEvals = evals && Object.keys(evals).length > 0;
+                       
+                       return (
+                         <div className="mt-4 border border-slate-100 rounded-xl overflow-hidden bg-slate-50/50">
+                           <div className="px-4 py-2 bg-slate-100/50 border-b border-slate-100 text-[11px] font-bold text-slate-500 uppercase tracking-wider flex items-center gap-2">
+                              <Target size={14} className="text-indigo-400" />
+                              覆盖需求判定
+                           </div>
+                           <div className="divide-y divide-slate-100">
+                           {!hasEvals ? (
+                             <div className="p-4 text-sm text-slate-400 text-center italic">该工序暂无需求判定记录</div>
+                           ) : (
+                             Object.entries(evals).map(([reqId, val]: [string, any]) => {
+                             const score = val?.score ?? 0;
+                             let scoreColor = "text-rose-600";
+                             if (score >= 0.8) scoreColor = "text-emerald-600";
+                             else if (score >= 0.5) scoreColor = "text-amber-600";
+                             
+                             const explainText = val?.explanation || val?.explaination || val?.reason || '';
+                             const reqObj = allReqsMap[reqId];
+                             const reqDescription = reqObj?.description || reqObj?.subject || reqObj?.name || '暂无描述';
+                             const isExpanded = expandedReqs[`${strategy.id}-${reqId}`];
+                             
+                             return (
+                               <div key={reqId} className="flex flex-col bg-white hover:bg-slate-50/50 transition-colors">
+                                 <div 
+                                    className="flex flex-col md:flex-row gap-4 p-4 items-start cursor-pointer group"
+                                    onClick={(e) => {
+                                      e.stopPropagation();
+                                      const key = `${strategy.id}-${reqId}`;
+                                      setExpandedReqs(prev => ({ ...prev, [key]: !prev[key] }));
+                                    }}
+                                 >
+                                   <div className="w-6 shrink-0 flex items-center justify-center text-slate-300 group-hover:text-indigo-400 transition-colors pt-1">
+                                     {isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
+                                   </div>
+                                   <div className="w-[120px] shrink-0 pt-0.5">
+                                     <span className="inline-block px-2 py-1 bg-slate-100 text-slate-600 border border-slate-200 rounded text-[11px] font-bold font-mono truncate max-w-full" title={reqId}>
+                                       {reqId}
+                                     </span>
+                                   </div>
+                                   <div className="flex-1 min-w-[200px] pt-1">
+                                     <div className="text-[13px] font-medium text-slate-800 leading-relaxed whitespace-pre-wrap">
+                                       {reqDescription}
+                                     </div>
+                                   </div>
+                                   <div className={`w-[80px] shrink-0 text-right font-mono font-black text-[16px] pt-0.5 ${scoreColor}`}>
+                                     {Number(score).toFixed(2)}
+                                   </div>
+                                 </div>
+                                 {isExpanded && (
+                                   <div className="px-4 pb-4 md:pl-[11.5rem] animate-in fade-in slide-in-from-top-1">
+                                     <div className="p-3 bg-slate-50 rounded-lg border border-slate-100 text-[12px] text-slate-500 leading-relaxed whitespace-pre-wrap">
+                                       {explainText || <span className="italic text-slate-400">暂无判定说明</span>}
+                                     </div>
+                                   </div>
+                                 )}
+                               </div>
+                             );
+                           })
+                           )}
+                           </div>
+                         </div>
+                       );
+                     })()}
+                  </div>
 
-              <div className="p-6 bg-white">
-                <h3 className="font-black text-[13px] text-indigo-900 tracking-wide mb-4 flex items-center gap-2">
-                   <Activity size={16} className="text-indigo-500" />
-                   执行流展开 (Workflow)
-                </h3>
-                
+                  <div className="overflow-x-auto p-5">
                 {steps.length === 0 ? (
                   <div className="text-sm text-slate-400 p-4 bg-slate-50 rounded-xl border border-slate-100 flex items-center gap-2">
                      该工序暂未定义内部执行流
                   </div>
                 ) : (
-                  <div className="space-y-3">
-                    {steps.map((step, idx) => (
-                      <div key={idx} className="group flex gap-4 p-4 bg-slate-50/50 border border-slate-100 rounded-2xl items-stretch hover:bg-indigo-50/30 hover:border-indigo-100 transition-colors">
-                        
-                        <div className="w-8 shrink-0 flex flex-col items-center">
-                           <span className="w-7 h-7 rounded-xl bg-white border border-slate-200 text-indigo-600 font-black text-sm flex items-center justify-center shadow-sm group-hover:border-indigo-300 group-hover:bg-indigo-600 group-hover:text-white transition-all">
-                             {idx + 1}
-                           </span>
-                           {idx !== steps.length - 1 && (
-                             <div className="w-[2px] h-full bg-slate-200 mt-2 mb-[-16px] group-hover:bg-indigo-200 transition-colors"></div>
-                           )}
-                        </div>
-                        
-                        <div className="flex-1 py-1">
-                           {step.title && <div className="font-bold text-slate-800 text-[15px] mb-1.5">{step.title}</div>}
-                           <div className="text-slate-600 text-[13px] leading-relaxed break-words whitespace-pre-wrap">{step.description}</div>
-                        </div>
-                        
-                        <div className="w-1/3 shrink-0 py-1 pl-4 border-l border-slate-200 border-dashed flex flex-col justify-start">
-                           <span className="text-[10px] font-black text-slate-400 mb-2 uppercase tracking-wide">调用的原子能力</span>
-                           {(!step.capabilities || step.capabilities.length === 0) ? (
-                             <span className="text-[11px] text-slate-400">无特定关联能力</span>
-                           ) : (
-                             <div className="flex flex-wrap gap-2">
-                               {step.capabilities.map((capObj: any, i: number) => {
-                                 const capIdStr = typeof capObj === 'string' ? capObj : (capObj?.capability_id || capObj?.id);
-                                 const cap = capabilities[capIdStr] || (typeof capObj === 'object' ? capObj : null);
-                                 const fallbackStr = typeof capObj === 'string' ? capObj : (capObj?.capability_name || capObj?.name || capIdStr);
-                                 return (
-                                   <div key={i} className="flex items-center gap-1.5 bg-white border border-emerald-100 px-2 py-1.5 rounded-lg text-xs hover:border-emerald-400 hover:bg-emerald-50 transition-colors shadow-sm w-full">
-                                     <Cpu size={12} className="text-emerald-500 shrink-0" />
-                                     <span className="text-emerald-900 font-bold truncate">{cap?.capability_name || cap?.name || fallbackStr}</span>
-                                   </div>
-                                 );
-                               })}
+                  <table className="w-full text-left text-sm whitespace-nowrap min-w-[700px]">
+                    <thead className="bg-slate-50 border-b border-slate-100 text-[13px] text-slate-500 font-bold">
+                      <tr>
+                        <th className="px-5 py-3 w-16 text-center">步骤</th>
+                        <th className="px-5 py-3 w-[30%] whitespace-normal">描述</th>
+                        <th className="px-5 py-3 w-[20%] whitespace-normal">工具</th>
+                        <th className="px-5 py-3 w-[30%] whitespace-normal">原子能力分类</th>
+                      </tr>
+                    </thead>
+                    <tbody className="divide-y divide-slate-100/50 text-[13px]">
+                      {steps.map((step, idx) => {
+                         const isStepExpanded = expandedSteps[`${strategy.id}-${idx}`];
+                         return (
+                         <tr 
+                           key={idx} 
+                           className="hover:bg-indigo-50/20 transition-colors cursor-pointer group"
+                           onClick={() => {
+                             const key = `${strategy.id}-${idx}`;
+                             setExpandedSteps(prev => ({...prev, [key]: !prev[key]}));
+                           }}
+                         >
+                           <td className="px-5 py-4 text-center font-black text-slate-400">
+                             <div className="flex items-center gap-2 justify-center">
+                               <div className="text-slate-300 group-hover:text-indigo-400 transition-colors">
+                                 {isStepExpanded ? <ChevronDown size={14}/> : <ChevronRight size={14}/>}
+                               </div>
+                               <span>{idx + 1}</span>
                              </div>
-                           )}
-                        </div>
-                        
-                      </div>
-                    ))}
-                  </div>
+                           </td>
+                           <td className="px-5 py-4 font-medium text-slate-800 whitespace-normal leading-relaxed">
+                              {step.title ? (
+                                <div className="font-bold">{step.title}</div>
+                              ) : (
+                                <div className="font-bold text-slate-500">{(step.description || '').slice(0, 30)}...</div>
+                              )}
+                              
+                              {isStepExpanded && step.description && (
+                                <div className="text-slate-600 font-normal mt-2 whitespace-pre-wrap animate-in fade-in slide-in-from-top-1">
+                                  {step.description}
+                                </div>
+                              )}
+                           </td>
+                           <td className="px-5 py-4 text-slate-600 whitespace-normal">
+                              {(() => {
+                                 const derivedToolIds = new Set<string>();
+                                 if (step.capabilities && step.capabilities.length > 0) {
+                                     step.capabilities.forEach((capObj: any) => {
+                                        const capIdStr = typeof capObj === 'string' ? capObj : (capObj?.capability_id || capObj?.id);
+                                        const cap = capabilities[capIdStr];
+                                        if (cap && cap.tool_ids) {
+                                            cap.tool_ids.forEach((tid: string) => derivedToolIds.add(tid));
+                                        }
+                                     });
+                                 }
+                                 let derivedTools = Array.from(derivedToolIds).map(tid => allToolsMap[tid]?.name || tid);
+                                 if (derivedTools.length === 0 && step.tools && step.tools.length > 0) {
+                                     derivedTools = step.tools;
+                                 }
+                                 return derivedTools.length > 0 ? derivedTools.map((t, i) => (
+                                    <div key={i} className="mb-1">{t}</div>
+                                 )) : <span className="text-slate-400 italic">暂无</span>;
+                              })()}
+                           </td>
+                           <td className="px-5 py-4 whitespace-normal">
+                              {(!step.capabilities || step.capabilities.length === 0) ? (
+                                <span className="text-slate-400 italic">暂无</span>
+                              ) : (
+                                <div className="flex flex-wrap gap-1.5">
+                                  {step.capabilities.map((capObj: any, i: number) => {
+                                      const capIdStr = typeof capObj === 'string' ? capObj : (capObj?.capability_id || capObj?.id);
+                                      const cap = capabilities[capIdStr] || (typeof capObj === 'object' ? capObj : null);
+                                      const capName = cap?.capability_name || cap?.name || capObj?.capability_name || capObj?.name || capIdStr;
+                                      return (
+                                        <span key={i} className="px-2 py-1 bg-indigo-50 text-indigo-600 rounded text-xs font-bold whitespace-nowrap">
+                                          {capName}
+                                        </span>
+                                      );
+                                  })}
+                                </div>
+                              )}
+                           </td>
+                         </tr>
+                       );
+                      })}
+                    </tbody>
+                  </table>
                 )}
               </div>
             </div>

+ 12 - 5
knowhub/knowhub_db/pg_capability_store.py

@@ -34,7 +34,7 @@ _REL_SUBQUERIES = """
      FROM capability_resource cr WHERE cr.capability_id = capability.id) AS resource_ids
 """
 
-_BASE_FIELDS = "id, name, criterion, description, version"
+_BASE_FIELDS = "id, name, criterion, description,version, effects"
 
 _SELECT_FIELDS = f"{_BASE_FIELDS}, {_REL_SUBQUERIES}"
 
@@ -142,13 +142,21 @@ class PostgreSQLCapabilityStore:
             cursor.execute("DELETE FROM capability WHERE id = %s", (cap['id'],))
             cursor.execute("""
                 INSERT INTO capability (
-                    id, name, criterion, description, embedding, version
-                ) VALUES (%s, %s, %s, %s, %s, %s)
+                    id, name, criterion, description, effects, embedding, version
+                ) VALUES (%s, %s, %s, %s, %s, %s, %s)
+                ON CONFLICT (id) DO UPDATE SET
+                    name = EXCLUDED.name,
+                    criterion = EXCLUDED.criterion,
+                    description = EXCLUDED.description,
+                    effects = EXCLUDED.effects,
+                    embedding = EXCLUDED.embedding,
+                    version = EXCLUDED.version
             """, (
                 cap['id'],
                 cap.get('name', ''),
                 cap.get('criterion', ''),
                 cap.get('description', ''),
+                json.dumps(cap.get('effects', [])),
                 cap.get('embedding'),
                 cap.get('version', 'v0'),
             ))
@@ -255,8 +263,7 @@ class PostgreSQLCapabilityStore:
         if not row:
             return None
         result = dict(row)
-        for field in ('requirement_ids', 'tool_ids', 'knowledge_ids',
-                      'resource_ids', 'knowledge_links'):
+        for field in ('requirement_ids', 'tool_ids', 'knowledge_ids', 'effects'):
             if field in result and isinstance(result[field], str):
                 result[field] = json.loads(result[field])
             elif field in result and result[field] is None:

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