فهرست منبع

chore: stop tracking pyc files

guantao 1 ماه پیش
والد
کامیت
828c6bef06
25فایلهای تغییر یافته به همراه756 افزوده شده و 3 حذف شده
  1. BIN
      src/tool_agent/__pycache__/__init__.cpython-312.pyc
  2. BIN
      src/tool_agent/__pycache__/__main__.cpython-312.pyc
  3. BIN
      src/tool_agent/__pycache__/config.cpython-312.pyc
  4. BIN
      src/tool_agent/__pycache__/messaging.cpython-312.pyc
  5. BIN
      src/tool_agent/__pycache__/models.cpython-312.pyc
  6. BIN
      src/tool_agent/registry/__pycache__/__init__.cpython-312.pyc
  7. BIN
      src/tool_agent/registry/__pycache__/registry.cpython-312.pyc
  8. BIN
      src/tool_agent/router/__pycache__/__init__.cpython-312.pyc
  9. BIN
      src/tool_agent/router/__pycache__/agent.cpython-312.pyc
  10. BIN
      src/tool_agent/router/__pycache__/dispatcher.cpython-312.pyc
  11. BIN
      src/tool_agent/router/__pycache__/server.cpython-312.pyc
  12. BIN
      src/tool_agent/router/__pycache__/status.cpython-312.pyc
  13. BIN
      src/tool_agent/runtime/__pycache__/__init__.cpython-312.pyc
  14. BIN
      src/tool_agent/runtime/__pycache__/docker_runner.cpython-312.pyc
  15. BIN
      src/tool_agent/runtime/__pycache__/local_runner.cpython-312.pyc
  16. BIN
      src/tool_agent/runtime/__pycache__/resource.cpython-312.pyc
  17. 10 0
      src/tool_agent/service/agent.py
  18. BIN
      src/tool_agent/tool/__pycache__/__init__.cpython-312.pyc
  19. BIN
      src/tool_agent/tool/__pycache__/agent.cpython-312.pyc
  20. 320 0
      src/tool_agent/tool/maintenance.py
  21. 52 0
      tests/test_extrct/extract_atomic_capabilities.prompt
  22. 325 0
      tests/test_extrct/extract_capabilities_auto.py
  23. BIN
      tools/local/image_stitcher/__pycache__/stitch_core.cpython-312.pyc
  24. BIN
      tools/local/liblibai_controlnet/__pycache__/liblibai_client.cpython-312.pyc
  25. 49 3
      tools/local/nano_banana/tests/server.log

BIN
src/tool_agent/__pycache__/__init__.cpython-312.pyc


BIN
src/tool_agent/__pycache__/__main__.cpython-312.pyc


BIN
src/tool_agent/__pycache__/config.cpython-312.pyc


BIN
src/tool_agent/__pycache__/messaging.cpython-312.pyc


BIN
src/tool_agent/__pycache__/models.cpython-312.pyc


BIN
src/tool_agent/registry/__pycache__/__init__.cpython-312.pyc


BIN
src/tool_agent/registry/__pycache__/registry.cpython-312.pyc


BIN
src/tool_agent/router/__pycache__/__init__.cpython-312.pyc


BIN
src/tool_agent/router/__pycache__/agent.cpython-312.pyc


BIN
src/tool_agent/router/__pycache__/dispatcher.cpython-312.pyc


BIN
src/tool_agent/router/__pycache__/server.cpython-312.pyc


BIN
src/tool_agent/router/__pycache__/status.cpython-312.pyc


BIN
src/tool_agent/runtime/__pycache__/__init__.cpython-312.pyc


BIN
src/tool_agent/runtime/__pycache__/docker_runner.cpython-312.pyc


BIN
src/tool_agent/runtime/__pycache__/local_runner.cpython-312.pyc


BIN
src/tool_agent/runtime/__pycache__/resource.cpython-312.pyc


+ 10 - 0
src/tool_agent/service/agent.py

@@ -18,6 +18,12 @@ from claude_agent_sdk import (
     AssistantMessage, TextBlock, ClaudeAgentOptions, tool,
     create_sdk_mcp_server, ClaudeSDKClient, ToolUseBlock,
 )
+from tool_agent.tool.maintenance import (
+    create_merge_unconnected_tools_ticket,
+    track_maintenance_ticket_status,
+    approve_maintenance_ticket,
+)
+from tool_agent.tool.capability_extractor import sync_atomic_capabilities
 
 if TYPE_CHECKING:
     from tool_agent.router.agent import Router
@@ -133,6 +139,10 @@ async def submit_task_fn(args):
 ALL_TOOLS = [
     list_tools_fn, get_tool_details_fn, list_groups_fn,
     list_backend_runtimes_fn, check_task_status_fn, submit_task_fn,
+    create_merge_unconnected_tools_ticket,
+    track_maintenance_ticket_status,
+    approve_maintenance_ticket,
+    sync_atomic_capabilities,
 ]
 
 

BIN
src/tool_agent/tool/__pycache__/__init__.cpython-312.pyc


BIN
src/tool_agent/tool/__pycache__/agent.cpython-312.pyc


+ 320 - 0
src/tool_agent/tool/maintenance.py

@@ -0,0 +1,320 @@
+import asyncio
+import json
+import logging
+import os
+import uuid
+from typing import Dict, Any
+from datetime import datetime
+from pathlib import Path
+
+from psycopg2.extras import RealDictCursor
+from claude_agent_sdk import tool, ClaudeAgentOptions, ClaudeSDKClient, AssistantMessage, TextBlock
+
+# import app specific configurations
+from tool_agent.tool.tool_store import PostgreSQLToolStore
+from tool_agent.tool.capability import PostgreSQLCapabilityStore
+
+logger = logging.getLogger(__name__)
+
+# 数据存储目录,用于存放工单与匹配缓存结果
+TICKET_DIR = Path("data/maintenance_tickets")
+TICKET_DIR.mkdir(parents=True, exist_ok=True)
+
+SYSTEM_PROMPT = """你是一个专业的 API 工具库去重专家。
+你的任务是将一份【未接入工具列表】与一份核心的【已接入工具列表】进行比对归并。
+如果某一个【未接入工具】(哪怕换了名字叫别的)在【已接入工具】中已经有同样的本质功能,请找出这种映射关系。
+
+例如:
+未接入:"通过 Midjourney 生成图片" 或 "Midjourney 查进度"
+已接入:由于我们有 `midjourney_submit_job` 和 `midjourney_query_job_status`,请寻找最合理的对应。
+
+【重要领域等价字典】请你在判定时,严格遵从以下我们架构中的内置别名等价逻辑:
+1. "Nano Banana" 或者 "nanobanana" 等同于 "Google Gemini 生图" 或 "Imagen 3"
+2. "BFL" 等同于 "Flux 官方生图 API"
+3. "LiblibAI" 或 "哩布哩布" 等同于任何有关 Controlnet (不论是 OpenPose/Canny 等变体) 的支持
+4. "RunComfy" 等同于 "ComfyUI 远程实例或工作流执行"
+5. "即梦" 或 "ji_meng" 同等对应其系列任务
+
+如果未能找到合理的已接入工具应对,则不输出映射。
+
+请输出一个合法的 JSON 数组,必须是一个 list of objects:
+[
+  {
+    "unconnected_id": "<未接入的工具id>",
+    "connected_id": "<对应最合理的已接入的工具id>",
+    "reason": "<非常简短的一句话匹配理由>"
+  }
+]
+
+请绝对不要包含任何 Markdown 语法、```json 标签,直接输出原生 JSON 字符串。
+"""
+
+async def match_tools_with_claude(connected_tools, unconnected_batch):
+    conn_str = json.dumps([{"id": t["id"], "name": t["name"], "desc": t["introduction"]} for t in connected_tools], ensure_ascii=False)
+    unconn_str = json.dumps([{"id": t["id"], "name": t["name"], "desc": t["introduction"]} for t in unconnected_batch], ensure_ascii=False)
+    
+    prompt = f"【已接入工具库(作为对标参考)】:\n{conn_str}\n\n【本次需判定匹配的未接入工具】:\n{unconn_str}\n\n请输出JSON结果:"
+    
+    options = ClaudeAgentOptions(model="claude-sonnet-4-5", system_prompt=SYSTEM_PROMPT)
+    result_text = ""
+    try:
+        async with ClaudeSDKClient(options=options) as client:
+            await client.query(prompt)
+            async for msg in client.receive_response():
+                if isinstance(msg, AssistantMessage):
+                    for block in msg.content:
+                        if isinstance(block, TextBlock):
+                            result_text += block.text
+    except Exception as e:
+        logger.error(f"Claude API failed: {e}")
+        return []
+
+    # 毫无保留地保存最原始的字符串落盘(防崩备用方案)
+    with open("raw_claude_responses.log", "a", encoding="utf-8") as f:
+        f.write(f"\n--- Batch Output ---\n{result_text}\n")
+
+    try:
+        clean_json = result_text.strip()
+        if clean_json.startswith("```json"):
+            clean_json = clean_json[7:]
+        elif clean_json.startswith("```"):
+            clean_json = clean_json[3:]
+        if clean_json.endswith("```"):
+            clean_json = clean_json[:-3]
+        
+        data = json.loads(clean_json.strip())
+        if isinstance(data, dict):
+            if "unconnected_id" in data:
+                return [data]
+            return []
+        elif isinstance(data, list):
+            return [item for item in data if isinstance(item, dict)]
+        return []
+    except Exception as e:
+        logger.error(f"Failed to parse JSON response: {e}\nResponse was: {result_text[:200]}")
+        return []
+
+def apply_database_merge(store: PostgreSQLToolStore, match_plan: list):
+    """根据生成的合法匹配图,执行数据库替换"""
+    if not match_plan:
+        return 0
+        
+    c = store.conn.cursor(cursor_factory=RealDictCursor)
+    merged_count = 0
+    
+    for match in match_plan:
+        uid = match.get("unconnected_id")
+        cid = match.get("connected_id")
+        if not uid or not cid:
+            continue
+            
+        try:
+            c.execute("SELECT introduction, tutorial, version FROM tool WHERE id = %s", (uid,))
+            u_tool = c.fetchone()
+            
+            c.execute("SELECT introduction, tutorial, version FROM tool WHERE id = %s", (cid,))
+            c_tool = c.fetchone()
+            
+            if u_tool and c_tool:
+                u_ver = u_tool.get('version') or ''
+                c_ver = c_tool.get('version') or ''
+                latest_ver = c_ver if c_ver and c_ver != "1.0.0" else (u_ver if u_ver else "1.0.0")
+                version_prefix = f"已支持的最新版本: {latest_ver}\n\n"
+                
+                new_intro = version_prefix + (c_tool['introduction'] or '') + "\n\n[融合补充描述]:\n" + (u_tool['introduction'] or '')
+                new_tutorial = (c_tool['tutorial'] or '') + "\n\n" + (u_tool['tutorial'] or '')
+                c.execute("UPDATE tool SET introduction=%s, tutorial=%s WHERE id=%s", (new_intro[:4000], new_tutorial[:4000], cid))
+                
+            tables_to_migrate = [ 
+                ("capability_tool", "capability_id", "tool_id"), 
+                ("tool_knowledge", "knowledge_id", "tool_id"),
+                ("tool_provider", "provider_id", "tool_id")
+            ]
+            
+            for table, _, tool_col in tables_to_migrate:
+                c.execute(f"SELECT * FROM {table} WHERE {tool_col} = %s", (uid,))
+                rows = c.fetchall()
+                for row in rows:
+                    if table == "capability_tool":
+                        c.execute("INSERT INTO capability_tool (capability_id, tool_id) VALUES (%s, %s) ON CONFLICT DO NOTHING", (row['capability_id'], cid))
+                    elif table == "tool_knowledge":
+                        c.execute("INSERT INTO tool_knowledge (knowledge_id, tool_id) VALUES (%s, %s) ON CONFLICT DO NOTHING", (row['knowledge_id'], cid))
+                    elif table == "tool_provider":
+                        c.execute("INSERT INTO tool_provider (provider_id, tool_id) VALUES (%s, %s) ON CONFLICT DO NOTHING", (row['provider_id'], cid))
+                        
+            c.execute("DELETE FROM capability_tool WHERE tool_id = %s", (uid,))
+            c.execute("DELETE FROM tool_knowledge WHERE tool_id = %s", (uid,))
+            c.execute("DELETE FROM tool_provider WHERE tool_id = %s", (uid,))
+            c.execute("DELETE FROM tool WHERE id = %s", (uid,))
+            merged_count += 1
+            store.conn.commit()
+        except Exception as e:
+            logger.error(f"处理 {uid} 的融合时出错: {e}")
+            store.conn.rollback() # 出错则回滚该条
+            continue
+            
+    c.close()
+    return merged_count
+
+
+# ===========================================================================
+#  异步提单状态机 (Tickets State Machine)
+# ===========================================================================
+
+class TicketManager:
+    @classmethod
+    def save_ticket(cls, ticket_id: str, data: Dict[str, Any]):
+        filepath = TICKET_DIR / f"{ticket_id}.json"
+        with open(filepath, "w", encoding="utf-8") as f:
+            json.dump(data, f, ensure_ascii=False, indent=2)
+
+    @classmethod
+    def load_ticket(cls, ticket_id: str) -> Dict[str, Any] | None:
+        filepath = TICKET_DIR / f"{ticket_id}.json"
+        if not filepath.exists():
+            return None
+        with open(filepath, "r", encoding="utf-8") as f:
+            return json.load(f)
+
+
+async def _deduplication_background_task(ticket_id: str):
+    logger.info(f"[Ticket {ticket_id}] 启动异步清洗去重计算任务...")
+    
+    TicketManager.save_ticket(ticket_id, {
+        "status": "PROCESSING",
+        "progress": "Initialization",
+        "created_at": datetime.now().isoformat(),
+        "matches": []
+    })
+    
+    store = PostgreSQLToolStore()
+    tools = store.list_all(limit=2000)
+    
+    connected = [t for t in tools if t.get("status") == "已接入"]
+    unconnected = [t for t in tools if t.get("status") != "已接入"]
+    
+    if not unconnected:
+        TicketManager.save_ticket(ticket_id, {
+            "status": "COMPLETED",
+            "progress": "No unconnected tools found.",
+            "matches": []
+        })
+        store.close()
+        return
+
+    batch_size = 10
+    all_matches = []
+    
+    total_batches = (len(unconnected) // batch_size) + (1 if len(unconnected) % batch_size != 0 else 0)
+    
+    try:
+        for i in range(0, len(unconnected), batch_size):
+            batch = unconnected[i:i + batch_size]
+            logger.info(f"正在交给 Claude 引擎评估第 {i//batch_size + 1}/{total_batches} 批 ({len(batch)} items) ...")
+            
+            # 更新状态防失联
+            TicketManager.save_ticket(ticket_id, {
+                "status": "PROCESSING",
+                "progress": f"Evaluating batch {i//batch_size + 1}/{total_batches}",
+                "matches": all_matches
+            })
+            
+            matches = await match_tools_with_claude(connected, batch)
+            if matches:
+                all_matches.extend(matches)
+
+        # 任务完毕,准备等待人工审批
+        TicketManager.save_ticket(ticket_id, {
+            "status": "READY_FOR_REVIEW",
+            "progress": "All LLM deductions complete. Ready for manual apply.",
+            "matches": all_matches
+        })
+    except Exception as e:
+        logger.error(f"[Ticket {ticket_id}] 运维背景清洗错误: {e}")
+        TicketManager.save_ticket(ticket_id, {
+            "status": "FAILED",
+            "progress": f"Error: {str(e)}",
+            "matches": all_matches
+        })
+    finally:
+        store.close()
+
+
+# ===========================================================================
+#  Agents 外暴的 MCP Tools
+# ===========================================================================
+
+def _result(data: dict) -> dict:
+    return {"content": [{"type": "text", "text": json.dumps(data, ensure_ascii=False, indent=2)}]}
+
+
+@tool(name="create_merge_unconnected_tools_ticket", description="""
+发起一个异步评估工具。用于调用大模型全量分析所有的“已接入工具”和“未接入工具”,
+进行智能清理注册表的后台任务。这是一个非常耗时的操作(可能需要 15 分钟),
+所以调用它会立即返回一个 Ticket ID,你需要记录下来并在稍后轮询。
+""", input_schema={"type": "object", "properties": {}})
+async def create_merge_unconnected_tools_ticket(args):
+    ticket_id = f"MERGE-{uuid.uuid4().hex[:8].upper()}"
+    asyncio.create_task(_deduplication_background_task(ticket_id))
+    return _result({
+        "info": "异步清理计算作业已下发!请不要阻碍当前进程流。",
+        "ticket_id": ticket_id,
+        "next_step": "调用 track_maintenance_ticket_status,带着这个 ticket_id 查看执行是否完毕。"
+    })
+
+
+@tool(name="track_maintenance_ticket_status", description="""
+追踪工单的处理情况。当 status 为 READY_FOR_REVIEW 时,代表匹配的去重方案已经生成完毕。
+你需要将 matches 里的干预信息详细打印出来展示给用户确认审查,征求他们意见。
+""", input_schema={
+    "type": "object",
+    "properties": {"ticket_id": {"type": "string"}},
+    "required": ["ticket_id"]
+})
+async def track_maintenance_ticket_status(args):
+    ticket = TicketManager.load_ticket(args["ticket_id"])
+    if not ticket:
+        return _result({"error": "找不到该工单,请检查输入或文件丢失。"})
+    
+    # 限制过长的 matches,以防 MCP 塞爆
+    if ticket.get("status") == "READY_FOR_REVIEW":
+        preview = ticket.get("matches", [])
+        if len(preview) > 50:
+            preview = preview[:50] + [{"notice": "为防止通信拥堵,其余被截断隐去..."}]
+        ticket["matches_preview"] = preview
+        if "matches" in ticket:
+            del ticket["matches"]
+
+    return _result(ticket)
+
+
+@tool(name="approve_maintenance_ticket", description="""
+当用户同意了你的工具合并匹配干预报表,你就可以带着票号调用这个指令。
+它会立即触发物理上的外键数据库更替、重组、剔除操作,完成真正的合并洗污!
+""", input_schema={
+    "type": "object",
+    "properties": {"ticket_id": {"type": "string"}},
+    "required": ["ticket_id"]
+})
+async def approve_maintenance_ticket(args):
+    ticket_id = args["ticket_id"]
+    ticket = TicketManager.load_ticket(ticket_id)
+    if not ticket:
+        return _result({"error": "找不到该工单,请检查。"})
+        
+    if ticket.get("status") != "READY_FOR_REVIEW":
+        return _result({"error": f"目前的工单状态为 {ticket.get('status')},而不是 READY_FOR_REVIEW, 不具备施加的前提!"})
+        
+    store = PostgreSQLToolStore()
+    try:
+        count = apply_database_merge(store, ticket.get("matches", []))
+        ticket["status"] = "COMPLETED"
+        ticket["progress"] = f"成功融合淘汰了 {count} 个边缘旧工具与它们的关系网!"
+        TicketManager.save_ticket(ticket_id, ticket)
+        return _result({"success": True, "info": ticket["progress"]})
+    except Exception as e:
+        return _result({"error": f"物理落地时产生了报错:{e}"})
+    finally:
+        store.close()
+
+

+ 52 - 0
tests/test_extrct/extract_atomic_capabilities.prompt

@@ -0,0 +1,52 @@
+---
+name: extract_atomic_capabilities
+model: anthropic/claude-sonnet-4.6
+temperature: 0.3
+max_tokens: 16000
+---
+
+$system$
+你是一个专业的能力分析师。你的任务是从工具的使用介绍和实际用例中提取**原子能力**。
+
+## 什么是原子能力?
+
+原子能力是一种**面向需求的、跨工具的高维能力**。它不是某个工具的具体技术实现细节,而是一种独立完整的、可直接面对用户需求的能力单元。
+
+### 核心定义
+- 它是**面向需求**的:每个原子能力都直接对应用户的某一类创作需求
+- 它是**跨工具**的:同一个原子能力可以由不同工具以不同方式实现
+- 它是**不可分割**的:拆分后将无法独立满足任何需求
+- 它是**可组合**的:多个原子能力按顺序组合可形成完整的工序/流水线
+
+### 关于工具的自由度差异
+
+工具分为两类,提取原子能力时请注意区分:
+
+**端到端工具**(如 Midjourney、DALL-E):输入 prompt → 输出图像,能力边界清晰。
+→ 从其参数/功能中直接提取原子能力。
+
+**编排平台型工具**(如 ComfyUI、HTML):内部高度自由,可任意组合节点/模块,能力边界开放。
+→ 不要试图原子化平台本身,而是从其**具体的工作流和用例**中提取原子能力。
+→ 在「实现方式」中标注具体的工作流/方案,而非泛泛地写"ComfyUI"。
+→ 例如:ComfyUI 实现「角色一致性」的方式是「IP-Adapter 节点 + 参考图」,这就是一种实现方案。
+
+### 举例说明
+✅ **正确的原子能力**:「保持角色一致性」— 跨多张图保持同一角色的面部/身体/服装特征不变。这个能力可由 ControlNet、IP-Adapter、--cref 参数、多图参考等不同工具/方式实现,但核心需求是一样的。
+✅ **正确的原子能力**:「图内文字渲染」— 在生成的图像中嵌入清晰可读的指定文字。
+❌ **错误(太底层)**:「使用 KSampler 采样」— 这是具体技术操作,不是面向需求的能力。
+❌ **错误(太底层)**:「设置 --ar 16:9」— 这是参数设置,不是独立能力。
+❌ **错误(可再分)**:「制作电商产品图」— 这可以分解为「背景替换」+「产品一致性保持」+「光照调整」等多个原子能力的组合。
+
+## 原子能力的格式
+
+每个原子能力应包含:
+
+### [能力ID]: [能力名称]
+- **功能描述**: [做什么,满足什么需求]
+- **判定标准**: [怎样算做到了,怎样算没做到]
+- **实现方式**: [列举可实现的工具/方案。端到端工具直接写工具名+参数;编排平台写具体工作流,如「ComfyUI: IP-Adapter节点+参考图输入」]
+- **典型场景**: [什么时候需要用到这个能力]
+- **来源依据**: [从哪些具体用例/文档提炼出来的,简述来源帖子或用例的大概内容]
+
+$user$
+{user_prompt}

+ 325 - 0
tests/test_extrct/extract_capabilities_auto.py

@@ -0,0 +1,325 @@
+#!/usr/bin/env python3
+"""
+原子能力提取工作流 - 自动化版本
+使用 openrouter (claude-sonnet) 逐个读取工具文档,迭代式提取和融合原子能力
+"""
+
+import asyncio
+import json
+import os
+import re
+import sys
+from pathlib import Path
+
+# 添加项目根目录
+sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+from dotenv import load_dotenv
+load_dotenv()
+
+from agent.llm.openrouter import openrouter_llm_call
+
+# ===== 配置 =====
+BASE_DIR = Path(__file__).parent
+TOOL_RESULTS_DIR = BASE_DIR / "tool_results"
+OUTPUT_FILE = BASE_DIR / "atomic_capabilities.md"
+PROMPT_FILE = BASE_DIR / "extract_atomic_capabilities.prompt"
+
+
+# ===== Prompt 加载(复用 match_nodes.py 的模式) =====
+
+def load_prompt(filepath: str) -> dict:
+    """加载 .prompt 文件,解析 frontmatter 和 $role$ 分段"""
+    text = Path(filepath).read_text(encoding="utf-8")
+
+    config = {}
+    if text.startswith("---"):
+        _, fm, text = text.split("---", 2)
+        for line in fm.strip().splitlines():
+            if ":" in line:
+                k, v = line.split(":", 1)
+                k, v = k.strip(), v.strip()
+                if v.replace(".", "", 1).isdigit():
+                    v = float(v) if "." in v else int(v)
+                config[k] = v
+
+    messages = []
+    parts = re.split(r'^\$(\w+)\$\s*$', text.strip(), flags=re.MULTILINE)
+    for i in range(1, len(parts), 2):
+        role = parts[i].strip()
+        content = parts[i + 1].strip() if i + 1 < len(parts) else ""
+        messages.append({"role": role, "content": content})
+
+    return {"config": config, "messages": messages}
+
+
+def render_messages(prompt_data: dict, variables: dict) -> list[dict]:
+    """用变量替换 prompt 模板中的 {var} 占位符"""
+    rendered = []
+    for msg in prompt_data["messages"]:
+        content = msg["content"]
+        for k, v in variables.items():
+            content = content.replace(f"{{{k}}}", str(v))
+        rendered.append({"role": msg["role"], "content": content})
+    return rendered
+
+
+# ===== 文件读取 =====
+
+def get_all_tool_dirs():
+    """获取所有工具目录"""
+    dirs = sorted([d for d in TOOL_RESULTS_DIR.iterdir() if d.is_dir()])
+    return dirs
+
+
+def read_file(file_path):
+    """读取文件内容"""
+    with open(file_path, 'r', encoding='utf-8') as f:
+        return f.read()
+
+
+def read_tool_files(tool_dir):
+    """读取工具的使用介绍和实际用例"""
+    usage_file = tool_dir / "使用介绍.md"
+    case_file = tool_dir / "实际用例.md"
+
+    content = ""
+    if usage_file.exists():
+        content += "# 使用介绍\n\n" + read_file(usage_file) + "\n\n"
+    if case_file.exists():
+        content += "# 实际用例\n\n" + read_file(case_file)
+
+    return content
+
+
+# ===== 构建 user prompt =====
+
+def build_user_prompt(file_content, tool_name, existing_capabilities=""):
+    """构建每轮迭代的 user prompt"""
+
+    if existing_capabilities:
+        user_prompt = f"""## 当前状态
+
+### 已提取的原子能力
+
+{existing_capabilities}
+
+## 你的工作
+
+1. 仔细阅读下面的工具文档(使用介绍 + 实际用例)
+2. 从中识别出**面向需求的原子能力**(注意:不是工具的技术操作)
+3. 与已有能力对比:
+   - 如果是全新的能力 → 添加,并说明来源
+   - 如果已有能力可由新工具实现 → 融合,在「实现方式」中补充该工具
+   - 如果是多个已有能力的组合 → 不添加,但在「发现的能力组合」中记录
+4. 对于来源依据,要说明从哪个用例/帖子/文档章节提炼的,并简述其大概内容
+"""
+    else:
+        user_prompt = """## 当前状态
+
+这是第一次提取,当前没有已有能力。
+
+## 你的工作
+
+1. 仔细阅读下面的工具文档(使用介绍 + 实际用例)
+2. 从中识别出**面向需求的原子能力**(注意:不是工具的技术操作)
+3. 对于来源依据,要说明从哪个用例/帖子/文档章节提炼的,并简述其大概内容
+"""
+
+    user_prompt += f"""
+## 当前要处理的工具
+
+**工具名称**: {tool_name}
+
+**文档内容**(包含使用介绍和实际用例):
+
+{file_content}
+
+## 输出要求
+
+请按以下格式输出:
+
+# 原子能力清单(更新后)
+
+## 本轮分析
+简要说明从 {tool_name} 中发现了哪些能力,哪些是新的,哪些与已有能力融合了。
+
+## 新增能力
+[列出本次新增的能力,使用上述格式,每个能力都要有来源依据]
+
+## 融合能力
+[列出本次融合/更新的能力,说明新增了哪些实现方式]
+
+## 发现的能力组合
+[列出发现的能力组合关系,例如:能力A + 能力B + 能力C = 完成「电商产品图批量生成」]
+
+## 完整能力清单
+[输出完整的、更新后的原子能力清单,包含所有能力(新增 + 已有 + 融合后的)]
+"""
+
+    return user_prompt
+
+
+# ===== LLM 调用 =====
+
+async def extract_capabilities_from_tool(prompt_data, tool_dir, existing_capabilities=""):
+    """从工具目录提取原子能力"""
+    tool_name = tool_dir.name
+    print(f"\n📖 正在处理: {tool_name}")
+
+    # 读取使用介绍和实际用例
+    content = read_tool_files(tool_dir)
+
+    # 构建 user prompt
+    user_prompt = build_user_prompt(content, tool_name, existing_capabilities)
+
+    # 渲染 prompt 模板
+    messages = render_messages(prompt_data, {"user_prompt": user_prompt})
+
+    # 从 prompt 文件读取配置
+    model = prompt_data["config"].get("model", "anthropic/claude-sonnet-4-20250514")
+    temperature = prompt_data["config"].get("temperature", 0.3)
+    max_tokens = prompt_data["config"].get("max_tokens", 16000)
+
+    try:
+        result = await openrouter_llm_call(
+            messages, model=model, temperature=temperature, max_tokens=max_tokens
+        )
+        response = result["content"]
+
+        # 打印 token 用量
+        pt = result.get("prompt_tokens", 0)
+        ct = result.get("completion_tokens", 0)
+        cost = result.get("cost", 0)
+        print(f"   tokens: {pt} prompt + {ct} completion | cost: ${cost:.4f}")
+
+        # 提取"完整能力清单"部分
+        if "## 完整能力清单" in response:
+            complete_list = response.split("## 完整能力清单")[1].strip()
+        else:
+            complete_list = response
+
+        print(f"✅ {tool_name} 处理完成")
+        return response, complete_list
+
+    except Exception as e:
+        print(f"❌ {tool_name} 处理失败: {e}")
+        return None, existing_capabilities
+
+
+async def generate_json_index(prompt_data, capabilities_md):
+    """把 markdown 格式的能力清单转成简洁 JSON"""
+    prompt = f"""请把以下原子能力清单转成 JSON 数组,每个能力包含以下字段:
+
+```json
+[
+  {{
+    "id": "能力ID",
+    "name": "能力名称",
+    "description": "一句话功能描述",
+    "criteria": "判定标准(简洁)",
+    "tools": ["支持的工具/方案1", "支持的工具/方案2"],
+    "scenarios": ["典型场景1", "典型场景2"],
+    "source_summary": "来源依据的简要概括"
+  }}
+]
+```
+
+要求:
+- 只输出 JSON,不要任何其他文字
+- 保持所有能力,不要遗漏
+- description 控制在 30 字以内
+- criteria 控制在 30 字以内
+
+原子能力清单:
+
+{capabilities_md}
+"""
+
+    model = prompt_data["config"].get("model", "anthropic/claude-sonnet-4-20250514")
+
+    messages = [{"role": "user", "content": prompt}]
+    try:
+        result = await openrouter_llm_call(messages, model=model, temperature=0.1, max_tokens=8000)
+        content = result["content"].strip()
+        # 清理 markdown 代码块包裹
+        if content.startswith("```"):
+            content = content.split("\n", 1)[1]
+            content = content.rsplit("```", 1)[0]
+        # 验证是合法 JSON
+        json.loads(content)
+        return content
+    except Exception as e:
+        print(f"❌ JSON 索引生成失败: {e}")
+        return None
+
+
+# ===== 主流程 =====
+
+async def main():
+    print("🚀 开始提取原子能力...")
+    print()
+
+    # 加载 prompt 模板
+    prompt_data = load_prompt(PROMPT_FILE)
+    model = prompt_data["config"].get("model", "anthropic/claude-sonnet-4-20250514")
+    print(f"🤖 使用模型: {model}")
+    print()
+
+    # 获取所有工具目录
+    tool_dirs = get_all_tool_dirs()
+    print(f"📁 找到 {len(tool_dirs)} 个工具:")
+    for d in tool_dirs:
+        files = list(d.glob("*.md"))
+        print(f"   - {d.name} ({len(files)} 个文件)")
+    print()
+
+    # 迭代处理每个工具
+    existing_capabilities = ""
+    all_responses = []
+
+    for i, tool_dir in enumerate(tool_dirs, 1):
+        print(f"{'='*60}")
+        print(f"进度: [{i}/{len(tool_dirs)}]")
+
+        response, complete_list = await extract_capabilities_from_tool(
+            prompt_data, tool_dir, existing_capabilities
+        )
+
+        if response:
+            all_responses.append({
+                "tool": tool_dir.name,
+                "response": response
+            })
+            existing_capabilities = complete_list
+
+    # 保存最终结果
+    print(f"\n{'='*60}")
+    print("💾 保存结果...")
+
+    # 保存完整能力清单(markdown)
+    OUTPUT_FILE.write_text(existing_capabilities, encoding='utf-8')
+    print(f"✅ 原子能力清单已保存到: {OUTPUT_FILE}")
+
+    # 保存详细过程
+    detail_file = BASE_DIR / "atomic_capabilities_detail.json"
+    with open(detail_file, 'w', encoding='utf-8') as f:
+        json.dump(all_responses, f, ensure_ascii=False, indent=2)
+    print(f"✅ 详细过程已保存到: {detail_file}")
+
+    # 最终一轮:让 LLM 把完整能力清单转成简洁 JSON
+    print(f"\n{'='*60}")
+    print("📋 生成简洁 JSON 索引...")
+    json_result = await generate_json_index(prompt_data, existing_capabilities)
+    if json_result:
+        json_index_file = BASE_DIR / "atomic_capabilities_index.json"
+        with open(json_index_file, 'w', encoding='utf-8') as f:
+            f.write(json_result)
+        print(f"✅ JSON 索引已保存到: {json_index_file}")
+
+    print("\n🎉 所有文件处理完成!")
+
+
+if __name__ == "__main__":
+    os.environ.setdefault("no_proxy", "*")
+    asyncio.run(main())

BIN
tools/local/image_stitcher/__pycache__/stitch_core.cpython-312.pyc


BIN
tools/local/liblibai_controlnet/__pycache__/liblibai_client.cpython-312.pyc


+ 49 - 3
tools/local/nano_banana/tests/server.log

@@ -1,5 +1,51 @@
-INFO:     Started server process [297450]
+INFO:     Started server process [37671]
 INFO:     Waiting for application startup.
 INFO:     Application startup complete.
-INFO:     Uvicorn running on http://0.0.0.0:50491 (Press CTRL+C to quit)
-INFO:     127.0.0.1:49314 - "POST /generate HTTP/1.1" 200 OK
+INFO:     Uvicorn running on http://0.0.0.0:43491 (Press CTRL+C to quit)
+INFO:     127.0.0.1:40568 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:42416 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:52562 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:36744 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:44220 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:48284 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:50378 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:43888 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:57694 - "POST /generate HTTP/1.1" 503 Service Unavailable
+INFO:     127.0.0.1:43960 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:60810 - "POST /generate HTTP/1.1" 503 Service Unavailable
+INFO:     127.0.0.1:33084 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:56366 - "POST /generate HTTP/1.1" 503 Service Unavailable
+INFO:     127.0.0.1:42362 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:34458 - "POST /generate HTTP/1.1" 503 Service Unavailable
+INFO:     127.0.0.1:58652 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:48248 - "POST /generate HTTP/1.1" 503 Service Unavailable
+INFO:     127.0.0.1:35054 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:48354 - "POST /generate HTTP/1.1" 503 Service Unavailable
+INFO:     127.0.0.1:45664 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:33144 - "POST /generate HTTP/1.1" 503 Service Unavailable
+INFO:     127.0.0.1:58242 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:37418 - "POST /generate HTTP/1.1" 503 Service Unavailable
+INFO:     127.0.0.1:33290 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:34660 - "POST /generate HTTP/1.1" 503 Service Unavailable
+INFO:     127.0.0.1:32782 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:57172 - "POST /generate HTTP/1.1" 503 Service Unavailable
+INFO:     127.0.0.1:49650 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:41564 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:50996 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:56596 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:55124 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:39236 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:58458 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:49850 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:54664 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:38734 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:38066 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:40502 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:52104 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:42742 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:33532 - "POST /generate HTTP/1.1" 200 OK
+INFO:     127.0.0.1:51234 - "POST /generate HTTP/1.1" 200 OK
+INFO:     Shutting down
+INFO:     Waiting for application shutdown.
+INFO:     Application shutdown complete.
+INFO:     Finished server process [37671]