diff --git a/docs/feishu_development_guide.md b/docs/feishu_development_guide.md
index d7185a4..297c06e 100644
--- a/docs/feishu_development_guide.md
+++ b/docs/feishu_development_guide.md
@@ -37,7 +37,7 @@ Agent/
│ └─ main_agent.py # 主 Agent 装配入口
├─ core/
│ ├─ runtime.py # Agent 执行、Plan 确认、人工交互续跑
- │ ├─ graph_builder.py # LangGraph ReAct 图构建
+ │ ├─ graph.py # LangGraph ReAct 图构建
│ ├─ nodes.py # LLM 节点与工具产物处理
│ ├─ routing.py # 工具调用路由/审批识别
│ ├─ plan.py # Plan 模式路径选择 pending action
diff --git a/langgraph.json b/langgraph.json
deleted file mode 100644
index d6cc441..0000000
--- a/langgraph.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "dependencies": [
- "."
- ],
- "graphs": {
- "agent": "./src/agent/agent.py:agent"
- },
- "env": ".env"
-}
\ No newline at end of file
diff --git a/src/__pycache__/__init__.cpython-312.pyc b/src/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 72175c5..0000000
Binary files a/src/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/src/__pycache__/__init__.cpython-313.pyc b/src/__pycache__/__init__.cpython-313.pyc
deleted file mode 100644
index 8743c99..0000000
Binary files a/src/__pycache__/__init__.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/__init__.py b/src/agent/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/src/agent/__pycache__/__init__.cpython-312.pyc b/src/agent/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 54b9c69..0000000
Binary files a/src/agent/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/src/agent/__pycache__/__init__.cpython-313.pyc b/src/agent/__pycache__/__init__.cpython-313.pyc
deleted file mode 100644
index 3d30ff6..0000000
Binary files a/src/agent/__pycache__/__init__.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/__pycache__/chatbot_agent.cpython-312.pyc b/src/agent/__pycache__/chatbot_agent.cpython-312.pyc
index ac61aeb..f5b236c 100644
Binary files a/src/agent/__pycache__/chatbot_agent.cpython-312.pyc and b/src/agent/__pycache__/chatbot_agent.cpython-312.pyc differ
diff --git a/src/agent/__pycache__/chatbot_agent.cpython-313.pyc b/src/agent/__pycache__/chatbot_agent.cpython-313.pyc
deleted file mode 100644
index a7bc611..0000000
Binary files a/src/agent/__pycache__/chatbot_agent.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/__pycache__/llm.cpython-312.pyc b/src/agent/__pycache__/llm.cpython-312.pyc
deleted file mode 100644
index 4d2f8a0..0000000
Binary files a/src/agent/__pycache__/llm.cpython-312.pyc and /dev/null differ
diff --git a/src/agent/__pycache__/llm.cpython-313.pyc b/src/agent/__pycache__/llm.cpython-313.pyc
deleted file mode 100644
index 63cd085..0000000
Binary files a/src/agent/__pycache__/llm.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/agents/__init__.py b/src/agent/agents/__init__.py
index 0f3c597..e5d1194 100644
--- a/src/agent/agents/__init__.py
+++ b/src/agent/agents/__init__.py
@@ -2,18 +2,24 @@
系统定位:
对外导出各 Agent 的 ``AgentRuntime`` 单例及多 Agent 运行时。
+ 所有 Agent 实例均从 ``agent_config.json`` 动态构建,不再 hardcoded。
Agent 列表:
- - main_agent_runtime: 主 Agent 运行时(单 Agent 模式)。
+ - main_agent_runtime: 主 Agent 运行时(从配置中 role="main" 的 agent 动态创建)。
+ - reload_all_agent_runtimes: 重新加载配置并重建所有 Agent 运行时。
- build_multi_agent_runtime: 构建一主多子 Agent 运行时。
- get_agent_graph_snapshot: 获取多 Agent 图状态快照。
可扩展性:
- 子 Agent 实现后在此追加导出,如::
- from agent.agents.research_agent import research_agent_runtime
- __all__ = ["main_agent_runtime", "research_agent_runtime"]
+ 新增 Agent 只需在 agent_config.json 中添加配置项,无需编写 .py 文件。
"""
-from agent.agents.main_agent import main_agent_runtime, reload_main_agent_runtime
+from agent.agents.agent import (
+ build_all_agent_runtimes,
+ get_main_agent_runtime,
+ get_all_agent_runtimes,
+ get_agent_runtime,
+ reload_all_agent_runtimes,
+)
from agent.agents.multi_agent_runtime import (
MultiAgentRuntime,
SubAgentConfig,
@@ -21,9 +27,19 @@ from agent.agents.multi_agent_runtime import (
get_agent_graph_snapshot,
)
+# 首次导入时从配置构建,并暴露为模块级属性(向后兼容旧导入路径)
+_built = build_all_agent_runtimes()
+main_agent_runtime = get_main_agent_runtime()
+reload_main_agent_runtime = reload_all_agent_runtimes # 向后兼容别名
+
__all__ = [
"main_agent_runtime",
"reload_main_agent_runtime",
+ "reload_all_agent_runtimes",
+ "build_all_agent_runtimes",
+ "get_main_agent_runtime",
+ "get_all_agent_runtimes",
+ "get_agent_runtime",
"MultiAgentRuntime",
"SubAgentConfig",
"build_multi_agent_runtime",
diff --git a/src/agent/agents/__pycache__/__init__.cpython-312.pyc b/src/agent/agents/__pycache__/__init__.cpython-312.pyc
index 3d664d3..08cf924 100644
Binary files a/src/agent/agents/__pycache__/__init__.cpython-312.pyc and b/src/agent/agents/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/agent/agents/__pycache__/__init__.cpython-313.pyc b/src/agent/agents/__pycache__/__init__.cpython-313.pyc
deleted file mode 100644
index 57814d3..0000000
Binary files a/src/agent/agents/__pycache__/__init__.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/agents/__pycache__/agent.cpython-312.pyc b/src/agent/agents/__pycache__/agent.cpython-312.pyc
new file mode 100644
index 0000000..9f67f3e
Binary files /dev/null and b/src/agent/agents/__pycache__/agent.cpython-312.pyc differ
diff --git a/src/agent/agents/__pycache__/browser_agent.cpython-312.pyc b/src/agent/agents/__pycache__/browser_agent.cpython-312.pyc
deleted file mode 100644
index 3591b5e..0000000
Binary files a/src/agent/agents/__pycache__/browser_agent.cpython-312.pyc and /dev/null differ
diff --git a/src/agent/agents/__pycache__/browser_agent.cpython-313.pyc b/src/agent/agents/__pycache__/browser_agent.cpython-313.pyc
deleted file mode 100644
index 2c3181a..0000000
Binary files a/src/agent/agents/__pycache__/browser_agent.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/agents/__pycache__/code_agent.cpython-312.pyc b/src/agent/agents/__pycache__/code_agent.cpython-312.pyc
deleted file mode 100644
index 7518ff6..0000000
Binary files a/src/agent/agents/__pycache__/code_agent.cpython-312.pyc and /dev/null differ
diff --git a/src/agent/agents/__pycache__/code_agent.cpython-313.pyc b/src/agent/agents/__pycache__/code_agent.cpython-313.pyc
deleted file mode 100644
index 304a822..0000000
Binary files a/src/agent/agents/__pycache__/code_agent.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/agents/__pycache__/main_agent.cpython-312.pyc b/src/agent/agents/__pycache__/main_agent.cpython-312.pyc
deleted file mode 100644
index b976594..0000000
Binary files a/src/agent/agents/__pycache__/main_agent.cpython-312.pyc and /dev/null differ
diff --git a/src/agent/agents/__pycache__/main_agent.cpython-313.pyc b/src/agent/agents/__pycache__/main_agent.cpython-313.pyc
deleted file mode 100644
index c784715..0000000
Binary files a/src/agent/agents/__pycache__/main_agent.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/agents/__pycache__/multi_agent_runtime.cpython-312.pyc b/src/agent/agents/__pycache__/multi_agent_runtime.cpython-312.pyc
index 6ef8c03..e3d9460 100644
Binary files a/src/agent/agents/__pycache__/multi_agent_runtime.cpython-312.pyc and b/src/agent/agents/__pycache__/multi_agent_runtime.cpython-312.pyc differ
diff --git a/src/agent/agents/__pycache__/research_agent.cpython-312.pyc b/src/agent/agents/__pycache__/research_agent.cpython-312.pyc
deleted file mode 100644
index 63ddb09..0000000
Binary files a/src/agent/agents/__pycache__/research_agent.cpython-312.pyc and /dev/null differ
diff --git a/src/agent/agents/__pycache__/research_agent.cpython-313.pyc b/src/agent/agents/__pycache__/research_agent.cpython-313.pyc
deleted file mode 100644
index de0c8c0..0000000
Binary files a/src/agent/agents/__pycache__/research_agent.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/agents/agent.py b/src/agent/agents/agent.py
new file mode 100644
index 0000000..80cabe2
--- /dev/null
+++ b/src/agent/agents/agent.py
@@ -0,0 +1,193 @@
+"""动态 Agent 工厂:根据 agent_config.json 全动态创建 AgentRuntime。
+
+系统定位:
+ 替代原有的 hardcoded main_agent / code_agent / research_agent 等单文件,
+ 实现从配置文件全动态加载 Agent 实例。所有 Agent 的运行参数(图类型、
+ 模型、工具集、系统提示词、最大迭代次数)均由 agent_config.json 驱动。
+
+使用方式:
+ from agent.agents.agent import get_main_agent_runtime, reload_all_agent_runtimes
+ runtime = get_main_agent_runtime()
+"""
+from __future__ import annotations
+
+from agent.core.config_manager import load_agent_configs, load_models, AgentConfig
+from agent.core.graph import build_graph_by_name
+from agent.core.runtime import AgentRuntime
+from agent.tools import get_tools
+
+
+# ---------- 缓存 ----------
+_all_agent_runtimes: dict[str, AgentRuntime] = {}
+_main_agent_name: str = "main"
+
+
+def _build_llm_for_agent(cfg: AgentConfig):
+ """根据 Agent 配置中指定的 llm_model_id 构建 LLM 实例。
+
+ 从 env_config.json 的 models 列表中查找匹配的模型配置,
+ 用其参数构造 ChatQwen 实例。若未找到匹配模型则回退到环境变量默认值。
+ """
+ from agent.core.llm import ChatQwen
+
+ models = load_models()
+ model_config = None
+ for m in models:
+ if m.get("id") == cfg.llm_model_id:
+ model_config = m
+ break
+
+ if model_config:
+ return ChatQwen(
+ model=model_config.get("model", ""),
+ api_key=model_config.get("api_key", ""),
+ base_url=model_config.get("base_url", ""),
+ timeout=float(model_config.get("timeout", 60)),
+ max_retries=int(model_config.get("max_retries", 0)),
+ )
+ # 回退:使用环境变量中已加载的默认 LLM 配置
+ from agent.core.llm import llm as _default_llm
+ return ChatQwen(
+ model=_default_llm.model_name,
+ api_key=_default_llm.openai_api_key or "",
+ base_url=_default_llm.openai_api_base or "",
+ )
+
+
+def _build_agent_runtime(cfg: AgentConfig) -> AgentRuntime:
+ """根据单个 AgentConfig 构建 AgentRuntime 实例。
+
+ 输入:
+ cfg: Agent 配置(name/graph/tools/llm_model_id/system_prompt/max_iterations)。
+
+ 输出:
+ 完整的 AgentRuntime 实例,可被 execute_agent 直接使用。
+ """
+ # 1. 构建 LLM
+ agent_llm = _build_llm_for_agent(cfg)
+
+ # 2. 获取所有可用工具并按配置过滤
+ all_tools = get_tools()
+ if cfg.tools:
+ agent_tools = [t for t in all_tools if t.name in cfg.tools]
+ else:
+ agent_tools = all_tools
+
+ # 3. bind_tools
+ model_with_tools = agent_llm.bind_tools(agent_tools, tool_choice="auto")
+
+ # 4. 根据 graph 配置动态构建图
+ graph = build_graph_by_name(
+ graph_name=cfg.graph,
+ tools=agent_tools,
+ model_with_tools=model_with_tools,
+ trajectory_rounds=cfg.trajectory_rounds,
+ parallel_tools=cfg.parallel_tools,
+ )
+
+ # 5. 组装 AgentRuntime
+ return AgentRuntime(
+ name=cfg.name,
+ graph=graph,
+ llm=agent_llm,
+ tools=agent_tools,
+ tools_by_name={tool.name: tool for tool in agent_tools},
+ system_prompt=cfg.system_prompt,
+ max_iterations=cfg.max_iterations,
+ trajectory_rounds=cfg.trajectory_rounds,
+ )
+
+
+def build_all_agent_runtimes() -> dict[str, AgentRuntime]:
+ """从 agent_config.json 读取所有启用 Agent 并构建运行时字典。
+
+ 输出:
+ {agent_name: AgentRuntime} 字典,其中第一个 role="main" 的为 main_agent。
+
+ 系统定位:
+ 启动时调用一次,构建全局运行时缓存。
+ """
+ global _all_agent_runtimes, _main_agent_name
+ configs = load_agent_configs()
+ _all_agent_runtimes = {}
+ _main_agent_name = "main"
+
+ for cfg in configs:
+ if not cfg.enabled:
+ continue
+ try:
+ runtime = _build_agent_runtime(cfg)
+ _all_agent_runtimes[cfg.name] = runtime
+ if cfg.role == "main":
+ _main_agent_name = cfg.name
+ except Exception as e:
+ print(f"[AgentFactory] 构建 Agent '{cfg.name}' 失败: {e}")
+
+ # 确保至少有一个 main agent
+ if _main_agent_name not in _all_agent_runtimes and _all_agent_runtimes:
+ first_name = next(iter(_all_agent_runtimes))
+ _main_agent_name = first_name
+ print(f"[AgentFactory] 未找到 role='main' 的 Agent,自动将 '{first_name}' 设为主 Agent。")
+
+ return _all_agent_runtimes
+
+
+def get_main_agent_runtime() -> AgentRuntime | None:
+ """获取当前配置中 role="main" 的 AgentRuntime。
+
+ 若运行时缓存为空则自动构建。
+ """
+ global _all_agent_runtimes
+ if not _all_agent_runtimes:
+ build_all_agent_runtimes()
+ return _all_agent_runtimes.get(_main_agent_name)
+
+
+def get_all_agent_runtimes() -> dict[str, AgentRuntime]:
+ """获取所有已构建的 Agent 运行时字典。"""
+ if not _all_agent_runtimes:
+ build_all_agent_runtimes()
+ return dict(_all_agent_runtimes)
+
+
+def get_agent_runtime(name: str) -> AgentRuntime | None:
+ """按名称获取指定 Agent 运行时。"""
+ if not _all_agent_runtimes:
+ build_all_agent_runtimes()
+ return _all_agent_runtimes.get(name)
+
+
+def reload_all_agent_runtimes() -> dict:
+ """重新加载配置并重建所有 Agent 运行时,返回当前状态。
+
+ 输出:
+ {"agents": [{"name": str, "tools_count": int, "graph": str, "max_iterations": int}, ...],
+ "main_agent": str}
+ """
+ global _all_agent_runtimes, _main_agent_name
+ _all_agent_runtimes.clear()
+ build_all_agent_runtimes()
+
+ # 同步更新 chatbot_agent 中的缓存引用
+ import agent.chatbot_agent as ca
+ main_rt = _all_agent_runtimes.get(_main_agent_name)
+ if main_rt:
+ ca.main_agent_runtime = main_rt
+ ca.agent_graph = main_rt.graph
+ ca.tools = main_rt.tools
+ ca.TOOLS_BY_NAME = main_rt.tools_by_name
+ ca.llm = main_rt.llm
+
+ agents_info = []
+ for name, rt in _all_agent_runtimes.items():
+ agents_info.append({
+ "name": name,
+ "tools_count": len(rt.tools),
+ "graph": rt.graph.__class__.__name__ if rt.graph else "unknown",
+ "max_iterations": rt.max_iterations,
+ })
+
+ return {
+ "agents": agents_info,
+ "main_agent": _main_agent_name,
+ }
diff --git a/src/agent/agents/code_agent.py b/src/agent/agents/code_agent.py
deleted file mode 100644
index 2c65d72..0000000
--- a/src/agent/agents/code_agent.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""代码子 Agent(预留)。
-
-系统定位:
- 计划用于代码生成、静态分析、仓库操作等,可绑定代码执行沙箱工具
- 与目录读写类工具。
-
-可扩展性:
- 可实现独立 AgentRuntime,或在 team 模式下由 main Agent 委派任务;
- 建议对执行类工具保持 confirm 策略。
-"""
diff --git a/src/agent/agents/doc_agent.py b/src/agent/agents/doc_agent.py
deleted file mode 100644
index e69de29..0000000
diff --git a/src/agent/agents/gui_agent.py b/src/agent/agents/gui_agent.py
deleted file mode 100644
index 032ad16..0000000
--- a/src/agent/agents/gui_agent.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""GUI子 Agent(预留)。
-
-系统定位:
- 计划用于GUI自动化、页面理解与操作编排,工具以 gui_control
- 及 screen_capture 为主。
-
-可扩展性:
- 实现时可单独配置 tool_policy 白名单、更严格的 confirm 策略,
- 并绑定专用 system_prompt 与较短上下文窗口。
-"""
-
diff --git a/src/agent/agents/main_agent.py b/src/agent/agents/main_agent.py
deleted file mode 100644
index e5a3dca..0000000
--- a/src/agent/agents/main_agent.py
+++ /dev/null
@@ -1,77 +0,0 @@
-"""主 Agent 装配入口:工具、模型、图与运行时单例。
-
-系统定位:
- 项目默认 Agent 实例 ``main_agent_runtime``,由 Web 层导入并调用
- ``core.runtime.execute_agent`` / ``continue_after_human_action``。
-
-装配内容:
- - get_tools(): 主 Agent 可用工具集
- - llm.bind_tools: 工具调用能力
- - build_agent_graph: 标准 ReAct 图
- - MAIN_SYSTEM_PROMPT: 系统提示词
-
-可扩展性:
- - 可按环境变量切换工具子集或模型。
- - 子 Agent(research/browser/code)可仿照本文件独立装配 AgentRuntime。
-"""
-from __future__ import annotations
-
-from agent.core.graph_builder import build_agent_graph
-from agent.core.llm import llm
-from agent.core.runtime import AgentRuntime
-from agent.memory.system_prompt import MAIN_SYSTEM_PROMPT
-from agent.tools import get_tools
-
-
-def _build_main_agent_runtime():
- """根据当前配置重新构建主 Agent 运行时。"""
- from agent.core.llm import llm as _llm
- _system_prompt = MAIN_SYSTEM_PROMPT
- _max_iterations = 200
- _tools = get_tools()
- try:
- from agent.core.config_manager import load_agent_configs
- configs = load_agent_configs()
- for cfg in configs:
- if cfg.name == "main" and cfg.enabled:
- if cfg.system_prompt:
- _system_prompt = cfg.system_prompt
- _max_iterations = cfg.max_iterations
- # 根据 agent_config.json 中的 tools 列表过滤工具
- if cfg.tools:
- _tools = [t for t in _tools if t.name in cfg.tools]
- break
- except Exception:
- pass
-
- model_with_tools = _llm.bind_tools(_tools, tool_choice="auto")
- return AgentRuntime(
- name="main",
- graph=build_agent_graph(_tools, model_with_tools),
- llm=_llm,
- tools=_tools,
- tools_by_name={tool.name: tool for tool in _tools},
- system_prompt=_system_prompt,
- max_iterations=_max_iterations,
- )
-
-
-main_agent_runtime = _build_main_agent_runtime()
-
-
-def reload_main_agent_runtime() -> dict:
- """重新加载配置并重建主 Agent 运行时,返回当前状态供调试。"""
- global main_agent_runtime
- import agent.chatbot_agent as ca
- main_agent_runtime = _build_main_agent_runtime()
- # 更新 chatbot_agent 中缓存的引用
- ca.main_agent_runtime = main_agent_runtime
- ca.agent_graph = main_agent_runtime.graph
- ca.tools = main_agent_runtime.tools
- ca.TOOLS_BY_NAME = main_agent_runtime.tools_by_name
- ca.llm = main_agent_runtime.llm
- return {
- "name": main_agent_runtime.name,
- "tools_count": len(main_agent_runtime.tools),
- "max_iterations": main_agent_runtime.max_iterations,
- }
diff --git a/src/agent/agents/multi_agent_runtime.py b/src/agent/agents/multi_agent_runtime.py
index ef9b7e5..8f16ffa 100644
--- a/src/agent/agents/multi_agent_runtime.py
+++ b/src/agent/agents/multi_agent_runtime.py
@@ -25,7 +25,7 @@ from agent.core.multi_agent_state import (
ToolCallEvent,
init_multi_agent_state,
)
-from agent.tools.tool_agent_call import AgentCallTool
+from agent.tools.tool_agent_call import CallSubAgentTool
@dataclass
@@ -92,9 +92,9 @@ def build_multi_agent_runtime(
示例::
- from agent.agents.main_agent import main_agent_runtime
+ from agent.agents.agent import get_main_agent_runtime
runtime = build_multi_agent_runtime(
- main_runtime=main_agent_runtime,
+ main_runtime=get_main_agent_runtime(),
sub_runtimes=[
{"runtime": code_agent_runtime, "icon": "💻"},
{"runtime": research_agent_runtime, "icon": "🔍"},
@@ -104,9 +104,9 @@ def build_multi_agent_runtime(
sub_runtimes = sub_runtimes or []
# 组装主 agent 工具(含 agent_call)
- main_tools = list(main_runtime.tools) + [AgentCallTool()]
+ main_tools = list(main_runtime.tools) + [CallSubAgentTool()]
main_tools_by_name = dict(main_runtime.tools_by_name)
- main_tools_by_name["agent_call"] = AgentCallTool()
+ main_tools_by_name["agent_call"] = CallSubAgentTool()
main_model_with_tools = main_runtime.llm.bind_tools(main_tools, tool_choice="auto")
# 子 agent 配置列表
diff --git a/src/agent/agents/research_agent.py b/src/agent/agents/research_agent.py
deleted file mode 100644
index 76a7d8a..0000000
--- a/src/agent/agents/research_agent.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""研究子 Agent(预留)。
-
-系统定位:
- 计划用于深度检索、文献综述等专用任务,与 main Agent 通过 team 模式
- 或子图委派协作。
-
-可扩展性:
- 实现步骤建议:
- 1. 定义 RESEARCH_SYSTEM_PROMPT 与精简工具集(web_search、RAG 等)
- 2. build_agent_graph(tools, model) 或自定义图(含检索节点)
- 3. 导出 research_agent_runtime = AgentRuntime(...)
-"""
diff --git a/src/agent/chatbot_agent.py b/src/agent/chatbot_agent.py
index 5579c56..c785313 100644
--- a/src/agent/chatbot_agent.py
+++ b/src/agent/chatbot_agent.py
@@ -2,12 +2,24 @@ from __future__ import annotations
from typing import Any, Literal
-from agent.agents.main_agent import main_agent_runtime
+from agent.agents.agent import (
+ build_all_agent_runtimes,
+ get_main_agent_runtime,
+ reload_all_agent_runtimes,
+)
from agent.core.runtime import continue_after_human_action as continue_runtime_after_human_action
from agent.core.runtime import execute_agent as execute_runtime_agent
AgentMode = Literal["agent", "team"]
+# 首次导入时从配置构建主 Agent 运行时并缓存引用
+_built = build_all_agent_runtimes()
+main_agent_runtime = get_main_agent_runtime()
+agent_graph = main_agent_runtime.graph
+tools = main_agent_runtime.tools
+TOOLS_BY_NAME = main_agent_runtime.tools_by_name
+llm = main_agent_runtime.llm
+
def execute_agent(
chat_history: list[dict[str, Any]],
@@ -43,9 +55,3 @@ def continue_after_human_action(
instruction=instruction,
output_type=output_type,
)
-
-
-agent_graph = main_agent_runtime.graph
-tools = main_agent_runtime.tools
-TOOLS_BY_NAME = main_agent_runtime.tools_by_name
-llm = main_agent_runtime.llm
diff --git a/src/agent/core/__init__.py b/src/agent/core/__init__.py
index c2a4525..65bc090 100644
--- a/src/agent/core/__init__.py
+++ b/src/agent/core/__init__.py
@@ -1,12 +1,12 @@
"""Agent 核心运行时包。
包含:
- - state / graph_builder: LangGraph 状态与图拓扑
+ - state / graph: LangGraph 状态与图拓扑
- nodes / routing: 图节点与条件路由
- runtime: 对外执行入口 execute_agent / continue_after_human_action
- llm: ChatQwen 多模态网关
- - approvals / human_interaction / tool_policy: HITL 与安全策略
- - plan: Plan 模式 UI 构建
+ - approvals: HITL 待确认管理(含 human_interaction 参数规范化)
+ - tool_policy: 工具执行安全策略
系统定位:
Agent 业务逻辑中枢,``agents`` 包在此之上组装具体 Agent 实例。
diff --git a/src/agent/core/__pycache__/__init__.cpython-312.pyc b/src/agent/core/__pycache__/__init__.cpython-312.pyc
index eb135f9..fea417d 100644
Binary files a/src/agent/core/__pycache__/__init__.cpython-312.pyc and b/src/agent/core/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/agent/core/__pycache__/__init__.cpython-313.pyc b/src/agent/core/__pycache__/__init__.cpython-313.pyc
deleted file mode 100644
index a535309..0000000
Binary files a/src/agent/core/__pycache__/__init__.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/approvals.cpython-312.pyc b/src/agent/core/__pycache__/approvals.cpython-312.pyc
index 2cc2dab..2c67bb0 100644
Binary files a/src/agent/core/__pycache__/approvals.cpython-312.pyc and b/src/agent/core/__pycache__/approvals.cpython-312.pyc differ
diff --git a/src/agent/core/__pycache__/approvals.cpython-313.pyc b/src/agent/core/__pycache__/approvals.cpython-313.pyc
deleted file mode 100644
index a159393..0000000
Binary files a/src/agent/core/__pycache__/approvals.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/config_manager.cpython-312.pyc b/src/agent/core/__pycache__/config_manager.cpython-312.pyc
index 08873b5..7ffb088 100644
Binary files a/src/agent/core/__pycache__/config_manager.cpython-312.pyc and b/src/agent/core/__pycache__/config_manager.cpython-312.pyc differ
diff --git a/src/agent/core/__pycache__/graph.cpython-312.pyc b/src/agent/core/__pycache__/graph.cpython-312.pyc
new file mode 100644
index 0000000..16600d3
Binary files /dev/null and b/src/agent/core/__pycache__/graph.cpython-312.pyc differ
diff --git a/src/agent/core/__pycache__/graph_builder.cpython-312.pyc b/src/agent/core/__pycache__/graph_builder.cpython-312.pyc
deleted file mode 100644
index acff7f3..0000000
Binary files a/src/agent/core/__pycache__/graph_builder.cpython-312.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/graph_builder.cpython-313.pyc b/src/agent/core/__pycache__/graph_builder.cpython-313.pyc
deleted file mode 100644
index e0a80ee..0000000
Binary files a/src/agent/core/__pycache__/graph_builder.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/human_interaction.cpython-312.pyc b/src/agent/core/__pycache__/human_interaction.cpython-312.pyc
deleted file mode 100644
index c9b0bd3..0000000
Binary files a/src/agent/core/__pycache__/human_interaction.cpython-312.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/human_interaction.cpython-313.pyc b/src/agent/core/__pycache__/human_interaction.cpython-313.pyc
deleted file mode 100644
index e2ca262..0000000
Binary files a/src/agent/core/__pycache__/human_interaction.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/llm.cpython-312.pyc b/src/agent/core/__pycache__/llm.cpython-312.pyc
index 567cd61..f5d6d93 100644
Binary files a/src/agent/core/__pycache__/llm.cpython-312.pyc and b/src/agent/core/__pycache__/llm.cpython-312.pyc differ
diff --git a/src/agent/core/__pycache__/llm.cpython-313.pyc b/src/agent/core/__pycache__/llm.cpython-313.pyc
deleted file mode 100644
index f882e02..0000000
Binary files a/src/agent/core/__pycache__/llm.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/multi_agent_graph.cpython-312.pyc b/src/agent/core/__pycache__/multi_agent_graph.cpython-312.pyc
index d31a527..dbb0e09 100644
Binary files a/src/agent/core/__pycache__/multi_agent_graph.cpython-312.pyc and b/src/agent/core/__pycache__/multi_agent_graph.cpython-312.pyc differ
diff --git a/src/agent/core/__pycache__/multi_agent_state.cpython-312.pyc b/src/agent/core/__pycache__/multi_agent_state.cpython-312.pyc
index 8d56cbc..491215d 100644
Binary files a/src/agent/core/__pycache__/multi_agent_state.cpython-312.pyc and b/src/agent/core/__pycache__/multi_agent_state.cpython-312.pyc differ
diff --git a/src/agent/core/__pycache__/multi_agent_state.cpython-313.pyc b/src/agent/core/__pycache__/multi_agent_state.cpython-313.pyc
deleted file mode 100644
index 99b879c..0000000
Binary files a/src/agent/core/__pycache__/multi_agent_state.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/nodes.cpython-312.pyc b/src/agent/core/__pycache__/nodes.cpython-312.pyc
index 341ee25..df564b5 100644
Binary files a/src/agent/core/__pycache__/nodes.cpython-312.pyc and b/src/agent/core/__pycache__/nodes.cpython-312.pyc differ
diff --git a/src/agent/core/__pycache__/nodes.cpython-313.pyc b/src/agent/core/__pycache__/nodes.cpython-313.pyc
deleted file mode 100644
index 2443de7..0000000
Binary files a/src/agent/core/__pycache__/nodes.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/plan.cpython-312.pyc b/src/agent/core/__pycache__/plan.cpython-312.pyc
deleted file mode 100644
index e060a31..0000000
Binary files a/src/agent/core/__pycache__/plan.cpython-312.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/plan.cpython-313.pyc b/src/agent/core/__pycache__/plan.cpython-313.pyc
deleted file mode 100644
index eb601fb..0000000
Binary files a/src/agent/core/__pycache__/plan.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/routing.cpython-312.pyc b/src/agent/core/__pycache__/routing.cpython-312.pyc
index 7c64f49..d4cc34c 100644
Binary files a/src/agent/core/__pycache__/routing.cpython-312.pyc and b/src/agent/core/__pycache__/routing.cpython-312.pyc differ
diff --git a/src/agent/core/__pycache__/routing.cpython-313.pyc b/src/agent/core/__pycache__/routing.cpython-313.pyc
deleted file mode 100644
index 751a04d..0000000
Binary files a/src/agent/core/__pycache__/routing.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/runtime.cpython-312.pyc b/src/agent/core/__pycache__/runtime.cpython-312.pyc
index d2d342b..37b5d81 100644
Binary files a/src/agent/core/__pycache__/runtime.cpython-312.pyc and b/src/agent/core/__pycache__/runtime.cpython-312.pyc differ
diff --git a/src/agent/core/__pycache__/runtime.cpython-313.pyc b/src/agent/core/__pycache__/runtime.cpython-313.pyc
deleted file mode 100644
index c98d6d8..0000000
Binary files a/src/agent/core/__pycache__/runtime.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/state.cpython-312.pyc b/src/agent/core/__pycache__/state.cpython-312.pyc
index 13cb367..53edabd 100644
Binary files a/src/agent/core/__pycache__/state.cpython-312.pyc and b/src/agent/core/__pycache__/state.cpython-312.pyc differ
diff --git a/src/agent/core/__pycache__/state.cpython-313.pyc b/src/agent/core/__pycache__/state.cpython-313.pyc
deleted file mode 100644
index f387c17..0000000
Binary files a/src/agent/core/__pycache__/state.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/todo_manager.cpython-312.pyc b/src/agent/core/__pycache__/todo_manager.cpython-312.pyc
deleted file mode 100644
index 5814011..0000000
Binary files a/src/agent/core/__pycache__/todo_manager.cpython-312.pyc and /dev/null differ
diff --git a/src/agent/core/__pycache__/tool_policy.cpython-312.pyc b/src/agent/core/__pycache__/tool_policy.cpython-312.pyc
index eba5259..41a4322 100644
Binary files a/src/agent/core/__pycache__/tool_policy.cpython-312.pyc and b/src/agent/core/__pycache__/tool_policy.cpython-312.pyc differ
diff --git a/src/agent/core/__pycache__/tool_policy.cpython-313.pyc b/src/agent/core/__pycache__/tool_policy.cpython-313.pyc
deleted file mode 100644
index 08578dc..0000000
Binary files a/src/agent/core/__pycache__/tool_policy.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/core/agent_config.json b/src/agent/core/agent_config.json
index 910886d..b7be52c 100644
--- a/src/agent/core/agent_config.json
+++ b/src/agent/core/agent_config.json
@@ -1,20 +1,17 @@
[
{
"name": "main",
- "system_prompt": "你是一个多模态助手,能够直接理解用户上传的图片、语音等内容。\n 【关键行为准则】\n - 你的默认行为是直接回答用户问题,不要调用任何工具。\n - 只有当某个信息是你**绝对无法从当前对话或上传内容中获取**,且该信息必须通过外部工具才能获得时,才允许调用工具。\n - 对于任何图片/音频输入,请始终**直接**利用你的视觉/听觉能力回答,**绝对不要**调用截图、OCR、日期查询等无关工具。\n - 如果你不确定是否需要工具,选择**不调用**,直接给出你能给出的最佳答案。\n - 违反上述规则是一种严重错误。",
+ "system_prompt": "你是一个多模态助手,能够直接理解用户上传的图片、语音等内容。\n 【关键行为准则】\n - 你的默认行为是优先调用gui工具解决用户问题。\n - 只有当某个信息是你**绝对无法从当前对话或上传内容中获取**,且该信息必须通过外部工具才能获得时,才允许调用工具。\n - 对于任何图片/音频输入,请始终**直接**利用你的视觉/听觉能力回答,**绝对不要**调用截图、OCR、日期查询等无关工具。\n - 如果你不确定是否需要工具,选择**不调用**,直接给出你能给出的最佳答案。\n - 违反上述规则是一种严重错误。",
"max_iterations": 200,
"llm_model_id": "a44b226e",
"tools": [
- "todo_list",
"gui_tool",
- "directory_listing",
- "web_search_class",
- "date_query_class",
- "human_interaction",
- "software_control",
"browser_control"
],
"enabled": true,
- "role": "main"
+ "role": "main",
+ "graph": "react",
+ "trajectory_rounds": 3,
+ "parallel_tools": true
}
]
\ No newline at end of file
diff --git a/src/agent/core/approvals.py b/src/agent/core/approvals.py
index e94d348..9b667f8 100644
--- a/src/agent/core/approvals.py
+++ b/src/agent/core/approvals.py
@@ -1,8 +1,13 @@
-"""待确认操作(HITL)内存注册与 UI 元数据构建。
+"""待确认操作(HITL):内存注册、UI 元数据构建、参数规范化与结果生成。
系统定位:
- 维护进程内 ``PENDING_TOOL_APPROVALS`` 字典,在图因需确认工具而暂停时
- 保存上下文;``runtime.continue_after_human_action`` 消费并清理。
+ 维护进程内 ``PENDING_TOOL_APPROVALS`` 字典;在图因需确认工具而暂停时
+ 保存上下文并构建前端 UI 元数据;在用户操作后生成 ToolMessage 供图继续执行。
+
+内容分区:
+ - PENDING 存储与元数据构建(build_approval_meta / build_human_interaction_meta)
+ - UI 消息辅助(pending_assistant_message / remove_pending_ui_message)
+ - human_interaction 工具的参数规范化与结果文案(normalize / result)
注意:
当前为内存存储,进程重启后待确认项丢失;生产环境可改为 Redis/DB。
@@ -10,6 +15,7 @@
可扩展性:
- 增加 TTL、session 级清理、持久化后端。
- ``build_*_meta`` 可与前端组件协议版本化。
+ - 新增 interaction_type 时同步扩展 normalize / result 分支。
"""
from __future__ import annotations
@@ -20,12 +26,20 @@ from typing import Any, Sequence
from langchain_core.messages import BaseMessage
from langchain_core.messages import messages_to_dict
-from agent.core.human_interaction import normalize_human_interaction_args
+from agent.tools.tool import HumanInteraction
from agent.utils.tool_call_utils import json_safe
+# ============================================================
+# PENDING 存储
+# ============================================================
+
PENDING_TOOL_APPROVALS: dict[str, dict[str, Any]] = {}
+# ============================================================
+# UI 元数据构建
+# ============================================================
+
def build_approval_meta(session_id: str, tool_call: dict[str, Any], messages: Sequence[BaseMessage]) -> dict[str, Any]:
"""为需确认的普通工具调用构建 UI 元数据并注册 pending。
@@ -36,12 +50,6 @@ def build_approval_meta(session_id: str, tool_call: dict[str, Any], messages: Se
输出:
``{"pending_action": {...}}``,含 id、type=tool_call、args、options 等。
-
- 系统定位:
- ``routing.first_pending_tool_meta`` 对 confirm 类工具调用。
-
- 可扩展性:
- 可按工具名定制 title、风险提示、参数展示模板。
"""
approval_id = uuid.uuid4().hex
tool_name = tool_call.get("name") or "unknown_tool"
@@ -78,12 +86,6 @@ def build_human_interaction_meta(session_id: str, tool_call: dict[str, Any], mes
输出:
``{"pending_action": {...}}``,type=human_interaction,含 interaction_type 等。
-
- 系统定位:
- 模型主动请求用户补充信息/选择/审批时使用。
-
- 可扩展性:
- 可扩展表单 schema、多步向导式交互。
"""
approval_id = uuid.uuid4().hex
args = normalize_human_interaction_args(tool_call.get("args") or {})
@@ -120,21 +122,7 @@ def build_human_interaction_meta(session_id: str, tool_call: dict[str, Any], mes
def pending_assistant_message(meta: dict[str, Any], content: str = "需要你确认后继续执行。") -> dict[str, Any]:
- """构造带 pending_action 的 assistant 聊天消息 dict。
-
- 输入:
- meta: 通常含 pending_action 的字典。
- content: 展示给用户的提示文案。
-
- 输出:
- Web UI 使用的 ``{"role": "assistant", "content": ..., "meta": ...}``。
-
- 系统定位:
- ``runtime.execute_agent`` 检测到待确认时追加到 chat_history。
-
- 可扩展性:
- 可统一 output_type、国际化 content 模板。
- """
+ """构造带 pending_action 的 assistant 聊天消息 dict。"""
return {
"role": "assistant",
"content": content,
@@ -143,24 +131,60 @@ def pending_assistant_message(meta: dict[str, Any], content: str = "需要你确
def remove_pending_ui_message(chat_history: list[dict[str, Any]], approval_id: str) -> None:
- """在 chat_history 中将指定 approval_id 的 pending_action 标记为已解决。
-
- 输入:
- chat_history: 可变聊天历史列表(原地修改)。
- approval_id: 待确认项 ID。
-
- 输出:
- 无(副作用:匹配消息的 meta.pending_action.resolved = True)。
-
- 系统定位:
- ``runtime.continue_after_human_action`` 用户操作后更新 UI 状态。
-
- 可扩展性:
- 可改为从 history 中移除而非仅标记 resolved。
- """
+ """在 chat_history 中将指定 approval_id 的 pending_action 标记为已解决(原地修改)。"""
for msg in reversed(chat_history):
pending = ((msg.get("meta") or {}).get("pending_action") or {}) if isinstance(msg, dict) else {}
if pending.get("id") == approval_id:
pending["resolved"] = True
msg.setdefault("meta", {})["pending_action"] = pending
return
+
+
+# ============================================================
+# human_interaction 工具的参数规范化与结果文案
+# ============================================================
+
+def normalize_human_interaction_args(args: dict[str, Any]) -> dict[str, Any]:
+ """规范化 human_interaction 工具参数为固定 schema。
+
+ 输入:
+ args: 模型传入的原始参数字典。
+
+ 输出:
+ 含 interaction_type、title、prompt、options、suggested_response 的字典;
+ interaction_type 限定为 information | selection | approval。
+ """
+ normalized = dict(args or {})
+ normalized["interaction_type"] = str(normalized.get("interaction_type") or "information")
+ if normalized["interaction_type"] not in {"information", "selection", "approval"}:
+ normalized["interaction_type"] = "information"
+ normalized["title"] = str(normalized.get("title") or "需要人工处理")
+ normalized["prompt"] = str(normalized.get("prompt") or "请处理后继续。")
+ normalized["options"] = HumanInteraction.normalize_options(normalized.get("options"))
+ normalized["suggested_response"] = str(normalized.get("suggested_response") or "")
+ return normalized
+
+
+def human_interaction_result(pending: dict[str, Any], decision: str, instruction: str | None) -> str:
+ """根据用户决策生成写入 ToolMessage 的文本结果。
+
+ 输入:
+ pending: PENDING_TOOL_APPROVALS 中 type=human_interaction 的条目。
+ decision: approve | reject | edit。
+ instruction: 用户输入文本(补充信息、选项、修改说明等)。
+
+ 输出:
+ 供 LangGraph 继续推理的 ToolMessage 字符串内容。
+ """
+ tool_call = pending.get("tool_call") or {}
+ args = normalize_human_interaction_args(tool_call.get("args") or {})
+ interaction_type = args.get("interaction_type") or "information"
+ text = (instruction or "").strip()
+ if interaction_type == "approval":
+ if decision == "reject":
+ return "用户拒绝"
+ return "用户同意"
+ if interaction_type == "selection":
+ selected = text or args.get("suggested_response") or ""
+ return f"用户选择:{selected}"
+ return f"用户补充信息如下:{text}"
diff --git a/src/agent/core/config_manager.py b/src/agent/core/config_manager.py
index ece2cbe..ece57e8 100644
--- a/src/agent/core/config_manager.py
+++ b/src/agent/core/config_manager.py
@@ -49,6 +49,9 @@ class AgentConfig:
tools: list[str] = field(default_factory=list)
enabled: bool = True
role: str = "sub" # "main" | "sub"
+ graph: str = "react" # 图名,对应 graph.py 中 GRAPH_REGISTRY 的 key
+ trajectory_rounds: int = 3 # 短期记忆:执行过程中保留最近 N 轮工具返回结果
+ parallel_tools: bool = False # 是否并行执行同一轮中的多个工具调用
@dataclass
@@ -99,6 +102,9 @@ def _default_agent_config() -> list[dict]:
"tools": get_all_available_tool_names(),
"enabled": True,
"role": "main",
+ "graph": "react",
+ "trajectory_rounds": 3,
+ "parallel_tools": False,
}
]
@@ -120,6 +126,9 @@ def load_agent_configs() -> list[AgentConfig]:
tools=item.get("tools", []),
enabled=item.get("enabled", True),
role=item.get("role", "sub"),
+ graph=item.get("graph", "react"),
+ trajectory_rounds=item.get("trajectory_rounds", 3),
+ parallel_tools=item.get("parallel_tools", False),
))
return configs
@@ -329,3 +338,93 @@ def get_registered_tool_names() -> list[str]:
"""返回所有可用工具名称(含数据库工具)。"""
from agent.tools import get_all_available_tool_names
return get_all_available_tool_names()
+
+
+# ---------- GUI 显示器配置 ----------
+GUI_CONFIG_PATH = _CORE_DIR / "gui_config.json"
+
+
+def _default_gui_config() -> dict:
+ """生成默认 GUI 显示器配置(主显示器)。"""
+ try:
+ from screeninfo import get_monitors
+ monitors = get_monitors()
+ primary = next((m for m in monitors if m.is_primary), monitors[0] if monitors else None)
+ if primary:
+ return {
+ "gui_monitor_name": primary.name or "",
+ "gui_monitor_index": 0,
+ "monitors_snapshot": _serialize_monitors(monitors),
+ }
+ except Exception:
+ pass
+ return {
+ "gui_monitor_name": "",
+ "gui_monitor_index": 0,
+ "monitors_snapshot": [],
+ }
+
+
+def _serialize_monitors(monitors) -> list[dict]:
+ """将 screeninfo Monitor 对象序列化为可 JSON 化的字典列表。"""
+ result = []
+ for m in monitors:
+ result.append({
+ "name": m.name or "",
+ "x": m.x,
+ "y": m.y,
+ "width": m.width,
+ "height": m.height,
+ "is_primary": m.is_primary,
+ })
+ return result
+
+
+def _get_live_monitors() -> list[dict]:
+ """实时获取当前连接的显示器列表(通过 screeninfo)。"""
+ from screeninfo import get_monitors
+ return _serialize_monitors(get_monitors())
+
+
+def load_gui_config() -> dict:
+ """从 gui_config.json 加载 GUI 显示器配置,无文件时返回默认。"""
+ raw = _read_json(GUI_CONFIG_PATH)
+ if not raw or not isinstance(raw, dict):
+ raw = _default_gui_config()
+ _write_json(GUI_CONFIG_PATH, raw)
+ return raw
+
+
+def save_gui_config(config: dict) -> bool:
+ """将 GUI 显示器配置写入 gui_config.json。"""
+ return _write_json(GUI_CONFIG_PATH, config)
+
+
+def get_active_monitor_region() -> dict | None:
+ """获取当前选中的 GUI 操作显示器的区域信息。
+
+ 返回:
+ {"x": int, "y": int, "width": int, "height": int, "name": str} 或 None
+
+ 系统定位:
+ 供 gui_tool.py 截取指定显示器区域并做坐标变换。
+ """
+ config = load_gui_config()
+ monitor_name = config.get("gui_monitor_name", "")
+
+ monitors = _get_live_monitors()
+
+ if not monitors:
+ return None
+
+ # 按名称模糊匹配
+ if monitor_name:
+ for m in monitors:
+ if monitor_name.lower() in (m.get("name", "") or "").lower():
+ return {"x": m["x"], "y": m["y"], "width": m["width"], "height": m["height"], "name": m["name"]}
+
+ # 回退:主显示器
+ primary = next((m for m in monitors if m.get("is_primary")), monitors[0] if monitors else None)
+ if primary:
+ return {"x": primary["x"], "y": primary["y"], "width": primary["width"], "height": primary["height"], "name": primary["name"]}
+ return None
diff --git a/src/agent/core/graph.py b/src/agent/core/graph.py
new file mode 100644
index 0000000..ba1ca2c
--- /dev/null
+++ b/src/agent/core/graph.py
@@ -0,0 +1,162 @@
+"""LangGraph 工作流图构建与图注册中心。
+
+系统定位:
+ 将 LLM(已 bind_tools)、工具列表组装为可编译的 StateGraph,
+ 供 ``agents/agent`` 及各子 Agent 的 ``AgentRuntime`` 使用。
+
+图注册中心:
+ GRAPH_REGISTRY: {图名: 构建函数} 字典映射,供 agent_config.json 的 graph 字段
+ 动态选择使用哪个图。用户可在此注册自定义图。
+
+图结构:
+ react: START -> agent -> (tools | END)
+ tools -> process_tool_artifact -> agent
+
+ multi_agent: START -> supervisor -> main | sub_N | END
+ main -> supervisor, sub_N -> supervisor
+
+可扩展性:
+ - 在 GRAPH_REGISTRY 中注册新图的 (build_fn, info) 元组。
+ - 可增加 checkpoint、interrupt 节点、子图路由。
+ - 可为不同 Agent 注入不同的 conditional_edges 或额外节点(如 RAG 检索)。
+"""
+from __future__ import annotations
+
+from typing import Any
+
+from langgraph.graph import END, START, StateGraph
+from langgraph.prebuilt import ToolNode
+
+from agent.core.nodes import make_call_model_node, process_tool_artifact
+from agent.core.routing import should_continue
+from agent.core.state import AgentState
+
+
+def build_agent_graph(tools: list[Any], model_with_tools: Any, trajectory_rounds: int = 3, parallel_tools: bool = False):
+ """构建并编译标准 ReAct Agent 图。
+
+ 输入:
+ tools: LangChain BaseTool 列表,供 ToolNode 执行。
+ model_with_tools: 已 ``bind_tools`` 的聊天模型,供 agent 节点调用。
+ trajectory_rounds: 短期记忆保留最近 N 轮工具返回结果,默认 3。
+ parallel_tools: 是否启用并行工具执行(多个 tool_call 在同一轮并行执行)。
+
+ 输出:
+ 编译后的 LangGraph CompiledGraph,支持 ``invoke({"messages": ...})``。
+
+ 系统定位:
+ 默认图类型 "react",供各 Agent 使用。
+
+ 可扩展性:
+ 返回前可 ``workflow.compile(checkpointer=...)`` 启用持久化;
+ 可参数化节点名以支持多 Agent 图合并。
+ """
+ from agent.tools.tool_agent_call import make_parallel_tool_node
+
+ workflow = StateGraph(AgentState)
+ workflow.add_node("agent", make_call_model_node(model_with_tools, trajectory_rounds=trajectory_rounds))
+
+ # 根据 parallel_tools 选择工具执行节点
+ if parallel_tools:
+ workflow.add_node("tools", make_parallel_tool_node(tools))
+ else:
+ workflow.add_node("tools", ToolNode(tools))
+
+ workflow.add_node("process_tool_artifact", process_tool_artifact)
+
+ workflow.add_edge(START, "agent")
+ workflow.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
+ workflow.add_edge("tools", "process_tool_artifact")
+ workflow.add_edge("process_tool_artifact", "agent")
+ return workflow.compile()
+
+
+
+
+# ---------- 图注册中心 ----------
+# 格式: {图名: {"builder": 构建函数, "display_name": 显示名, "description": 描述}}
+# 用户可在此追加自定义图定义。
+# 图结构可视化通过 Mermaid PNG 自动生成,无需手动配置 nodes/edges。
+
+GRAPH_REGISTRY: dict[str, dict[str, Any]] = {
+ "react": {
+ "builder": build_agent_graph,
+ "display_name": "ReAct (标准)",
+ "description": "标准 ReAct 循环:agent 推理 -> 工具执行 -> 结果处理 -> agent 继续推理,直至完成。适用于绝大多数单 Agent 场景。",
+ },
+}
+
+
+def get_available_graphs() -> list[dict]:
+ """返回所有已注册图的信息列表,供前端下拉框与图可视化使用。
+
+ 输出: [{"name": str, "display_name": str, "description": str}, ...]
+ """
+ result = []
+ for name, info in GRAPH_REGISTRY.items():
+ result.append({
+ "name": name,
+ "display_name": info.get("display_name", name),
+ "description": info.get("description", ""),
+ })
+ return result
+
+
+def get_graph_png_bytes(graph_name: str) -> bytes | None:
+ """为指定图名构建编译图并生成 Mermaid PNG 字节。
+
+ 输入:
+ graph_name: 图名(如 "react"),必须在 GRAPH_REGISTRY 中。
+
+ 输出:
+ PNG 字节数据,失败返回 None。
+
+ 系统定位:
+ 供前端 /api/config/graphs/png/{graph_name} 接口使用,
+ 动态生成图结构可视化图片。
+
+ 如何新增图:
+ 1. 在 GRAPH_REGISTRY 中注册: {"builder": your_build_fn, "display_name": "名称", "description": "描述"}
+ 2. builder 函数签名为 (tools, model_with_tools, **extra_kwargs) -> CompiledGraph
+ 3. PNG 自动生成,无需额外配置
+ """
+ from unittest.mock import MagicMock
+
+ entry = GRAPH_REGISTRY.get(graph_name)
+ if entry is None:
+ return None
+
+ try:
+ # 使用 minimal mock 构建仅用于可视化的编译图
+ mock_llm = MagicMock()
+ mock_llm.bind_tools.return_value = MagicMock()
+ mock_tools: list = []
+ compiled = entry["builder"](mock_tools, mock_llm)
+ graph_obj = compiled.get_graph()
+ return graph_obj.draw_mermaid_png()
+ except Exception as e:
+ print(f"[GraphViz] 生成图 '{graph_name}' 的 PNG 失败: {e}")
+ return None
+
+
+def build_graph_by_name(graph_name: str, tools: list[Any], model_with_tools: Any, **extra_kwargs):
+ """根据图名从注册中心查找构建函数并构建编译后的图。
+
+ 输入:
+ graph_name: 图名(如 "react"),必须在 GRAPH_REGISTRY 中。
+ tools: LangChain BaseTool 列表。
+ model_with_tools: 已 bind_tools 的 LLM。
+ **extra_kwargs: 传递给 builder 的额外参数(如 trajectory_rounds)。
+
+ 输出:
+ 编译后的 LangGraph CompiledGraph。
+
+ 异常:
+ ValueError: graph_name 未在注册中心找到。
+ """
+ entry = GRAPH_REGISTRY.get(graph_name)
+ if entry is None:
+ available = list(GRAPH_REGISTRY.keys())
+ raise ValueError(f"未找到图 '{graph_name}',可用图: {available}")
+ builder = entry["builder"]
+ return builder(tools, model_with_tools, **extra_kwargs)
diff --git a/src/agent/core/graph_builder.py b/src/agent/core/graph_builder.py
deleted file mode 100644
index 1168926..0000000
--- a/src/agent/core/graph_builder.py
+++ /dev/null
@@ -1,53 +0,0 @@
-"""LangGraph ReAct 工作流图构建。
-
-系统定位:
- 将 LLM(已 bind_tools)、工具列表组装为可编译的 StateGraph,
- 供 ``agents/main_agent`` 及各子 Agent 的 ``AgentRuntime`` 使用。
-
-图结构:
- START -> agent -> (tools | END)
- tools -> process_tool_artifact -> agent
-
-可扩展性:
- - 可增加 checkpoint、interrupt 节点、子图路由。
- - 可为不同 Agent 注入不同的 conditional_edges 或额外节点(如 RAG 检索)。
-"""
-from __future__ import annotations
-
-from typing import Any
-
-from langgraph.graph import END, START, StateGraph
-from langgraph.prebuilt import ToolNode
-
-from agent.core.nodes import make_call_model_node, process_tool_artifact
-from agent.core.routing import should_continue
-from agent.core.state import AgentState
-
-
-def build_agent_graph(tools: list[Any], model_with_tools: Any):
- """构建并编译标准 ReAct Agent 图。
-
- 输入:
- tools: LangChain BaseTool 列表,供 ToolNode 执行。
- model_with_tools: 已 ``bind_tools`` 的聊天模型,供 agent 节点调用。
-
- 输出:
- 编译后的 LangGraph CompiledGraph,支持 ``invoke({"messages": ...})``。
-
- 系统定位:
- ``agents/main_agent`` 入口;子 Agent 可复用同一拓扑并替换 tools/model。
-
- 可扩展性:
- 返回前可 ``workflow.compile(checkpointer=...)`` 启用持久化;
- 可参数化节点名以支持多 Agent 图合并。
- """
- workflow = StateGraph(AgentState)
- workflow.add_node("agent", make_call_model_node(model_with_tools))
- workflow.add_node("tools", ToolNode(tools))
- workflow.add_node("process_tool_artifact", process_tool_artifact)
-
- workflow.add_edge(START, "agent")
- workflow.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
- workflow.add_edge("tools", "process_tool_artifact")
- workflow.add_edge("process_tool_artifact", "agent")
- return workflow.compile()
diff --git a/src/agent/core/gui_config.json b/src/agent/core/gui_config.json
new file mode 100644
index 0000000..7359ba5
--- /dev/null
+++ b/src/agent/core/gui_config.json
@@ -0,0 +1,22 @@
+{
+ "gui_monitor_name": "\\\\.\\DISPLAY5",
+ "gui_monitor_index": 0,
+ "monitors_snapshot": [
+ {
+ "name": "\\\\.\\DISPLAY5",
+ "x": 0,
+ "y": 0,
+ "width": 2560,
+ "height": 1440,
+ "is_primary": true
+ },
+ {
+ "name": "\\\\.\\DISPLAY1",
+ "x": -1350,
+ "y": 149,
+ "width": 1350,
+ "height": 2400,
+ "is_primary": false
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/agent/core/human_interaction.py b/src/agent/core/human_interaction.py
deleted file mode 100644
index f0bf1ba..0000000
--- a/src/agent/core/human_interaction.py
+++ /dev/null
@@ -1,75 +0,0 @@
-"""人工交互(HITL)参数规范化与结果文案生成。
-
-系统定位:
- 连接 ``human_interaction`` 工具与 ``core/approvals`` / ``core/runtime``,
- 将模型发起的人工交互请求转为 UI 可展示结构,并在用户操作后生成
- ToolMessage 内容供图继续执行。
-
-可扩展性:
- - 新增 interaction_type 时同步扩展 ``normalize_human_interaction_args``
- 与 ``human_interaction_result`` 的分支。
- - 可与前端约定更丰富的 payload(表单字段、文件上传等)。
-"""
-from __future__ import annotations
-
-from typing import Any
-
-from agent.tools.tool import HumanInteraction
-
-
-def normalize_human_interaction_args(args: dict[str, Any]) -> dict[str, Any]:
- """规范化 human_interaction 工具参数为固定 schema。
-
- 输入:
- args: 模型传入的原始参数字典。
-
- 输出:
- 含 interaction_type、title、prompt、options、suggested_response 的字典;
- interaction_type 限定为 information | selection | approval。
-
- 系统定位:
- ``approvals.build_human_interaction_meta`` 构建待确认 UI 前调用。
-
- 可扩展性:
- 可增加 locale、超时、默认值校验;options 可支持 label/value 结构。
- """
- normalized = dict(args or {})
- normalized["interaction_type"] = str(normalized.get("interaction_type") or "information")
- if normalized["interaction_type"] not in {"information", "selection", "approval"}:
- normalized["interaction_type"] = "information"
- normalized["title"] = str(normalized.get("title") or "需要人工处理")
- normalized["prompt"] = str(normalized.get("prompt") or "请处理后继续。")
- normalized["options"] = HumanInteraction.normalize_options(normalized.get("options"))
- normalized["suggested_response"] = str(normalized.get("suggested_response") or "")
- return normalized
-
-
-def human_interaction_result(pending: dict[str, Any], decision: str, instruction: str | None) -> str:
- """根据用户决策生成写入 ToolMessage 的文本结果。
-
- 输入:
- pending: PENDING_TOOL_APPROVALS 中 type=human_interaction 的条目。
- decision: approve | reject | edit。
- instruction: 用户输入文本(补充信息、选项、修改说明等)。
-
- 输出:
- 供 LangGraph 继续推理的 ToolMessage 字符串内容。
-
- 系统定位:
- ``runtime.continue_after_human_action`` 在人工交互分支调用。
-
- 可扩展性:
- 可返回结构化 JSON 字符串,供下游节点解析;支持多语言模板。
- """
- tool_call = pending.get("tool_call") or {}
- args = normalize_human_interaction_args(tool_call.get("args") or {})
- interaction_type = args.get("interaction_type") or "information"
- text = (instruction or "").strip()
- if interaction_type == "approval":
- if decision == "reject":
- return "用户拒绝"
- return "用户同意"
- if interaction_type == "selection":
- selected = text or args.get("suggested_response") or ""
- return f"用户选择:{selected}"
- return f"用户补充信息如下:{text}"
diff --git a/src/agent/core/llm.py b/src/agent/core/llm.py
index 37c1db3..c4a083a 100644
--- a/src/agent/core/llm.py
+++ b/src/agent/core/llm.py
@@ -476,13 +476,10 @@ class ChatQwen(ChatOpenAI):
# ---------- 打印原始 tool_calls ----------
try:
raw = response if isinstance(response, dict) else response.model_dump()
- print("[RAW_RESPONSE]", json.dumps(raw, indent=2, ensure_ascii=False))
choices = raw.get("choices", [])
if choices:
msg = choices[0].get("message", {})
tool_calls = msg.get("tool_calls")
- if tool_calls:
- print("[RAW_TOOLCALLS]", json.dumps(tool_calls, indent=2, ensure_ascii=False))
except Exception:
pass
diff --git a/src/agent/core/multi_agent_graph.py b/src/agent/core/multi_agent_graph.py
index a5f6ccf..40f3ee5 100644
--- a/src/agent/core/multi_agent_graph.py
+++ b/src/agent/core/multi_agent_graph.py
@@ -350,6 +350,7 @@ def make_main_agent_node(
tools: list[Any],
agent_name: str = "main",
tools_by_name: dict[str, Any] | None = None,
+ trajectory_rounds: int = 3,
) -> Callable[[dict], dict]:
"""创建主 Agent 的图节点(含完整的 ReAct 循环 + 委派能力)。
@@ -362,6 +363,7 @@ def make_main_agent_node(
tools: 工具列表(含 agent_call 工具)。
agent_name: 主 agent 名称。
tools_by_name: 工具名映射。
+ trajectory_rounds: 短期记忆保留最近 N 轮工具返回结果。
输出:
LangGraph 节点函数。
@@ -377,8 +379,10 @@ def make_main_agent_node(
# 执行完整的 ReAct 循环(限制步数防止无限循环)
max_steps = 50
for step in range(max_steps):
- # 调用 LLM
- response = model_with_tools.invoke(messages)
+ # 过滤短期记忆后再调用 LLM
+ from agent.core.nodes import _filter_short_term_memory
+ filtered = _filter_short_term_memory(messages, trajectory_rounds)
+ response = model_with_tools.invoke(filtered)
messages.append(response)
# 记录工具调用事件
@@ -471,6 +475,7 @@ def build_multi_agent_graph(
main_tools_by_name: dict[str, Any],
sub_agents: list[dict[str, Any]],
main_agent_name: str = "main",
+ trajectory_rounds: int = 3,
) -> Any:
"""构建一主多子 SupervisorGraph。
@@ -505,7 +510,7 @@ def build_multi_agent_graph(
# 主 Agent 节点
workflow.add_node(
main_agent_name,
- make_main_agent_node(main_model_with_tools, main_tools, main_agent_name, main_tools_by_name),
+ make_main_agent_node(main_model_with_tools, main_tools, main_agent_name, main_tools_by_name, trajectory_rounds=trajectory_rounds),
)
# 子 Agent 节点
diff --git a/src/agent/core/nodes.py b/src/agent/core/nodes.py
index 7803af5..bdf251f 100644
--- a/src/agent/core/nodes.py
+++ b/src/agent/core/nodes.py
@@ -2,7 +2,7 @@
系统定位:
实现 ReAct 循环中的 ``agent`` 节点工厂与 ``process_tool_artifact`` 节点,
- 被 ``graph_builder`` 注册到 StateGraph。
+ 被 ``graph`` 注册到 StateGraph。
可扩展性:
- ``make_call_model_node`` 可注入 streaming、重试、用量统计回调。
@@ -19,11 +19,59 @@ from agent.utils.image_utils import image_bytes_to_openai_image_url_part
from agent.utils.tool_call_utils import normalize_tool_call
-def make_call_model_node(model_with_tools: Any) -> Callable[[AgentState], dict]:
+def _filter_short_term_memory(messages: list, trajectory_rounds: int) -> list:
+ """过滤短期记忆:保留所有工具调用,工具返回结果仅保留最近 trajectory_rounds 轮。
+
+ 输入:
+ messages: 完整消息列表。
+ trajectory_rounds: 保留最近 N 轮的工具返回结果。0 或负数表示不过滤。
+
+ 输出:
+ 过滤后的消息列表。
+
+ 系统定位:
+ call_model 节点在调用 LLM 前进行消息裁剪,避免 GUI 等多轮工具调用
+ 场景下上下文过长。
+
+ 规则:
+ - 保留所有非 ToolMessage 消息(SystemMessage、HumanMessage、AIMessage)
+ - ToolMessage 仅保留最近 trajectory_rounds 轮工具调用对应的返回结果
+ - 每轮由一个 AIMessage(含 tool_calls)定义,其后的 ToolMessage 属于该轮
+ """
+ if trajectory_rounds <= 0:
+ return list(messages)
+
+ # 从后向前遍历,统计已遇到的含 tool_calls 的 AIMessage 轮数
+ rounds_seen = 0
+ kept_indices = set()
+
+ for i in range(len(messages) - 1, -1, -1):
+ msg = messages[i]
+ from langchain_core.messages import AIMessage, ToolMessage
+
+ if isinstance(msg, AIMessage) and getattr(msg, "tool_calls", None):
+ rounds_seen += 1
+
+ if isinstance(msg, ToolMessage):
+ if rounds_seen < trajectory_rounds:
+ kept_indices.add(i)
+ else:
+ kept_indices.add(i)
+
+ return [messages[i] for i in sorted(kept_indices)]
+
+
+def make_call_model_node(
+ model_with_tools: Any,
+ trajectory_rounds: int = 3,
+) -> Callable[[AgentState], dict]:
"""创建调用已绑定工具的 LLM 的图节点闭包。"""
def call_model(state: AgentState) -> dict:
- response = model_with_tools.invoke(state["messages"])
+ # 过滤短期记忆后再传给 LLM
+ all_messages = list(state["messages"])
+ filtered_messages = _filter_short_term_memory(all_messages, trajectory_rounds)
+ response = model_with_tools.invoke(filtered_messages)
if hasattr(response, "tool_calls") and response.tool_calls:
for tc in response.tool_calls:
@@ -52,11 +100,8 @@ def make_call_model_node(model_with_tools: Any) -> Callable[[AgentState], dict]:
def process_tool_artifact(state: AgentState) -> dict:
last_msg = state["messages"][-1]
+ # ---- 实时记录工具结果到前端 ----
if isinstance(last_msg, ToolMessage):
- content_preview = str(last_msg.content)
- if len(content_preview) > 200:
- content_preview = content_preview[:200] + "..."
-
try:
from agent.history.tool_call_recorder import record_tool_result_live, get_current_session
sid = get_current_session()
diff --git a/src/agent/core/routing.py b/src/agent/core/routing.py
index 8aa88fd..22356f3 100644
--- a/src/agent/core/routing.py
+++ b/src/agent/core/routing.py
@@ -1,7 +1,7 @@
"""LangGraph 条件路由:工具循环 vs 结束 vs 待确认暂停。
系统定位:
- 连接 ``graph_builder`` 的条件边与 ``runtime.execute_agent`` 的
+ 连接 ``graph`` 的条件边与 ``runtime.execute_agent`` 的
后处理逻辑,在模型输出 tool_calls 时决定图走向或提取 UI 待确认元数据。
可扩展性:
@@ -31,7 +31,7 @@ def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
``"tools"`` 进入 ToolNode;``"__end__"`` 结束图(含需确认工具场景)。
系统定位:
- ``graph_builder`` 中 agent 节点的 conditional_edges 路由函数。
+ ``graph`` 中 agent 节点的 conditional_edges 路由函数。
若任一 tool_call 策略为 confirm,则直接 END,由 runtime 展示确认 UI。
可扩展性:
diff --git a/src/agent/core/runtime.py b/src/agent/core/runtime.py
index 0baad0e..cd00e99 100644
--- a/src/agent/core/runtime.py
+++ b/src/agent/core/runtime.py
@@ -18,11 +18,11 @@ from dataclasses import dataclass
from typing import Any, Literal
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
+from langgraph.errors import GraphRecursionError
from langchain_core.messages import messages_from_dict
from langgraph.errors import GraphRecursionError
-from agent.core.approvals import PENDING_TOOL_APPROVALS, pending_assistant_message, remove_pending_ui_message
-from agent.core.human_interaction import human_interaction_result
+from agent.core.approvals import PENDING_TOOL_APPROVALS, pending_assistant_message, remove_pending_ui_message, human_interaction_result
from agent.history.tool_call_recorder import save_turn, start_live_turn, end_live_turn, set_current_turn, set_current_session
from agent.memory.system_prompt import TEAM_MODE_PROMPT
from agent.core.routing import first_pending_tool_meta
@@ -61,6 +61,7 @@ class AgentRuntime:
tools_by_name: dict[str, Any]
system_prompt: str
max_iterations: int = 200
+ trajectory_rounds: int = 3
_OOM_MESSAGE = "OOM: 已达到最大迭代次数限制,任务未能完成。请简化任务或增加 max_iterations 配置后重试。"
@@ -196,15 +197,33 @@ def _invoke_after_manual_tool_result(
tool_call = normalize_tool_call(raw_tool_call)
tool_name = tool_call.get("name")
tool_args = tool_call.get("args") or {}
+ tool_call_id = tool_call.get("id") or f"manual_{session_id}_{len(next_messages)}"
if classify_tool_execution(tool_name, tool_args) != "direct":
continue
tool = runtime.tools_by_name.get(tool_name or "")
if not tool:
continue
- result = tool.invoke(tool_args)
- tool_content = result[0] if isinstance(result, tuple) else result
- call_id = tool_call.get("id") or f"manual_{session_id}_{len(next_messages)}"
- next_messages.append(ToolMessage(content=str(tool_content), tool_call_id=call_id, name=tool_name or "unknown_tool"))
+ # 构造完整 ToolCall 格式,确保 content_and_artifact 工具的 artifact 被保留
+ tool_call_input = {
+ "name": tool_name,
+ "args": tool_args,
+ "id": tool_call_id,
+ "type": "tool_call",
+ }
+ result = tool.invoke(tool_call_input)
+ if isinstance(result, ToolMessage):
+ tool_content = str(result.content) if result.content else ""
+ tool_artifact = getattr(result, "artifact", None)
+ elif isinstance(result, tuple):
+ tool_content = str(result[0]) if result[0] else ""
+ tool_artifact = result[1] if len(result) > 1 else None
+ else:
+ tool_content = str(result)
+ tool_artifact = None
+ msg_kwargs = {"content": tool_content, "tool_call_id": tool_call_id, "name": tool_name or "unknown_tool"}
+ if tool_artifact:
+ msg_kwargs["artifact"] = tool_artifact
+ next_messages.append(ToolMessage(**msg_kwargs))
progressed = True
if not progressed:
return all_messages, None
@@ -324,6 +343,7 @@ def execute_agent(
"output_type": output_type_from_config,
"autoplay": False,
"audio": audio_data.get("audio"),
+ "turn_id": turn_id,
}
if output_type_from_config == "audio" and assistant_meta.get("audio"):
@@ -423,7 +443,8 @@ def continue_after_human_action(
traceback.print_exc()
content = "处理人工交互结果时遇到问题。"
persisted_messages = messages_from_dict(pending.get("messages") or [])
- chat_history.append({"role": "assistant", "content": content, "meta": {"output_type": output_type, "autoplay": False}})
+ human_turn_id = f"human_{approval_id}"
+ chat_history.append({"role": "assistant", "content": content, "meta": {"output_type": output_type, "autoplay": False, "turn_id": human_turn_id}})
_persist_messages_from_pending_turn(session_id, persisted_messages, pending)
if session_id:
@@ -441,11 +462,29 @@ def continue_after_human_action(
return chat_history
try:
- result = tool.invoke(tool_args)
- tool_content = result[0] if isinstance(result, tuple) else result
+ # 构造完整 ToolCall 格式,确保 content_and_artifact 工具的 artifact 被保留
+ tool_call_input = {
+ "name": tool_name,
+ "args": tool_args,
+ "id": tool_call.get("id", ""),
+ "type": "tool_call",
+ }
+ result = tool.invoke(tool_call_input)
+ if isinstance(result, ToolMessage):
+ tool_content = str(result.content) if result.content else ""
+ tool_artifact = getattr(result, "artifact", None)
+ elif isinstance(result, tuple):
+ tool_content = str(result[0]) if result[0] else ""
+ tool_artifact = result[1] if len(result) > 1 else None
+ else:
+ tool_content = str(result)
+ tool_artifact = None
prior_messages = messages_from_dict(pending.get("messages") or [])
call_id = tool_call.get("id") or f"manual_{approval_id}"
- prior_messages.append(ToolMessage(content=str(tool_content), tool_call_id=call_id, name=tool_name))
+ msg_kwargs = {"content": tool_content, "tool_call_id": call_id, "name": tool_name}
+ if tool_artifact:
+ msg_kwargs["artifact"] = tool_artifact
+ prior_messages.append(ToolMessage(**msg_kwargs))
all_messages, pending_meta = _invoke_after_manual_tool_result(runtime, prior_messages, session_id)
if pending_meta:
_remember_pending_user_content(pending_meta, pending.get("user_content"))
@@ -468,9 +507,10 @@ def continue_after_human_action(
])
persisted_messages = prior_messages
- chat_history.append({"role": "assistant", "content": content, "meta": {"output_type": output_type, "autoplay": False}})
+ manual_turn_id = f"human_{approval_id}"
+ chat_history.append({"role": "assistant", "content": content, "meta": {"output_type": output_type, "autoplay": False, "turn_id": manual_turn_id}})
_persist_messages_from_pending_turn(session_id, persisted_messages, pending)
if session_id:
- save_turn(session_id, f"human_{approval_id}", {"role": "user", "content": pending.get("user_content") or ""}, persisted_messages)
+ save_turn(session_id, manual_turn_id, {"role": "user", "content": pending.get("user_content") or ""}, persisted_messages)
sync_manifest_from_chat_history(session_id, chat_history, "")
return chat_history
diff --git a/src/agent/core/state.py b/src/agent/core/state.py
index aaf9c01..1c88012 100644
--- a/src/agent/core/state.py
+++ b/src/agent/core/state.py
@@ -1,7 +1,7 @@
"""LangGraph Agent 状态类型定义。
系统定位:
- 定义图运行时共享状态 ``AgentState``,被 ``graph_builder``、``nodes``、
+ 定义图运行时共享状态 ``AgentState``,被 ``graph``、``nodes``、
``routing`` 作为 StateGraph 的 schema 使用。
可扩展性:
diff --git a/src/agent/core/tool_config.json b/src/agent/core/tool_config.json
index 2b49144..677b157 100644
--- a/src/agent/core/tool_config.json
+++ b/src/agent/core/tool_config.json
@@ -11,7 +11,7 @@
"name": "date_query_class",
"display_name": "DateQuery",
"description": "一个日期查询工具,可以返回当前的日期信息。仅当需要查询当前时间时调用,其余任何情况严禁调用。",
- "permission": "confirm",
+ "permission": "direct",
"auto_execute_rules": [],
"enabled": true
},
@@ -50,7 +50,13 @@
"display_name": "SoftwareControl",
"description": "操作本机已安装的软件。工具会自动扫描 Windows 开始菜单快捷方式、桌面快捷方式和 App Paths,建立软件索引后根据用户输入做模糊匹配。action=open 时打开软件;action=close 时仅做正常关闭;action=force_close 时才允许在正常关闭失败后强制结束进程。仅当用户明确要求打开、关闭或强制关闭某个本机软件时调用。",
"permission": "confirm",
- "auto_execute_rules": [],
+ "auto_execute_rules": [
+ {
+ "parameter": "action",
+ "operator": "equals",
+ "value": "open"
+ }
+ ],
"enabled": true
},
{
@@ -90,5 +96,13 @@
"permission": "direct",
"auto_execute_rules": [],
"enabled": true
+ },
+ {
+ "name": "call_subagent",
+ "display_name": "CallSubAgentTool",
+ "description": "调用已启用的子 Agent 执行指定任务。子 Agent 会独立运行并返回最终结果。",
+ "permission": "direct",
+ "auto_execute_rules": [],
+ "enabled": true
}
]
\ No newline at end of file
diff --git a/src/agent/history/__pycache__/tool_call_recorder.cpython-312.pyc b/src/agent/history/__pycache__/tool_call_recorder.cpython-312.pyc
index e12d71e..84098e9 100644
Binary files a/src/agent/history/__pycache__/tool_call_recorder.cpython-312.pyc and b/src/agent/history/__pycache__/tool_call_recorder.cpython-312.pyc differ
diff --git a/src/agent/history/__pycache__/tool_call_recorder.cpython-313.pyc b/src/agent/history/__pycache__/tool_call_recorder.cpython-313.pyc
deleted file mode 100644
index 25cc524..0000000
Binary files a/src/agent/history/__pycache__/tool_call_recorder.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/history/data_base/chat_history.db b/src/agent/history/data_base/chat_history.db
index 90ce534..b2020db 100644
Binary files a/src/agent/history/data_base/chat_history.db and b/src/agent/history/data_base/chat_history.db differ
diff --git a/src/agent/history/multi_inputs/20260618_111759/ui_messages.json b/src/agent/history/multi_inputs/20260618_111759/ui_messages.json
deleted file mode 100644
index f0ff54d..0000000
--- a/src/agent/history/multi_inputs/20260618_111759/ui_messages.json
+++ /dev/null
@@ -1,80 +0,0 @@
-{
- "version": 3,
- "messages": [
- {
- "role": "user",
- "user": {
- "content": [
- {
- "type": "text",
- "text": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划"
- }
- ]
- }
- },
- {
- "role": "assistant",
- "assistant": {
- "text": "已为您生成详细的**西红柿炒鸡蛋**制作计划,共分为8个步骤:\n\n1. **准备食材**:2-3个鸡蛋、2个中等大小西红柿、葱花少许、食用油适量、盐适量、糖少许(可选)\n2. **处理食材**:将鸡蛋打入碗中,加少许盐打散;西红柿洗净切块;葱切成葱花备用\n3. **热锅倒油**:开中火加热炒锅,倒入适量食用油,待油微热\n4. **炒鸡蛋**:倒入蛋液,用筷子或铲子快速划散,炒至凝固后盛出备用\n5. **炒西红柿**:锅中留底油,放入西红柿块翻炒,可用铲子压一压帮助出汁,炒至软烂\n6. **调味**:根据口味加入适量盐和少许糖提鲜,翻炒均匀\n7. **混合**:将炒好的鸡蛋倒回锅中,与西红柿翻炒均匀,让鸡蛋吸收汤汁\n8. **出锅**:撒上葱花点缀,装盘即可享用\n\n祝您烹饪愉快!如果需要标记某一步骤完成,请告诉我。",
- "output_type": "text",
- "autoplay": false,
- "audio_duration_seconds": null,
- "audio_relpath": null,
- "pending_action": null,
- "agent_mode": null
- }
- },
- {
- "role": "user",
- "user": {
- "content": [
- {
- "type": "text",
- "text": "你的上下文中有什么"
- }
- ]
- }
- },
- {
- "role": "assistant",
- "assistant": {
- "text": "需要你确认后继续执行。",
- "output_type": "text",
- "autoplay": false,
- "audio_duration_seconds": null,
- "audio_relpath": null,
- "pending_action": {
- "id": "b5de8b0742564742ba20ec9df9803b51",
- "type": "tool_call",
- "title": "待确认执行工具:rag_tool",
- "tool_name": "rag_tool",
- "args": {
- "name": "rag_tool",
- "args": {
- "action_type": "list_sources"
- }
- },
- "instruction": "{\n \"name\": \"rag_tool\",\n \"args\": {\n \"action_type\": \"list_sources\"\n }\n}",
- "options": [
- "approve",
- "reject"
- ],
- "resolved": true
- },
- "agent_mode": null
- }
- },
- {
- "role": "assistant",
- "assistant": {
- "text": "已拒绝,本次待确认操作不会执行。",
- "output_type": "text",
- "autoplay": false,
- "audio_duration_seconds": null,
- "audio_relpath": null,
- "pending_action": null,
- "agent_mode": null
- }
- }
- ]
-}
\ No newline at end of file
diff --git a/src/agent/history/multi_inputs/20260620_170530/ui_messages.json b/src/agent/history/multi_inputs/20260620_170530/ui_messages.json
new file mode 100644
index 0000000..0b5082d
--- /dev/null
+++ b/src/agent/history/multi_inputs/20260620_170530/ui_messages.json
@@ -0,0 +1,120 @@
+{
+ "version": 3,
+ "messages": [
+ {
+ "role": "user",
+ "user": {
+ "content": [
+ {
+ "type": "text",
+ "text": "帮我点击加号新建标签页"
+ }
+ ]
+ }
+ },
+ {
+ "role": "assistant",
+ "assistant": {
+ "text": "已成功点击加号,新建了一个标签页。",
+ "output_type": "text",
+ "autoplay": false,
+ "audio_duration_seconds": null,
+ "audio_relpath": null,
+ "pending_action": null,
+ "agent_mode": null
+ }
+ },
+ {
+ "role": "user",
+ "user": {
+ "content": [
+ {
+ "type": "text",
+ "text": "帮我左键点击当前视频进行暂停"
+ }
+ ]
+ }
+ },
+ {
+ "role": "assistant",
+ "assistant": {
+ "text": "已成功左键点击当前视频画面,视频已暂停。",
+ "output_type": "text",
+ "autoplay": false,
+ "audio_duration_seconds": null,
+ "audio_relpath": null,
+ "pending_action": null,
+ "agent_mode": null
+ }
+ },
+ {
+ "role": "user",
+ "user": {
+ "content": [
+ {
+ "type": "text",
+ "text": "帮我点击加号新建标签页"
+ }
+ ]
+ }
+ },
+ {
+ "role": "assistant",
+ "assistant": {
+ "text": "已成功点击加号,新建了一个标签页。",
+ "output_type": "text",
+ "autoplay": false,
+ "audio_duration_seconds": null,
+ "audio_relpath": null,
+ "pending_action": null,
+ "agent_mode": null
+ }
+ },
+ {
+ "role": "user",
+ "user": {
+ "content": [
+ {
+ "type": "text",
+ "text": "帮我点击加号新建标签页"
+ }
+ ]
+ }
+ },
+ {
+ "role": "assistant",
+ "assistant": {
+ "text": "已成功点击加号,新建了一个标签页。",
+ "output_type": "text",
+ "autoplay": false,
+ "audio_duration_seconds": null,
+ "audio_relpath": null,
+ "pending_action": null,
+ "agent_mode": null
+ }
+ },
+ {
+ "role": "user",
+ "user": {
+ "content": [
+ {
+ "type": "text",
+ "text": "截张图看看我再干啥,如果读不到图片请直接告诉我"
+ }
+ ]
+ }
+ },
+ {
+ "role": "assistant",
+ "assistant": {
+ "text": "根据截图,我可以看到你正在使用一个浏览器(看起来像是 Edge 浏览器),并且打开了多个标签页。\n\n当前屏幕显示的内容包括:\n1. **浏览器顶部**:有几个打开的标签页,包括“哔哩哔哩”、“【野火】这视频怎么讲?... ”和当前的“新建标签页”。\n2. **页面内容**:这是一个浏览器的新标签页界面,背景是雪山风景图。中间有一个搜索框,下方有一些快捷方式图标,例如“哔哩哔哩”、“京东直播”、“山东大学”等。\n3. **底部任务栏**:显示了 Windows 系统的任务栏,有开始菜单、搜索、文件资源管理器、Edge 浏览器、微信等图标。右下角显示时间是 11:24,日期是 2025/8/27。\n\n所以,你目前的状态是在电脑上使用浏览器浏览网页或准备搜索新的内容。",
+ "output_type": "text",
+ "autoplay": false,
+ "audio_duration_seconds": null,
+ "audio_relpath": null,
+ "pending_action": null,
+ "agent_mode": null
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/agent/history/multi_inputs/20260620_174345/ui_messages.json b/src/agent/history/multi_inputs/20260620_174345/ui_messages.json
new file mode 100644
index 0000000..d392599
--- /dev/null
+++ b/src/agent/history/multi_inputs/20260620_174345/ui_messages.json
@@ -0,0 +1,51 @@
+{
+ "version": 3,
+ "messages": [
+ {
+ "role": "user",
+ "user": {
+ "content": [
+ {
+ "type": "text",
+ "text": "截张图看看我再干啥,如果读不到图片请直接告诉我"
+ }
+ ]
+ }
+ },
+ {
+ "role": "assistant",
+ "assistant": {
+ "text": "根据截图,你当前正在使用 **Google Chrome** 浏览器。\n\n屏幕上打开了两个标签页:\n1. **左侧标签页**:显示的是 **YouTube** 网站,页面标题是 \"YouTube\",看起来可能是在浏览视频列表或者首页。\n2. **右侧标签页(当前激活)**:显示的是 **Wikipedia(维基百科)** 的中文页面,具体是关于 **\"中国\"** (China) 的条目。页面内容展示了中国的地图、国旗以及相关的介绍文字。\n\n此外,任务栏上还可以看到其他已打开的应用程序图标,包括文件资源管理器、Edge 浏览器等。",
+ "output_type": "text",
+ "autoplay": false,
+ "audio_duration_seconds": null,
+ "audio_relpath": null,
+ "pending_action": null,
+ "agent_mode": null
+ }
+ },
+ {
+ "role": "user",
+ "user": {
+ "content": [
+ {
+ "type": "text",
+ "text": "截张图看看我再干啥,如果读不到图片请直接告诉我"
+ }
+ ]
+ }
+ },
+ {
+ "role": "assistant",
+ "assistant": {
+ "text": "截图已获取。\n\n从截图中可以看到,你当前正在使用 **Google Chrome** 浏览器。\n1. **左侧标签页**:打开的是 **YouTube** 网站。\n2. **右侧标签页(当前激活)**:显示的是 **Wikipedia(维基百科)** 的中文页面,标题为“中国”。\n\n你似乎正在浏览关于中国的百科信息。",
+ "output_type": "text",
+ "autoplay": false,
+ "audio_duration_seconds": null,
+ "audio_relpath": null,
+ "pending_action": null,
+ "agent_mode": null
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/agent/history/multi_inputs/20260618_173653/ui_messages.json b/src/agent/history/multi_inputs/20260620_174454/ui_messages.json
similarity index 68%
rename from src/agent/history/multi_inputs/20260618_173653/ui_messages.json
rename to src/agent/history/multi_inputs/20260620_174454/ui_messages.json
index 0dc4ce4..8b1ed8a 100644
--- a/src/agent/history/multi_inputs/20260618_173653/ui_messages.json
+++ b/src/agent/history/multi_inputs/20260620_174454/ui_messages.json
@@ -7,7 +7,7 @@
"content": [
{
"type": "text",
- "text": "帮我关掉DeepSeek开放平台的标签页"
+ "text": "帮我点击加号新建标签页"
}
]
}
@@ -15,7 +15,7 @@
{
"role": "assistant",
"assistant": {
- "text": "已经帮您关闭了 DeepSeek 开放平台的标签页。现在浏览器显示的是 minor - Chatbot 页面。",
+ "text": "已为您点击新建标签页。现在应该已经打开了一个新的空白标签页。",
"output_type": "text",
"autoplay": false,
"audio_duration_seconds": null,
diff --git a/src/agent/history/multi_inputs/20260620_183029/ui_messages.json b/src/agent/history/multi_inputs/20260620_183029/ui_messages.json
new file mode 100644
index 0000000..2fe6590
--- /dev/null
+++ b/src/agent/history/multi_inputs/20260620_183029/ui_messages.json
@@ -0,0 +1,51 @@
+{
+ "version": 3,
+ "messages": [
+ {
+ "role": "user",
+ "user": {
+ "content": [
+ {
+ "type": "text",
+ "text": "截张图看看我在干啥,如果读不到图片请直接告诉我"
+ }
+ ]
+ }
+ },
+ {
+ "role": "assistant",
+ "assistant": {
+ "text": "根据截图,你正在使用 **DeepSeek 开放平台**(platform.deepseek.com)的网页界面。\n\n具体来说,你当前查看的是 **“用量信息”** 页面。页面上显示了以下关键信息:\n* **充值余额**:¥22.77 CNY\n* **六月消费**(按 UTC+0 时间):¥17.22 CNY\n* **每月用量图表**:展示了近期(6月1日至6月30日)的消费金额波动。\n* **模型使用情况**:下方还列出了 `deepseek-v4-pro` 模型的 API 请求次数和 Tokens 消耗量。\n\n简单来说,你正在查看自己在 DeepSeek 平台上的账户余额、消费记录以及 API 的使用情况。",
+ "output_type": "text",
+ "autoplay": false,
+ "audio_duration_seconds": null,
+ "audio_relpath": null,
+ "pending_action": null,
+ "agent_mode": null
+ }
+ },
+ {
+ "role": "user",
+ "user": {
+ "content": [
+ {
+ "type": "text",
+ "text": "你现在的对话记录中有图片吗"
+ }
+ ]
+ }
+ },
+ {
+ "role": "assistant",
+ "assistant": {
+ "text": "根据刚才截取的屏幕截图,我**没有**在当前的对话记录中看到任何图片。\n\n这张截图显示的是你正在浏览 **DeepSeek 开放平台** 的“用量信息”页面,主要内容包括:\n1. **充值余额**:¥22.77 CNY\n2. **六月消费**(按 UTC+0 时间):¥17.22 CNY\n3. **每月用量图表**:展示了近期每天的消费金额波动。\n4. **模型使用情况**:下方列出了 `deepseek-v4-pro` 模型的 API 请求次数和 Tokens 消耗量。\n\n所以,目前的对话中只有文字交互,并没有上传图片。",
+ "output_type": "text",
+ "autoplay": false,
+ "audio_duration_seconds": null,
+ "audio_relpath": null,
+ "pending_action": null,
+ "agent_mode": null
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/agent/history/multi_inputs/20260620_205109/ui_messages.json b/src/agent/history/multi_inputs/20260620_205109/ui_messages.json
new file mode 100644
index 0000000..1aaa0ae
--- /dev/null
+++ b/src/agent/history/multi_inputs/20260620_205109/ui_messages.json
@@ -0,0 +1,51 @@
+{
+ "version": 3,
+ "messages": [
+ {
+ "role": "user",
+ "user": {
+ "content": [
+ {
+ "type": "text",
+ "text": "能不能帮我打开虎牙直播"
+ }
+ ]
+ }
+ },
+ {
+ "role": "assistant",
+ "assistant": {
+ "text": "虎牙直播已经成功打开了!可以看到:\n\n- 页面顶部显示\"虎牙直播\"的Logo\n- 当前正在播放主播\"王子酱\"的直播间\n- 页面下方展示了各种游戏分类,包括网游竞技(LOL、CS2等)、单机热游等\n- 右侧还有其他推荐的主播和直播间\n\n你现在可以浏览你感兴趣的直播内容了。如果你想观看特定的主播或游戏类别,可以直接点击相应的选项。",
+ "output_type": "text",
+ "autoplay": false,
+ "audio_duration_seconds": null,
+ "audio_relpath": null,
+ "pending_action": null,
+ "agent_mode": null
+ }
+ },
+ {
+ "role": "user",
+ "user": {
+ "content": [
+ {
+ "type": "text",
+ "text": "帮我打开b站"
+ }
+ ]
+ }
+ },
+ {
+ "role": "assistant",
+ "assistant": {
+ "text": "好的,B站(bilibili)已经成功打开。\n\n页面显示了:\n* **顶部导航栏**:包含首页、番剧、直播、游戏中心等主要分区。\n* **推荐内容**:首页展示了多个视频推荐,例如左侧的“BML-PLAY! 2025”活动预告,以及右侧的“GPT如何解决80年未解单位距离猜想?”等热门视频。\n* **分类标签**:可以看到动画、番剧、国创、音乐、舞蹈、游戏等多个内容分类。\n\n你现在可以浏览你感兴趣的内容了。",
+ "output_type": "text",
+ "autoplay": false,
+ "audio_duration_seconds": null,
+ "audio_relpath": null,
+ "pending_action": null,
+ "agent_mode": null
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/agent/history/multi_inputs/20260621_114011/ui_messages.json b/src/agent/history/multi_inputs/20260621_114011/ui_messages.json
new file mode 100644
index 0000000..08ef6a7
--- /dev/null
+++ b/src/agent/history/multi_inputs/20260621_114011/ui_messages.json
@@ -0,0 +1,28 @@
+{
+ "version": 3,
+ "messages": [
+ {
+ "role": "user",
+ "user": {
+ "content": [
+ {
+ "type": "text",
+ "text": "帮我打开b站"
+ }
+ ]
+ }
+ },
+ {
+ "role": "assistant",
+ "assistant": {
+ "text": "B站(哔哩哔哩)已经为您打开了。请问有什么我可以帮您的吗?",
+ "output_type": "text",
+ "autoplay": false,
+ "audio_duration_seconds": null,
+ "audio_relpath": null,
+ "pending_action": null,
+ "agent_mode": null
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/agent/history/tool_call_recorder.py b/src/agent/history/tool_call_recorder.py
index 4fe939c..5ade465 100644
--- a/src/agent/history/tool_call_recorder.py
+++ b/src/agent/history/tool_call_recorder.py
@@ -176,28 +176,35 @@ def session_tool_dir(session_id: str) -> Path:
def extract_tool_calls_from_messages(messages: list[BaseMessage]) -> list[dict[str, Any]]:
"""从图执行后的消息列表中提取完整的工具调用链。"""
records: list[dict[str, Any]] = []
- pending: dict[str, dict[str, Any]] = {}
+ pending: list[dict[str, Any]] = []
+ # 同名工具调用计数器,避免多张截图文件名冲突
+ name_counter: dict[str, int] = {}
for msg in messages:
if isinstance(msg, AIMessage) and msg.tool_calls:
for tc in msg.tool_calls:
d = tc if isinstance(tc, dict) else {"name": tc.name, "args": tc.args, "id": tc.id}
- if d.get("id"):
- pending[d["id"]] = {
- "name": d.get("name", "unknown"),
- "args": _json_safe(d.get("args", {})),
- "result_text": "",
- "result_files": [],
- }
- elif isinstance(msg, ToolMessage) and msg.tool_call_id:
- cid = msg.tool_call_id
- if cid in pending:
- entry = pending.pop(cid)
+ pending.append({
+ "name": d.get("name", "unknown"),
+ "args": _json_safe(d.get("args", {})),
+ "result_text": "",
+ "result_files": [],
+ "result_images": [],
+ })
+ elif isinstance(msg, ToolMessage):
+ if pending:
+ entry = pending.pop(0)
entry["result_text"] = str(msg.content) if msg.content else ""
- if msg.artifact:
- entry["result_files"] = _save_artifact_files(msg.artifact, msg.tool_call_id)
+ artifact = getattr(msg, "artifact", None)
+ if artifact:
+ tool_name = msg.name or entry["name"]
+ idx = name_counter.get(tool_name, 0)
+ name_counter[tool_name] = idx + 1
+ unique_name = f"{tool_name}_{idx}"
+ result_files, image_bytes_map = _save_artifact_files(artifact, unique_name)
+ entry["result_files"] = result_files
+ entry["_image_bytes"] = image_bytes_map # 供 save_turn 落盘
records.append(entry)
- for entry in pending.values():
- records.append(entry)
+ records.extend(pending)
return records
@@ -213,36 +220,29 @@ def _json_safe(value: Any) -> Any:
return str(value)
-def _save_artifact_files(artifact: Any, base_name: str) -> list[str]:
+def _save_artifact_files(artifact: Any, base_name: str) -> tuple[list[str], dict[str, bytes]]:
+ """从 artifact 中提取文件名列表和 {文件名: bytes} 映射。"""
files: list[str] = []
+ bytes_map: dict[str, bytes] = {}
if isinstance(artifact, dict) and "image_bytes" in artifact:
- files.append(f"_artifact_{base_name}_0.png")
+ fname = f"_artifact_{base_name}_0.png"
+ files.append(fname)
+ bytes_map[fname] = artifact["image_bytes"]
elif isinstance(artifact, list):
for i, item in enumerate(artifact):
if isinstance(item, dict) and "image_bytes" in item:
- files.append(f"_artifact_{base_name}_{i}.png")
+ fname = f"_artifact_{base_name}_{i}.png"
+ files.append(fname)
+ bytes_map[fname] = item["image_bytes"]
elif isinstance(item, bytes):
- files.append(f"_artifact_{base_name}_{i}.png")
+ fname = f"_artifact_{base_name}_{i}.png"
+ files.append(fname)
+ bytes_map[fname] = item
elif isinstance(artifact, bytes):
- files.append(f"_artifact_{base_name}.png")
- return files
-
-
-def _find_artifact_bytes(messages: list[BaseMessage], fname: str) -> bytes | None:
- for msg in messages:
- if isinstance(msg, ToolMessage) and msg.artifact:
- a = msg.artifact
- if isinstance(a, dict) and "image_bytes" in a:
- return a["image_bytes"]
- if isinstance(a, list):
- for item in a:
- if isinstance(item, dict) and "image_bytes" in item:
- return item["image_bytes"]
- if isinstance(item, bytes):
- return item
- if isinstance(a, bytes):
- return a
- return None
+ fname = f"_artifact_{base_name}.png"
+ files.append(fname)
+ bytes_map[fname] = artifact
+ return files, bytes_map
def save_turn(session_id: str, turn_id: str, user_message: dict[str, Any],
@@ -263,12 +263,9 @@ def save_turn(session_id: str, turn_id: str, user_message: dict[str, Any],
_ensure_dir(turn_dir)
for tc in tool_calls:
- for fname in list(tc.get("result_files", [])):
- img_bytes = _find_artifact_bytes(messages, fname)
- if img_bytes:
- (turn_dir / fname).write_bytes(img_bytes)
- else:
- tc["result_files"].remove(fname)
+ image_bytes_map = tc.pop("_image_bytes", {})
+ for fname, img_bytes in image_bytes_map.items():
+ (turn_dir / fname).write_bytes(img_bytes)
record = {
"session_id": session_id,
@@ -310,14 +307,40 @@ def get_tool_calls_for_latest_turn(session_id: str) -> list[dict[str, Any]]:
latest = get_latest_turn(session_id)
if not latest:
return []
- turn_id = latest.get("turn_id", "")
+ return _tool_calls_from_turn_record(latest, session_id)
+
+
+def get_tool_calls_for_turn(session_id: str, turn_id: str) -> list[dict[str, Any]]:
+ """获取指定 turn_id 的工具调用(供 UI 按消息还原)。"""
+ if not turn_id:
+ return []
+ session_dir = session_tool_dir(session_id)
+ json_path = session_dir / f"turn_{turn_id}.json"
+ if not json_path.is_file():
+ return []
+ try:
+ with open(json_path, "r", encoding="utf-8") as f:
+ record = json.load(f)
+ except (json.JSONDecodeError, OSError):
+ return []
+ return _tool_calls_from_turn_record(record, session_id)
+
+
+def _tool_calls_from_turn_record(record: dict[str, Any], session_id: str) -> list[dict[str, Any]]:
+ """从 turn 记录中提取工具调用列表,补充图片路径。"""
+ turn_id = record.get("turn_id", "")
result = []
- for tc in latest.get("tool_calls", []):
+ for tc in record.get("tool_calls", []):
entry = dict(tc)
full_files = []
for fname in tc.get("result_files", []):
full_files.append(str(session_tool_dir(session_id) / turn_id / fname))
entry["result_files_full"] = full_files
+ # result_images 存图片文件路径而非 base64,前端通过 /api/tool-image 读取
+ entry["result_images"] = [
+ f"/api/tool-image?path={fp}&session={turn_id}"
+ for fp in full_files
+ ]
entry["turn_id"] = turn_id
result.append(entry)
return result
diff --git a/src/agent/history/tool_calling/20260618_111759/turn_20260618_111855.json b/src/agent/history/tool_calling/20260618_111759/turn_20260618_111855.json
deleted file mode 100644
index 01635fe..0000000
--- a/src/agent/history/tool_calling/20260618_111759/turn_20260618_111855.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "session_id": "20260618_111759",
- "turn_id": "20260618_111855",
- "user_message": {
- "role": "user",
- "content": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划"
- },
- "tool_calls": [
- {
- "name": "todo_list",
- "args": {
- "steps": [
- "准备食材:2-3个鸡蛋、2个中等大小西红柿、葱花少许、食用油适量、盐适量、糖少许(可选)",
- "处理食材:将鸡蛋打入碗中,加少许盐打散;西红柿洗净切块;葱切成葱花备用",
- "热锅倒油:开中火加热炒锅,倒入适量食用油,待油微热",
- "炒鸡蛋:倒入蛋液,用筷子或铲子快速划散,炒至凝固后盛出备用",
- "炒西红柿:锅中留底油,放入西红柿块翻炒,可用铲子压一压帮助出汁,炒至软烂",
- "调味:根据口味加入适量盐和少许糖提鲜,翻炒均匀",
- "混合:将炒好的鸡蛋倒回锅中,与西红柿翻炒均匀,让鸡蛋吸收汤汁",
- "出锅:撒上葱花点缀,装盘即可享用"
- ]
- },
- "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a2-3\\u4e2a\\u9e21\\u86cb\\u30012\\u4e2a\\u4e2d\\u7b49\\u5927\\u5c0f\\u897f\\u7ea2\\u67ff\\u3001\\u8471\\u82b1\\u5c11\\u8bb8\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\\u3001\\u76d0\\u9002\\u91cf\\u3001\\u7cd6\\u5c11\\u8bb8\\uff08\\u53ef\\u9009\\uff09\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\uff0c\\u52a0\\u5c11\\u8bb8\\u76d0\\u6253\\u6563\\uff1b\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u8471\\u5207\\u6210\\u8471\\u82b1\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\uff0c\\u5f85\\u6cb9\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u7528\\u7b77\\u5b50\\u6216\\u94f2\\u5b50\\u5feb\\u901f\\u5212\\u6563\\uff0c\\u7092\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u5757\\u7ffb\\u7092\\uff0c\\u53ef\\u7528\\u94f2\\u5b50\\u538b\\u4e00\\u538b\\u5e2e\\u52a9\\u51fa\\u6c41\\uff0c\\u7092\\u81f3\\u8f6f\\u70c2\", \"\\u8c03\\u5473\\uff1a\\u6839\\u636e\\u53e3\\u5473\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\u548c\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff0c\\u7ffb\\u7092\\u5747\\u5300\", \"\\u6df7\\u5408\\uff1a\\u5c06\\u7092\\u597d\\u7684\\u9e21\\u86cb\\u5012\\u56de\\u9505\\u4e2d\\uff0c\\u4e0e\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u5747\\u5300\\uff0c\\u8ba9\\u9e21\\u86cb\\u5438\\u6536\\u6c64\\u6c41\", \"\\u51fa\\u9505\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u88c5\\u76d8\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": []}}\n[Todo List]\n- [1/8] [Pending]: 准备食材:2-3个鸡蛋、2个中等大小西红柿、葱花少许、食用油适量、盐适量、糖少许(可选)\n- [2/8] [Pending]: 处理食材:将鸡蛋打入碗中,加少许盐打散;西红柿洗净切块;葱切成葱花备用\n- [3/8] [Pending]: 热锅倒油:开中火加热炒锅,倒入适量食用油,待油微热\n- [4/8] [Pending]: 炒鸡蛋:倒入蛋液,用筷子或铲子快速划散,炒至凝固后盛出备用\n- [5/8] [Pending]: 炒西红柿:锅中留底油,放入西红柿块翻炒,可用铲子压一压帮助出汁,炒至软烂\n- [6/8] [Pending]: 调味:根据口味加入适量盐和少许糖提鲜,翻炒均匀\n- [7/8] [Pending]: 混合:将炒好的鸡蛋倒回锅中,与西红柿翻炒均匀,让鸡蛋吸收汤汁\n- [8/8] [Pending]: 出锅:撒上葱花点缀,装盘即可享用",
- "result_files": []
- }
- ],
- "assistant_response": "已为您生成详细的**西红柿炒鸡蛋**制作计划,共分为8个步骤:\n\n1. **准备食材**:2-3个鸡蛋、2个中等大小西红柿、葱花少许、食用油适量、盐适量、糖少许(可选)\n2. **处理食材**:将鸡蛋打入碗中,加少许盐打散;西红柿洗净切块;葱切成葱花备用\n3. **热锅倒油**:开中火加热炒锅,倒入适量食用油,待油微热\n4. **炒鸡蛋**:倒入蛋液,用筷子或铲子快速划散,炒至凝固后盛出备用\n5. **炒西红柿**:锅中留底油,放入西红柿块翻炒,可用铲子压一压帮助出汁,炒至软烂\n6. **调味**:根据口味加入适量盐和少许糖提鲜,翻炒均匀\n7. **混合**:将炒好的鸡蛋倒回锅中,与西红柿翻炒均匀,让鸡蛋吸收汤汁\n8. **出锅**:撒上葱花点缀,装盘即可享用\n\n祝您烹饪愉快!如果需要标记某一步骤完成,请告诉我。",
- "timestamp": "20260618_111855"
-}
\ No newline at end of file
diff --git a/src/agent/history/tool_calling/20260618_112443/turn_20260618_112445.json b/src/agent/history/tool_calling/20260618_112443/turn_20260618_112445.json
deleted file mode 100644
index 2a0d3cb..0000000
--- a/src/agent/history/tool_calling/20260618_112443/turn_20260618_112445.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "session_id": "20260618_112443",
- "turn_id": "20260618_112445",
- "user_message": {
- "role": "user",
- "content": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划"
- },
- "tool_calls": [
- {
- "name": "todo_list",
- "args": {
- "steps": [
- "准备食材:2-3个鸡蛋、2-3个中等大小西红柿、葱花适量",
- "准备调料:食用油、盐、白糖(可选,用于提鲜)、番茄酱(可选)",
- "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用",
- "热锅凉油:开中火加热炒锅,倒入适量食用油",
- "炒鸡蛋:油热后倒入蛋液,快速划散至凝固,盛出备用",
- "炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁变软",
- "调味:加入适量盐和少许白糖,喜欢浓郁口感可加一勺番茄酱",
- "混合翻炒:将炒好的鸡蛋倒回锅中,与西红柿均匀翻炒混合",
- "出锅装盘:撒上葱花点缀,即可享用美味的西红柿炒鸡蛋"
- ]
- },
- "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a2-3\\u4e2a\\u9e21\\u86cb\\u30012-3\\u4e2a\\u4e2d\\u7b49\\u5927\\u5c0f\\u897f\\u7ea2\\u67ff\\u3001\\u8471\\u82b1\\u9002\\u91cf\", \"\\u51c6\\u5907\\u8c03\\u6599\\uff1a\\u98df\\u7528\\u6cb9\\u3001\\u76d0\\u3001\\u767d\\u7cd6\\uff08\\u53ef\\u9009\\uff0c\\u7528\\u4e8e\\u63d0\\u9c9c\\uff09\\u3001\\u756a\\u8304\\u9171\\uff08\\u53ef\\u9009\\uff09\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u51c9\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u6cb9\\u70ed\\u540e\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\uff0c\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u518d\\u52a0\\u5c11\\u8bb8\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u5757\\u7ffb\\u7092\\u81f3\\u51fa\\u6c41\\u53d8\\u8f6f\", \"\\u8c03\\u5473\\uff1a\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\u548c\\u5c11\\u8bb8\\u767d\\u7cd6\\uff0c\\u559c\\u6b22\\u6d53\\u90c1\\u53e3\\u611f\\u53ef\\u52a0\\u4e00\\u52fa\\u756a\\u8304\\u9171\", \"\\u6df7\\u5408\\u7ffb\\u7092\\uff1a\\u5c06\\u7092\\u597d\\u7684\\u9e21\\u86cb\\u5012\\u56de\\u9505\\u4e2d\\uff0c\\u4e0e\\u897f\\u7ea2\\u67ff\\u5747\\u5300\\u7ffb\\u7092\\u6df7\\u5408\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\\u7f8e\\u5473\\u7684\\u897f\\u7ea2\\u67ff\\u7092\\u9e21\\u86cb\"], \"done_steps\": []}}\n[Todo List]\n- [1/9] [Pending]: 准备食材:2-3个鸡蛋、2-3个中等大小西红柿、葱花适量\n- [2/9] [Pending]: 准备调料:食用油、盐、白糖(可选,用于提鲜)、番茄酱(可选)\n- [3/9] [Pending]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [4/9] [Pending]: 热锅凉油:开中火加热炒锅,倒入适量食用油\n- [5/9] [Pending]: 炒鸡蛋:油热后倒入蛋液,快速划散至凝固,盛出备用\n- [6/9] [Pending]: 炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁变软\n- [7/9] [Pending]: 调味:加入适量盐和少许白糖,喜欢浓郁口感可加一勺番茄酱\n- [8/9] [Pending]: 混合翻炒:将炒好的鸡蛋倒回锅中,与西红柿均匀翻炒混合\n- [9/9] [Pending]: 出锅装盘:撒上葱花点缀,即可享用美味的西红柿炒鸡蛋",
- "result_files": []
- }
- ],
- "assistant_response": "已为您生成详细的西红柿炒鸡蛋制作计划,共9个步骤:\n\n1. **准备食材**:2-3个鸡蛋、2-3个中等大小西红柿、葱花适量\n2. **准备调料**:食用油、盐、白糖(可选,用于提鲜)、番茄酱(可选)\n3. **处理食材**:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n4. **热锅凉油**:开中火加热炒锅,倒入适量食用油\n5. **炒鸡蛋**:油热后倒入蛋液,快速划散至凝固,盛出备用\n6. **炒西红柿**:锅中再加少许油,放入西红柿块翻炒至出汁变软\n7. **调味**:加入适量盐和少许白糖,喜欢浓郁口感可加一勺番茄酱\n8. **混合翻炒**:将炒好的鸡蛋倒回锅中,与西红柿均匀翻炒混合\n9. **出锅装盘**:撒上葱花点缀,即可享用美味的西红柿炒鸡蛋\n\n祝您烹饪愉快!",
- "timestamp": "20260618_112445"
-}
\ No newline at end of file
diff --git a/src/agent/history/tool_calling/20260618_113558/turn_20260618_113615.json b/src/agent/history/tool_calling/20260618_113558/turn_20260618_113615.json
deleted file mode 100644
index 4668da3..0000000
--- a/src/agent/history/tool_calling/20260618_113558/turn_20260618_113615.json
+++ /dev/null
@@ -1,147 +0,0 @@
-{
- "session_id": "20260618_113558",
- "turn_id": "20260618_113615",
- "user_message": {
- "role": "user",
- "content": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划,并逐步更新这份计划"
- },
- "tool_calls": [
- {
- "name": "todo_list",
- "args": {
- "steps": [
- "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量",
- "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用",
- "热锅倒油:开中火加热炒锅,倒入适量食用油至微热",
- "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用",
- "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)",
- "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁",
- "出锅装盘:撒上葱花点缀,即可享用"
- ]
- },
- "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": []}}\n[Todo List]\n- [1/7] [Pending]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Pending]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Pending]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Pending]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用",
- "result_files": []
- },
- {
- "name": "todo_list",
- "args": {
- "steps": [
- "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量",
- "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用",
- "热锅倒油:开中火加热炒锅,倒入适量食用油至微热",
- "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用",
- "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)",
- "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁",
- "出锅装盘:撒上葱花点缀,即可享用"
- ],
- "done_step": 1
- },
- "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Pending]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Pending]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Pending]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用",
- "result_files": []
- },
- {
- "name": "todo_list",
- "args": {
- "steps": [
- "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量",
- "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用",
- "热锅倒油:开中火加热炒锅,倒入适量食用油至微热",
- "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用",
- "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)",
- "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁",
- "出锅装盘:撒上葱花点缀,即可享用"
- ],
- "done_step": 2
- },
- "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Pending]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Pending]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用",
- "result_files": []
- },
- {
- "name": "todo_list",
- "args": {
- "steps": [
- "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量",
- "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用",
- "热锅倒油:开中火加热炒锅,倒入适量食用油至微热",
- "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用",
- "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)",
- "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁",
- "出锅装盘:撒上葱花点缀,即可享用"
- ],
- "done_step": 3
- },
- "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Pending]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用",
- "result_files": []
- },
- {
- "name": "todo_list",
- "args": {
- "steps": [
- "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量",
- "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用",
- "热锅倒油:开中火加热炒锅,倒入适量食用油至微热",
- "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用",
- "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)",
- "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁",
- "出锅装盘:撒上葱花点缀,即可享用"
- ],
- "done_step": 4
- },
- "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2, 3]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Done]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用",
- "result_files": []
- },
- {
- "name": "todo_list",
- "args": {
- "steps": [
- "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量",
- "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用",
- "热锅倒油:开中火加热炒锅,倒入适量食用油至微热",
- "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用",
- "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)",
- "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁",
- "出锅装盘:撒上葱花点缀,即可享用"
- ],
- "done_step": 5
- },
- "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2, 3, 4]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Done]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Done]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用",
- "result_files": []
- },
- {
- "name": "todo_list",
- "args": {
- "steps": [
- "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量",
- "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用",
- "热锅倒油:开中火加热炒锅,倒入适量食用油至微热",
- "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用",
- "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)",
- "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁",
- "出锅装盘:撒上葱花点缀,即可享用"
- ],
- "done_step": 6
- },
- "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2, 3, 4, 5]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Done]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Done]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Done]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用",
- "result_files": []
- },
- {
- "name": "todo_list",
- "args": {
- "steps": [
- "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量",
- "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用",
- "热锅倒油:开中火加热炒锅,倒入适量食用油至微热",
- "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用",
- "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)",
- "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁",
- "出锅装盘:撒上葱花点缀,即可享用"
- ],
- "done_step": 7
- },
- "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2, 3, 4, 5, 6]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Done]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Done]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Done]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Done]: 出锅装盘:撒上葱花点缀,即可享用",
- "result_files": []
- }
- ],
- "assistant_response": "太棒了!西红柿炒鸡蛋计划已全部完成。\n\n**最终状态:**\n- [x] 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [x] 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [x] 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [x] 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [x] 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [x] 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [x] 出锅装盘:撒上葱花点缀,即可享用\n\n现在你可以享受这道美味的家常菜了!🍅🥚",
- "timestamp": "20260618_113615"
-}
\ No newline at end of file
diff --git a/src/agent/history/tool_calling/20260618_120438/turn_20260618_120441.json b/src/agent/history/tool_calling/20260618_120438/turn_20260618_120441.json
deleted file mode 100644
index 886ed9a..0000000
--- a/src/agent/history/tool_calling/20260618_120438/turn_20260618_120441.json
+++ /dev/null
@@ -1,84 +0,0 @@
-{
- "session_id": "20260618_120438",
- "turn_id": "20260618_120441",
- "user_message": {
- "role": "user",
- "content": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划,并逐步更新这份计划"
- },
- "tool_calls": [
- {
- "name": "todo_list",
- "args": {
- "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]",
- "done_step": -1
- },
- "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': -1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.",
- "result_files": []
- },
- {
- "name": "todo_list",
- "args": {
- "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]",
- "done_step": -1
- },
- "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': -1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.",
- "result_files": []
- },
- {
- "name": "todo_list",
- "args": {
- "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]",
- "done_step": 1
- },
- "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.",
- "result_files": []
- },
- {
- "name": "todo_list",
- "args": {
- "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]",
- "done_step": 1
- },
- "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.",
- "result_files": []
- },
- {
- "name": "todo_list",
- "args": {
- "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]",
- "done_step": 1
- },
- "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.",
- "result_files": []
- },
- {
- "name": "todo_list",
- "args": {
- "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]",
- "done_step": 1
- },
- "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.",
- "result_files": []
- },
- {
- "name": "todo_list",
- "args": {
- "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]",
- "done_step": 1
- },
- "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.",
- "result_files": []
- },
- {
- "name": "todo_list",
- "args": {
- "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]",
- "done_step": 1
- },
- "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.",
- "result_files": []
- }
- ],
- "assistant_response": "",
- "timestamp": "20260618_120441"
-}
\ No newline at end of file
diff --git a/src/agent/history/tool_calling/20260618_173653/20260618_173727/_artifact_call_fd502781107945c7a6880635_0.png b/src/agent/history/tool_calling/20260618_173653/20260618_173727/_artifact_call_fd502781107945c7a6880635_0.png
deleted file mode 100644
index 13e1730..0000000
Binary files a/src/agent/history/tool_calling/20260618_173653/20260618_173727/_artifact_call_fd502781107945c7a6880635_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260618_173653/turn_20260618_173727.json b/src/agent/history/tool_calling/20260618_173653/turn_20260618_173727.json
deleted file mode 100644
index 01a3ed4..0000000
--- a/src/agent/history/tool_calling/20260618_173653/turn_20260618_173727.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "session_id": "20260618_173653",
- "turn_id": "20260618_173727",
- "user_message": {
- "role": "user",
- "content": "帮我关掉DeepSeek开放平台的标签页"
- },
- "tool_calls": [
- {
- "name": "gui_tool",
- "args": {
- "action": "click",
- "element_description": "DeepSeek开放平台的标签页关闭按钮(X)"
- },
- "result_text": "Clicked target. grounded (308,21)",
- "result_files": [
- "_artifact_call_fd502781107945c7a6880635_0.png"
- ]
- }
- ],
- "assistant_response": "已经帮您关闭了 DeepSeek 开放平台的标签页。现在浏览器显示的是 minor - Chatbot 页面。",
- "timestamp": "20260618_173727"
-}
\ No newline at end of file
diff --git a/src/agent/history/tool_calling/20260620_174454/turn_20260620_174456.json b/src/agent/history/tool_calling/20260620_174454/turn_20260620_174456.json
new file mode 100644
index 0000000..b5ed746
--- /dev/null
+++ b/src/agent/history/tool_calling/20260620_174454/turn_20260620_174456.json
@@ -0,0 +1,22 @@
+{
+ "session_id": "20260620_174454",
+ "turn_id": "20260620_174456",
+ "user_message": {
+ "role": "user",
+ "content": "帮我点击加号新建标签页"
+ },
+ "tool_calls": [
+ {
+ "name": "gui_tool",
+ "args": {
+ "action": "click",
+ "element_description": "浏览器标签栏右侧的加号图标,用于新建标签页"
+ },
+ "result_text": "",
+ "result_files": [],
+ "result_images": []
+ }
+ ],
+ "assistant_response": "已为您点击新建标签页。现在应该已经打开了一个新的空白标签页。",
+ "timestamp": "20260620_174456"
+}
\ No newline at end of file
diff --git a/src/agent/history/tool_calling/20260620_183029/20260620_183034/_artifact_gui_tool_0_0.png b/src/agent/history/tool_calling/20260620_183029/20260620_183034/_artifact_gui_tool_0_0.png
new file mode 100644
index 0000000..b9958e1
Binary files /dev/null and b/src/agent/history/tool_calling/20260620_183029/20260620_183034/_artifact_gui_tool_0_0.png differ
diff --git a/src/agent/history/tool_calling/20260620_183029/20260620_183128/_artifact_gui_tool_0_0.png b/src/agent/history/tool_calling/20260620_183029/20260620_183128/_artifact_gui_tool_0_0.png
new file mode 100644
index 0000000..e431d64
Binary files /dev/null and b/src/agent/history/tool_calling/20260620_183029/20260620_183128/_artifact_gui_tool_0_0.png differ
diff --git a/src/agent/history/tool_calling/20260620_183029/turn_20260620_183034.json b/src/agent/history/tool_calling/20260620_183029/turn_20260620_183034.json
new file mode 100644
index 0000000..59962de
--- /dev/null
+++ b/src/agent/history/tool_calling/20260620_183029/turn_20260620_183034.json
@@ -0,0 +1,23 @@
+{
+ "session_id": "20260620_183029",
+ "turn_id": "20260620_183034",
+ "user_message": {
+ "role": "user",
+ "content": "截张图看看我在干啥,如果读不到图片请直接告诉我"
+ },
+ "tool_calls": [
+ {
+ "name": "gui_tool",
+ "args": {
+ "action": "screenshot"
+ },
+ "result_text": "",
+ "result_files": [
+ "_artifact_gui_tool_0_0.png"
+ ],
+ "result_images": []
+ }
+ ],
+ "assistant_response": "根据截图,你正在使用 **DeepSeek 开放平台**(platform.deepseek.com)的网页界面。\n\n具体来说,你当前查看的是 **“用量信息”** 页面。页面上显示了以下关键信息:\n* **充值余额**:¥22.77 CNY\n* **六月消费**(按 UTC+0 时间):¥17.22 CNY\n* **每月用量图表**:展示了近期(6月1日至6月30日)的消费金额波动。\n* **模型使用情况**:下方还列出了 `deepseek-v4-pro` 模型的 API 请求次数和 Tokens 消耗量。\n\n简单来说,你正在查看自己在 DeepSeek 平台上的账户余额、消费记录以及 API 的使用情况。",
+ "timestamp": "20260620_183034"
+}
\ No newline at end of file
diff --git a/src/agent/history/tool_calling/20260620_183029/turn_20260620_183128.json b/src/agent/history/tool_calling/20260620_183029/turn_20260620_183128.json
new file mode 100644
index 0000000..dadcffc
--- /dev/null
+++ b/src/agent/history/tool_calling/20260620_183029/turn_20260620_183128.json
@@ -0,0 +1,23 @@
+{
+ "session_id": "20260620_183029",
+ "turn_id": "20260620_183128",
+ "user_message": {
+ "role": "user",
+ "content": "你现在的对话记录中有图片吗"
+ },
+ "tool_calls": [
+ {
+ "name": "gui_tool",
+ "args": {
+ "action": "screenshot"
+ },
+ "result_text": "",
+ "result_files": [
+ "_artifact_gui_tool_0_0.png"
+ ],
+ "result_images": []
+ }
+ ],
+ "assistant_response": "根据刚才截取的屏幕截图,我**没有**在当前的对话记录中看到任何图片。\n\n这张截图显示的是你正在浏览 **DeepSeek 开放平台** 的“用量信息”页面,主要内容包括:\n1. **充值余额**:¥22.77 CNY\n2. **六月消费**(按 UTC+0 时间):¥17.22 CNY\n3. **每月用量图表**:展示了近期每天的消费金额波动。\n4. **模型使用情况**:下方列出了 `deepseek-v4-pro` 模型的 API 请求次数和 Tokens 消耗量。\n\n所以,目前的对话中只有文字交互,并没有上传图片。",
+ "timestamp": "20260620_183128"
+}
\ No newline at end of file
diff --git a/src/agent/history/tool_calling/20260620_205109/20260620_205218/_artifact_browser_control_0_0.png b/src/agent/history/tool_calling/20260620_205109/20260620_205218/_artifact_browser_control_0_0.png
new file mode 100644
index 0000000..38a8de7
Binary files /dev/null and b/src/agent/history/tool_calling/20260620_205109/20260620_205218/_artifact_browser_control_0_0.png differ
diff --git a/src/agent/history/tool_calling/20260620_205109/20260620_205218/_artifact_browser_control_1_0.png b/src/agent/history/tool_calling/20260620_205109/20260620_205218/_artifact_browser_control_1_0.png
new file mode 100644
index 0000000..99b6bde
Binary files /dev/null and b/src/agent/history/tool_calling/20260620_205109/20260620_205218/_artifact_browser_control_1_0.png differ
diff --git a/src/agent/history/tool_calling/20260620_205109/20260621_113525/_artifact_browser_control_0_0.png b/src/agent/history/tool_calling/20260620_205109/20260621_113525/_artifact_browser_control_0_0.png
new file mode 100644
index 0000000..f48232f
Binary files /dev/null and b/src/agent/history/tool_calling/20260620_205109/20260621_113525/_artifact_browser_control_0_0.png differ
diff --git a/src/agent/history/tool_calling/20260620_205109/20260621_113525/_artifact_browser_control_1_0.png b/src/agent/history/tool_calling/20260620_205109/20260621_113525/_artifact_browser_control_1_0.png
new file mode 100644
index 0000000..7f2bd13
Binary files /dev/null and b/src/agent/history/tool_calling/20260620_205109/20260621_113525/_artifact_browser_control_1_0.png differ
diff --git a/src/agent/history/tool_calling/20260620_205109/turn_20260620_205218.json b/src/agent/history/tool_calling/20260620_205109/turn_20260620_205218.json
new file mode 100644
index 0000000..265f684
--- /dev/null
+++ b/src/agent/history/tool_calling/20260620_205109/turn_20260620_205218.json
@@ -0,0 +1,36 @@
+{
+ "session_id": "20260620_205109",
+ "turn_id": "20260620_205218",
+ "user_message": {
+ "role": "user",
+ "content": "能不能帮我打开虎牙直播"
+ },
+ "tool_calls": [
+ {
+ "name": "browser_control",
+ "args": {
+ "action": "open",
+ "url": "https://www.huya.com"
+ },
+ "result_text": "已尝试打开 URL: https://www.huya.com",
+ "result_files": [
+ "_artifact_browser_control_0_0.png"
+ ],
+ "result_images": []
+ },
+ {
+ "name": "browser_control",
+ "args": {
+ "action": "open",
+ "url": "https://www.huya.com"
+ },
+ "result_text": "已尝试打开 URL: https://www.huya.com",
+ "result_files": [
+ "_artifact_browser_control_1_0.png"
+ ],
+ "result_images": []
+ }
+ ],
+ "assistant_response": "虎牙直播已经成功打开了!可以看到:\n\n- 页面顶部显示\"虎牙直播\"的Logo\n- 当前正在播放主播\"王子酱\"的直播间\n- 页面下方展示了各种游戏分类,包括网游竞技(LOL、CS2等)、单机热游等\n- 右侧还有其他推荐的主播和直播间\n\n你现在可以浏览你感兴趣的直播内容了。如果你想观看特定的主播或游戏类别,可以直接点击相应的选项。",
+ "timestamp": "20260620_205218"
+}
\ No newline at end of file
diff --git a/src/agent/history/tool_calling/20260620_205109/turn_20260621_113525.json b/src/agent/history/tool_calling/20260620_205109/turn_20260621_113525.json
new file mode 100644
index 0000000..8986033
--- /dev/null
+++ b/src/agent/history/tool_calling/20260620_205109/turn_20260621_113525.json
@@ -0,0 +1,36 @@
+{
+ "session_id": "20260620_205109",
+ "turn_id": "20260621_113525",
+ "user_message": {
+ "role": "user",
+ "content": "帮我打开b站"
+ },
+ "tool_calls": [
+ {
+ "name": "browser_control",
+ "args": {
+ "action": "open",
+ "url": "https://www.bilibili.com"
+ },
+ "result_text": "已尝试打开 URL: https://www.bilibili.com",
+ "result_files": [
+ "_artifact_browser_control_0_0.png"
+ ],
+ "result_images": []
+ },
+ {
+ "name": "browser_control",
+ "args": {
+ "action": "open",
+ "url": "https://www.bilibili.com"
+ },
+ "result_text": "已尝试打开 URL: https://www.bilibili.com",
+ "result_files": [
+ "_artifact_browser_control_1_0.png"
+ ],
+ "result_images": []
+ }
+ ],
+ "assistant_response": "好的,B站(bilibili)已经成功打开。\n\n页面显示了:\n* **顶部导航栏**:包含首页、番剧、直播、游戏中心等主要分区。\n* **推荐内容**:首页展示了多个视频推荐,例如左侧的“BML-PLAY! 2025”活动预告,以及右侧的“GPT如何解决80年未解单位距离猜想?”等热门视频。\n* **分类标签**:可以看到动画、番剧、国创、音乐、舞蹈、游戏等多个内容分类。\n\n你现在可以浏览你感兴趣的内容了。",
+ "timestamp": "20260621_113525"
+}
\ No newline at end of file
diff --git a/src/agent/history/tool_calling/20260621_114011/20260621_114012/_artifact_browser_control_0_0.png b/src/agent/history/tool_calling/20260621_114011/20260621_114012/_artifact_browser_control_0_0.png
new file mode 100644
index 0000000..0afa92c
Binary files /dev/null and b/src/agent/history/tool_calling/20260621_114011/20260621_114012/_artifact_browser_control_0_0.png differ
diff --git a/src/agent/history/tool_calling/20260621_114011/turn_20260621_114012.json b/src/agent/history/tool_calling/20260621_114011/turn_20260621_114012.json
new file mode 100644
index 0000000..3bfa30d
--- /dev/null
+++ b/src/agent/history/tool_calling/20260621_114011/turn_20260621_114012.json
@@ -0,0 +1,24 @@
+{
+ "session_id": "20260621_114011",
+ "turn_id": "20260621_114012",
+ "user_message": {
+ "role": "user",
+ "content": "帮我打开b站"
+ },
+ "tool_calls": [
+ {
+ "name": "browser_control",
+ "args": {
+ "action": "open",
+ "url": "https://www.bilibili.com"
+ },
+ "result_text": "已尝试打开 URL: https://www.bilibili.com",
+ "result_files": [
+ "_artifact_browser_control_0_0.png"
+ ],
+ "result_images": []
+ }
+ ],
+ "assistant_response": "B站(哔哩哔哩)已经为您打开了。请问有什么我可以帮您的吗?",
+ "timestamp": "20260621_114012"
+}
\ No newline at end of file
diff --git a/src/agent/llm.py b/src/agent/llm.py
deleted file mode 100644
index a88b66b..0000000
--- a/src/agent/llm.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from agent.core.llm import ChatQwen, llm
-
-__all__ = ["ChatQwen", "llm"]
diff --git a/src/agent/memory/__pycache__/__init__.cpython-313.pyc b/src/agent/memory/__pycache__/__init__.cpython-313.pyc
deleted file mode 100644
index a2d3427..0000000
Binary files a/src/agent/memory/__pycache__/__init__.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc b/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc
index 2bd94a4..382e635 100644
Binary files a/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc and b/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc differ
diff --git a/src/agent/memory/__pycache__/system_prompt.cpython-313.pyc b/src/agent/memory/__pycache__/system_prompt.cpython-313.pyc
deleted file mode 100644
index a3d7fa1..0000000
Binary files a/src/agent/memory/__pycache__/system_prompt.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/memory/system_prompt.py b/src/agent/memory/system_prompt.py
index 1e0040d..7090c2c 100644
--- a/src/agent/memory/system_prompt.py
+++ b/src/agent/memory/system_prompt.py
@@ -1,10 +1,14 @@
-MAIN_SYSTEM_PROMPT = """你是一个多模态助手,能够直接理解用户上传的图片、语音等内容。
- 【关键行为准则】
- - 你的默认行为是直接回答用户问题,不要调用任何工具。
- - 只有当某个信息是你**绝对无法从当前对话或上传内容中获取**,且该信息必须通过外部工具才能获得时,才允许调用工具。
- - 对于任何图片/音频输入,请始终**直接**利用你的视觉/听觉能力回答,**绝对不要**调用截图、OCR、日期查询等无关工具。
- - 如果你不确定是否需要工具,选择**不调用**,直接给出你能给出的最佳答案。
- - 违反上述规则是一种严重错误。
+MAIN_SYSTEM_PROMPT = """你是一个具备多模态理解能力的桌面操作助手,能够直接理解用户上传的图片、语音和文件,并通过调用工具完成桌面自动化任务。
+
+【核心行为准则】
+- 你的首要职责是理解用户的意图,并高效地完成用户的任务。
+- 对于任何用户上传的图片、音频或文件,直接利用你的多模态能力分析并回答,不要为此调用截图、OCR 等无关工具。
+- 只有在信息确实无法从当前对话或上传内容中获取,且必须通过外部工具才能获得时,才调用工具。例如:需要查看当前屏幕状态、需要执行 GUI 操作、需要搜索网络信息等。
+- 当你调用工具收到截图或返回结果后,必须仔细观察返回内容,基于实际结果继续推进任务,严禁脱离实际结果凭空编造。
+- 每次工具调用后,向用户简要汇报当前进度和下一步计划,确保用户始终了解任务状态。
+- 如果不确定是否需要工具,优先选择不调用,用已有信息给出最佳答案。
+- 对于需要多次操作才能完成的任务,应主动使用 todo_list 工具规划步骤并逐步推进。
+- 当任务完成时,向用户总结执行过程和结果。
""".strip()
SUMMARY_SYSTEM_PROMPT ="""
@@ -38,12 +42,16 @@ SUMMARY_SYSTEM_PROMPT ="""
VISION_SYSTEM_PROMPT = """
- 你是专业的图像理解助手。
- 首先请用客观、准确的中文描述用户提供的图片,覆盖主要物体、场景、人物动作、界面元素、图表数据与清晰可读的文字;
- 涉及到你认识的具体品牌、时间、地点、人物、影视剧、物品等特定内容时请直接命名说明;
- 如果用户提供了多张图片,请分别编号描述(如「图1」「图2」等),便于后续问答引用。
- 看不清的内容请如实说明,不要臆测。
- 其次请认真回答用户的问题,结合图片内容进行分析推理,给出具体、简短、清晰、实用的回复;
+ 你是专业的图像理解助手。你的任务是对用户提供的图片进行准确、全面的描述。
+
+ 【描述要求】
+ 1. 用客观、准确的中文描述图片内容,包括主要物体、场景、人物动作、界面元素、图表数据和清晰可读的文字。
+ 2. 涉及具体品牌、时间、地点、人物、影视剧名称、知名物品等特定内容时,请直接命名说明。
+ 3. 如果用户提供了多张图片,请分别编号描述(如「图1」「图2」等),便于后续问答引用。
+ 4. 看不清或无法确定的内容请如实说明,不要臆测或编造。
+
+ 【回答要求】
+ 在完成图片描述后,请认真回答用户的问题,结合图片内容进行分析推理,给出具体、简短、清晰、实用的回复。
""".strip()
DB_SYSTEM_PROMT = """
@@ -68,6 +76,6 @@ DB_SYSTEM_PROMT = """
""".strip()
TEAM_MODE_PROMPT = """
-team 模式目前仅作为多 agent 协同的预留模式,尚未实现。
-请简短说明当前无法启动 team 协同,并建议用户改用 agent 模式。
+ Team 模式目前处于预留阶段,多 Agent 协同功能尚未实现。
+ 请向用户说明当前无法启动 Team 协同模式,并建议使用 Agent 模式来完成单 Agent 任务。
""".strip()
diff --git a/src/agent/tools/__init__.py b/src/agent/tools/__init__.py
index ade392b..acae83e 100644
--- a/src/agent/tools/__init__.py
+++ b/src/agent/tools/__init__.py
@@ -2,11 +2,11 @@ from .tool import *
from .tool_software_control import SoftwareControl
from .gui_tool import GUITool
from .tool_rag import RAGTool
+from .tool_agent_call import CallSubAgentTool
_all_tools = [
WebSearch(),
DateQuery(),
- ScreenCapture(),
BrowserControl(),
HumanInteraction(),
SoftwareControl(),
@@ -14,6 +14,7 @@ _all_tools = [
DirectoryListing(),
TodoList(),
GUITool(),
+ CallSubAgentTool(),
]
# 注册所有可用工具名称(供前端配置面板展示),从 _all_tools 实例推导
diff --git a/src/agent/tools/__pycache__/__init__.cpython-312.pyc b/src/agent/tools/__pycache__/__init__.cpython-312.pyc
index 9f5f68b..48fa3e2 100644
Binary files a/src/agent/tools/__pycache__/__init__.cpython-312.pyc and b/src/agent/tools/__pycache__/__init__.cpython-312.pyc differ
diff --git a/src/agent/tools/__pycache__/__init__.cpython-313.pyc b/src/agent/tools/__pycache__/__init__.cpython-313.pyc
deleted file mode 100644
index 100e5fe..0000000
Binary files a/src/agent/tools/__pycache__/__init__.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/tools/__pycache__/gui_tool.cpython-312.pyc b/src/agent/tools/__pycache__/gui_tool.cpython-312.pyc
index aaa13e8..af869ac 100644
Binary files a/src/agent/tools/__pycache__/gui_tool.cpython-312.pyc and b/src/agent/tools/__pycache__/gui_tool.cpython-312.pyc differ
diff --git a/src/agent/tools/__pycache__/tool.cpython-312.pyc b/src/agent/tools/__pycache__/tool.cpython-312.pyc
index 92361e1..2d06923 100644
Binary files a/src/agent/tools/__pycache__/tool.cpython-312.pyc and b/src/agent/tools/__pycache__/tool.cpython-312.pyc differ
diff --git a/src/agent/tools/__pycache__/tool.cpython-313.pyc b/src/agent/tools/__pycache__/tool.cpython-313.pyc
deleted file mode 100644
index 3dae7d2..0000000
Binary files a/src/agent/tools/__pycache__/tool.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/tools/__pycache__/tool_agent_call.cpython-312.pyc b/src/agent/tools/__pycache__/tool_agent_call.cpython-312.pyc
index bc2aeb1..09c51f6 100644
Binary files a/src/agent/tools/__pycache__/tool_agent_call.cpython-312.pyc and b/src/agent/tools/__pycache__/tool_agent_call.cpython-312.pyc differ
diff --git a/src/agent/tools/__pycache__/tool_database.cpython-312.pyc b/src/agent/tools/__pycache__/tool_database.cpython-312.pyc
deleted file mode 100644
index 5a32225..0000000
Binary files a/src/agent/tools/__pycache__/tool_database.cpython-312.pyc and /dev/null differ
diff --git a/src/agent/tools/__pycache__/tool_database.cpython-313.pyc b/src/agent/tools/__pycache__/tool_database.cpython-313.pyc
deleted file mode 100644
index 93f071c..0000000
Binary files a/src/agent/tools/__pycache__/tool_database.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/tools/__pycache__/tool_rag.cpython-312.pyc b/src/agent/tools/__pycache__/tool_rag.cpython-312.pyc
index 43a58a1..2fc4b41 100644
Binary files a/src/agent/tools/__pycache__/tool_rag.cpython-312.pyc and b/src/agent/tools/__pycache__/tool_rag.cpython-312.pyc differ
diff --git a/src/agent/tools/__pycache__/tool_rag.cpython-313.pyc b/src/agent/tools/__pycache__/tool_rag.cpython-313.pyc
deleted file mode 100644
index 3816508..0000000
Binary files a/src/agent/tools/__pycache__/tool_rag.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/tools/__pycache__/tool_software_control.cpython-313.pyc b/src/agent/tools/__pycache__/tool_software_control.cpython-313.pyc
deleted file mode 100644
index 2240ded..0000000
Binary files a/src/agent/tools/__pycache__/tool_software_control.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/tools/gui_tool.py b/src/agent/tools/gui_tool.py
index fd3d34a..3d17fc5 100644
--- a/src/agent/tools/gui_tool.py
+++ b/src/agent/tools/gui_tool.py
@@ -26,6 +26,7 @@ GUIAction = Literal[
"hotkey",
"hold_and_press",
"wait",
+ "screenshot",
]
@@ -56,8 +57,21 @@ class GUIToolInput(BaseModel):
wait_seconds: float = Field(default=0.5, description="Seconds to wait.")
+def _get_gui_region() -> dict | None:
+ """获取当前 GUI 操作显示器区域,失败返回 None(全屏)。"""
+ try:
+ from agent.core.config_manager import get_active_monitor_region
+ return get_active_monitor_region()
+ except Exception:
+ return None
+
+
def _screenshot_png() -> tuple[bytes, tuple[int, int]]:
- image = pyautogui.screenshot()
+ region = _get_gui_region()
+ if region:
+ image = pyautogui.screenshot(region=(region["x"], region["y"], region["width"], region["height"]))
+ else:
+ image = pyautogui.screenshot()
width, height = image.size
buf = io.BytesIO()
image.save(buf, format="PNG")
@@ -89,12 +103,16 @@ def _parse_coordinates(text: str) -> tuple[int, int]:
def _scale_to_screen(coords: tuple[int, int], screen_size: tuple[int, int]) -> tuple[int, int]:
+ """将 grounding 坐标映射到绝对屏幕坐标(考虑 GUI 操作区域偏移)。"""
screen_width, screen_height = screen_size
x, y = coords
- return (
- round(x * screen_width / GROUNDING_WIDTH),
- round(y * screen_height / GROUNDING_HEIGHT),
- )
+ local_x = round(x * screen_width / GROUNDING_WIDTH)
+ local_y = round(y * screen_height / GROUNDING_HEIGHT)
+ # 加上目标显示器在屏幕空间的偏移
+ region = _get_gui_region()
+ offset_x = region["x"] if region else 0
+ offset_y = region["y"] if region else 0
+ return (offset_x + local_x, offset_y + local_y)
def _ground_element(element_description: str) -> tuple[int, int, str]:
@@ -166,14 +184,15 @@ class GUITool(BaseTool):
description: str = (
"GUI automation tool. Visually locates desktop elements from a screenshot, maps coordinates to the "
"real screen, and executes pyautogui actions. Supported actions: click, type, drag_and_drop, scroll, "
- "hotkey, hold_and_press, wait. If no action is given, returns a screenshot of the current screen."
+ "hotkey, hold_and_press, wait, screenshot. Returns a screenshot of the current screen after the action."
)
response_format: Literal["content", "content_and_artifact"] = "content_and_artifact"
def _artifact_screenshot(self) -> dict:
try:
image_bytes, _ = _screenshot_png()
- return {"image_bytes": image_bytes}
+ resized_bytes = _resize_for_grounding(image_bytes)
+ return {"image_bytes": resized_bytes}
except Exception:
return {}
@@ -194,16 +213,16 @@ class GUITool(BaseTool):
shift: bool = False,
wait_seconds: float = 0.0,
) -> tuple[str, dict]:
- # ---------- 新增:无动作时直接截图 ----------
+ # ---------- screenshot 动作:仅截图 ----------
+ if action == "screenshot":
+ return ("", self._artifact_screenshot())
+ # ---------- 无动作时直接截图 ----------
if not action:
- raw_png, size = _screenshot_png()
- return (
- f"Screenshot captured (size {size[0]}x{size[1]}).",
- self._artifact_screenshot(), # 再次截图,间隔极短,也可直接返回 {"image_bytes": raw_png}
- )
+ return ("", self._artifact_screenshot())
# -------------------------------------------
+ dispatch_error: str | None = None
try:
- message = self._dispatch(
+ self._dispatch(
action=action,
element_description=element_description,
ending_description=ending_description,
@@ -219,10 +238,13 @@ class GUITool(BaseTool):
shift=shift,
wait_seconds=wait_seconds,
)
- time.sleep(0.2)
- return message, self._artifact_screenshot()
except Exception as exc:
- return f"GUI tool action failed: {exc}", self._artifact_screenshot()
+ dispatch_error = f"[GUI Error] {action}: {exc}"
+ print(dispatch_error)
+ time.sleep(0.5)
+ if dispatch_error:
+ return dispatch_error, self._artifact_screenshot()
+ return "", self._artifact_screenshot()
def _dispatch(self, **kwargs: Any) -> str:
action = str(kwargs["action"]).strip()
diff --git a/src/agent/tools/tool.py b/src/agent/tools/tool.py
index 108671d..f1c12fe 100644
--- a/src/agent/tools/tool.py
+++ b/src/agent/tools/tool.py
@@ -11,11 +11,12 @@ import pyautogui
import asyncio
from pathlib import Path
+from PIL import Image
from langchain.tools import BaseTool
from pydantic import BaseModel, Field, create_model, model_validator
from agent.core.llm import llm
-from agent.utils.env_utils import BROWSER_MAP
+from agent.utils.env_utils import BROWSER_MAP, GROUNDING_HEIGHT, GROUNDING_WIDTH
class WebSearch(BaseTool):
@@ -61,56 +62,6 @@ class DateQuery(BaseTool):
return self._run()
-class ScreenCapture(BaseTool):
- """截取当前屏幕,生成文字描述并同时提供原始截图供模型直接查看。"""
-
- args_schema: type[BaseModel] = create_model(
- "ScreenCaptureInput",
- focus_query=(
- Optional[str],
- Field(
- default=None,
- description="可选。希望重点观察的界面或问题,例如「错误提示全文」「浏览器地址栏」;留空则整体描述屏幕。",
- ),
- ),
- )
- name: str = "screen_capture"
- description: str = (
- "截取当前主屏幕画面。除了返回文字描述外,也会提供原始截图给模型进行视觉分析。"
- "注意:这个工具仅用于获取当前屏幕的实时画面,其余任何情况严禁调用。"
- )
- response_format: Literal["content", "content_and_artifact"] = "content_and_artifact"
-
- def _run(self, focus_query: Optional[str] = None) -> Tuple[str, dict]:
- # 1. 截屏
- try:
- pil_img = pyautogui.screenshot()
- buf = io.BytesIO()
- pil_img.save(buf, format="PNG")
- raw_png = buf.getvalue()
- except Exception as e:
- return f"截屏失败: {e}", {}
-
- query = (focus_query or "").strip()
-
- if query: # focus_query 存在且非空
- # 生成视觉描述,返回文字内容,不返回图片字节
- try:
- description = llm.describe_image_bytes(raw_png, prompt=query)
- except Exception as e:
- description = f"屏幕图像视觉解析失败: {e}"
- if not description.strip():
- description = "(未返回有效描述)"
- content_text = f"【当前屏幕视觉描述】\n{description.strip()}"
- return content_text, {}
- else:
- # 不生成描述,直接返回图片字节
- artifact = {"image_bytes": raw_png}
- return "", artifact
-
- async def _arun(self, focus_query: Optional[str] = None) -> Tuple[str, dict]:
- return await asyncio.to_thread(self._run, focus_query)
-
class BrowserControl(BaseTool):
"""统一浏览器控制工具:支持打开/关闭浏览器、刷新/关闭标签页、打开指定 URL"""
@@ -179,6 +130,13 @@ class BrowserControl(BaseTool):
pyautogui.hotkey(*keys)
# ---------- 核心操作(返回操作结果描述) ----------
+ def _try_activate_any_browser(self) -> bool:
+ """尝试激活任意已知浏览器窗口(不指定 browser 时的回退方案)。"""
+ for _reg_name, title_keyword in BROWSER_MAP.values():
+ if self._activate_window(title_keyword):
+ return True
+ return False
+
def _open_action(self, url: Optional[str], browser: Optional[str]) -> str:
"""执行打开首页、刷新当前页或打开指定 URL。"""
controller, title_keyword = self._get_browser_info(browser)
@@ -186,6 +144,8 @@ class BrowserControl(BaseTool):
controller.open("about:blank")
if title_keyword:
self._activate_window(title_keyword)
+ else:
+ self._try_activate_any_browser()
browser_info = f" (使用 {browser})" if browser else ""
return f"已打开浏览器首页{browser_info}"
elif url.lower() == "current":
@@ -202,6 +162,10 @@ class BrowserControl(BaseTool):
controller.open(url)
if title_keyword:
self._activate_window(title_keyword)
+ else:
+ self._try_activate_any_browser()
+ # 等待浏览器窗口出现并开始加载
+ time.sleep(1.0)
return f"已尝试打开 URL: {url}"
def _close_action(self, url: Optional[str], browser: Optional[str]) -> str:
@@ -211,6 +175,8 @@ class BrowserControl(BaseTool):
if title_keyword:
if not self._activate_window(title_keyword):
return f"未找到 {browser or '默认'} 浏览器窗口,无法关闭"
+ else:
+ self._try_activate_any_browser()
time.sleep(0.2)
if sys.platform == "darwin":
self._send_keys("command", "q")
@@ -221,6 +187,8 @@ class BrowserControl(BaseTool):
if title_keyword:
if not self._activate_window(title_keyword):
return f"未找到 {browser or '默认'} 浏览器窗口,无法关闭标签页"
+ else:
+ self._try_activate_any_browser()
time.sleep(0.2)
if sys.platform == "darwin":
self._send_keys("command", "w")
@@ -240,9 +208,11 @@ class BrowserControl(BaseTool):
except Exception as e:
msg = f"执行浏览器操作时出错: {e}"
- # 无论操作是否成功,都尝试截屏
+ # 无论操作是否成功,都尝试截屏(等待窗口切换)
+ time.sleep(0.3)
try:
pil_img = pyautogui.screenshot()
+ pil_img = pil_img.resize((GROUNDING_WIDTH, GROUNDING_HEIGHT), Image.Resampling.LANCZOS)
buf = io.BytesIO()
pil_img.save(buf, format="PNG")
raw_png = buf.getvalue()
diff --git a/src/agent/tools/tool_agent_call.py b/src/agent/tools/tool_agent_call.py
index b705d4f..84d258c 100644
--- a/src/agent/tools/tool_agent_call.py
+++ b/src/agent/tools/tool_agent_call.py
@@ -1,52 +1,241 @@
-"""Agent 委派工具:主 Agent 通过此工具将任务委派给子 Agent。
+"""Agent 委派工具:主 Agent 通过 call_subagent 工具将任务委派给子 Agent 执行。
系统定位:
- 作为主 Agent 工具集中的特殊工具,调用此工具将触发
- SupervisorGraph 将执行路由到对应子 Agent。
+ 封装在 ReAct 图的工具集中,使主 Agent 可以像调用普通工具一样
+ 调用子 Agent 执行子任务。支持一轮中发起多个 call_subagent 调用,
+ 结合并行 ToolNode 实现子 Agent 并行执行。
工具签名:
- agent_call(agent_name, task_description)
+ call_subagent(agent_name, task_description) -> str
+
+并行说明:
+ 将主 agent 图的 ToolNode 替换为 parallel_tool_node(本模块提供),
+ 即可在一次 LLM 响应中的多个 call_subagent 被并行执行。
"""
from __future__ import annotations
-from typing import Literal
+import concurrent.futures
+from typing import Any
from langchain.tools import BaseTool
-from pydantic import BaseModel, Field, create_model
+from langchain_core.messages import HumanMessage, SystemMessage
+from pydantic import BaseModel, Field
+
+from agent.core.state import AgentState
-class AgentCallInput(BaseModel):
- """agent_call 工具的输入参数。"""
+class CallSubAgentInput(BaseModel):
+ """call_subagent 工具的输入参数。"""
agent_name: str = Field(
- description="要委派的子 Agent 名称,例如 'code_agent'、'research_agent'。"
+ description="要调用的子 Agent 名称。必须是 agent_config.json 中已启用且 role='sub' 的 Agent。"
)
task_description: str = Field(
- description="委派给子 Agent 的详细任务描述,子 Agent 将根据此描述执行任务。"
+ description="委派给子 Agent 的详细任务描述,子 Agent 将根据此描述独立执行任务。"
)
-class AgentCallTool(BaseTool):
- """主 Agent 委派任务给子 Agent 的工具。
+class CallSubAgentTool(BaseTool):
+ """主 Agent 调用子 Agent 执行任务的工具。
- 调用后 SupervisorGraph 将暂停主 Agent、激活对应子 Agent,
- 子 Agent 完成后结果回传给主 Agent。
+ 调用后直接运行子 Agent 的编译图,返回最终结果。
+ 子 Agent 拥有独立的 system_prompt、工具集和 LLM,完全独立执行。
"""
- args_schema: type[BaseModel] = AgentCallInput
- name: str = "agent_call"
+ args_schema: type[BaseModel] = CallSubAgentInput
+ name: str = "call_subagent"
description: str = (
- "将子任务委派给指定的子 Agent 执行。"
- "仅在需要专业子 Agent 协助完成特定类型任务时调用。"
- "可用的子 Agent 名称由系统配置决定。"
+ "调用一个已启用的子 Agent 执行指定任务。子 Agent 会独立运行并返回最终结果。"
+ "适用于需要专业能力(如代码分析、文档处理等)进行辅助的场景。"
+ "可用的子 Agent 名称取决于 agent_config.json 中已启用且 role='sub' 的配置。"
)
+ def _get_sub_agent_runtime(self, agent_name: str):
+ """查找并返回已启用的子 Agent 运行时。"""
+ from agent.agents.agent import get_agent_runtime, get_main_agent_runtime
+ runtime = get_agent_runtime(agent_name)
+ if runtime is None:
+ available = []
+ from agent.agents.agent import get_all_agent_runtimes
+ all_rt = get_all_agent_runtimes()
+ for name, rt in all_rt.items():
+ main_rt = get_main_agent_runtime()
+ if main_rt and rt is not main_rt:
+ available.append(name)
+ raise ValueError(
+ f"子 Agent '{agent_name}' 未找到或未启用。"
+ f"当前可用的子 Agent: {available if available else '无'}"
+ )
+ # 确保不是主 agent 自己调自己
+ main_rt = get_main_agent_runtime()
+ if main_rt and runtime is main_rt:
+ raise ValueError(f"不能委派任务给主 Agent 自身 '{agent_name}'")
+ return runtime
+
def _run(self, agent_name: str, task_description: str) -> str:
- """占位实现:实际路由由 SupervisorGraph 处理。"""
- return (
- f"已委派任务给子 Agent [{agent_name}]。\n"
- f"任务描述: {task_description}\n"
- f"等待子 Agent 返回结果..."
- )
+ """同步执行子 Agent 任务并返回结果。"""
+ try:
+ runtime = self._get_sub_agent_runtime(agent_name)
+ except ValueError as e:
+ return f"call_subagent 失败: {e}"
+
+ # 构建子 Agent 的输入消息
+ messages = [
+ SystemMessage(content=runtime.system_prompt),
+ HumanMessage(content=task_description),
+ ]
+
+ # 配置 recursion_limit
+ config = {"recursion_limit": runtime.max_iterations}
+
+ try:
+ result = runtime.graph.invoke({"messages": messages}, config=config)
+ all_messages = result.get("messages", [])
+
+ # 提取最后一条非工具调用的助手消息作为最终结果
+ from langchain_core.messages import AIMessage
+ final_text = ""
+ for msg in reversed(all_messages):
+ if isinstance(msg, AIMessage) and not getattr(msg, "tool_calls", None):
+ content = msg.content
+ if isinstance(content, str):
+ final_text = content
+ break
+ elif isinstance(content, list):
+ # 多模态内容,提取文本部分
+ text_parts = [
+ p.get("text", "") for p in content
+ if isinstance(p, dict) and p.get("type") == "text"
+ ]
+ final_text = "\n".join(text_parts)
+ break
+
+ if not final_text:
+ final_text = "子 Agent 已完成任务但未返回文字结果。"
+
+ return f"[子Agent: {agent_name}] 执行结果:\n{final_text}"
+
+ except Exception as e:
+ return f"[子Agent: {agent_name}] 执行出错: {str(e)}"
async def _arun(self, agent_name: str, task_description: str) -> str:
- return self._run(agent_name, task_description)
+ """异步执行(委托给同步实现)。"""
+ import asyncio
+ return await asyncio.to_thread(self._run, agent_name, task_description)
+
+
+# ---------- 并行工具节点 ----------
+def make_parallel_tool_node(tools: list[Any], max_workers: int = 5):
+ """创建并行工具执行节点,替换 LangGraph 默认的 ToolNode。
+
+ 输入:
+ tools: LangChain BaseTool 列表。
+ max_workers: 线程池最大并发数。
+
+ 输出:
+ LangGraph 节点函数(签名兼容 ToolNode)。
+
+ 系统定位:
+ 在 build_agent_graph 中替代 ToolNode,使一轮内的多个工具调用
+ (包括多个 call_subagent)可以并行执行,显著提升多子 Agent 场景效率。
+
+ 可扩展性:
+ 可增加超时控制、错误隔离(单个工具失败不影响其他)。
+ """
+
+ # 构建工具名 → 工具实例的映射
+ tools_by_name: dict[str, BaseTool] = {t.name: t for t in tools}
+
+ def _execute_single(tool_name: str, tool_args: dict, tool_call_id: str = "") -> tuple[str, str, Any]:
+ """在线程池中执行单个工具。
+
+ 构造完整的 ToolCall 格式,确保 BaseTool 能正确识别 content_and_artifact。
+ 返回 (tool_name, content, artifact),artifact 为 None 表示无产物。
+ """
+ from langchain_core.messages import ToolMessage
+
+ tool = tools_by_name.get(tool_name)
+ if tool is None:
+ return tool_name, f"工具 '{tool_name}' 未找到", None
+ try:
+ # 构造完整 ToolCall 格式,使 BaseTool._prep_run_args 能提取 tool_call_id,
+ # 进而 _format_output 会正确包装 ToolMessage(保留 artifact)
+ tool_call_input = {
+ "name": tool_name,
+ "args": tool_args,
+ "id": tool_call_id or "",
+ "type": "tool_call",
+ }
+ result = tool.invoke(tool_call_input)
+ if isinstance(result, ToolMessage):
+ content = str(result.content) if result.content else ""
+ artifact = getattr(result, "artifact", None)
+ return tool_name, content, artifact
+ if isinstance(result, tuple):
+ content = str(result[0]) if result[0] else ""
+ artifact = result[1] if len(result) > 1 else None
+ return tool_name, content, artifact
+ return tool_name, str(result), None
+ except Exception as e:
+ return tool_name, f"工具执行出错: {e}", None
+
+ def parallel_tool_node(state: AgentState) -> dict:
+ """并行执行最后一条 AIMessage 中的所有 tool_calls。"""
+ from langchain_core.messages import AIMessage, ToolMessage
+
+ last_msg = state["messages"][-1]
+ if not isinstance(last_msg, AIMessage) or not last_msg.tool_calls:
+ return {}
+
+ tool_calls = last_msg.tool_calls
+
+ # 只有 1 个工具调用时直接串行执行,避免线程开销
+ if len(tool_calls) == 1:
+ tc = tool_calls[0]
+ tc_name = tc.get("name") if isinstance(tc, dict) else getattr(tc, "name", "")
+ tc_args = tc.get("args") if isinstance(tc, dict) else getattr(tc, "args", {})
+ tc_id = tc.get("id") if isinstance(tc, dict) else getattr(tc, "id", "")
+ name, content, artifact = _execute_single(tc_name, tc_args, tc_id)
+ msg_kwargs = {"content": str(content), "tool_call_id": tc_id, "name": name}
+ if artifact:
+ msg_kwargs["artifact"] = artifact
+ return {
+ "messages": [ToolMessage(**msg_kwargs)]
+ }
+
+ # 多个工具调用:线程池并行执行
+ futures: dict[concurrent.futures.Future, dict] = {}
+ with concurrent.futures.ThreadPoolExecutor(max_workers=min(max_workers, len(tool_calls))) as executor:
+ for tc in tool_calls:
+ tc_name = tc.get("name") if isinstance(tc, dict) else getattr(tc, "name", "")
+ tc_args = tc.get("args") if isinstance(tc, dict) else getattr(tc, "args", {})
+ tc_id = tc.get("id") if isinstance(tc, dict) else getattr(tc, "id", "")
+ future = executor.submit(_execute_single, tc_name, tc_args, tc_id)
+ futures[future] = {"id": tc_id, "name": tc_name}
+
+ # 收集结果,保持顺序
+ tool_messages = []
+ for tc in tool_calls:
+ tc_id = tc.get("id") if isinstance(tc, dict) else getattr(tc, "id", "")
+ tc_name = tc.get("name") if isinstance(tc, dict) else getattr(tc, "name", "")
+ # 查找对应 future 的结果(无法保持顺序时降级为错误)
+ matched = False
+ for future, meta in dict(futures).items():
+ if meta["id"] == tc_id:
+ _, content, artifact = future.result()
+ msg_kwargs = {"content": str(content), "tool_call_id": tc_id, "name": tc_name}
+ if artifact:
+ msg_kwargs["artifact"] = artifact
+ tool_messages.append(ToolMessage(**msg_kwargs))
+ matched = True
+ break
+ if not matched:
+ # 降级:尝试单次执行
+ name, content, artifact = _execute_single(tc_name, {}, tc_id)
+ msg_kwargs = {"content": str(content), "tool_call_id": tc_id, "name": tc_name}
+ if artifact:
+ msg_kwargs["artifact"] = artifact
+ tool_messages.append(ToolMessage(**msg_kwargs))
+
+ return {"messages": tool_messages}
+
+ return parallel_tool_node
diff --git a/src/agent/utils/__pycache__/__init__.cpython-313.pyc b/src/agent/utils/__pycache__/__init__.cpython-313.pyc
deleted file mode 100644
index 99c4056..0000000
Binary files a/src/agent/utils/__pycache__/__init__.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/utils/__pycache__/agent_utils.cpython-313.pyc b/src/agent/utils/__pycache__/agent_utils.cpython-313.pyc
deleted file mode 100644
index 6fbbddf..0000000
Binary files a/src/agent/utils/__pycache__/agent_utils.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/utils/__pycache__/database_utils.cpython-312.pyc b/src/agent/utils/__pycache__/database_utils.cpython-312.pyc
deleted file mode 100644
index 5479945..0000000
Binary files a/src/agent/utils/__pycache__/database_utils.cpython-312.pyc and /dev/null differ
diff --git a/src/agent/utils/__pycache__/database_utils.cpython-313.pyc b/src/agent/utils/__pycache__/database_utils.cpython-313.pyc
deleted file mode 100644
index 8303bea..0000000
Binary files a/src/agent/utils/__pycache__/database_utils.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/utils/__pycache__/env_utils.cpython-313.pyc b/src/agent/utils/__pycache__/env_utils.cpython-313.pyc
deleted file mode 100644
index bcadbaa..0000000
Binary files a/src/agent/utils/__pycache__/env_utils.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/utils/__pycache__/image_utils.cpython-313.pyc b/src/agent/utils/__pycache__/image_utils.cpython-313.pyc
deleted file mode 100644
index 9815099..0000000
Binary files a/src/agent/utils/__pycache__/image_utils.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/utils/__pycache__/log_utils.cpython-312.pyc b/src/agent/utils/__pycache__/log_utils.cpython-312.pyc
deleted file mode 100644
index c236a80..0000000
Binary files a/src/agent/utils/__pycache__/log_utils.cpython-312.pyc and /dev/null differ
diff --git a/src/agent/utils/__pycache__/log_utils.cpython-313.pyc b/src/agent/utils/__pycache__/log_utils.cpython-313.pyc
deleted file mode 100644
index ad22777..0000000
Binary files a/src/agent/utils/__pycache__/log_utils.cpython-313.pyc and /dev/null differ
diff --git a/src/agent/utils/__pycache__/tool_call_utils.cpython-313.pyc b/src/agent/utils/__pycache__/tool_call_utils.cpython-313.pyc
deleted file mode 100644
index b49d4f7..0000000
Binary files a/src/agent/utils/__pycache__/tool_call_utils.cpython-313.pyc and /dev/null differ
diff --git a/src/web/__pycache__/__init__.cpython-313.pyc b/src/web/__pycache__/__init__.cpython-313.pyc
deleted file mode 100644
index 1537bed..0000000
Binary files a/src/web/__pycache__/__init__.cpython-313.pyc and /dev/null differ
diff --git a/src/web/__pycache__/server.cpython-312.pyc b/src/web/__pycache__/server.cpython-312.pyc
index e470113..b3584f7 100644
Binary files a/src/web/__pycache__/server.cpython-312.pyc and b/src/web/__pycache__/server.cpython-312.pyc differ
diff --git a/src/web/__pycache__/server.cpython-313.pyc b/src/web/__pycache__/server.cpython-313.pyc
deleted file mode 100644
index 2f29706..0000000
Binary files a/src/web/__pycache__/server.cpython-313.pyc and /dev/null differ
diff --git a/src/web/__pycache__/ui_session.cpython-313.pyc b/src/web/__pycache__/ui_session.cpython-313.pyc
deleted file mode 100644
index 919b49e..0000000
Binary files a/src/web/__pycache__/ui_session.cpython-313.pyc and /dev/null differ
diff --git a/src/web/app.css b/src/web/app.css
index 6ba5f26..321b09d 100644
--- a/src/web/app.css
+++ b/src/web/app.css
@@ -1973,6 +1973,29 @@ body {
margin-top: 1.2rem;
}
+/* ===== GUI 显示器设置 ===== */
+.settings-hint {
+ color: #94a3b8;
+ font-size: 0.78rem;
+ margin: 0 0 0.8rem;
+ line-height: 1.4;
+}
+
+.gui-monitors-grid {
+ margin-bottom: 1rem;
+}
+
+.gui-monitor-box {
+ user-select: none;
+}
+.gui-monitor-box:hover {
+ border-color: #818cf8 !important;
+ background: rgba(79,70,229,0.1) !important;
+}
+.gui-monitor-box--selected {
+ box-shadow: 0 0 12px rgba(79,70,229,0.35);
+}
+
/* ===== 模型表格 ===== */
.model-table {
border: 1px solid rgba(148,163,184,0.3);
@@ -2123,120 +2146,210 @@ body {
overflow: hidden;
}
-/* ===== 工具调用气泡(chat 视图) ===== */
-.tool-call-bubble-wrap {
+/* ===== 工具调用展示(折叠式低透明度文本) ===== */
+
+/* 指示器:"agent运行中,详情可展开" */
+.agent-running-indicator {
margin-top: 0.5rem;
- border: 1px solid rgba(148, 163, 184, 0.3);
- border-radius: 10px;
- overflow: hidden;
- background: rgba(255, 255, 255, 0.18);
- transition: background 0.3s;
-}
-
-.tool-call-toggle {
- display: block;
- width: 100%;
- padding: 0.35rem 0.65rem;
- border: none;
- background: rgba(255, 255, 255, 0.3);
- font-size: 0.72rem;
- font-weight: 600;
- color: #475569;
+ padding: 0.3rem 0;
+ border-top: 1px solid rgba(148, 163, 184, 0.12);
cursor: pointer;
- text-align: left;
- transition: background 0.2s;
- font-family: inherit;
+ user-select: none;
+ -webkit-user-select: none;
}
-.tool-call-toggle:hover {
- background: rgba(255, 255, 255, 0.5);
-}
-
-.tool-call-collapsed {
- display: flex;
- flex-wrap: wrap;
- gap: 0.3rem;
- padding: 0.4rem 0.65rem;
-}
-
-.tool-call-bubble-wrap--expanded .tool-call-collapsed {
- display: none;
-}
-
-.tool-call-chip {
+.agent-running-text {
+ font-size: 0.75rem;
+ color: rgba(100, 116, 139, 0.55);
+ font-weight: 400;
+ letter-spacing: 0.02em;
+ transition: color 0.3s;
display: inline-block;
- padding: 0.15rem 0.45rem;
- border-radius: 8px;
- background: rgba(79, 70, 229, 0.1);
- font-size: 0.68rem;
- font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
- color: #4f46e5;
- font-weight: 500;
- transition: opacity 0.5s;
}
-.tool-call-more {
+/* 文字从左到右跳动的动画 */
+.agent-running-text::after {
+ content: " ▸ ▹ ▸";
display: inline-block;
- padding: 0.15rem 0.45rem;
- border-radius: 8px;
- background: rgba(148, 163, 184, 0.15);
- font-size: 0.65rem;
- color: #94a3b8;
- font-weight: 600;
+ animation: agentJumpText 1.4s steps(3, end) infinite;
+ color: rgba(100, 116, 139, 0.40);
+ font-size: 0.7rem;
}
-.tool-call-expanded {
+@keyframes agentJumpText {
+ 0% { content: " ▸ ▹ ▸"; }
+ 33% { content: " ▹ ▸ ▹"; }
+ 66% { content: " ▸ ▸ ▸"; }
+ 100% { content: " ▸ ▹ ▸"; }
+}
+
+.agent-running-indicator:hover .agent-running-text {
+ color: rgba(100, 116, 139, 0.75);
+}
+
+/* 工具调用列表容器 */
+.tool-call-list-wrap {
display: none;
- padding: 0.4rem 0.65rem 0.65rem;
- max-height: 240px;
+ margin-top: 0.35rem;
+ max-height: 360px;
overflow-y: auto;
+ padding: 0.25rem 0;
+ border-top: 1px solid rgba(148, 163, 184, 0.08);
}
-.tool-call-bubble-wrap--expanded .tool-call-expanded {
+.tool-call-list-wrap--expanded {
display: block;
}
-.tool-call-detail {
- margin-bottom: 0.5rem;
- padding: 0.4rem 0.55rem;
- border-radius: 8px;
- background: rgba(241, 245, 249, 0.6);
- border: 1px solid rgba(148, 163, 184, 0.2);
+/* 持久化的列表(在最终消息中) */
+.tool-call-list-wrap--persistent {
+ display: block;
+ max-height: none;
+ margin-top: 0.55rem;
+ border-top: 1px solid rgba(148, 163, 184, 0.15);
}
-.tool-call-detail--running {
- border-color: rgba(245, 158, 11, 0.4);
- background: rgba(245, 158, 11, 0.05);
-}
-
-.tool-call-detail--running .tool-call-detail-name {
- color: #d97706;
-}
-
-.tool-call-detail-name {
- font-weight: 700;
- font-size: 0.72rem;
- color: #1e293b;
- margin-bottom: 0.25rem;
-}
-
-.tool-call-detail-text {
- margin: 0.2rem 0;
- padding: 0.3rem 0.45rem;
+/* 单条工具调用项 */
+.tool-call-item {
+ display: flex;
+ flex-direction: column;
+ padding: 0.22rem 0.3rem;
border-radius: 6px;
- background: rgba(148, 163, 184, 0.12);
+ font-size: 0.72rem;
+ color: rgba(51, 65, 85, 0.55);
+ line-height: 1.35;
+ transition: color 0.3s;
+}
+
+.tool-call-item-summary {
+ display: flex;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ gap: 0.25rem 0.35rem;
+}
+
+.tool-call-item--running {
+ color: rgba(180, 115, 5, 0.60);
+}
+
+.tool-call-item--done {
+ color: rgba(51, 65, 85, 0.50);
+}
+
+.tool-call-item:hover {
+ background: rgba(148, 163, 184, 0.06);
+}
+
+.tool-call-item-name {
+ font-weight: 600;
+ font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
+ margin-right: 0.3rem;
+}
+
+.tool-call-item-args {
+ font-size: 0.62rem;
+ font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
+ color: rgba(100, 116, 139, 0.45);
+ background: rgba(148, 163, 184, 0.06);
+ padding: 0.1rem 0.35rem;
+ border-radius: 4px;
+ max-width: 260px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.tool-call-item-result {
+ font-size: 0.62rem;
+ color: rgba(51, 65, 85, 0.40);
+ background: rgba(148, 163, 184, 0.05);
+ padding: 0.1rem 0.35rem;
+ border-radius: 4px;
+ max-width: 280px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.tool-call-item-img {
+ display: block;
+ max-width: min(160px, 100%);
+ max-height: 120px;
+ object-fit: contain;
+ border-radius: 6px;
+ border: 1px solid rgba(148, 163, 184, 0.15);
+ margin-top: 0.2rem;
+}
+
+/* ✓ 对号 */
+.tc-check {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 1em;
+ height: 1em;
+ color: rgba(34, 197, 94, 0.6);
+ font-weight: 700;
+ font-size: 0.78rem;
+ flex-shrink: 0;
+}
+
+/* 加载中旋转圆圈 */
+.tc-spinner {
+ display: inline-block;
+ width: 0.7em;
+ height: 0.7em;
+ border: 1.5px solid rgba(245, 158, 11, 0.5);
+ border-top-color: rgba(245, 158, 11, 0.85);
+ border-radius: 50%;
+ animation: tcSpin 0.7s linear infinite;
+ flex-shrink: 0;
+ margin-top: 0.15em;
+}
+
+@keyframes tcSpin {
+ to { transform: rotate(360deg); }
+}
+
+/* 摘要行缩略图 */
+.tool-call-item-thumb {
+ display: block;
+ max-width: min(120px, 100%);
+ max-height: 80px;
+ object-fit: cover;
+ border-radius: 4px;
+ border: 1px solid rgba(148, 163, 184, 0.2);
+ margin-top: 0.15rem;
+ cursor: pointer;
+}
+
+/* 展开详情面板 */
+.tool-call-item-detail {
+ margin-top: 0.35rem;
+ padding: 0.4rem 0.5rem;
+ border-radius: 6px;
+ background: rgba(241, 245, 249, 0.5);
+ border: 1px solid rgba(148, 163, 184, 0.12);
+}
+
+.tool-call-detail-pre {
+ margin: 0.2rem 0;
+ padding: 0.35rem 0.5rem;
+ border-radius: 5px;
+ background: rgba(148, 163, 184, 0.08);
font-size: 0.65rem;
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
- color: #64748b;
+ color: #475569;
white-space: pre-wrap;
word-break: break-all;
- max-height: 80px;
+ max-height: 200px;
overflow-y: auto;
+ line-height: 1.45;
}
.tool-call-detail-result {
color: #334155;
- background: rgba(148, 163, 184, 0.08);
+ background: rgba(148, 163, 184, 0.05);
}
.tool-call-detail-media {
@@ -2246,61 +2359,21 @@ body {
margin-top: 0.35rem;
}
-.tool-call-detail .tool-call-detail-image {
+.tool-call-item-img--full {
display: block;
- max-width: min(220px, 100%);
- max-height: 180px;
+ max-width: min(320px, 100%);
+ max-height: 260px;
object-fit: contain;
border-radius: 8px;
- border: 1px solid rgba(148, 163, 184, 0.28);
+ border: 1px solid rgba(148, 163, 184, 0.25);
background: rgba(255, 255, 255, 0.45);
cursor: zoom-in;
- margin: 0;
}
-.tool-call-detail .tool-call-detail-image:hover {
+.tool-call-item-img--full:hover {
opacity: 0.92;
}
-/* ===== 实时工具调用(thinking 气泡内) ===== */
-.tool-call-live-wrap {
- display: block;
- width: min(560px, 100%);
- margin-top: 0.6rem;
- padding-top: 0.55rem;
- border-top: 1px solid rgba(148, 163, 184, 0.2);
-}
-
-.tool-call-live-wrap .tool-call-bubble-wrap {
- margin-top: 0;
- width: 100%;
-}
-
-.tool-call-live-chip {
- display: inline-flex;
- align-items: center;
- gap: 0.2rem;
- padding: 0.15rem 0.5rem;
- border-radius: 8px;
- background: rgba(99, 102, 241, 0.08);
- font-size: 0.68rem;
- font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
- color: #6366f1;
- font-weight: 500;
- transition: all 0.3s;
-}
-
-.tool-call-live-chip--running {
- background: rgba(245, 158, 11, 0.1);
- color: #d97706;
- animation: livePulse 1.2s ease-in-out infinite;
-}
-
-@keyframes livePulse {
- 0%, 100% { opacity: 0.7; }
- 50% { opacity: 1; }
-}
-
.todo-list-overlay {
position: fixed;
top: 80px;
diff --git a/src/web/app.js b/src/web/app.js
index c16b10e..18e7965 100644
--- a/src/web/app.js
+++ b/src/web/app.js
@@ -724,6 +724,11 @@
bubble.appendChild(renderPendingActionCard(msg.meta.pending_action));
}
+ if (msg.meta && Array.isArray(msg.meta.tool_calls) && msg.meta.tool_calls.length > 0) {
+ const tcList = renderPersistentToolCalls(msg.meta.tool_calls);
+ if (tcList) bubble.appendChild(tcList);
+ }
+
scrollChatToBottom();
}
@@ -746,6 +751,16 @@
}
bubble.appendChild(label);
bubble.appendChild(dots);
+ // 工具调用折叠指示器(初始隐藏,有工具调用时才显示)
+ const indicator = document.createElement("div");
+ indicator.className = "agent-running-indicator";
+ indicator.style.display = "none";
+ indicator.title = "点击展开/收起工具执行详情";
+ const runningText = document.createElement("span");
+ runningText.className = "agent-running-text";
+ runningText.textContent = "agent运行中,详情可展开";
+ indicator.appendChild(runningText);
+ bubble.appendChild(indicator);
wrap.appendChild(bubble);
chatMessages.appendChild(wrap);
scrollChatToBottom();
@@ -1154,114 +1169,253 @@
document.querySelectorAll(".todo-list-overlay").forEach((el) => el.remove());
}
- // ===== 工具调用展示(chat 视图) =====
+ // ===== 工具调用展示(chat 视图 / 持久化) =====
let liveToolCallExpanded = false;
+ /** 在思考气泡内更新工具调用指示器与列表(增量更新,不重建 DOM) */
function injectLiveToolCalls(typingEl, liveCalls) {
if (!typingEl || !liveCalls.length) return;
const bubble = typingEl.querySelector(".msg-bubble") || typingEl;
- let liveWrap = bubble.querySelector(".tool-call-live-wrap");
- if (!liveWrap) {
- liveWrap = document.createElement("div");
- liveWrap.className = "tool-call-live-wrap";
- bubble.appendChild(liveWrap);
+
+ // 显示 "agent运行中,详情可展开" 指示器
+ const indicator = bubble.querySelector(".agent-running-indicator");
+ if (indicator) {
+ indicator.style.display = "";
+ // 更新指示文字中的下标提示
+ const runCount = liveCalls.filter((c) => c.status === "running").length;
+ const doneCount = liveCalls.filter((c) => c.status === "done").length;
+ const textEl = indicator.querySelector(".agent-running-text");
+ if (textEl) {
+ textEl.textContent = `agent运行中 (${doneCount}/${liveCalls.length}),详情可${liveToolCallExpanded ? "收起" : "展开"}`;
+ }
}
- const total = liveCalls.length;
- let wrap = liveWrap.querySelector(".tool-call-bubble-wrap");
- const isNewPanel = !wrap;
- if (!wrap) {
- wrap = document.createElement("div");
- liveWrap.appendChild(wrap);
+ // 展开/收起列表容器
+ let listWrap = bubble.querySelector(".tool-call-list-wrap");
+ if (!listWrap) {
+ listWrap = document.createElement("div");
+ listWrap.className = "tool-call-list-wrap" + (liveToolCallExpanded ? " tool-call-list-wrap--expanded" : "");
+ bubble.appendChild(listWrap);
}
+ listWrap.classList.toggle("tool-call-list-wrap--expanded", liveToolCallExpanded);
- if (isNewPanel) liveToolCallExpanded = true;
- liveToolCallExpanded = liveToolCallExpanded || wrap.classList.contains("tool-call-bubble-wrap--expanded");
- wrap.className = "tool-call-bubble-wrap" + (liveToolCallExpanded ? " tool-call-bubble-wrap--expanded" : "");
- wrap.innerHTML = "";
-
- const runningCount = liveCalls.filter((call) => call.status === "running").length;
- const header = document.createElement("button");
- header.type = "button";
- header.className = "tool-call-toggle";
- header.textContent = `🔧 工具调用 (${total}${runningCount ? `,执行中 ${runningCount}` : ""}) ${liveToolCallExpanded ? "▾" : "▸"}`;
- header.addEventListener("click", () => {
- liveToolCallExpanded = !liveToolCallExpanded;
- wrap.classList.toggle("tool-call-bubble-wrap--expanded", liveToolCallExpanded);
- header.textContent = `🔧 工具调用 (${total}${runningCount ? `,执行中 ${runningCount}` : ""}) ${liveToolCallExpanded ? "▾" : "▸"}`;
- });
- wrap.appendChild(header);
-
- const collapsed = document.createElement("div");
- collapsed.className = "tool-call-collapsed";
- liveCalls.slice(-Math.min(2, total)).forEach((call) => {
- const chip = document.createElement("span");
- chip.className = "tool-call-chip" + (call.status === "running" ? " tool-call-live-chip--running" : "");
- chip.textContent = call.name || "unknown";
- collapsed.appendChild(chip);
- });
- if (total > 2) {
- const more = document.createElement("span");
- more.className = "tool-call-more";
- more.textContent = `+${total - 2}`;
- more.addEventListener("click", () => {
- liveToolCallExpanded = true;
- injectLiveToolCalls(typingEl, liveCalls);
+ // 指示器点击切换展开
+ if (indicator && !indicator._toggleBound) {
+ indicator._toggleBound = true;
+ indicator.addEventListener("click", () => {
+ liveToolCallExpanded = !liveToolCallExpanded;
+ if (listWrap) listWrap.classList.toggle("tool-call-list-wrap--expanded", liveToolCallExpanded);
+ const textEl = indicator.querySelector(".agent-running-text");
+ if (textEl) {
+ const runCount = liveCalls.filter((c) => c.status === "running").length;
+ textEl.textContent = `agent运行中 (${liveCalls.filter((c) => c.status === "done").length}/${liveCalls.length}),详情可${liveToolCallExpanded ? "收起" : "展开"}`;
+ }
});
- collapsed.appendChild(more);
}
- wrap.appendChild(collapsed);
- const expandedArea = document.createElement("div");
- expandedArea.className = "tool-call-expanded";
- liveCalls.forEach((call) => {
- expandedArea.appendChild(renderLiveToolCallDetail(call));
+ // 增量更新列表项(用 data-tc-id 做差量,避免每次重建 DOM)
+ const existingById = new Map();
+ const currentIds = [];
+ listWrap.querySelectorAll(".tool-call-item").forEach((el) => {
+ const id = el.getAttribute("data-tc-id");
+ if (id) existingById.set(id, el);
});
- wrap.appendChild(expandedArea);
- scrollChatToBottom();
+ liveCalls.forEach((call, idx) => {
+ const callId = `${call.name || "unknown"}_${idx}`;
+ currentIds.push(callId);
+ let item = existingById.get(callId);
+ const isNew = !item;
+ if (!item) {
+ item = document.createElement("div");
+ item.className = "tool-call-item";
+ item.setAttribute("data-tc-id", callId);
+ listWrap.appendChild(item);
+ }
+
+ // 状态图标
+ const statusHtml = call.status === "done"
+ ? '✓'
+ : '';
+ const name = escapeHtml(call.name || "unknown");
+
+ // 组装行内信息(摘要截断)
+ let detailHtml = "";
+ if (call.args && Object.keys(call.args).length > 0) {
+ const short = JSON.stringify(call.args).slice(0, 200);
+ detailHtml += `${escapeHtml(short)}`;
+ }
+ if (call.result_text) {
+ const txt = String(call.result_text).slice(0, 200);
+ detailHtml += `${escapeHtml(txt)}`;
+ }
+ // 摘要行直接展示第一张缩略图
+ if (Array.isArray(call.result_images) && call.result_images.length > 0) {
+ const first = call.result_images[0];
+ if (first) {
+ detailHtml += ``;
+ }
+ }
+
+ const summaryHtml = `${statusHtml} ${name}${detailHtml}`;
+ if (isNew || item.getAttribute("data-tc-summary") !== summaryHtml) {
+ // 只更新 summary,不破坏 detail
+ let summary = item.querySelector(".tool-call-item-summary");
+ if (!summary) {
+ summary = document.createElement("span");
+ summary.className = "tool-call-item-summary";
+ item.insertBefore(summary, item.firstChild);
+ }
+ summary.innerHTML = summaryHtml;
+ item.setAttribute("data-tc-summary", summaryHtml);
+ }
+ item.className = "tool-call-item" + (call.status === "running" ? " tool-call-item--running" : " tool-call-item--done");
+
+ // 存储完整数据供展开
+ item._callData = call;
+
+ // 点击切换展开详情
+ if (!item._boundClick) {
+ item._boundClick = true;
+ item.style.cursor = "pointer";
+ item.addEventListener("click", () => {
+ let detail = item.querySelector(".tool-call-item-detail");
+ if (detail) {
+ // 收起
+ detail.remove();
+ item.setAttribute("aria-expanded", "false");
+ return;
+ }
+ // 展开
+ const data = item._callData;
+ if (!data) return;
+ detail = document.createElement("div");
+ detail.className = "tool-call-item-detail";
+ // 完整参数
+ if (data.args && Object.keys(data.args).length > 0) {
+ const pre = document.createElement("pre");
+ pre.className = "tool-call-detail-pre";
+ pre.textContent = JSON.stringify(data.args, null, 2);
+ detail.appendChild(pre);
+ }
+ // 完整返回文本
+ if (data.result_text) {
+ const pre = document.createElement("pre");
+ pre.className = "tool-call-detail-pre tool-call-detail-result";
+ pre.textContent = data.result_text;
+ detail.appendChild(pre);
+ }
+ // 图片完整展示
+ if (Array.isArray(data.result_images) && data.result_images.length > 0) {
+ const mediaWrap = document.createElement("div");
+ mediaWrap.className = "tool-call-detail-media";
+ data.result_images.forEach((url) => {
+ if (!url) return;
+ const img = document.createElement("img");
+ img.className = "tool-call-item-img tool-call-item-img--full";
+ img.src = url;
+ img.alt = "工具返回图片";
+ img.addEventListener("click", (e) => { e.stopPropagation(); window.open(url, "_blank", "noopener"); });
+ mediaWrap.appendChild(img);
+ });
+ detail.appendChild(mediaWrap);
+ }
+ item.appendChild(detail);
+ item.setAttribute("aria-expanded", "true");
+ });
+ }
+ });
+
+ // 移除不再存在的旧项
+ const idSet = new Set(currentIds);
+ existingById.forEach((el, id) => {
+ if (!idSet.has(id)) el.remove();
+ });
}
- function renderLiveToolCallDetail(call) {
- const card = document.createElement("div");
- card.className = "tool-call-detail" + (call.status === "running" ? " tool-call-detail--running" : "");
-
- const nameEl = document.createElement("div");
- nameEl.className = "tool-call-detail-name";
- const statusIcon = call.status === "running" ? "⌛" : "✓";
- nameEl.textContent = `${statusIcon} ${escapeHtml(call.name || "unknown")}`;
- card.appendChild(nameEl);
-
- if (call.args && Object.keys(call.args).length > 0) {
- const argsEl = document.createElement("pre");
- argsEl.className = "tool-call-detail-text";
- argsEl.textContent = JSON.stringify(call.args, null, 2);
- card.appendChild(argsEl);
- }
-
- if (call.result_text) {
- const resultEl = document.createElement("pre");
- resultEl.className = "tool-call-detail-text tool-call-detail-result";
- resultEl.textContent = call.result_text;
- card.appendChild(resultEl);
- }
-
- if (Array.isArray(call.result_images) && call.result_images.length > 0) {
- const mediaWrap = document.createElement("div");
- mediaWrap.className = "tool-call-detail-media";
+ /** 根据消息 meta.tool_calls 渲染工具调用列表(持久化展示在最终消息中) */
+ function toolCallImageUrls(call) {
+ const urls = [];
+ if (Array.isArray(call.result_images)) {
call.result_images.forEach((url) => {
- if (!url) return;
- const img = document.createElement("img");
- img.className = "tool-call-detail-image";
- img.src = url;
- img.alt = "工具返回图片";
- img.addEventListener("click", () => window.open(url, "_blank", "noopener"));
- mediaWrap.appendChild(img);
+ if (typeof url === "string" && url) urls.push(url);
});
- card.appendChild(mediaWrap);
}
+ return urls;
+ }
- return card;
+ function renderPersistentToolCalls(toolCalls) {
+ if (!toolCalls || !toolCalls.length) return null;
+ const wrap = document.createElement("div");
+ wrap.className = "tool-call-list-wrap tool-call-list-wrap--persistent tool-call-list-wrap--expanded";
+ toolCalls.forEach((call) => {
+ const item = document.createElement("div");
+ item.className = "tool-call-item tool-call-item--done";
+ item.style.cursor = "pointer";
+
+ // 行内摘要
+ const summary = document.createElement("span");
+ summary.className = "tool-call-item-summary";
+ const name = escapeHtml(call.name || "unknown");
+ let detailHtml = "";
+ if (call.args && Object.keys(call.args).length > 0) {
+ const short = JSON.stringify(call.args).slice(0, 200);
+ detailHtml += `${escapeHtml(short)}`;
+ }
+ if (call.result_text) {
+ const txt = String(call.result_text).slice(0, 300);
+ detailHtml += `${escapeHtml(txt)}`;
+ }
+ const imageUrls = toolCallImageUrls(call);
+ if (imageUrls.length > 0) {
+ detailHtml += `
`;
+ }
+ summary.innerHTML = `✓ ${name}${detailHtml}`;
+ item.appendChild(summary);
+
+ // 存储数据
+ item._callData = { args: call.args, result_text: call.result_text, image_urls: imageUrls };
+
+ // 点击展开
+ item.addEventListener("click", () => {
+ let detail = item.querySelector(".tool-call-item-detail");
+ if (detail) { detail.remove(); item.setAttribute("aria-expanded", "false"); return; }
+ detail = document.createElement("div");
+ detail.className = "tool-call-item-detail";
+ const data = item._callData;
+ if (data.args && Object.keys(data.args).length > 0) {
+ const pre = document.createElement("pre");
+ pre.className = "tool-call-detail-pre";
+ pre.textContent = JSON.stringify(data.args, null, 2);
+ detail.appendChild(pre);
+ }
+ if (data.result_text) {
+ const pre = document.createElement("pre");
+ pre.className = "tool-call-detail-pre tool-call-detail-result";
+ pre.textContent = data.result_text;
+ detail.appendChild(pre);
+ }
+ if (Array.isArray(data.image_urls) && data.image_urls.length > 0) {
+ const mediaWrap = document.createElement("div");
+ mediaWrap.className = "tool-call-detail-media";
+ data.image_urls.forEach((imgUrl) => {
+ const img = document.createElement("img");
+ img.className = "tool-call-item-img tool-call-item-img--full";
+ img.src = imgUrl;
+ img.alt = "??????";
+ img.loading = "lazy";
+ img.addEventListener("click", (e) => { e.stopPropagation(); window.open(imgUrl, "_blank", "noopener"); });
+ mediaWrap.appendChild(img);
+ });
+ detail.appendChild(mediaWrap);
+ }
+ item.appendChild(detail);
+ item.setAttribute("aria-expanded", "true");
+ });
+
+ wrap.appendChild(item);
+ });
+ return wrap;
}
function renderMessage(msg, allowAutoplay = false) {
@@ -1306,6 +1460,11 @@
if (msg.meta && msg.meta.pending_action) {
bubble.appendChild(renderPendingActionCard(msg.meta.pending_action));
}
+ // 持久化的工具调用列表(来自 meta.tool_calls)
+ if (msg.meta && msg.meta.tool_calls && Array.isArray(msg.meta.tool_calls) && msg.meta.tool_calls.length) {
+ const tcList = renderPersistentToolCalls(msg.meta.tool_calls);
+ if (tcList) bubble.appendChild(tcList);
+ }
wrap.appendChild(bubble);
return wrap;
}
@@ -1451,7 +1610,7 @@
liveToolCallExpanded = false;
clearTodoOverlay();
- // 启动实时工具调用轮询
+ // 启动实时工具调用轮询(增量更新,低频率不卡)
let livePollTimer = null;
let livePollActive = true;
const pollLiveToolCalls = async () => {
@@ -1462,11 +1621,11 @@
injectLiveToolCalls(typingEl, liveData.tool_calls);
}
} catch { /* ignore */ }
- if (livePollActive) livePollTimer = setTimeout(pollLiveToolCalls, 800);
+ if (livePollActive) livePollTimer = setTimeout(pollLiveToolCalls, 1200);
};
pollLiveToolCalls();
- // 启动 todo_list 实时轮询
+ // 启动 todo_list 实时轮询(频率降低,减少卡顿)
let todoPollTimer = null;
const pollTodoList = async () => {
if (!livePollActive) return;
@@ -1476,7 +1635,7 @@
updateTodoOverlay(todoData.data);
}
} catch { /* ignore */ }
- if (livePollActive) todoPollTimer = setTimeout(pollTodoList, 500);
+ if (livePollActive) todoPollTimer = setTimeout(pollTodoList, 2000);
};
pollTodoList();
@@ -1723,17 +1882,22 @@
const settingsStatus = $("settingsStatus");
const agentsListEl = $("agentsList");
const toolsListEl = $("toolsList");
+ const graphsListEl = $("graphsList");
const envListEl = $("envList");
const agentConfigPath = $("agentConfigPath");
const toolConfigPath = $("toolConfigPath");
const envConfigPath = $("envConfigPath");
+ const guiConfigPath = $("guiConfigPath");
+ const guiMonitorsGrid = $("guiMonitorsGrid");
const addAgentBtn = $("addAgentBtn");
let cachedAgentConfigs = [];
let cachedToolConfigs = [];
+ let cachedGraphs = [];
let cachedEnvConfig = {};
let cachedModels = [];
let registeredToolNames = [];
+ let cachedGuiConfig = { monitors: [], selected_name: "" };
// 模型编辑状态
let editingModelIdx = -1;
@@ -1767,30 +1931,39 @@
// 切换时重新渲染对应面板
if (target === "agent") renderAgentConfigs();
else if (target === "tool") renderToolConfigs();
+ else if (target === "graph") renderGraphs();
else if (target === "env") renderEnvConfig();
+ else if (target === "other") renderGuiConfig();
});
});
// ===== 加载所有配置 =====
async function loadAllConfigs() {
- const [agentRes, toolRes, envRes] = await Promise.all([
+ const [agentRes, toolRes, envRes, graphRes, guiRes] = await Promise.all([
api("/api/config/agents"),
api("/api/config/tools"),
api("/api/config/env"),
+ api("/api/config/graphs"),
+ api("/api/config/gui"),
]);
cachedAgentConfigs = agentRes.agents || [];
cachedToolConfigs = toolRes.tools || [];
cachedEnvConfig = envRes.env || {};
cachedModels = envRes.models || [];
+ cachedGraphs = graphRes.graphs || [];
+ cachedGuiConfig = { monitors: guiRes.monitors || [], selected_name: guiRes.selected_name || "" };
registeredToolNames = toolRes.registered_tool_names || [];
window._toolParameters = toolRes.tool_parameters || {};
window._forcedPermissions = toolRes.forced_permissions || {};
if (agentConfigPath) agentConfigPath.textContent = agentRes.config_path || "";
if (toolConfigPath) toolConfigPath.textContent = toolRes.config_path || "";
if (envConfigPath) envConfigPath.textContent = envRes.config_path || "";
+ if (guiConfigPath) guiConfigPath.textContent = guiRes.config_path || "";
renderAgentConfigs();
renderToolConfigs();
+ renderGraphs();
renderEnvConfig();
+ renderGuiConfig();
}
// ===== Agent 配置渲染 =====
@@ -1874,6 +2047,33 @@
llmField.appendChild(llmLabel);
llmField.appendChild(llmRow);
+ // 图类型选择
+ const graphField = document.createElement("div");
+ graphField.className = "setting-field";
+ const graphLabel = document.createElement("label");
+ graphLabel.textContent = "图类型 (graph)";
+ const graphRow = document.createElement("div");
+ graphRow.style.cssText = "display:flex;gap:0.4rem;align-items:center;";
+ const graphSelect = document.createElement("select");
+ graphSelect.style.cssText = "flex:1;";
+ const graphEmptyOpt = document.createElement("option");
+ graphEmptyOpt.value = "react";
+ graphEmptyOpt.textContent = "— 请选择 —";
+ graphSelect.appendChild(graphEmptyOpt);
+ cachedGraphs.forEach((g) => {
+ const opt = document.createElement("option");
+ opt.value = g.name;
+ opt.textContent = `${g.display_name || g.name}`;
+ if (g.name === (agent.graph || "react")) opt.selected = true;
+ graphSelect.appendChild(opt);
+ });
+ graphSelect.addEventListener("change", () => {
+ agent.graph = graphSelect.value;
+ });
+ graphRow.appendChild(graphSelect);
+ graphField.appendChild(graphLabel);
+ graphField.appendChild(graphRow);
+
// 角色与启用
const metaRow = document.createElement("div");
metaRow.className = "setting-field";
@@ -2002,11 +2202,40 @@
addToolRow.appendChild(addToolBtn);
toolsField.appendChild(addToolRow);
+ // trajectory_rounds(短期记忆保留轮数)
+ const trajField = makeField("短期记忆轮数 (trajectory_rounds)", "number", agent.trajectory_rounds ?? 3, (v) => { agent.trajectory_rounds = parseInt(v) || 3; });
+
+ // parallel_tools(并行工具执行开关)
+ const parallelField = document.createElement("div");
+ parallelField.className = "setting-field";
+ const parallelWrap = document.createElement("div");
+ parallelWrap.className = "toggle-row";
+ const parallelToggleLabel = document.createElement("label");
+ parallelToggleLabel.className = "toggle-switch";
+ const parallelToggleInput = document.createElement("input");
+ parallelToggleInput.type = "checkbox";
+ parallelToggleInput.checked = agent.parallel_tools === true;
+ parallelToggleInput.addEventListener("change", () => { agent.parallel_tools = parallelToggleInput.checked; });
+ const parallelToggleSlider = document.createElement("span");
+ parallelToggleSlider.className = "toggle-slider";
+ parallelToggleLabel.appendChild(parallelToggleInput);
+ parallelToggleLabel.appendChild(parallelToggleSlider);
+ const parallelLabel = document.createElement("span");
+ parallelLabel.className = "toggle-label";
+ parallelLabel.textContent = "并行工具执行 (parallel_tools)";
+ parallelLabel.addEventListener("click", () => { parallelToggleInput.click(); });
+ parallelWrap.appendChild(parallelToggleLabel);
+ parallelWrap.appendChild(parallelLabel);
+ parallelField.appendChild(parallelWrap);
+
card.appendChild(header);
card.appendChild(nameField);
card.appendChild(maxIterField);
card.appendChild(spField);
card.appendChild(llmField);
+ card.appendChild(graphField);
+ card.appendChild(trajField);
+ card.appendChild(parallelField);
card.appendChild(metaRow);
card.appendChild(toolsField);
return card;
@@ -2045,6 +2274,9 @@
tools: [],
enabled: true,
role: isFirst ? "main" : "sub",
+ graph: "react",
+ trajectory_rounds: 3,
+ parallel_tools: false,
});
renderAgentConfigs();
});
@@ -2210,6 +2442,148 @@
return card;
}
+ // ===== 图配置渲染 =====
+ function renderGraphs() {
+ if (!graphsListEl) return;
+ graphsListEl.innerHTML = "";
+
+ if (!cachedGraphs || cachedGraphs.length === 0) {
+ const emptyHint = document.createElement("p");
+ emptyHint.style.cssText = "color:#94a3b8;font-size:0.8rem;text-align:center;padding:1rem;";
+ emptyHint.textContent = "暂无可用的图类型。";
+ graphsListEl.appendChild(emptyHint);
+ return;
+ }
+
+ cachedGraphs.forEach((graph) => {
+ const card = document.createElement("div");
+ card.className = "graph-card";
+ card.style.cssText = "border:1px solid rgba(79,70,229,0.2);border-radius:10px;padding:1rem;margin-bottom:0.8rem;background:rgba(255,255,255,0.03);";
+
+ // 标题行
+ const header = document.createElement("div");
+ header.style.cssText = "display:flex;align-items:center;justify-content:space-between;margin-bottom:0.5rem;";
+ const title = document.createElement("h4");
+ title.style.cssText = "margin:0;font-size:0.95rem;";
+ title.textContent = `${graph.display_name || graph.name}`;
+ const nameTag = document.createElement("code");
+ nameTag.style.cssText = "font-size:0.72rem;background:rgba(79,70,229,0.15);padding:0.1rem 0.4rem;border-radius:4px;";
+ nameTag.textContent = graph.name;
+ header.appendChild(title);
+ header.appendChild(nameTag);
+
+ // 描述
+ const desc = document.createElement("p");
+ desc.style.cssText = "color:#94a3b8;font-size:0.78rem;margin:0 0 0.6rem;";
+ desc.textContent = graph.description || "";
+
+ // Mermaid PNG 图结构可视化
+ const imgContainer = document.createElement("div");
+ imgContainer.style.cssText = "text-align:center;background:rgba(255,255,255,0.05);border-radius:8px;padding:0.5rem;overflow-x:auto;";
+ const img = document.createElement("img");
+ img.src = graph.png_url || `/api/config/graphs/png/${graph.name}`;
+ img.alt = `${graph.display_name || graph.name} 图结构`;
+ img.style.cssText = "max-width:100%;height:auto;border-radius:4px;";
+ img.onerror = () => {
+ imgContainer.innerHTML = '图结构图片生成中,请稍后刷新...';
+ };
+ imgContainer.appendChild(img);
+
+ card.appendChild(header);
+ card.appendChild(desc);
+ card.appendChild(imgContainer);
+
+ graphsListEl.appendChild(card);
+ });
+ }
+
+ // ===== GUI 显示器配置渲染 =====
+ function renderGuiConfig() {
+ if (!guiMonitorsGrid) return;
+ guiMonitorsGrid.innerHTML = "";
+
+ const monitors = cachedGuiConfig.monitors || [];
+ if (monitors.length === 0) {
+ const hint = document.createElement("p");
+ hint.style.cssText = "color:#94a3b8;font-size:0.8rem;text-align:center;padding:1rem;";
+ hint.textContent = "未检测到显示器,请确认 screeninfo 库已安装。";
+ guiMonitorsGrid.appendChild(hint);
+ return;
+ }
+
+ // 构建屏幕矩阵预览:计算包围盒以绘制相对位置
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
+ monitors.forEach((m) => {
+ minX = Math.min(minX, m.x);
+ minY = Math.min(minY, m.y);
+ maxX = Math.max(maxX, m.x + m.width);
+ maxY = Math.max(maxY, m.y + m.height);
+ });
+ const totalW = maxX - minX;
+ const totalH = maxY - minY;
+ const containerW = 560;
+ const containerH = 360;
+ const scale = Math.min(containerW / totalW, containerH / totalH, 1);
+
+ const matrixWrap = document.createElement("div");
+ matrixWrap.style.cssText = `position:relative;width:${containerW}px;height:${containerH}px;margin:0 auto;border:1px solid rgba(79,70,229,0.25);border-radius:10px;background:rgba(0,0,0,0.2);overflow:hidden;`;
+
+ monitors.forEach((m) => {
+ const left = (m.x - minX) * scale;
+ const top = (m.y - minY) * scale;
+ const w = m.width * scale;
+ const h = m.height * scale;
+ const isSelected = m.name === cachedGuiConfig.selected_name || (!cachedGuiConfig.selected_name && m.is_primary);
+
+ const box = document.createElement("div");
+ box.className = "gui-monitor-box" + (isSelected ? " gui-monitor-box--selected" : "");
+ box.style.cssText = `position:absolute;left:${left}px;top:${top}px;width:${w}px;height:${h}px;border:2px solid ${isSelected ? "#4f46e5" : "rgba(148,163,184,0.4)"};border-radius:6px;background:${isSelected ? "rgba(79,70,229,0.12)" : "rgba(255,255,255,0.03)"};cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;transition:all 0.15s;`;
+ box.title = `${m.name}\n${m.width}x${m.height}${m.is_primary ? " (主显示器)" : ""}`;
+
+ const nameEl = document.createElement("span");
+ nameEl.style.cssText = `font-size:${Math.max(9, w / 14)}px;color:${isSelected ? "#e0e7ff" : "#94a3b8"};text-align:center;line-height:1.2;pointer-events:none;`;
+ nameEl.textContent = (m.name || "Unknown").substring(0, 20) + (m.name && m.name.length > 20 ? "…" : "");
+ const resEl = document.createElement("span");
+ resEl.style.cssText = `font-size:${Math.max(8, w / 18)}px;color:${isSelected ? "#a5b4fc" : "#64748b"};pointer-events:none;`;
+ resEl.textContent = `${m.width}x${m.height}`;
+ const priTag = document.createElement("span");
+ priTag.style.cssText = `font-size:${Math.max(7, w / 22)}px;color:#cbd5e1;pointer-events:none;`;
+ priTag.textContent = m.is_primary ? "主屏" : "";
+
+ box.appendChild(nameEl);
+ box.appendChild(resEl);
+ box.appendChild(priTag);
+
+ box.addEventListener("click", () => {
+ cachedGuiConfig.selected_name = m.name;
+ renderGuiConfig();
+ });
+
+ // hover 高亮
+ box.addEventListener("mouseenter", () => {
+ box.style.borderColor = "#818cf8";
+ box.style.background = "rgba(79,70,229,0.08)";
+ });
+ box.addEventListener("mouseleave", () => {
+ if (!isSelected || m.name !== cachedGuiConfig.selected_name) {
+ box.style.borderColor = isSelected ? "#4f46e5" : "rgba(148,163,184,0.4)";
+ box.style.background = isSelected ? "rgba(79,70,229,0.12)" : "rgba(255,255,255,0.03)";
+ }
+ });
+
+ matrixWrap.appendChild(box);
+ });
+
+ guiMonitorsGrid.appendChild(matrixWrap);
+
+ // 当前选中提示
+ const selectedInfo = document.createElement("p");
+ selectedInfo.style.cssText = "text-align:center;color:#a5b4fc;font-size:0.78rem;margin-top:0.6rem;";
+ const selMon = monitors.find((m) => m.name === cachedGuiConfig.selected_name) || monitors.find((m) => m.is_primary);
+ selectedInfo.textContent = selMon ? `当前 GUI 操作区域: ${selMon.name} (${selMon.width}x${selMon.height})` : "当前 GUI 操作区域: 全屏";
+ guiMonitorsGrid.appendChild(selectedInfo);
+ }
+
// ===== Env 配置渲染 =====
function renderEnvConfig() {
envListEl.innerHTML = "";
@@ -2418,6 +2792,9 @@
tools: a.tools || [],
enabled: a.enabled !== false,
role: a.role || "sub",
+ graph: a.graph || "react",
+ trajectory_rounds: a.trajectory_rounds ?? 3,
+ parallel_tools: a.parallel_tools || false,
}));
await api("/api/config/agents", { method: "POST", body: JSON.stringify({ agents }) });
await api("/api/config/reload", { method: "POST", body: JSON.stringify({}) });
@@ -2437,6 +2814,8 @@
await api("/api/config/models", { method: "POST", body: JSON.stringify({ models: cachedModels }) });
await api("/api/config/env", { method: "POST", body: JSON.stringify({ env: cachedEnvConfig }) });
await api("/api/config/reload", { method: "POST", body: JSON.stringify({}) });
+ } else if (saveType === "gui") {
+ await api("/api/config/gui", { method: "POST", body: JSON.stringify({ gui_monitor_name: cachedGuiConfig.selected_name }) });
}
await loadAllConfigs();
setStatus("保存成功!配置已实时生效。", "success");
diff --git a/src/web/index.html b/src/web/index.html
index 9572557..747047c 100644
--- a/src/web/index.html
+++ b/src/web/index.html
@@ -154,7 +154,9 @@
选择 Agent 进行 GUI 自动化操作(点击、截图、拖拽等)的目标显示器。
+ +