Explorar el Código

update simple research example

Talegorithm hace 5 días
padre
commit
3ac52ef843

+ 0 - 106
examples/research/README.md

@@ -1,106 +0,0 @@
-# 浏览器调研示例
-
-支持云浏览器和本地浏览器两种模式的 Agent 自动化调研工具。
-
-## 功能特性
-
-1. **Agent 自动化调研** - 使用 LLM 驱动的 Agent 自动执行浏览器操作
-2. **手动接管模式** - 运行中随时按 [Enter] 键暂停 Agent,手动操作浏览器
-3. **自动清理** - 无论成功或崩溃,均安全关闭浏览器进程
-4. **灵活切换** - 支持云浏览器和本地浏览器模式切换
-
-## 浏览器模式配置
-
-### 切换方法
-
-编辑 `run.py` 文件顶部的配置变量:
-
-```python
-# ===== 浏览器模式配置 =====
-BROWSER_TYPE = "cloud"  # 可选: "cloud" 或 "local"
-HEADLESS = False        # 是否无头模式运行
-```
-
-### 模式说明
-
-#### 云浏览器模式 (`"cloud"`)
-- ✅ 不占用本地资源
-- ✅ 适合生产环境
-- ✅ 可在无 GUI 的服务器上运行
-- ⚠️ 需要配置 browser-use 云服务
-- ⚠️ 可能需要 API 密钥
-
-#### 本地浏览器模式 (`"local"`)
-- ✅ 速度更快
-- ✅ 支持可视化调试
-- ✅ 无需额外配置
-- ⚠️ 需要本地安装 Chrome
-- ⚠️ 占用本地资源
-
-## 使用方法
-
-### 1. 准备环境
-
-```bash
-# 安装依赖
-pip install -r requirements.txt
-
-# 配置环境变量(复制 .env.example 为 .env)
-cp .env.example .env
-# 编辑 .env 文件,配置 OPENROUTER_API_KEY 等
-```
-
-### 2. 配置任务
-
-编辑 `test.prompt` 文件,设置调研任务:
-
-```
----
-model: gemini-3-flash-preview
-temperature: 0.3
----
-
-[system]
-你是一个专业的网络调研助手...
-
-[user]
-请帮我调研...
-```
-
-### 3. 运行
-
-```bash
-python run.py
-```
-
-### 4. 手动接管(可选)
-
-运行过程中,如需手动操作浏览器(如登录、验证码等):
-
-1. 按下 **[Enter]** 键
-2. Agent 会在完成当前动作后暂停
-3. 在浏览器窗口完成必要操作
-4. 再次按 **[Enter]** 或点击页面交互按钮继续
-
-## 输出结果
-
-- 调研结果保存在 `output/` 目录
-- Trace 数据保存在项目根目录的 `.trace/` 目录
-- 可通过可视化面板查看详细执行过程
-
-## 故障排除
-
-### 云浏览器连接失败
-- 检查 browser-use 云服务配置
-- 确认 API 密钥正确
-- 检查网络连接
-
-### 本地浏览器启动失败
-- 确认已安装 Chrome 浏览器
-- 检查 Chrome 路径是否正确
-- 尝试关闭其他 Chrome 实例
-
-### Agent 执行异常
-- 查看终端日志输出
-- 检查 `.trace/` 目录中的 trace 数据
-- 调整 `test.prompt` 中的任务描述

+ 0 - 191
examples/research/TROUBLESHOOTING.md

@@ -1,191 +0,0 @@
-# 故障排除指南
-
-## Cookie 文件相关问题
-
-### 问题:提示"没有 Cookie 文件"或"Cookie 目录不存在"
-
-#### 原因
-这不是云浏览器特有的问题,而是首次使用时的正常情况:
-
-1. Agent 在执行某些任务时可能会尝试加载之前保存的 Cookie(用于保持登录态)
-2. 如果这是第一次运行,`.cache/.cookies` 目录不存在或没有对应的 Cookie 文件
-3. 工具会给出友好提示,并自动导航到目标页面
-
-#### 解决方案
-
-**方案 1:让 Agent 自动处理(推荐)**
-
-从 v2.0 开始,`browser_load_cookies` 工具已经优化:
-- 找不到 Cookie 时会自动导航到目标页面
-- Agent 可以继续执行任务,不会中断
-- 你可以手动登录后,Agent 会继续后续操作
-
-**方案 2:预先保存 Cookie**
-
-如果你需要频繁访问需要登录的网站:
-
-1. 首次运行时手动登录
-2. 在终端按 `[Enter]` 暂停 Agent
-3. 在浏览器中完成登录
-4. 让 Agent 继续,它会自动调用 `browser_export_cookies` 保存 Cookie
-5. 下次运行时会自动加载 Cookie,无需重复登录
-
-**方案 3:手动保存 Cookie**
-
-```bash
-# 1. 启动浏览器并访问目标网站
-# 2. 手动登录
-# 3. 在 Python 中执行:
-
-from agent.tools.builtin.browser.baseClass import browser_export_cookies
-
-# 保存当前页面的 Cookie
-await browser_export_cookies(name="example.com")
-
-# Cookie 会保存到 .cache/.cookies/example.com.json
-```
-
-### 问题:云浏览器和本地浏览器的 Cookie 是否共享?
-
-**是的**,Cookie 文件存储在本地文件系统(`.cache/.cookies/`),与浏览器类型无关:
-
-- 云浏览器保存的 Cookie 可以在本地浏览器中使用
-- 本地浏览器保存的 Cookie 可以在云浏览器中使用
-- 切换浏览器模式不会丢失已保存的 Cookie
-
-### Cookie 文件位置
-
-```
-项目根目录/
-  └── .cache/
-      └── .cookies/
-          ├── example.com.json
-          ├── github.com.json
-          └── ...
-```
-
-### Cookie 文件格式
-
-Cookie 文件使用 JSON 格式,符合 Chrome DevTools Protocol (CDP) 规范:
-
-```json
-[
-  {
-    "name": "session_id",
-    "value": "abc123...",
-    "domain": ".example.com",
-    "path": "/",
-    "expires": 1234567890,
-    "httpOnly": true,
-    "secure": true
-  }
-]
-```
-
-## 云浏览器特定问题
-
-### 问题:云浏览器连接失败
-
-#### 可能原因
-1. browser-use 云服务未配置
-2. API 密钥错误或过期
-3. 网络连接问题
-4. 云服务配额用尽
-
-#### 解决方案
-
-1. **检查配置**
-   ```bash
-   # 查看 .env 文件
-   cat .env
-
-   # 确认包含必要的配置(如果需要)
-   # BROWSER_USE_API_KEY=your_key_here
-   ```
-
-2. **切换到本地浏览器**
-   ```python
-   # 编辑 run.py
-   BROWSER_TYPE = "local"  # 改为 local
-   ```
-
-3. **查看详细日志**
-   ```python
-   # 在 run.py 中启用调试日志
-   logging.basicConfig(level=logging.DEBUG)
-   ```
-
-### 问题:云浏览器速度慢
-
-#### 原因
-- 网络延迟
-- 云服务器负载高
-- 需要传输大量数据(如图片、视频)
-
-#### 解决方案
-
-1. **使用本地浏览器**(如果可以)
-   ```python
-   BROWSER_TYPE = "local"
-   ```
-
-2. **启用无头模式**(减少渲染开销)
-   ```python
-   HEADLESS = True
-   ```
-
-3. **优化任务**
-   - 减少不必要的页面导航
-   - 使用 API 代替浏览器操作(如果可能)
-
-## 其他常见问题
-
-### 问题:Agent 卡住不动
-
-#### 可能原因
-1. 等待页面加载超时
-2. 等待元素出现超时
-3. 网络请求阻塞
-
-#### 解决方案
-
-1. **手动接管**
-   - 按 `[Enter]` 键暂停 Agent
-   - 检查浏览器状态
-   - 手动完成操作后继续
-
-2. **调整超时设置**
-   ```python
-   # 在 test.prompt 中添加
-   [system]
-   如果页面加载超过 30 秒,请跳过并继续下一步
-   ```
-
-### 问题:浏览器进程未正确关闭
-
-#### 解决方案
-
-```bash
-# Windows
-taskkill /F /IM chrome.exe
-
-# Linux/Mac
-pkill -9 chrome
-```
-
-或者在代码中确保清理:
-
-```python
-from agent.tools.builtin.browser.baseClass import kill_browser_session
-
-# 在 finally 块中调用
-await kill_browser_session()
-```
-
-## 获取帮助
-
-如果以上方案都无法解决问题:
-
-1. 查看完整日志输出
-2. 检查 `.trace/` 目录中的 trace 数据
-3. 在 GitHub 提交 issue:https://github.com/anthropics/claude-code/issues

+ 15 - 0
examples/research/research.prompt

@@ -0,0 +1,15 @@
+---
+model: sonnet-4.6
+temperature: 0.3
+---
+
+$system$
+你是最顶尖的AI助手,可以拆分并调用工具逐步解决复杂问题。
+
+$user$
+帮我调研一下,openclaw在企业中的应用有哪些方案和经验?如何实现组织层面的知识共享?
+
+- 要有可靠的依据(例如行业大牛的博客、榜单等等),要尽量全面,而不是随便选择一个平台深入
+- 我希望你在需要时能够打开浏览器做调研,并将调研收获在 examples/research/knowledge/ 目录下组织维护好,注意保留原始来源的URL
+- 调研过程中注意记录,以免意外中止时候失去所有进展;过程结果和最终结果要清晰整理好
+- **禁止降级解决**:不允许为了方便而使用效果显著更差的简单方案。你应该拆解子目标逐步完成,或者转交给sub agent解决某一项适合拆分的子任务

+ 564 - 163
examples/research/run.py

@@ -1,32 +1,28 @@
 """
-浏览器调研示例 (支持云浏览器/本地浏览器切换)
-
-功能:
-1. Agent 模式自动化调研
-2. 手动接管:随时按 [Enter] 键暂停 Agent 并手动操作浏览器
-3. 自动清理:无论成功或崩溃,均安全关闭浏览器进程
-4. 灵活切换:通过配置变量选择云浏览器或本地浏览器
-
-浏览器模式配置:
-- 修改下方 BROWSER_TYPE 变量来切换模式
-- "cloud": 云浏览器模式,不占用本地资源,需要配置 browser-use 云服务
-- "local": 本地浏览器模式,在本地运行 Chrome,速度更快,支持可视化调试
-"""
+示例(增强版)
+
+使用 Agent 模式 + Skills
 
-# ===== 浏览器模式配置 =====
-# 可选值: "cloud" (云浏览器) 或 "local" (本地浏览器)
-BROWSER_TYPE = "cloud"  # 修改这里来切换浏览器模式
-HEADLESS = False  # 是否无头模式运行
+新增功能:
+1. 支持命令行随时打断(输入 'p' 暂停,'q' 退出)
+2. 暂停后可插入干预消息
+3. 支持触发经验总结
+4. 查看当前 GoalTree
+5. 框架层自动清理不完整的工具调用
+6. 支持通过 --trace <ID> 恢复已有 Trace 继续执行
+"""
 
+import argparse
 import os
 import sys
+import select
 import asyncio
-import logging
-import re
-import uuid
 from pathlib import Path
-from datetime import datetime
-from argparse import Namespace
+
+# Clash Verge TUN 模式兼容:禁止 httpx/urllib 自动检测系统 HTTP 代理
+# TUN 虚拟网卡已在网络层接管所有流量,不需要应用层再走 HTTP 代理,
+# 否则 httpx 检测到 macOS 系统代理 (127.0.0.1:7897) 会导致 ConnectError
+os.environ.setdefault("no_proxy", "*")
 
 # 添加项目根目录到 Python 路径
 sys.path.insert(0, str(Path(__file__).parent.parent.parent))
@@ -34,176 +30,581 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
 from dotenv import load_dotenv
 load_dotenv()
 
-# --- 日志配置 ---
-logging.basicConfig(level=logging.WARNING)
-logging.getLogger("agent.core.message_manager").setLevel(logging.INFO)
-logging.getLogger("tools").setLevel(logging.INFO)
-
 from agent.llm.prompts import SimplePrompt
 from agent.core.runner import AgentRunner, RunConfig
-from agent.trace import FileSystemTraceStore, Trace, Message
+from agent.core.presets import AgentPreset, register_preset
+from agent.trace import (
+    FileSystemTraceStore,
+    Trace,
+    Message,
+)
 from agent.llm import create_openrouter_llm_call
-from agent.tools.builtin.browser.baseClass import kill_browser_session, init_browser_session
+from agent.tools import get_tool_registry
+
+
+# ===== 非阻塞 stdin 检测 =====
+if sys.platform == 'win32':
+    import msvcrt
+
+def check_stdin() -> str | None:
+    """
+    跨平台非阻塞检查 stdin 输入。
+    Windows: 使用 msvcrt.kbhit()
+    macOS/Linux: 使用 select.select()
+    """
+    if sys.platform == 'win32':
+        # 检查是否有按键按下
+        if msvcrt.kbhit():
+            # 读取按下的字符(msvcrt.getwch 是非阻塞读取宽字符)
+            ch = msvcrt.getwch().lower()
+            if ch == 'p':
+                return 'pause'
+            if ch == 'q':
+                return 'quit'
+            # 如果是其他按键,可以选择消耗掉或者忽略
+        return None
+    else:
+        # Unix/Mac 逻辑
+        ready, _, _ = select.select([sys.stdin], [], [], 0)
+        if ready:
+            line = sys.stdin.readline().strip().lower()
+            if line in ('p', 'pause'):
+                return 'pause'
+            if line in ('q', 'quit'):
+                return 'quit'
+        return None
+
+
+# ===== 交互菜单 =====
+
+def _read_multiline() -> str:
+    """
+    读取多行输入,以连续两次回车(空行)结束。
+
+    单次回车只是换行,不会提前终止输入。
+    """
+    print("\n请输入干预消息(连续输入两次回车结束):")
+    lines: list[str] = []
+    blank_count = 0
+    while True:
+        line = input()
+        if line == "":
+            blank_count += 1
+            if blank_count >= 2:
+                break
+            lines.append("")          # 保留单个空行
+        else:
+            blank_count = 0
+            lines.append(line)
+
+    # 去掉尾部多余空行
+    while lines and lines[-1] == "":
+        lines.pop()
+    return "\n".join(lines)
 
-# ===== 全局交互控制 =====
-pause_event = asyncio.Event()
 
-async def listen_for_interrupt():
-    """后台协程:监听标准输入,按下回车即触发暂停"""
+async def show_interactive_menu(
+    runner: AgentRunner,
+    trace_id: str,
+    current_sequence: int,
+    store: FileSystemTraceStore,
+):
+    """
+    显示交互式菜单,让用户选择操作。
+
+    进入本函数前不再有后台线程占用 stdin,所以 input() 能正常工作。
+    """
+    print("\n" + "=" * 60)
+    print("  执行已暂停")
+    print("=" * 60)
+    print("请选择操作:")
+    print("  1. 插入干预消息并继续")
+    print("  2. 触发经验总结(reflect)")
+    print("  3. 查看当前 GoalTree")
+    print("  4. 手动压缩上下文(compact)")
+    print("  5. 继续执行")
+    print("  6. 停止执行")
+    print("=" * 60)
+
     while True:
-        # 在执行器中运行同步的 readline,避免阻塞事件循环
-        await asyncio.get_event_loop().run_in_executor(None, sys.stdin.readline)
-        if not pause_event.is_set():
-            print("\n" + "!" * 40)
-            print("🛑 检测到手动干预请求!")
-            print("Agent 将在完成当前动作后暂停,请准备接管浏览器。")
-            print("!" * 40 + "\n")
-            pause_event.set()
+        choice = input("请输入选项 (1-6): ").strip()
+
+        if choice == "1":
+            text = _read_multiline()
+            if not text:
+                print("未输入任何内容,取消操作")
+                continue
+
+            print(f"\n将插入干预消息并继续执行...")
+            # 从 store 读取实际的 last_sequence,避免本地 current_sequence 过时
+            live_trace = await store.get_trace(trace_id)
+            actual_sequence = live_trace.last_sequence if live_trace and live_trace.last_sequence else current_sequence
+            return {
+                "action": "continue",
+                "messages": [{"role": "user", "content": text}],
+                "after_sequence": actual_sequence,
+            }
+
+        elif choice == "2":
+            # 触发经验总结
+            print("\n触发经验总结...")
+            focus = input("请输入反思重点(可选,直接回车跳过): ").strip()
+
+            # 触发反思
+            await perform_reflection(runner, store, trace_id, focus=focus)
+            continue
 
-# ===== 核心逻辑 =====
+        elif choice == "3":
+            goal_tree = await store.get_goal_tree(trace_id)
+            if goal_tree and goal_tree.goals:
+                print("\n当前 GoalTree:")
+                print(goal_tree.to_prompt())
+            else:
+                print("\n当前没有 Goal")
+            continue
+
+        elif choice == "4":
+            # 手动压缩上下文
+            print("\n正在执行上下文压缩(compact)...")
+            try:
+                goal_tree = await store.get_goal_tree(trace_id)
+                trace = await store.get_trace(trace_id)
+                if not trace:
+                    print("未找到 Trace,无法压缩")
+                    continue
+
+                # 重建当前 history
+                main_path = await store.get_main_path_messages(trace_id, trace.head_sequence)
+                history = [msg.to_llm_dict() for msg in main_path]
+                head_seq = main_path[-1].sequence if main_path else 0
+                next_seq = head_seq + 1
+
+                compact_config = RunConfig(trace_id=trace_id)
+                new_history, new_head, new_seq = await runner._compress_history(
+                    trace_id=trace_id,
+                    history=history,
+                    goal_tree=goal_tree,
+                    config=compact_config,
+                    sequence=next_seq,
+                    head_seq=head_seq,
+                )
+                print(f"\n✅ 压缩完成: {len(history)} 条消息 → {len(new_history)} 条")
+            except Exception as e:
+                print(f"\n❌ 压缩失败: {e}")
+            continue
+
+        elif choice == "5":
+            print("\n继续执行...")
+            return {"action": "continue"}
+
+        elif choice == "6":
+            print("\n停止执行...")
+            return {"action": "stop"}
+
+        else:
+            print("无效选项,请重新输入")
+
+async def perform_reflection(runner: AgentRunner, store: FileSystemTraceStore, trace_id: str, focus: str = ""):
+    """执行经验总结并保存(带结构化 YAML 解析)"""
+    from agent.trace.compaction import build_reflect_prompt
+    import re as _re2
+    import uuid as _uuid2
+    from datetime import datetime
+    
+    trace = await store.get_trace(trace_id)
+    if not trace:
+        return
+    saved_head = trace.head_sequence
+
+    prompt = build_reflect_prompt()
+    if focus:
+        prompt += f"\n\n请特别关注:{focus}"
+
+    print("正在生成反思...")
+    reflect_cfg = RunConfig(trace_id=trace_id, max_iterations=1, tools=[])
+    reflection_text = ""
+    
+    try:
+        result = await runner.run_result(
+            messages=[{"role": "user", "content": prompt}],
+            config=reflect_cfg,
+        )
+        reflection_text = result.get("summary", "")
+    finally:
+        # 恢复 head_sequence(反思消息成为侧枝,不污染主对话)
+        await store.update_trace(trace_id, head_sequence=saved_head)
+
+    # 追加到 experiences 文件
+    if reflection_text:
+        experiences_path = runner.experiences_path or "./.cache/experiences_how.md"
+        os.makedirs(os.path.dirname(experiences_path), exist_ok=True)
+
+        pattern = r"-\s*\[(?P<tags>.*?)\]\s*(?P<content>.*)"
+        matches = list(_re2.finditer(pattern, reflection_text))
+
+        structured_entries = []
+        for match in matches:
+            tags_str = match.group("tags")
+            content = match.group("content")
+
+            intent_match = _re2.search(r"intent:\s*(.*?)(?:,|$)", tags_str, _re2.IGNORECASE)
+            state_match = _re2.search(r"state:\s*(.*?)(?:,|$)", tags_str, _re2.IGNORECASE)
+
+            intents = [i.strip() for i in intent_match.group(1).split(",")] if intent_match and intent_match.group(1) else []
+            states = [s.strip() for s in state_match.group(1).split(",")] if state_match and state_match.group(1) else []
+
+            ex_id = f"ex_{datetime.now().strftime('%m%d%H%M')}_{_uuid2.uuid4().hex[:4]}"
+            
+            entry = f"---\nid: {ex_id}\ntrace_id: {trace_id}\ntags: {{intent: {intents}, state: {states}}}\nmetrics: {{helpful: 1, harmful: 0}}\ncreated_at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n---\n- {content}\n- 经验ID: [{ex_id}]"
+            structured_entries.append(entry)
+
+        if structured_entries:
+            final_output = "\n\n" + "\n\n".join(structured_entries)
+            with open(experiences_path, "a", encoding="utf-8") as f:
+                f.write(final_output)
+            print(f"\n✅ 提取了 {len(structured_entries)} 条经验,已结构化并保存到: {experiences_path}")
+            print("\n--- 反思内容(结构化后) ---")
+            print(final_output.strip())
+            print("--- 结束 ---\n")
+        else:
+            print("\n⚠️ 未能解析出符合格式的经验条目,已保存原始纯文本以供检查。")
+            header = f"\n\n---\n\n## [Raw] {trace_id} ({datetime.now().strftime('%Y-%m-%d %H:%M')})\n\n"
+            with open(experiences_path, "a", encoding="utf-8") as f:
+                f.write(header + reflection_text + "\n")
+            print(reflection_text)
+    else:
+        print("未生成反思内容")
 
 async def main():
-    # 1. 环境准备
+    # 解析命令行参数
+    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
-    trace_dir = project_root / ".trace"
-    prompt_path = base_dir / "test.prompt"
-    output_dir = base_dir / "output"
+    prompt_path = base_dir / "research.prompt"
+    output_dir = base_dir / "output_1"
     output_dir.mkdir(exist_ok=True)
 
+    # 加载项目级 presets(examples/how/presets.json)
+    presets_path = base_dir / "presets.json"
+    if presets_path.exists():
+        import json
+        with open(presets_path, "r", encoding="utf-8") as f:
+            project_presets = json.load(f)
+        for name, cfg in project_presets.items():
+            register_preset(name, AgentPreset(**cfg))
+        print(f"   - 已加载项目 presets: {list(project_presets.keys())}")
+
+    # Skills 目录(可选:用户自定义 skills)
+    # 注意:内置 skills(agent/memory/skills/)会自动加载
+    skills_dir = str(base_dir / "skills")
+
     print("=" * 60)
-    print("🚀 交互式浏览器调研 Agent")
-    print(f"🌐 浏览器模式: {'云浏览器 (Cloud)' if BROWSER_TYPE == 'cloud' else '本地浏览器 (Local)'}")
-    print("👉 操作指南:")
-    print("   - 运行中随时按下 [Enter] 键进入手动接管模式")
-    print("   - 在浏览器完成操作后,点击页面上的 'Done' 或回车返回")
-    print("=" * 60 + "\n")
-
-    # 2. 加载任务
+    print("mcp/skills 发现、获取、评价 分析任务 (Agent 模式 + 交互增强)")
+    print("=" * 60)
+    print()
+    print("💡 交互提示:")
+    print("   - 执行过程中输入 'p' 或 'pause' 暂停并进入交互模式")
+    print("   - 执行过程中输入 'q' 或 'quit' 停止执行")
+    print("=" * 60)
+    print()
+
+    # 1. 加载 prompt
+    print("1. 加载 prompt 配置...")
     prompt = SimplePrompt(prompt_path)
-    system_prompt = prompt._messages.get("system", "")
-    user_task = prompt._messages.get("user", "")
-    # 默认使用 cheap 模型进行调研,如 gemini-3-flash-preview
-    model_name = prompt.config.get('model', 'gemini-3-flash-preview')
-    temperature = float(prompt.config.get('temperature', 0.3))
 
+    # 2. 构建消息(仅新建时使用,恢复时消息已在 trace 中)
+    print("2. 构建任务消息...")
     messages = prompt.build_messages()
 
-    # 3. 初始化浏览器会话
-    browser_mode_name = "云浏览器" if BROWSER_TYPE == "cloud" else "本地浏览器"
-    print(f"🌐 正在初始化{browser_mode_name}...")
-    await init_browser_session(
-        browser_type=BROWSER_TYPE,
-        headless=HEADLESS,
-        url="about:blank"
-    )
-    print(f"✅ {browser_mode_name}初始化完成\n")
+    # 3. 创建 Agent Runner(配置 skills)
+    print("3. 创建 Agent Runner...")
+    print(f"   - Skills 目录: {skills_dir}")
+    print(f"   - 模型: {prompt.config.get('model', 'sonnet-4.5')}")
+
+    # 加载自定义工具
+    print("   - 加载自定义工具: nanobanana")
+    import examples.how.tool  # 导入自定义工具模块,触发 @tool 装饰器注册
 
-    # 4. 初始化 Runner
-    # 注意:确保你的 openrouter 配置正确
+    store = FileSystemTraceStore(base_path=".trace")
     runner = AgentRunner(
-        trace_store=FileSystemTraceStore(base_path=str(trace_dir)),
-        llm_call=create_openrouter_llm_call(model=f"google/{model_name}"),
-        skills_dir=None,
+        trace_store=store,
+        llm_call=create_openrouter_llm_call(model=f"anthropic/claude-{prompt.config.get('model', 'sonnet-4.5')}"),
+        skills_dir=skills_dir,
+        experiences_path="./.cache/experiences_how.md",
         debug=True
     )
 
-    # 5. 启动监听任务
-    interrupt_task = asyncio.create_task(listen_for_interrupt())
-    
+    # 4. 判断是新建还是恢复
+    resume_trace_id = args.trace
+    if resume_trace_id:
+        # 验证 trace 存在
+        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"4. 恢复已有 Trace: {resume_trace_id[:8]}...")
+        print(f"   - 状态: {existing_trace.status}")
+        print(f"   - 消息数: {existing_trace.total_messages}")
+        print(f"   - 任务: {existing_trace.task}")
+    else:
+        print(f"4. 启动新 Agent 模式...")
+
+    print()
+
     final_response = ""
-    current_trace_id = None
+    current_trace_id = resume_trace_id
+    current_sequence = 0
+    should_exit = False
 
     try:
-        # 启动 Agent 迭代
-        agent_stream = runner.run(
-            messages=messages,
-            config=RunConfig(
-                system_prompt=system_prompt,
-                model=f"google/{model_name}",
-                temperature=temperature,
-                max_iterations=30,
-                name=user_task[:50],
-            ),
-        )
+        # 恢复模式:不发送初始消息,只指定 trace_id 续跑
+        if resume_trace_id:
+            initial_messages = None  # None = 未设置,触发早期菜单检查
+            config = RunConfig(
+                model=f"claude-{prompt.config.get('model', 'sonnet-4.5')}",
+                temperature=float(prompt.config.get('temperature', 0.3)),
+                max_iterations=1000,
+                trace_id=resume_trace_id,
+            )
+        else:
+            initial_messages = messages
+            config = RunConfig(
+                model=f"claude-{prompt.config.get('model', 'sonnet-4.5')}",
+                temperature=float(prompt.config.get('temperature', 0.3)),
+                max_iterations=1000,
+                name="调研:openclaw在企业中的应用有哪些方案和经验?如何实现组织层面的知识共享?",
+            )
+
+        while not should_exit:
+            # 如果是续跑,需要指定 trace_id
+            if current_trace_id:
+                config.trace_id = current_trace_id
+
+            # 清理上一轮的响应,避免失败后显示旧内容
+            final_response = ""
 
-        async for item in agent_stream:
-            # --- 检查手动暂停信号 ---
-            if pause_event.is_set():
-                print("\n" + "🛠️" * 20)
-                print(">>> 人工接管模式激活 <<<")
-                print("1. 请在浏览器窗口进行必要操作(登录、过验证码等)")
-                print("2. 操作完成后,请在终端按 [Enter] 或在页面点击交互按钮继续")
-                
-                try:
-                    # 调用内置的等待交互工具
-                    await runner.tools.execute(
-                        "browser_wait_for_user_action",
-                        {"message": "人工干预中,请完成操作后恢复 Agent"},
-                        uid="human_admin",
-                        context={"runner": runner}
+            # 如果 trace 已完成/失败且没有新消息,直接进入交互菜单
+            # 注意:initial_messages 为 None 表示未设置(首次加载),[] 表示有意为空(用户选择"继续")
+            if current_trace_id and initial_messages is None:
+                check_trace = await store.get_trace(current_trace_id)
+                if check_trace and check_trace.status in ("completed", "failed"):
+                    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}")
+                    else:
+                        print(f"\n[Trace] ❌ 已失败: {check_trace.error_message}")
+                    current_sequence = check_trace.head_sequence
+
+                    menu_result = await show_interactive_menu(
+                        runner, current_trace_id, current_sequence, store
                     )
-                except Exception as e:
-                    print(f"⚠️ 交互工具调用失败: {e}")
-                
-                print(">>> 交互结束,交还控制权给 Agent <<<")
-                print("🛠️" * 20 + "\n")
-                pause_event.clear()
-
-            # --- 正常处理 Agent 消息输出 ---
-            if isinstance(item, Trace):
-                current_trace_id = item.trace_id
-                if item.status == "running":
-                    print(f"[{datetime.now().strftime('%H:%M:%S')}] 🛰️ Trace 启动: {item.trace_id[:8]}")
-                elif item.status == "completed":
-                    print(f"\n✅ 任务圆满完成!Cost: ${item.total_cost:.4f}")
-
-            elif isinstance(item, Message):
-                if item.role == "assistant":
-                    content = item.content
-                    if isinstance(content, dict):
-                        text = content.get("text", "")
-                        tool_calls = content.get("tool_calls")
-                        if text:
-                            # 打印摘要,带点 Wit
-                            print(f"\n🤖 Agent: {text[:200]}..." if len(text) > 200 else f"\n🤖 Agent: {text}")
-                        if tool_calls:
-                            for tc in tool_calls:
-                                t_name = tc.get("function", {}).get("name", "unknown")
-                                print(f"   🛠️  执行工具: {t_name}")
-                
-                elif item.role == "tool":
-                    t_content = item.content
-                    if isinstance(t_content, dict):
-                        t_name = t_content.get("tool_name", "unknown")
-                        print(f"   ✅ 工具返回: {t_name}")
-
-    except Exception as e:
-        print(f"\n🔥 发生严重错误: {e}")
-        import traceback
-        traceback.print_exc()
 
-    finally:
-        # 停止监听协程
-        interrupt_task.cancel()
-
-        # 6. 强制清理浏览器环境
-        print("\n" + "·" * 40)
-        print("🧹 正在执行环境清理...")
-        try:
-            await kill_browser_session()
-            print(f"✨ {browser_mode_name}进程已安全终止。")
-        except Exception as err:
-            print(f"❌ 清理失败: {err}")
-        print("·" * 40 + "\n")
-
-    # 7. 结果展示
+                    if menu_result["action"] == "stop":
+                        break
+                    elif menu_result["action"] == "continue":
+                        new_messages = menu_result.get("messages", [])
+                        if new_messages:
+                            initial_messages = new_messages
+                            config.after_sequence = menu_result.get("after_sequence")
+                        else:
+                            # 无新消息:对 failed trace 意味着重试,对 completed 意味着继续
+                            initial_messages = []
+                            config.after_sequence = None
+                        continue
+                    break
+
+                # 对 stopped/running 等非终态的 trace,直接续跑
+                initial_messages = []
+
+            print(f"{'▶️ 开始执行...' if not current_trace_id else '▶️ 继续执行...'}")
+
+            # 执行 Agent
+            paused = False
+            try:
+                async for item in runner.run(messages=initial_messages, config=config):
+                    # 检查用户中断
+                    cmd = check_stdin()
+                    if cmd == 'pause':
+                        # 暂停执行
+                        print("\n⏸️ 正在暂停执行...")
+                        if current_trace_id:
+                            await runner.stop(current_trace_id)
+
+                        # 等待一小段时间让 runner 处理 stop 信号
+                        await asyncio.sleep(0.5)
+
+                        # 显示交互菜单
+                        menu_result = await show_interactive_menu(
+                            runner, current_trace_id, current_sequence, store
+                        )
+
+                        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:
+                                    config.after_sequence = after_seq
+                                paused = True
+                                break
+                            else:
+                                # 没有新消息,需要重启执行
+                                initial_messages = []
+                                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 tokens: {item.total_tokens}")
+                            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}")
+
+                                if tool_calls:
+                                    for tc in tool_calls:
+                                        tool_name = tc.get("function", {}).get("name", "unknown")
+                                        print(f"[Tool Call] 🛠️  {tool_name}")
+
+                        elif item.role == "tool":
+                            content = item.content
+                            if isinstance(content, dict):
+                                tool_name = content.get("tool_name", "unknown")
+                                print(f"[Tool Result] ✅ {tool_name}")
+                            if item.description:
+                                desc = item.description[:80] if len(item.description) > 80 else item.description
+                                print(f"  {desc}...")
+
+            except Exception as e:
+                print(f"\n执行出错: {e}")
+                import traceback
+                traceback.print_exc()
+
+            # paused → 菜单已在暂停时内联显示过
+            if paused:
+                if should_exit:
+                    break
+                continue
+
+            # quit → 直接退出
+            if should_exit:
+                break
+
+            # Runner 退出(完成/失败/停止/异常)→ 显示交互菜单
+            if current_trace_id:
+                # 🌟 新增:自动触发反思的生命周期钩子
+                check_trace = await store.get_trace(current_trace_id)
+                if check_trace and check_trace.status in ("completed", "failed"):
+                    print(f"\n⚙️ 任务已结束 (状态: {check_trace.status}),正在自动触发经验总结...")
+                    
+                    # 如果是失败状态,自动带上针对性的 focus 提示
+                    auto_focus = "本次任务执行失败了,请重点反思失败的原因、踩坑点以及未来应如何避免。" if check_trace.status == "failed" else ""
+                    
+                    await perform_reflection(runner, store, current_trace_id, focus=auto_focus)
+
+                # 自动反思结束后,依然弹出菜单,让用户决定是彻底退出(6)还是查看总结(3)
+                menu_result = await show_interactive_menu(
+                    runner, current_trace_id, current_sequence, store
+                )
+
+                if menu_result["action"] == "stop":
+                    break
+                elif menu_result["action"] == "continue":
+                    new_messages = menu_result.get("messages", [])
+                    if new_messages:
+                        initial_messages = new_messages
+                        config.after_sequence = menu_result.get("after_sequence")
+                    else:
+                        initial_messages = []
+                        config.after_sequence = None
+                    continue
+            break
+
+    except KeyboardInterrupt:
+        print("\n\n用户中断 (Ctrl+C)")
+        if current_trace_id:
+            await runner.stop(current_trace_id)
+
+    # 6. 输出结果
+    if final_response:
+        print()
+        print("=" * 60)
+        print("Agent 响应:")
+        print("=" * 60)
+        print(final_response)
+        print("=" * 60)
+        print()
+
+        # 7. 保存结果
+        output_file = output_dir / "result.txt"
+        with open(output_file, 'w', encoding='utf-8') as f:
+            f.write(final_response)
+
+        print(f"✓ 结果已保存到: {output_file}")
+        print()
+
+    # 可视化提示
     if current_trace_id:
-        print(f"🔍 任务 Trace ID: {current_trace_id}")
-        print(f"📊 访问可视化面板查看详情。")
+        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__":
-    try:
-        asyncio.run(main())
-    except KeyboardInterrupt:
-        print("\n👋 收到退出信号,程序已停止。")
+    asyncio.run(main())

+ 0 - 11
examples/research/test.prompt

@@ -1,11 +0,0 @@
----
-model: gemini-2.5-flash
-temperature: 0.3
----
-
-$system$
-你是最顶尖的AI助手,可以拆分并调用工具逐步解决复杂问题。
-
-$user$
-登录一下小红书,在小红书内的搜索框搜索一下摄影.使用load_cookies来登录
-