实现skill功能

This commit is contained in:
minorQAQ committed 2026-07-05 22:51:54 +08:00
1 parent cabbb0a7ae
commit 287e1935f6
96 files changed
+2415 -1516

No files matched your search

+328 -1
View File
@@ -85,4 +85,331 @@ git push minor master
│
└─ 图结束后 (runtime.py / execute_agent)
└─ 检测到强制终止 → 向前端输出:
"检测到异常循环,工具 `xxx` 已连续调用 5 次..."
"检测到异常循环,工具 `xxx` 已连续调用 5 次..."
1、打开https://erpdemo.fanpusoft.com/main.aspx#
2、点击采购管理
3、点击新增供应商
4、在新增供应商页面进行以下操作:输入供应商名称:“minor”、输入供应商类别:“工程材料供应商”、输入部门:“采购部门B”、输入职务:“CEO”、输入联系人:“赵永顺”、输入生日:“2004/07/09”
5、点击保存、点击确定
---
## 提示词工程文档
本 Agent 系统的提示词采用**分层架构**,按作用域和注入时机分为四个层级。以下逐一说明。
### 一、架构总览
```
┌─────────────────────────────────────────────────┐
│ Layer 1: 系统级提示词 (System Prompt) │
│ 作用于整个会话,定义 Agent 角色与核心行为准则 │
│ 文件: system_prompt.py / agent_config.json │
├─────────────────────────────────────────────────┤
│ Layer 2: 工具描述提示词 (Tool Description) │
│ 随工具定义注入,指导 LLM 如何正确调用工具 │
│ 文件: gui_tool.py / tool.py / tool_rag.py 等 │
├─────────────────────────────────────────────────┤
│ Layer 3: 运行时注入提示词 (Runtime Injection) │
│ 每轮 ReAct 循环动态拼接到消息列表 │
│ 文件: system_prompt.py / loop_detector.py │
├─────────────────────────────────────────────────┤
│ Layer 4: 内部模型提示词 (Internal Model Prompt) │
│ 工具内部调用子模型时使用,用户不可见 │
│ 文件: gui_tool.py (_build_batch_prompt) │
└─────────────────────────────────────────────────┘
```
---
### 二、Layer 1 — 系统级提示词
#### 2.1 MAIN_SYSTEM_PROMPT(Agent 模式)
**位置:** `agent/memory/system_prompt.py` → `MAIN_SYSTEM_PROMPT`
**作用:** Agent 模式的顶层系统提示词,定义 Agent 的身份和全局行为准则。
**核心规则:**
| 规则 | 目的 |
|------|------|
| 优先理解用户意图,高效完成任务 | 确立目标导向 |
| 对图片/音频直接使用多模态能力分析,不调用无关工具 | 防止误调用截图/OCR |
| 截图返回后必须基于实际结果推进,严禁凭空编造 | 防止幻觉 |
| **同一页面的 GUI 操作必须合并为一次 gui_tool 调用** | 减少延迟,避免单步执行 |
| 不确定是否需要工具时优先不调用 | 防止过度工具调用 |
| 复杂任务主动使用 todo_list 规划并跟踪进度 | 确保多步任务可追踪 |
**设计要点:**
- 第 8 行从"优先考虑动作序列"升级为"**必须**合并",并明确"严禁逐个单步调用",以对抗 ReAct 循环天然倾向于单步执行的问题。
- 第 6 行"基于实际结果继续推进"是为了让模型把截图当作真实的视觉反馈,而非想象屏幕状态。
---
#### 2.2 PLAN_MODE_PROMPT(Plan 模式)
**位置:** `agent/memory/system_prompt.py` → `PLAN_MODE_PROMPT`
**作用:** 当 `agent_mode == "plan"` 时替换 MAIN_SYSTEM_PROMPT,强制 Agent 先规划后执行。
**核心规则:**
- 收到用户请求后**第一步必须调用 todo_list** 进行任务拆解
- 步骤拆解必须详尽,覆盖完整流程
- 规划完成后立即开始执行,不等待用户确认
- 每完成一步调用 todo_list 标记进度
---
#### 2.3 Agent 级 system_prompt
**位置:** `agent/core/agent_config.json`
```json
"system_prompt": "你是一个多模态助手...你的默认行为是优先调用工具解决用户问题。"
```
**作用:** 当 `agent_config.json` 中配置了 `system_prompt` 时,会作为额外的行为准则注入。当前版本的指令较为简略,主要作为兜底。
---
#### 2.4 其他专用 System Prompt
| Prompt | 用途 | 特点 |
|--------|------|------|
| VISION_SYSTEM_PROMPT | 图像理解子 Agent | 要求客观描述、不臆测、多图编号 |
| DB_SYSTEM_PROMPT | 数据库查询子 Agent | 只允许 SELECT/WITH,强调安全底线 |
| SUMMARY_SYSTEM_PROMPT | 摘要子 Agent | 压缩至 20%-30%,保留关键信息 |
| TEAM_MODE_PROMPT | Team 模式占位 | 当前未实现,告知用户使用 Agent 模式 |
---
### 三、Layer 2 — 工具描述提示词
工具描述提示词直接定义在工具类的 `description` 属性中,随 LangChain 工具绑定注入到 LLM 上下文。
#### 3.1 gui_tool 描述
**位置:** `agent/tools/gui_tool.py` → `GUITool.description`
**结构:**
```
GUI 自动化工具。通过截图视觉定位桌面元素...
↓
【type 使用说明】type 支持 element_description 自动聚焦输入框
↓
【网页滚动提示】推荐用 hotkey space 代替 scroll 滚轮
↓
【下拉框处理策略】直接使用 type 动作,系统自动匹配选项
↓
示例:登录表单 actions 列表
```
**设计要点:**
- **type 操作说明:** 强调 `element_description` 参数可让 type 自动完成"定位→点击→输入",无需额外 click。这是减少动作数量的关键设计。
- **网页滚动提示:** 引导模型使用 `hotkey space`(空格键即 Page Down)而非 `scroll` 滚轮,因为浏览器中滚轮定位不准确。
- **下拉框处理:** 精简为一步 `type`,去掉了之前的两步 fallback 策略(点击→方向键→回车),减少不必要的操作。
- **登录表单示例:** 提供一个多 action 的 one-shot 示例,示范如何将两个 type 操作合并到一个 gui_tool 调用中。
---
#### 3.2 todo_list 描述
**位置:** `agent/tools/tool.py` → `TodoList.description`
**关键指令:**
- **步骤粒度要求:** 同一页面/界面的连续操作合并为一条步骤,用"并"连接
- 包含正例(推荐)和反例(禁止)的对比
- Plan 模式下为必须调用,Agent 模式下按需
**示例:**
```
推荐:"点击搜索框,输入XXX并点击搜索"(合并了点击、输入、搜索)
禁止:"1.点击搜索框 2.输入文字 3.点击搜索"(过于细碎)
```
---
### 四、Layer 3 — 运行时注入提示词
这类提示词在每轮 ReAct 循环中动态拼接到消息列表,作为 synthetic HumanMessage 注入,不会被记录到前端消息历史。
#### 4.1 REFLECTION_PROMPT(反思提示)
**位置:** `agent/memory/system_prompt.py` → `REFLECTION_PROMPT`
**注入时机:** 每轮 ReAct 循环(除首轮外),由 `nodes.py` 的 `_build_reflection_prompt()` 构建并注入。
**结构:**
```
【请先在你的回复中尽可能简短地输出你的反思...】
1. 分析上一轮执行结果
2. GUI 未命中 → 重新估算坐标重试
3. 操作成功 → 进行下一步
4. 调用工具或回复用户
【gui_tool 批量调用规则 - 务必遵守】
同一页面的连续操作必须合并为一次 gui_tool 调用
严禁拆分多次调用——会造成严重延迟
仅页面跳转/加载后才分步调用
```
**设计要点:**
- 前 4 条建立"观察→反思→行动"的思维链模式
- **第 5 条是关键补充:** 明确告知模型在做"下一步"计划时,如果同一页面有多个操作,必须打包成一个 gui_tool 调用。这是对抗 ReAct 循环"做一步看一步"惯性的核心机制。
- 仅当需要等页面跳转/加载后才能确定后续操作时,才允许分步,给了模型合理的例外判断空间。
---
#### 4.2 PLAN_FIRST_CALL_PROMPT(Plan 模式首轮注入)
**位置:** `agent/memory/system_prompt.py` → `PLAN_FIRST_CALL_PROMPT`
**注入时机:** Plan 模式下的首轮调用,在 `call_model` 中检测到 `agent_mode == "plan" && is_first_call` 时注入。
**核心指令:**
- **立即调用 todo_list** 进行任务拆解
- **合并同类操作:** 同一页面的连续操作合并为一条步骤
- 包含三个正例和一个反例的详细示范
---
#### 4.3 反循环反思提示
**位置:** `agent/core/loop_detector.py` → `build_loop_reflection_prompt()`
**注入时机:** 当检测到循环风险时,拼接到 REFLECTION_PROMPT 之后。
**两级响应:**
| 级别 | 触发条件 | 响应 |
|------|----------|------|
| Level 1 (警告) | 连续 3 次调用同一工具且参数相同 | 注入反思提示:"你已经连续 N 次调用工具...请反思当前策略" |
| Level 1 (警告) | TodoList 停滞 10 轮无进展 | 注入反思提示:"TodoList 已有 N 轮没有进展...考虑调整计划" |
| Level 2 (终止) | 连续 5 次调用同一工具且参数相同 | 强制结束图执行,向前端输出错误报告 |
**设计要点:**
- 两级响应机制避免误杀——先警告引导模型自我纠偏,纠偏失败再强制终止
- 循环检测阈值可通过 `env_config.json` 中的 `LOOP_DETECT_*` 配置项热更新
---
#### 4.4 截图反馈注入
**位置:** `agent/core/nodes.py` → `process_tool_artifact()`
**注入时机:** 每次 gui_tool 返回 artifact(截图)后。
**注入内容:** `[图片数据] + "上面是工具返回的截图。"`
**作用:** 将截图作为视觉反馈注入对话上下文,让多模态 LLM 看到执行后的屏幕状态,实现闭环的视觉推理。
---
### 五、Layer 4 — 内部模型提示词
#### 5.1 批量坐标定位 Prompt
**位置:** `agent/tools/gui_tool.py` → `_build_batch_prompt()`
**调用链:** `_run()` → `_collect_grounding_queries()` → `_batch_ground_elements()` → `_build_batch_prompt()`
**作用:** 将一次截图中需要定位的所有元素,批量发送给视觉定位模型,一次性返回所有坐标。
**结构:**
```
You are a visual grounding model for desktop GUI control.
↓
=== OUTPUT FORMAT (STRICT) ===
每行一对坐标: (<x>, <y>)
↓
=== ONE-SHOT EXAMPLE ===
4 个 query → 4 行坐标的完整示例
↓
=== NOW YOUR TURN ===
实际需要定位的元素列表
↓
=== RULES ===
7 条严格的输出格式规则
```
**设计要点:**
- **One-shot 示例**是核心: 用一个完整的 4 元素定位示例纠正模型的输出格式偏好,防止输出 JSON、Markdown 或其他非预期格式。
- **规则 7** 特别强调"按顺序生成坐标",确保 action 顺序与坐标顺序一一对应。
- 使用的模型为 `_get_gui_llm()` 获取的 GUI 专用模型(如有配置),否则回退到主模型。
---
### 六、提示词注入流程
#### 6.1 Agent 模式标准流程
```
用户输入
│
▼
[MAIN_SYSTEM_PROMPT] ← 作为 SystemMessage,会话开始时注入
│
▼
┌─ ReAct 循环 ────────────────────────────────────┐
│ │
│ call_model 节点 │
│ ├─ 首轮: 直接调用 LLM │
│ └─ 后续: 注入 [REFLECTION_PROMPT] │
│ + [TodoList 状态] │
│ + [反循环警告(如有)] │
│ │ │
│ ▼ │
│ LLM 决策 → 调用 tool(带 [Tool Description]) │
│ │ │
│ ▼ │
│ tools 节点 → 执行工具 │
│ │ │
│ ▼ │
│ process_tool_artifact 节点 │
│ └─ gui_tool 返回 → 注入截图 + "上面是截图" │
│ │ │
│ ▼ │
│ routing → 继续/结束 │
│ │
└──────────────────────────────────────────────────┘
```
#### 6.2 Plan 模式特殊流程
```
用户输入
│
▼
[PLAN_MODE_PROMPT] ← 替换 MAIN_SYSTEM_PROMPT
│
▼
首轮 call_model:
注入 [PLAN_FIRST_CALL_PROMPT]
→ LLM 调用 todo_list 生成步骤规划
│
▼
后续 call_model:
注入 [REFLECTION_PROMPT] + [TodoList 状态]
→ LLM 按规划逐步执行
```
---
### 七、关键设计决策
| 决策 | 原因 | 实现方式 |
|------|------|----------|
| **动作序列优先于单步** | 单步调用每个 action 都经过一轮 LLM→截图→LLM 循环,N 个 action 需要 2N+1 次 LLM 调用。序列化后 N 个 action 只需 2 次 LLM 调用(1 次定位 + 1 次决策) | MAIN_SYSTEM_PROMPT + REFLECTION_PROMPT 双重强调 |
| **下拉框仅用 type** | 两步策略(type→键选 fallback)会增加不必要的 action 数量,type 本身即可完成下拉匹配 | gui_tool 描述中精简为单句 |
| **截图作为视觉闭环** | 让 LLM 看到真实屏幕状态而非猜测,是 GUI 自动化可靠性的基础 | process_tool_artifact 自动注入截图 |
| **反思提示每轮注入** | 让模型形成"观察→分析→行动"的习惯,减少盲目操作和幻觉 | REFLECTION_PROMPT 作为 synthetic HumanMessage |
| **循环检测两级响应** | 先警告引导,再强制终止,避免误杀同时防止无限循环 | loop_detector.py 的 warn→end 机制 |
| **坐标定位批量化** | N 个元素的坐标定位只需 1 次子模型调用,而非 N 次 | _batch_ground_elements + _build_batch_prompt |
| **One-shot 示例纠正格式** | 视觉定位模型容易输出 JSON/Markdown/解释文字,one-shot 强制纯坐标格式 | _build_batch_prompt 中的 4 行坐标示例 |
| **trajectory_rounds 短期记忆过滤** | 多轮 GUI 操作后上下文过长,仅保留最近 3 轮工具返回结果 | _filter_short_term_memory() |
+1 -1
View File
@@ -25,7 +25,7 @@ from agent.core.multi_agent_state import (
ToolCallEvent,
init_multi_agent_state,
)
from agent.tools.tool_agent_call import CallSubAgentTool
from agent.tools.agent_call import CallSubAgentTool
@dataclass
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -6,7 +6,7 @@
"llm_model_id": "b53afakf",
"tools": [
"browser_control",
"gui_tool(test)"
"gui_tool"
],
"enabled": true,
"role": "main",
+1 -1
View File
@@ -26,7 +26,7 @@ from typing import Any, Sequence
from langchain_core.messages import BaseMessage
from langchain_core.messages import messages_to_dict
from agent.tools.tool import HumanInteraction
from agent.tools.human_interaction import HumanInteraction
from agent.utils.tool_call_utils import json_safe
# ============================================================
+3
View File
@@ -148,6 +148,9 @@ _TOOL_DEFAULT_AUTO_RULES: dict[str, list[dict]] = {
"rag_tool": [
{"parameter": "action_type", "operator": "equals", "value": "query"},
],
"skill_router": [
{"parameter": "query", "operator": "exists", "value": ""},
],
}
+1 -1
View File
@@ -51,7 +51,7 @@ def build_agent_graph(tools: list[Any], model_with_tools: Any, trajectory_rounds
返回前可 ``workflow.compile(checkpointer=...)`` 启用持久化;
可参数化节点名以支持多 Agent 图合并。
"""
from agent.tools.tool_agent_call import make_parallel_tool_node
from agent.tools.agent_call import make_parallel_tool_node
workflow = StateGraph(AgentState)
workflow.add_node("agent", make_call_model_node(model_with_tools, trajectory_rounds=trajectory_rounds))
+9 -9
View File
@@ -3,13 +3,13 @@
系统定位:
在 ReAct 循环的 agent 节点和路由节点之间,检测工具调用死循环、
todolist 进度停滞等异常模式,提供分级响应:
- Level 1: 注入反思提示引导模型自我纠偏
- Level 1: 注入反思提示引导模型自我纠正
- Level 2: 强制终止图执行并向用户报告
配置来源:
env_config.json 中的 LOOP_DETECT_* 参数,可由前端动态修改。
可扩展性:
可扩展性:
可增加更多循环模式检测器、自定义检测规则。
"""
from __future__ import annotations
@@ -205,7 +205,7 @@ def _format_args_summary(args: dict, max_len: int = 80) -> str:
def _get_current_todolist_done_count(session_id: str) -> int:
"""获取当前 todolist 的已完成步骤数。"""
try:
from agent.tools.tool import get_todo_store_data
from agent.tools.todo_list import get_todo_store_data
todo = get_todo_store_data(session_id)
if todo and todo.get("steps"):
return len(todo.get("done_steps", []))
@@ -239,7 +239,7 @@ def check_and_record_todolist_progress(
current_done = _get_current_todolist_done_count(session_id)
# 如果没有 todolist,不检测
# 如果没有 todolist,不检查
if current_done < 0:
state["last_todolist_done_count"] = -1
state["todolist_stale_rounds"] = 0
@@ -259,7 +259,7 @@ def check_and_record_todolist_progress(
# 构建 todolist 摘要
try:
from agent.tools.tool import get_todo_store_data
from agent.tools.todo_list import get_todo_store_data
todo = get_todo_store_data(session_id)
steps = todo.get("steps", []) if todo else []
done_set = set(todo.get("done_steps", [])) if todo else set()
@@ -321,7 +321,7 @@ def build_loop_reflection_prompt(
"""
parts = []
# 检测 1: 连续相同工具调用 (Level 1 - 警告)
# 检查 1: 连续相同工具调用 (Level 1 - 警告)
tool_count, tool_name, args_summary = detect_repeated_tool_calls(messages, session_id)
cfg = get_loop_config()
warn_threshold = max(cfg["repeated_tool_warn"], 2)
@@ -336,7 +336,7 @@ def build_loop_reflection_prompt(
f"3. 如果确实无法推进,向用户说明当前困难并请求帮助"
)
# 检测 2: Todolist 进度停滞
# 检查 2: Todolist 进度停滞
is_stale, stale_rounds, stale_summary = check_and_record_todolist_progress(
messages, session_id
)
@@ -344,7 +344,7 @@ def build_loop_reflection_prompt(
if is_stale:
parts.append(
f"【进度停滞警告】TodoList 已有 {stale_rounds} 轮没有进展"
f"【进度停滞警告】TodoList 已有 {stale_rounds} 轮没有进度"
+ (f"({stale_summary})。" if stale_summary else "。")
+ f"\n请反思当前卡点,考虑是否需要调整计划或换一种方式继续。"
)
@@ -363,7 +363,7 @@ def build_force_end_message(
"""构建强制终止时向前端输出的异常循环消息。
输入:
tool_name: 循环调用的工具名。
tool_name: 循环调用的工具名称。
consecutive_count: 连续调用次数。
args_summary: 参数摘要。
+13 -13
View File
@@ -1,15 +1,15 @@
"""LangGraph 图节点:模型调用、反思机制与工具产物后处理。
系统定位:
实现 ReAct 循环中的 ``agent`` 节点工厂与 ``process_tool_artifact`` 节点,
被 ``graph`` 注册到 StateGraph。
实现 ReAct 循环中的 ``agent`` 节点工厂、``process_tool_artifact`` 节点。
在 ``graph`` 注册到 StateGraph。
反思机制:
反思机制:
``make_call_model_node`` 在每次调用 LLM 前注入反思引导提示,
模型收到后会在输出 tool_calls 的同时产出文本思考内容(思维链),
模型收到后会在输出 tool_calls 的同时产出文本思考内容(思维链)。
该文本思考内容被提取并记录到 think store 供前端实时展示。
可扩展性:
可扩展性:
- ``make_call_model_node`` 可注入 streaming、重试、用量统计回调。
- ``process_tool_artifact`` 可扩展 audio、文件、结构化 JSON 等 artifact 类型。
"""
@@ -32,7 +32,7 @@ def _filter_short_term_memory(messages: list, trajectory_rounds: int) -> list:
输入:
messages: 完整消息列表。
trajectory_rounds: 保留最近 N 轮的工具返回结果。0 或负数表示不过滤。
trajectory_rounds: 保留最近 N 轮的工具返回结果;或负数表示不过滤。
输出:
过滤后的消息列表。
@@ -76,7 +76,7 @@ def _get_todolist_status() -> str:
"""
try:
from agent.history.tool_call_recorder import get_current_session
from agent.tools.tool import get_todo_store_data
from agent.tools.todo_list import get_todo_store_data
sid = get_current_session()
if not sid:
return ""
@@ -125,7 +125,7 @@ def _build_reflection_prompt(agent_mode: str, is_first_call: bool, messages: lis
if todo_status:
parts.append(todo_status)
else:
parts.append("当前无 TodoList。")
parts.append("当前无 TodoList。)")
# 循环检测:注入反循环反思提示
loop_warning = build_loop_reflection_prompt(messages, session_id)
@@ -138,7 +138,7 @@ def _build_reflection_prompt(agent_mode: str, is_first_call: bool, messages: lis
def _is_first_agent_call(messages: list) -> bool:
"""判断是否为当前用户任务的首轮 agent 调用。
规则:找到最后一条非 synthetic 的 HumanMessage(即用户最新输入),
规则:找到最后一条非 synthetic 的 HumanMessage(即用户最新输入)。
检查其后是否存在 AIMessage。若不存在,说明 agent 尚未对该输入做出响应。
输入:
@@ -171,7 +171,7 @@ def make_call_model_node(
) -> Callable[[AgentState], dict]:
"""创建带反思机制的调用已绑定工具的 LLM 的图节点闭包。
每次调用前注入反思引导提示;调用后提取模型同时产出的文本思考内容(content)
每次调用前注入反思引导提示;调用后提取模型同时产出的文本思考内容(content),
记录到 think store,供前端实时展示思维链。Plan 模式下首次调用强制要求生成 todolist。
"""
@@ -193,7 +193,7 @@ def make_call_model_node(
filtered_messages = _filter_short_term_memory(all_messages, trajectory_rounds)
# 判断是否为首次 agent 调用(当前轮尚无 AIMessage)
# 判断是否为首轮 agent 调用(当前轮尚无 AIMessage)
is_first_call = _is_first_agent_call(filtered_messages)
# 获取 session_id 供循环检测使用
@@ -239,7 +239,7 @@ def make_call_model_node(
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
from agent.tools.todo_list import update_todo_store_from_tool_args
update_todo_store_from_tool_args(sid, tool_args)
except Exception:
pass
@@ -310,4 +310,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]}
+22 -22
View File
@@ -1,14 +1,14 @@
"""Agent 运行时:对话执行、历史持久化与人工确认续跑。
系统定位:
Web 层(如 ``web.ui_session``)与 LangGraph 之间的编排层。
Web 层(即 ``web.ui_session``)与 LangGraph 之间的编排层。
``agents/*`` 组装 ``AgentRuntime`` 实例,本模块提供统一执行 API。
主要流程:
execute_agent: 用户消息 -> summarizer 压缩 -> graph.invoke -> 响应/待确认
continue_after_human_action: 用户确认/拒绝 -> 工具执行或图续跑
可扩展性:
可扩展性:
- ``AgentRuntime`` 可增加 checkpointer、tracing、多模态配置。
- 可将 team 模式拆为独立 executor 类。
"""
@@ -45,15 +45,15 @@ class AgentRuntime:
graph: 编译后的 LangGraph。
llm: 原始 LLM 实例(压缩、TTS 等)。
tools: 工具列表。
tools_by_name: 工具名 -> 工具实例,供确认后手动 invoke。
tools_by_name: 工具名 → 工具实例,供确认后手动 invoke。
system_prompt: 系统提示词,传入 summarizer。
max_iterations: 最大迭代次数,超过则输出 OOM(默认 200)。
系统定位:
``agents/main_agent`` 等模块的装配结果,传给 execute_agent。
``agents/main_agent`` 等模块的装配结果,传入 execute_agent。
可扩展性:
可增加 default_mode、专属 checkpointer 等字段。
可扩展性:
可增加 default_mode、专用 checkpointer 等字段。
"""
name: str
@@ -114,7 +114,7 @@ def _invoke_with_recursion_limit(
try:
return graph.invoke(input_data, config=cfg)
except GraphRecursionError:
print(f"[OOM] Agent '{runtime.name}' 达到最大迭代次数 {runtime.max_iterations},输出 OOM。")
print(f"[OOM] Agent '{runtime.name}' 达到最大迭代次数 {runtime.max_iterations},输出 OOM。)")
return {"messages": [AIMessage(content=_OOM_MESSAGE)], "_oom": True}
@@ -132,7 +132,7 @@ def save_messages_to_history(session_id: str, user_msg: HumanMessage, assistant_
系统定位:
execute_agent 正常回复结束后持久化对话。
可扩展性:
可扩展性:
可改为异步写入、双写向量库、按 agent_name 分表。
"""
if not session_id:
@@ -181,7 +181,7 @@ def _persist_messages_from_pending_turn(
start_index = idx
break
# 只提取:
# - 用户消息(跳过 synthetic)
# - 用户消息(跳过 synthetic。)
# - 最终一条不包含 tool_calls 的助手消息
user_msg = None
final_assistant = None
@@ -220,7 +220,7 @@ def _invoke_after_manual_tool_result(
result = _invoke_with_recursion_limit(runtime, runtime.graph, {"messages": current_messages, "agent_mode": agent_mode}, {"configurable": {"session_id": session_id}})
all_messages = result["messages"]
# ★ 循环检测 Level 2:检查是否因死循环被强制终止
# 循环检测 Level 2:检查是否因死循环被强制终止
force_end, loop_tool, loop_args = loop_should_force_end(all_messages, session_id)
if force_end:
# 返回特殊标记:pending_meta 中携带 _loop_force_end 信息
@@ -291,12 +291,12 @@ def execute_agent(
agent_mode: agent(默认)| team(占位提示)。
输出:
更新后的 chat_history;可能追加 assistant 消息或 pending_action。
更新后的 chat_history;可能追加 assistant 消息、pending_action。
系统定位:
对外主入口,由 Web/API 层在每轮用户发送后调用。
可扩展性:
可扩展性:
可返回 generator 支持流式;可注入 middleware 做鉴权、限流。
"""
chat_history = chat_history or []
@@ -345,7 +345,7 @@ def execute_agent(
}
try:
# 开始实时记录(新 turn 隔离旧数据)
# 开始实时记录(按 turn 隔离旧数据)
turn_id = ""
if session_id:
turn_id = start_live_turn(session_id)
@@ -362,7 +362,7 @@ def execute_agent(
clear_abort_flag(session_id)
end_live_turn(session_id)
try:
from agent.tools.tool import clear_todo_store_session
from agent.tools.todo_list import clear_todo_store_session
clear_todo_store_session(session_id)
except Exception:
pass
@@ -388,7 +388,7 @@ def execute_agent(
if session_id:
clear_loop_session(session_id)
try:
from agent.tools.tool import clear_todo_store_session
from agent.tools.todo_list import clear_todo_store_session
clear_todo_store_session(session_id)
except Exception:
pass
@@ -399,7 +399,7 @@ def execute_agent(
if pending_meta:
end_live_turn(session_id)
_remember_pending_user_content(pending_meta, user_content)
# 存储 agent_mode 以便 continue 时恢复
# 存储 agent_mode 以便 continue 时恢复。
approval_id = _pending_action_id(pending_meta)
if approval_id and approval_id in PENDING_TOOL_APPROVALS:
PENDING_TOOL_APPROVALS[approval_id]["agent_mode"] = agent_mode
@@ -455,7 +455,7 @@ def execute_agent(
# 任务正常完成:清空 todolist 防止跨任务纠缠
if session_id:
try:
from agent.tools.tool import clear_todo_store_session
from agent.tools.todo_list import clear_todo_store_session
clear_todo_store_session(session_id)
except Exception:
pass
@@ -467,7 +467,7 @@ def execute_agent(
clear_abort_flag(session_id)
end_live_turn(session_id)
try:
from agent.tools.tool import clear_todo_store_session
from agent.tools.todo_list import clear_todo_store_session
clear_todo_store_session(session_id)
except Exception:
pass
@@ -483,7 +483,7 @@ def execute_agent(
set_current_turn("")
set_current_session("")
end_live_turn(session_id)
print("[ERROR] 调用模型时出错:")
print("[ERROR] 调用模型时出错")
traceback.print_exc()
chat_history.append({"role": "assistant", "content": "抱歉,我遇到了一些问题,请稍后再试。"})
except Exception:
@@ -491,7 +491,7 @@ def execute_agent(
set_current_turn("")
set_current_session("")
end_live_turn(session_id)
print("[ERROR] 调用模型时出错:")
print("[ERROR] 调用模型时出错")
traceback.print_exc()
chat_history.append({"role": "assistant", "content": "抱歉,我遇到了一些问题,请稍后再试。"})
@@ -527,7 +527,7 @@ def continue_after_human_action(
UI 确认框回调入口;按 pending.type 分支:
human_interaction / tool_call。
可扩展性:
可扩展性:
可增加 timeout 自动 reject;支持批量 approve 多个 tool_call。
"""
chat_history = chat_history or []
@@ -661,7 +661,7 @@ def continue_after_human_action(
save_turn(session_id, manual_turn_id, {"role": "user", "content": pending.get("user_content") or ""}, persisted_messages)
# 任务完成:清空 todolist
try:
from agent.tools.tool import clear_todo_store_session
from agent.tools.todo_list import clear_todo_store_session
clear_todo_store_session(session_id)
except Exception:
pass
+14
View File
@@ -120,5 +120,19 @@
"permission": "direct",
"auto_execute_rules": [],
"enabled": true
},
{
"name": "skill_router",
"display_name": "SkillRouter",
"description": "Skill 路由工具。根据用户的 query(问题或任务描述),从已安装的 skill 中智能匹配最相关的操作指南,并返回指南全文。",
"permission": "confirm",
"auto_execute_rules": [
{
"parameter": "query",
"operator": "exists",
"value": ""
}
],
"enabled": true
}
]
+4
View File
@@ -98,9 +98,13 @@ def _match_auto_execute_rule(rule: dict, args: dict) -> bool:
支持的 operator:
- "equals": 参数值(转为字符串后)完全等于 rule value。
- "exists": 参数存在(不为 None/空字符串)即匹配,忽略 value。
"""
param = rule.get("parameter", "")
op = rule.get("operator", "equals")
if op == "exists":
actual = args.get(param)
return actual is not None and actual != ""
expected = str(rule.get("value", ""))
actual = str(args.get(param, ""))
if op == "equals":
Binary file not shown.
@@ -1,58 +0,0 @@
{
"version": 3,
"messages": [
{
"role": "user",
"user": {
"content": [
{
"type": "text",
"text": "帮我整理2026年美加墨世界杯的小组赛出线情况以及1/16决赛对阵情况并将其保存到桌面"
}
]
}
},
{
"role": "assistant",
"assistant": {
"text": "需要你确认后继续执行。",
"output_type": "text",
"autoplay": false,
"audio_duration_seconds": null,
"audio_relpath": null,
"pending_action": {
"id": "4a6b7e530f51404198747baec7717c68",
"type": "tool_call",
"title": "待确认执行工具:doc_tool",
"tool_name": "doc_tool",
"args": {
"name": "doc_tool",
"args": {
"action": "create",
"content": "# 2026年美加墨世界杯小组赛出线情况与1/16决赛对阵\n\n## 一、基本信息\n- **赛事名称**:2026年FIFA世界杯(2026 FIFA World Cup 2026™)\n- **举办国家**:加拿大、墨西哥、美国\n- **参赛球队**:48支(首届48队赛制)\n- **小组数量**:12个小组(A-L组),每组4支球队\n- **出线规则**:每个小组前两名(共24支)+ 8个成绩最好的第三名,共32支球队晋级淘汰赛(Round of 32)\n\n---\n\n## 二、小组赛出线情况(12个小组最终 standings)\n\n### Group A\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 墨西哥 (Mexico) | 9分(小组第一) |\n| 2 | 南非 (South Africa) | 4分(小组第二) |\n| 3 | 捷克 (Czechia) | - |\n| 4 | 韩国 (Republic of Korea) | - |\n| **出线球队** | **墨西哥、南非** | |\n\n### Group B\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 瑞士 (Switzerland) | 7分(小组第一) |\n| 2 | 加拿大 (Canada) | -(小组第二) |\n| 3 | 波黑 (Bosnia and Herzegovina) | - |\n| 4 | 卡塔尔 (Qatar) | - |\n| **出线球队** | **瑞士、加拿大** | |\n\n### Group C\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 巴西 (Brazil) | -(小组第一) |\n| 2 | 摩洛哥 (Morocco) | -(小组第二) |\n| 3 | 海地 (Haiti) | - |\n| 4 | 苏格兰 (Scotland) | - |\n| **出线球队** | **巴西、摩洛哥** | |\n\n### Group D\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 德国 (Germany) | -(小组第一) |\n| 2 | 日本 (Japan) | -(小组第二) |\n| 3 | 巴拉圭 (Paraguay) | -(第三名出线) |\n| 4 | 美国 (USA) | - |\n| **出线球队** | **德国、日本、巴拉圭**(含最佳第三名) | |\n\n### Group E\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 西班牙 (Spain) | -(小组第一) |\n| 2 | 奥地利 (Austria) | -(小组第二) |\n| 3 | 佛得角 (Cape Verde) | - |\n| 4 | 乌拉圭 (Uruguay) | - |\n| **出线球队** | **西班牙、奥地利** | |\n\n### Group F\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 葡萄牙 (Portugal) | -(小组第一) |\n| 2 | 克罗地亚 (Croatia) | -(小组第二) |\n| 3 | 丹麦 (Denmark) | - |\n| 4 | 希腊 (Greece) | - |\n| **出线球队** | **葡萄牙、克罗地亚** | |\n\n### Group G\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 阿根廷 (Argentina) | -(小组第一) |\n| 2 | 埃及 (Egypt) | -(小组第二) |\n| 3 | 澳大利亚 (Australia) | -(第三名出线) |\n| 4 | 英格兰 (England) | - |\n| **出线球队** | **阿根廷、埃及、澳大利亚**(含最佳第三名) | |\n\n### Group H\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 法国 (France) | -(小组第一) |\n| 2 | 瑞典 (Sweden) | -(小组第二) |\n| 3 | 科特迪瓦 (Côte d'Ivoire) | - |\n| 4 | 挪威 (Norway) | - |\n| **出线球队** | **法国、瑞典** | |\n\n### Group I\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 哥伦比亚 (Colombia) | -(小组第一) |\n| 2 | 比利时 (Belgium) | -(小组第二) |\n| 3 | 塞内加尔 (Senegal) | -(第三名出线) |\n| 4 | 伊朗 (Iran) | - |\n| **出线球队** | **哥伦比亚、比利时、塞内加尔**(含最佳第三名) | |\n\n### Group J\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 英格兰 (England) | -(小组第一) |\n| 2 | 美国 (USA) | -(小组第二) |\n| 3 | 波黑 (Bosnia and Herzegovina) | - |\n| 4 | 厄瓜多尔 (Ecuador) | - |\n| **出线球队** | **英格兰、美国** | |\n\n### Group K\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 荷兰 (Netherlands) | -(小组第一) |\n| 2 | 阿尔及利亚 (Algeria) | -(小组第二) |\n| 3 | 其他球队 | - |\n| 4 | 其他球队 | - |\n| **出线球队** | **荷兰、阿尔及利亚** | |\n\n### Group L\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 加纳 (Ghana) | -(小组第一) |\n| 2 | 其他球队 | -(小组第二) |\n| 3 | 其他球队 | - |\n| 4 | 其他球队 | - |\n| **出线球队** | **加纳**(小组第一) | |\n\n> **注**:由于搜索结果信息存在部分碎片化和不一致,部分小组的完整 standings 可能存在不准确之处。以上信息基于多次网络搜索整合而成。\n\n---\n\n## 三、1/16决赛(Round of 32)对阵情况\n\n1/16决赛于2026年6月28日至7月3日举行,共16场比赛。以下为已确认的对阵和部分结果:\n\n### 已进行的比赛结果\n\n| 日期 | 对阵 | 比分 | 备注 |\n|------|------|------|------|\n| 6月28日 | 南非 (South Africa) vs 加拿大 (Canada) | 0-1 | 加拿大晋级 |\n| 6月29日 | 巴西 (Brazil) vs 日本 (Japan) | 2-1 | 巴西晋级 |\n| 6月29日 | 德国 (Germany) vs 巴拉圭 (Paraguay) | 1-1 (点球 3-4) | 巴拉圭点球胜出晋级 |\n| 6月28日 | 荷兰 (Netherlands) vs 摩洛哥 (Morocco) | 1-1 (点球 2-3) | 摩洛Line truncated
}
},
"instruction": "{\n \"name\": \"doc_tool\",\n \"args\": {\n \"action\": \"create\",\n \"content\": \"# 2026年美加墨世界杯小组赛出线情况与1/16决赛对阵\\n\\n## 一、基本信息\\n- **赛事名称**:2026年FIFA世界杯(2026 FIFA World Cup 2026™)\\n- **举办国家**:加拿大、墨西哥、美国\\n- **参赛球队**:48支(首届48队赛制)\\n- **小组数量**:12个小组(A-L组),每组4支球队\\n- **出线规则**:每个小组前两名(共24支)+ 8个成绩最好的第三名,共32支球队晋级淘汰赛(Round of 32)\\n\\n---\\n\\n## 二、小组赛出线情况(12个小组最终 standings)\\n\\n### Group A\\n| 排名 | 球队 | 积分 |\\n|------|------|------|\\n| 1 | 墨西哥 (Mexico) | 9分(小组第一) |\\n| 2 | 南非 (South Africa) | 4分(小组第二) |\\n| 3 | 捷克 (Czechia) | - |\\n| 4 | 韩国 (Republic of Korea) | - |\\n| **出线球队** | **墨西哥、南非** | |\\n\\n### Group B\\n| 排名 | 球队 | 积分 |\\n|------|------|------|\\n| 1 | 瑞士 (Switzerland) | 7分(小组第一) |\\n| 2 | 加拿大 (Canada) | -(小组第二) |\\n| 3 | 波黑 (Bosnia and Herzegovina) | - |\\n| 4 | 卡塔尔 (Qatar) | - |\\n| **出线球队** | **瑞士、加拿大** | |\\n\\n### Group C\\n| 排名 | 球队 | 积分 |\\n|------|------|------|\\n| 1 | 巴西 (Brazil) | -(小组第一) |\\n| 2 | 摩洛哥 (Morocco) | -(小组第二) |\\n| 3 | 海地 (Haiti) | - |\\n| 4 | 苏格兰 (Scotland) | - |\\n| **出线球队** | **巴西、摩洛哥** | |\\n\\n### Group D\\n| 排名 | 球队 | 积分 |\\n|------|------|------|\\n| 1 | 德国 (Germany) | -(小组第一) |\\n| 2 | 日本 (Japan) | -(小组第二) |\\n| 3 | 巴拉圭 (Paraguay) | -(第三名出线) |\\n| 4 | 美国 (USA) | - |\\n| **出线球队** | **德国、日本、巴拉圭**(含最佳第三名) | |\\n\\n### Group E\\n| 排名 | 球队 | 积分 |\\n|------|------|------|\\n| 1 | 西班牙 (Spain) | -(小组第一) |\\n| 2 | 奥地利 (Austria) | -(小组第二) |\\n| 3 | 佛得角 (Cape Verde) | - |\\n| 4 | 乌拉圭 (Uruguay) | - |\\n| **出线球队** | **西班牙、奥地利** | |\\n\\n### Group F\\n| 排名 | 球队 | 积分 |\\n|------|------|------|\\n| 1 | 葡萄牙 (Portugal) | -(小组第一) |\\n| 2 | 克罗地亚 (Croatia) | -(小组第二) |\\n| 3 | 丹麦 (Denmark) | - |\\n| 4 | 希腊 (Greece) | - |\\n| **出线球队** | **葡萄牙、克罗地亚** | |\\n\\n### Group G\\n| 排名 | 球队 | 积分 |\\n|------|------|------|\\n| 1 | 阿根廷 (Argentina) | -(小组第一) |\\n| 2 | 埃及 (Egypt) | -(小组第二) |\\n| 3 | 澳大利亚 (Australia) | -(第三名出线) |\\n| 4 | 英格兰 (England) | - |\\n| **出线球队** | **阿根廷、埃及、澳大利亚**(含最佳第三名) | |\\n\\n### Group H\\n| 排名 | 球队 | 积分 |\\n|------|------|------|\\n| 1 | 法国 (France) | -(小组第一) |\\n| 2 | 瑞典 (Sweden) | -(小组第二) |\\n| 3 | 科特迪瓦 (Côte d'Ivoire) | - |\\n| 4 | 挪威 (Norway) | - |\\n| **出线球队** | **法国、瑞典** | |\\n\\n### Group I\\n| 排名 | 球队 | 积分 |\\n|------|------|------|\\n| 1 | 哥伦比亚 (Colombia) | -(小组第一) |\\n| 2 | 比利时 (Belgium) | -(小组第二) |\\n| 3 | 塞内加尔 (Senegal) | -(第三名出线) |\\n| 4 | 伊朗 (Iran) | - |\\n| **出线球队** | **哥伦比亚、比利时、塞内加尔**(含最佳第三名) | |\\n\\n### Group J\\n| 排名 | 球队 | 积分 |\\n|------|------|------|\\n| 1 | 英格兰 (England) | -(小组第一) |\\n| 2 | 美国 (USA) | -(小组第二) |\\n| 3 | 波黑 (Bosnia and Herzegovina) | - |\\n| 4 | 厄瓜多尔 (Ecuador) | - |\\n| **出线球队** | **英格兰、美国** | |\\n\\n### Group K\\n| 排名 | 球队 | 积分 |\\n|------|------|------|\\n| 1 | 荷兰 (Netherlands) | -(小组第一) |\\n| 2 | 阿尔及利亚 (Algeria) | -(小组第二) |\\n| 3 | 其他球队 | - |\\n| 4 | 其他球队 | - |\\n| **出线球队** | **荷兰、阿尔及利亚** | |\\n\\n### Group L\\n| 排名 | 球队 | 积分 |\\n|------|------|------|\\n| 1 | 加纳 (Ghana) | -(小组第一) |\\n| 2 | 其他球队 | -(小组第二) |\\n| 3 | 其他球队 | - |\\n| 4 | 其他球队 | - |\\n| **出线球队** | **加纳**(小组第一) | |\\n\\n> **注**:由于搜索结果信息存在部分碎片化和不一致,部分小组的完整 standings 可能存在不准确之处。以上信息基于多次网络搜索整合而成。\\n\\n---\\n\\n## 三、1/16决赛(Round of 32)对阵情况\\n\\n1/16决赛于2026年6月28日至7月3日举行,共16场比赛。以下为已确认的对阵和部分结果:\\n\\n### 已进行的比赛结果\\n\\n| 日期 | 对阵 | 比分 | 备注 |\\n|------|------|------|------|\\n| 6月28日 | 南非 (South Africa) vs 加拿大 (Canada) | 0-1 | 加拿大晋级 |\\n| 6月29日 | 巴西 (Brazil) vs 日Line truncated
"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
}
}
]
}
@@ -1,16 +0,0 @@
{
"version": 3,
"messages": [
{
"role": "user",
"user": {
"content": [
{
"type": "text",
"text": "1、打开https://erpdemo.fanpusoft.com/main.aspx#\\r\\n2、点击采购管理\\r\\n3、点击新增供应商\\r\\n4、在新增供应商页面进行以下操作:\\r\\n输入供应商名称:“minor”、输入供应商类别:“工程材料供应商”、输入部门:“采购部门B”、输入职务:“CEO”"
}
]
}
}
]
}
@@ -1,28 +0,0 @@
{
"version": 3,
"messages": [
{
"role": "user",
"user": {
"content": [
{
"type": "text",
"text": "1、打开https://erpdemo.fanpusoft.com/main.aspx#\\\\r\\\\n2、点击采购管理\\\\r\\\\n3、点击新增供应商\\\\r\\\\n4、在新增供应商页面进行以下操作:\\\\r\\\\n输入供应商名称:“minor”、输入供应商类别:“工程材料供应商”、输入部门:“采购部门B”、输入职务:“CEO”"
}
]
}
},
{
"role": "assistant",
"assistant": {
"text": "1. 分析上一轮执行结果:上一步调用了gui_tool点击“确定”按钮,截图显示提示框已关闭,当前处于新增供应商页面,所有字段均已正确填写并保存成功。\n2. 任务目标达成:\n - 供应商名称:\"minor\" (已填)\n - 供应商类别:\"工程材料供应商\" (已填)\n - 部门:\"采购部门B\" (已填,且已修正并保存)\n - 职务:\"CEO\" (已填)\n3. 下一步操作:任务已完成,直接回复用户。\n\n任务已完成。已成功新增供应商,信息如下:\n- 供应商名称:minor\n- 供应商类别:工程材料供应商\n- 部门:采购部门B\n- 职务:CEO\n系统提示“保存数据成功”。",
"output_type": "text",
"autoplay": false,
"audio_duration_seconds": null,
"audio_relpath": null,
"pending_action": null,
"agent_mode": null
}
}
]
}
@@ -1,28 +0,0 @@
{
"version": 3,
"messages": [
{
"role": "user",
"user": {
"content": [
{
"type": "text",
"text": "1、打开https://erpdemo.fanpusoft.com/main.aspx#\\\\r\\\\n2、点击采购管理\\\\r\\\\n3、点击新增供应商\\\\r\\\\n4、在新增供应商页面进行以下操作:\\\\r\\\\n输入供应商名称:“minor”、输入供应商类别:“工程材料供应商”、输入部门:“采购部门B”、输入职务:“CEO”"
}
]
}
},
{
"role": "assistant",
"assistant": {
"text": "1. 先分析上一轮执行结果:上一个调用了 gui_tool(test) 在“职务”输入框中输入了“CEO”。从截图来看,“职务”输入框中已经成功显示了“CEO”,输入操作成功。\n2. 回顾任务要求,需要输入的信息包括:\n - 供应商名称:“minor” -> 已完成\n - 供应商类别:“工程材料供应商” -> 截图显示默认值即为“工程材料供应商”,符合要求。\n - 部门:“采购部门B” -> 已完成\n - 职务:“CEO” -> 已完成\n3. 所有要求的字段均已按要求填入。任务基本完成(虽然通常接下来会点击保存,但任务描述中只要求输入这些操作)。\n4. 既然所有指定的输入操作都已完成,我可以向用户报告任务完成。\n\n任务已完成。已在新增供应商页面成功输入:\n- 供应商名称:minor\n- 供应商类别:工程材料供应商\n- 部门:采购部门B\n- 职务:CEO",
"output_type": "text",
"autoplay": false,
"audio_duration_seconds": null,
"audio_relpath": null,
"pending_action": null,
"agent_mode": null
}
}
]
}
@@ -7,7 +7,7 @@
"content": [
{
"type": "text",
"text": "1、打开https://erpdemo.fanpusoft.com/main.aspx#\r\n2、点击采购管理\r\n3、点击新增供应商\r\n4、在新增供应商页面进行以下操作:\r\n输入供应商名称:“minor”、输入供应商类别:“工程材料供应商”、输入部门:“采购部门B”、输入职务:“CEO”"
"text": "1、打开https://erpdemo.fanpusoft.com/main.aspx#\r\n2、点击采购管理\r\n3、点击新增供应商\r\n4、在新增供应商页面进行以下操作:输入供应商名称:“minor”、输入供应商类别:“工程材料供应商”、输入部门:“采购部门B”、输入职务:“CEO”、输入联系人:“赵永顺”、输入生日:“2004/07/09”、点击保存"
}
]
}
@@ -0,0 +1,39 @@
{
"version": 3,
"messages": [
{
"role": "user",
"user": {
"content": [
{
"type": "text",
"text": "1、打开https://erpdemo.fanpusoft.com/main.aspx#\r\n2、点击采购管理\r\n3、点击新增供应商\r\n4、在新增供应商页面进行以下操作:输入供应商名称:“minor”、输入供应商类别:“工程材料供应商”、输入部门:“采购部门B”、输入职务:“CEO”、输入联系人:“赵永顺”、输入生日:“2004/07/09”\r\n5、点击保存、点击确定"
}
]
}
},
{
"role": "user",
"user": {
"content": [
{
"type": "text",
"text": "1、打开https://erpdemo.fanpusoft.com/main.aspx#\r\n2、点击采购管理\r\n3、点击新增供应商\r\n4、在新增供应商页面进行以下操作:输入供应商名称:“minor”、输入供应商类别:“工程材料供应商”、输入部门:“采购部门B”、输入职务:“CEO”、输入联系人:“赵永顺”、输入生日:“2004/07/09”\r\n5、点击保存、点击确定"
}
]
}
},
{
"role": "assistant",
"assistant": {
"text": "1. 分析上一轮执行结果:点击保存后,系统显示“保存成功”。通常接下来需要点击“确定”或刷新/返回列表。截图显示保存成功提示,但没有出现弹窗的“确定”按钮。\n2. 观察截图:点击保存后,页面上方显示“保存成功”的文字提示,没有弹窗。\n3. 重新审视任务:任务要求“点击保存、点击确定”。既然保存成功后没有弹窗,可能是点击保存后任务已完成,或者需要手动关闭提示。通常这种简单的提示点击空白处或忽略即可,或者系统自动跳转。\n4. 再次仔细看图,发现“保存成功”四个字,并没有“确定”按钮。这可能意味着保存操作本身就是一个确认,或者“确定”按钮在别处。\n5. 不过,有些系统保存后会弹出模态框。让我检查一下是否漏看了。并没有模态框。\n6. 也许“确定”是指完成整个流程,或者保存后自动返回列表。\n7. 如果确实没有“确定”按钮,可能任务在点击“保存”后实质上已经完成了数据的录入。\n8. 再次观察,确实没有“确定”按钮。那我就认为点击保存后操作结束,或者系统自动处理了确认。\n9. 但是,如果必须点击“确定”,而界面上没有,可能需要关闭浏览器的消息提示?也没有。\n10. 最可能的情况是,保存成功后,系统默认认为操作已完成。我将向用户确认任务已完成。\n\n最终回复:任务已完成。供应商“minor”的信息已成功保存。",
"output_type": "text",
"autoplay": false,
"audio_duration_seconds": null,
"audio_relpath": null,
"pending_action": null,
"agent_mode": null
}
}
]
}
@@ -1,383 +0,0 @@
{
"session_id": "20260703_181818",
"turn_id": "human_4a6b7e530f51404198747baec7717c68",
"user_message": {
"role": "user",
"content": "帮我整理2026年美加墨世界杯的小组赛出线情况以及1/16决赛对阵情况并将其保存到桌面"
},
"tool_calls": [
{
"name": "todo_list",
"args": {
"steps": "[\"打开浏览器并访问网络,搜索2026年美加墨世界杯小组赛出线情况\", \"查看并记录2026年美加墨世界杯32支球队的分组情况和出线球队信息\", \"搜索2026年美加墨世界杯1/16决赛对阵情况\", \"查看并记录1/16决赛的对阵安排\", \"整理收集到的小组赛出线情况和1/16决赛对阵信息\", \"创建文档并将整理好的信息保存到桌面\"]"
},
"result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"打开浏览器并访问网络,搜索2026年美加墨世界杯小组赛出线情况\", \"查看并记录2026年美加墨世界杯32支球队的分组情况和出线球队信息\", \"搜索2026年美加墨世界杯1/16决赛对阵情况\", \"查看并记录1/16决赛的对阵安排\", \"整理收集到的小组赛出线情况和1/16决赛对阵信息\", \"创建文档并将整理好的信息保存到桌面\"], \"done_steps\": []}}\n[Todo List]\n- [1/6] [Pending]: 打开浏览器并访问网络,搜索2026年美加墨世界杯小组赛出线情况\n- [2/6] [Pending]: 查看并记录2026年美加墨世界杯32支球队的分组情况和出线球队信息\n- [3/6] [Pending]: 搜索2026年美加墨世界杯1/16决赛对阵情况\n- [4/6] [Pending]: 查看并记录1/16决赛的对阵安排\n- [5/6] [Pending]: 整理收集到的小组赛出线情况和1/16决赛对阵信息\n- [6/6] [Pending]: 创建文档并将整理好的信息保存到桌面",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026年美加墨世界杯小组赛出线情况 16决赛对阵"
},
"result_text": "2026年7月1日凌晨,挪威国家队在美加墨世界杯1/16决赛中打败科特迪瓦成功晋级。挪威国内全民的“划船”助威,万人齐呼的助威声浪一度撼动测震仪,卑尔根大学公布测震仪 ... 全新赛制带来的悬念\":小组前两名+ 8 个最佳第三名\" 的晋级规则,让小组赛的每一场比赛都充满悬念。淘汰赛新增的1/16 决赛,也会带来更多的强强对话和爆冷机会。 目录 · 1 赛制. 1.1 十六分之一决赛对赛组合 · 2 晋级球队 · 3 晋级图 · 4 十六分之一决赛 · 5 八分之一决赛. 5.1 加拿大对摩洛哥; 5.2 巴拉圭对法国 · 6 四分之一决赛. 6.1 赛事 ... 1/16决赛:6月28日—7月3日,32支晋级球队两两对决,决出16支球队进入下一轮;. 1/8决赛:7月4日—7月8日,16支球队角逐,产生8支晋级球队;. 克罗地亚将对阵葡萄牙,巴西将迎战日本,德国将面对巴拉圭,法国队已经确定将在16强迎战瑞典,若顺利晋级,他们可能在八分之一决赛遭遇德国或巴拉圭。对阿根廷 ... 2026美加墨世界杯共有48支参赛队伍,分为16个小组,每组前两名晋级32强,再通过附加赛产生16强?请注意:2026年世界杯赛制改革,小组赛将产生32支晋级球队,随后进行1/16决赛,胜者 ... 1/16决赛(32强赛):6月29日-7月3日,合计16场对决,决出16个1/8决赛晋级名额 · 1/8决赛(16强赛):7月3日-7月5日,合计8场对决,决出8个四分之一决赛名额 · 四分之 ... 视频简介. 2026年美加墨世界杯:小组赛末轮继续进行,多支球队晋级淘汰赛。 · 栏目信息. 栏目名称:. 栏目介绍:. 2026美加墨世界杯扩军为48队参赛,本届F组入围球队为荷兰、日本、突尼斯、瑞典,6场小组赛将在阿灵顿、蒙特雷、休斯顿等场地进行。扩军后除小组前两名直接 ... 从小组赛中看法国队最具冠军相。阿根廷虽然不错但过分依赖梅西。 23:40. Go to channel FIF · 盘点日本历届世界杯淘汰赛,0胜绩5战皆败,2次输点球2次遭 ...",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup group stage qualified teams list"
},
"result_text": "Qualified teams for the FIFA World Cup 2026 ; CanadaCanada ; MexicoMexico ; USAUSA ; JapanJapan ; New ZealandNew Zealand. Find out which teams have booked their place in the Round of 32 and who has been eliminated from the group stage at the FIFA World Cup 2026. The 2026 FIFA World Cup qualification decided the 45 teams that joined hosts Canada, Mexico, and the United States at the 2026 FIFA World Cup. Visit FIFA.com to find the latest news, interviews, key stats, fixtures and results for each team competing at FIFA World Cup 2026™. Men's 2026 World Cup Standings ; 1. Colombia · 3 ; 2. Portugal · 3 ; 3. DR Congo · 3 ; 4. Uzbekistan · 3. The FIFA World Cup 2026™ will be the 23rd edition of the tournament but the first to feature 48 teams and three host countries: Canada, Mexico and the ... The 2026 World Cup is SO CLOSE so here is the ULTIMATE video to review how every team has qualified for the tournament. Below is a complete look at every group's final standings, which teams advanced, which were eliminated and the full knockout round schedule. Latest news and scores from the FIFA World Cup 26™ qualifiers, including in-depth qualification guides and live data. Come check this. OCR. TOP 10 FIFA RANKING TEAMS 1 TOP 10 GOAL SCORING TEAMS (FIFA WORLD CUP 2026) 1 Argentina 1 2 France Germany 9 Goals ...",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup all 48 qualified teams final list by confederation"
},
"result_text": "Visit FIFA.com to find the latest news, interviews, key stats, fixtures and results for each team competing at FIFA World Cup 2026™. Qualified teams for the FIFA World Cup 2026 ; CanadaCanada ; MexicoMexico ; USAUSA ; JapanJapan ; New ZealandNew Zealand. The 2026 FIFA World Cup qualification decided the 45 teams that joined hosts Canada, Mexico, and the United States at the 2026 FIFA World Cup. The graphic shows the growing list of qualified teams, featuring heavyweights like Brazil, Argentina, France, England, Germany, Portugal, ... The FIFA World Cup 2026™ will be the 23rd edition of the tournament but the first to feature 48 teams and three host countries: Canada, Mexico and the ... All Teams Qualified for World Cup 2026: Complete List and Final Bracket. 126 views. All Teams Qualified for 2026 World Cup: Complete FIFA World Cup 2026 Lineup. The complete 2026 qualified teams list contains 48 national sides arranged across 12 groups in Canada, Mexico and the United States. Find all of the matches, results, scores and group standings from the men's football World Cup in the USA, Canada and Mexico. Algeria (ALG). # POS. PLAYER NAME. FIRST NAME(S). LAST NAME(S). NAME ON SHIRT. DOB. CLUB. HEIGHT (CM) CAPS GOALS. GK MASTIL Melvin. Melvin Feycal. The 2026 FIFA World Cup, the 23rd edition of the competition, will feature 48 teams for the first time in its history.",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 World Cup qualification latest qualified teams each confederation 2025"
},
"result_text": "Qualified teams for the FIFA World Cup 2026 ; CanadaCanada ; MexicoMexico ; USAUSA ; JapanJapan ; New ZealandNew Zealand. The 2026 FIFA World Cup qualification decided the 45 teams that joined hosts Canada, Mexico, and the United States at the 2026 FIFA World Cup. Here's a running tracker of every qualified nation: ; 2. Canada, CONCACAF, Co-host ; 3. Mexico, CONCACAF, Co-host ; 4. Japan, AFC, AFC Group C winners ; 5. New ... Latest news and scores from the FIFA World Cup 26™ qualifiers, including in-depth qualification guides and live data. Find out which teams have booked their place in the Round of 32 and who has been eliminated from the group stage at the FIFA World Cup 2026. Confirmed Qualified Teams by Confederation ; Confederation. Qualified Teams. Slots Allocated ; UEFA (Europe). Germany, Spain, France, England, Italy, Netherlands, ... ... every team has qualified for the tournament. This is basically a HUGE supercut of all the world cup qualifier review vids so grab yourself a ... Qualified teams ; CAF · Second round winners, DR Congo, 16 November 2025 ; CONCACAF · Third round best two group runners-up · Jamaica, 18 November 2025. Qualified teams (45/45) ; - Europe (16 of 16 qualifiers): England, France, Croatia, Norway, Portugal, Germany, Netherlands, Switzerland, Scotland ... The complete 2026 qualified teams list contains 48 national sides arranged across 12 groups in Canada, Mexico and the United States.",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup complete 48 teams list all groups qualified"
},
"result_text": "Visit FIFA.com to find the latest news, interviews, key stats, fixtures and results for each team competing at FIFA World Cup 2026™. FIFA World Cup Czechia, England, France, Germany, Netherlands, Norway, Portugal, Scotland, Spain, Sweden, Switzerland, Türkiye (48 of 48 teams). Discover the 48 teams taking part in the FIFA World Cup 2026 Canada, Mexico, USA™, including host cities, groups and game dates. FIFA World Cup 2026: All group standings · Group A - Mexico, South Africa, Czechia, Republic of Korea · Group B - Bosnia and Herzegovina, Canada, ... The 2026 FIFA World Cup qualification decided the 45 teams that joined hosts Canada, Mexico, and the United States at the 2026 FIFA World Cup. The FIFA World Cup 2026TM ・ the first to feature 48 teams and three host countries: Canada, Mexico ・ showing all 104 FIFA World Cup 2026 matches . Read more ... The 2026 World Cup group stage is officially in the books, and we are left with 32 of the 48 teams that originally qualified for tournament ... The 2026 FIFA World Cup will feature 48 teams, including hosts USA, Mexico, and Canada, who qualify automatically. As of now, approximately 28 ... Men's 2026 World Cup Standings ; 1. England · 3 ; 2. Croatia · 3 ; 3. Ghana · 3 ; 4. Panama · 3. TRN World Football News 7 World Cup Power Rankings: Tiering all 48 tournament teams ... WORLD CUP 2026 RANKING ALL 48 TEAMS The definitive power ...",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 World Cup qualifiers 2024 2025 latest qualified teams complete list"
},
"result_text": "Qualified teams for the FIFA World Cup 2026 ; CanadaCanada ; MexicoMexico ; USAUSA ; JapanJapan ; New ZealandNew Zealand. The 2026 FIFA World Cup qualification decided the 45 teams that joined hosts Canada, Mexico, and the United States at the 2026 FIFA World Cup. Find out which teams have booked their place in the Round of 32 and who has been eliminated from the group stage at the FIFA World Cup 2026. Qualified teams (45/45) ; - Europe (16 of 16 qualifiers): England, France, Croatia, Norway, Portugal, Germany, Netherlands, Switzerland, Scotland ... Latest news and scores from the FIFA World Cup 26™ qualifiers, including in-depth qualification guides and live data. Teams that have qualified for Round of 16 at FIFA World Cup 2026 · Canada · Brazil · Paraguay · Morocco · Norway · France · Mexico · England ... Full List: The Teams That Have Qualified for FIFA World Cup 26™ ; Group C, Group D ; Brazil · USA ; Morocco · Paraguay ; Haiti · Australia ; Scotland ... All Teams Qualified for 2026 World Cup: Complete List & Schedule · Understanding the Qualification Structure · Confederation Winners and Pathways · Direct ... The 2026 World Cup is SO CLOSE so here is the ULTIMATE video to review how every team has qualified for the tournament. 2026 FIFA World Cup: Who Has Qualified? Who Can Make It? · Algeria · Cape Verde · Egypt · Ghana · Ivory Coast · Morocco · Senegal · South Africa ...",
"result_files": [],
"result_images": []
},
{
"name": "date_query_class",
"args": {},
"result_text": "2026-07-03 18:21:36",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup group stage results qualified teams round of 32 round of 16 fixtures"
},
"result_text": "Football: FIFA World Cup 2026 bracket: Round of 32 full schedule – Live updates · South Africa 0-1 Canada · Brazil 2-1 Japan · Germany 1-1 Paraguay ... On this page you'll find the full FIFA World Cup 2026™ schedule, including all match fixtures, dates and venues, plus live results and where ... 1 Format. 1.1 Combinations of matches in the round of 32 · 2 Qualified teams · 3 Bracket · 4 Round of 32 · 5 Round of 16. 5.1 Canada vs Morocco; 5.2 Paraguay vs ... How will they decide which teams two teams play knockout match against each other in round of 32. 15:42 · Go to channel Timnas Super ... World Cup 2026 bracket and knockout fixtures - who's facing who in the last 32 and route to final ; Germany · Paraguay ; France · Sweden ; South ... Spain clinched first place in the group with a 1-0 win over Uruguay. The Spaniards will take on Austria on Thursday in Los Angeles. Cape Verde ... Every team plays three matches within its group. The top two teams from each group, along with the eight best third-placed teams, advance to a new Round of 32. Group H: Spain 4-0 Saudi Arabia (Atlanta) Group G: Belgium 0-0 Iran (Inglewood, Calif.) Group H: Uruguay 2-2 Cape Verde (Miami Gardens, Fla.) 2026 World Cup: All the European sides' group stage and knockout stage fixtures and results ; Austria · : W 3-1 vs Jordan (San Francisco Bay Area) 2026 FIFA World Cup Qualified Teams Host Nations United States, Canada, Mexico European Qualifiers England, France, Croatia, Portugal, Norway, ...",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup all 48 teams groups final results qualified to round of 32 complete list"
},
"result_text": "Find all of the matches, results, scores and group standings from the men's football World Cup in the USA, Canada and Mexico. Group K: Colombia, Portugal, Democratic Republic of the Congo. Group L: England, Croatia, Ghana. Soccer Football - FIFA World Cup 2026 - Group ... Visit FIFA.com to find the latest news, interviews, key stats, fixtures and results for each team competing at FIFA World Cup 2026™. Eliminated teams in the round of 32 · South Africa · Japan · Germany · Netherlands · Ivory Coast · Sweden · Ecuador · DR Congo ... The 2026 FIFA World Cup qualification decided the 45 teams that joined hosts Canada, Mexico, and the United States at the 2026 FIFA World Cup. Men's 2026 World Cup Standings ; 1. Netherlands · 3 ; 2. Japan · 3 ; 3. Sweden · 3 ; 4. Tunisia · 3. The 2026 FIFA World Cup will feature 48 teams, including hosts USA, Mexico, and Canada, who qualify automatically. As of now, approximately 28 ... Below is a complete look at every group's final standings, which teams advanced, which were eliminated and the full knockout round schedule. FIFA WORLD CUP 2026 48 TEAMS, 16 GROUPS, 1 CHAMPION UNITED STATES ... FIFA WORLD CUP 2026 ALL GROUPS USA CANADA MEXICO ONE WORLD NEW YORK UNITED ... FIFA World Cup 2026 | Round Of 32 Teams List | Match Results & Table Standings | World Cup 2026 | Group A - L Matchday 18 This update breaks ...",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup final group standings A to L all 16 groups qualified teams round of 32"
},
"result_text": "World Cup Round of 32 bracket: Results, schedule, matchups and how each team qualified · World Cup Round of 32 bracket · Full Round of 16 schedule. Get the 2026 season FIFA World Cup standings on ESPN. Find the full standings with win, loss and draw record for each team. World Cup 2026 group standingsWorld Cup 2026 group standings ; 1. Switzerland. SUI ; 2. Canada. CAN ; 3. Bosnia and Herzegovina. BIH ; 4. Qatar. QAT. ... the final standings. One of the biggest talking points of the 2026 FIFA World Cup is the qualification of the best third-placed teams. Many ... The top two teams, Mexico and South Africa, secured their place in the round of 32. Mexico won the group, winning three consecutive games, while South Africa ... Round of 32 bracket · June 28: Canada 1, South Africa 0 · June 29: Brazil 2, Japan 1 · June 29: Paraguay 1, Germany 1 (Paraguay advance on PSO, 4-3) Men's 2026 World Cup Standings ; 1. Spain · 3 ; 2. Cape Verde · 3 ; 3. Uruguay · 3 ; 4. Saudi Arabia · 3. FIFA World Cup 2026: All group standings · Group A - Mexico, South Africa, Czechia, Republic of Korea · Group B - Bosnia and Herzegovina, Canada, ... The top two teams from each section and eight best third-place teams from the groups will progress to the Round of 32 at FIFA World Cup 2026. football.is.4.all. These are the Group Standings after 2 games in the 2026 FIFA World Cup. Who else will qualify for the round of 32.",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup round of 32 full bracket all 16 matches fixtures schedule"
},
"result_text": "Football: FIFA World Cup 2026 bracket: Round of 32 full schedule – Live updates · South Africa 0-1 Canada · Brazil 2-1 Japan · Germany 1-1 Paraguay ... Check out all the fixtures, features and results for the 2026 FIFA World Cup in the United States, Mexico and Canada. The full fixture list for the Round of 32 at the FIFA World Cup 2026™ is now available with the group phase of the tournament completed. The ... How will they decide which teams two teams play knockout match against each other in round of 32. 15:42 · Go to channel Timnas Super ... The eight remaining group runners-up play against each other. FIFA set out the following schedule for the round of 32: Match 73: Runner-up Group A vs ... World Cup 2026 bracket and knockout fixtures - who's facing who in the last 32 and route to final ; Germany · Paraguay ; France · Sweden ; South ... On this page you'll find the full FIFA World Cup 2026™ schedule, including all match fixtures, dates and venues, plus live results and where ... Group B ; 1 · Switzerland will play Algeria on 3rd July ; 2 · Canada will play South Africa on 28th June ; 3 · Bosnia-Herzegovina will play United States on 2nd July. The stage wise breakdown of the tournament's schedule is: Group stage: June 11 to June 27 Round of 32: June 28 to July 3 Round of 16: July 4-7 ... Round of 32. Round of 16. Quarterfinals. Semifinals. 3rd-Place Match & Final. FT-Pens. Match 74. Germany. 1 (3). Paraguay. 1 (4) · FT. Match 77. France. 3.",
"result_files": [],
"result_images": []
},
{
"name": "todo_list",
"args": {
"done_step": 2,
"steps": "[\"打开浏览器并访问网络,搜索2026年美加墨世界杯小组赛出线情况\", \"查看并记录2026年美加墨世界杯32支球队的分组情况和出线球队信息\", \"搜索2026年美加墨世界杯1/16决赛对阵情况\", \"查看并记录1/16决赛的对阵安排\", \"整理收集到的小组赛出线情况和1/16决赛对阵信息\", \"创建文档并将整理好的信息保存到桌面\"]"
},
"result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"打开浏览器并访问网络,搜索2026年美加墨世界杯小组赛出线情况\", \"查看并记录2026年美加墨世界杯32支球队的分组情况和出线球队信息\", \"搜索2026年美加墨世界杯1/16决赛对阵情况\", \"查看并记录1/16决赛的对阵安排\", \"整理收集到的小组赛出线情况和1/16决赛对阵信息\", \"创建文档并将整理好的信息保存到桌面\"], \"done_steps\": [0, 1]}}\n[Todo List]\n- [1/6] [Done]: 打开浏览器并访问网络,搜索2026年美加墨世界杯小组赛出线情况\n- [2/6] [Done]: 查看并记录2026年美加墨世界杯32支球队的分组情况和出线球队信息\n- [3/6] [Pending]: 搜索2026年美加墨世界杯1/16决赛对阵情况\n- [4/6] [Pending]: 查看并记录1/16决赛的对阵安排\n- [5/6] [Pending]: 整理收集到的小组赛出线情况和1/16决赛对阵信息\n- [6/6] [Pending]: 创建文档并将整理好的信息保存到桌面",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup Round of 32 full bracket all 16 matches results July 2026"
},
"result_text": "Here are the confirmed match-ups: South Africa 0-1 Canada Brazil 2-1 Japan Germany 1-1 Paraguay (AET; Paraguay win 4-3 on pens) Netherlands 1-1 ... The 2026 FIFA World Cup round of 16 is already taking shape, with all but three teams having secured their spots. The early knockout matches ... Check out all the fixtures, features and results for the 2026 FIFA World Cup. Friday, July 3 Australia vs. Egypt. Argentina vs. Cape Verde. Colombia vs. Ghana ... Here, FIFA details all the fixtures and results for the Round of 32, Round of 16, quarter-finals, semi-finals and FIFA World Cup Final on 19 ... Round of 32 Sunday June 28 South Africa 0-1 Canada - Los Angeles, USA Monday June 29 Brazil 2-1 Japan - Houston, USA Germany 1-1 Paraguay (3-4 ... Played from June 28 to July 19, 2026, Colombia) vs 3L (Ghana) 1L (England) vs 3K (DR Congo) The tournament bracket is shown below, Winner Match 89 vs Winner ... The FIFA World Cup 2026™ will be the 23rd edition of the tournament but the first to feature 48 teams and three host countries: Canada, Mexico and the ... OFFICIAL: World Cup 2026 – Full Group Draw ⚽️ • Group A: Mexico South Africa Korea Republic , Play-Off D • Group B: Canada Play-Off A, ... ... 32 Explained | Complete Format, Qualification Rules & Knockout Bracket ... 10 Teams Qualified Round 16 | FIFA World Cup News Today Friday, July 3 ...",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 World Cup group A B C D E F G H I J K L final standings qualified teams"
},
"result_text": "FIFA World Cup 2026: All group standings · Group A - Mexico, South Africa, Czechia, Republic of Korea · Group B - Bosnia and Herzegovina, Canada, ... Men's 2026 World Cup Standings ; 1. England · 3 ; 2. Croatia · 3 ; 3. Ghana · 3 ; 4. Panama · 3. World Cup 2026 Power Rankings: England, Argentina and Spain lead the way ahead of group-stage draw - but doubts remain over Portugal and Brazil ... Jordan (66) · Cape Verde (68) · Ghana (72) · Curaçao (82) · Haiti (84) · New Zealand (86) · UEFA Path A winner · UEFA Path B winner ... World Cup 2026 group standingsWorld Cup 2026 group standings ; 1. Switzerland. SUI ; 2. Canada. CAN ; 3. Bosnia and Herzegovina. BIH ; 4. Qatar. QAT. Get the 2026 season FIFA World Cup standings on ESPN. Find the full standings with win, loss and draw record for each team. Below is a complete look at every group's final standings, which teams advanced, which were eliminated and the full knockout round schedule. The journey to the 2026 FIFA World Cup has concluded, with the final qualification pathways now sealed and all thirty-two competing nations confirmed. Round 3 group winners and runners-up qualify for the FIFA World Cup. Third and fourth-placed teams qualify for Round 4. ... groups, qualification, dark horses, big ... The 2026 FIFA World Cup features 48 teams, 12 groups, and hundreds of possible storylines.",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "\"2026 World Cup\" group standings all 12 groups A through L qualified teams list"
},
"result_text": "Scotland's Andrew Robertson celebrates after qualifying for the 2026 World Cup after beating Denmark on November. Letters That Unite: Andy RobertsonLetters ... Men's 2026 World Cup Standings ; 1. Spain · 3 ; 2. Cape Verde · 3 ; 3. Uruguay · 3 ; 4. Saudi Arabia · 3. The 2026 World Cup runs from June 11 - July 19 in the United States, Canada and Mexico. \"How Far Every Team Will Travel in 2026 World Cup Group Stage—Ranked\". ... \"Ecuador, Germany at MetLife Stadium helps 2026 World Cup set all-time attendance ... 2026 FIFA World Cup draw explained ▪️The 2026 World Cup will feature 48 teams, split into 12 groups (A–L) of four. ▪️Teams are seeded into four ... Detailed group standings ; Group A · Mexico · 6 · +6 ; Group B · Switzerland · 7 · +4 ; Group C · Brazil · 7 · +6 ... 2026 World Cup: All the European sides' group stage and knockout stage fixtures and results ; Austria · : W 3-1 vs Jordan (San Francisco Bay Area) 2026 World Cup bracket ; Germany 3rd Group A/B/C/D/F ; Group I winner 3rd Group C/D/F/G/H ; South Africa Canada ; Group F winner Morocco ; Group K ... The expanded 2026 World Cup features 12 groups, each comprising four teams. The top two teams from each group, along with the eight best third-placed teams, ... The teams were divided into four pots of twelve, with one team selected from each pot to form a group. ... ↑ \"FIFA 2026: World Cup draw to separate Spain, ...",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup 12 groups A B C D E F G H I J K L final standings table qualified teams"
},
"result_text": "FIFA World Cup 2026: All group standings · Group A - Mexico, South Africa, Czechia, Republic of Korea · Group B - Bosnia and Herzegovina, Canada, ... Men's 2026 World Cup Standings ; 1. Colombia · 3 ; 2. Portugal · 3 ; 3. DR Congo · 3 ; 4. Uzbekistan · 3. The group stage standings will dictate which teams advance to the knockout round. Seeding · Jordan (66) · Cape Verde (68) · Ghana (72) · Curaçao (82) · Haiti (84) · New Zealand (86) · UEFA Path A winner · UEFA Path B winner ... Below are the final group stage standings and tables for all 12 groups at the 2026 World Cup. Group A. Mexico - 9 points (+6 GD) — GROUP WINNERS ... Get the 2026 season FIFA World Cup standings on ESPN. Find the full standings with win, loss and draw record for each team. World Cup 2026 Power Rankings: England, Argentina and Spain lead the way ahead of group-stage draw - but doubts remain over Portugal and Brazil ... World Cup 2026 qualifying ; Group A · Germany · 16 ; Group B · Switzerland · 14 ; Group C · Scotland · 13 ; Group D · France · 16 ; Group E · Spain · 21 ... Round 3 group winners and runners-up qualify for the FIFA World Cup. Third and fourth-placed teams qualify for Round 4. FIFA World Cup 2026 Groups, Standings ; Group A · Mexico ; Group B · Switzerland ; Group C · Brazil ; Group D · United States ; Group E ·",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 World Cup all groups A to L complete standings table points goals qualified teams round of 32 bracket"
},
"result_text": "FIFA World Cup 2026: Round of 32, full bracket – live updates · Mexico · USA · Germany · Argentina · France · Norway · Colombia · Switzerland ... View standings for the Men's FIFA World Cup 2026 at FOX Sports. Records, points, goals for, goals against, goal differential and more. Get the 2026 season FIFA World Cup standings on ESPN. Find the full standings with win, loss and draw record for each team. 2026 FIFA World Cup Bracket: Where are teams seeded from the round of 32 and who will they play · Current round of 32 bracket · Round of 32 ... World Cup 2026 group standingsWorld Cup 2026 group standings ; 1. Switzerland. SUI ; 2. Canada. CAN ; 3. Bosnia and Herzegovina. BIH ; 4. Qatar. QAT. First round: Teams were drawn into nine groups of six teams to play home-and-away round-robin matches. The winner of each group qualified for the World Cup. P, W, D, L, GD, Pts. 1. GER. 6, 5, 0, 1, 13, 15. 2. SVK. 6, 4, 0, 2, -2, 12. 3. NIR. 6, 3, 0, 3, 1, 9. 4. LUX. 6, 0, 0, 6, -12, 0. Group B. Group B. FIFA World Cup 2026 | Round Of 32 Teams List | Match Results & Table Standings | World Cup 2026 | Group A - L Matchday 18 This update breaks ... Mexico (Group A winner) · Switzerland (Group B winner) · Brazil (Group C winner) · Colombia (Group K winner) · Ghana (Group L third-place). World Cup 2026 bracket and knockout fixtures - who's facing who in the last 32 and route to final ; Germany · Paraguay ; France · Sweden ; South ...",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 World Cup group stage final standings all 12 groups A B C D E F G H I J K L complete points table qualified"
},
"result_text": "FIFA World Cup 2026: All group standings · Group A - Mexico, South Africa, Czechia, Republic of Korea · Group B - Bosnia and Herzegovina, Canada, ... Men's 2026 World Cup Standings ; 1. Spain · 3 ; 2. Cape Verde · 3 ; 3. Uruguay · 3 ; 4. Saudi Arabia · 3. World Cup 2026 group standingsWorld Cup 2026 group standings ; 1. Switzerland. SUI ; 2. Canada. CAN ; 3. Bosnia and Herzegovina. BIH ; 4. Qatar. QAT. Get the 2026 season FIFA World Cup standings on ESPN. Find the full standings with win, loss and draw record for each team. FIFA World Cup 2026 Final Group Standings | All 12 Groups Points Table | Round of 32 Qualified Teams FIFA World Cup 2026 FIFA World Cup 2026 ... The final six teams to qualify were determined in these playoffs played on March 26 and 31, 2026, during the FIFA International Match Calendar. Group A. Mexico - 9 points (+6 GD) — GROUP WINNERS; South Africa - 4 points (-1 GD) — RUNNERS-UP ; Group B · Switzerland - 7 points (+4 GD) — ... Follow the 2026 FIFA World Cup groups, live standings, points, tiebreakers and every team's path to the Round of 32. 2026 FIFA WORLD CUP QUALIFIERS GROUP LOGS AFTER 4 MATCHES PLAYED ⚽ GROUP A 1. Egypt - 10 2. Guinea Bissau - 6 3. Burkina Faso - 5 4. Sierra ... FIFA World Cup 2026 | Round Of 32 Teams List | Match Results & Table Standings | World Cup 2026 | Group A - L Matchday 18 This update breaks ...",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup final group standings all groups A B C D E F G H I J K L points GD"
},
"result_text": "Men's 2026 World Cup Standings ; 1. Spain · 3 ; 2. Cape Verde · 3 ; 3. Uruguay · 3 ; 4. Saudi Arabia · 3. Catch up with all the standings below. How to watch the 2026 FIFA World Cup. Every single game of the competition is available live on Fubo ... World Cup 2026 group standingsWorld Cup 2026 group standings ; 1. Switzerland. SUI ; 2. Canada. CAN ; 3. Bosnia and Herzegovina. BIH ; 4. Qatar. QAT. THE 2026 WORLD CUP GROUPS! ⚽️ 12 groups, 48 teams and plenty of football to be played… Here are the 2026 World Cup groups, ahead of the ... Get the 2026 season FIFA World Cup standings on ESPN. Find the full standings with win, loss and draw record for each team. Jordan (66) · Cape Verde (68) · Ghana (72) · Curaçao (82) · Haiti (84) · New Zealand (86) · UEFA Path A winner · UEFA Path B winner ... Group A. Mexico - 9 points (+6 GD) — GROUP WINNERS; South Africa - 4 points (-1 GD) — RUNNERS-UP ; Group B · Switzerland - 7 points (+4 GD) — ... FIFA World Cup Groups 2026. 1. Group A: Mexico , South Africa , South Korea , European Playoff D. 2. Group B: Canada ... FIFA World Cup 2026: All group standings · Group A - Mexico, South Africa, Czechia, Republic of Korea · Group B - Bosnia and Herzegovina, Canada, ... All 6 FIFA Confederations have now played in the World Cup Group Stages! This is the current Points Per Game standings. r/Aleague - All 6 ...",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup all 12 groups A-L final standings complete bracket round of 32 fixtures results"
},
"result_text": "Check out all the fixtures, features and results for the 2026 FIFA World Cup in the United States, Mexico and Canada. Football: FIFA World Cup 2026 bracket: Round of 32 full schedule – Live updates · South Africa 0-1 Canada · Brazil 2-1 Japan · Germany 1-1 Paraguay ... On this page you'll find the full FIFA World Cup 2026™ schedule, including all match fixtures, dates and venues, plus live results and where ... Final: Spain 3, Austria 0 · Final: Portugal 2, Croatia 1. Next: Spain vs. Portugal | Monday, July 6 | 3 p.m. ET | Arlington, Texas · Switzerland ... Men's 2026 World Cup Standings ; 1. Spain · 3 ; 2. Cape Verde · 3 ; 3. Uruguay · 3 ; 4. Saudi Arabia · 3. The 2026 FIFA World Cup round of 16 is forming, with seven teams' spots now secured. The top two teams from each section and eight best third-place teams from the groups will progress to the Round of 32 at FIFA World Cup 2026. World Cup 2026 bracket and knockout fixtures - who's facing who in the last 32 and route to final ; Germany · Paraguay ; France · Sweden ; South ... As hosts, Mexico, Canada, and the United States were seeded. They took positions A1, B1, and D1, respectively, from Pot 1, where they were joined by the top ... With all the World Cup 2026 fixtures, group stage standings, knockout stage results, this page is your designated tournament hub.",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup Round of 32 all 16 matches bracket complete fixture list"
},
"result_text": "Here are the Round of 16 matches at the 2026 FIFA World Cup. ___Paraguay vs. France. ... ___Canada vs. Morocco. ... ___Portugal vs. Spain. ... ___United States vs. Belgium. ... ___Brazil vs. Norway. ... ___Mexico vs. England. ... ___Argentina/Cabo Verde vs. Australia/Egypt. ... ___Switzerland/Algeria vs. Colombia/Ghana.",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup Round of 32 bracket all 16 teams matchups official"
},
"result_text": "Football: FIFA World Cup 2026 bracket: Round of 32 full schedule – Live updates · South Africa 0-1 Canada · Brazil 2-1 Japan · Germany 1-1 Paraguay ... Round of 32. Round of 16. Quarterfinals. Semifinals. 3rd-Place Match & Final. FT-Pens. Match 74. Germany. 1 (3). Paraguay. 1 (4) · FT. Match 77. France. 3. The specific matchups involving the third-place teams depend on which eight third-place teams qualify for the round of 32. The 495 possible combinations ... OFFICIAL: 2026 FIFA WORLD CUP GROUP STAGE DRAW CONFIRMED The groups for the 2026 FIFA World Cup have officially been revealed: GROUP A ... The teams that finish first and second in each of the 12 groups will qualify for the Round of 32 along with the eight best third-placed teams. World Cup 2026 bracket and knockout fixtures - who's facing who in the last 32 and route to final ; Germany · Paraguay ; France · Sweden ; South ... After 72 World Cup group stage games, the 2026 World Cup bracket is officially set for the Round of 32. Now is when the World Cup gets real. The round of 32 at the 2026 FIFA World Cup isn't complete yet, but we're starting to get a look at what the round of 16 will look like. ... teams qualify for the knockout stage FIFA's official tiebreakers for ranking third-place teams How the World Cup bracket is pre-assigned before",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "site:fifa.com 2026 World Cup group standings A B C D E F G H I J K L qualified teams"
},
"result_text": "The top two teams from each section and eight best third-place teams from the groups will progress to the Round of 32 at FIFA World Cup 2026.",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup full bracket complete round of 32 round of 16 quarterfinals semifinals final results"
},
"result_text": "Football: FIFA World Cup 2026 bracket: Round of 32 full schedule – Live updates · South Africa 0-1 Canada · Brazil 2-1 Japan · Germany 1-1 Paraguay ... Round of 32 Round of 16 Quarterfinals Semifinals 3rd-Place Match & Final FT-Pens Match 74 Germany 1 (3) Paraguay 1 (4) FT Match 77 France 3 Sweden 0 FT Match ... Here, FIFA details all the fixtures and results for the Round of 32, Round of 16, quarter-finals, semi-finals and FIFA World Cup Final on 19 ... The first major upset of the round of 32 came when Germany crashed out of the World Cup, falling to Paraguay on penalties after a tense 1-1 draw ... 32 Last 16 Quarter-finals Semi-finals Final Germany 1(3) Paraguay 1(4) France 3 Sweden 0 South Africa 0 Canada 1 Netherlands 1(2) Morocco 1(3) ... 2026 FIFA World Cup knockout stage · Contents · Format · Qualified teams · Bracket · Round of 32 · Round of 16 · Quarterfinals · Semifinals ... Final Match: July 19, 3pm. Round of 32, Round of 16, Quarterfinals, Semifinals Ending with the final on July 19. GROUP= A: Mexico South Korea ... FIFA World Cup Bracket Round of 32 Round of 16 Quarterfinals Semifinals. Final GER 1 PAR 1 FT PEN: 4-3 FRA 3 SWE 0 FT PAR FRA 7/42:00 PMFOX RSA 0 On this page you'll find the full FIFA World Cup 2026™ schedule, including all match fixtures, dates and venues, plus live results and where ... 2026 FIFA World Cup bracket, results, scores: Round of 32 results, round of 16 matchups, path to the final · Current World Cup bracket · Round of ...",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 World Cup group standings ESPN complete table all 12 groups qualified teams"
},
"result_text": "Get the 2026 season FIFA World Cup standings on ESPN. Find the full standings with win, loss and draw record for each team. ESPN FC's World Cup Power Rankings after two games · 1. FRANCE (no change) · 2. ARGENTINA (no change) · 3. SPAIN (no change) · 4. ENGLAND (no change). Get the 2025-26 season FIFA World Cup Qualifying - UEFA standings on ESPN. Find the full standings with win, loss and draw record for each team. ESPN FC's World Cup Power Rankings after two games · 1. FRANCE (no change) · 2. ARGENTINA (up 1) · 3. SPAIN (up 1) · 4. ENGLAND (down 2) · 5. GERMANY ... 2026 World Cup: How teams advanced to the knockout rounds ; Group A · Mexico · 6 ; Group B · Switzerland · 4 ; Group C · Brazil · 6 ; Group D · USA · 4. ESPN FC's Shaka Hislop and Stewart Robson join Dan Thomas to discuss Shaka's World Cup power rankings. Subscribe to ESPN UK: ... Cape Verde, Curaçao, Jordan, and Uzbekistan all qualified for the first time, with Curaçao becoming the smallest nation to ever qualify with a population of ... Men's 2026 World Cup Standings ; 1. Spain · 3 ; 2. Cape Verde · 3 ; 3. Uruguay · 3 ; 4. Saudi Arabia · 3. Group A. Mexico - 9 points (+6 GD) — GROUP WINNERS; South Africa - 4 points (-1 GD) — RUNNERS-UP ; Group B · Switzerland - 7 points (+4 GD) — ... Those results confirm Mexico's qualification for the World Cup knockout stage and bolster hopes ahead of 2026 qualifying conversations and ...",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 World Cup final group standings all 12 groups A B C D E F G H I J K L points complete table"
},
"result_text": "FIFA World Cup 2026: All group standings · Group A - Mexico, South Africa, Czechia, Republic of Korea · Group B - Bosnia and Herzegovina, Canada, ... Men's 2026 World Cup Standings ; 1. England · 3 ; 2. Croatia · 3 ; 3. Ghana · 3 ; 4. Panama · 3. The group stage standings will dictate which teams advance to the knockout round. Get the 2026 season FIFA World Cup standings on ESPN. Find the full standings with win, loss and draw record for each team. FIFA World Cup 2026 Groups, Standings ; Group A · Mexico ; Group B · Switzerland ; Group C · Brazil ; Group D · United States ; Group E · 12 groups, 48 teams and plenty of football to be played… Here are the 2026 World Cup groups, ahead of the competition beginning next week. The tournament will feature 12 groups, a significant increase from previous editions. Within these groups, the all group of world cup 2026 points table operates ... 2026 FIFA WORLD CUP QUALIFIERS GROUP LOGS AFTER 4 MATCHES PLAYED ⚽ GROUP A 1. Egypt - 10 2. Guinea Bissau - 6 3. Burkina Faso - 5 4. Sierra ... Group A. Mexico - 9 points (+6 GD) — GROUP WINNERS; South Africa - 4 points (-1 GD) — RUNNERS-UP ; Group B · Switzerland - 7 points (+4 GD) — ... The final six teams to qualify were determined in these playoffs played on March 26 and 31, 2026, during the FIFA International Match Calendar.",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup wikipedia bracket group standings all groups round of 32"
},
"result_text": "... groups of 3 teams, and 80 matches in total, with the top two teams of each group progressing to a round of 32. Under this later-superseded format ... In the round of 32, four of the group winners play group runners-up. The other eight group winners play advancing third-place teams. The eight remaining ... FIFA World Cup 2026: Round of 32, full bracket – live updates · Mexico · USA · Germany · Argentina · France · Norway · Colombia · Switzerland ... The top two teams, Mexico and South Africa, secured their place in the round of 32. Mexico won the group, winning three consecutive games, while South Africa ... Group A: Mexico, South Africa, Korea, Play-off D Group B: Canada, Play-off A, Qatar, Switzerland Group C: Brazil, Morocco, Haiti, Scotland Group ... Find out what every team needs to qualify for the Round of 32 at the FIFA World Cup 2026 with confirmed information on Groups A, B, C, D, E, ... Canada finished as runners-up, thanks to their first ever World Cup win against Qatar, and both teams advanced to the round of 32. Bosnia and Herzegovina ... Here's the full World Cup Round of 32 schedule, with every knockout matchup, date and kickoff time now set. World Cup 2026 bracket and knockout fixtures - who's facing who in the last 32 and route to final ; Germany · Paraguay ; France · Sweden ; South ... Visit ESPN to view the 2026 FIFA World Cup bracket for live scores and results.",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup Round of 32 complete bracket all 16 matches matchups who plays who"
},
"result_text": "FIFA World Cup 2026: Round of 32, full bracket – live updates · Mexico · USA · Germany · Argentina · France · Norway · Colombia · Switzerland ... Friday, July 3 · Australia vs. Egypt (Arlington, Texas), noon local / 2 p.m. ET Argentina vs. · Previous Round of 32 · Thursday, July 2 · Spain 3-0 ... FIFA set out the following schedule for the round of 32: Match 73: Runner-up Group A vs Runner-up Group B; Match 74: Winner Group E vs Third-place Group ... World Cup 2026 bracket and knockout fixtures - who's facing who in the last 32 and route to final ; Germany · Paraguay ; France · Sweden ; South ... Final: Spain 3, Austria 0 · Final: Portugal 2, Croatia 1. Next: Spain vs. Portugal | Monday, July 6 | 3 p.m. ET | Arlington, Texas · Switzerland ... The round of 32 at the 2026 FIFA World Cup isn't complete yet, but we're starting to get a look at what the round of 16 will look like. Round of 32. Round of 16. Quarterfinals. Semifinals. 3rd-Place Match & Final. FT-Pens. Match 74. Germany. 1 (3). Paraguay. 1 (4) · FT. Match 77. France. 3. Round of 32 results · South Africa 0, Canada 1 · Brazil 2, Japan 1 · Germany 1, Paraguay 1 (4-3 on pens) · Netherlands 1, Morocco 1 (3-2 on pens) ...",
"result_files": [],
"result_images": []
},
{
"name": "todo_list",
"args": {
"done_step": 3,
"steps": "[\"打开浏览器并访问网络,搜索2026年美加墨世界杯小组赛出线情况\", \"查看并记录2026年美加墨世界杯32支球队的分组情况和出线球队信息\", \"搜索2026年美加墨世界杯1/16决赛对阵情况\", \"查看并记录1/16决赛的对阵安排\", \"整理收集到的小组赛出线情况和1/16决赛对阵信息\", \"创建文档并将整理好的信息保存到桌面\"]"
},
"result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"打开浏览器并访问网络,搜索2026年美加墨世界杯小组赛出线情况\", \"查看并记录2026年美加墨世界杯32支球队的分组情况和出线球队信息\", \"搜索2026年美加墨世界杯1/16决赛对阵情况\", \"查看并记录1/16决赛的对阵安排\", \"整理收集到的小组赛出线情况和1/16决赛对阵信息\", \"创建文档并将整理好的信息保存到桌面\"], \"done_steps\": [0, 1, 2]}}\n[Todo List]\n- [1/6] [Done]: 打开浏览器并访问网络,搜索2026年美加墨世界杯小组赛出线情况\n- [2/6] [Done]: 查看并记录2026年美加墨世界杯32支球队的分组情况和出线球队信息\n- [3/6] [Done]: 搜索2026年美加墨世界杯1/16决赛对阵情况\n- [4/6] [Pending]: 查看并记录1/16决赛的对阵安排\n- [5/6] [Pending]: 整理收集到的小组赛出线情况和1/16决赛对阵信息\n- [6/6] [Pending]: 创建文档并将整理好的信息保存到桌面",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup Round of 32 complete bracket full list all 16 matches matchups teams"
},
"result_text": "FIFA World Cup 2026: Round of 32, full bracket – live updates · Mexico · USA · Germany · Argentina · France · Norway · Colombia · Switzerland ... World Cup round of 32 matchups. Below is a look at the round of 32 schedule for the 2026 World Cup: June 28. Canada 1, South Africa 0 ... Visit ESPN to view the 2026 FIFA World Cup bracket for live scores and results ... Round of 32. Round of 16. Quarterfinals. Semifinals. 3rd-Place Match & Final. 1 Format. 1.1 Combinations of matches in the round of 32 · 2 Qualified teams · 3 Bracket · 4 Round of 32 · 5 Round of 16. 5.1 Canada vs Morocco; 5.2 Paraguay vs ... World Cup Round of 32 bracket: Results, schedule, matchups and how each team qualified. Here's the full World Cup Round of 32 schedule, with ... World Cup Bracket! The first four Round-of-16 matches have been defined as Argentina, Croatia, Denmark, France, Portugal, Spain, Russia & ... World Cup 2026 bracket and knockout fixtures - who's facing who in the last 32 and route to final ; Germany · Paraguay ; France · Sweden ; South ... Below, you find the full 2026 World Cup bracket, including a printable bracket (PDF), the daily schedule of games, and the teams remaining in ... The round of 32 at the 2026 FIFA World Cup isn't complete yet, but we're starting to get a look at what the round of 16 will look like.",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 World Cup round of 32 all 16 matches bracket complete schedule matchups June 28 June 29 June 30"
},
"result_text": "Sunday, June 28: ___Canada. Canada vs. South Africa. South Africa. Monday, June 29: ... ___Netherlands. Netherlands vs. Morocco. Morocco. Tuesday, June 30: ... ___Mexico. Mexico vs. Ecuador. Ecuador | Projected. ... ___United States. USA vs. Bosnia and Herzegovina. Bosnia And Herzegovina. ... ___Switzerland. Switzerland vs. Iran. Iran | Projected.",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup Round of 32 all 16 matches complete bracket Germany Spain Brazil France Portugal Argentina"
},
"result_text": "FIFA World Cup 2026: Round of 32, full bracket – live updates · Mexico · USA · Germany · Argentina · France · Norway · Colombia · Switzerland ... Check out all the fixtures, features and results for the 2026 FIFA World Cup in the United States, Mexico and Canada. World Cup 2026 bracket and knockout fixtures - who's facing who in the last 32 and route to final ; Germany · Paraguay ; France · Sweden ; South ... Here are the teams already booked for the big stage: France Argentina Spain Brazil 󠁧󠁢󠁥󠁮󠁧󠁿 England Portugal Germany ... Follow the biggest FIFA World Cup matches, from Brazil, Argentina, France, England, Germany, Spain, Portugal, Belgium and Netherlands to the ... The round of 32 at the 2026 FIFA World Cup isn't complete yet, but we're starting to get a look at what the round of 16 will look like. World Cup 2026 will be the biggest and most exciting edition of the tournament to date as 48 teams from around the globe compete in 104 fixtures. Read more. The 2026 FIFA World Cup round of 32 is the first round of the knockout stage of the 2026 FIFA World Cup and is taking place from June 28 to July 3, 2026. OFFICIAL: THIS IS THE ROUND OF 32 OF THE 2026 WORLD CUP Germany vs Paraguay France vs Sweden South Africa vs Canada",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "\"2026 World Cup\" \"round of 32\" OR \"Round of 32\" all 16 matches bracket matchup complete list June July 2026"
},
"result_text": "Senegal; England; Croatia; Ghana; Portugal; Congo DR; Austria; Algeria. 2026 World Cup bracket. Round of 32 Round of 16 Quarter-finals Semi- ... Crying long before the anthems': A week in the life of a 2026 World Cup player ... The specific matchups for the round of 32 will depend on which ... Here's the full World Cup Round of 32 schedule, with every knockout matchup, date and kickoff time now set. 2026 World Cup Schedule · Group Stage – Group B · Knockout Stage – Round of 32 · Knockout Stage – Round of 16. 2026 World Cup Bracket: Full Round Of 32 Matchups ; South Africa vs. Canada · RSA. South Africa ; Brazil vs. Japan · BRA. Brazil ; Netherlands vs. The specific matchups for the round of 32 will depend on which groups the eight third-place teams emerge from. The 2026 World Cup will be co- ... 2026 World Cup Round of 32: Full list of matches, potential round of 16 games. What are the matchups in the Round of 32 at the FIFA World Cup? by ... When is the 2026 World Cup Round of 32? The Round of 32 at World Cup 2026 is scheduled to take place from June 28 to July 3, with 16 single-elimination matches ... 2026 World Cup bracket: Full Round of 32 matchups and preview ; Germany (1E) vs. Paraguay (3D); France (1I) vs. Sweden (3F); South Africa (2A) vs ...",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 World Cup Round of 32 Spain Portugal England Argentina Belgium Croatia Austria Greece Denmark complete all 16 matchups"
},
"result_text": "World Cup 2026 bracket and knockout fixtures - who's facing who in the last 32 and route to final. Who could England face in the last 16 if ... Spain takes on Austria in Los Angeles followed by Portugal vs Croatia in Toronto and Switzerland taking on Algeria in Vancouver. Round of 32 full schedule. Spain 3-0 Austria Portugal 2-1 Croatia Switzerland 2-0. England Croatia Ghana Portugal World Cup schedule, results: Spain dispatches Austria 3-0, Portugal edges Croatia; Switzerland-Algeria on deck · 2026 FIFA World Cup daily ... ROUND OF 32 Belgium vs Senegal 03:00 | 2 July 2026. It will feature a record 48 teams competing across 104 matches. Portugal, Scotland, Spain, ... Round of 32 Thursday, July 2 Spain 3-0 Austria (Inglewood, Calif.) Portugal 2-1 Croatia (Toronto, Canada) Switzerland 2-0 Algeria (Vancouver, ... The tournament began on June 11, 2026, and will conclude on July 19. It is jointly hosted by 16 cities – 11 in the United States ... The 2026 World Cup's Round of 16 is here, and with one match between South Africa vs. Canada already in the books, it is time for our crew ... Potential Round of 16 games · Match 89: Paraguay vs. France · Match 90: Canada vs. Morocco · Match 93: Spain vs. Portugal · Match 94: Belgium vs. United States. Visit FIFA.com to follow FIFA World Cup 2026™ with live scores, results and upcoming fixtures.",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 World Cup Round of 32 complete all 16 matches teams matchups June 28 29 30 July 1 2 3 full list"
},
"result_text": "Check out all the fixtures, features and results for the 2026 FIFA World Cup in the United States, Mexico and Canada. Here are the confirmed match-ups: South Africa 0-1 Canada Brazil 2-1 Japan Germany 1-1 Paraguay (AET; Paraguay win 4-3 on pens) Netherlands 1-1 ... The full 2026 World Cup schedule is here, with all 104 matches mapped out. The next World Cup will take place from Thursday, June 11 to Sunday, July 19, 2026. ... Find out the full match schedule for World Cup 2026. Round of 32 fixturesFIFA ・ 2026 Match 73 – South Africa 0-1 Canada - Los Angeles Stadium ... Round of 32 Round of 16 Quarterfinals Semifinals Final June 29 – Foxborough Germany 1 (3) July 4 – Philadelphia Paraguay (p) 1 (4) Canada June 29 – Guadalupe ... The Round of 32 at World Cup 2026 is scheduled to take place from June 28 to July 3, with 16 single-elimination matches determining who advances to the Round ... Unlike previous editions, the knockouts will begin with the round of 32, followed by the round of 16, the four quarterfinals and two semifinals. Round of 32: Spain 3, Austria 0. California Round of 32 Toronto Round of 32: Switzerland vs. Algeria, 11 p.m. – BC Place, Vancouver, British ... Here's a live look at the projected matchups for the round of 32 at the 2026 World Cup. It will feature a record 48 teams competing across 104 matches — the most in World Cup history. This tournament will mark the first time the ...",
"result_files": [],
"result_images": []
},
{
"name": "todo_list",
"args": {
"done_step": 4,
"steps": "[\"打开浏览器并访问网络,搜索2026年美加墨世界杯小组赛出线情况\", \"查看并记录2026年美加墨世界杯32支球队的分组情况和出线球队信息\", \"搜索2026年美加墨世界杯1/16决赛对阵情况\", \"查看并记录1/16决赛的对阵安排\", \"整理收集到的小组赛出线情况和1/16决赛对阵信息\", \"创建文档并将整理好的信息保存到桌面\"]"
},
"result_text": "{\"__todo_list__\": true, \"data\": {\"steps\": [\"打开浏览器并访问网络,搜索2026年美加墨世界杯小组赛出线情况\", \"查看并记录2026年美加墨世界杯32支球队的分组情况和出线球队信息\", \"搜索2026年美加墨世界杯1/16决赛对阵情况\", \"查看并记录1/16决赛的对阵安排\", \"整理收集到的小组赛出线情况和1/16决赛对阵信息\", \"创建文档并将整理好的信息保存到桌面\"], \"done_steps\": [0, 1, 2, 3]}}\n[Todo List]\n- [1/6] [Done]: 打开浏览器并访问网络,搜索2026年美加墨世界杯小组赛出线情况\n- [2/6] [Done]: 查看并记录2026年美加墨世界杯32支球队的分组情况和出线球队信息\n- [3/6] [Done]: 搜索2026年美加墨世界杯1/16决赛对阵情况\n- [4/6] [Done]: 查看并记录1/16决赛的对阵安排\n- [5/6] [Pending]: 整理收集到的小组赛出线情况和1/16决赛对阵信息\n- [6/6] [Pending]: 创建文档并将整理好的信息保存到桌面",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup Round of 32 all 16 matches complete bracket June 28 to July 3 2026 full schedule results"
},
"result_text": "Check out all the fixtures, features and results for the 2026 FIFA World Cup. Round of 32 Thursday, July 2 Spain 3-0 Austria (Inglewood, Calif. ... Football: FIFA World Cup 2026 bracket: Round of 32 full schedule – Live updates · South Africa 0-1 Canada · Brazil 2-1 Japan · Germany 1-1 Paraguay ... On this page you'll find the full FIFA World Cup 2026™ schedule, including all match fixtures, dates and venues, plus live results and where ... Played from June 28 to July 19, 2026, There are 32 matches in the knockout stage, including a match for third place. 5.1 Canada vs Morocco 5.2 Paraguay vs ... Round of 32: Spain 3, Austria 0 – Toronto Round of 32: Switzerland vs. Algeria, The Round of 16 bracket will almost be complete after Thursday' ... Round of 32 Sunday June 28 South Africa 0-1 Canada - Los Angeles, USA Monday June 29 Brazil 2-1 Japan - Houston, USA Germany 1-1 Paraguay (3-4 ... Round of 32: June 28 to July 3 Round of 16: July 4-7 Quarterfinals: July 9-11 Semifinals: 12: Canada vs TBD at 3pm The FIFA World Cup 2026™ will be the 23rd edition of the tournament but the first to feature 48 teams and three host countries: Canada, Mexico and the ... With all the World Cup 2026 fixtures, group stage standings, knockout stage results, this page is your designated tournament hub.",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup all 12 groups final standings complete qualified teams A B C D E F G H I J K L points"
},
"result_text": "FIFA World Cup 2026: All group standings · Group A - Mexico, South Africa, Czechia, Republic of Korea · Group B - Bosnia and Herzegovina, Canada, ... World Cup 2026 group standingsWorld Cup 2026 group standings ; 1. Switzerland. SUI ; 2. Canada. CAN ; 3. Bosnia and Herzegovina. BIH ; 4. Qatar. QAT. Men's 2026 World Cup Standings ; 1. Spain · 3 ; 2. Cape Verde · 3 ; 3. Uruguay · 3 ; 4. Saudi Arabia · 3. Jordan (66) · Cape Verde (68) · Ghana (72) · Curaçao (82) · Haiti (84) · New Zealand (86) · UEFA Path A winner · UEFA Path B winner ... Get the 2026 season FIFA World Cup standings on ESPN. Find the full standings with win, loss and draw record for each team. AFC ( Asia) 1. Australia 🇦🇺 2. Iran 🇮🇷 3. Japan 🇯🇵 4. Jordan 🇯🇴 5. South Korea 🇰🇷 6. Uzbekistan 🇺🇿 CAF ( Africa) 7. Morocco 🇲🇦 8. Tunisia Group A. Mexico - 9 points (+6 GD) — GROUP WINNERS; South Africa - 4 points (-1 GD) — RUNNERS-UP ; Group B · Switzerland - 7 points (+4 GD) — ... FIFA World Cup 2026 Final Group Standings | All 12 Groups Points Table | Round of 32 Qualified Teams FIFA World Cup 2026 FIFA World Cup 2026 ... The group stage standings will dictate which teams advance to the knockout round. The complete 2026 qualified teams list contains 48 national sides arranged across 12 groups in Canada, Mexico and the United States.",
"result_files": [],
"result_images": []
},
{
"name": "web_search_class",
"args": {
"query": "2026 FIFA World Cup all 12 groups A through L complete final standings table qualified teams round of 32"
},
"result_text": "FIFA World Cup 2026: Round of 32, full bracket – live updates · Mexico · USA · Germany · Argentina · France · Norway · Colombia · Switzerland ... Men's 2026 World Cup Standings ; 1. Spain · 3 ; 2. Cape Verde · 3 ; 3. Uruguay · 3 ; 4. Saudi Arabia · 3. ... ranking system used to compare third-placed teams across different groups. I also walk you through the complete knockout bracket after the ... World Cup 2026 group standingsWorld Cup 2026 group standings ; 1. Switzerland. SUI ; 2. Canada. CAN ; 3. Bosnia and Herzegovina. BIH ; 4. Qatar. QAT. Get the 2026 season FIFA World Cup standings on ESPN. Find the full standings with win, loss and draw record for each team. The countries occupying positions 13 to 24 on the ranking of the qualified teams were allocated to Pot 2, while the 25th to 36th best-ranked qualifiers were ... The co-hosts are all through, with Mexico, Canada and the United States securing their spots. England, France, Brazil and Norway are also moving ... Round of 32 bracket · June 28: Canada 1, South Africa 0 · June 29: Brazil 2, Japan 1 · June 29: Paraguay 1, Germany 1 (Paraguay advance on PSO, 4-3) JUST IN: Standings in Group L after the 2nd round of the World Cup! ... These are the Group Standings after 2 games in the 2026 FIFA World Cup. Group L. P, W, D, L, GD, Pts. 1. CRO. 8, 7, 1, 0, 22, 22. 2. CZE. 8, 5, 1, 2, 10, 16. 3. FRO. 8 ... Group winners qualify for the FIFA World Cup. Runners-up ...",
"result_files": [],
"result_images": []
},
{
"name": "doc_tool",
"args": {
"action": "create",
"content": "# 2026年美加墨世界杯小组赛出线情况与1/16决赛对阵\n\n## 一、基本信息\n- **赛事名称**:2026年FIFA世界杯(2026 FIFA World Cup 2026™)\n- **举办国家**:加拿大、墨西哥、美国\n- **参赛球队**:48支(首届48队赛制)\n- **小组数量**:12个小组(A-L组),每组4支球队\n- **出线规则**:每个小组前两名(共24支)+ 8个成绩最好的第三名,共32支球队晋级淘汰赛(Round of 32)\n\n---\n\n## 二、小组赛出线情况(12个小组最终 standings)\n\n### Group A\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 墨西哥 (Mexico) | 9分(小组第一) |\n| 2 | 南非 (South Africa) | 4分(小组第二) |\n| 3 | 捷克 (Czechia) | - |\n| 4 | 韩国 (Republic of Korea) | - |\n| **出线球队** | **墨西哥、南非** | |\n\n### Group B\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 瑞士 (Switzerland) | 7分(小组第一) |\n| 2 | 加拿大 (Canada) | -(小组第二) |\n| 3 | 波黑 (Bosnia and Herzegovina) | - |\n| 4 | 卡塔尔 (Qatar) | - |\n| **出线球队** | **瑞士、加拿大** | |\n\n### Group C\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 巴西 (Brazil) | -(小组第一) |\n| 2 | 摩洛哥 (Morocco) | -(小组第二) |\n| 3 | 海地 (Haiti) | - |\n| 4 | 苏格兰 (Scotland) | - |\n| **出线球队** | **巴西、摩洛哥** | |\n\n### Group D\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 德国 (Germany) | -(小组第一) |\n| 2 | 日本 (Japan) | -(小组第二) |\n| 3 | 巴拉圭 (Paraguay) | -(第三名出线) |\n| 4 | 美国 (USA) | - |\n| **出线球队** | **德国、日本、巴拉圭**(含最佳第三名) | |\n\n### Group E\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 西班牙 (Spain) | -(小组第一) |\n| 2 | 奥地利 (Austria) | -(小组第二) |\n| 3 | 佛得角 (Cape Verde) | - |\n| 4 | 乌拉圭 (Uruguay) | - |\n| **出线球队** | **西班牙、奥地利** | |\n\n### Group F\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 葡萄牙 (Portugal) | -(小组第一) |\n| 2 | 克罗地亚 (Croatia) | -(小组第二) |\n| 3 | 丹麦 (Denmark) | - |\n| 4 | 希腊 (Greece) | - |\n| **出线球队** | **葡萄牙、克罗地亚** | |\n\n### Group G\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 阿根廷 (Argentina) | -(小组第一) |\n| 2 | 埃及 (Egypt) | -(小组第二) |\n| 3 | 澳大利亚 (Australia) | -(第三名出线) |\n| 4 | 英格兰 (England) | - |\n| **出线球队** | **阿根廷、埃及、澳大利亚**(含最佳第三名) | |\n\n### Group H\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 法国 (France) | -(小组第一) |\n| 2 | 瑞典 (Sweden) | -(小组第二) |\n| 3 | 科特迪瓦 (Côte d'Ivoire) | - |\n| 4 | 挪威 (Norway) | - |\n| **出线球队** | **法国、瑞典** | |\n\n### Group I\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 哥伦比亚 (Colombia) | -(小组第一) |\n| 2 | 比利时 (Belgium) | -(小组第二) |\n| 3 | 塞内加尔 (Senegal) | -(第三名出线) |\n| 4 | 伊朗 (Iran) | - |\n| **出线球队** | **哥伦比亚、比利时、塞内加尔**(含最佳第三名) | |\n\n### Group J\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 英格兰 (England) | -(小组第一) |\n| 2 | 美国 (USA) | -(小组第二) |\n| 3 | 波黑 (Bosnia and Herzegovina) | - |\n| 4 | 厄瓜多尔 (Ecuador) | - |\n| **出线球队** | **英格兰、美国** | |\n\n### Group K\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 荷兰 (Netherlands) | -(小组第一) |\n| 2 | 阿尔及利亚 (Algeria) | -(小组第二) |\n| 3 | 其他球队 | - |\n| 4 | 其他球队 | - |\n| **出线球队** | **荷兰、阿尔及利亚** | |\n\n### Group L\n| 排名 | 球队 | 积分 |\n|------|------|------|\n| 1 | 加纳 (Ghana) | -(小组第一) |\n| 2 | 其他球队 | -(小组第二) |\n| 3 | 其他球队 | - |\n| 4 | 其他球队 | - |\n| **出线球队** | **加纳**(小组第一) | |\n\n> **注**:由于搜索结果信息存在部分碎片化和不一致,部分小组的完整 standings 可能存在不准确之处。以上信息基于多次网络搜索整合而成。\n\n---\n\n## 三、1/16决赛(Round of 32)对阵情况\n\n1/16决赛于2026年6月28日至7月3日举行,共16场比赛。以下为已确认的对阵和部分结果:\n\n### 已进行的比赛结果\n\n| 日期 | 对阵 | 比分 | 备注 |\n|------|------|------|------|\n| 6月28日 | 南非 (South Africa) vs 加拿大 (Canada) | 0-1 | 加拿大晋级 |\n| 6月29日 | 巴西 (Brazil) vs 日本 (Japan) | 2-1 | 巴西晋级 |\n| 6月29日 | 德国 (Germany) vs 巴拉圭 (Paraguay) | 1-1 (点球 3-4) | 巴拉圭点球胜出晋级 |\n| 6月28日 | 荷兰 (Netherlands) vs 摩洛哥 (Morocco) | 1-1 (点球 2-3) | 摩洛哥点Line truncated
},
"result_text": "执行确认后的工具调用时遇到问题。",
"result_files": [],
"result_images": []
}
],
"assistant_response": "执行确认后的工具调用时遇到问题。",
"timestamp": "human_4a6b7e530f51404198747baec7717c68"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

@@ -1,252 +0,0 @@
{
"session_id": "20260705_095248",
"turn_id": "20260705_095332",
"user_message": {
"role": "user",
"content": "1、打开https://erpdemo.fanpusoft.com/main.aspx#\\\\r\\\\n2、点击采购管理\\\\r\\\\n3、点击新增供应商\\\\r\\\\n4、在新增供应商页面进行以下操作:\\\\r\\\\n输入供应商名称:“minor”、输入供应商类别:“工程材料供应商”、输入部门:“采购部门B”、输入职务:“CEO”"
},
"tool_calls": [
{
"name": "browser_control",
"args": {
"action": "open",
"url": "https://erpdemo.fanpusoft.com/main.aspx#\\"
},
"result_text": "已尝试打开 URL: https://erpdemo.fanpusoft.com/main.aspx#\\",
"result_files": [
"_artifact_browser_control_0_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "click",
"element_description": "采购管理菜单",
"target_type": "link"
}
]
},
"result_text": "1. Clicked target. grounded (399,126)",
"result_files": [
"_artifact_gui_tool_0_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "click",
"element_description": "供应商管理菜单",
"target_type": "list_item"
}
]
},
"result_text": "1. Clicked target. grounded (44,355)",
"result_files": [
"_artifact_gui_tool_1_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "click",
"element_description": "新增供应商按钮",
"target_type": "link"
}
]
},
"result_text": "1. Clicked target. grounded (323,311)",
"result_files": [
"_artifact_gui_tool_2_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "type",
"element_description": "供应商名称输入框",
"text": "minor"
}
]
},
"result_text": "1. Clicked target and typed text. grounded (309,457)",
"result_files": [
"_artifact_gui_tool_3_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "type",
"element_description": "部门输入框",
"text": "采购部门B"
},
{
"action": "type",
"element_description": "职务输入框",
"text": "CEO"
}
]
},
"result_text": "1. Clicked target and typed text. grounded (308,560)\n2. Clicked target and typed text. grounded (460,566)",
"result_files": [
"_artifact_gui_tool_4_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "hotkey",
"keys": [
"enter"
]
}
]
},
"result_text": "1. Pressed hotkey ['enter'].",
"result_files": [
"_artifact_gui_tool_5_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "click",
"element_description": "保存按钮",
"target_type": "button"
}
]
},
"result_text": "1. Clicked target. grounded (176,234)",
"result_files": [
"_artifact_gui_tool_6_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "click",
"element_description": "部门输入框",
"target_type": "input"
}
]
},
"result_text": "1. Clicked target. grounded (299,565)",
"result_files": [
"_artifact_gui_tool_7_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "click",
"element_description": "确定按钮",
"target_type": "button"
}
]
},
"result_text": "1. Clicked target. grounded (572,640)",
"result_files": [
"_artifact_gui_tool_8_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "click",
"element_description": "部门输入框",
"target_type": "input"
}
]
},
"result_text": "1. Clicked target. grounded (302,563)",
"result_files": [
"_artifact_gui_tool_9_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "type",
"element_description": "部门输入框",
"text": "采购部门B",
"overwrite": true
}
]
},
"result_text": "1. Clicked target and typed text. grounded (304,562)",
"result_files": [
"_artifact_gui_tool_10_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "click",
"element_description": "保存按钮",
"target_type": "button"
}
]
},
"result_text": "1. Clicked target. grounded (176,235)",
"result_files": [
"_artifact_gui_tool_11_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "click",
"element_description": "确定按钮",
"target_type": "button"
}
]
},
"result_text": "1. Clicked target. grounded (571,646)",
"result_files": [
"_artifact_gui_tool_12_0.png"
],
"result_images": []
}
],
"assistant_response": "1. 分析上一轮执行结果:上一步调用了gui_tool点击“确定”按钮,截图显示提示框已关闭,当前处于新增供应商页面,所有字段均已正确填写并保存成功。\n2. 任务目标达成:\n - 供应商名称:\"minor\" (已填)\n - 供应商类别:\"工程材料供应商\" (已填)\n - 部门:\"采购部门B\" (已填,且已修正并保存)\n - 职务:\"CEO\" (已填)\n3. 下一步操作:任务已完成,直接回复用户。\n\n任务已完成。已成功新增供应商,信息如下:\n- 供应商名称:minor\n- 供应商类别:工程材料供应商\n- 部门:采购部门B\n- 职务:CEO\n系统提示“保存数据成功”。",
"timestamp": "20260705_095332"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 KiB

@@ -1,131 +0,0 @@
{
"session_id": "20260705_132839",
"turn_id": "20260705_132843",
"user_message": {
"role": "user",
"content": "1、打开https://erpdemo.fanpusoft.com/main.aspx#\\\\r\\\\n2、点击采购管理\\\\r\\\\n3、点击新增供应商\\\\r\\\\n4、在新增供应商页面进行以下操作:\\\\r\\\\n输入供应商名称:“minor”、输入供应商类别:“工程材料供应商”、输入部门:“采购部门B”、输入职务:“CEO”"
},
"tool_calls": [
{
"name": "browser_control",
"args": {
"action": "open",
"url": "https://erpdemo.fanpusoft.com/main.aspx"
},
"result_text": "已尝试打开 URL: https://erpdemo.fanpusoft.com/main.aspx",
"result_files": [
"_artifact_browser_control_0_0.png"
],
"result_images": []
},
{
"name": "gui_tool(test)",
"args": {
"actions": [
{
"action": "click",
"element_description": "顶部导航栏中的采购管理菜单",
"target_type": "button"
}
]
},
"result_text": "1. Clicked target at (791,136).",
"result_files": [
"_artifact_gui_tool(test)_0_0.png"
],
"result_images": []
},
{
"name": "gui_tool(test)",
"args": {
"actions": [
{
"action": "click",
"element_description": "新增供应商按钮",
"target_type": "button"
}
]
},
"result_text": "1. Clicked target at (636,319).",
"result_files": [
"_artifact_gui_tool(test)_1_0.png"
],
"result_images": []
},
{
"name": "gui_tool(test)",
"args": {
"actions": [
{
"action": "type",
"element_description": "供应商名称输入框",
"text": "minor"
}
]
},
"result_text": "1. Clicked target and typed text at (547,496).",
"result_files": [
"_artifact_gui_tool(test)_2_0.png"
],
"result_images": []
},
{
"name": "gui_tool(test)",
"args": {
"actions": [
{
"action": "hotkey",
"keys": [
"enter"
]
},
{
"action": "wait",
"wait_seconds": 0.5
}
]
},
"result_text": "1. Pressed hotkey ['enter'].\n2. Waited 0.50 seconds.",
"result_files": [
"_artifact_gui_tool(test)_3_0.png"
],
"result_images": []
},
{
"name": "gui_tool(test)",
"args": {
"actions": [
{
"action": "type",
"element_description": "部门输入框",
"text": "采购部门B"
}
]
},
"result_text": "1. Clicked target and typed text at (493,603).",
"result_files": [
"_artifact_gui_tool(test)_4_0.png"
],
"result_images": []
},
{
"name": "gui_tool(test)",
"args": {
"actions": [
{
"action": "type",
"element_description": "职务输入框",
"text": "CEO"
}
]
},
"result_text": "1. Clicked target and typed text at (887,611).",
"result_files": [
"_artifact_gui_tool(test)_5_0.png"
],
"result_images": []
}
],
"assistant_response": "1. 先分析上一轮执行结果:上一个调用了 gui_tool(test) 在“职务”输入框中输入了“CEO”。从截图来看,“职务”输入框中已经成功显示了“CEO”,输入操作成功。\n2. 回顾任务要求,需要输入的信息包括:\n - 供应商名称:“minor” -> 已完成\n - 供应商类别:“工程材料供应商” -> 截图显示默认值即为“工程材料供应商”,符合要求。\n - 部门:“采购部门B” -> 已完成\n - 职务:“CEO” -> 已完成\n3. 所有要求的字段均已按要求填入。任务基本完成(虽然通常接下来会点击保存,但任务描述中只要求输入这些操作)。\n4. 既然所有指定的输入操作都已完成,我可以向用户报告任务完成。\n\n任务已完成。已在新增供应商页面成功输入:\n- 供应商名称:minor\n- 供应商类别:工程材料供应商\n- 部门:采购部门B\n- 职务:CEO",
"timestamp": "20260705_132843"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

@@ -0,0 +1,122 @@
{
"session_id": "20260705_202711",
"turn_id": "20260705_224918",
"user_message": {
"role": "user",
"content": "1、打开https://erpdemo.fanpusoft.com/main.aspx#\r\n2、点击采购管理\r\n3、点击新增供应商\r\n4、在新增供应商页面进行以下操作:输入供应商名称:“minor”、输入供应商类别:“工程材料供应商”、输入部门:“采购部门B”、输入职务:“CEO”、输入联系人:“赵永顺”、输入生日:“2004/07/09”\r\n5、点击保存、点击确定"
},
"tool_calls": [
{
"name": "browser_control",
"args": {
"action": "open",
"url": "https://erpdemo.fanpusoft.com/main.aspx#"
},
"result_text": "已尝试打开 URL: https://erpdemo.fanpusoft.com/main.aspx#",
"result_files": [
"_artifact_browser_control_0_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "click",
"element_description": "顶部导航栏的采购管理图标",
"target_type": "button"
}
]
},
"result_text": "1. Clicked target at (789,114).",
"result_files": [
"_artifact_gui_tool_0_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "click",
"element_description": "供应商流程下的新增供应商图标",
"target_type": "button"
}
]
},
"result_text": "1. Clicked target at (628,337).",
"result_files": [
"_artifact_gui_tool_1_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "type",
"element_description": "供应商名称输入框",
"text": "minor"
},
{
"action": "type",
"element_description": "供应商类别下拉框",
"text": "工程材料供应商"
},
{
"action": "type",
"element_description": "部门输入框",
"text": "采购部门B"
},
{
"action": "type",
"element_description": "职务输入框",
"text": "CEO"
},
{
"action": "type",
"element_description": "联系人输入框",
"text": "赵永顺"
},
{
"action": "type",
"element_description": "生日输入框",
"text": "2004/07/09"
}
]
},
"result_text": "1. Clicked target and typed text at (593,490).\n2. Clicked target and typed text at (1344,490).\n3. Clicked target and typed text at (455,602).\n4. Clicked target and typed text at (833,602).\n5. Clicked target and typed text at (1740,537).\n6. Clicked target and typed text at (1724,602).",
"result_files": [
"_artifact_gui_tool_2_0.png"
],
"result_images": []
},
{
"name": "gui_tool",
"args": {
"actions": [
{
"action": "click",
"element_description": "页面空白处,用于关闭日期选择器",
"target_type": "button"
},
{
"action": "click",
"element_description": "顶部工具栏的保存按钮",
"target_type": "button"
}
]
},
"result_text": "1. Clicked target at (1158,612).\n2. Clicked target at (334,251).",
"result_files": [
"_artifact_gui_tool_3_0.png"
],
"result_images": []
}
],
"assistant_response": "1. 分析上一轮执行结果:点击保存后,系统显示“保存成功”。通常接下来需要点击“确定”或刷新/返回列表。截图显示保存成功提示,但没有出现弹窗的“确定”按钮。\n2. 观察截图:点击保存后,页面上方显示“保存成功”的文字提示,没有弹窗。\n3. 重新审视任务:任务要求“点击保存、点击确定”。既然保存成功后没有弹窗,可能是点击保存后任务已完成,或者需要手动关闭提示。通常这种简单的提示点击空白处或忽略即可,或者系统自动跳转。\n4. 再次仔细看图,发现“保存成功”四个字,并没有“确定”按钮。这可能意味着保存操作本身就是一个确认,或者“确定”按钮在别处。\n5. 不过,有些系统保存后会弹出模态框。让我检查一下是否漏看了。并没有模态框。\n6. 也许“确定”是指完成整个流程,或者保存后自动返回列表。\n7. 如果确实没有“确定”按钮,可能任务在点击“保存”后实质上已经完成了数据的录入。\n8. 再次观察,确实没有“确定”按钮。那我就认为点击保存后操作结束,或者系统自动处理了确认。\n9. 但是,如果必须点击“确定”,而界面上没有,可能需要关闭浏览器的消息提示?也没有。\n10. 最可能的情况是,保存成功后,系统默认认为操作已完成。我将向用户确认任务已完成。\n\n最终回复:任务已完成。供应商“minor”的信息已成功保存。",
"timestamp": "20260705_224918"
}
+47 -2
View File
@@ -3,8 +3,10 @@ MAIN_SYSTEM_PROMPT = """你是一个具备多模态理解能力的桌面操作
- 你的首要职责是理解用户的意图,并高效地完成用户的任务。
- 对于任何用户上传的图片、音频或文件,直接利用你的多模态能力分析并回答,不要为此调用截图等无关工具。
- 只有在信息确实无法从当前对话或上传内容中获取,且必须通过外部工具才能获得时,才调用工具。例如:需要查看当前屏幕状态、需要执行 GUI 操作、需要搜索网络信息等。
- 当面对复杂或专业性较强的任务时,优先调用 skill_router 工具查询是否有对应的操作指南(Skill),若有则严格遵循指南执行任务。
- 当你调用工具收到截图或返回结果后,必须仔细观察返回内容,基于实际结果继续推进任务,严禁脱离实际结果凭空编造。
- 进行浏览器操作时,慎用刷新工具,避免刷新导致页面状态丢失。
- 对于同一个页面下的GUI操作,必须将连续操作合并为一次 gui_tool 调用(actions 列表传入多个动作),严禁逐个单步调用。仅当页面发生跳转/加载后需要重新观察时,才允许分步调用,注意单次调用最多包含5个动作。
- 如果不确定是否需要工具,优先选择不调用,用已有信息给出最佳答案。
- 对于需要多次操作才能完成的任务,应主动使用 todo_list 工具规划步骤并逐步推进,并在任务完成后将todo_list中的所有项标记为已完成。
- 当任务完成时,向用户总结执行过程和结果。
@@ -113,15 +115,58 @@ PLAN_FIRST_CALL_PROMPT = """
""".strip()
REFLECTION_PROMPT = """
【请先在你的回复中输出你的反思,然后再调用工具或给出最终回复】
你需要先用简短的一句话在文字中思考上一步做了什么?下一步该做什么?然后进行相应的工具调用。格式如下:
【请先在你的回复中尽可能简短地输出你的反思,然后再调用工具或给出最终回复】
你需要先用尽可能简短的一句话在文字中思考上一步做了什么?下一步该做什么?然后进行相应的工具调用。格式如下:
1. 先分析上一轮执行结果:上一个调用了什么工具?结果是成功还是失败?
2. 如果上一轮是 GUI 操作(点击、输入等)但未命中目标或未达预期效果,请根据最新的截图重新估算目标位置,调整坐标或参数后重试。
3. 如果上一轮操作成功,说明你根据分析结果进行下一步操作。
4. 然后调用对应的工具执行下一步,或如果任务已完成则直接回复用户(注意只需要回复用户最终结果即可)。
【gui_tool 批量调用规则 - 务必遵守】
如果你计划在同一页面/界面执行多个连续的 GUI 操作(例如:依次点击多个按钮、填写多个表单字段、连续输入多项内容),必须将所有这些操作合并为一次 gui_tool 调用,在 actions 列表中一次性传入所有动作。严禁将同一页面的连续操作拆分多次 gui_tool 调用——这样会导致严重的延迟。仅当需要等页面跳转/加载后才能确定后续操作时,才分步调用。
""".strip()
TEAM_MODE_PROMPT = """
Team 模式目前处于预留阶段,多 Agent 协同功能尚未实现。
请向用户说明当前无法启动 Team 协同模式,并建议使用 Agent 模式来完成单 Agent 任务。
""".strip()
# ---------- Skill Router Prompt ----------
SKILL_ROUTER_PROMPT = """你是一个 Skill 路由助手。根据用户 query,从下方 Skill 列表中选出最相关的 1~3 个 Skill。
只输出匹配的 Skill name,每个一行;没有匹配的则输出 "NONE"。
不要输出任何解释、说明或额外文字。
输出格式(严格遵守):
skill_name_1
skill_name_2
=== 示例 ===
用户 query: "帮我打开浏览器搜索Python教程"
Skill 列表:
- name: hello_world
description: 一个示例 Skill,用于演示 skill 系统的基本用法。当用户询问如何打招呼或进行简单问候时匹配。
tags: 示例, 入门
- name: web_browser
description: 浏览器自动化操作指南,涵盖打开浏览器、搜索、导航、关闭标签页等操作。
tags: 浏览器, 自动化, 搜索
- name: file_cleanup
description: 系统文件清理指南,帮助用户清理临时文件、回收站、缓存等。
tags: 清理, 系统, 磁盘
输出:
web_browser
=== 当前请求 ===
用户 query: {query}
Skill 列表:
{skills_block}
输出:"""
SKILL_ROUTER_NO_MATCH_MSG = "skill_router: 大模型未匹配到与当前查询相关的 skill。"
SKILL_ROUTER_EMPTY_MSG = "skill_router: 匹配的 skill 内容为空。"
SKILL_ROUTER_NO_QUERY_MSG = "skill_router: 请提供有效的查询内容。"
SKILL_ROUTER_NO_SKILLS_MSG = (
"skill_router: 当前没有安装任何 skill。\n"
"你可以在系统配置 -> Skills 面板中创建新的 skill,"
"或者在 skills 目录下手动创建 skill 文件夹(含 skill.json 和 skill.md)。\n"
"创建后无需重启,热加载即可使用。"
)
+349
View File
@@ -0,0 +1,349 @@
"""Skill 管理器:负责 skill 的增删改查及附件管理。
每个 skill 是一个文件夹,位于 Skills 目录下,结构如下::
skills/
{skill_name}/
skill.json - 基本信息与描述
skill.md - 主体内容(指南)
attachments/ - 可选附件(.py/.docx/.csv/.xlsx 等)
系统定位:
为 skill_router 工具和前端 API 提供统一的数据读写接口。
可扩展性:
可增加版本校验、导入/导出、标签过滤等功能。
"""
from __future__ import annotations
import json
import os
import shutil
from pathlib import Path
from typing import Any
SKILLS_DIR = Path(__file__).resolve().parent
def _ensure_skills_dir() -> None:
"""确保 skills 目录存在。"""
os.makedirs(SKILLS_DIR, exist_ok=True)
# ---------- Skill 信息数据类 ----------
class SkillInfo:
"""单个 Skill 的基本信息。"""
name: str
description: str
version: str
author: str
tags: list[str]
def __init__(self, name: str = "", description: str = "", version: str = "1.0.0",
author: str = "", tags: list[str] | None = None):
self.name = name
self.description = description
self.version = version
self.author = author
self.tags = tags or []
def to_dict(self) -> dict:
return {
"name": self.name,
"description": self.description,
"version": self.version,
"author": self.author,
"tags": self.tags,
}
@classmethod
def from_dict(cls, d: dict, name: str = "") -> "SkillInfo":
return cls(
name=d.get("name", name),
description=d.get("description", ""),
version=d.get("version", "1.0.0"),
author=d.get("author", ""),
tags=d.get("tags", []),
)
# ---------- CRUD ----------
def list_skills() -> list[dict]:
"""列出所有 skill 的基本信息(不含主体内容)。
输出:
list[dict]: 每项含 name, description, version, author, tags, has_attachments
"""
_ensure_skills_dir()
result = []
for entry in sorted(SKILLS_DIR.iterdir()):
if not entry.is_dir():
continue
json_path = entry / "skill.json"
if not json_path.is_file():
continue
try:
with open(json_path, "r", encoding="utf-8") as f:
meta = json.load(f)
except (json.JSONDecodeError, OSError):
continue
meta["name"] = entry.name
meta.setdefault("description", "")
meta.setdefault("version", "1.0.0")
meta.setdefault("author", "")
meta.setdefault("tags", [])
# 检查是否有附件
attach_dir = entry / "attachments"
meta["has_attachments"] = attach_dir.is_dir() and bool(list(attach_dir.iterdir()))
meta["attachment_files"] = _list_attachments(entry.name)
result.append(meta)
return result
def get_skill(name: str) -> dict | None:
"""获取单个 skill 的完整信息(含主体内容和附件列表)。
输出:
dict 或 None,字段: name, description, version, author, tags,
content (skill.md 内容), attachment_files
"""
skill_dir = SKILLS_DIR / name
if not skill_dir.is_dir():
return None
json_path = skill_dir / "skill.json"
md_path = skill_dir / "skill.md"
meta = {}
if json_path.is_file():
try:
with open(json_path, "r", encoding="utf-8") as f:
meta = json.load(f)
except (json.JSONDecodeError, OSError):
pass
content = ""
if md_path.is_file():
try:
with open(md_path, "r", encoding="utf-8") as f:
content = f.read()
except OSError:
pass
return {
"name": name,
"description": meta.get("description", ""),
"version": meta.get("version", "1.0.0"),
"author": meta.get("author", ""),
"tags": meta.get("tags", []),
"content": content,
"attachment_files": _list_attachments(name),
}
def create_or_update_skill(name: str, description: str = "", content: str = "",
version: str = "1.0.0", author: str = "",
tags: list[str] | None = None) -> bool:
"""创建或更新一个 skill。
输入:
name: skill 名称(即文件夹名)。
description: 简述。
content: skill.md 的内容。
version: 版本号。
author: 作者。
tags: 标签列表。
输出:
成功返回 True。
"""
_ensure_skills_dir()
if not name or not name.strip():
return False
name = name.strip().replace(" ", "_")
skill_dir = SKILLS_DIR / name
os.makedirs(skill_dir, exist_ok=True)
# 写 skill.json
meta = {
"name": name,
"description": description,
"version": version,
"author": author,
"tags": tags or [],
}
json_path = skill_dir / "skill.json"
try:
with open(json_path, "w", encoding="utf-8") as f:
json.dump(meta, f, ensure_ascii=False, indent=2)
except OSError:
return False
# 写 skill.md
md_path = skill_dir / "skill.md"
try:
with open(md_path, "w", encoding="utf-8") as f:
f.write(content)
except OSError:
return False
return True
def delete_skill(name: str) -> bool:
"""删除一个 skill 及其所有附件。
输出:
存在且删除成功返回 True,不存在返回 False。
"""
skill_dir = SKILLS_DIR / name
if not skill_dir.is_dir():
return False
shutil.rmtree(skill_dir, ignore_errors=True)
return True
# ---------- 附件管理 ----------
def _attachments_dir(skill_name: str) -> Path:
return SKILLS_DIR / skill_name / "attachments"
def _list_attachments(skill_name: str) -> list[str]:
"""列出某 skill 的所有附件文件名。"""
ad = _attachments_dir(skill_name)
if not ad.is_dir():
return []
result = []
for root, dirs, files in os.walk(ad):
for fname in files:
rel = os.path.relpath(os.path.join(root, fname), ad)
result.append(rel.replace("\\", "/"))
result.sort()
return result
def add_attachment(skill_name: str, filename: str, content_bytes: bytes) -> bool:
"""上传/更新一个附件文件到 skill。
输入:
skill_name: skill 名称。
filename: 附件文件名(可包含相对子路径,如 "scripts/run.py")。
content_bytes: 文件二进制内容。
输出:
成功返回 True。
"""
skill_dir = SKILLS_DIR / skill_name
if not skill_dir.is_dir():
return False
ad = _attachments_dir(skill_name)
os.makedirs(ad, exist_ok=True)
# 安全检查:防止路径穿越
safe_name = filename.replace("\\", "/").strip("/")
if ".." in safe_name:
return False
target = ad / safe_name
target.parent.mkdir(parents=True, exist_ok=True)
try:
with open(target, "wb") as f:
f.write(content_bytes)
return True
except OSError:
return False
def delete_attachment(skill_name: str, filename: str) -> bool:
"""删除某 skill 下的一个附件。
输出:
存在且删除成功返回 True。
"""
ad = _attachments_dir(skill_name)
safe_name = filename.replace("\\", "/").strip("/")
if ".." in safe_name:
return False
target = ad / safe_name
if not target.is_file():
return False
try:
os.remove(target)
# 移除空父目录
parent = target.parent
while parent != ad and parent != SKILLS_DIR:
try:
parent.rmdir()
except OSError:
break
parent = parent.parent
return True
except OSError:
return False
def get_attachment_bytes(skill_name: str, filename: str) -> bytes | None:
"""获取附件文件的二进制内容。"""
ad = _attachments_dir(skill_name)
safe_name = filename.replace("\\", "/").strip("/")
if ".." in safe_name:
return None
target = ad / safe_name
if not target.is_file():
return None
try:
with open(target, "rb") as f:
return f.read()
except OSError:
return None
# ---------- 供 skill_router 使用的批量加载 ----------
def load_all_skill_contents() -> dict[str, str]:
"""加载所有 skill 的 skill.md 内容,键为 skill 名称。
输出:
{skill_name: markdown_content, ...}
"""
_ensure_skills_dir()
result = {}
for entry in sorted(SKILLS_DIR.iterdir()):
if not entry.is_dir():
continue
md_path = entry / "skill.md"
if not md_path.is_file():
continue
try:
with open(md_path, "r", encoding="utf-8") as f:
result[entry.name] = f.read()
except OSError:
pass
return result
def load_all_skill_metas() -> dict[str, dict]:
"""加载所有 skill 的 skill.json 元信息,键为 skill 名称。
输出:
{skill_name: {name, description, version, author, tags}, ...}
"""
_ensure_skills_dir()
result = {}
for entry in sorted(SKILLS_DIR.iterdir()):
if not entry.is_dir():
continue
json_path = entry / "skill.json"
if not json_path.is_file():
continue
try:
with open(json_path, "r", encoding="utf-8") as f:
meta = json.load(f)
except (json.JSONDecodeError, OSError):
continue
meta["name"] = entry.name
meta.setdefault("description", "")
meta.setdefault("version", "1.0.0")
meta.setdefault("author", "")
meta.setdefault("tags", [])
result[entry.name] = meta
return result
+7
View File
@@ -0,0 +1,7 @@
{
"name": "hello_world",
"description": "一个示例 Skill,用于演示 skill 系统的基本用法。当用户询问如何打招呼或进行简单问候时匹配。",
"version": "1.0.0",
"author": "minor",
"tags": ["示例", "入门"]
}
+28
View File
@@ -0,0 +1,28 @@
# Hello World Skill
这是一个示例 Skill,用于演示 Skill 系统的基本结构。
## 适用场景
当用户发出问候语(如"你好"、"hello"、"hi"等)时,Agent 可调用 skill_router 查询此 skill,获取本指南。
## 操作指南
1. 使用友好、热情的语气回复用户的问候
2. 自我介绍:说明你是 minor 桌面操作助手
3. 主动询问用户是否需要帮助完成某项任务
4. 简要介绍你的能力范围:GUI 自动化、文件处理、网页浏览、知识检索等
## 示例回复模板
```
你好!我是 minor,你的桌面操作助手。
我可以帮你:
- 进行 GUI 自动化操作(打开应用、点击、输入等)
- 搜索网络信息
- 处理文档文件
- 管理本地文件
有什么可以帮你的吗?
```
+14 -6
View File
@@ -1,9 +1,16 @@
from .tool import *
from .tool_software_control import SoftwareControl
from .gui_tool import GUITool
from .tool_rag import RAGTool
from .tool_agent_call import CallSubAgentTool
from .doc_tool import DocTool
from .web_search import WebSearch
from .date_query import DateQuery
from .browser_control import BrowserControl
from .human_interaction import HumanInteraction
from .directory_listing import DirectoryListing
from .todo_list import TodoList
from .software_control import SoftwareControl
from .gui import GUITool
from .rag import RAGTool
from .agent_call import CallSubAgentTool
from .doc import DocTool
from .skill_router import SkillRouter
_all_tools = [
WebSearch(),
DateQuery(),
@@ -16,6 +23,7 @@ _all_tools = [
GUITool(),
CallSubAgentTool(),
DocTool(),
SkillRouter(),
]
# 注册所有可用工具名称(供前端配置面板展示),从 _all_tools 实例推导
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File renamed without changes.
+187
View File
@@ -0,0 +1,187 @@
from __future__ import annotations
import io
import sys
import time
import webbrowser
import asyncio
from typing import Optional, Literal, Tuple
import pyautogui
from PIL import Image
from langchain.tools import BaseTool
from pydantic import BaseModel, Field, create_model
import agent.utils.env_utils as env_utils
class BrowserControl(BaseTool):
"""统一浏览器控制工具:支持打开/关闭浏览器、刷新/关闭标签页、打开指定 URL"""
args_schema: type[BaseModel] = create_model(
"BrowserControlInput",
action=(Literal["open", "close"], Field(
default="open",
description="操作类型:'open' 打开/刷新,'close' 关闭"
)),
url=(Optional[str], Field(
default=None,
description="URL 地址、'current' 表示当前页,留空则操作整个浏览器窗口"
)),
browser=(Optional[str], Field(
default=None,
description="浏览器名称,如 'edge', 'chrome', 'firefox',不指定则使用系统默认"
)),
)
name: str = "browser_control"
description: str = (
"统一控制浏览器:打开/关闭浏览器、刷新/关闭标签页、打开指定 URL。\n"
"- action='open', url=None: 打开浏览器首页\n"
"- action='open', url='current': 刷新当前标签页\n"
"- action='open', url='https://...': 在新标签页打开指定 URL\n"
"- action='close', url=None: 关闭整个浏览器窗口\n"
"- action='close', url='任意非空字符(例如current)': 关闭当前标签页\n"
"操作完成后会返回当前屏幕截图,帮助判断操作是否成功。"
"仅用于进行浏览器操作时调用,其余任何情况严禁调用。"
)
response_format: Literal["content", "content_and_artifact"] = "content_and_artifact"
# ---------- 辅助方法(与原版相同,无改动) ----------
def _get_browser_info(self, browser_name: Optional[str]):
"""解析浏览器名称,返回 webbrowser 控制器与窗口标题关键字。"""
if browser_name is None:
return webbrowser.get(), None
name_lower = browser_name.lower().strip()
if name_lower not in env_utils.BROWSER_MAP:
raise ValueError(f"不支持的浏览器: {browser_name},支持: {', '.join(env_utils.BROWSER_MAP.keys())}")
reg_name, title_keyword = env_utils.BROWSER_MAP[name_lower]
try:
controller = webbrowser.get(reg_name)
except webbrowser.Error:
controller = webbrowser.get(browser_name)
return controller, title_keyword
def _activate_window(self, title_keyword: str) -> bool:
"""在 Windows 上按标题关键字激活浏览器窗口(非 Win32 返回 False)。"""
if sys.platform != "win32":
return False
try:
time.sleep(0.3)
windows = pyautogui.getWindowsWithTitle(title_keyword)
if windows:
win = windows[0]
if win.isMinimized:
win.restore()
win.activate()
return True
except Exception:
pass
return False
def _send_keys(self, *keys):
"""通过 pyautogui 发送组合键。"""
pyautogui.hotkey(*keys)
# ---------- 核心操作(返回操作结果描述) ----------
def _try_activate_any_browser(self) -> bool:
"""尝试激活任意已知浏览器窗口(不指定 browser 时的回退方案)。"""
for _reg_name, title_keyword in env_utils.BROWSER_MAP.values():
if self._activate_window(title_keyword):
return True
return False
def _open_action(self, url: Optional[str], browser: Optional[str]) -> str:
"""执行打开首页、刷新当前页或打开指定 URL。"""
controller, title_keyword = self._get_browser_info(browser)
if url is None:
controller.open("about:blank")
if title_keyword:
self._activate_window(title_keyword)
else:
self._try_activate_any_browser()
browser_info = f" (使用 {browser})" if browser else ""
return f"已打开浏览器首页{browser_info}"
elif url.lower() == "current":
if title_keyword:
if not self._activate_window(title_keyword):
return f"未找到 {browser or '默认'} 浏览器窗口,无法刷新"
time.sleep(0.2)
if sys.platform == "darwin":
self._send_keys("command", "r")
else:
self._send_keys("f5")
return "已刷新当前标签页"
else:
controller.open(url)
if title_keyword:
self._activate_window(title_keyword)
else:
self._try_activate_any_browser()
# 等待浏览器窗口出现并开始加载
time.sleep(1.0)
return f"已尝试打开 URL: {url}"
def _close_action(self, url: Optional[str], browser: Optional[str]) -> str:
"""执行关闭浏览器窗口或当前标签页。"""
_, title_keyword = self._get_browser_info(browser)
if url is None:
if title_keyword:
if not self._activate_window(title_keyword):
return f"未找到 {browser or '默认'} 浏览器窗口,无法关闭"
else:
self._try_activate_any_browser()
time.sleep(0.2)
if sys.platform == "darwin":
self._send_keys("command", "q")
else:
self._send_keys("alt", "f4")
return "已尝试关闭浏览器窗口"
else:
if title_keyword:
if not self._activate_window(title_keyword):
return f"未找到 {browser or '默认'} 浏览器窗口,无法关闭标签页"
else:
self._try_activate_any_browser()
time.sleep(0.2)
if sys.platform == "darwin":
self._send_keys("command", "w")
else:
self._send_keys("ctrl", "w")
return "已尝试关闭当前标签页"
# ---------- 同步主逻辑,返回 (描述, artifact) ----------
def _run(self, action: str = "open", url: Optional[str] = None, browser: Optional[str] = None) -> Tuple[str, dict]:
try:
if action == "open":
msg = self._open_action(url, browser)
elif action == "close":
msg = self._close_action(url, browser)
else:
msg = f"不支持的操作类型: {action}"
except Exception as e:
msg = f"执行浏览器操作时出错: {e}"
# 无论操作是否成功,都尝试截屏(等待窗口切换)
time.sleep(0.3)
try:
# 先尝试获取 GUI 操作区域,限定截屏范围
try:
from agent.core.config_manager import get_active_monitor_region
region = get_active_monitor_region()
except Exception:
region = None
if region:
pil_img = pyautogui.screenshot(region=(region["x"], region["y"], region["width"], region["height"]))
else:
pil_img = pyautogui.screenshot()
pil_img = pil_img.resize((env_utils.GROUNDING_WIDTH, env_utils.GROUNDING_HEIGHT), Image.Resampling.LANCZOS)
buf = io.BytesIO()
pil_img.save(buf, format="PNG")
raw_png = buf.getvalue()
return msg, {"image_bytes": raw_png}
except Exception as e:
return f"{msg}\n(截屏失败: {e})", {}
# ---------- 异步入口 ----------
async def _arun(self, action: str = "open", url: Optional[str] = None, browser: Optional[str] = None) -> Tuple[str, dict]:
return await asyncio.to_thread(self._run, action, url, browser)
+23
View File
@@ -0,0 +1,23 @@
from __future__ import annotations
from langchain.tools import BaseTool
from pydantic import BaseModel, Field, create_model
class DateQuery(BaseTool):
# 定义工具的输入参数结构,这里没有输入参数,所以是一个空的模型
args_schema : type[BaseModel] = create_model("NoneInput")
name : str = "date_query_class"
description : str = "一个日期查询工具,可以返回当前的日期信息。仅当需要查询当前时间时调用,其余任何情况严禁调用。"
def _run(self) -> str:
try:
from datetime import datetime
now = datetime.now()
return now.strftime("%Y-%m-%d %H:%M:%S")
except Exception as e:
return f"执行日期查询时出错: {str(e)}"
async def _arun(self) -> str:
return self._run()
+61
View File
@@ -0,0 +1,61 @@
from __future__ import annotations
import os
import asyncio
from pathlib import Path
from typing import Optional
from langchain.tools import BaseTool
from pydantic import BaseModel, Field, create_model
class DirectoryListing(BaseTool):
"""列出指定目录下的文件和文件夹,帮助 Agent 了解本地文件结构。"""
args_schema: type[BaseModel] = create_model(
"DirectoryListingInput",
path=(
Optional[str],
Field(
default=None,
description="要查看的目录路径,默认为当前工作目录。"
),
),
show_hidden=(
bool,
Field(
default=False,
description="是否显示隐藏文件(以点开头的文件/文件夹),默认不显示。"
),
),
)
name: str = "directory_listing"
description: str = (
"列出指定目录下的所有文件和子目录。可用于探索文件系统、查找文件位置。"
"仅当需要获取目录内容或确认文件结构时调用,其余情况严禁调用。"
)
def _run(self, path: Optional[str] = None, show_hidden: bool = False) -> str:
try:
target = Path(path).resolve() if path else Path.cwd()
if not target.exists():
return f"路径不存在: {target}"
if not target.is_dir():
return f"路径不是目录: {target}"
items = []
with os.scandir(target) as entries:
for entry in entries:
if not show_hidden and entry.name.startswith('.'):
continue
item_type = "DIR" if entry.is_dir() else "FILE"
items.append(f"[{item_type}] {entry.name}")
if not items:
return f"目录 {target} 为空"
return f"目录 {target} 的内容:\n" + "\n".join(sorted(items))
except Exception as e:
return f"列出目录时出错: {str(e)}"
async def _arun(self, path: Optional[str] = None, show_hidden: bool = False) -> str:
return await asyncio.to_thread(self._run, path, show_hidden)
File renamed without changes.
@@ -77,6 +77,7 @@ class GUIActionItem(BaseModel):
hold_keys: list[str] = Field(default_factory=list, description="点击或拖拽时按住的功能键。")
keys: list[str] = Field(default_factory=list, description="快捷键按键 / 敲击按键。")
press_keys: list[str] = Field(default_factory=list, description="按住 hold_keys 时敲击的按键。")
hotkey_count: int = Field(default=1, description="快捷键重复按下次数,仅 hotkey 操作用。")
clicks: int = Field(default=0, description="滚轮量,负数为向下。")
shift: bool = Field(default=False, description="水平滚动(True 时为横向)。")
wait_seconds: float = Field(default=0.5, description="等待秒数。")
@@ -234,31 +235,14 @@ class GUITool(BaseTool):
"{\"action\":\"hotkey\",\"keys\":[\"space\"]}(空格键即 Page Down)。"
"向上滚动可使用 {\"action\":\"hotkey\",\"keys\":[\"shift\",\"space\"]}。\n"
"\n"
"【下拉框处理策略】操作下拉选择框时分两步:\n"
" 第1步(type 方式):先尝试 {\"action\":\"type\",\"element_description\":\"目标下拉框\",\"text\":\"选项名称\"} 直接输入,\n"
" 能自动匹配则任务完成;\n"
" 第2步(键选方式):若 type 不生效,则改为先点击展开下拉框,再用方向键选择,最后回车确认:\n"
" {\"action\":\"click\",\"element_description\":\"目标下拉框\"},\n"
" {\"action\":\"hotkey\",\"keys\":[\"down\"]}(按需多次 up/down 定位),\n"
" {\"action\":\"hotkey\",\"keys\":[\"enter\"]}。\n"
"【下拉框处理策略】操作下拉选择框直接使用 type 动作即可,系统会自动匹配选项:\n"
" {\"action\":\"type\",\"element_description\":\"目标下拉框\",\"text\":\"选项名称\"}\n"
"\n"
"示例:\n"
"- 登录表单:{\"actions\": ["
"{\"action\":\"type\",\"element_description\":\"用户名输入框\",\"text\":\"admin\"},"
"{\"action\":\"type\",\"element_description\":\"密码输入框\",\"text\":\"123456\",\"enter\":true}"
"]}\n"
"- 浏览器搜索:{\"actions\": ["
"{\"action\":\"type\",\"element_description\":\"浏览器地址栏\",\"text\":\"github.com\",\"enter\":true}"
"]}\n"
"- 网页翻页浏览:{\"actions\": ["
"{\"action\":\"hotkey\",\"keys\":[\"space\"]},"
"{\"action\":\"wait\",\"wait_seconds\":1.0},"
"{\"action\":\"hotkey\",\"keys\":[\"space\"]}"
"]}\n"
"- 等待并确认:{\"actions\": ["
"{\"action\":\"click\",\"element_description\":\"提交按钮\"},"
"{\"action\":\"wait\",\"wait_seconds\":2.0}"
"]}"
)
response_format: Literal["content", "content_and_artifact"] = "content_and_artifact"
@@ -539,8 +523,10 @@ class GUITool(BaseTool):
keys = kwargs["keys"]
if not keys:
raise ValueError("keys is required for hotkey")
pyautogui.hotkey(*keys)
return f"Pressed hotkey {keys}."
count = max(1, int(kwargs.get("hotkey_count", 1)))
for _ in range(count):
pyautogui.hotkey(*keys)
return f"Pressed hotkey {keys} x{count}." if count > 1 else f"Pressed hotkey {keys}."
def _hold_and_press(self, **kwargs: Any) -> str:
hold_keys = kwargs["hold_keys"]
+85
View File
@@ -0,0 +1,85 @@
from __future__ import annotations
import json
import asyncio
from typing import Optional, Literal
from langchain.tools import BaseTool
from pydantic import BaseModel, Field
class HumanInteractionInput(BaseModel):
interaction_type: Literal["information", "selection", "approval"] = Field(
default="information",
description="交互类型:information=人工补充信息,selection=人工选择,approval=人工审批。",
)
title: str = Field(default="需要人工处理", description="展示给人类的交互标题。")
prompt: str = Field(default="请处理后继续。", description="需要人类理解并响应的具体问题或说明。")
options: Optional[list[str] | str] = Field(
default=None,
description="selection/approval 可选项。优先传字符串数组;如果传 JSON 字符串,系统会自动解析。",
)
suggested_response: Optional[str] = Field(default=None, description="可选的建议回复、默认选择或默认审批意见。")
class HumanInteraction(BaseTool):
"""向人类请求补充信息、选择或审批;实际 UI 确认由图级人工干预层承接。"""
args_schema: type[BaseModel] = HumanInteractionInput
name: str = "human_interaction"
description: str = (
"当任务缺少关键信息、需要人类选择路径、或需要人类审批后才能继续时调用。"
"三种类型分别是:information=请求人类补充缺失信息;selection=让人类在多个选项中选择且可编辑;"
"approval=请求人类审批高影响决策或继续执行。"
"工具返回必须被视为用户原始意图的纯字符串反馈。其余情况严禁调用。"
)
@staticmethod
def normalize_options(options: Optional[list[str] | str]) -> list[str]:
"""将 options 参数规范化为字符串列表(支持 JSON 与逗号分隔)。"""
if options is None:
return []
if isinstance(options, str):
raw = options.strip()
if not raw:
return []
try:
parsed = json.loads(raw)
except json.JSONDecodeError:
return [item.strip() for item in raw.replace(",", ",").split(",") if item.strip()]
if isinstance(parsed, list):
return [str(item) for item in parsed]
return [str(parsed)]
return [str(item) for item in options]
def _run(
self,
interaction_type: str = "information",
title: str = "需要人工处理",
prompt: str = "请处理后继续。",
options: Optional[list[str] | str] = None,
suggested_response: Optional[str] = None,
) -> str:
try:
normalized_options = self.normalize_options(options)
if interaction_type == "approval":
default = (suggested_response or "").strip()
return default or "用户同意"
if interaction_type == "selection":
default = (suggested_response or "").strip()
selected = default or (normalized_options[0] if normalized_options else "")
return f"用户选择:{selected}" if selected else "用户选择:"
response = (suggested_response or "").strip()
return f"用户补充信息如下:{response}" if response else "用户没有补充信息"
except Exception as e:
return f"[ERROR] human_interaction 工具执行出错: {str(e)}"
async def _arun(
self,
interaction_type: str = "information",
title: str = "需要人工处理",
prompt: str = "请处理后继续。",
options: Optional[list[str] | str] = None,
suggested_response: Optional[str] = None,
) -> str:
return await asyncio.to_thread(self._run, interaction_type, title, prompt, options, suggested_response)
File renamed without changes.
+122
View File
@@ -0,0 +1,122 @@
"""skill_router 工具:根据用户 query 调用大模型智能匹配并返回相关 skill 指南。
系统定位:
作为 Agent 工具集的一员,当用户任务可能匹配已有 skill 时,
调用此工具获取对应的操作指南,指引 Agent 更高效地完成任务。
调用方式:
skill_router(query="用户原始问题或任务描述")
"""
from __future__ import annotations
from langchain.tools import BaseTool
from pydantic import BaseModel, Field, create_model
from agent.skills import load_all_skill_metas, load_all_skill_contents
from agent.memory.system_prompt import (
SKILL_ROUTER_PROMPT,
SKILL_ROUTER_NO_MATCH_MSG,
SKILL_ROUTER_EMPTY_MSG,
SKILL_ROUTER_NO_QUERY_MSG,
SKILL_ROUTER_NO_SKILLS_MSG,
)
class SkillRouter(BaseTool):
"""Skill 路由工具:输入 query,调用大模型筛选匹配的 skill 并返回其指南全文。
工作流程:
1. 收集所有已安装 skill 的 skill.json 元信息
2. 构建 prompt,将所有 skill 摘要 + query 送给大模型做语义匹配
3. 解析大模型返回的 skill name 列表
4. 按 name 加载对应 skill.md 并返回
"""
name: str = "skill_router"
description: str = (
"Skill 路由工具。根据用户的 query(问题或任务描述),从已安装的 skill 中智能匹配"
"最相关的操作指南,并返回指南全文。\n"
"参数说明:\n"
"- query: 用户的原始问题或任务描述,用于匹配 skill。\n"
"使用场景:\n"
"- 当用户任务可能已有对应的操作指南时,调用此工具获取指南。\n"
"- 当不确定如何完成某项任务时,先查是否有相关 skill。\n"
"调用后,你会获得匹配 skill 的完整指南内容,请据此执行任务。"
)
args_schema: type[BaseModel] = create_model(
"SkillRouterInput",
query=(str, Field(description="用户的问题或任务描述,用于匹配相关 skill。")),
)
def _run(self, query: str) -> str:
if not query or not query.strip():
return SKILL_ROUTER_NO_QUERY_MSG
metas = load_all_skill_metas()
contents = load_all_skill_contents()
if not metas:
return SKILL_ROUTER_NO_SKILLS_MSG
# 构建 skill 摘要列表(仅含 JSON 中的元信息,不含 skill.md)
skill_lines = []
for name, meta in metas.items():
tags_str = ", ".join(meta.get("tags", []))
skill_lines.append(
f"- name: {name}\n"
f" description: {meta.get('description', '')}\n"
f" tags: {tags_str}"
)
skills_block = "\n".join(skill_lines)
prompt = SKILL_ROUTER_PROMPT.format(query=query, skills_block=skills_block)
matched_names = self._call_llm(prompt)
if not matched_names:
return SKILL_ROUTER_NO_MATCH_MSG
# 按匹配到的 name 加载对应 skill.md
parts = []
for name in matched_names:
if name not in contents:
continue
meta = metas.get(name, {})
parts.append(
f"## Skill: {name}\n"
f"描述: {meta.get('description', '')}\n\n"
f"{contents[name]}\n"
)
if not parts:
return SKILL_ROUTER_EMPTY_MSG
header = f"skill_router: 从 {len(matched_names)} 个 skill 中匹配到以下指南:\n\n"
return header + "\n---\n".join(parts)
def _call_llm(self, prompt: str) -> list[str] | None:
"""调用大模型解析 prompt,返回匹配的 skill name 列表。
输出:
匹配的 name 列表;解析失败或模型返回 NONE 时返回 None。
"""
from agent.core.llm import llm
from langchain_core.messages import HumanMessage
try:
response = llm.invoke([HumanMessage(content=prompt)])
text = response.content if isinstance(response.content, str) else str(response.content)
text = text.strip()
except Exception:
return None
if not text or text.upper() == "NONE":
return None
names = [line.strip() for line in text.split("\n") if line.strip()]
return names if names else None
async def _arun(self, query: str) -> str:
import asyncio
return await asyncio.to_thread(self._run, query)
@@ -480,11 +480,11 @@ class SoftwareControl(BaseTool):
records = _build_index()
ranked = _rank_matches(query, records)
if not ranked:
return f"未找到与“{query}”匹配的软件,请换一个更具体的软件名。"
return f'未找到与"{query}"匹配的软件,请换一个更具体的软件名。'
best_score, best_record = ranked[0]
if best_score < 0.72:
return f"未找到与“{query}”足够接近的软件,请换一个更具体的软件名。"
return f'未找到与"{query}"足够接近的软件,请换一个更具体的软件名。'
if len(ranked) > 1 and best_score < 0.88 and (best_score - ranked[1][0]) < 0.08:
candidates = [
f"{item.display_name} (score={score:.2f}, source={item.source})"
@@ -514,7 +514,7 @@ class SoftwareControl(BaseTool):
try:
opened_target = _open_record(best_record)
except Exception as exc:
return f"已匹配到软件“{best_record.display_name}”,但启动失败:{exc}"
return f'已匹配到软件"{best_record.display_name}",但启动失败:{exc}'
process_hint = ", ".join(best_record.process_names) if best_record.process_names else "unknown"
return (
@@ -551,25 +551,25 @@ class SoftwareControl(BaseTool):
)
if not force_requested:
return (
f"已向软件“{best_record.display_name}”发送正常关闭请求,但仍有进程未退出。\n"
f'已向软件"{best_record.display_name}"发送正常关闭请求,但仍有进程未退出。\n'
f"剩余 PID:{', '.join(str(pid) for pid in remaining)}\n"
f"如需强制关闭,请明确使用 force_close 动作后重试。"
)
killed = _force_kill_pids(remaining)
return (
f"软件“{best_record.display_name}”正常关闭未完全成功,已执行强制关闭。\n"
f'软件"{best_record.display_name}"正常关闭未完全成功,已执行强制关闭。\n'
f"强制结束 PID:{', '.join(str(pid) for pid in killed) if killed else 'none'}"
)
if not force_requested:
return (
f"已找到软件“{best_record.display_name}”的运行进程,但未找到可见主窗口,暂未自动强制关闭。\n"
f'已找到软件"{best_record.display_name}"的运行进程,但未找到可见主窗口,暂未自动强制关闭。\n'
f"运行 PID:{', '.join(str(pid) for pid in running_pids)}\n"
f"如需强制关闭,请明确使用 force_close 动作后重试。"
)
killed = _force_kill_pids(running_pids)
return (
f"软件“{best_record.display_name}”未找到可见主窗口,已按进程强制关闭。\n"
f'软件"{best_record.display_name}"未找到可见主窗口,已按进程强制关闭。\n'
f"强制结束 PID:{', '.join(str(pid) for pid in killed) if killed else 'none'}"
)
except Exception as e:
+176
View File
@@ -0,0 +1,176 @@
from __future__ import annotations
import json
from typing import Any, Optional, Literal
from langchain.tools import BaseTool
from pydantic import BaseModel, Field, model_validator
# ---------- 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):
for i in range(done_step):
if i not in done_steps:
done_steps.append(i)
done_steps.sort()
todo_data = {"steps": stored_steps, "done_steps": done_steps}
_todo_store[session_id] = todo_data
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: Optional[list[str]] = Field(
default=None,
description="任务步骤列表,每项为一步的描述。仅在首次创建计划时传入,后续更新进度时请省略此参数以节省 token。",
)
done_step: int = Field(
default=-1,
description="当前已推进到的步骤编号(从1开始)。传入 n 则步骤 1~n 全部标记为完成。-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):
"""任务管理工具,用于拆分复杂任务为多个步骤并跟踪进度。
Plan 模式下此工具为**必须调用**,Agent 模式下为可选。
首次调用时传入完整步骤列表,后续仅传 done_step 标记进度。
"""
args_schema: type[BaseModel] = TodoListInput
name: str = "todo_list"
description: str = (
"任务管理工具。用于将复杂任务拆分为多个步骤并跟踪执行进度。\n"
"Plan 模式下必须首先调用此工具创建任务计划;Agent 模式下按需调用。\n"
"参数说明:\n"
"- steps (list[str], 可选):任务步骤列表。仅在首次创建计划时传入所有步骤。\n"
" **步骤粒度要求**:同一页面/界面上的连续操作应合并为一条步骤,用\"并\"连接。例如:\n"
' * 推荐:\"点击搜索框,输入XXX并点击搜索\"(合并了点击、输入、搜索)\n'
' * 推荐:\"在结果中点击目标进入主页\"(合并了查找+点击)\n'
' * 禁止:\"1.点击搜索框 2.输入文字 3.点击搜索\"(过于细碎)\n'
"- done_step (int, 可选):当前已推进到的步骤编号(从1开始)。传入 n 则步骤 1~n 全部标记为已完成。不传或传 -1 则只查看当前状态。\n"
"调用示例:\n"
'- 首次创建(B站搜索示例):{\"steps\": [\"打开哔哩哔哩网站\", \"点击顶部搜索框,输入\'燕三嘤嘤嘤\'并点击搜索\", \"在搜索结果中找到燕三嘤嘤嘤并点击头像或名称进入他的主页\", \"在他的主页点开视频列表里的第一个视频\", \"验证视频是否正在播放\"], \"done_step\": -1}\n'
'- 已完成前2步:{\"done_step\": 2} (步骤1和2均标记为完成)\n'
'- 已完成前4步:{\"done_step\": 4}\n'
'- 查看状态:{\"done_step\": -1}\n'
"任务全部完成后无需再次调用。"
)
response_format: Literal["content", "content_and_artifact"] = "content"
def _run(self, steps: list[str] | None = None, done_step: int = -1) -> str:
try:
steps = steps or []
# 用简单变量维护状态(同一实例在 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 = []
# 标记步骤完成:传入 done_step 时,该步及之前所有步骤均标记为完成(1-indexed → 0-indexed)
if self._stored_steps and 1 <= done_step <= len(self._stored_steps):
for i in range(done_step):
if i not in self._done_set:
self._done_set.append(i)
self._done_set.sort()
todo_data = {
"steps": self._stored_steps,
"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)
except Exception as e:
return f"[ERROR] todo_list 工具执行出错: {str(e)}"
async def _arun(self, steps: list[str] | None = None, done_step: int = -1) -> str:
return self._run(steps, done_step)
-531
View File
@@ -1,531 +0,0 @@
from __future__ import annotations
import os
import io
import json
from typing import Any, Optional, Literal, Tuple
import sys
import time
import webbrowser
import pyautogui
import asyncio
from pathlib import Path
from PIL import Image
from langchain.tools import BaseTool
from pydantic import BaseModel, Field, create_model, model_validator
from agent.core.llm import llm
import agent.utils.env_utils as env_utils
class WebSearch(BaseTool):
args_schema : type[BaseModel] = create_model("Input_type", query=(str, Field(description="搜索查询内容")))
name : str = "web_search_class"
description : str = "一个网络搜索工具,可以根据用户的查询返回相关的搜索结果。仅当需要查询近期知识或已有知识无法解答需要联网查询时调用,其余任何情况严禁调用。"
def _get_search_wrapper(self):
"""懒加载 Tavily 搜索客户端。"""
from langchain_community.utilities import TavilySearchAPIWrapper
TAVILY_API_KEY = os.environ.get("TAVILY_API_KEY", "")
return TavilySearchAPIWrapper(tavily_api_key=TAVILY_API_KEY)
def _run(self, query: str) -> str:
try:
return self._get_search_wrapper().run(query)
except Exception as e:
return f"执行网络搜索时出错: {str(e)}"
async def _arun(self, query: str) -> str:
try:
return await self._get_search_wrapper().arun(query)
except Exception as e:
return f"执行网络搜索时出错: {str(e)}"
class DateQuery(BaseTool):
# 定义工具的输入参数结构,这里没有输入参数,所以是一个空的模型
args_schema : type[BaseModel] = create_model("NoneInput")
name : str = "date_query_class"
description : str = "一个日期查询工具,可以返回当前的日期信息。仅当需要查询当前时间时调用,其余任何情况严禁调用。"
def _run(self) -> str:
try:
from datetime import datetime
now = datetime.now()
return now.strftime("%Y-%m-%d %H:%M:%S")
except Exception as e:
return f"执行日期查询时出错: {str(e)}"
async def _arun(self) -> str:
return self._run()
class BrowserControl(BaseTool):
"""统一浏览器控制工具:支持打开/关闭浏览器、刷新/关闭标签页、打开指定 URL"""
args_schema: type[BaseModel] = create_model(
"BrowserControlInput",
action=(Literal["open", "close"], Field(
default="open",
description="操作类型:'open' 打开/刷新,'close' 关闭"
)),
url=(Optional[str], Field(
default=None,
description="URL 地址、'current' 表示当前页,留空则操作整个浏览器窗口"
)),
browser=(Optional[str], Field(
default=None,
description="浏览器名称,如 'edge', 'chrome', 'firefox',不指定则使用系统默认"
)),
)
name: str = "browser_control"
description: str = (
"统一控制浏览器:打开/关闭浏览器、刷新/关闭标签页、打开指定 URL。\n"
"- action='open', url=None: 打开浏览器首页\n"
"- action='open', url='current': 刷新当前标签页\n"
"- action='open', url='https://...': 在新标签页打开指定 URL\n"
"- action='close', url=None: 关闭整个浏览器窗口\n"
"- action='close', url='任意非空字符(例如current)': 关闭当前标签页\n"
"操作完成后会返回当前屏幕截图,帮助判断操作是否成功。"
"仅用于进行浏览器操作时调用,其余任何情况严禁调用。"
)
response_format: Literal["content", "content_and_artifact"] = "content_and_artifact"
# ---------- 辅助方法(与原版相同,无改动) ----------
def _get_browser_info(self, browser_name: Optional[str]):
"""解析浏览器名称,返回 webbrowser 控制器与窗口标题关键字。"""
if browser_name is None:
return webbrowser.get(), None
name_lower = browser_name.lower().strip()
if name_lower not in env_utils.BROWSER_MAP:
raise ValueError(f"不支持的浏览器: {browser_name},支持: {', '.join(env_utils.BROWSER_MAP.keys())}")
reg_name, title_keyword = env_utils.BROWSER_MAP[name_lower]
try:
controller = webbrowser.get(reg_name)
except webbrowser.Error:
controller = webbrowser.get(browser_name)
return controller, title_keyword
def _activate_window(self, title_keyword: str) -> bool:
"""在 Windows 上按标题关键字激活浏览器窗口(非 Win32 返回 False)。"""
if sys.platform != "win32":
return False
try:
time.sleep(0.3)
windows = pyautogui.getWindowsWithTitle(title_keyword)
if windows:
win = windows[0]
if win.isMinimized:
win.restore()
win.activate()
return True
except Exception:
pass
return False
def _send_keys(self, *keys):
"""通过 pyautogui 发送组合键。"""
pyautogui.hotkey(*keys)
# ---------- 核心操作(返回操作结果描述) ----------
def _try_activate_any_browser(self) -> bool:
"""尝试激活任意已知浏览器窗口(不指定 browser 时的回退方案)。"""
for _reg_name, title_keyword in env_utils.BROWSER_MAP.values():
if self._activate_window(title_keyword):
return True
return False
def _open_action(self, url: Optional[str], browser: Optional[str]) -> str:
"""执行打开首页、刷新当前页或打开指定 URL。"""
controller, title_keyword = self._get_browser_info(browser)
if url is None:
controller.open("about:blank")
if title_keyword:
self._activate_window(title_keyword)
else:
self._try_activate_any_browser()
browser_info = f" (使用 {browser})" if browser else ""
return f"已打开浏览器首页{browser_info}"
elif url.lower() == "current":
if title_keyword:
if not self._activate_window(title_keyword):
return f"未找到 {browser or '默认'} 浏览器窗口,无法刷新"
time.sleep(0.2)
if sys.platform == "darwin":
self._send_keys("command", "r")
else:
self._send_keys("f5")
return "已刷新当前标签页"
else:
controller.open(url)
if title_keyword:
self._activate_window(title_keyword)
else:
self._try_activate_any_browser()
# 等待浏览器窗口出现并开始加载
time.sleep(1.0)
return f"已尝试打开 URL: {url}"
def _close_action(self, url: Optional[str], browser: Optional[str]) -> str:
"""执行关闭浏览器窗口或当前标签页。"""
_, title_keyword = self._get_browser_info(browser)
if url is None:
if title_keyword:
if not self._activate_window(title_keyword):
return f"未找到 {browser or '默认'} 浏览器窗口,无法关闭"
else:
self._try_activate_any_browser()
time.sleep(0.2)
if sys.platform == "darwin":
self._send_keys("command", "q")
else:
self._send_keys("alt", "f4")
return "已尝试关闭浏览器窗口"
else:
if title_keyword:
if not self._activate_window(title_keyword):
return f"未找到 {browser or '默认'} 浏览器窗口,无法关闭标签页"
else:
self._try_activate_any_browser()
time.sleep(0.2)
if sys.platform == "darwin":
self._send_keys("command", "w")
else:
self._send_keys("ctrl", "w")
return "已尝试关闭当前标签页"
# ---------- 同步主逻辑,返回 (描述, artifact) ----------
def _run(self, action: str = "open", url: Optional[str] = None, browser: Optional[str] = None) -> Tuple[str, dict]:
try:
if action == "open":
msg = self._open_action(url, browser)
elif action == "close":
msg = self._close_action(url, browser)
else:
msg = f"不支持的操作类型: {action}"
except Exception as e:
msg = f"执行浏览器操作时出错: {e}"
# 无论操作是否成功,都尝试截屏(等待窗口切换)
time.sleep(0.3)
try:
# 先尝试获取 GUI 操作区域,限定截屏范围
try:
from agent.core.config_manager import get_active_monitor_region
region = get_active_monitor_region()
except Exception:
region = None
if region:
pil_img = pyautogui.screenshot(region=(region["x"], region["y"], region["width"], region["height"]))
else:
pil_img = pyautogui.screenshot()
pil_img = pil_img.resize((env_utils.GROUNDING_WIDTH, env_utils.GROUNDING_HEIGHT), Image.Resampling.LANCZOS)
buf = io.BytesIO()
pil_img.save(buf, format="PNG")
raw_png = buf.getvalue()
return msg, {"image_bytes": raw_png}
except Exception as e:
return f"{msg}\n(截屏失败: {e})", {}
# ---------- 异步入口 ----------
async def _arun(self, action: str = "open", url: Optional[str] = None, browser: Optional[str] = None) -> Tuple[str, dict]:
return await asyncio.to_thread(self._run, action, url, browser)
class DirectoryListing(BaseTool):
"""列出指定目录下的文件和文件夹,帮助 Agent 了解本地文件结构。"""
args_schema: type[BaseModel] = create_model(
"DirectoryListingInput",
path=(
Optional[str],
Field(
default=None,
description="要查看的目录路径,默认为当前工作目录。"
),
),
show_hidden=(
bool,
Field(
default=False,
description="是否显示隐藏文件(以点开头的文件/文件夹),默认不显示。"
),
),
)
name: str = "directory_listing"
description: str = (
"列出指定目录下的所有文件和子目录。可用于探索文件系统、查找文件位置。"
"仅当需要获取目录内容或确认文件结构时调用,其余情况严禁调用。"
)
def _run(self, path: Optional[str] = None, show_hidden: bool = False) -> str:
try:
target = Path(path).resolve() if path else Path.cwd()
if not target.exists():
return f"路径不存在: {target}"
if not target.is_dir():
return f"路径不是目录: {target}"
items = []
with os.scandir(target) as entries:
for entry in entries:
if not show_hidden and entry.name.startswith('.'):
continue
item_type = "DIR" if entry.is_dir() else "FILE"
items.append(f"[{item_type}] {entry.name}")
if not items:
return f"目录 {target} 为空"
return f"目录 {target} 的内容:\n" + "\n".join(sorted(items))
except Exception as e:
return f"列出目录时出错: {str(e)}"
async def _arun(self, path: Optional[str] = None, show_hidden: bool = False) -> str:
return await asyncio.to_thread(self._run, path, show_hidden)
class HumanInteractionInput(BaseModel):
interaction_type: Literal["information", "selection", "approval"] = Field(
default="information",
description="交互类型:information=人工补充信息,selection=人工选择,approval=人工审批。",
)
title: str = Field(default="需要人工处理", description="展示给人类的交互标题。")
prompt: str = Field(default="请处理后继续。", description="需要人类理解并响应的具体问题或说明。")
options: Optional[list[str] | str] = Field(
default=None,
description="selection/approval 可选项。优先传字符串数组;如果传 JSON 字符串,系统会自动解析。",
)
suggested_response: Optional[str] = Field(default=None, description="可选的建议回复、默认选择或默认审批意见。")
class HumanInteraction(BaseTool):
"""向人类请求补充信息、选择或审批;实际 UI 确认由图级人工干预层承接。"""
args_schema: type[BaseModel] = HumanInteractionInput
name: str = "human_interaction"
description: str = (
"当任务缺少关键信息、需要人类选择路径、或需要人类审批后才能继续时调用。"
"三种类型分别是:information=请求人类补充缺失信息;selection=让人类在多个选项中选择且可编辑;"
"approval=请求人类审批高影响决策或继续执行。"
"工具返回必须被视为用户原始意图的纯字符串反馈。其余情况严禁调用。"
)
@staticmethod
def normalize_options(options: Optional[list[str] | str]) -> list[str]:
"""将 options 参数规范化为字符串列表(支持 JSON 与逗号分隔)。"""
if options is None:
return []
if isinstance(options, str):
raw = options.strip()
if not raw:
return []
try:
parsed = json.loads(raw)
except json.JSONDecodeError:
return [item.strip() for item in raw.replace(",", ",").split(",") if item.strip()]
if isinstance(parsed, list):
return [str(item) for item in parsed]
return [str(parsed)]
return [str(item) for item in options]
def _run(
self,
interaction_type: str = "information",
title: str = "需要人工处理",
prompt: str = "请处理后继续。",
options: Optional[list[str] | str] = None,
suggested_response: Optional[str] = None,
) -> str:
try:
normalized_options = self.normalize_options(options)
if interaction_type == "approval":
default = (suggested_response or "").strip()
return default or "用户同意"
if interaction_type == "selection":
default = (suggested_response or "").strip()
selected = default or (normalized_options[0] if normalized_options else "")
return f"用户选择:{selected}" if selected else "用户选择:"
response = (suggested_response or "").strip()
return f"用户补充信息如下:{response}" if response else "用户没有补充信息"
except Exception as e:
return f"[ERROR] human_interaction 工具执行出错: {str(e)}"
async def _arun(
self,
interaction_type: str = "information",
title: str = "需要人工处理",
prompt: str = "请处理后继续。",
options: Optional[list[str] | str] = None,
suggested_response: Optional[str] = None,
) -> str:
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):
for i in range(done_step):
if i not in done_steps:
done_steps.append(i)
done_steps.sort()
todo_data = {"steps": stored_steps, "done_steps": done_steps}
_todo_store[session_id] = todo_data
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: Optional[list[str]] = Field(
default=None,
description="任务步骤列表,每项为一步的描述。仅在首次创建计划时传入,后续更新进度时请省略此参数以节省 token。",
)
done_step: int = Field(
default=-1,
description="当前已推进到的步骤编号(从1开始)。传入 n 则步骤 1~n 全部标记为完成。-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):
"""任务管理工具,用于拆分复杂任务为多个步骤并跟踪进度。
Plan 模式下此工具为**必须调用**,Agent 模式下为可选。
首次调用时传入完整步骤列表,后续仅传 done_step 标记进度。
"""
args_schema: type[BaseModel] = TodoListInput
name: str = "todo_list"
description: str = (
"任务管理工具。用于将复杂任务拆分为多个步骤并跟踪执行进度。\n"
"Plan 模式下必须首先调用此工具创建任务计划;Agent 模式下按需调用。\n"
"参数说明:\n"
"- steps (list[str], 可选):任务步骤列表。仅在首次创建计划时传入所有步骤。\n"
" **步骤粒度要求**:同一页面/界面上的连续操作应合并为一条步骤,用\"并\"连接。例如:\n"
' * 推荐:"点击搜索框,输入XXX并点击搜索"(合并了点击、输入、搜索)\n'
' * 推荐:"在结果中点击目标进入主页"(合并了查找+点击)\n'
' * 禁止:"1.点击搜索框 2.输入文字 3.点击搜索"(过于细碎)\n'
"- done_step (int, 可选):当前已推进到的步骤编号(从1开始)。传入 n 则步骤 1~n 全部标记为已完成。不传或传 -1 则只查看当前状态。\n"
"调用示例:\n"
'- 首次创建(B站搜索示例):{"steps": ["打开哔哩哔哩网站", "点击顶部搜索框,输入\'燕三嘤嘤嘤\'并点击搜索", "在搜索结果中找到燕三嘤嘤嘤并点击头像或名称进入他的主页", "在他的主页点开视频列表里的第一个视频", "验证视频是否正在播放"], "done_step": -1}\n'
'- 已完成前2步:{"done_step": 2} (步骤1和2均标记为完成)\n'
'- 已完成前4步:{"done_step": 4}\n'
'- 查看状态:{"done_step": -1}\n'
"任务全部完成后无需再次调用。"
)
response_format: Literal["content", "content_and_artifact"] = "content"
def _run(self, steps: list[str] | None = None, done_step: int = -1) -> str:
try:
steps = steps or []
# 用简单变量维护状态(同一实例在 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 = []
# 标记步骤完成:传入 done_step 时,该步及之前所有步骤均标记为完成(1-indexed → 0-indexed)
if self._stored_steps and 1 <= done_step <= len(self._stored_steps):
for i in range(done_step):
if i not in self._done_set:
self._done_set.append(i)
self._done_set.sort()
todo_data = {
"steps": self._stored_steps,
"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)
except Exception as e:
return f"[ERROR] todo_list 工具执行出错: {str(e)}"
async def _arun(self, steps: list[str] | None = None, done_step: int = -1) -> str:
return self._run(steps, done_step)
+32
View File
@@ -0,0 +1,32 @@
from __future__ import annotations
import os
from typing import Optional
from langchain.tools import BaseTool
from pydantic import BaseModel, Field, create_model
class WebSearch(BaseTool):
args_schema : type[BaseModel] = create_model("Input_type", query=(str, Field(description="搜索查询内容")))
name : str = "web_search_class"
description : str = "一个网络搜索工具,可以根据用户的查询返回相关的搜索结果。仅当需要查询近期知识或已有知识无法解答需要联网查询时调用,其余任何情况严禁调用。"
def _get_search_wrapper(self):
"""懒加载 Tavily 搜索客户端。"""
from langchain_community.utilities import TavilySearchAPIWrapper
TAVILY_API_KEY = os.environ.get("TAVILY_API_KEY", "")
return TavilySearchAPIWrapper(tavily_api_key=TAVILY_API_KEY)
def _run(self, query: str) -> str:
try:
return self._get_search_wrapper().run(query)
except Exception as e:
return f"执行网络搜索时出错: {str(e)}"
async def _arun(self, query: str) -> str:
try:
return await self._get_search_wrapper().arun(query)
except Exception as e:
return f"执行网络搜索时出错: {str(e)}"
Binary file not shown.
+262
View File
@@ -2591,4 +2591,266 @@ body {
}
}
/* ===== Skills 管理面板 ===== */
.skills-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.skill-card {
border: 1px solid rgba(148, 163, 184, 0.3);
border-radius: 12px;
padding: 0.85rem;
background: rgba(255, 255, 255, 0.65);
transition: box-shadow 0.15s;
}
.skill-card:hover {
box-shadow: 0 3px 12px rgba(15, 23, 42, 0.08);
}
.skill-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.4rem;
}
.skill-card-header h4 {
margin: 0;
font-size: 0.92rem;
font-weight: 700;
color: #0f172a;
}
.skill-card-actions {
display: flex;
gap: 0.35rem;
}
.skill-card-desc {
font-size: 0.78rem;
color: #64748b;
margin-bottom: 0.4rem;
line-height: 1.4;
}
.skill-card-meta {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
font-size: 0.7rem;
color: #94a3b8;
margin-bottom: 0.4rem;
}
.skill-card-tags {
display: flex;
gap: 0.3rem;
flex-wrap: wrap;
}
.skill-tag {
font-size: 0.68rem;
padding: 0.15rem 0.45rem;
border-radius: 999px;
background: rgba(14, 116, 144, 0.1);
color: var(--accent);
border: 1px solid rgba(14, 116, 144, 0.2);
}
.skill-card-attachments {
display: flex;
gap: 0.3rem;
flex-wrap: wrap;
margin-top: 0.35rem;
}
.skill-attachment-chip {
font-size: 0.68rem;
padding: 0.15rem 0.45rem;
border-radius: 6px;
background: rgba(100, 116, 139, 0.08);
color: #475569;
border: 1px solid rgba(148, 163, 184, 0.25);
}
/* Skill 编辑器弹窗 */
.skill-editor-overlay {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
background: rgba(15, 23, 42, 0.55);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
animation: fadeIn 0.18s ease;
}
.skill-editor-overlay[hidden] {
display: none;
}
.skill-editor-dialog {
width: min(720px, 94vw);
max-height: 88vh;
max-height: 88dvh;
background: rgba(248, 250, 252, 0.97);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border-radius: 18px;
border: 1px solid rgba(148, 163, 184, 0.35);
box-shadow: 0 24px 64px rgba(15, 23, 42, 0.22);
display: flex;
flex-direction: column;
overflow: hidden;
animation: slideUp 0.22s ease;
}
.skill-editor-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.85rem 1.15rem;
border-bottom: 1px solid rgba(148, 163, 184, 0.25);
}
.skill-editor-header h3 {
margin: 0;
font-size: 1.05rem;
color: #0f172a;
}
.skill-editor-close {
width: 2rem;
height: 2rem;
padding: 0;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.4);
background: rgba(255, 255, 255, 0.5);
cursor: pointer;
font-size: 1rem;
color: #475569;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background 0.15s;
}
.skill-editor-close:hover {
background: rgba(239, 68, 68, 0.15);
color: #991b1b;
}
.skill-editor-body {
flex: 1;
overflow-y: auto;
padding: 1rem 1.15rem;
min-height: 0;
}
.skill-editor-body .setting-field {
margin-bottom: 0.7rem;
}
.skill-editor-body .setting-field label {
display: block;
font-size: 0.75rem;
font-weight: 700;
color: #475569;
margin-bottom: 0.25rem;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.skill-editor-body .setting-field input,
.skill-editor-body .setting-field textarea {
width: 100%;
border: 1px solid rgba(148, 163, 184, 0.45);
border-radius: 8px;
padding: 0.45rem 0.55rem;
font: inherit;
font-size: 0.85rem;
background: rgba(255, 255, 255, 0.8);
color: var(--text);
transition: border-color 0.15s, box-shadow 0.15s;
box-sizing: border-box;
}
.skill-editor-body .setting-field input:focus,
.skill-editor-body .setting-field textarea:focus {
border-color: var(--accent, #0e7490);
box-shadow: 0 0 0 3px rgba(14, 116, 144, 0.1);
outline: none;
}
.skill-editor-body .setting-field textarea {
resize: vertical;
min-height: 120px;
font-size: 0.82rem;
line-height: 1.45;
}
.skill-editor-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
padding: 0.7rem 1.15rem;
border-top: 1px solid rgba(148, 163, 184, 0.2);
background: rgba(241, 245, 249, 0.55);
}
.skill-editor-status {
font-size: 0.8rem;
font-weight: 500;
margin-right: auto;
color: #475569;
}
.skill-editor-status.success {
color: #16a34a;
}
.skill-editor-status.error {
color: #dc2626;
}
.skill-attachments {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-bottom: 0.35rem;
}
.skill-attachment-item {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 6px;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(148, 163, 184, 0.3);
}
.skill-attachment-item button {
border: none;
background: none;
cursor: pointer;
color: #991b1b;
font-size: 0.85rem;
padding: 0;
line-height: 1;
opacity: 0.6;
transition: opacity 0.15s;
}
.skill-attachment-item button:hover {
opacity: 1;
}
+283
View File
@@ -2082,6 +2082,7 @@
else if (target === "tool") renderToolConfigs();
else if (target === "graph") renderGraphs();
else if (target === "env") renderEnvConfig();
else if (target === "skills") renderSkills();
else if (target === "other") renderGuiConfig();
});
});
@@ -2112,6 +2113,7 @@
renderToolConfigs();
renderGraphs();
renderEnvConfig();
renderSkills();
renderGuiConfig();
}
@@ -3003,6 +3005,287 @@
return Math.random().toString(36).substring(2, 10);
}
// ===== Skills 管理 =====
let cachedSkills = [];
let editingSkillName = ""; // 空字符串 = 新建,非空 = 编辑已有
let editingSkillAttachments = []; // 当前编辑中的 skill 附件列表
const skillsListEl = $("skillsList");
const skillEditorOverlay = $("skillEditorOverlay");
const skillEditorTitle = $("skillEditorTitle");
const skillNameInput = $("skillNameInput");
const skillDescInput = $("skillDescInput");
const skillVersionInput = $("skillVersionInput");
const skillAuthorInput = $("skillAuthorInput");
const skillTagsInput = $("skillTagsInput");
const skillContentInput = $("skillContentInput");
const skillAttachmentsList = $("skillAttachmentsList");
const skillFileInput = $("skillFileInput");
const skillEditorStatus = $("skillEditorStatus");
const skillEditorSave = $("skillEditorSave");
const skillEditorCancel = $("skillEditorCancel");
const skillEditorClose = $("skillEditorClose");
function setSkillEditorStatus(msg, type) {
skillEditorStatus.textContent = msg;
skillEditorStatus.className = "skill-editor-status" + (type ? " " + type : "");
}
function openSkillEditor(skillNameToEdit) {
editingSkillName = skillNameToEdit || "";
editingSkillAttachments = [];
if (editingSkillName) {
// 编辑已有 skill
skillEditorTitle.textContent = "编辑 Skill: " + editingSkillName;
skillNameInput.value = editingSkillName;
skillNameInput.disabled = true;
// 加载完整数据
api("/api/skills/" + encodeURIComponent(editingSkillName)).then((res) => {
const s = res.skill;
skillDescInput.value = s.description || "";
skillVersionInput.value = s.version || "1.0.0";
skillAuthorInput.value = s.author || "";
skillTagsInput.value = (s.tags || []).join(", ");
skillContentInput.value = s.content || "";
editingSkillAttachments = s.attachment_files || [];
renderSkillAttachments();
}).catch((e) => {
setSkillEditorStatus("加载失败: " + e.message, "error");
});
} else {
// 新建
skillEditorTitle.textContent = "创建 Skill";
skillNameInput.value = "";
skillNameInput.disabled = false;
skillDescInput.value = "";
skillVersionInput.value = "1.0.0";
skillAuthorInput.value = "";
skillTagsInput.value = "";
skillContentInput.value = "";
editingSkillAttachments = [];
renderSkillAttachments();
}
setSkillEditorStatus("");
skillEditorOverlay.hidden = false;
}
function closeSkillEditor() {
skillEditorOverlay.hidden = true;
editingSkillName = "";
editingSkillAttachments = [];
}
function renderSkillAttachments() {
skillAttachmentsList.innerHTML = "";
editingSkillAttachments.forEach((fname) => {
const chip = document.createElement("span");
chip.className = "skill-attachment-item";
const nameSpan = document.createElement("span");
nameSpan.textContent = fname;
const delBtn = document.createElement("button");
delBtn.textContent = "×";
delBtn.title = "删除附件";
delBtn.addEventListener("click", async () => {
if (!confirm("确定删除附件 " + fname + " ?")) return;
try {
await api("/api/skills/" + encodeURIComponent(editingSkillName) + "/attachments/" + encodeURIComponent(fname), { method: "DELETE" });
editingSkillAttachments = editingSkillAttachments.filter((a) => a !== fname);
renderSkillAttachments();
setSkillEditorStatus("附件已删除", "success");
} catch (e) {
setSkillEditorStatus("删除失败: " + e.message, "error");
}
});
chip.appendChild(nameSpan);
chip.appendChild(delBtn);
skillAttachmentsList.appendChild(chip);
});
}
async function saveSkill() {
const name = (skillNameInput.value || "").trim();
if (!name) {
setSkillEditorStatus("名称不能为空", "error");
return;
}
setSkillEditorStatus("正在保存...", "");
const formData = new FormData();
formData.append("name", name);
formData.append("description", skillDescInput.value);
formData.append("content", skillContentInput.value);
formData.append("version", skillVersionInput.value);
formData.append("author", skillAuthorInput.value);
formData.append("tags", skillTagsInput.value);
try {
await api("/api/skills", { method: "POST", body: formData });
if (editingSkillName && editingSkillName !== name) {
// 名称变了,需要删除旧的
try { await api("/api/skills/" + encodeURIComponent(editingSkillName), { method: "DELETE" }); } catch {}
}
setSkillEditorStatus("保存成功", "success");
editingSkillName = name;
skillNameInput.disabled = true;
await renderSkills();
} catch (e) {
setSkillEditorStatus("保存失败: " + e.message, "error");
}
}
async function deleteSkill(name) {
if (!confirm("确定删除 Skill \"" + name + "\" 及其所有附件?此操作不可恢复。")) return;
try {
await api("/api/skills/" + encodeURIComponent(name), { method: "DELETE" });
await renderSkills();
setStatus("Skill 已删除", "success");
} catch (e) {
setStatus("删除失败: " + e.message, "error");
}
}
async function renderSkills() {
if (!skillsListEl) return;
try {
const res = await api("/api/skills");
cachedSkills = res.skills || [];
} catch (e) {
skillsListEl.innerHTML = '<p style="color:#dc2626;">加载 Skills 失败: ' + escapeHtml(e.message) + '</p>';
return;
}
skillsListEl.innerHTML = "";
if (cachedSkills.length === 0) {
skillsListEl.innerHTML = '<p style="color:#94a3b8;text-align:center;padding:2rem;">暂无 Skill。<br>点击"+ 创建 Skill"开始创建你的第一个操作指南。</p>';
return;
}
cachedSkills.forEach((skill) => {
const card = document.createElement("div");
card.className = "skill-card";
// Header
const header = document.createElement("div");
header.className = "skill-card-header";
const h4 = document.createElement("h4");
h4.textContent = skill.name;
const actions = document.createElement("div");
actions.className = "skill-card-actions";
const editBtn = document.createElement("button");
editBtn.className = "btn btn-secondary";
editBtn.style.cssText = "padding:0.2rem 0.5rem;font-size:0.75rem;";
editBtn.textContent = "编辑";
editBtn.addEventListener("click", () => openSkillEditor(skill.name));
const delBtn = document.createElement("button");
delBtn.className = "btn btn-danger";
delBtn.style.cssText = "padding:0.2rem 0.5rem;font-size:0.75rem;";
delBtn.textContent = "删除";
delBtn.addEventListener("click", () => deleteSkill(skill.name));
actions.appendChild(editBtn);
actions.appendChild(delBtn);
header.appendChild(h4);
header.appendChild(actions);
// Description
const desc = document.createElement("div");
desc.className = "skill-card-desc";
desc.textContent = skill.description || "(无描述)";
// Meta
const meta = document.createElement("div");
meta.className = "skill-card-meta";
meta.innerHTML = (skill.version ? '<span>v' + escapeHtml(skill.version) + '</span>' : '') +
(skill.author ? '<span>by ' + escapeHtml(skill.author) + '</span>' : '');
// Tags
const tagsWrap = document.createElement("div");
tagsWrap.className = "skill-card-tags";
(skill.tags || []).forEach((tag) => {
const tagEl = document.createElement("span");
tagEl.className = "skill-tag";
tagEl.textContent = tag;
tagsWrap.appendChild(tagEl);
});
// Attachments
let attachWrap = null;
if (skill.attachment_files && skill.attachment_files.length > 0) {
attachWrap = document.createElement("div");
attachWrap.className = "skill-card-attachments";
skill.attachment_files.forEach((fname) => {
const chip = document.createElement("span");
chip.className = "skill-attachment-chip";
chip.textContent = fname;
attachWrap.appendChild(chip);
});
}
card.appendChild(header);
card.appendChild(desc);
card.appendChild(meta);
card.appendChild(tagsWrap);
if (attachWrap) card.appendChild(attachWrap);
skillsListEl.appendChild(card);
});
}
// Skill Editor 事件绑定
function bindSkillEditorEvents() {
$("addSkillBtn").addEventListener("click", () => openSkillEditor(""));
skillEditorClose.addEventListener("click", closeSkillEditor);
skillEditorCancel.addEventListener("click", closeSkillEditor);
skillEditorOverlay.addEventListener("click", (e) => {
if (e.target === skillEditorOverlay) closeSkillEditor();
});
skillEditorSave.addEventListener("click", saveSkill);
// 附件上传
skillFileInput.addEventListener("change", async () => {
if (!editingSkillName) {
setSkillEditorStatus("请先保存 Skill 后再上传附件", "error");
skillFileInput.value = "";
return;
}
const files = skillFileInput.files;
if (!files || files.length === 0) return;
setSkillEditorStatus("正在上传...", "");
let successCount = 0;
let failCount = 0;
for (const file of files) {
const fd = new FormData();
fd.append("file", file);
try {
await api("/api/skills/" + encodeURIComponent(editingSkillName) + "/attachments", { method: "POST", body: fd });
if (!editingSkillAttachments.includes(file.name)) {
editingSkillAttachments.push(file.name);
}
successCount++;
} catch {
failCount++;
}
}
skillFileInput.value = "";
renderSkillAttachments();
if (failCount === 0) {
setSkillEditorStatus("上传成功 " + successCount + " 个文件", "success");
} else {
setSkillEditorStatus("上传: " + successCount + " 成功, " + failCount + " 失败", "error");
}
});
// ESC 关闭
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !skillEditorOverlay.hidden) {
// 只在 skill editor 打开时,且 settings 也打开时处理
if (!$("settingsOverlay").hidden) {
closeSkillEditor();
}
}
});
}
bindSkillEditorEvents();
// ===== 保存回调 =====
document.querySelectorAll(".settings-save-btn").forEach((btn) => {
btn.addEventListener("click", async () => {
+57
View File
@@ -157,6 +157,7 @@
<button type="button" class="settings-tab" data-tab="tool">工具</button>
<button type="button" class="settings-tab" data-tab="graph">Graph</button>
<button type="button" class="settings-tab" data-tab="env">环境变量</button>
<button type="button" class="settings-tab" data-tab="skills">Skills</button>
<button type="button" class="settings-tab" data-tab="other">其他设置</button>
</div>
<div class="settings-body">
@@ -192,6 +193,62 @@
<div class="settings-env-list" id="envList"></div>
<button type="button" class="btn btn-primary settings-save-btn" data-save="env">保存环境变量配置</button>
</div>
<!-- Skills 管理面板 -->
<div class="settings-panel" data-panel="skills" id="skillsPanel" hidden>
<div class="settings-toolbar">
<button type="button" class="btn btn-primary" id="addSkillBtn">+ 创建 Skill</button>
<span class="settings-path">Skills 目录:agent/skills/</span>
</div>
<div class="skills-list" id="skillsList"></div>
<!-- Skill 编辑/创建弹窗 -->
<div class="skill-editor-overlay" id="skillEditorOverlay" hidden>
<div class="skill-editor-dialog" role="dialog" aria-modal="true" aria-label="编辑 Skill">
<div class="skill-editor-header">
<h3 id="skillEditorTitle">创建 Skill</h3>
<button type="button" class="skill-editor-close" id="skillEditorClose">✕</button>
</div>
<div class="skill-editor-body">
<div class="setting-field">
<label>名称 (name)</label>
<input type="text" id="skillNameInput" placeholder="skill 名称(即文件夹名,英文+下划线)" />
</div>
<div class="setting-field">
<label>描述 (description)</label>
<input type="text" id="skillDescInput" placeholder="简要描述此 skill 的用途" />
</div>
<div class="setting-field">
<label>版本 (version)</label>
<input type="text" id="skillVersionInput" value="1.0.0" placeholder="版本号" />
</div>
<div class="setting-field">
<label>作者 (author)</label>
<input type="text" id="skillAuthorInput" placeholder="作者名" />
</div>
<div class="setting-field">
<label>标签 (tags,逗号分隔)</label>
<input type="text" id="skillTagsInput" placeholder="e.g. python, 自动化, GUI" />
</div>
<div class="setting-field">
<label>指南内容 (skill.md)</label>
<textarea id="skillContentInput" rows="14" placeholder="编写此 skill 的操作指南(Markdown 格式)..."></textarea>
</div>
<div class="setting-field">
<label>附件文件</label>
<div class="skill-attachments" id="skillAttachmentsList"></div>
<label class="btn btn-ghost" style="margin-top:0.4rem;display:inline-flex;align-items:center;gap:0.3rem;">
+ 上传附件
<input type="file" id="skillFileInput" multiple hidden />
</label>
</div>
</div>
<div class="skill-editor-footer">
<span class="skill-editor-status" id="skillEditorStatus"></span>
<button type="button" class="btn btn-secondary" id="skillEditorCancel">取消</button>
<button type="button" class="btn btn-primary" id="skillEditorSave">保存</button>
</div>
</div>
</div>
</div>
<!-- 其他设置面板 -->
<div class="settings-panel" data-panel="other" id="otherPanel" hidden>
<div class="settings-toolbar">
+109 -3
View File
@@ -607,7 +607,7 @@ def api_chat_abort(session_id: str = Form(...)) -> dict[str, Any]:
ui.sync_manifest_from_chat_history(session_id, rolled_back, "")
# 清除 todo store
try:
from agent.tools.tool import clear_todo_store_session
from agent.tools.todo_list import clear_todo_store_session
clear_todo_store_session(session_id)
except Exception:
pass
@@ -945,7 +945,7 @@ def api_get_tool_image(path: str, session: str = "") -> FileResponse:
def api_get_todo_list(session_id: str) -> dict[str, Any]:
"""获取当前会话最新的 todo list 数据(前端轮询)。"""
try:
from agent.tools.tool import get_todo_store_data
from agent.tools.todo_list import get_todo_store_data
data = get_todo_store_data(session_id)
except Exception:
data = None
@@ -954,10 +954,116 @@ def api_get_todo_list(session_id: str) -> dict[str, Any]:
return {"has_todo": False, "data": None}
# ---------- Skill 管理 API ----------
@app.get("/api/skills")
def api_list_skills() -> dict[str, Any]:
"""列出所有 skill 的基本信息。
输出: {"skills": list[dict]} 每项含 name, description, version, author, tags, has_attachments, attachment_files
"""
from agent.skills import list_skills
return {"skills": list_skills()}
@app.get("/api/skills/{name}")
def api_get_skill(name: str) -> dict[str, Any]:
"""获取单个 skill 的完整信息(含 skill.md 内容)。
输出: {"skill": dict} 含 name, description, version, author, tags, content, attachment_files
"""
from agent.skills import get_skill
skill = get_skill(name)
if skill is None:
raise HTTPException(status_code=404, detail=f"Skill '{name}' 不存在")
return {"skill": skill}
@app.post("/api/skills")
async def api_create_or_update_skill(
name: str = Form(...),
description: str = Form(""),
content: str = Form(""),
version: str = Form("1.0.0"),
author: str = Form(""),
tags: str = Form(""),
) -> dict[str, Any]:
"""创建或更新一个 skill。
输入:
name: skill 名称(必填)。
description: 简述。
content: skill.md 的 markdown 内容。
version: 版本号。
author: 作者。
tags: 逗号分隔的标签字符串。
输出: {"success": bool, "skill": dict}
"""
if not name or not name.strip():
raise HTTPException(status_code=400, detail="name 不能为空")
tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else []
from agent.skills import create_or_update_skill
ok = create_or_update_skill(
name=name.strip(),
description=description,
content=content,
version=version,
author=author,
tags=tag_list,
)
if not ok:
raise HTTPException(status_code=500, detail="保存 skill 失败")
from agent.skills import get_skill
skill = get_skill(name.strip())
return {"success": True, "skill": skill}
@app.delete("/api/skills/{name}")
def api_delete_skill(name: str) -> dict[str, Any]:
"""删除一个 skill 及其所有附件。
输出: {"success": bool}
"""
from agent.skills import delete_skill
ok = delete_skill(name)
return {"success": ok}
# ---------- Skill 附件管理 API ----------
@app.post("/api/skills/{name}/attachments")
async def api_add_skill_attachment(
name: str,
file: UploadFile = File(...),
) -> dict[str, Any]:
"""上传一个附件到指定 skill。
输出: {"success": bool, "filename": str}
"""
if not file.filename:
raise HTTPException(status_code=400, detail="文件名为空")
content = await file.read()
from agent.skills import add_attachment
ok = add_attachment(name, file.filename, content)
if not ok:
raise HTTPException(status_code=500, detail="上传附件失败(skill 不存在或路径非法)")
return {"success": True, "filename": file.filename}
@app.delete("/api/skills/{name}/attachments/{filename:path}")
def api_delete_skill_attachment(name: str, filename: str) -> dict[str, Any]:
"""删除 skill 下的一个附件。
输出: {"success": bool}
"""
from agent.skills import delete_attachment
ok = delete_attachment(name, filename)
return {"success": ok}
def _clear_tool_calling(session_id: str) -> None:
"""清理会话关联的 tool_calling 目录与 todo_list 缓存。"""
try:
from agent.tools.tool import clear_todo_store_session
from agent.tools.todo_list import clear_todo_store_session
clear_todo_store_session(session_id)
except Exception:
pass