From bf728c34f3be527aeffd4d978f0b849ea28a4db1 Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 9 Apr 2026 22:23:05 +0800 Subject: [PATCH] fix: saasStore require() bug + health check pool formula + DEV error details - saasStore.ts: replace require('./chat/conversationStore') with await import() to fix ReferenceError in Vite ESM environment (P1) - main.rs: fix health check pool usage formula from max_connections - num_idle to pool.size() - num_idle, preventing false "degraded" status (P1) - error.rs: show detailed error messages in ZCLAW_SAAS_DEV=true mode - Update bug tracker with BUG-003 through BUG-007 --- crates/zclaw-saas/src/error.rs | 8 +++-- crates/zclaw-saas/src/main.rs | 8 +++-- desktop/src/store/saasStore.ts | 2 +- .../2026-04-09-exploratory/bug-tracker.md | 34 +++++++++++++++++++ 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/crates/zclaw-saas/src/error.rs b/crates/zclaw-saas/src/error.rs index 6d3d876..a2c3217 100644 --- a/crates/zclaw-saas/src/error.rs +++ b/crates/zclaw-saas/src/error.rs @@ -127,11 +127,15 @@ impl IntoResponse for SaasError { fn into_response(self) -> Response { let status = self.status_code(); let (error_code, message) = match &self { - // 500 错误不泄露内部细节给客户端 + // 500 错误不泄露内部细节给客户端 (开发模式除外) Self::Database(_) | Self::Internal(_) | Self::Io(_) | Self::Jwt(_) | Self::Config(_) => { tracing::error!("内部错误 [{}]: {}", self.error_code(), self); - (self.error_code().to_string(), "服务内部错误".to_string()) + if std::env::var("ZCLAW_SAAS_DEV").as_deref() == Ok("true") { + (self.error_code().to_string(), format!("[DEV] {}", self)) + } else { + (self.error_code().to_string(), "服务内部错误".to_string()) + } } _ => (self.error_code().to_string(), self.to_string()), }; diff --git a/crates/zclaw-saas/src/main.rs b/crates/zclaw-saas/src/main.rs index e0be71b..f668016 100644 --- a/crates/zclaw-saas/src/main.rs +++ b/crates/zclaw-saas/src/main.rs @@ -160,8 +160,9 @@ async fn main() -> anyhow::Result<()> { interval.tick().await; let pool = &metrics_db; let total = pool.options().get_max_connections() as usize; + let size = pool.size() as usize; let idle = pool.num_idle() as usize; - let used = total.saturating_sub(idle); + let used = size.saturating_sub(idle); let usage_pct = if total > 0 { used * 100 / total } else { 0 }; tracing::info!( "[PoolMetrics] total={} idle={} used={} usage_pct={}%", @@ -248,9 +249,10 @@ async fn health_handler( let pool = &state.db; let total = pool.options().get_max_connections() as usize; if total > 0 { + let size = pool.size() as usize; let idle = pool.num_idle() as usize; - let used = total - idle; - let ratio = used * 100 / total; + let used = size.saturating_sub(idle); + let ratio = if size > 0 { used * 100 / total } else { 0 }; if ratio >= 80 { return ( axum::http::StatusCode::SERVICE_UNAVAILABLE, diff --git a/desktop/src/store/saasStore.ts b/desktop/src/store/saasStore.ts index 71cd1bb..dedbc30 100644 --- a/desktop/src/store/saasStore.ts +++ b/desktop/src/store/saasStore.ts @@ -478,7 +478,7 @@ export const useSaaSStore = create((set, get) => { // switch to the first available model to prevent 404 errors if (models.length > 0) { try { - const { useConversationStore } = require('./chat/conversationStore'); + const { useConversationStore } = await import('./chat/conversationStore'); const current = useConversationStore.getState().currentModel; const modelIds = models.map(m => m.alias || m.id); if (current && !modelIds.includes(current)) { diff --git a/docs/test-results/2026-04-09-exploratory/bug-tracker.md b/docs/test-results/2026-04-09-exploratory/bug-tracker.md index ec2d3ff..cce6ece 100644 --- a/docs/test-results/2026-04-09-exploratory/bug-tracker.md +++ b/docs/test-results/2026-04-09-exploratory/bug-tracker.md @@ -5,3 +5,37 @@ | Bug ID | 场景 | 严重度 | 标题 | 状态 | 修复提交 | |--------|------|--------|------|------|----------| +| BUG-001 | Relay 全场景 | P0 | SaaS Relay DATABASE_ERROR: SUM(token_count) 返回 NUMERIC 而非 bigint | FIXED | bd6cf8e | +| BUG-002 | 场景1.2 | P2 | 旧对话错误消息缓存: 修复后刷新页面旧错误仍显示,需点"重试"或新建对话 | WONTFIX | — | +| BUG-003 | 启动 | P1 | saasStore.ts fetchAvailableModels 使用 require() 导致模型同步失败 | FIXED | require → await import | +| BUG-004 | Health | P1 | Health check 连接池使用率公式错误 (max-idle 而非 size-idle) | FIXED | pool.size() 替代 max_connections | +| BUG-005 | 启动 | P2 | OfflineStore 无模型配置时重连循环过于频繁 | KNOWN | 首次启动预期行为 | +| BUG-006 | 启动 | P2 | WebMCP 注册失败 TypeError: Required member is undefined | KNOWN | 需 Chrome 146+ flag | +| BUG-007 | Admin | P2 | Admin V2 authStore 测试 19 个失败 (113 passed) | OPEN | 测试代码与实现不同步 | + +## BUG-001 详细 + +**根因**: PostgreSQL `SUM(bigint)` 返回 `NUMERIC` 类型,但 sqlx Rust 绑定期望 `i64` (INT8)。`key_pool.rs` 的 `select_best_key()` 查询中 `COALESCE(SUM(uw.token_count), 0)` 缺少 `::bigint` 转换。 + +**影响**: 所有 SaaS Relay 聊天请求 100% 失败,返回 `500 DATABASE_ERROR`。 + +**修复**: 4 处 SUM() 添加 `::bigint` 转换: +- `relay/key_pool.rs` — 根因 +- `relay/service.rs` — sort_candidates_by_quota +- `account/handlers.rs` — dashboard stats +- `workers/aggregate_usage.rs` — usage aggregation + +## BUG-002 详细 + +**现象**: 修复 BUG-001 后刷新页面,旧对话仍显示 "Relay error: 500 DATABASE_ERROR"。需手动点"重试"或新建对话才能恢复正常。 + +**原因**: 前端 IndexedDB 缓存了历史错误消息,刷新不会自动清除。 + +**决定**: WONTFIX — 这是预期行为(保留历史消息真实性),用户可通过"重试"恢复。 + +## 测试通过场景 + +| 场景 | 结果 | 备注 | +|------|------|------| +| 1.2 首次对话 (医务科) | PASS | Relay 完整链路: Desktop→SaaS→Key Pool→kimi-for-coding | +| 离线队列恢复 | PASS | "已恢复连接, 发送中 4 条" 自动重发 |