From a504a40395a9f706c899aa7f12da41b1dc258a2e Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 17 Apr 2026 03:31:06 +0800 Subject: [PATCH] =?UTF-8?q?fix:=207=20=E9=A1=B9=20E2E=20Bug=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20=E2=80=94=20Dashboard=20404=20/=20=E8=AE=B0?= =?UTF-8?q?=E5=BF=86=E5=8E=BB=E9=87=8D=20/=20=E8=AE=B0=E5=BF=86=E6=B3=A8?= =?UTF-8?q?=E5=85=A5=20/=20invoice=5Fid=20/=20Prompt=20=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0: - BUG-H1: Dashboard 路由 /api/v1/stats/dashboard → /api/v1/admin/dashboard P1: - BUG-H2: viking_add 预检查 content_hash 去重,返回 "deduped" 状态;SqliteStorage 启动时回填已有条目 content_hash - BUG-M5: saas-relay-client 发送前调用 viking_inject_prompt 注入跨会话记忆 P2: - BUG-M1: PaymentResult 添加 invoice_id 字段,query_payment_status 返回 invoice_id - BUG-M2: UpdatePromptRequest 添加内容字段,更新时自动创建新版本并递增 current_version - BUG-M3: viking_find scope 参数文档化(设计行为,调用方需传 agent scope) - BUG-M4: Dashboard 路由缺失已修复,handler 层 require_admin 已正确返回 403 P3 (确认已修复/非代码问题): - BUG-L1: pain_seed_categories 已统一,无 pain_seeds 残留 - BUG-L2: pipeline_create 参数格式正确,E2E 测试方法问题 --- crates/zclaw-growth/src/storage/sqlite.rs | 30 ++++++++++++++++ crates/zclaw-saas/src/account/mod.rs | 2 +- crates/zclaw-saas/src/billing/payment.rs | 8 +++-- crates/zclaw-saas/src/billing/types.rs | 1 + crates/zclaw-saas/src/prompt/handlers.rs | 9 +++-- crates/zclaw-saas/src/prompt/service.rs | 15 +++++++- crates/zclaw-saas/src/prompt/types.rs | 6 ++++ desktop/src-tauri/src/viking_commands.rs | 30 ++++++++++++++++ desktop/src/lib/saas-relay-client.ts | 44 +++++++++++++++++++++++ 9 files changed, 138 insertions(+), 7 deletions(-) diff --git a/crates/zclaw-growth/src/storage/sqlite.rs b/crates/zclaw-growth/src/storage/sqlite.rs index 094eb6a..3755b2b 100644 --- a/crates/zclaw-growth/src/storage/sqlite.rs +++ b/crates/zclaw-growth/src/storage/sqlite.rs @@ -179,6 +179,36 @@ impl SqliteStorage { .execute(&self.pool) .await; + // Backfill content_hash for existing entries that have NULL content_hash + { + use std::hash::{Hash, Hasher}; + + let rows: Vec<(String, String)> = sqlx::query_as( + "SELECT uri, content FROM memories WHERE content_hash IS NULL" + ) + .fetch_all(&self.pool) + .await + .unwrap_or_default(); + + if !rows.is_empty() { + for (uri, content) in &rows { + let normalized = content.trim().to_lowercase(); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + normalized.hash(&mut hasher); + let hash = format!("{:016x}", hasher.finish()); + let _ = sqlx::query("UPDATE memories SET content_hash = ? WHERE uri = ?") + .bind(&hash) + .bind(uri) + .execute(&self.pool) + .await; + } + tracing::info!( + "[SqliteStorage] Backfilled content_hash for {} existing entries", + rows.len() + ); + } + } + // Create metadata table sqlx::query( r#" diff --git a/crates/zclaw-saas/src/account/mod.rs b/crates/zclaw-saas/src/account/mod.rs index 8850f92..95e7311 100644 --- a/crates/zclaw-saas/src/account/mod.rs +++ b/crates/zclaw-saas/src/account/mod.rs @@ -16,7 +16,7 @@ pub fn routes() -> axum::Router { .route("/api/v1/tokens", post(handlers::create_token)) .route("/api/v1/tokens/:id", delete(handlers::revoke_token)) .route("/api/v1/logs/operations", get(handlers::list_operation_logs)) - .route("/api/v1/stats/dashboard", get(handlers::dashboard_stats)) + .route("/api/v1/admin/dashboard", get(handlers::dashboard_stats)) .route("/api/v1/devices", get(handlers::list_devices)) .route("/api/v1/devices/register", post(handlers::register_device)) .route("/api/v1/devices/heartbeat", post(handlers::device_heartbeat)) diff --git a/crates/zclaw-saas/src/billing/payment.rs b/crates/zclaw-saas/src/billing/payment.rs index fc3b361..aaae62d 100644 --- a/crates/zclaw-saas/src/billing/payment.rs +++ b/crates/zclaw-saas/src/billing/payment.rs @@ -101,6 +101,7 @@ pub async fn create_payment( Ok(PaymentResult { payment_id, + invoice_id, trade_no, pay_url, amount_cents: plan.price_cents, @@ -272,8 +273,8 @@ pub async fn query_payment_status( payment_id: &str, account_id: &str, ) -> SaasResult { - let payment: (String, String, i32, String, String) = sqlx::query_as::<_, (String, String, i32, String, String)>( - "SELECT id, method, amount_cents, currency, status \ + let payment: (String, String, String, i32, String, String) = sqlx::query_as::<_, (String, String, String, i32, String, String)>( + "SELECT id, invoice_id, method, amount_cents, currency, status \ FROM billing_payments WHERE id = $1 AND account_id = $2" ) .bind(payment_id) @@ -282,9 +283,10 @@ pub async fn query_payment_status( .await? .ok_or_else(|| SaasError::NotFound("支付记录不存在".into()))?; - let (id, method, amount, currency, status) = payment; + let (id, invoice_id, method, amount, currency, status) = payment; Ok(serde_json::json!({ "id": id, + "invoice_id": invoice_id, "method": method, "amount_cents": amount, "currency": currency, diff --git a/crates/zclaw-saas/src/billing/types.rs b/crates/zclaw-saas/src/billing/types.rs index f474cf8..8dcd15a 100644 --- a/crates/zclaw-saas/src/billing/types.rs +++ b/crates/zclaw-saas/src/billing/types.rs @@ -155,6 +155,7 @@ pub struct CreatePaymentRequest { #[derive(Debug, Serialize)] pub struct PaymentResult { pub payment_id: String, + pub invoice_id: String, pub trade_no: String, pub pay_url: String, pub amount_cents: i32, diff --git a/crates/zclaw-saas/src/prompt/handlers.rs b/crates/zclaw-saas/src/prompt/handlers.rs index 053e3a8..754e2f4 100644 --- a/crates/zclaw-saas/src/prompt/handlers.rs +++ b/crates/zclaw-saas/src/prompt/handlers.rs @@ -68,7 +68,7 @@ pub async fn get_prompt( Ok(Json(service::get_template_by_name(&state.db, &name).await?)) } -/// PUT /api/v1/prompts/{name} — 更新模板元数据 +/// PUT /api/v1/prompts/{name} — 更新模板元数据 + 可选自动创建新版本 pub async fn update_prompt( State(state): State, Extension(ctx): Extension, @@ -82,6 +82,11 @@ pub async fn update_prompt( &state.db, &tmpl.id, req.description.as_deref(), req.status.as_deref(), + req.system_prompt.as_deref(), + req.user_prompt_template.as_deref(), + req.variables.clone(), + req.changelog.as_deref(), + req.min_app_version.as_deref(), ).await?; log_operation(&state.db, &ctx.account_id, "prompt.update", "prompt", &tmpl.id, @@ -99,7 +104,7 @@ pub async fn archive_prompt( check_permission(&ctx, "prompt:admin")?; let tmpl = service::get_template_by_name(&state.db, &name).await?; - let result = service::update_template(&state.db, &tmpl.id, None, Some("archived")).await?; + let result = service::update_template(&state.db, &tmpl.id, None, Some("archived"), None, None, None, None, None).await?; log_operation(&state.db, &ctx.account_id, "prompt.archive", "prompt", &tmpl.id, None, ctx.client_ip.as_deref()).await?; diff --git a/crates/zclaw-saas/src/prompt/service.rs b/crates/zclaw-saas/src/prompt/service.rs index 50f2e9b..14f33f4 100644 --- a/crates/zclaw-saas/src/prompt/service.rs +++ b/crates/zclaw-saas/src/prompt/service.rs @@ -108,12 +108,20 @@ pub async fn list_templates( Ok(PaginatedResponse { items, total, page, page_size }) } -/// 更新模板元数据(不修改内容) +/// 更新模板元数据 + 可选自动创建新版本 +/// +/// 当传入 `system_prompt` 时,自动创建新版本并递增 `current_version`。 +/// 仅更新 `description`/`status` 时不会递增版本号。 pub async fn update_template( db: &PgPool, id: &str, description: Option<&str>, status: Option<&str>, + system_prompt: Option<&str>, + user_prompt_template: Option<&str>, + variables: Option, + changelog: Option<&str>, + min_app_version: Option<&str>, ) -> SaasResult { let now = chrono::Utc::now(); @@ -130,6 +138,11 @@ pub async fn update_template( .bind(st).bind(&now).bind(id).execute(db).await?; } + // Auto-create version when content is provided + if let Some(sp) = system_prompt { + create_version(db, id, sp, user_prompt_template, variables, changelog, min_app_version).await?; + } + get_template(db, id).await } diff --git a/crates/zclaw-saas/src/prompt/types.rs b/crates/zclaw-saas/src/prompt/types.rs index 494fd4e..90d486e 100644 --- a/crates/zclaw-saas/src/prompt/types.rs +++ b/crates/zclaw-saas/src/prompt/types.rs @@ -33,6 +33,12 @@ pub struct CreatePromptRequest { pub struct UpdatePromptRequest { pub description: Option, pub status: Option, + /// If provided, auto-creates a new version with this content + pub system_prompt: Option, + pub user_prompt_template: Option, + pub variables: Option, + pub changelog: Option, + pub min_app_version: Option, } // --- Prompt Version --- diff --git a/desktop/src-tauri/src/viking_commands.rs b/desktop/src-tauri/src/viking_commands.rs index c41470e..b59ead8 100644 --- a/desktop/src-tauri/src/viking_commands.rs +++ b/desktop/src-tauri/src/viking_commands.rs @@ -189,6 +189,36 @@ pub async fn viking_add(uri: String, content: String) -> Result = sqlx::query_as( + "SELECT uri FROM memories WHERE content_hash = ? AND uri LIKE ? LIMIT 1" + ) + .bind(&content_hash) + .bind(format!("{}%", scope_prefix)) + .fetch_optional(pool) + .await + .map_err(|e| format!("Dedup check failed: {}", e))?; + + if existing.is_some() { + return Ok(VikingAddResult { + uri, + status: "deduped".to_string(), + }); + } + let entry = MemoryEntry::new(&agent_id, memory_type, &category, content); storage diff --git a/desktop/src/lib/saas-relay-client.ts b/desktop/src/lib/saas-relay-client.ts index d338c32..0e9e8db 100644 --- a/desktop/src/lib/saas-relay-client.ts +++ b/desktop/src/lib/saas-relay-client.ts @@ -16,6 +16,39 @@ import { createLogger } from './logger'; const log = createLogger('SaaSRelayGateway'); +// --------------------------------------------------------------------------- +// Memory injection helper — injects relevant memories into system prompt +// before sending to SaaS relay (mirrors MemoryMiddleware in Tauri kernel path) +// --------------------------------------------------------------------------- + +/** + * Attempt to inject relevant memories into the system prompt via Tauri IPC. + * Falls back gracefully in non-Tauri contexts (browser mode). + */ +async function injectMemories( + agentId: string | undefined, + basePrompt: string, + userInput: string, +): Promise { + try { + // Dynamic import — only available in Tauri context + const { invoke } = await import('@tauri-apps/api/core'); + const enhanced = await invoke('viking_inject_prompt', { + agentId: agentId ?? 'default', + basePrompt, + userInput, + maxTokens: 500, + }); + if (enhanced && enhanced !== basePrompt) { + log.debug('Memory injection succeeded for relay request'); + return enhanced; + } + } catch { + // Non-Tauri context or viking not initialized — skip silently + } + return basePrompt; +} + // --------------------------------------------------------------------------- // Frontend DataMasking — mirrors Rust DataMasking middleware for SaaS Relay // --------------------------------------------------------------------------- @@ -180,6 +213,17 @@ export function createSaaSRelayGatewayClient( ? [...history, { role: 'user' as const, content: maskedMessage }] : [{ role: 'user' as const, content: maskedMessage }]; + // BUG-M5 fix: Inject relevant memories into system prompt via Tauri IPC. + // This mirrors the MemoryMiddleware that runs in the kernel path. + const enhancedSystemPrompt = await injectMemories( + opts?.agentId, + '', + message, + ); + if (enhancedSystemPrompt) { + messages.unshift({ role: 'system', content: enhancedSystemPrompt }); + } + const model = getModel(); if (!model) { callbacks.onError('No model available — please check SaaS relay configuration');