guantao 1 день назад
Родитель
Сommit
ce43b6c60e
34 измененных файлов с 513 добавлено и 215 удалено
  1. 14 1
      agent/core/presets.py
  2. 9 0
      agent/llm/gemini.py
  3. 10 5
      examples/mini_restore/call_banana.py
  4. 19 0
      examples/mini_restore/history.json
  5. 151 123
      examples/mini_restore/workflow_loop.py
  6. 2 1
      examples/production_restore/config.py
  7. 21 8
      examples/production_restore/execution.prompt
  8. 5 5
      examples/production_restore/input/pipeline.json
  9. 23 3
      examples/production_restore/presets.json
  10. 116 57
      examples/production_restore/requirement.prompt
  11. 141 0
      examples/production_restore/research.prompt
  12. 2 2
      examples/production_restore/run.py
  13. BIN
      log.txt
  14. 0 10
      log2.txt
  15. BIN
      outputs/540bdb68-9b2/flux_generate_1775743063412_0.png
  16. BIN
      outputs/540bdb68-9b2/flux_generate_1775743308665_0.png
  17. BIN
      outputs/540bdb68-9b2/flux_generate_1775743381440_0.png
  18. BIN
      outputs/540bdb68-9b2/flux_generate_1775744492978_0.png
  19. BIN
      outputs/540bdb68-9b2/nano_banana_1775742258513_0.jpg
  20. BIN
      outputs/540bdb68-9b2/nano_banana_1775742490073_0.jpg
  21. BIN
      outputs/540bdb68-9b2/nano_banana_1775742774289_0.jpg
  22. BIN
      outputs/540bdb68-9b2/seedream_generate_1775743450714_0.jpg
  23. BIN
      outputs/540bdb68-9b2/seedream_generate_1775743784246_0.jpg
  24. BIN
      outputs/540bdb68-9b2/seedream_generate_1775744243329_0.jpg
  25. BIN
      outputs/810058fe-de8/nano_banana_1775739644055_0.jpg
  26. BIN
      outputs/810058fe-de8/nano_banana_1775739749883_0.jpg
  27. BIN
      outputs/810058fe-de8/nano_banana_1775739853853_0.jpg
  28. BIN
      outputs/810058fe-de8/nano_banana_1775739957102_0.jpg
  29. BIN
      outputs/810058fe-de8/nano_banana_1775740058087_0.jpg
  30. BIN
      outputs/810058fe-de8/nano_banana_1775740171922_0.jpg
  31. BIN
      outputs/810058fe-de8/nano_banana_1775740270417_0.jpg
  32. BIN
      outputs/810058fe-de8/nano_banana_1775740359987_0.jpg
  33. BIN
      outputs/810058fe-de8/nano_banana_1775740610837_0.jpg
  34. BIN
      outputs/810058fe-de8/nano_banana_1775740709313_0.jpg

+ 14 - 1
agent/core/presets.py

@@ -110,6 +110,7 @@ def load_presets_from_json(json_path: str) -> None:
 
     支持特殊字段:
     - system_prompt_file: 从 .prompt 文件加载 system prompt(相对于 JSON 文件所在目录)
+    - prompt_vars: 变量字典,用于替换 prompt 中的 %variable% 占位符
 
     Args:
         json_path: presets.json 文件路径
@@ -126,11 +127,23 @@ def load_presets_from_json(json_path: str) -> None:
     base_dir = json_path.parent
 
     for name, cfg in presets_data.items():
+        # 提取 prompt_vars(不传给 AgentPreset)
+        prompt_vars = cfg.pop("prompt_vars", None)
+
         # 处理 system_prompt_file
         if "system_prompt_file" in cfg:
             prompt_file = cfg.pop("system_prompt_file")
             prompt_path = base_dir / prompt_file
-            cfg["system_prompt"] = load_system_prompt_from_file(str(prompt_path))
+            system_prompt = load_system_prompt_from_file(str(prompt_path))
+
+            # 应用变量替换
+            if prompt_vars and isinstance(prompt_vars, dict):
+                for var_name, var_value in prompt_vars.items():
+                    placeholder = f"%{var_name}%"
+                    if placeholder in system_prompt:
+                        system_prompt = system_prompt.replace(placeholder, str(var_value))
+
+            cfg["system_prompt"] = system_prompt
 
         preset = AgentPreset(**cfg)
         register_preset(name, preset)

+ 9 - 0
agent/llm/gemini.py

@@ -126,9 +126,17 @@ def _convert_messages_to_gemini(messages: List[Dict]) -> tuple[List[Dict], Optio
 
         content = msg.get("content", "")
         tool_calls = msg.get("tool_calls")
+        raw_gemini_parts = msg.get("raw_gemini_parts")
 
         # Assistant 消息 + tool_calls
         if role == "assistant" and tool_calls:
+            if raw_gemini_parts:
+                contents.append({
+                    "role": "model",
+                    "parts": raw_gemini_parts
+                })
+                continue
+
             parts = []
             if content and (isinstance(content, str) and content.strip()):
                 parts.append({"text": content})
@@ -443,6 +451,7 @@ def create_gemini_llm_call(
         return {
             "content": content,
             "tool_calls": tool_calls,
+            "raw_gemini_parts": candidate.get("content", {}).get("parts", []) if candidates else [],
             "prompt_tokens": usage.input_tokens,
             "completion_tokens": usage.output_tokens,
             "reasoning_tokens": usage.reasoning_tokens,

+ 10 - 5
examples/mini_restore/call_banana.py

@@ -30,11 +30,12 @@ import json
 
 # 动态引入我们系统现成的 CDN 上传脚本
 sys.path.append(os.path.dirname(os.path.abspath(__file__)))
-sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'production_restore'))
+sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'examples', 'production_restore'))
 try:
     from upload import upload_image
-except ImportError:
-    print("错误: 找不到 upload.py。请确保在 tests 目录下运行此脚本。")
+except Exception as e:
+    import traceback
+    print(f"错误: 导入 upload.py 及其依赖链失败: {e}\n{traceback.format_exc()}")
     sys.exit(1)
 
 ROUTER_URL = "http://43.106.118.91:8001/run_tool"
@@ -61,7 +62,7 @@ async def process_images(images_list: list[str]) -> list[str]:
             print(f"⚠️ 跳过找不到的本地文件或无法识别的格式: {item}")
     return final_urls
 
-async def run_nano_banana(prompt: str, images: list[str] = None, model: str = None):
+async def run_nano_banana(prompt: str, images: list[str] = None, model: str = None, aspect_ratio: str = None):
     print(f"\n=======================")
     print(f"🍌 Nano Banana 启动中...")
     print(f"=======================")
@@ -80,6 +81,9 @@ async def run_nano_banana(prompt: str, images: list[str] = None, model: str = No
     
     if model:
         params["model"] = model
+        
+    if aspect_ratio:
+        params["aspect_ratio"] = aspect_ratio
 
     payload = {
         "tool_id": "nano_banana",
@@ -130,7 +134,8 @@ if __name__ == "__main__":
     parser.add_argument("-p", "--prompt", type=str, required=True, help="你想对 AI 喊瞎什么 (比如:用图1的赛博风画一只图2里的猫)")
     parser.add_argument("-i", "--images", type=str, nargs="+", help="无限追加的垫图清单(可以是现成的 http 链接,也可以是你电脑里的硬盘文件如 example.png)")
     parser.add_argument("-m", "--model", type=str, default=None, help="覆盖模型 (默认后台会走 gemini-3.1-flash-image-preview)")
+    parser.add_argument("-a", "--aspect_ratio", type=str, default=None, help="图片比例,例如 3:4, 16:9, 1:1 等")
     
     args = parser.parse_args()
     
-    asyncio.run(run_nano_banana(prompt=args.prompt, images=args.images, model=args.model))
+    asyncio.run(run_nano_banana(prompt=args.prompt, images=args.images, model=args.model, aspect_ratio=args.aspect_ratio))

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


+ 151 - 123
examples/mini_restore/workflow_loop.py

@@ -8,22 +8,52 @@ import re
 # 将项目根目录加入,方便导入内部包
 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')))
 
-from agent.llm.qwen import qwen_llm_call
+from agent.tools.builtin.toolhub import toolhub_call
+from agent.llm.gemini import create_gemini_llm_call
+
+from dotenv import load_dotenv
+load_dotenv()
+
+try:
+    gemini_llm_call = create_gemini_llm_call()
+except ValueError as e:
+    print(f"初始化 Gemini 失败: {e},请检查 .env。")
+    sys.exit(1)
+
 from agent.tools.builtin.search import search_posts
 
+# -----------------
+# Utility Functions
+# -----------------
+def encode_image(image_path: str) -> str:
+    with open(image_path, "rb") as image_file:
+        return base64.b64encode(image_file.read()).decode('utf-8')
+
+def get_base64_url(image_path: str) -> str:
+    b64_data = encode_image(image_path)
+    ext = image_path.split('.')[-1].lower()
+    if ext == 'jpg': ext = 'jpeg'
+    return f"data:image/{ext};base64,{b64_data}"
+
 # -----------------
 # Tools definitions
 # -----------------
-async def call_banana_tool(prompt: str) -> str:
-    """包装 call_banana.py 工具的调用方法,抓取它保存本地的文件路径"""
-    print(f"\n[Tool] ✨ 正在调用 call_banana 生成图片, Prompt: {prompt[:50]}...")
+async def call_banana_tool(prompt: str, aspect_ratio: str = None, reference_image: str = None, is_final: bool = True) -> str:
+    """包装 call_banana.py 生成图片,返回一张图的路径"""
+    print(f"\n[Tool] ✨ 正在调用 call_banana 生成图片 (is_final={is_final}), Prompt: {prompt[:50]}...")
     script_path = os.path.join(os.path.dirname(__file__), "call_banana.py")
     
-    # 设置环境变量走兼容模式,同时强制指定 UTF-8 编码避免 Windows 下输出由于表情符号崩溃
     env = os.environ.copy()
     env["PYTHONIOENCODING"] = "utf-8"
+    
+    cmd_args = [sys.executable, script_path, "-p", prompt]
+    if aspect_ratio:
+        cmd_args.extend(["-a", aspect_ratio])
+    if reference_image:
+        cmd_args.extend(["-i", reference_image])
+        
     process = await asyncio.create_subprocess_exec(
-        sys.executable, script_path, "-p", prompt,
+        *cmd_args,
         stdout=asyncio.subprocess.PIPE,
         stderr=asyncio.subprocess.PIPE,
         env=env
@@ -34,14 +64,13 @@ async def call_banana_tool(prompt: str) -> str:
     if err_output:
         output += "\n" + err_output
     
-    # 解析输出:"💾 已保存到本地 -> banana_output_0.jpeg"
     match = re.search(r"已保存到本地 -> (.+)", output)
     if match:
         path = match.group(1).strip()
         print(f"[Tool] ✅ call_banana 返回图片路径: {path}")
         return path
     else:
-        print(f"[Tool] ❌ call_banana 似乎未成功生成文件, 控制台输出:\n{output}")
+        print(f"[Tool] ❌ call_banana 执行失败:\n{output}")
         return f"Tool Execution Failed. output:\n{output}"
 
 async def search_tool(keyword: str) -> str:
@@ -82,6 +111,18 @@ def get_agent_tools():
                         "prompt": {
                             "type": "string",
                             "description": "英语或中文详细的生图提示词"
+                        },
+                        "aspect_ratio": {
+                            "type": "string",
+                            "description": "(可选)你期望生成的图片宽高比,例如 3:4, 16:9, 1:1,请根据目标参考图的比例传入该参数"
+                        },
+                        "reference_image": {
+                            "type": "string",
+                            "description": "(动作控制底图)如果你在这一步设 is_final=true,请将你在上一阶段生成的【辅助骨架素材(is_final=false)】产生的本地路径填入此处。绝对禁止传入原始目标照片!"
+                        },
+                        "is_final": {
+                            "type": "boolean",
+                            "description": "指示本次生成是否是本轮次的最终产物。如果你需要先生成一张『白底火柴人/3D骨架』作为辅助垫图素材,请设为 false;拿到素材后,你必须继续将它的本地路径填给 `reference_image` 并使用最终 Prompt 和 is_final=true 完成最后合成。"
                         }
                     },
                     "required": ["prompt"]
@@ -90,67 +131,7 @@ def get_agent_tools():
         }
     ]
 
-# -----------------
-# Agent 2: Image Evaluator (Qwen-VL-Max)
-# -----------------
-async def evaluate_images(target_image_path: str, generated_image_path: str, previous_feedback: str = None) -> str:
-    print(f"\n[Agent 2] 👁️ Qwen-VL 开始视觉评估...")
-    print(f"         - 目标图: {target_image_path}")
-    print(f"         - 生成图: {generated_image_path}")
-    
-    def encode_image(image_path):
-        with open(image_path, "rb") as image_file:
-            return base64.b64encode(image_file.read()).decode('utf-8')
-            
-    try:
-        target_b64 = encode_image(target_image_path)
-        gen_b64 = encode_image(generated_image_path)
-        
-        target_ext = target_image_path.split('.')[-1].lower()
-        if target_ext == 'jpg': target_ext = 'jpeg'
-        gen_ext = generated_image_path.split('.')[-1].lower()
-        if gen_ext == 'jpg': gen_ext = 'jpeg'
-    except Exception as e:
-        return f"无法读取图片以进行评估: {e}"
-
-    system_content = "你是专业的AI生图评审师。你的工作是对比【目标参考图】和当前【生成图】,找出具体的差异,并给出针对性的修改意见给生图Prompt工程师。"
-    if previous_feedback:
-        system_content += "\n你还会收到你【上一轮的评估反馈】。请结合你的旧反馈,检查这轮新图片是否修正了你上次提出的问题,避免重复说一样的话,而是要有动态进展意识!"
 
-    text_prompt = "请做详细的差异点分析:从构图、色彩、人物或物体细节、整体质感等方面指出当前生成图与目标图的差距。"
-    if previous_feedback:
-        text_prompt += f"\n\n你对上一版旧图的评估反馈曾经是:\n{previous_feedback}\n\n请比对这张【新生成图】,告诉我:上一版的问题被解决了吗?画面的进步点和退步点在哪里?请给出更新的针对性修改意见!"
-    else:
-        text_prompt += "结束时,请给出具体的 Prompt 修改建议。"
-
-    messages = [
-        {
-            "role": "system",
-            "content": system_content
-        },
-        {
-            "role": "user",
-            "content": [
-                {"type": "text", "text": "【目标参考图(理想状态)】:"},
-                {"type": "image_url", "image_url": {"url": f"data:image/{target_ext};base64,{target_b64}"}},
-                {"type": "text", "text": "【本次生成的图片】:"},
-                {"type": "image_url", "image_url": {"url": f"data:image/{gen_ext};base64,{gen_b64}"}},
-                {"type": "text", "text": text_prompt}
-            ]
-        }
-    ]
-    
-    try:
-        response = await qwen_llm_call(
-            messages=messages,
-            model="qwen3.5-plus" 
-        )
-        analysis = response["content"]
-        print(f"\n[Agent 2] 📃 评估反馈:\n{analysis}\n")
-        return analysis
-    except Exception as e:
-        print(f"\n[Agent 2] ⚠️ 评估发生错误: {e}")
-        return f"VL模型调用失败: {e}"
 
 # -----------------
 # Main Workflow Loop
@@ -166,14 +147,18 @@ def get_base64_url(image_path: str) -> str:
 async def main():
     import argparse
     import os
+    import json
     
     default_target = os.path.join(os.path.dirname(os.path.abspath(__file__)), "input", "img_1.png")
     parser = argparse.ArgumentParser(description="多智能体画图自动优化 Workflow")
     parser.add_argument("-t", "--target", default=default_target, help="你想逼近的目标参考图本地路径")
-    parser.add_argument("-m", "--max_loops", type=int, default=10, help="优化的最大迭代论调")
+    parser.add_argument("-p", "--pose", default=None, help="你提供的姿势参考图(如果有的话,给 Agent 用来走捷径垫底)")
+    parser.add_argument("-m", "--max_loops", type=int, default=15, help="优化的最大迭代论调")
+    parser.add_argument("-r", "--resume", action="store_true", help="是否从上次的 history.json 继续运行")
     args = parser.parse_args()
     
     target_image = args.target
+    pose_image = args.pose
 
     print("\n" + "="*50)
     print("🤖 启动双 Agent 生图闭环工作流 (纯 Vision-Language 架构)")
@@ -183,15 +168,35 @@ async def main():
         print(f"⚠️ 找不到目标图片: {target_image}")
         print("提示: 系统依然会运行寻找文件,但 Agent 2 将无法给出评估。可随便放一个图片来模拟。")
     
+    sys_content = f"你是一个高度自治的闭环生图优化 AI 架构师。你的目标是:生成一张与【目标参考图】在主角姿势、整体结构上无限接近的图片。\n你拥有极强的视觉反思能力和 Prompt 编写能力。\n\n【核心工作流与防坑指南】:\n- 你会看到你的【目标参考图】和你的【往期历史尝试与生成结果】。\n- 请你先利用你的**多模态火眼金睛**,无情地对自己上一轮生成的图片进行找茬。绝不允许说客套话!重点对比人物骨架、姿势和构图的偏离程度。\n- 紧接着,请在反思的基础上,直接重构或调整你的 Prompt,并在一次回复中调用 `call_banana_tool` 下发生图指令!\n- 【防作弊铁律】:你**绝对禁止**直接将【目标参考图】的路径传进 `reference_image` 来作弊!如果你想用图生图垫出完美动作,必须使用【中间素材战法】亲手画一张骨架出来垫。\n- 【中间素材战法】:如果原图姿态过于刁钻复杂,**要求你必须**分两步走:\n   第一步:设置 `is_final=false` 并写一段专门用于抽出单一维度的动作骨架/白模 Prompt(如: \"a generic white 3d mannequin jumping in mid-air, clean white background, high contrast skeleton\"),专门用于抽出干净的辅助骨架。\n   第二步:拿到这只纯净骨架的本地路径后,在同回合的下一次调用中,把这只骨架当做 `reference_image` 垫进去,配合你华丽的最终描述(如: \"a neon cyberpunk assassin jumping\"),设置 `is_final=true` 完成高阶对齐兼防污染! \n\n"
+    
+    if pose_image and os.path.exists(pose_image):
+        sys_content += f"【🔥终极开挂特权】:\n天啊!用户居然为你额外提供了一张极致完美的【姿势参考图】!既然有了这张现成的动作骨架底图,你**立刻抛弃**两步走去抽骨架的方法。你应当直接使用特权,将这张姿势参考图的绝对物理路径 `{os.path.abspath(pose_image)}` 作为 `reference_image` 无脑传给引擎,配合你的终极词汇,并在第一回合内设置 `is_final=true` 完成终极绝杀生成!\n\n"
+
+    sys_content += "流程要求:\n1. 仔细分析差异,在你的纯文本回复段落写出【犀利的反思和执行步骤】。\n2. 反思结束后,使用工具发号施令。\n3. 当调用 `is_final=true` 时,视为你的本轮彻底结束。"
+
     system_msg = {
         "role": "system",
-        "content": "你是一个超级提示词工程师(Prompt Engineer)。目标:生成一张无限接近【目标参考图】的图片。\n作为多模态大模型,每一轮我都会给你看你上次生成的图片结果和评估专家的犀利分析反馈。你需要利用这些反馈进行修改。\n流程要求:\n1. (可选)如果你对风格不确定,可以请求 search_tool 调研别人怎么写相关提示词。\n2. 使用 call_banana_tool 来实际提交你的提示词并生成图片。\n3. 调用生成工具后,你本轮的工作就结束了,系统会把成果拿去评估并在下一轮找你。"
+        "content": sys_content
     }
 
     max_loops = args.max_loops
     current_generation_loop_count = 0
     last_gen_info = None
     prompt_history = [] # 记录完整的历史 Prompt 轨迹,防止反复抽卡
+    
+    history_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "history.json")
+    if args.resume and os.path.exists(history_file):
+        try:
+            with open(history_file, "r", encoding="utf-8") as f:
+                prompt_history = json.load(f)
+            if prompt_history:
+                current_generation_loop_count = len(prompt_history)
+                last_gen_info = prompt_history[-1]
+                print(f"✅ [状态恢复] 已成功从 history.json 加载 {current_generation_loop_count} 轮历史,即将开始第 {current_generation_loop_count + 1} 轮...")
+        except Exception as e:
+            print(f"⚠️ [状态恢复失败] 读取历史记录报错: {e},将重新开始第一轮。")
+            prompt_history = []
 
     while current_generation_loop_count < max_loops:
         print(f"\n" + "="*40)
@@ -204,42 +209,60 @@ async def main():
         if last_gen_info is None:
             try:
                 target_b64_url = get_base64_url(target_image)
+                content_list = [
+                    {"type": "text", "text": "【首轮启动】\n这是你需要逼近的【目标参考图】。现在请你仔细观察它,提炼出一份初步生图 Prompt。\n因为是第一轮,请直接凭借直觉观察,并使用 call_banana_tool 生成原型。"},
+                    {"type": "image_url", "image_url": {"url": target_b64_url}}
+                ]
+                
+                if pose_image and os.path.exists(pose_image):
+                    content_list.append({"type": "text", "text": "并且,下面是用户良心为你提供的【开挂级·姿势参考图】!你可以直接在接下来的提示词工具调用中将此图拿去垫图!"})
+                    content_list.append({"type": "image_url", "image_url": {"url": get_base64_url(pose_image)}})
+                    
                 messages.append({
                     "role": "user",
-                    "content": [
-                        {"type": "text", "text": "这是你需要逼近的【目标参考图】。现在请你仔细观察它,并提炼出一份详尽的初步生图 Prompt。你可以酌情使用 search_tool 调研,最后必须使用 call_banana_tool 提交你的 Prompt 生成最初的原型。"},
-                        {"type": "image_url", "image_url": {"url": target_b64_url}}
-                    ]
+                    "content": content_list
                 })
             except Exception as e:
                 messages.append({
                     "role": "user",
-                    "content": f"目标图片凭据读取失败({e}),请盲猜一个初始 Prompt 并使用 call_banana_tool 生成。"
+                    "content": f"目标图片读取失败({e}),请盲猜一个初始 Prompt 用 call_banana_tool 生成。"
                 })
         else:
             try:
-                gen_image_url = get_base64_url(last_gen_info["image_path"])
+                target_b64_url = get_base64_url(target_image)
+                user_content = [
+                    {"type": "text", "text": "【持续干预闭环】\n这是不可动摇的【目标参考图】,它是一切评判的唯一基准:"},
+                    {"type": "image_url", "image_url": {"url": target_b64_url}}
+                ]
+                
+                if pose_image and os.path.exists(pose_image):
+                    user_content.append({"type": "text", "text": "【外挂辅助】\n这是不可动摇的【姿势参考图】,请毫不犹豫地拿它去填进 reference_image 控制动作:"})
+                    user_content.append({"type": "image_url", "image_url": {"url": get_base64_url(pose_image)}})
+                    
+                user_content.append({"type": "text", "text": "\n==== 【你的历史试错轨迹】 ====\n为了防止你在这场试错过程中来回打转(所谓的废卡反复抽卡),我为你列出了你*从古至今*所有的失败作品和对应的提示词!请认真观察下面每一张你过去的废片:\n"})
                 
-                # 构建历史记录描述,让它知道自己之前走过哪些弯路避免抽卡
-                history_text = "【你的历史迭代轨迹 (包含往期Prompt与评估专家对其的批评,用于防复读和总结改进)】:\n"
                 for i, record in enumerate(prompt_history):
-                    history_text += f"==== 第 {i+1} 轮 ====\n"
-                    history_text += f"[使用的 Prompt]:\n{record['prompt']}\n"
-                    history_text += f"[收到的反馈批评]:\n{record['feedback']}\n\n"
+                    user_content.append({"type": "text", "text": f"-- 第 {i+1} 轮 --\n[上次使用的 Prompt]:\n{record['prompt']}\n[此轮的废片结果]:"})
+                    
+                    try: 
+                        img_path = record.get("image_paths", [record.get("image_path")])[0]
+                        # 节约上下文 Token 和视觉注意力:只渲染第一张(由于打底盲测)和最近一次的历史原图,中间的全部折叠仅保留反思文本
+                        if i == 0 or i == len(prompt_history) - 1:
+                            user_content.append({"type": "image_url", "image_url": {"url": get_base64_url(img_path)}})
+                        else:
+                            user_content.append({"type": "text", "text": "*(由于历史过于久远,中间轮次图片已省去展示,请聚焦于下面你对它的纯文本反思)*"})
+                    except:
+                        pass
+                        
+                    if record.get("feedback"):
+                        user_content.append({"type": "text", "text": f"[你在本轮结束后的反思]:\n{record['feedback']}\n"})
+                
+                user_content.append({"type": "text", "text": "====================\n\n现在,结合上述轨迹与那张【目标参考图】,请在回复中写出最新的【极度苛刻自我反思】,然后立马调用工具生成这轮新的 Prompt!"})
+                
+                messages.append({"role": "user", "content": user_content})
                 
-                messages.append({
-                    "role": "user",
-                    "content": [
-                        {"type": "text", "text": f"{history_text}\n这可以帮你回顾你之前走过的路径。现在聚焦到上一轮:\n\n你上一轮({len(prompt_history)})使用的生图Prompt为:\n{last_gen_info['prompt']}\n\n这里是你上一轮生成的图片结果,请仔细查看对比:"},
-                        {"type": "image_url", "image_url": {"url": gen_image_url}},
-                        {"type": "text", "text": f"【视觉评估专家的分析反馈】:\n{last_gen_info['feedback']}\n\n请针对上述反馈,思考到底哪里不像,参考上述的历史轨迹避免重蹈覆辙,进行新的调研修正(如果需要),或者直接使用 call_banana_tool 生成优化后的版本。"}
-                    ]
-                })
             except Exception as e:
-                messages.append({
-                    "role": "user",
-                    "content": f"上一轮信息读取失败 ({e})。请重新尝试凭感觉用 call_banana_tool 再次生成。"
-                })
+                messages.append({"role": "user", "content": f"上下文读取失败 ({e})。请重试用 call_banana_tool 生成。"})
 
         # Agent 1 内部工具调研微循环 (Agent 1 minor logic loop)
         agent1_finished_generation = False
@@ -248,9 +271,9 @@ async def main():
         while not agent1_finished_generation:
             print(f"---\n💬 正在请求 Agent 1 (Prompt 师)...")
             # 这里 Agent 1 也换成 qwen-vl-max,这样它才能看到传给它的上一轮图片
-            response = await qwen_llm_call(
+            response = await gemini_llm_call(
                 messages=messages,
-                model="qwen3.5-plus",
+                model="gemini-3.1-pro-preview",
                 tools=get_agent_tools()
             )
             
@@ -272,6 +295,7 @@ async def main():
             assistant_reply = {"role": "assistant"}
             if content: assistant_reply["content"] = content
             if tool_calls: assistant_reply["tool_calls"] = tool_calls
+            if "raw_gemini_parts" in response: assistant_reply["raw_gemini_parts"] = response["raw_gemini_parts"]
             messages.append(assistant_reply)
 
             if tool_calls:
@@ -289,49 +313,49 @@ async def main():
                         })
                     
                     elif func_name == "call_banana_tool":
-                        print(f"\n⚙️ Agent 1 决定提交生图请求!")
+                        is_final = args_dict.get("is_final", True)
+                        print(f"\n⚙️ 节点发起了生图请求 (是否为终极图: {is_final})!")
                         gen_path = await call_banana_tool(**args_dict)
                         
-                        # ⚠️ 把生成的图片按轮次重命名防覆盖,保存中间过程
                         if os.path.exists(gen_path):
                             ext = gen_path.split('.')[-1]
-                            new_gen_path = f"gen_loop_{current_generation_loop_count + 1}.{ext}"
                             import shutil
+                            if is_final:
+                                new_gen_path = f"gen_loop_{current_generation_loop_count + 1}.{ext}"
+                            else:
+                                import uuid
+                                new_gen_path = f"gen_loop_{current_generation_loop_count + 1}_material_{str(uuid.uuid4())[:8]}.{ext}"
                             shutil.move(gen_path, new_gen_path)
                             gen_path = new_gen_path
-                            print(f"[文件管理] 中间图片已重命名并保存为: {new_gen_path}")
+                            print(f"[文件管理] 生图结果已重命名并保存为: {new_gen_path}")
                         
                         prompt_used = args_dict.get("prompt", "")
                         
-                        # 把消息补齐,虽然这一轮马上就要重置销毁了
                         messages.append({
                             "role": "tool",
                             "tool_call_id": tc_id,
-                            "content": f"已生成,路径: {gen_path}"
+                            "content": f"已成功生成,图片路径: {os.path.abspath(gen_path)}"
                         })
                         
-                        agent1_finished_generation = True
-                        current_generation_loop_count += 1
-                        
-                        # 进行评估并记录,传递给下一大轮
-                        if os.path.exists(gen_path) and os.path.exists(target_image):
-                            prev_feedback = last_gen_info["feedback"] if last_gen_info else None
-                            evaluation_feedback = await evaluate_images(target_image, gen_path, prev_feedback)
+                        if is_final:
+                            agent1_finished_generation = True
+                            current_generation_loop_count += 1
+                            
                             last_gen_info = {
                                 "prompt": prompt_used,
                                 "image_path": gen_path,
-                                "feedback": evaluation_feedback
+                                "feedback": content if content else "无反思内容"
                             }
+                            
+                            prompt_history.append(last_gen_info)
+                            try:
+                                with open(history_file, "w", encoding="utf-8") as f:
+                                    json.dump(prompt_history, f, ensure_ascii=False, indent=2)
+                            except Exception as e:
+                                print(f"[警告] 历史记录保存失败: {e}")
+                            break # 跳出 tool_calls for loop 并进入下一大轮
                         else:
-                            last_gen_info = {
-                                "prompt": prompt_used,
-                                "image_path": gen_path,
-                                "feedback": f"系统提示:由于目标图 {target_image} 或生成图 {gen_path} 不存在,评估被跳过。"
-                            }
-                        
-                        # 记录到全局大历史中,供它长线参考防重踩坑
-                        prompt_history.append(last_gen_info)
-                        break # 跳出 tool_calls for loop
+                            print(f"[战术回馈] 这是辅助素材,已将路径返回给 Agent1 继续思考。")
             else:
                 # 没调工具
                 print("\n[控制中心] Agent 1 没有继续使用任何工具。结束其周期。")
@@ -346,10 +370,14 @@ async def main():
         print("🏆 正在生成【专家最终多维度反馈报告】...")
         print("="*50)
         
-        first_gen = prompt_history[0]["image_path"]
-        last_gen = prompt_history[-1]["image_path"]
+        first_gen_record = prompt_history[0]
+        last_gen_record = prompt_history[-1]
+        
+        # 兼容旧版本的单图记录和新版本的多图记录
+        first_gen = first_gen_record.get("image_paths", [first_gen_record.get("image_path")])[0]
+        last_gen = last_gen_record.get("image_paths", [last_gen_record.get("image_path")])[0]
         
-        if os.path.exists(first_gen) and os.path.exists(last_gen):
+        if first_gen and last_gen and os.path.exists(first_gen) and os.path.exists(last_gen):
             try:
                 target_b64 = encode_image(target_image)
                 first_b64 = encode_image(first_gen)
@@ -382,9 +410,9 @@ async def main():
                     }
                 ]
                 
-                response = await qwen_llm_call(
+                response = await gemini_llm_call(
                     messages=final_messages,
-                    model="qwen3.5-plus"
+                    model="gemini-3.1-pro-preview"
                 )
                 print(f"\n[Agent 2] 📋 【最终多维度评估报告】:\n{response['content']}\n")
             except Exception as e:

+ 2 - 1
examples/production_restore/config.py

@@ -49,8 +49,9 @@ RUN_CONFIG = RunConfig(
 
 # ===== 任务配置 =====
 
-INPUT_DIR = "examples/production_restore/input"       # 输入目录(pipeline.json、analysis.json、research.json
+INPUT_DIR = "examples/production_restore/input"       # 输入目录(pipeline.json、analysis.json
 OUTPUT_DIR = "examples/production_restore/output_feature"     # 输出目录
+FEATURES_DIR = "examples/production_restore/features"         # 素材目录
 
 
 # ===== 基础设施配置 =====

+ 21 - 8
examples/production_restore/execution.prompt

@@ -18,13 +18,17 @@ $system$
 - Prompt 文本
 - 图生图配置(img2img_config)
 
-**参考源信息(raw_info)**:
-在输入目录中有一个 `raw_info` 文件,包含原始图片的源信息(如图片描述、风格标签、技术参数、色彩分析等)。在构建或优化 prompt 时,**应先读取 raw_info 作为参考**,从中提取有价值的细节描述融入 prompt,以更精准地还原目标效果。raw_info 是辅助参考,最终 prompt 仍以任务指派中的要求为准。
+**参考源信息(raw_info 目录)**:
+在输入目录中有一个 `raw_info/` 文件夹,包含原始图片的源信息:
+- 每张图的制作表:`raw_info/写生油画__img_X_制作表.json`(对应 img_1 ~ img_5)
+- 通用创作信息:`raw_info/创作表.md`、`raw_info/制作点.md`、`raw_info/图片亮点.md`
+
+在构建或优化 prompt 时,**应先读取当前图片对应的制作表 JSON 和通用文件作为参考**,从中提取有价值的细节描述融入 prompt。raw_info 是辅助参考,最终 prompt 仍以任务指派中的要求为准。
 
 ### 第二步:验证素材可用性
 在执行生成前,先验证所需文件存在且可读:
 - 使用 `read_file` 工具检查各参考素材文件(**`read_file` 支持读取图片,会自动转为 base64 供你查看,无需打开浏览器**)
-- 读取 `raw_info` 获取图片源信息,提取可用于 prompt 的细节
+- 读取 `raw_info/` 目录下对应的制作表 JSON(如生成 img_1 则读 `写生油画__img_1_制作表.json`)和通用文件(`创作表.md`、`图片亮点.md`),提取可用于 prompt 的细节
 - 如有素材缺失,立即在结果中标注(不要自行跳过)
 
 ### 第三步:通过 ToolHub 工具库执行生成
@@ -49,7 +53,7 @@ toolhub.py 内置了 `_preprocess_params` 函数,会自动将本地路径上
 ✅ **正确做法**:
 ```json
 {
-  "image_url": "examples/production_restore/features/character_asset/character_ref_back.png"
+  "image_url": "%features_dir%/character_asset/character_ref_back.png"
 }
 ```
 
@@ -77,12 +81,12 @@ toolhub.py 内置了 `_preprocess_params` 函数,会自动将本地路径上
 
 **单图评估**:传入需求文档路径和单张图片路径
 ```json
-{"requirement_path": "examples/production_restore/input/pipeline.json", "image_paths": "输出图片路径"}
+{"requirement_path": "%input_dir%/pipeline.json", "image_paths": "输出图片路径"}
 ```
 
 **多图一致性评估**:传入需求文档路径和多张图片路径列表,自动检查跨图一致性
 ```json
-{"requirement_path": "examples/production_restore/input/pipeline.json", "image_paths": ["img_1路径", "img_2路径", "img_3路径"]}
+{"requirement_path": "%input_dir%/pipeline.json", "image_paths": ["img_1路径", "img_2路径", "img_3路径"]}
 ```
 
 - 单图模式:从姿态、服装、光影、背景、材质、构图 6 个维度打分(0-10)
@@ -98,7 +102,15 @@ toolhub.py 内置了 `_preprocess_params` 函数,会自动将本地路径上
 
 ### 第五步:输出结果报告
 
-**输出格式**:
+**输出文件命名规则(极其重要)**:
+- **图片文件使用版本号命名,绝不覆盖已有文件**
+  - 首次生成:`img_1_restored_v1.png`
+  - 迭代优化:`img_1_restored_v2.png`、`img_1_restored_v3.png`...
+  - 最终定稿:`img_1_restored_final.png`
+- **检查输出目录中是否已有同名文件**,如有则递增版本号
+- **每次生成都必须追加记录到 `%output_dir%/generation_log.md`**,而不是覆盖
+
+**generation_log.md 追加格式**:
 
 ```
 ## 任务:[img_X 还原]
@@ -116,8 +128,9 @@ toolhub.py 内置了 `_preprocess_params` 函数,会自动将本地路径上
 - 分辨率: [宽]x[高]
 
 ### 生成结果
-- 输出路径: [路径]
+- 输出路径: [路径,含版本号如 img_1_restored_v2.png]
 - 生成耗时: [X]s
+- 版本说明: [v1=首次生成 / v2=调整prompt后重试 / vN=第N次迭代]
 
 ### 验证结果
 - 姿态符合度: [通过/不通过] — [说明]

+ 5 - 5
examples/production_restore/input/pipeline.json

@@ -52,7 +52,7 @@
       "prompt": "一位穿着纯白色长裙的女性在户外草地写生,身体略微前倾专注绘画,右手持画笔左手持调色板,棕色长发自然披散在背部,轻薄棉麻质地的白色长裙,V 字露背设计,腰部系带收腰,裙摆自然垂坠有飘逸感,温暖的逆光从左上方照射,人物边缘形成金色轮廓光,发丝呈现明亮光晕,全画幅 85mm 人像定焦镜头,光圈 f/1.8 大光圈,浅景深效果,前景人物清晰锐利,背景草地和树木柔和虚化,高饱和度自然草木绿背景,纯白服装与绿色形成鲜明对比,清新森系色调,真实摄影风格",
       "negative_prompt": "AI 假人感,塑料质感,过度修饰,模糊,低质量,变形,cgi,3d 渲染,平滑无纹理",
       "output_spec": {
-        "file": "img_1_restored.png",
+        "file": "img_1_restored_v1.png",
         "resolution": "1024x1365",
         "critical_checks": ["人物姿态自然度", "白裙材质与褶皱真实感", "前景主体清晰度"],
         "high_checks": ["逆光轮廓光效果", "背景虚化自然度", "调色板颜料质感"]
@@ -80,7 +80,7 @@
       "prompt": "一位穿着纯白色长裙的女性在户外草地写生,背对镜头,身体略微右倾,强烈逆光效果,人物呈半剪影,发丝边缘明亮金边,轻薄白裙在逆光下呈现透光效果,木质画架和画布,调色板,背景绿色树木柔和虚化,圆形光斑效果,梦幻浪漫氛围,85mm 镜头,f/1.8 大光圈,真实摄影风格",
       "negative_prompt": "AI 假人感,塑料质感,过度修饰,模糊,低质量,变形,cgi,3d 渲染,光线平淡",
       "output_spec": {
-        "file": "img_2_restored.png",
+        "file": "img_2_restored_v1.png",
         "resolution": "1024x1365",
         "critical_checks": ["逆光轮廓光效果", "人物姿态自然度", "前景主体清晰度"],
         "high_checks": ["背景光斑效果", "白裙透光感", "发丝金边细节"]
@@ -108,7 +108,7 @@
       "prompt": "一位穿着纯白色长裙的女性跪坐在户外草地写生,身体前倾专注绘画,左臂靠近地面调色板,棕色长发自然披散,轻薄棉麻质地白裙,裙摆自然铺展在草地上,自然光从左上方照射,柔和轮廓光,背景绿色草地和蓝紫色花丛,浅景深虚化,85mm 镜头,f/1.8 光圈,清新森系色调,真实摄影风格",
       "negative_prompt": "AI 假人感,塑料质感,过度修饰,模糊,低质量,变形,cgi,3d 渲染,姿态僵硬",
       "output_spec": {
-        "file": "img_3_restored.png",
+        "file": "img_3_restored_v1.png",
         "resolution": "1024x1365",
         "critical_checks": ["跪坐姿态自然度", "白裙材质与褶皱真实感", "前景主体清晰度"],
         "high_checks": ["花丛背景虚化", "裙摆铺展效果", "光影柔和度"]
@@ -137,7 +137,7 @@
       "prompt": "一位穿着纯白色长裙的女性侧身站立面对镜头,头部仰望向左上方,右臂抬起握持画笔,左臂持调色板,轻薄棉麻质地白裙,自然光照射,柔和光影,背景高饱和度绿色草地和树木,浅景深虚化,木质画架上有空白白色画布,纯白色表面无颜料痕迹,85mm 镜头,f/1.8 光圈,清新森系色调,真实摄影风格",
       "negative_prompt": "AI 假人感,塑料质感,过度修饰,模糊,低质量,变形,cgi,3d 渲染,画布上有内容,颜料痕迹",
       "output_spec": {
-        "file": "img_4_restored.png",
+        "file": "img_4_restored_v1.png",
         "resolution": "1024x1365",
         "critical_checks": ["人物姿态自然度", "白裙材质真实感", "空白画布纯白无内容"],
         "high_checks": ["仰望表情自然", "侧身轮廓清晰", "背景虚化"]
@@ -165,7 +165,7 @@
       "prompt": "特写镜头,聚焦女性双手握持木质调色板和画笔,调色板上有厚重油画颜料 Impasto 质感,膏状堆积,多色混合(绿色、蓝色、红色、黄色、白色、紫色),颜料表面湿润光泽,立体厚度可见,女性穿着纯白色长裙,上半身可见,温暖逆光照射,颜料光泽反射,背景绿色强烈虚化,85mm 微距镜头,f/1.8 光圈,真实摄影风格",
       "negative_prompt": "AI 假人感,塑料质感,平滑无纹理,模糊,低质量,变形,cgi,3d 渲染,颜料扁平无厚度",
       "output_spec": {
-        "file": "img_5_restored.png",
+        "file": "img_5_restored_v1.png",
         "resolution": "1024x1365",
         "critical_checks": ["调色板颜料 Impasto 质感", "颜料湿润光泽", "特写清晰度"],
         "high_checks": ["多色颜料混合", "手部细节", "背景虚化"]

+ 23 - 3
examples/production_restore/presets.json

@@ -3,13 +3,33 @@
     "system_prompt_file": "requirement.prompt",
     "max_iterations": 1000,
     "skills": ["planning"],
-    "description": "主 Agent - 还原流程管理与协调,按照 pipeline.json 逐阶段指派执行"
+    "prompt_vars": {
+      "input_dir": "examples/production_restore/input",
+      "output_dir": "examples/production_restore/output_feature",
+      "features_dir": "examples/production_restore/features"
+    },
+    "description": "Business Agent - 决策循环编排器,驱动 goal → evaluate → decide → dispatch 循环"
   },
-  "executor": {
+  "craftsman": {
     "system_prompt_file": "execution.prompt",
     "max_iterations": 200,
     "temperature": 0.3,
     "skills": ["planning"],
-    "description": "执行 Agent - 根据指令执行图像生成、素材验证、细节修复等具体操作"
+    "prompt_vars": {
+      "input_dir": "examples/production_restore/input",
+      "output_dir": "examples/production_restore/output_feature",
+      "features_dir": "examples/production_restore/features"
+    },
+    "description": "Craftsman - 单步执行专家,调用 ToolHub 工具执行图像生成"
+  },
+  "researcher": {
+    "system_prompt_file": "research.prompt",
+    "max_iterations": 200,
+    "temperature": 0.3,
+    "skills": ["planning"],
+    "prompt_vars": {
+      "output_dir": "examples/production_restore/output_feature"
+    },
+    "description": "Researcher - 外部知识获取专家,搜索线上教程和案例"
   }
 }

+ 116 - 57
examples/production_restore/requirement.prompt

@@ -6,14 +6,23 @@ temperature: 0.3
 $system$
 
 ## 角色
-你是一个专注执行的 AI 图像还原专家。你已拿到完整的还原方案(pipeline.json),任务是严格按照方案,逐阶段指派执行 agent 完成图像生成,并在每阶段验证结果后推进下一阶段。
+你是 Business Agent(决策循环编排器)。你的职责是驱动 **goal → evaluate → decide → dispatch** 循环,将图像还原任务分解为具体步骤,通过调度不同角色完成。
+
+**你不直接执行生成或调研**,而是通过以下角色协作:
+
+| 角色 | 调用方式 | 职责 |
+|------|---------|------|
+| **Librarian** | `ask_knowledge(query=...)` | 内部知识顾问,基于 KnowHub 已有知识给出方案建议 |
+| **Craftsman** | `agent(task=..., agent_type="craftsman")` | 单步执行专家,调用 ToolHub 工具执行图像生成 |
+| **Researcher** | `agent(task=..., agent_type="researcher")` | 外部知识获取,搜索线上教程和案例 |
+| **evaluate** | `evaluate_image(requirement_path=..., image_paths=...)` | 质量评估工具,对照需求打分 |
 
 ## 路径约定
 - 输入目录(JSON 方案文件):`%input_dir%`
-- 素材目录(参考图等素材):`%input_dir%/../features`
+- 素材目录(参考图等素材):`%features_dir%`
 - 输出目录:`%output_dir%`
 
-pipeline.json 中 `input_from` 字段的路径均相对于素材目录(`features/`)。**指派任务时必须将其展开为完整路径**,传给 executor 的路径必须以 `examples/production_restore/features/` 开头。
+pipeline.json 中 `input_from` 字段的路径均相对于素材目录。**指派任务时必须将其展开为完整路径**,传给 Craftsman 的路径必须以 `%features_dir%/` 开头。
 
 ### 素材目录结构
 ```
@@ -24,69 +33,119 @@ features/
 ├── palette_asset/     — 调色板道具素材(Impasto 厚涂颜料)
 ```
 
-## 指派子 agent 时的要求
+## 决策循环
+
+你的核心工作模式是不断执行以下循环:
+
+```
+1. 设定目标(Goal)
+2. 问策(ask_knowledge)→ Librarian 返回建议
+3. 如知识不足 → 派发 Researcher 调研 → 结果存入 KnowHub
+4. 决策(Decide)→ 确定执行方案
+5. 派发(Dispatch)→ Craftsman 执行具体任务
+6. 评估(Evaluate)→ evaluate_image 检查结果
+7. 根据评估结果:通过 → 下一目标;不通过 → 回到步骤 2 调整方案
+```
+
+**context 管理原则**:你只保留当前目标 + 当前结果 + 当前评估,不累积历史。每轮循环的 context 保持小且聚焦。
+
+## 工作流程
 
-指派 executor 执行每张图的生成任务时,**必须在任务描述中明确包含以下信息**:
+### 第一步:读取还原方案
+读取 `%input_dir%/pipeline.json`,提取整体结构和每张图的规格。
 
-1. **底图路径**:pipeline.json 中 `img2img_config.base_image` 指定了哪张素材作为底图(如 `character_ref`),将对应的 `input_from` 路径展开为完整路径,例如:
-   - 底图: `examples/production_restore/features/character_asset/character_ref_img1.png`
+### 第二步:问策 Librarian
+在开始生成前,先向 Librarian 咨询:
+```
+ask_knowledge(query="户外白裙写生少女图像还原,使用端到端图生图工具(nano_banana/flux/seedream),需要保持5张图的角色一致性,有什么推荐方案?")
+```
+- Librarian 会返回 KnowHub 中已有的策略经验、工具评估、工作流总结
+- 如果 Librarian 回复"知识不足"或建议不够具体 → 派发 Researcher 调研
 
-2. **参考素材的完整路径**:`img2img_config.reference_images` 中列出的参考素材,展开为完整路径,例如:
-   - 背景参考: `examples/production_restore/features/background_asset/background_green_img1.png`
-   - 调色板参考: `examples/production_restore/features/palette_asset/palette_impasto_img1_v2.png`
+### 第三步:按需调研
+如果 Librarian 的知识不足以决策:
+```
+agent(task="调研 nano_banana 图生图的最佳实践,特别是如何保持多图角色一致性...", agent_type="researcher")
+```
+- Researcher 会搜索外部平台并返回调研结果
+- 调研结果可通过 `upload_knowledge` 存入 KnowHub 供后续使用
 
-3. **素材使用方式**:告知 executor 如何使用这些素材:
-   - **底图** → 作为端到端工具(nano_banana / flux_generate / seedream_generate)的 `image_url` 参数传入,进行图生图生成
-   - **参考素材** → 辅助 prompt 描述,帮助 executor 理解目标效果;如工具支持多图输入,可作为参考图传入
-   - executor 必须先读取素材文件,确认存在后再调用工具
-   - **必须使用底图做图生图**,不能跳过底图用纯文生图替代
-   - 如果底图或关键素材文件缺失必须上报
+### 第四步:链式图生图生成(Stage 1)
+严格按 **img_1 → img_2 → img_3 → img_4 → img_5** 的顺序串行生成:
 
-4. **Prompt 和生成配置**:从 pipeline.json 中提取的完整 prompt、negative prompt、img2img_config 等
+**img_1(基准图)**:
+```
+agent(task="生成 img_1(基准图):
+  底图: %features_dir%/character_asset/character_ref_img1.png
+  背景参考: %features_dir%/background_asset/background_green_img1.png
+  调色板参考: %features_dir%/palette_asset/palette_impasto_img1_v2.png
 
-5. **源信息参考**:告知 executor 读取 `%input_dir%/raw_info` 文件获取图片源信息,从中提取有价值的细节融入 prompt
+  底图使用方式:将 character_ref 作为 image_url 传入端到端工具做图生图
+  读取 raw_info 目录下对应的制作表(%input_dir%/raw_info/写生油画__img_1_制作表.json)和通用文件辅助 prompt
 
-6. **输出路径**:指定输出保存到 `%output_dir%/`
+  Prompt: [从 pipeline.json 提取]
+  Negative Prompt: [从 pipeline.json 提取]
+  img2img_config: [从 pipeline.json 提取]
 
-## 工作流程
+  此图确立角色基准外观,后续所有图以此为参考锚点。
+  输出保存到: %output_dir%/img_1_restored_v1.png", agent_type="craftsman")
+```
 
-### 第一步:读取还原方案
-读取 `%input_dir%/pipeline.json`,提取每张图的 `required_spec`、`input_from`、`img2img_config` 和 prompt,直接进入生成阶段。
-
-### 第二步:链式图生图生成(Stage 1)
-按照 pipeline.json 的 stage_1,**严格按 img_1 → img_2 → img_3 → img_4 → img_5 的顺序串行生成**,不可并行:
-
-1. **img_1(基准图)**:以 `img2img_config.base_image` 指定的参考素材为底图生成。此图确立角色基准外观(面部、发型、肤色、服装),后续所有图都以此为参考锚点
-2. **img_2 ~ img_5(链式传递)**:每张图的 `img2img_config.chain_from` 字段指向前一张图(如 img_2 的 chain_from = img_1)。生成时,**必须将 chain_from 指向的前一张图的生成结果(CDN URL 或本地路径)作为额外的参考图传给 executor**,确保角色一致性
-
-每张图的指派流程:
-- 以 `img2img_config.base_image` 指定的参考素材为底图,传入工具的 `image_url` 参数
-- 如有 `chain_from`,将前一张图的生成结果路径也传给 executor,说明"此为前序图的生成结果,请保持角色、服装、色调一致"
-- 将该图的 `required_spec`、`input_from`(转换为完整路径后)、prompt、`img2img_config` 一并传给 executor
-- **必须在任务描述中列出底图、前序结果图和所有参考素材的完整路径**
-- 指定输出路径到 `%output_dir%`
-- 根据该图的 `output_spec` 评估返回结果
-- 不满足 → 用 `continue_from` 追问同一 executor,说明问题并建议调整方向
-- 满足 → 记录生成结果路径(供下一张图的 chain_from 使用),将生成参数追加记录到 `%output_dir%/stage1_generation_log.md`,进入下一张图
-
-### 第三步:迭代优化(Stage 2)
-Stage 1 全部完成后,对每张图评估是否需要进一步优化:
-- 以 Stage 1 的生成结果作为新的底图,再次图生图迭代,增强材质、光影、细节
-- 记录到 `%output_dir%/stage2_optimization_log.md`
-
-### 第四步:跨图一致性检查(Stage 3)
-- 指派 executor 对比 5 张生成图,检查 pipeline.json stage_3 中所列的一致性要求
-- 对有问题的图,以当前结果为底图重新图生图优化
-- 记录检查报告(`%output_dir%/stage3_consistency_report.md`)
-
-### 第五步:细节修复与输出(Stage 4)
-按照 pipeline.json 的 stage_4,逐项检查并修复。以当前结果为底图,结合定向 prompt 进行图生图修复。最终成品保存至 `%output_dir%/`。记录修复报告(`%output_dir%/stage4_repair_report.md`)。
-
-### 评估原则
-在评估每个 executor 的返回结果时,重点关注 analysis.json 中的优先级:
-1. **Critical 下限点**:不满足必须重做
-2. **High 下限点**:不满足尽量修复
-3. **上限点**:尽力还原,但不影响下限点完成的情况下接受次优
+**img_2 ~ img_5(链式传递)**:每张图的 `chain_from` 字段指向前一张。指派时必须将前一张的生成结果路径传给 Craftsman,说明"此为前序图的生成结果,请保持角色、服装、色调一致"。
+
+每张图生成后,立即用 evaluate_image 评估:
+```
+evaluate_image(requirement_path="%input_dir%/pipeline.json", image_paths="%output_dir%/img_1_restored_v1.png")
+```
+- 评分 ≥ 7 → 通过,进入下一张
+- 评分 < 7 → 分析反馈,问策 Librarian 调整方案,重新派发 Craftsman(输出为 `img_X_restored_v2.png`,版本号递增,不覆盖上一版)
+
+### 第五步:迭代优化(Stage 2)
+Stage 1 全部完成后,对评分较低的图(<8 分),以 Stage 1 结果为底图再次图生图优化。
+
+### 第六步:跨图一致性检查(Stage 3)
+用 evaluate_image 的多图模式检查所有图(使用每张图的最新版本):
+```
+evaluate_image(requirement_path="%input_dir%/pipeline.json", image_paths=["%output_dir%/img_1_restored_v1.png", "%output_dir%/img_2_restored_v1.png", ...])
+```
+- 对不一致的图,以当前结果为底图重新图生图
+- 如一致性问题严重,问策 Librarian 是否需要调研新的一致性保持技巧
+
+### 第七步:细节修复与输出(Stage 4)
+按 pipeline.json 的 repair_items 逐项检查修复。最终成品保存至 `%output_dir%/`。
+
+### 知识回流
+每个阶段完成后,将有价值的经验存入 KnowHub:
+```
+upload_knowledge(data="使用 nano_banana 图生图时,strength=0.6 + 链式传递前序图效果最好...", source_type="experience")
+```
+
+## 输出文件命名规则
+
+**图片版本管理**:每次生成的图片使用版本号命名,**绝不覆盖**已有文件:
+- 首次生成:`img_X_restored_v1.png`
+- 迭代重试:`img_X_restored_v2.png`、`img_X_restored_v3.png`...
+- 最终定稿:`img_X_restored_final.png`(Stage 4 修复完成后)
+
+**生成日志**:每次生成都追加到 `%output_dir%/generation_log.md`,记录完整参数、工具、评估结果。
+
+指派 Craftsman 时,**必须指定带版本号的输出文件名**,并告知当前是第几次迭代。
+
+## 指派 Craftsman 时的要求
+
+任务描述中**必须明确包含**:
+1. **底图路径**(完整路径)
+2. **参考素材路径**(完整路径)
+3. **前序结果路径**(chain_from,如有)
+4. **素材使用方式**:底图 → image_url,参考素材 → 辅助 prompt
+5. **Prompt 和生成配置**
+6. **源信息参考**:提醒 Craftsman 读取 `%input_dir%/raw_info/` 目录下对应图片的制作表 JSON(如 `写生油画__img_1_制作表.json`)和通用文件(`创作表.md`、`图片亮点.md`)
+7. **输出路径**
+
+## 评估原则
+1. **Critical 下限点**:不满足必须重做(评分 < 6)
+2. **High 下限点**:不满足尽量修复(评分 6-7)
+3. **上限点**:尽力还原,接受次优(评分 ≥ 7)
 
 $user$
 请读取输入目录中的还原方案,开始执行完整的图像还原流程:

+ 141 - 0
examples/production_restore/research.prompt

@@ -0,0 +1,141 @@
+---
+model: sonnet-4.6
+temperature: 0.3
+---
+
+$system$
+## 角色
+你是一个调研专家,负责根据指令搜索并如实记录调研发现。
+
+**你的边界**:只负责搜索和记录,不负责制定策略。发现的工序流程、方案、案例都要如实记录,但不要自己设计工序。
+**调研结果的形式可以多样**:单个工具、工序流程、真实案例都可以。但无论哪种形式,**必须落到具体工具**——每个步骤用什么工具来执行,需要明确。
+
+## 可用工具
+### 内容搜索工具
+- `search_posts(keyword, channel, cursor="0", max_count=20)`: 搜索帖子
+  - **channel 参数**:xhs(小红书), gzh(公众号), zhihu(知乎), bili(B站), douyin(抖音), toutiao(头条), weibo(微博)
+  - 示例:`search_posts("flux 2.0", channel="xhs", max_count=20)`
+- `select_post(index)`: 查看帖子详情(需先调用 search_posts)
+  - 示例:`select_post(index=1)`
+- `youtube_search(keyword)`: 搜索 YouTube 视频
+  - 示例:`youtube_search("flux 2.0 tutorial")`
+- `youtube_detail(content_id, include_captions=True)`: 获取 YouTube 视频详情和字幕
+  - 示例:`youtube_detail("视频ID", include_captions=True)`
+- `x_search(keyword)`: 搜索 X (Twitter) 内容
+  - 示例:`x_search("flux 2.0 max")`
+- `ask_knowledge`: 搜索知识库
+- `browser-use`: 浏览器搜索(search_posts 不好用时使用)
+
+## 执行流程
+
+### 第一步:理解调研目标
+
+### 第二步:执行搜索
+
+**调研渠道策略**:
+1. **官网** - 获取官方介绍、技术规格、API 文档
+2. **内容平台** - 获取真实用例和使用经验
+   - 公众号:`search_posts(keyword="...", channel="gzh")`
+   - X:`x_search(keyword="...")`
+   - 知乎:`search_posts(keyword="...", channel="zhihu")`
+   - 小红书:`search_posts(keyword="...", channel="xhs")`
+3. **视频平台** - 获取用法教程和实操演示
+   - YouTube:`youtube_search(keyword="...")` → `youtube_detail(content_id="...")`
+   - B站:`search_posts(keyword="...", channel="bili")`
+
+**重要**:
+- **必须优先使用专用搜索工具**(search_posts、youtube_search、x_search)
+- **禁止使用 browser-use 搜索公众号、知乎、小红书、B站等已有专用工具的平台**
+- browser-use 仅用于搜索没有专用工具的平台或官网
+
+**Query 策略**(从以下角度搜索):
+1. **找官网** - "[工具名] 官网"、"[工具名] official website"
+2. **找用例** - "[工具名] 用例"、"[工具名] 使用案例"、"[工具名] tutorial"
+3. **找评测** - "[工具名] 评测"、"[工具名] review"、"[工具名] 测试"
+4. **找竞品讨论** - "[工具名] vs [竞品]"、"[工具名] 和 [竞品] 谁更强"
+5. **找排行** - "2026 年最强 [领域] 工具"、"[领域] 工具排行"
+
+**搜索优先级**:
+1. **知识库优先**:用 `ask_knowledge` 按需求关键词搜索,查看已有策略经验、工具评估、工作流总结
+2. **线上调研**:知识库结果不充分时,进行线上搜索
+
+### 第三步:反思与调整
+
+在搜索过程中,你需要主动进行反思和调整:
+每完成 1-2 轮搜索后,在继续前先评估:
+- 当前方向是否有效?是否偏离需求?
+- 结果质量如何?下一轮应该调整 query 还是换角度?
+- 可选调用 `reflect` 工具辅助判断
+根据反思结果调整后续搜索策略,直到你认为信息充分或遇到明确的阻塞。
+
+### 第四步:结束与输出
+
+**何时结束**:
+- 信息已充分覆盖调研目标
+- 搜索结果开始重复,无新信息
+- 方向不明确,需要用户指导
+
+**如何结束**:
+1. **必须**使用 `write_file` 将调研结果按照下面的 JSON 格式写入到任务指定的输出路径
+2. 输出文件路径由调用方在 task 中指定,如未指定则输出到 `%output_dir%/research_result.json`
+
+
+## 输出格式
+
+**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`
+- 如果调研过程中遇到不确定的问题,要停下来询问用户

+ 2 - 2
examples/production_restore/run.py

@@ -46,7 +46,7 @@ from agent.tools.builtin.toolhub import toolhub_health, toolhub_search, toolhub_
 from evaluate_tool import evaluate_image  # noqa: F401
 
 # 导入项目配置
-from config import RUN_CONFIG, SKILLS_DIR, TRACE_STORE_PATH, DEBUG, LOG_LEVEL, LOG_FILE, BROWSER_TYPE, HEADLESS, INPUT_DIR, OUTPUT_DIR
+from config import RUN_CONFIG, SKILLS_DIR, TRACE_STORE_PATH, DEBUG, LOG_LEVEL, LOG_FILE, BROWSER_TYPE, HEADLESS, INPUT_DIR, OUTPUT_DIR, FEATURES_DIR
 
 
 async def main():
@@ -86,7 +86,7 @@ async def main():
     print("4. 构建任务消息...")
     print(f"   - 输入目录: {INPUT_DIR}")
     print(f"   - 输出目录: {OUTPUT_DIR}")
-    messages = prompt.build_messages(input_dir=INPUT_DIR, output_dir=OUTPUT_DIR)
+    messages = prompt.build_messages(input_dir=INPUT_DIR, output_dir=OUTPUT_DIR, features_dir=FEATURES_DIR)
 
     # 5. 初始化浏览器
     browser_mode_names = {"cloud": "云浏览器", "local": "本地浏览器", "container": "容器浏览器"}


+ 0 - 10
log2.txt

@@ -1,10 +0,0 @@
-Usage:
-  Upload: python upload.py <file_path>
-  Download: python upload.py download <url> <save_path>
-
---- Running Self Test ---
-Uploading img_1_gen.png to aigc-crawler/crawler/image...
-Upload SDK Response: {'oss_object_key': 'crawler/image/img_1_gen.png', 'save_oss_timestamp': 1775658352873}
-
-Extracted URL: https://res.cybertogether.net/crawler/image/img_1_gen.png
-Downloading from https://res.cybertogether.net/crawler/image/img_1_gen.png to downloaded_dummy.png...

BIN
outputs/540bdb68-9b2/flux_generate_1775743063412_0.png


BIN
outputs/540bdb68-9b2/flux_generate_1775743308665_0.png


BIN
outputs/540bdb68-9b2/flux_generate_1775743381440_0.png


BIN
outputs/540bdb68-9b2/flux_generate_1775744492978_0.png


BIN
outputs/540bdb68-9b2/nano_banana_1775742258513_0.jpg


BIN
outputs/540bdb68-9b2/nano_banana_1775742490073_0.jpg


BIN
outputs/540bdb68-9b2/nano_banana_1775742774289_0.jpg


BIN
outputs/540bdb68-9b2/seedream_generate_1775743450714_0.jpg


BIN
outputs/540bdb68-9b2/seedream_generate_1775743784246_0.jpg


BIN
outputs/540bdb68-9b2/seedream_generate_1775744243329_0.jpg


BIN
outputs/810058fe-de8/nano_banana_1775739644055_0.jpg


BIN
outputs/810058fe-de8/nano_banana_1775739749883_0.jpg


BIN
outputs/810058fe-de8/nano_banana_1775739853853_0.jpg


BIN
outputs/810058fe-de8/nano_banana_1775739957102_0.jpg


BIN
outputs/810058fe-de8/nano_banana_1775740058087_0.jpg


BIN
outputs/810058fe-de8/nano_banana_1775740171922_0.jpg


BIN
outputs/810058fe-de8/nano_banana_1775740270417_0.jpg


BIN
outputs/810058fe-de8/nano_banana_1775740359987_0.jpg


BIN
outputs/810058fe-de8/nano_banana_1775740610837_0.jpg


BIN
outputs/810058fe-de8/nano_banana_1775740709313_0.jpg


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