diff --git a/bilibili_page.html b/bilibili_page.html
deleted file mode 100644
index d3cd089..0000000
--- a/bilibili_page.html
+++ /dev/null
@@ -1,344 +0,0 @@
-
-
- 哔哩哔哩 (゜-゜)つロ 干杯~-bilibili
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/agent/__pycache__/chatbot_agent.cpython-312.pyc b/src/agent/__pycache__/chatbot_agent.cpython-312.pyc
index f5b236c..be78a99 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
new file mode 100644
index 0000000..bb5e361
Binary files /dev/null and b/src/agent/__pycache__/chatbot_agent.cpython-313.pyc differ
diff --git a/src/agent/agents/__pycache__/agent.cpython-312.pyc b/src/agent/agents/__pycache__/agent.cpython-312.pyc
index 9f67f3e..53fe921 100644
Binary files a/src/agent/agents/__pycache__/agent.cpython-312.pyc and b/src/agent/agents/__pycache__/agent.cpython-312.pyc differ
diff --git a/src/agent/agents/__pycache__/agent.cpython-313.pyc b/src/agent/agents/__pycache__/agent.cpython-313.pyc
new file mode 100644
index 0000000..d8ee44d
Binary files /dev/null and b/src/agent/agents/__pycache__/agent.cpython-313.pyc differ
diff --git a/src/agent/agents/__pycache__/pyautogui.cpython-312.pyc b/src/agent/agents/__pycache__/pyautogui.cpython-312.pyc
new file mode 100644
index 0000000..aad2118
Binary files /dev/null and b/src/agent/agents/__pycache__/pyautogui.cpython-312.pyc differ
diff --git a/src/agent/agents/agent.py b/src/agent/agents/agent.py
index 80cabe2..a708739 100644
--- a/src/agent/agents/agent.py
+++ b/src/agent/agents/agent.py
@@ -38,12 +38,19 @@ def _build_llm_for_agent(cfg: AgentConfig):
break
if model_config:
+ # enable_thinking: true 时传入空 extra_body 恢复模型原生行为(启用思考)
+ # 默认不传 extra_body,由 ChatQwen 使用禁用思考的默认值
+ if model_config.get("enable_thinking"):
+ extra_body = {}
+ else:
+ extra_body = None
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)),
+ extra_body=extra_body,
)
# 回退:使用环境变量中已加载的默认 LLM 配置
from agent.core.llm import llm as _default_llm
diff --git a/src/agent/agents/gui.py b/src/agent/agents/gui.py
new file mode 100644
index 0000000..0452e2c
--- /dev/null
+++ b/src/agent/agents/gui.py
@@ -0,0 +1,3 @@
+import pyautogui
+
+pyautogui.displayMousePosition()
\ No newline at end of file
diff --git a/src/agent/chatbot_agent.py b/src/agent/chatbot_agent.py
index c785313..0af229f 100644
--- a/src/agent/chatbot_agent.py
+++ b/src/agent/chatbot_agent.py
@@ -10,7 +10,7 @@ from agent.agents.agent import (
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"]
+AgentMode = Literal["agent", "team", "plan"]
# 首次导入时从配置构建主 Agent 运行时并缓存引用
_built = build_all_agent_runtimes()
diff --git a/src/agent/core/__pycache__/__init__.cpython-313.pyc b/src/agent/core/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..c601a06
Binary files /dev/null and b/src/agent/core/__pycache__/__init__.cpython-313.pyc differ
diff --git a/src/agent/core/__pycache__/config_manager.cpython-312.pyc b/src/agent/core/__pycache__/config_manager.cpython-312.pyc
index 7ffb088..086c7fe 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
index 16600d3..f5a13b5 100644
Binary files a/src/agent/core/__pycache__/graph.cpython-312.pyc and b/src/agent/core/__pycache__/graph.cpython-312.pyc differ
diff --git a/src/agent/core/__pycache__/llm.cpython-312.pyc b/src/agent/core/__pycache__/llm.cpython-312.pyc
index f5d6d93..1e58a2f 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__/nodes.cpython-312.pyc b/src/agent/core/__pycache__/nodes.cpython-312.pyc
index df564b5..771bf01 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
new file mode 100644
index 0000000..15a1791
Binary files /dev/null and b/src/agent/core/__pycache__/nodes.cpython-313.pyc differ
diff --git a/src/agent/core/__pycache__/runtime.cpython-312.pyc b/src/agent/core/__pycache__/runtime.cpython-312.pyc
index 37b5d81..9f7e1d8 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
new file mode 100644
index 0000000..3f1606c
Binary files /dev/null and b/src/agent/core/__pycache__/runtime.cpython-313.pyc differ
diff --git a/src/agent/core/__pycache__/state.cpython-312.pyc b/src/agent/core/__pycache__/state.cpython-312.pyc
index 53edabd..0cb06b3 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/agent_config.json b/src/agent/core/agent_config.json
index f56a0bd..365ca10 100644
--- a/src/agent/core/agent_config.json
+++ b/src/agent/core/agent_config.json
@@ -1,12 +1,14 @@
[
{
"name": "main",
- "system_prompt": "你是一个多模态助手,能够直接理解用户上传的图片、语音等内容。\n 【关键行为准则】\n - 你的默认行为是优先调用gui工具解决用户问题。\n - 只有当某个信息是你**绝对无法从当前对话或上传内容中获取**,且该信息必须通过外部工具才能获得时,才允许调用工具。\n - 对于任何图片/音频输入,请始终**直接**利用你的视觉/听觉能力回答,**绝对不要**调用截图、OCR、日期查询等无关工具。\n - 如果你不确定是否需要工具,选择**不调用**,直接给出你能给出的最佳答案。\n - 违反上述规则是一种严重错误。",
+ "system_prompt": "你是一个多模态助手,能够直接理解用户上传的图片、语音等内容。\n 【关键行为准则】\n - 你的默认行为是优先调用工具解决用户问题。\n - 只有当某个信息是你**绝对无法从当前对话或上传内容中获取**,且该信息必须通过外部工具才能获得时,才允许调用工具。\n - 对于任何图片/音频输入,请始终**直接**利用你的视觉/听觉能力回答,**绝对不要**调用截图、OCR、日期查询等无关工具。\n - 如果你不确定是否需要工具,选择**不调用**,直接给出你能给出的最佳答案。\n - 违反上述规则是一种严重错误。",
"max_iterations": 200,
"llm_model_id": "affiaa7r",
"tools": [
"gui_tool",
- "browser_control"
+ "browser_control",
+ "human_interaction",
+ "todo_list"
],
"enabled": true,
"role": "main",
diff --git a/src/agent/core/config_manager.py b/src/agent/core/config_manager.py
index ece57e8..2bf1cfa 100644
--- a/src/agent/core/config_manager.py
+++ b/src/agent/core/config_manager.py
@@ -305,8 +305,8 @@ def _default_env_config() -> dict:
"IMG_SIZE": "768",
"RAG_CHUNK_SIZE": "500",
"RAG_CHUNK_OVERLAP": "50",
- "GROUNDING_WIDTH": "1000",
- "GROUNDING_HEIGHT": "1000",
+ "GROUNDING_WIDTH": "1024",
+ "GROUNDING_HEIGHT": "576",
}
diff --git a/src/agent/core/graph.py b/src/agent/core/graph.py
index ba1ca2c..08ca802 100644
--- a/src/agent/core/graph.py
+++ b/src/agent/core/graph.py
@@ -82,7 +82,7 @@ GRAPH_REGISTRY: dict[str, dict[str, Any]] = {
"react": {
"builder": build_agent_graph,
"display_name": "ReAct (标准)",
- "description": "标准 ReAct 循环:agent 推理 -> 工具执行 -> 结果处理 -> agent 继续推理,直至完成。适用于绝大多数单 Agent 场景。",
+ "description": "标准 ReAct 循环:agent 推理 -> 工具执行 -> 结果处理 -> agent 继续推理,直至完成。Plan 模式通过 agent_mode 参数控制(首次强制生成 todolist,后续每步注入反思提示),无需单独图。",
},
}
diff --git a/src/agent/core/llm.py b/src/agent/core/llm.py
index c4a083a..c011489 100644
--- a/src/agent/core/llm.py
+++ b/src/agent/core/llm.py
@@ -72,15 +72,26 @@ class ChatQwen(ChatOpenAI):
可扩展性:
可增加 default_headers、proxy 等 ChatOpenAI 参数。
"""
+ # 允许调用方覆盖 extra_body:
+ # - None(默认):使用禁用思考的默认值
+ # - 空 dict {}:清空 extra_body,恢复模型原生行为(含思考模式)
+ # - 非空 dict:直接使用传入值
+ custom_extra_body = kwargs.pop("extra_body", None)
+ if custom_extra_body is None:
+ extra_body = {
+ "chat_template_kwargs": {"enable_thinking": False},
+ "thinking": {"type": "disabled"},
+ }
+ else:
+ extra_body = custom_extra_body
+
super().__init__(
model=kwargs.pop("model", os.environ.get("LLM_MODEL", "")),
api_key=kwargs.pop("api_key", os.environ.get("LLM_API_KEY", "")),
base_url=kwargs.pop("base_url", os.environ.get("LLM_BASE_URL", "")),
timeout=kwargs.pop("timeout", float(os.environ.get("LLM_TIMEOUT", 60))),
max_retries=kwargs.pop("max_retries", 0),
- extra_body={
- "chat_template_kwargs": {"enable_thinking": False}
- },
+ extra_body=extra_body,
**kwargs
)
diff --git a/src/agent/core/nodes.py b/src/agent/core/nodes.py
index bdf251f..36b04fe 100644
--- a/src/agent/core/nodes.py
+++ b/src/agent/core/nodes.py
@@ -1,20 +1,27 @@
-"""LangGraph 图节点:模型调用与工具产物后处理。
+"""LangGraph 图节点:模型调用、反思机制与工具产物后处理。
系统定位:
实现 ReAct 循环中的 ``agent`` 节点工厂与 ``process_tool_artifact`` 节点,
被 ``graph`` 注册到 StateGraph。
+反思机制:
+ ``make_call_model_node`` 在每次调用 LLM 前注入反思引导提示,
+ 模型收到后会在输出 tool_calls 的同时产出文本思考内容(思维链),
+ 该文本思考内容被提取并记录到 think store 供前端实时展示。
+
可扩展性:
- ``make_call_model_node`` 可注入 streaming、重试、用量统计回调。
- ``process_tool_artifact`` 可扩展 audio、文件、结构化 JSON 等 artifact 类型。
"""
from __future__ import annotations
+import json
from typing import Any, Callable
-from langchain_core.messages import HumanMessage, ToolMessage
+from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
from agent.core.state import AgentState
+from agent.memory.system_prompt import PLAN_FIRST_CALL_PROMPT, REFLECTION_PROMPT
from agent.utils.image_utils import image_bytes_to_openai_image_url_part
from agent.utils.tool_call_utils import normalize_tool_call
@@ -47,7 +54,6 @@ def _filter_short_term_memory(messages: list, trajectory_rounds: int) -> list:
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
@@ -61,17 +67,151 @@ def _filter_short_term_memory(messages: list, trajectory_rounds: int) -> list:
return [messages[i] for i in sorted(kept_indices)]
+def _get_todolist_status() -> str:
+ """获取当前会话的 todolist 状态摘要。
+
+ 输出:
+ todolist 状态文本;无 todolist 时返回空字符串。
+ """
+ try:
+ from agent.history.tool_call_recorder import get_current_session
+ from agent.tools.tool import get_todo_store_data
+ sid = get_current_session()
+ if not sid:
+ return ""
+ todo = get_todo_store_data(sid)
+ if not todo or not todo.get("steps"):
+ return ""
+ steps = todo["steps"]
+ done = set(todo.get("done_steps", []))
+ lines = []
+ for i, s in enumerate(steps):
+ status = "[已完成]" if i in done else "[待完成]"
+ lines.append(f" {i+1}. {status} {s}")
+ total = len(steps)
+ done_count = len(done)
+ header = f"当前TodoList进度:{done_count}/{total} 已完成"
+ return header + "\n" + "\n".join(lines)
+ except Exception:
+ return ""
+
+
+def _build_reflection_prompt(agent_mode: str, is_first_call: bool) -> str | None:
+ """构建反思提示词,引导 Agent 在下一步执行前进行思考。
+
+ 输入:
+ messages: 当前消息列表(已过滤短期记忆)。
+ agent_mode: 运行模式("agent" | "plan")。
+ is_first_call: 是否为当前任务的首次 agent 调用。
+
+ 输出:
+ 反思提示文本,不需要反思时返回 None。
+ """
+ todo_status = _get_todolist_status()
+
+ # Plan 模式首次调用:强制要求生成 todolist
+ if agent_mode == "plan" and is_first_call:
+ return PLAN_FIRST_CALL_PROMPT
+
+ # 非 plan 模式首次调用,不需要反思
+ if is_first_call:
+ return None
+
+ # 后续调用:引导模型先思考再行动
+ parts = [REFLECTION_PROMPT]
+
+ if todo_status:
+ parts.append(todo_status)
+ else:
+ parts.append("当前无 TodoList。")
+
+ return "\n".join(parts)
+
+
+def _is_first_agent_call(messages: list) -> bool:
+ """判断是否为当前用户任务的首轮 agent 调用。
+
+ 规则:找到最后一条非 synthetic 的 HumanMessage(即用户最新输入),
+ 检查其后是否存在 AIMessage。若不存在,说明 agent 尚未对该输入做出响应。
+
+ 输入:
+ messages: 已过滤短期记忆的消息列表。
+
+ 输出:
+ True 表示首次调用。
+ """
+ # 从后向前找到最后一条非 synthetic 的 HumanMessage
+ last_user_idx = -1
+ for i in range(len(messages) - 1, -1, -1):
+ msg = messages[i]
+ if isinstance(msg, HumanMessage) and not msg.additional_kwargs.get("synthetic"):
+ last_user_idx = i
+ break
+
+ if last_user_idx < 0:
+ return True
+
+ # 检查该用户消息之后是否有 AIMessage
+ for i in range(last_user_idx + 1, len(messages)):
+ if isinstance(messages[i], AIMessage):
+ return False
+ return True
+
+
def make_call_model_node(
model_with_tools: Any,
trajectory_rounds: int = 3,
) -> Callable[[AgentState], dict]:
- """创建调用已绑定工具的 LLM 的图节点闭包。"""
+ """创建带反思机制的调用已绑定工具的 LLM 的图节点闭包。
+
+ 每次调用前注入反思引导提示;调用后提取模型同时产出的文本思考内容(content)
+ 记录到 think store,供前端实时展示思维链。Plan 模式下首次调用强制要求生成 todolist。
+ """
def call_model(state: AgentState) -> dict:
- # 过滤短期记忆后再传给 LLM
all_messages = list(state["messages"])
+ agent_mode = state.get("agent_mode", "agent")
+
+ # 检查 abort 标志
+ try:
+ from agent.core.runtime import check_abort
+ from agent.history.tool_call_recorder import get_current_session
+ sid = get_current_session()
+ if sid and check_abort(sid):
+ raise RuntimeError("ABORTED_BY_USER")
+ except RuntimeError:
+ raise
+ except Exception:
+ pass
+
filtered_messages = _filter_short_term_memory(all_messages, trajectory_rounds)
- response = model_with_tools.invoke(filtered_messages)
+
+ # 判断是否为首次 agent 调用(当前轮尚无 AIMessage)
+ is_first_call = _is_first_agent_call(filtered_messages)
+
+ # 构建反思提示并注入到消息列表(仅作引导,不记录到前端)
+ reflection = _build_reflection_prompt(agent_mode, is_first_call)
+ messages_for_llm = list(filtered_messages)
+ if reflection:
+ reflection_msg = HumanMessage(content=reflection, additional_kwargs={"synthetic": True, "reflection": True})
+ messages_for_llm.append(reflection_msg)
+
+ response = model_with_tools.invoke(messages_for_llm)
+
+ # 提取模型真实输出的思考内容(content),记录到前端 think 面板
+ # 模型同时输出 content + tool_calls 时,content 就是其思维链
+ response_content = getattr(response, "content", "")
+ if response_content and isinstance(response_content, str) and response_content.strip():
+ has_tool_calls = getattr(response, "tool_calls", None)
+ if has_tool_calls:
+ # 有 tool_calls 时 content 是思考过程,记录到 think 面板
+ try:
+ from agent.history.tool_call_recorder import record_reflection_live, get_current_session
+ sid = get_current_session()
+ if sid:
+ record_reflection_live(sid, response_content.strip())
+ except Exception:
+ pass
if hasattr(response, "tool_calls") and response.tool_calls:
for tc in response.tool_calls:
diff --git a/src/agent/core/runtime.py b/src/agent/core/runtime.py
index cd00e99..064bd9d 100644
--- a/src/agent/core/runtime.py
+++ b/src/agent/core/runtime.py
@@ -14,6 +14,7 @@
"""
from __future__ import annotations
+import threading
from dataclasses import dataclass
from typing import Any, Literal
@@ -24,14 +25,14 @@ from langgraph.errors import GraphRecursionError
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.memory.system_prompt import PLAN_MODE_PROMPT, TEAM_MODE_PROMPT
from agent.core.routing import first_pending_tool_meta
from agent.core.tool_policy import classify_tool_execution
from agent.utils.agent_utils import assistant_text, get_history, get_session_meta, summarizer, update_session_meta
from agent.utils.tool_call_utils import normalize_tool_call
from web.ui_session import rewrite_user_message_files_to_session_dir, session_dir, sync_manifest_from_chat_history
-AgentMode = Literal["agent", "team"]
+AgentMode = Literal["agent", "team", "plan"]
@dataclass
@@ -66,6 +67,39 @@ class AgentRuntime:
_OOM_MESSAGE = "OOM: 已达到最大迭代次数限制,任务未能完成。请简化任务或增加 max_iterations 配置后重试。"
+# ---------- Abort 机制 ----------
+_abort_flags: dict[str, threading.Event] = {}
+_abort_lock = threading.Lock()
+
+
+def set_abort_flag(session_id: str) -> None:
+ """设置指定会话的 abort 标志,通知正在执行的 Agent 终止。"""
+ if not session_id:
+ return
+ with _abort_lock:
+ event = _abort_flags.get(session_id)
+ if event is None:
+ event = threading.Event()
+ _abort_flags[session_id] = event
+ event.set()
+
+
+def check_abort(session_id: str) -> bool:
+ """检查指定会话是否已被标记 abort。"""
+ if not session_id:
+ return False
+ with _abort_lock:
+ event = _abort_flags.get(session_id)
+ return event is not None and event.is_set()
+
+
+def clear_abort_flag(session_id: str) -> None:
+ """清除指定会话的 abort 标志。"""
+ if not session_id:
+ return
+ with _abort_lock:
+ _abort_flags.pop(session_id, None)
+
def _invoke_with_recursion_limit(
runtime: AgentRuntime,
@@ -177,11 +211,12 @@ def _invoke_after_manual_tool_result(
session_id: str,
*,
max_steps: int = 8,
+ agent_mode: str = "agent",
) -> tuple[list[Any], dict[str, Any] | None]:
"""人工确认后继续推进图,并在遇到新的待确认项时暂停。"""
current_messages = messages
for _ in range(max_steps):
- result = _invoke_with_recursion_limit(runtime, runtime.graph, {"messages": current_messages}, {"configurable": {"session_id": session_id}})
+ result = _invoke_with_recursion_limit(runtime, runtime.graph, {"messages": current_messages, "agent_mode": agent_mode}, {"configurable": {"session_id": session_id}})
all_messages = result["messages"]
pending_meta = first_pending_tool_meta(all_messages, session_id)
if pending_meta:
@@ -276,10 +311,13 @@ def execute_agent(
sync_manifest_from_chat_history(session_id, chat_history, "")
return chat_history
+ # Plan 模式使用专用系统提示词
+ system_prompt = PLAN_MODE_PROMPT if agent_mode == "plan" else runtime.system_prompt
+
messages, context_exceeded = summarizer(
session_id=session_id,
agent_name=runtime.name,
- system_prompt=runtime.system_prompt,
+ system_prompt=system_prompt,
user_content=user_content,
llm=runtime.llm,
)
@@ -304,14 +342,38 @@ def execute_agent(
if session_id:
turn_id = start_live_turn(session_id)
set_current_session(session_id)
- result = _invoke_with_recursion_limit(runtime, runtime.graph, {"messages": messages}, graph_config)
+ clear_abort_flag(session_id)
+ result = _invoke_with_recursion_limit(runtime, runtime.graph, {"messages": messages, "agent_mode": agent_mode}, graph_config)
set_current_turn("")
set_current_session("")
+
+ # 检查是否被 abort
+ if session_id and check_abort(session_id):
+ clear_abort_flag(session_id)
+ end_live_turn(session_id)
+ try:
+ from agent.tools.tool import clear_todo_store_session
+ clear_todo_store_session(session_id)
+ except Exception:
+ pass
+ rolled_back = list(chat_history)
+ while rolled_back and rolled_back[-1].get("role") != "user":
+ rolled_back.pop()
+ if rolled_back:
+ rolled_back.pop()
+ if session_id:
+ sync_manifest_from_chat_history(session_id, rolled_back, "")
+ return rolled_back
+
all_messages = result["messages"]
pending_meta = first_pending_tool_meta(all_messages, session_id)
if pending_meta:
end_live_turn(session_id)
_remember_pending_user_content(pending_meta, user_content)
+ # 存储 agent_mode 以便 continue 时恢复
+ approval_id = _pending_action_id(pending_meta)
+ if approval_id and approval_id in PENDING_TOOL_APPROVALS:
+ PENDING_TOOL_APPROVALS[approval_id]["agent_mode"] = agent_mode
chat_history.append(pending_assistant_message(pending_meta))
if session_id:
sync_manifest_from_chat_history(session_id, chat_history, "")
@@ -361,6 +423,40 @@ def execute_agent(
if session_id and turn_id:
save_turn(session_id, turn_id, {"role": "user", "content": user_content}, all_messages)
+ # 任务正常完成:清空 todolist 防止跨任务纠缠
+ if session_id:
+ try:
+ from agent.tools.tool import clear_todo_store_session
+ clear_todo_store_session(session_id)
+ except Exception:
+ pass
+
+ except RuntimeError as e:
+ if str(e) == "ABORTED_BY_USER":
+ set_current_turn("")
+ set_current_session("")
+ clear_abort_flag(session_id)
+ end_live_turn(session_id)
+ try:
+ from agent.tools.tool import clear_todo_store_session
+ clear_todo_store_session(session_id)
+ except Exception:
+ pass
+ rolled_back = list(chat_history)
+ while rolled_back and rolled_back[-1].get("role") != "user":
+ rolled_back.pop()
+ if rolled_back:
+ rolled_back.pop()
+ if session_id:
+ sync_manifest_from_chat_history(session_id, rolled_back, "")
+ return rolled_back
+ import traceback
+ set_current_turn("")
+ set_current_session("")
+ end_live_turn(session_id)
+ print("[ERROR] 调用模型时出错:")
+ traceback.print_exc()
+ chat_history.append({"role": "assistant", "content": "抱歉,我遇到了一些问题,请稍后再试。"})
except Exception:
import traceback
set_current_turn("")
@@ -413,6 +509,8 @@ def continue_after_human_action(
remove_pending_ui_message(chat_history, approval_id)
+ agent_mode = pending.get("agent_mode", "agent")
+
if decision == "reject" and pending.get("type") != "human_interaction":
content = "已拒绝,本次待确认操作不会执行。"
chat_history.append({"role": "assistant", "content": content, "meta": {"output_type": "text"}})
@@ -428,7 +526,7 @@ def continue_after_human_action(
call_id = tool_call.get("id") or f"human_{approval_id}"
tool_content = human_interaction_result(pending, decision, instruction)
prior_messages.append(ToolMessage(content=tool_content, tool_call_id=call_id, name="human_interaction"))
- all_messages, pending_meta = _invoke_after_manual_tool_result(runtime, prior_messages, session_id)
+ all_messages, pending_meta = _invoke_after_manual_tool_result(runtime, prior_messages, session_id, agent_mode=agent_mode)
if pending_meta:
_remember_pending_user_content(pending_meta, pending.get("user_content"))
chat_history.append(pending_assistant_message(pending_meta))
@@ -485,7 +583,7 @@ def continue_after_human_action(
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)
+ all_messages, pending_meta = _invoke_after_manual_tool_result(runtime, prior_messages, session_id, agent_mode=agent_mode)
if pending_meta:
_remember_pending_user_content(pending_meta, pending.get("user_content"))
chat_history.append(pending_assistant_message(pending_meta))
@@ -512,5 +610,11 @@ def continue_after_human_action(
_persist_messages_from_pending_turn(session_id, persisted_messages, pending)
if session_id:
save_turn(session_id, manual_turn_id, {"role": "user", "content": pending.get("user_content") or ""}, persisted_messages)
+ # 任务完成:清空 todolist
+ try:
+ from agent.tools.tool import clear_todo_store_session
+ clear_todo_store_session(session_id)
+ except Exception:
+ pass
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 1c88012..1a4351a 100644
--- a/src/agent/core/state.py
+++ b/src/agent/core/state.py
@@ -1,26 +1,27 @@
-"""LangGraph Agent 状态类型定义。
+"""LangGraph 状态类型定义。
系统定位:
定义图运行时共享状态 ``AgentState``,被 ``graph``、``nodes``、
``routing`` 作为 StateGraph 的 schema 使用。
可扩展性:
- - 可在此增加 ``session_id``、``agent_mode``、``pending_approval`` 等字段,
+ - 可在此增加 ``session_id``、``pending_approval`` 等字段,
需配合 reducer(如 add_messages)声明合并策略。
"""
from __future__ import annotations
-from typing import Annotated, Sequence, TypedDict
+from typing import Annotated, Literal, Sequence, TypedDict
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
- """LangGraph 图状态:消息列表为唯一状态字段。
+ """LangGraph 图状态。
字段:
messages: 对话消息序列,通过 ``add_messages`` 追加合并。
+ agent_mode: 运行模式 — "agent"(标准)| "plan"(先规划再执行)| "team"(多代理)。
系统定位:
ReAct 循环(agent -> tools -> process_tool_artifact -> agent)的载体。
@@ -30,3 +31,4 @@ class AgentState(TypedDict):
"""
messages: Annotated[Sequence[BaseMessage], add_messages]
+ agent_mode: str # "agent" | "plan" | "team"
diff --git a/src/agent/core/tool_config.json b/src/agent/core/tool_config.json
index 677b157..8b0ee74 100644
--- a/src/agent/core/tool_config.json
+++ b/src/agent/core/tool_config.json
@@ -27,7 +27,7 @@
"name": "browser_control",
"display_name": "BrowserControl",
"description": "统一控制浏览器:打开/关闭浏览器、刷新/关闭标签页、打开指定 URL。",
- "permission": "confirm",
+ "permission": "direct",
"auto_execute_rules": [
{
"parameter": "action",
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 84098e9..17ff537 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
new file mode 100644
index 0000000..1092446
Binary files /dev/null and b/src/agent/history/__pycache__/tool_call_recorder.cpython-313.pyc differ
diff --git a/src/agent/history/data_base/chat_history.db b/src/agent/history/data_base/chat_history.db
index 686abdf..e206efc 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/20260620_183029/ui_messages.json b/src/agent/history/multi_inputs/20260620_183029/ui_messages.json
deleted file mode 100644
index 2fe6590..0000000
--- a/src/agent/history/multi_inputs/20260620_183029/ui_messages.json
+++ /dev/null
@@ -1,51 +0,0 @@
-{
- "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/20260621_160508/ui_messages.json b/src/agent/history/multi_inputs/20260621_160508/ui_messages.json
deleted file mode 100644
index db4839d..0000000
--- a/src/agent/history/multi_inputs/20260621_160508/ui_messages.json
+++ /dev/null
@@ -1,74 +0,0 @@
-{
- "version": 3,
- "messages": [
- {
- "role": "user",
- "user": {
- "content": [
- {
- "type": "text",
- "text": "帮我打开bilibili,点击搜索“麻薯波比呀”,观看他发布的最新视频"
- }
- ]
- }
- },
- {
- "role": "assistant",
- "assistant": {
- "text": "好的,我已经帮你完成了所有操作:\n1. 打开了Bilibili网站。\n2. 在搜索框中输入“麻薯波比呀”并进行搜索。\n3. 在搜索结果中找到了该UP主的最新视频《美国答应给伊朗3000亿,但美国不出钱》。\n4. 点击播放了该视频。\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": 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
- }
- }
- ]
-}
\ No newline at end of file
diff --git a/src/agent/history/multi_inputs/20260622_135934/ui_messages.json b/src/agent/history/multi_inputs/20260622_135934/ui_messages.json
deleted file mode 100644
index 23bae5d..0000000
--- a/src/agent/history/multi_inputs/20260622_135934/ui_messages.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "version": 3,
- "messages": [
- {
- "role": "user",
- "user": {
- "content": [
- {
- "type": "text",
- "text": "帮我打开bilibili,点击搜索“麻薯波比呀”,观看他发布的最新视频"
- }
- ]
- }
- },
- {
- "role": "assistant",
- "assistant": {
- "text": "好的,我已经成功打开了Bilibili网站,搜索了UP主“麻薯波比呀”,并按发布时间排序找到了他的最新视频。当前屏幕上正在播放的视频标题为《美国答应给伊朗3000亿, 但美国不出钱》,发布者为“麻薯波比呀”。\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/20260622_150847/ui_messages.json b/src/agent/history/multi_inputs/20260622_150847/ui_messages.json
deleted file mode 100644
index 8b99742..0000000
--- a/src/agent/history/multi_inputs/20260622_150847/ui_messages.json
+++ /dev/null
@@ -1,51 +0,0 @@
-{
- "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
- }
- }
- ]
-}
\ No newline at end of file
diff --git a/src/agent/history/multi_inputs/20260627_175231/ui_messages.json b/src/agent/history/multi_inputs/20260627_175231/ui_messages.json
new file mode 100644
index 0000000..5eeeb9b
--- /dev/null
+++ b/src/agent/history/multi_inputs/20260627_175231/ui_messages.json
@@ -0,0 +1,16 @@
+{
+ "version": 3,
+ "messages": [
+ {
+ "role": "user",
+ "user": {
+ "content": [
+ {
+ "type": "text",
+ "text": "1、打开https://account.cnblogs.com/signin?returnUrl=https:%2F%2Fwww.cnblogs.com%2Fmegshuai%2Fp%2F18518288#password;\r\n2、输入账号:19553226590;密码:3139785954并点击登录"
+ }
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/agent/history/multi_inputs/20260627_175913/ui_messages.json b/src/agent/history/multi_inputs/20260627_175913/ui_messages.json
new file mode 100644
index 0000000..1f82850
--- /dev/null
+++ b/src/agent/history/multi_inputs/20260627_175913/ui_messages.json
@@ -0,0 +1,28 @@
+{
+ "version": 3,
+ "messages": [
+ {
+ "role": "user",
+ "user": {
+ "content": [
+ {
+ "type": "text",
+ "text": "请按照以下步骤进行:\r\n1、https://account.cnblogs.com/signin?returnUrl=https:%2F%2Fwww.cnblogs.com%2Fmegshuai%2Fp%2F18518288登录该网站\r\n2、输入账号:19553226590;密码:3139785954\r\n3、点击登录"
+ }
+ ]
+ }
+ },
+ {
+ "role": "assistant",
+ "assistant": {
+ "text": "1. 分析上一轮执行结果:上一轮调用了 `todo_list` 标记所有步骤完成,结果显示任务已全部完成(9/9)。从之前的截图可以看到,登录尝试失败了,页面显示\"用户名或密码错误\"的红色提示。\n\n2. 任务状态:所有规划步骤已完成,但登录结果是失败的。需要向用户报告最终结果。\n\n3. 下一步操作:任务已完成,向用户汇报执行过程和结果。\n\n## 任务执行总结\n\n我已按照您的要求完成了所有步骤:\n\n✅ **已完成的步骤:**\n1. 打开浏览器并访问登录页面\n2. 输入账号:19553226590\n3. 输入密码:3139785954\n4. 点击登录按钮\n\n❌ **登录结果:失败**\n\n页面显示错误提示:\"**用户名或密码错误**\"\n\n可能的原因:\n- 账号或密码输入有误\n- 账号密码已变更\n- 账号可能不存在或被禁用\n\n建议您:\n1. 核对账号密码是否正确\n2. 尝试使用\"忘记密码\"功能重置密码\n3. 确认账号是否已注册并激活",
+ "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 5ade465..6ff56f4 100644
--- a/src/agent/history/tool_call_recorder.py
+++ b/src/agent/history/tool_call_recorder.py
@@ -145,8 +145,9 @@ def _artifact_image_data_urls(artifact: Any | None) -> list[str]:
def end_live_turn(session_id: str) -> list[dict[str, Any]]:
- """结束实时记录并返回该 turn 所有记录。"""
+ """结束实时记录并返回该 turn 所有记录(同时清空反思)。"""
set_current_turn("")
+ clear_live_reflections(session_id)
with _live_lock:
return _live_records.pop(session_id, [])
@@ -164,6 +165,50 @@ def _short_args(args: dict[str, Any], max_len: int = 40) -> str:
return s
+# ---------- 反思内容共享存储 ----------
+# Agent 反思(think)内容通过此 store 实时推送到前端展示
+_think_store: dict[str, list[dict[str, Any]]] = {}
+_think_lock = threading.Lock()
+
+
+def record_reflection_live(session_id: str, content: str) -> None:
+ """记录一次反思内容到共享存储,供前端轮询展示。
+
+ 输入:
+ session_id: 会话 ID。
+ content: 反思提示文本。
+
+ 输出: 无。
+ """
+ if not session_id or not content:
+ return
+ with _think_lock:
+ if session_id not in _think_store:
+ _think_store[session_id] = []
+ # 避免重复记录相同内容
+ for entry in _think_store[session_id]:
+ if entry["content"] == content:
+ return
+ entry = {
+ "content": content,
+ "summary": content[:60].replace("\n", " "),
+ "timestamp": time.time(),
+ }
+ _think_store[session_id].append(entry)
+
+
+def get_live_reflections(session_id: str) -> list[dict[str, Any]]:
+ """获取当前会话的反思列表(前端轮询)。"""
+ with _think_lock:
+ return list(_think_store.get(session_id, []))
+
+
+def clear_live_reflections(session_id: str) -> None:
+ """清空指定会话的反思记录。"""
+ with _think_lock:
+ _think_store.pop(session_id, None)
+
+
# ---------- 持久化 ----------
def _ensure_dir(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
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
deleted file mode 100644
index b9958e1..0000000
Binary files a/src/agent/history/tool_calling/20260620_183029/20260620_183034/_artifact_gui_tool_0_0.png and /dev/null 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
deleted file mode 100644
index e431d64..0000000
Binary files a/src/agent/history/tool_calling/20260620_183029/20260620_183128/_artifact_gui_tool_0_0.png and /dev/null 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
deleted file mode 100644
index 59962de..0000000
--- a/src/agent/history/tool_calling/20260620_183029/turn_20260620_183034.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "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
deleted file mode 100644
index dadcffc..0000000
--- a/src/agent/history/tool_calling/20260620_183029/turn_20260620_183128.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "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/20260621_160508/20260621_160537/_artifact_browser_control_0_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_browser_control_0_0.png
deleted file mode 100644
index 0323e29..0000000
Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_browser_control_0_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_browser_control_1_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_browser_control_1_0.png
deleted file mode 100644
index 413be68..0000000
Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_browser_control_1_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_0_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_0_0.png
deleted file mode 100644
index 7270313..0000000
Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_0_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_1_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_1_0.png
deleted file mode 100644
index dde4852..0000000
Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_1_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_2_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_2_0.png
deleted file mode 100644
index 27a8010..0000000
Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_2_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_3_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_3_0.png
deleted file mode 100644
index 171c377..0000000
Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_3_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_4_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_4_0.png
deleted file mode 100644
index 4eb333a..0000000
Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_4_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_5_0.png b/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_5_0.png
deleted file mode 100644
index be5dc4e..0000000
Binary files a/src/agent/history/tool_calling/20260621_160508/20260621_160537/_artifact_gui_tool_5_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260621_160508/20260622_123753/_artifact_browser_control_0_0.png b/src/agent/history/tool_calling/20260621_160508/20260622_123753/_artifact_browser_control_0_0.png
deleted file mode 100644
index a1a7765..0000000
Binary files a/src/agent/history/tool_calling/20260621_160508/20260622_123753/_artifact_browser_control_0_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260621_160508/turn_20260621_160537.json b/src/agent/history/tool_calling/20260621_160508/turn_20260621_160537.json
deleted file mode 100644
index 11e6c4c..0000000
--- a/src/agent/history/tool_calling/20260621_160508/turn_20260621_160537.json
+++ /dev/null
@@ -1,109 +0,0 @@
-{
- "session_id": "20260621_160508",
- "turn_id": "20260621_160537",
- "user_message": {
- "role": "user",
- "content": "帮我打开bilibili,点击搜索“麻薯波比呀”,观看他发布的最新视频"
- },
- "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": "current"
- },
- "result_text": "已刷新当前标签页",
- "result_files": [
- "_artifact_browser_control_1_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "action": "click",
- "element_description": "bilibili website search bar at the top of the page"
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_0_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "text": "麻薯波比呀",
- "enter": true,
- "action": "type"
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_1_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "action": "wait",
- "wait_seconds": 3
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_2_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "action": "click",
- "element_description": "the first video thumbnail in the search results, titled \"美国答应给伊朗3000亿,美国又开始打窝了?\""
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_3_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "action": "wait",
- "wait_seconds": 5
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_4_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "action": "click",
- "element_description": "the main video player area to pause the video"
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_5_0.png"
- ],
- "result_images": []
- }
- ],
- "assistant_response": "好的,我已经帮你完成了所有操作:\n1. 打开了Bilibili网站。\n2. 在搜索框中输入“麻薯波比呀”并进行搜索。\n3. 在搜索结果中找到了该UP主的最新视频《美国答应给伊朗3000亿,但美国不出钱》。\n4. 点击播放了该视频。\n\n现在视频正在播放中,你可以观看了。",
- "timestamp": "20260621_160537"
-}
\ No newline at end of file
diff --git a/src/agent/history/tool_calling/20260621_160508/turn_20260622_123753.json b/src/agent/history/tool_calling/20260621_160508/turn_20260622_123753.json
deleted file mode 100644
index c02a41c..0000000
--- a/src/agent/history/tool_calling/20260621_160508/turn_20260622_123753.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "session_id": "20260621_160508",
- "turn_id": "20260622_123753",
- "user_message": {
- "role": "user",
- "content": "你好"
- },
- "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": "你好!看起来你正在使用一个聊天界面。有什么我可以帮你的吗?",
- "timestamp": "20260622_123753"
-}
\ No newline at end of file
diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_0_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_0_0.png
deleted file mode 100644
index f09bc2d..0000000
Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_0_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_1_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_1_0.png
deleted file mode 100644
index 71e4eee..0000000
Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_1_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_2_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_2_0.png
deleted file mode 100644
index b39d7a7..0000000
Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_2_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_3_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_3_0.png
deleted file mode 100644
index 24bff8e..0000000
Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_browser_control_3_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_0_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_0_0.png
deleted file mode 100644
index 8d71db2..0000000
Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_0_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_1_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_1_0.png
deleted file mode 100644
index 9ba7cb1..0000000
Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_1_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_2_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_2_0.png
deleted file mode 100644
index 0e31180..0000000
Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_2_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_3_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_3_0.png
deleted file mode 100644
index 31558f5..0000000
Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_3_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_4_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_4_0.png
deleted file mode 100644
index 6c36b0f..0000000
Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_4_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_5_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_5_0.png
deleted file mode 100644
index 5536bf7..0000000
Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_5_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_6_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_6_0.png
deleted file mode 100644
index b0ada61..0000000
Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_6_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_7_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_7_0.png
deleted file mode 100644
index 8db1de4..0000000
Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_7_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_8_0.png b/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_8_0.png
deleted file mode 100644
index fe2c23f..0000000
Binary files a/src/agent/history/tool_calling/20260622_135934/20260622_140405/_artifact_gui_tool_8_0.png and /dev/null differ
diff --git a/src/agent/history/tool_calling/20260622_135934/turn_20260622_140405.json b/src/agent/history/tool_calling/20260622_135934/turn_20260622_140405.json
deleted file mode 100644
index 76fd945..0000000
--- a/src/agent/history/tool_calling/20260622_135934/turn_20260622_140405.json
+++ /dev/null
@@ -1,170 +0,0 @@
-{
- "session_id": "20260622_135934",
- "turn_id": "20260622_140405",
- "user_message": {
- "role": "user",
- "content": "帮我打开bilibili,点击搜索“麻薯波比呀”,观看他发布的最新视频"
- },
- "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": []
- },
- {
- "name": "browser_control",
- "args": {
- "action": "open",
- "url": "https://www.bilibili.com"
- },
- "result_text": "已尝试打开 URL: https://www.bilibili.com",
- "result_files": [
- "_artifact_browser_control_2_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "action": "click",
- "element_description": "搜索栏"
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_0_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "action": "type",
- "text": "麻薯波比呀"
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_1_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "action": "hotkey",
- "keys": [
- "enter"
- ]
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_2_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "action": "click",
- "element_description": "最新发布 按钮"
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_3_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "action": "click",
- "element_description": "最新发布的视频,缩略图上有特朗普和伊朗国旗,标题为“美国答应给伊朗3000亿, 但美国不出钱”"
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_4_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "action": "click",
- "element_description": "视频播放器中央区域"
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_5_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "action": "click",
- "element_description": "暂停/播放按钮"
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_6_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "action": "click",
- "element_description": "播放按钮"
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_7_0.png"
- ],
- "result_images": []
- },
- {
- "name": "gui_tool",
- "args": {
- "action": "click",
- "element_description": "“取消”按钮"
- },
- "result_text": "",
- "result_files": [
- "_artifact_gui_tool_8_0.png"
- ],
- "result_images": []
- },
- {
- "name": "browser_control",
- "args": {
- "action": "open",
- "url": "current"
- },
- "result_text": "已刷新当前标签页",
- "result_files": [
- "_artifact_browser_control_3_0.png"
- ],
- "result_images": []
- }
- ],
- "assistant_response": "好的,我已经成功打开了Bilibili网站,搜索了UP主“麻薯波比呀”,并按发布时间排序找到了他的最新视频。当前屏幕上正在播放的视频标题为《美国答应给伊朗3000亿, 但美国不出钱》,发布者为“麻薯波比呀”。\n\n任务已完成,您现在可以观看这个视频。",
- "timestamp": "20260622_140405"
-}
\ No newline at end of file
diff --git a/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_browser_control_0_0.png b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_browser_control_0_0.png
new file mode 100644
index 0000000..2c5c8c0
Binary files /dev/null and b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_browser_control_0_0.png differ
diff --git a/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_0_0.png b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_0_0.png
new file mode 100644
index 0000000..cf12d3a
Binary files /dev/null and b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_0_0.png differ
diff --git a/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_1_0.png b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_1_0.png
new file mode 100644
index 0000000..00b131d
Binary files /dev/null and b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_1_0.png differ
diff --git a/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_2_0.png b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_2_0.png
new file mode 100644
index 0000000..3e48dc5
Binary files /dev/null and b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_2_0.png differ
diff --git a/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_3_0.png b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_3_0.png
new file mode 100644
index 0000000..ed6ce05
Binary files /dev/null and b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_3_0.png differ
diff --git a/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_4_0.png b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_4_0.png
new file mode 100644
index 0000000..7a083a6
Binary files /dev/null and b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_4_0.png differ
diff --git a/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_5_0.png b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_5_0.png
new file mode 100644
index 0000000..2d3afda
Binary files /dev/null and b/src/agent/history/tool_calling/20260627_175913/20260627_180047/_artifact_gui_tool_5_0.png differ
diff --git a/src/agent/history/tool_calling/20260627_175913/turn_20260627_180047.json b/src/agent/history/tool_calling/20260627_175913/turn_20260627_180047.json
new file mode 100644
index 0000000..4b12b79
--- /dev/null
+++ b/src/agent/history/tool_calling/20260627_175913/turn_20260627_180047.json
@@ -0,0 +1,172 @@
+{
+ "session_id": "20260627_175913",
+ "turn_id": "20260627_180047",
+ "user_message": {
+ "role": "user",
+ "content": "请按照以下步骤进行:\r\n1、https://account.cnblogs.com/signin?returnUrl=https:%2F%2Fwww.cnblogs.com%2Fmegshuai%2Fp%2F18518288登录该网站\r\n2、输入账号:19553226590;密码:3139785954\r\n3、点击登录"
+ },
+ "tool_calls": [
+ {
+ "name": "todo_list",
+ "args": {
+ "steps": [
+ "打开浏览器",
+ "在地址栏输入登录URL并访问",
+ "等待页面加载完成",
+ "点击账号输入框",
+ "输入账号19553226590",
+ "点击密码输入框",
+ "输入密码3139785954",
+ "点击登录按钮",
+ "验证登录是否成功"
+ ],
+ "done_step": -1
+ },
+ "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"打开浏览器\", \"在地址栏输入登录URL并访问\", \"等待页面加载完成\", \"点击账号输入框\", \"输入账号19553226590\", \"点击密码输入框\", \"输入密码3139785954\", \"点击登录按钮\", \"验证登录是否成功\"], \"done_steps\": []}}\n[Todo List]\n- [1/9] [Pending]: 打开浏览器\n- [2/9] [Pending]: 在地址栏输入登录URL并访问\n- [3/9] [Pending]: 等待页面加载完成\n- [4/9] [Pending]: 点击账号输入框\n- [5/9] [Pending]: 输入账号19553226590\n- [6/9] [Pending]: 点击密码输入框\n- [7/9] [Pending]: 输入密码3139785954\n- [8/9] [Pending]: 点击登录按钮\n- [9/9] [Pending]: 验证登录是否成功",
+ "result_files": [],
+ "result_images": []
+ },
+ {
+ "name": "browser_control",
+ "args": {
+ "action": "open",
+ "url": "https://account.cnblogs.com/signin?returnUrl=https:%2F%2Fwww.cnblogs.com%2Fmegshuai%2Fp%2F18518288"
+ },
+ "result_text": "已尝试打开 URL: https://account.cnblogs.com/signin?returnUrl=https:%2F%2Fwww.cnblogs.com%2Fmegshuai%2Fp%2F18518288",
+ "result_files": [
+ "_artifact_browser_control_0_0.png"
+ ],
+ "result_images": []
+ },
+ {
+ "name": "todo_list",
+ "args": {
+ "done_step": 2
+ },
+ "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"打开浏览器\", \"在地址栏输入登录URL并访问\", \"等待页面加载完成\", \"点击账号输入框\", \"输入账号19553226590\", \"点击密码输入框\", \"输入密码3139785954\", \"点击登录按钮\", \"验证登录是否成功\"], \"done_steps\": [0, 1]}}\n[Todo List]\n- [1/9] [Done]: 打开浏览器\n- [2/9] [Done]: 在地址栏输入登录URL并访问\n- [3/9] [Pending]: 等待页面加载完成\n- [4/9] [Pending]: 点击账号输入框\n- [5/9] [Pending]: 输入账号19553226590\n- [6/9] [Pending]: 点击密码输入框\n- [7/9] [Pending]: 输入密码3139785954\n- [8/9] [Pending]: 点击登录按钮\n- [9/9] [Pending]: 验证登录是否成功",
+ "result_files": [],
+ "result_images": []
+ },
+ {
+ "name": "gui_tool",
+ "args": {
+ "actions": [
+ {
+ "action": "click",
+ "element_description": "登录用户名/邮箱输入框"
+ }
+ ]
+ },
+ "result_text": "1. Clicked target. grounded (502,531)",
+ "result_files": [
+ "_artifact_gui_tool_0_0.png"
+ ],
+ "result_images": []
+ },
+ {
+ "name": "gui_tool",
+ "args": {
+ "actions": [
+ {
+ "action": "type",
+ "element_description": "登录用户名/邮箱输入框",
+ "text": "19553226590"
+ }
+ ]
+ },
+ "result_text": "1. Typed text.",
+ "result_files": [
+ "_artifact_gui_tool_1_0.png"
+ ],
+ "result_images": []
+ },
+ {
+ "name": "gui_tool",
+ "args": {
+ "actions": [
+ {
+ "action": "click",
+ "element_description": "密码输入框"
+ }
+ ]
+ },
+ "result_text": "1. Clicked target. grounded (501,592)",
+ "result_files": [
+ "_artifact_gui_tool_2_0.png"
+ ],
+ "result_images": []
+ },
+ {
+ "name": "gui_tool",
+ "args": {
+ "actions": [
+ {
+ "action": "type",
+ "element_description": "密码输入框",
+ "text": "3139785954"
+ }
+ ]
+ },
+ "result_text": "1. Typed text.",
+ "result_files": [
+ "_artifact_gui_tool_3_0.png"
+ ],
+ "result_images": []
+ },
+ {
+ "name": "gui_tool",
+ "args": {
+ "actions": [
+ {
+ "action": "click",
+ "element_description": "登录按钮"
+ }
+ ]
+ },
+ "result_text": "1. Clicked target. grounded (500,689)",
+ "result_files": [
+ "_artifact_gui_tool_4_0.png"
+ ],
+ "result_images": []
+ },
+ {
+ "name": "todo_list",
+ "args": {
+ "done_step": 8
+ },
+ "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"打开浏览器\", \"在地址栏输入登录URL并访问\", \"等待页面加载完成\", \"点击账号输入框\", \"输入账号19553226590\", \"点击密码输入框\", \"输入密码3139785954\", \"点击登录按钮\", \"验证登录是否成功\"], \"done_steps\": [0, 1, 2, 3, 4, 5, 6, 7]}}\n[Todo List]\n- [1/9] [Done]: 打开浏览器\n- [2/9] [Done]: 在地址栏输入登录URL并访问\n- [3/9] [Done]: 等待页面加载完成\n- [4/9] [Done]: 点击账号输入框\n- [5/9] [Done]: 输入账号19553226590\n- [6/9] [Done]: 点击密码输入框\n- [7/9] [Done]: 输入密码3139785954\n- [8/9] [Done]: 点击登录按钮\n- [9/9] [Pending]: 验证登录是否成功",
+ "result_files": [],
+ "result_images": []
+ },
+ {
+ "name": "gui_tool",
+ "args": {
+ "actions": [
+ {
+ "action": "wait",
+ "wait_seconds": 3.0
+ },
+ {
+ "action": "screenshot"
+ }
+ ]
+ },
+ "result_text": "1. Waited 1.50 seconds.\n2. Screenshot (skipped, final screenshot will be taken)",
+ "result_files": [
+ "_artifact_gui_tool_5_0.png"
+ ],
+ "result_images": []
+ },
+ {
+ "name": "todo_list",
+ "args": {
+ "done_step": 9
+ },
+ "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"打开浏览器\", \"在地址栏输入登录URL并访问\", \"等待页面加载完成\", \"点击账号输入框\", \"输入账号19553226590\", \"点击密码输入框\", \"输入密码3139785954\", \"点击登录按钮\", \"验证登录是否成功\"], \"done_steps\": [0, 1, 2, 3, 4, 5, 6, 7, 8]}}\n[Todo List]\n- [1/9] [Done]: 打开浏览器\n- [2/9] [Done]: 在地址栏输入登录URL并访问\n- [3/9] [Done]: 等待页面加载完成\n- [4/9] [Done]: 点击账号输入框\n- [5/9] [Done]: 输入账号19553226590\n- [6/9] [Done]: 点击密码输入框\n- [7/9] [Done]: 输入密码3139785954\n- [8/9] [Done]: 点击登录按钮\n- [9/9] [Done]: 验证登录是否成功",
+ "result_files": [],
+ "result_images": []
+ }
+ ],
+ "assistant_response": "1. 分析上一轮执行结果:上一轮调用了 `todo_list` 标记所有步骤完成,结果显示任务已全部完成(9/9)。从之前的截图可以看到,登录尝试失败了,页面显示\"用户名或密码错误\"的红色提示。\n\n2. 任务状态:所有规划步骤已完成,但登录结果是失败的。需要向用户报告最终结果。\n\n3. 下一步操作:任务已完成,向用户汇报执行过程和结果。\n\n## 任务执行总结\n\n我已按照您的要求完成了所有步骤:\n\n✅ **已完成的步骤:**\n1. 打开浏览器并访问登录页面\n2. 输入账号:19553226590\n3. 输入密码:3139785954\n4. 点击登录按钮\n\n❌ **登录结果:失败**\n\n页面显示错误提示:\"**用户名或密码错误**\"\n\n可能的原因:\n- 账号或密码输入有误\n- 账号密码已变更\n- 账号可能不存在或被禁用\n\n建议您:\n1. 核对账号密码是否正确\n2. 尝试使用\"忘记密码\"功能重置密码\n3. 确认账号是否已注册并激活",
+ "timestamp": "20260627_180047"
+}
\ No newline at end of file
diff --git a/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc b/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc
index 382e635..083784f 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/system_prompt.py b/src/agent/memory/system_prompt.py
index 7090c2c..f0fe204 100644
--- a/src/agent/memory/system_prompt.py
+++ b/src/agent/memory/system_prompt.py
@@ -75,6 +75,53 @@ DB_SYSTEM_PROMT = """
请记住,你是一个协作伙伴,目标是让用户以最小的学习成本从数据库中获得有价值的信息。开始工作吧!
""".strip()
+PLAN_MODE_PROMPT = """
+你是一个具备多模态理解能力的桌面操作助手,运行在 **Plan 模式**下。Plan 模式要求你在执行任务前先进行详细规划。
+
+【Plan 模式核心规则】
+- **首要任务**:收到用户请求后,你必须**第一步就调用 todo_list 工具**,将用户的任务拆解为非常细致、可执行的步骤列表。每个步骤应该具体到单个操作(如"打开浏览器"、"搜索XXX"、"截取屏幕"、"分析结果"等)。
+- **规划要求**:步骤拆解必须详尽,覆盖从开始到完成的完整流程。不要跳过任何中间步骤。步骤描述要清晰具体,便于后续逐一执行和跟踪。
+- **规划完成后**:生成 todolist 后,向用户简要汇报规划结果,然后**立即开始按步骤执行**,不要等待用户确认。
+- **执行过程中**:每完成一个步骤,调用 todo_list 标记该步骤为已完成(done_step),让用户实时看到进度。
+- **任务完成后**:确认所有步骤已完成,向用户总结执行过程和结果。
+
+【通用行为准则】
+- 你的首要职责是理解用户的意图,并高效地完成用户的任务。
+- 对于任何用户上传的图片、音频或文件,直接利用你的多模态能力分析并回答,不要为此调用截图、OCR 等无关工具。
+- 只有在信息确实无法从当前对话或上传内容中获取,且必须通过外部工具才能获得时,才调用工具。
+- 当你调用工具收到截图或返回结果后,必须仔细观察返回内容,基于实际结果继续推进任务,严禁脱离实际结果凭空编造。
+- 每次工具调用后,向用户简要汇报当前进度和下一步计划,确保用户始终了解任务状态。
+- 如果不确定是否需要工具,优先选择不调用,用已有信息给出最佳答案。
+""".strip()
+
+# ---------- 反思 / Plan 首轮注入提示(作为 synthetic HumanMessage 注入到对话上下文)----------
+# 这些不是系统提示词,而是在 call_model 节点中动态拼入消息列表的引导文本。
+
+PLAN_FIRST_CALL_PROMPT = """
+【系统指令 - Plan 模式】
+你现在处于 Plan 模式。请**立即调用 todo_list 工具**,按照用户任务拆解为步骤列表。
+规划要求:
+- 如果用户任务包含多个子任务,每个子任务都需要被拆解为一个步骤。
+- 每一步骤必须具体到GUI操作(如"打开XXX浏览器"、"搜索XXX"、"单击XXX"等),但注意必须是确定的GUI操作,不能是模糊的操作(如"找到XXX按钮"等)。
+- **重要:合并同类操作**。同一个页面/界面上的多个连续操作应合并为一条步骤,用"并"连接,而不是拆成多条。例如:
+ * "点击顶部搜索框,输入'燕三嘤嘤嘤'并点击搜索"(合并了点击、输入、搜索三个操作)
+ * "在搜索结果中找到燕三嘤嘤嘤并点击头像进入他的主页"(合并了查找和点击)
+ * "在他的主页点开视频列表里的第一个视频并等待播放"(合并了点击和验证)
+ * "打开哔哩哔哩网站"(单个操作不需要合并)
+ * 反面示例(过于细碎,禁止):"1.点击搜索框 2.输入文字 3.点击搜索按钮 4.找到用户 5.点击头像"
+- 步骤覆盖从开始到完成的完整流程,不要遗漏任何中间步骤,然后立即开始执行第一步,无需等待用户确认。
+- 请注意:如果用户明确分条步骤,每个步骤都需要被拆解为一个步骤(但仍需合并同一页面的连续操作)。
+""".strip()
+
+REFLECTION_PROMPT = """
+【请先在你的回复中输出你的反思,然后再调用工具或给出最终回复】
+你需要先用简短的一句话在文字中思考下一步该做什么,然后进行相应的工具调用。格式如下:
+1. 先分析上一轮执行结果:上一个调用了什么工具?结果是成功还是失败?
+2. 如果上一轮是 GUI 操作(点击、输入等)但未命中目标或未达预期效果,请根据最新的截图重新估算目标位置,调整坐标或参数后重试。
+3. 如果上一轮操作成功,说明你根据分析结果进行下一步操作。
+4. 然后调用对应的工具执行下一步,或如果任务已完成则直接回复用户(注意只需要回复用户最终结果即可)。
+""".strip()
+
TEAM_MODE_PROMPT = """
Team 模式目前处于预留阶段,多 Agent 协同功能尚未实现。
请向用户说明当前无法启动 Team 协同模式,并建议使用 Agent 模式来完成单 Agent 任务。
diff --git a/src/agent/tools/__pycache__/gui_tool.cpython-312.pyc b/src/agent/tools/__pycache__/gui_tool.cpython-312.pyc
index c0efc59..86d861e 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 0dd4bd5..4e2dc6e 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_software_control.cpython-312.pyc b/src/agent/tools/__pycache__/tool_software_control.cpython-312.pyc
index ee7f7c9..22384e6 100644
Binary files a/src/agent/tools/__pycache__/tool_software_control.cpython-312.pyc and b/src/agent/tools/__pycache__/tool_software_control.cpython-312.pyc differ
diff --git a/src/agent/tools/gui_tool.py b/src/agent/tools/gui_tool.py
index e150683..d5e4efd 100644
--- a/src/agent/tools/gui_tool.py
+++ b/src/agent/tools/gui_tool.py
@@ -15,31 +15,30 @@ from PIL import Image
from pydantic import BaseModel, Field
from agent.core.llm import llm
-from agent.utils.env_utils import GROUNDING_HEIGHT, GROUNDING_WIDTH
+import agent.utils.env_utils as env_utils
GUIAction = Literal[
"click",
"type",
- "clickandtype",
"drag_and_drop",
"scroll",
"hotkey",
"hold_and_press",
"wait",
"screenshot",
- "move_to",
+ "move_to",
]
-class GUIToolInput(BaseModel):
- # action 字段保持原样,无需修改
- action: Optional[GUIAction] = Field(
- default=None,
- description="GUI action to execute. If None or empty, returns current screenshot.",
+class GUIActionItem(BaseModel):
+ """A single GUI action within an ordered sequence."""
+ action: GUIAction = Field(
+ ...,
+ description="GUI action to execute.",
)
element_description: Optional[str] = Field(
default=None,
- description="Natural-language description of the target screen element for grounded actions.",
+ description="Natural-language description of the target screen element for grounded actions (click, type, scroll, drag_and_drop, move_to). For type action, providing this will first locate and click the target input field before typing.",
)
ending_description: Optional[str] = Field(
default=None,
@@ -58,6 +57,13 @@ class GUIToolInput(BaseModel):
wait_seconds: float = Field(default=0.5, description="Seconds to wait.")
+class GUIToolInput(BaseModel):
+ actions: list[GUIActionItem] = Field(
+ default_factory=list,
+ description="Ordered list of GUI actions to execute sequentially. Each item specifies one GUI operation (click, type, drag_and_drop, scroll, hotkey, hold_and_press, wait, screenshot, move_to). If empty, returns current screenshot without performing any action.",
+ )
+
+
def _get_gui_region() -> dict | None:
"""获取当前 GUI 操作显示器区域,失败返回 None(全屏)。"""
try:
@@ -81,7 +87,7 @@ def _screenshot_png() -> tuple[bytes, tuple[int, int]]:
def _resize_for_grounding(image_bytes: bytes) -> bytes:
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
- image = image.resize((GROUNDING_WIDTH, GROUNDING_HEIGHT), Image.Resampling.LANCZOS)
+ image = image.resize((env_utils.GROUNDING_WIDTH, env_utils.GROUNDING_HEIGHT), Image.Resampling.LANCZOS)
buf = io.BytesIO()
image.save(buf, format="PNG")
return buf.getvalue()
@@ -98,8 +104,8 @@ def _parse_coordinates(text: str) -> tuple[int, int]:
raise ValueError(f"grounding model did not return coordinates: {text!r}")
x = int(float(numbers[0]))
y = int(float(numbers[1]))
- x = max(0, min(GROUNDING_WIDTH - 1, x))
- y = max(0, min(GROUNDING_HEIGHT - 1, y))
+ x = max(0, min(env_utils.GROUNDING_WIDTH - 1, x))
+ y = max(0, min(env_utils.GROUNDING_HEIGHT - 1, y))
return x, y
@@ -107,8 +113,8 @@ def _scale_to_screen(coords: tuple[int, int], screen_size: tuple[int, int]) -> t
"""将 grounding 坐标映射到绝对屏幕坐标(考虑 GUI 操作区域偏移)。"""
screen_width, screen_height = screen_size
x, y = coords
- local_x = round(x * screen_width / GROUNDING_WIDTH)
- local_y = round(y * screen_height / GROUNDING_HEIGHT)
+ local_x = round(x * screen_width / env_utils.GROUNDING_WIDTH)
+ local_y = round(y * screen_height / env_utils.GROUNDING_HEIGHT)
# 加上目标显示器在屏幕空间的偏移
region = _get_gui_region()
offset_x = region["x"] if region else 0
@@ -124,10 +130,10 @@ def _ground_element(element_description: str) -> tuple[int, int, str]:
grounding_png = _resize_for_grounding(raw_png)
prompt = (
"You are a visual grounding model for desktop GUI control.\n"
- f"The screenshot has been resized to exactly {GROUNDING_WIDTH}x{GROUNDING_HEIGHT} pixels.\n"
+ f"The screenshot has been resized to exactly {env_utils.GROUNDING_WIDTH}x{env_utils.GROUNDING_HEIGHT} pixels.\n"
f"Query: {element_description.strip()}\n"
"Return only one target point as pixel coordinates in this exact format: x,y\n"
- f"Example: {GROUNDING_WIDTH // 2},{GROUNDING_HEIGHT // 2}\n"
+ f"Example: {env_utils.GROUNDING_WIDTH // 2},{env_utils.GROUNDING_HEIGHT // 2}\n"
"Do not include any explanation."
)
response = llm.invoke(
@@ -183,9 +189,26 @@ class GUITool(BaseTool):
args_schema: type[BaseModel] = GUIToolInput
name: str = "gui_tool"
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, clickandtype, drag_and_drop, scroll, "
- "hotkey, hold_and_press, wait, screenshot, move_to. Returns a screenshot of the current screen after the action."
+ "GUI 自动化工具。通过截图视觉定位桌面元素,将坐标映射到真实屏幕,并执行 pyautogui 动作。"
+ "接收一个有序的动作列表,按顺序依次执行。每个动作项支持的操作:click(点击)、type(输入文本,可通过 element_description 指定目标输入框自动聚焦)、"
+ "drag_and_drop(拖拽)、scroll(滚动)、hotkey(快捷键)、"
+ "hold_and_press(按住组合键并敲击)、wait(等待)、screenshot(截图)、move_to(移动鼠标)。\n"
+ "【type 使用说明】type 支持 element_description 参数来描述在哪里输入。当提供了 element_description 时,"
+ "会先视觉定位并点击目标输入框,然后再输入文本,无需单独调用 click。不提供 element_description 则直接在当前焦点输入。\n"
+ "所有动作执行完毕后,返回当前屏幕的截图。\n"
+ "\n"
+ "示例:\n"
+ "- 登录表单:{\"actions\": ["
+ "{\"action\":\"type\",\"element_description\":\"用户名输入框\",\"text\":\"admin\"},"
+ "{\"action\":\"type\",\"element_description\":\"密码输入框\",\"text\":\"123456\",\"enter\":true}"
+ "]}\n"
+ "- 浏览器搜索:{\"actions\": ["
+ "{\"action\":\"type\",\"element_description\":\"浏览器地址栏\",\"text\":\"github.com\",\"enter\":true}"
+ "]}\n"
+ "- 等待并确认:{\"actions\": ["
+ "{\"action\":\"click\",\"element_description\":\"提交按钮\"},"
+ "{\"action\":\"wait\",\"wait_seconds\":2.0}"
+ "]}"
)
response_format: Literal["content", "content_and_artifact"] = "content_and_artifact"
@@ -199,53 +222,32 @@ class GUITool(BaseTool):
def _run(
self,
- action: Optional[str] = None,
- element_description: Optional[str] = None,
- ending_description: Optional[str] = None,
- text: str = "",
- overwrite: bool = False,
- enter: bool = False,
- num_clicks: int = 1,
- button_type: str = "left",
- hold_keys: Optional[list[str]] = None,
- keys: Optional[list[str]] = None,
- press_keys: Optional[list[str]] = None,
- clicks: int = 0,
- shift: bool = False,
- wait_seconds: float = 0.0,
+ actions: list[GUIActionItem] | None = None,
) -> tuple[str, dict]:
- # ---------- screenshot 动作:仅截图 ----------
- if action == "screenshot":
- return ("", self._artifact_screenshot())
# ---------- 无动作时直接截图 ----------
- if not action:
+ if not actions:
return ("", self._artifact_screenshot())
- # -------------------------------------------
- dispatch_error: str | None = None
- try:
- self._dispatch(
- action=action,
- element_description=element_description,
- ending_description=ending_description,
- text=text,
- overwrite=overwrite,
- enter=enter,
- num_clicks=num_clicks,
- button_type=button_type,
- hold_keys=hold_keys or [],
- keys=keys or [],
- press_keys=press_keys or [],
- clicks=clicks,
- shift=shift,
- wait_seconds=wait_seconds,
- )
- except Exception as exc:
- dispatch_error = f"[GUI Error] {action}: {exc}"
- print(dispatch_error)
- time.sleep(1)
- if dispatch_error:
- return dispatch_error, self._artifact_screenshot()
- return "", self._artifact_screenshot()
+
+ # ---------- 顺序执行动作列表 ----------
+ result_messages: list[str] = [] # 收集每个动作的返回信息
+ for idx, act in enumerate(actions, start=1):
+ if act.action == "screenshot":
+ # screenshot 动作不需要实际执行,跳过并可选记录
+ result_messages.append(f"{idx}. Screenshot (skipped, final screenshot will be taken)")
+ continue
+
+ try:
+ msg = self._dispatch(**act.model_dump())
+ result_messages.append(f"{idx}. {msg}")
+ except Exception as exc:
+ error_msg = f"{idx}. [GUI Error] {act.action}: {exc}"
+ result_messages.append(error_msg)
+ print(error_msg)
+
+ time.sleep(1.2)
+
+ final_text = "\n".join(result_messages)
+ return (final_text, self._artifact_screenshot())
def _dispatch(self, **kwargs: Any) -> str:
action = str(kwargs["action"]).strip()
@@ -253,8 +255,6 @@ class GUITool(BaseTool):
return self._click(**kwargs)
if action == "type":
return self._type(**kwargs)
- if action == "clickandtype":
- return self._clickandtype(**kwargs)
if action == "drag_and_drop":
return self._drag_and_drop(**kwargs)
if action == "scroll":
@@ -264,7 +264,7 @@ class GUITool(BaseTool):
if action == "hold_and_press":
return self._hold_and_press(**kwargs)
if action == "wait":
- seconds = min(1.0, float(kwargs["wait_seconds"]))
+ seconds = min(1.5, float(kwargs["wait_seconds"]))
time.sleep(seconds)
return f"Waited {seconds:.2f} seconds."
if action == "move_to":
@@ -282,6 +282,18 @@ class GUITool(BaseTool):
return f"Clicked target. {grounding}"
def _type(self, **kwargs: Any) -> str:
+ element_desc = (kwargs.get("element_description") or "").strip()
+ if element_desc:
+ # 提供了目标元素描述:先定位点击再输入
+ x, y, grounding = _ground_element(element_desc)
+ pyautogui.click(x, y)
+ if kwargs["overwrite"]:
+ _select_all_and_clear()
+ _type_text(kwargs["text"])
+ if kwargs["enter"]:
+ pyautogui.press("enter")
+ return f"Clicked target and typed text. {grounding}"
+ # 不提供 element_description:直接在当前焦点输入
if kwargs["overwrite"]:
_select_all_and_clear()
_type_text(kwargs["text"])
@@ -289,16 +301,6 @@ class GUITool(BaseTool):
pyautogui.press("enter")
return "Typed text."
- def _clickandtype(self, **kwargs: Any) -> str:
- x, y, grounding = _ground_element(kwargs["element_description"])
- pyautogui.click(x, y)
- if kwargs["overwrite"]:
- _select_all_and_clear()
- _type_text(kwargs["text"])
- if kwargs["enter"]:
- pyautogui.press("enter")
- return f"Clicked target and typed text. {grounding}"
-
def _drag_and_drop(self, **kwargs: Any) -> str:
x1, y1, start_grounding = _ground_element(kwargs["element_description"] or "")
x2, y2, end_grounding = _ground_element(kwargs["ending_description"] or "")
diff --git a/src/agent/tools/tool.py b/src/agent/tools/tool.py
index f1c12fe..3b6c34c 100644
--- a/src/agent/tools/tool.py
+++ b/src/agent/tools/tool.py
@@ -16,7 +16,7 @@ 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, GROUNDING_HEIGHT, GROUNDING_WIDTH
+import agent.utils.env_utils as env_utils
class WebSearch(BaseTool):
@@ -99,9 +99,9 @@ class BrowserControl(BaseTool):
if browser_name is None:
return webbrowser.get(), None
name_lower = browser_name.lower().strip()
- if name_lower not in BROWSER_MAP:
- raise ValueError(f"不支持的浏览器: {browser_name},支持: {', '.join(BROWSER_MAP.keys())}")
- reg_name, title_keyword = BROWSER_MAP[name_lower]
+ if name_lower not in env_utils.BROWSER_MAP:
+ raise ValueError(f"不支持的浏览器: {browser_name},支持: {', '.join(env_utils.BROWSER_MAP.keys())}")
+ reg_name, title_keyword = env_utils.BROWSER_MAP[name_lower]
try:
controller = webbrowser.get(reg_name)
except webbrowser.Error:
@@ -132,7 +132,7 @@ class BrowserControl(BaseTool):
# ---------- 核心操作(返回操作结果描述) ----------
def _try_activate_any_browser(self) -> bool:
"""尝试激活任意已知浏览器窗口(不指定 browser 时的回退方案)。"""
- for _reg_name, title_keyword in BROWSER_MAP.values():
+ for _reg_name, title_keyword in env_utils.BROWSER_MAP.values():
if self._activate_window(title_keyword):
return True
return False
@@ -212,7 +212,7 @@ class BrowserControl(BaseTool):
time.sleep(0.3)
try:
pil_img = pyautogui.screenshot()
- pil_img = pil_img.resize((GROUNDING_WIDTH, GROUNDING_HEIGHT), Image.Resampling.LANCZOS)
+ pil_img = pil_img.resize((env_utils.GROUNDING_WIDTH, env_utils.GROUNDING_HEIGHT), Image.Resampling.LANCZOS)
buf = io.BytesIO()
pil_img.save(buf, format="PNG")
raw_png = buf.getvalue()
@@ -375,10 +375,10 @@ def update_todo_store_session(
done_steps = []
if stored_steps and 1 <= done_step <= len(stored_steps):
- idx = done_step - 1
- if idx not in done_steps:
- done_steps.append(idx)
- done_steps.sort()
+ for i in range(done_step):
+ if i not in done_steps:
+ done_steps.append(i)
+ done_steps.sort()
todo_data = {"steps": stored_steps, "done_steps": done_steps}
_todo_store[session_id] = todo_data
@@ -423,7 +423,7 @@ class TodoListInput(BaseModel):
)
done_step: int = Field(
default=-1,
- description="本次新完成的步骤编号(从1开始)。-1 表示仅查看当前状态,不标记完成。",
+ description="当前已推进到的步骤编号(从1开始)。传入 n 则步骤 1~n 全部标记为完成。-1 表示仅查看当前状态。",
)
@model_validator(mode="before")
@@ -441,20 +441,30 @@ class TodoListInput(BaseModel):
class TodoList(BaseTool):
- """可选的任务列表工具,模型可主动调用以跟踪任务进度。"""
+ """任务管理工具,用于拆分复杂任务为多个步骤并跟踪进度。
+
+ Plan 模式下此工具为**必须调用**,Agent 模式下为可选。
+ 首次调用时传入完整步骤列表,后续仅传 done_step 标记进度。
+ """
args_schema: type[BaseModel] = TodoListInput
name: str = "todo_list"
description: str = (
- "可选的任务管理工具。当你需要拆分复杂任务为多个步骤并跟踪进度时调用。\n"
+ "任务管理工具。用于将复杂任务拆分为多个步骤并跟踪执行进度。\n"
+ "Plan 模式下必须首先调用此工具创建任务计划;Agent 模式下按需调用。\n"
"参数说明:\n"
- "- steps (list[str], 可选):任务步骤列表。仅在首次创建计划时传入所有步骤。后续更新进度时请省略此参数(传 null 或不传),工具会复用之前设定的步骤列表,省去重复传输。\n"
- "- done_step (int, 可选):刚完成的步骤编号(从1开始),一次只标记一个。不传或传 -1 则只查看当前状态不做修改。\n"
+ "- steps (list[str], 可选):任务步骤列表。仅在首次创建计划时传入所有步骤。\n"
+ " **步骤粒度要求**:同一页面/界面上的连续操作应合并为一条步骤,用\"并\"连接。例如:\n"
+ ' * 推荐:"点击搜索框,输入XXX并点击搜索"(合并了点击、输入、搜索)\n'
+ ' * 推荐:"在结果中点击目标进入主页"(合并了查找+点击)\n'
+ ' * 禁止:"1.点击搜索框 2.输入文字 3.点击搜索"(过于细碎)\n'
+ "- done_step (int, 可选):当前已推进到的步骤编号(从1开始)。传入 n 则步骤 1~n 全部标记为已完成。不传或传 -1 则只查看当前状态。\n"
"调用示例:\n"
- '- 首次创建:{"steps": ["准备食材", "切菜", "烹饪", "装盘"], "done_step": -1}\n'
- '- 完成第1步:"done_step": 1}(省略 steps)\n'
- '- 查看状态:不传任何参数或 {"done_step": -1}\n'
- "仅在需要跟踪多步骤任务进度时调用,简单任务不需要调用。"
+ '- 首次创建(B站搜索示例):{"steps": ["打开哔哩哔哩网站", "点击顶部搜索框,输入\'燕三嘤嘤嘤\'并点击搜索", "在搜索结果中找到燕三嘤嘤嘤并点击头像或名称进入他的主页", "在他的主页点开视频列表里的第一个视频", "验证视频是否正在播放"], "done_step": -1}\n'
+ '- 已完成前2步:{"done_step": 2} (步骤1和2均标记为完成)\n'
+ '- 已完成前4步:{"done_step": 4}\n'
+ '- 查看状态:{"done_step": -1}\n'
+ "任务全部完成后无需再次调用。"
)
response_format: Literal["content", "content_and_artifact"] = "content"
@@ -472,12 +482,12 @@ class TodoList(BaseTool):
self._stored_steps = steps
self._done_set = []
- # 标记新完成步骤(1-indexed → 0-indexed)
+ # 标记步骤完成:传入 done_step 时,该步及之前所有步骤均标记为完成(1-indexed → 0-indexed)
if self._stored_steps and 1 <= done_step <= len(self._stored_steps):
- idx = done_step - 1
- if idx not in self._done_set:
- self._done_set.append(idx)
- self._done_set.sort()
+ for i in range(done_step):
+ if i not in self._done_set:
+ self._done_set.append(i)
+ self._done_set.sort()
todo_data = {
"steps": self._stored_steps,
diff --git a/src/agent/tools/tool_software_control.py b/src/agent/tools/tool_software_control.py
index 54596a1..75efacd 100644
--- a/src/agent/tools/tool_software_control.py
+++ b/src/agent/tools/tool_software_control.py
@@ -23,6 +23,7 @@ except Exception:
_windll = getattr(ctypes, "windll", None)
user32: object | None = _windll.user32 if os.name == "nt" and _windll is not None else None
WM_CLOSE = 0x0010
+SW_RESTORE = 9
_INDEX_PATH = Path(__file__).resolve().parents[1] / "history" / "data_base" / "software_index.json"
@@ -48,6 +49,14 @@ _SKIP_KEYWORDS = (
"license",
"update",
)
+_CHAT_APP_PROCESS_NAMES = {
+ "qq": ["QQ", "NTQQ"],
+ "腾讯qq": ["QQ", "NTQQ"],
+ "腾讯QQ": ["QQ", "NTQQ"],
+ "wechat": ["WeChat", "Weixin"],
+ "weixin": ["WeChat", "Weixin"],
+ "微信": ["WeChat", "Weixin"],
+}
@dataclass
@@ -354,6 +363,43 @@ def _find_visible_windows_for_pids(pids: list[int]) -> list[tuple[int, int, str]
return results
+def _chat_app_process_names(query: str, record: SoftwareRecord) -> list[str]:
+ candidates = [query, record.display_name, *record.aliases]
+ for candidate in candidates:
+ key = _normalize(candidate)
+ if key in _CHAT_APP_PROCESS_NAMES:
+ return _CHAT_APP_PROCESS_NAMES[key]
+ return []
+
+
+def _find_pids_for_process_names(process_names: list[str]) -> list[int]:
+ process_keys = {_normalize(name) for name in process_names if name}
+ matched: list[int] = []
+ for proc in _list_running_processes():
+ if _normalize(proc["stem"]) in process_keys:
+ try:
+ matched.append(int(proc["pid"]))
+ except ValueError:
+ continue
+ return sorted(set(matched))
+
+
+def _activate_existing_window_for_pids(pids: list[int]) -> tuple[int, int, str] | None:
+ if user32 is None or not pids:
+ return None
+ windows = _find_visible_windows_for_pids(pids)
+ if not windows:
+ return None
+ pid, hwnd, title = windows[0]
+ try:
+ user32.ShowWindow(hwnd, SW_RESTORE)
+ user32.BringWindowToTop(hwnd)
+ user32.SetForegroundWindow(hwnd)
+ except Exception:
+ pass
+ return pid, hwnd, title
+
+
def _wait_for_pids_exit(pids: list[int], timeout_seconds: float = 5.0) -> list[int]:
deadline = time.time() + timeout_seconds
remaining = set(pids)
@@ -449,6 +495,21 @@ class SoftwareControl(BaseTool):
)
if action == "open":
+ running_pids = _find_running_pids(best_record)
+ chat_process_names = _chat_app_process_names(query, best_record)
+ if chat_process_names:
+ running_pids = sorted(set(running_pids + _find_pids_for_process_names(chat_process_names)))
+ existing_window = _activate_existing_window_for_pids(running_pids)
+ if existing_window:
+ pid, hwnd, title = existing_window
+ return (
+ f"Switched to existing software window: {best_record.display_name}\n"
+ f"Match score: {best_score:.2f}\n"
+ f"PID: {pid}\n"
+ f"Window title: {title}\n"
+ f"Window handle: {hwnd}\n"
+ "Note: an existing visible window was detected, so the shortcut was not launched again."
+ )
try:
opened_target = _open_record(best_record)
except Exception as exc:
diff --git a/src/agent/utils/__pycache__/env_utils.cpython-312.pyc b/src/agent/utils/__pycache__/env_utils.cpython-312.pyc
index f404c4f..d46cb51 100644
Binary files a/src/agent/utils/__pycache__/env_utils.cpython-312.pyc and b/src/agent/utils/__pycache__/env_utils.cpython-312.pyc differ
diff --git a/src/agent/utils/env_utils.py b/src/agent/utils/env_utils.py
index 6b8dad4..19e52d6 100644
--- a/src/agent/utils/env_utils.py
+++ b/src/agent/utils/env_utils.py
@@ -121,6 +121,22 @@ def _load_config() -> dict:
return {}
+def reload_config() -> dict:
+ """实时重载 env_config.json 配置,刷新所有模块级常量。
+
+ 可在不重启进程的情况下重新读取配置文件,更新 GROUNDING_WIDTH 等运行时参数。
+ 注意:调用方需通过 ``import agent.utils.env_utils as env_utils`` 然后访问
+ ``env_utils.GROUNDING_WIDTH`` 才能获取到更新后的值,直接 ``from ... import``
+ 的方式会绑定旧值。
+
+ 输出:
+ 加载到的配置字典,失败时返回空字典。
+ """
+ result = _load_config()
+ _refresh_globals()
+ return result
+
+
_load_config()
_refresh_globals() # 确保无论配置加载成功与否,都有默认值
diff --git a/src/web/__pycache__/server.cpython-312.pyc b/src/web/__pycache__/server.cpython-312.pyc
index b3584f7..e4d9a5d 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
new file mode 100644
index 0000000..2e4d3a9
Binary files /dev/null and b/src/web/__pycache__/server.cpython-313.pyc differ
diff --git a/src/web/app.css b/src/web/app.css
index 321b09d..a4ced32 100644
--- a/src/web/app.css
+++ b/src/web/app.css
@@ -1209,6 +1209,17 @@ body {
cursor: not-allowed;
}
+.btn-pause {
+ background: linear-gradient(165deg, #f59e0b, #d97706);
+ border-color: rgba(217, 119, 6, 0.85);
+ color: #fff;
+ box-shadow: 0 4px 14px rgba(245, 158, 11, 0.35);
+}
+
+.btn-pause:hover:not(:disabled) {
+ background: linear-gradient(165deg, #fbbf24, #f59e0b);
+}
+
.btn-secondary {
background: rgba(255, 255, 255, 0.35);
}
@@ -2034,6 +2045,7 @@ body {
.model-table .col-name { flex: 1.5; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; color: #1e293b; }
.model-table .col-model { flex: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #475569; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.78rem; }
.model-table .col-url { flex: 2.5; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #94a3b8; font-size: 0.72rem; }
+.model-table .col-think { flex: 0 0 52px; text-align: center; font-size: 0.72rem; font-weight: 600; }
.model-table .col-actions { flex: 0 0 90px; display: flex; gap: 0.35rem; justify-content: flex-end; align-items: center; }
/* 表格内操作按钮 */
@@ -2119,6 +2131,13 @@ body {
gap: 0.5rem;
}
+.toggle-hint {
+ font-size: 0.72rem;
+ color: #94a3b8;
+ margin-left: 0.35rem;
+ vertical-align: middle;
+}
+
.toggle-row .toggle-label {
font-size: 0.82rem;
font-weight: 600;
@@ -2374,6 +2393,91 @@ body {
opacity: 0.92;
}
+/* ===== 反思(think)面板 ===== */
+.think-indicator {
+ margin-top: 0.35rem;
+ padding: 0.25rem 0;
+ border-top: 1px solid rgba(148, 163, 184, 0.10);
+ cursor: pointer;
+ user-select: none;
+ -webkit-user-select: none;
+}
+
+.think-indicator-icon {
+ font-size: 0.8rem;
+}
+
+.think-indicator-text {
+ font-size: 0.72rem;
+ color: rgba(100, 116, 139, 0.55);
+ font-weight: 400;
+ letter-spacing: 0.02em;
+ transition: color 0.3s;
+}
+
+.think-indicator:hover .think-indicator-text {
+ color: rgba(100, 116, 139, 0.75);
+}
+
+.think-list-wrap {
+ display: none;
+ margin-top: 0.15rem;
+ max-height: 220px;
+ overflow-y: auto;
+ padding: 0.15rem 0;
+ border-top: 1px solid rgba(148, 163, 184, 0.06);
+}
+
+.think-list-wrap--expanded {
+ display: block;
+}
+
+.think-item {
+ padding: 0.18rem 0.3rem;
+ border-radius: 4px;
+ font-size: 0.7rem;
+ color: rgba(51, 65, 85, 0.48);
+ line-height: 1.35;
+ transition: color 0.3s;
+}
+
+.think-item:hover {
+ background: rgba(148, 163, 184, 0.05);
+}
+
+.think-item-summary {
+ display: flex;
+ align-items: center;
+ gap: 0.2rem;
+}
+
+.think-bullet {
+ color: rgba(100, 116, 139, 0.40);
+ font-size: 0.65rem;
+}
+
+.think-label {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.think-item-detail {
+ margin-top: 0.3rem;
+ padding: 0.35rem 0.45rem;
+ background: rgba(148, 163, 184, 0.06);
+ border-radius: 4px;
+ font-size: 0.68rem;
+ line-height: 1.5;
+ color: rgba(51, 65, 85, 0.55);
+ white-space: pre-wrap;
+ word-break: break-word;
+ max-height: 200px;
+ overflow-y: auto;
+ font-family: inherit;
+}
+
.todo-list-overlay {
position: fixed;
top: 80px;
diff --git a/src/web/app.js b/src/web/app.js
index 18e7965..90657e4 100644
--- a/src/web/app.js
+++ b/src/web/app.js
@@ -39,6 +39,7 @@
/** @type {File[]} */
let pendingFiles = [];
let sending = false;
+ let abortController = null;
let mediaRecorder = null;
let recorderStream = null;
let recordedChunks = [];
@@ -1171,6 +1172,93 @@
// ===== 工具调用展示(chat 视图 / 持久化) =====
let liveToolCallExpanded = false;
+ let thinkExpanded = false;
+
+ /** 在思考气泡内渲染反思(think)面板 */
+ function injectThinkPanel(typingEl, reflections) {
+ if (!typingEl) return;
+ const bubble = typingEl.querySelector(".msg-bubble") || typingEl;
+
+ // 反思指示器
+ let thinkIndicator = bubble.querySelector(".think-indicator");
+ if (!thinkIndicator) {
+ thinkIndicator = document.createElement("div");
+ thinkIndicator.className = "think-indicator";
+ thinkIndicator.title = "点击展开/收起 Agent 反思内容";
+ thinkIndicator.style.display = "none";
+ // 插入到 agent-running-indicator 前面
+ const runningIndicator = bubble.querySelector(".agent-running-indicator");
+ if (runningIndicator) {
+ bubble.insertBefore(thinkIndicator, runningIndicator);
+ } else {
+ bubble.appendChild(thinkIndicator);
+ }
+ thinkIndicator.addEventListener("click", () => {
+ thinkExpanded = !thinkExpanded;
+ const wrap = bubble.querySelector(".think-list-wrap");
+ if (wrap) wrap.classList.toggle("think-list-wrap--expanded", thinkExpanded);
+ updateThinkIndicatorText(thinkIndicator, reflections);
+ });
+ }
+
+ if (!reflections || !reflections.length) {
+ thinkIndicator.style.display = "none";
+ const wrap = bubble.querySelector(".think-list-wrap");
+ if (wrap) wrap.remove();
+ return;
+ }
+
+ thinkIndicator.style.display = "";
+ updateThinkIndicatorText(thinkIndicator, reflections);
+
+ // 反思列表容器
+ let listWrap = bubble.querySelector(".think-list-wrap");
+ if (!listWrap) {
+ listWrap = document.createElement("div");
+ listWrap.className = "think-list-wrap" + (thinkExpanded ? " think-list-wrap--expanded" : "");
+ // 插入到 think-indicator 后面
+ thinkIndicator.insertAdjacentElement("afterend", listWrap);
+ }
+ listWrap.classList.toggle("think-list-wrap--expanded", thinkExpanded);
+
+ // 增量更新列表项
+ const existingCount = listWrap.querySelectorAll(".think-item").length;
+ if (reflections.length <= existingCount) return; // 无新增
+
+ for (let i = existingCount; i < reflections.length; i++) {
+ const ref = reflections[i];
+ const item = document.createElement("div");
+ item.className = "think-item";
+ // 摘要行
+ const summary = document.createElement("div");
+ summary.className = "think-item-summary";
+ summary.innerHTML = `• ${escapeHtml(ref.summary || "反思")}`;
+ item.appendChild(summary);
+ // 存储完整内容
+ item._thinkContent = ref.content || "";
+ // 点击展开完整内容
+ item.style.cursor = "pointer";
+ item.addEventListener("click", () => {
+ let detail = item.querySelector(".think-item-detail");
+ if (detail) {
+ detail.remove();
+ item.setAttribute("aria-expanded", "false");
+ return;
+ }
+ detail = document.createElement("pre");
+ detail.className = "think-item-detail";
+ detail.textContent = item._thinkContent;
+ item.appendChild(detail);
+ item.setAttribute("aria-expanded", "true");
+ });
+ listWrap.appendChild(item);
+ }
+ }
+
+ function updateThinkIndicatorText(indicator, reflections) {
+ const count = reflections ? reflections.length : 0;
+ indicator.innerHTML = `🧠 think (${count})${thinkExpanded ? " ▾" : " ▸"}`;
+ }
/** 在思考气泡内更新工具调用指示器与列表(增量更新,不重建 DOM) */
function injectLiveToolCalls(typingEl, liveCalls) {
@@ -1552,6 +1640,43 @@
renderMessages(data.messages);
}
+ function setSendBtnPauseMode(pauseMode) {
+ if (!sendBtn) return;
+ if (pauseMode) {
+ sendBtn.textContent = "暂停";
+ sendBtn.classList.add("btn-pause");
+ } else {
+ sendBtn.textContent = "发送";
+ sendBtn.classList.remove("btn-pause");
+ }
+ }
+
+ async function handlePauseClick() {
+ if (!sessionId || !sending || !abortController) return;
+ if (!window.confirm("确认暂停当前任务?\n\n暂停后当前对话将恢复到上一条消息的状态,工具调用记录和TodoList将被清空。")) return;
+ // 1. 调用后端 abort
+ try {
+ const fd = new FormData();
+ fd.append("session_id", sessionId);
+ const data = await api("/api/chat/abort", { method: "POST", body: fd });
+ sessionId = data.sessionId;
+ sessions = data.sessions || [];
+ clearTodoOverlay();
+ renderSessions();
+ renderMessages(data.messages || []);
+ } catch (e) {
+ // 回退:至少恢复 priorMessages
+ }
+ // 2. 取消前端 fetch
+ if (abortController) {
+ abortController.abort();
+ abortController = null;
+ }
+ sending = false;
+ setSendBtnPauseMode(false);
+ sendBtn.disabled = false;
+ }
+
async function sendChat() {
if (!sessionId || sending) return;
const trimmed = textInput.value.trim();
@@ -1565,6 +1690,9 @@
const blobUrls = [];
const hadOptimisticMedia = savedFiles.length > 0;
+ // 切换发送按钮为暂停按钮
+ setSendBtnPauseMode(true);
+
if (hadOptimisticMedia) {
const fileParts = savedFiles.map((f) => {
const kind = inferUploadKind(f);
@@ -1585,6 +1713,7 @@
renderAttachmentChips();
}
+ abortController = new AbortController();
const fd = new FormData();
fd.append("session_id", sessionId);
fd.append("text", trimmed);
@@ -1608,17 +1737,24 @@
// 清空上轮工具调用状态(新 turn)
liveToolCallExpanded = false;
+ thinkExpanded = false;
clearTodoOverlay();
- // 启动实时工具调用轮询(增量更新,低频率不卡)
+ // 启动实时工具调用与反思轮询(增量更新,低频率不卡)
let livePollTimer = null;
let livePollActive = true;
const pollLiveToolCalls = async () => {
if (!livePollActive) return;
try {
const liveData = await api(`/api/tool-calls/${encodeURIComponent(sessionId)}/live`);
- if (liveData.tool_calls && liveData.tool_calls.length > 0 && typingEl) {
- injectLiveToolCalls(typingEl, liveData.tool_calls);
+ if (typingEl) {
+ // 先更新 think 面板
+ const reflections = liveData.reflections || [];
+ injectThinkPanel(typingEl, reflections);
+ // 再更新工具调用列表
+ if (liveData.tool_calls && liveData.tool_calls.length > 0) {
+ injectLiveToolCalls(typingEl, liveData.tool_calls);
+ }
}
} catch { /* ignore */ }
if (livePollActive) livePollTimer = setTimeout(pollLiveToolCalls, 1200);
@@ -1643,7 +1779,7 @@
fd2.append("session_id", sessionId);
fd2.append("output_type", outputModeSelect ? outputModeSelect.value : "text");
fd2.append("agent_mode", agentModeSelect ? agentModeSelect.value : "agent");
- const done = await api("/api/chat/complete", { method: "POST", body: fd2 });
+ const done = await api("/api/chat/complete", { method: "POST", body: fd2, signal: abortController.signal });
try {
const todoData = await api(`/api/todo-list/${encodeURIComponent(sessionId)}`);
@@ -1681,16 +1817,23 @@
}
} catch (e) {
revokeBlobUrlStack(blobUrls);
- if (hadOptimisticMedia) {
+ if (e && e.name === "AbortError") {
+ // 用户点击了暂停 → 不显示错误,恢复 priorMessages
+ renderMessages(priorMessages);
+ } else if (hadOptimisticMedia) {
textInput.value = savedTextRaw;
pendingFiles = savedFiles.slice();
renderAttachmentChips();
renderMessages(priorMessages);
}
if (typingEl && typingEl.parentNode) typingEl.remove();
- alert(e.message || String(e));
+ if (!(e && e.name === "AbortError")) {
+ alert(e.message || String(e));
+ }
} finally {
sending = false;
+ abortController = null;
+ setSendBtnPauseMode(false);
sendBtn.disabled = false;
}
}
@@ -1721,7 +1864,13 @@
}
});
- sendBtn.addEventListener("click", sendChat);
+ sendBtn.addEventListener("click", () => {
+ if (sending) {
+ handlePauseClick().catch((e) => alert(e.message || String(e)));
+ } else {
+ sendChat();
+ }
+ });
micBtn?.addEventListener("click", () => toggleMicRecording().catch((e) => alert(e.message || String(e))));
autoInputCheck?.addEventListener("change", (ev) => toggleAutoInput(ev.target.checked));
clearBtn.addEventListener("click", () => clearChat().catch((e) => alert(e.message)));
@@ -2615,14 +2764,15 @@
table.className = "model-table";
const thead = document.createElement("div");
thead.className = "model-table-head";
- thead.innerHTML = '名称ModelBase URL操作';
+ thead.innerHTML = '名称ModelBase URL思考操作';
table.appendChild(thead);
cachedModels.forEach((m, mi) => {
const row = document.createElement("div");
row.className = "model-table-row";
row.innerHTML = `${escapeHtml(m.name)}
${escapeHtml(m.model)}
- ${escapeHtml(m.base_url)}`;
+ ${escapeHtml(m.base_url)}
+ ${m.enable_thinking ? 'ON' : 'OFF'}`;
const actions = document.createElement("span");
actions.className = "col-actions";
const editBtn = document.createElement("button");
@@ -2676,6 +2826,27 @@
modalFields[key] = inp;
modalBody.appendChild(div);
});
+ // 思考模式开关
+ const thinkingDiv = document.createElement("div");
+ thinkingDiv.className = "modal-field";
+ const thinkingLabel = document.createElement("label");
+ thinkingLabel.textContent = "思考模式";
+ const thinkingWrap = document.createElement("label");
+ thinkingWrap.className = "toggle-switch";
+ const thinkingCheckbox = document.createElement("input");
+ thinkingCheckbox.type = "checkbox";
+ const thinkingSlider = document.createElement("span");
+ thinkingSlider.className = "toggle-slider";
+ thinkingWrap.appendChild(thinkingCheckbox);
+ thinkingWrap.appendChild(thinkingSlider);
+ const thinkingHint = document.createElement("span");
+ thinkingHint.className = "toggle-hint";
+ thinkingHint.textContent = " 关闭后禁用模型思考(默认关闭,减少延迟)";
+ thinkingDiv.appendChild(thinkingLabel);
+ thinkingDiv.appendChild(thinkingWrap);
+ thinkingDiv.appendChild(thinkingHint);
+ modalFields["enable_thinking"] = thinkingCheckbox;
+ modalBody.appendChild(thinkingDiv);
modalBox.appendChild(modalBody);
const modalActions = document.createElement("div");
modalActions.className = "model-modal-actions";
@@ -2697,6 +2868,7 @@
base_url: modalFields.base_url.value.trim(),
timeout: parseFloat(modalFields.timeout.value) || 60,
max_retries: 0,
+ enable_thinking: modalFields.enable_thinking.checked,
};
if (!data.name || !data.model) { alert("名称和模型名不能为空"); return; }
if (editingModelIdx >= 0) {
@@ -2758,12 +2930,14 @@
fields.api_key.value = m.api_key || "";
fields.base_url.value = m.base_url || "";
fields.timeout.value = m.timeout || 60;
+ fields.enable_thinking.checked = !!m.enable_thinking;
} else {
fields.name.value = "";
fields.model.value = "";
fields.api_key.value = "";
fields.base_url.value = "";
fields.timeout.value = 60;
+ fields.enable_thinking.checked = false;
}
overlay.classList.add("show");
}
diff --git a/src/web/index.html b/src/web/index.html
index 747047c..8a43e76 100644
--- a/src/web/index.html
+++ b/src/web/index.html
@@ -125,6 +125,7 @@
diff --git a/src/web/server.py b/src/web/server.py
index 6625ae8..1fc8f78 100644
--- a/src/web/server.py
+++ b/src/web/server.py
@@ -580,6 +580,47 @@ def api_chat_complete(
return _chat_complete_core(session_id, output_type=output_type, agent_mode=agent_mode)
+@app.post("/api/chat/abort")
+def api_chat_abort(session_id: str = Form(...)) -> dict[str, Any]:
+ """中止当前正在执行的 Agent 任务,回滚到最后一条消息之前的状态。
+
+ 输入:
+ session_id: 表单字段,会话 ID。
+
+ 输出:
+ {"sessionId": str, "sessions": list[str], "messages": list[dict]}
+ """
+ if not session_id:
+ raise HTTPException(status_code=400, detail="缺少 session_id")
+ try:
+ from agent.core.runtime import set_abort_flag
+ set_abort_flag(session_id)
+ except Exception:
+ pass
+ # 回滚 manifest:移除最后一条 user 消息及后续 assistant
+ hist = ui.load_ui_messages_for_session(session_id)
+ rolled_back = list(hist or [])
+ while rolled_back and rolled_back[-1].get("role") != "user":
+ rolled_back.pop()
+ if rolled_back:
+ rolled_back.pop()
+ ui.sync_manifest_from_chat_history(session_id, rolled_back, "")
+ # 清除 todo store
+ try:
+ from agent.tools.tool import clear_todo_store_session
+ clear_todo_store_session(session_id)
+ except Exception:
+ pass
+ ids = ui.list_session_ids_ordered()
+ if session_id not in ids:
+ ids = [session_id, *ids]
+ return {
+ "sessionId": session_id,
+ "sessions": ids,
+ "messages": _serialize_messages(rolled_back, session_id),
+ }
+
+
@@ -859,12 +900,15 @@ def api_reload_configs() -> dict[str, Any]:
# ---------- 工具调用历史 API ----------
@app.get("/api/tool-calls/{session_id}/live")
def api_get_live_tool_calls(session_id: str) -> dict[str, Any]:
- """获取当前进行中的工具调用(前端执行期间实时轮询)。"""
+ """获取当前进行中的工具调用与反思内容(前端执行期间实时轮询)。"""
try:
- from agent.history.tool_call_recorder import get_live_tool_calls
- return {"tool_calls": get_live_tool_calls(session_id)}
+ from agent.history.tool_call_recorder import get_live_tool_calls, get_live_reflections
+ return {
+ "tool_calls": get_live_tool_calls(session_id),
+ "reflections": get_live_reflections(session_id),
+ }
except Exception as e:
- return {"tool_calls": [], "error": str(e)}
+ return {"tool_calls": [], "reflections": [], "error": str(e)}
@app.get("/api/tool-image")