fix(runtime): 工具调用 P0 修复 — after_tool_call 接入 + stream_errored 工具抢救
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
P0-1: after_tool_call 中间件从未被调用 - 流式模式(run_streaming)和非流式模式(run)均添加 middleware_chain.run_after_tool_call() - ToolErrorMiddleware 错误计数恢复逻辑现在生效 - ToolOutputGuardMiddleware 敏感信息检测现在生效 P0-2: stream_errored 跳过所有工具执行 - 新增 completed_tool_ids 跟踪哪些工具已收到完整 ToolUseEnd - 流式错误时区分完整工具和不完整工具 - 完整工具照常执行(产物创建等不受影响) - 不完整工具发送取消 ToolEnd 事件(前端不再卡"执行中") - 工具执行后若 stream_errored,break outer 阻止无效 LLM 循环 参考文档: - docs/references/zclaw-toolcall-issues.md (10项问题分析) - docs/references/deerflow-toolcall-reference.md (DeerFlow工具调用完整参考)
This commit is contained in:
@@ -552,6 +552,20 @@ impl AgentLoop {
|
||||
sorted_indices.sort();
|
||||
for idx in sorted_indices {
|
||||
let (id, name, result) = results.remove(&idx).unwrap();
|
||||
// Run after_tool_call middleware (error counting, output guard, etc.)
|
||||
let mut mw_ctx = middleware::MiddlewareContext {
|
||||
agent_id: self.agent_id.clone(),
|
||||
session_id: session_id.clone(),
|
||||
user_input: String::new(),
|
||||
system_prompt: enhanced_prompt.clone(),
|
||||
messages: messages.clone(),
|
||||
response_content: Vec::new(),
|
||||
input_tokens: total_input_tokens,
|
||||
output_tokens: total_output_tokens,
|
||||
};
|
||||
if let Err(e) = self.middleware_chain.run_after_tool_call(&mut mw_ctx, &name, &result).await {
|
||||
tracing::warn!("[AgentLoop] after_tool_call middleware failed for '{}': {}", name, e);
|
||||
}
|
||||
messages.push(Message::tool_result(&id, zclaw_types::ToolId::new(&name), result, false));
|
||||
}
|
||||
}
|
||||
@@ -706,6 +720,7 @@ impl AgentLoop {
|
||||
|
||||
let mut stream = driver.stream(request);
|
||||
let mut pending_tool_calls: Vec<(String, String, serde_json::Value)> = Vec::new();
|
||||
let mut completed_tool_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut iteration_text = String::new();
|
||||
let mut reasoning_text = String::new(); // Track reasoning separately for API requirement
|
||||
|
||||
@@ -762,6 +777,7 @@ impl AgentLoop {
|
||||
// Update with final parsed input and emit ToolStart event
|
||||
if let Some(tool) = pending_tool_calls.iter_mut().find(|(tid, _, _)| tid == id) {
|
||||
tool.2 = input.clone();
|
||||
completed_tool_ids.insert(id.clone());
|
||||
if let Err(e) = tx.send(LoopEvent::ToolStart { name: tool.1.clone(), input: input.clone() }).await {
|
||||
tracing::warn!("[AgentLoop] Failed to send ToolStart event: {}", e);
|
||||
}
|
||||
@@ -869,11 +885,27 @@ impl AgentLoop {
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
// Skip tool processing if stream errored or timed out
|
||||
// Handle stream errors — execute complete tool calls, cancel incomplete ones
|
||||
if stream_errored {
|
||||
tracing::debug!("[AgentLoop] Stream errored, skipping tool processing and breaking");
|
||||
// Cancel incomplete tools (ToolStart sent but ToolUseEnd not received)
|
||||
let incomplete: Vec<_> = pending_tool_calls.iter()
|
||||
.filter(|(id, _, _)| !completed_tool_ids.contains(id))
|
||||
.collect();
|
||||
for (_, name, _) in &incomplete {
|
||||
tracing::warn!("[AgentLoop] Cancelling incomplete tool '{}' due to stream error", name);
|
||||
let error_output = serde_json::json!({ "error": "流式响应中断,工具调用未完成" });
|
||||
if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output }).await {
|
||||
tracing::warn!("[AgentLoop] Failed to send cancellation ToolEnd event: {}", e);
|
||||
}
|
||||
}
|
||||
// Retain only complete tools for execution
|
||||
pending_tool_calls.retain(|(id, _, _)| completed_tool_ids.contains(id));
|
||||
if pending_tool_calls.is_empty() {
|
||||
tracing::debug!("[AgentLoop] Stream errored with no complete tool calls, breaking");
|
||||
break 'outer;
|
||||
}
|
||||
tracing::info!("[AgentLoop] Stream errored but executing {} complete tool calls", pending_tool_calls.len());
|
||||
}
|
||||
|
||||
tracing::debug!("[AgentLoop] Processing {} tool calls (reasoning: {} chars)", pending_tool_calls.len(), reasoning_text.len());
|
||||
|
||||
@@ -1059,6 +1091,23 @@ impl AgentLoop {
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
// Run after_tool_call middleware chain (error counting, output guard, etc.)
|
||||
{
|
||||
let mut mw_ctx = middleware::MiddlewareContext {
|
||||
agent_id: agent_id.clone(),
|
||||
session_id: session_id_clone.clone(),
|
||||
user_input: String::new(),
|
||||
system_prompt: enhanced_prompt.clone(),
|
||||
messages: messages.clone(),
|
||||
response_content: Vec::new(),
|
||||
input_tokens: total_input_tokens,
|
||||
output_tokens: total_output_tokens,
|
||||
};
|
||||
if let Err(e) = middleware_chain.run_after_tool_call(&mut mw_ctx, &name, &result).await {
|
||||
tracing::warn!("[AgentLoop] after_tool_call middleware failed for '{}': {}", name, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add tool result to message history
|
||||
tracing::debug!("[AgentLoop] Adding tool_result to history: id={}, name={}, is_error={}", id, name, is_error);
|
||||
messages.push(Message::tool_result(
|
||||
@@ -1070,6 +1119,11 @@ impl AgentLoop {
|
||||
}
|
||||
|
||||
tracing::debug!("[AgentLoop] Continuing to next iteration for LLM to process tool results");
|
||||
// If stream errored, we executed complete tools but cannot continue the LLM loop
|
||||
if stream_errored {
|
||||
tracing::info!("[AgentLoop] Stream was errored — executed salvageable tools, now breaking");
|
||||
break 'outer;
|
||||
}
|
||||
// Continue loop - next iteration will call LLM with tool results
|
||||
}
|
||||
});
|
||||
|
||||
212
docs/references/deerflow-toolcall-reference.md
Normal file
212
docs/references/deerflow-toolcall-reference.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# DeerFlow 工具调用系统参考文档
|
||||
|
||||
> 调研 DeerFlow 的工具调用完整流程,为 ZCLAW 工具调用问题排查提供参考。
|
||||
> 分析日期:2026-04-24
|
||||
|
||||
---
|
||||
|
||||
## 一、端到端数据流
|
||||
|
||||
```
|
||||
用户消息
|
||||
→ FastAPI Gateway (/api/threads/{id}/runs/stream)
|
||||
→ services.start_run() → asyncio.create_task(run_agent(...))
|
||||
→ LangGraph Agent Graph (create_agent)
|
||||
→ LLM Model (ChatOpenAI / Claude)
|
||||
→ AIMessage (含 tool_calls 列表)
|
||||
→ 14 层 Middleware 链处理
|
||||
→ ToolNode (LangGraph 内置, 按 tool_call.name 路由)
|
||||
→ ToolMessage (执行结果)
|
||||
→ 再次调用 LLM (带着 ToolMessage 继续)
|
||||
→ StreamBridge.publish() → asyncio.Queue
|
||||
→ SSE → 前端 useStream hook
|
||||
→ React 组件渲染
|
||||
```
|
||||
|
||||
## 二、工具注册与执行
|
||||
|
||||
### 2.1 注册入口
|
||||
|
||||
**文件**: `G:/deerflow/backend/packages/harness/deerflow/tools/tools.py` — `get_available_tools()`
|
||||
|
||||
工具来自四个来源:
|
||||
|
||||
| 来源 | 加载方式 | 示例 |
|
||||
|------|----------|------|
|
||||
| Config 工具 | YAML 配置 + 反射导入 (`module:variable`) | `deerflow.sandbox.tools:bash_tool` |
|
||||
| Builtin 工具 | 硬编码导入 | `present_file_tool`, `ask_clarification_tool` |
|
||||
| MCP 工具 | `MultiServerMCPClient` 从 MCP 服务器缓存获取 | 第三方 MCP 工具 |
|
||||
| ACP 工具 | `build_invoke_acp_agent_tool()` 动态构建 | 外部 agent 调用 |
|
||||
|
||||
### 2.2 Sandbox 工具清单
|
||||
|
||||
**文件**: `G:/deerflow/backend/packages/harness/deerflow/sandbox/tools.py`
|
||||
|
||||
| 工具名 | 功能 |
|
||||
|--------|------|
|
||||
| `bash` | 沙箱中执行命令 |
|
||||
| `ls` | 列出目录 |
|
||||
| `read_file` | 读取文件 |
|
||||
| `write_file` | 写入文件(触发产物面板自动打开) |
|
||||
| `str_replace` | 字符串替换(触发产物面板自动打开) |
|
||||
|
||||
### 2.3 Builtin 工具
|
||||
|
||||
**文件**: `G:/deerflow/backend/packages/harness/deerflow/tools/builtins/`
|
||||
|
||||
| 工具 | 功能 |
|
||||
|------|------|
|
||||
| `ask_clarification` | 向用户提问澄清(中断执行等待回复) |
|
||||
| `present_file` | 展示文件给用户(触发产物卡片) |
|
||||
| `setup_agent` | 自定义 agent 创建 |
|
||||
| `task_tool` | 子 agent 任务委派 |
|
||||
| `view_image` | 图片查看(仅视觉模型) |
|
||||
| `tool_search` | 延迟工具搜索(MCP 工具按需暴露) |
|
||||
|
||||
## 三、中间件链(14 层)
|
||||
|
||||
**文件**: `G:/deerflow/backend/packages/harness/deerflow/agents/lead_agent/agent.py` — `_build_middlewares()`
|
||||
|
||||
与工具调用相关的关键中间件:
|
||||
|
||||
### 3.1 DanglingToolCallMiddleware
|
||||
|
||||
**文件**: `dangling_tool_call_middleware.py`
|
||||
|
||||
在 `wrap_model_call` 中检测消息历史中缺失 ToolMessage 的 AIMessage,自动注入占位 ToolMessage:
|
||||
```python
|
||||
ToolMessage(
|
||||
content="[Tool call was interrupted and did not return a result.]",
|
||||
tool_call_id=tc_id,
|
||||
name=tc.get("name", "unknown"),
|
||||
status="error",
|
||||
)
|
||||
```
|
||||
|
||||
### 3.2 ToolErrorHandlingMiddleware
|
||||
|
||||
**文件**: `tool_error_handling_middleware.py`
|
||||
|
||||
在 `wrap_tool_call` 中捕获工具执行异常,转换为错误 ToolMessage 而非让整个 run 崩溃。
|
||||
|
||||
### 3.3 LoopDetectionMiddleware
|
||||
|
||||
**文件**: `loop_detection_middleware.py`
|
||||
|
||||
在 `after_model` 中检测重复工具调用:
|
||||
- 阈值 3 次 → 注入警告 HumanMessage
|
||||
- 阈值 5 次 → 直接清空 tool_calls,强制 LLM 产出文本回答
|
||||
|
||||
### 3.4 DeferredToolFilterMiddleware
|
||||
|
||||
**文件**: `deferred_tool_filter_middleware.py`
|
||||
|
||||
在 `wrap_model_call` 中过滤延迟注册的 MCP 工具 schema,仅在 LLM 通过 `tool_search` 发现后才暴露。
|
||||
|
||||
### 3.5 ClarificationMiddleware
|
||||
|
||||
拦截 `ask_clarification` 工具调用,中断执行等待用户回复。
|
||||
|
||||
### 3.6 SubagentLimitMiddleware
|
||||
|
||||
截断过多的并行子 agent 调用。
|
||||
|
||||
## 四、工具结果回传
|
||||
|
||||
### 4.1 格式
|
||||
|
||||
LangChain 的 `ToolMessage`,包含:
|
||||
- `content`: 执行结果文本
|
||||
- `tool_call_id`: 匹配 AIMessage 中的 tool_call ID
|
||||
- `name`: 工具名称
|
||||
- `status`: `"error"` 或省略
|
||||
|
||||
### 4.2 特殊工具
|
||||
|
||||
`present_file_tool` 返回 `Command` 而非纯字符串,同时更新 `artifacts` 和 `messages` 两个 state channel。
|
||||
|
||||
## 五、前端工具调用展示
|
||||
|
||||
### 5.1 消息分组
|
||||
|
||||
**文件**: `G:/deerflow/frontend/src/core/messages/utils.ts` — `groupMessages()`
|
||||
|
||||
| 分组类型 | 触发条件 | 展示 |
|
||||
|----------|----------|------|
|
||||
| `assistant:processing` | AI 消息含 tool_calls 或 reasoning | MessageGroup (折叠) |
|
||||
| `assistant` | AI 消息有文本无 tool_calls | MessageListItem (气泡) |
|
||||
| `assistant:present-files` | 含 present_files tool call | ArtifactFileList |
|
||||
| `assistant:clarification` | ask_clarification 结果 | MarkdownContent |
|
||||
| `assistant:subagent` | 含 task tool call | SubtaskCard |
|
||||
|
||||
### 5.2 工具状态推断
|
||||
|
||||
前端**没有显式状态机**。通过消息序列推断:
|
||||
- AI 消息含 tool_calls 但无对应 ToolMessage → 正在执行
|
||||
- ToolMessage 出现 → 执行完成
|
||||
- `assistant:processing` 组由 `ChainOfThought` 折叠组件包裹
|
||||
|
||||
### 5.3 工具调用 UI
|
||||
|
||||
**文件**: `message-group.tsx` 第 186-423 行
|
||||
|
||||
按工具名渲染不同图标和内容:
|
||||
- `bash` → 终端图标 + 命令代码块
|
||||
- `read_file`/`write_file`/`str_replace` → 文件图标 + 路径链接(点击打开产物面板)
|
||||
- `web_search` → 搜索图标 + 结果链接
|
||||
- 默认 → 扳手图标 + 工具名
|
||||
|
||||
## 六、流式处理中的工具调用
|
||||
|
||||
### 6.1 架构
|
||||
|
||||
```
|
||||
agent.astream(stream_mode=["values"])
|
||||
→ StreamBridge (asyncio.Queue per run, maxsize=256)
|
||||
→ sse_consumer() → SSE frames → 前端
|
||||
```
|
||||
|
||||
### 6.2 关键特征
|
||||
|
||||
- 工具调用**不中断**流。LangGraph 自动在 agent_node 和 tool_node 之间路由
|
||||
- 每次状态变更产出完整的 `values` 快照,前端通过 `seen_ids` 去重
|
||||
- 15 秒心跳包保持 SSE 连接
|
||||
|
||||
### 6.3 前端看到的事件序列
|
||||
|
||||
1. `values` 事件: 含 `tool_calls` 的 AIMessage
|
||||
2. `values` 事件: ToolMessage(工具结果)
|
||||
3. `values` 事件: LLM 基于工具结果的最终回答
|
||||
|
||||
整个过程连续,不中断 SSE 连接。
|
||||
|
||||
## 七、与 ZCLAW 对比(工具调用)
|
||||
|
||||
| 维度 | DeerFlow | ZCLAW |
|
||||
|------|----------|-------|
|
||||
| 框架 | LangGraph (graph-based) | 自研 loop_runner (循环) |
|
||||
| 工具生命周期 | LangGraph ToolNode 自动管理 | 手动 ToolRegistry + loop_runner |
|
||||
| after_tool_call 中间件 | ✅ wrap_tool_call 钩子完整 | ❌ 流式和非流式模式均未调用 |
|
||||
| 并行工具执行 | LangGraph 自动处理 | 非流式有 JoinSet,流式全串行 |
|
||||
| 悬挂修复 | DanglingToolCallMiddleware | DanglingToolMiddleware (有) |
|
||||
| 错误恢复 | ToolErrorHandlingMiddleware (异常→ToolMessage) | ToolErrorMiddleware (计数器) |
|
||||
| 循环检测 | LoopDetectionMiddleware (3次警告/5次强停) | LoopGuardMiddleware (有) |
|
||||
| 前端状态 | 消息序列推断 | 显式 ToolCallStep 状态机 |
|
||||
| MCP 工具 | 延迟注册 + tool_search 按需暴露 | 全量注册 |
|
||||
|
||||
## 八、关键文件索引
|
||||
|
||||
| 功能 | DeerFlow 文件 |
|
||||
|------|-------------|
|
||||
| Agent 工厂 | `backend/packages/harness/deerflow/agents/lead_agent/agent.py` |
|
||||
| 中间件组装 | `backend/packages/harness/deerflow/agents/factory.py` |
|
||||
| 工具注册 | `backend/packages/harness/deerflow/tools/tools.py` |
|
||||
| Sandbox 工具 | `backend/packages/harness/deerflow/sandbox/tools.py` |
|
||||
| Builtin 工具 | `backend/packages/harness/deerflow/tools/builtins/` |
|
||||
| 错误处理中间件 | `agents/middlewares/tool_error_handling_middleware.py` |
|
||||
| 悬挂修复中间件 | `agents/middlewares/dangling_tool_call_middleware.py` |
|
||||
| 循环检测中间件 | `agents/middlewares/loop_detection_middleware.py` |
|
||||
| 延迟过滤中间件 | `agents/middlewares/deferred_tool_filter_middleware.py` |
|
||||
| 流式 Bridge | `runtime/stream_bridge/memory.py` |
|
||||
| 前端消息分组 | `frontend/src/core/messages/utils.ts` |
|
||||
| 前端工具调用组件 | `frontend/src/components/workspace/messages/message-group.tsx` |
|
||||
133
docs/references/zclaw-toolcall-issues.md
Normal file
133
docs/references/zclaw-toolcall-issues.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# ZCLAW 工具调用问题分析
|
||||
|
||||
> 对比 DeerFlow 工具调用系统,排查 ZCLAW 工具调用问题。
|
||||
> 分析日期:2026-04-24
|
||||
> 更新日期:2026-04-24(P0+P0-stream_errored 已修复)
|
||||
|
||||
---
|
||||
|
||||
## 一、发现的问题
|
||||
|
||||
### P0: `after_tool_call` 中间件从未被调用 — ✅ 已修复 (2026-04-24)
|
||||
|
||||
**文件**: `crates/zclaw-runtime/src/loop_runner.rs`
|
||||
|
||||
在 `run()`(非流式,第 400-558 行)和 `run_streaming`(流式,第 893-1070 行)中,工具执行后直接 push `Message::tool_result` 到消息历史,**没有调用 `middleware_chain.run_after_tool_call()`**。
|
||||
|
||||
**影响**:
|
||||
- `ToolErrorMiddleware.after_tool_call` 的错误计数和恢复消息逻辑不生效
|
||||
- `ToolOutputGuardMiddleware.after_tool_call` 的敏感信息检测不生效
|
||||
- 工具错误只能靠工具自身的错误返回传递,中间件层的防护形同虚设
|
||||
|
||||
**DeerFlow 对比**: `ToolErrorHandlingMiddleware` 通过 `wrap_tool_call` 钩子完整包裹每次工具执行。
|
||||
|
||||
### P0: `stream_errored` 跳过所有工具执行 — ✅ 已修复 (2026-04-24)
|
||||
|
||||
**文件**: `crates/zclaw-runtime/src/loop_runner.rs` 第 872-876 行
|
||||
|
||||
流式模式中,当 LLM 流出现任何错误(网络超时、API 错误、驱动错误)时,`stream_errored = true`,然后 `break 'outer` 直接退出循环,**跳过所有已解析的工具调用**。
|
||||
|
||||
**影响**:
|
||||
- ToolStart 事件已发送给前端(用户看到"执行中"按钮),但工具从未实际执行
|
||||
- ToolEnd 事件永远不会发送 → 前端工具状态卡在"执行中"
|
||||
- 已完整接收(ToolUseEnd)的工具调用也被丢弃
|
||||
|
||||
**修复**: 区分完整工具(收到 ToolUseEnd)和不完整工具(仅收到 ToolUseStart/Delta)。完整工具照常执行,不完整工具发送取消 ToolEnd 事件。
|
||||
|
||||
### P1: 流式模式工具全串行
|
||||
|
||||
**文件**: `loop_runner.rs` 第 893-1070 行
|
||||
|
||||
非流式模式有 `JoinSet` + `Semaphore(3)` 并行执行 ReadOnly 工具,但流式模式用简单 `for` 循环串行执行所有工具。
|
||||
|
||||
**影响**: 多工具调用时延迟显著增加。
|
||||
|
||||
### P2: OpenAI 驱动工具参数静默替换
|
||||
|
||||
**文件**: `crates/zclaw-runtime/src/driver/openai.rs` 第 222-228 行
|
||||
|
||||
```rust
|
||||
let parsed_args = if args.is_empty() {
|
||||
serde_json::json!({})
|
||||
} else {
|
||||
serde_json::from_str(args).unwrap_or_else(|e| {
|
||||
tracing::warn!("Failed to parse tool args '{}': {}", args, e);
|
||||
serde_json::json!({})
|
||||
})
|
||||
};
|
||||
```
|
||||
|
||||
JSON 解析失败时静默替换为 `{}`,结合 loop_runner.rs 的空参数处理(第 412-423 行),会注入 `_fallback_query` 替代实际参数。
|
||||
|
||||
### P2: ToolOutputGuard 过于激进
|
||||
|
||||
**文件**: `crates/zclaw-runtime/src/middleware/tool_output_guard.rs` 第 109 行
|
||||
|
||||
使用 `to_lowercase()` 匹配敏感模式,合法内容中包含 "password"、"system:" 等字符串会被误拦。
|
||||
|
||||
### P2: ToolErrorMiddleware 失败计数器是全局的
|
||||
|
||||
**文件**: `crates/zclaw-runtime/src/middleware/tool_error.rs` 第 27 行
|
||||
|
||||
`consecutive_failures: AtomicU32` 是结构体字段,所有 session 共享。高并发下 A session 失败 2 次 + B session 失败 1 次就会触发 AbortLoop(阈值 3)。
|
||||
|
||||
### P3: Gateway 客户端 onTool 回调语义不一致
|
||||
|
||||
**文件**: `desktop/src/lib/gateway-client.ts` 第 698-707 行
|
||||
|
||||
`tool_call` 和 `tool_result` 两个 case 共用 `onTool` 回调,但参数约定不同,调用者必须通过 `output` 是否为空判断 start/end。
|
||||
|
||||
---
|
||||
|
||||
## 二、根因分析
|
||||
|
||||
工具调用问题最常见的故障模式:
|
||||
|
||||
1. **LLM 返回的 tool_call 参数格式错误** → OpenAI 驱动静默替换为 `{}` → 工具以空参数执行 → 结果不符合预期
|
||||
2. **工具执行异常** → after_tool_call 中间件未调用 → 错误未格式化 → LLM 收到原始错误信息无法恢复
|
||||
3. **流被中断后重连** → DanglingToolMiddleware 修复悬挂 → 但如果修复逻辑本身有 bug(如重复修补),会导致消息膨胀
|
||||
|
||||
## 三、修复建议
|
||||
|
||||
### 修复 1: 在 loop_runner 中调用 after_tool_call
|
||||
|
||||
**优先级**: P0
|
||||
**影响文件**: `loop_runner.rs`
|
||||
|
||||
在非流式模式的工具执行循环中(约第 530 行),工具执行后调用:
|
||||
```rust
|
||||
let after_result = middleware_chain.run_after_tool_call(
|
||||
&name, &input_json, &output_str, &mut ctx
|
||||
).await;
|
||||
```
|
||||
|
||||
在流式模式的工具执行后(约第 1020 行),同样调用。
|
||||
|
||||
### 修复 2: 将 ToolErrorMiddleware 计数器改为 per-session
|
||||
|
||||
**优先级**: P2
|
||||
**影响文件**: `middleware/tool_error.rs`
|
||||
|
||||
使用 `HashMap<String, u32>` 以 session_id 为 key 存储计数。
|
||||
|
||||
### 修复 3: ToolOutputGuard 改为精确匹配
|
||||
|
||||
**优先级**: P2
|
||||
**影响文件**: `middleware/tool_output_guard.rs`
|
||||
|
||||
只在检测到独立的密钥值时触发(如 `sk-[48字符]`),而非单词级匹配。
|
||||
|
||||
---
|
||||
|
||||
## 四、关键文件
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `crates/zclaw-runtime/src/loop_runner.rs` | 主循环,工具调度 |
|
||||
| `crates/zclaw-runtime/src/tool.rs` | ToolRegistry + Tool trait |
|
||||
| `crates/zclaw-runtime/src/middleware/tool_error.rs` | 工具错误处理 |
|
||||
| `crates/zclaw-runtime/src/middleware/tool_output_guard.rs` | 输出安全检查 |
|
||||
| `crates/zclaw-runtime/src/middleware/dangling_tool.rs` | 断裂工具修复 |
|
||||
| `crates/zclaw-runtime/src/driver/openai.rs` | OpenAI 兼容驱动 |
|
||||
| `desktop/src/lib/gateway-client.ts` | 前端通信客户端 |
|
||||
| `desktop/src/store/chat/streamStore.ts` | 前端流式处理 |
|
||||
@@ -132,6 +132,8 @@ onComplete → createCompleteHandler
|
||||
|
||||
| 问题 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| after_tool_call 中间件未调用 | ✅ 已修复 (04-24) | 流式+非流式均添加调用,ToolErrorMiddleware/ToolOutputGuard 现在生效 |
|
||||
| stream_errored 跳过所有工具 | ✅ 已修复 (04-24) | 完整工具照常执行,不完整工具发送取消事件 |
|
||||
| B-CHAT-07 混合域截断 | P2 Open | 跨域消息时可能截断上下文 |
|
||||
| SSE Token 统计为 0 | ✅ 已修复 | SseUsageCapture stream_done flag |
|
||||
| Tauri invoke 参数名 | ✅ 已修复 (f6c5dd2) | camelCase 格式 |
|
||||
@@ -146,6 +148,7 @@ onComplete → createCompleteHandler
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 04-24 | 工具调用 P0 修复: after_tool_call 中间件接入(流式+非流式) + stream_errored 工具抢救(完整工具执行+不完整工具取消) |
|
||||
| 04-24 | 产物系统优化: MarkdownRenderer 提取共享 + ArtifactPanel react-markdown 渲染 + 文件选择器下拉 + 数据源扩展(file_write/str_replace 两路径) + artifactStore IndexedDB 持久化 |
|
||||
| 04-23 | 建议 prefetch: sendMessage 时启动 context 预取,流结束后立即消费,不等 memory extraction |
|
||||
| 04-23 | 建议 prompt 重写: 1深入追问+1实用行动+1管家关怀,上下文窗口 6→20 条 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 变更日志
|
||||
updated: 2026-04-22
|
||||
updated: 2026-04-24
|
||||
status: active
|
||||
tags: [log, history]
|
||||
---
|
||||
@@ -9,6 +9,12 @@ tags: [log, history]
|
||||
|
||||
> Append-only 操作记录。格式: `## [日期] 类型 | 描述`
|
||||
|
||||
## [2026-04-24] fix(runtime) | 工具调用两个 P0 修复
|
||||
- **P0: after_tool_call 中间件从未调用**: 流式+非流式模式均添加 `middleware_chain.run_after_tool_call()` 调用,ToolErrorMiddleware 和 ToolOutputGuardMiddleware 的 after 逻辑现在生效
|
||||
- **P0: stream_errored 跳过所有工具**: 流式模式中 `stream_errored` 不再 `break 'outer`,改为区分完整工具(ToolUseEnd 已接收)和不完整工具;完整工具照常执行,不完整工具发送取消 ToolEnd 事件
|
||||
- **影响文件**: `loop_runner.rs`
|
||||
- **测试**: 91 tests PASS, 0 cargo warnings
|
||||
|
||||
## [2026-04-24] feat(artifact) | 产物系统优化完善
|
||||
- **MarkdownRenderer**: 从 StreamingText 提取共享 Markdown 渲染组件(react-markdown + remark-gfm),ArtifactPanel 复用
|
||||
- **ArtifactPanel**: 替换手写 30 行 MarkdownPreview → 完整 GFM 渲染(表格/代码块/列表/引用);添加文件选择器下拉菜单
|
||||
|
||||
Reference in New Issue
Block a user