初步实现gui操作

This commit is contained in:
minorQAQ committed 2026-06-18 18:07:04 +08:00
1 parent 1396d652f7
commit d97e1581a4
56 files changed
+1721 -1586

No files matched your search

Binary file not shown.
+1
View File
@@ -8,3 +8,4 @@
实现时可单独配置 tool_policy 白名单、更严格的 confirm 策略,
并绑定专用 system_prompt 与较短上下文窗口。
"""
+3
View File
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5 -11
View File
@@ -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"
}
]
]
+15 -23
View File
@@ -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
+2 -2
View File
@@ -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
}
+14 -43
View File
@@ -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]}
+1 -1
View File
@@ -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
+40 -4
View File
@@ -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
}
]
Binary file not shown.
@@ -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
}
}
]
}
@@ -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
}
}
]
}
+69 -9
View File
@@ -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("")
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

@@ -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"
}
+2
View File
@@ -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 实例推导
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+314 -1
View File
@@ -1 +1,314 @@
from agent.utils.env_utils import GROUNDING_WIDTH, GROUNDING_HEIGHT
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)
+141 -18
View File
@@ -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)
-118
View File
@@ -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;"))
+1 -5
View File
@@ -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()
+1 -4
View File
@@ -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
-53
View File
@@ -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. 以清晰的结构呈现给用户,包括“原始标题”、“内容推测”和“我的理解”。
---
**备注**:在执行过程中,我将根据实际抓取到的标题实时更新后续的分析内容。如果首页因登录状态或算法推荐显示不同内容,我将基于当时可见的第一个视频进行处理。
Binary file not shown.
Binary file not shown.
Binary file not shown.
+137 -523
View File
@@ -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;
}
}
+167 -548
View File
@@ -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)
? '<span class="todo-check">✓</span>'
: '<span class="todo-spinner"></span>';
item.innerHTML = `${statusIcon}<span class="todo-label">${escapeHtml(s)}</span>`;
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 = `
<p><strong>名称:</strong> ${escapeHtml(agent.name)}</p>
<p><strong>角色:</strong> ${agent.role === "main" ? "主 Agent" : "子 Agent"}</p>
<p><strong>状态:</strong> <span style="color:${agent.status === 'running' ? '#f59e0b' : agent.status === 'done' ? '#22c55e' : agent.status === 'error' ? '#ef4444' : '#94a3b8'}">${sinfo.badge}</span></p>
${agent.error_message ? `<p><strong>错误:</strong> <span style="color:#ef4444">${escapeHtml(agent.error_message)}</span></p>` : ""}
`;
// 配置(只读)
const tools = agent.tools || [];
configDiv.innerHTML = `
<p><strong>工具列表:</strong> ${tools.length ? tools.map(t => `<code>${escapeHtml(t)}</code>`).join(", ") : "(无)"}</p>
`;
// 工具调用记录
const calls = (graphData.tool_calls || {})[agent.name] || [];
if (calls.length === 0) {
toolCallsDiv.innerHTML = `<p style="color:#94a3b8;font-style:italic;">暂无工具调用记录</p>`;
} else {
toolCallsDiv.innerHTML = [...calls].reverse().slice(0, 6).map(c => `
<div class="toolcall-card">
<div class="toolcall-card-head">
<span class="toolcall-card-name">${escapeHtml(c.tool_name)}</span>
<span class="toolcall-card-status ${c.status || 'success'}">${c.status || 'success'}</span>
</div>
${c.tool_args && Object.keys(c.tool_args).length > 0 ? `<div class="toolcall-card-args">${escapeHtml(JSON.stringify(c.tool_args, null, 1))}</div>` : ""}
${c.result_preview ? `<div class="toolcall-card-result">${escapeHtml(c.result_preview)}</div>` : ""}
</div>
`).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) => {
-66
View File
@@ -86,18 +86,8 @@
>
⚙
</button>
<button
type="button"
class="settings-btn graph-toggle-btn"
id="graphToggleBtn"
title="打开 Agent 图谱"
aria-label="打开 Agent 图谱"
>
🔗
</button>
</header>
<div class="main-content-wrap" id="mainContentWrap">
<!-- 聊天面板(始终可见) -->
<section class="chat-shell" id="chatPanel">
<div class="chat-placeholder" id="chatPlaceholder">
<h3>开始对话</h3>
@@ -106,39 +96,6 @@
</div>
<div class="chat-messages" id="chatMessages" hidden></div>
</section>
<!-- 图谱侧面板(默认隐藏) -->
<aside class="graph-panel" id="graphPanel" hidden>
<div class="graph-toolbar">
<span class="graph-toolbar-title">Agent 图谱</span>
<button type="button" class="btn btn-ghost graph-btn" id="graphRefreshBtn" title="刷新图谱">🔄</button>
<button type="button" class="btn btn-ghost graph-btn" id="graphFitBtn" title="适应画布">⊞</button>
<span class="graph-legend">
<span class="legend-dot legend-running"></span> 执行中
<span class="legend-dot legend-done"></span> 已完成
<span class="legend-dot legend-error"></span> 出错
<span class="legend-dot legend-idle"></span> 等待
</span>
<button type="button" class="graph-close-btn" id="graphCloseBtn" title="关闭图谱" aria-label="关闭图谱">✕</button>
</div>
<div class="graph-canvas-wrap" id="graphCanvasWrap">
<svg class="graph-canvas" id="graphCanvas">
<defs>
<marker id="arrowHighlight" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#f59e0b" />
</marker>
<marker id="arrowNormal" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="rgba(148,163,184,0.5)" />
</marker>
<marker id="arrowActive" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#14b8a6" />
</marker>
</defs>
<g id="graphEdgesLayer"></g>
<g id="graphNodesLayer"></g>
</svg>
</div>
<!-- 工具调用详情已移至 Agent 点击弹窗 -->
</aside>
</div>
<footer class="composer">
<div class="composer-attachments" id="attachmentPreview"></div>
@@ -187,29 +144,6 @@
</footer>
</main>
</div>
<!-- Agent 详情弹窗(图谱中点击 Agent 节点) -->
<div class="agent-detail-overlay" id="agentDetailOverlay" hidden>
<div class="agent-detail-dialog" id="agentDetailDialog">
<div class="agent-detail-header">
<h2 id="agentDetailTitle">Agent 详情</h2>
<button type="button" class="agent-detail-close" id="agentDetailClose">&times;</button>
</div>
<div class="agent-detail-body">
<div class="agent-detail-section">
<h3>状态</h3>
<div id="agentDetailState"></div>
</div>
<div class="agent-detail-section">
<h3>配置</h3>
<div id="agentDetailConfig"></div>
</div>
<div class="agent-detail-section">
<h3>工具调用记录</h3>
<div id="agentDetailToolCalls"></div>
</div>
</div>
</div>
</div>
<!-- 系统配置模态框 -->
<div class="settings-overlay" id="settingsOverlay" hidden>
<div class="settings-dialog" id="settingsDialog" role="dialog" aria-modal="true" aria-label="系统配置">
+34 -153
View File
@@ -9,7 +9,6 @@
from __future__ import annotations
import base64
import json
import mimetypes
import os
import shutil
@@ -325,11 +324,13 @@ def api_clear_session(session_id: str = Form(...)) -> dict[str, Any]:
{"sessionId": str, "sessions": list[str], "messages": []}
系统定位:
前端“清空对话”按钮调用。
前端"清空对话"按钮调用。
"""
if not session_id:
raise HTTPException(status_code=400, detail="缺少 session_id")
messages, _ = ui.clear_ui_and_manifest(session_id)
# 清理 tool_calling 目录
_clear_tool_calling(session_id)
ids = ui.list_session_ids_ordered()
if session_id not in ids:
ids = [session_id, *ids]
@@ -352,6 +353,7 @@ def api_delete_session(session_id: str = Form(...)) -> dict[str, Any]:
if not session_id:
raise HTTPException(status_code=400, detail="缺少 session_id")
ui.delete_session(session_id)
_clear_tool_calling(session_id)
sid, ids, hist = _bootstrap()
return {"sessionId": sid, "sessions": ids, "messages": _serialize_messages(hist)}
@@ -433,8 +435,8 @@ def _chat_start_core(session_id: str, query: dict[str, Any]) -> dict[str, Any]:
"""
hist = ui.load_ui_messages_for_session(session_id)
before = len(hist or [])
new_hist, cleared = ui.add_message(hist or [], query)
if cleared is None:
new_hist = ui.add_message(hist or [], query)
if new_hist is None:
raise HTTPException(status_code=400, detail="请输入文字或上传图片/音频")
if len(new_hist) == before:
raise HTTPException(status_code=400, detail="请输入文字或上传图片/音频")
@@ -547,43 +549,7 @@ def api_chat_complete(
return _chat_complete_core(session_id, output_type=output_type, agent_mode=agent_mode)
@app.post("/api/chat")
async def api_chat(
session_id: str = Form(...),
text: str = Form(""),
images: list[UploadFile] | None = File(None),
files: list[UploadFile] | None = File(None),
audio: UploadFile | None = File(None),
output_type: str = Form("text"),
agent_mode: str = Form("agent"),
) -> dict[str, Any]:
"""
兼容旧客户端:等价于依次 ``/api/chat/start`` + ``/api/chat/complete``(仍阻塞至推理结束)。
输入:
session_id: 表单字段,会话 ID。
text: 用户输入的文本。
images: 上传的图片文件列表。
files: 上传的普通文件列表。
audio: 上传的音频文件。
output_type: text | audio | both。
agent_mode: agent | plan | team。
输出:
{"sessionId": str, "sessions": list[str], "messages": list[dict]}
系统定位:
向后兼容的一次性对话接口。
"""
if not session_id:
raise HTTPException(status_code=400, detail="缺少 session_id")
query, tmp_root = _materialize_uploads_to_temp(images, files, audio, text)
try:
_chat_start_core(session_id, query)
return _chat_complete_core(session_id, output_type=output_type, agent_mode=agent_mode)
finally:
shutil.rmtree(tmp_root, ignore_errors=True)
@app.post("/api/human-action")
@@ -635,15 +601,16 @@ def api_get_agent_configs() -> dict[str, Any]:
输出: {"agents": list[dict], "config_path": str}
"""
from agent.core.config_manager import load_models
configs = load_agent_configs()
models = {m["id"]: m for m in load_models()}
return {
"agents": [{"name": c.name, "system_prompt": c.system_prompt, "max_iterations": c.max_iterations,
"llm": {"model": c.llm.model, "api_key": c.llm.api_key, "base_url": c.llm.base_url,
"timeout": c.llm.timeout, "max_retries": c.llm.max_retries},
"llm_model_id": c.llm_model_id,
"tools": c.tools,
"enabled": c.enabled,
"role": c.role} for c in configs],
"models": list(models.values()),
"config_path": str(AGENT_CONFIG_PATH),
}
@@ -795,70 +762,10 @@ def api_reload_configs() -> dict[str, Any]:
return {"success": False, "agent": {}, "detail": str(e)}
# ---------- 多 Agent 图面板 API ----------
@app.get("/api/graph/snapshot")
def api_get_graph_snapshot(session_id: str = "") -> dict[str, Any]:
"""获取当前多 Agent 运行时的图结构快照,供前端 graph 面板渲染。"""
try:
from agent.agents.multi_agent_runtime import get_agent_graph_snapshot, build_multi_agent_runtime
from agent.agents.main_agent import main_agent_runtime
from agent.history.tool_call_recorder import get_tool_calls_for_latest_turn
runtime = build_multi_agent_runtime(main_runtime=main_agent_runtime)
result = get_agent_graph_snapshot(runtime)
if session_id:
recorded = get_tool_calls_for_latest_turn(session_id)
if recorded:
existing = result.get("tool_calls", {}) or {}
main_calls = existing.get("main", [])
for r in recorded:
main_calls.append({
"tool_name": r.get("name", ""),
"tool_args": {},
"result_preview": r.get("result_text", "")[:80],
"status": r.get("status", "done"),
})
result["tool_calls"] = {**existing, "main": main_calls}
return result
except Exception as e:
return {
"agents": [
{"name": "main", "role": "main", "icon": "🧠", "status": "idle", "error_message": ""}
],
"edges": [],
"events": [],
"tool_calls": {},
"error": str(e),
}
@app.get("/api/graph/config")
def api_get_graph_config() -> dict[str, Any]:
"""获取 graph 面板的初始配置(从 agent_config.json 读取各 agent 的 role/icon 等)。
输出:
{"agents": [{"name": str, "role": str, "icon": str}, ...]}
"""
from agent.core.config_manager import load_agent_configs
configs = load_agent_configs()
role_icons = {"main": "🧠", "code": "💻", "research": "🔍", "doc": "📄", "gui": "🖥"}
agents_list = []
for cfg in configs:
if cfg.enabled:
agents_list.append({
"name": cfg.name,
"role": cfg.role,
"icon": role_icons.get(cfg.name, "🔧"),
"tools": cfg.tools,
})
if not agents_list:
agents_list = [{"name": "main", "role": "main", "icon": "🧠", "tools": []}]
return {"agents": agents_list}
# ---------- 工具调用历史 API ----------
@app.get("/api/tool-calls/{session_id}/live")
def api_get_live_tool_calls(session_id: str) -> dict[str, Any]:
"""获取当前进行中的工具调用(图表执行期间实时轮询)。"""
"""获取当前进行中的工具调用(前端执行期间实时轮询)。"""
try:
from agent.history.tool_call_recorder import get_live_tool_calls
return {"tool_calls": get_live_tool_calls(session_id)}
@@ -866,61 +773,35 @@ def api_get_live_tool_calls(session_id: str) -> dict[str, Any]:
return {"tool_calls": [], "error": str(e)}
@app.get("/api/tool-calls/{session_id}/recent")
def api_get_recent_tool_calls(session_id: str) -> dict[str, Any]:
"""获取最新 turn 的工具调用缩略(仅名称,无返回值)。"""
# ---------- Todo List 实时推送 ----------
# 数据直接从 agent.tools.tool 的共享 store 读取,无需额外存储
@app.get("/api/todo-list/{session_id}")
def api_get_todo_list(session_id: str) -> dict[str, Any]:
"""获取当前会话最新的 todo list 数据(前端轮询)。"""
try:
from agent.history.tool_call_recorder import get_tool_calls_for_latest_turn
calls = get_tool_calls_for_latest_turn(session_id)
result = []
for c in calls:
result.append({
"name": c.get("name", ""),
"args_summary": json.dumps(c.get("args", {}), ensure_ascii=False)[:40],
"status": "done",
"turn_id": c.get("turn_id", ""),
})
return {"tool_calls": result}
except Exception as e:
return {"tool_calls": [], "error": str(e)}
from agent.tools.tool import get_todo_store_data
data = get_todo_store_data(session_id)
except Exception:
data = None
if data:
return {"has_todo": True, "data": data}
return {"has_todo": False, "data": None}
@app.get("/api/tool-calls/{session_id}/detail")
def api_get_tool_call_detail(session_id: str) -> dict[str, Any]:
"""获取最新 turn 的工具调用详情(含返回值和图片 base64)。"""
def _clear_tool_calling(session_id: str) -> None:
"""清理会话关联的 tool_calling 目录与 todo_list 缓存。"""
try:
from agent.history.tool_call_recorder import get_tool_calls_for_latest_turn
details = get_tool_calls_for_latest_turn(session_id)
# 图片文件转 data URL
import base64
for d in details:
imgs = []
for fp in d.get("result_files_full", []):
if os.path.isfile(fp):
with open(fp, "rb") as f:
b64 = base64.standard_b64encode(f.read()).decode("ascii")
ext = os.path.splitext(fp)[1].lower()
mime_map = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif"}
mime = mime_map.get(ext, "image/png")
imgs.append(f"data:{mime};base64,{b64}")
d["result_images"] = imgs
return {"details": details}
except Exception as e:
return {"details": [], "error": str(e)}
@app.get("/api/tool-calls/{session_id}/has-pending")
def api_has_pending_tool_calls(session_id: str) -> dict[str, Any]:
"""检查是否有待处理的工具调用(thinking 状态时使用)。"""
from agent.tools.tool import clear_todo_store_session
clear_todo_store_session(session_id)
except Exception:
pass
try:
from agent.history.tool_call_recorder import list_turns
turns = list_turns(session_id)
has_tools = bool(turns)
# 判断最新 turn 是否已完成(有 assistant_response)
latest_complete = turns[0].get("assistant_response", "") if turns else ""
return {"has_recent": has_tools, "is_complete": bool(latest_complete)}
except Exception as e:
return {"has_recent": False, "is_complete": True, "error": str(e)}
import shutil as _shutil
from agent.history.tool_call_recorder import session_tool_dir
_shutil.rmtree(str(session_tool_dir(session_id)), ignore_errors=True)
except Exception:
pass
@app.get("/favicon.ico")
+9 -4
View File
@@ -142,6 +142,12 @@ def delete_session(session_id: str) -> None:
conn.close()
_memory_session_ids.discard(session_id)
shutil.rmtree(session_dir(session_id), ignore_errors=True)
# 清理 tool_calling 目录
try:
from agent.history.tool_call_recorder import session_tool_dir
shutil.rmtree(str(session_tool_dir(session_id)), ignore_errors=True)
except Exception:
pass
def _lc_content_for_ui_messages(content) -> str | list:
@@ -701,8 +707,7 @@ def add_message(chat_history, query):
query: {"text": str, "files": list[str], "file_names": list[str]}
输出:
(new_chat_history, cleared_query) 元组;
cleared_query 在旧 API 兼容中保留,新调用可忽略。
成功返回 new_chat_history,无有效内容返回 None。
系统定位:
API 层构造用户消息的核心函数。
@@ -710,9 +715,9 @@ def add_message(chat_history, query):
chat_history = chat_history or []
user_content = _build_user_chat_content(query)
if user_content is None:
return chat_history, None
return None
chat_history.append({"role": "user", "content": user_content})
return chat_history, {"text": "", "files": []}
return chat_history
def clear_ui_and_manifest(session_id: str):