guantao пре 12 часа
родитељ
комит
a38fa1439b

+ 6 - 0
agent/tools/builtin/__init__.py

@@ -20,6 +20,7 @@ from agent.tools.builtin.sandbox import (sandbox_create_environment, sandbox_run
                                          sandbox_rebuild_with_ports,sandbox_destroy_environment)
 from agent.tools.builtin.knowledge import(knowledge_search,knowledge_save,knowledge_list,knowledge_update,knowledge_batch_update,knowledge_slim)
 from agent.tools.builtin.context import get_current_context
+from agent.tools.builtin.toolhub import toolhub_health, toolhub_search, toolhub_call, toolhub_create
 from agent.trace.goal_tool import goal
 # 导入浏览器工具以触发注册
 import agent.tools.builtin.browser  # noqa: F401
@@ -54,6 +55,11 @@ __all__ = [
     "sandbox_destroy_environment",
     # 上下文工具
     "get_current_context",
+    # ToolHub 远程工具库
+    "toolhub_health",
+    "toolhub_search",
+    "toolhub_call",
+    "toolhub_create",
     # Goal 管理
     "goal",
 ]

+ 362 - 0
agent/tools/builtin/toolhub.py

@@ -0,0 +1,362 @@
+"""
+ToolHub - 远程工具库集成模块
+
+将 http://43.106.118.91:8001 的工具库 API 包装为 Agent 可调用的工具。
+提供四个工具:
+1. toolhub_health        - 健康检查
+2. toolhub_search        - 搜索/发现远程工具
+3. toolhub_call          - 调用远程工具
+4. toolhub_create        - 创建新远程工具(异步)
+
+设计要点:
+- toolhub_call 的 params 是动态的(取决于 tool_id),用 dict 类型兼容所有工具
+- toolhub_create 是异步任务,支持轮询等待完成
+- 所有接口返回 ToolResult,包含结构化输出和 long_term_memory 摘要
+"""
+
+import json
+import time
+from typing import Any, Dict, Optional
+
+import httpx
+
+from agent.tools import tool, ToolResult
+
+
+# ── 配置 ─────────────────────────────────────────────
+
+TOOLHUB_BASE_URL = "http://43.106.118.91:8001"
+DEFAULT_TIMEOUT = 30.0
+CREATE_POLL_TIMEOUT = 600.0   # create 最长轮询 10 分钟
+CREATE_POLL_INTERVAL = 5.0    # 每 5 秒轮询一次
+
+
+# ── 工具实现 ──────────────────────────────────────────
+
+@tool(
+    display={
+        "zh": {"name": "ToolHub 健康检查", "params": {}},
+        "en": {"name": "ToolHub Health Check", "params": {}},
+    }
+)
+async def toolhub_health() -> ToolResult:
+    """检查 ToolHub 远程工具库服务是否可用
+
+    检查 ToolHub 服务的健康状态,确认服务是否正常运行。
+    建议在调用其他 toolhub 工具之前先检查。
+
+    Returns:
+        ToolResult 包含服务健康状态信息
+    """
+    try:
+        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
+            resp = await client.get(f"{TOOLHUB_BASE_URL}/health")
+            resp.raise_for_status()
+            data = resp.json()
+
+        return ToolResult(
+            title="ToolHub 健康检查",
+            output=json.dumps(data, ensure_ascii=False, indent=2),
+            long_term_memory=f"ToolHub service at {TOOLHUB_BASE_URL} is healthy.",
+        )
+    except httpx.ConnectError:
+        return ToolResult(
+            title="ToolHub 健康检查",
+            output="",
+            error=f"无法连接到 ToolHub 服务 {TOOLHUB_BASE_URL},请确认服务已启动。",
+        )
+    except Exception as e:
+        return ToolResult(
+            title="ToolHub 健康检查",
+            output="",
+            error=str(e),
+        )
+
+
+@tool(
+    display={
+        "zh": {"name": "搜索 ToolHub 工具", "params": {"keyword": "搜索关键词"}},
+        "en": {"name": "Search ToolHub", "params": {"keyword": "Search keyword"}},
+    }
+)
+async def toolhub_search(keyword: Optional[str] = None) -> ToolResult:
+    """搜索 ToolHub 远程工具库中可用的工具
+
+    从 ToolHub 工具库中搜索可用工具,返回每个工具的完整信息,包括:
+    tool_id、名称、分类、状态、参数列表(含类型、是否必填、默认值、枚举值)、输出 schema 等。
+
+    调用 toolhub_call 之前,应先使用此工具了解目标工具的 tool_id 和所需参数。
+
+    Args:
+        keyword: 搜索关键词,为空则返回所有工具
+
+    Returns:
+        ToolResult 包含匹配的工具列表及其参数说明
+    """
+    try:
+        payload = {"keyword": keyword} if keyword else {}
+        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
+            resp = await client.post(
+                f"{TOOLHUB_BASE_URL}/search_tools", json=payload
+            )
+            resp.raise_for_status()
+            data = resp.json()
+
+        total = data.get("total", 0)
+        tools = data.get("tools", [])
+
+        # 构建给 LLM 的结构化摘要
+        summaries = []
+        for t in tools:
+            params_desc = []
+            for p in t.get("params", []):
+                req = "必填" if p["required"] else "可选"
+                desc = p.get("description", "")
+                default_str = f", 默认={p['default']}" if p.get("default") is not None else ""
+                enum_str = f", 可选值={p['enum']}" if p.get("enum") else ""
+                params_desc.append(
+                    f"  - {p['name']} ({p['type']}, {req}): {desc}{default_str}{enum_str}"
+                )
+
+            tool_block = (
+                f"[{t['tool_id']}] {t['name']}\n"
+                f"  状态: {t['state']}\n"
+                f"  描述: {t.get('description', '')}\n"
+                f"  流式: {t.get('stream_support', False)}"
+            )
+            if params_desc:
+                tool_block += "\n  参数:\n" + "\n".join(params_desc)
+            else:
+                tool_block += "\n  参数: 无"
+
+            summaries.append(tool_block)
+
+        output_text = f"共找到 {total} 个工具:\n\n" + "\n\n".join(summaries)
+
+        # 附上完整 JSON 供精确引用
+        full_json = json.dumps(data, ensure_ascii=False, indent=2)
+
+        return ToolResult(
+            title=f"ToolHub 搜索{f': {keyword}' if keyword else ''}",
+            output=f"{output_text}\n\n--- 完整数据 ---\n{full_json}",
+            long_term_memory=(
+                f"ToolHub {'搜索 ' + repr(keyword) + ' ' if keyword else ''}"
+                f"共 {total} 个工具: "
+                + ", ".join(t["tool_id"] for t in tools[:10])
+                + ("..." if total > 10 else "")
+            ),
+        )
+    except Exception as e:
+        return ToolResult(
+            title="搜索 ToolHub 工具失败",
+            output="",
+            error=str(e),
+        )
+
+
+@tool(
+    display={
+        "zh": {
+            "name": "调用 ToolHub 工具",
+            "params": {"tool_id": "工具ID", "params": "工具参数"},
+        },
+        "en": {
+            "name": "Call ToolHub Tool",
+            "params": {"tool_id": "Tool ID", "params": "Tool parameters"},
+        },
+    }
+)
+async def toolhub_call(
+    tool_id: str,
+    params: Optional[Dict[str, Any]] = None,
+) -> ToolResult:
+    """调用 ToolHub 远程工具库中的指定工具
+
+    通过 tool_id 调用 ToolHub 工具库中的某个工具,传入该工具所需的参数。
+    不同工具的参数不同,请先用 toolhub_search 查询目标工具的参数说明。
+
+    参数通过 params 字典传入,键名和类型需与工具定义一致。
+    例如调用图片拼接工具:
+        tool_id="image_stitcher"
+        params={"images": [...], "direction": "grid", "columns": 2}
+
+    Args:
+        tool_id: 要调用的工具 ID(从 toolhub_search 获取)
+        params: 工具参数字典,键值对根据目标工具的参数定义决定
+
+    Returns:
+        ToolResult 包含工具执行结果
+    """
+    try:
+        payload = {
+            "tool_id": tool_id,
+            "params": params or {},
+        }
+        async with httpx.AsyncClient(timeout=60.0) as client:
+            resp = await client.post(
+                f"{TOOLHUB_BASE_URL}/select_tool", json=payload
+            )
+            resp.raise_for_status()
+            data = resp.json()
+
+        status = data.get("status")
+        if status == "success":
+            result = data.get("result", {})
+            result_str = json.dumps(result, ensure_ascii=False, indent=2)
+
+            # 如果结果中有 base64 图片,提取为 images 附件
+            images = []
+            if isinstance(result, dict) and result.get("image"):
+                images.append({
+                    "type": "base64",
+                    "media_type": "image/png",
+                    "data": result["image"],
+                })
+                # 输出中替换超长 base64,避免占满 context
+                result_display = {k: v for k, v in result.items() if k != "image"}
+                result_display["image"] = f"<base64 image, {len(result['image'])} chars>"
+                result_str = json.dumps(result_display, ensure_ascii=False, indent=2)
+
+            return ToolResult(
+                title=f"ToolHub [{tool_id}] 执行成功",
+                output=result_str,
+                long_term_memory=f"Called ToolHub tool '{tool_id}' → success",
+                images=images,
+            )
+        else:
+            error_msg = data.get("error", "未知错误")
+            return ToolResult(
+                title=f"ToolHub [{tool_id}] 执行失败",
+                output=json.dumps(data, ensure_ascii=False, indent=2),
+                error=error_msg,
+            )
+    except httpx.TimeoutException:
+        return ToolResult(
+            title=f"ToolHub [{tool_id}] 调用超时",
+            output="",
+            error=f"调用工具 {tool_id} 超时(60s),工具可能需要更长处理时间。",
+        )
+    except Exception as e:
+        return ToolResult(
+            title=f"ToolHub [{tool_id}] 调用失败",
+            output="",
+            error=str(e),
+        )
+
+
+@tool(
+    display={
+        "zh": {
+            "name": "创建 ToolHub 工具",
+            "params": {"description": "工具描述", "wait": "是否等待完成"},
+        },
+        "en": {
+            "name": "Create ToolHub Tool",
+            "params": {"description": "Tool description", "wait": "Wait for completion"},
+        },
+    }
+)
+async def toolhub_create(
+    description: str,
+    wait: bool = True,
+) -> ToolResult:
+    """在 ToolHub 远程工具库中创建一个新工具
+
+    向 ToolHub 提交创建工具的请求。ToolHub 会根据描述自动生成工具代码、
+    构建运行环境并注册到工具库中。
+
+    创建过程是异步的:提交后返回 task_id,可通过轮询查看进度。
+    设置 wait=True(默认)会自动轮询直到完成或超时(10分钟)。
+
+    Args:
+        description: 工具的自然语言描述,说明工具应该做什么。
+            例如:"创建一个简单的文本计数工具,输入文本,返回字数和字符数"
+        wait: 是否等待任务完成。True=轮询等待结果(最长10分钟),
+            False=仅提交并返回 task_id
+
+    Returns:
+        ToolResult 包含创建结果或任务进度信息
+    """
+    try:
+        payload = {"description": description}
+
+        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
+            resp = await client.post(
+                f"{TOOLHUB_BASE_URL}/create_tool", json=payload
+            )
+            resp.raise_for_status()
+            data = resp.json()
+
+        task_id = data.get("task_id")
+        status = data.get("status")
+
+        if not task_id:
+            return ToolResult(
+                title="创建 ToolHub 工具失败",
+                output=json.dumps(data, ensure_ascii=False, indent=2),
+                error="未返回 task_id",
+            )
+
+        # 不等待,直接返回 task_id
+        if not wait:
+            return ToolResult(
+                title="ToolHub 工具创建已提交",
+                output=f"task_id: {task_id}\nstatus: {status}",
+                long_term_memory=f"Submitted ToolHub creation task {task_id}: {description[:80]}",
+            )
+
+        # 轮询等待完成
+        start_time = time.time()
+        async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
+            while time.time() - start_time < CREATE_POLL_TIMEOUT:
+                await _async_sleep(CREATE_POLL_INTERVAL)
+
+                resp = await client.get(
+                    f"{TOOLHUB_BASE_URL}/tasks/{task_id}", timeout=DEFAULT_TIMEOUT
+                )
+                task = resp.json()
+                task_status = task.get("status")
+
+                if task_status == "completed":
+                    result = task.get("result", "")
+                    return ToolResult(
+                        title="ToolHub 工具创建完成",
+                        output=(
+                            f"task_id: {task_id}\n"
+                            f"result: {str(result)[:500]}\n\n"
+                            f"耗时: {time.time() - start_time:.0f}s\n"
+                            f"可使用 toolhub_search 查看新工具。"
+                        ),
+                        long_term_memory=f"ToolHub tool created (task {task_id}): {description[:60]}",
+                    )
+
+                if task_status == "failed":
+                    error = task.get("error", "unknown")
+                    return ToolResult(
+                        title="ToolHub 工具创建失败",
+                        output=json.dumps(task, ensure_ascii=False, indent=2),
+                        error=f"Task {task_id} failed: {error}",
+                    )
+
+        # 超时
+        elapsed = time.time() - start_time
+        return ToolResult(
+            title="ToolHub 工具创建超时",
+            output=f"task_id: {task_id}\n轮询 {elapsed:.0f}s 后仍未完成。",
+            error=f"Task {task_id} 未在 {CREATE_POLL_TIMEOUT:.0f}s 内完成,请稍后查询。",
+        )
+
+    except Exception as e:
+        return ToolResult(
+            title="创建 ToolHub 工具失败",
+            output="",
+            error=str(e),
+        )
+
+
+# ── 辅助函数 ─────────────────────────────────────────
+
+async def _async_sleep(seconds: float):
+    """异步 sleep"""
+    import asyncio
+    await asyncio.sleep(seconds)

+ 67 - 0
examples/restore/config.py

@@ -0,0 +1,67 @@
+"""
+项目配置
+
+定义项目的运行配置。
+"""
+
+from agent.core.runner import KnowledgeConfig, RunConfig
+
+
+# ===== Agent 运行配置 =====
+
+RUN_CONFIG = RunConfig(
+    # 模型配置
+    model="qwen3.5-plus",
+    temperature=0.3,
+    max_iterations=1000,
+
+    # 启用 thinking 模式
+    extra_llm_params={"extra_body": {"enable_thinking": True}},
+
+    # Agent 预设(对应 presets.json 中的 "main")
+    agent_type="main",
+
+    # 任务名称
+    name="制作工序调研与制定",
+
+    # 知识管理配置
+    knowledge=KnowledgeConfig(
+        # 压缩时提取(消息量超阈值触发压缩时,用完整 history 反思)
+        enable_extraction=False,
+        reflect_prompt="",  # 自定义反思 prompt;空则使用默认,见 agent/core/prompts/knowledge.py:REFLECT_PROMPT
+
+        # agent运行完成后提取(不代表任务完成,agent 可能中途退出等待人工评估)
+        enable_completion_extraction=True,
+        completion_reflect_prompt="",  # 自定义复盘 prompt;空则使用默认,见 agent/core/prompts/knowledge.py:COMPLETION_REFLECT_PROMPT
+
+        # 知识注入(agent切换当前工作的goal时,自动注入相关知识)
+        enable_injection=False,
+
+        # 默认字段(保存/搜索时自动注入)
+        owner="sunlit.howard@gmail.com",  # 所有者(空则尝试从 git config user.email 获取,再空则用 agent:{agent_id})
+        default_tags={"project": "production_plan", "domain": "ai_agent"},  # 默认 tags(会与工具调用参数合并)
+        default_scopes=["org:cybertogether"],  # 默认 scopes
+        default_search_types=["strategy"],  # 默认搜索类型过滤
+        default_search_owner="sunlit.howard@gmail.com"  # 默认搜索 owner 过滤(空则不过滤)
+    )
+)
+
+
+# ===== 任务配置 =====
+
+INPUT_DIR = "examples/production_plan/huahua"       # 输入素材目录
+OUTPUT_DIR = "examples/production_plan/outputs/huahua_3"                   # 输出目录 ID,输出保存在 examples/plan/outputs/{OUTPUT_DIR}/
+
+
+# ===== 基础设施配置 =====
+
+SKILLS_DIR = "./skills"
+TRACE_STORE_PATH = ".trace"
+DEBUG = True
+LOG_LEVEL = "INFO"
+LOG_FILE = None  # 设置为文件路径可以同时输出到文件
+
+# ===== 浏览器配置 =====
+# 可选值: "cloud" (云浏览器) 或 "local" (本地浏览器) 或 "container" (容器浏览器,支持预配置账户)
+BROWSER_TYPE = "local"
+HEADLESS = False

+ 132 - 0
examples/restore/input/strategy_0.json

@@ -0,0 +1,132 @@
+{
+  "选定策略": {
+    "策略名称": "ComfyUI + FLUX 工作流驱动的多阶段增强策略",
+    "来源": "knowledge-20260317-192117-fbe1, knowledge-20260317-003013-ce1b, https://github.com/lllyasviel/IC-Light, https://github.com/kijai/ComfyUI-IC-Light",
+    "工作流概要": [
+      "阶段 1 - 角色设定:使用 NanoBanana Pro 生成 6 视角 Character Sheet(正面、左右 45°、左右 90°、背面),建立角色一致性基准",
+      "阶段 2 - 基础生成:使用 ComfyUI + FLUX.1 Kontext 生成主图,配合 InstantID 保持角色面部一致性,ControlNet OpenPose 控制绘画姿态",
+      "阶段 3 - 细节增强:使用 ADetailer 自动修复人脸/手部,Impasto 提示词生成调色板颜料厚涂质感,ControlNet Tile 保持服装褶皱结构",
+      "阶段 4 - 光影控制:使用 IC-Light 添加逆光/轮廓光效果,Flux Tools Depth 控制前景清晰背景虚化",
+      "阶段 5 - 画中画生成:使用 ControlNet Inpaint 局部重绘生成画布内容,Midjourney 印象派风格提示词生成画中画油画"
+    ],
+    "工具列表": [
+      {
+        "工具名称": "FLUX.1 Kontext",
+        "角色": "基础图像生成(写实人物、场景)",
+        "阶段": "阶段 2",
+        "可信度": 9,
+        "备选工具": ["Nano Banana Pro", "Midjourney V7"]
+      },
+      {
+        "工具名称": "ComfyUI",
+        "角色": "工作流编排与节点控制",
+        "阶段": "全阶段",
+        "可信度": 9,
+        "备选工具": ["AUTOMATIC1111 SD WebUI"]
+      },
+      {
+        "工具名称": "NanoBanana Pro Character Sheet",
+        "角色": "角色一致性控制(生成 6 视角角色表)",
+        "阶段": "阶段 1",
+        "可信度": 9,
+        "备选工具": ["InstantID", "LoRA 训练"]
+      },
+      {
+        "工具名称": "InstantID",
+        "角色": "面部一致性保持(零样本 90%+)",
+        "阶段": "阶段 2",
+        "可信度": 9,
+        "备选工具": ["IP-Adapter FaceID Plus v2", "LoRA 训练"]
+      },
+      {
+        "工具名称": "ControlNet OpenPose",
+        "角色": "姿态控制(专注绘画的动态姿态)",
+        "阶段": "阶段 2",
+        "可信度": 8,
+        "备选工具": ["ControlNet Depth", "ControlNet Canny"]
+      },
+      {
+        "工具名称": "ADetailer",
+        "角色": "细节修复(人脸、手部自动检测与修复)",
+        "阶段": "阶段 3",
+        "可信度": 9,
+        "备选工具": ["SUPIR", "ComfyUI FaceDetailer"]
+      },
+      {
+        "工具名称": "Impasto 提示词方案",
+        "角色": "调色板颜料厚涂质感生成",
+        "阶段": "阶段 3",
+        "可信度": 8,
+        "备选工具": ["厚涂风格 LoRA"]
+      },
+      {
+        "工具名称": "ControlNet Tile",
+        "角色": "服装褶皱结构保持",
+        "阶段": "阶段 3",
+        "可信度": 8,
+        "备选工具": ["ControlNet Canny", "ControlNet Depth"]
+      },
+      {
+        "工具名称": "IC-Light",
+        "角色": "逆光/轮廓光效果生成",
+        "阶段": "阶段 4",
+        "可信度": 9,
+        "备选工具": ["Banana 2 深度图 + 法线图工作流", "ComfyUI_Relight_Img"]
+      },
+      {
+        "工具名称": "Flux Tools Depth",
+        "角色": "前景清晰背景虚化控制",
+        "阶段": "阶段 4",
+        "可信度": 8,
+        "备选工具": ["ControlNet Depth Anything", "Nano Banana Pro 景深参数"]
+      },
+      {
+        "工具名称": "ControlNet Inpaint",
+        "角色": "画布区域局部重绘",
+        "阶段": "阶段 5",
+        "可信度": 8,
+        "备选工具": ["ComfyUI 局部重绘工作流", "Midjourney Vary Region"]
+      },
+      {
+        "工具名称": "Midjourney 印象派风格提示词",
+        "角色": "画中画油画风格生成(印象派/梵高风格)",
+        "阶段": "阶段 5",
+        "可信度": 8,
+        "备选工具": ["厚涂油画 LoRA", "风格迁移模型"]
+      }
+    ],
+    "亮点覆盖": [
+      "优雅的白裙写生少女 - FLUX.1 Kontext 写实人物生成 + InstantID 面部一致性 + ControlNet OpenPose 姿态控制",
+      "斑斓厚重的油画颜料 - Impasto 提示词方案(thick impasto brushwork, heavy paint texture, palette knife)",
+      "清新雅致的白绿配色 - FLUX.1 色彩控制 + 提示词描述(high saturation natural green, pure white dress)",
+      "唯美梦幻的光影与景深 - IC-Light 逆光/轮廓光 + Flux Tools Depth 前景清晰背景虚化",
+      "虚实呼应的画中画结构 - ControlNet Inpaint 局部重绘生成画布内容 + Midjourney 印象派风格提示词"
+    ],
+    "下限覆盖": [
+      "人物姿态自然度 - ControlNet OpenPose 控制专注绘画的动态姿态(身体前倾、手臂抬起握笔)",
+      "白裙材质与褶皱真实感 - FLUX.1 写实材质生成 + ControlNet Tile 保持褶皱结构 + 提示词描述(lightweight cotton/linen, natural drapery)",
+      "调色板颜料质感 - Impasto 提示词方案(膏状堆积、湿润光泽、多色混合)",
+      "前景主体清晰度 - ADetailer 人脸/手部修复 + FLUX.1 高细节生成",
+      "背景虚化自然度 - Flux Tools Depth 深度控制 + IC-Light 光影融合"
+    ],
+    "选择理由": "该策略基于 ComfyUI + FLUX 工作流,整合了调研中发现的最优工具组合:1)时效性:大部分工具在 2025 年有更新(FLUX.1 Kontext 2025 年 7 月、NanoBanana Pro 2025 年 6 月、Flux Tools 2025 年 9 月);2)智能化:采用 AI 工具而非传统工具(如 PS),ADetailer 自动检测修复、IC-Light 智能重光照;3)通用性:ComfyUI 工作流可复用于其他场景;4)外部置信度高:IC-Light(ControlNet 作者开发,1142 stars)、ADetailer(3.9k+ stars)、FLUX(SD 原团队开发)均有强背书;5)覆盖全面:5 个亮点和 5 个下限点均有对应工具方案覆盖。"
+  },
+  "备选方案对比": [
+    {
+      "备选策略": "NanoBanana Pro 一站式方案",
+      "不选原因": "虽然 NanoBanana Pro 角色一致性 90%+ 且操作简单,但所有图像有水印影响商业使用,且复杂画中画结构和 Impasto 质感控制不如 ComfyUI 工作流灵活",
+      "切换条件": "当需要快速出图且对水印无要求时,或团队缺乏 ComfyUI 技术能力时"
+    },
+    {
+      "备选策略": "Midjourney V7 主导方案",
+      "不选原因": "MJ V7 艺术风格顶级但人物一致性较差(--cref 仅 80%),且无法精确控制局部区域(画布内容、手部细节),需要多次尝试",
+      "切换条件": "当追求艺术风格优先于角色一致性,或需要快速生成概念图时"
+    },
+    {
+      "备选策略": "LoRA 训练主导方案",
+      "不选原因": "虽然 LoRA 训练一致性最高(90-98%),但需要 15-50 张训练图像和大量训练时间,不适合快速迭代",
+      "切换条件": "当需要长期复用同一角色(如系列图集、IP 形象)且有足够训练素材时"
+    }
+  ],
+  "未覆盖需求": []
+}

+ 369 - 0
examples/restore/input/写生油画__img_1_制作表.json

@@ -0,0 +1,369 @@
+[
+  {
+    "名称": "户外绘画场景",
+    "描述": "一名女性在户外草地上使用画架和调色板进行绘画,背景是绿色的树木和草地。",
+    "段落ID": "段落1",
+    "形式": {
+      "拍摄角度": {
+        "名称": "拍摄角度",
+        "描述": "相机位于人物右后方,略低于人物视线,以平视偏低的视角拍摄,使得人物和画架占据画面右侧和中央,背景的草地和树木在左侧和上方。",
+        "类型": "视角",
+        "形式ID": "形式5"
+      },
+      "景别": {
+        "名称": "景别",
+        "描述": "中景,画面中人物从腰部以上到头部完整呈现,画架大部分可见,背景的树木和草地也占据了较大比例,强调了人物与环境的互动。",
+        "类型": "构图",
+        "形式ID": "形式7"
+      },
+      "光照": {
+        "名称": "光照",
+        "描述": "自然光,光线柔和,从画面左上方照射,在人物的右侧和画架的左侧形成轻微阴影,整体画面亮度适中,无明显过曝或欠曝区域。",
+        "类型": "光影",
+        "形式ID": "形式2"
+      },
+      "色彩饱和度": {
+        "名称": "色彩饱和度",
+        "描述": "整体色彩饱和度中等偏高,绿色草地和树木的颜色鲜明,人物白色服装和调色板上的颜料色彩也较为突出,画面整体呈现出清新自然的色调。",
+        "类型": "色彩",
+        "形式ID": "形式11"
+      },
+      "清晰度": {
+        "名称": "清晰度",
+        "描述": "画面中心区域(人物、画架、画布)清晰度高,细节锐利可见。背景的树木和远处的草地有轻微虚化,呈现出景深效果。",
+        "类型": "清晰度",
+        "形式ID": "形式1"
+      },
+      "构图": {
+        "名称": "构图",
+        "描述": "采用开放式构图,人物和画架位于画面右侧偏中,占据了画面约60%的区域,背景的草地和树木占据左侧和上方约40%的区域。人物的视线和绘画动作引导观众看向画布,形成视觉焦点。画面整体平衡,右侧主体突出,左侧背景延伸。",
+        "类型": "构图",
+        "形式ID": "形式4"
+      },
+      "评分详情": {
+        "combined_score": 0.846
+      }
+    },
+    "子段落": [
+      {
+        "名称": "人物",
+        "描述": "一名女性,侧身背对镜头,正在进行绘画。",
+        "段落ID": "段落1.1",
+        "形式": {
+          "拍摄角度": {
+            "名称": "拍摄角度",
+            "描述": "人物侧身背对镜头,头部略微向左转,使得右耳和部分右脸颊可见。身体朝向画布,呈现出专注绘画的姿态。",
+            "类型": "视角",
+            "形式ID": "形式5"
+          },
+          "景别": {
+            "名称": "景别",
+            "描述": "人物从头部到脚部完整呈现,占据画面右侧约60%的区域,属于全身景别。",
+            "类型": "构图",
+            "形式ID": "形式7"
+          },
+          "光照": {
+            "名称": "光照",
+            "描述": "人物右侧受光,左侧(背对镜头一侧)有轻微阴影,光线柔和,使得服装的褶皱和头发的细节清晰可见。",
+            "类型": "光影",
+            "形式ID": "形式2"
+          },
+          "色彩饱和度": {
+            "名称": "色彩饱和度",
+            "描述": "人物的头发、肤色和白色服装的色彩饱和度适中,与背景的绿色形成对比,突出人物主体。",
+            "类型": "色彩",
+            "形式ID": "形式11"
+          },
+          "清晰度": {
+            "名称": "清晰度",
+            "描述": "人物主体清晰度高,头发丝、服装纹理、手部细节均清晰可见。",
+            "类型": "清晰度",
+            "形式ID": "形式1"
+          },
+          "构图": {
+            "名称": "构图",
+            "描述": "人物位于画面右侧,从画面顶部延伸至底部,占据了画面约60%的垂直空间,形成视觉上的引导线,将观众的注意力引向画布。",
+            "类型": "构图",
+            "形式ID": "形式4"
+          },
+          "评分详情": {
+            "combined_score": 0.745
+          }
+        },
+        "子段落": [
+          {
+            "名称": "头发",
+            "描述": "棕色长发,部分散落在肩上。",
+            "段落ID": "段落1.1.1",
+            "形式": {
+              "发色": {
+                "名称": "发色",
+                "描述": "棕色,呈现出自然光泽的深棕色,在光照下略显浅棕。",
+                "类型": "色彩",
+                "形式ID": "形式23"
+              },
+              "发型": {
+                "名称": "发型",
+                "描述": "长发,直发,部分散落在右肩和背部,发尾略有卷曲,发际线清晰可见。",
+                "类型": "形态",
+                "形式ID": "形式21"
+              },
+              "评分详情": {
+                "combined_score": 0.504
+              }
+            },
+            "评分详情": {
+              "combined_score": 0.525
+            }
+          },
+          {
+            "名称": "身体",
+            "描述": "女性的躯干和手臂。",
+            "段落ID": "段落1.1.2",
+            "形式": {
+              "姿态": {
+                "名称": "姿态",
+                "描述": "女性身体略微前倾,右臂抬起握持画笔,左臂弯曲握持调色板,头部转向画布,呈现出专注绘画的动态姿态。",
+                "类型": "动作",
+                "形式ID": "形式8"
+              },
+              "清晰度": {
+                "名称": "清晰度",
+                "描述": "身体部分(手臂、手、颈部)清晰度高,皮肤纹理和服装褶皱细节锐利。",
+                "类型": "清晰度",
+                "形式ID": "形式1"
+              },
+              "光照": {
+                "名称": "光照",
+                "描述": "身体右侧受光,左侧有轻微阴影,光线均匀柔和,突出了身体的轮廓和服装的立体感。",
+                "类型": "光影",
+                "形式ID": "形式2"
+              },
+              "评分详情": {
+                "combined_score": 0.588
+              }
+            },
+            "子段落": [
+              {
+                "名称": "服装",
+                "描述": "白色长裙,袖子宽松。",
+                "段落ID": "段落1.1.2.1",
+                "形式": {
+                  "服装颜色": {
+                    "名称": "服装颜色",
+                    "描述": "纯白色,无其他图案或颜色。",
+                    "类型": "色彩",
+                    "形式ID": "形式16"
+                  },
+                  "服装款式": {
+                    "名称": "服装款式",
+                    "描述": "长袖连衣裙,袖子宽松,裙摆飘逸,腰部有收紧设计,领口为V字形,背部有系带细节。",
+                    "类型": "形态",
+                    "形式ID": "形式14"
+                  },
+                  "材质": {
+                    "名称": "材质",
+                    "描述": "轻薄的棉麻或丝绸质地,具有良好的垂坠感和透气性。",
+                    "类型": "质感",
+                    "形式ID": "形式10"
+                  },
+                  "评分详情": {
+                    "combined_score": 0.835
+                  }
+                },
+                "评分详情": {
+                  "combined_score": 0.847
+                }
+              },
+              {
+                "名称": "画笔",
+                "描述": "女性右手握持的细长画笔。",
+                "段落ID": "段落1.1.2.2",
+                "形式": {
+                  "形状": {
+                    "名称": "形状",
+                    "描述": "细长杆状,笔尖为锥形,笔杆中部略粗。",
+                    "类型": "形状",
+                    "形式ID": "形式17"
+                  },
+                  "评分详情": {
+                    "combined_score": 0.42
+                  }
+                },
+                "评分详情": {
+                  "combined_score": 0.56
+                }
+              },
+              {
+                "名称": "调色板",
+                "描述": "女性左手握持的椭圆形调色板,上面有多种颜料。",
+                "段落ID": "段落1.1.2.3",
+                "形式": {
+                  "形状": {
+                    "名称": "形状",
+                    "描述": "不规则椭圆形,边缘圆润,中间有一个拇指孔。",
+                    "类型": "形状",
+                    "形式ID": "形式17"
+                  },
+                  "颜色": {
+                    "名称": "颜色",
+                    "描述": "调色板底色为深棕色,表面沾有多种颜料,包括绿色、蓝色、白色、粉色、黄色、红色等,其中绿色颜料面积最大。",
+                    "类型": "色彩",
+                    "形式ID": "形式3"
+                  },
+                  "颜料分布": {
+                    "名称": "颜料分布",
+                    "描述": "颜料呈不规则块状分布在调色板表面,绿色颜料集中在中央区域,其他颜色颜料散布在边缘。",
+                    "类型": "布局",
+                    "形式ID": "形式25"
+                  },
+                  "清晰度": {
+                    "名称": "清晰度",
+                    "描述": "调色板的木质纹理和颜料的堆叠感清晰可见。",
+                    "类型": "清晰度",
+                    "形式ID": "形式1"
+                  },
+                  "评分详情": {
+                    "combined_score": 0.644
+                  }
+                },
+                "评分详情": {
+                  "combined_score": 0.675
+                }
+              }
+            ],
+            "评分详情": {
+              "combined_score": 0.595
+            }
+          }
+        ],
+        "评分详情": {
+          "combined_score": 0.815
+        }
+      },
+      {
+        "名称": "画架",
+        "描述": "木质三脚画架,支撑着画布。",
+        "段落ID": "段落1.2",
+        "形式": {
+          "评分详情": {
+            "combined_score": 0.442
+          }
+        },
+        "子段落": [
+          {
+            "名称": "画布",
+            "描述": "画架上的一幅未完成的画作,描绘了一名背对镜头的女性。",
+            "段落ID": "段落1.2.1",
+            "形式": {
+              "绘画风格": {
+                "名称": "绘画风格",
+                "描述": "印象派风格,笔触粗犷,色彩鲜明,注重光影和氛围的表达,而非精确的细节描绘。",
+                "类型": "风格",
+                "形式ID": "形式27"
+              },
+              "色彩": {
+                "名称": "色彩",
+                "描述": "画面以绿色和蓝色为主色调,描绘了草地和花丛,人物服装为白色,色彩对比鲜明。",
+                "类型": "色彩",
+                "形式ID": "形式28"
+              },
+              "构图": {
+                "名称": "构图",
+                "描述": "画布中央偏下位置描绘了一名背对镜头的女性,周围是绿色的草地和蓝色的花朵,形成了一个景深感较强的画面。",
+                "类型": "构图",
+                "形式ID": "形式4"
+              },
+              "清晰度": {
+                "名称": "清晰度",
+                "描述": "画布上的画作清晰可见,但由于绘画风格,细节并非写实般锐利,而是呈现出笔触的模糊感。",
+                "类型": "清晰度",
+                "形式ID": "形式1"
+              },
+              "笔触": {
+                "名称": "笔触",
+                "描述": "笔触粗犷有力,颜料堆叠感明显,尤其是绿色和蓝色区域,呈现出明显的纹理。",
+                "类型": "笔触",
+                "形式ID": "形式29"
+              },
+              "内容主题": {
+                "名称": "内容主题",
+                "描述": "描绘了一名身穿白色裙子的女性在户外草地或花丛中行走的背影,与现实场景中的画家形成一种“画中画”的呼应。",
+                "类型": "内容",
+                "形式ID": "形式26"
+              },
+              "评分详情": {
+                "combined_score": 0.946
+              }
+            },
+            "评分详情": {
+              "combined_score": 0.958
+            }
+          },
+          {
+            "名称": "玫瑰花",
+            "描述": "画架下方放置的一朵白色玫瑰花。",
+            "段落ID": "段落1.2.2",
+            "形式": {
+              "清晰度": {
+                "名称": "清晰度",
+                "描述": "玫瑰花的花瓣纹理和叶片细节清晰可见,边缘锐利。",
+                "类型": "清晰度",
+                "形式ID": "形式1"
+              },
+              "评分详情": {
+                "combined_score": 0.318
+              }
+            },
+            "评分详情": {
+              "combined_score": 0.342
+            }
+          }
+        ],
+        "评分详情": {
+          "combined_score": 0.588
+        }
+      },
+      {
+        "名称": "背景",
+        "描述": "远处的绿色树木和近处的草地。",
+        "段落ID": "段落1.3",
+        "形式": {
+          "颜色": {
+            "名称": "颜色",
+            "描述": "背景以绿色为主,包括深绿色(树木)和浅绿色(草地),色彩鲜明且富有层次感。",
+            "类型": "色彩",
+            "形式ID": "形式3"
+          },
+          "清晰度": {
+            "名称": "清晰度",
+            "描述": "近处草地清晰度较高,远处树木和更远的背景有明显虚化,呈现出景深效果。",
+            "类型": "清晰度",
+            "形式ID": "形式1"
+          },
+          "光照": {
+            "名称": "光照",
+            "描述": "背景整体受光均匀,树木和草地有自然的光影变化,无明显过曝或欠曝区域。",
+            "类型": "光影",
+            "形式ID": "形式2"
+          },
+          "景深": {
+            "名称": "景深",
+            "描述": "景深较浅,前景(人物和画架)清晰,中景(近处草地)清晰,远景(树木)虚化,营造出空间层次感。",
+            "类型": "空间",
+            "形式ID": "形式9"
+          },
+          "评分详情": {
+            "combined_score": 0.595
+          }
+        },
+        "评分详情": {
+          "combined_score": 0.703
+        }
+      }
+    ],
+    "评分详情": {
+      "combined_score": 0.78
+    }
+  }
+]

+ 338 - 0
examples/restore/input/写生油画__img_2_制作表.json

@@ -0,0 +1,338 @@
+[
+  {
+    "名称": "户外绘画场景",
+    "描述": "一名女性在户外草地上使用画架和调色板进行绘画,背景是绿色的树木和阳光。",
+    "段落ID": "段落2",
+    "形式": {
+      "拍摄角度": {
+        "名称": "拍摄角度",
+        "描述": "从人物背后的中低角度拍摄,视线略微向上倾斜,将人物、画架和背景的树木都纳入画面,人物的头部位于画面顶部偏左,画架位于画面右侧,背景的树木和天空占据画面上半部分。",
+        "类型": "视角",
+        "形式ID": "形式5"
+      },
+      "景别": {
+        "名称": "景别",
+        "描述": "中景,画面中人物从腰部以上到头顶,以及画架和部分背景草地和树木清晰可见,人物占据画面约70%的高度,画架占据画面约80%的高度。",
+        "类型": "构图",
+        "形式ID": "形式7"
+      },
+      "光照": {
+        "名称": "光照",
+        "描述": "逆光,阳光从画面左上方透过树叶照射过来,形成强烈的光斑和光晕效果,人物和画架处于半剪影状态,但细节仍可见,草地受光均匀,整体画面明亮。",
+        "类型": "光影",
+        "形式ID": "形式2"
+      },
+      "色彩饱和度": {
+        "名称": "色彩饱和度",
+        "描述": "中等偏高,绿色草地和树木的色彩鲜艳,女性白色长裙和画架的木色饱和度适中,画面整体色彩明快。",
+        "类型": "色彩",
+        "形式ID": "形式11"
+      },
+      "清晰度": {
+        "名称": "清晰度",
+        "描述": "前景人物和画架清晰锐利,背景的树木和阳光呈现柔和的虚化效果,景深较浅。",
+        "类型": "清晰度",
+        "形式ID": "形式1"
+      },
+      "构图": {
+        "名称": "构图",
+        "描述": "采用三分法构图,人物主体位于画面左侧三分之一处,画架位于画面右侧三分之一处,形成平衡的视觉效果。人物的头部位于画面上方三分之一处,地平线位于画面下方三分之一处。",
+        "类型": "构图",
+        "形式ID": "形式4"
+      },
+      "评分详情": {
+        "combined_score": 0.811
+      }
+    },
+    "子段落": [
+      {
+        "名称": "人物",
+        "描述": "一名女性,背对镜头,正在进行绘画。",
+        "段落ID": "段落2.1",
+        "形式": {
+          "拍摄角度": {
+            "名称": "拍摄角度",
+            "描述": "从人物背后的中低角度拍摄,视线略微向上倾斜,人物背对镜头,头部略微偏向右侧,身体朝向画架。",
+            "类型": "视角",
+            "形式ID": "形式5"
+          },
+          "景别": {
+            "名称": "景别",
+            "描述": "中景,人物从腰部以上到头顶清晰可见,占据画面约70%的高度,其身体大部分位于画面左侧。",
+            "类型": "构图",
+            "形式ID": "形式7"
+          },
+          "光照": {
+            "名称": "光照",
+            "描述": "逆光,阳光从人物左后方照射,导致人物背部和头发边缘有明显的光晕,身体正面受光较少,但仍能看清细节。",
+            "类型": "光影",
+            "形式ID": "形式2"
+          },
+          "色彩饱和度": {
+            "名称": "色彩饱和度",
+            "描述": "中等偏高,头发的棕色和服装的白色饱和度适中,与背景的绿色形成对比。",
+            "类型": "色彩",
+            "形式ID": "形式11"
+          },
+          "清晰度": {
+            "名称": "清晰度",
+            "描述": "人物主体清晰锐利,头发丝和服装褶皱细节可见。",
+            "类型": "清晰度",
+            "形式ID": "形式1"
+          },
+          "构图": {
+            "名称": "构图",
+            "描述": "人物主体位于画面左侧三分之一处,头部位于画面上方三分之一处,形成视觉引导。",
+            "类型": "构图",
+            "形式ID": "形式4"
+          },
+          "评分详情": {
+            "combined_score": 0.849
+          }
+        },
+        "子段落": [
+          {
+            "名称": "头发",
+            "描述": "棕色长发,披散在背部。",
+            "段落ID": "段落2.1.1",
+            "形式": {
+              "发色": {
+                "名称": "发色",
+                "描述": "深棕色,在阳光下呈现出暖棕色调,发梢颜色略浅。",
+                "类型": "色彩",
+                "形式ID": "形式23"
+              },
+              "发型": {
+                "名称": "发型",
+                "描述": "长直发,自然披散在背部,发梢略带自然卷曲,长度及腰。",
+                "类型": "形态",
+                "形式ID": "形式21"
+              },
+              "发量": {
+                "名称": "发量",
+                "描述": "发量浓密,覆盖了大部分背部,从头顶到发梢呈现出厚重感。",
+                "类型": "量感",
+                "形式ID": "形式24"
+              },
+              "光泽度": {
+                "名称": "光泽度",
+                "描述": "头发表面有明显的光泽,尤其是在阳光照射下,发丝边缘呈现出明亮的光晕。",
+                "类型": "质感",
+                "形式ID": "形式22"
+              },
+              "评分详情": {
+                "combined_score": 0.532
+              }
+            },
+            "评分详情": {
+              "combined_score": 0.546
+            }
+          },
+          {
+            "名称": "身体",
+            "描述": "女性的躯干和手臂。",
+            "段落ID": "段落2.1.2",
+            "形式": {
+              "姿态": {
+                "名称": "姿态",
+                "描述": "女性身体略微向右倾斜,背部微弓,头部略微前倾,右手持画笔,左手持调色板,呈现出专注绘画的姿态。",
+                "类型": "动作",
+                "形式ID": "形式8"
+              },
+              "光照": {
+                "名称": "光照",
+                "描述": "逆光,身体背部和手臂边缘有明显的光晕,身体正面受光较少,形成一定的阴影。",
+                "类型": "光影",
+                "形式ID": "形式2"
+              },
+              "评分详情": {
+                "combined_score": 0.455
+              }
+            },
+            "子段落": [
+              {
+                "名称": "服装",
+                "描述": "白色长裙,露背设计。",
+                "段落ID": "段落2.1.2.1",
+                "形式": {
+                  "服装颜色": {
+                    "名称": "服装颜色",
+                    "描述": "纯白色,在阳光下略带米色调。",
+                    "类型": "色彩",
+                    "形式ID": "形式16"
+                  },
+                  "服装款式": {
+                    "名称": "服装款式",
+                    "描述": "长袖连衣裙,V字露背设计,腰部有系带收腰,裙摆为宽松的A字形长裙,长度及脚踝。",
+                    "类型": "形态",
+                    "形式ID": "形式14"
+                  },
+                  "材质": {
+                    "名称": "材质",
+                    "描述": "丝绸或棉麻混纺材质,表面光滑,有轻微的光泽感,质地轻薄。",
+                    "类型": "质感",
+                    "形式ID": "形式10"
+                  },
+                  "褶皱": {
+                    "名称": "褶皱",
+                    "描述": "裙摆和腰部有自然形成的垂坠褶皱,背部V领处也有轻微褶皱。",
+                    "类型": "形态",
+                    "形式ID": "形式20"
+                  },
+                  "评分详情": {
+                    "combined_score": 0.828
+                  }
+                },
+                "评分详情": {
+                  "combined_score": 0.834
+                }
+              },
+              {
+                "名称": "画笔",
+                "描述": "女性右手握持的细长画笔。",
+                "段落ID": "段落2.1.2.2",
+                "形式": {
+                  "材质": {
+                    "名称": "材质",
+                    "描述": "笔杆为木质或塑料,刷毛为动物毛或合成纤维。",
+                    "类型": "质感",
+                    "形式ID": "形式10"
+                  },
+                  "评分详情": {
+                    "combined_score": 0.294
+                  }
+                },
+                "评分详情": {
+                  "combined_score": 0.455
+                }
+              },
+              {
+                "名称": "调色板",
+                "描述": "女性左手握持的椭圆形调色板,上面有多种颜料。",
+                "段落ID": "段落2.1.2.3",
+                "形式": {
+                  "材质": {
+                    "名称": "材质",
+                    "描述": "木质或塑料材质,表面光滑。",
+                    "类型": "质感",
+                    "形式ID": "形式10"
+                  },
+                  "评分详情": {
+                    "combined_score": 0.455
+                  }
+                },
+                "评分详情": {
+                  "combined_score": 0.48
+                }
+              }
+            ],
+            "评分详情": {
+              "combined_score": 0.476
+            }
+          }
+        ],
+        "评分详情": {
+          "combined_score": 0.825
+        }
+      },
+      {
+        "名称": "画架",
+        "描述": "木质三脚画架,支撑着画布。",
+        "段落ID": "段落2.2",
+        "形式": {
+          "评分详情": {
+            "combined_score": 0.427
+          }
+        },
+        "子段落": [
+          {
+            "名称": "画布",
+            "描述": "画架上的一幅未完成的画作,描绘了一名背对镜头的女性。",
+            "段落ID": "段落2.2.1",
+            "形式": {
+              "绘画风格": {
+                "名称": "绘画风格",
+                "描述": "印象派或写意风格,笔触粗犷,色彩鲜明,注重光影和氛围的表达,而非精确的细节描绘。",
+                "类型": "风格",
+                "形式ID": "形式27"
+              },
+              "色彩": {
+                "名称": "色彩",
+                "描述": "以绿色、蓝色、紫色为主,辅以白色和黄色,色彩明亮且饱和度较高。",
+                "类型": "色彩",
+                "形式ID": "形式28"
+              },
+              "构图": {
+                "名称": "构图",
+                "描述": "画面中央描绘了一名背对镜头的女性形象,周围是模糊的绿色植物和花朵,背景有光斑效果。",
+                "类型": "构图",
+                "形式ID": "形式4"
+              },
+              "笔触": {
+                "名称": "笔触",
+                "描述": "笔触明显,可见颜料堆叠和涂抹的痕迹,呈现出粗犷而富有表现力的特点。",
+                "类型": "笔触",
+                "形式ID": "形式29"
+              },
+              "内容主题": {
+                "名称": "内容主题",
+                "描述": "描绘了一名身穿白色裙子的女性在户外草地上的背影,周围是绿色的植物和花朵,暗示着户外绘画或休闲场景。",
+                "类型": "内容",
+                "形式ID": "形式26"
+              },
+              "评分详情": {
+                "combined_score": 0.892
+              }
+            },
+            "评分详情": {
+              "combined_score": 0.895
+            }
+          }
+        ],
+        "评分详情": {
+          "combined_score": 0.511
+        }
+      },
+      {
+        "名称": "背景",
+        "描述": "远处的绿色树木和草地,有阳光透过树叶。",
+        "段落ID": "段落2.3",
+        "形式": {
+          "颜色": {
+            "名称": "颜色",
+            "描述": "以绿色为主,包括深绿色、浅绿色和黄绿色,天空部分呈现淡黄色和白色,整体色彩清新明亮。",
+            "类型": "色彩",
+            "形式ID": "形式3"
+          },
+          "清晰度": {
+            "名称": "清晰度",
+            "描述": "背景整体呈现柔和的虚化效果,景深较浅,树木轮廓模糊,光斑明显。",
+            "类型": "清晰度",
+            "形式ID": "形式1"
+          },
+          "光照": {
+            "名称": "光照",
+            "描述": "逆光,阳光从画面左上方透过树叶,形成大量圆形和不规则形状的光斑和光晕,使背景呈现出明亮而梦幻的效果。",
+            "类型": "光影",
+            "形式ID": "形式2"
+          },
+          "景深": {
+            "名称": "景深",
+            "描述": "景深较浅,背景的树木和草地被虚化,与前景的人物和画架形成对比,突出主体。",
+            "类型": "空间",
+            "形式ID": "形式9"
+          },
+          "评分详情": {
+            "combined_score": 0.695
+          }
+        },
+        "评分详情": {
+          "combined_score": 0.703
+        }
+      }
+    ],
+    "评分详情": {
+      "combined_score": 0.756
+    }
+  }
+]

+ 283 - 0
examples/restore/input/写生油画__img_3_制作表.json

@@ -0,0 +1,283 @@
+[
+  {
+    "名称": "户外绘画场景",
+    "描述": "一名女性在户外草地上跪坐,使用画架和调色板进行绘画,背景是绿色的树木和远处的建筑。",
+    "段落ID": "段落3",
+    "形式": {
+      "拍摄角度": {
+        "名称": "拍摄角度",
+        "描述": "从人物背部略偏右侧的低角度拍摄,视线略高于人物头部,呈现出人物、画架和背景的广阔视野。",
+        "类型": "视角",
+        "形式ID": "形式5"
+      },
+      "景别": {
+        "名称": "景别",
+        "描述": "中景偏全景,画面包含了人物的全身(从头顶到膝盖以下部分),画架的完整结构,以及远处的背景,强调了人物与环境的互动。",
+        "类型": "构图",
+        "形式ID": "形式7"
+      },
+      "光照": {
+        "名称": "光照",
+        "描述": "自然光,光源主要来自画面左上方,呈现出逆光效果。人物和前景草地部分受光较少,略显阴影,背景树木边缘有明显的光晕,整体光线柔和,营造出温暖的氛围。",
+        "类型": "光影",
+        "形式ID": "形式2"
+      },
+      "色彩饱和度": {
+        "名称": "色彩饱和度",
+        "描述": "整体色彩饱和度中等偏高,草地的绿色和树木的绿色鲜明,人物白色服装纯净,画作上的色彩也较为鲜艳,但整体色调和谐,不刺眼。",
+        "类型": "色彩",
+        "形式ID": "形式11"
+      },
+      "清晰度": {
+        "名称": "清晰度",
+        "描述": "前景人物和画架清晰锐利,中景草地清晰,背景树木和远景建筑略有虚化,呈现出景深效果。",
+        "类型": "清晰度",
+        "形式ID": "形式1"
+      },
+      "构图": {
+        "名称": "构图",
+        "描述": "采用开放式构图,人物位于画面右侧偏中,画架位于画面中央偏左,两者形成对角线构图。背景广阔,画面元素分布均衡,引导视线从人物到画架再到背景。",
+        "类型": "构图",
+        "形式ID": "形式4"
+      },
+      "评分详情": {
+        "combined_score": 0.733
+      }
+    },
+    "子段落": [
+      {
+        "名称": "人物",
+        "描述": "一名女性,背对镜头,跪坐在草地上。",
+        "段落ID": "段落3.1",
+        "形式": {
+          "拍摄角度": {
+            "名称": "拍摄角度",
+            "描述": "从人物背部略偏右侧的低角度拍摄,视线略高于人物头部,呈现出人物的背影和侧面。",
+            "类型": "视角",
+            "形式ID": "形式5"
+          },
+          "景别": {
+            "名称": "景别",
+            "描述": "中景,画面包含了人物的全身(从头顶到膝盖以下部分),强调了人物的姿态和服装细节。",
+            "类型": "构图",
+            "形式ID": "形式7"
+          },
+          "光照": {
+            "名称": "光照",
+            "描述": "自然光,光源主要来自人物左前方,人物背部和右侧受光较少,处于阴影中,左侧手臂和部分头发有少量高光,整体光线柔和。",
+            "类型": "光影",
+            "形式ID": "形式2"
+          },
+          "色彩饱和度": {
+            "名称": "色彩饱和度",
+            "描述": "人物服装为纯白色,头发为棕色,色彩饱和度适中,与周围环境色彩形成对比。",
+            "类型": "色彩",
+            "形式ID": "形式11"
+          },
+          "清晰度": {
+            "名称": "清晰度",
+            "描述": "人物主体清晰锐利,头发丝、服装褶皱等细节清晰可见。",
+            "类型": "清晰度",
+            "形式ID": "形式1"
+          },
+          "构图": {
+            "名称": "构图",
+            "描述": "人物位于画面右侧偏中,占据了画面约三分之二的垂直空间,形成主体突出。",
+            "类型": "构图",
+            "形式ID": "形式4"
+          },
+          "评分详情": {
+            "combined_score": 0.792
+          }
+        },
+        "子段落": [
+          {
+            "名称": "头发",
+            "描述": "棕色长发,披散在背部。",
+            "段落ID": "段落3.1.1",
+            "形式": {
+              "清晰度": {
+                "名称": "清晰度",
+                "描述": "头发丝细节清晰可见,发梢的层次感明显。",
+                "类型": "清晰度",
+                "形式ID": "形式1"
+              },
+              "评分详情": {
+                "combined_score": 0.413
+              }
+            },
+            "评分详情": {
+              "combined_score": 0.434
+            }
+          },
+          {
+            "名称": "身体",
+            "描述": "女性的躯干和手臂。",
+            "段落ID": "段落3.1.2",
+            "形式": {
+              "姿态": {
+                "名称": "姿态",
+                "描述": "女性跪坐在草地上,身体略微前倾,头部转向画架方向,左手自然放置在调色板旁,右手可能正在作画(未完全显示)。",
+                "类型": "动作",
+                "形式ID": "形式8"
+              },
+              "评分详情": {
+                "combined_score": 0.497
+              }
+            },
+            "子段落": [
+              {
+                "名称": "服装",
+                "描述": "白色长裙,露背设计。",
+                "段落ID": "段落3.1.2.1",
+                "形式": {
+                  "服装颜色": {
+                    "名称": "服装颜色",
+                    "描述": "纯白色,无其他杂色,呈现出干净、明亮的视觉效果。",
+                    "类型": "色彩",
+                    "形式ID": "形式16"
+                  },
+                  "服装款式": {
+                    "名称": "服装款式",
+                    "描述": "长袖连衣裙,V字露背设计,背部有白色细绳交叉系带,腰部有系带收腰,裙摆宽松,自然垂坠,长度及地。",
+                    "类型": "形态",
+                    "形式ID": "形式14"
+                  },
+                  "材质": {
+                    "名称": "材质",
+                    "描述": "目测为轻薄、柔软的棉麻或雪纺材质,具有良好的垂坠感和透气性。",
+                    "类型": "质感",
+                    "形式ID": "形式10"
+                  },
+                  "评分详情": {
+                    "combined_score": 0.834
+                  }
+                },
+                "评分详情": {
+                  "combined_score": 0.84
+                }
+              },
+              {
+                "名称": "调色板",
+                "描述": "女性左手旁放置的椭圆形调色板,上面有多种颜料。",
+                "段落ID": "段落3.1.2.2",
+                "形式": {
+                  "评分详情": {
+                    "combined_score": 0.415
+                  }
+                },
+                "评分详情": {
+                  "combined_score": 0.479
+                }
+              }
+            ],
+            "评分详情": {
+              "combined_score": 0.525
+            }
+          }
+        ],
+        "评分详情": {
+          "combined_score": 0.826
+        }
+      },
+      {
+        "名称": "画架",
+        "描述": "木质三脚画架,支撑着画布。",
+        "段落ID": "段落3.2",
+        "形式": {
+          "结构": {
+            "名称": "结构",
+            "描述": "三脚架结构,由三根木杆支撑,顶部有可调节的画板支撑杆和固定装置,整体结构稳固。",
+            "类型": "结构",
+            "形式ID": "形式18"
+          },
+          "评分详情": {
+            "combined_score": 0.551
+          }
+        },
+        "子段落": [
+          {
+            "名称": "画布",
+            "描述": "画架上的一幅未完成的画作,描绘了一名背对镜头的女性。",
+            "段落ID": "段落3.2.1",
+            "形式": {
+              "绘画风格": {
+                "名称": "绘画风格",
+                "描述": "印象派风格,笔触粗犷,色彩鲜明,注重光影和氛围的表达,而非精确的细节描绘。",
+                "类型": "风格",
+                "形式ID": "形式27"
+              },
+              "色彩": {
+                "名称": "色彩",
+                "描述": "以绿色和蓝色为主色调,描绘了草地和花丛,人物服装为白色,色彩对比鲜明,整体色调明亮。",
+                "类型": "色彩",
+                "形式ID": "形式28"
+              },
+              "构图": {
+                "名称": "构图",
+                "描述": "画作中央偏右描绘了一名背对镜头的女性,周围是绿色的草地和蓝紫色的花丛,背景有白色遮阳伞,形成开放式构图。",
+                "类型": "构图",
+                "形式ID": "形式4"
+              },
+              "笔触": {
+                "名称": "笔触",
+                "描述": "笔触粗犷有力,颜料堆叠感明显,尤其在花丛和草地的描绘上,呈现出明显的纹理。",
+                "类型": "笔触",
+                "形式ID": "形式29"
+              },
+              "内容主题": {
+                "名称": "内容主题",
+                "描述": "描绘了一名身穿白色连衣裙的女性在户外花丛中撑伞的场景,与现实场景中的女性形象相似,形成画中画的意境。",
+                "类型": "内容",
+                "形式ID": "形式26"
+              },
+              "评分详情": {
+                "combined_score": 0.874
+              }
+            },
+            "评分详情": {
+              "combined_score": 0.891
+            }
+          }
+        ],
+        "评分详情": {
+          "combined_score": 0.584
+        }
+      },
+      {
+        "名称": "背景",
+        "描述": "远处的绿色树木和草地,以及远处的城市建筑。",
+        "段落ID": "段落3.3",
+        "形式": {
+          "颜色": {
+            "名称": "颜色",
+            "描述": "以绿色为主,包括前景草地的鲜绿色、中景树木的深绿色和远景树木的浅绿色,远处建筑为浅灰色,天空为淡黄色,整体色彩清新自然。",
+            "类型": "色彩",
+            "形式ID": "形式3"
+          },
+          "光照": {
+            "名称": "光照",
+            "描述": "背景光线充足,尤其在画面左上方,阳光穿透树叶形成光斑和光晕,营造出温暖、明亮的氛围。",
+            "类型": "光影",
+            "形式ID": "形式2"
+          },
+          "景深": {
+            "名称": "景深",
+            "描述": "景深较浅,前景人物和画架清晰,背景树木和建筑逐渐虚化,层次感明显。",
+            "类型": "空间",
+            "形式ID": "形式9"
+          },
+          "评分详情": {
+            "combined_score": 0.558
+          }
+        },
+        "评分详情": {
+          "combined_score": 0.633
+        }
+      }
+    ],
+    "评分详情": {
+      "combined_score": 0.755
+    }
+  }
+]

+ 319 - 0
examples/restore/input/写生油画__img_4_制作表.json

@@ -0,0 +1,319 @@
+[
+  {
+    "名称": "户外绘画场景",
+    "描述": "一名女性在户外草地上站立,使用画架和调色板进行绘画,背景是绿色的树木。",
+    "段落ID": "段落4",
+    "形式": {
+      "拍摄角度": {
+        "名称": "拍摄角度",
+        "描述": "平视角度,相机与人物大致处于同一水平线,略微仰视,使得人物和画架的顶部略高于画面中心,背景的树木占据画面上半部分。",
+        "类型": "视角",
+        "形式ID": "形式5"
+      },
+      "景别": {
+        "名称": "景别",
+        "描述": "中景,画面中人物从膝盖以上到头部完整呈现,画架也完整呈现,背景的树木和草地占据画面大部分,强调人物与环境的互动。",
+        "类型": "构图",
+        "形式ID": "形式7"
+      },
+      "光照": {
+        "名称": "光照",
+        "描述": "自然光照,光线明亮,从画面右上方射入,在人物和画架上形成清晰的亮部和柔和的阴影,整体光线均匀,无明显过曝或欠曝区域。",
+        "类型": "光影",
+        "形式ID": "形式2"
+      },
+      "色彩饱和度": {
+        "名称": "色彩饱和度",
+        "描述": "中等偏高饱和度,绿色草地和树木的色彩鲜艳,人物白色服装和肤色自然,调色板上的颜料色彩丰富且饱和度高,整体画面色彩生动。",
+        "类型": "色彩",
+        "形式ID": "形式11"
+      },
+      "清晰度": {
+        "名称": "清晰度",
+        "描述": "高清晰度,画面主体人物和画架细节清晰锐利,背景树木和草地有轻微虚化,但仍能辨认出其形态,整体画面清晰度良好。",
+        "类型": "清晰度",
+        "形式ID": "形式1"
+      },
+      "构图": {
+        "名称": "构图",
+        "描述": "采用开放式构图,人物位于画面右侧,画架位于画面左侧,两者形成对角线构图,引导视线从左下方的画架到右上方的人物,背景的树木和草地延伸至画面边缘,营造出开阔感。人物头部位于画面上方1/4处,画架顶部位于画面上方1/8处。",
+        "类型": "构图",
+        "形式ID": "形式4"
+      },
+      "评分详情": {
+        "combined_score": 0.858
+      }
+    },
+    "子段落": [
+      {
+        "名称": "人物",
+        "描述": "一名女性,侧身面对镜头,正在进行绘画。",
+        "段落ID": "段落4.1",
+        "形式": {
+          "拍摄角度": {
+            "名称": "拍摄角度",
+            "描述": "平视角度,相机与人物大致处于同一水平线,略微仰视,人物头部略高于画面中心。",
+            "类型": "视角",
+            "形式ID": "形式5"
+          },
+          "景别": {
+            "名称": "景别",
+            "描述": "中景,人物从膝盖以上到头部完整呈现,占据画面右侧大部分区域,强调人物的姿态和动作。",
+            "类型": "构图",
+            "形式ID": "形式7"
+          },
+          "光照": {
+            "名称": "光照",
+            "描述": "自然光照,光线明亮,从画面右上方射入,在人物右侧形成亮部,左侧形成柔和阴影,面部光线均匀,无明显过曝或欠曝。",
+            "类型": "光影",
+            "形式ID": "形式2"
+          },
+          "色彩饱和度": {
+            "名称": "色彩饱和度",
+            "描述": "中等偏高饱和度,肤色自然,头发棕色饱和度适中,白色服装色彩纯净,调色板上的颜料色彩鲜艳,整体色彩和谐。",
+            "类型": "色彩",
+            "形式ID": "形式11"
+          },
+          "清晰度": {
+            "名称": "清晰度",
+            "描述": "高清晰度,人物面部、头发、服装和手部细节清晰锐利,无模糊现象。",
+            "类型": "清晰度",
+            "形式ID": "形式1"
+          },
+          "构图": {
+            "名称": "构图",
+            "描述": "人物位于画面右侧,身体略微向左倾斜,形成对角线构图,头部位于画面上方1/4处,视线向左上方延伸,与画架形成互动。",
+            "类型": "构图",
+            "形式ID": "形式4"
+          },
+          "评分详情": {
+            "combined_score": 0.788
+          }
+        },
+        "子段落": [
+          {
+            "名称": "头发",
+            "描述": "棕色长发,部分散落在肩上。",
+            "段落ID": "段落4.1.1",
+            "形式": {
+              "评分详情": {
+                "combined_score": 0.301
+              }
+            },
+            "评分详情": {
+              "combined_score": 0.315
+            }
+          },
+          {
+            "名称": "身体",
+            "描述": "女性的躯干和手臂。",
+            "段落ID": "段落4.1.2",
+            "形式": {
+              "姿态": {
+                "名称": "姿态",
+                "描述": "站立姿态,身体略微侧向左前方,头部向左上方仰望,右手持画笔,左手持调色板,双臂自然抬起,呈绘画动作。",
+                "类型": "动作",
+                "形式ID": "形式8"
+              },
+              "肤色": {
+                "名称": "肤色",
+                "描述": "健康白皙的肤色,在光照下呈现自然光泽,面部和手臂肤色均匀。",
+                "类型": "色彩",
+                "形式ID": "形式12"
+              },
+              "清晰度": {
+                "名称": "清晰度",
+                "描述": "高清晰度,躯干和手臂的轮廓、服装褶皱、手部细节清晰可见。",
+                "类型": "清晰度",
+                "形式ID": "形式1"
+              },
+              "光照": {
+                "名称": "光照",
+                "描述": "自然光照,光线从右上方射入,在右臂和身体右侧形成亮部,左臂和身体左侧形成柔和阴影,光影过渡自然。",
+                "类型": "光影",
+                "形式ID": "形式2"
+              },
+              "评分详情": {
+                "combined_score": 0.765
+              }
+            },
+            "子段落": [
+              {
+                "名称": "服装",
+                "描述": "白色长裙,袖子宽松。",
+                "段落ID": "段落4.1.2.1",
+                "形式": {
+                  "服装颜色": {
+                    "名称": "服装颜色",
+                    "描述": "纯白色,无图案或装饰,色彩纯净明亮。",
+                    "类型": "色彩",
+                    "形式ID": "形式16"
+                  },
+                  "服装款式": {
+                    "名称": "服装款式",
+                    "描述": "长袖连衣裙,圆领,袖子宽松,裙摆宽松垂坠,长度及脚踝,腰部有轻微收腰设计。",
+                    "类型": "形态",
+                    "形式ID": "形式14"
+                  },
+                  "材质": {
+                    "名称": "材质",
+                    "描述": "目测为棉麻或雪纺等轻薄透气的面料,具有柔软垂坠感。",
+                    "类型": "质感",
+                    "形式ID": "形式10"
+                  },
+                  "评分详情": {
+                    "combined_score": 0.806
+                  }
+                },
+                "评分详情": {
+                  "combined_score": 0.81
+                }
+              },
+              {
+                "名称": "画笔",
+                "描述": "女性右手握持的细长画笔。",
+                "段落ID": "段落4.1.2.2",
+                "形式": {
+                  "形状": {
+                    "名称": "形状",
+                    "描述": "细长杆状,笔杆为深色,笔头为绿色颜料。",
+                    "类型": "形状",
+                    "形式ID": "形式17"
+                  },
+                  "材质": {
+                    "名称": "材质",
+                    "描述": "笔杆目测为木质或塑料,笔头为合成纤维或动物毛。",
+                    "类型": "质感",
+                    "形式ID": "形式10"
+                  },
+                  "评分详情": {
+                    "combined_score": 0.455
+                  }
+                },
+                "评分详情": {
+                  "combined_score": 0.525
+                }
+              },
+              {
+                "名称": "调色板",
+                "描述": "女性左手握持的椭圆形调色板,上面有多种颜料。",
+                "段落ID": "段落4.1.2.3",
+                "形式": {
+                  "形状": {
+                    "名称": "形状",
+                    "描述": "椭圆形,边缘光滑,中间略微凹陷。",
+                    "类型": "形状",
+                    "形式ID": "形式17"
+                  },
+                  "颜色": {
+                    "名称": "颜色",
+                    "描述": "调色板主体为深棕色或黑色,表面沾有多种鲜艳颜料,包括绿色、蓝色、红色、黄色、白色等,颜料分布不规则。",
+                    "类型": "色彩",
+                    "形式ID": "形式3"
+                  },
+                  "材质": {
+                    "名称": "材质",
+                    "描述": "目测为木质或塑料材质,表面光滑。",
+                    "类型": "质感",
+                    "形式ID": "形式10"
+                  },
+                  "颜料分布": {
+                    "名称": "颜料分布",
+                    "描述": "颜料呈不规则块状分布在调色板表面,主要集中在调色板的左侧和下方,绿色颜料面积最大,位于调色板中央偏左位置,其他颜料点缀其间。",
+                    "类型": "布局",
+                    "形式ID": "形式25"
+                  },
+                  "评分详情": {
+                    "combined_score": 0.628
+                  }
+                },
+                "评分详情": {
+                  "combined_score": 0.653
+                }
+              }
+            ],
+            "评分详情": {
+              "combined_score": 0.773
+            }
+          }
+        ],
+        "评分详情": {
+          "combined_score": 0.764
+        }
+      },
+      {
+        "名称": "画架",
+        "描述": "木质三脚画架,支撑着画布。",
+        "段落ID": "段落4.2",
+        "形式": {
+          "结构": {
+            "名称": "结构",
+            "描述": "三脚架结构,由三根木杆支撑,顶部有可调节的画板支撑架,底部有横向支撑杆,整体结构稳固。",
+            "类型": "结构",
+            "形式ID": "形式18"
+          },
+          "评分详情": {
+            "combined_score": 0.547
+          }
+        },
+        "子段落": [
+          {
+            "名称": "画布",
+            "描述": "画架上的一幅空白画布。",
+            "段落ID": "段落4.2.1",
+            "形式": {
+              "画布颜色": {
+                "名称": "画布颜色",
+                "描述": "纯白色,表面干净,无任何颜料痕迹。",
+                "类型": "色彩"
+              },
+              "光照": {
+                "名称": "光照",
+                "描述": "自然光照,光线从右上方射入,画布表面受光均匀,无明显阴影,呈现纯白色。",
+                "类型": "光影",
+                "形式ID": "形式2"
+              },
+              "评分详情": {
+                "combined_score": 0.606
+              }
+            },
+            "评分详情": {
+              "combined_score": 0.79
+            }
+          }
+        ],
+        "评分详情": {
+          "combined_score": 0.608
+        }
+      },
+      {
+        "名称": "背景",
+        "描述": "远处的绿色树木和草地。",
+        "段落ID": "段落4.3",
+        "形式": {
+          "颜色": {
+            "名称": "颜色",
+            "描述": "绿色为主,草地为鲜绿色,树木为深浅不一的绿色,远处有少量棕色树干和灰色建筑,整体色调清新自然。",
+            "类型": "色彩",
+            "形式ID": "形式3"
+          },
+          "景深": {
+            "名称": "景深",
+            "描述": "景深较浅,前景人物和画架清晰,背景草地和树木逐渐虚化,营造出空间感和层次感。",
+            "类型": "空间",
+            "形式ID": "形式9"
+          },
+          "评分详情": {
+            "combined_score": 0.434
+          }
+        },
+        "评分详情": {
+          "combined_score": 0.542
+        }
+      }
+    ],
+    "评分详情": {
+      "combined_score": 0.845
+    }
+  }
+]

+ 389 - 0
examples/restore/input/写生油画__img_5_制作表.json

@@ -0,0 +1,389 @@
+[
+  {
+    "名称": "户外绘画场景",
+    "描述": "画面展示了户外绘画的局部场景,主要聚焦于人物手持调色板和部分身体,以及背景的草地和画架。",
+    "段落ID": "段落5",
+    "形式": {
+      "拍摄角度": {
+        "名称": "拍摄角度",
+        "描述": "从人物胸部以上,略微俯视的角度拍摄,画面中心偏右是人物手持调色板的区域,左侧可见画架局部,背景是模糊的草地。",
+        "类型": "视角",
+        "形式ID": "形式5"
+      },
+      "景别": {
+        "名称": "景别",
+        "描述": "中景偏近景,主要聚焦于人物的上半身(胸部以上)和手持的调色板,占据画面约80%的区域,背景草地虚化。",
+        "类型": "构图",
+        "形式ID": "形式7"
+      },
+      "光照": {
+        "名称": "光照",
+        "描述": "自然光照,光线充足且柔和,从画面右上方照射,使得人物右侧手臂和调色板右侧受光较亮,左侧略有阴影,整体画面亮度适中,无明显过曝或欠曝区域。",
+        "类型": "光影",
+        "形式ID": "形式2"
+      },
+      "色彩饱和度": {
+        "名称": "色彩饱和度",
+        "描述": "色彩饱和度较高,尤其是调色板上的颜料和背景草地的绿色,色彩鲜明且富有活力。",
+        "类型": "色彩",
+        "形式ID": "形式11"
+      },
+      "清晰度": {
+        "名称": "清晰度",
+        "描述": "画面中心区域(人物手臂、调色板、画笔)清晰锐利,细节可见;背景草地和画架边缘部分虚化,呈现景深效果。",
+        "类型": "清晰度",
+        "形式ID": "形式1"
+      },
+      "构图": {
+        "名称": "构图",
+        "描述": "采用开放式构图,人物和调色板占据画面主体,调色板位于画面右下角至中心区域,人物左臂从画面左上角延伸,右臂从画面右侧延伸,画架位于画面左下角,背景草地作为衬托,引导视线集中于绘画活动。",
+        "类型": "构图",
+        "形式ID": "形式4"
+      },
+      "评分详情": {
+        "combined_score": 0.85
+      }
+    },
+    "子段落": [
+      {
+        "名称": "人物",
+        "描述": "画面中部的女性,穿着白色服装,正在进行绘画活动。",
+        "段落ID": "段落5.1",
+        "形式": {
+          "拍摄角度": {
+            "名称": "拍摄角度",
+            "描述": "从人物胸部以上,略微俯视的角度拍摄,主要展现人物进行绘画时的上半身姿态。",
+            "类型": "视角",
+            "形式ID": "形式5"
+          },
+          "景别": {
+            "名称": "景别",
+            "描述": "中景偏近景,聚焦于人物的上半身(胸部以上),占据画面约80%的区域,突出人物的绘画动作。",
+            "类型": "构图",
+            "形式ID": "形式7"
+          },
+          "光照": {
+            "名称": "光照",
+            "描述": "自然光照,光线充足且柔和,从画面右上方照射,使得人物右侧身体和手臂受光较亮,左侧略有阴影,整体亮度适中。",
+            "类型": "光影",
+            "形式ID": "形式2"
+          },
+          "色彩饱和度": {
+            "名称": "色彩饱和度",
+            "描述": "人物服装为白色,肤色自然,色彩饱和度适中,与背景的鲜艳绿色形成对比。",
+            "类型": "色彩",
+            "形式ID": "形式11"
+          },
+          "清晰度": {
+            "名称": "清晰度",
+            "描述": "人物主体清晰锐利,服装纹理和手臂细节可见,与虚化的背景形成对比。",
+            "类型": "清晰度",
+            "形式ID": "形式1"
+          },
+          "构图": {
+            "名称": "构图",
+            "描述": "人物身体呈对角线构图,左臂从画面左上角延伸,右臂从画面右侧延伸,调色板位于画面右下角至中心区域,形成视觉引导。",
+            "类型": "构图",
+            "形式ID": "形式4"
+          },
+          "评分详情": {
+            "combined_score": 0.823
+          }
+        },
+        "子段落": [
+          {
+            "名称": "手臂",
+            "描述": "人物露出的手臂部分,包括左臂和右臂。",
+            "段落ID": "段落5.1.1",
+            "形式": {
+              "评分详情": {
+                "combined_score": 0.28
+              }
+            },
+            "子段落": [
+              {
+                "名称": "左臂",
+                "描述": "人物左侧手臂,部分可见,手持画笔。",
+                "段落ID": "段落5.1.1.1",
+                "形式": {
+                  "清晰度": {
+                    "名称": "清晰度",
+                    "描述": "左臂皮肤纹理、指甲、画笔等细节清晰可见,无模糊现象。",
+                    "类型": "清晰度",
+                    "形式ID": "形式1"
+                  },
+                  "光照": {
+                    "名称": "光照",
+                    "描述": "自然光照,左臂受光均匀,无明显阴影或反光。",
+                    "类型": "光影",
+                    "形式ID": "形式2"
+                  },
+                  "评分详情": {
+                    "combined_score": 0.196
+                  }
+                },
+                "子段落": [
+                  {
+                    "名称": "画笔",
+                    "描述": "人物左手持有的细长画笔。",
+                    "段落ID": "段落5.1.1.1.1",
+                    "形式": {
+                      "形状": {
+                        "名称": "形状",
+                        "描述": "画笔呈细长圆柱形,笔杆笔直,笔尖略尖。",
+                        "类型": "形状",
+                        "形式ID": "形式17"
+                      },
+                      "颜色": {
+                        "名称": "颜色",
+                        "描述": "画笔笔杆为深色(可能为黑色或深棕色),笔尖部分沾有少量绿色颜料。",
+                        "类型": "色彩",
+                        "形式ID": "形式3"
+                      },
+                      "尺寸": {
+                        "名称": "尺寸",
+                        "描述": "画笔长度约为150像素,宽度约为10像素,相对于人物手指显得细长。",
+                        "类型": "大小",
+                        "形式ID": "形式19"
+                      },
+                      "清晰度": {
+                        "名称": "清晰度",
+                        "描述": "画笔轮廓清晰,笔尖细节可见,无模糊现象。",
+                        "类型": "清晰度",
+                        "形式ID": "形式1"
+                      },
+                      "评分详情": {
+                        "combined_score": 0.224
+                      }
+                    },
+                    "评分详情": {
+                      "combined_score": 0.245
+                    }
+                  }
+                ],
+                "评分详情": {
+                  "combined_score": 0.21
+                }
+              },
+              {
+                "名称": "右臂",
+                "描述": "人物右侧手臂,手持调色板,佩戴手镯。",
+                "段落ID": "段落5.1.1.2",
+                "形式": {
+                  "清晰度": {
+                    "名称": "清晰度",
+                    "描述": "右臂皮肤纹理、指甲、手镯等细节清晰可见,无模糊现象。",
+                    "类型": "清晰度",
+                    "形式ID": "形式1"
+                  },
+                  "光照": {
+                    "名称": "光照",
+                    "描述": "自然光照,右臂受光面较亮,光影过渡自然,手镯表面有反光点。",
+                    "类型": "光影",
+                    "形式ID": "形式2"
+                  },
+                  "评分详情": {
+                    "combined_score": 0.266
+                  }
+                },
+                "子段落": [
+                  {
+                    "名称": "手镯",
+                    "描述": "佩戴在右腕上的银色手镯。",
+                    "段落ID": "段落5.1.1.2.1",
+                    "形式": {
+                      "形状": {
+                        "名称": "形状",
+                        "描述": "手镯呈圆形环状,边缘光滑。",
+                        "类型": "形状",
+                        "形式ID": "形式17"
+                      },
+                      "颜色": {
+                        "名称": "颜色",
+                        "描述": "手镯为银色,表面有金属光泽。",
+                        "类型": "色彩",
+                        "形式ID": "形式3"
+                      },
+                      "清晰度": {
+                        "名称": "清晰度",
+                        "描述": "手镯轮廓清晰,表面光泽细节可见,无模糊现象。",
+                        "类型": "清晰度",
+                        "形式ID": "形式1"
+                      },
+                      "评分详情": {
+                        "combined_score": 0.098
+                      }
+                    },
+                    "评分详情": {
+                      "combined_score": 0.105
+                    }
+                  }
+                ],
+                "评分详情": {
+                  "combined_score": 0.28
+                }
+              }
+            ],
+            "评分详情": {
+              "combined_score": 0.315
+            }
+          },
+          {
+            "名称": "服装",
+            "描述": "人物穿着的白色长袖衬衫和裙子。",
+            "段落ID": "段落5.1.2",
+            "形式": {
+              "服装颜色": {
+                "名称": "服装颜色",
+                "描述": "服装主体为纯白色,无其他图案或颜色点缀。",
+                "类型": "色彩",
+                "形式ID": "形式16"
+              },
+              "服装款式": {
+                "名称": "服装款式",
+                "描述": "长袖衬衫和裙子,衬衫袖口有纽扣,衣身宽松,腰部有收腰设计,裙子部分为长裙,整体风格简约。",
+                "类型": "形态",
+                "形式ID": "形式14"
+              },
+              "清晰度": {
+                "名称": "清晰度",
+                "描述": "服装的材质纹理和褶皱细节清晰可见,无模糊现象。",
+                "类型": "清晰度",
+                "形式ID": "形式1"
+              },
+              "评分详情": {
+                "combined_score": 0.816
+              }
+            },
+            "评分详情": {
+              "combined_score": 0.818
+            }
+          },
+          {
+            "名称": "调色板",
+            "描述": "人物右手持有的椭圆形调色板,上面沾满了各种颜色的颜料。",
+            "段落ID": "段落5.1.3",
+            "形式": {
+              "形状": {
+                "名称": "形状",
+                "描述": "调色板呈不规则的椭圆形,边缘圆润,中间有一个拇指孔,方便手持。",
+                "类型": "形状",
+                "形式ID": "形式17"
+              },
+              "颜色": {
+                "名称": "颜色",
+                "描述": "调色板底色为深棕色,表面覆盖着大量混合的颜料,主要颜色包括深绿色、浅绿色、蓝色、红色、黄色、白色、紫色、黑色等多种鲜艳色彩。",
+                "类型": "色彩",
+                "形式ID": "形式3"
+              },
+              "尺寸": {
+                "名称": "尺寸",
+                "描述": "调色板占据画面右下角至中心区域,其宽度约为画面宽度的75%(900像素),高度约为画面高度的37.5%(600像素),尺寸较大,方便调色。",
+                "类型": "大小",
+                "形式ID": "形式19"
+              },
+              "颜料分布": {
+                "名称": "颜料分布",
+                "描述": "颜料呈不规则块状和条状分布在调色板表面,部分颜料相互混合,形成过渡色,没有明显的区域划分,呈现出使用过的痕迹。",
+                "类型": "布局",
+                "形式ID": "形式25"
+              },
+              "评分详情": {
+                "combined_score": 0.646
+              }
+            },
+            "子段落": [
+              {
+                "名称": "颜料",
+                "描述": "调色板上混合的多种颜色的颜料,包括绿色、蓝色、红色等。",
+                "段落ID": "段落5.1.3.1",
+                "形式": {
+                  "颜色种类": {
+                    "名称": "颜色种类",
+                    "描述": "颜料种类丰富,包括深绿色、浅绿色、蓝色、红色、黄色、白色、紫色、黑色、粉色、棕色等至少10种颜色,部分颜色相互混合。",
+                    "类型": "色彩"
+                  },
+                  "颜料质地": {
+                    "名称": "颜料质地",
+                    "描述": "颜料呈膏状,堆积在调色板表面,具有一定的厚度和立体感,表面有光泽,显示出湿润的质地。",
+                    "类型": "质感"
+                  },
+                  "分布模式": {
+                    "名称": "分布模式",
+                    "描述": "颜料呈不规则的块状和条状分布,没有严格的区域划分,部分颜料相互融合,形成自然的混色效果,其中绿色颜料占据了调色板中心区域的大部分。",
+                    "类型": "布局"
+                  },
+                  "清晰度": {
+                    "名称": "清晰度",
+                    "描述": "颜料的颜色、质地和混合细节清晰可见,无模糊现象。",
+                    "类型": "清晰度",
+                    "形式ID": "形式1"
+                  },
+                  "评分详情": {
+                    "combined_score": 0.743
+                  }
+                },
+                "评分详情": {
+                  "combined_score": 0.754
+                }
+              }
+            ],
+            "评分详情": {
+              "combined_score": 0.659
+            }
+          }
+        ],
+        "评分详情": {
+          "combined_score": 0.858
+        }
+      },
+      {
+        "名称": "画架",
+        "描述": "画面左侧部分可见的木质画架。",
+        "段落ID": "段落5.2",
+        "形式": {
+          "评分详情": {
+            "combined_score": 0.373
+          }
+        },
+        "评分详情": {
+          "combined_score": 0.412
+        }
+      },
+      {
+        "名称": "背景",
+        "描述": "画面后方的绿色草地。",
+        "段落ID": "段落5.3",
+        "形式": {
+          "颜色": {
+            "名称": "颜色",
+            "描述": "背景草地呈现鲜艳的绿色,深浅不一,部分区域略带黄色调,整体色彩饱和度较高。",
+            "类型": "色彩",
+            "形式ID": "形式3"
+          },
+          "光照": {
+            "名称": "光照",
+            "描述": "自然光照,草地受光均匀,无明显阴影或高光区域,整体亮度适中。",
+            "类型": "光影",
+            "形式ID": "形式2"
+          },
+          "景深": {
+            "名称": "景深",
+            "描述": "背景草地处于景深之外,呈现出明显的虚化效果,与前景清晰的人物和调色板形成对比,突出主体。",
+            "类型": "空间",
+            "形式ID": "形式9"
+          },
+          "评分详情": {
+            "combined_score": 0.544
+          }
+        },
+        "评分详情": {
+          "combined_score": 0.569
+        }
+      }
+    ],
+    "评分详情": {
+      "combined_score": 0.965
+    }
+  }
+]

+ 14 - 0
examples/restore/presets.json

@@ -0,0 +1,14 @@
+{
+  "main": {
+    "max_iterations": 1000,
+    "skills": ["planning"],
+    "description": "主 Agent - 调研任务管理与协调"
+  },
+  "research": {
+    "system_prompt_file": "research.prompt",
+    "max_iterations": 200,
+    "temperature": 0.3,
+    "skills": ["planning", "research", "browser"],
+    "description": "调研 Agent - 根据指令搜索策略、工具、方法论等信息"
+  }
+}

+ 67 - 0
examples/restore/requirement.prompt

@@ -0,0 +1,67 @@
+---
+model: qwen3.5-plus
+temperature: 0.3
+---
+
+$system$
+
+## 角色
+你是有空杯心态、擅长搜索调研SOTA工具和方案的社媒内容制作专家,擅长规划内容制作流程和计划。
+你的任务是,根据需求,充分调研最新实现方案,制定内容制作策略。尽可能使用AI工具或获取网络资源来完成内容制作,尽量减少实景拍摄。
+
+
+## 工作流程
+
+### 第一步:了解制作需求
+读取 `%input_dir%/analysis.json`,了解内容品类、主要特征,并提取亮点、下限点和需求清单。
+
+### 第二步:以制定内容制作的流程和计划为目标,梳理调研需求
+根据制作需求,判断为了制定SOTA、可靠的制作工序规划,需要调研哪些方向的信息。
+
+### 第三步:循环迭代地向调研agent提问、评估调研结果、更新内容制作工序计划
+1. **提问**:向 subagent 提出调研问题
+   - MUST 调用工具 `agent(task="string - 一句话描述调研需求", agent_type="research")`
+   - **严格禁止**在 task 中预设猜想的具体工具名称或示例
+2. **评估**:subagent 返回后(可能是阶段性结果),读取调研结果并评估:
+   - **相关性**:找到的方案/工具是不是我要的方向?
+   - **可用性**:找到的工具能不能被 agent 使用?(过滤纯手机 app、本地桌面应用如 PS 等)
+   - **时效性**:找到的工具是不是过时了?(AI工具迭代很快,6个月前的信息都大概率过时了)
+   - **信息完整性**:找到的信息是否足够支撑后续选择?(信息够不够)
+3. **追问或结束**:
+   - 结论为"需补充"→ 用 `continue_from` 调用同一个 subagent,**在 task 中明确告知**:
+     - 还缺什么:缺少哪些必需/建议信息,或需要补充哪些方向的工具
+     - 建议搜索方向:给出具体的搜索建议(如"搜索该工具的用户评价"、"寻找该领域的其他工具")
+   - 结论为"通过"→ 进入下一个问题或结束调研
+4. **基于最新信息思考**:
+   - 最新信息是否带来了新的思路?
+   - 是否需要更新原来的调研需求分析、提出新的调研问题?
+5. **创建或更新制作思路**
+   - 根据最新信息,撰写或更新制作思路(路径:%output_dir%/plan_thinking.md")
+循环1-5的步骤,直到你对获取到的信息感到充分和满意。预期每个调研方向会经历 2-3 轮追问。
+
+### 第四步:制定制作工序计划
+综合所有调研结果和制作思路思考,确定最终的制作计划。
+
+**基于工具评估,选择合适工具**:
+1. **内在维度**(工具自带的属性)
+   - 时效性:越新越好
+   - 智能化:越智能的越好(如:AI 工具比非 AI 工具工具好)
+   - 通用性:越通用的越好
+2. **外部置信度**(外界的反馈与背书)
+   - 交叉验证(曝光率):在不同平台、不同内容中提及次数越多的越好
+   - 专家/平台背书(权威性):
+     - 赛道内头部 KOL 的推荐
+     - 专业平台的榜单(如 Hugging Face 榜单、liblib 热门榜单)
+   - 帖子本身热度高、评论正面反馈多
+   - 有实际效果案例展示
+
+**选择策略**:
+- 优先选择"内在维度强 + 外部置信度高"的工具
+- 如果工具在某个维度较弱,需要在"备选工具"中列出替代方案
+- 如果多个工具能力相近,选择外部置信度更高的
+
+**输出**:`%output_dir%/plan.md`,需要包含:主要步骤和各步骤的:理由、输入、输出、关联需求、风险(若有)、其他(若有)
+
+$user$
+基于以下输入目录中的需求分析,进行充分的调研,并在调研过程中持续思考、根据调研所得信息不断调整调研方向;最终制定指定内容的基于 SOTA 工具的内容制作计划:
+%input_dir%

+ 116 - 0
examples/restore/research.prompt

@@ -0,0 +1,116 @@
+---
+model: sonnet-4.6
+temperature: 0.3
+---
+
+$system$
+## 角色
+你是一个调研专家,负责根据指令搜索并如实记录调研发现。
+
+**你的边界**:只负责搜索和记录,不负责制定策略。发现的工序流程、方案、案例都要如实记录,但不要自己设计工序。
+**调研结果的形式可以多样**:单个工具、工序流程、真实案例都可以。但无论哪种形式,**必须落到具体工具**——每个步骤用什么工具来执行,需要明确。
+
+## 执行流程
+
+### 第一步:理解调研目标
+
+### 第二步:执行搜索
+
+**搜索优先级**:
+1. **知识库优先**:用 `knowledge_search` 按需求关键词搜索,查看已有策略经验、工具评估、工作流总结
+2. **线上调研**:知识库结果不充分时,进行线上搜索
+
+**搜索方法**:
+
+- **需求驱动,不预设工具**:从需求出发构建 query,从结果中发现工具
+  - **query 构建原则**:从需求出发,不要预设工具
+    - ✓ 正确示例:"如何生成高质量的角色一致性图像"、"AI 生成食物素材的最佳方案"
+    - ✗ 错误示例:"Midjourney 角色一致性教程"、"ComfyUI 食物生成工作流"
+  - 先搜索"如何解决某个需求",从结果中发现工具,而不是一开始就锁定某个工具
+  - 工具不对可以换:如果调研中发现某工具无法满足需求,立即换方向,不要死磕
+
+- **双向推演**:
+  - 需求常见时 → 找案例分享 → 提取背后的工具组合
+  - 需求冷门时 → 发现工具 → 搜索工具用例 → 判断质量
+
+- **粗到细**:先找该类型下有哪些工具/方案,再对相关的深入调研
+
+### 第三步:反思与调整
+
+在搜索过程中,你需要主动进行反思和调整:
+每完成 1-2 轮搜索后,在继续前先评估:
+- 当前方向是否有效?是否偏离需求?
+- 结果质量如何?下一轮应该调整 query 还是换角度?
+- 可选调用 `reflect` 工具辅助判断
+根据反思结果调整后续搜索策略,直到你认为信息充分或遇到明确的阻塞。
+
+### 第四步:结束与输出
+
+**何时结束**:
+- 信息已充分覆盖调研目标
+- 搜索结果开始重复,无新信息
+- 方向不明确,需要用户指导
+
+**如何结束**:
+输出一条纯文本消息(不带 tool_call),概括:发现了什么、还缺什么
+
+
+## 输出格式
+
+**Schema**:
+
+```jsonschema
+{
+  "搜索主题": "string — 本次搜索主题",
+  "搜索轨迹": "string — 搜索过程:尝试了哪些 query、如何调整方向等",
+  "调研发现": [
+    {
+      "名称": "string — 发现项名称(工具名/方案名/案例名)",
+      "类型": "tool | workflow | case — 单个工具 / 工序流程或整体方案 / 真实案例",
+      "来源": "string — 来源(knowledge_id / URL / 帖子链接)",
+      "核心描述": "string — 核心思路或能力描述",
+      "工序步骤": [
+        {
+          "步骤名称": "string — 步骤名称(如:生成线稿、角色一致性处理)",
+          "使用工具": "string — 该步骤使用的具体工具名称",
+          "说明": "string — 该步骤的操作说明"
+        }
+      ],
+      "工具信息": {
+        "工具名称": "string — 工具名称(类型为 tool 时必填)",
+        "仓库或链接": "string — 仓库或官网链接",
+        "输入格式": "string — 输入格式",
+        "输出格式": "string — 输出格式",
+        "最近更新": "string — 最近更新时间",
+        "能力": ["string — 工具能力"],
+        "限制": ["string — 工具限制"]
+      },
+      "外部评价": {
+        "专家或KOL推荐": ["string — 来源 + 评价摘要"],
+        "社区反馈": ["string — 来源 + 反馈摘要"],
+        "热度指标": "string — 提及次数、榜单排名、帖子热度等"
+      },
+      "使用案例": [
+        {
+          "描述": "string — 用例描述",
+          "来源链接": "string — 来源链接",
+          "相似度": "high | medium | low"
+        }
+      ],
+      "优点": ["string"],
+      "缺点": ["string"],
+      "风险": ["string"]
+    }
+  ]
+}
+```
+
+**字段说明**:
+- `工序步骤`:类型为 `workflow` 或 `case` 时填写,逐步骤记录用了什么工具
+- `工具信息`:类型为 `tool` 时必填;`workflow`/`case` 类型中,如果整体方案依赖某个核心工具(如 ComfyUI),也可填写
+- `外部评价`:尽量填写,是主 agent 选择工具时的重要参考;找不到可留空
+
+
+## 注意事项
+- `search_posts` 不好用时改用 `browser-use`
+- 如果调研过程中遇到不确定的问题,要停下来询问用户

+ 378 - 0
examples/restore/run.py

@@ -0,0 +1,378 @@
+"""
+示例(简化版 - 使用框架交互功能)
+
+使用 Agent 模式 + Skills + 框架交互控制器
+
+新功能:
+1. 使用框架提供的 InteractiveController
+2. 使用配置文件管理运行参数
+3. 支持命令行随时打断(输入 'p' 暂停,'q' 退出)
+4. 暂停后可插入干预消息
+5. 支持触发经验总结
+6. 查看当前 GoalTree
+7. 支持通过 --trace <ID> 恢复已有 Trace 继续执行
+"""
+
+import argparse
+import os
+import sys
+import asyncio
+from pathlib import Path
+
+# Clash Verge TUN 模式兼容:禁止 httpx/urllib 自动检测系统 HTTP 代理
+os.environ.setdefault("no_proxy", "*")
+
+# 添加项目根目录到 Python 路径
+sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+from dotenv import load_dotenv
+load_dotenv()
+
+from agent.llm.prompts import SimplePrompt
+from agent.core.runner import AgentRunner, RunConfig
+from agent.trace import (
+    FileSystemTraceStore,
+    Trace,
+    Message,
+)
+from agent.llm import create_qwen_llm_call
+from agent.cli import InteractiveController
+from agent.utils import setup_logging
+from agent.tools.builtin.browser.baseClass import init_browser_session, kill_browser_session
+
+# 导入自定义工具(触发 @tool 注册)
+from .tools.reflect import reflect
+
+# 导入项目配置
+from config import RUN_CONFIG, SKILLS_DIR, TRACE_STORE_PATH, DEBUG, LOG_LEVEL, LOG_FILE, BROWSER_TYPE, HEADLESS, INPUT_DIR, OUTPUT_DIR
+
+
+async def main():
+    # 解析命令行参数
+    parser = argparse.ArgumentParser(description="任务 (Agent 模式 + 交互增强)")
+    parser.add_argument(
+        "--trace", type=str, default=None,
+        help="已有的 Trace ID,用于恢复继续执行(不指定则新建)",
+    )
+    args = parser.parse_args()
+
+    # 路径配置
+    base_dir = Path(__file__).parent
+    project_root = base_dir.parent.parent
+    prompt_path = base_dir / "requirement.prompt"
+    output_dir = project_root / OUTPUT_DIR
+    output_dir.mkdir(parents=True, exist_ok=True)
+
+    # 1. 配置日志
+    setup_logging(level=LOG_LEVEL, file=LOG_FILE)
+
+    # 2. 加载项目级 presets
+    print("2. 加载 presets...")
+    presets_path = base_dir / "presets.json"
+    if presets_path.exists():
+        from agent.core.presets import load_presets_from_json
+        load_presets_from_json(str(presets_path))
+        print(f"   - 已加载项目 presets")
+    else:
+        print(f"   - 未找到 presets.json,跳过")
+
+    # 3. 加载 prompt
+    print("3. 加载 prompt...")
+    prompt = SimplePrompt(prompt_path)
+
+    # 4. 构建任务消息
+    print("4. 构建任务消息...")
+    print(f"   - 输入目录: {INPUT_DIR}")
+    print(f"   - 输出 ID: {OUTPUT_DIR}")
+    messages = prompt.build_messages(input_dir=INPUT_DIR, output_dir=OUTPUT_DIR)
+
+    # 5. 初始化浏览器
+    browser_mode_names = {"cloud": "云浏览器", "local": "本地浏览器", "container": "容器浏览器"}
+    browser_mode_name = browser_mode_names.get(BROWSER_TYPE, BROWSER_TYPE)
+    print(f"5. 正在初始化{browser_mode_name}...")
+    await init_browser_session(
+        browser_type=BROWSER_TYPE,
+        headless=HEADLESS,
+        url="https://www.google.com/",
+        profile_name=""
+    )
+    print(f"   ✅ {browser_mode_name}初始化完成\n")
+
+    # 6. 创建 Agent Runner
+    print("6. 创建 Agent Runner...")
+    print(f"   - Skills 目录: {SKILLS_DIR}")
+
+    # 从 prompt 的 frontmatter 中提取模型配置(优先于 config.py)
+    prompt_model = prompt.config.get("model", None)
+    if prompt_model:
+        model_for_llm = prompt_model
+        print(f"   - 模型 (from prompt): {model_for_llm}")
+    else:
+        model_for_llm = RUN_CONFIG.model
+        print(f"   - 模型 (from config): {model_for_llm}")
+
+    store = FileSystemTraceStore(base_path=TRACE_STORE_PATH)
+    runner = AgentRunner(
+        trace_store=store,
+        llm_call=create_qwen_llm_call(model=model_for_llm),
+        skills_dir=SKILLS_DIR,
+        debug=DEBUG
+    )
+
+    # 7. 创建交互控制器
+    interactive = InteractiveController(
+        runner=runner,
+        store=store,
+        enable_stdin_check=True
+    )
+    # 将 stdin 检查回调注入 runner,供子 agent 执行期间使用
+    runner.stdin_check = interactive.check_stdin
+
+    # 8. 任务信息
+    task_name = RUN_CONFIG.name or base_dir.name
+    print("=" * 60)
+    print(f"{task_name}")
+    print("=" * 60)
+    print("💡 交互提示:")
+    print("   - 执行过程中输入 'p' 或 'pause' 暂停并进入交互模式")
+    print("   - 执行过程中输入 'q' 或 'quit' 停止执行")
+    print("=" * 60)
+    print()
+
+    # 9. 判断是新建还是恢复
+    resume_trace_id = args.trace
+    if resume_trace_id:
+        existing_trace = await store.get_trace(resume_trace_id)
+        if not existing_trace:
+            print(f"\n错误: Trace 不存在: {resume_trace_id}")
+            sys.exit(1)
+        print(f"恢复已有 Trace: {resume_trace_id[:8]}...")
+        print(f"   - 状态: {existing_trace.status}")
+        print(f"   - 消息数: {existing_trace.total_messages}")
+        print(f"\n💡 提示:恢复 Trace 时会先进入交互菜单,您可以选择从指定消息续跑")
+    else:
+        print(f"启动新 Agent...")
+
+    print()
+
+    final_response = ""
+    current_trace_id = resume_trace_id
+    current_sequence = 0
+    should_exit = False
+
+    try:
+        # 配置
+        run_config = RUN_CONFIG
+        if resume_trace_id:
+            initial_messages = None
+            run_config.trace_id = resume_trace_id
+        else:
+            initial_messages = messages
+            run_config.name = f"{task_name}:调研任务"
+
+        while not should_exit:
+            if current_trace_id:
+                run_config.trace_id = current_trace_id
+
+            final_response = ""
+
+            # 如果是恢复 trace 或 trace 已完成/失败且没有新消息,进入交互菜单
+            if current_trace_id and initial_messages is None:
+                check_trace = await store.get_trace(current_trace_id)
+                if check_trace:
+                    # 显示 trace 状态
+                    if check_trace.status == "completed":
+                        print(f"\n[Trace] ✅ 已完成")
+                        print(f"  - Total messages: {check_trace.total_messages}")
+                        print(f"  - Total cost: ${check_trace.total_cost:.4f}")
+                    elif check_trace.status == "failed":
+                        print(f"\n[Trace] ❌ 已失败: {check_trace.error_message}")
+                    elif check_trace.status == "stopped":
+                        print(f"\n[Trace] ⏸️ 已停止")
+                        print(f"  - Total messages: {check_trace.total_messages}")
+                    else:
+                        print(f"\n[Trace] 📊 状态: {check_trace.status}")
+                        print(f"  - Total messages: {check_trace.total_messages}")
+
+                    current_sequence = check_trace.head_sequence
+
+                    menu_result = await interactive.show_menu(current_trace_id, current_sequence)
+
+                    if menu_result["action"] == "stop":
+                        break
+                    elif menu_result["action"] == "continue":
+                        new_messages = menu_result.get("messages", [])
+                        if new_messages:
+                            initial_messages = new_messages
+                            run_config.after_sequence = menu_result.get("after_sequence")
+                        else:
+                            initial_messages = []
+                            run_config.after_sequence = None
+                        continue
+                    break
+
+            # 如果没有进入菜单(新建 trace),设置初始消息
+            if initial_messages is None:
+                initial_messages = []
+
+            print(f"{'▶️ 开始执行...' if not current_trace_id else '▶️ 继续执行...'}")
+
+            # 执行 Agent
+            paused = False
+            try:
+                async for item in runner.run(messages=initial_messages, config=run_config):
+                    # 检查用户中断
+                    cmd = interactive.check_stdin()
+                    if cmd == 'pause':
+                        print("\n⏸️ 正在暂停执行...")
+                        if current_trace_id:
+                            await runner.stop(current_trace_id)
+                        await asyncio.sleep(0.5)
+
+                        menu_result = await interactive.show_menu(current_trace_id, current_sequence)
+
+                        if menu_result["action"] == "stop":
+                            should_exit = True
+                            paused = True
+                            break
+                        elif menu_result["action"] == "continue":
+                            new_messages = menu_result.get("messages", [])
+                            if new_messages:
+                                initial_messages = new_messages
+                                after_seq = menu_result.get("after_sequence")
+                                if after_seq is not None:
+                                    run_config.after_sequence = after_seq
+                                paused = True
+                                break
+                            else:
+                                initial_messages = []
+                                run_config.after_sequence = None
+                                paused = True
+                                break
+
+                    elif cmd == 'quit':
+                        print("\n🛑 用户请求停止...")
+                        if current_trace_id:
+                            await runner.stop(current_trace_id)
+                        should_exit = True
+                        break
+
+                    # 处理 Trace 对象
+                    if isinstance(item, Trace):
+                        current_trace_id = item.trace_id
+                        if item.status == "running":
+                            print(f"[Trace] 开始: {item.trace_id[:8]}...")
+                        elif item.status == "completed":
+                            print(f"\n[Trace] ✅ 完成")
+                            print(f"  - Total messages: {item.total_messages}")
+                            print(f"  - Total cost: ${item.total_cost:.4f}")
+                        elif item.status == "failed":
+                            print(f"\n[Trace] ❌ 失败: {item.error_message}")
+                        elif item.status == "stopped":
+                            print(f"\n[Trace] ⏸️ 已停止")
+
+                    # 处理 Message 对象
+                    elif isinstance(item, Message):
+                        current_sequence = item.sequence
+
+                        if item.role == "assistant":
+                            content = item.content
+                            if isinstance(content, dict):
+                                text = content.get("text", "")
+                                tool_calls = content.get("tool_calls")
+
+                                if text and not tool_calls:
+                                    final_response = text
+                                    print(f"\n[Response] Agent 回复:")
+                                    print(text)
+                                elif text:
+                                    preview = text[:150] + "..." if len(text) > 150 else text
+                                    print(f"[Assistant] {preview}")
+
+                        elif item.role == "tool":
+                            content = item.content
+                            tool_name = "unknown"
+                            if isinstance(content, dict):
+                                tool_name = content.get("tool_name", "unknown")
+
+                            if item.description and item.description != tool_name:
+                                desc = item.description[:80] if len(item.description) > 80 else item.description
+                                print(f"[Tool Result] ✅ {tool_name}: {desc}...")
+                            else:
+                                print(f"[Tool Result] ✅ {tool_name}")
+
+            except Exception as e:
+                print(f"\n执行出错: {e}")
+                import traceback
+                traceback.print_exc()
+
+            if paused:
+                if should_exit:
+                    break
+                continue
+
+            if should_exit:
+                break
+
+            # Runner 退出后显示交互菜单
+            if current_trace_id:
+                menu_result = await interactive.show_menu(current_trace_id, current_sequence)
+
+                if menu_result["action"] == "stop":
+                    break
+                elif menu_result["action"] == "continue":
+                    new_messages = menu_result.get("messages", [])
+                    if new_messages:
+                        initial_messages = new_messages
+                        run_config.after_sequence = menu_result.get("after_sequence")
+                    else:
+                        initial_messages = []
+                        run_config.after_sequence = None
+                    continue
+            break
+
+    except KeyboardInterrupt:
+        print("\n\n用户中断 (Ctrl+C)")
+        if current_trace_id:
+            await runner.stop(current_trace_id)
+    finally:
+        # 清理浏览器会话
+        try:
+            await kill_browser_session()
+        except Exception:
+            pass
+
+    # 7. 输出结果
+    if final_response:
+        print()
+        print("=" * 60)
+        print("Agent 响应:")
+        print("=" * 60)
+        print(final_response)
+        print("=" * 60)
+        print()
+
+        output_file = output_dir / "result.txt"
+        with open(output_file, 'w', encoding='utf-8') as f:
+            f.write(final_response)
+
+        print(f"✓ 结果已保存到: {output_file}")
+        print()
+
+    # 可视化提示
+    if current_trace_id:
+        print("=" * 60)
+        print("可视化 Step Tree:")
+        print("=" * 60)
+        print("1. 启动 API Server:")
+        print("   python3 api_server.py")
+        print()
+        print("2. 浏览器访问:")
+        print("   http://localhost:8000/api/traces")
+        print()
+        print(f"3. Trace ID: {current_trace_id}")
+        print("=" * 60)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 139 - 0
examples/restore/tools/reflect.py

@@ -0,0 +1,139 @@
+"""
+reflect 工具 — 轻量反思
+
+用普通模型快速评估当前搜索结果,输出简短思路和下一步建议。
+继承调用者的完整对话历史作为上下文。
+"""
+
+import logging
+import os
+from typing import Any, Dict, List, Optional
+
+from agent.llm.qwen import qwen_llm_call
+from agent.tools import tool
+from agent.tools.models import ToolContext, ToolResult
+
+
+logger = logging.getLogger(__name__)
+
+# 反思模型配置(普通模式,非 thinking)
+REFLECT_MODEL = os.getenv("REFLECT_MODEL", "qwen-plus")
+
+REFLECT_SYSTEM_PROMPT = """你是调研助手。根据对话历史和当前搜索结果,简要回答:
+1. 本轮搜到了什么有价值的信息?缺了什么?
+2. 下一轮搜什么?给出 1-3 个具体 query 词
+3. 是否需要换搜索渠道或角度?
+
+要求:直接输出思路,不要分析框架,不要长篇大论。3-5 句话即可。"""
+
+REFLECT_USER_TEMPLATE = """需求:{question}
+
+本轮搜索结果:
+{findings}
+
+简要反思,给出下一步 query。"""
+
+
+async def _fetch_caller_history(context: Optional[Dict[str, Any]]) -> List[Dict]:
+    """从 context 中获取调用者的历史消息队列"""
+    if not context:
+        return []
+
+    store = context.get("store")
+    trace_id = context.get("trace_id")
+    if not store or not trace_id:
+        return []
+
+    try:
+        trace = await store.get_trace(trace_id)
+        if not trace:
+            return []
+
+        messages = await store.get_main_path_messages(
+            trace_id, trace.head_sequence
+        )
+
+        # 转为 LLM 消息格式
+        history = []
+        for msg in messages:
+            llm_dict = msg.to_llm_dict()
+            if llm_dict:
+                history.append(llm_dict)
+
+        logger.info(f"reflect: 获取到 {len(history)} 条历史消息")
+        return history
+
+    except Exception as e:
+        logger.warning(f"reflect: 获取历史消息失败: {e}")
+        return []
+
+
+@tool(
+    description="轻量反思:快速评估本轮搜索结果,输出简短思路和下一步 query 建议",
+    hidden_params=["context"],
+)
+async def reflect(
+    question: str,
+    findings: str,
+    context: Optional[ToolContext] = None,
+) -> ToolResult:
+    """
+    对本轮搜索结果进行轻量反思,给出下一步搜索思路。
+
+    Args:
+        question: 原始调研问题/需求描述
+        findings: 本轮搜索结果摘要
+    """
+    # 获取调用者的历史消息
+    # context 可能是 ToolContextImpl(有 .context 属性)或直接是 dict
+    if context is None:
+        caller_context = None
+    elif isinstance(context, dict):
+        caller_context = context
+    else:
+        caller_context = getattr(context, 'context', None)
+    caller_history = await _fetch_caller_history(caller_context)
+
+    # 构建消息:system + 调用者历史 + 反思请求
+    messages = [{"role": "system", "content": REFLECT_SYSTEM_PROMPT}]
+
+    if caller_history:
+        messages.extend(caller_history)
+
+    messages.append({
+        "role": "user",
+        "content": REFLECT_USER_TEMPLATE.format(
+            question=question,
+            findings=findings,
+        ),
+    })
+
+    try:
+        result = await qwen_llm_call(
+            messages=messages,
+            model=REFLECT_MODEL,
+            temperature=0.2,
+        )
+
+        content = result["content"]
+        msg_count = len(caller_history)
+        cost = result.get("cost", 0.0)
+        reasoning_tokens = result.get("reasoning_tokens", 0)
+
+        return ToolResult(
+            title=f"反思完成 (model: {REFLECT_MODEL}, 继承 {msg_count} 条历史)",
+            output=content,
+            tool_usage={
+                "model": REFLECT_MODEL,
+                "prompt_tokens": result.get("prompt_tokens", 0),
+                "completion_tokens": result.get("completion_tokens", 0),
+                "reasoning_tokens": reasoning_tokens,
+                "cost": cost,
+            },
+        )
+
+    except Exception as e:
+        return ToolResult(
+            title="reflect 失败",
+            output=f"调用反思模型出错: {str(e)}",
+        )

+ 90 - 0
tests/test_liblibai_tool.py

@@ -0,0 +1,90 @@
+"""测试 liblibai_controlnet 工具 — 通过 Router API 调用
+
+用法:
+    1. 先启动 Router:uv run python -m tool_agent
+    2. 运行测试:python tests/test_liblibai_tool.py
+"""
+
+import sys
+import time
+
+if sys.platform == 'win32':
+    sys.stdout.reconfigure(encoding='utf-8')
+
+import httpx
+
+ROUTER_URL = "http://43.106.118.91:8001"
+IMAGE_URL = "https://liblibai-airship-temp.oss-cn-beijing.aliyuncs.com/aliyun-cn-prod/73ed6ae42b144d21bf566e05b5a6c138.png"
+
+
+def main():
+    print("=" * 50)
+    print("测试 liblibai_controlnet 工具")
+    print("=" * 50)
+
+    # 1. 检查 Router 是否在线
+    try:
+        resp = httpx.get(f"{ROUTER_URL}/health", timeout=3)
+        print(f"Router 状态: {resp.json()}")
+    except httpx.ConnectError:
+        print(f"无法连接 Router ({ROUTER_URL})")
+        print("请先启动: uv run python -m tool_agent")
+        sys.exit(1)
+
+    # 2. 搜索工具,确认已注册
+    print("\n--- 搜索工具 ---")
+    resp = httpx.post(f"{ROUTER_URL}/search_tools", json={"keyword": "liblib"})
+    tools = resp.json()
+    print(f"找到 {tools['total']} 个工具")
+    for t in tools["tools"]:
+        print(f"  {t['tool_id']}: {t['name']} (state={t['state']})")
+
+    # 3. 调用 liblibai_controlnet 工具
+    print("\n--- 调用 ControlNet 生图 ---")
+    print(f"图片: {IMAGE_URL}")
+    print(f"提示词: simple white line art, cat, black background")
+    print("提交中...")
+
+    resp = httpx.post(
+        f"{ROUTER_URL}/select_tool",
+        json={
+            "tool_id": "liblibai_controlnet",
+            "params": {
+                "image": IMAGE_URL,
+                "prompt": "simple white line art, cat, black background",
+                "negative_prompt": "lowres, bad anatomy, text, error",
+                "width": 512,
+                "height": 512,
+                "steps": 20,
+                "cfg_scale": 7,
+                "img_count": 1,
+                "control_weight": 1.0,
+                "preprocessor": 1,
+                "canny_low": 100,
+                "canny_high": 200
+            }
+        },
+        timeout=300  # 生图可能需要几分钟
+    )
+
+    result = resp.json()
+    print(f"\n响应状态: {result.get('status')}")
+
+    if result.get("status") == "success":
+        data = result.get("result", {})
+        print(f"任务状态: {data.get('status')}")
+        print(f"任务 ID: {data.get('task_id')}")
+        images = data.get("images", [])
+        if images:
+            print(f"生成图片数: {len(images)}")
+            for i, url in enumerate(images):
+                print(f"  [{i+1}] {url}")
+            print("\n测试通过!")
+        else:
+            print("未返回图片,可能任务仍在处理中")
+    else:
+        print(f"错误: {result.get('error')}")
+
+
+if __name__ == "__main__":
+    main()

+ 289 - 0
tests/test_router_api.py

@@ -0,0 +1,289 @@
+"""测试 Router 核心接口
+
+用法:
+    uv run python tests/test_router_api.py                               # 只跑 health check
+    uv run python tests/test_router_api.py --search                      # 搜索工具列表
+    uv run python tests/test_router_api.py --search image                # 关键词搜索
+    uv run python tests/test_router_api.py --status                      # 工具运行状态
+    uv run python tests/test_router_api.py --select image_stitcher       # 调用指定工具
+    uv run python tests/test_router_api.py --stitch                      # 测试图片拼接
+    uv run python tests/test_router_api.py --create                      # 默认任务
+    uv run python tests/test_router_api.py --create image_stitcher       # 指定任务文件
+"""
+
+import argparse
+import base64
+import json
+import sys
+import time
+from pathlib import Path
+
+import httpx
+
+BASE_URL = "http://43.106.118.91:8001"
+TASKS_DIR = Path(__file__).parent / "tasks"
+TEST_IMAGES_DIR = TASKS_DIR / "stitcher_images"
+OUTPUT_DIR = Path(__file__).parent / "output"
+
+
+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 test_health():
+    print("=== Health Check ===")
+    resp = httpx.get(f"{BASE_URL}/health")
+    print(f"  Status : {resp.status_code}")
+    print(f"  Body   : {json.dumps(resp.json(), ensure_ascii=False, indent=4)}")
+    assert resp.status_code == 200
+    print("  [PASS]")
+
+
+def test_search_tools(keyword: str = None):
+    print(f"=== Search Tools{f' (keyword={keyword!r})' if keyword else ''} ===")
+    payload = {"keyword": keyword} if keyword else {}
+    resp = httpx.post(f"{BASE_URL}/search_tools", json=payload)
+    print(f"  Status : {resp.status_code}")
+    if resp.status_code != 200:
+        print(f"  Body   : {resp.text}")
+        print("  [FAIL]")
+        return
+    data = resp.json()
+    print(f"  Total  : {data['total']}")
+    for t in data["tools"]:
+        print(f"\n  [{t['tool_id']}]")
+        print(f"    name          : {t['name']}")
+        print(f"    category      : {t.get('category', '')}")
+        print(f"    state         : {t['state']}")
+        print(f"    runtime       : {t.get('runtime_type', '')}  host_dir={t.get('host_dir', '')}")
+        print(f"    endpoint      : {t.get('http_method', '')} {t.get('endpoint_path', '')}  port={t.get('port')}")
+        print(f"    stream_support: {t.get('stream_support', False)}")
+        print(f"    description   : {t.get('description', '')}")
+        print(f"    params ({len(t.get('params', []))}):")
+        for p in t.get("params", []):
+            req_mark = "*" if p["required"] else " "
+            default_str = f"  default={p['default']}" if p.get("default") is not None else ""
+            enum_str = f"  enum={p['enum']}" if p.get("enum") else ""
+            print(f"      {req_mark} {p['name']:<25} {p['type']:<12} {p.get('description', '')}{default_str}{enum_str}")
+        if t.get("output_schema"):
+            out_props = t["output_schema"].get("properties", {})
+            print(f"    output ({len(out_props)}):")
+            for oname, odef in out_props.items():
+                print(f"        {oname:<25} {odef.get('type', ''):<12} {odef.get('description', '')}")
+    print("\n  [PASS]")
+
+
+def test_tools_status():
+    print("=== Tools Status ===")
+    resp = httpx.get(f"{BASE_URL}/tools/status")
+    print(f"  Status : {resp.status_code}")
+    data = resp.json()
+    print(f"  Total  : {len(data['tools'])}")
+    for t in data["tools"]:
+        print(f"    - {t['tool_id']}")
+        print(f"        state  : {t['state']}")
+        print(f"        port   : {t.get('port')}")
+        print(f"        pid    : {t.get('pid')}")
+        print(f"        sources: {[s['type'] for s in t.get('sources', [])]}")
+        if t.get("last_error"):
+            print(f"        error  : {t['last_error']}")
+    print("  [PASS]")
+
+
+def test_select_tool(tool_id: str):
+    print(f"=== Select Tool (tool_id={tool_id!r}) ===")
+    resp = httpx.post(f"{BASE_URL}/select_tool", json={
+        "tool_id": tool_id,
+        "params": {}
+    }, timeout=30)
+    print(f"  Status : {resp.status_code}")
+    data = resp.json()
+    print(f"  Result :")
+    print(f"    status: {data.get('status')}")
+    if data.get("error"):
+        print(f"    error : {data['error']}")
+    else:
+        result_str = json.dumps(data.get("result"), ensure_ascii=False, indent=6)
+        print(f"    result: {result_str[:500]}")
+    print("  [PASS]")
+
+
+def test_stitch_images():
+    print("=== Test Image Stitcher ===")
+    if not TEST_IMAGES_DIR.exists():
+        print(f"  ERROR: Test images directory not found: {TEST_IMAGES_DIR}")
+        print("  [SKIP]")
+        return
+
+    image_files = sorted(TEST_IMAGES_DIR.glob("*.png"))
+    if len(image_files) < 2:
+        print(f"  ERROR: Need at least 2 images, found {len(image_files)}")
+        print("  [SKIP]")
+        return
+
+    print(f"  Images : {len(image_files)} found")
+    images_b64 = []
+    for img_path in image_files[:6]:
+        with open(img_path, "rb") as f:
+            images_b64.append(base64.b64encode(f.read()).decode())
+        print(f"    - {img_path.name}")
+
+    print(f"  Calling image_stitcher (grid, 2 columns)...")
+    try:
+        resp = httpx.post(f"{BASE_URL}/select_tool", json={
+            "tool_id": "image_stitcher",
+            "params": {
+                "images": images_b64,
+                "direction": "grid",
+                "columns": 2,
+                "spacing": 10,
+                "background_color": "#FFFFFF",
+            }
+        }, timeout=60)
+        print(f"  Status : {resp.status_code}")
+        data = resp.json()
+        if data["status"] == "success":
+            result = data["result"]
+            print(f"  Result :")
+            print(f"    width : {result['width']}")
+            print(f"    height: {result['height']}")
+            OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
+            output_path = OUTPUT_DIR / "stitched_result.png"
+            with open(output_path, "wb") as f:
+                f.write(base64.b64decode(result["image"]))
+            print(f"    saved : {output_path}")
+            print("  [PASS]")
+        else:
+            print(f"  ERROR : {data.get('error', 'unknown')}")
+            print("  [FAIL]")
+    except httpx.TimeoutException:
+        print("  ERROR : Request timeout")
+        print("  [FAIL]")
+    except Exception as e:
+        print(f"  ERROR : {e}")
+        print("  [FAIL]")
+
+
+def load_task_spec(task_name: str) -> dict:
+    task_file = TASKS_DIR / f"{task_name}.json"
+    if not task_file.exists():
+        print(f"  ERROR: Task file not found: {task_file}")
+        print("  Available tasks:")
+        if TASKS_DIR.exists():
+            for f in TASKS_DIR.glob("*.json"):
+                print(f"    - {f.stem}")
+        sys.exit(1)
+    with open(task_file, "r", encoding="utf-8") as f:
+        return json.load(f)
+
+
+def test_create_tool(task_name: str = None):
+    print(f"=== Create Tool{f' (task={task_name!r})' if task_name else ''} ===")
+    if task_name:
+        task_data = load_task_spec(task_name)
+        print(f"  File       : tests/tasks/{task_name}.json")
+        print(f"  Description: {task_data['description'][:80]}")
+    else:
+        task_data = {"description": "创建一个简单的文本计数工具,输入文本,返回字数和字符数"}
+        print(f"  Description: {task_data['description']}")
+
+    resp = httpx.post(f"{BASE_URL}/create_tool", json=task_data)
+    data = resp.json()
+    task_id = data["task_id"]
+    print(f"  Task ID    : {task_id}")
+    print(f"  Status     : {data['status']}")
+    assert data["status"] == "pending"
+    print("  [SUBMITTED]")
+
+    print(f"\n  Polling task {task_id} (timeout 10min)...")
+    for i in range(120):
+        time.sleep(5)
+        resp = httpx.get(f"{BASE_URL}/tasks/{task_id}", timeout=30)
+        task = resp.json()
+        status = task["status"]
+
+        if i % 6 == 0:
+            print(f"    [{i*5}s] status={status}")
+
+        if status == "completed":
+            print(f"\n  Completed!")
+            print(f"  Result     : {str(task.get('result', ''))[:300]}")
+            resp2 = httpx.post(f"{BASE_URL}/search_tools", json={})
+            tools = resp2.json()["tools"]
+            print(f"  Registered : {[t['tool_id'] for t in tools]}")
+            print("  [PASS]")
+            return
+
+        if status == "failed":
+            print(f"\n  Failed!")
+            print(f"  Error      : {task.get('error', 'unknown')}")
+            print("  [FAIL]")
+            return
+
+    print(f"\n  Timeout after 600s")
+    print("  [TIMEOUT]")
+
+
+def main():
+    parser = argparse.ArgumentParser(description="Router API Test")
+    parser.add_argument("--search", nargs="?", const="", metavar="KEYWORD",
+                        help="search tools, optional keyword")
+    parser.add_argument("--status", action="store_true",
+                        help="show tools status")
+    parser.add_argument("--select", metavar="TOOL_ID",
+                        help="call a tool by tool_id")
+    parser.add_argument("--stitch", action="store_true",
+                        help="test image stitcher with sample images")
+    parser.add_argument("--create", nargs="?", const="", metavar="TASK_NAME",
+                        help="create tool, optional task file name")
+    args = parser.parse_args()
+
+    print(f"Target: {BASE_URL}\n")
+    check_connection()
+
+    # 始终跑 health check
+    test_health()
+
+    ran_any = False
+
+    if args.search is not None:
+        print()
+        test_search_tools(args.search or None)
+        ran_any = True
+
+    if args.status:
+        print()
+        test_tools_status()
+        ran_any = True
+
+    if args.select:
+        print()
+        test_select_tool(args.select)
+        ran_any = True
+
+    if args.stitch:
+        print()
+        test_stitch_images()
+        ran_any = True
+
+    if args.create is not None:
+        print()
+        test_create_tool(args.create or None)
+        ran_any = True
+
+    if not ran_any:
+        print()
+        print("No test specified. Available options:")
+        parser.print_help()
+
+    print("\n=== DONE ===")
+
+
+if __name__ == "__main__":
+    main()