# 故障排查指南 > 记录开发过程中遇到的问题、根因分析和解决方案。 --- ## 1. 连接问题 ### 1.1 WebSocket 连接失败 **症状**: `WebSocket connection failed` 或 `Unexpected server response: 400` **排查步骤**: ```bash # 1. 检查 ZCLAW 是否运行 curl http://127.0.0.1:50051/api/health # 2. 检查端口是否正确 netstat -ano | findstr "50051" # 3. 验证 Agent ID curl http://127.0.0.1:50051/api/agents ``` **常见原因**: | 原因 | 解决方案 | |------|----------| | ZCLAW 未启动 | `./zclaw.exe start` | | 端口错误 | ZCLAW 使用 50051,不是 4200 | | Agent ID 无效 | 使用 `/api/agents` 获取真实 UUID | ### 1.2 端口被占用 **症状**: `Port 1420 is already in use` **解决方案**: ```bash # Windows - 查找并终止进程 netstat -ano | findstr "1420" taskkill /PID /F # 或使用 PowerShell Stop-Process -Id -Force ``` ### 1.3 Vite 代理不工作 **症状**: 前端请求返回 404 或 CORS 错误 **检查清单**: - [ ] `vite.config.ts` 中配置了 `/api` 代理 - [ ] `ws: true` 已启用(WebSocket 需要) - [ ] `changeOrigin: true` 已设置 - [ ] 重启 Vite 开发服务器 --- ## 2. 聊天问题 ### 2.1 LLM API Key 未配置 **症状**: ``` Missing API key: No LLM provider configured. Set an API key (e.g. GROQ_API_KEY) and restart ``` **根本原因**: Agent 使用的 LLM 提供商没有配置 API Key **解决方案**: 1. 检查 Agent 使用的提供商: ```bash curl -s http://127.0.0.1:50051/api/status | jq '.agents[] | {name, model_provider}' ``` 2. 配置对应的 API Key: ```bash # 编辑 ~/.zclaw/.env echo "ZHIPU_API_KEY=your_key" >> ~/.zclaw/.env echo "BAILIAN_API_KEY=your_key" >> ~/.zclaw/.env echo "GEMINI_API_KEY=your_key" >> ~/.zclaw/.env ``` 3. 重启 ZCLAW: ```bash ./zclaw.exe restart ``` **快速解决**: 使用已配置的 Agent | Agent | 提供商 | 状态 | |-------|--------|------| | General Assistant | zhipu | 通常已配置 | ### 2.1.1 配置热重载限制(重要) **症状**: 修改 `config.toml` 后,`/api/config` 和 `/api/status` 仍然返回旧配置 **根本原因**: ZCLAW 将配置持久化在 SQLite 数据库中,`config.toml` 只在启动时读取 **验证问题**: ```bash # 检查 config.toml 内容 cat ~/.zclaw/config.toml # 输出: provider = "zhipu" # 检查 API 返回的配置 curl -s http://127.0.0.1:50051/api/config # 输出: {"default_model":{"provider":"bailian",...}} # 不一致! ``` **解决方案**: 1. **必须完全重启 ZCLAW**(热重载 `/api/config/reload` 不会更新持久化配置) ```bash # 方法 1: 通过 API 关闭(然后手动重启) curl -X POST http://127.0.0.1:50051/api/shutdown # 方法 2: 使用 CLI ./zclaw.exe stop ./zclaw.exe start ``` 2. **验证配置已生效**: ```bash curl -s http://127.0.0.1:50051/api/status | grep -E "default_provider|default_model" # 应输出: "default_provider":"zhipu" ``` **配置文件位置**: | 文件 | 用途 | |------|------| | `~/.zclaw/config.toml` | 主配置(启动时读取) | | `~/.zclaw/.env` | API Key 环境变量 | | `~/.zclaw/secrets.env` | 敏感信息 | | `~/.zclaw/data/zclaw.db` | SQLite 数据库(持久化配置) | **支持的 Provider**: | Provider | 环境变量 | 模型示例 | |----------|----------|----------| | zhipu | `ZHIPU_API_KEY` | glm-4-flash, GLM-4-Plus | | bailian | `BAILIAN_API_KEY` | qwen3.5-plus, qwen3-coder-next | | gemini | `GEMINI_API_KEY` | gemini-2.5-flash | | deepseek | `DEEPSEEK_API_KEY` | deepseek-chat | | openai | `OPENAI_API_KEY` | gpt-4, gpt-3.5-turbo | ### 2.2 Agent ID 获取失败导致无法对话 **症状**: Gateway 显示已连接,但发送消息无响应或报错 "No agent available" **根本原因**: `fetchDefaultAgentId()` 使用错误的 API 端点 **错误代码**: ```typescript // ❌ 错误 - /api/status 不返回 agents 字段 const status = await this.restGet('/api/status'); if (status?.agents && status.agents.length > 0) { ... } ``` **修复代码**: ```typescript // ✅ 正确 - 使用 /api/agents 端点 const agents = await this.restGet>('/api/agents'); if (agents && agents.length > 0) { const runningAgent = agents.find(a => a.state === 'Running'); this.defaultAgentId = (runningAgent || agents[0]).id; } ``` **历史背景**: 这个问题与 ZCLAW 的握手认证问题类似,但根因不同: - ZCLAW: 需要 `cli/cli/operator` 身份 + Ed25519 配对 - ZCLAW: 不需要认证握手,但需要正确的 Agent UUID **验证修复**: ```bash # 确认 /api/agents 返回数据 curl http://127.0.0.1:50051/api/agents # 应返回: [{ "id": "uuid", "name": "...", "state": "Running" }] ``` ### 2.3 流式响应不显示 **症状**: 消息发送后无响应或响应不完整 **排查步骤**: 1. 确认 WebSocket 连接状态: ```typescript console.log(client.getState()); // 应为 'connected' ``` 2. 检查事件处理: ```typescript // 确保处理了 text_delta 事件 ws.on('message', (data) => { const event = JSON.parse(data.toString()); if (event.type === 'text_delta') { console.log('Delta:', event.content); } }); ``` 3. 验证消息格式: ```javascript // ✅ 正确 { type: 'message', content: 'Hello', session_id: 'xxx' } // ❌ 错误 { type: 'chat', message: { role: 'user', content: 'Hello' } } ``` ### 2.3 消息格式错误 **症状**: WebSocket 连接成功,但发送消息后收到错误 **根本原因**: 使用了文档中的格式,而非实际格式 **正确的消息格式**: ```json { "type": "message", "content": "你的消息内容", "session_id": "唯一会话ID" } ``` --- ## 3. 前端问题 ### 3.1 Zustand 状态不更新 **症状**: UI 不反映状态变化 **检查**: 1. 确保使用 selector: ```typescript // ✅ 正确 - 使用 selector const messages = useChatStore((state) => state.messages); // ❌ 错误 - 可能导致不必要的重渲染 const store = useChatStore(); const messages = store.messages; ``` 2. 检查 immer/persist 配置 ### 3.2 切换 Agent 后对话消失 **症状**: 点击其他 Agent 后,之前的对话内容丢失 **根本原因**: `setCurrentAgent` 切换 Agent 时清空了 `messages`,但没有恢复该 Agent 之前的对话 **解决方案**: 修改 `chatStore.ts` 中的 `setCurrentAgent` 函数: ```typescript setCurrentAgent: (agent) => set((state) => { if (state.currentAgent?.id === agent.id) { return { currentAgent: agent }; } // Save current conversation before switching const conversations = upsertActiveConversation([...state.conversations], state); // Try to find existing conversation for this agent const agentConversation = conversations.find(c => c.agentId === agent.id); if (agentConversation) { // Restore the agent's previous conversation return { conversations, currentAgent: agent, messages: [...agentConversation.messages], sessionKey: agentConversation.sessionKey, currentConversationId: agentConversation.id, }; } // No existing conversation, start fresh return { conversations, currentAgent: agent, messages: [], sessionKey: null, currentConversationId: null, }; }), ``` 修改 `partialize` 配置以保存 `currentAgentId`: ```typescript partialize: (state) => ({ conversations: state.conversations, currentModel: state.currentModel, currentAgentId: state.currentAgent?.id, // 添加此行 currentConversationId: state.currentConversationId, }), ``` 添加 `onRehydrateStorage` 钩子恢复消息: ```typescript onRehydrateStorage: () => (state) => { // Rehydrate Date objects from JSON strings if (state?.conversations) { for (const conv of state.conversations) { conv.createdAt = new Date(conv.createdAt); conv.updatedAt = new Date(conv.updatedAt); for (const msg of conv.messages) { msg.timestamp = new Date(msg.timestamp); msg.streaming = false; } } } // Restore messages from current conversation if exists if (state?.currentConversationId && state.conversations) { const currentConv = state.conversations.find(c => c.id === state.currentConversationId); if (currentConv) { state.messages = [...currentConv.messages]; state.sessionKey = currentConv.sessionKey; } } }, ``` **验证修复**: 1. 与 Agent A 对话 2. 切换到 Agent B 3. 切换回 Agent A → 对话应恢复 **文件**: `desktop/src/store/chatStore.ts` ### 3.3 流式消息累积错误 **症状**: 流式内容显示不正确或重复 **解决方案**: ```typescript onDelta: (delta: string) => { set((state) => ({ messages: state.messages.map((m) => m.id === assistantId ? { ...m, content: m.content + delta } // 累积内容 : m ), })); } ``` --- ## 4. Tauri 桌面端问题 ### 4.1 Tauri 编译失败 **常见错误**: - Rust 版本不兼容 - 依赖缺失 - Cargo.toml 配置错误 **解决方案**: ```bash # 更新 Rust rustup update # 清理并重新构建 cd desktop/src-tauri cargo clean cargo build ``` ### 4.2 Tauri 窗口白屏 **原因**: Vite 开发服务器未启动或连接失败 **解决方案**: 1. 确保 `pnpm dev` 在运行 2. 检查 `tauri.conf.json` 中的 `beforeDevCommand` 3. 检查浏览器控制台错误 ### 4.3 Tauri 热重载不工作 **检查**: - `beforeDevCommand` 配置正确 - 文件监听未超出限制(Linux: `fs.inotify.max_user_watches`) --- ## 5. 调试技巧 ### 5.1 启用详细日志 ```typescript // gateway-client.ts private log(level: string, message: string, data?: unknown) { if (this.debug) { console.log(`[GatewayClient:${level}]`, message, data || ''); } } ``` ### 5.2 WebSocket 抓包 ```bash # 使用 wscat 测试 npm install -g wscat wscat -c ws://127.0.0.1:50051/api/agents/{agentId}/ws ``` ### 5.3 检查 ZCLAW 状态 ```bash # 完整状态 curl -s http://127.0.0.1:50051/api/status | jq # Agent 状态 curl -s http://127.0.0.1:50051/api/agents | jq '.[] | {id, name, state}' # Hands 状态 curl -s http://127.0.0.1:50051/api/hands | jq '.[] | {id, name, requirements_met}' ``` --- ## 6. 错误代码参考 | 错误信息 | 原因 | 解决方案 | |---------|------|----------| | `Port 1420 is already in use` | Vite 服务器已运行 | 终止现有进程 | | `Unexpected server response: 400` | Agent ID 无效 | 使用真实 UUID | | `Missing API key` | LLM 提供商未配置 | 配置 API Key | | `Connection refused` | ZCLAW 未运行 | 启动服务 | | `CORS error` | 代理未配置 | 检查 vite.config.ts | --- ## 7. 新用户引导 (Onboarding) ### 7.1 首次使用引导流程 **需求**: 当用户第一次使用系统时,引导用户设置默认助手的人格信息(about me、you in my eye 等)。 **实现方案**: 1. **检测首次使用** - 使用 `useOnboarding` hook 检查 localStorage: ```typescript // desktop/src/lib/use-onboarding.ts const ONBOARDING_COMPLETED_KEY = 'zclaw-onboarding-completed'; const USER_PROFILE_KEY = 'zclaw-user-profile'; export function useOnboarding(): OnboardingState { // 检查 localStorage 是否有完成记录 // 返回 { isNeeded, isLoading, markCompleted, resetOnboarding } } ``` 2. **引导向导** - 使用 `AgentOnboardingWizard` 组件: ```typescript // App.tsx 中的集成 if (showOnboarding) { return ( { markCompleted({ userName: 'User', userRole: 'user' }); setShowOnboarding(false); }} onSuccess={(clone) => { markCompleted({ userName: clone.userName || 'User', userRole: clone.userRole, }); setCurrentAgent({ id: clone.id, name: clone.name, icon: clone.emoji || '🦞', // ... }); setShowOnboarding(false); }} /> ); } ``` 3. **数据持久化** - 用户信息存储在 localStorage: ```typescript interface UserProfile { userName: string; userRole?: string; completedAt: string; } ``` **文件位置**: | 文件 | 用途 | |------|------| | `desktop/src/lib/use-onboarding.ts` | 引导状态管理 hook | | `desktop/src/components/AgentOnboardingWizard.tsx` | 5 步引导向导组件 | | `desktop/src/App.tsx` | 引导流程集成 | **引导步骤**: 1. 认识用户 - 收集用户名称和角色 2. Agent 身份 - 设置助手名称、昵称、emoji 3. 人格风格 - 选择沟通风格 4. 使用场景 - 选择应用场景 5. 工作环境 - 配置工作目录 ### 7.2 Onboarding 创建 Agent 失败 **症状**: 首次使用引导完成后,点击"完成"按钮报错,Agent 创建失败 **根本原因**: Onboarding 应该**更新现有的默认 Agent**,而不是创建新的 Agent **错误代码**: ```typescript // ❌ 错误 - 总是尝试创建新 Agent const clone = await createClone(createOptions); ``` **修复代码**: ```typescript // ✅ 正确 - 如果存在现有 Agent 则更新 let clone: Clone | undefined; if (clones && clones.length > 0) { // 更新现有的默认 Agent clone = await updateClone(clones[0].id, personalityUpdates); } else { // 没有现有 Agent 才创建新的 clone = await createClone(createOptions); } ``` **文件**: `desktop/src/components/AgentOnboardingWizard.tsx` **验证修复**: 1. 清除 localStorage 中的 onboarding 标记 2. 重新启动应用 3. 完成引导流程 → 应该成功更新默认 Agent ### 3.4 刷新页面后对话内容丢失 **症状**: 切换 Tab 时对话正常保留,但按 F5 刷新页面后对话内容消失 **根本原因**: `onComplete` 回调中没有将当前对话保存到 `conversations` 数组,导致 `persist` 中间件无法持久化 **问题分析**: Zustand 的 `persist` 中间件只保存 `partialize` 中指定的字段: ```typescript partialize: (state) => ({ conversations: state.conversations, // ← 从这里恢复 currentModel: state.currentModel, currentAgentId: state.currentAgent?.id, currentConversationId: state.currentConversationId, }), ``` 但 `messages` 数组只在内存中,刷新后丢失。恢复逻辑依赖 `conversations` 数组: ```typescript onRehydrateStorage: () => (state) => { if (state?.currentConversationId && state.conversations) { const currentConv = state.conversations.find(c => c.id === state.currentConversationId); if (currentConv) { state.messages = [...currentConv.messages]; // ← 从 conversations 恢复 } } }, ``` **问题**: `onComplete` 回调中只更新了 `messages`,没有调用 `upsertActiveConversation` 保存到 `conversations` **修复代码**: ```typescript onComplete: () => { const state = get(); // Save conversation to persist across refresh const conversations = upsertActiveConversation([...state.conversations], state); const currentConvId = state.currentConversationId || conversations[0]?.id; set({ isStreaming: false, conversations, // ← 保存到 conversations 数组 currentConversationId: currentConvId, // ← 确保 ID 被设置 messages: state.messages.map((m) => m.id === assistantId ? { ...m, streaming: false, runId } : m ), }); // ... rest of the callback }, ``` **文件**: `desktop/src/store/chatStore.ts` **验证修复**: 1. 发送消息并获得 AI 回复 2. 按 F5 刷新页面 3. 对话内容应该保留 ### 3.5 ChatArea 输入框布局错乱 **症状**: 对话过程中输入框被移动到页面顶部,而不是固定在底部 **根本原因**: `ChatArea` 组件返回的是 React Fragment (`<>...`),没有包裹在 flex 容器中 **问题代码**: ```typescript // ❌ 错误 - Fragment 没有 flex 布局 return ( <>
Header
Messages
Input
); ``` **修复代码**: ```typescript // ✅ 正确 - 使用 flex 容器 return (
Header
Messages
Input
); ``` **文件**: `desktop/src/components/ChatArea.tsx` **布局原理**: ``` ┌─────────────────────────────────────┐ │ Header (h-14, flex-shrink-0) │ ← 固定高度 ├─────────────────────────────────────┤ │ │ │ Messages (flex-1, overflow-y-auto) │ ← 占据剩余空间,可滚动 │ │ ├─────────────────────────────────────┤ │ Input (flex-shrink-0) │ ← 固定在底部 └─────────────────────────────────────┘ ``` --- ## 7. 记忆系统问题 ### 7.1 Memory Tab 为空,多轮对话后无记忆 **症状**: 经过多次对话后,右侧面板的"记忆"Tab 内容为空 **根本原因**: 多个配置问题导致记忆未被提取 **问题分析**: 1. **LLM 提取默认禁用**: `useLLM: false` 导致只使用规则提取 2. **提取阈值过高**: `minMessagesForExtraction: 4` 短对话不会触发 3. **agentId 不一致**: `MemoryPanel` 用 `'zclaw-main'`,`MemoryGraph` 用 `'default'` 4. **Gateway 端点不存在**: `GatewayLLMAdapter` 调用 `/api/llm/complete`,ZCLAW 无此端点 **修复方案**: ```typescript // 1. 启用 LLM 提取 (memory-extractor.ts) export const DEFAULT_EXTRACTION_CONFIG: ExtractionConfig = { useLLM: true, // 从 false 改为 true minMessagesForExtraction: 2, // 从 4 降低到 2 // ... }; // 2. 统一 agentId fallback (MemoryGraph.tsx) const agentId = currentAgent?.id || 'zclaw-main'; // 从 'default' 改为 'zclaw-main' // 3. 修复 Gateway 适配器端点 (llm-service.ts) // 使用 ZCLAW 的 /api/agents/{id}/message 端点 const response = await fetch(`/api/agents/${agentId}/message`, { method: 'POST', body: JSON.stringify({ message: fullPrompt, ... }), }); ``` **文件**: - `desktop/src/lib/memory-extractor.ts` - `desktop/src/lib/llm-service.ts` - `desktop/src/components/MemoryGraph.tsx` **验证修复**: 1. 打开浏览器控制台 2. 进行至少 2 轮对话 3. 查看日志: `[MemoryExtractor] Using LLM-powered semantic extraction` 4. 检查 Memory Tab 是否显示提取的记忆 ### 7.2 Memory Graph UI 与系统风格不一致 **症状**: 记忆图谱使用深色主题,与系统浅色主题不协调 **根本原因**: `MemoryGraph.tsx` 硬编码深色背景 `#1a1a2e` **修复方案**: ```typescript // Canvas 背景 - 支持亮/暗双主题 ctx.fillStyle = '#f9fafb'; // gray-50 (浅色) // 工具栏 - 添加 dark: 变体
// 图谱画布 - 添加 dark: 变体
``` **文件**: `desktop/src/components/MemoryGraph.tsx` **设计规范**: - 使用 Tailwind 的 `dark:` 变体支持双主题 - 强调色使用 `orange-500` 而非 `blue-600` - 文字颜色使用 `gray-700 dark:text-gray-300` --- ## 8. 端口配置问题 ### 8.1 ZCLAW 端口不匹配导致 Network Error **症状**: 创建 Agent 或其他 API 操作时报错 `Failed to create agent: Network Error`,控制台显示 `POST http://localhost:1420/api/agents net::ERR_CONNECTION_REFUSED` **根本原因**: `runtime-manifest.json` 声明端口 4200,但实际 ZCLAW 运行在 **50051** 端口 **正确配置**: | 配置位置 | 正确端口 | |---------|----------| | `runtime-manifest.json` | 4200 (声明,但实际不使用) | | **实际运行端口** | **50051** | | `vite.config.ts` 代理 | **50051** | | `gateway-client.ts` | **50051** | **解决方案**: 1. 更新 `vite.config.ts`: ```typescript proxy: { '/api': { target: 'http://127.0.0.1:50051', // 使用实际运行端口 // ... }, } ``` 2. 更新 `gateway-client.ts`: ```typescript export const DEFAULT_GATEWAY_URL = `${DEFAULT_WS_PROTOCOL}127.0.0.1:50051/ws`; export const FALLBACK_GATEWAY_URLS = [ DEFAULT_GATEWAY_URL, `${DEFAULT_WS_PROTOCOL}127.0.0.1:4200/ws`, // 保留作为备选 ]; ``` **验证端口**: ```bash # 检查实际运行的端口 netstat -ano | findstr "50051" netstat -ano | findstr "4200" ``` **注意**: `runtime-manifest.json` 中的端口声明与实际运行端口不一致,以实际监听端口为准。 **涉及文件**: - `desktop/vite.config.ts` - Vite 代理配置 - `desktop/src/lib/gateway-client.ts` - WebSocket 客户端默认 URL - `desktop/src/components/Settings/General.tsx` - 设置页面显示地址 - `desktop/src/components/Settings/ModelsAPI.tsx` - 模型 API 重连逻辑 **排查流程**: 1. 先用 `netstat` 确认实际监听端口 2. 对比 `runtime-manifest.json` 声明端口与实际端口 3. 确保所有前端配置使用**实际监听端口** 4. 重启 Vite 开发服务器 **验证修复**: ```bash # 检查端口监听 netstat -ano | findstr "50051" # 应显示 LISTENING # 重启 Vite 后测试 curl http://localhost:1420/api/agents # 应返回 JSON 数组而非 404/502 ``` **文件**: 多个配置文件 --- ## 9. 内核 LLM 响应问题 ### 9.1 聊天显示"思考中..."但无响应 **症状**: 发送消息后,UI 显示"思考中..."状态,但永远不会收到 AI 响应 **根本原因**: `loop_runner.rs` 中的代码存在两个严重问题: 1. **模型 ID 硬编码**: 使用固定的 `"claude-sonnet-4-20250514"` 而非用户配置的模型 2. **响应被丢弃**: 返回硬编码的 `"Response placeholder"` 而非实际 LLM 响应内容 **问题代码** (`crates/zclaw-runtime/src/loop_runner.rs`): ```rust // ❌ 错误 - 硬编码模型和响应 let request = CompletionRequest { model: "claude-sonnet-4-20250514".to_string(), // 硬编码! // ... }; // ... Ok(AgentLoopResult { response: "Response placeholder".to_string(), // 丢弃真实响应! // ... }) ``` **修复方案**: 1. **添加配置字段到 AgentLoop**: ```rust pub struct AgentLoop { // ... existing fields model: String, system_prompt: Option, max_tokens: u32, temperature: f32, } impl AgentLoop { pub fn with_model(mut self, model: impl Into) -> Self { self.model = model.into(); self } // ... other builder methods } ``` 2. **使用配置的模型**: ```rust let request = CompletionRequest { model: self.model.clone(), // 使用配置的模型 // ... }; ``` 3. **提取实际响应内容**: ```rust // 从 CompletionResponse.content 提取文本 let response_text = response.content .iter() .filter_map(|block| match block { ContentBlock::Text { text } => Some(text.clone()), ContentBlock::Thinking { thinking } => Some(format!("[思考] {}", thinking)), ContentBlock::ToolUse { name, input, .. } => { Some(format!("[工具调用] {}({})", name, serde_json::to_string(input).unwrap_or_default())) } }) .collect::>() .join("\n"); Ok(AgentLoopResult { response: response_text, // 返回真实响应 // ... }) ``` 4. **在 kernel.rs 中传递模型配置**: ```rust pub async fn send_message(&self, agent_id: &AgentId, message: String) -> Result { let agent_config = self.registry.get(agent_id)?; // 确定使用的模型:agent 配置优先,然后是 kernel 配置 let model = if !agent_config.model.model.is_empty() { &agent_config.model.model } else { &self.config.default_model }; let loop_runner = AgentLoop::new(/* ... */) .with_model(model) .with_max_tokens(agent_config.max_tokens.unwrap_or(self.config.max_tokens)) .with_temperature(agent_config.temperature.unwrap_or(self.config.temperature)); // ... } ``` **影响范围**: - `crates/zclaw-runtime/src/loop_runner.rs` - 核心修复 - `crates/zclaw-kernel/src/kernel.rs` - 模型配置传递 **验证修复**: 1. 配置 Coding Plan API(如 `https://coding.dashscope.aliyuncs.com/v1`) 2. 发送消息 3. 应该收到实际的 LLM 响应而非占位符 **特别说明**: 此问题影响所有 LLM 提供商,不仅限于 Coding Plan API。任何自定义模型配置都会被忽略。 ### 9.2 Coding Plan API 配置流程 **支持的 Coding Plan 端点**: | 提供商 | Provider ID | Base URL | |--------|-------------|----------| | Kimi Coding Plan | `kimi-coding` | `https://api.kimi.com/coding/v1` | | 百炼 Coding Plan | `qwen-coding` | `https://coding.dashscope.aliyuncs.com/v1` | | 智谱 GLM Coding Plan | `zhipu-coding` | `https://open.bigmodel.cn/api/coding/paas/v4` | **配置流程**: 1. **前端** (`ModelsAPI.tsx`): 用户选择 Provider,输入 API Key 和 Model ID 2. **存储** (`localStorage`): 保存为 `CustomModel` 对象 3. **连接时** (`connectionStore.ts`): 从 localStorage 读取配置 4. **传递给内核** (`kernel-client.ts`): 通过 `kernel_init` 命令传递 5. **内核处理** (`kernel_commands.rs`): 根据 Provider 和 Base URL 创建驱动 **关键代码路径**: ``` UI 配置 → localStorage → connectionStore.getDefaultModelConfig() → kernelClient.setConfig() → invoke('kernel_init', { configRequest }) → KernelConfig → create_driver() → OpenAiDriver::with_base_url() ``` **注意事项**: - Coding Plan 使用 OpenAI 兼容协议 (`api_protocol: "openai"`) - Base URL 必须包含完整路径(如 `/v1`) - 未知 Provider 会走 fallback 逻辑,使用 `local_base_url` 作为自定义端点 ### 9.3 更换模型配置后仍使用旧模型 **症状**: 在"模型与 API"页面切换模型后,对话仍然使用旧模型,API 请求中的 model 字段是旧的值 **示例日志**: ``` [kernel_init] Final config: model=qwen3.5-plus, base_url=https://coding.dashscope.aliyuncs.com/v1 [OpenAiDriver] Request body: {"model":"kimi-for-coding",...} # 旧模型! ``` **根本原因**: Agent 配置持久化在数据库中,其 `model` 字段优先于 Kernel 的配置 **问题代码** (`crates/zclaw-kernel/src/kernel.rs`): ```rust // ❌ 错误 - Agent 的 model 优先于 Kernel 的 model let model = if !agent_config.model.model.is_empty() { agent_config.model.model.clone() // 持久化的旧值 } else { self.config.model().to_string() }; ``` **问题分析**: 1. Agent 配置在创建时保存到 SQLite 数据库 2. Kernel 启动时从数据库恢复 Agent 配置 3. `send_message` 中 Agent 的 model 配置优先于 Kernel 的当前配置 4. 用户在"模型与 API"页面更改的是 Kernel 配置,不影响已持久化的 Agent 配置 **修复方案**: 让 Kernel 的当前配置优先,确保用户的"模型与 API"设置生效: ```rust // ✅ 正确 - 始终使用 Kernel 的当前 model 配置 let model = self.config.model().to_string(); eprintln!("[Kernel] send_message: using model={} from kernel config", model); ``` **影响范围**: - `crates/zclaw-kernel/src/kernel.rs` - `send_message` 和 `send_message_stream` 方法 **设计决策**: ZCLAW 的设计是让用户在"模型与 API"页面设置全局模型,而不是为每个 Agent 单独设置。因此: - Kernel 配置应该优先于 Agent 配置 - Agent 配置主要用于存储 personality、system_prompt 等 - model 配置应该由全局设置控制 **验证修复**: 1. 在"模型与 API"页面配置新模型 2. 发送消息 3. 检查终端日志,应显示 `using model=新模型 from kernel config` 4. 检查 API 请求体,`model` 字段应为新模型 --- ## 9.4 自我进化系统启动错误 ### 问题:DateTime 类型不匹配导致编译失败 **症状**: ``` error[E0277]: cannot subtract `chrono::DateTime` from `chrono::DateTime` --> desktop\src-tauri\src\intelligence\heartbeat.rs:542:27 | 542 | let idle_hours = (now - last_time).num_hours(); | ^ no implementation for `chrono::DateTime - chrono::DateTime` ``` **根本原因**: `chrono::DateTime::parse_from_rfc3339()` 返回 `DateTime`,但 `chrono::Utc::now()` 返回 `DateTime`,两种类型不能直接相减。 **解决方案**: 将 `DateTime` 转换为 `DateTime` 后再计算: ```rust // 错误写法 let last_time = chrono::DateTime::parse_from_rfc3339(&last_interaction).ok()?; let now = chrono::Utc::now(); let idle_hours = (now - last_time).num_hours(); // 编译错误! // 正确写法 let last_time = chrono::DateTime::parse_from_rfc3339(&last_interaction) .ok()? .with_timezone(&chrono::Utc); // 转换为 UTC let now = chrono::Utc::now(); let idle_hours = (now - last_time).num_hours(); // OK ``` **相关文件**: - `desktop/src-tauri/src/intelligence/heartbeat.rs` ### 问题:未使用的导入警告 **症状**: ``` warning: unused import: `Manager` warning: unused import: `futures::StreamExt` ``` **解决方案**: 1. 手动移除未使用的导入 2. 或使用 `cargo fix --lib -p --allow-dirty` 自动修复 **自动修复命令**: ```bash cargo fix --lib -p desktop --allow-dirty cargo fix --lib -p zclaw-hands --allow-dirty cargo fix --lib -p zclaw-runtime --allow-dirty cargo fix --lib -p zclaw-kernel --allow-dirty cargo fix --lib -p zclaw-protocols --allow-dirty ``` **注意**: `dead_code` 警告(未使用的字段、方法)不影响编译,可以保留供将来使用。 ### 9.5 阿里云百炼 Coding Plan 工具调用 400 错误 **症状**: - 普通对话正常,但需要调用 skill/tool 时返回 400 错误 - API 返回 `function.arguments must be in JSON format` - 或者响应为空,但显示有 `output_tokens` **根本原因**: 多层问题叠加 1. **流式模式不支持工具调用**: 阿里云百炼 (DashScope) Coding Plan API 的限制: > "tools暂时无法与stream=True同时使用" - 当同时启用 `stream: true` 和 `tools` 时,API 行为异常 - 工具调用参数无法正确传输 2. **响应解析优先级错误**: `convert_response` 方法优先处理 `content` 字段,即使它是空字符串 - 当 API 返回 `content: Some("")` 和 `tool_calls: [...]` 时 - 代码错误地选择了空的 content,导致响应为空 3. **ToolUse 消息 JSON 序列化错误**: 当 `input` 为 `Null` 时 - `serde_json::to_string(input)` 产生 `"null"` 字符串 - API 要求 `"{}"` (空对象) 格式 **问题分析**: 工具调用的完整流程: ``` 用户消息 → LLM 决定调用工具 → 返回 tool_calls → 执行工具 → 返回结果 → LLM 生成最终响应 ``` 在百炼 API 中,由于流式 + 工具不兼容: ``` stream=true + tools → API 行为异常 → tool_calls 参数丢失 → 空工具名/重复调用 ``` **修复方案**: 1. **检测不兼容的 Provider 并使用非流式模式** (`openai.rs:stream`): ```rust fn stream(&self, request: CompletionRequest) -> Pin> + Send + '_>> { let has_tools = !request.tools.is_empty(); let needs_non_streaming = self.base_url.contains("dashscope") || self.base_url.contains("aliyuncs") || self.base_url.contains("bigmodel.cn"); if has_tools && needs_non_streaming { eprintln!("[OpenAiDriver:stream] Provider detected that may not support streaming with tools, using non-streaming mode"); return self.stream_from_complete(request); // 使用非流式模式 } // ... 正常流式逻辑 } ``` 2. **实现 `stream_from_complete` 方法**: 调用非流式 API,然后模拟流式输出 ```rust fn stream_from_complete(&self, request: CompletionRequest) -> Pin> + Send + '_>> { let mut complete_request = self.build_api_request(&request); complete_request.stream = false; // 强制非流式 Box::pin(stream! { // 1. 发送非流式请求 let response = client.execute(request).await?; // 2. 解析响应 let api_response: OpenAiResponse = response.json().await?; // 3. 转换为流式事件 for tool_call in tool_calls { yield Ok(StreamChunk::ToolUseStart { id, name }); yield Ok(StreamChunk::ToolUseDelta { id, delta }); yield Ok(StreamChunk::ToolUseEnd { id, input }); } // 4. 文本内容 yield Ok(StreamChunk::TextDelta { delta: content }); // 5. 完成 yield Ok(StreamChunk::Complete { ... }); }) } ``` 3. **修复响应解析优先级** (`convert_response`): ```rust let (content, stop_reason) = match choice { Some(c) => { let has_tool_calls = c.message.tool_calls.as_ref().map(|tc| !tc.is_empty()).unwrap_or(false); let has_content = c.message.content.as_ref().map(|t| !t.is_empty()).unwrap_or(false); let blocks = if has_tool_calls { // ✅ 工具调用优先于空内容 tool_calls.iter().map(|tc| ContentBlock::ToolUse { id: tc.id.clone(), name: tc.function.name.clone(), input: serde_json::from_str(&tc.function.arguments).unwrap_or(Value::Null), }).collect() } else if has_content { // 非空文本内容 vec![ContentBlock::Text { text: c.message.content.as_ref().unwrap().clone() }] } else { vec![ContentBlock::Text { text: String::new() }] }; // ... } }; ``` 4. **修复 ToolUse 消息的 JSON 序列化**: ```rust zclaw_types::Message::ToolUse { id, tool, input } => { let args = if input.is_null() { "{}".to_string() // ✅ Null 转换为空对象 } else { serde_json::to_string(input).unwrap_or_else(|_| "{}".to_string()) }; // ... } ``` **影响范围**: - `crates/zclaw-runtime/src/driver/openai.rs` - OpenAI 兼容驱动 **已知的兼容性问题 Provider**: | Provider | Base URL 特征 | 问题 | |----------|--------------|------| | 阿里云百炼 | `dashscope.aliyuncs.com` | 流式 + 工具不兼容 | | 阿里云百炼 Coding Plan | `coding.dashscope.aliyuncs.com` | 流式 + 工具不兼容 | | 智谱 GLM | `bigmodel.cn` | 可能存在同样问题 | **验证修复**: 1. 配置百炼 Coding Plan API (`https://coding.dashscope.aliyuncs.com/v1`) 2. 发送需要调用 skill 的消息(如"查询腾讯财报") 3. 应看到日志:`[OpenAiDriver:stream] Provider detected that may not support streaming with tools` 4. 工具应正确执行,参数完整 **调试日志示例**: ``` [OpenAiDriver:stream] base_url=https://coding.dashscope.aliyuncs.com/v1, has_tools=true, needs_non_streaming=true [OpenAiDriver:stream] Provider detected that may not support streaming with tools, using non-streaming mode [OpenAiDriver] Non-streaming response received, tool_calls=1 [AgentLoop] ToolUseEnd: id=call_xxx, input={"skill_id":"finance-tracker","input":{...}} ``` ### 9.6 日志截断导致 UTF-8 字符边界 Panic **症状**: - 会话一直卡在"思考中..."状态 - 终端显示 panic:`byte index 100 is not a char boundary; it is inside '务' (bytes 99..102)` **错误信息**: ``` thread 'tokio-rt-worker' panicked at crates\zclaw-runtime\src\driver\openai.rs:502:82: byte index 100 is not a char boundary; it is inside '务' (bytes 99..102) of `你好!我是 **Agent Soul**...` ``` **根本原因**: 使用 `&c[..100]` 按字节截断 UTF-8 字符串用于日志输出 **问题代码** (`crates/zclaw-runtime/src/driver/openai.rs:502`): ```rust // ❌ 错误 - 按字节截断,可能切断多字节字符 choice.message.content.as_ref().map(|c| if c.len() > 100 { &c[..100] } else { c.as_str() }) ``` **问题分析**: Rust 字符串是 UTF-8 编码的: - ASCII 字符:1 字节 - 中文字符:3 字节(如 '务' = bytes 99..102) - 当截断位置正好落在多字节字符内部时,程序 panic **修复方案**: 使用 `floor_char_boundary()` 找到最近的合法字符边界: ```rust // ✅ 正确 - 使用 floor_char_boundary 确保不截断多字节字符 choice.message.content.as_ref().map(|c| { if c.len() > 100 { let end = c.floor_char_boundary(100); // 找到 <= 100 的最近字符边界 &c[..end] } else { c.as_str() } }) ``` **相关文件**: - `crates/zclaw-runtime/src/driver/openai.rs:502` - 日志截断逻辑 **验证修复**: 1. 启动应用 2. 发送包含中文的消息 3. 查看终端日志,应正常显示截断的内容 4. 会话不应卡住 **最佳实践**: Rust 中截断 UTF-8 字符串的正确方式: | 方法 | 用途 | |------|------| | `s.floor_char_boundary(n)` | 找到 <= n 的最近字符边界 | | `s.ceil_char_boundary(n)` | 找到 >= n 的最近字符边界 | | `s.chars().take(n).collect()` | 取前 n 个字符(创建新 String) | **注意**: `floor_char_boundary()` 需要 Rust 1.65+ --- ## 10. 技能系统问题 ### 10.1 Agent 无法调用合适的技能 **症状**: 用户发送消息(如"查询某公司财报"),Agent 没有调用相关技能,只是直接回复文本 **根本原因**: 1. **系统提示词缺少技能列表**: LLM 不知道有哪些技能可用 2. **SkillManifest 缺少 triggers 字段**: 触发词无法传递给 LLM 3. **技能触发词覆盖不足**: "财报" 无法匹配 "财务报告" **问题分析**: Agent 调用技能的完整链路: ``` 用户消息 → LLM → 选择 execute_skill 工具 → 传入 skill_id → 执行技能 ``` 如果 LLM 不知道有哪些 skill_id 可用,就无法主动调用。 **修复方案**: 1. **在系统提示词中注入技能列表** (`kernel.rs`): ```rust /// Build a system prompt with skill information injected fn build_system_prompt_with_skills(&self, base_prompt: Option<&String>) -> String { let skills = futures::executor::block_on(self.skills.list()); let mut prompt = base_prompt .map(|p| p.clone()) .unwrap_or_else(|| "You are a helpful AI assistant.".to_string()); if !skills.is_empty() { prompt.push_str("\n\n## Available Skills\n\n"); prompt.push_str("Use the `execute_skill` tool with the skill_id to invoke them:\n\n"); for skill in skills { prompt.push_str(&format!( "- **{}**: {}", skill.id.as_str(), skill.description )); if !skill.triggers.is_empty() { prompt.push_str(&format!( " (Triggers: {})", skill.triggers.join(", ") )); } prompt.push('\n'); } } prompt } ``` 2. **添加 triggers 字段到 SkillManifest** (`skill.rs`): ```rust pub struct SkillManifest { // ... existing fields /// Trigger words for skill activation #[serde(default)] pub triggers: Vec, } ``` 3. **解析 SKILL.md 中的 triggers** (`loader.rs`): ```rust // Parse triggers list in frontmatter if in_triggers_list && line.starts_with("- ") { triggers.push(line[2..].trim().trim_matches('"').to_string()); continue; } ``` 4. **添加常见触发词** (`skills/finance-tracker/SKILL.md`): ```yaml triggers: - "财务分析" - "财报" # 新增 - "财务数据" # 新增 - "盈利" - "营收" - "利润" ``` **影响范围**: - `crates/zclaw-kernel/src/kernel.rs` - 系统提示词构建 - `crates/zclaw-skills/src/skill.rs` - SkillManifest 结构 - `crates/zclaw-skills/src/loader.rs` - SKILL.md 解析 - `skills/*/SKILL.md` - 技能定义文件 **验证修复**: 1. 重启应用 2. 发送"查询腾讯财报" 3. Agent 应该调用 `execute_skill` 工具,传入 `skill_id: "finance-tracker"` ### 10.2 `skills_dir: None` 导致技能系统完全失效 **症状**: - Agent 无法调用任何技能,总是直接回复文本 - `skills.list()` 返回空列表 - 系统提示词中没有任何技能信息 **根本原因**: `KernelConfig::from_provider()` 方法中 `skills_dir` 被硬编码为 `None` **问题代码** (`crates/zclaw-kernel/src/config.rs:337`): ```rust // ❌ 错误 - from_provider() 中硬编码为 None pub fn from_provider( provider: &str, api_key: &str, model: &str, base_url: Option<&str>, api_protocol: &str, ) -> Self { let llm = match provider { // ... provider matching logic }; Self { database_url: default_database_url(), llm, skills_dir: None, // ← 硬编码!导致技能永不加载 } } ``` **影响分析**: Tauri 初始化 Kernel 时使用 `from_provider()` 创建配置: ``` kernel_init → KernelConfig::from_provider() → skills_dir: None → Kernel::boot() → skills_dir 不存在,跳过扫描 → skills.list() 返回空列表 → 系统提示词中无技能信息 → LLM 不知道有 execute_skill 工具可用 ``` **修复方案**: ```rust // ✅ 正确 - 使用默认技能目录 Self { database_url: default_database_url(), llm, skills_dir: default_skills_dir(), // 使用 ./skills 目录 } ``` **修复代码** (`config.rs:161-165`): ```rust fn default_skills_dir() -> Option { std::env::current_dir() .ok() .map(|cwd| cwd.join("skills")) } ``` **相关文件**: - `crates/zclaw-kernel/src/config.rs:337` - 修复位置 - `crates/zclaw-kernel/src/kernel.rs:79-83` - 技能目录扫描逻辑 **验证修复**: 1. 启动应用,查看终端日志 2. 应看到 `[Kernel] Scanning skills directory: ./skills` 3. 发送 "查询腾讯财报" 4. Agent 应调用 `execute_skill("finance-tracker", {...})` **已知限制**: `default_skills_dir()` 依赖 `current_dir()`,如果工作目录不同可能失效。更可靠的方案是使用可执行文件目录: ```rust // 建议改进 fn default_skills_dir() -> Option { std::env::current_exe() .ok() .and_then(|exe| exe.parent().map(|p| p.join("skills"))) .or_else(|| std::env::current_dir().ok().map(|cwd| cwd.join("skills"))) } ``` ### 10.3 技能页面显示"暂无技能"但技能目录存在 **症状**: - 技能市场显示 "暂无技能" 和 "0 技能" - 控制台日志显示 `[skill_list] Found 0 skills` - 技能目录 `G:\ZClaw_zclaw\skills` 存在且包含 70+ 个 SKILL.md 文件 **根本原因**: 多层问题叠加 1. **技能目录路径解析失败**: Tauri dev 模式下 `current_exe()` 和 `current_dir()` 返回意外路径 - `current_dir()` 可能返回 `desktop/src-tauri` 而非项目根目录 - `current_exe()` 可能返回 Tauri CLI 或 node.exe 而非编译后的 exe 2. **SkillRegistry.async 上下文使用 blocking_write()**: 在 tokio 异步运行时中调用 `blocking_write()` 导致 panic ``` thread 'tokio-rt-worker' panicked at registry.rs:86:38: Cannot block the current thread from within a runtime. ``` **问题代码** (`crates/zclaw-skills/src/registry.rs`): ```rust // ❌ 错误 - 在 async 函数调用的 sync 函数中使用 blocking_write pub async fn add_skill_dir(&self, dir: PathBuf) -> Result<()> { // ... for skill_path in skill_paths { self.load_skill_from_dir(&skill_path)?; // 调用 sync 函数 } } fn load_skill_from_dir(&self, dir: &PathBuf) -> Result<()> { // ... let mut skills = self.skills.blocking_write(); // 在 async 上下文中 panic! } ``` **修复方案**: 1. **使用编译时路径作为技能目录备选** (`config.rs:default_skills_dir`): ```rust fn default_skills_dir() -> Option { // 1. 环境变量 if let Ok(dir) = std::env::var("ZCLAW_SKILLS_DIR") { return Some(PathBuf::from(dir)); } // 2. 编译时路径 - CARGO_MANIFEST_DIR 是 crates/zclaw-kernel // 向上两级找到 workspace root let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); if let Some(workspace_root) = manifest_dir.parent().and_then(|p| p.parent()) { let workspace_skills = workspace_root.join("skills"); if workspace_skills.exists() { return Some(workspace_skills); } } // 3. 当前工作目录及向上搜索 // ... 其他备选方案 } ``` 2. **将 load_skill_from_dir 改为 async** (`registry.rs`): ```rust // ✅ 正确 - 使用 async write async fn load_skill_from_dir(&self, dir: &PathBuf) -> Result<()> { // ... 解析 SKILL.md // 使用 async write 而非 blocking_write let mut skills = self.skills.write().await; let mut manifests = self.manifests.write().await; skills.insert(manifest.id.clone(), skill); manifests.insert(manifest.id.clone(), manifest); Ok(()) } ``` **调试日志示例** (修复后): ``` [default_skills_dir] CARGO_MANIFEST_DIR: G:\ZClaw_zclaw\crates\zclaw-kernel [default_skills_dir] Workspace skills: G:\ZClaw_zclaw\skills (exists: true) [kernel_init] Skills directory: G:\ZClaw_zclaw\skills (exists: true) [skill_list] Found 77 skills ``` **影响范围**: - `crates/zclaw-kernel/src/config.rs` - default_skills_dir() 函数 - `crates/zclaw-skills/src/registry.rs` - load_skill_from_dir() 函数 - `desktop/src-tauri/src/kernel_commands.rs` - SkillInfoResponse 结构体(添加 triggers 和 category 字段) **前端配套修改**: - `desktop/src-tauri/src/kernel_commands.rs`: 添加 `triggers: Vec` 和 `category: Option` 字段 - `desktop/src/lib/kernel-client.ts`: 更新 `listSkills()` 返回类型 - `desktop/src/store/configStore.ts`: 更新 `createConfigClientFromKernel` 中的字段映射 - `desktop/src/lib/skill-adapter.ts`: 更新 `extractTriggers` 和 `extractCapabilities` **验证修复**: 1. 启动应用,查看终端日志 2. 应看到 `[kernel_init] Skills directory: ... (exists: true)` 3. 技能市场应显示 77 个技能 4. 点击技能可展开查看详情 **技能目录发现优先级**: 1. `ZCLAW_SKILLS_DIR` 环境变量 2. `CARGO_MANIFEST_DIR`/../skills (编译时路径) 3. `current_dir()`/skills 及向上搜索 4. `current_exe()`/skills 及向上搜索 5. 回退到 `current_dir()`/skills --- ## 10.4 Pipeline YAML 解析失败 - 类型不匹配 **症状**: - Pipeline 列表显示为空(Found 0 pipelines) - 后端调试日志显示扫描目录成功但没有找到任何 Pipeline - 没有明显的错误消息 **根本原因**: YAML 文件中的字段类型与 Rust 类型定义不匹配 **问题分析**: 1. **FileExport action formats 字段类型不匹配**: - Rust 定义:`formats: Vec`(枚举数组) - YAML 写法:`formats: ${inputs.export_formats}`(表达式字符串) - serde_yaml 无法将字符串解析为枚举数组,静默失败 2. **InputType serde rename_all 配置错误**: - YAML 使用 `multi-select`(kebab-case) - Rust serde 配置 `rename_all = "snake_case"` - 期望 `multi_select` 但收到 `multi-select`,解析失败 **修复方案**: 1. **将 formats 字段改为 String 类型** (`types.rs`): ```rust FileExport { formats: String, // 从 Vec 改为 String input: String, output_dir: Option, } ``` 2. **在运行时解析 formats 表达式** (`executor.rs`): ```rust let resolved_formats = context.resolve(formats)?; let format_strings: Vec = if resolved_formats.is_array() { resolved_formats.as_array()? .iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect() } else if resolved_formats.is_string() { // 尝试解析为 JSON 数组 serde_json::from_str(s).unwrap_or_else(|_| vec![s.to_string()]) } else { return Err(...); }; // 转换为 ExportFormat 枚举 let export_formats: Vec = format_strings .iter() .filter_map(|s| match s.to_lowercase().as_str() { "pptx" => Some(ExportFormat::Pptx), "html" => Some(ExportFormat::Html), "pdf" => Some(ExportFormat::Pdf), "markdown" | "md" => Some(ExportFormat::Markdown), "json" => Some(ExportFormat::Json), _ => None, }) .collect(); ``` 3. **修正 InputType serde 配置** (`types.rs`): ```rust #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "kebab-case")] // 从 snake_case 改为 kebab-case pub enum InputType { #[default] String, Number, Boolean, Select, MultiSelect, // YAML 中写 multi-select File, Text, } ``` **影响范围**: - `crates/zclaw-pipeline/src/types.rs` - InputType serde, FileExport formats - `crates/zclaw-pipeline/src/executor.rs` - 运行时解析 formats - `pipelines/**/*.yaml` - 确保使用 `multi-select` 而非 `multi_select` **验证修复**: ``` [DEBUG pipeline_list] Found 5 pipelines [DEBUG pipeline_list] Pipeline: classroom-generator -> category: education, industry: 'education' ``` **最佳实践**: - YAML 中的表达式(如 `${inputs.xxx}`)应该定义为 String 类型 - 在运行时通过 ExecutionContext.resolve() 解析表达式 - 使用 `kebab-case` 命名风格更符合 YAML 惯例 --- ## 11. 开发调试问题 ### 11.1 Web 端无法连接后端进行调试 **症状**: - Web 端 (`pnpm dev`) 无法连接后端 Gateway - 只能通过 Tauri 桌面端调试,效率低下 - Vite 代理配置正确但仍然无法连接 **根本原因**: Tauri 应用在开发模式下不暴露 API 端口给外部 **解决方案**: 使用开发模式服务器 ZCLAW 提供了可选的开发模式 HTTP/WebSocket 服务器,通过 feature flag 控制: **启动方式**: ```bash # 启动带开发服务器的 Tauri pnpm tauri:dev:web # 常规开发模式(无服务器) pnpm tauri:dev ``` **技术实现**: - 服务器代码: `desktop/src-tauri/src/dev_server.rs` - Feature flag: `dev-server` - 端口: `localhost:50051` (与生产 Gateway 相同) - 协议: HTTP + WebSocket **安全设计**: 1. **Feature Flag 控制**: 只有显式启用 `dev-server` feature 才会编译服务器代码 2. **仅 localhost 绑定**: 不暴露到外部网络 3. **CORS 白名单**: 只允许 Vite 开发服务器端口 (1420, 5173) 4. **生产构建排除**: `tauri build` 不会包含服务器代码 **API 端点**: | 端点 | 方法 | 说明 | |------|------|------| | `/health` | GET | 健康检查 | | `/ws` | GET | WebSocket 连接 | | `/api/kernel/status` | GET | 内核状态 | | `/api/agents` | GET | Agent 列表 | | `/api/skills` | GET | 技能列表 | | `/api/hands` | GET | Hands 列表 | | `/api/pipelines` | GET | Pipeline 列表 | | `/api/rpc` | POST | JSON-RPC 调用 | **验证服务器运行**: ```bash curl http://localhost:50051/health # 应返回: {"status":"ok","mode":"development",...} ``` **注意事项**: - 开发服务器仅提供基础 API 端点,完整功能需要 Tauri 运行时 - 生产构建 (`tauri build`) 自动排除开发服务器代码 - 如果端口 50051 被占用,服务器会跳过启动并输出警告 --- ## 12. 相关文档 - [ZCLAW 配置指南](./zclaw-configuration.md) - 配置文件位置、格式和最佳实践 - [Agent 和 LLM 提供商配置](./agent-provider-config.md) - Agent 管理和 Provider 配置 - [ZCLAW WebSocket 协议](./zclaw-websocket-protocol.md) - WebSocket 通信协议 --- ## 更新历史 | 日期 | 变更 | |------|------| | 2026-03-26 | 添加 11.1 节:Web 端无法连接后端进行调试 - 开发模式服务器方案 | | 2026-03-24 | 添加 9.6 节:日志截断导致 UTF-8 字符边界 Panic - floor_char_boundary 修复方案 | | 2026-03-24 | 添加 9.5 节:阿里云百炼 Coding Plan 工具调用 400 错误 - 流式+工具不兼容、响应解析优先级、JSON 序列化问题 | | 2026-03-24 | 添加 10.2 节:`skills_dir: None` 导致技能系统完全失效 - from_provider() 硬编码问题 | | 2026-03-24 | 添加 10.1 节:Agent 无法调用合适的技能 - 系统提示词注入技能列表 + triggers 字段 | | 2026-03-24 | 添加 9.4 节:自我进化系统启动错误 - DateTime 类型不匹配和未使用导入警告 | | 2026-03-23 | 添加 9.3 节:更换模型配置后仍使用旧模型 - Agent 配置优先于 Kernel 配置导致的问题 | | 2026-03-22 | 添加内核 LLM 响应问题:loop_runner.rs 硬编码模型和响应导致 Coding Plan API 不工作 | | 2026-03-20 | 添加端口配置问题:runtime-manifest.json 声明 4200 但实际运行 50051 | | 2026-03-18 | 添加记忆提取和图谱 UI 问题 | | 2026-03-18 | 添加刷新后对话丢失问题和 ChatArea 布局问题 | | 2026-03-17 | 添加首次使用引导流程 | | 2026-03-17 | 添加配置热重载限制问题 | | 2026-03-14 | 初始版本 |