chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成

包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
iven
2026-03-29 10:46:26 +08:00
parent 9a5fad2b59
commit 5fdf96c3f5
268 changed files with 22011 additions and 3886 deletions

View File

@@ -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 不工作 |