Files
zclaw_openfang/docs/knowledge-base/troubleshooting.md
iven 9981a4674e fix(skills): inject skill list into system prompt for LLM awareness
Problem: Agent could not invoke appropriate skills when user asked about
financial reports because LLM didn't know which skills were available.

Root causes:
1. System prompt lacked available skill list
2. SkillManifest struct missing 'triggers' field
3. SKILL.md loader not parsing triggers list
4. "财报" keyword not matching "财务报告" trigger

Changes:
- Add triggers field to SkillManifest struct
- Parse triggers list from SKILL.md frontmatter
- Inject skill list into system prompt in kernel.rs
- Add "财报", "财务数据", "盈利", "营收" triggers to finance-tracker
- Add "财报分析" trigger to analytics-reporter
- Document fix in troubleshooting.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 15:39:18 +08:00

1190 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 故障排查指南
> 记录开发过程中遇到的问题、根因分析和解决方案。
---
## 1. 连接问题
### 1.1 WebSocket 连接失败
**症状**: `WebSocket connection failed``Unexpected server response: 400`
**排查步骤**:
```bash
# 1. 检查 OpenFang 是否运行
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
```
**常见原因**:
| 原因 | 解决方案 |
|------|----------|
| OpenFang 未启动 | `./openfang.exe start` |
| 端口错误 | OpenFang 使用 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
# 编辑 ~/.openfang/.env
echo "ZHIPU_API_KEY=your_key" >> ~/.openfang/.env
echo "BAILIAN_API_KEY=your_key" >> ~/.openfang/.env
echo "GEMINI_API_KEY=your_key" >> ~/.openfang/.env
```
3. 重启 OpenFang
```bash
./openfang.exe restart
```
**快速解决**: 使用已配置的 Agent
| Agent | 提供商 | 状态 |
|-------|--------|------|
| General Assistant | zhipu | 通常已配置 |
### 2.1.1 配置热重载限制(重要)
**症状**: 修改 `config.toml` 后,`/api/config``/api/status` 仍然返回旧配置
**根本原因**: OpenFang 将配置持久化在 SQLite 数据库中,`config.toml` 只在启动时读取
**验证问题**:
```bash
# 检查 config.toml 内容
cat ~/.openfang/config.toml
# 输出: provider = "zhipu"
# 检查 API 返回的配置
curl -s http://127.0.0.1:50051/api/config
# 输出: {"default_model":{"provider":"bailian",...}} # 不一致!
```
**解决方案**:
1. **必须完全重启 OpenFang**(热重载 `/api/config/reload` 不会更新持久化配置)
```bash
# 方法 1: 通过 API 关闭(然后手动重启)
curl -X POST http://127.0.0.1:50051/api/shutdown
# 方法 2: 使用 CLI
./openfang.exe stop
./openfang.exe start
```
2. **验证配置已生效**:
```bash
curl -s http://127.0.0.1:50051/api/status | grep -E "default_provider|default_model"
# 应输出: "default_provider":"zhipu"
```
**配置文件位置**:
| 文件 | 用途 |
|------|------|
| `~/.openfang/config.toml` | 主配置(启动时读取) |
| `~/.openfang/.env` | API Key 环境变量 |
| `~/.openfang/secrets.env` | 敏感信息 |
| `~/.openfang/data/openfang.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;
}
```
**历史背景**: 这个问题与 OpenClaw 的握手认证问题类似,但根因不同:
- OpenClaw: 需要 `cli/cli/operator` 身份 + Ed25519 配对
- OpenFang: 不需要认证握手,但需要正确的 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 检查 OpenFang 状态
```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` | OpenFang 未运行 | 启动服务 |
| `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) │ ← 固定在底部
└─────────────────────────────────────┘
```
---
## 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`OpenFang 无此端点
**修复方案**:
```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)
// 使用 OpenFang 的 /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 OpenFang 端口不匹配导致 Network Error
**症状**: 创建 Agent 或其他 API 操作时报错 `Failed to create agent: Network Error`,控制台显示 `POST http://localhost:1420/api/agents net::ERR_CONNECTION_REFUSED`
**根本原因**: `runtime-manifest.json` 声明端口 4200但实际 OpenFang 运行在 **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` 警告(未使用的字段、方法)不影响编译,可以保留供将来使用。
---
## 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"`
---
## 11. 相关文档
- [OpenFang 配置指南](./openfang-configuration.md) - 配置文件位置、格式和最佳实践
- [Agent 和 LLM 提供商配置](./agent-provider-config.md) - Agent 管理和 Provider 配置
- [OpenFang WebSocket 协议](./openfang-websocket-protocol.md) - WebSocket 通信协议
---
## 更新历史
| 日期 | 变更 |
|------|------|
| 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 | 初始版本 |