chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、 文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
@@ -1269,6 +1269,174 @@ Rust 中截断 UTF-8 字符串的正确方式:
|
||||
|
||||
**注意**: `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. 技能系统问题
|
||||
@@ -1726,7 +1894,94 @@ curl http://localhost:50051/health
|
||||
|
||||
---
|
||||
|
||||
## 12. 相关文档
|
||||
## 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. 相关文档
|
||||
|
||||
- [ZCLAW 配置指南](./zclaw-configuration.md) - 配置文件位置、格式和最佳实践
|
||||
- [Agent 和 LLM 提供商配置](./agent-provider-config.md) - Agent 管理和 Provider 配置
|
||||
@@ -1738,11 +1993,104 @@ curl http://localhost:50051/health
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 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() 硬编码问题 |
|
||||
| 2026-03-24 | 添加 10.1 节:Agent 无法调用合适的技能 - 系统提示词注入技能列表 + triggers 字段 |
|
||||
### 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 不工作 |
|
||||
|
||||
Reference in New Issue
Block a user