Files
zclaw_openfang/docs/knowledge-base/troubleshooting.md
iven 8898bb399e
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 reports + feature docs + skills + admin-v2 + config sync
Update audit tracker, roadmap, architecture docs,
add admin-v2 Roles page + Billing tests,
sync CLAUDE.md, Cargo.toml, docker-compose.yml,
add deep-research / frontend-design / chart-visualization skills

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 19:25:00 +08:00

2314 lines
76 KiB
Markdown
Raw Permalink 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.

# 故障排查指南
> 记录开发过程中遇到的问题、根因分析和解决方案。
---
## 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 <PID> /F
# 或使用 PowerShell
Stop-Process -Id <PID> -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<Array<{ id: string; name?: string; state?: string }>>('/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 (
<AgentOnboardingWizard
isOpen={true}
onClose={() => {
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 (
<>
<div className="h-14 ...">Header</div>
<div className="flex-1 ...">Messages</div>
<div className="border-t ...">Input</div>
</>
);
```
**修复代码**:
```typescript
// ✅ 正确 - 使用 flex 容器
return (
<div className="flex flex-col h-full">
<div className="h-14 flex-shrink-0 ...">Header</div>
<div className="flex-1 overflow-y-auto ...">Messages</div>
<div className="flex-shrink-0 ...">Input</div>
</div>
);
```
**文件**: `desktop/src/components/ChatArea.tsx`
**布局原理**:
```
┌─────────────────────────────────────┐
│ Header (h-14, flex-shrink-0) │ ← 固定高度
├─────────────────────────────────────┤
│ │
│ Messages (flex-1, overflow-y-auto) │ ← 占据剩余空间,可滚动
│ │
├─────────────────────────────────────┤
│ Input (flex-shrink-0) │ ← 固定在底部
└─────────────────────────────────────┘
```
### 3.6 Agent 对话窗口无法滚动 — Flexbox 高度链断裂
**症状**: 对话消息较多时,消息区域无法滚动,输入框被推到视口外不可见。
**根本原因**: CSS flexbox 高度传递链中两层断裂,导致 `overflow-y-auto` 从未生效:
1. **`ResizableChatLayout` 使用 `flex-1` 但父级不是 flex 容器** — ChatArea 外层是 `<div className="relative h-full">`(非 flex`flex-1` 无效,容器高度塌缩为内容高度。
2. **`Conversation` 组件缺少 `min-h-0`** — flex 子元素默认 `min-height: auto`(等于内容高度),阻止容器收缩,内容撑开后 `overflow-y-auto` 无效。
**高度传递链(修复前)**:
```
ChatArea: relative h-full ← 继承父级 ✓
└─ ResizableChatLayout: flex-1 ← 父级非 flexflex-1 无效 ✗
└─ chatPanel: h-full ← 继承塌缩高度 ✗
└─ Conversation: flex-1 ← min-height:auto 阻止收缩 ✗
└─ overflow-y-auto ← 永远不触发滚动 ✗
```
**修复**:
| 文件 | 修改 | 原因 |
|------|------|------|
| `desktop/src/components/ai/ResizableChatLayout.tsx` | `flex-1` → `h-full`(两处) | 父级非 flex 容器,`flex-1` 无效 |
| `desktop/src/components/ai/Conversation.tsx` | 添加 `min-h-0` | flex 子元素需要 `min-h-0` 才能收缩 |
**高度传递链(修复后)**:
```
ChatArea: relative h-full ← 继承父级 ✓
└─ ResizableChatLayout: h-full ← 直接继承高度 ✓
└─ chatPanel: h-full ← 继承正确高度 ✓
├─ Header: h-14 shrink-0 ← 固定高度 ✓
├─ Conversation: flex-1 min-h-0 overflow-y-auto ← 占剩余空间,可收缩 ✓
└─ Input: shrink-0 ← 固定底部 ✓
```
**CSS 原理**:
- `flex-1` = `flex: 1 1 0%`,仅在父级是 flex 容器时生效。非 flex 父级中等于未设置。
- `min-height: auto`(默认值)= 内容最小高度,阻止 flex 子元素收缩到比内容更小。
- `min-h-0``min-height: 0`)允许 flex 子元素收缩,`overflow` 才能生效。
**验证修复**:
1. 进行多轮对话使消息超出视口高度
2. 消息区域应出现滚动条,输入框始终固定在底部可见
**相关文件**:
- `desktop/src/components/ChatArea.tsx` — 布局结构
- `desktop/src/components/ai/ResizableChatLayout.tsx` — 双面板布局
- `desktop/src/components/ai/Conversation.tsx` — 消息滚动容器
---
## 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: 变体
<div className="bg-gray-100 dark:bg-gray-800/50">
// 图谱画布 - 添加 dark: 变体
<div className="bg-gray-50 dark:bg-gray-900">
```
**文件**: `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<String>,
max_tokens: u32,
temperature: f32,
}
impl AgentLoop {
pub fn with_model(mut self, model: impl Into<String>) -> 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::<Vec<_>>()
.join("\n");
Ok(AgentLoopResult {
response: response_text, // 返回真实响应
// ...
})
```
4. **在 kernel.rs 中传递模型配置**:
```rust
pub async fn send_message(&self, agent_id: &AgentId, message: String) -> Result<MessageResponse> {
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<FixedOffset>` from `chrono::DateTime<Utc>`
--> desktop\src-tauri\src\intelligence\heartbeat.rs:542:27
|
542 | let idle_hours = (now - last_time).num_hours();
| ^ no implementation for `chrono::DateTime<Utc> - chrono::DateTime<FixedOffset>`
```
**根本原因**: `chrono::DateTime::parse_from_rfc3339()` 返回 `DateTime<FixedOffset>`,但 `chrono::Utc::now()` 返回 `DateTime<Utc>`,两种类型不能直接相减。
**解决方案**:
将 `DateTime<FixedOffset>` 转换为 `DateTime<Utc>` 后再计算:
```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 <package> --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<Box<dyn Stream<Item = Result<StreamChunk>> + 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<Box<dyn Stream<Item = Result<StreamChunk>> + 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+
### 9.9 对话上下文丢失 — Agent 不记得上一轮说了什么
**症状**: 每轮对话 Agent 都像失忆一样,重复问相同的问题,完全不知道之前聊了什么。
```
用户: 我要制作小学数学启蒙课件
Agent: [问了一堆需求确认问题]
用户: 用 涵盖加减法
Agent: 你是需要我帮你创建关于加减法的内容吗? ← 完全忘了上一轮
```
**根本原因**: `kernel.rs` 的 `send_message_stream_with_prompt()` 每次调用都执行 `self.memory.create_session(agent_id)` 创建全新 session。前端虽然传了 `session_id` 用于事件路由,但 kernel 完全忽略这个 ID导致 `loop_runner` 的 `get_messages()` 永远返回空数组LLM 从未收到历史消息。
```rust
// kernel.rs 修复前 — 每次创建新 session
let session_id = self.memory.create_session(agent_id).await?;
// 修复后 — 复用已有 session
let session_id = match session_id_override {
Some(id) => {
let existing = self.memory.get_messages(&id).await;
match existing {
Ok(msgs) if !msgs.is_empty() => id,
_ => self.memory.create_session(agent_id).await?,
}
}
None => self.memory.create_session(agent_id).await?,
};
```
**修复**:
| 文件 | 改动 |
|------|------|
| `crates/zclaw-kernel/src/kernel.rs` | `send_message_stream_with_prompt` 新增 `session_id_override` 参数,存在且非空则复用 |
| `desktop/src-tauri/src/kernel_commands.rs` | 解析前端 `session_id` 为 `SessionId`,传入 kernel |
**相关流程**:
1. 前端 `chatStore` 发送 `sessionId: "session_xxx"` 到 Tauri 命令
2. `kernel_commands.rs` 解析为 `SessionId` 传给 kernel
3. kernel 复用已有 session → `loop_runner.get_messages()` 返回历史
4. LLM 收到完整对话上下文
### 9.10 多轮工具调用 `tool_call_id is not found` 400 错误
**症状**: Agent 调用工具后,第二轮将工具结果发回 LLM 时报 400
```
LLM error: API error 400 Bad Request: {"error":{"message":"tool_call_id is not found","type":"invalid_request_error"}}
```
**根本原因**: `openai.rs` 的 `build_api_request()` 有两个关键缺陷:
1. **`OpenAiMessage` 缺少 `tool_call_id` 字段**: OpenAI 协议要求 `role: "tool"` 的消息必须携带 `tool_call_id` 来匹配对应的工具调用。当前代码用 `tool_call_id: _` 丢弃了这个值,且结构体中没有该字段。
2. **连续的 `ToolUse` 消息没有合并**: 同一轮 LLM 响应的多个工具调用应该在同一个 assistant 消息中(`tool_calls` 数组),而不是每个工具调用生成一个独立的 assistant 消息。
修复前的 API 请求格式(错误):
```json
[
{ "role": "assistant", "tool_calls": [{ "id": "call_1", ... }] },
{ "role": "assistant", "tool_calls": [{ "id": "call_2", ... }] },
{ "role": "tool", "content": "result1" }, // ← 缺少 tool_call_id
{ "role": "tool", "content": "result2" } // ← 缺少 tool_call_id
]
```
修复后(正确):
```json
[
{ "role": "assistant", "tool_calls": [{ "id": "call_1", ... }, { "id": "call_2", ... }] },
{ "role": "tool", "tool_call_id": "call_1", "content": "result1" },
{ "role": "tool", "tool_call_id": "call_2", "content": "result2" }
]
```
**修复** (`crates/zclaw-runtime/src/driver/openai.rs`):
1. **`OpenAiMessage` 添加 `tool_call_id` 字段**:
```rust
struct OpenAiMessage {
role: String,
content: Option<String>,
tool_calls: Option<Vec<OpenAiToolCall>>,
tool_call_id: Option<String>, // ← 新增
}
```
2. **重写消息转换逻辑** — 合并连续 `ToolUse` 消息 + 传递 `tool_call_id`:
```rust
// ToolResult 消息现在正确传递 tool_call_id
zclaw_types::Message::ToolResult { tool_call_id, output, is_error, .. } => {
messages.push(OpenAiMessage {
role: "tool",
content: Some(output.to_string()),
tool_calls: None,
tool_call_id: Some(tool_call_id.clone()), // ← 传递 ID
});
}
// ToolUse 消息累积到同一个 assistant 消息中
zclaw_types::Message::ToolUse { id, tool, input } => {
pending_tool_calls.get_or_insert_with(Vec::new).push(...);
}
```
**相关文件**:
- `crates/zclaw-runtime/src/driver/openai.rs` — `OpenAiMessage` 结构体 + `build_api_request()` 消息转换
**适用范围**: 所有 OpenAI 兼容提供商Kimi、百炼、智谱、OpenAI 等)的多轮工具调用。
---
### 9.11 thinking 启用时工具调用 `reasoning_content is missing` 400 错误
**症状**: 使用 Kimi 等 thinking 模式 API 时,第二轮工具结果发回后报 400
```
API error 400 Bad Request: {"error":{"message":"thinking is enabled but reasoning_content is missing in assistant tool call message at index 2"}}
```
**根本原因**: Kimi 要求包含工具调用的 assistant 消息必须携带 `reasoning_content` 字段。当前有两层缺陷:
1. **loop_runner 未分离 reasoning 和 text**: `ThinkingDelta` 和 `TextDelta` 混在 `iteration_text` 中(带 `[思考]` 前缀),且工具调用时不保存 Assistant 消息,导致 `reasoning_content` 完全丢失。
2. **openai.rs 未传递 `reasoning_content`**: `OpenAiMessage` 缺少 `reasoning_content` 字段,且 `Message::Assistant.thinking` 被忽略(`thinking: _`)。
**修复**:
**a) loop_runner — 分别追踪 reasoning 和 text** (`loop_runner.rs`):
```rust
// ThinkingDelta 只追加到 reasoning_text不混入 iteration_text
StreamChunk::ThinkingDelta { delta } => {
reasoning_text.push_str(delta);
}
// 工具调用前推 Assistant 消息(含 thinking
messages.push(Message::assistant_with_thinking(&iteration_text, &reasoning_text));
// 然后推 ToolUse 消息
for (id, name, input) in &pending_tool_calls {
messages.push(Message::tool_use(id, ...));
}
```
**b) openai.rs — 合并 [Assistant, ToolUse*] 并传递 reasoning_content**:
- `OpenAiMessage` 新增 `reasoning_content: Option<String>` 字段
- `build_api_request()` 检测 `[Assistant, ToolUse*]` 模式,合并为一个 assistant 消息,同时携带 `content`、`reasoning_content`、`tool_calls`
修复后的 API 请求格式:
```json
{
"role": "assistant",
"content": "text content",
"reasoning_content": "thinking content",
"tool_calls": [{ "id": "call_1", ... }]
}
```
**相关文件**:
- `crates/zclaw-runtime/src/loop_runner.rs` — 流式/非流式路径的 reasoning 分离
- `crates/zclaw-runtime/src/driver/openai.rs` — `OpenAiMessage` + `build_api_request()` 合并逻辑
**适用范围**: 所有启用 thinking/reasoning 的 OpenAI 兼容提供商Kimi、百炼、DeepSeek 等)。
---
## 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<String>,
}
```
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::path::PathBuf> {
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<PathBuf> {
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<std::path::PathBuf> {
// 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<String>` 和 `category: Option<String>` 字段
- `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<ExportFormat>`(枚举数组)
- 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<ExportFormat> 改为 String
input: String,
output_dir: Option<String>,
}
```
2. **在运行时解析 formats 表达式** (`executor.rs`):
```rust
let resolved_formats = context.resolve(formats)?;
let format_strings: Vec<String> = 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<ExportFormat> = 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. SaaS 后端问题
### 12.1 Admin 登录页无网络请求
**症状**: 点击 Admin 面板 (`localhost:3000/login`) 的登录按钮后,页面无任何反应,无网络请求、无控制台错误。
**根本原因**: 前端 `api-client.ts` 的 `BASE_URL` 与后端路由前缀不匹配。
- 前端: `BASE_URL = 'http://localhost:8080'`,请求路径 `/auth/login` → 实际请求 `http://localhost:8080/auth/login`
- 后端: 路由前缀 `/api/v1/auth/login`
**修复**:
1. 将 `BASE_URL` 改为 `'http://localhost:8080/api/v1'`
2. 移除所有 API 路径中多余的 `/api/` 前缀
**涉及文件**: `admin/src/lib/api-client.ts`
### 12.2 SQLite → PostgreSQL 遗留语法导致 500 错误
**症状**: 登录成功后,仪表盘页面 (`/stats/dashboard`) 和操作日志 (`/logs/operations`) 返回 500 `DATABASE_ERROR`。
**根本原因**: 后端代码从 SQLite 迁移到 PostgreSQL 时,部分文件遗漏了 SQL 语法转换。
**SQLite → PostgreSQL 语法差异**:
| SQLite | PostgreSQL | 影响位置 |
|--------|-----------|----------|
| `?1`, `?2` 占位符 | `$1`, `$2` | 所有 SQL 查询 |
| `date('now')` | `CURRENT_DATE` | dashboard_stats |
| `enabled = 1` / `= 0` | `enabled = true` / `= false` | dashboard_stats, totp |
| `LIMIT ?1 OFFSET ?2` | `LIMIT $1 OFFSET $2` | list_operation_logs |
| `INSERT OR IGNORE` | `INSERT ... ON CONFLICT DO NOTHING` | schema |
| `datetime('now')` | `NOW()` | schema |
| `INTEGER PRIMARY KEY AUTOINCREMENT` | `BIGSERIAL PRIMARY KEY` | schema |
| `REAL` | `DOUBLE PRECISION` | schema |
**遗漏修复的文件**:
- `crates/zclaw-saas/src/account/handlers.rs` — dashboard_stats、list_operation_logs、device 相关
- `crates/zclaw-saas/src/relay/handlers.rs` — provider api_key 查询、retry_task
- `crates/zclaw-saas/src/auth/totp.rs` — TOTP 设置/验证/禁用
- `crates/zclaw-saas/src/auth/mod.rs` — API Token 验证、last_used_at 更新
**排查方法**: 全局搜索 `?` 数字占位符和 SQLite 特有函数:
```bash
grep -rn '?[0-9]\|date(.now.)\|enabled = [01]' crates/zclaw-saas/src/
```
### 12.3 Admin 账号角色权限不足 (403)
**症状**: 登录成功后,`/stats/dashboard` 和 `/logs/operations` 返回 403 Forbidden。
**根本原因**: 通过 `/auth/register` 注册的账号默认角色为 `user`,只有 `model:read`、`relay:use`、`config:read` 权限,无法访问 admin 端点。
**解决方案**: 需要将账号升级为 `super_admin` 角色。两种方式:
1. **设置环境变量自动种子**(推荐):
```bash
ZCLAW_ADMIN_USERNAME=admin ZCLAW_ADMIN_PASSWORD=your_password ./zclaw-saas
```
2. **直接修改数据库**:
```bash
# PostgreSQL
psql -U postgres -d zclaw -c "UPDATE accounts SET role = 'super_admin' WHERE username = 'admin'"
```
**权限映射**:
| 角色 | 权限 |
|------|------|
| `user` | `model:read`, `relay:use`, `config:read` |
| `super_admin` | `admin:full`, `account:admin`, `provider:manage`, `model:manage`, `relay:admin`, `config:write` |
**注意**: 普通用户通过 API 无法自提升角色 — `update_account` handler 会剥离非管理员的 `role` 字段。
### 12.4 前端 usage 路由与后端不匹配 (404)
**症状**: 仪表盘请求 `/usage/daily?days=30` 返回 404。
**根本原因**: 前端 `api-client.ts` 使用 `/usage/daily` 和 `/usage/by-model` 路径,但后端只有一个统一的 `/api/v1/usage` 端点,参数为 `?from=...&to=...&provider_id=...&model_id=...`。
**修复**: 将前端 `usage.daily()` 和 `usage.byModel()` 合并为 `usage.get(params)`,路径改为 `/usage`。
**涉及文件**: `admin/src/lib/api-client.ts`
---
## 13. Admin 前端 ERR_ABORTED / 后端卡死 (2026-03-30)
### 13.1 症状
- Admin 前端登录后所有 GET 请求返回 `ERR_ABORTED`Network 面板显示红色 cancelled
- 后端日志显示请求已接收并处理,但响应无法到达客户端
- 后端 PostgreSQL 连接池50 max被耗尽所有后续请求 hang
- `GET /api/health` 也无法响应,整个后端完全卡死
### 13.2 根因分析(因果链)
```
Next.js App Router SSR
→ React 组件服务端渲染 + 客户端 hydration 重建
→ SWR 在 hydration mount 时触发 fetch
→ hydration 卸载旧组件 → AbortController abort 请求
→ Vite proxy 已将请求转发到后端
→ 后端不知道请求已被 abort继续处理占用 DB 连接)
→ SWR retry 重新发起 → 又被 abort → 死循环
→ PostgreSQL 连接池耗尽 → 后端完全卡死
```
**根本矛盾**: Next.js SSR/hydration 机制与 SWR fetch-on-mount 模式存在不可调和的冲突。abort 信号无法从浏览器传播到后端,导致后端持续处理已废弃的请求。
### 13.3 已尝试的修复(均未解决)
| 尝试 | 方案 | 结果 |
|------|------|------|
| 1 | SWRConfig 全局配置dedupingInterval, revalidateOnFocus | 无效abort 发生在更底层 |
| 2 | AuthGuard 路由守卫重构 | 无效,请求在 guard 之前就发出 |
| 3 | `dynamic ssr: false` 页面级禁用 SSR | 部分改善,但 hydration 仍触发 abort |
| 4 | 前端直连后端(绕过 Vite proxy | 无效,问题不在 proxy |
| 5 | health handler 3s 超时 | 只保护了 health 端点,不解决根因 |
| 6 | AbortError 不重试 | 减少了 retry但首次 abort 仍占用连接 |
### 13.4 最终解决方案
**用纯 SPAAnt Design Pro彻底重写 Admin 前端**,消除 SSR/hydration 问题。
**admin-v2 技术栈**:
- Vite 6纯客户端无 SSR
- React 19 + antd v5 + @ant-design/pro-components
- React Router v7 + TanStack Query v5 + Axios + Zustand
**关键修复**:
1. 移除 React `StrictMode`(开发模式双重 mount 触发重复请求 + abort
2. Axios timeout 10s → 30s防止慢请求被误杀
3. JWT 拦截器(自动附加 token + 401 刷新)
### 13.5 验证结果
- 全部 12 页面功能正常18 个网络请求全部 200
- 零 `ERR_ABORTED`,后端连接池不再耗尽
- 后端 health 检查持续返回 200
**涉及文件**: `admin-v2/` 目录(全新项目,替换 `admin/`
---
## 14. SaaS Relay 模式未生效 — llm_routing 读取路径 Bug (2026-03-31)
**症状**: Admin 后台配置 `llm_routing=relay`,但桌面端仍走本地 Kernel 模式(需要本地 API Key不经过 SaaS Key Pool 中转。
**根本原因**: `desktop/src/store/connectionStore.ts` 第 362 行的 `llm_routing` 读取路径多了一层嵌套:
```typescript
// ❌ 错误storedAccount 是 SaaSAccountInfo 对象本身,不是 { account: SaaSAccountInfo }
const adminRouting = storedAccount?.account?.llm_routing;
// ✅ 正确:直接读取 SaaSAccountInfo.llm_routing
const adminRouting = storedAccount?.llm_routing;
```
`saveSaaSSession` 将 `session.account`(即 `SaaSAccountInfo`)直接 `JSON.stringify` 存入 localStorage 的 `zclaw-saas-account` 键。但 connectionStore 误以为存储结构是 `{ account: { llm_routing: ... } }`,实际是 `{ llm_routing: ... }`。
**修复**: 修改 `desktop/src/store/connectionStore.ts` 第 362 行。
**影响**: 此 bug 导致 `llm_routing` 管理端配置从未生效,所有桌面端都忽略 admin 路由优先级,走默认连接模式。
**验证**: 登录后检查 SaaS 后端 relay tasks 表是否有新记录。如果没有新 relay task 创建,说明走了本地模式。
---
## 15. SaaS Relay 403 — Coding Plan 不识别 User-Agent (2026-03-31)
**症状**: SaaS relay 模式调用 Kimi Coding Plan 时返回 403
```json
{"error":{"message":"Kimi For Coding is currently only available for Coding Agents such as Kimi CLI, Claude Code, Roo Code, Kilo Code, etc.","type":"access_terminated_error"}}
```
**根本原因**: SaaS relay 的 `execute_relay()` 使用默认 reqwest User-Agent`reqwest/0.12.x`Kimi Coding Plan 检查 User-Agent 白名单。
**修复**: `crates/zclaw-saas/src/relay/service.rs` 的 HTTP 请求构建中添加:
```rust
.header("User-Agent", "claude-code/1.0")
```
**注意**: 此修复与 9.7 节类似,但 9.7 修复的是桌面端直连时的 User-Agent此处修复的是 SaaS 后端作为代理转发时的 User-Agent。两者都需要。
---
## 16. 桌面端会话持久化三连 Bug (2026-04-01)
### 16.1 页面刷新跳回登录页
**症状**: F5 刷新页面后被踢回登录界面,必须重新输入密码。
**根本原因**: `saasStore.restoreSession()` 方法已实现(从 OS keyring 读取 token + 验证但从未被调用。Zustand store 初始化 `isLoggedIn: false``App.tsx` 同步检查后立即渲染 `<LoginPage />`。
**修复**: `desktop/src/App.tsx` — 添加 `isRestoring` 状态,启动时调用 `restoreSession()`,恢复完成前显示加载屏。
### 16.2 登录后空白对话页
**症状**: 登录后看到空白对话页面,必须手动点击 agent 才能看到之前的对话。
**根本原因**: `CloneManager` 在 `loadClones()` 后没有调用 `chatStore.syncAgents()`。`syncAgents` 包含恢复 `currentAgent` 和从 conversations 数组恢复 messages 的安全网逻辑。
**修复**: `desktop/src/components/CloneManager.tsx` — `loadClones().then()` 中调用 `syncAgents(clones)`。
### 16.3 Agent 无上下文记忆 — session ID 映射断裂
**症状**: 每条消息 Agent 都说"这是第一条消息",多轮对话上下文完全丢失。
**根本原因**: 前端生成的 `sessionKey` UUID 传到 kernel 后,`get_messages()` 查不到(因为该 ID 从未存入 sessions 表),于是 `create_session()` 生成全新的 UUID。下一轮前端再传原 UUID又创建新 session。前端 session ID 和数据库 session ID 永远无法匹配。
```
Turn 1: 前端 "abc-123" → DB 查不到 → 创建 "xyz-789" → 消息存到 "xyz-789"
Turn 2: 前端 "abc-123" → DB 查不到 → 创建 "def-456" → 消息存到 "def-456"
结果: 永远找不到历史消息
```
**修复**:
1. `crates/zclaw-memory/src/store.rs` — 添加 `get_or_create_session()` 方法,直接用前端提供的 ID 创建 session
2. `crates/zclaw-kernel/src/kernel.rs` — 使用 `get_or_create_session` 替代 lookup-then-create
### 16.4 `messages[N] must not be empty` — assistant 消息空 content
**症状**: Bug 16.3 修复后暴露:`API error 400: messages[4] with role 'assistant' must not be empty`
**根本原因**: Bug 16.3 修复后对话历史终于被正确发送给 LLM。但历史中的 `ToolUse` 消息对应的 assistant 消息 content 为空(只有 tool_calls 没有 text。Kimi/Qwen API 要求 assistant 消息的 `content` 不能为空字符串。
**修复**: `crates/zclaw-runtime/src/driver/openai.rs` — `flush_pending` 中 assistant 消息的 `content` 字段:空字符串 → 有意义的占位符(`"正在思考..."` / `"正在调用工具..."`)。
**涉及文件**:
- `desktop/src/App.tsx`
- `desktop/src/components/CloneManager.tsx`
- `crates/zclaw-memory/src/store.rs`
- `crates/zclaw-kernel/src/kernel.rs`
- `crates/zclaw-runtime/src/driver/openai.rs`
---
## 17. 相关文档
- [ZCLAW 配置指南](./zclaw-configuration.md) - 配置文件位置、格式和最佳实践
- [Agent 和 LLM 提供商配置](./agent-provider-config.md) - Agent 管理和 Provider 配置
- [ZCLAW WebSocket 协议](./zclaw-websocket-protocol.md) - WebSocket 通信协议
---
## 更新历史
| 日期 | 变更 |
|------|------|
| 2026-04-02 | 添加 3.6 节Agent 对话窗口无法滚动 — Flexbox 高度链断裂ResizableChatLayout flex-1 在非 flex 父级无效 + Conversation 缺少 min-h-0 |
| 2026-04-01 | 添加第 16 节:桌面端会话持久化四连 Bug — 页面刷新跳登录 + 空白对话页 + Agent 无记忆(session ID 映射断裂) + assistant 空 content 400 |
| 2026-03-31 | 添加第 14/15 节llm_routing 读取路径 Bug + SaaS Relay 403 User-Agent 缺失 — relay 模式从未生效的根因分析 |
| 2026-03-30 | 添加第 13 节Admin 前端 ERR_ABORTED / 后端卡死 — Next.js SSR/hydration + SWR 根本冲突导致连接池耗尽admin-v2 (Ant Design Pro 纯 SPA) 替代方案 |
| 2026-03-28 | 添加 12.1-12.4 节SaaS 后端问题 — Admin 登录无请求、SQLite→PostgreSQL 遗留语法、角色权限不足、usage 路由不匹配 |
| 2026-03-27 | 添加 9.10/9.11 节:多轮工具调用 tool_call_id + reasoning_content 缺失 — OpenAiMessage 字段补全、[Assistant,ToolUse*] 合并、reasoning 分离追踪 |
| 2026-03-27 | 添加 9.10 节:多轮工具调用 tool_call_id is not found — OpenAiMessage 缺少 tool_call_id 字段 + 连续 ToolUse 未合并 |
| 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() 硬编码问题 |
### 9.7 Coding Plan API (Kimi/百炼/智谱) User-Agent 403 拒绝
**症状**: 使用 Kimi Coding Plan (`api.kimi.com/coding`) 时报 403
```
LLM error: API error 403 Forbidden: {"error":{"message":"Kimi For Coding is currently only available for Coding Agents such as Kimi CLI, Claude Code, Roo Code, Kilo Code, etc.","type":"access_terminated_error"}}
```
**根本原因**: Coding Plan 提供商检查 `User-Agent` 请求头,只允许已知 Coding Agent 客户端访问。ZCLAW 发送的 `ZCLAW/0.1.0` 不在白名单中。
**修复**: `crates/zclaw-runtime/src/lib.rs` — 将 `USER_AGENT` 改为 `claude-code/0.1.0`(精确匹配白名单格式,全小写)。
同时修复 `desktop/src-tauri/src/llm/mod.rs` 中两处 `reqwest::Client::new()` 也加上 User-Agent。
```rust
// 修复前
pub const USER_AGENT: &str = "ZCLAW/0.1.0";
// 修复后
pub const USER_AGENT: &str = "claude-code/0.1.0";
```
**相关文件**:
- `crates/zclaw-runtime/src/lib.rs` — USER_AGENT 常量
- `crates/zclaw-runtime/src/driver/openai.rs` — 使用 USER_AGENT 构建 HTTP client
- `desktop/src-tauri/src/llm/mod.rs` — Tauri 侧 LLM 客户端call_api / call_embedding_api
---
### 9.8 Coding Plan 模型返回空响应(显示 "..."
**症状**: User-Agent 修复后,模型可以连接,但前端只显示 "..." 无实质性内容。
**根本原因**: 多层问题叠加:
1. **`reasoning_content` 字段未解析**: Kimi/Qwen/DeepSeek/GLM 等模型的思考过程通过 `delta.reasoning_content` 字段返回(而非 `delta.content`OpenAI 驱动的 `OpenAiDelta` 结构体缺少此字段,所有 thinking 内容被静默丢弃。
2. **ThinkingDelta 未累积到 `iteration_text`**: `loop_runner.rs` 中 `ThinkingDelta` 只发送给前端(`LoopEvent::Delta`),不累积到 `iteration_text`,导致最终 `Complete` 事件的 `response` 为空。
3. **每个 reasoning token 重复加 `[思考]` 前缀**: 每收到一个 `ThinkingDelta` 就加一次 `[思考] `,导致显示为 `[思考] 用户[思考] 只是[思考] 简单地...`。
**修复**:
**a) OpenAI 驱动添加 `reasoning_content` 支持** (`openai.rs`):
```rust
// OpenAiDelta 结构体 — 添加字段
struct OpenAiDelta {
content: Option<String>,
reasoning_content: Option<String>, // ← 新增
tool_calls: Option<Vec<OpenAiToolCallDelta>>,
}
// OpenAiResponseMessage 结构体 — 同样添加
struct OpenAiResponseMessage {
content: Option<String>,
reasoning_content: Option<String>, // ← 新增
tool_calls: Option<Vec<OpenAiToolCallResponse>>,
}
```
流式路径:`reasoning_content` → `StreamChunk::ThinkingDelta`
非流式路径:当 `content` 为空但 `reasoning_content` 有值时,用 reasoning 作为文本返回。
**b) loop_runner 累积 thinking 内容但不发给用户** (`loop_runner.rs`):
```rust
StreamChunk::ThinkingDelta { delta } => {
// 只累积到 iteration_text后端 memory不发给前端
if !in_thinking_phase {
iteration_text.push_str("[思考] ");
in_thinking_phase = true;
}
iteration_text.push_str(delta);
// 不调用 tx.send() — 用户不看到思考过程
}
```
思考内容仅存入后端 memory 用于上下文,用户界面只显示正式回复。
**适用范围**: 此修复适用于所有使用 `reasoning_content` 字段的 Coding Plan 提供商:
- Kimi Coding Plan (`api.kimi.com/coding`)
- 百炼/DashScope Coding Plan (`coding.dashscope.aliyuncs.com`)
- 智谱 GLM Coding Plan (`open.bigmodel.cn/api/coding/paas/v4`)
- DeepSeek R1 等推理模型
**无需额外适配**:三个提供商都走同一个 OpenAI 兼容驱动,修复一处通用覆盖。
| 2026-03-27 | 添加 9.7/9.8 节Coding Plan 403 拒绝 + 空响应User-Agent、reasoning_content、thinking 显示) |
| 2026-03-27 | 添加 9.7/9.8/9.9 节Coding Plan 403 拒绝、空响应、对话上下文丢失 |
| 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 | 初始版本 |