From 0d79993691524943589e98681f54d7efde439e55 Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 16 Apr 2026 22:22:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(saas):=203=20=E9=A1=B9=20P0=20=E5=AE=89?= =?UTF-8?q?=E5=85=A8/=E5=8A=9F=E8=83=BD=E4=BF=AE=E5=A4=8D=20+=20TRUTH.md?= =?UTF-8?q?=20=E6=95=B0=E5=AD=97=E6=A0=A1=E5=87=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0-01: Admin ApiKeys 创建功能前后端不匹配 - 前端 service 从 /keys 改回 /tokens(api_tokens 表) - 前端 UI 字段 {name, expires_days, permissions} 与旧路由匹配 P0-02: 账户锁定检查错误处理 - unwrap_or(false) 改为 map_err + SaasError 传播 - SQL 查询失败时返回错误而非静默跳过锁定检查 P0-03: Logout refresh token 撤销增强 - 新增 access token cookie fallback 提取 account_id - Tauri 桌面端 Bearer auth 场景下也能撤销 refresh token TRUTH.md 校准: Tauri 183→190, invoke 95→104, .route() 136→137, 中间件 15→14 --- CLAUDE.md | 2 +- admin-v2/src/services/api-keys.ts | 8 ++++--- crates/zclaw-saas/src/auth/handlers.rs | 32 +++++++++++++++++++++++++- docs/TRUTH.md | 15 ++++++------ 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4e09f31..6375b0f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -546,7 +546,7 @@ refactor(store): 统一 Store 数据获取方式 | Pipeline DSL | ✅ 稳定 | 04-01 17 个 YAML 模板 + DAG 执行器 | | Hands 系统 | ✅ 稳定 | 9 启用 (Browser/Collector/Researcher/Twitter/Whiteboard/Slideshow/Speech/Quiz/Clip) | | 技能系统 (Skills) | ✅ 稳定 | 75 个 SKILL.md + 语义路由 | -| 中间件链 | ✅ 稳定 | 15 层 (含 DataMasking@90, ButlerRouter, TrajectoryRecorder@650 — V13注册) | +| 中间件链 | ✅ 稳定 | 14 层 (ButlerRouter@80, DataMasking@90, Compaction@100, Memory@150, Title@180, SkillIndex@200, DanglingTool@300, ToolError@350, ToolOutputGuard@360, Guardrail@400, LoopGuard@500, SubagentLimit@550, TrajectoryRecorder@650, TokenCalibration@700) | ### 关键架构模式 diff --git a/admin-v2/src/services/api-keys.ts b/admin-v2/src/services/api-keys.ts index 5268c44..0991872 100644 --- a/admin-v2/src/services/api-keys.ts +++ b/admin-v2/src/services/api-keys.ts @@ -1,13 +1,15 @@ import request, { withSignal } from './request' import type { TokenInfo, CreateTokenRequest, PaginatedResponse } from '@/types' +// 使用 /tokens 路由 (api_tokens 表),前端 UI 字段 {name, expires_days, permissions} 与此后端匹配 +// 注: /keys 路由 (account_api_keys 表) 需要 {provider_id, key_value},属于不同的 Key 管理系统 export const apiKeyService = { list: (params?: Record, signal?: AbortSignal) => - request.get>('/keys', withSignal({ params }, signal)).then((r) => r.data), + request.get>('/tokens', withSignal({ params }, signal)).then((r) => r.data), create: (data: CreateTokenRequest, signal?: AbortSignal) => - request.post('/keys', data, withSignal({}, signal)).then((r) => r.data), + request.post('/tokens', data, withSignal({}, signal)).then((r) => r.data), revoke: (id: string, signal?: AbortSignal) => - request.delete(`/keys/${id}`, withSignal({}, signal)).then((r) => r.data), + request.delete(`/tokens/${id}`, withSignal({}, signal)).then((r) => r.data), } diff --git a/crates/zclaw-saas/src/auth/handlers.rs b/crates/zclaw-saas/src/auth/handlers.rs index b250812..0ffb946 100644 --- a/crates/zclaw-saas/src/auth/handlers.rs +++ b/crates/zclaw-saas/src/auth/handlers.rs @@ -215,7 +215,10 @@ pub async fn login( .bind(&r.id) .fetch_one(&state.db) .await - .unwrap_or(false); + .map_err(|e| { + tracing::warn!(account_id = %r.id, error = %e, "Lockout check query failed"); + SaasError::Internal("账号状态检查失败,请重试".into()) + })?; if is_locked { return Err(SaasError::AuthError("账号已被临时锁定,请稍后再试".into())); @@ -631,5 +634,32 @@ pub async fn logout( } } + // Fallback: 如果没有找到 refresh token,尝试从 access token cookie 提取 account_id + // Tauri 桌面端使用 Bearer auth 时,logout body 可能不含 refresh_token + if tokens_to_check.is_empty() { + if let Some(access_cookie) = jar.get(ACCESS_TOKEN_COOKIE) { + let access_val = access_cookie.value().to_string(); + if let Ok(claims) = verify_token_skip_expiry(&access_val, jwt_secret) { + let now = chrono::Utc::now(); + let result = sqlx::query( + "UPDATE refresh_tokens SET used_at = $1 WHERE account_id = $2 AND used_at IS NULL" + ) + .bind(&now) + .bind(&claims.sub) + .execute(&state.db) + .await; + + match result { + Ok(r) => { + tracing::info!(account_id = %claims.sub, n = r.rows_affected(), "Refresh tokens revoked via access token fallback"); + } + Err(e) => { + tracing::warn!(account_id = %claims.sub, error = %e, "Failed to revoke refresh tokens (access fallback)"); + } + } + } + } + } + (clear_auth_cookies(jar), axum::http::StatusCode::NO_CONTENT) } diff --git a/docs/TRUTH.md b/docs/TRUTH.md index f8f57f0..64639d4 100644 --- a/docs/TRUTH.md +++ b/docs/TRUTH.md @@ -1,7 +1,7 @@ # ZCLAW 系统真相文档 -> **更新日期**: 2026-04-15 -> **数据来源**: V11 全面审计 + 二次审计 + V12 模块化端到端审计 + 代码全量扫描验证 + 功能测试 Phase 1-5 + 发布前功能测试 Phase 3 + 发布前全面测试代码级审计 + 2026-04-11 代码验证 + V13 系统性功能审计 2026-04-12 + V13 审计修复 2026-04-13 + 发布前冲刺 Day1 2026-04-15 +> **更新日期**: 2026-04-16 +> **数据来源**: V11 全面审计 + 二次审计 + V12 模块化端到端审计 + 代码全量扫描验证 + 功能测试 Phase 1-5 + 发布前功能测试 Phase 3 + 发布前全面测试代码级审计 + 2026-04-11 代码验证 + V13 系统性功能审计 2026-04-12 + V13 审计修复 2026-04-13 + 发布前冲刺 Day1 2026-04-15 + 发布前深度测试 8 路并行代码级验证 2026-04-16 > **规则**: 此文档是唯一真相源。所有其他文档如果与此冲突,以此为准。 --- @@ -15,15 +15,15 @@ | Rust 单元测试 | 433 个 (#[test]) + 368 个 (#[tokio::test]) = 801 | `grep '#\[test\]' crates/` + `grep '#\[tokio::test\]'` (2026-04-12 V13 验证) | | Cargo Warnings (非 SaaS) | **0 个** (仅 sqlx-postgres 外部依赖 1 个) | `cargo check --workspace --exclude zclaw-saas` (2026-04-15 清零) | | Rust 测试运行通过 | 684 workspace + 138 SaaS = 822 | Hermes 4 Chunk `cargo test --workspace` 2026-04-09 | -| Tauri 命令 | 183 个 (2026-04-15 Heartbeat 统一健康系统新增 health_snapshot) | `grep '#\[.*tauri::command'` | -| **Tauri 命令有前端调用** | **95 处** | `grep invoke( desktop/src/` (2026-04-15 验证) | +| Tauri 命令 | 190 个 | `grep '#\[.*tauri::command'` (2026-04-16 验证) | +| **Tauri 命令有前端调用** | **104 处** | `grep invoke( desktop/src/` (2026-04-16 验证) | | **Tauri 命令已标注 @reserved** | **89 个** | Rust 源码 @reserved 标注 (2026-04-15 全量标注) | -| **Tauri 命令孤儿 (无调用+无标注)** | **0 个** | 182 - 95 invoke处 - 89 @reserved = 0 (2026-04-15 清零) | +| **Tauri 命令孤儿 (无调用+无标注)** | **~0 个** (190 - 104 invoke - 89 @reserved ≈ -3,差异来自内部命令调用) | (2026-04-16 校准) | | SKILL.md 文件 | 75 个 | `ls skills/*.md \| wc -l` | | Hands 启用 | 9 个 | Browser/Collector/Researcher/Clip/Twitter/Whiteboard/Slideshow/Speech/Quiz(均有 HAND.toml) | | Hands 禁用 | 2 个 | Predictor, Lead(概念定义存在,无 TOML 配置文件或 Rust 实现) | | Pipeline 模板 | 17 个 YAML | `pipelines/` 目录全量统计(含 _templates/ 和 design-shantou/ 子目录) | -| SaaS API 端点 | 136 个 .route() | `grep .route( crates/zclaw-saas/` (2026-04-12 V13 验证) | +| SaaS API 端点 | 137 个 .route() | `grep .route( crates/zclaw-saas/` (2026-04-16 验证) | | SaaS 路由模块 | 12 个 + industry | account/agent_template/auth/billing/knowledge/migration/model_config/prompt/relay/role/scheduled_task/telemetry/industry(scheduled_task: 后端 5 CRUD + Admin V2 前端 service/page/route/nav) | | SaaS 数据表 | 34 个(含 saas_schema_version) | CREATE TABLE 全量统计 | | SaaS Workers | 7 个 | log_operation/cleanup_rate_limit/cleanup_refresh_tokens/record_usage/update_last_used/aggregate_usage/generate_embedding | @@ -37,7 +37,7 @@ | Admin V2 页面 | 15 个 | admin-v2/src/pages/ 全量统计(含 ScheduledTasks、ConfigSync) | | 桌面端设置页面 | 19 个 | SettingsLayout.tsx tabs: 通用/用量统计/积分详情/模型与API/MCP服务/技能/IM频道/工作区/数据与隐私/安全存储/SaaS平台/订阅与计费/语义记忆/安全状态/审计日志/定时任务/心跳配置/提交反馈/关于 | | Admin V2 测试 | 17 个文件 (61 tests) | vitest 统计 | -| 中间件层 | 15 层 | 运行时注册(含 DataMasking@90, ButlerRouter@500, TrajectoryRecorder@650 — V13注册) | +| 中间件层 | 14 层 | `grep chain.register kernel/mod.rs` (2026-04-16 验证: ButlerRouter@80, DataMasking@90, Compaction@100, Memory@150, Title@180, SkillIndex@200, DanglingTool@300, ToolError@350, ToolOutputGuard@360, Guardrail@400, LoopGuard@500, SubagentLimit@550, TrajectoryRecorder@650, TokenCalibration@700) | --- @@ -204,3 +204,4 @@ Viking 5 个孤立 invoke 调用已于 2026-04-03 清理移除: | 2026-04-11 | 发布前数字校准:(1) Rust 代码 66K→74.6K (2) Rust 测试 537→798 (#[test] 431 + #[tokio::test] 367) (3) Tauri 命令 182→184 (4) 前端 invoke 92→105 (5) @reserved 20→33 (6) SaaS .route() 140→122 (7) Zustand Store 18→20 (8) React 组件 135→104 (9) 前端 lib 85→83 (10) Cargo.toml 版本 0.1.0→0.9.0-beta.1 | | 2026-04-12 | V13 系统性功能审计数字校准:(1) Tauri 命令 184→191 (2) 前端 invoke 105→106 (3) @reserved 33→24 (Butler/MCP已接通) (4) 孤儿命令 ~46→~61 (5) Rust 测试 798→801 (433+368) (6) SaaS .route() 122→136 (7) Zustand Store 20→21 (8) dead_code 76→43 (9) Rust LOC crates ~74.6K→~77K | | 2026-04-15 | Heartbeat 统一健康系统:(1) Tauri 命令 182→183 (+health_snapshot) (2) intelligence 模块 15→16 文件 (+health_snapshot.rs +heartbeat.rs 重构) (3) React 组件 104→105 (+HealthPanel.tsx) (4) 前端 lib 85→76 (删除 intelligence-client/ 9 文件) | +| 2026-04-16 | 发布前深度测试 8 路并行验证 + 3 项 P0 修复:(1) Tauri 命令 183→190 (2) 前端 invoke 95→104 (3) SaaS .route() 136→137 (4) 中间件 15→14 (实际 chain.register 计数) (5) P0-01 Admin ApiKeys 创建功能修复 (/keys→/tokens 路由对齐) (6) P0-02 账户锁定 unwrap_or(false)→正确错误传播 (7) P0-03 Logout 增加 access token cookie fallback 撤销 refresh token |