diff --git a/bilibili_page.html b/bilibili_page.html new file mode 100644 index 0000000..d3cd089 --- /dev/null +++ b/bilibili_page.html @@ -0,0 +1,344 @@ + + + 哔哩哔哩 (゜-゜)つロ 干杯~-bilibili + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
  • 大会员
  • 消息
  • 动态
  • 收藏
  • 历史
  • 创作中心
  • 投稿
  • 点击视频卡片右上角添加!
    + + + + + + + + + + +
    card
    查看详情
    \ 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 2a92f8e..ac61aeb 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/agents/__pycache__/__init__.cpython-312.pyc b/src/agent/agents/__pycache__/__init__.cpython-312.pyc index 62a1d83..3d664d3 100644 Binary files a/src/agent/agents/__pycache__/__init__.cpython-312.pyc and b/src/agent/agents/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/agent/agents/__pycache__/main_agent.cpython-312.pyc b/src/agent/agents/__pycache__/main_agent.cpython-312.pyc index bb84c50..b976594 100644 Binary files a/src/agent/agents/__pycache__/main_agent.cpython-312.pyc and b/src/agent/agents/__pycache__/main_agent.cpython-312.pyc differ diff --git a/src/agent/agents/gui_agent.py b/src/agent/agents/gui_agent.py index 72f6ace..032ad16 100644 --- a/src/agent/agents/gui_agent.py +++ b/src/agent/agents/gui_agent.py @@ -8,3 +8,4 @@ 实现时可单独配置 tool_policy 白名单、更严格的 confirm 策略, 并绑定专用 system_prompt 与较短上下文窗口。 """ + diff --git a/src/agent/agents/main_agent.py b/src/agent/agents/main_agent.py index 894599d..e5a3dca 100644 --- a/src/agent/agents/main_agent.py +++ b/src/agent/agents/main_agent.py @@ -37,6 +37,9 @@ def _build_main_agent_runtime(): if cfg.system_prompt: _system_prompt = cfg.system_prompt _max_iterations = cfg.max_iterations + # 根据 agent_config.json 中的 tools 列表过滤工具 + if cfg.tools: + _tools = [t for t in _tools if t.name in cfg.tools] break except Exception: pass diff --git a/src/agent/core/__pycache__/__init__.cpython-312.pyc b/src/agent/core/__pycache__/__init__.cpython-312.pyc index ef548de..eb135f9 100644 Binary files a/src/agent/core/__pycache__/__init__.cpython-312.pyc and b/src/agent/core/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/agent/core/__pycache__/config_manager.cpython-312.pyc b/src/agent/core/__pycache__/config_manager.cpython-312.pyc index fe336dc..08873b5 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__/llm.cpython-312.pyc b/src/agent/core/__pycache__/llm.cpython-312.pyc index 61e5fb7..567cd61 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 fd97c8f..341ee25 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 index c9163d8..2443de7 100644 Binary files a/src/agent/core/__pycache__/nodes.cpython-313.pyc 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 e99b13e..d2d342b 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 index 0ba67fa..c98d6d8 100644 Binary files a/src/agent/core/__pycache__/runtime.cpython-313.pyc and b/src/agent/core/__pycache__/runtime.cpython-313.pyc differ diff --git a/src/agent/core/__pycache__/tool_policy.cpython-312.pyc b/src/agent/core/__pycache__/tool_policy.cpython-312.pyc index 67d1119..eba5259 100644 Binary files a/src/agent/core/__pycache__/tool_policy.cpython-312.pyc and b/src/agent/core/__pycache__/tool_policy.cpython-312.pyc differ diff --git a/src/agent/core/agent_config.json b/src/agent/core/agent_config.json index 67a19c5..910886d 100644 --- a/src/agent/core/agent_config.json +++ b/src/agent/core/agent_config.json @@ -3,24 +3,18 @@ "name": "main", "system_prompt": "你是一个多模态助手,能够直接理解用户上传的图片、语音等内容。\n 【关键行为准则】\n - 你的默认行为是直接回答用户问题,不要调用任何工具。\n - 只有当某个信息是你**绝对无法从当前对话或上传内容中获取**,且该信息必须通过外部工具才能获得时,才允许调用工具。\n - 对于任何图片/音频输入,请始终**直接**利用你的视觉/听觉能力回答,**绝对不要**调用截图、OCR、日期查询等无关工具。\n - 如果你不确定是否需要工具,选择**不调用**,直接给出你能给出的最佳答案。\n - 违反上述规则是一种严重错误。", "max_iterations": 200, - "llm": { - "model": "qwen3.6-flash", - "api_key": "sk-a44b226ec79a40fd8fcdbbaa74ba133e", - "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", - "timeout": 60.0, - "max_retries": 0 - }, "llm_model_id": "a44b226e", "tools": [ + "todo_list", + "gui_tool", + "directory_listing", "web_search_class", "date_query_class", - "screen_capture", - "browser_control", "human_interaction", "software_control", - "directory_listing" + "browser_control" ], "enabled": true, "role": "main" } -] +] \ No newline at end of file diff --git a/src/agent/core/config_manager.py b/src/agent/core/config_manager.py index 7815c66..ece2cbe 100644 --- a/src/agent/core/config_manager.py +++ b/src/agent/core/config_manager.py @@ -17,7 +17,7 @@ from __future__ import annotations import json import os import uuid -from dataclasses import dataclass, field, asdict +from dataclasses import dataclass, field from pathlib import Path from typing import Any @@ -40,23 +40,12 @@ class ModelConfig: max_retries: int = 0 -# ---------- LLM 配置结构(保留兼容) ---------- -@dataclass -class LLMConfig: - model: str = "" - api_key: str = "" - base_url: str = "" - timeout: float = 60.0 - max_retries: int = 0 - - @dataclass class AgentConfig: name: str = "" system_prompt: str = "" max_iterations: int = 200 - llm: LLMConfig = field(default_factory=LLMConfig) # 保留兼容 - llm_model_id: str = "" # 新:引用 models 列表中的模型 ID + llm_model_id: str = "" # 引用 env_config.json models 列表中的模型 ID tools: list[str] = field(default_factory=list) enabled: bool = True role: str = "sub" # "main" | "sub" @@ -123,18 +112,10 @@ def load_agent_configs() -> list[AgentConfig]: configs = [] for item in raw: - llm_raw = item.get("llm", {}) configs.append(AgentConfig( name=item.get("name", ""), system_prompt=item.get("system_prompt", ""), max_iterations=item.get("max_iterations", 200), - llm=LLMConfig( - model=llm_raw.get("model", ""), - api_key=llm_raw.get("api_key", ""), - base_url=llm_raw.get("base_url", ""), - timeout=llm_raw.get("timeout", 60.0), - max_retries=llm_raw.get("max_retries", 0), - ), llm_model_id=item.get("llm_model_id", ""), tools=item.get("tools", []), enabled=item.get("enabled", True), @@ -217,12 +198,23 @@ def _default_tool_config() -> list[dict]: def load_tool_configs() -> list[ToolConfig]: - """从 tool_config.json 加载工具配置,无文件时返回默认。""" + """从 tool_config.json 加载工具配置,无文件时返回默认;有新工具时自动补入。""" raw = _read_json(TOOL_CONFIG_PATH) if not raw or not isinstance(raw, list): raw = _default_tool_config() _write_json(TOOL_CONFIG_PATH, raw) + # 补上代码中已注册但 JSON 中缺失的新工具 + registered = get_registered_tool_names() + existing_names = {item.get("name", "") for item in raw} + missing = [n for n in registered if n not in existing_names] + if missing: + defaults = {t["name"]: t for t in _default_tool_config()} + for name in missing: + if name in defaults: + raw.append(defaults[name]) + _write_json(TOOL_CONFIG_PATH, raw) + configs = [] for item in raw: configs.append(ToolConfig( @@ -317,7 +309,7 @@ def load_env_config() -> dict: raw["models"] = _default_models() _write_json(ENV_CONFIG_PATH, raw) return raw - # 确保有 models 字段(向后兼容旧配置文件) + # 确保有 models 字段 if "models" not in raw or not isinstance(raw.get("models"), list): raw["models"] = _default_models() return raw diff --git a/src/agent/core/env_config.json b/src/agent/core/env_config.json index bdb3a46..86f3858 100644 --- a/src/agent/core/env_config.json +++ b/src/agent/core/env_config.json @@ -14,10 +14,10 @@ "models": [ { "id": "a44b226e", - "name": "阿里云Qwen", + "name": "阿里云qwen3.6-35b-a3b", "api_key": "sk-a44b226ec79a40fd8fcdbbaa74ba133e", "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", - "model": "qwen3.6-flash", + "model": "qwen3.6-35b-a3b", "timeout": 60, "max_retries": 0 } diff --git a/src/agent/core/nodes.py b/src/agent/core/nodes.py index 72e57eb..7803af5 100644 --- a/src/agent/core/nodes.py +++ b/src/agent/core/nodes.py @@ -10,8 +10,6 @@ """ from __future__ import annotations -import json -import time from typing import Any, Callable from langchain_core.messages import HumanMessage, ToolMessage @@ -22,43 +20,27 @@ from agent.utils.tool_call_utils import normalize_tool_call def make_call_model_node(model_with_tools: Any) -> Callable[[AgentState], dict]: - """创建调用已绑定工具的 LLM 的图节点闭包。 - - 输入: - model_with_tools: bind_tools 后的 LangChain 聊天模型。 - - 输出: - 符合 LangGraph 节点签名的 ``call_model(state) -> {"messages": [AIMessage]}``。 - - 系统定位: - 图中 ``agent`` 节点:读取 state.messages,调用模型,追加 AI 响应。 - - 可扩展性: - 可包装为 async 节点;可在此统一记录 token 用量到 session_meta。 - """ + """创建调用已绑定工具的 LLM 的图节点闭包。""" def call_model(state: AgentState) -> dict: response = model_with_tools.invoke(state["messages"]) - usage = response.usage_metadata if hasattr(response, "usage_metadata") else {} - if usage: - input_tokens = usage.get("input_tokens", "?") - output_tokens = usage.get("output_tokens", "?") - total_tokens = usage.get("total_tokens", "?") - print(f"[LLM] Token使用: input={input_tokens}, output={output_tokens}, total={total_tokens}") - if hasattr(response, "tool_calls") and response.tool_calls: for tc in response.tool_calls: tc_normalized = normalize_tool_call(tc) tool_name = tc_normalized.get("name", "unknown") tool_args = tc_normalized.get("args", {}) - print(f"[LLM] 工具调用: {tool_name}, 参数: {tool_args}") - # 实时记录工具调用 try: from agent.history.tool_call_recorder import record_tool_call_live, get_current_session sid = get_current_session() if sid: record_tool_call_live(sid, tool_name, tool_args) + if tool_name == "todo_list": + try: + from agent.tools.tool import update_todo_store_from_tool_args + update_todo_store_from_tool_args(sid, tool_args) + except Exception: + pass except Exception: pass @@ -74,28 +56,17 @@ def process_tool_artifact(state: AgentState) -> dict: content_preview = str(last_msg.content) if len(content_preview) > 200: content_preview = content_preview[:200] + "..." - print(f"[Tool] 工具返回 (id={last_msg.tool_call_id}): {content_preview}") - # 实时记录工具返回结果 try: from agent.history.tool_call_recorder import record_tool_result_live, get_current_session sid = get_current_session() if sid: - img_bytes = None - if last_msg.artifact: - if isinstance(last_msg.artifact, dict) and "image_bytes" in last_msg.artifact: - img_bytes = last_msg.artifact["image_bytes"] - elif isinstance(last_msg.artifact, list): - for item in last_msg.artifact: - if isinstance(item, dict) and "image_bytes" in item: - img_bytes = item["image_bytes"] - break - elif isinstance(item, bytes): - img_bytes = item - break - elif isinstance(last_msg.artifact, bytes): - img_bytes = last_msg.artifact - record_tool_result_live(sid, last_msg.name or "unknown", str(last_msg.content) if last_msg.content else "", img_bytes) + record_tool_result_live( + sid, + last_msg.name or "unknown", + str(last_msg.content) if last_msg.content else "", + last_msg.artifact, + ) except Exception: pass @@ -140,4 +111,4 @@ def process_tool_artifact(state: AgentState) -> dict: content_parts[-1]["text"] += f"(其中 {failed_count} 张图片处理失败,请忽略)" human_msg = HumanMessage(content=content_parts, additional_kwargs={"synthetic": True}) - return {"messages": [human_msg]} + return {"messages": [human_msg]} \ No newline at end of file diff --git a/src/agent/core/runtime.py b/src/agent/core/runtime.py index 8a605e8..0baad0e 100644 --- a/src/agent/core/runtime.py +++ b/src/agent/core/runtime.py @@ -471,6 +471,6 @@ def continue_after_human_action( chat_history.append({"role": "assistant", "content": content, "meta": {"output_type": output_type, "autoplay": False}}) _persist_messages_from_pending_turn(session_id, persisted_messages, pending) if session_id: - save_turn(session_id, pending.get("user_content") or {}, persisted_messages) + save_turn(session_id, f"human_{approval_id}", {"role": "user", "content": pending.get("user_content") or ""}, persisted_messages) sync_manifest_from_chat_history(session_id, chat_history, "") return chat_history diff --git a/src/agent/core/tool_config.json b/src/agent/core/tool_config.json index 2d14630..2b49144 100644 --- a/src/agent/core/tool_config.json +++ b/src/agent/core/tool_config.json @@ -11,7 +11,7 @@ "name": "date_query_class", "display_name": "DateQuery", "description": "一个日期查询工具,可以返回当前的日期信息。仅当需要查询当前时间时调用,其余任何情况严禁调用。", - "permission": "direct", + "permission": "confirm", "auto_execute_rules": [], "enabled": true }, @@ -27,8 +27,14 @@ "name": "browser_control", "display_name": "BrowserControl", "description": "统一控制浏览器:打开/关闭浏览器、刷新/关闭标签页、打开指定 URL。", - "permission": "direct", - "auto_execute_rules": [], + "permission": "confirm", + "auto_execute_rules": [ + { + "parameter": "action", + "operator": "equals", + "value": "open" + } + ], "enabled": true }, { @@ -43,10 +49,24 @@ "name": "software_control", "display_name": "SoftwareControl", "description": "操作本机已安装的软件。工具会自动扫描 Windows 开始菜单快捷方式、桌面快捷方式和 App Paths,建立软件索引后根据用户输入做模糊匹配。action=open 时打开软件;action=close 时仅做正常关闭;action=force_close 时才允许在正常关闭失败后强制结束进程。仅当用户明确要求打开、关闭或强制关闭某个本机软件时调用。", - "permission": "direct", + "permission": "confirm", "auto_execute_rules": [], "enabled": true }, + { + "name": "rag_tool", + "display_name": "RAGTool", + "description": "个人知识库管理工具,支持增删查及列出所有来源操作。", + "permission": "confirm", + "auto_execute_rules": [ + { + "parameter": "action_type", + "operator": "equals", + "value": "query" + } + ], + "enabled": true + }, { "name": "directory_listing", "display_name": "DirectoryListing", @@ -54,5 +74,21 @@ "permission": "direct", "auto_execute_rules": [], "enabled": true + }, + { + "name": "todo_list", + "display_name": "TodoList", + "description": "可选的任务管理工具。当你需要拆分复杂任务为多个步骤并跟踪进度时调用。输入 steps (list[str]) 为每一步的描述,step (int) 为已完成的步骤索引(从0开始)。任务默认状态为 pending,根据 step 自动标记完成。工具返回当前 todo list 文本。仅在需要跟踪多步骤任务进度时调用,简单任务不需要调用。", + "permission": "direct", + "auto_execute_rules": [], + "enabled": true + }, + { + "name": "gui_tool", + "display_name": "GUITool", + "description": "GUI automation tool. Visually locates desktop elements from a screenshot, maps coordinates to the real screen, and executes pyautogui actions. Supported actions: click, type, drag_and_drop, scroll, ho", + "permission": "direct", + "auto_execute_rules": [], + "enabled": true } ] \ No newline at end of file diff --git a/src/agent/history/__pycache__/tool_call_recorder.cpython-312.pyc b/src/agent/history/__pycache__/tool_call_recorder.cpython-312.pyc new file mode 100644 index 0000000..e12d71e Binary files /dev/null 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..25cc524 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 new file mode 100644 index 0000000..90ce534 Binary files /dev/null and b/src/agent/history/data_base/chat_history.db differ diff --git a/src/agent/history/multi_inputs/20260618_111759/ui_messages.json b/src/agent/history/multi_inputs/20260618_111759/ui_messages.json new file mode 100644 index 0000000..f0ff54d --- /dev/null +++ b/src/agent/history/multi_inputs/20260618_111759/ui_messages.json @@ -0,0 +1,80 @@ +{ + "version": 3, + "messages": [ + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "已为您生成详细的**西红柿炒鸡蛋**制作计划,共分为8个步骤:\n\n1. **准备食材**:2-3个鸡蛋、2个中等大小西红柿、葱花少许、食用油适量、盐适量、糖少许(可选)\n2. **处理食材**:将鸡蛋打入碗中,加少许盐打散;西红柿洗净切块;葱切成葱花备用\n3. **热锅倒油**:开中火加热炒锅,倒入适量食用油,待油微热\n4. **炒鸡蛋**:倒入蛋液,用筷子或铲子快速划散,炒至凝固后盛出备用\n5. **炒西红柿**:锅中留底油,放入西红柿块翻炒,可用铲子压一压帮助出汁,炒至软烂\n6. **调味**:根据口味加入适量盐和少许糖提鲜,翻炒均匀\n7. **混合**:将炒好的鸡蛋倒回锅中,与西红柿翻炒均匀,让鸡蛋吸收汤汁\n8. **出锅**:撒上葱花点缀,装盘即可享用\n\n祝您烹饪愉快!如果需要标记某一步骤完成,请告诉我。", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": null, + "agent_mode": null + } + }, + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "你的上下文中有什么" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "需要你确认后继续执行。", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": { + "id": "b5de8b0742564742ba20ec9df9803b51", + "type": "tool_call", + "title": "待确认执行工具:rag_tool", + "tool_name": "rag_tool", + "args": { + "name": "rag_tool", + "args": { + "action_type": "list_sources" + } + }, + "instruction": "{\n \"name\": \"rag_tool\",\n \"args\": {\n \"action_type\": \"list_sources\"\n }\n}", + "options": [ + "approve", + "reject" + ], + "resolved": true + }, + "agent_mode": null + } + }, + { + "role": "assistant", + "assistant": { + "text": "已拒绝,本次待确认操作不会执行。", + "output_type": "text", + "autoplay": false, + "audio_duration_seconds": null, + "audio_relpath": null, + "pending_action": null, + "agent_mode": null + } + } + ] +} \ No newline at end of file diff --git a/src/agent/history/multi_inputs/20260618_173653/ui_messages.json b/src/agent/history/multi_inputs/20260618_173653/ui_messages.json new file mode 100644 index 0000000..0dc4ce4 --- /dev/null +++ b/src/agent/history/multi_inputs/20260618_173653/ui_messages.json @@ -0,0 +1,28 @@ +{ + "version": 3, + "messages": [ + { + "role": "user", + "user": { + "content": [ + { + "type": "text", + "text": "帮我关掉DeepSeek开放平台的标签页" + } + ] + } + }, + { + "role": "assistant", + "assistant": { + "text": "已经帮您关闭了 DeepSeek 开放平台的标签页。现在浏览器显示的是 minor - Chatbot 页面。", + "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 7101914..4fe939c 100644 --- a/src/agent/history/tool_call_recorder.py +++ b/src/agent/history/tool_call_recorder.py @@ -7,9 +7,8 @@ """ from __future__ import annotations -import json -import os import base64 +import json import time import threading from contextvars import ContextVar @@ -25,6 +24,9 @@ TOOL_CALLING_ROOT = _HISTORY_ROOT / "tool_calling" # ---------- 当前 turn 上下文 ---------- _current_turn_id: ContextVar[str] = ContextVar("current_turn_id", default="") _current_session_id: ContextVar[str] = ContextVar("current_session_id", default="") +_context_fallback_lock = threading.Lock() +_current_turn_fallback = "" +_current_session_fallback = "" # ---------- 实时记录(线程安全) ---------- _live_lock = threading.Lock() @@ -32,19 +34,33 @@ _live_records: dict[str, list[dict[str, Any]]] = {} # key = session_id def set_current_turn(turn_id: str) -> None: + global _current_turn_fallback _current_turn_id.set(turn_id) + with _context_fallback_lock: + _current_turn_fallback = turn_id def get_current_turn() -> str: - return _current_turn_id.get() + turn_id = _current_turn_id.get() + if turn_id: + return turn_id + with _context_fallback_lock: + return _current_turn_fallback def set_current_session(session_id: str) -> None: + global _current_session_fallback _current_session_id.set(session_id) + with _context_fallback_lock: + _current_session_fallback = session_id def get_current_session() -> str: - return _current_session_id.get() + session_id = _current_session_id.get() + if session_id: + return session_id + with _context_fallback_lock: + return _current_session_fallback def start_live_turn(session_id: str) -> str: @@ -63,6 +79,7 @@ def record_tool_call_live(session_id: str, name: str, args: dict[str, Any]) -> N _live_records[session_id] = [] _live_records[session_id].append({ "name": name, + "args": _json_safe(args), "args_summary": _short_args(args), "status": "running", "result_text": "", @@ -70,20 +87,63 @@ def record_tool_call_live(session_id: str, name: str, args: dict[str, Any]) -> N }) -def record_tool_result_live(session_id: str, name: str, result_text: str, - image_bytes: bytes | None = None) -> None: - """图节点中调用:记录工具返回结果。""" +def record_tool_result_live( + session_id: str, + name: str, + result_text: str, + artifact: Any | None = None, +) -> None: + """记录工具返回结果,供前端实时展示文本与图片预览。""" with _live_lock: records = _live_records.get(session_id, []) for rec in reversed(records): if rec["name"] == name and rec["status"] == "running": rec["status"] = "done" rec["result_text"] = (result_text or "")[:500] - if image_bytes: - rec["image_base64"] = base64.standard_b64encode(image_bytes).decode("ascii") + images = _artifact_image_data_urls(artifact) + if images: + rec["result_images"] = images return +def _artifact_image_data_urls(artifact: Any | None) -> list[str]: + if not artifact: + return [] + + images: list[str] = [] + image_bytes_list: list[bytes] = [] + + def collect(item: Any) -> None: + if isinstance(item, bytes): + image_bytes_list.append(item) + return + if not isinstance(item, dict): + return + if isinstance(item.get("image_bytes"), bytes): + image_bytes_list.append(item["image_bytes"]) + for key in ("image_url", "url", "data_url"): + value = item.get(key) + if isinstance(value, str) and (value.startswith("data:image/") or value.startswith("http")): + images.append(value) + image_url = item.get("image_url") + if isinstance(image_url, dict): + value = image_url.get("url") + if isinstance(value, str) and (value.startswith("data:image/") or value.startswith("http")): + images.append(value) + + if isinstance(artifact, list): + for item in artifact: + collect(item) + else: + collect(artifact) + + images.extend( + "data:image/png;base64," + base64.standard_b64encode(img).decode("ascii") + for img in image_bytes_list + ) + return images + + def end_live_turn(session_id: str) -> list[dict[str, Any]]: """结束实时记录并返回该 turn 所有记录。""" set_current_turn("") diff --git a/src/agent/history/tool_calling/20260618_111759/turn_20260618_111855.json b/src/agent/history/tool_calling/20260618_111759/turn_20260618_111855.json new file mode 100644 index 0000000..01635fe --- /dev/null +++ b/src/agent/history/tool_calling/20260618_111759/turn_20260618_111855.json @@ -0,0 +1,29 @@ +{ + "session_id": "20260618_111759", + "turn_id": "20260618_111855", + "user_message": { + "role": "user", + "content": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划" + }, + "tool_calls": [ + { + "name": "todo_list", + "args": { + "steps": [ + "准备食材:2-3个鸡蛋、2个中等大小西红柿、葱花少许、食用油适量、盐适量、糖少许(可选)", + "处理食材:将鸡蛋打入碗中,加少许盐打散;西红柿洗净切块;葱切成葱花备用", + "热锅倒油:开中火加热炒锅,倒入适量食用油,待油微热", + "炒鸡蛋:倒入蛋液,用筷子或铲子快速划散,炒至凝固后盛出备用", + "炒西红柿:锅中留底油,放入西红柿块翻炒,可用铲子压一压帮助出汁,炒至软烂", + "调味:根据口味加入适量盐和少许糖提鲜,翻炒均匀", + "混合:将炒好的鸡蛋倒回锅中,与西红柿翻炒均匀,让鸡蛋吸收汤汁", + "出锅:撒上葱花点缀,装盘即可享用" + ] + }, + "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a2-3\\u4e2a\\u9e21\\u86cb\\u30012\\u4e2a\\u4e2d\\u7b49\\u5927\\u5c0f\\u897f\\u7ea2\\u67ff\\u3001\\u8471\\u82b1\\u5c11\\u8bb8\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\\u3001\\u76d0\\u9002\\u91cf\\u3001\\u7cd6\\u5c11\\u8bb8\\uff08\\u53ef\\u9009\\uff09\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\uff0c\\u52a0\\u5c11\\u8bb8\\u76d0\\u6253\\u6563\\uff1b\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u8471\\u5207\\u6210\\u8471\\u82b1\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\uff0c\\u5f85\\u6cb9\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u7528\\u7b77\\u5b50\\u6216\\u94f2\\u5b50\\u5feb\\u901f\\u5212\\u6563\\uff0c\\u7092\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u5757\\u7ffb\\u7092\\uff0c\\u53ef\\u7528\\u94f2\\u5b50\\u538b\\u4e00\\u538b\\u5e2e\\u52a9\\u51fa\\u6c41\\uff0c\\u7092\\u81f3\\u8f6f\\u70c2\", \"\\u8c03\\u5473\\uff1a\\u6839\\u636e\\u53e3\\u5473\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\u548c\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff0c\\u7ffb\\u7092\\u5747\\u5300\", \"\\u6df7\\u5408\\uff1a\\u5c06\\u7092\\u597d\\u7684\\u9e21\\u86cb\\u5012\\u56de\\u9505\\u4e2d\\uff0c\\u4e0e\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u5747\\u5300\\uff0c\\u8ba9\\u9e21\\u86cb\\u5438\\u6536\\u6c64\\u6c41\", \"\\u51fa\\u9505\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u88c5\\u76d8\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": []}}\n[Todo List]\n- [1/8] [Pending]: 准备食材:2-3个鸡蛋、2个中等大小西红柿、葱花少许、食用油适量、盐适量、糖少许(可选)\n- [2/8] [Pending]: 处理食材:将鸡蛋打入碗中,加少许盐打散;西红柿洗净切块;葱切成葱花备用\n- [3/8] [Pending]: 热锅倒油:开中火加热炒锅,倒入适量食用油,待油微热\n- [4/8] [Pending]: 炒鸡蛋:倒入蛋液,用筷子或铲子快速划散,炒至凝固后盛出备用\n- [5/8] [Pending]: 炒西红柿:锅中留底油,放入西红柿块翻炒,可用铲子压一压帮助出汁,炒至软烂\n- [6/8] [Pending]: 调味:根据口味加入适量盐和少许糖提鲜,翻炒均匀\n- [7/8] [Pending]: 混合:将炒好的鸡蛋倒回锅中,与西红柿翻炒均匀,让鸡蛋吸收汤汁\n- [8/8] [Pending]: 出锅:撒上葱花点缀,装盘即可享用", + "result_files": [] + } + ], + "assistant_response": "已为您生成详细的**西红柿炒鸡蛋**制作计划,共分为8个步骤:\n\n1. **准备食材**:2-3个鸡蛋、2个中等大小西红柿、葱花少许、食用油适量、盐适量、糖少许(可选)\n2. **处理食材**:将鸡蛋打入碗中,加少许盐打散;西红柿洗净切块;葱切成葱花备用\n3. **热锅倒油**:开中火加热炒锅,倒入适量食用油,待油微热\n4. **炒鸡蛋**:倒入蛋液,用筷子或铲子快速划散,炒至凝固后盛出备用\n5. **炒西红柿**:锅中留底油,放入西红柿块翻炒,可用铲子压一压帮助出汁,炒至软烂\n6. **调味**:根据口味加入适量盐和少许糖提鲜,翻炒均匀\n7. **混合**:将炒好的鸡蛋倒回锅中,与西红柿翻炒均匀,让鸡蛋吸收汤汁\n8. **出锅**:撒上葱花点缀,装盘即可享用\n\n祝您烹饪愉快!如果需要标记某一步骤完成,请告诉我。", + "timestamp": "20260618_111855" +} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260618_112443/turn_20260618_112445.json b/src/agent/history/tool_calling/20260618_112443/turn_20260618_112445.json new file mode 100644 index 0000000..2a0d3cb --- /dev/null +++ b/src/agent/history/tool_calling/20260618_112443/turn_20260618_112445.json @@ -0,0 +1,30 @@ +{ + "session_id": "20260618_112443", + "turn_id": "20260618_112445", + "user_message": { + "role": "user", + "content": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划" + }, + "tool_calls": [ + { + "name": "todo_list", + "args": { + "steps": [ + "准备食材:2-3个鸡蛋、2-3个中等大小西红柿、葱花适量", + "准备调料:食用油、盐、白糖(可选,用于提鲜)、番茄酱(可选)", + "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", + "热锅凉油:开中火加热炒锅,倒入适量食用油", + "炒鸡蛋:油热后倒入蛋液,快速划散至凝固,盛出备用", + "炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁变软", + "调味:加入适量盐和少许白糖,喜欢浓郁口感可加一勺番茄酱", + "混合翻炒:将炒好的鸡蛋倒回锅中,与西红柿均匀翻炒混合", + "出锅装盘:撒上葱花点缀,即可享用美味的西红柿炒鸡蛋" + ] + }, + "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a2-3\\u4e2a\\u9e21\\u86cb\\u30012-3\\u4e2a\\u4e2d\\u7b49\\u5927\\u5c0f\\u897f\\u7ea2\\u67ff\\u3001\\u8471\\u82b1\\u9002\\u91cf\", \"\\u51c6\\u5907\\u8c03\\u6599\\uff1a\\u98df\\u7528\\u6cb9\\u3001\\u76d0\\u3001\\u767d\\u7cd6\\uff08\\u53ef\\u9009\\uff0c\\u7528\\u4e8e\\u63d0\\u9c9c\\uff09\\u3001\\u756a\\u8304\\u9171\\uff08\\u53ef\\u9009\\uff09\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u51c9\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u6cb9\\u70ed\\u540e\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\uff0c\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u518d\\u52a0\\u5c11\\u8bb8\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u5757\\u7ffb\\u7092\\u81f3\\u51fa\\u6c41\\u53d8\\u8f6f\", \"\\u8c03\\u5473\\uff1a\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\u548c\\u5c11\\u8bb8\\u767d\\u7cd6\\uff0c\\u559c\\u6b22\\u6d53\\u90c1\\u53e3\\u611f\\u53ef\\u52a0\\u4e00\\u52fa\\u756a\\u8304\\u9171\", \"\\u6df7\\u5408\\u7ffb\\u7092\\uff1a\\u5c06\\u7092\\u597d\\u7684\\u9e21\\u86cb\\u5012\\u56de\\u9505\\u4e2d\\uff0c\\u4e0e\\u897f\\u7ea2\\u67ff\\u5747\\u5300\\u7ffb\\u7092\\u6df7\\u5408\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\\u7f8e\\u5473\\u7684\\u897f\\u7ea2\\u67ff\\u7092\\u9e21\\u86cb\"], \"done_steps\": []}}\n[Todo List]\n- [1/9] [Pending]: 准备食材:2-3个鸡蛋、2-3个中等大小西红柿、葱花适量\n- [2/9] [Pending]: 准备调料:食用油、盐、白糖(可选,用于提鲜)、番茄酱(可选)\n- [3/9] [Pending]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [4/9] [Pending]: 热锅凉油:开中火加热炒锅,倒入适量食用油\n- [5/9] [Pending]: 炒鸡蛋:油热后倒入蛋液,快速划散至凝固,盛出备用\n- [6/9] [Pending]: 炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁变软\n- [7/9] [Pending]: 调味:加入适量盐和少许白糖,喜欢浓郁口感可加一勺番茄酱\n- [8/9] [Pending]: 混合翻炒:将炒好的鸡蛋倒回锅中,与西红柿均匀翻炒混合\n- [9/9] [Pending]: 出锅装盘:撒上葱花点缀,即可享用美味的西红柿炒鸡蛋", + "result_files": [] + } + ], + "assistant_response": "已为您生成详细的西红柿炒鸡蛋制作计划,共9个步骤:\n\n1. **准备食材**:2-3个鸡蛋、2-3个中等大小西红柿、葱花适量\n2. **准备调料**:食用油、盐、白糖(可选,用于提鲜)、番茄酱(可选)\n3. **处理食材**:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n4. **热锅凉油**:开中火加热炒锅,倒入适量食用油\n5. **炒鸡蛋**:油热后倒入蛋液,快速划散至凝固,盛出备用\n6. **炒西红柿**:锅中再加少许油,放入西红柿块翻炒至出汁变软\n7. **调味**:加入适量盐和少许白糖,喜欢浓郁口感可加一勺番茄酱\n8. **混合翻炒**:将炒好的鸡蛋倒回锅中,与西红柿均匀翻炒混合\n9. **出锅装盘**:撒上葱花点缀,即可享用美味的西红柿炒鸡蛋\n\n祝您烹饪愉快!", + "timestamp": "20260618_112445" +} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260618_113558/turn_20260618_113615.json b/src/agent/history/tool_calling/20260618_113558/turn_20260618_113615.json new file mode 100644 index 0000000..4668da3 --- /dev/null +++ b/src/agent/history/tool_calling/20260618_113558/turn_20260618_113615.json @@ -0,0 +1,147 @@ +{ + "session_id": "20260618_113558", + "turn_id": "20260618_113615", + "user_message": { + "role": "user", + "content": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划,并逐步更新这份计划" + }, + "tool_calls": [ + { + "name": "todo_list", + "args": { + "steps": [ + "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", + "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", + "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", + "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", + "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", + "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", + "出锅装盘:撒上葱花点缀,即可享用" + ] + }, + "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": []}}\n[Todo List]\n- [1/7] [Pending]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Pending]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Pending]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Pending]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用", + "result_files": [] + }, + { + "name": "todo_list", + "args": { + "steps": [ + "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", + "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", + "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", + "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", + "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", + "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", + "出锅装盘:撒上葱花点缀,即可享用" + ], + "done_step": 1 + }, + "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Pending]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Pending]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Pending]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用", + "result_files": [] + }, + { + "name": "todo_list", + "args": { + "steps": [ + "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", + "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", + "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", + "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", + "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", + "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", + "出锅装盘:撒上葱花点缀,即可享用" + ], + "done_step": 2 + }, + "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Pending]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Pending]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用", + "result_files": [] + }, + { + "name": "todo_list", + "args": { + "steps": [ + "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", + "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", + "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", + "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", + "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", + "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", + "出锅装盘:撒上葱花点缀,即可享用" + ], + "done_step": 3 + }, + "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Pending]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用", + "result_files": [] + }, + { + "name": "todo_list", + "args": { + "steps": [ + "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", + "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", + "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", + "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", + "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", + "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", + "出锅装盘:撒上葱花点缀,即可享用" + ], + "done_step": 4 + }, + "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2, 3]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Done]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Pending]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用", + "result_files": [] + }, + { + "name": "todo_list", + "args": { + "steps": [ + "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", + "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", + "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", + "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", + "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", + "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", + "出锅装盘:撒上葱花点缀,即可享用" + ], + "done_step": 5 + }, + "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2, 3, 4]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Done]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Done]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Pending]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用", + "result_files": [] + }, + { + "name": "todo_list", + "args": { + "steps": [ + "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", + "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", + "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", + "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", + "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", + "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", + "出锅装盘:撒上葱花点缀,即可享用" + ], + "done_step": 6 + }, + "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2, 3, 4, 5]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Done]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Done]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Done]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Pending]: 出锅装盘:撒上葱花点缀,即可享用", + "result_files": [] + }, + { + "name": "todo_list", + "args": { + "steps": [ + "准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量", + "处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用", + "热锅倒油:开中火加热炒锅,倒入适量食用油至微热", + "炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用", + "炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)", + "混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁", + "出锅装盘:撒上葱花点缀,即可享用" + ], + "done_step": 7 + }, + "result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"\\u51c6\\u5907\\u98df\\u6750\\uff1a\\u897f\\u7ea2\\u67ff2-3\\u4e2a\\uff0c\\u9e21\\u86cb3\\u4e2a\\uff0c\\u8471\\u82b1\\u5c11\\u8bb8\\uff0c\\u76d0\\u3001\\u7cd6\\u3001\\u98df\\u7528\\u6cb9\\u9002\\u91cf\", \"\\u5904\\u7406\\u98df\\u6750\\uff1a\\u5c06\\u897f\\u7ea2\\u67ff\\u6d17\\u51c0\\u5207\\u5757\\uff1b\\u9e21\\u86cb\\u6253\\u5165\\u7897\\u4e2d\\u6405\\u6563\\u5907\\u7528\", \"\\u70ed\\u9505\\u5012\\u6cb9\\uff1a\\u5f00\\u4e2d\\u706b\\u52a0\\u70ed\\u7092\\u9505\\uff0c\\u5012\\u5165\\u9002\\u91cf\\u98df\\u7528\\u6cb9\\u81f3\\u5fae\\u70ed\", \"\\u7092\\u9e21\\u86cb\\uff1a\\u5012\\u5165\\u86cb\\u6db2\\uff0c\\u5feb\\u901f\\u5212\\u6563\\u81f3\\u51dd\\u56fa\\u540e\\u76db\\u51fa\\u5907\\u7528\", \"\\u7092\\u897f\\u7ea2\\u67ff\\uff1a\\u9505\\u4e2d\\u7559\\u5e95\\u6cb9\\uff0c\\u653e\\u5165\\u897f\\u7ea2\\u67ff\\u7ffb\\u7092\\u51fa\\u6c41\\uff08\\u53ef\\u52a0\\u5c11\\u8bb8\\u7cd6\\u63d0\\u9c9c\\uff09\", \"\\u6df7\\u5408\\u8c03\\u5473\\uff1a\\u5012\\u5165\\u7092\\u597d\\u7684\\u9e21\\u86cb\\uff0c\\u52a0\\u5165\\u9002\\u91cf\\u76d0\\uff0c\\u7ffb\\u7092\\u5747\\u5300\\u4f7f\\u9e21\\u86cb\\u5438\\u6ee1\\u6c64\\u6c41\", \"\\u51fa\\u9505\\u88c5\\u76d8\\uff1a\\u6492\\u4e0a\\u8471\\u82b1\\u70b9\\u7f00\\uff0c\\u5373\\u53ef\\u4eab\\u7528\"], \"done_steps\": [0, 1, 2, 3, 4, 5, 6]}}\n[Todo List]\n- [1/7] [Done]: 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [2/7] [Done]: 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [3/7] [Done]: 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [4/7] [Done]: 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [5/7] [Done]: 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [6/7] [Done]: 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [7/7] [Done]: 出锅装盘:撒上葱花点缀,即可享用", + "result_files": [] + } + ], + "assistant_response": "太棒了!西红柿炒鸡蛋计划已全部完成。\n\n**最终状态:**\n- [x] 准备食材:西红柿2-3个,鸡蛋3个,葱花少许,盐、糖、食用油适量\n- [x] 处理食材:将西红柿洗净切块;鸡蛋打入碗中搅散备用\n- [x] 热锅倒油:开中火加热炒锅,倒入适量食用油至微热\n- [x] 炒鸡蛋:倒入蛋液,快速划散至凝固后盛出备用\n- [x] 炒西红柿:锅中留底油,放入西红柿翻炒出汁(可加少许糖提鲜)\n- [x] 混合调味:倒入炒好的鸡蛋,加入适量盐,翻炒均匀使鸡蛋吸满汤汁\n- [x] 出锅装盘:撒上葱花点缀,即可享用\n\n现在你可以享受这道美味的家常菜了!🍅🥚", + "timestamp": "20260618_113615" +} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260618_120438/turn_20260618_120441.json b/src/agent/history/tool_calling/20260618_120438/turn_20260618_120441.json new file mode 100644 index 0000000..886ed9a --- /dev/null +++ b/src/agent/history/tool_calling/20260618_120438/turn_20260618_120441.json @@ -0,0 +1,84 @@ +{ + "session_id": "20260618_120438", + "turn_id": "20260618_120441", + "user_message": { + "role": "user", + "content": "调用一次todolist生成一份详细的做西红柿炒鸡蛋计划,并逐步更新这份计划" + }, + "tool_calls": [ + { + "name": "todo_list", + "args": { + "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", + "done_step": -1 + }, + "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': -1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", + "result_files": [] + }, + { + "name": "todo_list", + "args": { + "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", + "done_step": -1 + }, + "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': -1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", + "result_files": [] + }, + { + "name": "todo_list", + "args": { + "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", + "done_step": 1 + }, + "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", + "result_files": [] + }, + { + "name": "todo_list", + "args": { + "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", + "done_step": 1 + }, + "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", + "result_files": [] + }, + { + "name": "todo_list", + "args": { + "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", + "done_step": 1 + }, + "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", + "result_files": [] + }, + { + "name": "todo_list", + "args": { + "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", + "done_step": 1 + }, + "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", + "result_files": [] + }, + { + "name": "todo_list", + "args": { + "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", + "done_step": 1 + }, + "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", + "result_files": [] + }, + { + "name": "todo_list", + "args": { + "steps": "[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]", + "done_step": 1 + }, + "result_text": "Error invoking tool 'todo_list' with kwargs {'steps': '[\"准备食材:2-3个西红柿、2-3个鸡蛋、葱花、盐、糖、食用油\", \"处理西红柿:洗净,去蒂,切成小块备用\", \"打散鸡蛋:将鸡蛋打入碗中,加入少许盐,充分搅打均匀\", \"热锅凉油(炒蛋):锅中倒适量油,油热后倒入蛋液,凝固后盛出备用\", \"炒西红柿:锅中再加少许油,放入西红柿块翻炒至出汁软烂\", \"调味:根据口味加入适量的盐和糖,翻炒均匀\", \"混合:将之前炒好的鸡蛋倒回锅中,与西红柿混合翻炒均匀\", \"出锅:撒上葱花,装盘即可\"]', 'done_step': 1} with error:\n steps: Input should be a valid list\n Please fix the error and try again.", + "result_files": [] + } + ], + "assistant_response": "", + "timestamp": "20260618_120441" +} \ No newline at end of file diff --git a/src/agent/history/tool_calling/20260618_173653/20260618_173727/_artifact_call_fd502781107945c7a6880635_0.png b/src/agent/history/tool_calling/20260618_173653/20260618_173727/_artifact_call_fd502781107945c7a6880635_0.png new file mode 100644 index 0000000..13e1730 Binary files /dev/null and b/src/agent/history/tool_calling/20260618_173653/20260618_173727/_artifact_call_fd502781107945c7a6880635_0.png differ diff --git a/src/agent/history/tool_calling/20260618_173653/turn_20260618_173727.json b/src/agent/history/tool_calling/20260618_173653/turn_20260618_173727.json new file mode 100644 index 0000000..01a3ed4 --- /dev/null +++ b/src/agent/history/tool_calling/20260618_173653/turn_20260618_173727.json @@ -0,0 +1,23 @@ +{ + "session_id": "20260618_173653", + "turn_id": "20260618_173727", + "user_message": { + "role": "user", + "content": "帮我关掉DeepSeek开放平台的标签页" + }, + "tool_calls": [ + { + "name": "gui_tool", + "args": { + "action": "click", + "element_description": "DeepSeek开放平台的标签页关闭按钮(X)" + }, + "result_text": "Clicked target. grounded (308,21)", + "result_files": [ + "_artifact_call_fd502781107945c7a6880635_0.png" + ] + } + ], + "assistant_response": "已经帮您关闭了 DeepSeek 开放平台的标签页。现在浏览器显示的是 minor - Chatbot 页面。", + "timestamp": "20260618_173727" +} \ No newline at end of file diff --git a/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc b/src/agent/memory/__pycache__/system_prompt.cpython-312.pyc index 81b095e..2bd94a4 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/tools/__init__.py b/src/agent/tools/__init__.py index d8191d6..ade392b 100644 --- a/src/agent/tools/__init__.py +++ b/src/agent/tools/__init__.py @@ -1,5 +1,6 @@ from .tool import * from .tool_software_control import SoftwareControl +from .gui_tool import GUITool from .tool_rag import RAGTool _all_tools = [ @@ -12,6 +13,7 @@ _all_tools = [ RAGTool(), DirectoryListing(), TodoList(), + GUITool(), ] # 注册所有可用工具名称(供前端配置面板展示),从 _all_tools 实例推导 diff --git a/src/agent/tools/__pycache__/__init__.cpython-312.pyc b/src/agent/tools/__pycache__/__init__.cpython-312.pyc index a75a70b..9f5f68b 100644 Binary files a/src/agent/tools/__pycache__/__init__.cpython-312.pyc and b/src/agent/tools/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/agent/tools/__pycache__/gui_tool.cpython-312.pyc b/src/agent/tools/__pycache__/gui_tool.cpython-312.pyc new file mode 100644 index 0000000..aaa13e8 Binary files /dev/null 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 5c25270..92361e1 100644 Binary files a/src/agent/tools/__pycache__/tool.cpython-312.pyc and b/src/agent/tools/__pycache__/tool.cpython-312.pyc differ diff --git a/src/agent/tools/__pycache__/tool.cpython-313.pyc b/src/agent/tools/__pycache__/tool.cpython-313.pyc index baddf9d..3dae7d2 100644 Binary files a/src/agent/tools/__pycache__/tool.cpython-313.pyc and b/src/agent/tools/__pycache__/tool.cpython-313.pyc differ diff --git a/src/agent/tools/__pycache__/tool_rag.cpython-312.pyc b/src/agent/tools/__pycache__/tool_rag.cpython-312.pyc index b32880f..43a58a1 100644 Binary files a/src/agent/tools/__pycache__/tool_rag.cpython-312.pyc and b/src/agent/tools/__pycache__/tool_rag.cpython-312.pyc differ diff --git a/src/agent/tools/__pycache__/tool_software_control.cpython-312.pyc b/src/agent/tools/__pycache__/tool_software_control.cpython-312.pyc index 9173923..ee7f7c9 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 318d037..fd3d34a 100644 --- a/src/agent/tools/gui_tool.py +++ b/src/agent/tools/gui_tool.py @@ -1 +1,314 @@ -from agent.utils.env_utils import GROUNDING_WIDTH, GROUNDING_HEIGHT \ No newline at end of file +from __future__ import annotations + +import asyncio +import base64 +import io +import re +import sys +import time +from typing import Any, Literal, Optional + +import pyautogui +from langchain.tools import BaseTool +from langchain_core.messages import HumanMessage +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 + + +GUIAction = Literal[ + "click", + "type", + "drag_and_drop", + "scroll", + "hotkey", + "hold_and_press", + "wait", +] + + +class GUIToolInput(BaseModel): + # 将 action 改为可选,未提供时直接返回屏幕截图 + action: Optional[GUIAction] = Field( + default=None, # ← 允许为 None + description="GUI action to execute. If None or empty, returns current screenshot.", + ) + element_description: Optional[str] = Field( + default=None, + description="Natural-language description of the target screen element for grounded actions.", + ) + ending_description: Optional[str] = Field( + default=None, + description="Natural-language description of the drag end target.", + ) + text: str = Field(default="", description="Text to type.") + overwrite: bool = Field(default=False, description="Select all existing text before typing.") + enter: bool = Field(default=False, description="Press Enter after typing.") + num_clicks: int = Field(default=1, description="Number of mouse clicks.") + button_type: Literal["left", "middle", "right"] = Field(default="left", description="Mouse button.") + hold_keys: list[str] = Field(default_factory=list, description="Modifier keys to hold while clicking or dragging.") + keys: list[str] = Field(default_factory=list, description="Hotkey keys or keys to press.") + press_keys: list[str] = Field(default_factory=list, description="Keys to press while holding hold_keys.") + clicks: int = Field(default=0, description="Scroll click amount. Negative means down.") + shift: bool = Field(default=False, description="Use horizontal scrolling when true.") + wait_seconds: float = Field(default=0.5, description="Seconds to wait.") + + +def _screenshot_png() -> tuple[bytes, tuple[int, int]]: + image = pyautogui.screenshot() + width, height = image.size + buf = io.BytesIO() + image.save(buf, format="PNG") + return buf.getvalue(), (width, height) + + +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) + buf = io.BytesIO() + image.save(buf, format="PNG") + return buf.getvalue() + + +def _image_data_url(image_bytes: bytes) -> str: + encoded = base64.b64encode(image_bytes).decode("ascii") + return f"data:image/png;base64,{encoded}" + + +def _parse_coordinates(text: str) -> tuple[int, int]: + numbers = re.findall(r"-?\d+(?:\.\d+)?", text or "") + if len(numbers) < 2: + 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)) + return x, y + + +def _scale_to_screen(coords: tuple[int, int], screen_size: tuple[int, int]) -> tuple[int, int]: + screen_width, screen_height = screen_size + x, y = coords + return ( + round(x * screen_width / GROUNDING_WIDTH), + round(y * screen_height / GROUNDING_HEIGHT), + ) + + +def _ground_element(element_description: str) -> tuple[int, int, str]: + if not element_description or not element_description.strip(): + raise ValueError("element_description is required for this GUI action.") + + raw_png, screen_size = _screenshot_png() + 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"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" + "Do not include any explanation." + ) + response = llm.invoke( + [ + HumanMessage( + content=[ + {"type": "image_url", "image_url": {"url": _image_data_url(grounding_png)}}, + {"type": "text", "text": prompt}, + ] + ) + ] + ) + response_text = str(response.content) + gx, gy = _parse_coordinates(response_text) + x, y = _scale_to_screen((gx, gy), screen_size) + return x, y, f"grounded ({gx},{gy})" + + +def _type_text(text: str) -> None: + if not text: + return + has_unicode = any(ord(ch) > 127 for ch in text) + if has_unicode: + try: + import pyperclip + + pyperclip.copy(text) + modifier = "command" if sys.platform == "darwin" else "ctrl" + pyautogui.hotkey(modifier, "v") + return + except Exception: + pass + pyautogui.write(text) + + +def _select_all_and_clear() -> None: + modifier = "command" if sys.platform == "darwin" else "ctrl" + pyautogui.hotkey(modifier, "a") + pyautogui.press("backspace") + + +def _with_hold_keys_down(keys: list[str]) -> None: + for key in keys or []: + pyautogui.keyDown(key) + + +def _with_hold_keys_up(keys: list[str]) -> None: + for key in reversed(keys or []): + pyautogui.keyUp(key) + + +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, drag_and_drop, scroll, " + "hotkey, hold_and_press, wait. If no action is given, returns a screenshot of the current screen." + ) + response_format: Literal["content", "content_and_artifact"] = "content_and_artifact" + + def _artifact_screenshot(self) -> dict: + try: + image_bytes, _ = _screenshot_png() + return {"image_bytes": image_bytes} + except Exception: + return {} + + def _run( + self, + action: Optional[str] = None, # ← 允许为 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, + ) -> tuple[str, dict]: + # ---------- 新增:无动作时直接截图 ---------- + if not action: + raw_png, size = _screenshot_png() + return ( + f"Screenshot captured (size {size[0]}x{size[1]}).", + self._artifact_screenshot(), # 再次截图,间隔极短,也可直接返回 {"image_bytes": raw_png} + ) + # ------------------------------------------- + try: + message = 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, + ) + time.sleep(0.2) + return message, self._artifact_screenshot() + except Exception as exc: + return f"GUI tool action failed: {exc}", self._artifact_screenshot() + + def _dispatch(self, **kwargs: Any) -> str: + action = str(kwargs["action"]).strip() + if action == "click": + return self._click(**kwargs) + if action == "type": + return self._type(**kwargs) + if action == "drag_and_drop": + return self._drag_and_drop(**kwargs) + if action == "scroll": + return self._scroll(**kwargs) + if action == "hotkey": + return self._hotkey(**kwargs) + if action == "hold_and_press": + return self._hold_and_press(**kwargs) + if action == "wait": + seconds = min(1.0, float(kwargs["wait_seconds"])) + time.sleep(seconds) + return f"Waited {seconds:.2f} seconds." + raise ValueError(f"unsupported GUI action: {action}") + + def _click(self, **kwargs: Any) -> str: + x, y, grounding = _ground_element(kwargs["element_description"] or "") + hold_keys = kwargs["hold_keys"] + _with_hold_keys_down(hold_keys) + try: + pyautogui.click(x, y, clicks=max(1, int(kwargs["num_clicks"])), button=kwargs["button_type"]) + finally: + _with_hold_keys_up(hold_keys) + return f"Clicked target. {grounding}" + + def _type(self, **kwargs: Any) -> str: + if kwargs["element_description"]: + x, y, grounding = _ground_element(kwargs["element_description"]) + pyautogui.click(x, y) + else: + grounding = "typed into current focus" + if kwargs["overwrite"]: + _select_all_and_clear() + _type_text(kwargs["text"]) + if kwargs["enter"]: + pyautogui.press("enter") + return f"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 "") + hold_keys = kwargs["hold_keys"] + pyautogui.moveTo(x1, y1) + _with_hold_keys_down(hold_keys) + try: + pyautogui.dragTo(x2, y2, duration=1.0, button="left") + pyautogui.mouseUp() + finally: + _with_hold_keys_up(hold_keys) + return f"Dragged target. Start: {start_grounding}. End: {end_grounding}" + + def _scroll(self, **kwargs: Any) -> str: + x, y, grounding = _ground_element(kwargs["element_description"] or "") + pyautogui.moveTo(x, y) + time.sleep(0.1) + clicks = int(kwargs["clicks"]) + if kwargs["shift"]: + pyautogui.hscroll(clicks) + else: + pyautogui.vscroll(clicks) + return f"Scrolled target by {clicks}. {grounding}" + + def _hotkey(self, **kwargs: Any) -> str: + keys = kwargs["keys"] + if not keys: + raise ValueError("keys is required for hotkey") + pyautogui.hotkey(*keys) + return f"Pressed hotkey {keys}." + + def _hold_and_press(self, **kwargs: Any) -> str: + hold_keys = kwargs["hold_keys"] + press_keys = kwargs["press_keys"] or kwargs["keys"] + if not press_keys: + raise ValueError("press_keys or keys is required for hold_and_press") + _with_hold_keys_down(hold_keys) + try: + pyautogui.press(press_keys) + finally: + _with_hold_keys_up(hold_keys) + return f"Held {hold_keys} and pressed {press_keys}." + + async def _arun(self, **kwargs: Any) -> tuple[str, dict]: + return await asyncio.to_thread(self._run, **kwargs) \ No newline at end of file diff --git a/src/agent/tools/tool.py b/src/agent/tools/tool.py index 49b9a77..108671d 100644 --- a/src/agent/tools/tool.py +++ b/src/agent/tools/tool.py @@ -3,7 +3,7 @@ from __future__ import annotations import os import io import json -from typing import Optional, Literal, Tuple +from typing import Any, Optional, Literal, Tuple import sys import time import webbrowser @@ -12,7 +12,7 @@ import asyncio from pathlib import Path from langchain.tools import BaseTool -from pydantic import BaseModel, Field, create_model +from pydantic import BaseModel, Field, create_model, model_validator from agent.core.llm import llm from agent.utils.env_utils import BROWSER_MAP @@ -380,9 +380,94 @@ class HumanInteraction(BaseTool): return await asyncio.to_thread(self._run, interaction_type, title, prompt, options, suggested_response) +# ---------- Todo List 实时推送共享存储 ---------- +# TodoList 工具本身和 web server 共享此 dict,无需跨模块 import server +_todo_store: dict[str, dict] = {} + + +def get_todo_store_data(session_id: str) -> dict | None: + return _todo_store.get(session_id) + + +def update_todo_store_session( + session_id: str, + steps: list[str] | None = None, + done_step: int = -1, +) -> dict | None: + if not session_id: + return None + current = _todo_store.get(session_id) or {"steps": [], "done_steps": []} + stored_steps = list(current.get("steps") or []) + done_steps = list(current.get("done_steps") or []) + + if steps: + stored_steps = list(steps) + 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() + + todo_data = {"steps": stored_steps, "done_steps": done_steps} + _todo_store[session_id] = todo_data + return todo_data + + +def update_todo_store_from_tool_args(session_id: str, tool_args: dict[str, Any]) -> dict | None: + raw_steps = tool_args.get("steps") if isinstance(tool_args, dict) else None + if isinstance(raw_steps, str): + try: + raw_steps = json.loads(raw_steps) + except (json.JSONDecodeError, ValueError): + raw_steps = None + steps = raw_steps if isinstance(raw_steps, list) else None + + done_step = tool_args.get("done_step", -1) if isinstance(tool_args, dict) else -1 + try: + done_step = int(done_step) + except (TypeError, ValueError): + done_step = -1 + + return update_todo_store_session(session_id, steps, done_step) + + +def set_todo_store_data(session_id: str, todo_data: dict) -> None: + if not session_id: + return + _todo_store[session_id] = { + "steps": list(todo_data.get("steps") or []), + "done_steps": list(todo_data.get("done_steps") or []), + } + + +def clear_todo_store_session(session_id: str) -> None: + _todo_store.pop(session_id, None) + + class TodoListInput(BaseModel): - steps: list[str] = Field(default_factory=list, description="任务步骤列表,每项为一步要做什么的描述") - step: int = Field(default=-1, description="已完成的步骤索引(从0开始),-1表示无已完成步骤") + steps: Optional[list[str]] = Field( + default=None, + description="任务步骤列表,每项为一步的描述。仅在首次创建计划时传入,后续更新进度时请省略此参数以节省 token。", + ) + done_step: int = Field( + default=-1, + description="本次新完成的步骤编号(从1开始)。-1 表示仅查看当前状态,不标记完成。", + ) + + @model_validator(mode="before") + @classmethod + def parse_steps_string(cls, data: Any) -> Any: + """容错:如果 steps 以 JSON 字符串形式传入(某些 LLM 的 function calling 行为),自动解析为列表。""" + if isinstance(data, dict) and isinstance(data.get("steps"), str): + raw = data["steps"].strip() + if raw.startswith("["): + try: + data["steps"] = json.loads(raw) + except (json.JSONDecodeError, ValueError): + pass + return data class TodoList(BaseTool): @@ -391,23 +476,61 @@ class TodoList(BaseTool): args_schema: type[BaseModel] = TodoListInput name: str = "todo_list" description: str = ( - "可选的任务管理工具。当你需要拆分复杂任务为多个步骤并跟踪进度时调用。" - "输入 steps (list[str]) 为每一步的描述,step (int) 为已完成的步骤索引(从0开始)。" - "任务默认状态为 pending,根据 step 自动标记完成。工具返回当前 todo list 文本。" + "可选的任务管理工具。当你需要拆分复杂任务为多个步骤并跟踪进度时调用。\n" + "参数说明:\n" + "- steps (list[str], 可选):任务步骤列表。仅在首次创建计划时传入所有步骤。后续更新进度时请省略此参数(传 null 或不传),工具会复用之前设定的步骤列表,省去重复传输。\n" + "- done_step (int, 可选):刚完成的步骤编号(从1开始),一次只标记一个。不传或传 -1 则只查看当前状态不做修改。\n" + "调用示例:\n" + '- 首次创建:{"steps": ["准备食材", "切菜", "烹饪", "装盘"], "done_step": -1}\n' + '- 完成第1步:"done_step": 1}(省略 steps)\n' + '- 查看状态:不传任何参数或 {"done_step": -1}\n' "仅在需要跟踪多步骤任务进度时调用,简单任务不需要调用。" ) response_format: Literal["content", "content_and_artifact"] = "content" - def _run(self, steps: list[str] | None = None, step: int = -1) -> str: + def _run(self, steps: list[str] | None = None, done_step: int = -1) -> str: steps = steps or [] - step = max(-1, min(step, len(steps) - 1)) - lines = ["## 📋 任务列表"] - for i, s in enumerate(steps): - status = "✅ 已完成" if i <= step else "⏳ 待处理" - lines.append(f"- [{i+1}/{len(steps)}] {status}: {s}") - if not steps: - lines.append("(无任务)") - return "\n".join(lines) - async def _arun(self, steps: list[str] | None = None, step: int = -1) -> str: - return self._run(steps, step) + # 用简单变量维护状态(同一实例在 AgentRuntime 生命周期内复用) + if not hasattr(self, "_stored_steps"): + self._stored_steps: list[str] = [] + if not hasattr(self, "_done_set"): + self._done_set: list[int] = [] + + # 如果传入了新的 steps,更新存储(可能是新任务替换旧任务) + if steps: + self._stored_steps = steps + self._done_set = [] + + # 标记新完成步骤(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() + + todo_data = { + "steps": self._stored_steps, + "done_steps": list(self._done_set), + } + + # 实时推送到前端 todo 浮窗(共享 store,无需 import server) + try: + from agent.history.tool_call_recorder import get_current_session + sid = get_current_session() + if sid: + set_todo_store_data(sid, todo_data) + except Exception: + pass + + if not self._stored_steps: + return json.dumps({"__todo_list__": True, "data": todo_data}, ensure_ascii=False) + "\n[Todo List]\n(当前无任务)" + + lines = ["[Todo List]"] + for i, s in enumerate(self._stored_steps): + status = "[Done]" if i in self._done_set else "[Pending]" + lines.append(f"- [{i+1}/{len(self._stored_steps)}] {status}: {s}") + return json.dumps({"__todo_list__": True, "data": todo_data}, ensure_ascii=False) + "\n" + "\n".join(lines) + + async def _arun(self, steps: list[str] | None = None, done_step: int = -1) -> str: + return self._run(steps, done_step) diff --git a/src/agent/tools/tool_database.py b/src/agent/tools/tool_database.py deleted file mode 100644 index 947c95f..0000000 --- a/src/agent/tools/tool_database.py +++ /dev/null @@ -1,118 +0,0 @@ -from langchain.tools import BaseTool -from pydantic import Field, create_model, BaseModel -from utils.database_utils import PostgreSQL_Manager -import json -from typing import Optional - -class ListTableComment(BaseTool): - - name: str = "list_table_comment" - description: str = "一个列出数据库中所有表名和对应描述的工具,可以根据用户的查询返回相关的表名和描述列表。" - db_manager : PostgreSQL_Manager - - def _run(self, query: str) -> str: - # 这里可以实现具体的列出数据库表的逻辑,例如调用数据库管理类的方法 - # 下面是一个示例实现,实际使用时请替换为你自己的数据库查询逻辑 - try: - result_json = self.db_manager.get_table_comments() - results = json.loads(result_json) - if not isinstance(results, dict): - raise ValueError("get_table_comments did not return a JSON object") - result = f"数据库中共有 {len(results)} 张表" - for i, (table_name, comment) in enumerate(results.items()): - if not comment or comment.isspace(): - comment = "(暂无描述)" - result += f"\n{i+1}. {table_name}: {comment}" - return result - except Exception as e: - return f"执行列出数据库表和描述时出错: {str(e)}" - - async def _arun(self, query: str) -> str: - return self._run(query) - -class ListTableSchema(BaseTool): - - name: str = "list_table_schema" - description: str = "一个列出数据库中表名对应模式的工具,可以根据用户的查询返回相关的表名和模式信息,包括列定义、主键、外键等。输入应为逗号分隔的表名列表。" - db_manager: PostgreSQL_Manager - args_schema: type[BaseModel] = create_model("Input_type", table_names=(Optional[str], Field(description="要查询的表名列表,如果为None则返回所有表的模式信息,例如:'table1', 'table2'"))) - - def _run(self, table_names : Optional[str] = None) -> str: - try: - if table_names: - table_list = [name.strip() for name in table_names.split(",")] - result_json = self.db_manager.get_table_schema(table_list) - results = json.loads(result_json) - if not isinstance(results, dict): - raise ValueError("get_table_schema did not return a JSON object") - return json.dumps(results, ensure_ascii=False, indent=2) - except Exception as e: - return f"执行列出数据库表和模式时出错: {str(e)}" - - async def _arun(self, table_names : Optional[str] = None) -> str: - return self._run(table_names) - -class SQLQuery(BaseTool): - - name: str = "sql_query" - description: str = "在PostgreSQL数据库中执行安全的SELECT或WITH查询并返回结果。" - db_manager: PostgreSQL_Manager - args_schema: type[BaseModel] = create_model("Input_type", query=(str, Field(description="要执行的SQL SELECT查询语句"))) - - def _run(self, query: str) -> str: - try: - result_json = self.db_manager.execute_query(query) - results = json.loads(result_json) - if isinstance(results, list): - # 成功返回数据行 - return json.dumps(results, ensure_ascii=False, indent=2) - elif isinstance(results, dict): - if "error" in results: - # 返回错误信息 - return f"查询执行失败: {results['error']}" - elif "message" in results: - # 查询成功但无数据返回 - return results["message"] - else: - # 其他字典格式的结果 - return json.dumps(results, ensure_ascii=False, indent=2) - else: - raise ValueError(f"execute_query 返回了意外的数据类型: {type(results)}") - except Exception as e: - return f"执行SQL查询时出错: {str(e)}" - - async def _arun(self, query: str) -> str: - return self._run(query) - -class SQLQueryChecker(BaseTool): - - name: str = "sql_query_checker" - description: str = "一个检查SQL查询语句是否安全的工具,主要用于防止SQL注入攻击。输入为SQL查询语句,输出为检查结果。" - args_schema: type[BaseModel] = create_model("Input_type", query=(str, Field(description="要检查的SQL查询语句"))) - db_manager: PostgreSQL_Manager - - def _run(self, query: str) -> str: - try: - result_json = self.db_manager.validate_query(query) - result = json.loads(result_json) - if not isinstance(result, dict): - raise ValueError("validate_query did not return a JSON object") - return json.dumps(result, ensure_ascii=False, indent=2) - except Exception as e: - return f"执行SQL查询安全检查时出错: {str(e)}" - async def _arun(self, query: str) -> str: - return self._run(query) - -if __name__ == "__main__": - manager = PostgreSQL_Manager("postgresql+psycopg2://postgres:12345678@localhost:5432/agent_controller") - list_table_tool = ListTableComment(db_manager=manager) - print(list_table_tool.run("列出数据库中的所有表和描述")) - # list_table_tool = ListTableSchema(db_manager=manager) - # print(list_table_tool.run({"table_names": []})) - # sql_query = SQLQuery(db_manager=manager) - # print(sql_query.run("SELECT * FROM tasks;")) - # sql_query_checker = SQLQueryChecker(db_manager=manager) - # print("Valid query:") - # print(sql_query_checker.run("SELECT * FROM tasks;")) - # print("\nInvalid query:") - # print(sql_query_checker.run("DROP TABLE tasks;")) diff --git a/src/agent/tools/tool_software_control.py b/src/agent/tools/tool_software_control.py index 8f33d63..54596a1 100644 --- a/src/agent/tools/tool_software_control.py +++ b/src/agent/tools/tool_software_control.py @@ -404,7 +404,6 @@ class SoftwareControl(BaseTool): action=(str, Field(default="open", description="操作类型:open 表示打开软件,close 表示正常关闭软件,force_close 表示强制关闭软件。")), software_name=(str, Field(description="要操作的软件名称,例如微信、Visual Studio Code、MATLAB。")), rescan=(bool, Field(default=False, description="是否强制重新扫描本机软件并刷新索引。")), - force=(bool, Field(default=False, description="兼容旧参数。当前仅当 action=force_close 时才会真正触发强制关闭。")), ) name: str = "software_control" description: str = ( @@ -413,7 +412,7 @@ class SoftwareControl(BaseTool): "action=force_close 时才允许在正常关闭失败后强制结束进程。仅当用户明确要求打开、关闭或强制关闭某个本机软件时调用。" ) - def _run(self, action: str = "open", software_name: str = "", rescan: bool = False, force: bool = False) -> str: + def _run(self, action: str = "open", software_name: str = "", rescan: bool = False) -> str: if os.name != "nt": return "software_control 目前仅支持 Windows 本机软件操作。" query = (software_name or "").strip() @@ -421,11 +420,8 @@ class SoftwareControl(BaseTool): return "软件名称不能为空。" action = (action or "open").strip().lower() rescan = _coerce_bool(rescan) - force = _coerce_bool(force) if action not in {"open", "close", "force_close"}: return "action 仅支持 open、close 或 force_close。" - if force and action == "close": - action = "force_close" force_requested = action == "force_close" records = _build_index() if rescan else _load_index() diff --git a/src/agent/utils/__pycache__/env_utils.cpython-312.pyc b/src/agent/utils/__pycache__/env_utils.cpython-312.pyc index dcd0fef..f404c4f 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/__pycache__/tool_call_utils.cpython-312.pyc b/src/agent/utils/__pycache__/tool_call_utils.cpython-312.pyc index abab379..10b9ada 100644 Binary files a/src/agent/utils/__pycache__/tool_call_utils.cpython-312.pyc and b/src/agent/utils/__pycache__/tool_call_utils.cpython-312.pyc differ diff --git a/src/agent/utils/tool_call_utils.py b/src/agent/utils/tool_call_utils.py index 21f2dfc..0116ba9 100644 --- a/src/agent/utils/tool_call_utils.py +++ b/src/agent/utils/tool_call_utils.py @@ -86,8 +86,7 @@ def parse_edited_instruction(instruction: str, fallback: dict[str, Any]) -> dict """解析用户在「编辑」确认流程中提交的指令文本。 功能描述: - 优先按 JSON 解析完整工具调用;失败则将原文本写入 ``args.instruction``; - 兼容 ``tool_name`` 别名字段。 + 优先按 JSON 解析完整工具调用;失败则将原文本写入 ``args.instruction``。 输入: instruction: 用户编辑后的文本(可为 JSON 或纯文本)。 @@ -113,8 +112,6 @@ def parse_edited_instruction(instruction: str, fallback: dict[str, Any]) -> dict updated["args"] = {"instruction": raw} return updated if isinstance(parsed, dict): - if "name" not in parsed and "tool_name" in parsed: - parsed["name"] = parsed.get("tool_name") if "args" not in parsed: parsed["args"] = fallback.get("args") or {} return parsed diff --git a/src/agent/workspace/.todo b/src/agent/workspace/.todo deleted file mode 100644 index 9e86f67..0000000 --- a/src/agent/workspace/.todo +++ /dev/null @@ -1,53 +0,0 @@ -# Plan TodoList - -路径选择:路径一:使用浏览器控制 - -# 执行计划:读取 Bilibili 首页第一个视频标题并分析 - -**说明**:本计划旨在通过浏览器自动化控制访问 Bilibili 首页,提取首个视频标题,并提供基于当前网络热点和一般认知的理解。由于我无法直接“看到”实时变化的互联网内容,以下流程将指导如何获取最新数据并进行分析。在实际执行中,我会根据获取到的具体标题进行针对性解读。 - -## Todo List - -- [x] **步骤 1:环境准备与导航** · 状态:进行中 · 状态:已完成 - - **依赖**:无 - - **状态**:待开始 - - **任务描述**: - 1. 启动内置浏览器或模拟浏览器会话。 - 2. 导航至 Bilibili 首页 URL (`https://www.bilibili.com`)。 - 3. 等待页面完全加载,确保动态内容(如推荐流)已渲染完毕。 - -- [x] **步骤 2:定位首个视频元素** · 状态:进行中 · 状态:已完成 - - **依赖**:步骤 1 完成 - - **状态**:待开始 - - **任务描述**: - 1. 识别首页的主要视频推荐区域(通常是“推荐”标签页下的第一个卡片)。 - 2. 定位包含视频标题的 DOM 元素(通常位于视频封面下方或悬浮层中)。 - 3. *注意*:需排除广告位或非视频内容(如直播入口、专栏文章),确保选取的是标准的 UP 主投稿视频。 - -- [x] **步骤 3:提取视频标题文本** · 状态:进行中 · 状态:已完成 - - **依赖**:步骤 2 完成 - - **状态**:待开始 - - **任务描述**: - 1. 从定位到的元素中提取纯文本标题。 - 2. 清理多余的空白字符或隐藏的控制符。 - 3. 记录提取结果,若提取失败则重试或尝试备用选择器。 - -- [x] **步骤 4:信息分析与理解生成** · 状态:进行中 · 状态:已完成 - - **依赖**:步骤 3 完成 - - **状态**:待开始 - - **任务描述**: - 1. 基于提取的标题,结合当前的社会热点、B站社区文化及常见视频类型进行分析。 - 2. 判断视频可能的主题(如科技评测、生活 vlog、游戏实况、知识科普等)。 - 3. 评估标题是否具有吸引力(如是否使用悬念、热点词汇、情绪化表达)。 - 4. 给出简短的个性化理解和建议观看理由。 - -- [x] **步骤 5:输出最终报告** · 状态:进行中 · 状态:已完成 - - **依赖**:步骤 4 完成 - - **状态**:待开始 - - **任务描述**: - 1. 整合提取的标题和分析结果。 - 2. 以清晰的结构呈现给用户,包括“原始标题”、“内容推测”和“我的理解”。 - ---- - -**备注**:在执行过程中,我将根据实际抓取到的标题实时更新后续的分析内容。如果首页因登录状态或算法推荐显示不同内容,我将基于当时可见的第一个视频进行处理。 diff --git a/src/web/__pycache__/server.cpython-312.pyc b/src/web/__pycache__/server.cpython-312.pyc index 8fd28a5..e470113 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 index 5706e41..2f29706 100644 Binary files a/src/web/__pycache__/server.cpython-313.pyc and b/src/web/__pycache__/server.cpython-313.pyc differ diff --git a/src/web/__pycache__/ui_session.cpython-312.pyc b/src/web/__pycache__/ui_session.cpython-312.pyc index 5aa0596..9294505 100644 Binary files a/src/web/__pycache__/ui_session.cpython-312.pyc and b/src/web/__pycache__/ui_session.cpython-312.pyc differ diff --git a/src/web/app.css b/src/web/app.css index dc58921..6ba5f26 100644 --- a/src/web/app.css +++ b/src/web/app.css @@ -487,85 +487,6 @@ body { background: var(--bubble-assistant); } -.pending-action-card { - margin-top: 0.65rem; - padding: 0.7rem; - border-radius: 12px; - border: 1px solid rgba(14, 116, 144, 0.22); - background: rgba(236, 254, 255, 0.28); - color: rgba(51, 65, 85, 0.82); - font-size: 0.78rem; -} - -.pending-action-card--resolved { - opacity: 0.58; - pointer-events: none; -} - -.pending-action-title { - font-weight: 700; - color: rgba(15, 23, 42, 0.72); - margin-bottom: 0.35rem; -} - -.pending-action-params { - margin: 0 0 0.45rem; - padding: 0.5rem; - max-height: 160px; - overflow: auto; - border-radius: 10px; - background: rgba(255, 255, 255, 0.32); - color: rgba(30, 41, 59, 0.72); - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-size: 0.72rem; - white-space: pre-wrap; -} - -.pending-action-editor { - width: 100%; - resize: vertical; - border: 1px solid rgba(148, 163, 184, 0.42); - border-radius: 10px; - padding: 0.45rem 0.5rem; - background: rgba(255, 255, 255, 0.36); - color: rgba(15, 23, 42, 0.78); - font: inherit; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-size: 0.72rem; -} - -.pending-action-buttons { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; - margin-top: 0.5rem; -} - -.pending-action-btn { - border: 1px solid rgba(148, 163, 184, 0.45); - border-radius: 999px; - padding: 0.32rem 0.62rem; - background: rgba(255, 255, 255, 0.42); - color: #334155; - cursor: pointer; - font: inherit; - font-size: 0.75rem; -} - -.pending-action-btn:hover:not(:disabled) { - background: rgba(255, 255, 255, 0.7); -} - -.pending-action-btn--approve { - border-color: rgba(20, 184, 166, 0.45); - color: #0f766e; -} - -.pending-action-btn--reject { - border-color: rgba(248, 113, 113, 0.45); - color: #b91c1c; -} - /* 图片在气泡内留出呼吸空间 */ .msg-bubble img { display: block; @@ -2193,18 +2114,7 @@ body { to { opacity: 1; transform: translateY(0); } } -/* ===== 图谱切换按钮 ===== */ -.graph-toggle-btn { - margin-left: 0.15rem; -} - -.graph-toggle-btn.graph-active { - background: rgba(14, 116, 144, 0.18); - border-color: rgba(14, 116, 144, 0.5); - color: #0e7490; -} - -/* ===== 主内容区(chat + graph 并排) ===== */ +/* ===== 主内容区 ===== */ .main-content-wrap { flex: 1; min-height: 0; @@ -2213,329 +2123,6 @@ body { overflow: hidden; } -/* 图谱开启时聊天面板缩窄,圆角只保留左侧 */ -.main.has-graph .chat-shell { - flex: 1 1 58%; - min-width: 320px; - border-radius: var(--radius) 0 0 var(--radius); -} - -/* ===== 图谱侧面板 ===== */ -.graph-panel { - display: flex; - flex-direction: column; - width: 42%; - min-width: 340px; - max-width: 520px; - border-left: 1px solid rgba(148, 163, 184, 0.35); - border-radius: 0 var(--radius) var(--radius) 0; - background: rgba(255, 255, 255, 0.25); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - overflow: hidden; - box-shadow: -4px 0 24px rgba(15, 23, 42, 0.06); - transition: width 0.3s ease, min-width 0.3s ease, max-width 0.3s ease, opacity 0.25s ease; -} - -.graph-panel[hidden] { - display: none; -} - -.graph-toolbar { - flex-shrink: 0; - display: flex; - align-items: center; - gap: 0.4rem; - padding: 0.45rem 0.65rem; - border-bottom: 1px solid rgba(148, 163, 184, 0.22); - background: rgba(255, 255, 255, 0.15); -} - -.graph-toolbar-title { - font-size: 0.82rem; - font-weight: 700; - color: #1e293b; - margin-right: 0.25rem; -} - -.graph-btn { - font-size: 0.7rem; - padding: 0.2rem 0.45rem; - border-radius: 6px; -} - -.graph-legend { - margin-left: auto; - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.65rem; - color: #64748b; -} - -.graph-close-btn { - width: 1.65rem; - height: 1.65rem; - padding: 0; - border: 1px solid rgba(148, 163, 184, 0.4); - border-radius: 6px; - background: rgba(255, 255, 255, 0.4); - color: #64748b; - font-size: 0.75rem; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.graph-close-btn:hover { - background: rgba(255, 255, 255, 0.65); - color: #1e293b; -} - -.legend-dot { - display: inline-block; - width: 10px; - height: 10px; - border-radius: 50%; - margin-right: 0.1rem; -} - -.legend-running { - background: #f59e0b; - box-shadow: 0 0 6px rgba(245, 158, 11, 0.5); - animation: legendPulse 1.2s ease-in-out infinite; -} - -.legend-done { - background: #22c55e; -} - -.legend-error { - background: #ef4444; -} - -.legend-idle { - background: #94a3b8; -} - -@keyframes legendPulse { - 0%, 100% { opacity: 0.7; box-shadow: 0 0 4px rgba(245, 158, 11, 0.3); } - 50% { opacity: 1; box-shadow: 0 0 10px rgba(245, 158, 11, 0.7); } -} - -/* 图谱画布 */ -.graph-canvas-wrap { - flex: 1; - min-height: 0; - overflow: hidden; - position: relative; - cursor: grab; - background: - radial-gradient(circle, rgba(148, 163, 184, 0.06) 1px, transparent 1px); - background-size: 24px 24px; -} - -.graph-canvas-wrap:active { - cursor: grabbing; -} - -.graph-canvas-wrap.panning { - cursor: grabbing; -} - -.graph-canvas { - width: 100%; - height: 100%; - display: block; - user-select: none; -} - -/* Agent 节点 */ -.graph-node { - cursor: pointer; - transition: transform 0.2s ease; -} - -.graph-node:hover .graph-node-rect { - filter: brightness(1.05); -} - -.graph-node-rect { - fill: rgba(255, 255, 255, 0.75); - stroke: rgba(148, 163, 184, 0.5); - stroke-width: 1.5; - rx: 16; - transition: fill 0.35s ease, stroke 0.35s ease, filter 0.35s ease; -} - -/* 状态颜色 */ -.graph-node.status-running .graph-node-rect { - fill: rgba(255, 200, 120, 0.45); - stroke: #f59e0b; - stroke-width: 2.5; - filter: drop-shadow(0 0 8px rgba(245, 158, 11, 0.35)); -} - -.graph-node.status-done .graph-node-rect { - fill: rgba(187, 247, 208, 0.45); - stroke: #22c55e; - stroke-width: 2; -} - -.graph-node.status-error .graph-node-rect { - fill: rgba(254, 202, 202, 0.5); - stroke: #ef4444; - stroke-width: 2.5; - filter: drop-shadow(0 0 6px rgba(239, 68, 68, 0.3)); -} - -.graph-node.status-idle .graph-node-rect { - fill: rgba(255, 255, 255, 0.5); - stroke: rgba(148, 163, 184, 0.4); -} - -/* Agent 节点内部文字 */ -.graph-node-icon { - font-size: 1.4rem; - text-anchor: middle; - dominant-baseline: central; - pointer-events: none; -} - -.graph-node-label { - font-size: 0.75rem; - font-weight: 600; - fill: #1e293b; - text-anchor: middle; - dominant-baseline: central; - pointer-events: none; -} - -.graph-node-role { - font-size: 0.6rem; - fill: #94a3b8; - text-anchor: middle; - dominant-baseline: central; - pointer-events: none; -} - -.graph-node-badge { - font-size: 0.6rem; - font-weight: 700; - text-anchor: middle; - dominant-baseline: central; - pointer-events: none; -} - -/* 连线 */ -.graph-edge { - transition: stroke 0.4s ease, stroke-width 0.4s ease, opacity 0.4s ease; -} - -.graph-edge.highlighted { - stroke: #f59e0b; - stroke-width: 2.5; - opacity: 1; - stroke-dasharray: none; -} - -.graph-edge.active { - stroke: #14b8a6; - stroke-width: 2; - opacity: 1; - stroke-dasharray: 6 3; - animation: edgeFlow 0.6s linear infinite; -} - -.graph-edge.normal { - stroke: rgba(148, 163, 184, 0.4); - stroke-width: 1.5; - opacity: 0.7; -} - -@keyframes edgeFlow { - from { stroke-dashoffset: 18; } - to { stroke-dashoffset: 0; } -} - -/* 工具调用面板 */ -/* 工具调用卡片 */ -.toolcall-card { - padding: 0.4rem 0.55rem; - border-radius: 10px; - border: 1px solid rgba(148, 163, 184, 0.3); - background: rgba(255, 255, 255, 0.5); - font-size: 0.72rem; -} - -.toolcall-card-head { - display: flex; - align-items: center; - gap: 0.4rem; - margin-bottom: 0.2rem; -} - -.toolcall-card-name { - font-weight: 700; - color: #1e293b; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; -} - -.toolcall-card-status { - font-size: 0.62rem; - padding: 0.1rem 0.35rem; - border-radius: 999px; - font-weight: 600; -} - -.toolcall-card-status.success { - background: rgba(34, 197, 94, 0.15); - color: #15803d; -} - -.toolcall-card-status.error { - background: rgba(239, 68, 68, 0.15); - color: #b91c1c; -} - -.toolcall-card-status.pending { - background: rgba(245, 158, 11, 0.15); - color: #b45309; -} - -.toolcall-card-args { - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-size: 0.65rem; - color: #64748b; - white-space: pre-wrap; -} - -.toolcall-card-result { - margin-top: 0.25rem; - padding: 0.3rem 0.4rem; - border-radius: 6px; - background: rgba(241, 245, 249, 0.7); - font-size: 0.65rem; - color: #475569; - max-height: 48px; - overflow: hidden; - text-overflow: ellipsis; - white-space: pre-wrap; - word-break: break-all; -} - -/* 消息传递动画脉冲 */ -@keyframes msgPulse { - 0%, 100% { opacity: 0.4; } - 50% { opacity: 1; } -} - -.graph-edge.active { - animation: edgeFlow 0.6s linear infinite, msgPulse 0.8s ease-in-out infinite; -} - /* ===== 工具调用气泡(chat 视图) ===== */ .tool-call-bubble-wrap { margin-top: 0.5rem; @@ -2617,6 +2204,15 @@ body { border: 1px solid rgba(148, 163, 184, 0.2); } +.tool-call-detail--running { + border-color: rgba(245, 158, 11, 0.4); + background: rgba(245, 158, 11, 0.05); +} + +.tool-call-detail--running .tool-call-detail-name { + color: #d97706; +} + .tool-call-detail-name { font-weight: 700; font-size: 0.72rem; @@ -2643,37 +2239,43 @@ body { background: rgba(148, 163, 184, 0.08); } -.tool-call-detail-img { - border-radius: 6px; - margin: 0.2rem; - max-width: 120px; - max-height: 90px; - object-fit: cover; - opacity: 0.6; - transition: opacity 0.3s; +.tool-call-detail-media { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + margin-top: 0.35rem; } -.tool-call-detail-img:hover { - opacity: 0.9; +.tool-call-detail .tool-call-detail-image { + display: block; + max-width: min(220px, 100%); + max-height: 180px; + object-fit: contain; + border-radius: 8px; + border: 1px solid rgba(148, 163, 184, 0.28); + background: rgba(255, 255, 255, 0.45); + cursor: zoom-in; + margin: 0; } -/* 淡出动画 */ -.tool-call-block--fading { - transition: opacity 0.8s ease-out; - opacity: 0 !important; - pointer-events: none; +.tool-call-detail .tool-call-detail-image:hover { + opacity: 0.92; } /* ===== 实时工具调用(thinking 气泡内) ===== */ .tool-call-live-wrap { - display: flex; - flex-wrap: wrap; - gap: 0.3rem; - margin-top: 0.5rem; - padding: 0.4rem 0.5rem; + display: block; + width: min(560px, 100%); + margin-top: 0.6rem; + padding-top: 0.55rem; border-top: 1px solid rgba(148, 163, 184, 0.2); } +.tool-call-live-wrap .tool-call-bubble-wrap { + margin-top: 0; + width: 100%; +} + .tool-call-live-chip { display: inline-flex; align-items: center; @@ -2699,105 +2301,117 @@ body { 50% { opacity: 1; } } -/* 任务完成提示 */ -.tool-call-done-badge { - display: inline-block; - padding: 0.2rem 0.5rem; - border-radius: 999px; - background: rgba(34, 197, 94, 0.15); - color: #15803d; - font-size: 0.68rem; - font-weight: 600; - margin-bottom: 0.4rem; -} - -/* ===== Graph 节点工具调用缩略 ===== */ -.graph-node-tool-chip { - font-size: 0.55rem; - font-family: ui-monospace, SFMono-Regular, Consolas, monospace; - fill: #6366f1; - font-weight: 600; - text-anchor: start; -} - -/* ===== Agent 详情弹窗 ===== */ -.agent-detail-overlay { +.todo-list-overlay { position: fixed; - inset: 0; - z-index: 999; - background: rgba(15, 23, 42, 0.55); + top: 80px; + left: 50%; + transform: translateX(-50%); + z-index: 900; + min-width: 280px; + max-width: 420px; + width: max-content; + background: rgba(255, 255, 255, 0.82); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); + border: 1px solid rgba(148, 163, 184, 0.2); + overflow: hidden; + transition: all 0.3s ease; +} + +.todo-list-header { + padding: 0.6rem 1rem; + font-size: 0.85rem; + font-weight: 700; + color: #334155; + cursor: pointer; + user-select: none; + display: flex; + align-items: center; + gap: 0.4rem; + border-bottom: 1px solid rgba(148, 163, 184, 0.15); +} + +.todo-list-header:hover { + background: rgba(99, 102, 241, 0.06); +} + +.todo-list-body { + list-style: none; + margin: 0; + padding: 0.5rem 0; + max-height: 300px; + overflow-y: auto; +} + +.todo-list-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 1rem; + font-size: 0.8rem; + color: #475569; + border-bottom: 1px solid rgba(148, 163, 184, 0.08); +} +.todo-list-item:last-child { + border-bottom: none; +} + +.todo-label { + flex: 1; + line-height: 1.4; +} + +/* ----- 勾已完成 ----- */ +.todo-check { + flex-shrink: 0; + width: 20px; + height: 20px; + border-radius: 50%; + background: #22c55e; + color: #fff; display: flex; align-items: center; justify-content: center; + font-size: 0.7rem; + font-weight: 700; + line-height: 1; } -.agent-detail-overlay[hidden] { +/* ----- 加载旋转动画(pending) ----- */ +.todo-spinner { + flex-shrink: 0; + width: 20px; + height: 20px; + border-radius: 50%; + border: 2.5px solid rgba(99, 102, 241, 0.2); + border-top-color: #6366f1; + animation: todo-spin 0.8s linear infinite; +} + +@keyframes todo-spin { + to { transform: rotate(360deg); } +} + +/* ----- 折叠态 ----- */ +.todo-list-overlay--collapsed .todo-list-body { display: none; } - -.agent-detail-dialog { - background: rgba(255, 255, 255, 0.92); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 16px; - width: min(520px, 92vw); - max-height: 80vh; - overflow: hidden; - display: flex; - flex-direction: column; - box-shadow: 0 20px 60px rgba(0,0,0,0.2); +.todo-list-overlay--collapsed { + min-width: 180px; + max-width: 240px; } -.agent-detail-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1rem 1.25rem; - border-bottom: 1px solid rgba(148, 163, 184, 0.25); -} - -.agent-detail-header h2 { - margin: 0; - font-size: 1.1rem; - font-weight: 700; - color: #1e293b; -} - -.agent-detail-close { - background: none; - border: none; - font-size: 1.5rem; - cursor: pointer; - color: #64748b; - line-height: 1; - padding: 0.2rem; -} - -.agent-detail-body { - padding: 1rem 1.25rem; - overflow-y: auto; - flex: 1; -} - -.agent-detail-section { - margin-bottom: 1.2rem; -} - -.agent-detail-section h3 { - margin: 0 0 0.5rem; - font-size: 0.85rem; - font-weight: 700; - color: #475569; - text-transform: uppercase; - letter-spacing: 0.03em; -} - -.agent-detail-section code { - padding: 0.1rem 0.3rem; - border-radius: 4px; - background: rgba(79, 70, 229, 0.08); - font-size: 0.75rem; - color: #4f46e5; +/* ===== 响应式 ===== */ +@media (max-width: 640px) { + .todo-list-overlay { + left: 1rem; + right: 1rem; + transform: none; + min-width: unset; + max-width: unset; + } } diff --git a/src/web/app.js b/src/web/app.js index 346c69d..c16b10e 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -1094,206 +1094,174 @@ } } - // ===== 工具调用展示(chat 视图) ===== - let toolCallDataCache = { recent: [], detail: [], complete: false }; - let toolCallTimer = null; + // ===== Todo List 悬浮窗 ===== + let _currentTodoOverlay = null; - async function fetchToolCallData() { - if (!sessionId) return; - try { - const [recentRes, detailRes, pendingRes] = await Promise.all([ - api(`/api/tool-calls/${encodeURIComponent(sessionId)}/recent`), - api(`/api/tool-calls/${encodeURIComponent(sessionId)}/detail`), - api(`/api/tool-calls/${encodeURIComponent(sessionId)}/has-pending`), - ]); - toolCallDataCache = { - recent: recentRes.tool_calls || [], - detail: detailRes.details || [], - complete: pendingRes.is_complete, - }; - } catch { - /* ignore */ + function buildTodoListOverlay(todoData) { + // 移除旧的 overlay + if (_currentTodoOverlay && _currentTodoOverlay.parentNode) { + _currentTodoOverlay.parentNode.removeChild(_currentTodoOverlay); } - } - - function startToolCallPolling() { - fetchToolCallData(); - toolCallTimer = setInterval(fetchToolCallData, 2000); - } - - function stopToolCallPolling(keepDisplay = false) { - if (toolCallTimer) { - clearInterval(toolCallTimer); - toolCallTimer = null; + if (!todoData || !todoData.steps || !todoData.steps.length) { + _currentTodoOverlay = null; + return null; } - if (!keepDisplay) { - // 清除过期工具调用显示(渐变消失) - document.querySelectorAll(".tool-call-block--fading").forEach((el) => { - el.addEventListener("transitionend", () => el.remove()); - el.style.opacity = "0"; - }); - } - } - function renderToolCallBlock(call, index, total, isComplete) { - const block = document.createElement("div"); - block.className = "tool-call-block" + (isComplete ? " tool-call-block--fading" : ""); + const overlay = document.createElement("div"); + overlay.className = "todo-list-overlay"; + // 头部 const header = document.createElement("div"); - header.className = "tool-call-header"; - const name = document.createElement("span"); - name.className = "tool-call-name"; - name.textContent = `🔧 ${escapeHtml(call.name || "")}`; - const args = document.createElement("span"); - args.className = "tool-call-args"; - args.textContent = call.args_summary || ""; - const status = document.createElement("span"); - status.className = "tool-call-status"; - status.textContent = call.status === "done" ? "✓" : "…"; - header.appendChild(name); - header.appendChild(args); - header.appendChild(status); - block.appendChild(header); + header.className = "todo-list-header"; + header.textContent = "📋 Todo List"; + header.addEventListener("click", () => { + overlay.classList.toggle("todo-list-overlay--collapsed"); + }); + overlay.appendChild(header); - return block; + // 列表 + const list = document.createElement("ul"); + list.className = "todo-list-body"; + todoData.steps.forEach((s, i) => { + const item = document.createElement("li"); + item.className = "todo-list-item"; + const statusIcon = todoData.done_steps && todoData.done_steps.includes(i) + ? '✓' + : ''; + item.innerHTML = `${statusIcon}${escapeHtml(s)}`; + list.appendChild(item); + }); + overlay.appendChild(list); + + _currentTodoOverlay = overlay; + return overlay; } - function renderToolCallDetail(call) { - const detail = document.createElement("div"); - detail.className = "tool-call-detail"; + function updateTodoOverlay(todoData) { + const overlay = buildTodoListOverlay(todoData); + // 找到 chat area 的容器,插入或移除 overlay + const container = chatMessages.closest(".chat-area") || chatMessages.parentElement; + const existing = container.querySelector(".todo-list-overlay"); + if (existing) existing.remove(); + if (overlay) container.appendChild(overlay); + } - const name = document.createElement("div"); - name.className = "tool-call-detail-name"; - name.textContent = `🔧 ${escapeHtml(call.name || "unknown")}`; - detail.appendChild(name); + function clearTodoOverlay() { + if (_currentTodoOverlay && _currentTodoOverlay.parentNode) { + _currentTodoOverlay.parentNode.removeChild(_currentTodoOverlay); + } + _currentTodoOverlay = null; + document.querySelectorAll(".todo-list-overlay").forEach((el) => el.remove()); + } + + // ===== 工具调用展示(chat 视图) ===== + let liveToolCallExpanded = false; + + function injectLiveToolCalls(typingEl, liveCalls) { + if (!typingEl || !liveCalls.length) return; + const bubble = typingEl.querySelector(".msg-bubble") || typingEl; + let liveWrap = bubble.querySelector(".tool-call-live-wrap"); + if (!liveWrap) { + liveWrap = document.createElement("div"); + liveWrap.className = "tool-call-live-wrap"; + bubble.appendChild(liveWrap); + } + + const total = liveCalls.length; + let wrap = liveWrap.querySelector(".tool-call-bubble-wrap"); + const isNewPanel = !wrap; + if (!wrap) { + wrap = document.createElement("div"); + liveWrap.appendChild(wrap); + } + + if (isNewPanel) liveToolCallExpanded = true; + liveToolCallExpanded = liveToolCallExpanded || wrap.classList.contains("tool-call-bubble-wrap--expanded"); + wrap.className = "tool-call-bubble-wrap" + (liveToolCallExpanded ? " tool-call-bubble-wrap--expanded" : ""); + wrap.innerHTML = ""; + + const runningCount = liveCalls.filter((call) => call.status === "running").length; + const header = document.createElement("button"); + header.type = "button"; + header.className = "tool-call-toggle"; + header.textContent = `🔧 工具调用 (${total}${runningCount ? `,执行中 ${runningCount}` : ""}) ${liveToolCallExpanded ? "▾" : "▸"}`; + header.addEventListener("click", () => { + liveToolCallExpanded = !liveToolCallExpanded; + wrap.classList.toggle("tool-call-bubble-wrap--expanded", liveToolCallExpanded); + header.textContent = `🔧 工具调用 (${total}${runningCount ? `,执行中 ${runningCount}` : ""}) ${liveToolCallExpanded ? "▾" : "▸"}`; + }); + wrap.appendChild(header); + + const collapsed = document.createElement("div"); + collapsed.className = "tool-call-collapsed"; + liveCalls.slice(-Math.min(2, total)).forEach((call) => { + const chip = document.createElement("span"); + chip.className = "tool-call-chip" + (call.status === "running" ? " tool-call-live-chip--running" : ""); + chip.textContent = call.name || "unknown"; + collapsed.appendChild(chip); + }); + if (total > 2) { + const more = document.createElement("span"); + more.className = "tool-call-more"; + more.textContent = `+${total - 2}`; + more.addEventListener("click", () => { + liveToolCallExpanded = true; + injectLiveToolCalls(typingEl, liveCalls); + }); + collapsed.appendChild(more); + } + wrap.appendChild(collapsed); + + const expandedArea = document.createElement("div"); + expandedArea.className = "tool-call-expanded"; + liveCalls.forEach((call) => { + expandedArea.appendChild(renderLiveToolCallDetail(call)); + }); + wrap.appendChild(expandedArea); + + scrollChatToBottom(); + } + + function renderLiveToolCallDetail(call) { + const card = document.createElement("div"); + card.className = "tool-call-detail" + (call.status === "running" ? " tool-call-detail--running" : ""); + + const nameEl = document.createElement("div"); + nameEl.className = "tool-call-detail-name"; + const statusIcon = call.status === "running" ? "⌛" : "✓"; + nameEl.textContent = `${statusIcon} ${escapeHtml(call.name || "unknown")}`; + card.appendChild(nameEl); if (call.args && Object.keys(call.args).length > 0) { const argsEl = document.createElement("pre"); argsEl.className = "tool-call-detail-text"; argsEl.textContent = JSON.stringify(call.args, null, 2); - detail.appendChild(argsEl); + card.appendChild(argsEl); } if (call.result_text) { const resultEl = document.createElement("pre"); resultEl.className = "tool-call-detail-text tool-call-detail-result"; resultEl.textContent = call.result_text; - detail.appendChild(resultEl); + card.appendChild(resultEl); } - if (call.result_images && call.result_images.length > 0) { - call.result_images.forEach((imgUrl) => { + if (Array.isArray(call.result_images) && call.result_images.length > 0) { + const mediaWrap = document.createElement("div"); + mediaWrap.className = "tool-call-detail-media"; + call.result_images.forEach((url) => { + if (!url) return; const img = document.createElement("img"); - img.src = imgUrl; - img.className = "tool-call-detail-img"; + img.className = "tool-call-detail-image"; + img.src = url; img.alt = "工具返回图片"; - img.style.maxWidth = "160px"; - img.style.opacity = "0.5"; - detail.appendChild(img); + img.addEventListener("click", () => window.open(url, "_blank", "noopener")); + mediaWrap.appendChild(img); }); + card.appendChild(mediaWrap); } - return detail; - } - - function injectLiveToolCalls(typingEl, liveCalls) { - if (!typingEl || !liveCalls.length) return; - // 查找或创建工具调用容器 - let liveWrap = typingEl.querySelector(".tool-call-live-wrap"); - if (!liveWrap) { - liveWrap = document.createElement("div"); - liveWrap.className = "tool-call-live-wrap"; - typingEl.appendChild(liveWrap); - } - // 更新显示 - liveWrap.innerHTML = ""; - liveCalls.forEach((call, i) => { - const chip = document.createElement("span"); - chip.className = "tool-call-live-chip"; - const statusIcon = call.status === "running" ? "⏳" : "✓"; - chip.textContent = `${statusIcon} ${escapeHtml(call.name || "")}`; - chip.style.opacity = Math.max(0.3, 1 - i * 0.15); - if (call.status === "running") chip.classList.add("tool-call-live-chip--running"); - liveWrap.appendChild(chip); - }); - scrollChatToBottom(); - } - - function buildToolCallBubble() { - const data = toolCallDataCache; - if (!data.recent.length && !data.detail.length) return null; - - const total = data.recent.length; - const visibleCount = Math.min(2, total); - const moreCount = total - visibleCount; - - const wrap = document.createElement("div"); - wrap.className = "tool-call-bubble-wrap"; - - // 折叠按钮 - const toggleBtn = document.createElement("button"); - toggleBtn.type = "button"; - toggleBtn.className = "tool-call-toggle"; - toggleBtn.textContent = `🔧 工具调用 (${total}) ▸`; - toggleBtn.addEventListener("click", () => { - const expanded = wrap.classList.toggle("tool-call-bubble-wrap--expanded"); - toggleBtn.textContent = `🔧 工具调用 (${total}) ${expanded ? "▾" : "▸"}`; - }); - - // 折叠态:只显示最近2个工具名称,渐变透明度动态消失 - const collapsed = document.createElement("div"); - collapsed.className = "tool-call-collapsed"; - data.recent.slice(-visibleCount).forEach((call, i) => { - const chip = document.createElement("span"); - chip.className = "tool-call-chip"; - chip.textContent = call.name || ""; - // 越旧的越透明 - chip.style.opacity = Math.max(0.25, 1 - (visibleCount - 1 - i) * 0.35); - collapsed.appendChild(chip); - }); - if (moreCount > 0) { - const more = document.createElement("span"); - more.className = "tool-call-more"; - more.textContent = `+${moreCount}`; - more.style.cursor = "pointer"; - more.addEventListener("click", () => { - wrap.classList.add("tool-call-bubble-wrap--expanded"); - toggleBtn.textContent = `🔧 工具调用 (${total}) ▾`; - }); - collapsed.appendChild(more); - } - - // 展开态:显示所有详情 - const expanded = document.createElement("div"); - expanded.className = "tool-call-expanded"; - data.detail.forEach((call) => { - expanded.appendChild(renderToolCallDetail(call)); - }); - - wrap.appendChild(toggleBtn); - wrap.appendChild(collapsed); - wrap.appendChild(expanded); - - return wrap; - } - - function injectToolCallDisplay(typingEl) { - if (!typingEl) return; - if (!toolCallDataCache.recent.length && !toolCallDataCache.detail.length) return; - - const existingBlock = typingEl.querySelector(".tool-call-bubble-wrap"); - if (existingBlock) { - // 更新现有显示 - const newBlock = buildToolCallBubble(); - if (newBlock) existingBlock.replaceWith(newBlock); - return; - } - - const block = buildToolCallBubble(); - if (block) { - typingEl.appendChild(block); - scrollChatToBottom(); - } + return card; } function renderMessage(msg, allowAutoplay = false) { @@ -1381,6 +1349,7 @@ const data = await api(`/api/sessions/${encodeURIComponent(id)}/messages`); sessionId = data.sessionId; sessions = data.sessions || []; + clearTodoOverlay(); renderSessions(); renderMessages(data.messages); closeDrawerIfMobile(); @@ -1392,6 +1361,7 @@ sessions = data.sessions || []; pendingFiles = []; fileInput.value = ""; + clearTodoOverlay(); renderSessions(); renderAttachmentChips(); renderMessages(data.messages); @@ -1477,8 +1447,9 @@ revokeBlobUrlStack(blobUrls); typingEl = appendThinkingIndicator(); - // 清空上轮工具调用缓存(新 turn) - toolCallDataCache = { recent: [], detail: [], complete: false }; + // 清空上轮工具调用状态(新 turn) + liveToolCallExpanded = false; + clearTodoOverlay(); // 启动实时工具调用轮询 let livePollTimer = null; @@ -1495,15 +1466,37 @@ }; pollLiveToolCalls(); + // 启动 todo_list 实时轮询 + let todoPollTimer = null; + const pollTodoList = async () => { + if (!livePollActive) return; + try { + const todoData = await api(`/api/todo-list/${encodeURIComponent(sessionId)}`); + if (todoData && todoData.has_todo && todoData.data) { + updateTodoOverlay(todoData.data); + } + } catch { /* ignore */ } + if (livePollActive) todoPollTimer = setTimeout(pollTodoList, 500); + }; + pollTodoList(); + const fd2 = new FormData(); 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 }); + try { + const todoData = await api(`/api/todo-list/${encodeURIComponent(sessionId)}`); + if (todoData && todoData.has_todo && todoData.data) { + updateTodoOverlay(todoData.data); + } + } catch { /* ignore */ } + // 停止轮询 livePollActive = false; if (livePollTimer) clearTimeout(livePollTimer); + if (todoPollTimer) clearTimeout(todoPollTimer); sessionId = done.sessionId; sessions = done.sessions || []; @@ -1522,13 +1515,6 @@ renderMessages(head); revokeBlobUrlStack(blobUrls); await streamAssistantMessage(last); - // 获取工具调用数据并注入到最后一条助手消息 - await fetchToolCallData(); - const lastBubble = chatMessages.querySelector(".msg.assistant:last-child .msg-bubble"); - if (lastBubble && toolCallDataCache.recent.length) { - const toolBlock = buildToolCallBubble(); - if (toolBlock) lastBubble.appendChild(toolBlock); - } } else { renderMessages(messages); revokeBlobUrlStack(blobUrls); @@ -2428,13 +2414,6 @@ name: a.name, system_prompt: a.system_prompt, max_iterations: a.max_iterations, - llm: { - model: (a.llm || {}).model || "", - api_key: (a.llm || {}).api_key || "", - base_url: (a.llm || {}).base_url || "", - timeout: (a.llm || {}).timeout || 60, - max_retries: (a.llm || {}).max_retries || 0, - }, llm_model_id: a.llm_model_id || "", tools: a.tools || [], enabled: a.enabled !== false, @@ -2476,366 +2455,6 @@ }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && !settingsOverlay.hidden) closeSettings(); - if (e.key === "Escape" && agentDetailOverlay && !agentDetailOverlay.hidden) closeAgentDetailModal(); - }); - - // ========== 图谱侧面板 ========== - const graphToggleBtn = $("graphToggleBtn"); - const graphPanel = $("graphPanel"); - const graphCloseBtn = $("graphCloseBtn"); - const graphCanvas = $("graphCanvas"); - const graphCanvasWrap = $("graphCanvasWrap"); - const graphEdgesLayer = $("graphEdgesLayer"); - const graphNodesLayer = $("graphNodesLayer"); - const graphRefreshBtn = $("graphRefreshBtn"); - const graphFitBtn = $("graphFitBtn"); - - // 图谱数据与状态 - let graphData = { agents: [], edges: [], events: [], tool_calls: {} }; - let graphOpen = false; - // 可拖拽偏移 - let panOffset = { x: 80, y: 60 }; - let isPanning = false; - let panStart = { x: 0, y: 0 }; - let panStartOffset = { x: 0, y: 0 }; - // 节点布局参数 - const NODE_W = 160; - const NODE_H = 100; - - // 图谱开关(始终可聊天) - function toggleGraph(open) { - graphOpen = open; - graphPanel.hidden = !open; - graphToggleBtn.classList.toggle("graph-active", open); - appRoot.classList.toggle("has-graph", open); - if (open) { - loadGraphData(); - } - } - - graphToggleBtn.addEventListener("click", () => toggleGraph(!graphOpen)); - graphCloseBtn.addEventListener("click", () => toggleGraph(false)); - - // Agent 详情弹窗事件 - const agentDetailOverlay = document.getElementById("agentDetailOverlay"); - const agentDetailClose = document.getElementById("agentDetailClose"); - if (agentDetailClose) agentDetailClose.addEventListener("click", closeAgentDetailModal); - if (agentDetailOverlay) agentDetailOverlay.addEventListener("click", (e) => { - if (e.target === agentDetailOverlay) closeAgentDetailModal(); - }); - - // 加载图谱数据 - async function loadGraphData() { - try { - const snap = await api(`/api/graph/snapshot?session_id=${encodeURIComponent(sessionId)}`); - graphData = snap; - renderGraph(); - } catch (e) { - console.error("加载图谱数据失败:", e); - } - } - - // 自动布局:主 agent 在中心,子 agent 环绕 - function computeLayout() { - const agents = graphData.agents || []; - if (agents.length === 0) return {}; - - const positions = {}; - const wrapW = graphCanvasWrap.clientWidth || 600; - const wrapH = graphCanvasWrap.clientHeight || 400; - const cx = wrapW / 2; - const cy = wrapH / 2; - - // 主 agent 在中心 - const mainAgent = agents.find(a => a.role === "main"); - if (mainAgent) { - positions[mainAgent.name] = { x: cx - NODE_W / 2, y: cy - NODE_H / 2 }; - } - - // 子 agent 均匀环绕 - const subs = agents.filter(a => a.role !== "main"); - const radius = Math.min(wrapW, wrapH) * 0.32; - subs.forEach((agent, i) => { - const angle = (2 * Math.PI * i) / subs.length - Math.PI / 2; - positions[agent.name] = { - x: cx + radius * Math.cos(angle) - NODE_W / 2, - y: cy + radius * Math.sin(angle) - NODE_H / 2, - }; - }); - - return positions; - } - - // 状态到 CSS 类 + 文字 - function statusInfo(status) { - switch (status) { - case "running": return { cls: "status-running", badge: "执行中" }; - case "done": return { cls: "status-done", badge: "已完成" }; - case "error": return { cls: "status-error", badge: "出错" }; - default: return { cls: "status-idle", badge: "等待" }; - } - } - - // 渲染图谱 - function renderGraph() { - const agents = graphData.agents || []; - const edges = graphData.edges || []; - const positions = computeLayout(); - - // 计算 SVG viewBox - const wrapW = graphCanvasWrap.clientWidth || 600; - const wrapH = graphCanvasWrap.clientHeight || 400; - graphCanvas.setAttribute("viewBox", `0 0 ${wrapW} ${wrapH}`); - - // --- 渲染连线 --- - graphEdgesLayer.innerHTML = ""; - edges.forEach((edge, i) => { - const srcPos = positions[edge.source]; - const tgtPos = positions[edge.target]; - if (!srcPos || !tgtPos) return; - - const x1 = srcPos.x + NODE_W / 2; - const y1 = srcPos.y + NODE_H / 2; - const x2 = tgtPos.x + NODE_W / 2; - const y2 = tgtPos.y + NODE_H / 2; - - // 计算从边缘出发(而非中心) - const dx = x2 - x1; - const dy = y2 - y1; - const dist = Math.sqrt(dx * dx + dy * dy) || 1; - const nx = dx / dist; - const ny = dy / dist; - - const sx = x1 + nx * (NODE_W / 2); - const sy = y1 + ny * (NODE_H / 2); - const ex = x2 - nx * (NODE_W / 2); - const ey = y2 - ny * (NODE_H / 2); - - let edgeClass = "normal"; - let markerEnd = "url(#arrowNormal)"; - if (edge.highlighted) { - edgeClass = "highlighted"; - markerEnd = "url(#arrowHighlight)"; - } - - const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); - line.setAttribute("x1", sx + panOffset.x); - line.setAttribute("y1", sy + panOffset.y); - line.setAttribute("x2", ex + panOffset.x); - line.setAttribute("y2", ey + panOffset.y); - line.setAttribute("class", `graph-edge ${edgeClass}`); - line.setAttribute("marker-end", markerEnd); - line.setAttribute("data-source", edge.source); - line.setAttribute("data-target", edge.target); - graphEdgesLayer.appendChild(line); - }); - - // --- 渲染节点 --- - graphNodesLayer.innerHTML = ""; - agents.forEach((agent) => { - const pos = positions[agent.name]; - if (!pos) return; - - const sinfo = statusInfo(agent.status || "idle"); - const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); - g.setAttribute("class", `graph-node ${sinfo.cls}`); - g.setAttribute("transform", `translate(${pos.x + panOffset.x}, ${pos.y + panOffset.y})`); - g.setAttribute("data-agent", agent.name); - - // 圆角矩形 - const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - rect.setAttribute("width", NODE_W); - rect.setAttribute("height", NODE_H); - rect.setAttribute("class", "graph-node-rect"); - g.appendChild(rect); - - // 图标 - const iconText = document.createElementNS("http://www.w3.org/2000/svg", "text"); - iconText.setAttribute("x", 28); - iconText.setAttribute("y", NODE_H / 2); - iconText.setAttribute("class", "graph-node-icon"); - iconText.textContent = agent.icon || "🧠"; - g.appendChild(iconText); - - // 名称 - const labelText = document.createElementNS("http://www.w3.org/2000/svg", "text"); - labelText.setAttribute("x", 90); - labelText.setAttribute("y", NODE_H / 2 - 8); - labelText.setAttribute("class", "graph-node-label"); - labelText.textContent = agent.name; - g.appendChild(labelText); - - // 角色 - const roleText = document.createElementNS("http://www.w3.org/2000/svg", "text"); - roleText.setAttribute("x", 90); - roleText.setAttribute("y", NODE_H / 2 + 12); - roleText.setAttribute("class", "graph-node-role"); - roleText.textContent = agent.role === "main" ? "主 Agent" : "子 Agent"; - g.appendChild(roleText); - - // 状态标签(右上角) - const badgeText = document.createElementNS("http://www.w3.org/2000/svg", "text"); - badgeText.setAttribute("x", NODE_W - 8); - badgeText.setAttribute("y", 14); - badgeText.setAttribute("class", "graph-node-badge"); - const badgeColor = agent.status === "running" ? "#f59e0b" : agent.status === "done" ? "#22c55e" : agent.status === "error" ? "#ef4444" : "#94a3b8"; - badgeText.setAttribute("fill", badgeColor); - badgeText.textContent = sinfo.badge; - g.appendChild(badgeText); - - // 工具调用缩略(仅最近2轮,渐变透明) - const agentToolCalls = (graphData.tool_calls || {})[agent.name] || []; - const recentCalls = agentToolCalls.slice(0, 4); - recentCalls.forEach((tc, idx) => { - const chip = document.createElementNS("http://www.w3.org/2000/svg", "text"); - chip.setAttribute("x", 12 + idx * 38); - chip.setAttribute("y", NODE_H - 10); - chip.setAttribute("class", "graph-node-tool-chip"); - chip.setAttribute("opacity", Math.max(0.2, 1 - idx * 0.35)); - chip.textContent = (tc.tool_name || "").substring(0, 5); - g.appendChild(chip); - }); - - // 点击事件 → 弹出 Agent 详情 modal - g.style.cursor = "pointer"; - g.addEventListener("click", () => openAgentDetailModal(agent)); - - graphNodesLayer.appendChild(g); - }); - } - - // 渲染工具调用列表 → 已移至 Agent 点击弹窗 - function openAgentDetailModal(agent) { - const overlay = document.getElementById("agentDetailOverlay"); - const stateDiv = document.getElementById("agentDetailState"); - const configDiv = document.getElementById("agentDetailConfig"); - const toolCallsDiv = document.getElementById("agentDetailToolCalls"); - const titleEl = document.getElementById("agentDetailTitle"); - if (!overlay || !stateDiv || !configDiv || !toolCallsDiv) return; - - titleEl.textContent = `Agent: ${agent.name}`; - const sinfo = statusInfo(agent.status || "idle"); - - // 状态 - stateDiv.innerHTML = ` -

    名称: ${escapeHtml(agent.name)}

    -

    角色: ${agent.role === "main" ? "主 Agent" : "子 Agent"}

    -

    状态: ${sinfo.badge}

    - ${agent.error_message ? `

    错误: ${escapeHtml(agent.error_message)}

    ` : ""} - `; - - // 配置(只读) - const tools = agent.tools || []; - configDiv.innerHTML = ` -

    工具列表: ${tools.length ? tools.map(t => `${escapeHtml(t)}`).join(", ") : "(无)"}

    - `; - - // 工具调用记录 - const calls = (graphData.tool_calls || {})[agent.name] || []; - if (calls.length === 0) { - toolCallsDiv.innerHTML = `

    暂无工具调用记录

    `; - } else { - toolCallsDiv.innerHTML = [...calls].reverse().slice(0, 6).map(c => ` -
    -
    - ${escapeHtml(c.tool_name)} - ${c.status || 'success'} -
    - ${c.tool_args && Object.keys(c.tool_args).length > 0 ? `
    ${escapeHtml(JSON.stringify(c.tool_args, null, 1))}
    ` : ""} - ${c.result_preview ? `
    ${escapeHtml(c.result_preview)}
    ` : ""} -
    - `).join(""); - } - - overlay.hidden = false; - } - - // 关闭 Agent 详情弹窗 - function closeAgentDetailModal() { - const overlay = document.getElementById("agentDetailOverlay"); - if (overlay) overlay.hidden = true; - } - - - // 画布拖拽 - graphCanvasWrap.addEventListener("mousedown", (e) => { - if (e.target === graphCanvas || e.target === graphCanvasWrap || e.target.closest("svg")) { - isPanning = true; - panStart = { x: e.clientX, y: e.clientY }; - panStartOffset = { ...panOffset }; - graphCanvasWrap.classList.add("panning"); - e.preventDefault(); - } - }); - - window.addEventListener("mousemove", (e) => { - if (!isPanning) return; - const dx = e.clientX - panStart.x; - const dy = e.clientY - panStart.y; - panOffset = { - x: panStartOffset.x + dx, - y: panStartOffset.y + dy, - }; - renderGraph(); - }); - - window.addEventListener("mouseup", () => { - if (isPanning) { - isPanning = false; - graphCanvasWrap.classList.remove("panning"); - } - }); - - // 触摸拖拽 - graphCanvasWrap.addEventListener("touchstart", (e) => { - if (e.touches.length === 1) { - isPanning = true; - panStart = { x: e.touches[0].clientX, y: e.touches[0].clientY }; - panStartOffset = { ...panOffset }; - graphCanvasWrap.classList.add("panning"); - } - }, { passive: false }); - - graphCanvasWrap.addEventListener("touchmove", (e) => { - if (!isPanning || e.touches.length !== 1) return; - const dx = e.touches[0].clientX - panStart.x; - const dy = e.touches[0].clientY - panStart.y; - panOffset = { - x: panStartOffset.x + dx, - y: panStartOffset.y + dy, - }; - renderGraph(); - e.preventDefault(); - }, { passive: false }); - - graphCanvasWrap.addEventListener("touchend", () => { - if (isPanning) { - isPanning = false; - graphCanvasWrap.classList.remove("panning"); - } - }); - - // 鼠标滚轮缩放(可选) - - // 按钮事件 - graphRefreshBtn.addEventListener("click", () => { - loadGraphData(); - }); - - graphFitBtn.addEventListener("click", () => { - panOffset = { x: 80, y: 60 }; - renderGraph(); - }); - - // 窗口大小变化时重新渲染 - let resizeGraphTimer = null; - window.addEventListener("resize", () => { - clearTimeout(resizeGraphTimer); - resizeGraphTimer = setTimeout(() => { - if (graphOpen) { - renderGraph(); - } - }, 200); }); bootstrap().catch((e) => { diff --git a/src/web/index.html b/src/web/index.html index ac59600..9572557 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -86,18 +86,8 @@ > ⚙ -
    -

    开始对话

    @@ -106,39 +96,6 @@
    - -
    - -