# 模块 M1 智能对话 审计报告 > **审计版本**: V12 > **审计日期**: 2026-04-04 > **审计范围**: 用户输入 → ChatArea → chatStore → Kernel → LLM Driver → 流式回传 --- ## 1. 模块概况 ### 功能描述 ZCLAW 智能对话模块是用户与 AI Agent 交互的核心入口,支持流式响应、8 个 LLM Provider、推理模式、工具调用、多轮对话。 ### 涉及文件清单 **前端 (Desktop)** - UI 组件: `desktop/src/components/ai/` (ChatMode, StreamingText, ReasoningBlock, ToolCallChain, TokenMeter, SuggestionChips, ArtifactPanel, ModelSelector) - 入口组件: `desktop/src/components/ChatArea.tsx` - Store 层: `desktop/src/store/chat/streamStore.ts`, `conversationStore.ts`, `messageStore.ts`, `artifactStore.ts` - Client 层: `desktop/src/lib/kernel-chat.ts`, `gateway-client.ts`, `saas-relay-client.ts` - 类型: `desktop/src/types/chat.ts`, `kernel-types.ts` **后端 (Rust)** - Kernel: `crates/zclaw-kernel/src/kernel/messaging.rs` - Agent Loop: `crates/zclaw-runtime/src/loop_runner.rs` - Driver: `crates/zclaw-runtime/src/driver/` (anthropic.rs, openai.rs, gemini.rs, local.rs) - 中间件: `crates/zclaw-runtime/src/middleware/` (11 个中间件) - SaaS Relay: `crates/zclaw-saas/src/relay/` (handlers.rs, service.rs, key_pool.rs) - 存储: `crates/zclaw-memory/src/store.rs` - Tauri 命令: `desktop/src-tauri/src/kernel_commands/chat.rs` ### 调用链路图 ``` 用户输入 → ChatArea.handleSend() → streamStore.sendMessage() → getClient().chatStream(content, callbacks, opts) ├─ [Kernel 模式] kernel-chat.ts → invoke('agent_chat_stream', { request }) │ → Rust: agent_chat_stream() → Kernel.send_message_stream_with_prompt() │ → AgentLoop.run_streaming() │ → create_middleware_chain() (11层 before_completion) │ → driver.stream() (LLM调用) │ → 工具执行循环 (max 10 iterations) │ → app.emit("stream:chunk", payload) │ ← listen("stream:chunk") → streamCallbacks.onDelta/onTool/onComplete │ ├─ [Gateway 模式] gateway-client.ts → WebSocket │ → ZCLAW WebSocket 协议 │ ← WS onmessage → streamCallbacks │ └─ [SaaS 模式] saas-relay-client.ts → HTTP SSE → POST /api/v1/relay/chat/completions ← SSE stream → streamCallbacks ← streamStore 更新 isStreaming/activeRunId ← UI 重渲染 ``` --- ## 2. 五维检查结果 ### 2.1 链路完整性 | 链路 | 起点 | 终点 | 状态 | 备注 | |------|------|------|------|------| | Kernel 流式聊天 | ChatArea.tsx | agent_chat_stream (Rust) | ✅ 连通 | invoke 参数一致 | | Gateway 聊天 | ChatArea.tsx | WebSocket server | ✅ 连通 | 独立路径 | | SaaS Relay 聊天 | ChatArea.tsx | /api/v1/relay/chat/completions | ✅ 连通 | SSE 流 | | 流式事件 stream:chunk | Rust emit | kernel-chat.ts listen | ✅ 连通 | listen 在 line 91 | | 取消流 cancel_stream | streamStore | Rust cancel_stream | ✅ 连通 | AtomicBool 标志 | | hand-execution-complete | Rust emit | ChatArea.tsx + kernel-hands.ts listen | ✅ 连通 | 双重监听 | | 消息持久化 | AgentLoop | SqliteStorage.append_message | ✅ 连通 | | | Token 用量记录 | AgentLoop after_completion | usage 数据 | ✅ 连通 | | | SaaS 计费 | relay service.rs | quota 递增 | ✅ 连通 | Semaphore(16) 限流 | | 中间件链 | Kernel | 11 层中间件 | ✅ 连通 | 按 priority 排序 | ### 2.2 参数/类型一致性 | 接口 | 前端类型 | 后端类型 | 一致性 | 备注 | |------|---------|---------|--------|------| | agent_chat_stream.request | `{ agentId, sessionId, message, thinkingEnabled?, reasoningEffort?, planMode? }` | `StreamChatRequest { agent_id, session_id, message, thinking_enabled?, reasoning_effort?, plan_mode? }` | ✅ 一致 | `#[serde(rename_all = "camelCase")]` 自动转换 | | stream:chunk payload | `StreamChunkPayload` | `serde_json::json!({ sessionId, type, ... })` | ✅ 一致 | | | cancel_stream | `{ sessionId }` | `session_id: String` | ✅ 一致 | camelCase 转换 | | ChatStreamOptions | `thinking_enabled?, reasoning_effort?, plan_mode?` | `thinking_enabled, reasoning_effort, plan_mode` | ✅ 一致 | 全部 Optional | **参数一致性结论**: 无断链。Tauri 的 `rename_all = "camelCase"` 正确处理了命名风格差异。 ### 2.3 边界与错误处理 | 场景 | 输入 | 预期行为 | 实际行为 | 级别 | |------|------|---------|---------|------| | 空消息发送 | `""` | 阻止发送 | ChatArea line 211 检查空输入并 return | ✅ 正确 | | 流式中重复发送 | 连续点击发送 | 阻止重复 | streamStore line 193 `if (isStreaming) return` | ✅ 正确 | | 同 session 并发流 | 相同 sessionId 两次 invoke | 拒绝第二个 | Rust AtomicBool CAS 保证 (chat.rs:131-133) | ✅ 正确 | | 后端断开 | 流式中后端 crash | 超时或错误回调 | StreamBridge 90s 超时 (6个心跳) + channel close 检测 | ✅ 正确 | | 流式中断(悬空工具) | 中途断网留下 ToolUse 无 ToolResult | 修补悬空 | DanglingToolMiddleware 自动插入占位 ToolResult | ✅ 正确 | | 超长消息 | 100K 字符 | 拒绝或截断 | Rust 端 validate_string_length(100000) 限制 | ✅ 正确 | | Payload 过大 | 大量工具结果 | 截断 | OpenAiDriver 1.8MB payload limit + 紧急截断 (保留 sys+最近4条) | ✅ 正确 | | SaaS Key 全部冷却 | 无可用 API Key | 等待提示 | key_pool.rs 返回预计等待时间 | ✅ 正确 | | LLM 请求超时 | Provider 不响应 | 超时终止 | 流式 chunk 超时 60s (loop_runner.rs:683) | ✅ 正确 | | 模型只输出 reasoning 无 text | thinking 模式下 | 回退为 reasoning 内容 | loop_runner.rs:769-774 自动回退 | ✅ 正确 | | 循环调用检测 | 重复工具调用 | warn/block/abort | LoopGuard SHA256 指纹 + 三级阈值 (3/5/30) | ✅ 正确 | | SaaS 请求体过大 | > 1MB | 拒绝 | handlers.rs:41 大小限制 | ✅ 正确 | | temperature 越界 | > 2.0 或 < 0 | 拒绝 | handlers.rs:91-103 范围验证 | ✅ 正确 | | max_tokens 越界 | > 128000 | 拒绝 | handlers.rs:106-118 范围验证 | ✅ 正确 | | 前端 cancel 时 Rust 端清理 | 取消后继续收到 chunk | 丢弃并清理 | cancel_flag 检测 + channel close + stream_guard 释放 | ✅ 正确 | ### 2.4 状态管理 | Store/组件 | 状态机完整性 | 持久化 | 竞态风险 | |-----------|------------|--------|---------| | streamStore | ✅ isStreaming/activeRunId 完整 | ❌ 无持久化(纯运行时状态) | ⚠️ **低风险** — cancelStream 中使用 stale `activeRunId` | | conversationStore | ✅ 会话列表/当前会话 | ✅ IndexedDB 持久化 | ✅ 低风险 | | messageStore | ✅ 消息追加/更新 | ❌ 消息持久化在 SQLite 后端 | ✅ 低风险 | | artifactStore | ✅ 文件列表/选中/开关 | ❌ 无持久化 | ✅ 无风险 | | ChatArea 组件 | ✅ 输入/流式状态 | ❌ 无持久化 | ✅ 低风险 | **cancelStream 竞态风险详情**: - `streamStore.ts:476`: `const sessionId = useConversationStore.getState().sessionKey || activeRunId || ''` - 如果用户在 cancel 和新 stream 之间快速操作,`sessionKey` 可能已指向新 session,导致 cancel 错误的 stream - **风险等级**: P3(极端操作顺序,且 cancel 错误 stream 只会导致空操作) ### 2.5 安全与资源 | 检查项 | 状态 | 说明 | |--------|------|------| | API Key 安全存储 | ✅ | `SecretString` 包装,`expose_secret()` 仅在 HTTP header 中使用 | | API Key 加密存储 (SaaS) | ✅ | AES-256-GCM + 随机 nonce | | 对话内容不进入日志 | ✅ | 仅记录前 50 字符 (memory.rs:65) | | SSRF 防护 | ✅ | 精确的 URL 验证 (service.rs:810-922) | | SQL 注入防护 | ✅ | 全部参数化查询 | | 请求体大小限制 | ✅ | SaaS 1MB, OpenAI driver 1.8MB | | 流式 channel 泄漏 | ✅ | stream_guard 在所有路径(成功/错误/cancel)都清理 | | 工具输出敏感信息检测 | ⚠️ **只 warn 不 block** | 检测到 API Key 等敏感信息时仅 warn,仍然传递给 LLM | | **Gemini API Key URL 泄漏** | ⚠️ **P2 风险** | Key 通过 `?key=` query param 传递,出现在日志/代理/网络面板 | --- ## 3. 问题清单 | ID | 描述 | 文件:行号 | 级别 | 修复建议 | 验证方法 | |----|------|----------|------|---------|---------| | M1-01 | GeminiDriver API Key 通过 URL query param 传递,出现在日志和代理 | `driver/gemini.rs:71-74` | **P2** | 改用 HTTP header 认证(Gemini 支持 `x-goog-api-key` header) | 检查 HTTP 日志是否包含 key | | M1-02 | ToolOutputGuardMiddleware 检测到敏感信息只 warn 不 block | `middleware/tool_output_guard.rs:99-128` | **P2** | 对确认的 API Key/密码模式应 Block 或至少截断 | 构造含 `sk-xxx` 的工具输出测试 | | M1-03 | MemoryMiddleware 中 `std::sync::Mutex::unwrap()` 在 async 上下文 | `middleware/memory.rs:46` | **P2** | 改用 `lock().unwrap_or_else(\|e\| e.into_inner())` 或 `tokio::sync::Mutex` | stress test 并发记忆提取 | | M1-04 | LoopGuardMiddleware 中 `std::sync::Mutex::unwrap()` 在 async 上下文 | `middleware/loop_guard.rs:40` | **P2** | 同 M1-03 | stress test 循环检测 | | M1-05 | Agent Loop 迭代上限硬编码为 10 | `loop_runner.rs:298` | **P3** | 提取为 KernelConfig 配置项 | 修改配置验证生效 | | M1-06 | TitleMiddleware 是空占位符 | `middleware/title.rs` | **P3** | 完成实现或移除以减少 chain 开销 | 确认无功能依赖后移除 | | M1-07 | OpenAiDriver `trace` 级别记录完整请求体 | `driver/openai.rs:127` | **P3** | 生产环境确保 trace 级别不启用,或移除内容日志 | 检查日志配置 | | M1-08 | cancelStream 竞态:sessionKey 可能指向新 session | `streamStore.ts:476` | **P3** | cancel 时使用 `activeRunId` 而非 `sessionKey` | 快速 cancel + 新 stream 测试 | | M1-09 | LoopGuard 不在 agent turn 间 reset | `middleware/loop_guard.rs` | **P3** | 考虑在 `after_completion` 中 reset | 多轮对话验证计数器不累积 | | M1-10 | OpenAiDriver 将 SecretString 转为普通 String | `driver/openai.rs:130` | **P3** | String 在 async 闭包生命周期内可被内存转储获取;考虑使用零化 | 内存安全审计 | | M1-11 | `loop_runner.rs:513/804` 使用 `unwrap_or_default()` 静默吞掉数据库错误 | `loop_runner.rs:513,804` | **P3** | 改为 `log::warn!` + 默认值 | 断开 DB 验证有 warn 日志 | --- ## 4. 改进建议 ### 短期修复项(按优先级排序) 1. **[P2] Gemini API Key URL → Header**: 将 `?key=` 改为 `x-goog-api-key` header 传递 2. **[P2] ToolOutputGuard Block**: 对检测到 `sk-`、`AKIA` 等确认敏感信息的工具输出应 Block 3. **[P2] Mutex unwrap → 安全处理**: MemoryMiddleware 和 LoopGuardMiddleware 的 `std::sync::Mutex` 改用安全解包方式 ### 长期架构建议 - 将 Agent Loop 迭代上限和 LoopGuard 阈值提取为 KernelConfig 配置项 - 为 TitleMiddleware 完成实现或清理移除 - 考虑 Driver 层的 SecretString 生命周期管理改进(避免 `to_string()`) --- ## 5. 健康度评分 | 维度 | 评分 | 说明 | |------|------|------| | 链路完整性 | **95/100** | 三种连接模式全部连通,无断链 | | 参数一致性 | **100/100** | 全部接口参数匹配,Tauri camelCase 自动转换正确 | | 边界处理 | **90/100** | 空值/超长/并发/超时全覆盖 | | 状态管理 | **85/100** | 基本完整,cancelStream 有低概率竞态 | | 安全资源 | **85/100** | Gemini Key 泄漏和 ToolOutputGuard warn-only 需修复 | **综合健康度: 91/100** 模块整体质量很高,11 层中间件设计参考了 DeerFlow 2.0 最佳实践,三层连接模式(Kernel/Gateway/SaaS)全部连通。发现的问题均为 P2/P3 级别,无 P0/P1 断链。