Files
zclaw_openfang/docs/features/audit-v12/M1-intelligent-chat.md
iven 442ec0eeef
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
docs(audit): V12 模块化端到端审计报告 — 11 模块 + 总报告
混合矩阵式审计:10 个功能模块 × 五维检查清单
- 项目整体健康度: 76/100
- 2 个 P0 (M4 双数据库 + 反思引擎 LLM 未接入)
- 15 个 P1 (跨 M2/M3/M4/M5/M6/M7/M11)
- 三类断链模式: 写了没接/接了不对/双实现未统一
- 三阶段修复路线图: P0(2-3天) → P1(5-7天) → P2(5-7天)
2026-04-04 17:55:03 +08:00

187 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 模块 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 断链。