guantao před 1 týdnem
revize
5b60fc77a1
100 změnil soubory, kde provedl 14429 přidání a 0 odebrání
  1. 14 0
      .env
  2. 8 0
      .env.template
  3. 11 0
      .gitignore
  4. 3 0
      README.md
  5. binární
      control_reference.png
  6. 1 0
      data/billing_log.json
  7. 11 0
      data/config.json
  8. 18 0
      data/containers.json
  9. 185 0
      data/registry.json
  10. 32 0
      data/sources.json
  11. binární
      depth_map.png
  12. 736 0
      docs/design.md
  13. 570 0
      docs/internal_api.md
  14. 7162 0
      docs/liblib_api.md
  15. 415 0
      docs/liblibai_uuid_matching_rules.md
  16. 40 0
      pyproject.toml
  17. 330 0
      scripts/feishu_to_md.py
  18. 3 0
      src/tool_agent/__init__.py
  19. 37 0
      src/tool_agent/__main__.py
  20. binární
      src/tool_agent/__pycache__/__init__.cpython-312.pyc
  21. binární
      src/tool_agent/__pycache__/__main__.cpython-312.pyc
  22. binární
      src/tool_agent/__pycache__/config.cpython-312.pyc
  23. binární
      src/tool_agent/__pycache__/messaging.cpython-312.pyc
  24. binární
      src/tool_agent/__pycache__/models.cpython-312.pyc
  25. 44 0
      src/tool_agent/config.py
  26. 29 0
      src/tool_agent/messaging.py
  27. 96 0
      src/tool_agent/models.py
  28. 0 0
      src/tool_agent/registry/__init__.py
  29. binární
      src/tool_agent/registry/__pycache__/__init__.cpython-312.pyc
  30. binární
      src/tool_agent/registry/__pycache__/registry.cpython-312.pyc
  31. 29 0
      src/tool_agent/registry/catalog.py
  32. 195 0
      src/tool_agent/registry/registry.py
  33. 17 0
      src/tool_agent/registry/schema.py
  34. 0 0
      src/tool_agent/router/__init__.py
  35. binární
      src/tool_agent/router/__pycache__/__init__.cpython-312.pyc
  36. binární
      src/tool_agent/router/__pycache__/agent.cpython-312.pyc
  37. binární
      src/tool_agent/router/__pycache__/dispatcher.cpython-312.pyc
  38. binární
      src/tool_agent/router/__pycache__/server.cpython-312.pyc
  39. binární
      src/tool_agent/router/__pycache__/status.cpython-312.pyc
  40. 59 0
      src/tool_agent/router/agent.py
  41. 49 0
      src/tool_agent/router/dispatcher.py
  42. 39 0
      src/tool_agent/router/health.py
  43. 24 0
      src/tool_agent/router/mcp_server.py
  44. 0 0
      src/tool_agent/router/middleware/__init__.py
  45. 14 0
      src/tool_agent/router/middleware/auth.py
  46. 33 0
      src/tool_agent/router/middleware/cache.py
  47. 43 0
      src/tool_agent/router/middleware/metrics.py
  48. 45 0
      src/tool_agent/router/scheduler.py
  49. 192 0
      src/tool_agent/router/server.py
  50. 324 0
      src/tool_agent/router/status.py
  51. 0 0
      src/tool_agent/runtime/__init__.py
  52. binární
      src/tool_agent/runtime/__pycache__/__init__.cpython-312.pyc
  53. binární
      src/tool_agent/runtime/__pycache__/docker_runner.cpython-312.pyc
  54. binární
      src/tool_agent/runtime/__pycache__/local_runner.cpython-312.pyc
  55. binární
      src/tool_agent/runtime/__pycache__/resource.cpython-312.pyc
  56. 28 0
      src/tool_agent/runtime/api_proxy.py
  57. 605 0
      src/tool_agent/runtime/docker_runner.py
  58. 366 0
      src/tool_agent/runtime/local_runner.py
  59. 109 0
      src/tool_agent/runtime/resource.py
  60. 0 0
      src/tool_agent/tool/__init__.py
  61. binární
      src/tool_agent/tool/__pycache__/__init__.cpython-312.pyc
  62. binární
      src/tool_agent/tool/__pycache__/agent.cpython-312.pyc
  63. 726 0
      src/tool_agent/tool/agent.py
  64. 34 0
      src/tool_agent/tool/auditor.py
  65. 27 0
      src/tool_agent/tool/browser.py
  66. 20 0
      src/tool_agent/tool/builder.py
  67. 29 0
      src/tool_agent/tool/deployer.py
  68. 37 0
      src/tool_agent/tool/finance.py
  69. 22 0
      src/tool_agent/tool/promoter.py
  70. 22 0
      src/tool_agent/tool/repairer.py
  71. 0 0
      tests/__init__.py
  72. 46 0
      tests/check.py
  73. 40 0
      tests/cleanup_tool.py
  74. binární
      tests/control_reference.png
  75. 175 0
      tests/liblibai_comfyui_runner.py
  76. binární
      tests/output/stitched_result.png
  77. 194 0
      tests/run_comfy/check_workflow.py
  78. 104 0
      tests/run_comfy/convert_workflow.py
  79. binární
      tests/run_comfy/input/2.png
  80. binární
      tests/run_comfy/input/ref.jpg
  81. binární
      tests/run_comfy/output/ComfyUI_00006_.png
  82. 149 0
      tests/run_comfy/refcontrol_pose.json
  83. 315 0
      tests/run_comfy/refcontrol_pose_api.json
  84. 340 0
      tests/run_comfy/run_workflow.py
  85. 4 0
      tests/tasks/image_stitcher.json
  86. 2 0
      tests/tasks/liblibai_controlnet.json
  87. 4 0
      tests/tasks/runcomfy_check_workflow.json
  88. 4 0
      tests/tasks/runcomfy_convert_workflow.json
  89. 7 0
      tests/tasks/runcomfy_launch_env.json
  90. 2 0
      tests/tasks/runcomfy_run_only.json
  91. 2 0
      tests/tasks/runcomfy_run_workflow.json
  92. 7 0
      tests/tasks/runcomfy_stop_env.json
  93. binární
      tests/tasks/stitcher_images/01.png
  94. binární
      tests/tasks/stitcher_images/02.png
  95. binární
      tests/tasks/stitcher_images/03.png
  96. binární
      tests/tasks/stitcher_images/04.png
  97. binární
      tests/tasks/stitcher_images/05.png
  98. 94 0
      tests/test_create_runcomfy_atomic.py
  99. 10 0
      tests/test_dispatcher.py
  100. 97 0
      tests/test_docker_git.py

+ 14 - 0
.env

@@ -0,0 +1,14 @@
+QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
+QWEN_API_KEY=sk-9453c827b9e14108b53d2b30ef7c75fe
+GEMINI_API_KEY=AIzaSyBcERQpAUpTaibOI7VYZ6tq6NYI05vLPco
+ANTHROPIC_API_KEY=sk-90286f71a5aac6abbf1736d3f981d05d7dbf012a0e6df9d156df1627dd4eb38e
+ANTHROPIC_BASE_URL=https://imds.ai/
+
+# LiblibAI 配置
+LIBLIBAI_ACCESS_KEY=3aHj6mtp8rUfNzO8X_GzBg
+LIBLIBAI_API_DOMAIN=https://openapi.liblibai.cloud
+LIBLIBAI_SECRET_KEY=7guKoYyckYNMMswTocV9QIf2Ks7iUn52
+
+# RunComfy 配置
+RUNCOMFY_USER_ID=93325160-df5c-4ab8-9b2d-3f1018e7ced8
+API_TOKEN=fc0f2bdc-6ecf-4abe-8902-9be475894283

+ 8 - 0
.env.template

@@ -0,0 +1,8 @@
+# LiblibAI 配置
+LIBLIBAI_ACCESS_KEY=
+LIBLIBAI_API_DOMAIN=https://openapi.liblibai.cloud
+LIBLIBAI_SECRET_KEY=
+
+# Claude code 配置
+ANTHROPIC_API_KEY=
+ANTHROPIC_BASE_URL=

+ 11 - 0
.gitignore

@@ -0,0 +1,11 @@
+# Python 字节码缓存
+__pycache__/
+*.pyc
+*.pyo
+*.pyd
+
+# 如果你使用的是常见的 Python 环境,建议也加上这些
+.venv/
+env/
+venv/
+.env

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# Tool_Agent
+
+工具库agent

binární
control_reference.png


+ 1 - 0
data/billing_log.json

@@ -0,0 +1 @@
+{"transactions": []}

+ 11 - 0
data/config.json

@@ -0,0 +1,11 @@
+{
+  "cold_tool_idle_timeout_s": 300,
+  "hot_tool_max_containers": 5,
+  "eviction_policy": "lru",
+  "budget": {
+    "monthly_limit_usd": 100,
+    "single_tx_limit_usd": 20,
+    "require_approval_above_usd": 10,
+    "spent_this_month_usd": 0
+  }
+}

+ 18 - 0
data/containers.json

@@ -0,0 +1,18 @@
+{
+  "containers": [
+    {
+      "container_id": "25e884ca6cecfa87e19ea737315a8773d35e05f6638830c99449efd64a68ec68",
+      "tool_id": "test_git_tool",
+      "image": "ubuntu:22.04",
+      "port_mapping": {},
+      "mem_limit": "512m",
+      "nano_cpus": 1000000000,
+      "use_gpu": false,
+      "gpu_count": -1,
+      "status": "destroyed",
+      "created_at": "2026-03-20T07:31:50.421101Z",
+      "last_accessed": "2026-03-20T07:33:16.214642+00:00",
+      "destroyed_at": "2026-03-20T07:33:16.277798+00:00"
+    }
+  ]
+}

+ 185 - 0
data/registry.json

@@ -0,0 +1,185 @@
+{
+  "tools": [
+    {
+      "tool_id": "image_stitcher",
+      "name": "图片拼接工具",
+      "category": "cv",
+      "description": "将多张图片按指定方向(水平/垂直/网格)拼接成一张大图。支持间距设置、背景色填充和统一缩放模式。输入输出均为 Base64 编码的 PNG 图片。",
+      "input_schema": {
+        "properties": {
+          "background_color": {
+            "default": "#FFFFFF",
+            "description": "间距填充背景色,十六进制颜色值",
+            "type": "string"
+          },
+          "columns": {
+            "default": 2,
+            "description": "grid 模式下每行的列数",
+            "minimum": 1,
+            "type": "integer"
+          },
+          "direction": {
+            "default": "horizontal",
+            "description": "拼接方向:horizontal=水平,vertical=垂直,grid=网格",
+            "enum": [
+              "horizontal",
+              "vertical",
+              "grid"
+            ],
+            "type": "string"
+          },
+          "images": {
+            "description": "Base64 编码的图片列表,至少 2 张",
+            "items": {
+              "type": "string"
+            },
+            "minItems": 2,
+            "type": "array"
+          },
+          "resize_mode": {
+            "default": "none",
+            "description": "缩放模式:none=不缩放,fit_width=统一宽度,fit_height=统一高度",
+            "enum": [
+              "none",
+              "fit_width",
+              "fit_height"
+            ],
+            "type": "string"
+          },
+          "spacing": {
+            "default": 0,
+            "description": "图片间距(像素)",
+            "minimum": 0,
+            "type": "integer"
+          }
+        },
+        "reason": "图片拼接工具输入参数定义",
+        "required": [
+          "images"
+        ],
+        "type": "object"
+      },
+      "output_schema": {
+        "properties": {
+          "height": {
+            "description": "结果图高度(像素)",
+            "type": "integer"
+          },
+          "image": {
+            "description": "拼接结果,Base64 编码的 PNG 图片",
+            "type": "string"
+          },
+          "width": {
+            "description": "结果图宽度(像素)",
+            "type": "integer"
+          }
+        },
+        "reason": "图片拼接工具输出结果定义",
+        "required": [
+          "image",
+          "width",
+          "height"
+        ],
+        "type": "object"
+      },
+      "stream_support": false,
+      "status": "active"
+    },
+    {
+      "tool_id": "liblibai_controlnet",
+      "name": "LibLib ControlNet 图生图",
+      "category": "cv",
+      "description": "基于 LibLib AI 开放 API 的 ControlNet Canny 图生图工具,支持通过边缘检测控制图像生成",
+      "input_schema": {
+        "type": "object",
+        "properties": {
+          "image": {
+            "type": "string",
+            "description": "图片来源,支持 Base64 编码的图片数据或 HTTP 图片 URL"
+          },
+          "prompt": {
+            "type": "string",
+            "description": "正向提示词"
+          },
+          "negative_prompt": {
+            "type": "string",
+            "description": "反向提示词",
+            "default": "lowres, bad anatomy, text, error"
+          },
+          "width": {
+            "type": "integer",
+            "description": "输出宽度",
+            "default": 512
+          },
+          "height": {
+            "type": "integer",
+            "description": "输出高度",
+            "default": 512
+          },
+          "steps": {
+            "type": "integer",
+            "description": "采样步数",
+            "default": 20
+          },
+          "cfg_scale": {
+            "type": "number",
+            "description": "CFG Scale",
+            "default": 7
+          },
+          "img_count": {
+            "type": "integer",
+            "description": "生成图片数量",
+            "default": 1
+          },
+          "control_weight": {
+            "type": "number",
+            "description": "ControlNet 权重",
+            "default": 1
+          },
+          "preprocessor": {
+            "type": "integer",
+            "description": "预处理器枚举",
+            "default": 1
+          },
+          "canny_low": {
+            "type": "integer",
+            "description": "Canny 低阈值",
+            "default": 100
+          },
+          "canny_high": {
+            "type": "integer",
+            "description": "Canny 高阈值",
+            "default": 200
+          }
+        },
+        "required": [
+          "image",
+          "prompt"
+        ]
+      },
+      "output_schema": {
+        "type": "object",
+        "properties": {
+          "images": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            },
+            "description": "生成的图片 URL 列表"
+          },
+          "task_id": {
+            "type": "string",
+            "description": "任务 ID"
+          },
+          "status": {
+            "type": "string",
+            "description": "任务状态(success/failed/timeout)"
+          }
+        }
+      },
+      "stream_support": false,
+      "status": "active"
+    }
+  ],
+  "version": "2.0"
+}

+ 32 - 0
data/sources.json

@@ -0,0 +1,32 @@
+{
+  "sources": {
+    "image_stitcher": [
+      {
+        "type": "local",
+        "host_dir": "tools/local/image_stitcher",
+        "container_id": "",
+        "image": "",
+        "hub_url": "",
+        "hub_tool_path": "",
+        "hub_api_key": "",
+        "endpoint_path": "/stitch",
+        "http_method": "POST",
+        "internal_port": 0
+      }
+    ],
+    "liblibai_controlnet": [
+      {
+        "type": "local",
+        "host_dir": "tools/local/liblibai_controlnet",
+        "container_id": "",
+        "image": "",
+        "hub_url": "",
+        "hub_tool_path": "",
+        "hub_api_key": "",
+        "endpoint_path": "/generate",
+        "http_method": "POST",
+        "internal_port": 8001
+      }
+    ]
+  }
+}

binární
depth_map.png


+ 736 - 0
docs/design.md

@@ -0,0 +1,736 @@
+# Tool Agent 项目设计方案
+
+## 1. 项目概述
+
+- 项目名称:tool_agent
+- 项目目标:构建一个可以自动封装、自动接入、自动部署、自动编写工具的 Agent + 工具库系统
+- 目标用户:其他 Agent 系统(作为工具供应商角色)
+
+## 2. 系统定位
+
+tool_agent 是一个本地常驻的智能工具管理系统,内部由两个 Agent 协作驱动:
+
+```
+┌─────────────────────────────────────────────┐
+│              Router Agent                    │
+│  统领全局,维护路由层                         │
+│  - 对外接口管理(FastAPI/MCP/WS)            │
+│  - 请求分发与调度                             │
+│  - 冷热调度、健康监控                         │
+│  - 中间件管理(鉴权/缓存/计量)              │
+├─────────────────────────────────────────────┤
+│              Code Agent                      │
+│  维护工具库                                   │
+│  - 编写/获取/部署新工具                       │
+│  - staging 验证与 promote                    │
+│  - 逆向 API 自修复                           │
+│  - 财务管理(账号注册/API 充值)              │
+└─────────────────────────────────────────────┘
+```
+
+| 角色         | 职责                               | 关注点                              |
+| ------------ | ---------------------------------- | ----------------------------------- |
+| Router Agent | 维护路由层,管理对外接口和内部分发 | "怎么调" — 调度、监控、流量         |
+| Code Agent   | 维护工具库,获取和生产工具         | "有什么可调" — 工具供给、质量、演进 |
+
+两者通过内部消息通信:
+
+- Code Agent 完成新工具 promote 后,通知 Router Agent 更新注册表和路由规则
+- Router Agent 发现工具异常时,通知 Tool Agent 介入修复
+
+## 3. 对外接口
+
+### 3.1 工具目录查询
+
+外部 Agent 请求获取当前可用工具列表,按类别展示,供其挑选。
+
+### 3.2 工具调用执行
+
+外部 Agent 选定工具后发起调用请求,tool_agent 启动对应工具服务,执行并返回结果。
+
+### 3.3 新工具需求响应
+
+外部 Agent 提出工具库中不存在的工具需求时,tool_agent 自主决策获取方式:
+
+| 策略            | 说明                             |
+| --------------- | -------------------------------- |
+| A. 注册购买 API | 直接对接第三方付费/免费 API      |
+| B. 本地部署     | 拉取开源项目,本地或 Docker 部署 |
+| C. 逆向 API     | 注册账号后逆向分析目标服务接口   |
+| D. Browser-Use  | 通过浏览器自动化操作网页工具     |
+
+## 4. 工具库三层架构
+
+```
+┌─────────────────────────────────────────────┐
+│              外部 Agent 请求                  │
+└──────────────────┬──────────────────────────┘
+                   │
+                   ▼
+┌─────────────────────────────────────────────┐
+│            注册层 (Registry)                  │
+│  - 工具分类注册                               │
+│  - 统一接口封装(输入/输出规范)               │
+│  - 工具元信息描述(用法、参数、返回值)        │
+└──────────────────┬──────────────────────────┘
+                   │
+                   ▼
+┌─────────────────────────────────────────────┐
+│     路由层 (Router) — 兼任对外网关            │
+│  - 对外:FastAPI (HTTP+SSE) / MCP 接口       │
+│  - 请求解析与工具匹配                         │
+│  - 冷热调度(按需唤醒/空闲休眠)              │
+│  - 环境感知(工具在哪、怎么启动、怎么对接)    │
+│  - 参数转换与传递                             │
+│  - 中间件:鉴权 / 结果缓存 / 调用计量        │
+└──────────────────┬──────────────────────────┘
+                   │
+                   ▼
+┌─────────────────────────────────────────────┐
+│            环境层 (Runtime)                   │
+│  - 工具物理存储与管理                         │
+│  - 环境隔离(本地进程 / Docker 容器)         │
+│  - 生命周期管理(启动/停止/健康检查)          │
+│  - 资源配额(CPU/内存/显存限制)               │
+└─────────────────────────────────────────────┘
+```
+
+## 5. 接口设计
+
+### 5.1 对外接口(路由层直接暴露)
+
+路由层同时承担网关职责,对外仅暴露两个固定端口:
+
+| 协议                         | 端口 | 用途                                          |
+| ---------------------------- | ---- | --------------------------------------------- |
+| FastAPI (HTTP)               | 8001 | RESTful 接口(含 SSE 流式推送)               |
+| MCP (Model Context Protocol) | 8001 | 标准 MCP Server,供支持 MCP 的 Agent 直接对接 |
+
+不单独开 WebSocket 端口。流式返回用 SSE,异步任务用回调/轮询,理由:
+
+- SSE 基于普通 HTTP,单向推流,客户端实现简单,出问题好调试
+- 外部 Agent 多为短生命周期进程,维护 WebSocket 长连接反而是负担
+- 如果未来有明确的多事件长连接需求,再加 WebSocket 也不迟
+
+#### FastAPI 路由
+
+```
+GET  /tools                     # 获取工具目录(按类别)
+GET  /tools/{tool_id}/schema    # 获取单个工具的输入输出规范
+POST /tools/{tool_id}/invoke    # 调用工具(Accept: text/event-stream 时返回 SSE)
+POST /tools/request             # 提交新工具需求,返回 task_id
+GET  /tasks/{task_id}/status    # 异步任务状态轮询
+GET  /health                    # 服务健康检查
+GET  /openapi.json              # OpenAPI 定义自动导出
+```
+
+#### 异步任务(新工具需求)
+
+```json
+// POST /tools/request
+{
+  "description": "需要一个图片压缩工具",
+  "callback_url": "http://caller-agent/callback"   // 可选
+}
+
+// 响应
+{
+  "task_id": "uuid",
+  "status": "pending"
+}
+
+// 任务完成后:
+// 方式 A:tool_agent POST callback_url 推送结果
+// 方式 B:外部 Agent 轮询 GET /tasks/{task_id}/status
+```
+
+#### MCP 接口
+
+以标准 MCP Tool 协议暴露,每个注册工具自动映射为一个 MCP Tool,外部 Agent 可通过 `tools/list` 和 `tools/call` 直接使用。
+
+### 5.2 内部接口(按环境分层)
+
+uv 本地工具各自独立 venv,通过子进程 + stdio JSON 通信(零网络开销,保持环境隔离);Docker 工具通过端口映射 HTTP 通信。
+
+```
+                    外部 Agent
+                        │
+              ┌─────────┴─────────┐
+              ▼                   ▼
+        :8001 FastAPI        :8001 MCP
+        (HTTP + SSE)
+              └─────────┬─────────┘
+                        │ 路由层按 tool_id 分发
+              ┌─────────┴─────────┐
+              ▼                   ▼
+     ┌── uv 本地工具 ──┐   ┌── Docker 工具 ──┐
+     │  子进程 + stdio  │   │  HTTP 端口映射   │
+     │  各自独立 venv   │   │  :9001  :9002   │
+     │  tool_a  tool_c  │   │  tool_b  tool_d │
+     └─────────────────┘   └─────────────────┘
+```
+
+#### uv 本地工具接口规范(子进程 + stdio JSON)
+
+每个 uv 工具必须实现统一的 Python 入口,通过 stdin 接收请求、stdout 输出结果:
+
+```python
+# tools/local/tool_a/main.py
+import sys, json
+
+class Tool:
+    name: str = "tool_a"
+    description: str = "工具描述"
+
+    def schema(self) -> dict:
+        return {"input": {...}, "output": {...}}
+
+    def run(self, params: dict, stream: bool = False) -> dict:
+        return {"result": ...}
+
+    def health(self) -> bool:
+        return True
+
+if __name__ == "__main__":
+    tool = Tool()
+    request = json.loads(sys.stdin.read())
+    action = request.get("action", "run")
+
+    if action == "schema":
+        output = tool.schema()
+    elif action == "health":
+        output = {"healthy": tool.health()}
+    else:
+        output = tool.run(request.get("params", {}), request.get("stream", False))
+
+    json.dump(output, sys.stdout)
+```
+
+路由层调用方式:
+
+```python
+import subprocess, json
+
+def call_local_tool(tool_id: str, request: dict) -> dict:
+    result = subprocess.run(
+        ["uv", "run", "--directory", f"tools/local/{tool_id}", "python", "main.py"],
+        input=json.dumps(request),
+        capture_output=True, text=True
+    )
+    return json.loads(result.stdout)
+```
+
+- 每次调用启动独立子进程,天然隔离
+- 无需管理端口或长驻进程
+- 冷启动有一定开销,高频工具可考虑进程池复用
+
+#### Docker 工具接口规范(HTTP)
+
+Docker 工具因环境隔离,必须在容器内启动 HTTP 服务:
+
+```
+POST /run          # 执行工具主逻辑(支持流式 SSE)
+GET  /health       # 健康检查
+GET  /schema       # 返回自身输入输出 schema
+```
+
+请求/响应格式:
+
+```json
+// POST /run 请求
+{
+  "request_id": "uuid",
+  "params": { },
+  "stream": false
+}
+
+// POST /run 响应(非流式)
+{
+  "request_id": "uuid",
+  "status": "success | error",
+  "result": { },
+  "error": "错误信息(可选)"
+}
+
+// POST /run 响应(流式,Content-Type: text/event-stream)
+data: {"request_id": "uuid", "chunk": "部分结果...", "done": false}
+data: {"request_id": "uuid", "chunk": "最终结果", "done": true}
+```
+
+#### 路由层中间件
+
+| 中间件   | 作用                                                     |
+| -------- | -------------------------------------------------------- |
+| 结果缓存 | 相同 tool_id + params 的重复调用直接返回缓存,可配置 TTL |
+| 调用计量 | 记录每个工具的调用频次、耗时、错误率,供调度决策使用     |
+
+### 5.3 环境间通信
+
+| 场景                   | 通信方式                  | 说明                                                |
+| ---------------------- | ------------------------- | --------------------------------------------------- |
+| uv 工具 ↔ 路由层       | 子进程 stdio JSON         | `uv run` 启动独立 venv,stdin/stdout 传参,天然隔离 |
+| Docker(conda) ↔ 路由层 | localhost:{映射端口} HTTP | 容器端口映射到宿主机                                |
+| Docker ↔ Docker        | docker network            | 同一 bridge 内可直接通信                            |
+| uv ↔ Docker(conda)     | 经路由层中转              | 不直接通信,统一走路由层                            |
+
+### 5.4 端口分配与服务发现
+
+```
+对外(固定):
+  8001        - FastAPI(HTTP + SSE)
+  8001        - MCP Server
+
+对内:
+  uv 本地工具  - 无需端口,子进程 stdio 通信
+  Docker 工具  - 9001+ 端口映射,按注册顺序递增
+  9000         - 路由层管理端口
+```
+
+- 对外只有 8001/8001,外部 Agent 永远只访问这两个入口
+- uv 工具零端口占用,路由层 import 后直接调用
+- Docker 工具启动时分配端口,写入注册表,停止后回收
+
+#### 备选:Unix Domain Sockets (UDS)
+
+对于同机通信场景,可选用 UDS 替代 TCP 端口:
+
+- 路径规范:`/tmp/tool_agent/sockets/{tool_id}.sock`
+- 优势:不占端口号、无网络协议栈开销、速度更快
+- 适用于 uv 本地工具;Docker 工具仍需端口映射
+
+#### 注册表增强
+
+`registry.json` 中每个工具条目增加调度相关字段:
+
+```json
+{
+  "tool_id": "tool_a",
+  "port": 9001,
+  "socket": "/tmp/tool_agent/sockets/tool_a.sock",
+  "last_used_time": "2026-03-19T10:00:00Z",
+  "call_count": 42,
+  "avg_latency_ms": 120,
+  "state": "running | sleeping | stopped"
+}
+```
+
+### 5.5 部署设计
+
+#### 主服务(tool_agent 本体)
+
+- 运行环境:uv 管理的 Python 虚拟环境
+- 启动方式:`uv run python -m tool_agent`
+- 包含:路由层(对外接口 + 内部分发)+ MCP Server + Agent 逻辑
+
+#### uv 本地工具
+
+- 每个工具一个独立目录,各自 `pyproject.toml`
+- 由路由层通过 `uv run` 启动子进程
+- 适用于轻量工具、纯 Python 工具
+
+#### Docker 工具(conda 环境)
+
+- 每个工具一个 Dockerfile,内部使用 conda 管理依赖
+- 由路由层通过 Docker SDK 启动/停止容器
+- 适用于:重依赖工具、GPU 工具、需要特殊系统库的工具
+
+```
+tools/
+├── local/                    # uv 本地工具(生产)
+│   ├── tool_a/
+│   │   ├── pyproject.toml
+│   │   ├── main.py           # 实现 /run /health /schema
+│   │   └── ...
+│   └── tool_b/
+├── docker/                   # Docker 工具(生产)
+│   ├── tool_c/
+│   │   ├── Dockerfile
+│   │   ├── environment.yml   # conda 环境定义
+│   │   ├── main.py
+│   │   └── ...
+│   └── tool_d/
+├── staging/                  # 预发环境(Agent 新写的工具先放这里)
+│   └── tool_new/
+│       ├── main.py
+│       └── test_tool.py      # Agent 自动生成的测试脚本
+└── registry.json             # 工具注册表
+```
+
+### 5.6 冷热调度机制
+
+路由层根据工具类型和使用频率,自动管理工具进程的生命周期:
+
+| 类型              | 策略        | 说明                                                       |
+| ----------------- | ----------- | ---------------------------------------------------------- |
+| 冷工具(uv 本地) | 按需唤醒    | 收到请求时 `uv run` 启动,空闲超时后自动杀掉进程释放内存   |
+| 热工具(Docker)  | 常驻 + 置换 | 高频工具保持容器运行;显存/内存满时按 LRU 策略置换低频容器 |
+| API 代理          | 无状态      | 无需管理进程,直接转发                                     |
+
+调度参数(可配置):
+
+```json
+{
+  "cold_tool_idle_timeout_s": 300,
+  "hot_tool_max_containers": 5,
+  "eviction_policy": "lru"
+}
+```
+
+### 5.7 Staging 预发环境
+
+Agent 自主编写的新工具不直接进入生产目录,需经过预发验证:
+
+```
+Agent 编写代码 → staging/ 目录
+       ↓
+路由层分配临时端口启动
+       ↓
+Agent 编写并执行测试脚本
+       ↓
+  ┌─ 通过 → Promote 到 local/ 或 docker/,注册到 registry
+  └─ 失败 → Agent 修复后重试,或标记为 failed 等待人工介入
+```
+
+安全审计:Agent 生成的代码在 promote 前需通过基础安全检查(禁止危险系统调用如 `rm -rf`、`os.system` 等)。
+
+### 5.8 逆向 API 自修复
+
+当逆向接入的工具出现故障时,触发自动修复闭环:
+
+```
+工具返回 error(403/签名失效/接口变更)
+       ↓
+路由层标记工具状态为 degraded
+       ↓
+通知 Tool Agent 介入
+       ↓
+  ┌─ 尝试 Browser-Use 重新抓包更新接口参数
+  ├─ 尝试切换到备用 API 策略
+  └─ 均失败 → 标记 inactive,通知调用方降级
+```
+
+对外部调用者完全透明:调用方只看到短暂延迟或降级通知,无需感知内部策略切换。
+
+## 6. 双 Agent 架构
+
+### 6.1 Router Agent(路由层维护者)
+
+常驻进程,统领全局,是系统的"大脑"。
+
+| 能力         | 说明                                                  |
+| ------------ | ----------------------------------------------------- |
+| 对外接口管理 | 维护 FastAPI / MCP / WebSocket 服务,处理外部请求     |
+| 请求路由     | 解析请求,匹配工具,分发到 uv 子进程或 Docker 容器    |
+| 冷热调度     | 按需唤醒冷工具、LRU 置换热工具、管理进程/容器生命周期 |
+| 健康监控     | 定期检查工具状态,发现异常时通知 Tool Agent 修复      |
+| 中间件管理   | 鉴权、结果缓存、调用计量、流量控制                    |
+| 注册表维护   | 接收 Tool Agent 的 promote 通知,更新路由规则         |
+
+### 6.2 Tool Agent(工具库维护者)
+
+按需唤醒或常驻,负责工具的"生产"和"维修"。
+
+| 能力          | 说明                                                               |
+| ------------- | ------------------------------------------------------------------ |
+| 工程编码      | 拥有独立工作区(staging/),自主编写工具代码、测试脚本、接口适配器 |
+| 代码审计      | 对生成代码进行安全检查(禁止危险系统调用、SQL 注入等)             |
+| 财务能力      | 自动注册第三方账号、管理 API Key、自动充值/订阅付费 API            |
+| 决策推理      | 分析工具需求,选择最优获取策略(API/部署/逆向/浏览器)             |
+| Browser-Use   | 浏览器自动化,用于网页操作、信息采集、账号注册、逆向抓包           |
+| 子 Agent 调度 | 将子任务分发给专用子 Agent 并行处理                                |
+| 自修复        | 接收 Router Agent 的异常通知,自动修复失效工具                     |
+| 知识维护      | 实时总结工具库结构变更,维护各层元信息                             |
+
+### 6.3 双 Agent 协作流程
+
+```
+外部 Agent 请求新工具
+       │
+       ▼
+Router Agent 收到请求,查注册表无此工具
+       │
+       ▼ 通知
+Tool Agent 介入 → 决策获取策略 → 编码/部署/购买
+       │
+       ▼
+staging 验证通过 → promote
+       │
+       ▼ 通知
+Router Agent 更新注册表 + 路由规则
+       │
+       ▼
+Router Agent 响应外部 Agent:工具已就绪
+```
+
+```
+Router Agent 健康检查发现工具异常
+       │
+       ▼ 通知
+Tool Agent 介入 → 自修复(重新抓包/切换策略)
+       │
+       ▼ 修复完成通知
+Router Agent 更新工具状态为 active
+```
+
+### 6.4 财务管理(Tool Agent)
+
+Agent 在获取新工具时可能涉及付费操作,需要一套财务管控机制:
+
+```json
+{
+  "budget": {
+    "monthly_limit_usd": 100,
+    "single_tx_limit_usd": 20,
+    "require_approval_above_usd": 10,
+    "spent_this_month_usd": 0
+  },
+  "accounts": [
+    {
+      "provider": "openai",
+      "api_key": "sk-***",
+      "balance_usd": 50,
+      "auto_recharge": false
+    }
+  ]
+}
+```
+
+- 低于单笔限额:Agent 自主完成注册/充值
+- 超过审批阈值:暂停操作,通知用户审批后继续
+- 所有支出记录到 `billing_log.json`,可追溯
+
+### 6.5 独立编码工作区(Tool Agent)
+
+Agent 编写代码的完整闭环:
+
+```
+staging/
+├── {task_id}/              # 每个编码任务一个隔离目录
+│   ├── main.py             # Agent 编写的工具代码
+│   ├── test_main.py        # Agent 编写的测试脚本
+│   ├── pyproject.toml      # 依赖声明
+│   └── run_log.txt         # 测试运行日志
+```
+
+工作流程:
+
+```
+1. 创建任务目录 staging/{task_id}/
+2. Agent 编写代码 + 测试脚本
+3. 在隔离环境中 uv run pytest 执行测试
+4. 测试失败 → Agent 读取日志 → 修复 → 重新测试(最多 N 轮)
+5. 测试通过 → 安全审计 → Promote 到生产目录
+```
+
+### 6.6 运行模式
+
+- Router Agent:常驻进程,始终在线,通过 WebSocket 保持长连接
+- Tool Agent:按需唤醒(收到新工具需求或异常通知时启动),也可常驻
+- 两者通过进程内消息队列通信,无需额外网络开销
+
+## 7. 数据模型(工具元信息)
+
+```json
+{
+  "tool_id": "string",
+  "name": "工具名称",
+  "category": "分类标签",
+  "description": "功能描述",
+  "input_schema": {},
+  "output_schema": {},
+  "stream_support": false,
+  "runtime": {
+    "type": "local | docker | api | browser",
+    "entry": "启动入口",
+    "env": {},
+    "resource_limits": {
+      "cpu": "1.0",
+      "memory_mb": 512,
+      "gpu": false
+    }
+  },
+  "scheduling": {
+    "mode": "cold | hot | stateless",
+    "idle_timeout_s": 300,
+    "state": "running | sleeping | stopped | degraded"
+  },
+  "stats": {
+    "last_used_time": "ISO8601",
+    "call_count": 0,
+    "avg_latency_ms": 0,
+    "error_rate": 0.0
+  },
+  "fallback_strategy": ["api", "browser"],
+  "status": "active | inactive | staging | building"
+}
+```
+
+## 8. 通信协议
+
+### 8.1 对外协议(HTTP + SSE)
+
+```json
+// 同步调用 POST /tools/{tool_id}/invoke
+// 请求
+{
+  "params": { },
+  "stream": false
+}
+
+// 响应(非流式)
+{
+  "status": "success | error",
+  "result": { }
+}
+
+// 响应(流式,Accept: text/event-stream)
+data: {"chunk": "部分结果...", "done": false}
+data: {"chunk": "最终结果", "done": true}
+```
+
+### 8.2 内部协议(双 Agent 消息总线)
+
+Router Agent 与 Tool Agent 通过 `asyncio.Queue` 通信,零网络开销:
+
+```python
+# 消息格式
+{
+  "type": "tool_request | tool_ready | tool_error | health_alert",
+  "payload": { }
+}
+```
+
+## 9. 项目代码结构
+
+```
+tool_agent/
+├── pyproject.toml                  # 主项目依赖(fastapi, uvicorn, docker, mcp-sdk 等)
+├── README.md
+│
+├── src/
+│   └── tool_agent/
+│       ├── __init__.py
+│       ├── __main__.py             # 入口:uv run python -m tool_agent
+│       ├── config.py               # 全局配置(端口、调度参数、预算等)
+│       │
+│       ├── router/                 # Router Agent — 路由层
+│       │   ├── __init__.py
+│       │   ├── agent.py            # Router Agent 主逻辑(调度决策、健康监控)
+│       │   ├── server.py           # FastAPI 应用定义 + 路由注册 + SSE 流式
+│       │   ├── mcp_server.py       # MCP Server 适配层
+│       │   ├── dispatcher.py       # 请求分发:按 tool_id 路由到 uv 子进程 / Docker
+│       │   ├── scheduler.py        # 冷热调度(唤醒/休眠/LRU 置换)
+│       │   ├── middleware/
+│       │   │   ├── __init__.py
+│       │   │   ├── auth.py         # 鉴权
+│       │   │   ├── cache.py        # 结果缓存
+│       │   │   └── metrics.py      # 调用计量
+│       │   └── health.py           # 工具健康检查
+│       │
+│       ├── tool/                   # Tool Agent — 工具库维护
+│       │   ├── __init__.py
+│       │   ├── agent.py            # Tool Agent 主逻辑(决策、编排)
+│       │   ├── builder.py          # 工具编码:生成代码 + 测试脚本
+│       │   ├── deployer.py         # 工具部署:uv init / docker build
+│       │   ├── promoter.py         # staging → 生产目录 promote 流程
+│       │   ├── auditor.py          # 代码安全审计
+│       │   ├── repairer.py         # 逆向 API 自修复
+│       │   ├── browser.py          # Browser-Use 能力封装
+│       │   └── finance.py          # 财务管理(账号注册/充值/预算)
+│       │
+│       ├── registry/               # 注册层
+│       │   ├── __init__.py
+│       │   ├── registry.py         # 注册表 CRUD(读写 registry.json)
+│       │   ├── catalog.py          # 工具目录(按类别组织、搜索)
+│       │   └── schema.py           # 工具元信息 schema 定义(Pydantic models)
+│       │
+│       ├── runtime/                # 环境层
+│       │   ├── __init__.py
+│       │   ├── local_runner.py     # uv 子进程调用(subprocess + stdio JSON)
+│       │   ├── docker_runner.py    # Docker 容器管理(Docker SDK)
+│       │   ├── api_proxy.py        # 外部 API 代理转发
+│       │   └── resource.py         # 资源配额管理(CPU/内存/显存)
+│       │
+│       ├── messaging.py            # 双 Agent 内部消息队列(asyncio.Queue)
+│       └── models.py               # 公共数据模型(请求/响应/工具元信息)
+│
+├── tools/                          # 工具库(与主项目代码分离)
+│   ├── local/                      # uv 本地工具(生产)
+│   │   └── example_tool/
+│   │       ├── pyproject.toml
+│   │       └── main.py
+│   ├── docker/                     # Docker 工具(生产)
+│   │   └── example_gpu_tool/
+│   │       ├── Dockerfile
+│   │       ├── environment.yml
+│   │       └── main.py
+│   └── staging/                    # 预发环境
+│       └── .gitkeep
+│
+├── data/                           # 运行时数据
+│   ├── registry.json               # 工具注册表
+│   ├── billing_log.json            # 财务支出记录
+│   └── config.json                 # 运行时配置覆盖
+│
+└── tests/
+    ├── test_dispatcher.py
+    ├── test_registry.py
+    ├── test_scheduler.py
+    └── test_local_runner.py
+```
+
+### 9.1 模块职责映射
+
+| 目录                       | 归属              | 职责                                          |
+| -------------------------- | ----------------- | --------------------------------------------- |
+| `src/tool_agent/router/`   | Router Agent      | 对外接口、请求分发、调度、中间件、健康监控    |
+| `src/tool_agent/tool/`     | Tool Agent        | 工具编码、部署、审计、修复、财务、browser-use |
+| `src/tool_agent/registry/` | 共享              | 注册表读写,双 Agent 都会访问                 |
+| `src/tool_agent/runtime/`  | Router Agent 调用 | 实际执行工具的运行时(子进程/Docker/API代理) |
+| `tools/`                   | Tool Agent 维护   | 工具代码本身,与主项目解耦                    |
+| `data/`                    | 共享              | 运行时状态数据                                |
+
+### 9.2 关键入口
+
+```python
+# src/tool_agent/__main__.py
+import asyncio
+from tool_agent.router.agent import RouterAgent
+from tool_agent.tool.agent import ToolAgent
+from tool_agent.messaging import MessageBus
+
+async def main():
+    bus = MessageBus()
+    router_agent = RouterAgent(bus)
+    tool_agent = ToolAgent(bus)
+
+    await asyncio.gather(
+        router_agent.start(),   # 启动 FastAPI/MCP/WS + 调度循环
+        tool_agent.start(),     # 监听消息队列,按需处理任务
+    )
+
+if __name__ == "__main__":
+    asyncio.run(main())
+```
+
+## 10. 里程碑计划
+
+| 阶段    | 内容         | 说明                                                 |
+| ------- | ------------ | ---------------------------------------------------- |
+| Phase 1 | 基础框架     | 路由层(FastAPI + MCP + SSE)+ 注册层 + 工具目录查询 |
+| Phase 2 | 路由与执行   | 路由层实现 + 冷热调度 + 本地工具调用链路打通         |
+| Phase 3 | 环境层       | Docker 隔离 + 资源配额 + 工具生命周期管理            |
+| Phase 4 | Staging 闭环 | 预发环境 + 代码审计 + 自动测试 + Promote 流程        |
+| Phase 5 | Agent 智能   | 新工具自动获取(API/部署/逆向/browser-use)+ 自修复  |
+| Phase 6 | 自维护       | 知识总结、健康检查、工具自动更新、调用缓存           |
+
+## 11. 风险与应对
+
+| 风险                | 影响       | 应对措施                                                   |
+| ------------------- | ---------- | ---------------------------------------------------------- |
+| 逆向 API 稳定性差   | 工具不可用 | 自修复闭环:Browser-Use 重新抓包 → 备用策略切换 → 降级通知 |
+| Docker 环境资源占用 | 本地性能   | 冷热调度 + 资源配额(CPU/内存/显存上限)                   |
+| 工具间依赖冲突      | 环境污染   | 严格隔离,每工具独立环境                                   |
+| 外部 Agent 恶意调用 | 安全风险   | 鉴权机制 + 调用频率限制                                    |
+| Agent 生成危险代码  | 系统安全   | staging 预发验证 + 代码安全审计                            |
+| 端口碎片化          | 管理复杂   | UDS 备选方案 + 端口回收 + LRU 淘汰                         |

+ 570 - 0
docs/internal_api.md

@@ -0,0 +1,570 @@
+# Tool Agent 内部接口文档
+
+## 目录
+
+1. [数据模型](#1-数据模型)
+2. [工具注册表格式](#2-工具注册表格式-registryjson)
+3. [容器状态表格式](#3-容器状态表格式-containersjson)
+4. [任务书格式](#4-任务书格式-task_spec)
+5. [内部消息格式](#5-内部消息格式-agentmessage)
+6. [对外 HTTP 接口](#6-对外-http-接口)
+7. [Coding Agent 工具接口](#7-coding-agent-工具接口)
+8. [资源监控格式](#8-资源监控格式)
+9. [配置参数](#9-配置参数)
+10. [工作日志格式](#10-工作日志格式)
+11. [财务记录格式](#11-财务记录格式)
+
+---
+
+## 1. 数据模型
+
+### 1.1 枚举类型
+
+| 枚举 | 值 | 说明 |
+|------|-----|------|
+| RuntimeType | `local`, `docker`, `api`, `browser` | 运行时类型 |
+| SchedulingMode | `cold`, `hot`, `stateless` | 调度模式 |
+| ToolState | `running`, `sleeping`, `stopped`, `degraded` | 工具状态 |
+| ToolStatus | `active`, `inactive`, `staging`, `building` | 工具生命周期 |
+| MessageType | `tool_request`, `tool_ready`, `tool_error`, `health_alert` | 内部消息类型 |
+| ContainerStatus | `running`, `destroyed` | 容器状态 |
+
+### 1.2 ToolMeta(工具元信息)
+
+```json
+{
+  "tool_id": "image_compress_api",
+  "name": "图片压缩 API",
+  "category": "cv",
+  "description": "基于 PIL 的图片压缩服务",
+  "input_schema": {
+    "type": "object",
+    "properties": {
+      "image_path": {"type": "string"},
+      "quality": {"type": "integer", "minimum": 1, "maximum": 100}
+    },
+    "required": ["image_path"]
+  },
+  "output_schema": {
+    "type": "object",
+    "properties": {
+      "compressed_size": {"type": "integer"},
+      "compression_ratio": {"type": "number"}
+    }
+  },
+  "stream_support": false,
+  "runtime": {
+    "type": "docker",
+    "entry": "",
+    "container_id": "25e884ca6cec...",
+    "host_dir": "C:/Users/user/staging/image_tool",
+    "container_dir": "/app",
+    "host_port": 9001,
+    "internal_port": 8080,
+    "endpoint_path": "/api/compress",
+    "http_method": "POST",
+    "env": {},
+    "resource_limits": {"cpu": "1.0", "memory_mb": 512, "gpu": false}
+  },
+  "scheduling": {
+    "mode": "hot",
+    "idle_timeout_s": 300,
+    "state": "running"
+  },
+  "stats": {
+    "last_used_time": "2026-03-20T10:30:00Z",
+    "call_count": 42,
+    "avg_latency_ms": 125.5,
+    "error_rate": 0.02
+  },
+  "fallback_strategy": [],
+  "status": "active"
+}
+```
+
+### 1.3 RuntimeInfo(运行时信息)
+
+| 字段 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `type` | RuntimeType | `"local"` | 运行环境类型 |
+| `container_id` | str | `""` | Docker 容器 ID |
+| `host_dir` | str | `""` | 宿主机工作目录 |
+| `container_dir` | str | `"/app"` | 容器内工作目录 |
+| `host_port` | int | `0` | 宿主机映射端口(Router 调用入口) |
+| `internal_port` | int | `0` | 容器/进程内服务端口 |
+| `endpoint_path` | str | `"/"` | HTTP API 路径 |
+| `http_method` | str | `"POST"` | HTTP 方法 |
+| `env` | dict | `{}` | 环境变量 |
+| `resource_limits` | ResourceLimits | `{}` | 资源限制 |
+
+### 1.4 ContainerInfo(容器信息)
+
+```json
+{
+  "container_id": "25e884ca6cecfa87e19ea737315a8773d...",
+  "tool_id": "test_git_tool",
+  "image": "ubuntu:22.04",
+  "port_mapping": {"8080": 9001, "3306": 9002},
+  "volumes": {"C:/staging/project": "/app"},
+  "mem_limit": "1g",
+  "nano_cpus": 1000000000,
+  "use_gpu": false,
+  "gpu_count": -1,
+  "status": "running",
+  "created_at": "2026-03-20T07:31:50.421101Z",
+  "last_accessed": "2026-03-20T07:33:16.214642+00:00",
+  "destroyed_at": null
+}
+```
+
+---
+
+## 2. 工具注册表格式 (registry.json)
+
+路径:`data/registry.json`
+
+```json
+{
+  "tools": [
+    { /* ToolMeta 对象,见 1.2 */ }
+  ],
+  "version": "1.0"
+}
+```
+
+### Registry 查询接口
+
+**get_endpoint(tool_id)** 返回格式:
+
+Docker 类型:
+```json
+{
+  "type": "docker",
+  "url": "http://localhost:9001/api/compress",
+  "host_port": 9001,
+  "internal_port": 8080,
+  "container_id": "abc123...",
+  "host_dir": "C:/staging/project",
+  "http_method": "POST"
+}
+```
+
+Local 类型:
+```json
+{
+  "type": "local",
+  "host_dir": "C:/tools/local/my_tool",
+  "http_method": "POST",
+  "endpoint_path": "/"
+}
+```
+
+---
+
+## 3. 容器状态表格式 (containers.json)
+
+路径:`data/containers.json`
+
+```json
+{
+  "containers": [
+    { /* ContainerInfo 对象,见 1.4 */ }
+  ]
+}
+```
+
+---
+
+## 4. 任务书格式 (task_spec)
+
+Router Agent 生成任务书,通过 MessageBus 发送给 Coding Agent。
+
+### 4.1 GitHub 项目接入任务
+
+```json
+{
+  "type": "github_deploy",
+  "repo_url": "https://github.com/user/project",
+  "tool_name": "project_api",
+  "runtime": "docker",
+  "image": "python:3.12-slim",
+  "ports": [8080],
+  "description": "将该项目部署为 HTTP API 工具",
+  "requirements": {
+    "gpu": false,
+    "memory_mb": 512
+  },
+  "api_spec": {
+    "endpoint_path": "/api/run",
+    "http_method": "POST",
+    "input_schema": {
+      "type": "object",
+      "properties": {
+        "text": {"type": "string"}
+      }
+    }
+  }
+}
+```
+
+### 4.2 自主编写工具任务
+
+```json
+{
+  "type": "build_tool",
+  "tool_name": "text_summarizer_api",
+  "runtime": "uv",
+  "description": "编写一个文本摘要工具,接受文本输入,返回摘要",
+  "requirements": {
+    "gpu": false,
+    "memory_mb": 256,
+    "dependencies": ["transformers", "torch"]
+  },
+  "api_spec": {
+    "endpoint_path": "/summarize",
+    "http_method": "POST",
+    "input_schema": {
+      "type": "object",
+      "properties": {
+        "text": {"type": "string", "description": "待摘要文本"},
+        "max_length": {"type": "integer", "description": "摘要最大长度"}
+      },
+      "required": ["text"]
+    }
+  }
+}
+```
+
+### 4.3 工具修复任务
+
+```json
+{
+  "type": "repair_tool",
+  "tool_id": "image_compress_api",
+  "error": "HTTP 503: Service Unavailable",
+  "container_id": "abc123...",
+  "description": "工具健康检查失败,需要诊断并修复"
+}
+```
+
+---
+
+## 5. 内部消息格式 (AgentMessage)
+
+Router Agent 与 Coding Agent 通过 `asyncio.Queue` 通信。
+
+```json
+{
+  "type": "tool_request | tool_ready | tool_error | health_alert",
+  "payload": { }
+}
+```
+
+### 5.1 tool_request(Router → Coding)
+
+```json
+{
+  "type": "tool_request",
+  "payload": {
+    "task_spec": "{ /* 任务书 JSON,见第 4 节 */ }",
+    "task_id": "550e8400-e29b-41d4-a716-446655440000",
+    "callback_url": "http://caller/callback"
+  }
+}
+```
+
+### 5.2 tool_ready(Coding → Router)
+
+```json
+{
+  "type": "tool_ready",
+  "payload": {
+    "tool_id": "image_compress_api",
+    "result": "部署成功,工具已注册",
+    "url": "http://localhost:9001/api/compress",
+    "task_id": "550e8400-..."
+  }
+}
+```
+
+### 5.3 tool_error(Coding → Router)
+
+```json
+{
+  "type": "tool_error",
+  "payload": {
+    "tool_id": "failed_tool",
+    "error": "依赖安装失败:torch 需要 CUDA 但未检测到 GPU",
+    "task_id": "550e8400-..."
+  }
+}
+```
+
+### 5.4 health_alert(Router → Coding)
+
+```json
+{
+  "type": "health_alert",
+  "payload": {
+    "tool_id": "image_compress_api",
+    "container_id": "abc123...",
+    "error": "HTTP 503",
+    "last_healthy": "2026-03-20T10:00:00Z"
+  }
+}
+```
+
+---
+
+## 6. 对外 HTTP 接口
+
+基础地址:`http://localhost:8001`
+
+### GET /health
+
+```json
+// Response
+{"status": "ok"}
+```
+
+### GET /tools
+
+返回工具目录(按类别)。
+
+```json
+// Response
+{
+  "tools": [
+    {
+      "tool_id": "image_compress_api",
+      "name": "图片压缩 API",
+      "category": "cv",
+      "description": "基于 PIL 的图片压缩",
+      "status": "active"
+    }
+  ]
+}
+```
+
+### GET /tools/{tool_id}/schema
+
+```json
+// Response
+{
+  "tool_id": "image_compress_api",
+  "input_schema": { /* JSON Schema */ },
+  "output_schema": { /* JSON Schema */ }
+}
+```
+
+### POST /tools/{tool_id}/invoke
+
+```json
+// Request
+{
+  "params": {"image_path": "/path/to/img.jpg", "quality": 85},
+  "stream": false
+}
+
+// Response(非流式)
+{
+  "status": "success",
+  "result": {"compressed_size": 256000, "compression_ratio": 0.25},
+  "error": null
+}
+
+// Response(流式 SSE,Accept: text/event-stream)
+data: {"chunk": "处理中...", "done": false}
+data: {"chunk": "完成", "done": true}
+```
+
+### POST /tools/request
+
+提交新工具需求(异步)。
+
+```json
+// Request
+{
+  "description": "需要一个图片压缩工具",
+  "callback_url": "http://caller/callback"
+}
+
+// Response
+{
+  "task_id": "550e8400-...",
+  "status": "pending"
+}
+```
+
+### GET /tasks/{task_id}/status
+
+```json
+// Response
+{
+  "task_id": "550e8400-...",
+  "status": "completed",
+  "result": {"tool_id": "image_compress_api", "url": "http://localhost:9001/api/compress"}
+}
+```
+
+---
+
+## 7. Coding Agent 工具接口
+
+Coding Agent 通过 claude_agent_sdk 暴露以下 10 个工具:
+
+### Docker 环境
+
+| 工具 | 必填参数 | 可选参数 | 返回 |
+|------|----------|----------|------|
+| `create_docker_env` | `image` | `mem_limit`, `nano_cpus`, `ports`, `volumes`, `use_gpu` | `container_id`, `port_mapping` |
+| `run_in_docker` | `container_id`, `command` | `is_background`, `timeout` | `exit_code`, `stdout`, `stderr` 或 `log_file` |
+| `rebuild_docker_ports` | `container_id`, `ports` | `mem_limit`, `nano_cpus` | `new_container_id`, `port_mapping` |
+| `destroy_docker_env` | `container_id` | — | `status`, `message` |
+
+### 本地 uv 环境
+
+| 工具 | 必填参数 | 可选参数 | 返回 |
+|------|----------|----------|------|
+| `create_uv_project` | `name` | `python_version` | `project_dir` |
+| `run_in_uv` | `project_dir`, `command` | `timeout` | `exit_code`, `stdout`, `stderr` |
+| `uv_add_dependency` | `project_dir`, `package` | `dev` | `status`, `message` |
+
+### 文件操作
+
+| 工具 | 必填参数 | 返回 |
+|------|----------|------|
+| `write_file` | `path`, `content` | `status`, `path`, `size` |
+| `read_file` | `path` | `status`, `path`, `content` |
+
+### 注册
+
+| 工具 | 必填参数 | 可选参数 | 返回 |
+|------|----------|----------|------|
+| `register_tool` | `tool_id`, `name`, `description`, `runtime_type`, `host_port`, `internal_port` | `category`, `input_schema`, `output_schema`, `container_id`, `host_dir`, `endpoint_path`, `http_method` | `status`, `tool_id`, `url` |
+
+---
+
+## 8. 资源监控格式
+
+### SystemInfo(系统概览)
+
+```json
+{
+  "cpu_count": 16,
+  "cpu_percent": 11.1,
+  "memory_total_mb": 15653,
+  "memory_available_mb": 2606,
+  "memory_percent": 83.4,
+  "disk_total_gb": 924.3,
+  "disk_free_gb": 327.3
+}
+```
+
+### ResourceUsage(单工具资源占用)
+
+```json
+{
+  "cpu_cores": 1.0,
+  "memory_mb": 512.0,
+  "gpu_memory_mb": 0.0
+}
+```
+
+### 资源分配检查规则
+
+- CPU:已分配 + 新请求 ≤ 系统总核数
+- 内存:已分配 + 新请求 ≤ 系统总内存 × 80%(保留 20% 安全余量)
+
+---
+
+## 9. 配置参数
+
+### Settings(环境变量前缀:`TOOL_AGENT_`)
+
+| 参数 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `fastapi_port` | int | `8001` | 对外 HTTP 端口 |
+| `mcp_port` | int | `8001` | MCP Server 端口 |
+| `docker_port_start` | int | `9001` | Docker 端口起始 |
+| `docker_base_image` | str | `"agent-sandbox:latest"` | 默认基础镜像 |
+| `docker_mem_limit` | str | `"1g"` | 默认容器内存 |
+| `docker_nano_cpus` | int | `1000000000` | 默认 CPU(1 核) |
+| `docker_ttl_seconds` | int | `1800` | 容器自动清理 TTL |
+| `cold_tool_idle_timeout_s` | int | `300` | 冷工具空闲超时 |
+| `hot_tool_max_containers` | int | `5` | 最大热工具容器数 |
+| `eviction_policy` | str | `"lru"` | 置换策略 |
+| `monthly_limit_usd` | float | `100.0` | 月预算上限 |
+| `single_tx_limit_usd` | float | `20.0` | 单笔上限 |
+| `require_approval_above_usd` | float | `10.0` | 需审批阈值 |
+| `health_check_interval_s` | int | `60` | 健康检查间隔 |
+
+### 运行时配置覆盖 (data/config.json)
+
+```json
+{
+  "cold_tool_idle_timeout_s": 300,
+  "hot_tool_max_containers": 5,
+  "eviction_policy": "lru",
+  "budget": {
+    "monthly_limit_usd": 100,
+    "single_tx_limit_usd": 20,
+    "require_approval_above_usd": 10,
+    "spent_this_month_usd": 0
+  }
+}
+```
+
+---
+
+## 10. 工作日志格式
+
+### LocalRunner 命令日志 (last_run.log)
+
+路径:`{project_dir}/last_run.log`
+
+```
+Command: python main.py
+Exit Code: 0
+--- STDOUT ---
+Server started on port 8080
+--- STDERR ---
+WARNING: Using development server
+```
+
+### Coding Agent 执行日志(stdout)
+
+```
+2026-03-20 15:09:32 [tool_agent.tool.agent] INFO: [CodingAgent] Starting task: 部署 flask 项目...
+2026-03-20 15:09:35 [tool_agent.tool.agent] INFO: [TOOL_USE] create_docker_env | {"image":"python:3.12-slim","ports":[8080]}
+2026-03-20 15:09:40 [tool_agent.tool.agent] INFO: [TEXT] 正在创建 Docker 环境...
+2026-03-20 15:10:15 [tool_agent.tool.agent] INFO: [TOOL_USE] register_tool | {"tool_id":"flask_api",...}
+2026-03-20 15:10:16 [tool_agent.tool.agent] INFO: [DONE] duration=44000ms
+2026-03-20 15:10:16 [tool_agent.tool.agent] INFO: [COST] $0.12
+```
+
+---
+
+## 11. 财务记录格式
+
+路径:`data/billing_log.json`
+
+```json
+{
+  "transactions": [
+    {
+      "provider": "openai",
+      "amount_usd": 5.0,
+      "description": "API Key 充值"
+    }
+  ]
+}
+```
+
+### 预算检查规则
+
+| 条件 | 行为 |
+|------|------|
+| `amount ≤ single_tx_limit_usd` | Agent 自主完成 |
+| `amount > require_approval_above_usd` | 暂停,通知用户审批 |
+| `spent_this_month + amount > monthly_limit_usd` | 拒绝,通知预算超限 |

+ 7162 - 0
docs/liblib_api.md

@@ -0,0 +1,7162 @@
+LiblibAI 图像&视频大模型API 使用说明
+LiblibAI-API产品主页和购买下单:https://www.liblib.art/apis
+产品简介
+欢迎使用LiblibAI x 星流 图像大模型API来进行创作!无论你是进行个人项目还是为其他终端用户提供的企业服务,我们的API都能满足你的需求。
+全新AI图像模型和工作流API,提供极致的图像质量,在输出速度、生图成本和图像卓越性之间实现平衡。
+您有任何问题,可随时电话联系商务:17521599324。
+我们提供了工作流API和5款生图模型API:
+
+- LiblibAI工作流:社区商用工作流和个人本地工作流均可支持调用。工作流挑选和商用查询可至https://www.liblib.art/workflows
+- F.1 Kontext:将文本生成图像与高级图像编辑能力相结合,在真实感、风格一致性和复杂场景还原上均处于行业领先地位。
+- 智能算法IMG 1:以超强风格一致性、Prompt 还原能力为优势。
+- LibDream:对中文指令理解良好,出中文、海报能力最强。
+- 星流Star-3 Alpha:搭载自带LoRA推荐算法,对自然语言的精准响应,能够生成具有照片级真实感的视觉效果,不能自由添加LoRA,仅支持部分ControlNet。
+- LiblibAI自定义模型:若需要特定LoRA和ControlNet只能选此模式,适合高度自由、精准控制和特定风格的场景,基于F.1/XL/v3/v1.5等基础算法,支持自定义调用LiblibAI内全量50万+可商用模型和任意私有模型。
+
+API试用计划:https://www.liblib.art/apis登录后可领取500试用积分,限时7天免费测试体验。
+文档版本更新
+日期
+说明
+2025.12.21
+增加Seedream4.5接口
+增加Kling2.6接口
+2025.11.4
+增加Kling2.5接口
+seedream4.0接口
+2025.8.19
+10 增加可灵生成视频接口
+2025.8.19
+9 增加libDream&libEdit
+2025.6.16
+5 增加F.1 Kontext
+2025.6.16
+6 增加智能算法IMG-1
+2025.4.30
+支持图片上传: LiblibAI-API文件上传
+2025.3.18
+增加F.1-ControlNet(PuLID人像换脸、主体参考)
+2025.1.17
+8 增加调用ComfyUI工作流
+2025.1.2
+3.4 增加Comfyui接入星流API
+2024.12.18
+查询生图结果的返回字段,新增pointsCost(当次任务消耗积分)和accountBalance(账户剩余积分数)
+2024.12.5
+原【进阶模式】更名为【LiblibAI自定义模型】原【简易模式-经典模型XL】不再维护,不再支持新接入开放LiblibAI全网可商用模型和私有模型调用,查询和调用模型接口详见文档4.1.1
+2024.11.15
+支持F.1风格迁移:参考《F.1风格迁移参数示例》
+
+1. 能力地图
+
+- API KEY的使用
+- 星流Star-3生图
+- 自定义模型生图
+- F.1 Kontext
+- 智能算法IMG 1
+- LibDream
+- 图片上传,获取oss地址
+
+2. 开始使用
+   在这一部分,我们将展示如何开通API的权益,以及如何创建你的API密钥。
+   2.1 访问地址
+   Liblib开放平台域名:https://openapi.liblibai.cloud(无法直接打开,需配合密钥访问)
+   2.2 计费规则
+   非固定消耗,每次生图任务消耗的积分与以下参数有关:
+
+- 选用模型
+- 采样步数(steps)
+- 采样方法(sampler,SDE系列会产生额外消耗)
+- 生成图片宽度
+- 生成图片高度
+- 生成图片张数
+- 重绘幅度(denoisingStrength)
+- 高分辨率修复的重绘步数和重绘幅度
+- Controlnet数量
+  2.3 并发数和QPS
+- 生图任务并发数,默认5(因生图需要时间,指同时可进行的生图任务数)
+- 发起生图任务接口,QPS默认1秒1次,(可用每天预计生图张数/24h/60m/60s来估算平均值)
+- 查询生图结果接口,QPS无限制
+  2.4 生成API密钥
+  在登录Liblib领取API试用积分或购买API积分后,Liblib会生成开放平台访问密钥,用于后续API接口访问,密钥包括:
+- AccessKey,API访问凭证,唯一识别访问用户,长度通常在20-30位左右,如:KIQMFXjHaobx7wqo9XvYKA
+- SecretKey,API访问密钥,用于加密请求参数,避免请求参数被篡改,长度通常在30位以上,如:KppKsn7ezZxhi6lIDjbo7YyVYzanSu2d
+  2.4.1 使用密钥
+  申请API密钥之后,需要在每次请求API接口的查询字符串中固定传递以下参数:
+  参数
+  类型
+  是否必需
+  说明
+  AccessKey
+  String
+  是
+  开通开放平台授权的访问AccessKey
+  Signature
+  String
+  是
+  加密请求参数生成的签名,签名公式见下节“生成签名”
+  Timestamp
+  String
+  是
+  生成签名时的毫秒时间戳,整数字符串,有效期5分钟
+  SignatureNonce
+  String
+  是
+  生成签名时的随机字符串
+  如请求地址:https://test.xxx.com/api/genImg?AccessKey=KIQMFXjHaobx7wqo9XvYKA&Signature=test1232132&Timestamp=1725458584000&SignatureNonce=random1232
+  2.4.2 生成签名
+  签名生成公式如下:
+
+# 1. 用"&"拼接参数
+
+# URL地址:以上方请求地址为例,为“/api/genImg”
+
+# 毫秒时间戳:即上节“使用密钥”中要传递的“Timestamp”
+
+# 随机字符串:即上节“使用密钥”中要传递的“SignatureNonce”
+
+原文 = URL地址 + "&" + 毫秒时间戳 + "&" + 随机字符串
+
+# 2. 用SecretKey加密原文,使用hmacsha1算法
+
+密文 = hmacSha1(原文, SecretKey)
+
+# 3. 生成url安全的base64签名
+
+# 注:base64编码时不要补全位数
+
+签名 = encodeBase64URLSafeString(密文)
+Java生成签名示例,以访问上方“使用密钥”的请求地址为例:
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.RandomStringUtils;
+
+public class SignUtil {
+
+    /**
+     * 生成请求签名
+     * 其中相关变量均为示例,请替换为您的实际数据
+     */
+    public static String makeSign() {
+
+        // API访问密钥
+        String secretKey = "KppKsn7ezZxhi6lIDjbo7YyVYzanSu2d";
+
+        // 请求API接口的uri地址
+        String uri = "/api/generate/webui/text2img";
+        // 当前毫秒时间戳
+        Long timestamp = System.currentTimeMillis();
+        // 随机字符串
+        String signatureNonce = RandomStringUtils.randomAlphanumeric(10);
+        // 拼接请求数据
+        String content = uri + "&" + timestamp + "&" + signatureNonce;
+
+        try {
+            // 生成签名
+            SecretKeySpec secret = new SecretKeySpec(secretKey.getBytes(), "HmacSHA1");
+            Mac mac = Mac.getInstance("HmacSHA1");
+            mac.init(secret);
+            return Base64.encodeBase64URLSafeString(mac.doFinal(content.getBytes()));
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("no such algorithm");
+        } catch (InvalidKeyException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}
+Python生成签名示例,以访问上方“使用密钥”的请求地址为例:
+import hmac
+from hashlib import sha1
+import base64
+import time
+import uuid
+
+def make_sign():
+"""
+生成签名
+"""
+
+    # API访问密钥
+    secret_key = 'KppKsn7ezZxhi6lIDjbo7YyVYzanSu2d'
+
+    # 请求API接口的uri地址
+    uri = "/api/genImg"
+    # 当前毫秒时间戳
+    timestamp = str(int(time.time() * 1000))
+    # 随机字符串
+    signature_nonce= str(uuid.uuid4())
+    # 拼接请求数据
+    content = '&'.join((uri, timestamp, signature_nonce))
+
+    # 生成签名
+    digest = hmac.new(secret_key.encode(), content.encode(), sha1).digest()
+    # 移除为了补全base64位数而填充的尾部等号
+    sign = base64.urlsafe_b64encode(digest).rstrip(b'=').decode()
+    return sign
+
+NodeJs 生成签名示例,以访问上方“使用密钥”的请求地址为例:
+const hmacsha1 = require("hmacsha1");
+const randomString = require("string-random");
+// 生成签名
+const urlSignature = (url) => {
+if (!url) return;
+const timestamp = Date.now(); // 当前时间戳
+const signatureNonce = randomString(16); // 随机字符串,你可以任意设置,这个没有要求
+// 原文 = URl地址 + "&" + 毫秒时间戳 + "&" + 随机字符串
+const str = `${url}&${timestamp}&${signatureNonce}`;
+const secretKey = "官网上的 SecretKey "; // 下单后在官网中,找到自己的 SecretKey'
+const hash = hmacsha1(secretKey, str);
+// 最后一步: encodeBase64URLSafeString(密文)
+// 这一步很重要,生成安全字符串。java、Python 以外的语言,可以参考这个 JS 的处理
+let signature = hash
+.replace(/\+/g, "-")
+.replace(/\//g, "\_")
+.replace(/=+$/, "");
+  return {
+    signature,
+    timestamp,
+    signatureNonce,
+  };
+};
+// 例子:原本查询生图进度接口是 https://openapi.liblibai.cloud/api/generate/webui/status
+// 加密后,url 就变更为 https://openapi.liblibai.cloud/api/generate/webui/status?AccessKey={YOUR_ACCESS_KEY}&Signature={签名}&Timestamp={时间戳}&SignatureNonce={随机字符串}
+const getUrl = () => {
+  const url = "/api/generate/webui/status";
+  const { signature, timestamp, signatureNonce } = urlSignature(url);
+  const accessKey = "替换自己的 AccessKey"; // '下单后在官网中,找到自己的 AccessKey'
+  return `${url}?AccessKey=${accessKey}&Signature=${signature}&Timestamp=${timestamp}&SignatureNonce=${signatureNonce}`;
+};
+
+3. 星流Star-3 Alpha
+   3.1 星流Star-3 Alpha生图
+   3.1.1 星流Star-3 Alpha文生图
+
+- 接口:POST /api/generate/webui/text2img/ultra
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  是
+- 星流Star-3 Alpha文生图:5d7e67009b344550bc1aa6ccbfa1d7f4
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+
+- 返回值:
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+- 参数说明
+  变量名
+  格式
+  备注
+  数值范围
+  必填
+  示例
+  prompt
+
+string
+正向提示词,文本
+
+- 不超过2000字符
+- 纯英文文本
+  是
+  {
+  "templateUuid":"5d7e67009b344550bc1aa6ccbfa1d7f4",
+  "generateParams":{
+  "prompt":"1 girl,lotus leaf,masterpiece,best quality,finely detail,highres,8k,beautiful and aesthetic,no watermark,",
+  "aspectRatio":"portrait",
+  //或者配置imageSize设置具体宽高
+  "imageSize": {
+  "width": 768,
+  "height": 1024
+  },
+  "imgCount":1,
+  "steps": 30, // 采样步数,建议30
+          //高级设置,可不填写
+          "controlnet":{
+              "controlType":"depth",
+              "controlImage": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/7c1cc38e-522c-43fe-aca9-07d5420d743e.png",
+          }
+      }
+  }
+
+aspectRatio
+string
+图片宽高比预设
+,与imageSize二选一配置即可
+
+1. square:
+
+- 宽高比:1:1,通用
+- 具体尺寸:1024\*1024
+
+2. portrait:
+1. 宽高比:3:4,适合人物肖像
+1. 具体尺寸:768\*1024
+1. landscape:
+1. 宽高比:16:9,适合影视画幅
+1. 具体尺寸:1280\*720
+   二选一配置
+
+imageSize
+Object
+图片具体宽高,与aspectRatio二选一配置即可
+
+1. width:int,512~2048
+2. height:int,512~2048
+
+imgCount
+int
+单次生图张数
+1 ~ 4
+是
+
+controlnet
+Object
+构图控制
+
+1. controlType:
+1. line:线稿轮廓
+1. depth:空间关系
+1. pose:人物姿态
+1. IPAdapter:风格迁移
+1. controlImage:参考图可公网访问的完整URL
+   否
+
+3.1.2 星流Star-3 Alpha图生图
+
+- 接口:POST /api/generate/webui/img2img/ultra
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUUID
+  string
+  是
+- 星流Star-3 Alpha图生图:07e00af4fc464c7ab55ff906f8acf1b7
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+
+- 返回值:
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+- 参数说明
+  变量名
+  格式
+  备注
+  数值范围
+  必填
+  示例
+  prompt
+
+string
+正向提示词,文本
+
+- 不超过2000字符
+- 纯英文文本
+  是
+
+https://liblibai.feishu.cn/sync/TF7jdgTOOsQCP4bxO2bcib7znsg
+sourceImage
+string
+参考图URL
+参考图可公网访问的完整URL
+是
+
+imgCount
+int
+单次生图张数
+1 ~ 4
+是
+
+controlnet
+Object
+构图控制
+
+1. controlType:
+1. line:线稿轮廓
+1. depth:空间关系
+1. pose:人物姿态
+1. IPAdapter:风格迁移
+1. controlImage:参考图可公网访问的完整URL
+   否
+
+3.2 查询生图结果
+
+- 接口:POST /api/generate/webui/status
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  备注
+  generateUuid
+  string
+  是
+  生图任务uuid,发起生图任务时返回该字段
+- 返回值:
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  generateStatus
+  int
+  生图状态见下方3.3.1节
+  percentCompleted
+  float
+  生图进度,0到1之间的浮点数,(暂未实现)
+  generateMsg
+  string
+  生图信息,提供附加信息,如生图失败信息
+  pointsCost
+  int
+  本次生图任务消耗积分数
+  accountBalance
+  int
+  账户剩余积分数
+  images
+  []object
+  图片列表,只提供审核通过的图片
+  images.0.imageUrl
+  string
+  图片地址,可直接访问,地址有时效性:7天
+  images.0.seed
+  int
+  随机种子值
+  images.0.auditStatus
+  int
+  审核状态见下方4.3.1节
+  示例:
+  {
+  "code": 0,
+  "msg": "",
+  "data": {
+  "generateUuid": "8dcbfa2997444899b71357ccb7db378b",
+  "generateStatus": 5,
+  "percentCompleted": 0,
+  "generateMsg": "",
+  "pointsCost": 10,// 本次任务消耗积分数
+  "accountBalance": 1356402,// 账户剩余积分数
+  "images": [
+  {
+  "imageUrl": "https://liblibai-online.liblib.cloud/sd-images/08efe30c1cacc4bb08df8585368db1f9c082b6904dd8150e6e0de5bc526419ee.png",
+  "seed": 12345,
+  "auditStatus": 3
+  }
+  ]
+  }
+  }
+  3.3 参数模版预设
+  还提供了一些封装后的参数预设,您可以只提供必要的生图参数,极大简化了配置成本,欢迎体验~
+  3.3.1 模版选择(templateUuid)
+  模板名称
+  模板UUID
+  备注
+  星流Star-3 Alpha文生图
+  5d7e67009b344550bc1aa6ccbfa1d7f4
+- Checkpoint默认为官方自研模型Star-3 Alpha
+- 支持指定的几款Controlnet
+  星流Star-3 Alpha图生图
+  07e00af4fc464c7ab55ff906f8acf1b7
+- Checkpoint默认为官方自研模型Star-3 Alpha
+- 支持指定的几款Controlnet
+  3.3.2 模版传参示例
+  以下提供了调用各类模版时的传参示例,方便您理解不同模版的使用方式。
+  注:如果要使用如下参数示例生图,请把其中的注释删掉后再使用。
+  星流Star-3 Alpha文生图 - 简易版本
+  https://liblibai.feishu.cn/sync/AjdCdCiVHsxk2IblvGzcINM1nde
+  星流Star-3 Alpha图生图 - 简易版本
+  https://liblibai.feishu.cn/sync/TF7jdgTOOsQCP4bxO2bcib7znsg
+  F.1 - 主体参考参数示例(仅支持文生图)
+- 接口:POST /api/generate/webui/text2img/ultra
+  {
+  "templateUuid":"5d7e67009b344550bc1aa6ccbfa1d7f4",
+  "generateParams":{
+  "prompt": "A fluffy cat lounges on a plush cushion.",
+  "promptMagic": 1,
+  "aspectRatio":"square",
+  "imgCount":1 ,
+
+          "controlnet":{
+              "controlType":"subject",
+              "controlImage": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/3c65a38d7df2589c4bf834740385192128cf035c7c779ae2bbbc354bf0efcfcb.png",
+          }
+      }
+
+  }
+
+  3.4 ComfyUI接入星流API
+
+- 准备Comfyui环境,到https://github.com/comfyanonymous/ComfyUI下载免安装文件,解压,有显卡点击run_nvidia_gpu.bat启动Comfyui,没有显卡点击run_cpu.bat启动,启动后保留运行后台不关闭,在web进行配置操作。
+- 下载星流节点文件https://github.com/lib-teamwork/ComfyUI-liblib,放到./ComfyUl/custom_nodes文件夹下。
+- 重启Comfyui打开workflow文件夹,图片生成工作流文件
+- 鉴权信息需要API密钥,appkey对应Accesskey,appsecret对应Secretkey
+  [图片]
+- 建议自己再安装一个comfyui manager维护各种新节点: https://github.com/ltdrdata/ComfyUI-Manager
+
+4. LiblibAI自定义模型
+
+- 可自由调用LiblibAI网站内F.1-dev/XL/v3/v1.5全量模型(暂不支持混元和PixArt),适合高度自由和精准控制的场景。
+- 调用条件
+  - 同账号下的个人主页内所有模型,本地模型可先在LiblibAI官网右上角“发布”上传个人模型,可按需设置“仅个人可见”,即可仅被本账号在API调用,不会被公开查看或调用。
+  - LiblibAI官网内,模型详情页右侧,作者授权“可出售生成图片或用于商业目的”的所有模型。
+
+  4.1 接口文档
+  4.1.1 查询模型版本
+  在LiblibAI网站上挑选作者授权可商用的模型,个人私有模型上传时选择“自见”的模型也可被个人api账号调用,获取模型链接结尾的version_uuid,调接口进行查询。
+  4.1.2 查询模型版本参数示例
+
+- 接口:POST /api/model/version/get
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  versionUuid
+  string
+  是
+  要查询的模型版本uuid
+
+1. 目前Lib已开放全站的可商用模型供API使用,您可以在Lib站内检索可商用的Checkpoint和LoRA模型
+   [图片]
+2. 选择喜欢的模型版本,从浏览器网址中复制versionUuid
+   [图片]
+3. 粘贴到文生图或图生图的参数模板中使用;
+4. 若您忘记了在生图参数中应用的模型是哪一款,您可以调用本接口进行查询。
+   4.1.2.1 返回值示例
+   {
+   "version_uuid": "21df5d84cca74f7a885ba672b5a80d19",//LiblibAI官网模型链接后缀
+   "model_name": "AWPortrait XL"
+   "version_name": "1.1"
+   "baseAlgo": "基础算法 XL",
+   "show_type": "1",//公开可用的模型
+   "commercial_use": "1",//可商用为1,不可商用为0
+   "model_url": "https://www.liblib.art/modelinfo/f8b990b20cb943e3aa0e96f34099d794?versionUuid=21df5d84cca74f7a885ba672b5a80d19"
+   }
+   }
+   4.1.2.2 异常情况:
+   未匹配到:提示“未找到与{version_uuid}对应的模型,请检查version_uuid是否正确,或所选模型是否为Checkpoint或LoRA”;
+   baseAlgo不在给定范围内的,提示“{version_uuid}不在API目前支持的baseAlgo范围内”。
+
+4.1.3 提交文生图任务
+
+- 接口:POST /api/generate/webui/text2img
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  否
+  参数模板uuid
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+
+- 返回值:
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  4.1.3.1 文生图参数示例
+  注:如果要使用如下参数示例生图,请把其中的注释删掉后再使用。
+  {
+  "templateUuid": "e10adc3949ba59abbe56e057f20f883e",
+  "generateParams": {
+  "checkPointId": "0ea388c7eb854be3ba3c6f65aac6bfd3", // 底模 modelVersionUUID
+  "prompt": "Asian portrait,A young woman wearing a green baseball cap,covering one eye with her hand", // 选填
+  "negativePrompt": "ng_deepnegative_v1_75t,(badhandv4:1.2),EasyNegative,(worst quality:2),", //选填
+  "sampler": 15, // 采样方法
+  "steps": 20, // 采样步数
+  "cfgScale": 7, // 提示词引导系数
+  "width": 768, // 宽
+  "height": 1024, // 高
+  "imgCount": 1, // 图片数量  
+   "randnSource": 0, // 随机种子生成器 0 cpu,1 Gpu
+  "seed": 2228967414, // 随机种子值,-1表示随机  
+   "restoreFaces": 0, // 面部修复,0关闭,1开启
+          // Lora添加,最多5个
+          "additionalNetwork": [
+              {
+                  "modelId": "31360f2f031b4ff6b589412a52713fcf", //LoRA的模型版本versionuuid
+                  "weight": 0.3 // LoRA权重
+              },
+              {
+                  "modelId": "365e700254dd40bbb90d5e78c152ec7f", //LoRA的模型版本uuid
+                  "weight": 0.6 // LoRA权重
+              }
+          ],
+
+          // 高分辨率修复
+          "hiResFixInfo": {
+              "hiresSteps": 20, // 高分辨率修复的重绘步数
+              "hiresDenoisingStrength": 0.75, // 高分辨率修复的重绘幅度
+              "upscaler": 10, // 放大算法模型枚举
+              "resizedWidth": 1024,  // 放大后的宽度
+              "resizedHeight": 1536  // 放大后的高度
+          }
+      }
+  }
+  4.1.3.2 返回值示例
+  {
+  "code": 0,
+  "msg": "",
+  "data": {
+  "generateUuid": "8dcbfa2997444899b71357ccb7db378b"
+  }
+  }
+  4.1.4 提交图生图任务
+- 接口:POST /api/generate/webui/img2img
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUUID
+  string
+  否
+  参数模板uuid
+
+generateParams
+object
+否
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+
+- 返回值:
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  4.1.4.1 图生图参数示例
+  注:如果要使用如下参数示例生图,请把其中的注释删掉后再使用。
+  {
+  "templateUuid": "9c7d531dc75f476aa833b3d452b8f7ad", // 预设参数模板ID
+  "generateParams": {
+  // 基础参数
+  "checkPointId": "0ea388c7eb854be3ba3c6f65aac6bfd3", //底模
+  "prompt": "1 girl wear sunglasses", //正向提示词
+  "negativePrompt": //负向提示词
+  "clipSkip": 2, // Clip跳过层
+  "sampler": 15, //采样方法
+  "steps": 20, // 采样步数
+  "cfgScale": 7, // 提示词引导系数  
+   "randnSource": 0, // 随机种子来源,0表示CPU,1表示GPU
+  "seed": -1, // 随机种子值,-1表示随机
+  "imgCount": 1, // 1到4
+  "restoreFaces": 0, // 面部修复,0关闭,1开启
+          // 图像相关参数
+          "sourceImage": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/7c1cc38e-522c-43fe-aca9-07d5420d743e.png",
+          "resizeMode": 0, // 缩放模式, 0 拉伸,1 裁剪,2 填充
+          "resizedWidth": 1024, // 图像缩放后的宽度
+          "resizedHeight": 1536, // 图像缩放后的高度
+          "mode": 4, // 0图生图,4局部重绘
+          "denoisingStrength": 0.75, // 重绘幅度
+
+          // Lora添加,最多5个
+          "additionalNetwork": [
+              {
+                  "modelId": "31360f2f031b4ff6b589412a52713fcf", //LoRA的模型版本uuid
+                  "weight": 0.3 // LoRA权重
+              },
+              {
+                  "modelId": "365e700254dd40bbb90d5e78c152ec7f", //LoRA的模型版本uuid
+                  "weight": 0.6 // LoRA权重
+              }
+          ],
+
+          // 局部重绘相关参数
+          "inpaintParam": {
+              "maskImage": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/323fc358-618b-4c7d-b431-7d890209e5a5.png", // 蒙版地址
+              "maskBlur": 4, // 蒙版模糊度
+              "maskPadding": 32, //蒙版边缘预留像素,也称蒙版扩展量
+              "maskMode": 0, // 蒙版模式
+              "inpaintArea": 0, //重绘区域, 0重绘全图,1仅重绘蒙版区域
+              "inpaintingFill": 1 //蒙版内容的填充模式
+          },
+
+          // controlNet,最多4组
+          "controlNet": [
+              {
+                  "unitOrder": 1, // 执行顺序
+                  "sourceImage": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/7c1cc38e-522c-43fe-aca9-07d5420d743e.png",
+                  "width": 1024, // 参考图宽度
+                  "height": 1536, // 参考图高度
+                  "preprocessor": 3, // 预处理器枚举值
+                  "annotationParameters": { // 预处理器参数, 不同预处理器不同,此处仅为示意
+                      "depthLeres": { // 3 预处理器 对应的参数
+                          "preprocessorResolution": 1024,
+                          "removeNear": 0,
+                          "removeBackground": 0
+                      }
+                  },
+                  "model": "6349e9dae8814084bd9c1585d335c24c", // controlnet的模型
+                  "controlWeight": 1, // 控制权重
+                  "startingControlStep": 0, //开始控制步数
+                  "endingControlStep": 1, // 结束控制步数
+                  "pixelPerfect": 1, // 完美像素
+                  "controlMode": 0, // 控制模式 ,0 均衡,1 更注重提示词,2 更注重controlnet,
+                  "resizeMode": 1, // 缩放模式, 0 拉伸,1 裁剪,2 填充
+                  "maskImage": "" // 蒙版图
+              }
+          ]
+      }
+  }
+  4.1.4.2 返回值示例
+  {
+  "code": 0,
+  "msg": "",
+  "data": {
+  "generateUuid": "8dcbfa2997444899b71357ccb7db378b"
+  }
+  }
+  4.1.5 查询生图结果
+- 接口:POST /api/generate/webui/status
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  备注
+  generateUuid
+  string
+  是
+  生图任务uuid,发起生图任务时返回该字段
+- 返回值:
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  generateStatus
+  int
+  生图状态见下方3.3.1节
+  percentCompleted
+  float
+  生图进度,0到1之间的浮点数,(暂未实现生图进度)
+  generateMsg
+  string
+  生图信息,提供附加信息,如生图失败信息
+  pointsCost
+  int
+  本次生图任务消耗积分数
+  accountBalance
+  int
+  账户剩余积分数
+  images
+  []object
+  图片列表,只提供审核通过的图片
+  images.0.imageUrl
+  string
+  图片地址,可直接访问,地址有时效性:7天
+  images.0.seed
+  int
+  随机种子值
+  images.0.auditStatus
+  int
+  审核状态见下方2.5.2节
+  示例:
+  {
+  "code": 0,
+  "msg": "",
+  "data": {
+  "generateUuid": "8dcbfa2997444899b71357ccb7db378b",
+  "generateStatus": 5,
+  "percentCompleted": 0,
+  "generateMsg": "",
+  "pointsCost": 10,// 本次任务消耗积分数
+  "accountBalance": 1356402,// 账户剩余积分数
+  "images": [
+  {
+  "imageUrl": "https://liblibai-online.liblib.cloud/sd-images/08efe30c1cacc4bb08df8585368db1f9c082b6904dd8150e6e0de5bc526419ee.png",
+  "seed": 12345,
+  "auditStatus": 3
+  }
+  ]
+  }
+  }
+  4.2 参数说明
+  4.2.1 文生图基础参数
+  变量名
+  格式
+  备注
+  数值范围
+  必填
+  示例
+  checkPointId
+
+String
+模型uuid
+
+从全网可商用模型和自有模型中选择,详见文档3.1.1
+是
+{
+"templateUuid": "e10adc3949ba59abbe56e057f20f883e", // 参数模板ID
+"generateParams": {
+// 基础参数
+"checkPointId": "0ea388c7eb854be3ba3c6f65aac6bfd3", // 底模 modelVersionUUID
+"vaeId": "",
+"prompt": "Asian portrait,A young woman wearing a green baseball cap,covering one eye with her hand", // 选填
+"negativePrompt": "ng_deepnegative_v1_75t,(badhandv4:1.2),EasyNegative,(worst quality:2),nsfw", //选填
+"clipSkip": 2, // 1到12,正整数值
+"sampler": 15, // 采样方法
+"steps": 20, // 采样步数
+"cfgScale": 7, // 提示词引导系数
+"width": 768, // 宽
+"height": 1024, // 高
+"imgCount": 1, // 图片数量  
+ "randnSource": 0, // 随机种子生成器 0 cpu,1 Gpu
+"seed": -1, // 随机种子值,-1表示随机  
+ "restoreFaces": 0, // 面部修复,0关闭,1开启
+
+        // Lora添加,最多5个
+        "additionalNetwork": [],
+
+        // 高分辨率修复
+        "hiResFixInfo": {},
+
+        // controlNet,最多4组
+        "controlNet": []
+    }
+
+}
+
+additionalNetwork
+
+list[object]
+
+- LoRA组合及权重设置
+- LoRA的基础算法类型需要与checkpoint一致
+  参考additionalNetwork的参数配置
+
+否
+
+vaeId
+String
+VAE的模型uuid
+
+- 从提供的VAE列表中选择
+- 可为空,空值表示取checkpoint的VAE
+  否
+
+prompt
+
+string
+正向提示词,文本
+
+- 不超过2000字符
+- 纯英文文本
+  是
+
+negativePrompt
+string
+负向提示词,文本
+
+- 不超过2000字符
+- 纯英文文本不超过2000字符
+  是
+
+clipSkip
+int
+Clip跳过层
+1 ~ 12。默认值2
+是
+
+sampler
+int
+采样器枚举值
+从采样方法列表中选择
+是
+
+steps
+int
+采样步数
+1 ~ 60
+是
+
+cfgScale
+double
+cfg_scale
+1.0 ~ 15.0
+是
+
+width
+int
+初始宽度
+
+- 范围:128 ~ 1536
+- 基础算法1.5 建议区间:512~768
+- 基础算法XL 建议区间:768~1344
+- 基础算法F.1 建议区间:768~1536
+  是
+
+height
+int
+初始高度
+
+- 范围:128 ~ 1536
+- 基础算法1.5 建议区间:512~768
+- 基础算法XL 建议区间:768~1344
+- 基础算法F.1 建议区间:768~1536
+  是
+
+imgCount
+int
+单次生图张数
+1 ~ 4
+是
+
+randnSource
+int
+随机种子生成来源
+0: CPU,1: GPU。默认值0
+是
+
+seed
+Long
+随机种子
+
+- 范围:-1 ~ 9999999999
+- -1表示随机
+  是
+
+restoreFaces
+int
+面部修复
+0:关闭,1:开启。默认值0
+是
+
+hiResFixInfo
+Object
+高分辨率修复
+参考高分辨率修复的相关参数
+否
+
+controlNet
+
+list[Object]
+模型加载的ControlNet组合及各自参数
+参考controlnet参数配置
+
+否
+
+4.2.2 additionalNetwork
+变量名
+格式
+备注
+数值范围
+必填
+示例
+modelId
+String
+LoRA的模型uuid
+从全网可商用模型和自有模型中选择,详见文档3.1.1
+否
+// Lora添加,最多5个
+"additionalNetwork": [
+{
+"modelId": "31360f2f031b4ff6b589412a52713fcf", //LoRA的模型版本uuid
+"weight": 0.3 // LoRA权重
+},
+{
+"modelId": "365e700254dd40bbb90d5e78c152ec7f", //LoRA的模型版本uuid
+"weight": 0.6 // LoRA权重
+}
+],
+weight
+double
+LoRA权重
+-4.00 ~ +4.00,默认0.8
+否
+
+4.2.3 高分辨率修复 hiResFixInfo
+变量名
+格式
+备注
+数值范围
+必填
+示例
+hiresSteps
+int
+高清修复采样步数
+1 ~ 30
+否
+// 高分辨率修复
+"hiResFixInfo": {
+"hiresSteps": 20, // 高分辨率修复的重绘步数
+"hiresDenoisingStrength": 0.75, // 高分辨率修复的重绘幅度
+"upscaler": 10, // 放大算法模型枚举
+"resizedWidth": 1024, // 放大后的宽度
+"resizedHeight": 1536 // 放大后的高度
+},
+hiresDenoisingStrength
+double
+高清修复去噪强度
+
+0 ~ 1,精确到百分位
+
+否
+
+upscaler
+int
+放大算法枚举
+从提供的放大算法模型枚举中选择
+否
+
+resizedWidth
+int
+缩放宽度
+128 ~ 2048
+否
+
+resizedHeight
+int
+缩放高度
+128 ~ 2048
+否
+
+4.2.4 图生图基础参数
+变量名
+格式
+备注
+数值范围
+必填
+示例
+templateUuid
+String
+预设模版uuid
+从提供的预设参数模版中选择
+是
+{
+"templateUuid": "9c7d531dc75f476aa833b3d452b8f7ad", // 预设参数模板ID
+"generateParams": {
+// 基础参数
+"checkPointId": "0ea388c7eb854be3ba3c6f65aac6bfd3", //底模
+"vaeId": "", // vae模型,可为空
+"prompt": "1 girl wear glasses", //正向提示词
+"negativePrompt": "ng_deepnegative_v1_75t,(badhandv4:1.2),EasyNegative,(worst quality:2),nsfw", //负向提示词
+"clipSkip": 2, // Clip跳过层
+"sampler": 15, //采样方法
+"steps": 20, // 采样步数
+"cfgScale": 7, // 提示词引导系数  
+ "randnSource": 0, // 随机种子来源,0表示CPU,1表示GPU
+"seed": -1, // 随机种子值,-1表示随机
+"imgCount": 1, // 1到4
+"restoreFaces": 0, // 面部修复,0关闭,1开启
+
+        // 图像相关参数
+        "sourceImage": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/7c1cc38e-522c-43fe-aca9-07d5420d743e.png",
+        "resizeMode": 0, // 缩放模式, 0 拉伸,1 裁剪,2 填充
+        "resizedWidth": 1024, // 图像缩放后的宽度
+        "resizedHeight": 1536, // 图像缩放后的高度
+        "mode": 0, // 0图生图,4蒙版重绘
+        "denoisingStrength": 0.75, // 重绘幅度
+
+        // 蒙版重绘相关参数
+        "inpaintParam": {},
+
+        // Lora添加,最多5个
+        "additionalNetwork": [],
+
+        // Controlnet,最多4组
+        "controlNet": []
+    }
+
+}
+
+checkPointId
+
+String
+模型uuid
+从全网可商用模型和自有模型中选择,详见文档3.1.1
+
+是
+
+additionalNetwork
+list[object]
+LoRA模型的附加组合及各自参数
+参考additionalNetwork的参数配置
+
+否
+
+vaeId
+String
+VAE的模型uuid
+从提供的VAE列表中选择
+否
+
+prompt
+string
+正向提示词,文本
+
+- 不超过2000字符
+- 纯英文文本
+  是
+
+negativePrompt
+string
+负向提示词,文本
+
+- 不超过2000字符
+- 纯英文文本
+  是
+
+clipSkip
+int
+Clip跳过层
+1 ~ 12
+是
+
+sampler
+int
+采样器枚举值
+从采样方法列表中选择
+是
+
+steps
+int
+采样步数
+1 ~ 60
+是
+
+cfgScale
+double
+cfg_scale
+1.0 ~ 15.0
+是
+
+randnSource
+int
+
+类型
+
+- 0: CPU
+- 1: GPU
+  是
+
+seed
+int
+随机种子
+
+- 范围:-1 ~ 9999999999
+- -1表示随机
+  是
+
+imgCount
+int
+单次生图张数
+1 ~ 4
+是
+
+restoreFaces
+int
+面部修复
+0:关闭,1:开启。默认值0
+是
+
+sourceImage
+
+string
+参考图地址
+可公网访问的完整URL
+
+是
+
+resizeMode
+
+int
+缩放模式
+
+- 0:just_resize
+- 1:crop_and_resize
+- 2:resize_and_fill
+  是
+
+resizedWidth
+int
+调整后的图片宽度
+128 ~ 2048
+是
+
+resizedHeight
+int
+调整后的图片高度
+128 ~ 2048
+是
+
+mode
+
+int
+生图模式
+
+- 0:img2img,图生图
+- 4:inpaint upload mask,蒙版重绘
+  是
+
+denoisingStrength
+
+double
+去噪强度(图生图重绘幅度)
+0 ~ 1。默认值0.75
+是
+
+inpaintParam
+Object
+蒙版重绘相关参数
+参考蒙版重绘相关参数配置
+mode=4时必填
+
+controlNet
+
+list[Object]
+模型加载的ControlNet组合及各自参数
+参考controlnet参数配置
+
+否
+
+4.2.5 蒙版重绘相关参数
+变量名
+格式
+备注
+数值范围
+必填
+示例
+maskImage
+
+string
+蒙版文件地址,只用文件名png
+
+- 蒙版图URL
+- 要求:白色蒙版,黑色底色
+  mode=4时必填
+
+// 蒙版重绘相关参数
+"inpaintParam": {
+"maskImage": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/323fc358-618b-4c7d-b431-7d890209e5a5.png", // 蒙版地址
+"maskBlur": 4, // 蒙版模糊度
+"maskPadding": 32, //蒙版边缘预留像素,也称蒙版扩展量
+"maskMode": 0, // 蒙版模式  
+ "inpaintArea": 0, //重绘区域, 0重绘全图,1仅重绘蒙版区域
+"inpaintingFill": 1 //蒙版内容的填充模式
+},
+
+maskBlur
+int
+蒙版模糊度
+0 ~ 64,默认为4
+
+mode=4时必填
+
+maskPadding
+
+int
+蒙版边缘预留像素,也称蒙版扩展量
+0 ~ 256,默认32
+
+mode=4时必填
+
+maskMode
+
+int
+蒙版模式
+
+- 0:Inpaint_masked,重绘蒙版区域
+- 1:Inpaint_not_masked,重绘非蒙版区域
+  mode=4时必填
+
+inpaintArea
+int
+
+重绘区域
+
+- 0:whole_picture,重绘全图
+- 1:only_masked,仅重绘蒙版区域
+  mode=4时必填
+
+inpaintingFill
+
+int
+
+蒙版内容的填充模式
+
+- 0:fill,填充
+- 1:original,原图
+- 2:latent_noise,潜空间噪声
+- 3:latent_nothing,空白潜空间
+  mode=4时必填
+
+  4.2.6 Controlnet相关参数
+  ① Controlnet基础参数
+  变量名
+  格式
+  备注
+  数值范围
+  必填
+  示例
+  unitOrder
+  int
+  Controlnet单元顺序
+  1 ~ 4
+  是
+  // controlNet,最多4组
+  "controlNet": [
+  {
+  "unitOrder": 1, // 执行顺序
+  "sourceImage": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/7c1cc38e-522c-43fe-aca9-07d5420d743e.png",
+  "width": 1024, // 参考图宽度
+  "height": 1536, // 参考图高度
+  "preprocessor": 3, // 预处理器枚举值
+  "annotationParameters": { // 预处理器参数, 不同预处理器不同,此处仅为示意
+  "depthLeres": { // 3 预处理器 对应的参数
+  "preprocessorResolution": 1024,
+  "removeNear": 0,
+  "removeBackground": 0
+  }
+  },
+  "model": "6349e9dae8814084bd9c1585d335c24c", // controlnet的模型
+  "controlWeight": 1, // 控制权重
+  "startingControlStep": 0, //开始控制步数
+  "endingControlStep": 1, // 结束控制步数
+  "pixelPerfect": 1, // 完美像素
+  "controlMode": 0, // 控制模式 ,0 均衡,1 更注重提示词,2 更注重controlnet,
+  "resizeMode": 1, // 缩放模式, 0 拉伸,1 裁剪,2 填充
+  "maskImage": "" // 蒙版图
+  }
+  ]
+
+sourceImage
+string
+图片地址
+可公网访问的完整url
+是
+
+width
+int
+参考图宽度
+不超过4096
+是
+
+height
+int
+参考图高度
+不超过4096
+是
+
+preprocessor
+int
+预处理器枚举值
+从Controlnet预处理器列表中选择
+是
+
+annotationParameters
+
+object
+
+预处理参数
+参考预处理器参数配置
+是
+
+model
+string
+Controlnet模型uuid
+从提供的controlnet模型列表中选择
+是
+
+controlWeight
+double
+controlnet权重
+0 ~ 2,默认值1
+是
+
+startingControlStep
+
+double
+controlnet生效起始step,输入的值实际是表示占采样步数的百分比
+0 ~ 1,默认值0
+
+是
+
+endingControlStep
+double
+controlnet生效终止step,输入的值实际是表示占采样步数的百分比
+0 ~ 1,默认值1
+是
+
+pixelPerfect
+int
+完美像素模式
+0是关闭,1是开启。默认值1
+是
+
+controlMode
+int
+控制模式
+
+- 0:balanced,均衡
+- 1:prompt_important,更注重提示词
+- 2:controlnet_important,更注重controlnet
+  是
+
+resizeMode
+
+int
+缩放模式
+
+- 0:just_resize,直接缩放
+- 1:crop_and_resize,裁剪并缩放
+- 2:resize_and_fill,缩放并填充
+  是
+
+maskImage
+string
+mask图片地址
+
+- 蒙版图url,务必与参考图尺寸一致
+- 要求:白色蒙版,黑色底色
+  否
+
+② ControlNet预处理器
+适用方向
+Controlnet 类型
+预处理器
+预处理器名称映射
+预处理器枚举值
+预处理结果示意
+预处理器参数
+示例
+建议搭配的Controlnet model
+线稿类
+Canny(硬边缘)
+
+Canny(硬边缘)
+
+canny
+1
+
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+1. lowThreshold
+1. 变量名称:低阈值
+1. 数据格式:int
+1. 数值范围:1 ~ 255
+1. 默认值:100
+1. highThreshold
+1. 变量名称:高阈值
+1. 数据格式:int
+1. 数值范围:1 ~ 255
+1. 默认值:200
+   "preprocessor":1,
+   "annotationParameters": {
+   "canny": {
+   "preprocessorResolution": 512,
+   "lowThreshold": 100,
+   "highThreshold": 200
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_canny
+- 基础算法 XL:xinsir_controlnet-canny-sdxl_V2
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+SoftEdge(软边缘)
+hed
+hed
+5
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":5,
+   "annotationParameters": {
+   "hed": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_softedge
+- 基础算法 XL:mistoLine_rank256
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+hed_safe
+hedSafe
+6
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":6,
+   "annotationParameters": {
+   "hedSafe": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_softedge
+- 基础算法 XL:mistoLine_rank256
+- 基础算法 F.1: InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+pidinet
+pidinet
+17
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":17,
+   "annotationParameters": {
+   "pidinet": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_softedge
+- 基础算法 XL:mistoLine_rank256
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+pidinet_safe
+pidinetSafe
+18
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":18,
+   "annotationParameters": {
+   "pidinetSafe": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_softedge
+- 基础算法 XL:mistoLine_rank256
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+softedge_teed
+softedgeTeed
+58
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+1. safeSteps
+1. 变量名称:离散程度
+1. 数据格式:int
+1. 数值范围:0 ~ 64
+1. 默认值:2
+   "preprocessor":58,
+   "annotationParameters": {
+   "softedgeTeed": {
+   "preprocessorResolution": 512,
+   "safeSteps": 2
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_softedge
+- 基础算法 XL:controlnet-sd-xl-1.0-softedge-dexined
+- 基础算法 F.1:F.1_mistoline_dev_v1
+
+softedge_anyline
+
+softedgeAnyline
+65
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+1. safeSteps
+1. 变量名称:离散程度
+1. 数据格式:int
+1. 数值范围:0 ~ 64
+1. 默认值:2
+   "preprocessor":65,
+   "annotationParameters": {
+   "softedgeAnyline": {
+   "preprocessorResolution": 512,
+   "safeSteps": 2
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_softedge
+- 基础算法 XL:mistoLine_rank256, controlnet-sd-xl-1.0-softedge-dexined
+- 基础算法 F.1:F.1_mistoline_dev_v1
+
+MLSD(直线)
+mlsd (M-LSD 直线线条检测)
+mlsd
+
+8
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+1. valueThreshold
+1. 变量名称:值阈值
+1. 数据格式:double
+1. 数值范围:0.01 ~ 2.00
+1. 默认值:0.1
+1. distanceThreshold
+1. 变量名称:距离阈值
+1. 数据格式:double
+1. 数值范围:0.01 ~ 20.00
+1. 默认值:0.1
+   "preprocessor":8,
+   "annotationParameters": {
+   "mlsd": {
+   "preprocessorResolution": 512,
+   "valueThreshold": 0.1,
+   "distanceThreshold": 0.1
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_mlsd
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:暂无模型
+
+Scribble/Sketch(涂鸦/草图)
+scribble_pidinet(涂鸦- 手绘)
+scribblePidinet
+
+20
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":20,
+   "annotationParameters": {
+   "scribblePidinet": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_scribble
+- 基础算法 XL:xinsir_anime_painter
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+scribble_xdog (涂鸦- 强化边缘)
+
+scribbleXdog
+21
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+1. XDoGThreshold
+1. 变量名称:二值化阈值
+1. 数据格式:int
+1. 数值范围:1 ~ 64
+1. 默认值:32
+   "preprocessor":21,
+   "annotationParameters": {
+   "scribbleXdog": {
+   "preprocessorResolution": 512,
+   "XDoGThreshold": 32
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_scribble
+- 基础算法 XL:xinsir_anime_painter
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+scribble_hed(涂鸦 -合成)
+
+scribbleHed
+
+22
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":22,
+   "annotationParameters": {
+   "scribbleHed": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_scribble
+- 基础算法 XL:xinsir_anime_painter
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+Lineart(线稿)
+
+lineart_realistic (写实线稿提取)
+
+lineartRealistic
+29
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":29,
+   "annotationParameters": {
+   "lineartRealistic": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_lineart
+- 基础算法 XL:xinsir_anime_painter
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+lineart standard (标准线稿提取 -白底黑线反色)
+lineartStandard
+32
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":32,
+   "annotationParameters": {
+   "lineartStandard": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_lineart
+- 基础算法 XL:xinsir_anime_painter
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+lineart coarse (粗略线稿提取)
+lineartCoarse
+
+30
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":30,
+   "annotationParameters": {
+   "lineartCoarse": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_lineart
+- 基础算法 XL:xinsir_anime_painter
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+lineart_anime (动漫线稿提取)
+
+lineartAnime
+31
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":31,
+   "annotationParameters": {
+   "lineartAnime": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15s2_lineart_anime
+- 基础算法 XL:xinsir_anime_painter
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+lineart_anime_denoise(动漫线稿提取-去噪)
+
+lineartAnimeDenoise
+36
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":36,
+   "annotationParameters": {
+   "lineartAnimeDenoise": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15s2_lineart_anime
+- 基础算法 XL:xinsir_anime_painter
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+  空间关系类
+
+Depth(深度图)
+depth_midas
+
+depthMidas
+
+2
+
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":2,
+   "annotationParameters": {
+   "depthMidas": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11f1p_sd15_depth
+- 基础算法 XL:xinsir_controlnet_depth_sdxl_1.0
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+depth_leres (LeRes 深度图估算)
+
+depthLeres
+3
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+1. removeNear
+1. 变量名称:删除前景
+1. 数据格式:double
+1. 数值范围:0.0 ~ 100.0
+1. 默认值:0
+1. removeBackground
+1. 变量名称:删除背景
+1. 数据格式:double
+1. 数值范围:0.0 ~ 100.0
+1. 默认值:0
+   "preprocessor":3,
+   "annotationParameters": {
+   "depthLeres": {
+   "preprocessorResolution": 512,
+   "removeNear": 0,
+   "removeBackground": 0
+   }
+   }
+
+- 基础算法 1.5:control_v11f1p_sd15_depth
+- 基础算法 XL:xinsir_controlnet_depth_sdxl_1.0
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+depth_leres++
+
+depthLeresPlus
+4
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+1. removeNear
+1. 变量名称:删除前景
+1. 数据格式:double
+1. 数值范围:0.0 ~ 100.0
+1. 默认值:0
+1. removeBackground
+1. 变量名称:删除背景
+1. 数据格式:double
+1. 数值范围:0.0 ~ 100.0
+1. 默认值:0
+   "preprocessor":4,
+   "annotationParameters": {
+   "depthLeresPlus": {
+   "preprocessorResolution": 512,
+   "removeNear": 0,
+   "removeBackground": 0
+   }
+   }
+
+- 基础算法 1.5:control_v11f1p_sd15_depth
+- 基础算法 XL:xinsir_controlnet_depth_sdxl_1.0
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+depth_zoe (ZoE 深度图估算)
+depthZoe
+25
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":25,
+   "annotationParameters": {
+   "depthZoe": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11f1p_sd15_depth
+- 基础算法 XL:xinsir_controlnet_depth_sdxl_1.0
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+depth_hand_refiner
+
+depthHandRefiner
+57
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":57,
+   "annotationParameters": {
+   "depthHandRefiner": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_sd15_inpaint_depth_hand_fp16
+- 基础算法 XL:xinsir_controlnet_depth_sdxl_1.0
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+depth_anything
+depthAnything
+64
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":64,
+   "annotationParameters": {
+   "depthAnything": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11f1p_sd15_depth
+- 基础算法 XL:xinsir_controlnet_depth_sdxl_1.0
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+Segment(语义分割)
+segmentation
+
+segmentation
+23
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":23,
+   "annotationParameters": {
+   "segmentation": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_seg
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:暂无模型
+
+oneformer_coco
+
+oneformerCoco
+27
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":27,
+   "annotationParameters": {
+   "oneformerCoco": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_seg
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:暂无模型
+
+oneformer_ade20k
+
+oneformerAde20k
+28
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":28,
+   "annotationParameters": {
+   "oneformerAde20k": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_seg
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:暂无模型
+
+anime_face_segment
+
+animeFaceSegment
+54
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":54,
+   "annotationParameters": {
+   "animeFaceSegment": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_seg
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:暂无模型
+
+Normal(正态)
+normal_map
+normalMap
+9
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+1. backgroundThreshold
+1. 变量名称:背景阈值
+1. 数据格式:double
+1. 数值范围:0 ~ 1.0
+1. 默认值:0.4
+   "preprocessor":9,
+   "annotationParameters": {
+   "normalMap": {
+   "preprocessorResolution": 512,
+   "backgroundThreshold": 0.4
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_normalbae
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:Flux.1-dev-Controlnet-Surface-Normal
+
+normal bae (Bae 法线贴图提取)
+normalBae
+26
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":26,
+   "annotationParameters": {
+   "normalBae": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_normalbae
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:Flux.1-dev-Controlnet-Surface-Normal
+  姿态类
+
+OpenPose(姿态)
+mediapipe_face
+
+mediapipeFace
+7
+
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+1. maxFaces
+1. 变量名称:最大数量
+1. 数据格式:int
+1. 数值范围:1 ~ 10
+1. 默认值:1
+1. minConfidence
+1. 变量名称:最小置信度
+1. 数据格式:double
+1. 数值范围:0.01 ~ 1
+1. 默认值:0.5
+   "preprocessor":7,
+   "annotationParameters": {
+   "mediapipeFace": {
+   "preprocessorResolution": 512,
+   "maxFaces": 1,
+   "minConfidence": 0.5
+   }
+   }
+
+- 基础算法 1.5:control_v2p_sd15_mediapipe_face
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:F.1-ControlNet-Pose-V1
+
+openpose (OpenPose 姿态)
+openpose
+10
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":10,
+   "annotationParameters": {
+   "openpose": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_openpose
+- 基础算法 XL:xinsir_controlnet-openpose-sdxl-1.0
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+openpose hand (OpenPose 姿态及手部)
+openposeHand
+11
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":11,
+   "annotationParameters": {
+   "openposeHand": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_openpose
+- 基础算法 XL:xinsir_controlnet-openpose-sdxl-1.0
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+openpose face (OpenPose 姿态及脸部)
+openposeFace
+12
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":12,
+   "annotationParameters": {
+   "openposeFace": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_openpose
+- 基础算法 XL:xinsir_controlnet-openpose-sdxl-1.0
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+openpose_faceonly (OpenPose 仅脸部)
+openposeFaceonly
+13
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":13,
+   "annotationParameters": {
+   "openposeFaceonly": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_openpose
+- 基础算法 XL:xinsir_controlnet-openpose-sdxl-1.0
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+openpose_full (OpenPose 姿态、手部及脸部)
+
+openposeFull
+14
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":14,
+   "annotationParameters": {
+   "openposeFull": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_openpose
+- 基础算法 XL:xinsir_controlnet-openpose-sdxl-1.0
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+dw_openpose_full
+
+dwOpenposeFull
+45
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":45,
+   "annotationParameters": {
+   "dwOpenposeFull": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_openpose
+- 基础算法 XL:xinsir_controlnet-openpose-sdxl-1.0
+- 基础算法 F.1:InstantX-FLUX.1-dev-Controlnet-Union-Pro
+
+animal_openpose
+animalOpenpose
+53
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":53,
+   "annotationParameters": {
+   "animalOpenpose": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_sd15_animal_openpose_fp16
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:暂无模型
+
+densepose
+
+densepose
+55
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":55,
+   "annotationParameters": {
+   "densepose": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_densepose_fp16
+- 基础算法 XL:controlnet-densepose-sdxl
+- 基础算法 F.1:暂无模型
+
+densepose_parula
+
+denseposeParula
+56
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":56,
+   "annotationParameters": {
+   "denseposeParula": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11p_sd15_densepose_fp16
+- 基础算法 XL:controlnet-densepose-sdxl
+- 基础算法 F.1:暂无模型
+  画面参考
+  Tile/Blur(分块/模糊)
+  tile_resample(分块重采样)
+
+tileResample
+34
+/
+
+1. downSamplingRate
+1. 变量名称:下采样率
+1. 数据格式:double
+1. 数值范围:1.00 ~ 8.00
+1. 默认值:1
+   "preprocessor":34,
+   "annotationParameters": {
+   "tileResample": {
+   "downSamplingRate": 1
+   }
+   }
+
+- 基础算法 1.5:control_v11f1e_sd15_tile
+- 基础算法 XL:xinsir_controlnet_tile_sdxl_1.0
+- 基础算法 F.1:Flux.1-dev-Controlnet-Upscaler
+
+tile_colorfix
+tileColorfix
+43
+/
+
+1. variation
+1. 变量名称:变化率
+1. 数据格式:int
+1. 数值范围:3 ~ 32
+1. 默认值:8
+   "preprocessor":43,
+   "annotationParameters": {
+   "tileColorfix": {
+   "variation": 8
+   }
+   }
+
+- 基础算法 1.5:control_v11f1e_sd15_tile
+- 基础算法 XL:xinsir_controlnet_tile_sdxl_1.0
+- 基础算法 F.1:Flux.1-dev-Controlnet-Upscaler
+
+tile_colorfix+sharp
+tileColorfixSharp
+44
+/
+
+1. variation
+1. 变量名称:变化率
+1. 数据格式:int
+1. 数值范围:3 ~ 32
+1. 默认值:8
+1. sharpness
+1. 变量名称:锐度
+1. 数据格式:double
+1. 数值范围:0 ~ 2.00
+1. 默认值:1
+   "preprocessor":44,
+   "annotationParameters": {
+   "tileColorfixSharp": {
+   "variation": 8,
+   "sharpness": 1
+   }
+   }
+
+- 基础算法 1.5:control_v11f1e_sd15_tile
+- 基础算法 XL:xinsir_controlnet_tile_sdxl_1.0
+- 基础算法 F.1:Flux.1-dev-Controlnet-Upscaler
+
+blur_gaussian
+
+blurGaussian
+52
+/
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+1. sigma
+1. 变量名称:离散程度
+1. 数据格式:int
+1. 数值范围:0 ~ 64
+1. 默认值:9
+   "preprocessor":52,
+   "annotationParameters": {
+   "blurGaussian": {
+   "preprocessorResolution": 512,
+   "sigma": 9
+   }
+   }
+
+- 基础算法 1.5:暂无模型
+- 基础算法 XL:kohya_controllllite_xl_blur
+- 基础算法 F.1:Flux.1-dev-Controlnet-Upscaler
+
+Reference(参考)
+reference_only
+referenceOnly
+37
+/
+
+1. styleFidelity
+1. 变量名称:风格忠实度
+1. 数据格式:double
+1. 数值范围:0 ~ 1.0
+1. 默认值:0.5
+   "preprocessor":37,
+   "annotationParameters": {
+   "referenceOnly": {
+   "styleFidelity": 0.5
+   }
+   }
+
+- 基础算法 1.5:None
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:暂无模型
+
+reference_adain
+referenceAdain
+38
+/
+
+1. styleFidelity
+1. 变量名称:风格忠实度
+1. 数据格式:double
+1. 数值范围:0 ~ 1.0
+1. 默认值:0.5
+   "preprocessor":38,
+   "annotationParameters": {
+   "referenceAdain": {
+   "styleFidelity": 0.5
+   }
+   }
+
+- 基础算法 1.5:None
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:暂无模型
+
+reference_adain+attn
+
+referenceAdainAttn
+39
+/
+
+1. styleFidelity
+1. 变量名称:风格忠实度
+1. 数据格式:double
+1. 数值范围:0 ~ 1.0
+1. 默认值:0.5
+   "preprocessor":39,
+   "annotationParameters": {
+   "referenceAdainAttn": {
+   "styleFidelity": 0.5
+   }
+   }
+
+- 基础算法 1.5:None
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:暂无模型
+  风格迁移
+  IP-Adapter
+  ip-adapter_clip_sd15
+  ipAdapterClipSd15
+  48
+  /
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":48,
+   "annotationParameters": {
+   "ipAdapterClipSd15": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:ip-adapter_sd15
+- 基础算法 XL:不可搭配
+- 基础算法 F.1:暂无模型
+
+ip-adapter_clip_sdxl
+
+ipAdapterClipSdxl
+49
+/
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":49,
+   "annotationParameters": {
+   "ipAdapterClipSdxl": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:不可搭配
+- 基础算法 XL:ip-adapter_xl, ip-adapter_sdxl_vit-h
+- 基础算法 F.1:暂无模型
+
+ip-adapter_clip_sdxl_plus_vith
+ipAdapterClipSdxlPlusVith
+61
+/
+/
+"preprocessor":61,
+"annotationParameters": {
+"ipAdapterClipSdxlPlusVith": {}
+}
+
+- 基础算法 1.5:不可搭配
+- 基础算法 XL:ip-adapter-plus_sdxl_vit-h
+- 基础算法 F.1:暂无模型
+
+ip-adapter-siglip
+ipAdapterSiglip
+66
+/
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":66,
+   "annotationParameters": {
+   "ipAdapterSiglip": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:不可搭配
+- 基础算法 XL:不可搭配
+- 基础算法 F.1: InstantX-F.1-dev-IP-Adapter
+
+T2I-Adapter
+clip_vision
+clipVision
+15
+/
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":15,
+   "annotationParameters": {
+   "clipVision": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:t2iadapter_style_sd14v1
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:暂无模型
+
+color
+color
+16
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":16,
+   "annotationParameters": {
+   "color": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:t2iadapter_color_sd14v1
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:暂无模型
+
+pidinet_sketch
+pidinetSketch
+19
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":19,
+   "annotationParameters": {
+   "pidinetSketch": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:t2iadapter_sketch_sd15v2
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:暂无模型
+
+Shuffle (随机洗牌)
+shuffle (随机洗牌)
+shuffle
+33
+[图片]
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":33,
+   "annotationParameters": {
+   "shuffle": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:control_v11e_sd15_shuffle
+- 基础算法 XL:暂无模型
+- 基础算法 F.1:暂无模型
+  上色
+  Recolor(重上色)
+  recolor_luminance
+  recolorLuminance
+  50
+  [图片]
+
+1. gammaCorrection
+1. 变量名称:伽马修正
+1. 数据格式:double
+1. 数值范围:0.1 ~ 2.0
+1. 默认值:1
+   "preprocessor":50,
+   "annotationParameters": {
+   "recolorLuminance": {
+   "gammaCorrection": 1
+   }
+   }
+
+- 基础算法 1.5:ioclab_sd15_recolor
+- 基础算法 XL:sai_xl_recolor_256lora
+- 基础算法 F.1:暂无模型
+
+recolor_intensity
+recolorIntensity
+
+51
+[图片]
+
+1. gammaCorrection
+1. 变量名称:伽马修正
+1. 数据格式:double
+1. 数值范围:0.1 ~ 2.0
+1. 默认值:1
+   "preprocessor":51,
+   "annotationParameters": {
+   "recolorIntensity": {
+   "gammaCorrection": 1
+   }
+   }
+
+- 基础算法 1.5:ioclab_sd15_recolor
+- 基础算法 XL:sai_xl_recolor_256lora
+- 基础算法 F.1:暂无模型
+  局部重绘
+  Inpaint(局部重绘)
+  inpaint_global_harmonious
+  inpaintGlobalHarmonious
+  40
+  /
+  /
+  "preprocessor":40,
+  "annotationParameters": {
+  "inpaintGlobalHarmonious": {}
+  }
+- 基础算法 1.5:segmentation_mask_brushnet_ckpt
+- 基础算法 XL:segmentation_mask_brushnet_ckpt_sdxl_v1
+- 基础算法 F.1:F.1-dev-Controlnet-Inpainting-Beta
+
+inpaint_only
+inpaintOnly
+41
+/
+/
+"preprocessor":41,
+"annotationParameters": {
+"inpaintOnly": {}
+}
+
+- 基础算法 1.5:segmentation_mask_brushnet_ckpt
+- 基础算法 XL:segmentation_mask_brushnet_ckpt_sdxl_v1
+- 基础算法 F.1:F.1-dev-Controlnet-Inpainting-Beta
+
+inpaint_only+lama
+inpaintOnlyLama
+42
+/
+/
+"preprocessor":42,
+"annotationParameters": {
+"inpaintOnlyLama": {}
+}
+
+- 基础算法 1.5:segmentation_mask_brushnet_ckpt
+- 基础算法 XL:segmentation_mask_brushnet_ckpt_sdxl_v1
+- 基础算法 F.1:F.1-dev-Controlnet-Inpainting-Beta
+
+换脸
+IP-Adapter
+ip-adapter_face_id
+ipAdapterFaceId
+62
+/
+/
+"preprocessor":62,
+"annotationParameters": {
+"ipAdapterFaceId": {}
+}
+
+- 基础算法 1.5:ip-adapter_face_id
+- 基础算法 XL:ip-adapter-faceid_sdxl
+- 基础算法 F.1:暂无模型
+
+ip-adapter_face_id_plus
+ipAdapterFaceIdPlus
+63
+/
+/
+"preprocessor":63,
+"annotationParameters": {
+"ipAdapterFaceIdPlus": {}
+}
+
+- 基础算法 1.5:ip-adapter-faceid-plusv2_sd15
+- 基础算法 XL:ip-adapter-faceid-plusv2_sdxl
+- 基础算法 F.1:暂无模型
+
+Instant ID
+instant_id_face_keypoints
+instantIdFaceKeypoints
+59
+/
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":59,
+   "annotationParameters": {
+   "instantIdFaceKeypoints": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:暂无模型
+- 基础算法 XL:control_instant_id_sdxl
+- 基础算法 F.1:暂无模型
+
+instant_id_face_embedding
+
+instantIdFaceEmbedding
+60
+/
+
+1. preprocessorResolution
+1. 变量名称:预处理器分辨率
+1. 数据格式:int
+1. 数值范围:64 ~ 2048
+1. 默认值:512
+   "preprocessor":60,
+   "annotationParameters": {
+   "instantIdFaceEmbedding": {
+   "preprocessorResolution": 512
+   }
+   }
+
+- 基础算法 1.5:暂无模型
+- 基础算法 XL:ip-adapter_instant_id_sdxl
+- 基础算法 F.1:暂无模型
+  其他
+  /
+  None
+  none
+  0
+  /
+  /
+  "preprocessor":0,
+  "annotationParameters": {
+  "none": {}
+  }
+  仅在参考图是处理后的线稿、深度图、骨骼图时使用
+
+/
+invert (白底黑线反色)
+invert
+35
+/
+/
+"preprocessor":35,
+"annotationParameters": {
+"invert": {}
+}
+仅在参考图是白色线条,黑色背景,且要应用线稿模型时使用
+
+③ ControlNet模型列表
+适用方向
+Controlnet 类型
+模型名称
+基础算法类型
+模型版本UUID
+线稿类
+
+Canny(硬边缘)
+control_v11p_sd15_canny
+基础算法 1.5
+7d917ec7e55c5805db737d3b493c91ce
+
+t2iadapter_canny_sd14v1
+基础算法 1.5
+a2c41c4e97944f3aa71f913bdc45b1ca
+
+t2iadapter_canny_sd15v2
+基础算法 1.5
+c04144bcf017232483181cd8607097c2
+
+diffusers_xl_canny_full
+基础算法 XL
+56de5edadb6f2891aff05ff078dc0470
+
+diffusers_xl_canny_mid
+基础算法 XL
+efb97e9d8c237573298c3a5a7869b89c
+
+diffusers_xl_canny_small
+基础算法 XL
+dccde738064e9748f93b48ec5868968e
+
+kohya_controllllite_xl_canny
+基础算法 XL
+5242e3d18cc18689bd8af11dd2d675c1
+
+kohya_controllllite_xl_canny_anime
+基础算法 XL
+4f3e1cfe79f87496ec69a37826c3afeb
+
+sai_xl_canny_128lora
+基础算法 XL
+63c7f2c6c354336513831aa522d7e0f4
+
+sai_xl_canny_256lora
+基础算法 XL
+5bf551f53651764cad56363e17900d87
+
+t2i-adapter_diffusers_xl_canny
+基础算法 XL
+618390ab2957a422612cb2ba92a2788f
+
+t2i-adapter_xl_canny
+基础算法 XL
+7cd56501c336c1edba78430355c9d081
+
+xinsir_controlnet-canny-sdxl_V2
+基础算法 XL
+b6806516962f4e1599a93ac4483c3d23
+
+XLabs-flux-canny-controlnet_v3
+基础算法 F.1
+017997cd6ba44c4dbe8f60e0a26cd0df
+
+InstantX-FLUX.1-dev-Controlnet-Union-Pro
+基础算法 F.1
+13c1e1b96ba64f9cbb2b54f89b5fe873
+
+InstantX-Qwen-Image-Controlnet-Union
+Qwen Image
+5b5f21d2b80445598db19e924bd3a409
+
+SoftEdge(软边缘)
+
+control_v11p_sd15_softedge
+基础算法 1.5
+0929722d9047ec6498a50ff5d1081629
+
+sargezt_xl_softedge
+基础算法 XL
+dda1a0c480bfab9833d9d9a1e4a71fff
+
+controlnet-sd-xl-1.0-softedge-dexined
+基础算法 XL
+37bddde3d45c11ee9b5e00163e365853
+
+mistoLine_softedge_sdxl_fp16
+基础算法 XL
+4f6726be104a432f8039b018c92ed4bf
+
+mistoLine_rank256
+基础算法 XL
+83286d0e66a845c58f7d23442f9dedf9
+
+XLabs-flux-hed-controlnet_v3
+基础算法 F.1
+6c4d620df3644514903b8189735c6ae9
+
+F.1_mistoline_dev_v1
+基础算法 F.1
+3e6860a3b9444f25ae07d9c1b5d1ba9e
+
+InstantX-FLUX.1-dev-Controlnet-Union-Pro
+基础算法 F.1
+13c1e1b96ba64f9cbb2b54f89b5fe873
+
+InstantX-Qwen-Image-Controlnet-Union
+Qwen Image
+5b5f21d2b80445598db19e924bd3a409
+
+MLSD(直线)
+control_v11p_sd15_mlsd
+基础算法 1.5
+7168cece6a0d491375aa1753ff3bdc21
+
+Scribble/Sketch(涂鸦/草图)
+control_v11p_sd15_scribble
+基础算法 1.5
+fe57911f7ba1b84eb27f1e1ecead3367
+
+kohya_controllllite_xl_scribble_anime
+基础算法 XL
+4a399a87f1ffbc26d065a38765d30d24
+
+xinsir_controlnet-scribble-sdxl-1.0
+基础算法 XL
+888cf8985bd6442cba1f2d975b6eb022
+
+xinsir_anime_painter
+基础算法 XL
+f936bf22cb8e4dcfa6b0f3b96cdd8eb7
+
+InstantX-Qwen-Image-Controlnet-Union
+Qwen Image
+5b5f21d2b80445598db19e924bd3a409
+
+Lineart(线稿)
+
+control_v11p_sd15_lineart
+基础算法 1.5
+b06dfbd1a61c35e933d9f8caa8a0e031
+
+control_v11p_sd15s2_lineart_anime
+基础算法 1.5
+c263e039c57b8a958ee0a936039af654
+
+t2i-adapter_diffusers_xl_lineart
+基础算法 XL
+a0f01da42bf48b0ba02c86b6c26b5699
+
+InstantX-Qwen-Image-Controlnet-Union
+Qwen Image
+5b5f21d2b80445598db19e924bd3a409
+空间关系类
+
+Depth(深度图)
+
+control_v11f1p_sd15_depth
+基础算法 1.5
+cf63d214734760dcdc108b1bd094921b
+
+t2iadapter_depth_sd15v2
+基础算法 1.5
+f08a4a889b56d4099e8a947503cabc14
+
+t2iadapter_depth_sd14v1
+基础算法 1.5
+8b74bf9ea84f592c069b523d9bef9dab
+
+t2iadapter_zoedepth_sd15v1
+基础算法 1.5
+fc8b79f97eeceda388b43df12509c311
+
+control_sd15_inpaint_depth_hand_fp16
+基础算法 1.5
+3497061cd45c11ee9b5e00163e365853
+
+t2i-adapter_diffusers_xl_depth_zoe
+基础算法 XL
+a35993a2d1cde4a6c800364a68731c67
+
+sai_xl_depth_128lora
+基础算法 XL
+3156f3428afc7122c66b2b950f09d4cd
+
+t2i-adapter_diffusers_xl_depth_midas
+基础算法 XL
+c22ec6a7a24eed6b91889ae1a1e94b2e
+
+diffusers_xl_depth_mid
+基础算法 XL
+740d6d428e70d4b40888efa4d9eb642a
+
+xinsir_controlnet_depth_sdxl_1.0
+基础算法 XL
+6349e9dae8814084bd9c1585d335c24c
+
+sai_xl_depth_256lora
+基础算法 XL
+08d0fbb72d7fab601218df26978a46e0
+
+sargezt_xl_depth
+基础算法 XL
+feb9ee5779bf2eb3fdd669f2e3e6b1aa
+
+sargezt_xl_depth_zeed
+基础算法 XL
+4216d4b49a6b559d76d181908f866eb8
+
+kohya_controllllite_xl_depth_anime
+基础算法 XL
+dea707d52e3a8f243da5579579cb3a3d
+
+kohya_controllllite_xl_depth
+基础算法 XL
+693d7182db5293c0087524580111fd96
+
+sargezt_xl_depth_faid_vidit
+基础算法 XL
+1c6d32d0fb004cf1becc2b526fd83690
+
+diffusers_xl_depth_small
+基础算法 XL
+6a786af31a13776100e9c6a90f99aebf
+
+diffusers_xl_depth_full
+基础算法 XL
+04dcab4b18c7b821e96660d6c19de50b
+
+XLabs-flux-depth-controlnet_v3
+基础算法 F.1
+0cc4e6b8206b44cdab51e30fb8b9c328
+
+InstantX-FLUX.1-dev-Controlnet-Union-Pro
+基础算法 F.1
+13c1e1b96ba64f9cbb2b54f89b5fe873
+
+Flux.1-dev-Controlnet-Depth
+基础算法 F.1
+64dd7a6c714f4512a4500f6a01b016b7
+
+InstantX-Qwen-Image-Controlnet-Union
+Qwen Image
+5b5f21d2b80445598db19e924bd3a409
+
+Segment(语义分割)
+control_v11p_sd15_seg
+基础算法 1.5
+94571f4bb5136464afc1540a92ae3ee8
+
+Normal(正态)
+control_v11p_sd15_normalbae
+基础算法 1.5
+9a85fdca18a8b58b2fb2ff13ab339be4
+
+Flux.1-dev-Controlnet-Surface-Normal
+基础算法 F.1
+e51fdccdf3b8417aab246bde40b5f360
+姿态类
+
+OpenPose(姿态)
+
+control_v11p_sd15_openpose
+基础算法 1.5
+b46dd34ef9c2fe189446599d62516cbf
+
+t2iadapter_openpose_sd14v1
+基础算法 1.5
+5a8b19a8809e00be4e17517e8ab174ad
+
+control_v11p_sd15_densepose_fp16
+基础算法 1.5
+3b4e0830d45c11ee9b5e00163e365853
+
+control_sd15_animal_openpose_fp16
+基础算法 1.5
+329f0073d45c11ee9b5e00163e365853
+
+control_v2p_sd15_mediapipe_face
+基础算法 1.5
+73de0752a7a8431ba21637cda6723c95
+
+kohya_controllllite_xl_openpose_anime_v2
+基础算法 XL
+4cbbd2483088ef5f0d41bfef0d7141fb
+
+kohya_controllllite_xl_openpose_anime
+基础算法 XL
+abb5d55cf94c504f6f8c64abc0b1483f
+
+thibaud_xl_openpose_256lora
+基础算法 XL
+4dd1f4df2a9d3a9db8aeaa9480196d02
+
+t2i-adapter_xl_openpose
+基础算法 XL
+9deac5a5c60abfd03261bd174ddba47d
+
+t2i-adapter_diffusers_xl_openpose
+基础算法 XL
+9cd43e1856040c2436f00802d5b54ee5
+
+thibaud_xl_openpose
+基础算法 XL
+2fe4f992a81c5ccbdf8e9851c8c96ff2
+
+controlnet-densepose-sdxl
+基础算法 XL
+3ae77dfdd45c11ee9b5e00163e365853
+
+xinsir_controlnet-openpose-sdxl-1.0
+基础算法 XL
+23ef8ab803d64288afdb7106b8967a55
+
+F.1-ControlNet-Pose-V1
+基础算法 F.1
+7c6d889cb9c04b78858d8fece80f9f85
+
+InstantX-Qwen-Image-Controlnet-Union
+Qwen Image
+5b5f21d2b80445598db19e924bd3a409
+画面参考
+Tile/Blur(分块/模糊)
+control_v11f1e_sd15_tile
+基础算法 1.5
+37e42c6bdb6fab4c24a662100f20f722
+
+kohya_controllllite_xl_blur_anime
+基础算法 XL
+46a34a643f6855e9b3861515712df5d9
+
+xinsir_controlnet_tile_sdxl_1.0
+基础算法 XL
+0f47ef6d4f4b40afab8b290c98baac0e
+
+kohya_controllllite_xl_blur_anime_beta
+基础算法 XL
+44199bb6dcf4f65e09a4e5e57ebdf9b4
+
+kohya_controllllite_xl_blur
+基础算法 XL
+aac5fe593565f0673673731d54ecfab8
+
+TTPLanet_SDXL_Controlnet_Tile_Realistic_v1
+基础算法 XL
+13bfaf39f9214c658507a92cd15fd02d
+
+TTPLanet_SDXL_Controlnet_Tile_Realistic_v2
+基础算法 XL
+163d505651a64d6bac9a907b213dc8b0
+
+Flux.1-dev-Controlnet-Upscaler
+基础算法 F.1
+a696b5bdadc740119fd76505b33d6898
+
+Reference(参考)
+None
+基础算法 1.5
+/
+风格迁移
+IP-Adapter
+
+ip-adapter_sd15
+基础算法 1.5
+18801062fe4289dd0a984e69de9f9e7c
+
+ip-adapter_sd15_plus
+基础算法 1.5
+ad4bd9b4b05c4ac8faf7f81d9fdcadc8
+
+ip-adapter_sd15_light
+基础算法 1.5
+3a1ddfd0d45c11ee9b5e00163e365853
+
+ip-adapter_sd15_vit-G
+基础算法 1.5
+36f3d2a0d45c11ee9b5e00163e365853
+
+ip-adapter_xl
+基础算法 XL
+8ea2538fdd7dcdea52b2da6b5151f875
+
+ip-adapter-plus_sdxl_vit-h
+基础算法 XL
+38ee73f1d45c11ee9b5e00163e365853
+
+ip-adapter_sdxl_vit-h
+基础算法 XL
+375866e3d45c11ee9b5e00163e365853
+
+InstantX-F.1-dev-IP-Adapter
+基础算法 F.1
+c6ed70879cf011ef96d600163e37ec70
+
+F.1-redux-dev
+基础算法 F.1
+8ddf6f3ba8a111efbb1700163e031cf1
+
+T2I-Adapter
+t2iadapter_canny_sd15v2
+基础算法 1.5
+c04144bcf017232483181cd8607097c2
+
+t2iadapter_depth_sd15v2
+基础算法 1.5
+f08a4a889b56d4099e8a947503cabc14
+
+t2iadapter_canny_sd14v1
+基础算法 1.5
+a2c41c4e97944f3aa71f913bdc45b1ca
+
+t2iadapter_color_sd14v1
+基础算法 1.5
+8e581a4e7c986950d71f1102accad5d0
+
+t2iadapter_depth_sd14v1
+基础算法 1.5
+8b74bf9ea84f592c069b523d9bef9dab
+
+t2iadapter_keypose_sd14v1
+基础算法 1.5
+181d8d213381458cb6e326760637d4b4
+
+t2iadapter_openpose_sd14v1
+基础算法 1.5
+5a8b19a8809e00be4e17517e8ab174ad
+
+t2iadapter_seg_sd14v1
+基础算法 1.5
+3c680cc8edfbc4479423549e01f21897
+
+t2iadapter_sketch_sd14v1
+基础算法 1.5
+0d19dd02091ec2d01f3cdd99a4f4b442
+
+t2iadapter_sketch_sd15v2
+基础算法 1.5
+bd6c5dbb73c2c2e538850c23ab2dcbf5
+
+t2iadapter_style_sd14v1
+基础算法 1.5
+e33777a1f374eccd9464623c56a82c91
+
+t2iadapter_zoedepth_sd15v1
+基础算法 1.5
+fc8b79f97eeceda388b43df12509c311
+
+Shuffle (随机洗牌)
+control_v11e_sd15_shuffle
+基础算法 1.5
+9efba1cc2d469bf4be8fc135689bc8a0
+上色
+Recolor(重上色)
+ioclab_sd15_recolor
+基础算法 1.5
+e0db5b9e227eac932c71498cf7e03a78
+
+sai_xl_recolor_128lora
+基础算法 XL
+af92235f1de682ceac136c06450c9a51
+
+sai_xl_recolor_256lora
+基础算法 XL
+03051a3606b4974ec02fc55b079757e7
+局部重绘
+
+Inpaint(局部重绘)
+
+control_v11p_sd15_inpaint
+基础算法 1.5
+ebeada0aa92959b4e905ab6980d5d203
+
+segmentation_mask_brushnet_ckpt
+基础算法 1.5
+14aa553bf6534a419a9a465eba900f3a
+
+random_mask_brushnet_cpkt
+基础算法 1.5
+de44488f84a74e02a1fac604d790698c
+
+segmentation_mask_brushnet_ckpt_sdxl_v1
+基础算法 XL
+a311363995dd4f2fa42ee3fc9582d920
+
+random_mask_brushnet_ckpt_sdxl
+基础算法 XL
+3161fc68c59847b0ad826a9fb18c857f
+
+F.1-dev-Controlnet-Inpainting-Alpha
+基础算法 F.1
+012d2f780c0b44dba829bb223207e608
+
+F.1-dev-Controlnet-Inpainting-Beta
+基础算法 F.1
+31df01fc271d484ca4d496179d69a665
+
+InstantX-Qwen-Image-ControlNet-Inpainting
+Qwen Image
+2228ab9234a34aa5abf77caa907c0de1
+换脸
+IP-Adapter
+
+ip-adapter_face_id
+基础算法 1.5
+368e6a37d45c11ee9b5e00163e365853
+
+ip-adapter-faceid-portrait_sd15
+基础算法 1.5
+330504bcd45c11ee9b5e00163e365853
+
+ip-adapter-faceid-plusv2_sd15
+基础算法 1.5
+34fb8ef6d45c11ee9b5e00163e365853
+
+ip-adapter-faceid-plus_sd15
+基础算法 1.5
+362a215ad45c11ee9b5e00163e365853
+
+ip-adapter-faceid-portrait-v11_sd15
+基础算法 1.5
+35c50016d45c11ee9b5e00163e365853
+
+ip-adapter-faceid_sdxl
+基础算法 XL
+38879e1ad45c11ee9b5e00163e365853
+
+ip-adapter-faceid-plusv2_sdxl
+基础算法 XL
+3953f672d45c11ee9b5e00163e365853
+
+ip-adapter-plus-face_sdxl_vit-h
+基础算法 XL
+336955e4d45c11ee9b5e00163e365853
+
+Instant ID
+ip-adapter_instant_id_sdxl
+基础算法 XL
+3a8267c7d45c11ee9b5e00163e365853
+
+control_instant_id_sdxl
+基础算法 XL
+3560664ad45c11ee9b5e00163e365853
+
+puLID
+pulid_flux_v0.9.1
+基础算法 F.1
+405836d1ae2646b4ba2716ed6bd5453a
+其他
+光影
+control_v1u_sd15_illumination
+基础算法 1.5
+3109072a5cf6403faba6162003b8f483
+
+control_v1p_sd15_brightness
+基础算法 1.5
+39b8eac0d45c11ee9b5e00163e365853
+
+二维码
+control_v1p_sd15_qrcode_monster
+基础算法 1.5
+1fa6070c35626e760b1473926852cbbc
+
+4.3 生图状态
+4.3.1 生图状态(generateStatus)
+状态枚举值
+描述
+备注
+1
+等待执行
+
+2
+执行中
+
+3
+已生图
+
+4
+审核中
+
+5
+成功
+
+6
+失败
+
+7
+超时
+任务创建30分钟后没有执行结果就计入timeout状态,并解冻积分。
+4.3.2 审核状态(auditStatus)
+状态枚举值
+描述
+备注
+1
+待审核
+
+2
+审核中
+
+3
+审核通过
+
+4
+审核拦截
+
+5
+审核失败
+
+4.4 参数模版预设
+完整版的生图参数可以满足基础算法F.1、基础算法XL、基础算法1.5下的各类生图任务,但需要非常理解这些参数的含义。
+因此除了完整参数的模版以外,我们还提供了一些封装后的参数预设,您可以只提供必要的生图参数,极大简化了配置成本,欢迎体验~
+4.4.1 模版选择(templateUuid)
+适用方向
+模板名称
+模板UUID
+备注
+F.1文生图
+F.1文生图 - 自定义完整参数
+6f7c4652458d4802969f8d089cf5b91f
+
+- Checkpoint默认为官方模型
+- 可用模型范围:基础算法F.1
+- 支持additional network
+  F.1图生图
+  F.1图生图 - 自定义完整参数
+  63b72710c9574457ba303d9d9b8df8bd
+- Checkpoint默认为官方模型
+- 可用模型范围:基础算法F.1
+- 支持additional network
+  1.5和XL文生图
+
+  1.5和XL文生图 - 自定义完整参数
+
+e10adc3949ba59abbe56e057f20f883e
+
+- 可用模型范围:基础算法1.5,基础算法XL
+- 支持additional network,高分辨率修复和controlnet
+- 可通过自由拼接参数实现各类的文生图诉求
+  1.5和XL图生图
+  1.5和XL图生图 - 自定义完整参数
+  9c7d531dc75f476aa833b3d452b8f7ad
+- 可用模型范围:基础算法1.5,基础算法XL
+- 支持additional network和controlnet
+- 可通过自由拼接参数实现各类的图生图和蒙版重绘诉求
+  局部重绘
+  Controlnet局部重绘
+  b689de89e8c9407a874acd415b3aa126
+- 提取自文生图完整参数
+- 支持additional network和controlnet
+- 不支持高分辨率修复(hiresfix)
+  局部重绘
+
+图生图局部重绘
+74509e1b072a4c45a7f1843a963c8462
+
+- 提取自图生图完整参数
+- 支持additionalNetwork
+- 不支持Controlnet
+  人物换脸
+  InstantID人像换脸
+  7d888009f81d4252a7c458c874cd017f
+- 仅用于人像换脸
+- 注意人像参考图中的人物面部特征务必清晰
+  4.4.2 模版传参示例
+  以下提供了调用各类模版时的传参示例,方便您理解不同模版的使用方式。
+  注:如果要使用如下参数示例生图,请把其中的注释删掉后再使用。
+  F.1文生图 - 自定义完整参数示例
+- 接口:POST /api/generate/webui/text2img
+  https://liblibai.feishu.cn/sync/UklAdrkqos0NNubQ42jcymktnSe
+  F.1图生图 - 自定义完整参数示例
+- 接口:POST /api/generate/webui/img2img
+  https://liblibai.feishu.cn/sync/YasbdeCAasWRaibd0tkc0ZU4nkd
+  1.5和XL文生图 - 自定义完整参数示例
+- 接口:POST /api/generate/webui/text2img
+  https://liblibai.feishu.cn/sync/VrLRdFII0sSVtJbpj8NccFOqnYb
+  1.5和XL图生图 - 自定义完整参数示例
+- 接口:POST /api/generate/webui/img2img
+  https://liblibai.feishu.cn/sync/R6HUdfvpEsAHnvbF7i0cf76XnSf
+  1.5和XL文生图 - 最简版参数示例
+- 接口:POST /api/generate/webui/text2img
+  https://liblibai.feishu.cn/sync/FDSPd57O2seBDwbfJOzcpvvCntg
+  1.5和XL图生图 - 最简版参数示例
+- 接口:POST /api/generate/webui/img2img
+  https://liblibai.feishu.cn/sync/JPsPdxCIvskntObd6vNc3a0knAb
+  图生图 - 局部重绘参数示例
+- 接口:POST /api/generate/webui/img2img
+  https://liblibai.feishu.cn/sync/HH8UdbOOzsNQ8Vb3kktcKm7JnHg
+  Controlnet局部重绘参数示例
+- 接口:POST /api/generate/webui/text2img
+  https://liblibai.feishu.cn/sync/V6jkdvktosdIrfbNiK3cgpB1nOg
+  InstantID人像换脸参数示例
+- 接口:POST /api/generate/webui/text2img
+  https://liblibai.feishu.cn/sync/BeY5dsCs4sFOtcb2027c8X8Lnrb
+  F.1 - PuLID人像换脸参数示例
+- 接口:POST /api/generate/webui/text2img
+  https://liblibai.feishu.cn/sync/UBHXdg5FesSfgybW8avcqvLZn4e
+  F.1 - 风格迁移参数示例
+- 接口:POST /api/generate/webui/text2img
+  {
+  "templateUuid": "6f7c4652458d4802969f8d089cf5b91f", // 参数模板ID
+  "generateParams": {
+  // 基础参数
+  "prompt": "The image is a portrait of a young woman with a bouquet of flowers in her hair. She is wearing a white blouse and has a happy expression on her face. The flowers are pink and white daisies with green leaves and stems. The background is a light blue color. The overall mood of the image is dreamy and ethereal.", // 选填
+  "steps": 25, // 采样步数
+  "width": 768, // 宽
+  "height": 1024, // 高
+  "imgCount": 1, // 图片数量
+          // 风格参考的相关配置
+          "controlNet": [
+              {
+                  "unitOrder": 0,
+                  "sourceImage": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/a9cf89f2d4bec50d81feb021dd25c505865fbc7b19a3979d76773fcf1f581dee.png",
+                  "width": 1024,
+                  "height": 1024,
+                  "preprocessor": 66,
+                  "annotationParameters": {
+                      "ipAdapterSiglip": {
+                          "preprocessorResolution": 1024
+                      }
+                  },
+                  "model": "c6ed70879cf011ef96d600163e37ec70",
+                  "controlWeight": 0.75, // 控制权重推荐取0.6 ~ 0.75之间
+                  "startingControlStep": 0,
+                  "endingControlStep": 1,
+                  "pixelPerfect": 1,
+                  "controlMode": 0,
+                  "resizeMode": 1
+              }
+          ]
+      }
+  }
+  F.1 - 主体参考参数示例(仅支持文生图)
+- 接口:POST /api/generate/webui/text2img
+  {
+  "templateUuid": "5d7e67009b344550bc1aa6ccbfa1d7f4",
+  "generateParams": {
+  "prompt": "focus on the cat,there is a cat holding a bag of mcdonald, product advertisement,",
+  "width": 768,
+  "height": 1024,
+  "imgCount": 1,
+  "cfgScale": 3.5,
+  "randnSource": 0,
+  "seed": -1,
+  "clipSkip": 2,
+  "sampler": 1,
+  "steps": 30,
+  "restoreFaces": 0,
+  "controlNet": [
+  {
+  "unitOrder": 0,
+  "sourceImage": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/5fae2d9099c208487bc97867bece2bf3d904068e307c7bd30c646c9f3059af33.png",
+  "width": 768,
+  "height": 1024,
+  "preprocessor": 68,
+  "annotationParameters": {
+  "entityControl": {}
+  },
+  "model": "6f1767b5f9eb47289525d06ae882a0e5",
+  "controlWeight": 0.9,
+  "startingControlStep": 0,
+  "endingControlStep": 1,
+  "pixelPerfect": 1,
+  "controlMode": 0,
+  "resizeMode": 1
+  }
+  ]
+  }
+  }
+
+5. F.1 Kontext
+   单次调用消耗api积分
+   pro版本: 29积分,原价0.29元/张
+   max版本: 58积分,原价0.58元/张
+   5.1 F.1 Kontext - 文生图
+   5.1.1 接口定义
+
+- 请求地址:
+  POST /api/generate/kontext/text2img
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  是
+  fe9928fde1b4491c9b360dd24aa2b115
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+5.1.2 参数说明
+变量名
+格式
+备注
+数值范围
+必填
+示例
+model
+enums
+模型
+
+- pro
+- max:默认
+  否
+
+prompt
+
+string
+正向提示词,文本
+
+- 不超过2000字符
+
+是
+
+{
+"templateUuid":"fe9928fde1b4491c9b360dd24aa2b115",
+"generateParams":{
+"model":"pro",
+"prompt":"画一个LibLib公司的品牌海报",
+"aspectRatio":"3:4",
+"guidance_scale":3.5,
+"imgCount":1  
+ }
+}
+
+aspectRatio
+
+enums
+图片宽高比
+
+- 1:1 - 默认
+- 2:3
+- 3:2
+- 3:4
+- 4:3
+- 9:16
+- 16:9
+- 9:21
+- 21:9
+
+否
+
+imgCount
+int
+单次生图张数
+
+1. 默认值:1
+2. 阈值范围:1 ~ 4
+   否
+
+guidance_scale
+double
+提示词引导系数
+
+1. 默认值:3.5
+2. 阈值范围:1.0 ~ 20.0
+   否
+
+5.1.3 返回值
+参数
+类型
+备注
+generateUuid
+string
+生图任务uuid,使用该uuid查询生图进度
+
+5.2 F.1 Kontext - 图生图(指令编辑&多图参考)
+5.2.1 接口定义
+
+- 请求地址:
+  POST /api/generate/kontext/img2img
+- headers:
+  header
+  value
+  Content-Type
+  application/json
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  是
+- 1c0a9712b3d84e1b8a9f49514a46d88c
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+5.2.2 参数说明
+变量名
+格式
+备注
+数值范围
+必填
+示例
+model
+enums
+模型
+
+- pro:暂不支持多图参考
+- max:默认
+  否
+
+prompt
+
+string
+正向提示词,文本
+
+- 不超过2000字符
+  是
+
+{
+"templateUuid":"1c0a9712b3d84e1b8a9f49514a46d88c",
+"generateParams":{
+"prompt":"Turn this image into a Ghibli-style, a traditional Japanese anime aesthetics.",
+"aspectRatio":"2:3",
+"guidance_scale":3.5,
+"imgCount":1,
+"image_list":[
+"https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/3c65a38d7df2589c4bf834740385192128cf035c7c779ae2bbbc354bf0efcfcb.png"]  
+ }
+}
+aspectRatio
+
+enums
+图片宽高比
+
+- 1:1 - 默认
+- 2:3
+- 3:2
+- 3:4
+- 4:3
+- 9:16
+- 16:9
+- 9:21
+- 21:9
+
+否
+
+imgCount
+int
+单次生图张数
+
+1. 默认值:1
+2. 阈值范围:1 ~ 4
+   否
+
+guidance_scale
+double
+提示词引导系数
+
+1. 默认值:3.5
+2. 阈值范围:1.0 ~ 20.0
+   否
+
+image_list
+
+Array
+参考图
+
+- 图片数量:1~4,可公网访问的图片地址
+- 图片格式:PNG, JPG, JPEG, WEBP
+- 图片大小:每张图都不超过10MB
+  是
+
+  5.2.3 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  5.3 查询任务结果
+  5.3.1 接口定义
+
+说明
+接口定义
+
+- 接口:POST /api/generate/status
+- 请求body:
+  参数
+  类型
+  是否必需
+  备注
+  generateUuid
+  string
+  是
+  生图任务uuid,发起生图任务时返回该字段
+  5.3.2 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  generateStatus
+  int
+  生图状态见下方3.3.1节
+  percentCompleted
+  float
+  生图进度(智能算法IMG1不支持)
+  generateMsg
+  string
+  生图信息,提供附加信息,如生图失败信息
+  pointsCost
+  int
+  本次生图任务消耗积分数
+  accountBalance
+  int
+  账户剩余积分数
+  images
+  []object
+  图片列表,只提供审核通过的图片
+  images.0.imageUrl
+  string
+  图片地址,可直接访问,地址有时效性:7天
+  images.0.seed
+  int
+  随机种子值(智能算法IMG1不支持)
+  images.0.auditStatus
+  int
+  审核状态说明
+  5.4 示例demo
+  暂时无法在飞书文档外展示此内容
+
+6. 智能算法 IMG1
+   6.1 智能算法 IMG1 - 生图
+   6.1.1 接口定义
+
+- 请求地址:
+  POST /api/generate/smart-img1/generate
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  是
+  86c58ea26e9a45bd9f562c6306c17c0f
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+6.1.2 参数说明
+变量名
+格式
+备注
+数值范围
+必填
+示例
+prompt
+
+string
+正向提示词,文本
+
+- 不超过2000字符
+  是
+
+{
+"templateUuid":"86c58ea26e9a45bd9f562c6306c17c0f",
+"generateParams":{
+"prompt":"参考以下两张图,让黄猫坐在椅子上,画一张海报",
+"aspectRatio":"auto",
+"quality":"normal",
+"imgCount":1,
+"image_list":[
+"https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/3c65a38d7df2589c4bf834740385192128cf035c7c779ae2bbbc354bf0efcfcb.png",
+"https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/92cc6b39931ed0932dfe49a7b354ce1a8f6ede819ccbf8a9f3a2fc315b0be42a.png"
+]
+}  
+ }
+}
+aspectRatio
+
+enums
+图片宽高比
+
+1. auto:
+1. 自适应
+1. square:
+
+- 宽高比:1:1,通用
+- 具体尺寸:1024\*1024
+
+3. portrait:
+1. 宽高比:2:3,适合人物肖像
+1. 具体尺寸:1024\*1536
+1. landscape:
+1. 宽高比:3:2,适合横幅画面
+1. 具体尺寸:1536\*1024
+
+否
+
+imgCount
+int
+单次生图张数
+1 ~ 4
+是
+
+quality
+enums
+图片质量
+
+- turbo
+- normal
+- masterpiece
+  否
+
+image_list
+
+Array
+参考图
+
+- 图片数量:1~8,可公网访问的图片地址
+- 图片格式:PNG, JPG, JPEG, WEBP
+- 图片大小:每张图都不超过10MB
+  否
+
+  6.1.3 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+
+  6.2 智能算法 IMG1 - 局部重绘
+  6.2.1 接口定义
+
+- 请求地址:
+  POST /api/generate/smart-img1/inpaint
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  是
+  0fb3ddb15a094e74b1241fbda5db3199
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+6.2.2 参数说明
+变量名
+格式
+备注
+数值范围
+必填
+示例
+prompt
+
+string
+正向提示词,文本
+
+- 不超过2000字符
+  是
+  {
+  "templateUuid":"0fb3ddb15a094e74b1241fbda5db3199",
+  "generateParams":{
+  "prompt":"把黄猫变成一只看书的狗",
+  "aspectRatio":"auto",
+  "quality":"normal",
+  "imgCount":1,
+  "image":"https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/3c65a38d7df2589c4bf834740385192128cf035c7c779ae2bbbc354bf0efcfcb.png",
+  "mask":"https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/d3a972172506362134b7d26502afd747f9ed68b4ce39045c6793abf6afd864f0.png"
+  }  
+   }
+  }
+
+image
+
+string
+
+原图URL地址
+
+- 可公网访问的图片地址
+- 图片格式:PNG, JPG, JPEG, WEBP
+- 图片大小:不超过4MB
+  是
+
+mask
+
+string
+蒙版文件地址
+
+- 蒙版图URL
+- 图片格式:png
+- 要求:白色蒙版,黑色底色
+- 图片大小:不超过4MB
+  是
+
+quality
+
+enums
+图片质量
+
+- turbo
+- normal
+- masterpiece
+  否
+
+aspectRatio
+string
+图片宽高比
+
+1. auto:
+1. 自适应
+1. square:
+
+- 宽高比:1:1,通用
+- 具体尺寸:1024\*1024
+
+3. portrait:
+1. 宽高比:2:3,适合人物肖像
+1. 具体尺寸:1024\*1536
+1. landscape:
+1. 宽高比:3:2,适合横幅画面
+1. 具体尺寸:1536\*1024
+   否
+
+imgCount
+int
+单次生图张数
+1 ~ 4
+是
+
+6.2.3 返回值
+参数
+类型
+备注
+generateUuid
+string
+生图任务uuid,使用该uuid查询生图进度
+6.3 查询任务结果
+6.3.1 接口定义
+
+说明
+原型
+接口定义
+
+- 接口:POST /api/generate/status
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  备注
+  generateUuid
+  string
+  是
+  生图任务uuid,发起生图任务时返回该字段
+  6.3.2 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  generateStatus
+  int
+  生图状态见下方3.3.1节
+  percentCompleted
+  float
+  生图进度(智能算法IMG1不支持)
+  generateMsg
+  string
+  生图信息,提供附加信息,如生图失败信息
+  pointsCost
+  int
+  本次生图任务消耗积分数
+  accountBalance
+  int
+  账户剩余积分数
+  images
+  []object
+  图片列表,只提供审核通过的图片
+  images.0.imageUrl
+  string
+  图片地址,可直接访问,地址有时效性:7天
+  images.0.seed
+  int
+  随机种子值(智能算法IMG1不支持)
+  images.0.auditStatus
+  int
+  审核状态说明
+  6.4 示例demo
+  暂时无法在飞书文档外展示此内容
+
+7. 模型选择
+   为了确保API服务的生图速度快速且稳定,生图效果有保障,平台精选了各方向下的高质量模型,仅做参考。
+   全网可商用模型和自有模型皆可调用,详见文档3.1.1。
+   7.1 Checkpoint
+   适用方向
+   基础算法类型
+   模型名称
+   模型版本
+   模型链接
+   模型版本UUID
+   效果参考
+   通用
+   基础算法 F.1
+   F.1基础算法模型-哩布在线可运行
+
+F.1-dev-fp8
+
+https://www.liblib.art/modelinfo/488cd9d58cd4421b9e8000373d7da123
+412b427ddb674b4dbab9e5abd5ae6057
+
+[图片]
+通用
+基础算法 XL
+
+Dream Tech XL | 筑梦工业XL
+v6.0 - 寄语星河
+https://www.liblib.art/modelinfo/5611e2f826be47f5b8c7eae45ed5434a
+
+0ea388c7eb854be3ba3c6f65aac6bfd3
+
+[图片]
+通用
+基础算法 XL
+
+Dream Tech XL | 筑梦工业XL
+v5.0 - 与光同尘
+https://www.liblib.art/modelinfo/5611e2f826be47f5b8c7eae45ed5434a
+a57911b5dfe64c6aa78821be99367276
+
+[图片]
+人像摄影
+基础算法 XL
+AWPortrait XL
+1.1
+https://www.liblib.art/modelinfo/f8b990b20cb943e3aa0e96f34099d794
+21df5d84cca74f7a885ba672b5a80d19
+[图片]
+现代创意插画
+基础算法 1.5
+
+ComicTrainee丨动漫插画模型
+
+v2.0
+https://www.liblib.art/modelinfo/d6053875cca7478a8ab39522b4e7cc1a
+c291e0d339f44a98a973f138e6b0b9dc
+[图片]
+现代创意插画
+基础算法 XL
+niji-动漫二次元-sdxl
+2
+https://www.liblib.art/modelinfo/3ecd30364b564a7cadbf4f7f7e7110cf
+bd065cff3a854af2b28659ed0f6d289d
+[图片]
+现代创意插画
+基础算法 XL
+Neta Art XL 二次元角色 (更新V2)
+V2.0
+https://www.liblib.art/modelinfo/55b06e35dd724862b3524ff00b069fe8
+bfb95ad44a2c4d88963d3147de547600
+[图片]
+视觉海报
+基础算法 XL
+真境写真XL Elite KV | 电商产品摄影海报视觉设计
+VisionX 万物绘
+https://www.liblib.art/modelinfo/75656a71d6c3448cb621d03f67198f6b
+dfe59b044783487e8fb0800fc4e8ccc3
+[图片]
+建筑设计
+基础算法 1.5
+城市设计大模型 | UrbanDesign
+v7
+https://www.liblib.art/modelinfo/5e1b4ea7f9554e46b2509f59269b1ea8
+f40405b7404a455db689a6646a75c103
+[图片]
+建筑设计
+基础算法 XL
+比鲁斯大型建筑大模型
+XL0.35*PRO
+https://www.liblib.art/modelinfo/a7177a52c3e74e04a65aff5bab87d01a
+d3bfdeba43bc4b5ca44e35d9fcd2f487
+[图片]
+7.2 LoRA
+适用方向
+基础算法类型
+模型名称
+模型版本名称
+触发词
+模型链接
+模型版本UUID
+效果参考
+人像摄影
+基础算法 F.1
+Filmfotos*日系胶片写真
+
+FLUX
+
+filmfotos,film grain,reversal film photography
+
+https://www.liblib.art/modelinfo/ec983ff3497d46ea977dbfcd1d989f67
+b59f7eb734864a74ba476af3aa28c2f3
+
+[图片]
+人像摄影
+基础算法F.1
+极氪白白酱F.1-人像V6MAX
+
+V6MAX
+JKBB
+https://www.liblib.art/modelinfo/922d83dbec8e4b4b9033851f0038ae90?from=feed&versionUuid=169505112cee468b95d5e4a5db0e5669
+169505112cee468b95d5e4a5db0e5669
+[图片]
+电商场景
+基础算法 F.1
+电商-F.1- | 运营启动页
+v1.0
+yun
+https://www.liblib.art/modelinfo/033c3ddf8c6f4baba02b2d149ca8310b
+76af914cc3434937aa13aeb038aae838
+[图片]
+视觉海报
+基础算法 F.1
+UNIT-F.1-MandelaEffect-LoRA
+
+曼德拉效应
+
+/
+
+https://www.liblib.art/modelinfo/02b89792af674243b46db46349393c02
+50284151e507431facc2325cd62f73a3
+[图片]
+创意插画
+基础算法 F.1
+万物调节丨Flux 情绪插画
+
+V1.0
+Simple vector illustration
+https://www.liblib.art/modelinfo/6256d14b3a5545cba79f6ca84ab04491?from=feed&versionUuid=be3909c5d7114d3b8717e966c884d3e1
+be3909c5d7114d3b8717e966c884d3e1
+[图片]
+创意插画
+基础算法 F.1
+嘉嘉\_国潮插画\_F.1
+v1.0
+/
+https://www.liblib.art/modelinfo/2b4cb7c1799e4f73a00535dc71af73fc
+b1d4b896d69d408b815b545126a92df0
+
+[图片]
+创意插画
+基础算法 F.1
+风月无边illustrations
+v1.0
+/
+https://www.liblib.art/modelinfo/b275bf18078b41b38e3dbc40d5b3fead
+85a2a6bd4dd945a78f6430c9c4911cf0
+[图片]
+创意插画
+基础算法 F.1
+岩彩材质绘画
+v1
+mineral
+https://www.liblib.art/modelinfo/dcd294b15ee0445ebb1917ec011e9f37
+46d4086b1a60448dbbeea52e1218bb8b
+[图片]
+视觉海报
+基础算法 XL
+筑梦工业 | 海报美学XL
+v1.0
+Movie Poster Style
+https://www.liblib.art/modelinfo/7bcd8a2e75bf4962baaadca9cd01e982
+31360f2f031b4ff6b589412a52713fcf
+
+[图片]
+扁平插画
+基础算法 XL
+CJ_illustration丨商业扁平插画XL
+v1.0
+Illustration
+https://www.liblib.art/modelinfo/d6e507424dcd4c728e587db7ddfb9c41
+1fe2174f51d04fedb724b28f48d55b7a
+[图片]
+扁平插画
+基础算法 1.5
+CJ_illustration丨商业扁平插画
+
+v1.3
+Illustration
+https://www.liblib.art/modelinfo/760bc28e05b2422fb5b059c18579497b
+82f1db0f9fbd4c4b85137e6a4e6bba6d
+[图片]
+电商场景
+基础算法 XL
+筑梦工业 | 电商场景-银河系漫游指南XL
+v1.0
+Creative Showcase
+https://www.liblib.art/modelinfo/efeea73d36b541ceaf31a625370d5595
+098f08f604ec4c6c9b4ecf9167d39e63
+[图片]
+电商场景
+基础算法 XL
+电商-超现实主义v2
+超现实主义v2
+changjingA
+https://www.liblib.art/modelinfo/e332caf6720143ab998235489e270de9
+7ba01e531f424ca3b86b4bf00e3abd10
+[图片]
+电商场景
+基础算法 XL
+VisionX 万物绘 | 工业产品设计 | 电商产品摄影
+万物绘LORA*V1
+/
+https://www.liblib.art/modelinfo/b8d0784d423e4c33b7402b28ee2a5b9b
+de0db8bac1844d078e1782bd01a64f35
+[图片]
+电商产品
+基础算法 XL
+【摸鱼】商业写实渲染 | 电商产品场景
+V1
+/
+https://www.liblib.art/modelinfo/b76df870c8d2437bb96c039a13539f53
+b50b9cce2147400cb161d9be5d4adb6e
+[图片]
+电商产品
+基础算法 XL
+【油条】商业产品大片PRO-XL版
+无限创作XL-v1
+Realistic product commercial blockbuster
+https://www.liblib.art/modelinfo/fbc202e8c7d242c581421c171adedcac
+f1119d1dc33a46b8b460dd29ef6dabd2
+[图片]
+电商产品
+基础算法 1.5
+产品摄影,北欧极简高质感
+1.0
+dofas
+https://www.liblib.art/modelinfo/85dd9bc4ed6d42f3b9b9a2e89c3281f6
+f465b7ed06244afa96f5560a5890bad2
+[图片]
+logo&icon设计
+基础算法 XL
+字体logo材质效果-lora-XL-expert
+V1.0
+CZG, Fluid texture
+https://www.liblib.art/modelinfo/8bdabec4b7f44b69954af770744b521b
+bbc080acca124995b3dfbd26e56bb278
+[图片]
+毛绒风
+基础算法 XL
+WDR*毛绒质感ICON
+1.0
+a plush app icon
+https://www.liblib.art/modelinfo/a442656707b14560aaebce87620e39dd
+3dc63c4fe3df4147ac8a875db3621e9f
+[图片]
+毛毡风
+基础算法 1.5
+微缩毛毡风格 | Miniature Felt Style
+V1.0
+Microphotography,Felt style
+https://www.liblib.art/modelinfo/177c72eca76248efa63ab97118ce4c93
+
+f3134ad192a14ea6a7c361e04cb74aea
+[图片]
+蒸汽朋克
+基础算法 XL
+筑梦工业 | 蒸汽朋克XL
+v1.0
+SP style,Steampunk aesthetic
+https://www.liblib.art/modelinfo/a306e642e11d482983aff1591f85c5d9
+0ad44fc3ca564bba864c82a36f3a8f65
+[图片]
+经典艺术插画
+基础算法 1.5
+波普艺术\_SD 1.5
+v1.0
+BoPu
+https://www.liblib.art/modelinfo/94625fe77493410285701ae8c0a9162a
+3b069d49839d4b38b067481ff847fbd8
+[图片]
+现代创意插画
+基础算法 1.5
+白泽MARS-治愈系插画
+S1.0
+/
+https://www.liblib.art/modelinfo/56b1c778a22a4fba8481aa18be2c7795
+1e20fa53df254ff8a0eeee26230952c3
+[图片]
+现代创意插画
+基础算法 1.5
+小清新治愈画风插画
+v2.0
+/
+https://www.liblib.art/modelinfo/4e1d69769c3a499fbbda7bdbd5c775e1
+21b92b68ea9142cba052aaee9a2f5410
+[图片]
+现代创意插画
+基础算法 XL
+HandDrawing l 卡通手绘-SDXL
+
+v1.0
+Cartoon Chinese style
+https://www.liblib.art/modelinfo/5c1be02d031d4b3498d47e1e9b504edb
+5aad2800df224473acbd27d92aea3f3f
+[图片]
+现代创意插画
+基础算法 XL
+筑梦工业 | 风格漫画XL
+v1.0
+Dream Comic Style
+https://www.liblib.art/modelinfo/1993afa92c9443f0b07e84926f2cb773
+7aa06b226feb46f485a6793a8d5a5184
+[图片]
+现代创意插画
+基础算法 1.5
+Dissney Fable 迪士尼风格插画丨CJ_3D
+v1.0
+3D
+https://www.liblib.art/modelinfo/54af7361461a491ab5c0c03e5c64fb56
+9719136dcf26415a8f756ba6cc0946ac
+[图片]
+现代创意插画
+基础算法 XL
+99art·治愈系绘本插画壁纸·小笔触
+1.0
+
+/
+https://www.liblib.art/modelinfo/28471841ac0645e890f92fdd4efeacd5
+ba5e04de8f2d4f8a8e8d6e9bfe93a9b4
+[图片]
+中国风插画
+基础算法 XL
+Muertu XL丨国风绘本插图画风加强
+v1.0
+guofeng
+https://www.liblib.art/modelinfo/407d4f5126e24e7c84e75b7679e76516
+2bc8ff1e8bc847008fd40e40efcdd096
+[图片]
+中国风插画
+基础算法 XL
+筑梦工业 | 新派国画水墨XL
+v1.0
+New Chinese Art Style
+https://www.liblib.art/modelinfo/0f7c3c7c374344d88d802b120d548a04
+c8d2fcf503d04c10af770bd48145ba30
+
+[图片]
+细节优化
+基础算法 1.5
+极致肤感 | 提升皮肤纹理质感
+v-001
+/
+
+https://www.liblib.art/modelinfo/6e5e77d53efe414eb675409d5c834b07
+6da50214cd4743d4b1ce819411594bbe
+[图片]
+对比度调节
+基础算法 1.5
+光泽调节器/Gloss_Tweaker/光沢調整器
+v2.0
+/
+
+https://www.liblib.art/modelinfo/b11668631ddf4b28a3967e84b33e15f2
+d8d47c33f5e34588a1595c8e9bea0d7a
+[图片]
+手部优化
+基础算法 1.5
+万物调节丨手部修复2.0
+V2.0
+perfect hands, delicate hands
+https://www.liblib.art/modelinfo/89f67e2790314a1db744b5a1d0ad4d15
+365e700254dd40bbb90d5e78c152ec7f
+[图片]
+
+7.3 Textual Inversion负向提示词
+适用方向
+模型名称
+模型链接
+Trigger word
+负向提示词 - 通用型提升画面质量
+坏图修复EasyNegative
+https://www.liblib.art/modelinfo/458a14b2267d32c4dde4c186f4724364
+easynegative,EasyNegative_EasyNegative,EasyNegative
+负向提示词 - 通用型提升画面质量
+坏图修复DeepNegativeV1.x
+https://www.liblib.art/modelinfo/03bae325c623ca55c70db828c5e9ef6c
+ng_deepnegative_v1_75t,DeepNegativeV1.x_V175T
+
+负向提示词 - 防止手部崩坏
+badhandv4-AnimeIllustDiffusion
+https://www.liblib.art/modelinfo/9720584f1c3108640eab0994f9a7b678
+badhandv4,badhandv4-AnimeIllustDiffusion_badhandv4
+负向提示词 - 通用型提升画面质量
+坏图修复veryBadImageNegative
+https://www.liblib.art/modelinfo/cbaa93b1001c969c99b6b91a201686ad
+verybadimagenegative_v1.3,veryBadImageNegative_veryBadImageNegative_v1.3
+负向提示词 - 防止手部崩坏
+坏手修复negative_hand Negative Embedding
+https://www.liblib.art/modelinfo/388589a91619d4be3ce0a0d970d4318b
+negative_hand
+负向提示词 - 防止手部崩坏
+Bad-Hands-5
+https://www.liblib.art/modelinfo/eafbd93338474dcea0d7432b6229dea9
+bad-hands-5,BadHandsV5
+负向提示词 - 通用型提升画面质量
+EasyNegativeV2
+https://www.liblib.art/modelinfo/1bfae4494f3549ce8125021f3f9307ae
+EasyNegativeV2
+负向提示词 - 动漫类提升画面质量
+坏图修复bad-picturenegativeembeddingforChilloutMix
+https://www.liblib.art/modelinfo/bc840f95f5f88d8f5bd3d2598616ca56
+bad-picture-chill-75v,bad-picturenegativeembeddingforChilloutMix_75VectorVersion
+负向提示词 - 通用型提升画面质量
+FastNegativeV2
+https://www.liblib.art/modelinfo/5c10feaad1994bf2ae2ea1332bc6ac35
+FastNegativeV2
+负向提示词 - 动漫类提升画面质量
+bad-artist-anime
+https://www.liblib.art/modelinfo/f0377e81350e49a98b40a57865070de4
+bad-artist-anime
+负向提示词 - 通用型提升画面质量
+bad_prompt Negative Embedding
+https://www.liblib.art/modelinfo/a84f2a2bcc38445482d095594873e118
+bad_prompt_version2,bad_prompt_version2-neg
+
+负向提示词 - 通用型提升画面质量
+美女BadDream + UnrealisticDream (Negative Embeddings)
+https://www.liblib.art/modelinfo/5ca778dac416e05b0bd0e98a0f4b82db
+BadDream
+7.4 VAE
+基础算法类型
+模型版本名称
+模型版本UUID
+通用
+Automatic
+传空值
+基础算法 1.5
+vae-ft-mse-840000-ema-pruned.safetensors
+2c1a337416e029dd65ab58784e8a4763
+基础算法 1.5
+klF8Anime2VAE_klF8Anime2VAE.ckpt
+d4a03b32d8d59552194a9453297180c1
+基础算法 1.5
+color101VAE_v1.pt
+d9be20ad5a7195ff0d97925e5afc7912
+基础算法 1.5
+cute vae.safetensors
+88ae7501f5194e691a1dc32d6f7c6f1a
+基础算法 1.5
+ClearVAE_V2.3.safetensors
+73f6e055eade7a85bda2856421d786fe
+基础算法 1.5
+difconsistencyRAWVAE_v10.pt
+5e93d0d2a64143a9d28988e75f28cb29
+基础算法 XL
+sd_xl_vae_1.0
+3cefd3e4af2b8effb230b960da41a980
+7.5 采样方法
+采样方法名称
+枚举值
+推荐度
+Euler a
+0
+⭐⭐⭐⭐⭐
+Euler
+1
+⭐⭐⭐
+LMS
+2
+⭐⭐⭐
+HEUN
+3
+⭐⭐⭐
+DPM2
+4
+⭐⭐⭐
+DPM2 a
+5
+⭐⭐⭐
+DPM++ 2S a
+6
+⭐⭐⭐
+DPM++ 2M
+7
+⭐⭐⭐
+DPM++ SDE
+8
+⭐⭐⭐
+DPM++ FAST
+9
+⭐⭐⭐
+DPM++ Adaptive
+10
+⭐⭐⭐⭐
+LMS Karras
+11
+⭐⭐⭐
+DPM2 Karras
+12
+⭐⭐⭐
+DPM2 a Karras
+13
+⭐⭐⭐
+DPM++ 2S a
+14
+⭐⭐⭐
+DPM++ 2M Karras
+15
+⭐⭐⭐⭐⭐
+DPM++ SDE Karras
+16
+⭐⭐⭐⭐⭐
+DDIM
+17
+⭐⭐⭐
+PLMS
+18
+⭐⭐⭐
+UNIPC
+19
+⭐⭐⭐
+DPM++ 2M SDE Karras
+20
+⭐⭐⭐⭐⭐
+DPM++ 2M SDE EXPONENTIAL
+21
+⭐⭐⭐⭐
+DPM++ 2M SDE Heun Karras
+24
+⭐⭐⭐
+DPM++ 2M SDE Heun Exponential
+25
+⭐⭐⭐
+DPM++ 3M SDE Karras
+27
+⭐⭐⭐⭐
+DPM++ 3M SDE Exponential
+28
+⭐⭐⭐⭐
+Restart
+29
+⭐⭐⭐
+LCM
+30
+⭐⭐⭐
+7.6 放大算法模型
+模型名称
+模型枚举值
+原理简介
+适用方向
+缺点
+推荐度
+Latent
+0
+传统放大
+低分辨率图像的适度放大,对原图保留度高
+Latent系列普遍细节恢复能力弱,可能出现锯齿状边缘。使用Latent放大时,建议重绘幅度 > 0.5,否则图像可能是模糊的。
+⭐⭐⭐
+Latent (antialiased)
+1
+在 Latent 的基础上增加了抗锯齿处理,适合需要平滑边缘的图像。
+低分辨率图像的适度放大,对原图保留度高
+Latent系列普遍细节恢复能力弱,可能出现锯齿状边缘。使用Latent放大时,建议重绘幅度 > 0.5,否则图像可能是模糊的。
+⭐⭐
+Latent (bicubic)
+2
+使用双三次插值算法,适合需要较高质量放大的图像。
+低分辨率图像的适度放大,对原图保留度高
+Latent系列普遍细节恢复能力弱,可能出现锯齿状边缘。使用Latent放大时,建议重绘幅度 > 0.5,否则图像可能是模糊的。
+⭐⭐
+Latent (bicubic antialiased)
+3
+结合双三次插值和抗锯齿,适合高质量且平滑的图像放大。
+低分辨率图像的适度放大,对原图保留度高
+Latent系列普遍细节恢复能力弱,可能出现锯齿状边缘。使用Latent放大时,建议重绘幅度 > 0.5,否则图像可能是模糊的。
+⭐⭐
+Latent (nearest)
+4
+使用最近邻插值,速度快但质量较低,适合简单图形。
+低分辨率图像的适度放大,对原图保留度高
+Latent系列普遍细节恢复能力弱,可能出现锯齿状边缘。使用Latent放大时,建议重绘幅度 > 0.5,否则图像可能是模糊的。
+⭐⭐
+Latent (nearest-exact)
+5
+使用精确的最近邻插值算法,适合需要保留原始像素的图像。
+低分辨率图像的适度放大,对原图保留度高
+Latent系列普遍细节恢复能力弱,可能出现锯齿状边缘。使用Latent放大时,建议重绘幅度 > 0.5,否则图像可能是模糊的。
+⭐⭐
+Lanczos
+6
+一种高质量的插值算法,适合需要高保真度的图像放大,尤其在细节丰富的图像中表现良好。
+高细节图像(如风景、建筑)的放大,能够很好地保留图像细节和清晰度。
+对原图质量要求较高,处理速度可能较慢。这种算法适用于升级分辨率较低的图像、文档或照片,以获得更高质量、更清晰的图像。对原图没有任何优化,仅仅只是放大像素
+⭐⭐⭐
+
+Nearest
+7
+最近邻插值,简单快速,适合低质量图像放大,通常用于图形和图标。
+这种算法通常适用于对速度需求较高而不需要过多细节的场景下。适合低清晰度图像的放大。
+可能会造成图像边缘模糊、细节丢失或图像瑕疵等。
+⭐⭐⭐
+ESRGAN_4x
+8
+基于增强型超分辨率生成对抗网络,适合高质量图像放大,尤其在细节和纹理复原方面表现突出。
+GAN系列普遍能够有效恢复图像中的细节,适合图像细节的补充。
+计算复杂度高,处理速度较慢。
+⭐⭐⭐⭐
+LDSR
+9
+基于深度学习的超分辨率算法,适合需要处理复杂细节的图像。
+能够有效恢复细节和纹理,生成的图像通常质量较高。适用于对 CT、MRI 等医学图像进行重建和处理
+计算复杂度高,处理速度较慢。
+⭐⭐⭐
+R-ESRGAN_4x+
+10
+改进版的 ESRGAN,适合高质量放大,特别是在图像细节和清晰度方面。
+主要用于增强细节和保留更多纹理信息,对写实的图片和照片最合适,比较全能。
+动漫场景会略逊一筹
+⭐⭐⭐⭐⭐
+R-ESRGAN_4x+ Anime6B
+11
+针对动漫图像优化的 R-ESRGAN,适合动漫风格的图像放大。
+专门针对动漫风格优化,能保持色彩鲜艳和边缘清晰。
+可能对现实图像效果不佳。
+⭐⭐⭐⭐⭐
+ScuNET GAN
+12
+ScuNET GAN 是基于生成对抗网络的超分辨率方法
+适用于对比较复杂、高精度的图像超分辨率场景
+在处理复杂纹理或图案时,可能影响最终图像的真实感。
+⭐⭐⭐
+ScuNET PSNR
+13
+相较于ScuNET GAN,PSNR 版本则更注重图像质量。
+在自然、艺术、人像等需要保持色彩的鲜艳度和细节完整性的领域表现突出。
+在处理复杂纹理或图案时,可能影响最终图像的真实感。
+⭐⭐⭐
+SwinIR_4x
+14
+基于 Swin Transformer 的超分辨率方法,适合复杂场景的图像放大。
+适合低清晰度图片的增强,以及高细节图像(如风景、建筑)的放大,能够很好地保留图像细节和清晰度,适用于厚涂插画。
+可能会过度增强,生成伪影,影响图像的真实感和视觉质量。
+⭐⭐⭐
+4x-UltraSharp
+15
+专注于图像锐化的超分辨率算法,适合需要增强边缘和细节的图像。
+适合增强图像边缘和细节。
+对原图质量要求较高,不适合低清图片的放大。
+⭐⭐⭐⭐⭐
+8x-NMKD-Superscale
+16
+该算法专注于高倍放大(8倍),可以有效提升图像分辨率。
+采用深度学习技术来增强图像细节和纹理,能够处理复杂的图像内容。
+高倍放大算法,适合需要极高分辨率的图像。拥有了更真实的处理细节,不仅仅只追求把人物还原得光滑好看,它还增加了很多噪点和毛孔细节,让人物看起来更加真实可信,并且因为训练集中含有大量的胶片摄影素材,因此很适合真实人像的放大。色调相对偏冷一些。
+
+对原图质量要求较高,不适合低清图片的放大。
+
+⭐⭐⭐⭐⭐
+4x_NMKD-Siax_200k
+17
+该算法为4倍放大,基于特定的数据集(200k)进行训练,优化了在该数据集上的表现。
+侧重于图像的细节恢复,尤其在处理低质量图像时表现良好。
+适合于需要中等放大的图像,尤其是那些在特定领域(如医学图像、卫星图像)中应用。
+对原图质量要求较高,不适合低清图片的放大。
+⭐⭐⭐⭐
+4x_NMKD-Superscale-SP_178000_G
+18
+同样为4倍放大,基于不同的数据集(178000)进行训练,具有不同的优化目标。
+可能在特定类型的图像上表现更好,尤其是在处理特定风格或特征的图像时。
+适合对图像质量有较高要求的应用,特别是在需要保持图像特征的情况下。
+对原图质量要求较高,不适合低清图片的放大。
+
+⭐⭐⭐⭐
+4x-AnimeSharp
+19
+针对动漫图像的锐化和放大算法,适合动漫风格图像。
+专门针对动漫风格优化,能保持色彩鲜艳和边缘清晰。
+可能对现实图像效果不佳。
+⭐⭐⭐⭐⭐
+4x_foolhardy_Remacri
+20
+强调细节恢复的放大算法,适合需要高细节保留的图像。
+强调细节恢复,能改善模糊效果。
+处理时间较长,且效果依赖于原始图像质量。
+⭐⭐⭐
+BSRGAN
+21
+基于对抗学习的超分辨率方法,适合高质量图像放大。
+能够有效恢复压缩图像中的细节。
+对原图质量要求较高,不适合低清图片的放大。
+⭐⭐⭐
+DAT 2
+22
+主要用于图像解压和放大,适合需要处理压缩图像的场景。DAT2, DAT3和DAT4是基于不同版本的深度学习超分辨率算法,通常针对不同的应用场景和数据集进行优化。
+强调细节恢复,能改善模糊效果。DAT2的放大质量是3款中最佳的。
+处理时间较长,且效果依赖于原始图像质量。
+⭐⭐⭐⭐⭐
+DAT 3
+23
+主要用于图像解压和放大,适合需要处理压缩图像的场景。
+强调细节恢复,能改善模糊效果。
+处理时间较长,且效果依赖于原始图像质量。
+⭐⭐⭐
+DAT 4
+24
+主要用于图像解压和放大,适合需要处理压缩图像的场景。
+强调细节恢复,能改善模糊效果。DAT4是3款中最快的。
+处理时间较长,且效果依赖于原始图像质量。
+⭐⭐⭐
+4x-DeCompress
+25
+主要用于图像解压和放大,适合需要处理压缩图像的场景。
+适合材质效果的增强
+对原图质量要求较高,不适合低清图片的放大。
+⭐⭐⭐⭐
+4x-DeCompress Strong
+26
+主要用于图像解压和放大,适合需要处理压缩图像的场景。
+适合材质效果的增强
+对原图质量要求较高,不适合低清图片的放大。
+⭐⭐⭐⭐⭐ 8. ComfyUI工作流
+8.1 ComfyUI工作流生图
+
+- 接口:POST /api/generate/comfyui/app
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  否
+  默认模版:4df2efa0f18d46dc9758803e478eb51c
+
+generateParams
+object
+是
+生图参数,json结构
+前端自动创建该工作流版本的API参数示例
+
+1. 目前Lib已开放全站的可商用、可在线运行工作流供API使用,您可以在Lib站内工作流合集检索,https://www.liblib.art/workflows
+   [图片]
+2. 在工作流的详情页会出现【本工作流已提供API服务】,且可查看API相关参数。(详情页未出现API参数的工作流,暂不支持API调用)
+   [图片]
+   [图片]
+
+- 返回值:
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+- 参数示例
+  request_body ={
+  "templateUuid": "4df2efa0f18d46dc9758803e478eb51c",
+  "generateParams": {
+  "12": {
+  "class_type": "LoadImage",
+  "inputs": {
+  "image": "https://liblibai-tmp-image.liblib.cloud/img/baf2e419ce1cb06812314957efd2e067/af0c523d3d2b4092ab45c64c72e4deb76babb12e9b8a178eb524143c3b71bf85.png"
+  }
+  },
+  "112": {
+  "class_type": "ImageScale",
+  "inputs": {
+  "width": 768
+  }
+  },
+  "136": {
+  "class_type": "RepeatLatentBatch",
+  "inputs": {
+  "amount": 4
+  }
+  },
+  "137": {
+  "class_type": "LatentUpscaleBy",
+  "inputs": {
+  "scale_by": 1.5
+  }
+  },
+  "workflowUuid": "2f22ab7ce4c044afb6d5eee2e61547f3"
+  }
+  }
+- 参数说明示例(仅少量节点)
+  节点ID
+  节点类型
+  节点名称
+  参数项
+  参数名称
+  参数说明
+  80
+
+LoadImage
+
+风格图像
+
+image
+图像
+
+{
+"parentId": 80,
+"id": "image",
+"name": "image",
+"displayName": "图像",
+"type": "IMAGE",
+"defaultValue": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/aa1a1459986e5cc2b1236f7dc43a029119d6fe6ac26f1961a6639d21ca0b0bbe.png",
+"image_upload": true,
+"isMaskImage": false
+}
+79
+ApplyIPAdapterFlux
+风格设置
+
+weight
+风格强度
+
+{
+"parentId": 79
+"id": "weight",
+"name": "weight",
+"displayName": "风格强度",
+"type": "FLOAT",
+"defaultValue": 0.75,
+"min": -1,
+"max": 5,
+"step": 0.05  
+}
+76
+
+SeargePromptCombiner
+请描述要绘制的画面
+
+prompt1
+画面描述
+
+{
+"parentId": 76,
+"id": "prompt1",
+"name": "prompt1",
+"displayName": "画面描述",
+"type": "STRING",
+"defaultValue": "Anime art, low angle shot back view silhouette of a boy standing on a building rooftop next to a telescope at night, looking up towards the glowing milky way and shooting stars in the starry night, gradient blue orange and pink night sky, dim lighting, dark lighting, highly detailed, ultra-high resolutions, 32K UHD, best quality, masterpiece\n",
+},
+
+8.2 查询生图结果
+
+- 接口:POST /api/generate/comfy/status
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  备注
+  generateUuid
+  string
+  是
+  生图任务uuid,发起生图任务时返回该字段
+- 返回值:
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  generateStatus
+  int
+  生图任务的执行状态:
+- 1:等待执行
+- 2:执行中
+- 3:已生图
+- 4:审核中
+- 5:任务成功
+- 6:任务失败
+  percentCompleted
+  float
+  生图进度,0到1之间的浮点数,(暂未实现)
+  generateMsg
+  string
+  生图信息,提供附加信息,如生图失败信息
+  pointsCost
+  int
+  本次生图任务消耗积分数
+  accountBalance
+  int
+  账户剩余积分数
+  images
+  []object
+  图片列表,只提供审核通过的图片
+  images.0.imageUrl
+  string
+  图片地址,可直接访问,地址有时效性:7天
+  images.0.seed
+  int
+  随机种子值
+  iamges.0.auditStatus
+  int
+  审核状态:
+- 1:待审核
+- 2:审核中
+- 3:审核通过
+- 4:审核拦截
+- 5:审核失败
+  videos
+  []object
+  图片列表,只提供审核通过的图片
+  videos.0.videoUrl
+  string
+  视频列表,只提供审核通过的视频
+  videos.0.coverPath
+  string
+  视频地址,可直接访问,地址有时效性:7天
+  videos.0.nodeId
+  string
+  输出视频的节点ID(可忽略)
+  videos.0.outputName
+  string
+  输出视频的节点名称
+  videos.0.auditStatus
+  int
+  审核状态:
+- 1:待审核
+- 2:审核中
+- 3:审核通过
+- 4:审核拦截
+- 5:审核失败
+  示例:
+  {
+  "code": 0,
+  "data": {
+  "accountBalance": 91111,
+  "generateStatus": 5,
+  "generateUuid": "a996794faff8424a8ff56acb421e7305",
+  "images": [
+  {
+  "auditStatus": 3,
+  "imageUrl": "https://liblibai-tmp-image.liblib.cloud/img/360643a3d8414af8b99664b208bc9302/35801ecbf6e6ea8ad89c2606b68d30dfc9579713f5d917694d1616c57afe82fb.png",
+  "nodeId": "91",
+  "outputName": "SaveImage"
+  }
+  ],
+  "percentCompleted": 1,
+  "pointsCost": 10,
+  "videos": []
+  },
+  "msg": ""
+  }}
+
+  8.3 部分工作流推荐
+  全量请至https://www.liblib.art/workflows挑选。
+  使用以下工作流时,只有inputs中的参数是需要自定义的,其他部分请不要动。
+  功能方向
+  链接
+  API参数
+  标准版*按分辨率缩放
+  比较推荐,很快
+  https://www.liblib.art/modelinfo/1bf585fa9ae7455395ee7a595c3920a3?from=personal_page&versionUuid=fa2e042e32fa4aabbbacc255b4ab2cca
+  {
+  "templateUuid": "4df2efa0f18d46dc9758803e478eb51c",
+  "generateParams": {
+  "workflowUuid": "fa2e042e32fa4aabbbacc255b4ab2cca",
+  "30":
+  {
+  "class_type": "LoadImage",
+  "inputs":
+  {
+  "image": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/5fae2d9099c208487bc97867bece2bf3d904068e307c7bd30c646c9f3059af33.png"
+  }
+  },
+  "31":
+  {
+  "class_type": "ImageScale",
+  "inputs":
+  {
+  "width": 2048,
+  "height": 2048
+  }
+  }
+  }
+  }
+  标准版*按系数放大
+
+https://www.liblib.art/modelinfo/1bf585fa9ae7455395ee7a595c3920a3?from=personal_page&versionUuid=9a1c74ae498640c28e4269958b1a1b15
+{
+"templateUuid": "4df2efa0f18d46dc9758803e478eb51c",
+"generateParams": {
+"workflowUuid": "9a1c74ae498640c28e4269958b1a1b15",
+"30":
+{
+"class_type": "LoadImage",
+"inputs":
+{
+"image": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/5fae2d9099c208487bc97867bece2bf3d904068e307c7bd30c646c9f3059af33.png"
+}
+},
+"37":
+{
+"class_type": "CR Upscale Image",
+"inputs":
+{
+"upscale_model": "ESRGAN_4x",
+"rescale_factor": 2
+}
+}
+}
+}
+SD放大
+https://www.liblib.art/modelinfo/1bf585fa9ae7455395ee7a595c3920a3?from=personal_page&versionUuid=b2c5e10ee73d4cf69a0e51cb1cbc1622
+{
+"templateUuid": "4df2efa0f18d46dc9758803e478eb51c",
+"generateParams": {
+"workflowUuid": "b2c5e10ee73d4cf69a0e51cb1cbc1622",
+"30":
+{
+"class_type": "UltimateSDUpscale",
+"inputs":
+{
+"upscale_by": 2,
+"steps": 30
+}
+},
+"40":
+{
+"class_type": "LoadImage",
+"inputs":
+{
+"image": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/5fae2d9099c208487bc97867bece2bf3d904068e307c7bd30c646c9f3059af33.png"
+}
+},
+"41":
+{
+"class_type": "UpscaleModelLoader",
+"inputs":
+{
+"model_name": "ESRGAN_4x"
+}
+}  
+ }
+}
+图像外扩
+https://www.liblib.art/modelinfo/ef740b8a4f384db48fcf9f208372493a?from=personal_page&versionUuid=99fa146a003743bdb676179fa2e546ca
+{
+"templateUuid": "4df2efa0f18d46dc9758803e478eb51c",
+"generateParams": {
+"workflowUuid": "99fa146a003743bdb676179fa2e546ca",
+"17":
+{
+"class_type": "LoadImage",
+"inputs":
+{
+"image": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/ed68325cbfcf4b8f724b6b5aa5914e7d91358c3bbf81fccd5002950a2f8180df.png"
+}
+},
+"23":
+{
+"class_type": "CLIPTextEncode",
+"inputs":
+{
+"text": "beautiful scenery"
+}
+},
+"44":
+{
+"class_type": "ImagePadForOutpaint",
+"inputs":
+{
+"left": 400,
+"top": 400,
+"right": 400,
+"bottom": 400,
+"feathering": 24
+}
+}  
+ }
+}
+
+8.4 个人工作流调用方法
+需要编辑工作流后发布,务必看完6.4.2⚠️⚠️⚠️
+6.4.1 发布本地工作流
+个人本地搭建的ComfyUI工作流,需要先在LiblibAI主页右上方发布至平台,可按需选择【自见】,必须选【生成图片可出售或用于商业目的】。
+[图片]
+[图片]
+[图片]
+
+6.4.2 编辑工作流(⚠️⚠️⚠️易被忽略的步骤)
+编辑方法,详见:LiblibAI--AI应用指南
+节点适配范围和调整方式详见:ComfyUI FAQ
+成功编辑好的工作流,会出现“运行应用”的button;若未出现,将无法调用API。
+[图片]
+
+6.4.3 发布工作流
+我们需要约30秒-20分钟,自动试跑该工作流,试跑完成后,该工作流的详情页将会出现API调用参数,可完成API支持调用。
+[图片]
+
+8.5 工作流调用费用
+每个工作流不同,消耗积分数可以参考API参数详情页左方试跑示范。
+[图片] 9. libDream&libEdit
+9.1 libDream - 文生图
+9.1.1 接口定义
+
+- 请求地址:
+  POST /api/generate/libDream
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  是
+  aa835a39c1a14cfca47c6fc941137c51
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+9.1.2 参数定义
+变量名
+格式
+备注
+数值范围
+必填
+示例
+prompt
+
+string
+用于生成图像的提示词 ,中英文均可输入
+
+- 画面描述技巧:运用连贯的自然语言描述画面内容(主体+行为+环境等),用短词语描述画面美学(风格、色彩、光影、构图等)
+- 专业词汇英文表述:推荐使用词源语言或英文描述专业词汇,以获更精准效果
+- 图像用途说明:推荐写出图像用途和类型,例如:用途PPT封面背景图、背景素材图/ 类型广告海报设计、纪实摄影
+- 文字排版描述:将文字内容置于双引号“”内,并通过指令描述文字的大小、字体、颜色、风格和位置,实现排版效果的精确调整
+- 不超过2000字符
+
+是
+
+{
+"templateUuid":"aa835a39c1a14cfca47c6fc941137c51",
+"generateParams":{
+"prompt":"画一个LibLib公司的品牌页海报,这是一家AI生图的公司,因此海报要具有高品质艺术感",
+"usePreLlm":False,
+"width":1328,
+"height":1328,
+"scale":2.5,
+"seed":-1,
+"imgCount":1  
+ }
+}
+usePreLlm
+
+bool
+开启文本扩写,会针对输入prompt进行扩写优化,如果输入prompt较短建议开启,如果输入prompt较长建议关闭
+
+- False:默认
+- True
+  否
+
+width
+int
+生成图像的宽
+
+- 默认值:1328
+- 阈值范围:512 ~ 3072
+
+注意:宽高乘积不可超过2048\*2048
+
+否
+
+height
+int
+生成图像的高
+
+- 默认值:1328
+- 阈值范围:512 ~ 3072
+
+注意:宽高乘积不可超过2048\*2048
+否
+
+imgCount
+int
+单次生图张数
+
+- 默认值:1
+- 阈值范围: 1 ~ 4
+  否
+
+scale
+double
+影响文本描述的程度
+
+1. 默认值:2.5
+2. 阈值范围:1.0 ~ 10.0
+3. 步长:0.01
+   否
+
+seed
+int
+随机种子,作为确定扩散初始状态的基础,默认-1(随机)。
+若随机种子为相同正整数且其他参数均一致,则生成图片极大概率效果一致
+
+- 范围:-1 ~ 9999999999
+- -1表示随机
+  否
+
+  9.1.3 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  9.2 libEdit - 指令编辑&智能参考
+  9.2.1 接口定义
+
+- 请求地址:
+  POST /api/generate/libEdit
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  是
+  cd3a6751086b4483ba5f0523aef53a79
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+9.2.2 参数定义
+变量名
+格式
+备注
+数值范围
+必填
+示例
+prompt
+
+string
+用于编辑图像的提示词
+
+建议:
+
+- 建议长度 <= 120字符,prompt过长有概率出图异常或不生效
+- 编辑指令使用自然语言即可
+- 每次编辑使用单指令会更好
+- 局部编辑时指令描述尽量精准,尤其是画面有多个实体的时候,描述清楚对谁做什么,能获取更精准的编辑效果
+- 发现编辑效果不明显的时候,可以调整一下编辑强度scale,数值越大越贴近指令执行
+- 尽量使用清晰的,分辨率高的底图,豆包模型生成的图片编辑效果会更好。
+  参考示例:
+- 添加/删除实体:添加/删除xxx(删除图上的女孩/添加一道彩虹)
+- 修改实体:把xxx改成xxx(把手里的鸡腿变成汉堡)
+- 修改风格:改成xxx风格(改成漫画风格)
+- 修改色彩:把xxx改成xx颜色(把衣服改成粉色的)
+- 修改动作:修改表情动作(让他哭/笑/生气)
+- 修改环境背景:背景换成xxx,在xxx(背景换成海边/在星空下)
+- 不超过2000字符
+
+是
+
+{
+"templateUuid":"cd3a6751086b4483ba5f0523aef53a79",
+"generateParams":{
+"prompt":"Turn this image into a Ghibli-style, a traditional Japanese anime aesthetics.",
+"promptMagic":0,
+"scale":0.5,
+"seed":-1,
+"imgCount":1,
+"image_urls":[
+"https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/3c65a38d7df2589c4bf834740385192128cf035c7c779ae2bbbc354bf0efcfcb.png"]  
+ }
+}
+promptMagic
+int
+
+提示词扩写
+
+- 0:关闭,默认
+- 1:开启
+  否
+
+image_urls
+Array of string
+图片文件URL
+
+- 需输入1张图片
+  是
+
+scale
+double
+提示词引导系数
+
+1. 默认值:0.5
+2. 阈值范围:0 ~ 1
+3. 步长:0.01
+   否
+
+imgCount
+
+int
+单次生图张数
+
+1. 默认值:1
+2. 阈值范围:1 ~ 4
+   否
+
+seed
+int
+随机种子,作为确定扩散初始状态的基础,默认-1(随机)。若随机种子为相同正整数且其他参数均一致,则生成图片极大概率效果一致
+
+- 范围:-1 ~ 9999999999
+- -1表示随机
+  否
+
+  9.2.3 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+
+  9.3 libEditV2 - 指令编辑&智能参考
+  9.3.1 接口定义
+
+- 请求地址:
+  POST /api/generate/libEditV2
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  是
+  cd3a6751086b4483ba5f0523aef53a79
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+9.3.2 参数定义
+变量名
+格式
+备注
+数值范围
+必填
+示例
+prompt
+
+string
+用于编辑图像的提示词
+
+建议:
+
+- 建议长度 <= 120字符,prompt过长有概率出图异常或不生效
+- 编辑指令使用自然语言即可
+- 每次编辑使用单指令会更好
+- 局部编辑时指令描述尽量精准,尤其是画面有多个实体的时候,描述清楚对谁做什么,能获取更精准的编辑效果
+- 发现编辑效果不明显的时候,可以调整一下编辑强度scale,数值越大越贴近指令执行
+- 尽量使用清晰的,分辨率高的底图,豆包模型生成的图片编辑效果会更好。
+  参考示例:
+- 添加/删除实体:添加/删除xxx(删除图上的女孩/添加一道彩虹)
+- 修改实体:把xxx改成xxx(把手里的鸡腿变成汉堡)
+- 修改风格:改成xxx风格(改成漫画风格)
+- 修改色彩:把xxx改成xx颜色(把衣服改成粉色的)
+- 修改动作:修改表情动作(让他哭/笑/生气)
+- 修改环境背景:背景换成xxx,在xxx(背景换成海边/在星空下)
+- 不超过2000字符
+  是
+
+{
+"templateUuid":"c92f91c771db42e2b5dbff66e2e4f7a2",
+"generateParams":{
+"prompt":"Turn this image into a Ghibli-style, a traditional Japanese anime aesthetics.",
+"promptMagic":0,
+"scale":0.5,
+"seed":-1,
+"width":1328,
+"height":1328,
+"imgCount":1,
+"image_urls":[
+"https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/3c65a38d7df2589c4bf834740385192128cf035c7c779ae2bbbc354bf0efcfcb.png"]  
+ }
+}
+
+promptMagic
+int
+
+提示词扩写
+
+- 0:关闭,默认
+- 1:开启
+  否
+
+image_urls
+Array of string
+图片文件URL
+
+- 需输入1张图片
+  是
+
+width
+int
+1、生成图像宽高,系统默认生成1328 \* 1328的图像;
+2、支持自定义生成图像宽高,范围在[512, 2016]内;推荐可选的宽高比:
+
+- 1328 \* 1328(1:1)
+- 1472 \* 1104 (4:3)
+- 1584 \* 1056(3:2)
+- 1664 \* 936(16:9)
+- 2016 \* 864(21:9)
+  注意:
+- 需同时传width和height才会生效;
+- 如果自定义生图宽高都比1024小很多(如:600以下)可能出图全黑,建议优先设置接近1024的生图宽高;
+- 最终输出图宽高与传入宽高相关但不完全相等,为“与传入宽高最接近16整数倍”的像素值,范围在 [512, 1536] 内;
+- 阈值范围:512~2016
+- 默认值:1328
+  否
+
+height
+int
+
+- 阈值范围:512~2016
+- 默认值:1328
+  否
+
+scale
+double
+提示词引导系数
+
+1. 默认值:0.5
+2. 阈值范围:0 ~ 1
+3. 步长:0.01
+   否
+
+imgCount
+
+int
+单次生图张数
+
+1. 默认值:1
+2. 阈值范围:1 ~ 4
+   否
+
+seed
+int
+随机种子,作为确定扩散初始状态的基础,默认-1(随机)。若随机种子为相同正整数且其他参数均一致,则生成图片极大概率效果一致
+
+- 范围:-1 ~ 9999999999
+- -1表示随机
+  否
+
+  9.3.3 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+
+  9.4 libdream 4.0
+  9.4.1 接口定义
+
+- 请求地址:
+  POST /api/generate/seedreamV4
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  是
+  0b6bad2fd350433ebb5abc7eb91f2ec9
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+9.4.2 参数定义
+变量名
+格式
+备注
+数值范围
+必填
+示例
+prompt
+
+string
+
+- 用于生成图像的提示词,支持中英文。
+  建议不超过300个汉字或600个英文单词。字数过多信息容易分散,模型可能因此忽略细节,只关注重点,造成视图片缺失部分元素。
+- 不超过2000字符
+  是
+
+- 文生图
+  {
+  "templateUuid": "0b6bad2fd350433ebb5abc7eb91f2ec9",
+  "generateParams": {
+  "prompt": "画一个LibLib公司的品牌页海报,这是一家AI生图的公司,因此海报要具有高品质艺术感",
+  "width": 2048,
+  "height": 2048,
+  "imgCount": 1,
+  "sequentialImageGeneration": "disabled"
+  }
+  }
+
+- 图生图
+  {
+  "templateUuid": "0b6bad2fd350433ebb5abc7eb91f2ec9",
+  "generateParams": {
+  "prompt": "把这张图片处理成吉卜力风格",
+  "width": 2048,
+  "height": 2048,
+  "imgCount": 1,
+  "sequentialImageGeneration": "disabled",
+  "referenceImages": [
+  "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/3c65a38d7df2589c4bf834740385192128cf035c7c779ae2bbbc354bf0efcfcb.png"
+  ]
+  }
+  }
+
+- 组图模式
+  {
+  "templateUuid": "0b6bad2fd350433ebb5abc7eb91f2ec9",
+  "generateParams": {
+  "prompt": "做一套电影分镜稿",
+  "promptMagic": 1,
+  "width": 2048,
+  "height": 2048,
+  "imgCount": 5,
+  "sequentialImageGeneration": "auto",
+  "referenceImages": [
+  "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/d1ec8fa957683b8d641d9275d26c46b259d55d0d9f925b94460038c67a24022b.png"
+  ]
+  }
+  }
+  sequentialImageGeneration
+  string
+- disabled: 默认,关闭组图功能;
+- auto: 自动判断模式,模型会根据用户提供的提示词自主判断是否返回组图以及组图包含的图片数量;
+- disabled:默认
+- auto
+  否
+
+width
+int
+生成图像的宽
+
+- 默认值:2048
+- 阈值范围:1024 ~ 4096
+
+注意:宽高乘积不可小于1024*1024, 不可超过4096*4096
+
+否
+
+height
+int
+生成图像的高
+
+否
+
+imgCount
+int
+单次生图张数
+注意:实际可生成的图片数量,除受到 imgCount影响外,还受到输入的参考图数量影响。输入的参考图数量+最终生成的图片数量≤15张。
+
+- 默认值:1
+- 阈值范围: 1 ~ 15
+
+否
+
+referenceImages
+
+Array
+参考图
+
+- 图片数量:1~10,可公网访问的图片地址
+- 图片格式:PNG, JPG, JPEG, WEBP
+- 图片大小:每张图都不超过10MB
+- 宽高比(宽/高)范围:[1/3, 3]
+  否
+
+  9.4.3 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  9.5 Libdream4.5
+  9.5.1 接口定义
+
+说明
+接口定义
+
+- 接口:POST /api/generate/seedreamV4
+
+templateUuid
+
+- 0b6bad2fd350433ebb5abc7eb91f2ec9
+  9.5.2 参数定义
+  变量名
+  格式
+  备注
+  数值范围
+  必填
+  示例
+  model
+  string
+- 模型
+- "doubao-seedream-4-0-250828"(默认)
+- "doubao-seedream-4-5-251128"
+  否
+
+prompt
+
+string
+用于生成图像的提示词 ,中英文均可输入
+
+- 画面描述技巧:运用连贯的自然语言描述画面内容(主体+行为+环境等),用短词语描述画面美学(风格、色彩、光影、构图等)
+- 专业词汇英文表述:推荐使用词源语言或英文描述专业词汇,以获更精准效果
+- 图像用途说明:推荐写出图像用途和类型,例如:用途PPT封面背景图、背景素材图/ 类型广告海报设计、纪实摄影
+- 文字排版描述:将文字内容置于双引号“”内,并通过指令描述文字的大小、字体、颜色、风格和位置,实现排版效果的精确调整
+
+- 不超过2000字符
+
+是
+
+- 用户传参示例(组图):
+  {
+  "templateUuid":"0b6bad2fd350433ebb5abc7eb91f2ec9",
+  "generateParams":{
+  "prompt":"做一套电影分镜稿",
+  "width":2048,
+  "height":2048,
+  "imgCount":5,
+  "sequentialImageGeneration":"auto",
+  "referenceImages": ["https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/d1ec8fa957683b8d641d9275d26c46b259d55d0d9f925b94460038c67a24022b.png"]
+  }
+  }
+
+- 用户传参示例(关闭组图):
+  {
+  "templateUuid":"0b6bad2fd350433ebb5abc7eb91f2ec9",
+  "generateParams":{
+  "prompt":"guitar",
+  "width":2048,
+  "height":2048,
+  "sequentialImageGeneration":"disabled",
+  "imgCount": 1,  
+   "referenceImages": ["https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/d1ec8fa957683b8d641d9275d26c46b259d55d0d9f925b94460038c67a24022b.png"]
+  }
+  }
+
+promptMagic
+int
+提示词扩写
+
+- 0:关闭,默认
+- 1:开启
+  否
+
+sequentialImageGeneration
+string
+
+- 默认,关闭组图功能;
+- auto: 自动判断模式,模型会根据用户提供的提示词自主判断是否返回组图以及组图包含的图片数量;
+- disabled:默认
+- auto
+
+width
+int
+生成图像的宽
+
+- 对于 seedream 4.0
+  - 默认值:2048
+  - 阈值范围:512 ~ 4096
+  - 宽高乘积不可超过2048\*2048
+- 对于 seedream 4.5
+  - 默认值:2048
+  - 范围:需满足总像素 [3,686,400, 16,777,216](2560×1440 到 4096×4096)
+  - 宽高比范围:[1/16, 16]
+  - 注意:最小总像素为 3,686,400(2560×1440),不再支持 512×512
+
+否
+
+height
+
+int
+生成图像的高
+
+- 对于 seedream 4.0
+  - 默认值:2048
+  - 阈值范围:512 ~ 4096
+  - 注意:宽高乘积不可超过2048\*2048
+- 对于 seedream 4.5
+  - 默认值:2048
+  - 范围:需满足总像素 [3,686,400, 16,777,216]
+  - 宽高比范围:[1/16, 16]
+  - 注意:需与 width 一起校验总像素和宽高比
+
+否
+
+imgCount
+int
+单次生图张数
+注意:实际可生成的图片数量,除受到 imgCount影响外,还受到输入的参考图数量影响。输入的参考图数量+最终生成的图片数量≤15张。
+
+- 对于 seedream 4.0,对于 seedream 4.5
+  - 默认值:1
+  - 阈值范围: 1 ~ 15
+  - 注意:宽高乘积不可超过2048\*2048
+
+否
+
+referenceImages
+
+Array
+参考图
+
+- 对于 seedream 4.0
+  - 图片数量:1~10,可公网访问的图片地址
+  - 图片格式:PNG, JPG, JPEG, WEBP
+  - 图片大小:每张图都不超过10MB
+  - 宽高比(宽/高)范围:[1/3, 3]
+- seedream 4.5:
+  - 数量:1~14(变更:从 10 增加到 14)
+  - 格式:PNG, JPG, JPEG, WEBP, BMP, TIFF, GIF(新增格式)
+  - 大小:每张 ≤ 10MB
+  - 宽高比(宽/高):[1/16, 16](变更:从 [1/3, 3] 扩展到 [1/16, 16])
+  - 总像素:≤ 6000×6000 px
+    否
+
+  9.5.3 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+
+  9.6 查询任务结果
+  9.6.1 接口定义
+
+说明
+原型
+接口定义
+
+- 接口:POST /api/generate/status
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  备注
+  generateUuid
+  string
+  是
+  生图任务uuid,发起生图任务时返回该字段
+  9.6.2 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  generateStatus
+  int
+  生图状态见下方4.3.1节
+  percentCompleted
+  float
+  生图进度(智能算法IMG1不支持)
+  generateMsg
+  string
+  生图信息,提供附加信息,如生图失败信息
+  pointsCost
+  int
+  本次生图任务消耗积分数
+  accountBalance
+  int
+  账户剩余积分数
+  images
+  []object
+  图片列表,只提供审核通过的图片
+  images.0.imageUrl
+  string
+  图片地址,可直接访问,地址有时效性:7天
+  images.0.seed
+  int
+  随机种子值(智能算法IMG1不支持)
+  images.0.auditStatus
+  int
+  审核状态说明
+  9.7 示例demo
+  暂时无法在飞书文档外展示此内容
+
+10. 可灵
+    10.1 可灵 - 文生视频
+    10.1.1 接口定义
+
+- 请求地址:
+  POST /api/generate/video/kling/text2video
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  是
+  61cd8b60d340404394f2a545eeaf197a
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+10.1.2 参数定义
+变量名
+格式
+备注
+数值范围
+必填
+示例
+model
+enums
+支持的可灵模型
+
+- kling-v1-6
+- kling-v2-master
+- kling-v2-1-master:默认
+- kling-v2-5-turbo
+- kling-v2-6
+  否
+  {
+  "templateUuid":"61cd8b60d340404394f2a545eeaf197a",
+  "generateParams":{
+  "model": "kling-v2-1-master",
+  "prompt": "一个摇滚乐队的演出现场,主唱拿着麦克风在台上唱歌,吉他手在卖力弹吉他,贝斯手弹贝斯,鼓手在摇头晃脑的在敲鼓,键盘手在弹钢琴。",
+  "promptMagic":1,
+  "aspectRatio":"16:9",
+  "duration":"5",
+  "sound":"on",
+  "mode":"std"
+  }
+  }
+
+prompt
+
+string
+用于生成图像的提示词 ,中英文均可输入
+
+- 不超过2000字符
+- kling-v2-6
+  - 不超过2500字符
+
+是
+
+promptMagic
+int
+提示词扩写
+
+- 0:关闭,默认
+- 1:开启
+  否
+
+aspectRatio
+enums
+视频的画面宽高比
+
+- 16:9:默认
+- 9:16
+- 1:1
+
+否
+
+duration
+string
+视频时长,单位s
+
+- 5:默认
+- 10
+  否
+
+mode(新增)
+enums
+
+生成视频的模式
+
+- 其中std:标准模式(标准),基础模式,性价比高
+- 其中pro:专家模式(高品质),高表现模式,生成视频质量更佳
+  注意:
+  含有尾帧图片时,mode必须为"pro"
+  model 为kling-v2-5-turbo时,mode必须为"pro"
+- 枚举值:
+  - std:默认
+  - pro
+
+否
+
+sound
+
+string
+生成视频时是否同时生成声音
+
+- 枚举值:on,off
+  仅V2.6及后续版本模型支持当前参数
+- 枚举值:on,off
+  否
+
+  10.1.3 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+
+  10.2 可灵 - 图生视频
+  10.2.1 接口定义
+
+- 请求地址:
+  POST /api/generate/video/kling/img2video
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  是
+  180f33c6748041b48593030156d2a71d
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+10.2.2 参数定义
+变量名
+格式
+备注
+数值范围
+必填
+示例
+model
+enums
+支持的可灵模型
+
+- kling-v1-6
+- kling-v2-master
+- kling-v2-1-master
+- kling-v2-1:默认
+- kling-v2-5-turbo(新增)
+  否
+- 首帧参考
+  {
+  "templateUuid":"180f33c6748041b48593030156d2a71d",
+  "generateParams":{
+  "model": "kling-v2-1",
+  "prompt":"电影场景,破旧的机甲拳头高高地扬起,然后重重地砸向地面,碎石快速飞溅,镜头快速摇晃",
+  "promptMagic":1,
+  "mode":"std",
+  "startFrame":"https://liblibai-online.liblib.cloud/img/9b0e9abdefc9f3ab198b6677feb42c89/ca89839d4ba8c5eba0521ea003106c99e6df9286c53bfae12c2e7852c634fdf4.png",
+  "duration":"5"
+  }
+  }
+- 首尾帧参考
+  {
+  "templateUuid":"180f33c6748041b48593030156d2a71d",
+  "generateParams":{
+  "model": "kling-v1-6",
+  "prompt":"镜头前拿着宣传海报的手放下",
+  "promptMagic":1,
+  "mode":"pro",
+  "startFrame":"https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/097297f639b8a2850be8187c2a8d9465dc1afabfb813b76f6c188effd42a34c4.png",
+  "endFrame":"https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/da0e158ebeb23a52d832b16f45c0ac43d7c60f07e36bdc0a438602c4a251cfab.png"
+  "duration":"5"
+  }
+  }
+- 用户传参示例 - kling-v2-6
+  {
+  "templateUuid":"180f33c6748041b48593030156d2a71d",
+  "generateParams":{
+  "prompt":"镜头前拿着传单的手放下",
+  "promptMagic":1,
+  "model": "kling-v2-6",
+  "duration":"5",
+  "images": ["${https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/da0e158ebeb23a52d832b16f45c0ac43d7c60f07e36bdc0a438602c4a251cfab.png!""}"]
+  "mode":"std",
+  "sound": "on"
+  }
+  }
+  prompt
+
+string
+用于生成图像的提示词 ,中英文均可输入
+
+- 不超过2000字符
+
+是
+
+promptMagic
+int
+提示词扩写
+
+- 0:关闭,默认
+- 1:开启
+  否
+
+image
+string
+
+- kling-v2-6图生视频参考图
+- 对kling-v2-6,必填
+  否
+
+startFrame
+string
+
+- 图片格式支持.jpg / .jpeg / .png
+- 图片文件大小不能超过10MB
+- 图片宽高尺寸不小于300px
+- 图片宽高比介于1:2.5 ~ 2.5:1之间
+  可公网访问的Url
+  start_frame参数与end_frame参数至少二选一,二者不能同时为空
+  对kling-v2-6,不用传
+
+endFrame
+string
+
+- 图片格式支持.jpg / .jpeg / .png
+- 图片文件大小不能超过10MB
+- 图片宽高尺寸不小于300px
+- 图片宽高比介于1:2.5 ~ 2.5:1之间
+  注意:仅model="kling-v1-6"支持含有尾帧图片的请求
+  可公网访问的Url
+
+mode
+enums
+生成视频的模式
+
+- 其中std:标准模式(标准),基础模式,性价比高
+- 其中pro:专家模式(高品质),高表现模式,生成视频质量更佳
+  注意:含有尾帧图片时,mode必须为"pro"
+  model 为kling-v2-5-turbo时,mode必须为"pro"
+- 枚举值:
+  - std:默认
+  - pro
+
+否
+
+duration
+string
+视频时长,单位s
+
+- 5:默认
+- 10
+  否
+
+sound
+string
+生成视频时是否同时生成声音
+
+- 枚举值:on,off
+  仅V2.6及后续版本模型支持当前参数
+- 枚举值:on,off
+  否
+
+  10.2.3 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+
+  10.3 可灵 - 多图参考
+  10.3.1 接口定义
+
+- 请求地址:
+  POST /api/generate/video/kling/multiImg2video
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  是
+  ca01e798b4424587b0dfdb98b089da05
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+10.3.2 参数定义
+变量名
+格式
+备注
+数值范围
+必填
+示例
+model
+enums
+支持范围内的可灵模型
+
+- kling-v1-6:默认
+  否
+
+prompt
+
+string
+用于生成图像的提示词 ,中英文均可输入
+
+- 不超过2000字符
+
+是
+
+{
+"templateUuid":"ca01e798b4424587b0dfdb98b089da05",
+"generateParams":{
+"prompt":"一个卡通风格的老爷爷在咖啡馆里,端起咖啡杯喝咖啡",
+"promptMagic":1,
+"mode":"std",
+"referenceImages":[
+"https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/b47511a385a1c3101624643dcf0748a9c669d5f8f97c5fa07fe0fb08b19af57d.jpeg",
+"https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/5089fa6c3476e92255175a3246baa8db4c6c9e717d2aa1272cc4c74a0556c4c1.jpeg",
+"https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/444c6469f498950feef471000338f3568c89b879402b3fc972efc089ab6f569f.jpeg"
+],
+"aspectRatio":"16:9",
+"duration":"5"
+}
+}
+
+promptMagic
+int
+提示词扩写
+
+- 0:关闭,默认
+- 1:开启
+  否
+
+referenceImages
+
+Array of string
+
+- 图片格式支持.jpg / .jpeg / .png
+- 图片文件大小不能超过10MB
+- 图片宽高尺寸不小于300px
+- 图片宽高比介于1:2.5 ~ 2.5:1之间
+
+可公网访问的Url构成的list
+start_frame参数与end_frame参数至少二选一,二者不能同时为空
+
+mode
+enums
+生成视频的模式
+
+- 其中std:标准模式(标准),基础模式,性价比高
+- 其中pro:专家模式(高品质),高表现模式,生成视频质量更佳
+- 枚举值:
+  - std:默认
+  - pro
+
+否
+
+aspectRatio
+enums
+视频的画面宽高比
+
+- 16:9:默认
+- 9:16
+- 1:1
+  否
+
+duration
+string
+视频时长,单位s
+
+- 5:默认
+- 10
+  否
+
+  10.3.3 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+
+  10.4 查询任务结果
+  10.4.1 接口定义
+
+说明
+原型
+接口定义
+
+- 接口:POST /api/generate/status
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  备注
+  generateUuid
+  string
+  是
+  生图任务uuid,发起生图任务时返回该字段
+  10.4.2 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  generateStatus
+  int
+  生图状态见下方4.3.1节
+  percentCompleted
+  float
+  生图进度(智能算法IMG1不支持)
+  generateMsg
+  string
+  生图信息,提供附加信息,如生图失败信息
+  pointsCost
+  int
+  本次生图任务消耗积分数
+  accountBalance
+  int
+  账户剩余积分数
+  videos
+  []object
+  图片列表,只提供审核通过的图片
+  videos.0.videoUrl
+  string
+  图片地址,可直接访问,地址有时效性:7天
+  videos.0.coverPath
+  int
+  封面图
+  videos.0.auditStatus
+  int
+  审核状态说明
+  10.5 示例demo
+  暂时无法在飞书文档外展示此内容
+
+11. Kling Omni-Video V1
+    11.1 接口定义
+
+说明
+接口定义
+
+- 接口:POST /api/generate/video/kling/omni-video
+  templateUuid
+- 新增:9f3a7c4e8b2d4f1a9c6e5d7b0a2e4c81
+  11.2 参数定义
+  变量名
+  格式
+  备注
+  数值范围
+  必填
+  示例
+  model
+
+enums
+支持范围内的可灵模型
+
+- kling-video-o1
+
+否
+用户传参示例:
+
+- 首尾帧:
+  {
+  "templateUuid": "9f3a7c4e8b2d4f1a9c6e5d7b0a2e4c81",
+  "generateParams": {
+  "model": "kling-video-o1",
+  "prompt": "镜头前拿着宣传海报的手放下。",
+  "aspectRatio": "16:9",
+  "duration": "5",
+  "images": [
+  {
+  "image_url": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/097297f639b8a2850be8187c2a8d9465dc1afabfb813b76f6c188effd42a34c4.png",
+  "type": "start_frame"
+  },
+  {
+  "image_url": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/da0e158ebeb23a52d832b16f45c0ac43d7c60f07e36bdc0a438602c4a251cfab.png",
+  "type": "end_frame"
+  }
+  ]
+  }
+  }
+
+- 多模态参考:prompt
+  {
+  "templateUuid":"9f3a7c4e8b2d4f1a9c6e5d7b0a2e4c81",
+  "generateParams":{
+  "model": "kling-video-o1",
+  "prompt": "一个摇滚乐队的演出现场,主唱拿着麦克风在台上唱歌,吉他手在卖力弹吉他,贝斯手弹贝斯,鼓手在摇头晃脑的在敲鼓,键盘手在弹钢琴。",
+  "aspectRatio":"16:9",
+  "duration":"5"
+  }
+  }
+- 多模态参考:prompt + images
+  {
+  "templateUuid": "9f3a7c4e8b2d4f1a9c6e5d7b0a2e4c81",
+  "generateParams": {
+  "model": "kling-video-o1",
+  "prompt": "一个<<<image_1>>>卡通风格的老爷爷在<<<image_2>>>咖啡馆里,端起<<<image_3>>>咖啡杯喝咖啡",
+  "aspectRatio": "16:9",
+  "duration": "5",
+  "images": [
+  {
+  "image_url": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/b47511a385a1c3101624643dcf0748a9c669d5f8f97c5fa07fe0fb08b19af57d.jpeg"
+  },
+  {
+  "image_url": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/444c6469f498950feef471000338f3568c89b879402b3fc972efc089ab6f569f.jpeg"
+  },
+  {
+  "image_url": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/5089fa6c3476e92255175a3246baa8db4c6c9e717d2aa1272cc4c74a0556c4c1.jpeg"
+  }
+  ]
+  }
+  }
+- 多模态参考:prompt + images + video
+  {
+  "templateUuid": "9f3a7c4e8b2d4f1a9c6e5d7b0a2e4c81",
+  "generateParams": {
+  "model": "kling-video-o1",
+  "prompt": "一个<<<image_1>>>卡通风格的老爷爷在<<<image_2>>>咖啡馆里,端起<<<image_3>>>咖啡杯喝咖啡,参考<<<video_1>>>的视频",
+  "aspectRatio": "16:9",
+  "duration": "5",
+  "images": [
+  {
+  "image_url": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/b47511a385a1c3101624643dcf0748a9c669d5f8f97c5fa07fe0fb08b19af57d.jpeg"
+  },
+  {
+  "image_url": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/444c6469f498950feef471000338f3568c89b879402b3fc972efc089ab6f569f.jpeg"
+  },
+  {
+  "image_url": "https://liblibai-online.liblib.cloud/img/081e9f07d9bd4c2ba090efde163518f9/5089fa6c3476e92255175a3246baa8db4c6c9e717d2aa1272cc4c74a0556c4c1.jpeg"
+  }
+  ],
+  "videos": [
+  {
+  "video_url": "https://liblibai-tmp-image.liblib.cloud/sd-images/90de75eb2ac24155ee7806dfbacdb5d6078ea7abed9fbe82e439c32fca592938.mp4",
+  "refer_type": "feature",
+  "keep_original_sound": "yes"
+  }
+  ]
+  }
+  }
+- 视频编辑:
+  {
+  "templateUuid": "9f3a7c4e8b2d4f1a9c6e5d7b0a2e4c81",
+  "generateParams": {
+  "model": "kling-video-o1",
+  "prompt": "把图片中的咖啡杯换成粉色的",
+  "aspectRatio": "16:9",
+  "duration": "5",
+  "videos": [
+  {
+  "video_url": "https://liblibai-tmp-image.liblib.cloud/sd-images/90de75eb2ac24155ee7806dfbacdb5d6078ea7abed9fbe82e439c32fca592938.mp4",
+  "refer_type": "base",
+  "keep_original_sound": "yes"
+  }
+  ]
+  }
+  }
+
+prompt
+
+string
+用于生成图像的提示词 ,中英文均可输入
+
+- 不超过2500字符
+- 可以通过<<<>>>的格式来指定某个图片或视频,如:<<<image_1>>>、<<<video_1>>>
+
+是
+
+images
+
+array
+
+- 包括场景、风格等参考图片,也可作为首帧或尾帧生成视频;当作为首帧或尾帧生成视频时:
+  - 通过type参数来定义图片是否为首尾帧:start_frame为首帧,end_frame为尾帧
+  - 暂时不支持仅尾帧,即有尾帧图时必须有首帧图
+  - 首帧或首尾帧生视频时,不能使用参考视频
+- 用key:value承载,如下:
+
+"images":[
+{
+"image_url":"image_url",
+"type":"start_frame"
+},
+{
+"image_url":"image_url",
+"type":"end_frame"
+}
+]
+
+- 图片格式支持.jpg / .jpeg / .png
+- 图片文件大小不能超过10MB,图片宽高尺寸不小于300px
+- 有参考视频时,参考图片数量不得超过4;无参考视频时,参考图片数量不得超过7
+- 数组中超过2张图片时,不支持设置首尾帧
+- image_url参数值不得为空
+
+否
+
+videos
+
+array
+参考视频,通过URL方式获取
+
+- 可作为特征参考视频,也可作为待编辑视频,默认为待编辑视频;可选择性保留视频原声
+  - 通过refer_type参数区分参考视频类型:
+    - feature为特征参考视频,默认
+    - base为待编辑视频
+  - 通过keep_original_sound参数选择是否保留视频原声,
+    - yes为保留,默认
+    - no为不保留
+  - 有参考视频时,不能定义视频首尾帧
+- 用key:value承载,如下:
+
+"videos":[
+{
+"video_url":"video_url",
+"refer_type":"base",
+"keep_original_sound":"yes"
+}
+]
+
+- video_url参数值不得为空
+- 至多仅支持上传1段视频,视频大小不超过200MB
+- 视频格式仅支持MP4/MOV
+- 仅支持时长≥3秒且≤10秒的视频
+- 视频宽高尺寸需介于720px(含)和2160px(含)之间
+
+否
+
+aspectRatio
+
+enums
+视频的画面宽高比
+
+- 16:9:默认
+- 9:16
+- 1:1
+  可灵O1,指令变换(视频编辑),图生视频(包括首尾帧)时,aspect ratio不生效,视频宽高比会根据参考图进行调整
+
+否
+
+duration
+
+enums
+视频时长,单位s
+
+- 枚举值:5(默认),10
+- 使用视频编辑功能("refer_type":"base")时,输出结果与传入视频时长相同,此时当前参数无效
+  否
+
+mode
+enums
+生成视频的模式
+枚举值:
+
+- pro(默认)
+- std
+  pro:专家模式(高品质),高表现模式,生成视频质量更佳
+  std:标准模式(标准),基础模式,性价比高
+  否
+
+  11.3 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  11.4 查询任务结果
+  同10.4
+
+12. Qwen Image - 文生图
+    12.1 文生图
+    12.1.1 接口定义
+
+- 请求地址:
+  POST /api/generate/webui/text2img
+- headers:
+  header
+  value
+  备注
+  Content-Type
+  application/json
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  说明
+  备注
+  templateUuid
+  string
+  是
+  bf085132c7134622895b783b520b39ff
+
+generateParams
+object
+是
+生图参数,json结构
+参数中的图片字段需提供可访问的完整图片地址
+12.1.2 参数说明
+变量名
+格式
+备注
+数值范围
+必填
+示例
+checkPointId
+
+String
+模型uuid
+
+- Qwen-Image: 75e0be0c93b34dd8baeec9c968013e0c
+  是
+  {
+  "templateUuid": "bf085132c7134622895b783b520b39ff",
+  "generateParams": {
+  // 基础参数
+  "checkPointId": "75e0be0c93b34dd8baeec9c968013e0c",
+  "prompt": "Asian portrait,A young woman wearing a green baseball cap,covering one eye with her hand", // 选填
+  "negativePrompt": "ng_deepnegative_v1_75t,(badhandv4:1.2),EasyNegative,(worst quality:2),nsfw", //选填
+  "clipSkip": 2,  
+   "sampler": 1,
+  "steps": 30,
+  "cfgScale": 4.0,
+  "width": 768,
+  "height": 1024,
+  "imgCount": 1,  
+   "randnSource": 0,
+  "seed": -1,
+          // controlNet,最多4组
+          "controlNet": []
+      }
+  }
+
+additionalNetwork
+
+list[object]
+
+- LoRA组合及权重设置
+- LoRA的基础算法类型需要与checkpoint一致
+- 参考additionalNetwork的参数配置
+
+否
+
+prompt
+
+string
+正向提示词,文本
+
+- 不超过2000字符
+- 纯英文文本
+  是
+
+negativePrompt
+string
+负向提示词,文本
+
+- 不超过2000字符
+- 纯英文文本不超过2000字符
+  否
+
+clipSkip
+int
+Clip跳过层
+1 ~ 12。默认值2
+否
+
+sampler
+int
+采样器枚举值
+从采样方法列表中选择
+否
+
+steps
+int
+采样步数
+
+- 阈值范围:1 ~ 60
+- 默认值:30
+  否
+
+cfgScale
+double
+cfg_scale
+
+- 阈值范围:1.0 ~ 15.0
+- 默认值:4.0
+  否
+
+width
+int
+初始宽度
+
+- 范围:128 ~ 2048
+- 默认值:1328
+- 推荐尺寸:
+  - 1:1 - 1328 x 1328
+  - 3:4 - 1140\*1472
+  - 4:3 - 1472\*1140
+  - 9:16 - 928\*1664
+  - 16:9 - 1664\*928
+    否
+
+height
+int
+初始高度
+
+否
+
+imgCount
+int
+单次生图张数
+1 ~ 4
+否
+
+randnSource
+int
+随机种子生成来源
+0: CPU,1: GPU。默认值0
+否
+
+seed
+Long
+随机种子
+
+- 范围:-1 ~ 9999999999
+- -1表示随机
+  否
+
+controlNet
+
+list[Object]
+模型加载的ControlNet组合及各自参数
+参考controlnet参数配置
+
+- InstantX-Qwen-Image-Controlnet-Union:5b5f21d2b80445598db19e924bd3a409
+- InstantX-Qwen-Image-ControlNet-Inpainting:2228ab9234a34aa5abf77caa907c0de1
+  否
+
+  12.1.3 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  12.2 查询任务结果
+  12.2.1 接口定义
+
+说明
+原型
+接口定义
+
+- 接口:POST /api/generate/status
+
+- 请求body:
+  参数
+  类型
+  是否必需
+  备注
+  generateUuid
+  string
+  是
+  生图任务uuid,发起生图任务时返回该字段
+  12.2.2 返回值
+  参数
+  类型
+  备注
+  generateUuid
+  string
+  生图任务uuid,使用该uuid查询生图进度
+  generateStatus
+  int
+  生图状态见下方3.3.1节
+  percentCompleted
+  float
+  生图进度(智能算法IMG1不支持)
+  generateMsg
+  string
+  生图信息,提供附加信息,如生图失败信息
+  pointsCost
+  int
+  本次生图任务消耗积分数
+  accountBalance
+  int
+  账户剩余积分数
+  images
+  []object
+  图片列表,只提供审核通过的图片
+  images.0.imageUrl
+  string
+  图片地址,可直接访问,地址有时效性:7天
+  images.0.seed
+  int
+  随机种子值(智能算法IMG1不支持)
+  images.0.auditStatus
+  int
+  审核状态说明
+
+13. 生图示例完整demo
+    我们提供了以下Python脚本用于参考,演示了从发起生图任务到查询生图结果的调用流程,提供了以下接口的使用:
+1.  星流Star-3 Alpha文生图
+1.  星流Star-3 Alpha图生图
+1.  LiblibAI自定义模型文生图
+1.  LiblibAI自定义模型图生图
+1.  查询生图结果
+
+- 文生图示例:(含Star-3 Alpha和自定义模型)
+  暂时无法在飞书文档外展示此内容
+- 图生图示例:(含简易模式和进阶模式)
+  暂时无法在飞书文档外展示此内容
+
+14. 错误码汇总
+    错误码
+    错误信息
+    备注
+    401
+    签名验证失败
+
+403
+访问拒绝
+访问拒绝场景包括:
+
+1. 没有开通API权益
+2. 账户API积分不足
+   (API积分和LiblibAI会员算力不通用,分别是两套会员和积分体系)
+   429
+   请求太多,请稍后重试
+   QPS超限,发起生图任务接口QPS限制1秒1次
+   100000
+   参数无效
+   通用参数校验失败
+   100010
+   AccessKey过期
+   API商业化权益已过期
+   100020
+   用户不存在
+   /
+   100021
+   用户积分不足
+   /
+   100030
+   图片地址无法访问,或大小超出限制
+   目前图片大小不能大于10M
+   100031
+   图片包含违规内容
+   图片地址无效、无法下载或图片过大
+   图片需要先过数美审核,请给数美服务端加白,https://console.ishumei.com/,https://www.fengkongcloud.com(referer)
+   100032
+   图片下载失败
+
+网络不稳定导致,可考虑使用我们的文件上传接口
+LiblibAI-API文件上传
+100050
+生图参数未通过参数完整度校验,请检查参数配置
+检查模板和Checkpoint和LoRA和Controlnet的匹配关系,需要同一底模
+
+100051
+生图任务不存在
+/
+100052
+提示词中包含敏感内容,请修改
+包括prompt、negativePrompt等提示词参数中包含敏感内容
+100053
+当前使用的模型不在提供的模型列表内,请检查
+请从平台提供的Checkpoint、LoRA、VAE、Controlnet列表中选择
+100054
+当前进行中任务数量已达到并发任务上限
+/
+100055
+生图结果中包含敏感内容,请检查参数配置
+/
+100120
+参数模板不存在
+传的模板uuid有问题,找不到对应模板
+200000
+内部服务错误
+具体错误包括:
+
+1. 图片上传失败
+2. LiblibAI官网系统维护
+   200001
+   模型不存在
+
+210000
+调用外部服务失败,请重试

+ 415 - 0
docs/liblibai_uuid_matching_rules.md

@@ -0,0 +1,415 @@
+# LibLib AI API UUID 匹配规则完整指南
+
+> 基于 output.md 深度分析整理
+> 最后更新:2026-03-23
+
+## 目录
+1. [核心匹配原则](#核心匹配原则)
+2. [模板 UUID 列表](#模板-uuid-列表)
+3. [Checkpoint ID 列表](#checkpoint-id-列表)
+4. [ControlNet 模型完整列表](#controlnet-模型完整列表)
+5. [匹配规则详解](#匹配规则详解)
+6. [常见错误和解决方案](#常见错误和解决方案)
+7. [推荐组合方案](#推荐组合方案)
+
+---
+
+## 核心匹配原则
+
+### BaseType 三角匹配规则
+
+```
+Template baseType ← → Checkpoint baseType ← → ControlNet baseType
+```
+
+**关键要点**:
+- Template、Checkpoint、ControlNet 必须使用**相同的 baseType**
+- 不匹配会导致错误:`Cn模型baseType不匹配`
+- 支持的 baseType:`F.1`、`XL`、`1.5`
+
+---
+
+## 模板 UUID 列表
+
+### 1. 自定义模型模板(推荐)
+
+| 模板名称 | Template UUID | 基础算法类型 | 适用场景 | 支持功能 |
+|---------|---------------|-----------|--------|---------|
+| 1.5和XL文生图 | `e10adc3949ba59abbe56e057f20f883e` | 1.5/XL | 文生图 | ControlNet, LoRA, HiresFix |
+| 1.5和XL图生图 | `9c7d531dc75f476aa833b3d452b8f7ad` | 1.5/XL | 图生图 | ControlNet, LoRA, 蒙版重绘 |
+
+### 2. F.1 系列模板
+
+| 模板名称 | Template UUID | 基础算法类型 | 适用场景 |
+|---------|---------------|-----------|--------|
+| F.1文生图 | `6f7c4652458d4802969f8d089cf5b91f` | F.1 | 文生图 |
+| F.1图生图 | `63b72710c9574457ba303d9d9b8df8bd` | F.1 | 图生图 |
+
+### 3. 专用模板
+
+| 模板名称 | Template UUID | 用途 |
+|---------|---------------|------|
+| Controlnet局部重绘 | `b689de89e8c9407a874acd415b3aa126` | 局部重绘 |
+| 图生图局部重绘 | `74509e1b072a4c45a7f1843a963c8462` | 局部重绘 |
+| InstantID人像换脸 | `7d888009f81d4252a7c458c874cd017f` | 人像换脸 |
+
+---
+
+## Checkpoint ID 列表
+
+| Checkpoint 名称 | Checkpoint UUID | 基础算法类型 | 说明 |
+|----------------|-----------------|-----------|------|
+| SD 1.5 官方底模 | `6320087784014d5f850259e8f49890a1` | 1.5 | 推荐用于 1.5 系列 |
+| 其他底模 | `0ea388c7eb854be3ba3c6f65aac6bfd3` | 待确认 | 需验证 baseType |
+
+---
+
+## ControlNet 模型完整列表
+
+### 1. 线稿类
+
+#### 1.1 Canny(硬边缘)
+
+| 模型名称 | 基础算法 | 模型UUID | 预处理器值 |
+|---------|--------|---------|----------|
+| control_v11p_sd15_canny | 1.5 | `7d917ec7e55c5805db737d3b493c91ce` | 1 |
+| t2iadapter_canny_sd14v1 | 1.5 | `a2c41c4e97944f3aa71f913bdc45b1ca` | 1 |
+| t2iadapter_canny_sd15v2 | 1.5 | `c04144bcf017232483181cd8607097c2` | 1 |
+| diffusers_xl_canny_full | XL | `56de5edadb6f2891aff05ff078dc0470` | 1 |
+| diffusers_xl_canny_mid | XL | `efb97e9d8c237573298c3a5a7869b89c` | 1 |
+| diffusers_xl_canny_small | XL | `dccde738064e9748f93b48ec5868968e` | 1 |
+| xinsir_controlnet-canny-sdxl_V2 | XL | `b6806516962f4e1599a93ac4483c3d23` | 1 |
+| XLabs-flux-canny-controlnet_v3 | F.1 | `017997cd6ba44c4dbe8f60e0a26cd0df` | 1 |
+
+**Canny 参数配置**:
+```json
+{
+  "preprocessor": 1,
+  "annotationParameters": {
+    "canny": {
+      "preprocessorResolution": 512,
+      "lowThreshold": 100,
+      "highThreshold": 200
+    }
+  }
+}
+```
+
+#### 1.2 SoftEdge(软边缘)
+
+| 模型名称 | 基础算法 | 模型UUID | 预处理器值 |
+|---------|--------|---------|----------|
+| control_v11p_sd15_softedge | 1.5 | `0929722d9047ec6498a50ff5d1081629` | 5 |
+| sargezt_xl_softedge | XL | `dda1a0c480bfab9833d9d9a1e4a71fff` | 5 |
+| XLabs-flux-hed-controlnet_v3 | F.1 | `6c4d620df3644514903b8189735c6ae9` | 5 |
+
+#### 1.3 Lineart(线稿)
+
+| 模型名称 | 基础算法 | 模型UUID | 预处理器值 |
+|---------|--------|---------|----------|
+| control_v11p_sd15_lineart | 1.5 | `b06dfbd1a61c35e933d9f8caa8a0e031` | 29 |
+| control_v11p_sd15s2_lineart_anime | 1.5 | `c263e039c57b8a958ee0a936039af654` | 31 |
+| t2i-adapter_diffusers_xl_lineart | XL | `a0f01da42bf48b0ba02c86b6c26b5699` | 29 |
+
+### 2. 空间关系类
+
+#### 2.1 Depth(深度图)
+
+| 模型名称 | 基础算法 | 模型UUID | 预处理器值 |
+|---------|--------|---------|----------|
+| control_v11f1p_sd15_depth | 1.5 | `cf63d214734760dcdc108b1bd094921b` | 3 |
+| xinsir_controlnet_depth_sdxl_1.0 | XL | `6349e9dae8814084bd9c1585d335c24c` | 3 |
+| XLabs-flux-depth-controlnet_v3 | F.1 | `0cc4e6b8206b44cdab51e30fb8b9c328` | 3 |
+| Flux.1-dev-Controlnet-Depth | F.1 | `64dd7a6c714f4512a4500f6a01b016b7` | 3 |
+
+**Depth 参数配置**:
+```json
+{
+  "preprocessor": 3,
+  "annotationParameters": {
+    "depth_leres": {
+      "preprocessorResolution": 512,
+      "removeNear": 0,
+      "removeBg": 0
+    }
+  }
+}
+```
+
+#### 2.2 Normal(法线图)
+
+| 模型名称 | 基础算法 | 模型UUID | 预处理器值 |
+|---------|--------|---------|----------|
+| control_v11p_sd15_normalbae | 1.5 | `9a85fdca18a8b58b2fb2ff13ab339be4` | 10 |
+| Flux.1-dev-Controlnet-Surface-Normal | F.1 | `e51fdccdf3b8417aab246bde40b5f360` | 10 |
+
+### 3. 姿态类
+
+#### 3.1 OpenPose(姿态检测)
+
+| 模型名称 | 基础算法 | 模型UUID | 预处理器值 |
+|---------|--------|---------|----------|
+| control_v11p_sd15_openpose | 1.5 | `b46dd34ef9c2fe189446599d62516cbf` | 11 |
+| xinsir_controlnet-openpose-sdxl-1.0 | XL | `23ef8ab803d64288afdb7106b8967a55` | 11 |
+| F.1-ControlNet-Pose-V1 | F.1 | `7c6d889cb9c04b78858d8fece80f9f85` | 11 |
+
+### 4. 画面参考类
+
+#### 4.1 Tile/Blur(分块/超分)
+
+| 模型名称 | 基础算法 | 模型UUID | 预处理器值 |
+|---------|--------|---------|----------|
+| control_v11f1e_sd15_tile | 1.5 | `37e42c6bdb6fab4c24a662100f20f722` | 14 |
+| xinsir_controlnet_tile_sdxl_1.0 | XL | `0f47ef6d4f4b40afab8b290c98baac0e` | 14 |
+| Flux.1-dev-Controlnet-Upscaler | F.1 | `a696b5bdadc740119fd76505b33d6898` | 14 |
+
+### 5. 风格迁移类
+
+#### 5.1 IP-Adapter
+
+| 模型名称 | 基础算法 | 模型UUID | 用途 |
+|---------|--------|---------|------|
+| ip-adapter_sd15 | 1.5 | `18801062fe4289dd0a984e69de9f9e7c` | 风格迁移 |
+| ip-adapter_sd15_plus | 1.5 | `ad4bd9b4b05c4ac8faf7f81d9fdcadc8` | 风格迁移增强 |
+| ip-adapter_xl | XL | `8ea2538fdd7dcdea52b2da6b5151f875` | 风格迁移 |
+| InstantX-F.1-dev-IP-Adapter | F.1 | `c6ed70879cf011ef96d600163e37ec70` | 风格迁移 |
+
+---
+
+## 匹配规则详解
+
+### 规则 1:BaseType 必须一致
+
+```python
+# ✅ 正确示例(都是 1.5)
+{
+  "templateUuid": "e10adc3949ba59abbe56e057f20f883e",  # 1.5/XL
+  "generateParams": {
+    "checkPointId": "6320087784014d5f850259e8f49890a1",  # 1.5
+    "controlNet": [{
+      "model": "7d917ec7e55c5805db737d3b493c91ce"  # 1.5 Canny
+    }]
+  }
+}
+
+# ❌ 错误示例(baseType 不匹配)
+{
+  "templateUuid": "e10adc3949ba59abbe56e057f20f883e",  # 1.5/XL
+  "generateParams": {
+    "checkPointId": "6320087784014d5f850259e8f49890a1",  # 1.5
+    "controlNet": [{
+      "model": "017997cd6ba44c4dbe8f60e0a26cd0df"  # F.1 Canny ❌
+    }]
+  }
+}
+```
+
+### 规则 2:预处理器参数必须匹配
+
+| ControlNet 类型 | 预处理器 | 预处理器值 | 参数配置 |
+|----------------|--------|----------|--------|
+| Canny | canny | 1 | lowThreshold, highThreshold |
+| Depth | depth_leres | 3 | removeNear, removeBg |
+| OpenPose | openpose | 11 | 无额外参数 |
+| Tile | tile_resample | 14 | 无额外参数 |
+
+### 规则 3:ControlNet 参数类型
+
+**重要**:以下参数必须是**整数**,不能是布尔值:
+- `pixelPerfect`: 1 或 0(不能是 true/false)
+- `controlMode`: 0, 1, 2(不能是 false/true)
+- `resizeMode`: 0, 1, 2(不能是 false/true)
+
+```python
+# ✅ 正确
+"pixelPerfect": 1,
+"controlMode": 0,
+"resizeMode": 1
+
+# ❌ 错误
+"pixelPerfect": True,  # 会报错:参数无效
+"controlMode": False,
+"resizeMode": True
+```
+
+---
+
+## 常见错误和解决方案
+
+### 错误 1:Cn模型baseType不匹配
+
+**错误信息**:
+```json
+{
+  "code": 100050,
+  "msg": "生图参数未通过参数完整度校验,请检查参数配置: [1002011]Cn模型baseType不匹配;"
+}
+```
+
+**原因**:ControlNet 模型和 Checkpoint 底模的基础算法类型不一致
+
+**解决方案**:
+1. 检查 Checkpoint ID 的 baseType
+2. 选择相同 baseType 的 ControlNet 模型
+3. 参考上面的模型列表,确保三者匹配
+
+### 错误 2:参数无效 pixelPerfect
+
+**错误信息**:
+```json
+{
+  "code": 100000,
+  "msg": "参数无效: controlNet[0].pixelPerfect"
+}
+```
+
+**原因**:使用了布尔值而不是整数
+
+**解决方案**:
+```python
+# 改为整数
+"pixelPerfect": 1,  # 不是 True
+"controlMode": 0,   # 不是 False
+"resizeMode": 1     # 不是 True
+```
+
+### 错误 3:内部服务错误
+
+**错误信息**:
+```json
+{
+  "code": 200000,
+  "msg": "内部服务错误"
+}
+```
+
+**可能原因**:
+1. 图片上传失败(OSS 返回 403)
+2. LibLib 服务维护中
+3. 参数配置错误(但未被前置校验捕获)
+
+**解决方案**:
+1. 确认图片 URL 可访问
+2. 等待服务恢复
+3. 检查所有参数配置
+
+---
+
+## 推荐组合方案
+
+### 方案 1:精准线稿生成(1.5)
+
+```json
+{
+  "templateUuid": "e10adc3949ba59abbe56e057f20f883e",
+  "generateParams": {
+    "checkPointId": "6320087784014d5f850259e8f49890a1",
+    "prompt": "masterpiece, best quality, detailed",
+    "width": 512,
+    "height": 512,
+    "controlNet": [{
+      "unitOrder": 1,
+      "sourceImage": "图片URL",
+      "model": "7d917ec7e55c5805db737d3b493c91ce",  # Canny 1.5
+      "preprocessor": 1,
+      "annotationParameters": {
+        "canny": {
+          "preprocessorResolution": 512,
+          "lowThreshold": 100,
+          "highThreshold": 200
+        }
+      },
+      "controlWeight": 1,
+      "startingControlStep": 0,
+      "endingControlStep": 1,
+      "pixelPerfect": 1,
+      "controlMode": 0,
+      "resizeMode": 1
+    }]
+  }
+}
+```
+
+### 方案 2:深度图 + 姿态控制(XL)
+
+```json
+{
+  "templateUuid": "e10adc3949ba59abbe56e057f20f883e",
+  "generateParams": {
+    "checkPointId": "0ea388c7eb854be3ba3c6f65aac6bfd3",
+    "controlNet": [
+      {
+        "unitOrder": 1,
+        "model": "6349e9dae8814084bd9c1585d335c24c",  # Depth XL
+        "preprocessor": 3,
+        "controlWeight": 0.8
+      },
+      {
+        "unitOrder": 2,
+        "model": "23ef8ab803d64288afdb7106b8967a55",  # OpenPose XL
+        "preprocessor": 11,
+        "controlWeight": 1.0
+      }
+    ]
+  }
+}
+```
+
+### 方案 3:超分辨率增强(F.1)
+
+```json
+{
+  "templateUuid": "6f7c4652458d4802969f8d089cf5b91f",
+  "generateParams": {
+    "width": 1024,
+    "height": 1024,
+    "controlNet": [{
+      "model": "a696b5bdadc740119fd76505b33d6898",  # Tile F.1
+      "preprocessor": 14,
+      "controlWeight": 0.6
+    }]
+  }
+}
+```
+
+---
+
+## 快速查询表
+
+### BaseType 对应关系
+
+| BaseType | Template UUID | Checkpoint ID | ControlNet 前缀 |
+|---------|---------------|---------------|----------------|
+| 1.5 | e10adc3949ba59abbe56e057f20f883e | 6320087784014d5f850259e8f49890a1 | control_v11p_sd15_*, t2iadapter_* |
+| XL | e10adc3949ba59abbe56e057f20f883e | 0ea388c7eb854be3ba3c6f65aac6bfd3 | *_xl_*, xinsir_*, sai_xl_* |
+| F.1 | 6f7c4652458d4802969f8d089cf5b91f | 待定 | XLabs-flux-*, F.1-*, Flux.1-* |
+
+### 预处理器枚举值
+
+| 预处理器名称 | 枚举值 | 用途 |
+|------------|-------|------|
+| canny | 1 | 硬边缘检测 |
+| depth_leres | 3 | 深度图(LeReS) |
+| depth_zoe | 4 | 深度图(ZoeDepth) |
+| hed | 5 | 软边缘检测 |
+| mlsd | 8 | 直线检测 |
+| seg_ufade20k | 9 | 语义分割 |
+| normal_bae | 10 | 法线图 |
+| openpose | 11 | 姿态检测 |
+| densepose | 12 | 密集姿态 |
+| tile_resample | 14 | 分块/超分 |
+| lineart_realistic | 29 | 写实线稿 |
+| lineart_anime | 31 | 动漫线稿 |
+
+---
+
+## 参考资源
+
+- **原始文档**:`C:\Users\11304\gitlab\cybertogether\tool_agent\output.md`
+- **测试脚本**:`tests/test_lib.py`
+- **任务书**:`tests/tasks/liblibai_controlnet.json`
+
+---
+
+**最后更新**:2026-03-23
+**维护者**:Tool Agent Team

+ 40 - 0
pyproject.toml

@@ -0,0 +1,40 @@
+[project]
+name = "tool-agent"
+version = "0.1.0"
+description = "智能工具管理系统 - 自动封装、接入、部署、编写工具的 Agent + 工具库"
+requires-python = ">=3.11"
+dependencies = [
+    "fastapi>=0.115.0",
+    "uvicorn[standard]>=0.30.0",
+    "pydantic>=2.0.0",
+    "pydantic-settings>=2.0.0",
+    "docker>=7.0.0",
+    "httpx>=0.27.0",
+    "psutil>=6.0.0",
+    "claude-agent-sdk",
+    "python-dotenv>=1.0.0",
+]
+
+[project.optional-dependencies]
+dev = [
+    "pytest>=8.0.0",
+    "pytest-asyncio>=0.24.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/tool_agent"]
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+testpaths = ["tests"]
+
+[tool.uv.workspace]
+members = [
+    "tools/local/image_stitcher",
+    "tools/local/liblibai_controlnet",
+    "tools/local/launch_comfy_env",
+]

+ 330 - 0
scripts/feishu_to_md.py

@@ -0,0 +1,330 @@
+#!/usr/bin/env python3
+"""
+飞书文档转 Markdown 工具
+
+飞书文档是客户端渲染的,文档内容嵌在 JS 变量 window.DATA 中。
+本脚本从 HTML 中提取 block_map JSON,解析文档结构树,转换为 Markdown。
+
+使用方法:
+    python feishu_to_md.py <飞书文档URL> -o output.md
+
+依赖安装:
+    pip install httpx
+"""
+
+import argparse
+import json
+import re
+import sys
+from pathlib import Path
+
+try:
+    import httpx
+except ImportError:
+    print("缺少依赖:pip install httpx")
+    sys.exit(1)
+
+
+def fetch_html(url: str) -> str:
+    print(f"正在抓取: {url}")
+    with httpx.Client(follow_redirects=True, timeout=30.0) as client:
+        resp = client.get(url, headers={
+            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
+        })
+        resp.raise_for_status()
+        return resp.text
+
+
+def extract_block_data(html: str) -> dict:
+    """从 HTML 中提取 window.DATA 里的 block_map 和 block_sequence"""
+    # 匹配 clientVars: Object({...})
+    match = re.search(r'clientVars:\s*Object\((\{.+?\})\)\s*\}', html, re.DOTALL)
+    if not match:
+        raise ValueError("无法从页面中提取 clientVars 数据")
+
+    raw = match.group(1)
+    # 飞书的 JSON 用了 Unicode 转义,直接 parse
+    try:
+        data = json.loads(raw)
+    except json.JSONDecodeError:
+        # 有时候 JSON 被截断,尝试找完整的
+        match2 = re.search(r'"block_map"\s*:\s*\{', html)
+        if not match2:
+            raise ValueError("无法解析 block_map")
+        # 从 block_map 开始,找到对应的闭合括号
+        start = match2.start()
+        depth = 0
+        for i in range(start, len(html)):
+            if html[i] == '{':
+                depth += 1
+            elif html[i] == '}':
+                depth -= 1
+                if depth == 0:
+                    try:
+                        data = {"data": json.loads(html[start:i+1].replace('"block_map":', '{"block_map":') + '}')}
+                        break
+                    except:
+                        continue
+        else:
+            raise ValueError("无法解析 block_map JSON")
+
+    return data.get("data", data)
+
+
+def extract_text_from_block(block_data: dict) -> str:
+    """从 block 的 text 字段提取纯文本"""
+    text_info = block_data.get("text", {})
+    texts = text_info.get("initialAttributedTexts", {}).get("text", {})
+    if not texts:
+        return ""
+    # texts 是 {"0": "...", "1": "..."} 格式,拼接所有
+    return "".join(texts.get(str(i), "") for i in range(len(texts)))
+
+
+def extract_link_from_block(block_data: dict) -> str:
+    """提取 block 中的链接"""
+    text_info = block_data.get("text", {})
+    apool = text_info.get("apool", {}).get("numToAttrib", {})
+    for _, attr in apool.items():
+        if isinstance(attr, list) and len(attr) == 2:
+            if attr[0] == "link":
+                from urllib.parse import unquote
+                return unquote(attr[1])
+    return ""
+
+
+def blocks_to_markdown(block_map: dict, block_sequence: list, page_id: str) -> str:
+    """将 block_map 按 block_sequence 顺序转换为 Markdown"""
+    lines = []
+    in_code_block = False
+    code_lang = ""
+    code_lines = []
+
+    for block_id in block_sequence:
+        block = block_map.get(block_id)
+        if not block:
+            continue
+
+        data = block.get("data", {})
+        block_type = data.get("type", "")
+        text = extract_text_from_block(data)
+        link = extract_link_from_block(data)
+
+        # 跳过页面根节点本身(标题已单独处理)
+        if block_type == "page":
+            if text:
+                lines.append(f"# {text}")
+                lines.append("")
+            continue
+
+        # 处理代码块的结束
+        if in_code_block and block_type != "code":
+            lines.append(f"```{code_lang}")
+            for cl in code_lines:
+                lines.append(cl)
+            lines.append("```")
+            lines.append("")
+            in_code_block = False
+            code_lines = []
+
+        if block_type == "heading1":
+            lines.append(f"# {text}")
+            lines.append("")
+        elif block_type == "heading2":
+            lines.append(f"## {text}")
+            lines.append("")
+        elif block_type == "heading3":
+            lines.append(f"### {text}")
+            lines.append("")
+        elif block_type == "heading4":
+            lines.append(f"#### {text}")
+            lines.append("")
+        elif block_type == "heading5":
+            lines.append(f"##### {text}")
+            lines.append("")
+        elif block_type == "heading6":
+            lines.append(f"###### {text}")
+            lines.append("")
+        elif block_type == "text":
+            if text:
+                if link:
+                    lines.append(f"{text}")
+                    lines.append(f"  链接: {link}")
+                else:
+                    lines.append(text)
+                lines.append("")
+            else:
+                lines.append("")
+        elif block_type == "bullet":
+            if link:
+                lines.append(f"- [{text}]({link})")
+            else:
+                lines.append(f"- {text}")
+        elif block_type == "ordered":
+            if link:
+                lines.append(f"1. [{text}]({link})")
+            else:
+                lines.append(f"1. {text}")
+        elif block_type == "todo":
+            checked = data.get("checked", False)
+            mark = "x" if checked else " "
+            lines.append(f"- [{mark}] {text}")
+        elif block_type == "code":
+            if not in_code_block:
+                in_code_block = True
+                code_lang = data.get("language", "")
+                code_lines = []
+            code_lines.append(text)
+        elif block_type == "callout":
+            # callout 容器,内容在子节点中
+            if text:
+                lines.append(f"> {text}")
+                lines.append("")
+        elif block_type == "quote":
+            lines.append(f"> {text}")
+            lines.append("")
+        elif block_type == "divider":
+            lines.append("---")
+            lines.append("")
+        elif block_type == "image":
+            token = data.get("token", "")
+            if token:
+                lines.append(f"![image]({token})")
+                lines.append("")
+        elif block_type in ("table", "table_cell"):
+            # 表格结构复杂,提取子节点文本
+            pass
+        else:
+            # 其他未知类型,如果有文本就输出
+            if text:
+                lines.append(text)
+                lines.append("")
+
+    # 处理最后一个代码块
+    if in_code_block:
+        lines.append(f"```{code_lang}")
+        for cl in code_lines:
+            lines.append(cl)
+        lines.append("```")
+        lines.append("")
+
+    return "\n".join(lines)
+
+
+def extract_tables(block_map: dict) -> list[str]:
+    """提取表格内容为 Markdown 格式"""
+    tables = []
+    for block_id, block in block_map.items():
+        data = block.get("data", {})
+        if data.get("type") != "table":
+            continue
+
+        columns = data.get("columns_id", [])
+        rows = data.get("rows_id", [])
+        cell_set = data.get("cell_set", {})
+
+        if not columns or not rows:
+            continue
+
+        table_rows = []
+        for row_id in rows:
+            row_cells = []
+            for col_id in columns:
+                cell_key = f"{row_id}{col_id}"
+                cell_info = cell_set.get(cell_key, {})
+                cell_block_id = cell_info.get("block_id", "")
+                if cell_block_id and cell_block_id in block_map:
+                    cell_block = block_map[cell_block_id]
+                    cell_data = cell_block.get("data", {})
+                    # 单元格内容在 children 中
+                    children = cell_data.get("children", [])
+                    cell_texts = []
+                    for child_id in children:
+                        if child_id in block_map:
+                            child_text = extract_text_from_block(block_map[child_id].get("data", {}))
+                            if child_text:
+                                cell_texts.append(child_text)
+                    row_cells.append(" ".join(cell_texts) if cell_texts else "")
+                else:
+                    row_cells.append("")
+            table_rows.append(row_cells)
+
+        if table_rows:
+            # 生成 Markdown 表格
+            md_lines = []
+            # 表头
+            md_lines.append("| " + " | ".join(table_rows[0]) + " |")
+            md_lines.append("| " + " | ".join(["---"] * len(columns)) + " |")
+            # 数据行
+            for row in table_rows[1:]:
+                md_lines.append("| " + " | ".join(row) + " |")
+            md_lines.append("")
+            tables.append("\n".join(md_lines))
+
+    return tables
+
+
+def convert(html: str) -> str:
+    """主转换函数"""
+    data = extract_block_data(html)
+    block_map = data.get("block_map", {})
+    block_sequence = data.get("block_sequence", [])
+
+    if not block_map:
+        raise ValueError("block_map 为空,无法提取文档内容")
+
+    # 找到页面根节点
+    page_id = ""
+    for bid, block in block_map.items():
+        if block.get("data", {}).get("type") == "page":
+            page_id = bid
+            break
+
+    # 按 block_sequence 转换主体内容
+    markdown = blocks_to_markdown(block_map, block_sequence, page_id)
+
+    # 提取表格
+    tables = extract_tables(block_map)
+
+    # 如果有表格,附加到文档末尾(因为表格在主体中被跳过)
+    # 实际上更好的做法是在原位插入,但飞书的表格嵌套结构比较复杂
+    # 这里先简单追加
+    if tables:
+        markdown += "\n\n## 附录:表格数据\n\n"
+        for i, table in enumerate(tables, 1):
+            markdown += f"### 表格 {i}\n\n{table}\n"
+
+    # 清理多余空行
+    markdown = re.sub(r"\n{3,}", "\n\n", markdown)
+
+    return markdown.strip()
+
+
+def main():
+    parser = argparse.ArgumentParser(description="飞书文档转 Markdown")
+    parser.add_argument("url", help="飞书文档 URL")
+    parser.add_argument("-o", "--output", default="output.md", help="输出文件路径")
+    args = parser.parse_args()
+
+    try:
+        html = fetch_html(args.url)
+        print("正在解析文档结构...")
+        markdown = convert(html)
+
+        output_path = Path(args.output)
+        output_path.write_text(markdown, encoding="utf-8")
+
+        print(f"成功保存到: {output_path.absolute()}")
+        print(f"文件大小: {len(markdown)} 字符")
+
+    except httpx.HTTPStatusError as e:
+        print(f"HTTP 错误: {e.response.status_code}")
+        print("可能是文档需要登录或权限不足")
+        sys.exit(1)
+    except Exception as e:
+        print(f"错误: {e}")
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()

+ 3 - 0
src/tool_agent/__init__.py

@@ -0,0 +1,3 @@
+"""tool_agent - 智能工具管理系统"""
+
+__version__ = "0.1.0"

+ 37 - 0
src/tool_agent/__main__.py

@@ -0,0 +1,37 @@
+import asyncio
+import logging
+import sys
+
+from tool_agent.router.agent import Router
+
+# 配置日志:确保级别是 INFO,这样你能看到 uvicorn 的启动日志
+logging.basicConfig(
+    level=logging.INFO, 
+    format="%(asctime)s [%(name)s] %(levelname)s: %(message)s"
+)
+logger = logging.getLogger(__name__)
+
+async def main():
+    logger.info("正在初始化 Router...")
+    router = Router()
+    try:
+        # 增加超时保护或确保它是非阻塞启动的
+        logger.info("正在启动服务,尝试监听端口 8001...")
+        await router.start(port=8001)
+    except Exception as e:
+        logger.error(f"启动失败: {e}")
+    finally:
+        logger.info("正在停止所有服务...")
+        router.stop_all()
+
+if __name__ == "__main__":
+    # --- Windows 兼容性补丁 ---
+    if sys.platform == 'win32':
+        # 强制使用 ProactorEventLoopPolicy 以支持 Windows 的异步 I/O
+        asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
+    
+    try:
+        asyncio.run(main())
+    except KeyboardInterrupt:
+        # 优雅处理 Ctrl+C
+        pass

binární
src/tool_agent/__pycache__/__init__.cpython-312.pyc


binární
src/tool_agent/__pycache__/__main__.cpython-312.pyc


binární
src/tool_agent/__pycache__/config.cpython-312.pyc


binární
src/tool_agent/__pycache__/messaging.cpython-312.pyc


binární
src/tool_agent/__pycache__/models.cpython-312.pyc


+ 44 - 0
src/tool_agent/config.py

@@ -0,0 +1,44 @@
+"""全局配置"""
+
+from pathlib import Path
+from pydantic_settings import BaseSettings
+
+
+class Settings(BaseSettings):
+    """tool_agent 全局配置,支持环境变量覆盖"""
+
+    # 项目路径
+    base_dir: Path = Path(__file__).resolve().parent.parent.parent
+    tools_dir: Path = base_dir / "tools"
+    data_dir: Path = base_dir / "data"
+
+    # 对外端口
+    fastapi_port: int = 8001
+    mcp_port: int = 8001
+
+    # Docker 工具端口起始
+    docker_port_start: int = 9001
+
+    # Docker 容器默认配置
+    docker_base_image: str = "agent-sandbox:latest"
+    docker_mem_limit: str = "1g"
+    docker_nano_cpus: int = 1_000_000_000  # 1 CPU
+    docker_ttl_seconds: int = 1800  # 容器自动清理 TTL
+
+    # 冷热调度
+    cold_tool_idle_timeout_s: int = 300
+    hot_tool_max_containers: int = 5
+    eviction_policy: str = "lru"
+
+    # 财务
+    monthly_limit_usd: float = 100.0
+    single_tx_limit_usd: float = 20.0
+    require_approval_above_usd: float = 10.0
+
+    # 健康检查
+    health_check_interval_s: int = 60
+
+    model_config = {"env_prefix": "TOOL_AGENT_"}
+
+
+settings = Settings()

+ 29 - 0
src/tool_agent/messaging.py

@@ -0,0 +1,29 @@
+"""双 Agent 内部消息总线 — asyncio.Queue 封装"""
+
+from __future__ import annotations
+
+import asyncio
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from tool_agent.models import AgentMessage
+
+
+class MessageBus:
+    """Router Agent 与 Tool Agent 之间的异步消息队列"""
+
+    def __init__(self) -> None:
+        self._to_tool: asyncio.Queue[AgentMessage] = asyncio.Queue()
+        self._to_router: asyncio.Queue[AgentMessage] = asyncio.Queue()
+
+    async def send_to_tool(self, message: AgentMessage) -> None:
+        await self._to_tool.put(message)
+
+    async def send_to_router(self, message: AgentMessage) -> None:
+        await self._to_router.put(message)
+
+    async def recv_as_tool(self) -> AgentMessage:
+        return await self._to_tool.get()
+
+    async def recv_as_router(self) -> AgentMessage:
+        return await self._to_router.get()

+ 96 - 0
src/tool_agent/models.py

@@ -0,0 +1,96 @@
+"""公共数据模型"""
+
+from __future__ import annotations
+
+from datetime import datetime
+from enum import Enum
+from typing import Any
+
+from pydantic import BaseModel, Field
+
+
+# ---- 枚举 ----
+
+class ToolStatus(str, Enum):
+    ACTIVE = "active"
+    INACTIVE = "inactive"
+    STAGING = "staging"
+    BUILDING = "building"
+
+
+class MessageType(str, Enum):
+    TOOL_REQUEST = "tool_request"
+    TOOL_READY = "tool_ready"
+    TOOL_ERROR = "tool_error"
+    HEALTH_ALERT = "health_alert"
+
+
+class ContainerStatus(str, Enum):
+    RUNNING = "running"
+    DESTROYED = "destroyed"
+
+
+# ---- 工具元信息(纯元数据,不含运行时) ----
+
+class ToolMeta(BaseModel):
+    """工具元信息 — 只描述工具是什么,不管在哪跑"""
+    model_config = {"extra": "ignore"}   # 兼容旧 registry.json 中的多余字段
+
+    tool_id: str
+    name: str
+    category: str = ""
+    description: str = ""
+    input_schema: dict[str, Any] = Field(default_factory=dict)
+    output_schema: dict[str, Any] = Field(default_factory=dict)
+    stream_support: bool = False
+    status: ToolStatus = ToolStatus.ACTIVE
+
+
+# ---- 容器信息(Docker 运行时,独立于工具元数据) ----
+
+class ContainerInfo(BaseModel):
+    container_id: str
+    tool_id: str = ""
+    image: str = ""
+    port_mapping: dict[int, int] = Field(default_factory=dict)
+    volumes: dict[str, str] = Field(default_factory=dict)
+    mem_limit: str = "1g"
+    nano_cpus: int = 1_000_000_000
+    use_gpu: bool = False
+    gpu_count: int = -1
+    status: ContainerStatus = ContainerStatus.RUNNING
+    created_at: datetime | None = None
+    last_accessed: datetime | None = None
+    destroyed_at: datetime | None = None
+
+
+# ---- 请求/响应 ----
+
+class InvokeRequest(BaseModel):
+    params: dict[str, Any] = Field(default_factory=dict)
+    stream: bool = False
+
+
+class InvokeResponse(BaseModel):
+    status: str = "success"
+    result: Any = None
+    error: str | None = None
+
+
+class ToolRequest(BaseModel):
+    """外部提交新工具需求"""
+    description: str
+    callback_url: str | None = None
+
+
+class TaskStatus(BaseModel):
+    task_id: str
+    status: str = "pending"
+    result: Any = None
+
+
+# ---- 内部消息 ----
+
+class AgentMessage(BaseModel):
+    type: MessageType
+    payload: dict[str, Any] = Field(default_factory=dict)

+ 0 - 0
src/tool_agent/registry/__init__.py


binární
src/tool_agent/registry/__pycache__/__init__.cpython-312.pyc


binární
src/tool_agent/registry/__pycache__/registry.cpython-312.pyc


+ 29 - 0
src/tool_agent/registry/catalog.py

@@ -0,0 +1,29 @@
+"""工具目录 — 按类别组织、搜索"""
+
+from __future__ import annotations
+
+from tool_agent.models import ToolMeta, ToolStatus
+
+
+class Catalog:
+    """工具目录查询"""
+
+    def __init__(self, tools: list[ToolMeta]) -> None:
+        self._tools = tools
+
+    def by_category(self) -> dict[str, list[ToolMeta]]:
+        """按类别分组"""
+        result: dict[str, list[ToolMeta]] = {}
+        for t in self._tools:
+            if t.status == ToolStatus.ACTIVE:
+                result.setdefault(t.category or "uncategorized", []).append(t)
+        return result
+
+    def search(self, keyword: str) -> list[ToolMeta]:
+        """关键词搜索"""
+        keyword = keyword.lower()
+        return [
+            t for t in self._tools
+            if t.status == ToolStatus.ACTIVE
+            and (keyword in t.name.lower() or keyword in t.description.lower())
+        ]

+ 195 - 0
src/tool_agent/registry/registry.py

@@ -0,0 +1,195 @@
+"""注册表 CRUD — 纯元数据管理,读写 registry.json"""
+
+from __future__ import annotations
+
+import json
+import logging
+import shutil
+import time
+import threading
+from pathlib import Path
+
+from tool_agent.config import settings
+from tool_agent.models import ToolMeta, ToolStatus
+
+logger = logging.getLogger(__name__)
+
+
+class ToolRegistry:
+    """工具注册表 — 只管工具是什么,不管在哪跑"""
+
+    def __init__(self) -> None:
+        self._path = settings.data_dir / "registry.json"
+        self._tools: dict[str, ToolMeta] = {}
+        self._lock = threading.Lock()
+        self._load()
+
+    def _load(self) -> None:
+        if self._path.exists():
+            data = json.loads(self._path.read_text(encoding="utf-8"))
+            for item in data.get("tools", []):
+                tool = ToolMeta(**item)
+                self._tools[tool.tool_id] = tool
+
+    def _save(self) -> None:
+        self._path.parent.mkdir(parents=True, exist_ok=True)
+        data = {"tools": [t.model_dump(mode="json") for t in self._tools.values()], "version": "2.0"}
+        self._path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
+
+    # ---- CRUD ----
+
+    def get(self, tool_id: str) -> ToolMeta | None:
+        return self._tools.get(tool_id)
+
+    def list_all(self) -> list[ToolMeta]:
+        return list(self._tools.values())
+
+    def register(self, tool: ToolMeta) -> None:
+        with self._lock:
+            self._tools[tool.tool_id] = tool
+            self._save()
+        logger.info(f"Registered tool: {tool.tool_id}")
+
+    def unregister(self, tool_id: str) -> bool:
+        with self._lock:
+            if tool_id in self._tools:
+                del self._tools[tool_id]
+                self._save()
+                logger.info(f"Unregistered tool: {tool_id}")
+                return True
+        return False
+
+    def update(self, tool: ToolMeta) -> None:
+        with self._lock:
+            self._tools[tool.tool_id] = tool
+            self._save()
+
+    # ---- 查询 ----
+
+    def list_active(self) -> list[ToolMeta]:
+        return [t for t in self._tools.values() if t.status == ToolStatus.ACTIVE]
+
+    def find_by_category(self, category: str) -> list[ToolMeta]:
+        return [t for t in self._tools.values() if t.category == category and t.status == ToolStatus.ACTIVE]
+
+    def search(self, keyword: str) -> list[ToolMeta]:
+        kw = keyword.lower()
+        return [
+            t for t in self._tools.values()
+            if t.status == ToolStatus.ACTIVE
+            and (kw in t.name.lower() or kw in t.description.lower())
+        ]
+
+    # ---- 清理 ----
+
+    @staticmethod
+    def _kill_processes_in_dir(directory: Path) -> None:
+        try:
+            import psutil
+        except ImportError:
+            return
+
+        dir_str = str(directory.resolve())
+        killed = []
+        for proc in psutil.process_iter(["pid", "cwd", "open_files"]):
+            try:
+                if proc.info.get("cwd") and str(Path(proc.info["cwd"]).resolve()).startswith(dir_str):
+                    logger.info(f"Killing process {proc.pid} (cwd in {directory.name})")
+                    proc.kill()
+                    killed.append(proc)
+                    continue
+                open_files = proc.info.get("open_files")
+                if open_files:
+                    for f in open_files:
+                        if str(Path(f.path).resolve()).startswith(dir_str):
+                            logger.info(f"Killing process {proc.pid} (open file in {directory.name})")
+                            proc.kill()
+                            killed.append(proc)
+                            break
+            except Exception:
+                pass
+
+        for proc in killed:
+            try:
+                proc.wait(timeout=5)
+            except Exception:
+                pass
+        if killed:
+            time.sleep(0.5)
+
+    @staticmethod
+    def _safe_rmtree(path: Path) -> bool:
+        try:
+            shutil.rmtree(path)
+            return True
+        except PermissionError:
+            time.sleep(1)
+            try:
+                shutil.rmtree(path)
+                return True
+            except Exception as e:
+                logger.warning(f"Failed to delete {path}: {e}")
+                return False
+
+    def destroy(self, tool_id: str) -> dict:
+        """彻底删除工具:注册信息 + 项目文件 + staging 残留"""
+        cleaned = []
+
+        if self.unregister(tool_id):
+            cleaned.append("registry")
+
+        local_dir = settings.tools_dir / "local" / tool_id
+        if local_dir.exists():
+            self._kill_processes_in_dir(local_dir)
+            if self._safe_rmtree(local_dir):
+                cleaned.append(f"local:{local_dir}")
+                logger.info(f"Deleted local dir: {local_dir}")
+
+        staging_dir = settings.data_dir / "staging"
+        if staging_dir.exists():
+            for d in staging_dir.iterdir():
+                if not d.is_dir():
+                    continue
+                try:
+                    for f in d.rglob("*"):
+                        if f.is_file() and tool_id in f.read_text(errors="ignore")[:1000]:
+                            self._safe_rmtree(d)
+                            cleaned.append(f"staging:{d.name}")
+                            break
+                except Exception:
+                    pass
+
+        # 清理来源存储
+        try:
+            from tool_agent.router.status import SourceStore
+            SourceStore().remove_tool(tool_id)
+            cleaned.append("sources")
+        except Exception:
+            pass
+
+        logger.info(f"Destroyed tool '{tool_id}': {cleaned}")
+        return {"status": "success", "tool_id": tool_id, "cleaned": cleaned}
+
+    def destroy_all(self) -> dict:
+        tool_ids = [t.tool_id for t in self.list_all()]
+        results = []
+        for tid in tool_ids:
+            results.append(self.destroy(tid))
+
+        local_base = settings.tools_dir / "local"
+        if local_base.exists():
+            for d in local_base.iterdir():
+                if d.is_dir() and d.name not in tool_ids:
+                    self._kill_processes_in_dir(d)
+                    if self._safe_rmtree(d):
+                        logger.info(f"Deleted orphan local dir: {d.name}")
+                        results.append({"tool_id": d.name, "cleaned": [f"local:{d}"]})
+
+        staging_dir = settings.data_dir / "staging"
+        if staging_dir.exists():
+            for d in staging_dir.iterdir():
+                if d.is_dir():
+                    self._safe_rmtree(d)
+            logger.info("Cleared all staging directories")
+
+        return {"status": "success", "count": len(results), "results": results}

+ 17 - 0
src/tool_agent/registry/schema.py

@@ -0,0 +1,17 @@
+"""工具元信息 schema 定义 — 重导出 models 中的核心类型"""
+
+from tool_agent.models import (
+    InvokeRequest,
+    InvokeResponse,
+    TaskStatus,
+    ToolMeta,
+    ToolStatus,
+)
+
+__all__ = [
+    "ToolMeta",
+    "ToolStatus",
+    "InvokeRequest",
+    "InvokeResponse",
+    "TaskStatus",
+]

+ 0 - 0
src/tool_agent/router/__init__.py


binární
src/tool_agent/router/__pycache__/__init__.cpython-312.pyc


binární
src/tool_agent/router/__pycache__/agent.cpython-312.pyc


binární
src/tool_agent/router/__pycache__/dispatcher.cpython-312.pyc


binární
src/tool_agent/router/__pycache__/server.cpython-312.pyc


binární
src/tool_agent/router/__pycache__/status.cpython-312.pyc


+ 59 - 0
src/tool_agent/router/agent.py

@@ -0,0 +1,59 @@
+"""Router — 路由层入口
+
+直接启动 FastAPI 服务,不依赖 MessageBus / Agent 抽象。
+管理工具状态表,对外暴露 search_tools / select_tool / create_tool 接口。
+"""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+from typing import Any
+
+from tool_agent.registry.registry import ToolRegistry
+from tool_agent.router.dispatcher import Dispatcher
+from tool_agent.router.server import create_app
+from tool_agent.router.status import ToolStatusManager
+
+logger = logging.getLogger(__name__)
+
+
+class Router:
+    """路由层:工具状态管理 + 对外 API"""
+
+    def __init__(self) -> None:
+        self.registry = ToolRegistry()
+        self.status_manager = ToolStatusManager(self.registry)
+        self.dispatcher = Dispatcher(self.status_manager)
+        self._tasks: dict[str, dict[str, Any]] = {}
+        self.app = create_app(self)
+
+    async def start(self, port: int = 8001) -> None:
+        import uvicorn
+        config = uvicorn.Config(self.app, host="0.0.0.0", port=port, log_level="info")
+        server = uvicorn.Server(config)
+        logger.info(f"Router starting on port {port}")
+        await server.serve()
+
+    def submit_create_task(self, task_id: str, task_spec: str) -> None:
+        self._tasks[task_id] = {"status": "pending", "task_spec": task_spec}
+        asyncio.get_event_loop().create_task(self._run_create_task(task_id, task_spec))
+
+    async def _run_create_task(self, task_id: str, task_spec: str) -> None:
+        self._tasks[task_id]["status"] = "running"
+        try:
+            from tool_agent.tool.agent import CodingAgent
+            agent = CodingAgent()
+            result = await agent.execute(task_spec)
+            self._tasks[task_id].update({"status": "completed", "result": result})
+            self.registry._load()
+            self.status_manager._sync_from_registry()
+        except Exception as e:
+            logger.error(f"Create task {task_id} failed: {e}")
+            self._tasks[task_id].update({"status": "failed", "error": str(e)})
+
+    def get_task_status(self, task_id: str) -> dict[str, Any] | None:
+        return self._tasks.get(task_id)
+
+    def stop_all(self) -> int:
+        return self.status_manager.stop_all()

+ 49 - 0
src/tool_agent/router/dispatcher.py

@@ -0,0 +1,49 @@
+"""请求分发 — 按 tool_id 路由到实际运行的服务端点"""
+
+from __future__ import annotations
+
+import logging
+from typing import Any, TYPE_CHECKING
+
+import httpx
+
+if TYPE_CHECKING:
+    from tool_agent.router.status import ToolStatusManager
+
+logger = logging.getLogger(__name__)
+
+
+class Dispatcher:
+    """根据工具的运行状态分发请求到对应端点"""
+
+    def __init__(self, status_manager: ToolStatusManager) -> None:
+        self._status_manager = status_manager
+
+    async def dispatch(self, tool_id: str, params: dict[str, Any], stream: bool = False) -> dict[str, Any]:
+        """分发调用请求到工具的活跃端点"""
+        # 1. 获取端点信息
+        endpoint = self._status_manager.get_active_endpoint(tool_id)
+        if not endpoint:
+            return {"status": "error", "error": f"Tool '{tool_id}' is not running or has no active endpoint"}
+
+        # 2. 根据端点类型调用
+        try:
+            async with httpx.AsyncClient(timeout=600.0) as client:
+                headers = {}
+                if endpoint.get("api_key"):
+                    headers["Authorization"] = f"Bearer {endpoint['api_key']}"
+
+                if endpoint["http_method"].upper() == "POST":
+                    response = await client.post(endpoint["url"], json=params, headers=headers)
+                else:
+                    response = await client.get(endpoint["url"], params=params, headers=headers)
+
+                response.raise_for_status()
+                return response.json()
+
+        except httpx.HTTPStatusError as e:
+            logger.error(f"HTTP error calling '{tool_id}': {e.response.status_code} {e.response.text}")
+            return {"status": "error", "error": f"HTTP {e.response.status_code}: {e.response.text}"}
+        except Exception as e:
+            logger.error(f"Error calling '{tool_id}': {e}")
+            return {"status": "error", "error": str(e)}

+ 39 - 0
src/tool_agent/router/health.py

@@ -0,0 +1,39 @@
+"""工具健康检查"""
+
+from __future__ import annotations
+
+import logging
+
+import httpx
+
+from tool_agent.router.status import ToolStatusManager, ProcessState
+
+logger = logging.getLogger(__name__)
+
+
+class HealthChecker:
+    """定期检查工具状态"""
+
+    def __init__(self, status_manager: ToolStatusManager) -> None:
+        self._status_manager = status_manager
+
+    async def check(self, tool_id: str) -> bool:
+        """检查单个工具健康状态"""
+        endpoint = self._status_manager.get_active_endpoint(tool_id)
+        if not endpoint:
+            return False
+        try:
+            url = endpoint["url"].rsplit("/", 1)[0] + "/health"
+            async with httpx.AsyncClient(timeout=5) as client:
+                resp = await client.get(url)
+                return resp.status_code == 200
+        except Exception:
+            return False
+
+    async def check_all(self) -> dict[str, bool]:
+        """批量健康检查"""
+        results = {}
+        for route in self._status_manager.list_status():
+            if route.state == ProcessState.RUNNING:
+                results[route.tool_id] = await self.check(route.tool_id)
+        return results

+ 24 - 0
src/tool_agent/router/mcp_server.py

@@ -0,0 +1,24 @@
+"""MCP Server 适配层"""
+
+from __future__ import annotations
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class MCPServer:
+    """标准 MCP Server,将注册工具映射为 MCP Tool"""
+
+    def __init__(self, port: int = 8001) -> None:
+        self.port = port
+
+    async def start(self) -> None:
+        """启动 MCP Server"""
+        # TODO: 接入 MCP SDK,暴露 tools/list 和 tools/call
+        logger.info(f"MCP Server placeholder on port {self.port}")
+
+    async def refresh_tools(self) -> None:
+        """注册表变更时刷新 MCP Tool 列表"""
+        # TODO: 重新加载注册表
+        pass

+ 0 - 0
src/tool_agent/router/middleware/__init__.py


+ 14 - 0
src/tool_agent/router/middleware/auth.py

@@ -0,0 +1,14 @@
+"""鉴权中间件"""
+
+from __future__ import annotations
+
+from fastapi import Request, Response
+from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
+
+
+class AuthMiddleware(BaseHTTPMiddleware):
+    """API 鉴权 — 验证请求携带的 token/key"""
+
+    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
+        # TODO: 实现鉴权逻辑(API key / token 验证)
+        return await call_next(request)

+ 33 - 0
src/tool_agent/router/middleware/cache.py

@@ -0,0 +1,33 @@
+"""结果缓存中间件"""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import time
+from typing import Any
+
+
+class ResultCache:
+    """相同 tool_id + params 的重复调用直接返回缓存"""
+
+    def __init__(self, default_ttl: int = 300) -> None:
+        self._cache: dict[str, tuple[Any, float]] = {}
+        self.default_ttl = default_ttl
+
+    def _make_key(self, tool_id: str, params: dict) -> str:
+        raw = json.dumps({"tool_id": tool_id, "params": params}, sort_keys=True)
+        return hashlib.sha256(raw.encode()).hexdigest()
+
+    def get(self, tool_id: str, params: dict) -> Any | None:
+        key = self._make_key(tool_id, params)
+        if key in self._cache:
+            value, expire_at = self._cache[key]
+            if time.time() < expire_at:
+                return value
+            del self._cache[key]
+        return None
+
+    def set(self, tool_id: str, params: dict, result: Any, ttl: int | None = None) -> None:
+        key = self._make_key(tool_id, params)
+        self._cache[key] = (result, time.time() + (ttl or self.default_ttl))

+ 43 - 0
src/tool_agent/router/middleware/metrics.py

@@ -0,0 +1,43 @@
+"""调用计量中间件"""
+
+from __future__ import annotations
+
+import time
+from dataclasses import dataclass, field
+
+
+@dataclass
+class ToolMetrics:
+    call_count: int = 0
+    total_latency_ms: float = 0.0
+    error_count: int = 0
+
+    @property
+    def avg_latency_ms(self) -> float:
+        return self.total_latency_ms / self.call_count if self.call_count else 0.0
+
+    @property
+    def error_rate(self) -> float:
+        return self.error_count / self.call_count if self.call_count else 0.0
+
+
+class MetricsCollector:
+    """记录每个工具的调用频次、耗时、错误率"""
+
+    def __init__(self) -> None:
+        self._metrics: dict[str, ToolMetrics] = {}
+
+    def record(self, tool_id: str, latency_ms: float, error: bool = False) -> None:
+        if tool_id not in self._metrics:
+            self._metrics[tool_id] = ToolMetrics()
+        m = self._metrics[tool_id]
+        m.call_count += 1
+        m.total_latency_ms += latency_ms
+        if error:
+            m.error_count += 1
+
+    def get(self, tool_id: str) -> ToolMetrics | None:
+        return self._metrics.get(tool_id)
+
+    def all(self) -> dict[str, ToolMetrics]:
+        return dict(self._metrics)

+ 45 - 0
src/tool_agent/router/scheduler.py

@@ -0,0 +1,45 @@
+"""冷热调度 — 空闲超时停止、LRU 置换(基于 ToolStatusManager)"""
+
+from __future__ import annotations
+
+import logging
+from datetime import datetime, timezone
+
+from tool_agent.config import settings
+from tool_agent.router.status import ToolStatusManager, ProcessState
+
+logger = logging.getLogger(__name__)
+
+
+class Scheduler:
+    """管理工具进程生命周期:空闲休眠、LRU 置换"""
+
+    def __init__(self, status_manager: ToolStatusManager) -> None:
+        self._status_manager = status_manager
+
+    async def check_idle(self) -> list[str]:
+        """检查空闲超时的工具,停止并返回 tool_id 列表"""
+        now = datetime.now(timezone.utc)
+        stopped = []
+        for route in self._status_manager.list_status():
+            if route.state != ProcessState.RUNNING or not route.started_at:
+                continue
+            elapsed = (now - route.started_at).total_seconds()
+            if elapsed > settings.cold_tool_idle_timeout_s:
+                self._status_manager.stop_tool(route.tool_id)
+                stopped.append(route.tool_id)
+                logger.info(f"Idle timeout: stopped '{route.tool_id}'")
+        return stopped
+
+    async def evict_lru(self) -> str | None:
+        """LRU 置换:运行中工具数超限时,停止最久启动的"""
+        running = [
+            r for r in self._status_manager.list_status()
+            if r.state == ProcessState.RUNNING and r.started_at
+        ]
+        if len(running) <= settings.hot_tool_max_containers:
+            return None
+        oldest = min(running, key=lambda r: r.started_at)
+        self._status_manager.stop_tool(oldest.tool_id)
+        logger.info(f"LRU evicted: '{oldest.tool_id}'")
+        return oldest.tool_id

+ 192 - 0
src/tool_agent/router/server.py

@@ -0,0 +1,192 @@
+"""FastAPI 应用定义 — 对外三个核心接口"""
+
+from __future__ import annotations
+
+import uuid
+import logging
+from typing import Any, TYPE_CHECKING
+
+from fastapi import FastAPI, HTTPException
+from pydantic import BaseModel, Field
+
+if TYPE_CHECKING:
+    from tool_agent.router.agent import Router
+
+logger = logging.getLogger(__name__)
+
+
+# ---- 请求/响应模型 ----
+
+class SearchToolsRequest(BaseModel):
+    keyword: str = ""
+    category: str = ""
+
+class ToolParamInfo(BaseModel):
+    name: str
+    type: str = ""
+    description: str = ""
+    required: bool = False
+    default: Any = None
+    enum: list[Any] | None = None
+
+class ToolInfo(BaseModel):
+    tool_id: str
+    name: str
+    description: str
+    category: str
+    stream_support: bool = False
+    # 解析后的参数列表(方便直接阅读)
+    params: list[ToolParamInfo] = Field(default_factory=list)
+    required_params: list[str] = Field(default_factory=list)
+    # 原始 schema(供程序使用)
+    input_schema: dict[str, Any] = Field(default_factory=dict)
+    output_schema: dict[str, Any] = Field(default_factory=dict)
+    # 运行时信息
+    runtime_type: str = ""
+    host_dir: str = ""
+    endpoint_path: str = ""
+    http_method: str = ""
+    state: str = "stopped"
+    port: int | None = None
+    pid: int | None = None
+
+class SearchToolsResponse(BaseModel):
+    tools: list[ToolInfo]
+    total: int
+
+class SelectToolRequest(BaseModel):
+    tool_id: str
+    params: dict[str, Any] = Field(default_factory=dict)
+    stream: bool = False
+
+class SelectToolResponse(BaseModel):
+    status: str = "success"
+    result: Any = None
+    error: str | None = None
+
+class CreateToolRequest(BaseModel):
+    description: str
+    task_spec: str = ""
+
+class CreateToolResponse(BaseModel):
+    task_id: str
+    status: str = "pending"
+    message: str = ""
+
+
+def create_app(router: Router) -> FastAPI:
+    app = FastAPI(title="Tool Agent Router", version="0.1.0")
+
+    @app.get("/health")
+    async def health():
+        return {"status": "ok"}
+
+    # ---- 1. 搜索工具 ----
+
+    @app.post("/search_tools", response_model=SearchToolsResponse)
+    async def search_tools(request: SearchToolsRequest):
+        """搜索可用工具列表,返回工具信息及运行状态"""
+        all_tools = router.registry.list_all()
+
+        matched = all_tools
+        if request.category:
+            matched = [t for t in matched if t.category == request.category]
+        if request.keyword:
+            kw = request.keyword.lower()
+            matched = [t for t in matched if kw in t.name.lower() or kw in t.description.lower()]
+
+        result = []
+        for tool in matched:
+            route = router.status_manager.get_status(tool.tool_id)
+            source = route.sources[route.active_source] if route and route.sources else None
+
+            # 解析 input_schema 为可读参数列表
+            params = []
+            required_names = tool.input_schema.get("required", [])
+            for pname, pdef in tool.input_schema.get("properties", {}).items():
+                params.append(ToolParamInfo(
+                    name=pname,
+                    type=pdef.get("type", pdef.get("$ref", "")),
+                    description=pdef.get("description", ""),
+                    required=pname in required_names,
+                    default=pdef.get("default"),
+                    enum=pdef.get("enum"),
+                ))
+
+            result.append(ToolInfo(
+                tool_id=tool.tool_id,
+                name=tool.name,
+                description=tool.description,
+                category=tool.category,
+                stream_support=tool.stream_support,
+                params=params,
+                required_params=required_names,
+                input_schema=tool.input_schema,
+                output_schema=tool.output_schema,
+                runtime_type=source.type.value if source else "unknown",
+                host_dir=source.host_dir if source else "",
+                endpoint_path=source.endpoint_path if source else "",
+                http_method=source.http_method if source else "",
+                state=route.state.value if route else "stopped",
+                port=route.port if route else None,
+                pid=route.pid if route else None,
+            ))
+
+        return SearchToolsResponse(tools=result, total=len(result))
+
+    # ---- 2. 选择并调用工具 ----
+
+    @app.post("/select_tool", response_model=SelectToolResponse)
+    async def select_tool(request: SelectToolRequest):
+        """选择工具并调用:未启动则自动启动"""
+        tool = router.registry.get(request.tool_id)
+        if not tool:
+            raise HTTPException(status_code=404, detail=f"Tool '{request.tool_id}' not found")
+
+        proc = router.status_manager.get_status(request.tool_id)
+        if not proc or proc.state != "running":
+            proc = router.status_manager.start_tool(request.tool_id)
+            if proc.state != "running":
+                return SelectToolResponse(status="error", error=f"Failed to start tool: {proc.last_error}")
+
+        try:
+            result = await router.dispatcher.dispatch(request.tool_id, request.params, request.stream)
+            return SelectToolResponse(status="success", result=result)
+        except Exception as e:
+            return SelectToolResponse(status="error", error=str(e))
+
+    # ---- 3. 请求创建新工具 ----
+
+    @app.post("/create_tool", response_model=CreateToolResponse)
+    async def create_tool(request: CreateToolRequest):
+        """提交新工具创建需求,异步交给 CodingAgent"""
+        task_id = f"create_{uuid.uuid4().hex[:8]}"
+        task_spec = request.task_spec or request.description
+
+        router.submit_create_task(task_id, task_spec)
+
+        return CreateToolResponse(task_id=task_id, status="pending", message="Task submitted")
+
+    # ---- 辅助接口(仅 localhost) ----
+
+    @app.get("/tools/status")
+    async def tools_status():
+        statuses = router.status_manager.list_status()
+        return {"tools": [s.model_dump() for s in statuses]}
+
+    @app.post("/tools/{tool_id}/start")
+    async def start_tool(tool_id: str):
+        return router.status_manager.start_tool(tool_id).model_dump()
+
+    @app.post("/tools/{tool_id}/stop")
+    async def stop_tool(tool_id: str):
+        return router.status_manager.stop_tool(tool_id).model_dump()
+
+    @app.get("/tasks/{task_id}")
+    async def get_task(task_id: str):
+        task = router.get_task_status(task_id)
+        if not task:
+            raise HTTPException(status_code=404, detail="Task not found")
+        return task
+
+    return app

+ 324 - 0
src/tool_agent/router/status.py

@@ -0,0 +1,324 @@
+"""工具运行状态 + 来源管理
+
+职责:
+- 管理每个工具的来源(source):本地 uv、Docker、外部 Hub 等
+- 管理运行状态:启动/停止工具进程
+- 同一个 tool_id 可以有多个 source,路由层选择活跃的那个
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+import socket
+import subprocess
+import threading
+import time
+from datetime import datetime, timezone
+from enum import Enum
+from pathlib import Path
+from typing import Any
+
+from pydantic import BaseModel, Field
+
+from tool_agent.config import settings
+from tool_agent.models import ToolMeta, ToolStatus
+from tool_agent.registry.registry import ToolRegistry
+
+logger = logging.getLogger(__name__)
+
+
+# ---- 来源模型 ----
+
+class SourceType(str, Enum):
+    LOCAL = "local"       # 本地 uv 项目
+    DOCKER = "docker"     # Docker 容器
+    HUB = "hub"           # 外部 API Hub(枚举内部原子工具)
+
+
+class ToolSource(BaseModel):
+    """工具的一个来源"""
+    type: SourceType
+    # local
+    host_dir: str = ""                     # 本地项目目录
+    # docker
+    container_id: str = ""
+    image: str = ""
+    # hub
+    hub_url: str = ""                      # Hub 的基础 URL
+    hub_tool_path: str = ""                # Hub 内该原子工具的路径
+    hub_api_key: str = ""
+    # 通用
+    endpoint_path: str = "/"               # HTTP API 路径
+    http_method: str = "POST"
+    internal_port: int = 0                 # 进程/容器内服务端口
+
+
+class ProcessState(str, Enum):
+    STOPPED = "stopped"
+    STARTING = "starting"
+    RUNNING = "running"
+    ERROR = "error"
+
+
+class ToolRoute(BaseModel):
+    """工具的路由信息 = 来源列表 + 运行状态"""
+    tool_id: str
+    sources: list[ToolSource] = Field(default_factory=list)
+    active_source: int = 0                 # 当前使用的 source 索引
+    state: ProcessState = ProcessState.STOPPED
+    pid: int | None = None
+    port: int | None = None                # 宿主机上的实际服务端口
+    started_at: datetime | None = None
+    last_error: str | None = None
+
+
+# ---- 持久化 ----
+
+class SourceStore:
+    """来源信息持久化 — data/sources.json"""
+
+    def __init__(self, path: Path | None = None) -> None:
+        self._path = path or (settings.data_dir / "sources.json")
+        self._lock = threading.Lock()
+
+    def load(self) -> dict[str, list[dict]]:
+        if not self._path.exists():
+            return {}
+        data = json.loads(self._path.read_text(encoding="utf-8"))
+        return data.get("sources", {})
+
+    def save(self, sources: dict[str, list[dict]]) -> None:
+        with self._lock:
+            self._path.parent.mkdir(parents=True, exist_ok=True)
+            self._path.write_text(
+                json.dumps({"sources": sources}, indent=2, ensure_ascii=False, default=str),
+                encoding="utf-8",
+            )
+
+    def add_source(self, tool_id: str, source: ToolSource) -> None:
+        with self._lock:
+            all_sources = self.load()
+            tool_sources = all_sources.get(tool_id, [])
+            tool_sources.append(source.model_dump(mode="json"))
+            all_sources[tool_id] = tool_sources
+            # 直接写入,不调用 save() 避免重复获取锁
+            self._path.parent.mkdir(parents=True, exist_ok=True)
+            self._path.write_text(
+                json.dumps({"sources": all_sources}, indent=2, ensure_ascii=False, default=str),
+                encoding="utf-8",
+            )
+
+    def get_sources(self, tool_id: str) -> list[ToolSource]:
+        data = self.load()
+        return [ToolSource(**s) for s in data.get(tool_id, [])]
+
+    def remove_tool(self, tool_id: str) -> None:
+        with self._lock:
+            all_sources = self.load()
+            if tool_id in all_sources:
+                del all_sources[tool_id]
+                self.save(all_sources)
+
+
+# ---- 状态管理器 ----
+
+class ToolStatusManager:
+    """工具进程生命周期管理 + 来源管理"""
+
+    def __init__(self, registry: ToolRegistry) -> None:
+        self._registry = registry
+        self._source_store = SourceStore()
+        self._routes: dict[str, ToolRoute] = {}
+        self._popen_refs: dict[str, subprocess.Popen] = {}
+        self._lock = threading.Lock()
+        self._sync_from_registry()
+
+    def _sync_from_registry(self) -> None:
+        """从注册表 + 来源存储同步"""
+        for tool in self._registry.list_all():
+            tid = tool.tool_id
+            if tid not in self._routes:
+                sources = self._source_store.get_sources(tid)
+                # 自动发现本地项目
+                if not sources:
+                    local_dir = settings.tools_dir / "local" / tid
+                    if local_dir.exists() and (local_dir / "main.py").exists():
+                        # 使用相对路径
+                        relative_path = f"tools/local/{tid}"
+                        src = ToolSource(type=SourceType.LOCAL, host_dir=relative_path)
+                        self._source_store.add_source(tid, src)
+                        sources = [src]
+                self._routes[tid] = ToolRoute(tool_id=tid, sources=sources)
+
+    def add_source(self, tool_id: str, source: ToolSource) -> None:
+        """为工具添加来源"""
+        self._source_store.add_source(tool_id, source)
+        if tool_id in self._routes:
+            self._routes[tool_id].sources.append(source)
+        else:
+            self._routes[tool_id] = ToolRoute(tool_id=tool_id, sources=[source])
+
+    @staticmethod
+    def _get_free_port() -> int:
+        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+            s.bind(("", 0))
+            return s.getsockname()[1]
+
+    def start_tool(self, tool_id: str) -> ToolRoute:
+        """启动工具"""
+        route = self._routes.get(tool_id)
+        if not route or not route.sources:
+            return self._set_error(tool_id, "No source available")
+
+        if route.state == ProcessState.RUNNING:
+            return route
+
+        source = route.sources[route.active_source]
+
+        if source.type == SourceType.LOCAL:
+            return self._start_local(tool_id, source)
+        elif source.type == SourceType.DOCKER:
+            return self._check_docker(tool_id, source)
+        elif source.type == SourceType.HUB:
+            # Hub 工具不需要启动本地进程,直接标记 running
+            route.state = ProcessState.RUNNING
+            with self._lock:
+                self._routes[tool_id] = route
+            return route
+        else:
+            return self._set_error(tool_id, f"Unsupported source type: {source.type}")
+
+    def _start_local(self, tool_id: str, source: ToolSource) -> ToolRoute:
+        # 将相对路径转为绝对路径
+        host_dir_path = Path(source.host_dir)
+        if not host_dir_path.is_absolute():
+            tool_dir = (settings.base_dir / host_dir_path).resolve()
+        else:
+            tool_dir = host_dir_path
+
+        main_py = tool_dir / "main.py"
+        if not main_py.exists():
+            return self._set_error(tool_id, f"main.py not found in {tool_dir}")
+
+        port = self._get_free_port()
+        try:
+            log_dir = tool_dir / "tests"
+            log_dir.mkdir(parents=True, exist_ok=True)
+
+            with open(log_dir / "server.log", "w") as lf:
+                proc = subprocess.Popen(
+                    ["uv", "run", "--directory", str(tool_dir),
+                     "python", "main.py", "--port", str(port)],
+                    stdout=lf, stderr=subprocess.STDOUT, text=True,
+                )
+
+            if self._wait_for_port(port, timeout=10):
+                route = ToolRoute(
+                    tool_id=tool_id,
+                    sources=self._routes.get(tool_id, ToolRoute(tool_id=tool_id)).sources,
+                    state=ProcessState.RUNNING,
+                    pid=proc.pid, port=port,
+                    started_at=datetime.now(timezone.utc),
+                )
+                with self._lock:
+                    self._routes[tool_id] = route
+                    self._popen_refs[tool_id] = proc
+                logger.info(f"Started '{tool_id}' on port {port} (PID {proc.pid})")
+                return route
+            else:
+                proc.kill()
+                return self._set_error(tool_id, f"Service failed to start. Check {log_dir / 'server.log'}")
+        except Exception as e:
+            return self._set_error(tool_id, str(e))
+
+    def _check_docker(self, tool_id: str, source: ToolSource) -> ToolRoute:
+        if source.internal_port:
+            route = self._routes.get(tool_id, ToolRoute(tool_id=tool_id))
+            route.state = ProcessState.RUNNING
+            route.port = source.internal_port
+            with self._lock:
+                self._routes[tool_id] = route
+            return route
+        return self._set_error(tool_id, "No port mapping for Docker tool")
+
+    @staticmethod
+    def _wait_for_port(port: int, timeout: int = 10) -> bool:
+        start = time.time()
+        while time.time() - start < timeout:
+            try:
+                with socket.create_connection(("127.0.0.1", port), timeout=1):
+                    return True
+            except (ConnectionRefusedError, OSError):
+                time.sleep(0.3)
+        return False
+
+    def stop_tool(self, tool_id: str) -> ToolRoute:
+        with self._lock:
+            proc = self._popen_refs.pop(tool_id, None)
+            if proc:
+                try:
+                    proc.kill()
+                    proc.wait(timeout=5)
+                except Exception:
+                    pass
+                logger.info(f"Stopped '{tool_id}'")
+            route = self._routes.get(tool_id, ToolRoute(tool_id=tool_id))
+            route.state = ProcessState.STOPPED
+            route.pid = None
+            route.port = None
+            self._routes[tool_id] = route
+            return route
+
+    def stop_all(self) -> int:
+        count = 0
+        for tool_id in list(self._popen_refs.keys()):
+            self.stop_tool(tool_id)
+            count += 1
+        return count
+
+    def get_status(self, tool_id: str) -> ToolRoute | None:
+        with self._lock:
+            proc = self._popen_refs.get(tool_id)
+            if proc and proc.poll() is not None:
+                route = self._routes.get(tool_id, ToolRoute(tool_id=tool_id))
+                route.state = ProcessState.ERROR
+                route.last_error = f"Process exited with code {proc.returncode}"
+                self._routes[tool_id] = route
+                del self._popen_refs[tool_id]
+        return self._routes.get(tool_id)
+
+    def get_active_endpoint(self, tool_id: str) -> dict | None:
+        """获取工具当前活跃的调用端点"""
+        route = self._routes.get(tool_id)
+        if not route or route.state != ProcessState.RUNNING or not route.sources:
+            return None
+        source = route.sources[route.active_source]
+        if source.type == SourceType.HUB:
+            return {
+                "type": "hub",
+                "url": f"{source.hub_url.rstrip('/')}{source.hub_tool_path}",
+                "http_method": source.http_method,
+                "api_key": source.hub_api_key,
+            }
+        else:
+            return {
+                "type": source.type.value,
+                "url": f"http://127.0.0.1:{route.port}{source.endpoint_path}",
+                "http_method": source.http_method,
+            }
+
+    def list_status(self) -> list[ToolRoute]:
+        self._sync_from_registry()
+        for tool_id in list(self._popen_refs.keys()):
+            self.get_status(tool_id)
+        return list(self._routes.values())
+
+    def _set_error(self, tool_id: str, error: str) -> ToolRoute:
+        route = self._routes.get(tool_id, ToolRoute(tool_id=tool_id))
+        route.state = ProcessState.ERROR
+        route.last_error = error
+        with self._lock:
+            self._routes[tool_id] = route
+        logger.error(f"Tool '{tool_id}' error: {error}")
+        return route

+ 0 - 0
src/tool_agent/runtime/__init__.py


binární
src/tool_agent/runtime/__pycache__/__init__.cpython-312.pyc


binární
src/tool_agent/runtime/__pycache__/docker_runner.cpython-312.pyc


binární
src/tool_agent/runtime/__pycache__/local_runner.cpython-312.pyc


binární
src/tool_agent/runtime/__pycache__/resource.cpython-312.pyc


+ 28 - 0
src/tool_agent/runtime/api_proxy.py

@@ -0,0 +1,28 @@
+"""外部 API 代理转发"""
+
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+import httpx
+
+from tool_agent.models import ToolMeta
+
+logger = logging.getLogger(__name__)
+
+
+class APIProxy:
+    """无状态代理,直接转发请求到第三方 API"""
+
+    async def run(self, tool: ToolMeta, params: dict[str, Any], stream: bool = False) -> dict[str, Any]:
+        """转发请求到外部 API"""
+        endpoint = tool.runtime.entry
+        headers = {k: v for k, v in tool.runtime.env.items() if k.startswith("HEADER_")}
+
+        try:
+            async with httpx.AsyncClient(timeout=30) as client:
+                resp = await client.post(endpoint, json=params, headers=headers)
+                return {"status": "success", "result": resp.json()}
+        except Exception as e:
+            return {"status": "error", "error": str(e)}

+ 605 - 0
src/tool_agent/runtime/docker_runner.py

@@ -0,0 +1,605 @@
+"""Docker 容器管理 — 以 sandbox_manager 为蓝本实现"""
+
+from __future__ import annotations
+
+import base64
+import json
+import logging
+import os
+import socket
+import threading
+import time
+import uuid
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Any, Callable
+
+import docker
+import docker.errors
+import docker.types
+import httpx
+
+from tool_agent.config import settings
+from tool_agent.models import ContainerInfo, ContainerStatus, ToolMeta
+
+logger = logging.getLogger(__name__)
+
+
+class ContainerStore:
+    """容器状态 JSON 持久化层 — 替代 sandbox_repository 的 MySQL"""
+
+    def __init__(self, path: Path | None = None) -> None:
+        self._path = path or (settings.data_dir / "containers.json")
+        self._lock = threading.Lock()
+
+    def _load(self) -> list[dict[str, Any]]:
+        if not self._path.exists():
+            return []
+        data = json.loads(self._path.read_text(encoding="utf-8"))
+        return data.get("containers", [])
+
+    def _save(self, containers: list[dict[str, Any]]) -> None:
+        self._path.parent.mkdir(parents=True, exist_ok=True)
+        self._path.write_text(
+            json.dumps({"containers": containers}, indent=2, ensure_ascii=False, default=str),
+            encoding="utf-8",
+        )
+
+    def create(self, info: ContainerInfo) -> None:
+        with self._lock:
+            containers = self._load()
+            containers.append(info.model_dump(mode="json"))
+            self._save(containers)
+
+    def get(self, container_id: str) -> ContainerInfo | None:
+        for item in self._load():
+            if item["container_id"] == container_id and item["status"] == ContainerStatus.RUNNING:
+                return ContainerInfo(**item)
+        return None
+
+    def get_all_active(self) -> list[ContainerInfo]:
+        return [
+            ContainerInfo(**item)
+            for item in self._load()
+            if item["status"] == ContainerStatus.RUNNING
+        ]
+
+    def update_last_accessed(self, container_id: str) -> None:
+        with self._lock:
+            containers = self._load()
+            for item in containers:
+                if item["container_id"] == container_id and item["status"] == ContainerStatus.RUNNING:
+                    item["last_accessed"] = datetime.now(timezone.utc).isoformat()
+                    break
+            self._save(containers)
+
+    def mark_destroyed(self, container_id: str) -> None:
+        with self._lock:
+            containers = self._load()
+            for item in containers:
+                if item["container_id"] == container_id and item["status"] == ContainerStatus.RUNNING:
+                    item["status"] = ContainerStatus.DESTROYED
+                    item["destroyed_at"] = datetime.now(timezone.utc).isoformat()
+                    break
+            self._save(containers)
+
+    def exists(self, container_id: str) -> bool:
+        return any(
+            item["container_id"] == container_id and item["status"] == ContainerStatus.RUNNING
+            for item in self._load()
+        )
+
+    def get_expired(self, ttl_seconds: int) -> list[ContainerInfo]:
+        now = datetime.now(timezone.utc)
+        expired = []
+        for item in self._load():
+            if item["status"] != ContainerStatus.RUNNING:
+                continue
+            last = item.get("last_accessed") or item.get("created_at")
+            if last:
+                last_dt = datetime.fromisoformat(last) if isinstance(last, str) else last
+                if last_dt.tzinfo is None:
+                    last_dt = last_dt.replace(tzinfo=timezone.utc)
+                if (now - last_dt).total_seconds() > ttl_seconds:
+                    expired.append(ContainerInfo(**item))
+        return expired
+
+    def count_active(self) -> int:
+        return sum(1 for item in self._load() if item["status"] == ContainerStatus.RUNNING)
+
+
+class DockerRunner:
+    """Docker 容器完整生命周期管理
+
+    以 sandbox_manager.py 为蓝本,提供:
+    - Docker 客户端懒加载
+    - 容器创建(端口映射、资源限制、GPU)
+    - 容器内命令执行(前台带超时 + 后台)
+    - HTTP 调用容器内工具服务
+    - 容器启停/销毁/重建
+    - 健康检查
+    - 线程安全容器缓存
+    - 启动时从 JSON 恢复
+    """
+
+    def __init__(self, lazy_init: bool = True) -> None:
+        self._docker_client: docker.DockerClient | None = None
+        self._container_cache: dict[str, docker.models.containers.Container] = {}
+        self._lock = threading.Lock()
+        self._on_destroy_callbacks: list[Callable[[str], None]] = []
+        self._store = ContainerStore()
+
+        if not lazy_init:
+            self._init_docker()
+
+    # ---- Docker 客户端 ----
+
+    @property
+    def client(self) -> docker.DockerClient:
+        if self._docker_client is None:
+            self._init_docker()
+        return self._docker_client
+
+    def _init_docker(self) -> None:
+        self._docker_client = docker.from_env()
+        self._ensure_base_image()
+        self._restore_container_cache()
+
+    def _ensure_base_image(self) -> None:
+        """检查基础镜像是否存在,不存在则从 Dockerfile 构建"""
+        image_name = settings.docker_base_image
+        try:
+            self.client.images.get(image_name)
+            logger.info(f"Base image '{image_name}' found locally.")
+        except docker.errors.ImageNotFound:
+            logger.info(f"Base image '{image_name}' not found. Building...")
+            dockerfile_dir = settings.tools_dir / "docker"
+            dockerfile_path = dockerfile_dir / "Dockerfile.sandbox"
+            if not dockerfile_path.exists():
+                logger.warning(f"Dockerfile '{dockerfile_path}' not found, skipping build.")
+                return
+            try:
+                image, build_logs = self.client.images.build(
+                    path=str(dockerfile_dir),
+                    dockerfile="Dockerfile.sandbox",
+                    tag=image_name,
+                    rm=True,
+                )
+                for chunk in build_logs:
+                    if "stream" in chunk:
+                        logger.debug(chunk["stream"].strip())
+                logger.info(f"Successfully built '{image_name}'.")
+            except Exception as e:
+                logger.error(f"Failed to build base image: {e}")
+
+    def _restore_container_cache(self) -> None:
+        """启动时从 JSON 恢复活跃容器到内存缓存"""
+        for info in self._store.get_all_active():
+            try:
+                container = self.client.containers.get(info.container_id)
+                if container.status == "running":
+                    with self._lock:
+                        self._container_cache[info.container_id] = container
+                    logger.info(f"Restored container cache: {info.container_id[:12]}")
+                else:
+                    self._store.mark_destroyed(info.container_id)
+                    logger.warning(f"Container stopped, marked destroyed: {info.container_id[:12]}")
+            except docker.errors.NotFound:
+                self._store.mark_destroyed(info.container_id)
+                logger.warning(f"Container not found, marked destroyed: {info.container_id[:12]}")
+
+    # ---- 回调 ----
+
+    def add_on_destroy_callback(self, callback: Callable[[str], None]) -> None:
+        self._on_destroy_callbacks.append(callback)
+
+    def _trigger_destroy_callbacks(self, container_id: str) -> None:
+        for cb in self._on_destroy_callbacks:
+            try:
+                cb(container_id)
+            except Exception as e:
+                logger.error(f"Destroy callback failed: {e}")
+
+    # ---- 端口 ----
+
+    @staticmethod
+    def _get_free_port() -> int:
+        """获取一个空闲的宿主机端口"""
+        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+            s.bind(("", 0))
+            return s.getsockname()[1]
+
+    # ---- 容器生命周期 ----
+
+    def create_container(
+        self,
+        tool_id: str = "",
+        image: str | None = None,
+        mem_limit: str | None = None,
+        nano_cpus: int | None = None,
+        ports: list[int] | None = None,
+        volumes: dict[str, str] | None = None,
+        use_gpu: bool = False,
+        gpu_count: int = -1,
+    ) -> dict[str, Any]:
+        """创建新容器
+
+        Args:
+            tool_id: 关联的工具 ID
+            image: 镜像名称,默认使用 settings.docker_base_image
+            mem_limit: 内存限制,如 "1g"
+            nano_cpus: CPU 限制,1_000_000_000 = 1 CPU
+            ports: 需要映射的容器端口列表,如 [8080, 3000]
+            volumes: 目录挂载,{宿主机路径: 容器路径},如 {"/home/user/project": "/app"}
+            use_gpu: 是否启用 GPU
+            gpu_count: GPU 数量,-1 表示全部
+        """
+        image = image or settings.docker_base_image
+        mem_limit = mem_limit or settings.docker_mem_limit
+        nano_cpus = nano_cpus or settings.docker_nano_cpus
+
+        # 容器数量上限检查
+        active_count = self._store.count_active()
+        if active_count >= settings.hot_tool_max_containers:
+            return {"error": f"Container limit reached ({settings.hot_tool_max_containers}). Destroy unused containers first."}
+
+        try:
+            # 端口映射
+            port_bindings: dict[str, int] = {}
+            port_mapping: dict[int, int] = {}
+            if ports:
+                for container_port in ports:
+                    host_port = self._get_free_port()
+                    port_bindings[f"{container_port}/tcp"] = host_port
+                    port_mapping[container_port] = host_port
+
+            # 目录挂载
+            docker_volumes = {}
+            if volumes:
+                for host_path, container_path in volumes.items():
+                    docker_volumes[host_path] = {"bind": container_path, "mode": "ro"}
+
+            # GPU 配置
+            device_requests = None
+            if use_gpu:
+                device_requests = [
+                    docker.types.DeviceRequest(count=gpu_count, capabilities=[["gpu"]])
+                ]
+
+            container = self.client.containers.run(
+                image,
+                command="tail -f /dev/null",
+                detach=True,
+                ports=port_bindings or None,
+                volumes=docker_volumes or None,
+                working_dir="/app",
+                mem_limit=mem_limit,
+                nano_cpus=nano_cpus,
+                device_requests=device_requests,
+                security_opt=["no-new-privileges"],
+            )
+
+            now = datetime.now(timezone.utc)
+            info = ContainerInfo(
+                container_id=container.id,
+                tool_id=tool_id,
+                image=image,
+                port_mapping=port_mapping,
+                volumes=volumes or {},
+                mem_limit=mem_limit,
+                nano_cpus=nano_cpus,
+                use_gpu=use_gpu,
+                gpu_count=gpu_count,
+                created_at=now,
+                last_accessed=now,
+            )
+            self._store.create(info)
+
+            with self._lock:
+                self._container_cache[container.id] = container
+
+            logger.info(f"Created container {container.id[:12]} for tool '{tool_id}', ports={port_mapping}")
+            return {
+                "container_id": container.id,
+                "tool_id": tool_id,
+                "port_mapping": port_mapping,
+                "message": "Container created.",
+            }
+        except Exception as e:
+            logger.error(f"Failed to create container: {e}")
+            return {"error": str(e)}
+
+    def _get_container(self, container_id: str) -> docker.models.containers.Container | None:
+        """从缓存或 Docker 获取容器对象"""
+        with self._lock:
+            container = self._container_cache.get(container_id)
+        if container:
+            return container
+
+        try:
+            container = self.client.containers.get(container_id)
+            with self._lock:
+                self._container_cache[container_id] = container
+            return container
+        except docker.errors.NotFound:
+            self._store.mark_destroyed(container_id)
+            return None
+
+    def run_command(
+        self,
+        container_id: str,
+        command: str,
+        background: bool = False,
+        timeout: int = 120,
+    ) -> dict[str, Any]:
+        """在容器内执行命令
+
+        Args:
+            container_id: 容器 ID
+            command: Shell 命令
+            background: 是否后台执行
+            timeout: 前台命令超时秒数
+        """
+        if not self._store.exists(container_id):
+            return {"error": "Container not found"}
+
+        self._store.update_last_accessed(container_id)
+        container = self._get_container(container_id)
+        if not container:
+            return {"error": "Container object not found"}
+
+        logger.info(f"Running in {container_id[:12]}: {command} (bg={background})")
+
+        try:
+            if background:
+                log_file = f"background_{uuid.uuid4().hex[:8]}.log"
+                encoded_cmd = base64.b64encode(command.encode()).decode()
+                safe_cmd = f"echo {encoded_cmd} | base64 -d | nohup sh > /app/{log_file} 2>&1 &"
+                container.exec_run(["sh", "-c", safe_cmd], detach=True)
+                return {
+                    "status": "success",
+                    "message": "Command started in background",
+                    "log_file": f"/app/{log_file}",
+                }
+            else:
+                result_box: dict[str, Any] = {}
+
+                def _exec():
+                    try:
+                        result_box["exec"] = container.exec_run(
+                            ["sh", "-c", command], demux=True,
+                        )
+                    except Exception as e:
+                        result_box["error"] = str(e)
+
+                thread = threading.Thread(target=_exec, daemon=True)
+                thread.start()
+                thread.join(timeout=timeout)
+
+                if thread.is_alive():
+                    return {"error": f"Command timeout after {timeout}s"}
+
+                if "error" in result_box:
+                    return {"error": result_box["error"]}
+
+                exec_result = result_box["exec"]
+                stdout, stderr = exec_result.output
+                return {
+                    "exit_code": exec_result.exit_code,
+                    "stdout": stdout.decode("utf-8", errors="replace") if stdout else "",
+                    "stderr": stderr.decode("utf-8", errors="replace") if stderr else "",
+                }
+        except Exception as e:
+            return {"error": str(e)}
+
+    def destroy_container(self, container_id: str) -> dict[str, Any]:
+        """销毁容器并释放资源"""
+        if not self._store.exists(container_id):
+            return {"error": "Container not found"}
+
+        with self._lock:
+            container = self._container_cache.pop(container_id, None)
+
+        if not container:
+            try:
+                container = self.client.containers.get(container_id)
+            except docker.errors.NotFound:
+                self._store.mark_destroyed(container_id)
+                return {"error": "Container not found in Docker"}
+
+        self._store.mark_destroyed(container_id)
+        self._trigger_destroy_callbacks(container_id)
+
+        try:
+            container.remove(force=True)
+            logger.info(f"Destroyed container {container_id[:12]}")
+            return {"status": "success", "message": f"Container {container_id[:12]} destroyed"}
+        except Exception as e:
+            return {"error": str(e)}
+
+    def rebuild_with_ports(
+        self,
+        container_id: str,
+        ports: list[int],
+        mem_limit: str | None = None,
+        nano_cpus: int | None = None,
+        use_gpu: bool = False,
+        gpu_count: int = -1,
+    ) -> dict[str, Any]:
+        """重建容器并应用新端口映射,保留文件系统状态
+
+        通过 commit → 重建 → 清理临时镜像实现。
+        """
+        container = self._get_container(container_id)
+        if not container:
+            return {"error": "Container not found"}
+
+        info = self._store.get(container_id)
+        mem_limit = mem_limit or (info.mem_limit if info else settings.docker_mem_limit)
+        nano_cpus = nano_cpus or (info.nano_cpus if info else settings.docker_nano_cpus)
+        tool_id = info.tool_id if info else ""
+        old_volumes = info.volumes if info else {}
+
+        try:
+            # 1. commit 当前容器为临时镜像
+            temp_tag = f"sandbox-temp-{uuid.uuid4().hex[:8]}"
+            logger.info(f"Committing {container_id[:12]} → {temp_tag}")
+            container.commit(repository=temp_tag)
+
+            # 2. 新端口映射
+            port_bindings: dict[str, int] = {}
+            port_mapping: dict[int, int] = {}
+            for p in ports:
+                hp = self._get_free_port()
+                port_bindings[f"{p}/tcp"] = hp
+                port_mapping[p] = hp
+
+            # 恢复目录挂载
+            docker_volumes = {}
+            if old_volumes:
+                for host_path, container_path in old_volumes.items():
+                    docker_volumes[host_path] = {"bind": container_path, "mode": "ro"}
+
+            device_requests = None
+            if use_gpu:
+                device_requests = [
+                    docker.types.DeviceRequest(count=gpu_count, capabilities=[["gpu"]])
+                ]
+
+            # 3. 创建新容器
+            new_container = self.client.containers.run(
+                temp_tag,
+                command="tail -f /dev/null",
+                detach=True,
+                ports=port_bindings or None,
+                volumes=docker_volumes or None,
+                working_dir="/app",
+                mem_limit=mem_limit,
+                nano_cpus=nano_cpus,
+                device_requests=device_requests,
+                security_opt=["no-new-privileges"],
+            )
+
+            # 4. 清理旧容器
+            self._store.mark_destroyed(container_id)
+            with self._lock:
+                self._container_cache.pop(container_id, None)
+            container.remove(force=True)
+
+            # 5. 清理临时镜像
+            try:
+                self.client.images.remove(temp_tag, force=True)
+            except Exception as e:
+                logger.warning(f"Failed to remove temp image: {e}")
+
+            # 6. 保存新容器
+            now = datetime.now(timezone.utc)
+            new_info = ContainerInfo(
+                container_id=new_container.id,
+                tool_id=tool_id,
+                image=info.image if info else settings.docker_base_image,
+                port_mapping=port_mapping,
+                volumes=old_volumes,
+                mem_limit=mem_limit,
+                nano_cpus=nano_cpus,
+                use_gpu=use_gpu,
+                gpu_count=gpu_count,
+                created_at=now,
+                last_accessed=now,
+            )
+            self._store.create(new_info)
+            with self._lock:
+                self._container_cache[new_container.id] = new_container
+
+            logger.info(f"Rebuilt {container_id[:12]} → {new_container.id[:12]}, ports={port_mapping}")
+            return {
+                "old_container_id": container_id,
+                "new_container_id": new_container.id,
+                "port_mapping": port_mapping,
+                "message": "Container rebuilt with new port mappings. All files preserved.",
+            }
+        except Exception as e:
+            logger.error(f"Failed to rebuild container: {e}")
+            return {"error": str(e)}
+
+    def start_container(self, container_id: str) -> bool:
+        """启动已停止的容器"""
+        container = self._get_container(container_id)
+        if not container:
+            return False
+        try:
+            container.start()
+            logger.info(f"Started container {container_id[:12]}")
+            return True
+        except Exception as e:
+            logger.error(f"Failed to start container: {e}")
+            return False
+
+    def stop_container(self, container_id: str) -> bool:
+        """停止运行中的容器"""
+        container = self._get_container(container_id)
+        if not container:
+            return False
+        try:
+            container.stop(timeout=10)
+            logger.info(f"Stopped container {container_id[:12]}")
+            return True
+        except Exception as e:
+            logger.error(f"Failed to stop container: {e}")
+            return False
+
+    # ---- 工具调用 (HTTP) ----
+
+    async def run(self, tool: ToolMeta, params: dict[str, Any], stream: bool = False) -> dict[str, Any]:
+        """通过 HTTP 调用容器内工具服务"""
+        # 从注册表中找到该工具对应的容器端口
+        for info in self._store.get_all_active():
+            if info.tool_id == tool.tool_id and info.port_mapping:
+                # 取第一个映射端口作为服务端口
+                host_port = next(iter(info.port_mapping.values()))
+                self._store.update_last_accessed(info.container_id)
+                url = f"http://localhost:{host_port}/run"
+                payload = {"params": params, "stream": stream}
+                try:
+                    async with httpx.AsyncClient(timeout=60) as client:
+                        resp = await client.post(url, json=payload)
+                        return resp.json()
+                except Exception as e:
+                    return {"status": "error", "error": str(e)}
+
+        return {"status": "error", "error": f"No running container for tool '{tool.tool_id}'"}
+
+    async def health_check(self, container_id: str) -> bool:
+        """HTTP 健康检查"""
+        info = self._store.get(container_id)
+        if not info or not info.port_mapping:
+            return False
+
+        host_port = next(iter(info.port_mapping.values()))
+        try:
+            async with httpx.AsyncClient(timeout=5) as client:
+                resp = await client.get(f"http://localhost:{host_port}/health")
+                return resp.status_code == 200
+        except Exception:
+            return False
+
+    # ---- 自动清理 ----
+
+    def cleanup_expired(self) -> list[str]:
+        """清理超过 TTL 的容器,返回被清理的 container_id 列表"""
+        expired = self._store.get_expired(settings.docker_ttl_seconds)
+        cleaned = []
+        for info in expired:
+            logger.info(f"Auto-cleaning expired container {info.container_id[:12]}")
+            result = self.destroy_container(info.container_id)
+            if "error" not in result:
+                cleaned.append(info.container_id)
+        return cleaned
+
+    # ---- 查询 ----
+
+    def list_active(self) -> list[ContainerInfo]:
+        return self._store.get_all_active()
+
+    def get_container_info(self, container_id: str) -> ContainerInfo | None:
+        return self._store.get(container_id)

+ 366 - 0
src/tool_agent/runtime/local_runner.py

@@ -0,0 +1,366 @@
+"""uv 本地环境管理 — 创建/运行/管理 uv 虚拟环境"""
+
+from __future__ import annotations
+
+import json
+import logging
+import subprocess
+from pathlib import Path
+from typing import Any
+
+from tool_agent.config import settings
+from tool_agent.models import ToolMeta
+
+logger = logging.getLogger(__name__)
+
+
+class LocalRunner:
+    """uv 本地工具运行器 + 环境管理"""
+
+    def __init__(self) -> None:
+        self._background_procs: list[subprocess.Popen] = []
+
+    def cleanup_background(self) -> int:
+        """杀掉所有后台进程,返回清理数量"""
+        killed = 0
+        for proc in self._background_procs:
+            try:
+                proc.kill()
+                proc.wait(timeout=5)
+                killed += 1
+            except Exception:
+                pass
+        self._background_procs.clear()
+        if killed:
+            logger.info(f"Cleaned up {killed} background processes")
+        return killed
+
+    # ---- 环境创建与管理 ----
+
+    def create_project(
+        self,
+        name: str,
+        path: Path | None = None,
+        python_version: str = "3.12",
+    ) -> dict[str, Any]:
+        """创建新的 uv 项目
+
+        Args:
+            name: 项目名称
+            path: 项目路径,默认在 tools/local/{name}
+            python_version: Python 版本,默认 3.12
+        """
+        project_dir = path or (settings.tools_dir / "local" / name)
+        project_dir.mkdir(parents=True, exist_ok=True)
+
+        try:
+            # uv init 创建项目
+            result = subprocess.run(
+                ["uv", "init", "--name", name, "--python", python_version],
+                cwd=str(project_dir),
+                capture_output=True,
+                text=True,
+                timeout=60,
+            )
+            if result.returncode != 0:
+                return {"status": "error", "error": result.stderr, "stdout": result.stdout}
+
+            # 清理 uv init 自动创建的 git 仓库
+            import shutil
+            git_dir = project_dir / ".git"
+            gitignore = project_dir / ".gitignore"
+            if git_dir.exists():
+                shutil.rmtree(git_dir)
+            if gitignore.exists():
+                gitignore.unlink()
+
+            logger.info(f"Created uv project: {project_dir}")
+            return {
+                "status": "success",
+                "project_dir": str(project_dir),
+                "message": f"Project '{name}' created with Python {python_version}",
+            }
+        except Exception as e:
+            return {"status": "error", "error": str(e)}
+
+    def create_venv(self, project_dir: Path, python_version: str = "3.12") -> dict[str, Any]:
+        """在指定目录创建 venv
+
+        Args:
+            project_dir: 项目目录
+            python_version: Python 版本
+        """
+        try:
+            result = subprocess.run(
+                ["uv", "venv", "--python", python_version],
+                cwd=str(project_dir),
+                capture_output=True,
+                text=True,
+                timeout=120,
+            )
+            if result.returncode != 0:
+                return {"status": "error", "error": result.stderr}
+
+            logger.info(f"Created venv in: {project_dir}")
+            return {
+                "status": "success",
+                "venv_path": str(project_dir / ".venv"),
+                "message": f"Venv created with Python {python_version}",
+            }
+        except Exception as e:
+            return {"status": "error", "error": str(e)}
+
+    def add_dependency(
+        self,
+        project_dir: Path,
+        package: str,
+        dev: bool = False,
+    ) -> dict[str, Any]:
+        """添加依赖
+
+        Args:
+            project_dir: 项目目录
+            package: 包名,如 "flask" 或 "flask>=2.0"
+            dev: 是否为开发依赖
+        """
+        cmd = ["uv", "add", package]
+        if dev:
+            cmd.append("--dev")
+
+        try:
+            result = subprocess.run(
+                cmd,
+                cwd=str(project_dir),
+                capture_output=True,
+                text=True,
+                timeout=120,
+            )
+            if result.returncode != 0:
+                return {"status": "error", "error": result.stderr, "stdout": result.stdout}
+
+            logger.info(f"Added dependency '{package}' to {project_dir}")
+            return {
+                "status": "success",
+                "package": package,
+                "message": f"Added {'dev ' if dev else ''}dependency: {package}",
+            }
+        except Exception as e:
+            return {"status": "error", "error": str(e)}
+
+    def remove_dependency(self, project_dir: Path, package: str) -> dict[str, Any]:
+        """移除依赖"""
+        try:
+            result = subprocess.run(
+                ["uv", "remove", package],
+                cwd=str(project_dir),
+                capture_output=True,
+                text=True,
+                timeout=60,
+            )
+            if result.returncode != 0:
+                return {"status": "error", "error": result.stderr}
+
+            return {"status": "success", "message": f"Removed dependency: {package}"}
+        except Exception as e:
+            return {"status": "error", "error": str(e)}
+
+    def sync_dependencies(self, project_dir: Path) -> dict[str, Any]:
+        """同步依赖(uv sync)"""
+        try:
+            result = subprocess.run(
+                ["uv", "sync"],
+                cwd=str(project_dir),
+                capture_output=True,
+                text=True,
+                timeout=300,
+            )
+            if result.returncode != 0:
+                return {"status": "error", "error": result.stderr}
+
+            return {"status": "success", "message": "Dependencies synced", "output": result.stdout}
+        except Exception as e:
+            return {"status": "error", "error": str(e)}
+
+    def run_command(
+        self,
+        project_dir: Path,
+        command: str,
+        timeout: int = 120,
+        background: bool = False,
+        log_file: str = "last_run.log"
+    ) -> dict[str, Any]:
+        """在项目环境中运行命令,并同步写入日志文件"""
+        # 日志统一写到 tests/ 目录
+        tests_dir = project_dir / "tests"
+        tests_dir.mkdir(parents=True, exist_ok=True)
+        log_path = tests_dir / log_file
+        try:
+            # 拼接 uv run 命令
+            full_cmd = ["uv", "run", "--directory", str(project_dir)] + command.split()
+
+            if background:
+                bg_log = tests_dir / f"background_{command.split()[0]}.log"
+                with open(bg_log, "w") as f:
+                    proc = subprocess.Popen(
+                        full_cmd,
+                        stdout=f,
+                        stderr=subprocess.STDOUT,
+                        text=True,
+                    )
+                self._background_procs.append(proc)
+                return {
+                    "status": "success",
+                    "message": f"Command started in background (PID {proc.pid})",
+                    "pid": proc.pid,
+                    "log_file": str(bg_log),
+                }
+
+            # 使用 subprocess.run 并捕获输出
+            result = subprocess.run(
+                full_cmd,
+                capture_output=True,
+                text=True,
+                timeout=timeout,
+            )
+
+            # --- 持久化日志 ---
+            with open(log_path, "w", encoding="utf-8") as f:
+                f.write(f"Command: {command}\n")
+                f.write(f"Exit Code: {result.returncode}\n")
+                f.write("--- STDOUT ---\n")
+                f.write(result.stdout)
+                f.write("\n--- STDERR ---\n")
+                f.write(result.stderr)
+
+            return {
+                "exit_code": result.returncode,
+                "stdout": result.stdout,
+                "stderr": result.stderr,
+                "log_path": str(log_path)
+            }
+        except subprocess.TimeoutExpired as e:
+            # 超时也记录已捕获的部分输出
+            return {"status": "error", "error": f"Timeout", "stdout": e.stdout, "stderr": e.stderr}
+        
+
+    def run_python(
+        self,
+        project_dir: Path,
+        script: str,
+        timeout: int = 120,
+    ) -> dict[str, Any]:
+        """在项目环境中运行 Python 脚本
+
+        Args:
+            project_dir: 项目目录
+            script: Python 脚本路径(相对于项目目录)
+            timeout: 超时秒数
+        """
+        return self.run_command(project_dir, f"python {script}", timeout)
+
+    def run_code(
+        self,
+        project_dir: Path,
+        code: str,
+        timeout: int = 60,
+    ) -> dict[str, Any]:
+        """在项目环境中运行 Python 代码片段
+
+        Args:
+            project_dir: 项目目录
+            code: Python 代码
+            timeout: 超时秒数
+        """
+        try:
+            result = subprocess.run(
+                ["uv", "run", "--directory", str(project_dir), "python", "-c", code],
+                capture_output=True,
+                text=True,
+                timeout=timeout,
+            )
+            return {
+                "exit_code": result.returncode,
+                "stdout": result.stdout,
+                "stderr": result.stderr,
+            }
+        except subprocess.TimeoutExpired:
+            return {"status": "error", "error": f"Code timeout after {timeout}s"}
+        except Exception as e:
+            return {"status": "error", "error": str(e)}
+
+    def list_dependencies(self, project_dir: Path) -> dict[str, Any]:
+        """列出项目依赖"""
+        try:
+            result = subprocess.run(
+                ["uv", "pip", "list", "--format", "json"],
+                cwd=str(project_dir),
+                capture_output=True,
+                text=True,
+                timeout=30,
+            )
+            if result.returncode != 0:
+                return {"status": "error", "error": result.stderr}
+
+            packages = json.loads(result.stdout) if result.stdout else []
+            return {"status": "success", "packages": packages}
+        except Exception as e:
+            return {"status": "error", "error": str(e)}
+
+    def get_python_version(self, project_dir: Path) -> dict[str, Any]:
+        """获取项目 Python 版本"""
+        result = self.run_code(project_dir, "import sys; print(sys.version)")
+        if result.get("exit_code") == 0:
+            return {"status": "success", "version": result["stdout"].strip()}
+        return {"status": "error", "error": result.get("stderr", result.get("error"))}
+
+    # ---- 工具调用(原有功能) ----
+
+    async def run(self, tool: ToolMeta, params: dict[str, Any], stream: bool = False) -> dict[str, Any]:
+        """调用本地工具(通过 stdio JSON 协议)"""
+        tool_dir = settings.tools_dir / "local" / tool.tool_id
+        request = json.dumps({"action": "run", "params": params, "stream": stream})
+
+        try:
+            result = subprocess.run(
+                ["uv", "run", "--directory", str(tool_dir), "python", "main.py"],
+                input=request,
+                capture_output=True,
+                text=True,
+                timeout=60,
+            )
+            if result.returncode != 0:
+                return {"status": "error", "error": result.stderr}
+            return json.loads(result.stdout)
+        except subprocess.TimeoutExpired:
+            return {"status": "error", "error": "timeout"}
+        except Exception as e:
+            return {"status": "error", "error": str(e)}
+
+    async def health_check(self, tool: ToolMeta) -> bool:
+        """工具健康检查"""
+        tool_dir = settings.tools_dir / "local" / tool.tool_id
+        request = json.dumps({"action": "health"})
+        try:
+            result = subprocess.run(
+                ["uv", "run", "--directory", str(tool_dir), "python", "main.py"],
+                input=request,
+                capture_output=True,
+                text=True,
+                timeout=10,
+            )
+            data = json.loads(result.stdout)
+            return data.get("healthy", False)
+        except Exception:
+            return False
+        
+    def read_logs(self, project_dir: Path, log_file: str = "last_run.log") -> dict[str, Any]:
+        """读取项目内的日志文件"""
+        log_path = project_dir / log_file
+        if not log_path.exists():
+            return {"status": "error", "error": "Log file not found"}
+        
+        try:
+            content = log_path.read_text(encoding="utf-8")
+            return {"status": "success", "content": content}
+        except Exception as e:
+            return {"status": "error", "error": str(e)}

+ 109 - 0
src/tool_agent/runtime/resource.py

@@ -0,0 +1,109 @@
+"""资源配额管理 — 基于 psutil 的真实资源检查"""
+
+from __future__ import annotations
+
+import logging
+import shutil
+from dataclasses import dataclass, field
+
+import psutil
+
+from tool_agent.config import settings
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class ResourceUsage:
+    cpu_cores: float = 0.0
+    memory_mb: float = 0.0
+    gpu_memory_mb: float = 0.0
+
+
+@dataclass
+class SystemInfo:
+    """系统资源概览"""
+    cpu_count: int = 0
+    cpu_percent: float = 0.0
+    memory_total_mb: float = 0.0
+    memory_available_mb: float = 0.0
+    memory_percent: float = 0.0
+    disk_total_gb: float = 0.0
+    disk_free_gb: float = 0.0
+
+
+class ResourceManager:
+    """管理工具的 CPU/内存/显存配额,基于 psutil 做真实资源检查"""
+
+    def __init__(self) -> None:
+        self._allocated: dict[str, ResourceUsage] = {}
+
+    @staticmethod
+    def get_system_info() -> SystemInfo:
+        """获取当前系统资源概览"""
+        mem = psutil.virtual_memory()
+        disk = shutil.disk_usage("/")
+        return SystemInfo(
+            cpu_count=psutil.cpu_count() or 1,
+            cpu_percent=psutil.cpu_percent(interval=0.1),
+            memory_total_mb=mem.total / (1024 * 1024),
+            memory_available_mb=mem.available / (1024 * 1024),
+            memory_percent=mem.percent,
+            disk_total_gb=disk.total / (1024 ** 3),
+            disk_free_gb=disk.free / (1024 ** 3),
+        )
+
+    def get_allocated_total(self) -> ResourceUsage:
+        """已分配资源总计"""
+        total = ResourceUsage()
+        for usage in self._allocated.values():
+            total.cpu_cores += usage.cpu_cores
+            total.memory_mb += usage.memory_mb
+            total.gpu_memory_mb += usage.gpu_memory_mb
+        return total
+
+    def check_available(self, cpu_cores: float, memory_mb: int, gpu: bool = False) -> bool:
+        """检查是否有足够资源启动新工具
+
+        比较已分配资源 + 新请求 与 系统可用资源。
+        预留 20% 系统内存作为安全余量。
+        """
+        sys_info = self.get_system_info()
+        allocated = self.get_allocated_total()
+
+        # CPU 检查:已分配 + 新请求 不超过总核数
+        if allocated.cpu_cores + cpu_cores > sys_info.cpu_count:
+            logger.warning(
+                f"CPU insufficient: allocated={allocated.cpu_cores}, "
+                f"requested={cpu_cores}, total={sys_info.cpu_count}"
+            )
+            return False
+
+        # 内存检查:已分配 + 新请求 不超过总内存的 80%
+        safe_memory = sys_info.memory_total_mb * 0.8
+        if allocated.memory_mb + memory_mb > safe_memory:
+            logger.warning(
+                f"Memory insufficient: allocated={allocated.memory_mb}MB, "
+                f"requested={memory_mb}MB, safe_limit={safe_memory:.0f}MB"
+            )
+            return False
+
+        return True
+
+    def allocate(self, tool_id: str, cpu_cores: float, memory_mb: float, gpu_memory_mb: float = 0.0) -> None:
+        """登记资源分配"""
+        self._allocated[tool_id] = ResourceUsage(
+            cpu_cores=cpu_cores,
+            memory_mb=memory_mb,
+            gpu_memory_mb=gpu_memory_mb,
+        )
+        logger.info(f"Allocated for '{tool_id}': cpu={cpu_cores}, mem={memory_mb}MB")
+
+    def release(self, tool_id: str) -> None:
+        """释放资源"""
+        if tool_id in self._allocated:
+            usage = self._allocated.pop(tool_id)
+            logger.info(f"Released for '{tool_id}': cpu={usage.cpu_cores}, mem={usage.memory_mb}MB")
+
+    def list_allocations(self) -> dict[str, ResourceUsage]:
+        return dict(self._allocated)

+ 0 - 0
src/tool_agent/tool/__init__.py


binární
src/tool_agent/tool/__pycache__/__init__.cpython-312.pyc


binární
src/tool_agent/tool/__pycache__/agent.cpython-312.pyc


+ 726 - 0
src/tool_agent/tool/agent.py

@@ -0,0 +1,726 @@
+"""Coding Agent — 基于 claude_agent_sdk,深度集成 DockerRunner 和 LocalRunner
+
+接收 Router Agent 的任务书(task_spec),在 Docker 或本地 uv 环境中
+编写代码、配置环境、封装接口,完成后注册到 registry 并回报 Router。
+"""
+
+from __future__ import annotations
+
+import asyncio
+import json
+import logging
+import uuid
+from pathlib import Path
+from typing import Any, TYPE_CHECKING
+
+from dotenv import load_dotenv
+
+from claude_agent_sdk import (
+    AssistantMessage, UserMessage, SystemMessage, ResultMessage,
+    TextBlock, ClaudeAgentOptions, tool,
+    create_sdk_mcp_server, ClaudeSDKClient, ThinkingBlock, ToolUseBlock,
+)
+
+from tool_agent.config import settings
+from tool_agent.models import (
+    AgentMessage, MessageType, ToolMeta, ToolStatus,
+)
+from tool_agent.runtime.docker_runner import DockerRunner
+from tool_agent.runtime.local_runner import LocalRunner
+from tool_agent.registry.registry import ToolRegistry
+
+if TYPE_CHECKING:
+    from tool_agent.messaging import MessageBus
+
+load_dotenv(override=True)
+
+logger = logging.getLogger(__name__)
+
+# ---- 全局运行时实例(所有 @tool 函数共享) ----
+_docker_runner = DockerRunner(lazy_init=True)
+_local_runner = LocalRunner()
+_registry = ToolRegistry()
+
+
+# ===========================================================================
+#  Docker 环境工具
+# ===========================================================================
+
+@tool(name="create_docker_env", description="""
+创建一个隔离的 Docker 开发环境。
+
+Args:
+    image: Docker 镜像,如 python:3.12-slim, node:18-slim, ubuntu:22.04
+    mem_limit: 内存限制,默认 1g
+    nano_cpus: CPU 限制,默认 1000000000 (1 CPU)
+    ports: 需要映射的端口列表,如 [8080, 3000]
+    volumes: 目录挂载,{宿主机绝对路径: 容器路径}。用于将宿���机 staging 目录挂载到容器,实现文件实时同步。
+    use_gpu: 是否启用 GPU,默认 False
+
+Returns:
+    container_id, port_mapping 等信息。后续操作需要用 container_id。
+""", input_schema={
+    "type": "object",
+    "properties": {
+        "image": {"type": "string", "description": "Docker 镜像名称,如 python:3.12-slim"},
+        "mem_limit": {"type": "string", "description": "内存限制,默认 1g"},
+        "nano_cpus": {"type": "integer", "description": "CPU 限制,默认 1000000000 (1CPU)"},
+        "ports": {"type": "array", "items": {"type": "integer"}, "description": "端口列表,如 [8080]"},
+        "volumes": {"type": "object", "description": "目录挂载 {宿主机路径: 容器路径}"},
+        "use_gpu": {"type": "boolean", "description": "是否启用 GPU,默认 False"},
+    },
+    "required": ["image"],
+})
+async def create_docker_env(args):
+    result = _docker_runner.create_container(
+        image=args.get("image"),
+        mem_limit=args.get("mem_limit", "1g"),
+        nano_cpus=args.get("nano_cpus", 1_000_000_000),
+        ports=args.get("ports"),
+        volumes=args.get("volumes"),
+        use_gpu=args.get("use_gpu", False),
+        gpu_count=args.get("gpu_count", -1),
+    )
+    return _text_result(result)
+
+
+@tool(name="run_in_docker", description="""
+在 Docker 容器中执行 Shell 命令。支持前台阻塞执行和后台执行。
+
+Args:
+    container_id: 容器 ID,由 create_docker_env 返回
+    command: Shell 命令,如 "pip install flask", "python app.py", "git clone ..."
+    is_background: 是否后台执行(适合启动长期服务),默认 False
+    timeout: 前台命令超时秒数,默认 120
+
+Returns:
+    前台: exit_code, stdout, stderr
+    后台: status, log_file
+""", input_schema={
+    "type": "object",
+    "properties": {
+        "container_id": {"type": "string", "description": "容器 ID"},
+        "command": {"type": "string", "description": "Shell 命令"},
+        "is_background": {"type": "boolean", "description": "是否后台执行,默认 False"},
+        "timeout": {"type": "integer", "description": "超时秒数,默认 120"},
+    },
+    "required": ["container_id", "command"],
+})
+async def run_in_docker(args):
+    result = _docker_runner.run_command(
+        container_id=args["container_id"],
+        command=args["command"],
+        background=args.get("is_background", False),
+        timeout=args.get("timeout", 120),
+    )
+    return _text_result(result)
+
+
+@tool(name="rebuild_docker_ports", description="""
+重建容器并应用新端口映射,保留容器内所有文件。
+
+使用场景:先 clone 项目,读完 README 后才知道需要暴露哪些端口。
+重建后返回新 container_id,旧 ID 失效,后续操作必须使用新 ID。
+
+Args:
+    container_id: 当前容器 ID
+    ports: 新端口列表,如 [8080, 3306]
+""", input_schema={
+    "type": "object",
+    "properties": {
+        "container_id": {"type": "string", "description": "当前容器 ID"},
+        "ports": {"type": "array", "items": {"type": "integer"}, "description": "新端口列表"},
+    },
+    "required": ["container_id", "ports"],
+})
+async def rebuild_docker_ports(args):
+    result = _docker_runner.rebuild_with_ports(
+        container_id=args["container_id"],
+        ports=args["ports"],
+        mem_limit=args.get("mem_limit"),
+        nano_cpus=args.get("nano_cpus"),
+    )
+    return _text_result(result)
+
+
+@tool(name="destroy_docker_env", description="""
+销毁 Docker 容器,释放资源。
+
+Args:
+    container_id: 容器 ID
+""", input_schema={
+    "type": "object",
+    "properties": {
+        "container_id": {"type": "string", "description": "容器 ID"},
+    },
+    "required": ["container_id"],
+})
+async def destroy_docker_env(args):
+    result = _docker_runner.destroy_container(args["container_id"])
+    return _text_result(result)
+
+
+# ===========================================================================
+#  本地 uv 环境工具
+# ===========================================================================
+
+@tool(name="create_uv_project", description="""
+在本地创建 uv 项目(隔离虚拟环境)。项目创建在 tools/local/{name} 目录下。
+
+Args:
+    name: 项目名称
+    python_version: Python 版本,默认 3.12
+
+Returns:
+    project_dir 路径
+""", input_schema={
+    "type": "object",
+    "properties": {
+        "name": {"type": "string", "description": "项目名称"},
+        "python_version": {"type": "string", "description": "Python 版本,默认 3.12"},
+    },
+    "required": ["name"],
+})
+async def create_uv_project(args):
+    result = _local_runner.create_project(
+        name=args["name"],
+        python_version=args.get("python_version", "3.12"),
+    )
+    return _text_result(result)
+
+
+@tool(name="run_in_uv", description="""
+在 uv 项目环境中执行命令(uv run)。支持前台阻塞执行和后台执行。
+
+Args:
+    project_dir: 项目目录绝对路径
+    command: 要执行的命令,如 "python main.py", "pytest"
+    is_background: 是否后台执行(适合启动 HTTP 服务),默认 False
+    timeout: 前台命令超时秒数,默认 120
+
+Returns:
+    前台: exit_code, stdout, stderr
+    后台: pid, log_file
+""", input_schema={
+    "type": "object",
+    "properties": {
+        "project_dir": {"type": "string", "description": "项目目录绝对路径"},
+        "command": {"type": "string", "description": "要执行的命令"},
+        "is_background": {"type": "boolean", "description": "是否后台执行,默认 False"},
+        "timeout": {"type": "integer", "description": "超时秒数,默认 120"},
+    },
+    "required": ["project_dir", "command"],
+})
+async def run_in_uv(args):
+    result = _local_runner.run_command(
+        project_dir=Path(args["project_dir"]),
+        command=args["command"],
+        background=args.get("is_background", False),
+        timeout=args.get("timeout", 120),
+    )
+    
+    # 构造给 Agent 的反馈
+    feedback = f"Exit Code: {result.get('exit_code')}\n"
+    if result.get("exit_code") != 0:
+        feedback += f"Stderr: {result.get('stderr')}\n"
+        feedback += f"完整日志已写入: {result.get('log_path')},你可以使用 read_file 查看。"
+    else:
+        feedback += f"Stdout: {result.get('stdout')}"
+        
+    return {"content": [{"type": "text", "text": feedback}]}
+
+
+@tool(name="uv_add_dependency", description="""
+为 uv 项目添加依赖(uv add)。
+
+Args:
+    project_dir: 项目目录绝对路径
+    package: 包名,如 "flask" 或 "flask>=2.0"
+    dev: 是否为开发依赖,默认 False
+""", input_schema={
+    "type": "object",
+    "properties": {
+        "project_dir": {"type": "string", "description": "项目目录"},
+        "package": {"type": "string", "description": "包名"},
+        "dev": {"type": "boolean", "description": "是否为开发依赖"},
+    },
+    "required": ["project_dir", "package"],
+})
+async def uv_add_dependency(args):
+    result = _local_runner.add_dependency(
+        project_dir=Path(args["project_dir"]),
+        package=args["package"],
+        dev=args.get("dev", False),
+    )
+    return _text_result(result)
+
+
+# ===========================================================================
+#  文件读写工具(宿主机文件操作,配合 volume 挂载使用)
+# ===========================================================================
+
+@tool(name="write_file", description="""
+将内容写入宿主机文件。用于编写工具代码、API 包装器、配置文件等。
+配合 Docker volume 挂载,写入宿主机的文件会实时同步到容器内。
+
+Args:
+    path: 文件绝对路径
+    content: 文件内容
+""", input_schema={
+    "type": "object",
+    "properties": {
+        "path": {"type": "string", "description": "文件绝对路径"},
+        "content": {"type": "string", "description": "文件内容"},
+    },
+    "required": ["path", "content"],
+})
+async def write_file(args):
+    try:
+        p = _validate_path(args["path"])
+        if p is None:
+            return _text_result({"status": "error", "error": f"Path not allowed: {args['path']}. Must be under data/ or tools/."})
+        p.parent.mkdir(parents=True, exist_ok=True)
+        p.write_text(args["content"], encoding="utf-8")
+        result = {"status": "success", "path": str(p), "size": len(args["content"])}
+    except Exception as e:
+        result = {"status": "error", "error": str(e)}
+    return _text_result(result)
+
+
+@tool(name="read_file", description="""
+读取宿主机文件内容。用于阅读 README、配置文件、代码等。
+
+Args:
+    path: 文件绝对路径
+""", input_schema={
+    "type": "object",
+    "properties": {
+        "path": {"type": "string", "description": "文件绝对路径"},
+    },
+    "required": ["path"],
+})
+async def read_file(args):
+    try:
+        p = _validate_path(args["path"])
+        if p is None:
+            return _text_result({"status": "error", "error": f"Path not allowed: {args['path']}. Must be under data/ or tools/."})
+        if not p.exists():
+            result = {"status": "error", "error": f"File not found: {p}"}
+        else:
+            content = p.read_text(encoding="utf-8", errors="replace")
+            result = {"status": "success", "path": str(p), "content": content}
+    except Exception as e:
+        result = {"status": "error", "error": str(e)}
+    return _text_result(result)
+
+
+# ===========================================================================
+#  工具注册
+# ===========================================================================
+
+@tool(name="register_tool", description="""
+将部署好的服务注册到工具库,注册后可被外部 Agent 通过 Router 调用。
+
+Args:
+    tool_id: 工具唯一标识(小写字母+下划线),如 "flask_demo_api"
+    name: 工具显示名称
+    description: 工具功能描述
+    category: 分类标签,如 "nlp", "cv", "devtools"
+    input_schema: JSON Schema 格式的输入参数定义
+    output_schema: JSON Schema 格式的输出定义
+    stream_support: 是否支持流式返回,默认 False
+    runtime_type: "local" 或 "docker"
+    container_id: Docker 容器 ID(docker 类型必填)
+    host_dir: 宿主机工作目录(代码所在目录,local 类型必填)
+    internal_port: 容器/进程内服务端口
+    endpoint_path: HTTP API 路径,如 "/api/render",默认 "/"
+    http_method: HTTP 方法,默认 "POST"
+
+Returns:
+    注册结果
+""", input_schema={
+    "type": "object",
+    "properties": {
+        "tool_id": {"type": "string", "description": "工具 ID(小写字母+下划线)"},
+        "name": {"type": "string", "description": "工具显示名称"},
+        "description": {"type": "string", "description": "工具功能描述"},
+        "category": {"type": "string", "description": "分类标签"},
+        "input_schema": {"type": "object", "description": "输入参数 JSON Schema"},
+        "output_schema": {"type": "object", "description": "输出 JSON Schema"},
+        "stream_support": {"type": "boolean", "description": "是否支持流式"},
+        "runtime_type": {"type": "string", "enum": ["local", "docker"], "description": "运行时类型"},
+        "container_id": {"type": "string", "description": "Docker 容器 ID(docker 类型必填)"},
+        "host_dir": {"type": "string", "description": "宿主机工作目录(local 类型必填)"},
+        "internal_port": {"type": "integer", "description": "容器/进程内服务端口"},
+        "endpoint_path": {"type": "string", "description": "API 路径,默认 /"},
+        "http_method": {"type": "string", "description": "HTTP 方法,默认 POST"},
+    },
+    "required": ["tool_id", "name", "description", "runtime_type", "internal_port"],
+})
+async def register_tool_fn(args):
+    import re
+    from tool_agent.router.status import ToolSource, SourceType, SourceStore
+
+    tool_id = args["tool_id"]
+    if not re.match(r'^[a-z][a-z0-9_]*$', tool_id):
+        return _text_result({"status": "error", "error": f"Invalid tool_id '{tool_id}'. Must match [a-z][a-z0-9_]*."})
+
+    try:
+        runtime_type = args["runtime_type"]
+
+        # 验证必填参数
+        if runtime_type == "docker":
+            cid = args.get("container_id", "")
+            if not cid or not _docker_runner.get_container_info(cid):
+                return _text_result({"status": "error", "error": f"Container not found: {cid}"})
+        elif runtime_type == "local":
+            host_dir_input = args.get("host_dir", "")
+            if not host_dir_input:
+                return _text_result({"status": "error", "error": "host_dir is required for local runtime"})
+
+            # 转换为相对路径
+            host_dir_path = Path(host_dir_input)
+            try:
+                base_dir = Path(settings.base_dir).resolve()
+                host_dir_abs = host_dir_path.resolve()
+                host_dir = str(host_dir_abs.relative_to(base_dir))
+            except ValueError:
+                # 如果不在项目根目录下,保持原样
+                host_dir = host_dir_input
+
+        # 1. 注册元数据到 Registry
+        tool_meta = ToolMeta(
+            tool_id=tool_id,
+            name=args["name"],
+            description=args["description"],
+            category=args.get("category", ""),
+            input_schema=args.get("input_schema", {}),
+            output_schema=args.get("output_schema", {}),
+            stream_support=args.get("stream_support", False),
+            status=ToolStatus.ACTIVE,
+        )
+        _registry.register(tool_meta)
+
+        # 2. 注册来源到 Router(通过 SourceStore)
+        source_store = SourceStore()
+
+        if runtime_type == "docker":
+            source = ToolSource(
+                type=SourceType.DOCKER,
+                container_id=args.get("container_id", ""),
+                internal_port=args.get("internal_port", 0),
+                endpoint_path=args.get("endpoint_path", "/"),
+                http_method=args.get("http_method", "POST"),
+            )
+        else:  # local
+            source = ToolSource(
+                type=SourceType.LOCAL,
+                host_dir=args.get("host_dir", ""),
+                internal_port=args.get("internal_port", 0),
+                endpoint_path=args.get("endpoint_path", "/"),
+                http_method=args.get("http_method", "POST"),
+            )
+
+        source_store.add_source(tool_id, source)
+
+        result = {
+            "status": "success",
+            "tool_id": tool_id,
+            "runtime": runtime_type,
+            "message": f"Tool '{tool_meta.name}' registered successfully. Metadata saved to registry, source info saved to router.",
+        }
+    except Exception as e:
+        result = {"status": "error", "error": str(e)}
+    return _text_result(result)
+
+
+# ===========================================================================
+#  辅助函数
+# ===========================================================================
+
+def _text_result(data: dict) -> dict:
+    """将 dict 包装为 claude_agent_sdk 的 tool 返回格式"""
+    return {"content": [{"type": "text", "text": json.dumps(data, ensure_ascii=False, indent=2)}]}
+
+
+def _validate_path(path_str: str) -> Path | None:
+    """校验路径是否在允许的目录范围内,返回 resolved Path 或 None"""
+    p = Path(path_str).resolve()
+    allowed_roots = [
+        Path(settings.data_dir).resolve(),
+        Path(settings.tools_dir).resolve(),
+    ]
+    if any(p.is_relative_to(root) for root in allowed_roots):
+        return p
+    return None
+
+
+# ===========================================================================
+#  System Prompt
+# ===========================================================================
+
+SYSTEM_PROMPT = """你是 Coding Agent,负责根据任务书(task_spec)编写代码、配置环境、部署服务、注册工具。
+
+## 你拥有的工具
+
+### Docker 环境(适合:重依赖、GPU、非 Python、需要系统隔离的项目)
+- create_docker_env: 创建 Docker 容器(支持端口映射、目录挂载)
+- run_in_docker: 在容器中执行 Shell 命令(前台/后台)
+- rebuild_docker_ports: 重建容器加端口映射(保留文件系统)
+- destroy_docker_env: 销毁容器
+
+### 本地 uv 环境(适合:纯 Python 轻量工具)
+- create_uv_project: 创建 uv 项目(独立 venv)
+- run_in_uv: 在 uv 环境中执行命令
+- uv_add_dependency: 添加依赖
+
+### 文件操作(宿主机)
+- write_file: 写文件(代码、配置文件)— 配合 Docker volume 挂载实时同步
+- read_file: 读文件(README、文档、代码)
+
+### 注册
+- register_tool: 将工具注册到 registry,使其可被外部调用
+
+## 工作流程
+
+### GitHub 项目接入:
+1. 根据 task_spec 选择环境类型
+   - Python 项目且依赖简单 → uv
+   - 需要系统库/端口服务/GPU/非 Python → Docker + volume 挂载
+2. 创建环境
+3. 对于 Docker:创建宿主机 staging 目录,挂载到容器 /app
+4. git clone 项目到环境中
+5. read_file 读取 README/文档,理解功能和部署方式
+6. 安装依赖
+7. 如果项目本身提供 HTTP API → 直接部署启动
+8. 如果项目是 CLI/库 → write_file 编写 HTTP API 包装器(FastAPI)
+9. 启动服务,测试是否正常(curl 测试)
+10. register_tool 注册到工具库
+11. 返回结果报告:工具名、功能、访问方式
+
+### 自主编写新工具:
+1. 创建环境
+2. write_file 编写工具代码
+3. write_file 编写 HTTP API 接口
+4. 安装依赖并测试
+5. register_tool 注册
+6. 返回结果报告
+
+## 项目结构规范
+工具项目根目录只放核心代码,测试和临时文件放在 tests/ 子目录:
+```
+{tool_id}/
+├── pyproject.toml       # 项目配置
+├── main.py              # API 入口(FastAPI + uvicorn)
+├── {核心模块}.py         # 核心业务逻辑
+└── tests/               # 测试与临时文件
+    ├── test_xxx.py      # 测试脚本
+    └── output/          # 测试产物(图片、文件等)
+```
+- 测试脚本必须放在 tests/ 目录下
+- 测试中产生的文件(图片、音频、中间结果)保存到 tests/output/
+- 运行测试时用 `python tests/test_xxx.py`
+
+## 环境选择原则
+- 纯 Python + 依赖少 → uv(零端口,冷启动快)
+- 需要端口/服务常驻/GPU/系统依赖 → Docker
+- Docker 推荐用 volume 挂载:宿主机写文件 → 容器内直接运行
+
+## 重要规则
+- 工具必须提供 HTTP API 才能注册(原生 API 或自己包装)
+- 注册时必须提供 input_schema(JSON Schema 格式)
+- tool_id 用小写字母和下划线,如 "image_compress_api"
+- 必须测试通过后才能注册
+- 遇到错误要尝试修复,不要轻易放弃
+- 失败时给出清晰的错误原因和已尝试的方案
+- 测试阶段只验证核心逻辑(单元测试),不要启动 HTTP 服务器。写好 FastAPI 代码即可,服务启动由 Router 层在调用时负责
+"""
+
+
+# ===========================================================================
+#  CodingAgent 类
+# ===========================================================================
+
+ALL_TOOLS = [
+    create_docker_env, run_in_docker, rebuild_docker_ports, destroy_docker_env,
+    create_uv_project, run_in_uv, uv_add_dependency,
+    write_file, read_file,
+    register_tool_fn,
+]
+
+MCP_SERVER_NAME = "coding_tools"
+
+
+class CodingAgent:
+    """Coding Agent:接收任务书,调用 Claude SDK 自主完成工具编写和部署"""
+
+    def __init__(self, bus: MessageBus | None = None, model: str = "claude-sonnet-4-5") -> None:
+        self.bus = bus
+        self.model = model
+
+    async def start(self) -> None:
+        """作为常驻服务运行,监听 MessageBus 消息"""
+        if not self.bus:
+            logger.warning("CodingAgent started without MessageBus, standalone mode.")
+            return
+
+        logger.info("CodingAgent started, waiting for tasks...")
+        while True:
+            msg = await self.bus.recv_as_tool()
+            await self._handle_message(msg)
+
+    async def _handle_message(self, msg: AgentMessage) -> None:
+        if msg.type == MessageType.TOOL_REQUEST:
+            task_spec = msg.payload.get("task_spec", "")
+            if not task_spec:
+                task_spec = msg.payload.get("description", "")
+            logger.info(f"Received task: {task_spec[:100]}...")
+
+            # 读取参考文件内容
+            ref_contents: dict[str, str] = {}
+            for ref_path in msg.payload.get("reference_files", []):
+                p = Path(ref_path)
+                if not p.is_absolute():
+                    p = settings.base_dir / p
+                if p.exists():
+                    ref_contents[str(ref_path)] = p.read_text(encoding="utf-8")
+                else:
+                    logger.warning(f"Reference file not found: {p}")
+
+            result = await self.execute(task_spec, ref_contents)
+            await self.bus.send_to_router(AgentMessage(
+                type=MessageType.TOOL_READY,
+                payload={"result": result, **msg.payload},
+            ))
+
+        elif msg.type == MessageType.HEALTH_ALERT:
+            tool_id = msg.payload.get("tool_id", "")
+            error = msg.payload.get("error", "unknown")
+            logger.warning(f"Health alert for tool: {tool_id}")
+            result = await self.execute(
+                f"工具 {tool_id} 出现故障,错误信息:{error}。请诊断并修复。"
+            )
+            await self.bus.send_to_router(AgentMessage(
+                type=MessageType.TOOL_READY,
+                payload={"result": result, "tool_id": tool_id},
+            ))
+
+    async def execute(self, task: str, reference_files: dict[str, str] | None = None) -> str:
+        """执行任务:启动 Claude Agent 会话,自主完成编码部署,返回结果"""
+        
+        # 1. 动态生成本次任务的唯一标识和 Staging 路径
+        task_id = f"task_{uuid.uuid4().hex[:8]}"
+        # 建议路径:tools/staging/task_xxxx
+        staging_dir = Path(settings.data_dir) / "staging" / task_id
+        staging_dir.mkdir(parents=True, exist_ok=True)
+        staging_path_abs = str(staging_dir.resolve())
+
+        # 2. 准备工具服务器
+        mcp_server = create_sdk_mcp_server(
+            name=MCP_SERVER_NAME,
+            version="1.0.0",
+            tools=ALL_TOOLS,
+        )
+
+        # 3. 注入带路径感知的 System Prompt
+        new_system_prompt = SYSTEM_PROMPT + f"""
+
+## 调试与自修复指南
+1. **当前工作目录**:你的本次任务 Staging 路径为 `{staging_path_abs}`。
+2. **日志意识**:当你执行 `run_in_uv` 或 `run_in_docker` 失败(exit_code != 0)时,系统会自动将 stderr 返回给你。
+3. **深度诊断**:如果 stderr 信息不足,请主动使用 `read_file` 读取该目录下的 `last_run.log` 文件。
+4. **修复闭环**:根据日志中的 Traceback 报错信息,判断是缺少依赖(调用 uv_add_dependency)、代码语法错误(重新 write_file)还是环境配置问题。
+5. **重要**:在 Docker 模式下,务必将此路径挂载到容器内部(如 /app)。
+"""
+
+        options = ClaudeAgentOptions(
+            system_prompt=new_system_prompt,
+            mcp_servers={MCP_SERVER_NAME: mcp_server},
+            model=self.model,
+            setting_sources=["project"],
+            allowed_tools=[
+                f"mcp__{MCP_SERVER_NAME}__{t.name}"
+                for t in ALL_TOOLS
+            ]
+        )
+
+        result_text = ""
+
+        # 4. 启动客户端交互
+        async with ClaudeSDKClient(options=options) as client:
+            logger.info(f"[CodingAgent] Starting task: {task[:100]}... in {staging_path_abs}")
+            
+            # 构造包含路径的任务触发语
+            ref_section = ""
+            if reference_files:
+                parts = ["\n## 参考代码文件\n以下是可供参考的现有代码,请理解其结构和风格后再编写:\n"]
+                for path, content in reference_files.items():
+                    parts.append(f"### {path}\n```\n{content}\n```\n")
+                ref_section = "\n".join(parts)
+
+            context_msg = f"""【任务指令】
+工作区绝对路径: {staging_path_abs}
+任务描述: {task}
+{ref_section}
+请开始执行。完成后请给出总结报告。"""
+            
+            await client.query(context_msg)
+
+            async for message in client.receive_response():
+                if isinstance(message, AssistantMessage):
+                    for block in message.content:
+                        if isinstance(block, TextBlock):
+                            logger.info(f"[TEXT] {block.text}")
+                            result_text = block.text
+                        elif isinstance(block, ThinkingBlock):
+                            logger.debug(f"[THINKING] {block.thinking[:200]}...")
+                        elif isinstance(block, ToolUseBlock):
+                            logger.info(f"[TOOL_USE] {block.name} | {json.dumps(block.input, ensure_ascii=False)[:200]}")
+
+                elif isinstance(message, UserMessage):
+                    if message.tool_use_result:
+                        # 如果工具返回了 log_path,这里可以在日志中记录一下
+                        logger.debug(f"[TOOL_RESULT] {json.dumps(message.tool_use_result, ensure_ascii=False)[:300]}")
+
+                elif isinstance(message, ResultMessage):
+                    logger.info(f"[DONE] duration={message.duration_ms}ms | cost=${message.total_cost_usd or 0}")
+                    if message.result:
+                        result_text = message.result
+
+        # 任务结束,清理后台进程
+        _local_runner.cleanup_background()
+
+        return result_text
+
+# ===========================================================================
+#  独立运行入口
+# ===========================================================================
+
+async def main():
+    """交互式运行 CodingAgent"""
+    logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s: %(message)s")
+    agent = CodingAgent()
+
+    print("=" * 50)
+    print("Coding Agent (claude_agent_sdk)")
+    print("=" * 50)
+    print("输入任务(GitHub 地址 / 工具需求描述)")
+    print("-" * 50)
+
+    task = input("请输入任务: ").strip()
+    if not task:
+        print("错误:输入不能为空")
+        return
+
+    print("-" * 50)
+    print("正在处理...")
+    print("-" * 50)
+
+    result = await agent.execute(task)
+    print("=" * 50)
+    print("结果:")
+    print(result)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 34 - 0
src/tool_agent/tool/auditor.py

@@ -0,0 +1,34 @@
+"""代码安全审计"""
+
+from __future__ import annotations
+
+import logging
+import re
+from pathlib import Path
+
+logger = logging.getLogger(__name__)
+
+# 禁止的危险模式
+DANGEROUS_PATTERNS = [
+    r"os\.system\s*\(",
+    r"subprocess\.call\s*\(.*shell\s*=\s*True",
+    r"rm\s+-rf",
+    r"eval\s*\(",
+    r"exec\s*\(",
+    r"__import__\s*\(",
+]
+
+
+class Auditor:
+    """对 Agent 生成的代码进行安全检查"""
+
+    def audit(self, tool_dir: Path) -> list[str]:
+        """扫描目录下所有 .py 文件,返回安全问题列表"""
+        issues: list[str] = []
+        for py_file in tool_dir.rglob("*.py"):
+            content = py_file.read_text(encoding="utf-8")
+            for pattern in DANGEROUS_PATTERNS:
+                matches = re.findall(pattern, content)
+                if matches:
+                    issues.append(f"{py_file.name}: found dangerous pattern '{pattern}'")
+        return issues

+ 27 - 0
src/tool_agent/tool/browser.py

@@ -0,0 +1,27 @@
+"""Browser-Use 能力封装"""
+
+from __future__ import annotations
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class BrowserAgent:
+    """浏览器自动化:网页操作、信息采集、账号注册、逆向抓包"""
+
+    async def navigate(self, url: str) -> str:
+        """访问页面,返回页面内容"""
+        # TODO: 接入 browser-use / playwright
+        logger.info(f"Navigate to: {url}")
+        return ""
+
+    async def register_account(self, service: str, credentials: dict) -> dict:
+        """自动注册第三方账号"""
+        # TODO: 浏览器自动化注册流程
+        return {"status": "not_implemented"}
+
+    async def capture_api(self, url: str) -> dict:
+        """逆向抓包分析 API"""
+        # TODO: 拦截网络请求,分析接口
+        return {"endpoints": []}

+ 20 - 0
src/tool_agent/tool/builder.py

@@ -0,0 +1,20 @@
+"""工具编码 — 生成代码 + 测试脚本"""
+
+from __future__ import annotations
+
+import logging
+from pathlib import Path
+
+logger = logging.getLogger(__name__)
+
+
+class ToolBuilder:
+    """Agent 自主编写工具代码"""
+
+    async def build(self, task_id: str, description: str, staging_dir: Path) -> Path:
+        """根据需求描述生成工具代码和测试脚本,返回工具目录"""
+        tool_dir = staging_dir / task_id
+        tool_dir.mkdir(parents=True, exist_ok=True)
+        # TODO: LLM 生成代码 → main.py + test_main.py + pyproject.toml
+        logger.info(f"Built tool skeleton at {tool_dir}")
+        return tool_dir

+ 29 - 0
src/tool_agent/tool/deployer.py

@@ -0,0 +1,29 @@
+"""工具部署 — uv init / docker build"""
+
+from __future__ import annotations
+
+import logging
+from pathlib import Path
+
+from tool_agent.router.status import SourceType
+
+logger = logging.getLogger(__name__)
+
+
+class Deployer:
+    """将工具部署到对应运行环境"""
+
+    async def deploy_local(self, tool_dir: Path) -> bool:
+        logger.info(f"Deployed local tool: {tool_dir}")
+        return True
+
+    async def deploy_docker(self, tool_dir: Path) -> bool:
+        logger.info(f"Deployed docker tool: {tool_dir}")
+        return True
+
+    async def deploy(self, tool_dir: Path, source_type: SourceType) -> bool:
+        if source_type == SourceType.LOCAL:
+            return await self.deploy_local(tool_dir)
+        elif source_type == SourceType.DOCKER:
+            return await self.deploy_docker(tool_dir)
+        return False

+ 37 - 0
src/tool_agent/tool/finance.py

@@ -0,0 +1,37 @@
+"""财务管理 — 账号注册/充值/预算"""
+
+from __future__ import annotations
+
+import json
+import logging
+from pathlib import Path
+
+from tool_agent.config import settings
+
+logger = logging.getLogger(__name__)
+
+
+class FinanceManager:
+    """管理 API 账号和预算"""
+
+    def __init__(self) -> None:
+        self.billing_path = settings.data_dir / "billing_log.json"
+
+    def check_budget(self, amount_usd: float) -> bool:
+        """检查是否在预算内"""
+        return amount_usd <= settings.single_tx_limit_usd
+
+    def needs_approval(self, amount_usd: float) -> bool:
+        """是否需要人工审批"""
+        return amount_usd > settings.require_approval_above_usd
+
+    def record_transaction(self, provider: str, amount_usd: float, description: str) -> None:
+        """记录支出"""
+        log = json.loads(self.billing_path.read_text()) if self.billing_path.exists() else {"transactions": []}
+        log["transactions"].append({
+            "provider": provider,
+            "amount_usd": amount_usd,
+            "description": description,
+        })
+        self.billing_path.write_text(json.dumps(log, indent=2, ensure_ascii=False))
+        logger.info(f"Recorded transaction: {provider} ${amount_usd}")

+ 22 - 0
src/tool_agent/tool/promoter.py

@@ -0,0 +1,22 @@
+"""staging -> 生产目录 promote 流程"""
+
+from __future__ import annotations
+
+import logging
+import shutil
+from pathlib import Path
+
+from tool_agent.router.status import SourceType
+
+logger = logging.getLogger(__name__)
+
+
+class Promoter:
+    """将通过验证的工具从 staging 提升到生产目录"""
+
+    async def promote(self, staging_path: Path, source_type: SourceType, tools_dir: Path) -> Path:
+        target_subdir = "local" if source_type == SourceType.LOCAL else "docker"
+        target = tools_dir / target_subdir / staging_path.name
+        shutil.copytree(staging_path, target, dirs_exist_ok=True)
+        logger.info(f"Promoted {staging_path.name} -> {target}")
+        return target

+ 22 - 0
src/tool_agent/tool/repairer.py

@@ -0,0 +1,22 @@
+"""逆向 API 自修复"""
+
+from __future__ import annotations
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class Repairer:
+    """当逆向接入的工具出现故障时,触发自动修复"""
+
+    async def repair(self, tool_id: str, error_info: dict) -> bool:
+        """
+        修复流程:
+        1. Browser-Use 重新抓包更新接口参数
+        2. 切换到备用 API 策略
+        3. 均失败 → 标记 inactive
+        """
+        # TODO: 实现修复闭环
+        logger.info(f"Attempting repair for tool: {tool_id}")
+        return False

+ 0 - 0
tests/__init__.py


+ 46 - 0
tests/check.py

@@ -0,0 +1,46 @@
+def check_model_info(self, version_uuid):
+        """查询模型详细信息,确认其基础算法(baseAlgo)"""
+        uri = "/api/model/version/get"
+        url = self._generate_signature(uri)
+        
+        payload = {
+            "versionUuid": version_uuid
+        }
+
+        try:
+            print(f"🔍 正在查询模型信息: {version_uuid}...")
+            resp = requests.post(
+                url,
+                headers={"Content-Type": "application/json"},
+                json=payload,
+                timeout=10
+            )
+            result = resp.json()
+            
+            if result.get("code") == 0:
+                data = result.get("data", {})
+                print(f"--- 模型信息 ---")
+                print(f"📌 模型名称: {data.get('model_name')}")
+                print(f"📌 版本名称: {data.get('version_name')}")
+                print(f"🚀 基础算法 (baseAlgo): {data.get('baseAlgo')}") # 重点看这个!
+                print(f"💰 是否商用: {'是' if data.get('commercial_use') == '1' else '否'}")
+                print(f"----------------")
+                return data
+            else:
+                print(f"❌ 查询失败: {result.get('msg')}")
+                return None
+        except Exception as e:
+            print(f"❌ 请求异常: {str(e)}")
+            return None
+
+# --- 在 main 中使用 ---
+if __name__ == "__main__":
+    client = LibLibControlNet()
+    
+    # 1. 先查底模
+    print("检查底模架构...")
+    client.check_model_info(client.CHECKPOINT_ID)
+    
+    # 2. 再查 ControlNet 模型
+    print("\n检查 ControlNet 模型架构...")
+    client.check_model_info(client.CANNY_MODEL_ID)

+ 40 - 0
tests/cleanup_tool.py

@@ -0,0 +1,40 @@
+"""清理工具:调用注册层删除工具的项目文件和注册信息
+
+用法:
+    uv run python -m tests.cleanup_tool image_stitcher
+    uv run python -m tests.cleanup_tool --all
+"""
+
+import argparse
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+sys.path.insert(0, str(ROOT / "src"))
+
+from tool_agent.registry.registry import ToolRegistry
+
+
+def main():
+    parser = argparse.ArgumentParser(description="清理工具")
+    parser.add_argument("tool_id", nargs="?", help="要清理的 tool_id")
+    parser.add_argument("--all", action="store_true", help="清除所有工具")
+    args = parser.parse_args()
+
+    import logging
+    logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s")
+
+    registry = ToolRegistry()
+
+    if args.all:
+        result = registry.destroy_all()
+        print(f"Cleaned {result['count']} tools")
+    elif args.tool_id:
+        result = registry.destroy(args.tool_id)
+        print(f"Cleaned: {result['cleaned']}")
+    else:
+        parser.print_help()
+
+
+if __name__ == "__main__":
+    main()

binární
tests/control_reference.png


+ 175 - 0
tests/liblibai_comfyui_runner.py

@@ -0,0 +1,175 @@
+#!/usr/bin/env python3
+"""LibLibAI ComfyUI Workflow Runner
+
+用法:
+    python liblibai_comfyui_runner.py workflow.json
+    python liblibai_comfyui_runner.py workflow.json --output result.png
+"""
+
+import argparse
+import base64
+import hashlib
+import hmac
+import json
+import os
+import sys
+import time
+import uuid
+from pathlib import Path
+
+import requests
+from dotenv import load_dotenv
+
+load_dotenv()
+
+DOMAIN = os.getenv("LIBLIBAI_DOMAIN", "https://openapi.liblibai.cloud")
+ACCESS_KEY = os.getenv("LIBLIBAI_ACCESS_KEY")
+SECRET_KEY = os.getenv("LIBLIBAI_SECRET_KEY")
+DEFAULT_TEMPLATE = "4df2efa0f18d46dc9758803e478eb51c"
+
+
+def generate_auth_url(uri: str) -> str:
+    """生成带 HMAC-SHA1 签名的完整 URL"""
+    ts = str(int(time.time() * 1000))
+    nonce = uuid.uuid4().hex
+    sign_str = f"{uri}&{ts}&{nonce}"
+    dig = hmac.new(SECRET_KEY.encode(), sign_str.encode(), hashlib.sha1).digest()
+    signature = base64.urlsafe_b64encode(dig).rstrip(b"=").decode()
+    return f"{DOMAIN}{uri}?AccessKey={ACCESS_KEY}&Timestamp={ts}&SignatureNonce={nonce}&Signature={signature}"
+
+
+def submit_workflow(workflow_data: dict) -> str:
+    """提交 ComfyUI workflow 生图任务"""
+    uri = "/api/generate/comfyui/app"
+    url = generate_auth_url(uri)
+
+    payload = {
+        "templateUuid": DEFAULT_TEMPLATE,
+        "generateParams": workflow_data
+    }
+
+    print(f"提交任务...")
+    resp = requests.post(url, json=payload, headers={"Content-Type": "application/json"})
+    resp.raise_for_status()
+
+    data = resp.json()
+    if data.get("code") != 0:
+        raise Exception(f"提交失败: {data.get('msg', 'unknown error')}")
+
+    generate_uuid = data["data"]["generateUuid"]
+    print(f"任务 ID: {generate_uuid}")
+    return generate_uuid
+
+
+def poll_result(generate_uuid: str, timeout: int = 600) -> dict:
+    """轮询任务结果"""
+    uri = "/api/generate/comfy/status"
+    url = generate_auth_url(uri)
+
+    start_time = time.time()
+    print(f"轮询结果 (超时 {timeout}s)...")
+
+    while time.time() - start_time < timeout:
+        resp = requests.post(url, json={"generateUuid": generate_uuid})
+        resp.raise_for_status()
+
+        data = resp.json()
+        if data.get("code") != 0:
+            raise Exception(f"查询失败: {data.get('msg', 'unknown error')}")
+
+        result = data["data"]
+        status = result["generateStatus"]
+
+        # 1=等待 2=执行中 3=已生图 4=审核中 5=成功 6=失败
+        if status in [1, 2, 3, 4]:
+            status_text = {1: "等待", 2: "执行中", 3: "已生图", 4: "审核中"}[status]
+            print(f"  状态: {status_text}  进度: {result.get('percentCompleted', 0):.0%}")
+            time.sleep(5)
+            continue
+
+        if status == 5:
+            print(f"✓ 任务成功")
+            print(f"  消耗积分: {result.get('pointsCost', 0)}")
+            print(f"  剩余积分: {result.get('accountBalance', 0)}")
+            return result
+
+        if status == 6:
+            raise Exception(f"任务失败: {result.get('generateMsg', 'unknown')}")
+
+    raise TimeoutError(f"轮询超时 ({timeout}s)")
+
+
+def save_results(result: dict, output_dir: Path):
+    """保存生成的图片和视频"""
+    output_dir.mkdir(parents=True, exist_ok=True)
+
+    images = result.get("images", [])
+    videos = result.get("videos", [])
+
+    print(f"\n生成结果:")
+    print(f"  图片: {len(images)} 张")
+    print(f"  视频: {len(videos)} 个")
+
+    for i, img in enumerate(images):
+        if img.get("auditStatus") != 3:
+            print(f"  图片 {i+1}: 审核未通过 (status={img.get('auditStatus')})")
+            continue
+
+        url = img["imageUrl"]
+        filename = f"image_{i+1}.png"
+        filepath = output_dir / filename
+
+        print(f"  下载: {filename}")
+        resp = requests.get(url)
+        resp.raise_for_status()
+        filepath.write_bytes(resp.content)
+        print(f"    -> {filepath}")
+
+    for i, vid in enumerate(videos):
+        if vid.get("auditStatus") != 3:
+            print(f"  视频 {i+1}: 审核未通过 (status={vid.get('auditStatus')})")
+            continue
+
+        url = vid["videoUrl"]
+        filename = f"video_{i+1}.mp4"
+        filepath = output_dir / filename
+
+        print(f"  下载: {filename}")
+        resp = requests.get(url)
+        resp.raise_for_status()
+        filepath.write_bytes(resp.content)
+        print(f"    -> {filepath}")
+
+
+def main():
+    parser = argparse.ArgumentParser(description="LibLibAI ComfyUI Workflow Runner")
+    parser.add_argument("workflow", help="workflow JSON 文件路径")
+    parser.add_argument("--output", "-o", default="output", help="输出目录,默认 output/")
+    parser.add_argument("--timeout", "-t", type=int, default=600, help="轮询超时秒数,默认 600")
+    args = parser.parse_args()
+
+    if not ACCESS_KEY or not SECRET_KEY:
+        print("ERROR: 请设置环境变量 LIBLIBAI_ACCESS_KEY 和 LIBLIBAI_SECRET_KEY")
+        sys.exit(1)
+
+    workflow_path = Path(args.workflow)
+    if not workflow_path.exists():
+        print(f"ERROR: 文件不存在: {workflow_path}")
+        sys.exit(1)
+
+    print(f"加载 workflow: {workflow_path}")
+    with open(workflow_path, "r", encoding="utf-8") as f:
+        workflow_data = json.load(f)
+
+    try:
+        generate_uuid = submit_workflow(workflow_data)
+        result = poll_result(generate_uuid, timeout=args.timeout)
+        save_results(result, Path(args.output))
+        print("\n✓ 完成")
+    except Exception as e:
+        print(f"\n✗ 错误: {e}")
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()

binární
tests/output/stitched_result.png


+ 194 - 0
tests/run_comfy/check_workflow.py

@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+"""检查 workflow_api.json 需要哪些外部输入
+
+分析所有 LoadImage / LoadVideo 等输入节点,列出需要提供的文件。
+检查所有节点连接是否完整,发现缺失的输入。
+
+用法:
+    python check_workflow.py workflow_api.json
+    python check_workflow.py workflow_api.json --images ref.png pose.jpg
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+# 需要外部文件输入的节点类型,以及对应的文件参数名
+FILE_INPUT_NODES = {
+    "LoadImage": ["image"],
+    "LoadVideo": ["video"],
+    "LoadVideoPath": ["video"],
+    "LoadImageMask": ["image"],
+    "LoadImageOutput": ["image"],  # 从 output 目录加载,特殊处理
+    "VHS_LoadVideo": ["video"],
+    "VHS_LoadImages": ["directory"],
+}
+
+# 纯 UI / 注释节点,无实际输入
+SKIP_NODES = {"Note", "MarkdownNote", "PrimitiveNode"}
+
+
+def analyze(workflow: dict) -> dict:
+    node_ids = set(workflow.keys())
+
+    # 收集所有被其他节点引用的 [node_id, slot] 连接
+    referenced = set()
+    for node_id, node in workflow.items():
+        for key, val in node.get("inputs", {}).items():
+            if isinstance(val, list) and len(val) == 2 and isinstance(val[0], str):
+                referenced.add(val[0])
+
+    issues = []
+    file_inputs = []   # 需要上传的文件
+    widget_params = [] # widget_* 占位参数(转换不准确的)
+
+    for node_id, node in workflow.items():
+        class_type = node.get("class_type", "")
+
+        if class_type in SKIP_NODES:
+            continue
+
+        inputs = node.get("inputs", {})
+
+        # 检查连接引用是否存在
+        for key, val in inputs.items():
+            if isinstance(val, list) and len(val) == 2 and isinstance(val[0], str):
+                ref_node = val[0]
+                if ref_node not in node_ids:
+                    issues.append(f"节点 [{node_id}] {class_type}: 输入 '{key}' 引用了不存在的节点 [{ref_node}]")
+
+        # 检查文件输入节点
+        if class_type in FILE_INPUT_NODES:
+            param_names = FILE_INPUT_NODES[class_type]
+            for param in param_names:
+                # 可能是真实参数名,也可能被转成了 widget_*
+                value = inputs.get(param)
+                if value is None:
+                    # 找 widget_* 里的值
+                    widget_vals = {k: v for k, v in inputs.items() if k.startswith("widget_")}
+                    value = next(iter(widget_vals.values()), None) if widget_vals else None
+
+                is_output_node = class_type == "LoadImageOutput"
+                file_inputs.append({
+                    "node_id": node_id,
+                    "class_type": class_type,
+                    "param": param,
+                    "current_value": value,
+                    "is_output": is_output_node,
+                })
+
+        # 标记 widget_* 占位参数(说明转换不准确)
+        widget_keys = [k for k in inputs if k.startswith("widget_")]
+        if widget_keys and class_type not in FILE_INPUT_NODES:
+            widget_params.append({
+                "node_id": node_id,
+                "class_type": class_type,
+                "params": {k: inputs[k] for k in widget_keys},
+            })
+
+    return {
+        "file_inputs": file_inputs,
+        "widget_params": widget_params,
+        "issues": issues,
+    }
+
+
+def check_files_exist(file_inputs: list, provided: list[Path]) -> list:
+    provided_names = {p.name for p in provided if p.exists()}
+    missing = []
+    for fi in file_inputs:
+        if fi["is_output"]:
+            continue  # LoadImageOutput 从服务器 output 目录读,不需要上传
+        val = fi["current_value"]
+        if val and isinstance(val, str):
+            # 去掉 [output] 等后缀
+            filename = val.split(" ")[0]
+            if filename not in provided_names:
+                missing.append({**fi, "filename": filename})
+    return missing
+
+
+def main():
+    parser = argparse.ArgumentParser(description="检查 workflow_api.json 所需输入")
+    parser.add_argument("workflow", help="workflow_api.json 路径")
+    parser.add_argument("--input-dir", default="input", metavar="DIR", help="输入文件目录,默认 input/")
+    args = parser.parse_args()
+
+    workflow_path = Path(args.workflow)
+    if not workflow_path.exists():
+        print(f"ERROR: 文件不存在: {workflow_path}")
+        return 1
+
+    with open(workflow_path, "r", encoding="utf-8") as f:
+        workflow = json.load(f)
+
+    input_dir = Path(args.input_dir)
+    all_input_files = list(input_dir.rglob("*")) if input_dir.exists() else []
+
+    print(f"=== 检查 {workflow_path.name} ===\n")
+    print(f"节点总数: {len(workflow)}")
+    print(f"input 目录: {input_dir} ({'存在' if input_dir.exists() else '不存在'})")
+    if all_input_files:
+        print(f"  已有文件:")
+        for f in sorted(all_input_files):
+            if f.is_file():
+                print(f"    - {f.relative_to(input_dir)}")
+
+    result = analyze(workflow)
+
+    # ── 文件输入 ──
+    print(f"\n── 文件输入节点 ({len(result['file_inputs'])} 个) ──")
+    if result["file_inputs"]:
+        for fi in result["file_inputs"]:
+            tag = "[output目录]" if fi["is_output"] else "[需上传]"
+            print(f"  [{fi['node_id']}] {fi['class_type']}.{fi['param']}")
+            print(f"      当前值: {fi['current_value']}  {tag}")
+    else:
+        print("  无")
+
+    # ── widget_* 警告 ──
+    if result["widget_params"]:
+        print(f"\n── ⚠️  widget_* 占位参数(参数名不准确,建议用 ComfyUI 导出 API 格式)──")
+        for wp in result["widget_params"]:
+            print(f"  [{wp['node_id']}] {wp['class_type']}")
+            for k, v in wp["params"].items():
+                print(f"      {k}: {v}")
+
+    # ── 连接问题 ──
+    if result["issues"]:
+        print(f"\n── ❌ 连接问题 ({len(result['issues'])} 个) ──")
+        for issue in result["issues"]:
+            print(f"  {issue}")
+
+    # ── 文件缺失检查 ──
+    missing = check_files_exist(result["file_inputs"], all_input_files)
+
+    print(f"\n── 文件准备状态 ──")
+    needs_upload = [fi for fi in result["file_inputs"] if not fi["is_output"]]
+    if not needs_upload:
+        print("  无需上传文件")
+    else:
+        for fi in needs_upload:
+            filename = (fi["current_value"] or "").split(" ")[0]
+            found = any(f.is_file() and f.name == filename for f in all_input_files)
+            status = "✓ 已提供" if found else "✗ 缺失"
+            print(f"  {status}  {filename}  (节点 [{fi['node_id']}] {fi['class_type']})")
+
+    # ── 最终结论 ──
+    print()
+    has_error = bool(result["issues"]) or bool(missing)
+    has_warn = bool(result["widget_params"])
+
+    if has_error:
+        print("❌ 检查未通过,请将缺失文件放入 input/ 目录后再提交")
+        return 1
+    elif has_warn:
+        print("⚠️  存在 widget_* 占位参数,建议用 ComfyUI 导出正确的 API 格式,否则可能运行出错")
+        return 0
+    else:
+        print("✓ 检查通过,可以提交")
+        return 0
+
+
+if __name__ == "__main__":
+    exit(main())

+ 104 - 0
tests/run_comfy/convert_workflow.py

@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+"""将 ComfyUI workflow.json (UI格式) 转换为 workflow_api.json (API格式)
+
+用法:
+    python convert_workflow.py refcontrol_pose.json
+    python convert_workflow.py refcontrol_pose.json -o output_api.json
+"""
+
+import argparse
+import json
+from pathlib import Path
+
+
+def convert(workflow: dict) -> dict:
+    """
+    workflow.json → workflow_api.json
+
+    UI格式: nodes[] 每个节点有 id/type/inputs[]/outputs[]/widgets_values[]/links[]
+    API格式: {node_id: {class_type, inputs: {param: value_or_link}}}
+
+    连接关系: links[] = [link_id, src_node, src_slot, dst_node, dst_slot]
+    widgets_values 按顺序对应节点没有连线的输入参数
+    """
+    nodes = workflow.get("nodes", [])
+    links = workflow.get("links", [])
+
+    # 建立 link_id → [src_node_id, src_slot] 的映射
+    link_map = {}
+    for link in links:
+        link_id, src_node, src_slot, dst_node, dst_slot = link[:5]
+        link_map[link_id] = [str(src_node), src_slot]
+
+    api = {}
+
+    for node in nodes:
+        node_id = str(node["id"])
+        class_type = node.get("type", "")
+
+        # 跳过纯 UI 节点(Note、MarkdownNote 等无实际计算的节点)
+        if class_type in ("Note", "MarkdownNote", "PrimitiveNode"):
+            continue
+        # 跳过 mode=4(disabled)的节点
+        if node.get("mode") == 4:
+            continue
+
+        inputs_def = node.get("inputs", [])      # 有连线的输入槽
+        widgets_values = node.get("widgets_values", [])  # 无连线的参数值
+
+        inputs = {}
+
+        # 先处理有连线的输入槽
+        linked_slots = set()
+        for inp in inputs_def:
+            link_id = inp.get("link")
+            if link_id is not None and link_id in link_map:
+                inputs[inp["name"]] = link_map[link_id]
+                linked_slots.add(inp["name"])
+
+        # 再处理 widgets_values(按顺序填入没有连线的参数)
+        # 需要知道节点有哪些 widget 参数,通过排除已连线的输入来推断
+        # widgets 对应的参数名无法从 workflow.json 直接获取,只能按顺序赋值
+        # 用 widget_idx_0, widget_idx_1 ... 作为 key,实际使用时按需重命名
+        for i, val in enumerate(widgets_values):
+            key = f"widget_{i}"
+            inputs[key] = val
+
+        api[node_id] = {
+            "class_type": class_type,
+            "inputs": inputs,
+        }
+
+    return api
+
+
+def main():
+    parser = argparse.ArgumentParser(description="Convert workflow.json to workflow_api.json")
+    parser.add_argument("input", help="输入 workflow.json 路径")
+    parser.add_argument("-o", "--output", help="输出路径,默认在原文件名加 _api 后缀")
+    args = parser.parse_args()
+
+    input_path = Path(args.input)
+    output_path = Path(args.output) if args.output else input_path.with_stem(input_path.stem + "_api")
+
+    with open(input_path, "r", encoding="utf-8") as f:
+        workflow = json.load(f)
+
+    # 支持两种格式:直接是 nodes[] 的对象,或者包含 nodes[] 的对象
+    if "nodes" not in workflow:
+        print("ERROR: 不是有效的 workflow.json,缺少 nodes 字段")
+        return
+
+    api = convert(workflow)
+
+    with open(output_path, "w", encoding="utf-8") as f:
+        json.dump(api, f, indent=2, ensure_ascii=False)
+
+    print(f"转换完成: {output_path}")
+    print(f"节点数: {len(api)}")
+    for node_id, node in api.items():
+        print(f"  [{node_id}] {node['class_type']}")
+
+
+if __name__ == "__main__":
+    main()

binární
tests/run_comfy/input/2.png


binární
tests/run_comfy/input/ref.jpg


binární
tests/run_comfy/output/ComfyUI_00006_.png


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 149 - 0
tests/run_comfy/refcontrol_pose.json


+ 315 - 0
tests/run_comfy/refcontrol_pose_api.json

@@ -0,0 +1,315 @@
+{
+  "1": {
+    "inputs": {
+      "vae_name": "ae.safetensors"
+    },
+    "class_type": "VAELoader",
+    "_meta": {
+      "title": "Load VAE"
+    }
+  },
+  "2": {
+    "inputs": {
+      "conditioning": [
+        "18",
+        0
+      ]
+    },
+    "class_type": "ConditioningZeroOut",
+    "_meta": {
+      "title": "ConditioningZeroOut"
+    }
+  },
+  "3": {
+    "inputs": {
+      "guidance": 2.5,
+      "conditioning": [
+        "6",
+        0
+      ]
+    },
+    "class_type": "FluxGuidance",
+    "_meta": {
+      "title": "FluxGuidance"
+    }
+  },
+  "6": {
+    "inputs": {
+      "conditioning": [
+        "18",
+        0
+      ],
+      "latent": [
+        "15",
+        0
+      ]
+    },
+    "class_type": "ReferenceLatent",
+    "_meta": {
+      "title": "ReferenceLatent"
+    }
+  },
+  "9": {
+    "inputs": {
+      "seed": 219,
+      "steps": 20,
+      "cfg": 1,
+      "sampler_name": "euler",
+      "scheduler": "simple",
+      "denoise": 1,
+      "model": [
+        "25",
+        0
+      ],
+      "positive": [
+        "3",
+        0
+      ],
+      "negative": [
+        "2",
+        0
+      ],
+      "latent_image": [
+        "15",
+        0
+      ]
+    },
+    "class_type": "KSampler",
+    "_meta": {
+      "title": "KSampler"
+    }
+  },
+  "12": {
+    "inputs": {
+      "unet_name": "flux1-dev-kontext_fp8_scaled.safetensors",
+      "weight_dtype": "default"
+    },
+    "class_type": "UNETLoader",
+    "_meta": {
+      "title": "Load Diffusion Model"
+    }
+  },
+  "13": {
+    "inputs": {
+      "clip_name1": "clip_l.safetensors",
+      "clip_name2": "t5xxl_fp8_e4m3fn_scaled.safetensors",
+      "type": "flux",
+      "device": "default"
+    },
+    "class_type": "DualCLIPLoader",
+    "_meta": {
+      "title": "DualCLIPLoader"
+    }
+  },
+  "14": {
+    "inputs": {
+      "direction": "right",
+      "match_image_size": true,
+      "image1": [
+        "29",
+        0
+      ],
+      "image2": [
+        "30",
+        0
+      ]
+    },
+    "class_type": "ImageConcanate",
+    "_meta": {
+      "title": "Image Concatenate"
+    }
+  },
+  "15": {
+    "inputs": {
+      "pixels": [
+        "20",
+        0
+      ],
+      "vae": [
+        "1",
+        0
+      ]
+    },
+    "class_type": "VAEEncode",
+    "_meta": {
+      "title": "VAE Encode"
+    }
+  },
+  "16": {
+    "inputs": {
+      "images": [
+        "20",
+        0
+      ]
+    },
+    "class_type": "PreviewImage",
+    "_meta": {
+      "title": "Preview Image"
+    }
+  },
+  "17": {
+    "inputs": {
+      "filename_prefix": "ComfyUI",
+      "images": [
+        "19",
+        0
+      ]
+    },
+    "class_type": "SaveImage",
+    "_meta": {
+      "title": "Save Image"
+    }
+  },
+  "18": {
+    "inputs": {
+      "text": "refcontrolpose change pose to photo with reference from left side",
+      "clip": [
+        "25",
+        1
+      ]
+    },
+    "class_type": "CLIPTextEncode",
+    "_meta": {
+      "title": "CLIP Text Encode (Positive Prompt)"
+    }
+  },
+  "19": {
+    "inputs": {
+      "samples": [
+        "9",
+        0
+      ],
+      "vae": [
+        "1",
+        0
+      ]
+    },
+    "class_type": "VAEDecode",
+    "_meta": {
+      "title": "VAE Decode"
+    }
+  },
+  "20": {
+    "inputs": {
+      "image": [
+        "14",
+        0
+      ]
+    },
+    "class_type": "FluxKontextImageScale",
+    "_meta": {
+      "title": "FluxKontextImageScale"
+    }
+  },
+  "21": {
+    "inputs": {
+      "filename_prefix": "ComfyUI",
+      "images": [
+        "23",
+        0
+      ]
+    },
+    "class_type": "SaveImage",
+    "_meta": {
+      "title": "Save Image"
+    }
+  },
+  "22": {
+    "inputs": {
+      "filename_prefix": "ComfyUI",
+      "images": [
+        "20",
+        0
+      ]
+    },
+    "class_type": "SaveImage",
+    "_meta": {
+      "title": "Save Image"
+    }
+  },
+  "23": {
+    "inputs": {
+      "direction": "down",
+      "match_image_size": true,
+      "image1": [
+        "20",
+        0
+      ],
+      "image2": [
+        "19",
+        0
+      ]
+    },
+    "class_type": "ImageConcanate",
+    "_meta": {
+      "title": "Image Concatenate"
+    }
+  },
+  "24": {
+    "inputs": {
+      "filename_prefix": "ComfyUI",
+      "images": [
+        "19",
+        0
+      ]
+    },
+    "class_type": "SaveImage",
+    "_meta": {
+      "title": "Save Image"
+    }
+  },
+  "25": {
+    "inputs": {
+      "lora_name": "refcontrol_pose.safetensors",
+      "strength_model": 1,
+      "strength_clip": 1,
+      "model": [
+        "12",
+        0
+      ],
+      "clip": [
+        "13",
+        0
+      ]
+    },
+    "class_type": "LoraLoader",
+    "_meta": {
+      "title": "Load LoRA (Model and CLIP)"
+    }
+  },
+  "28": {
+    "inputs": {
+      "image": "2.png"
+    },
+    "class_type": "LoadImage",
+    "_meta": {
+      "title": "Load Image (Pose)"
+    }
+  },
+  "29": {
+    "inputs": {
+      "image": "ref.jpg"
+    },
+    "class_type": "LoadImage",
+    "_meta": {
+      "title": "Load Image (Reference)"
+    }
+  },
+  "30": {
+    "inputs": {
+      "detect_hand": "enable",
+      "detect_body": "enable",
+      "detect_face": "enable",
+      "resolution": 1024,
+      "scale_stick_for_xinsr_cn": "disable",
+      "image": [
+        "28",
+        0
+      ]
+    },
+    "class_type": "OpenposePreprocessor",
+    "_meta": {
+      "title": "OpenPose Pose"
+    }
+  }
+}

+ 340 - 0
tests/run_comfy/run_workflow.py

@@ -0,0 +1,340 @@
+#!/usr/bin/env python3
+"""RunComfy Server API + ComfyUI Backend API
+
+流程:启动机器 → 上传 input/ 目录文件 → 提交 workflow → WebSocket 监听 → 下载结果 → 关机
+
+input/ 目录结构:
+    input/
+    ├── images/          → 上传到 ComfyUI input/(LoadImage 节点用)
+    ├── loras/           → 上传到 ComfyUI models/loras/
+    ├── checkpoints/     → 上传到 ComfyUI models/checkpoints/
+    ├── vae/             → 上传到 ComfyUI models/vae/
+    └── (其他文件)        → 上传到 ComfyUI input/
+
+用法:
+    python run_workflow.py workflow_api.json
+    python run_workflow.py workflow_api.json --input-dir ./input
+    python run_workflow.py workflow_api.json --input-dir ./input --server-type large
+"""
+
+import argparse
+import json
+import os
+import sys
+import time
+import urllib.parse
+import uuid
+from pathlib import Path
+
+import requests
+import websocket
+from dotenv import load_dotenv
+from check_workflow import analyze, check_files_exist
+
+load_dotenv(Path(__file__).parent.parent.parent / ".env")
+
+BASE_URL = "https://beta-api.runcomfy.net/prod/api"
+USER_ID = os.getenv("RUNCOMFY_USER_ID")
+API_TOKEN = os.getenv("API_TOKEN")
+
+HEADERS = {
+    "Authorization": f"Bearer {API_TOKEN}",
+    "Content-Type": "application/json",
+}
+
+DEFAULT_VERSION_ID = "90f77137-ba75-400d-870f-204c614ae8a3"  # RunComfy/ComfyUI-NodesLoaded
+
+# input/ 子目录 → ComfyUI 上传类型和 subfolder 映射
+# type="input" 对应 ComfyUI 的 input 目录
+# type="model" 暂无官方支持,lora 等模型走 subfolder 区分
+SUBDIR_UPLOAD_MAP = {
+    "images":      {"type": "input",  "subfolder": ""},
+    "loras":       {"type": "input",  "subfolder": "loras"},
+    "checkpoints": {"type": "input",  "subfolder": "checkpoints"},
+    "vae":         {"type": "input",  "subfolder": "vae"},
+    "controlnet":  {"type": "input",  "subfolder": "controlnet"},
+    "upscale":     {"type": "input",  "subfolder": "upscale_models"},
+}
+
+
+# ── 机器管理 ──────────────────────────────────────────────
+
+def launch_machine(version_id: str, server_type: str = "medium", duration: int = 3600) -> str:
+    payload = {
+        "workflow_version_id": version_id,
+        "server_type": server_type,
+        "estimated_duration": duration,
+    }
+    resp = requests.post(f"{BASE_URL}/users/{USER_ID}/servers", headers=HEADERS, json=payload)
+    if not resp.ok:
+        print(f"  HTTP {resp.status_code}: {resp.text}")
+    resp.raise_for_status()
+    print(f"  响应: {resp.json()}")
+    server_id = resp.json()["server_id"]
+    print(f"机器已创建: {server_id}  (type={server_type})")
+    return server_id
+
+
+def wait_for_ready(server_id: str, timeout: int = 300) -> str:
+    print("等待机器就绪...")
+    start = time.time()
+    while time.time() - start < timeout:
+        resp = requests.get(f"{BASE_URL}/users/{USER_ID}/servers/{server_id}", headers=HEADERS)
+        resp.raise_for_status()
+        data = resp.json()
+        status = data.get("current_status", "")
+        print(f"  状态: {status}")
+        if status == "Ready":
+            url = data["main_service_url"].rstrip("/")
+            print(f"  就绪: {url}")
+            return url
+        if status in ("Error", "Failed"):
+            raise Exception(f"机器启动失败: {status}")
+        time.sleep(5)
+    raise TimeoutError(f"等待超时 ({timeout}s)")
+
+
+def stop_machine(server_id: str):
+    resp = requests.delete(f"{BASE_URL}/users/{USER_ID}/servers/{server_id}", headers=HEADERS)
+    resp.raise_for_status()
+    print(f"机器已关闭: {server_id}")
+
+
+# ── 文件上传 ──────────────────────────────────────────────
+
+def upload_file(comfy_url: str, file_path: Path, file_type: str = "input", subfolder: str = "") -> str:
+    """上传文件到 ComfyUI,返回服务器上的实际文件名"""
+    with open(file_path, "rb") as f:
+        files = [("image", (file_path.name, f, "application/octet-stream"))]
+        data = {"overwrite": "true", "type": file_type, "subfolder": subfolder}
+        resp = requests.post(f"{comfy_url}/upload/image", data=data, files=files)
+    resp.raise_for_status()
+    server_name = resp.json()["name"]
+    subfolder_str = f" → {subfolder}/{server_name}" if subfolder else f" → {server_name}"
+    print(f"  上传: {file_path.name}{subfolder_str}")
+    return server_name
+
+
+def upload_input_dir(comfy_url: str, input_dir: Path) -> dict[str, str]:
+    """
+    扫描 input_dir,按子目录上传文件,返回 {原文件名: 服务器文件名} 映射
+    - input/images/  → type=input, subfolder=""
+    - input/loras/   → type=input, subfolder="loras"
+    - input/*.png    → type=input, subfolder=""(根目录文件)
+    """
+    if not input_dir.exists():
+        print(f"  input 目录不存在: {input_dir}")
+        return {}
+
+    uploaded = {}
+    IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".bmp"}
+    VIDEO_EXTS = {".mp4", ".avi", ".mov", ".webm"}
+    MODEL_EXTS = {".safetensors", ".ckpt", ".pt", ".pth", ".gguf"}
+    ALL_EXTS = IMAGE_EXTS | VIDEO_EXTS | MODEL_EXTS
+
+    # 根目录文件 → input
+    for f in input_dir.iterdir():
+        if f.is_file() and f.suffix.lower() in ALL_EXTS:
+            server_name = upload_file(comfy_url, f, "input", "")
+            uploaded[f.name] = server_name
+
+    # 子目录文件
+    for subdir in input_dir.iterdir():
+        if not subdir.is_dir():
+            continue
+        mapping = SUBDIR_UPLOAD_MAP.get(subdir.name, {"type": "input", "subfolder": subdir.name})
+        for f in subdir.iterdir():
+            if f.is_file() and f.suffix.lower() in ALL_EXTS:
+                server_name = upload_file(comfy_url, f, mapping["type"], mapping["subfolder"])
+                uploaded[f.name] = server_name
+
+    return uploaded
+
+
+# ── 提交 workflow ─────────────────────────────────────────
+
+def submit_prompt(comfy_url: str, workflow_api: dict, client_id: str) -> str:
+    payload = {"prompt": workflow_api, "client_id": client_id}
+    resp = requests.post(f"{comfy_url}/prompt", json=payload)
+    resp.raise_for_status()
+    data = resp.json()
+    if data.get("node_errors"):
+        print(f"  节点错误: {data['node_errors']}")
+    prompt_id = data["prompt_id"]
+    print(f"任务已提交: {prompt_id}")
+    return prompt_id
+
+
+# ── WebSocket 监听 ────────────────────────────────────────
+
+def wait_for_completion(comfy_url: str, client_id: str, prompt_id: str, timeout: int = 600):
+    scheme = "wss" if comfy_url.startswith("https") else "ws"
+    ws_url = f"{scheme}://{comfy_url.split('://', 1)[-1]}/ws?clientId={client_id}"
+    print("WebSocket 监听中...")
+
+    ws = websocket.WebSocket()
+    ws.settimeout(timeout)
+    ws.connect(ws_url)
+
+    try:
+        while True:
+            out = ws.recv()
+            if not out or isinstance(out, bytes):
+                continue
+            msg = json.loads(out)
+            msg_type = msg.get("type", "")
+            data = msg.get("data", {})
+
+            if msg_type == "executing":
+                node = data.get("node")
+                if data.get("prompt_id") == prompt_id and node is None:
+                    print("  执行完成")
+                    break
+                if node:
+                    print(f"  执行节点: {node}")
+
+            elif msg_type == "progress":
+                value = data.get("value", 0)
+                max_val = data.get("max", 1)
+                print(f"  进度: {value}/{max_val}")
+
+            elif msg_type == "execution_error":
+                if data.get("prompt_id") == prompt_id:
+                    raise Exception(f"执行错误: {data.get('exception_message', 'unknown')}")
+    finally:
+        ws.close()
+
+
+# ── 下载结果 ──────────────────────────────────────────────
+
+def download_outputs(comfy_url: str, prompt_id: str, output_dir: Path) -> list[str]:
+    resp = requests.get(f"{comfy_url}/history/{prompt_id}")
+    resp.raise_for_status()
+    data = resp.json().get(prompt_id, {})
+    outputs = data.get("outputs", {})
+
+    output_dir.mkdir(parents=True, exist_ok=True)
+    saved = []
+
+    for node_id, node_output in outputs.items():
+        if "images" in node_output:
+            for image in node_output["images"]:
+                params = {"filename": image["filename"], "subfolder": image.get("subfolder", ""), "type": image.get("temp") or image.get("type", "output")}
+                resp = requests.get(f"{comfy_url}/view?{urllib.parse.urlencode(params)}")
+                resp.raise_for_status()
+                out_path = output_dir / image["filename"]
+                out_path.write_bytes(resp.content)
+                print(f"  图片: {out_path}")
+                saved.append(str(out_path))
+
+        if "gifs" in node_output:
+            for video in node_output["gifs"]:
+                params = {"filename": video["filename"], "subfolder": video.get("subfolder", ""), "format": video.get("format", "mp4")}
+                resp = requests.get(f"{comfy_url}/view?{urllib.parse.urlencode(params)}")
+                resp.raise_for_status()
+                out_path = output_dir / video["filename"]
+                out_path.write_bytes(resp.content)
+                print(f"  视频: {out_path}")
+                saved.append(str(out_path))
+
+    return saved
+
+
+# ── 主流程 ────────────────────────────────────────────────
+
+def main():
+    parser = argparse.ArgumentParser(description="RunComfy workflow runner")
+    parser.add_argument("workflow", help="workflow_api.json 路径")
+    parser.add_argument("--input-dir", default="input", metavar="DIR",
+                        help="输入文件目录,默认 input/。子目录 images/loras/checkpoints/vae/ 自动上传到对应位置")
+    parser.add_argument("--version-id", default=DEFAULT_VERSION_ID, help="RunComfy workflow version_id")
+    parser.add_argument("--server-type", default="medium",
+                        choices=["medium", "large", "extra-large", "2x-large", "2xl-turbo"])
+    parser.add_argument("--duration", type=int, default=3600, help="预估运行时长(秒),默认3600")
+    parser.add_argument("--keep-alive", action="store_true", help="完成后不自动关机")
+    parser.add_argument("--server-id", metavar="ID", help="复用已有机器,跳过启动步骤")
+    parser.add_argument("--skip-upload", action="store_true", help="跳过文件上传,直接提交 workflow")
+    parser.add_argument("--output-dir", default="output", metavar="DIR", help="结果下载目录,默认 output/")
+    args = parser.parse_args()
+
+    if not USER_ID or not API_TOKEN:
+        print("ERROR: 请设置 RUNCOMFY_USER_ID 和 API_TOKEN 环境变量")
+        sys.exit(1)
+
+    print(f"USER_ID : {USER_ID}")
+    print(f"API_TOKEN: {API_TOKEN[:8]}...")
+
+    workflow_path = Path(args.workflow)
+    if not workflow_path.exists():
+        print(f"ERROR: 文件不存在: {workflow_path}")
+        sys.exit(1)
+
+    with open(workflow_path, "r", encoding="utf-8") as f:
+        workflow_api = json.load(f)
+
+    # 提交前 check
+    input_dir = Path(args.input_dir)
+    all_input_files = list(input_dir.rglob("*")) if input_dir.exists() else []
+    result = analyze(workflow_api)
+    missing = check_files_exist(result["file_inputs"], all_input_files)
+
+    if result["issues"] or missing:
+        print("❌ workflow 检查未通过:")
+        for issue in result["issues"]:
+            print(f"  {issue}")
+        for fi in missing:
+            print(f"  缺少文件: {fi['filename']}  (节点 [{fi['node_id']}] {fi['class_type']})")
+            print(f"  请将该文件放入 {input_dir}/ 目录")
+        sys.exit(1)
+
+    if result["widget_params"]:
+        print("⚠️  存在 widget_* 占位参数,参数名可能不准确,继续运行...")
+    else:
+        print("✓ workflow 检查通过")
+
+    client_id = str(uuid.uuid4())
+    server_id = args.server_id  # None if not provided
+
+    try:
+        # 1. 启动机器(或复用已有机器)
+        if server_id:
+            print(f"复用已有机器: {server_id}")
+            comfy_url = wait_for_ready(server_id)
+        else:
+            server_id = launch_machine(args.version_id, args.server_type, args.duration)
+            comfy_url = wait_for_ready(server_id)
+
+        # 2. 上传 input 目录
+        if args.skip_upload:
+            print("跳过文件上传 (--skip-upload)")
+        else:
+            print(f"\n上传 input 目录: {input_dir}")
+            upload_input_dir(comfy_url, input_dir)
+
+        # 3. 提交 workflow
+        print(f"\n提交 workflow...")
+        prompt_id = submit_prompt(comfy_url, workflow_api, client_id)
+
+        # 4. 监听执行进度
+        wait_for_completion(comfy_url, client_id, prompt_id)
+
+        # 5. 下载结果
+        print(f"\n下载结果...")
+        saved = download_outputs(comfy_url, prompt_id, Path(args.output_dir))
+        print(f"\n完成,共 {len(saved)} 个文件")
+
+        if args.keep_alive:
+            print(f"\n--keep-alive 模式,机器保持运行: {server_id}")
+            print(f"ComfyUI URL: {comfy_url}")
+        else:
+            print("\n关闭机器...")
+            stop_machine(server_id)
+
+    except Exception as e:
+        print(f"\n错误: {e}")
+        print(f"机器 {server_id} 未自动关闭,请手动处理")
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()

+ 4 - 0
tests/tasks/image_stitcher.json

@@ -0,0 +1,4 @@
+{
+  "description": "图片拼接工具:将多张图片按指定方式拼接成一张大图",
+  "task_spec": "请创建一个「图片拼接工具」,使用本地 uv 环境。\n\n## 功能需求\n将多张图片按指定方式拼接成一张大图。\n\n## 输入参数(POST /stitch,JSON Body)\n- images: list[str] — Base64 编码的图片列表(至少 2 张)\n- direction: str — 拼接方向,可选 \"horizontal\" | \"vertical\" | \"grid\",默认 \"horizontal\"\n- columns: int — grid 模式下每行列数,默认 2\n- spacing: int — 图片间距(像素),默认 0\n- background_color: str — 间距填充色,默认 \"#FFFFFF\"\n- resize_mode: str — \"none\" 不缩放 | \"fit_width\" 统一宽度 | \"fit_height\" 统一高度,默认 \"none\"\n\n## 输出(JSON)\n- image: str — 拼接结果,Base64 编码的 PNG\n- width: int — 结果图宽度\n- height: int — 结果图高度\n\n## 技术要求\n1. 使用 uv 环境,项目名 image_stitcher\n2. 核心依赖:Pillow\n3. HTTP 接口:FastAPI + uvicorn,端口通过 --port 参数指定\n4. 路由:POST /stitch(拼接)、GET /health(健康检查,返回 {\"status\":\"ok\"})\n5. 编写自测脚本 tests/test_stitch.py:生成纯色小图 -> 调用拼接函数 -> 验证输出尺寸,测试产物保存到 tests/output/\n6. 自测通过后注册,tool_id = \"image_stitcher\",runtime_type = \"local\",host_dir 填项目目录绝对路径\n\n## 注意\n- 这是 uv 本地项目,不需要 Docker\n- 测试阶段只验证核心逻辑(单元测试),不要启动 HTTP 服务器\n- 先跑通自测脚本再注册,确保核心逻辑正确"
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 2 - 0
tests/tasks/liblibai_controlnet.json


+ 4 - 0
tests/tasks/runcomfy_check_workflow.json

@@ -0,0 +1,4 @@
+{
+  "description": "ComfyUI workflow 输入检查工具:分析 workflow_api.json 所需的外部文件,检查 input 目录是否齐全",
+  "task_spec": "脚本位置:tests/run_comfy/check_workflow.py\n\n## 功能\n分析 workflow_api.json 中所有需要外部文件输入的节点,列出缺失文件,并检查节点连接完整性。\n\n## 用法\n```bash\npython check_workflow.py workflow_api.json\npython check_workflow.py workflow_api.json --input-dir ./my_input\n```\n\n## 参数\n- workflow(位置参数):workflow_api.json 路径\n- --input-dir:本地输入文件目录,默认 input/,递归扫描所有子目录\n\n## 检查内容\n1. 文件输入节点(LoadImage、LoadVideo、LoadImageMask 等)所需的文件是否存在于 input 目录\n2. 节点连接是否完整(引用的 src_node_id 是否存在)\n3. 是否存在 widget_* 占位参数(参数名不准确的警告)\n\n## 输出\n- ✓ 检查通过:所有文件齐全,连接完整\n- ❌ 检查未通过:列出缺失文件和连接问题\n- ⚠️ 警告:存在 widget_* 占位参数\n\n## 支持的文件输入节点类型\n- LoadImage → image 参数\n- LoadVideo / LoadVideoPath / VHS_LoadVideo → video 参数\n- LoadImageMask → image 参数\n- LoadImageOutput → image 参数(从 output 目录加载,特殊情况)\n- VHS_LoadImages → directory 参数"
+}

+ 4 - 0
tests/tasks/runcomfy_convert_workflow.json

@@ -0,0 +1,4 @@
+{
+  "description": "ComfyUI workflow 格式转换工具:将 ComfyUI UI 导出的 workflow.json 转换为可通过 API 提交的 workflow_api.json",
+  "task_spec": "脚本位置:tests/run_comfy/convert_workflow.py\n\n## 功能\n将 ComfyUI 界面导出的 workflow.json(UI格式)转换为 API 可直接提交的 workflow_api.json(API格式)。\n\n## 两种格式的区别\n- UI格式:nodes[] 数组,每个节点含 id/type/pos/size/color/widgets_values/links 等 UI 信息\n- API格式:以 node_id 为 key 的对象,每个节点只含 class_type 和 inputs(参数值或连接引用)\n\n## 用法\n```bash\npython convert_workflow.py workflow.json\npython convert_workflow.py workflow.json -o output_api.json\n```\n\n## 参数\n- workflow(位置参数):输入的 UI 格式 workflow.json 路径\n- -o / --output:输出路径,默认在原文件名基础上加 _api 后缀\n\n## 转换规则\n1. 跳过纯 UI 节点(Note、MarkdownNote、PrimitiveNode)\n2. 跳过 mode != 0 的禁用节点\n3. 节点连接通过 links[] 映射还原为 [src_node_id, src_slot] 格式\n4. widgets_values 按顺序填入未连线的输入参数,参数名用 widget_0/widget_1/... 占位\n\n## 注意\n- widget_* 占位参数名不准确,建议直接在 ComfyUI 界面使用「Save (API Format)」按钮导出,可获得准确参数名\n- 转换后建议用 check_workflow.py 验证"
+}

+ 7 - 0
tests/tasks/runcomfy_launch_env.json

@@ -0,0 +1,7 @@
+{
+  "description": "RunComfy 机器管理工具:用于按需启动和获取云端 ComfyUI 机器的访问地址",
+  "task_spec": "脚本位置:tests/run_comfy/launch_comfy_env.py\n\n## 功能\n调用 RunComfy Server API 启动指定版本的云端机器,并轮询等待机器 Ready。\n这个工具是使用 ComfyUI 的基础设施,启动后需要记录 server_id 以便后续使用和关闭。\n\n## 环境变量\n- RUNCOMFY_USER_ID:RunComfy 用户 ID\n- API_TOKEN:RunComfy API Token\n\n## 命令行参数\n- --version-id:RunComfy workflow version ID,默认 \"90f77137-ba75-400d-870f-204c614ae8a3\"\n- --server-type:机器规格(medium/large/extra-large/2x-large/2xl-turbo),默认 \"medium\"\n- --duration:预估运行时长(秒),默认 3600\n- --timeout:启动超时时间(秒),默认 300\n\n## HTTP 接口需求(用于注册到 Router)\n如果是通过 FastAPI 暴露接口,需要实现 `POST /launch`:\n### 输入 JSON\n- version_id (str, 可选)\n- server_type (str, 可选)\n- duration (int, 可选)\n\n### 输出 JSON\n- server_id: str (机器唯一标识,用于后续操作)\n- comfy_url: str (机器访问地址)\n- status: str (\"Ready\" 或报错信息)\n- usage_instruction: str (返回给大模型的提示词,告知它如何使用这台机器,例如:“请使用 run_comfy_workflow 工具,并传入此 server_id 以及你的 workflow_api.json 来生成图片。用完后请务必调用 stop_comfy_env 工具关闭机器。”)\n\n## 核心逻辑参考\n1. API: `POST https://beta-api.runcomfy.net/prod/api/users/{USER_ID}/servers`\n   Body: `{\"workflow_version_id\": version_id, \"server_type\": server_type, \"estimated_duration\": duration}`\n2. 轮询 API: `GET https://beta-api.runcomfy.net/prod/api/users/{USER_ID}/servers/{server_id}`\n3. Header 需带 `Authorization: Bearer {API_TOKEN}`\n4. 等待字段 `current_status` 变为 `Ready`,并提取 `main_service_url`。",
+  "reference_files": [
+    "tests/run_comfy/run_workflow.py"
+  ]
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 2 - 0
tests/tasks/runcomfy_run_only.json


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 2 - 0
tests/tasks/runcomfy_run_workflow.json


+ 7 - 0
tests/tasks/runcomfy_stop_env.json

@@ -0,0 +1,7 @@
+{
+  "description": "RunComfy 环境销毁工具:根据 server_id 主动关闭已启动的 ComfyUI 机器,避免持续计费",
+  "task_spec": "脚本位置:tests/run_comfy/stop_comfy_env.py\n\n## 功能\n调用 RunComfy Server API 删除指定的机器实例,释放资源。配合 `launch_comfy_env` 使用,负责生命周期的收尾阶段。\n\n## 环境变量\n- RUNCOMFY_USER_ID:RunComfy 用户 ID\n- API_TOKEN:RunComfy API Token\n\n## 命令行参数\n- --server-id:(必填) 要关闭的机器 ID\n\n## HTTP 接口需求(用于注册到 Router)\n如果作为服务接口,实现 `POST /stop`:\n### 输入 JSON\n- server_id: str (必填,需关闭的机器)\n\n### 输出 JSON\n- server_id: str (关闭的机器 ID)\n- status: str (\"Deleted\" 或报错信息)\n- message: str (详细结果文本)\n\n## 核心逻辑参考\n1. 调用 API `DELETE https://beta-api.runcomfy.net/prod/api/users/{USER_ID}/servers/{server_id}`\n2. Header 需要 `Authorization: Bearer {API_TOKEN}`\n3. 处理 200 响应和 404 响应(说明已被清理或不存在),如果是其它错误,则抛出异常并返回失败状态。\n4. 这个工具应始终返回安全的明确状态,供 Agent 确认释放情况。",
+  "reference_files": [
+    "tests/run_comfy/run_workflow.py"
+  ]
+}

binární
tests/tasks/stitcher_images/01.png


binární
tests/tasks/stitcher_images/02.png


binární
tests/tasks/stitcher_images/03.png


binární
tests/tasks/stitcher_images/04.png


binární
tests/tasks/stitcher_images/05.png


+ 94 - 0
tests/test_create_runcomfy_atomic.py

@@ -0,0 +1,94 @@
+"""批量触发生成三个 RunComfy 原子化工具
+
+用法:
+    uv run python tests/test_create_runcomfy_atomic.py
+"""
+
+import sys
+import time
+from pathlib import Path
+
+import httpx
+
+BASE_URL = "http://127.0.0.1:8001"
+TASKS_DIR = Path(__file__).parent / "tasks"
+
+# 我们刚才写的三个原子化任务书
+TASKS = [
+    "runcomfy_launch_env",
+    "runcomfy_run_only",
+    "runcomfy_stop_env"
+]
+
+def check_connection():
+    try:
+        httpx.get(f"{BASE_URL}/health", timeout=3)
+    except httpx.ConnectError:
+        print(f"ERROR: Cannot connect to {BASE_URL}")
+        print("Please start the service first:")
+        print("  uv run python -m tool_agent")
+        sys.exit(1)
+
+def submit_task(task_name: str) -> str:
+    task_file = TASKS_DIR / f"{task_name}.json"
+    if not task_file.exists():
+        print(f"ERROR: Task file not found: {task_file}")
+        sys.exit(1)
+
+    import json
+    with open(task_file, "r", encoding="utf-8") as f:
+        task_data = json.load(f)
+
+    print(f"\n[{task_name}] Submitting...")
+    resp = httpx.post(f"{BASE_URL}/create_tool", json=task_data, timeout=30)
+    resp.raise_for_status()
+    data = resp.json()
+    task_id = data["task_id"]
+    print(f"[{task_name}] Task ID: {task_id}")
+    return task_id
+
+def poll_tasks(task_ids: dict[str, str], timeout: int = 900):
+    print("\n=== Polling Tasks ===")
+    pending = set(task_ids.values())
+
+    interval = 10
+    steps = timeout // interval
+
+    for i in range(steps):
+        if not pending:
+            print("\nAll tasks finished!")
+            break
+
+        time.sleep(interval)
+        elapsed = (i + 1) * interval
+
+        for task_name, task_id in list(task_ids.items()):
+            if task_id not in pending:
+                continue
+
+            resp = httpx.get(f"{BASE_URL}/tasks/{task_id}", timeout=30)
+            status = resp.json()["status"]
+
+            if status in ("completed", "failed"):
+                print(f"\n[{task_name}] Finished with status: {status}")
+                pending.remove(task_id)
+            elif elapsed % 30 == 0:
+                print(f"[{elapsed}s] {task_name}: {status}")
+
+    if pending:
+        print(f"\nTimeout! Still pending: {pending}")
+
+def main():
+    check_connection()
+
+    # 1. 批量提交
+    task_ids = {}
+    for task_name in TASKS:
+        task_id = submit_task(task_name)
+        task_ids[task_name] = task_id
+
+    # 2. 并行轮询
+    poll_tasks(task_ids)
+
+if __name__ == "__main__":
+    main()

+ 10 - 0
tests/test_dispatcher.py

@@ -0,0 +1,10 @@
+"""测试 Dispatcher"""
+
+import pytest
+from tool_agent.router.dispatcher import Dispatcher
+
+
+class TestDispatcher:
+    def test_dispatcher_init(self):
+        d = Dispatcher()
+        assert d is not None

+ 97 - 0
tests/test_docker_git.py

@@ -0,0 +1,97 @@
+"""测试 DockerRunner: 创建容器 → 安装配置 git → clone 仓库 → 清理"""
+
+import sys
+import logging
+
+logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s: %(message)s")
+logger = logging.getLogger("test_docker_git")
+
+
+def main():
+    from tool_agent.runtime.docker_runner import DockerRunner
+
+    runner = DockerRunner(lazy_init=True)
+
+    # ---- Step 1: 创建容器 ----
+    logger.info("=" * 60)
+    logger.info("Step 1: 创建容器 (ubuntu:22.04)")
+    result = runner.create_container(
+        tool_id="test_git_tool",
+        image="ubuntu:22.04",
+        mem_limit="512m",
+        nano_cpus=1_000_000_000,
+    )
+
+    if "error" in result:
+        logger.error(f"创建容器失败: {result['error']}")
+        sys.exit(1)
+
+    container_id = result["container_id"]
+    logger.info(f"容器已创建: {container_id[:12]}")
+
+    try:
+        # ---- Step 2: 安装 git ----
+        logger.info("=" * 60)
+        logger.info("Step 2: 安装 git")
+        res = runner.run_command(
+            container_id,
+            "apt-get update -qq && apt-get install -y -qq git > /dev/null 2>&1 && git --version",
+            timeout=120,
+        )
+        if res.get("exit_code", -1) != 0:
+            logger.error(f"安装 git 失败: {res}")
+            sys.exit(1)
+        logger.info(f"git 安装成功: {res['stdout'].strip()}")
+
+        # ---- Step 3: 配置 git ----
+        logger.info("=" * 60)
+        logger.info("Step 3: 配置 git 用户")
+        res = runner.run_command(
+            container_id,
+            'git config --global user.name "Tool Agent" && '
+            'git config --global user.email "agent@tool-agent.local" && '
+            'git config --global --list',
+        )
+        if res.get("exit_code", -1) != 0:
+            logger.error(f"配置 git 失败: {res}")
+            sys.exit(1)
+        logger.info(f"git 配置:\n{res['stdout'].strip()}")
+
+        # ---- Step 4: clone 仓库 ----
+        logger.info("=" * 60)
+        logger.info("Step 4: git clone 仓库")
+        res = runner.run_command(
+            container_id,
+            "cd /app && git clone --depth 1 https://github.com/pallets/flask.git",
+            timeout=120,
+        )
+        if res.get("exit_code", -1) != 0:
+            logger.error(f"clone 失败: {res}")
+            sys.exit(1)
+        logger.info("clone 完成")
+
+        # ---- Step 5: 验证 clone 结果 ----
+        logger.info("=" * 60)
+        logger.info("Step 5: 验证 clone 结果")
+        res = runner.run_command(
+            container_id,
+            "ls -la /app/flask/ && echo '---' && cd /app/flask && git log --oneline -3",
+        )
+        if res.get("exit_code", -1) != 0:
+            logger.error(f"验证失败: {res}")
+            sys.exit(1)
+        logger.info(f"仓库内容:\n{res['stdout'].strip()}")
+
+        logger.info("=" * 60)
+        logger.info("ALL TESTS PASSED!")
+
+    finally:
+        # ---- Step 6: 清理 ----
+        logger.info("=" * 60)
+        logger.info("Step 6: 销毁容器")
+        cleanup = runner.destroy_container(container_id)
+        logger.info(f"清理结果: {cleanup}")
+
+
+if __name__ == "__main__":
+    main()

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů