Просмотр исходного кода

Merge remote-tracking branch 'refs/remotes/origin/main'

Talegorithm 6 часов назад
Родитель
Сommit
ad04223898

+ 2 - 1
.claude/settings.local.json

@@ -14,7 +14,8 @@
       "Bash(tree:*)",
       "Bash(xargs grep:*)",
       "Bash(npm run:*)",
-      "Bash(sed:*)"
+      "Bash(sed:*)",
+      "Bash(PYTHONIOENCODING=utf-8 python:*)"
     ],
     "deny": [],
     "ask": []

+ 42 - 26
README.md

@@ -78,8 +78,12 @@ async def check_inventory(product_id: str, warehouse: str = "default") -> ToolRe
     "parameters": {
       "type": "object",
       "properties": {
-        "product_id": {"type": "string", "description": "产品唯一标识符"},
-        "warehouse": {"type": "string", "description": "仓库编码,默认为主仓库", "default": "default"}
+        "product_id": { "type": "string", "description": "产品唯一标识符" },
+        "warehouse": {
+          "type": "string",
+          "description": "仓库编码,默认为主仓库",
+          "default": "default"
+        }
       },
       "required": ["product_id"]
     }
@@ -111,6 +115,7 @@ description: 领域专属知识
 ---
 
 ## Guidelines
+
 - 规则 1
 - 规则 2
 ```
@@ -132,6 +137,7 @@ runner = AgentRunner(
 ### 核心流程
 
 **1. 提取(Extract)**
+
 - **触发时机**:
   - 压缩时提取:消息量超阈值触发压缩时,在 Level 1 过滤前用完整 history 反思
   - 完成时提取:Agent 运行完成后(不代表任务完成,可能中途退出等待人工评估)
@@ -139,6 +145,7 @@ runner = AgentRunner(
 - **自定义 Prompt**:可通过配置自定义反思 prompt,空则使用默认(见 `agent/core/prompts/knowledge.py`)
 
 **2. 存储(Store)**
+
 - **存储位置**:KnowHub 服务(默认 `http://localhost:8765`)
 - **知识结构**:
   - `title`: 知识标题
@@ -154,6 +161,7 @@ runner = AgentRunner(
   - 支持代码片段、API 凭证、cookies 等多种资源类型
 
 **3. 注入(Inject)**
+
 - **触发时机**:Agent 切换当前工作的 Goal 时自动触发
 - **检索策略**:基于 Goal 描述和上下文,从知识库检索相关知识
 - **注入方式**:将检索到的知识注入到 Agent 的上下文中
@@ -193,6 +201,7 @@ run_config = RunConfig(
 ```
 
 **参数注入规则**(通过框架 `inject_params` 机制实现,详见 `agent/docs/tools.md`):
+
 - `owner`:隐藏参数,LLM 不可见,框架自动注入(`mode: default`)
 - `tags`:LLM 可追加新 key,框架默认 key 不可被覆盖(`mode: merge`)
 - `scopes`:LLM 可追加,与框架默认值合并去重(`mode: merge`)
@@ -237,12 +246,15 @@ RunConfig(
     knowledge=KnowledgeConfig(),  # 知识管理配置
 )
 ```
+
     system_prompt=None,       # None=从 skills 自动构建
     agent_type="default",     # 预设类型:default / explore / analyst
     trace_id=None,            # 续跑/回溯时传入已有 trace ID
     after_sequence=None,      # 从哪条消息后续跑(message sequence)
+
 )
-```
+
+````
 
 ## LLM Providers
 
@@ -256,7 +268,7 @@ llm = create_openrouter_llm_call(model="anthropic/claude-sonnet-4.5")
 
 # Google Gemini
 llm = create_gemini_llm_call(model="gemini-2.5-flash")
-```
+````
 
 自定义 provider 只需实现签名:
 
@@ -279,15 +291,15 @@ async def my_llm_call(messages, model, tools, temperature, **kwargs) -> dict:
 python api_server.py
 ```
 
-| 方法 | 路径 | 说明 |
-|------|------|------|
-| GET | `/api/traces` | 列出 Traces |
-| GET | `/api/traces/{id}` | Trace 详情 |
-| GET | `/api/traces/{id}/messages` | 消息列表 |
-| POST | `/api/traces` | 新建并执行 |
-| POST | `/api/traces/{id}/run` | 续跑/回溯 |
-| POST | `/api/traces/{id}/stop` | 停止 |
-| WS | `/api/traces/{id}/watch` | 实时事件 |
+| 方法 | 路径                        | 说明        |
+| ---- | --------------------------- | ----------- |
+| GET  | `/api/traces`               | 列出 Traces |
+| GET  | `/api/traces/{id}`          | Trace 详情  |
+| GET  | `/api/traces/{id}/messages` | 消息列表    |
+| POST | `/api/traces`               | 新建并执行  |
+| POST | `/api/traces/{id}/run`      | 续跑/回溯   |
+| POST | `/api/traces/{id}/stop`     | 停止        |
+| WS   | `/api/traces/{id}/watch`    | 实时事件    |
 
 需在 `api_server.py` 中配置 Runner 才能启用 POST 端点。
 
@@ -304,8 +316,6 @@ agent/
 
 详细架构文档:[docs/README.md](./docs/README.md)
 
-
-
 ## 交互式 CLI(Interactive CLI)
 
 框架提供交互式控制器,支持实时监控、手动干预和经验总结。
@@ -338,10 +348,10 @@ async for item in runner.run(messages=messages, config=config):
 
 在执行过程中,可以通过命令行实时控制:
 
-| 按键 | 动作 | 说明 |
-| --- | --- | --- |
+| 按键          | 动作         | 说明                              |
+| ------------- | ------------ | --------------------------------- |
 | `p` / `pause` | **暂停执行** | 立即挂起 Agent 循环,进入交互菜单 |
-| `q` / `quit` | **停止执行** | 安全停止并保存当前的执行状态 |
+| `q` / `quit`  | **停止执行** | 安全停止并保存当前的执行状态      |
 
 ### 交互菜单功能
 
@@ -401,6 +411,7 @@ async for item in runner.run(messages=messages, config=RUN_CONFIG):
 ```
 
 **配置说明**:
+
 - 直接使用框架的 `RunConfig` 和 `KnowledgeConfig`,不需要自定义配置类
 - 基础设施配置(skills_dir, trace_store_path 等)用简单变量定义
 - 使用 `agent.utils.setup_logging()` 配置日志
@@ -409,19 +420,24 @@ async for item in runner.run(messages=messages, config=RUN_CONFIG):
 
 框架在运行期间会生成唯一的 `trace_id`。
 
-* **本地日志**:所有的执行细节、工具调用和 Goal 状态均持久化在 `.trace/` 目录下。
-* **Web 可视化**:
+- **本地日志**:所有的执行细节、工具调用和 Goal 状态均持久化在 `.trace/` 目录下。
+- **Web 可视化**:
+
 1. 启动服务器:`python api_server.py`
 2. 启动前端:
+
 ```
   cd frontend/react-template
   yarn
   yarn dev
 ```
-2. 访问控制台:`http://localhost:3000`
-3. 在前端界面中切换任务,即直观追踪 Agent 的思考链路。
+
+3. 访问控制台:`http://localhost:3000`
+4. 在前端界面中切换任务,即直观追踪 Agent 的思考链路。
+5. 因为该可视化读取的是根目录下的.trace文件,建议运行项目时,可以在根目录下用命令行运行`python examples/[project_name]/run.py`,使运行得到的trace存放在根目录
 
 ### 提示:目前前端可视化只供观看本地运行过的trace结果,新任务运行等功能正在开发中,运行可在命令行中执行
+
 ### 绿色节点为整体的goal(目标),蓝色节点为子goal(目标),灰色节点为基础信息节点。点击蓝色边/绿色边会折叠节点,点击节点会在右侧显示详情。
 
 ---
@@ -448,12 +464,12 @@ examples/[your_example]/
 
 针对 Clash Verge / TUN 模式等网络环境,本项目已内置代理自动避让逻辑:
 
-* **代理优化**:通过 `no_proxy` 配置防止 `httpx` 错误引导流量。
-* **Browser 模式**:支持 `cloud` (远程) 和 `local` (本地) 模式切换。
+- **代理优化**:通过 `no_proxy` 配置防止 `httpx` 错误引导流量。
+- **Browser 模式**:支持 `cloud` (远程) 和 `local` (本地) 模式切换。
 
 ## 运行结果存储
 
 运行过程中,会自动存储以下内容:
 
-* **运行轨迹**:根目录下 `.trace/` 文件夹下的实际运行路径结果
-* **知识库**:KnowHub 服务中保存的知识条目(通过 API 访问)
+- **运行轨迹**:根目录下 `.trace/` 文件夹下的实际运行路径结果
+- **知识库**:KnowHub 服务中保存的知识条目(通过 API 访问)

+ 10 - 2
agent/core/runner.py

@@ -178,6 +178,7 @@ BUILTIN_TOOLS = [
     # "sandbox_destroy_environment",
 
     # 浏览器工具
+    "browser_get_live_url",
     "browser_navigate_to_url",
     "browser_search_web",
     "browser_go_back",
@@ -200,10 +201,17 @@ BUILTIN_TOOLS = [
     "browser_get_visual_selector_map",
     "browser_evaluate",
     "browser_ensure_login_with_cookies",
-    "browser_wait_for_user_action",
+    # 可以暂时由飞书消息替代
+    #"browser_wait_for_user_action",
     "browser_done",
     "browser_export_cookies",
-    "browser_load_cookies"
+    "browser_load_cookies",
+
+    # 飞书工具
+    "feishu_send_message_to_contact",
+    "feishu_get_chat_history",
+    "feishu_get_contact_replies",
+    "feishu_get_contact_list",
 ]
 
 

+ 7 - 2
agent/skill/skills/browser.md

@@ -23,12 +23,17 @@ description: 浏览器自动化工具使用指南
 - **操作后等待**: 任何可能触发页面变化的操作(点击、输入、滚动)后都要调用 `browser_wait`
 - **登录处理**:
   - **正常登录**:当遇到需要登录的网页时,使用 `browser_load_cookies` 来登录
-  - **首次登录**:当没有该网站的 cookie 时,点击进入登录界面,然后等待人类来登录,登录后使用 `browser_export_cookies` 将账户信息存储下来
+  - **首次登录**:当没有该网站的 cookie 时,需要请求人类协助登录:
+    1. 调用 `browser_get_live_url` 获取云浏览器实时画面链接
+    2. 导航到目标网站的登录页面
+    3. 通过 `feishu_send_message_to_contact` 将 live URL 发送给相关人员,请求其在浏览器中完成登录
+    4. 使用 `feishu_get_contact_replies(contact_name="...", wait_time_seconds=300)` 等待对方回复确认登录完成
+    5. 收到回复后使用 `browser_export_cookies` 将登录态保存下来
 - **复杂操作用JS**: 当标准工具无法满足时,使用 `browser_evaluate` 执行 JavaScript 代码
 
 ### 工具分类
 
-**导航**: browser_navigate_to_url, browser_search_web, browser_go_back, browser_wait
+**导航**: browser_get_live_url, browser_navigate_to_url, browser_search_web, browser_go_back, browser_wait
 **交互**: browser_click_element, browser_input_text, browser_send_keys, browser_upload_file
 **视图**: browser_scroll_page, browser_find_text, browser_screenshot
 **提取**: browser_extract_content, browser_read_long_content, browser_get_page_html, browser_get_selector_map, browser_get_visual_selector_map

+ 7 - 2
agent/skill/skills/core.md

@@ -97,12 +97,17 @@ goal(abandon="方案A需要Redis,环境没有")
 - **操作后等待**: 任何可能触发页面变化的操作(点击、输入、滚动)后都要调用 `browser_wait`
 - **登录处理**:
   - **正常登录**:当遇到需要登录的网页时,使用`browser_load_cookies`来登录
-  - **首次登录**:当没有该网站的cookie时,点击进入登录界面,然后等待人类来登录,登录后使用`browser_export_cookies`将账户信息存储下来
+  - **首次登录**:当没有该网站的cookie时,需要请求人类协助登录:
+    1. 调用 `browser_get_live_url` 获取云浏览器实时画面链接
+    2. 导航到目标网站的登录页面
+    3. 通过 `feishu_send_message_to_contact` 将 live URL 发送给相关人员,请求其在浏览器中完成登录
+    4. 使用 `feishu_get_contact_replies(contact_name="...", wait_time_seconds=300)` 等待对方回复确认登录完成
+    5. 收到回复后使用 `browser_export_cookies` 将登录态保存下来
 - **复杂操作用JS**: 当标准工具无法满足时,使用 `browser_evaluate` 执行JavaScript代码
 
 ### 工具分类
 
-**导航**: browser_navigate_to_url, browser_search_web, browser_go_back, browser_wait
+**导航**: browser_get_live_url, browser_navigate_to_url, browser_search_web, browser_go_back, browser_wait
 **交互**: browser_click_element, browser_input_text, browser_send_keys, browser_upload_file
 **视图**: browser_scroll_page, browser_find_text, browser_screenshot
 **提取**: browser_extract_content, browser_read_long_content, browser_get_page_html, browser_get_selector_map, browser_get_visual_selector_map

+ 4 - 0
agent/tools/builtin/browser/__init__.py

@@ -8,10 +8,12 @@ from agent.tools.builtin.browser.baseClass import (
     # 会话管理
     init_browser_session,
     get_browser_session,
+    get_browser_live_url,
     cleanup_browser_session,
     kill_browser_session,
 
     # 导航类工具
+    browser_get_live_url,
     browser_navigate_to_url,
     browser_search_web,
     browser_go_back,
@@ -63,10 +65,12 @@ __all__ = [
     # 会话管理
     'init_browser_session',
     'get_browser_session',
+    'get_browser_live_url',
     'cleanup_browser_session',
     'kill_browser_session',
 
     # 导航类工具
+    'browser_get_live_url',
     'browser_navigate_to_url',
     'browser_search_web',
     'browser_go_back',

+ 38 - 2
agent/tools/builtin/browser/baseClass.py

@@ -100,6 +100,7 @@ _browser_tools: Optional[Tools] = None
 _file_system: Optional[FileSystem] = None
 _last_browser_type: str = "local"
 _last_headless: bool = True
+_live_url: Optional[str] = None
 
 async def create_container(url: str, account_name: str = "liuwenwu") -> Dict[str, Any]:
     """
@@ -235,7 +236,7 @@ async def init_browser_session(
     browser_profile: Optional[BrowserProfile] = None,
     **kwargs
 ) -> tuple[BrowserSession, Tools]:
-    global _browser_session, _browser_tools, _file_system, _last_browser_type, _last_headless
+    global _browser_session, _browser_tools, _file_system, _last_browser_type, _last_headless, _live_url
 
     if _browser_session is not None:
         return _browser_session, _browser_tools
@@ -304,12 +305,28 @@ async def init_browser_session(
 
     print(f"✅ 浏览器会话初始化成功 | 默认下载路径: {save_dir}")
 
+    # 云浏览器:捕获 live URL
+    if browser_type == "cloud":
+        import urllib.parse
+        cdp_url = getattr(_browser_session, 'cdp_url', '') or ''
+        if 'browser-use.com' in cdp_url:
+            # 从 cdp_url (wss://xxx.cdp1.browser-use.com/...) 提取主机名,用 https:// 拼接
+            parsed = urllib.parse.urlparse(cdp_url)
+            host_url = f"https://{parsed.hostname}"
+            _live_url = f"https://live.browser-use.com?wss={urllib.parse.quote(host_url)}"
+            print(f"📡 实时画面链接: {_live_url}")
+
     if browser_type in ["local", "cloud"] and url:
         await _browser_tools.navigate(url=url, browser_session=_browser_session)
 
     return _browser_session, _browser_tools
 
 
+def get_browser_live_url() -> Optional[str]:
+    """获取云浏览器的实时画面链接"""
+    return _live_url
+
+
 async def get_browser_session() -> tuple[BrowserSession, Tools]:
     """
     获取当前浏览器会话,如果不存在或连接已断开则自动重新创建
@@ -531,6 +548,25 @@ def _fetch_profile_id(cookie_type: str) -> Optional[str]:
 # 导航类工具 (Navigation Tools)
 # ============================================================
 
+@tool()
+async def browser_get_live_url() -> ToolResult:
+    """
+    获取云浏览器的实时画面链接(Live URL),可用于在本地浏览器中查看或分享给他人操作。
+    仅在云浏览器模式下有效,本地浏览器返回空。
+    """
+    url = get_browser_live_url()
+    if url:
+        return ToolResult(
+            title="云浏览器实时画面链接",
+            output=url,
+            metadata={"live_url": url}
+        )
+    return ToolResult(
+        title="无可用链接",
+        output="当前未使用云浏览器,或浏览器尚未初始化",
+    )
+
+
 @tool()
 async def browser_navigate_to_url(url: str, new_tab: bool = False) -> ToolResult:
     """
@@ -1144,7 +1180,7 @@ async def browser_screenshot() -> ToolResult:
     try:
         browser, tools = await get_browser_session()
 
-        result = await tools.screenshot(browser_session=browser)
+        result = await tools.screenshot(browser_session=browser, file_system=_file_system)
 
         return action_result_to_tool_result(result, "截图请求")
 

+ 5 - 0
agent/tools/builtin/feishu/chat_history/chat_summary.json

@@ -0,0 +1,5 @@
+{
+  "关涛": 0,
+  "谭景玉": 0,
+  "孙若天": 0
+}

+ 60 - 0
agent/tools/builtin/feishu/chat_history/chat_关涛.json

@@ -0,0 +1,60 @@
+[
+  {
+    "role": "assistant",
+    "message_id": "om_x100b55dc966810acc4cc4e7e2d5622a",
+    "content": [
+      {
+        "type": "text",
+        "text": "你好,我需要你帮忙登录一下小红书。\n\n请点击下面的链接,在浏览器中完成登录操作:\nhttps://live.browser-use.com?wss=wss%3A//ccc57863-bba1-457f-80eb-219046165133.cdp1.browser-use.com/devtools/browser/d451a1dc-4476-4084-bc4f-19244c946193\n\n登录完成后请告诉我一声,谢谢!"
+      }
+    ]
+  },
+  {
+    "role": "assistant",
+    "message_id": "om_x100b55dd5d9c00b4c3beef370384365",
+    "content": [
+      {
+        "type": "text",
+        "text": "你好,我在尝试访问小红书搜索摄影主题时遇到了登录问题。之前加载的Cookie似乎已经失效,系统一直弹出登录窗口要求重新登录。需要你帮忙处理一下小红书的登录认证问题。"
+      }
+    ]
+  },
+  {
+    "role": "assistant",
+    "message_id": "om_x100b55ddc713b4a4c3d127a29f88495",
+    "content": [
+      {
+        "type": "text",
+        "text": "你好关涛,我在执行小红书搜索摄影主题的任务时遇到了问题。虽然我已经加载了cookie,但是在搜索\"摄影\"后,页面仍然弹出登录窗口要求登录才能查看搜索结果。请问有什么解决方案吗?"
+      }
+    ]
+  },
+  {
+    "role": "assistant",
+    "message_id": "om_x100b55ddc563b8a4c4cef0e822739bd",
+    "content": [
+      {
+        "type": "text",
+        "text": "好的,这是浏览器的live url:https://live.browser-use.com?wss=https%3A//5c550871-7d57-4c04-a4fe-585fd7a346db.cdp5.browser-use.com\n\n你可以通过这个链接查看和操作浏览器。"
+      }
+    ]
+  },
+  {
+    "role": "assistant",
+    "message_id": "om_x100b55dded5da8a0c35c56f0dd0d78a",
+    "content": [
+      {
+        "type": "text",
+        "text": "你好,我需要访问小红书,但需要扫码登录。请帮我在以下链接中完成登录操作:\n\n"
+      },
+      {
+        "type": "text",
+        "text": "https://live.browser-use.com?wss=https%3A//fc0dfb3f-7854-41ef-8579-3aa3d86e3b02.cdp4.browser-use.com"
+      },
+      {
+        "type": "text",
+        "text": "\n\n登录完成后请回复我,谢谢!"
+      }
+    ]
+  }
+]

+ 12 - 0
agent/tools/builtin/feishu/chat_history/chat_孙若天.json

@@ -0,0 +1,12 @@
+[
+  {
+    "role": "assistant",
+    "message_id": "om_x100b55ddfb31b898c3a9ee232316f13",
+    "content": [
+      {
+        "type": "text",
+        "text": "你好,我需要访问小红书,但是需要登录。请帮我在这个浏览器中完成登录:\n\nhttps://live.browser-use.com?wss=https%3A//9dcf552b-18ea-4f7f-9ace-96ca6fc9a3ac.cdp3.browser-use.com\n\n登录完成后请回复我一下,谢谢!"
+      }
+    ]
+  }
+]

+ 12 - 0
agent/tools/builtin/feishu/chat_history/chat_谭景玉.json

@@ -0,0 +1,12 @@
+[
+  {
+    "role": "assistant",
+    "message_id": "om_x100b55dca36b98acc14eba0a6709aa0",
+    "content": [
+      {
+        "type": "text",
+        "text": "你好!我需要登录小红书来完成搜索摄影主题的任务,但是没有找到保存的cookie。\n\n请点击以下链接在浏览器中完成小红书登录:\nhttps://live.browser-use.com?wss=wss%3A//4599a061-1830-4cb0-99fc-fffb5503e99a.cdp1.browser-use.com/devtools/browser/f77323a4-3759-4558-85e0-f4eb3eb04368\n\n登录完成后请告诉我,我会保存登录状态。谢谢!"
+      }
+    ]
+  }
+]

+ 28 - 26
config/feishu_contacts.json

@@ -1,28 +1,30 @@
 [
-    {
-        "name": "谭景玉",
-        "description": "",
-        "open_id": "ou_11fdbd559cc6513ab53ff06d6c63413d",
-        "chat_id": "oc_56e85f0e2c97405d176729b62d8f56e5"
-    },
-    {
-        "name": "王华东",
-        "description": "",
-        "open_id": "ou_82340312cf9d215f49a41b67fa9c02c2"
-    },
-    {
-        "name": "孙若天",
-        "description": "",
-        "open_id": "ou_ede69f28c2617bf80a7574f059879c8d"
-    },
-    {
-        "name": "刘斌",
-        "description": "",
-        "open_id": "ou_50c2307c3531e6293b3d5533d14592e9"
-    },
-    {
-        "name": "关涛",
-        "description": "",
-        "open_id": "ou_90b80ed994fe41b7f038a63cb9182f72"
-    }
+  {
+    "name": "谭景玉",
+    "description": "",
+    "open_id": "ou_11fdbd559cc6513ab53ff06d6c63413d",
+    "chat_id": "oc_56e85f0e2c97405d176729b62d8f56e5"
+  },
+  {
+    "name": "王华东",
+    "description": "",
+    "open_id": "ou_82340312cf9d215f49a41b67fa9c02c2"
+  },
+  {
+    "name": "孙若天",
+    "description": "",
+    "open_id": "ou_ede69f28c2617bf80a7574f059879c8d",
+    "chat_id": "oc_98019f9a0419b46a215ca604b04c5cc6"
+  },
+  {
+    "name": "刘斌",
+    "description": "",
+    "open_id": "ou_50c2307c3531e6293b3d5533d14592e9"
+  },
+  {
+    "name": "关涛",
+    "description": "",
+    "open_id": "ou_90b80ed994fe41b7f038a63cb9182f72",
+    "chat_id": "oc_ac9633d2c61f43b5049c425305482491"
+  }
 ]

+ 55 - 0
examples/loggin/config.py

@@ -0,0 +1,55 @@
+"""
+项目配置
+
+定义项目的运行配置。
+"""
+
+from agent.core.runner import KnowledgeConfig, RunConfig
+
+
+# ===== Agent 运行配置 =====
+
+RUN_CONFIG = RunConfig(
+    # 模型配置
+    model="claude-sonnet-4.5",
+    temperature=0.3,
+    max_iterations=1000,
+
+    # 任务名称
+    name="Research Agent",
+
+    # 知识管理配置
+    knowledge=KnowledgeConfig(
+        # 压缩时提取(消息量超阈值触发压缩时,用完整 history 反思)
+        enable_extraction=True,
+        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=True,
+
+        # 默认字段(保存/搜索时自动注入)
+        owner="",  # 所有者(空则尝试从 git config user.email 获取,再空则用 agent:{agent_id})
+        default_tags={"project": "research", "domain": "ai_agent"},  # 默认 tags(会与工具调用参数合并)
+        default_scopes=["org:cybertogether"],  # 默认 scopes
+        default_search_types=["strategy", "tool"],  # 默认搜索类型过滤
+        default_search_owner=""  # 默认搜索 owner 过滤(空则不过滤)
+    )
+)
+
+
+# ===== 基础设施配置 =====
+
+SKILLS_DIR = "./skills"
+TRACE_STORE_PATH = ".trace"
+DEBUG = True
+LOG_LEVEL = "INFO"
+LOG_FILE = None  # 设置为文件路径可以同时输出到文件
+
+# ===== 浏览器配置 =====
+# 可选值: "cloud" (云浏览器) 或 "local" (本地浏览器)
+BROWSER_TYPE = "cloud"
+HEADLESS = True

+ 10 - 0
examples/loggin/research.prompt

@@ -0,0 +1,10 @@
+---
+model: sonnet-4.6
+temperature: 0.3
+---
+
+$system$
+你是最顶尖的AI助手,可以拆分并调用工具逐步解决复杂问题。
+
+$user$
+请打开浏览器,去小红书搜索一下摄影的主题,打开一个摄影博主的主页,查看他的最近10条内容。遇到问题时,可以feishu请求孙若天。

+ 358 - 0
examples/loggin/run.py

@@ -0,0 +1,358 @@
+"""
+示例(简化版 - 使用框架交互功能)
+
+使用 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.core.presets import AgentPreset, register_preset
+from agent.trace import (
+    FileSystemTraceStore,
+    Trace,
+    Message,
+)
+from agent.llm import create_openrouter_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
+
+# 导入项目配置
+from config import RUN_CONFIG, SKILLS_DIR, TRACE_STORE_PATH, DEBUG, LOG_LEVEL, LOG_FILE, BROWSER_TYPE, HEADLESS
+
+
+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 / "research.prompt"
+    output_dir = base_dir / "output_1"
+    output_dir.mkdir(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():
+        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())}")
+
+    # 3. 加载 prompt
+    print("3. 加载 prompt...")
+    prompt = SimplePrompt(prompt_path)
+
+    # 4. 构建任务消息
+    print("4. 构建任务消息...")
+    messages = prompt.build_messages()
+
+    # 5. 初始化浏览器
+    import platform
+    actual_browser_type = BROWSER_TYPE
+    if platform.system() == "Windows" and BROWSER_TYPE == "local":
+        actual_browser_type = "cloud"
+        print("⚠️ Windows 平台检测到本地浏览器配置,自动切换为云浏览器模式")
+
+    browser_mode_name = "云浏览器" if actual_browser_type == "cloud" else "本地浏览器"
+    print(f"5. 正在初始化{browser_mode_name}...")
+    await init_browser_session(
+        browser_type=actual_browser_type,
+        headless=HEADLESS,
+        url="about:blank"
+    )
+    print(f"   ✅ {browser_mode_name}初始化完成\n")
+
+    # 6. 创建 Agent Runner
+    print("6. 创建 Agent Runner...")
+    print(f"   - Skills 目录: {SKILLS_DIR}")
+    print(f"   - 模型: {RUN_CONFIG.model}")
+
+    store = FileSystemTraceStore(base_path=TRACE_STORE_PATH)
+    runner = AgentRunner(
+        trace_store=store,
+        llm_call=create_openrouter_llm_call(model=f"anthropic/{RUN_CONFIG.model}"),
+        skills_dir=SKILLS_DIR,
+        debug=DEBUG
+    )
+
+    # 7. 创建交互控制器
+    interactive = InteractiveController(
+        runner=runner,
+        store=store,
+        enable_stdin_check=True
+    )
+
+    # 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}")
+    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 已完成/失败且没有新消息,进入交互菜单
+            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 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
+
+                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())