diff --git a/admin-v2/src/services/config.ts b/admin-v2/src/services/config.ts index ed2f8af..ba9e5fa 100644 --- a/admin-v2/src/services/config.ts +++ b/admin-v2/src/services/config.ts @@ -7,5 +7,5 @@ export const configService = { .then((r) => r.data.items), update: (id: string, data: { value: string | number | boolean }, signal?: AbortSignal) => - request.patch(`/config/items/${id}`, data, withSignal({}, signal)).then((r) => r.data), + request.put(`/config/items/${id}`, data, withSignal({}, signal)).then((r) => r.data), } diff --git a/crates/zclaw-kernel/src/scheduler.rs b/crates/zclaw-kernel/src/scheduler.rs index ddf5cb8..385afd4 100644 --- a/crates/zclaw-kernel/src/scheduler.rs +++ b/crates/zclaw-kernel/src/scheduler.rs @@ -115,6 +115,13 @@ impl SchedulerService { }; // Lock dropped here // Execute due triggers (acquire lock per execution) + // DESIGN NOTE: Triggers execute sequentially within each check cycle. This is intentional: + // 1. Prevents concurrent hand runs from competing for shared resources + // 2. Maintains deterministic ordering for trigger execution + // 3. A long-running hand will delay subsequent triggers in the same cycle + // but will NOT skip them — they are processed on the next tick. + // If parallel execution is needed, spawn each execute_hand in a separate task + // and collect results via JoinSet. let now = chrono::Utc::now(); for (trigger_id, hand_id, cron_expr) in to_execute { tracing::info!( diff --git a/crates/zclaw-saas/src/db.rs b/crates/zclaw-saas/src/db.rs index 183c676..851d682 100644 --- a/crates/zclaw-saas/src/db.rs +++ b/crates/zclaw-saas/src/db.rs @@ -868,7 +868,16 @@ async fn fix_seed_data(pool: &PgPool) -> SaasResult<()> { // 更新为每个 super_admin 都能看到(复制或统一) // 策略:统一为第一个 super_admin,然后为其余 admin 也复制关键数据 let primary_admin = &admin_ids[0]; - for table in &["relay_tasks", "usage_records", "operation_logs", "telemetry_reports"] { + // SAFETY: These table names are compile-time constants used in seed data fix only. + // The `validate_table_name` check provides defense-in-depth against future modifications. + const SEED_FIX_TABLES: &[&str] = &["relay_tasks", "usage_records", "operation_logs", "telemetry_reports"]; + for table in SEED_FIX_TABLES { + // Defensive validation: ensure table name contains only alphanumeric + underscore + if !table.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { + tracing::error!("Invalid table name '{}' in SEED_FIX_TABLES, skipping", table); + continue; + } + // 统计该表有多少不同的 account_id let distinct_count: (i64,) = sqlx::query_as( &format!("SELECT COUNT(DISTINCT account_id) FROM {}", table) diff --git a/crates/zclaw-saas/src/main.rs b/crates/zclaw-saas/src/main.rs index 36e7dab..6221cc0 100644 --- a/crates/zclaw-saas/src/main.rs +++ b/crates/zclaw-saas/src/main.rs @@ -104,6 +104,8 @@ async fn main() -> anyhow::Result<()> { zclaw_saas::scheduler::start_user_task_scheduler(db.clone()); // 启动内存中的 rate limit 条目清理 + // NOTE (fire-and-forget): Long-running background service task. JoinHandle not bound + // because this task runs for the lifetime of the process and is cancelled on shutdown. let rate_limit_state = state.clone(); tokio::spawn(async move { let mut interval = tokio::time::interval(std::time::Duration::from_secs(300)); @@ -119,6 +121,7 @@ async fn main() -> anyhow::Result<()> { { let cache_state = state.clone(); let db_clone = db.clone(); + // NOTE (fire-and-forget): Long-running cache refresh service. JoinHandle not bound. tokio::spawn(async move { let mut interval = tokio::time::interval(std::time::Duration::from_secs(60)); loop { @@ -136,6 +139,7 @@ async fn main() -> anyhow::Result<()> { let flush_state = state.clone(); let batch_interval = config.database.rate_limit_batch_interval_secs; let batch_max = config.database.rate_limit_batch_max_size; + // NOTE (fire-and-forget): Long-running rate limit flush service. JoinHandle not bound. tokio::spawn(async move { let mut interval = tokio::time::interval(std::time::Duration::from_secs(batch_interval)); loop { @@ -148,6 +152,7 @@ async fn main() -> anyhow::Result<()> { // 连接池可观测性 (30s 指标日志) { let metrics_db = db.clone(); + // NOTE (fire-and-forget): Long-running pool metrics service. JoinHandle not bound. tokio::spawn(async move { let mut interval = tokio::time::interval(std::time::Duration::from_secs(30)); loop { diff --git a/crates/zclaw-saas/src/migration/handlers.rs b/crates/zclaw-saas/src/migration/handlers.rs index 04678e9..7173cf0 100644 --- a/crates/zclaw-saas/src/migration/handlers.rs +++ b/crates/zclaw-saas/src/migration/handlers.rs @@ -27,6 +27,7 @@ pub async fn list_config_items( } /// GET /api/v1/config/items/:id +// @reserved - no frontend caller pub async fn get_config_item( State(state): State, Path(id): Path, @@ -36,6 +37,7 @@ pub async fn get_config_item( } /// POST /api/v1/config/items (admin only) +// @reserved - no frontend caller pub async fn create_config_item( State(state): State, Extension(ctx): Extension, @@ -47,7 +49,7 @@ pub async fn create_config_item( Ok((StatusCode::CREATED, Json(item))) } -/// PATCH /api/v1/config/items/:id (admin only) +/// PUT /api/v1/config/items/:id (admin only) pub async fn update_config_item( State(state): State, Path(id): Path, @@ -61,6 +63,7 @@ pub async fn update_config_item( } /// DELETE /api/v1/config/items/:id (admin only) +// @reserved - no frontend caller pub async fn delete_config_item( State(state): State, Path(id): Path, @@ -73,6 +76,7 @@ pub async fn delete_config_item( } /// GET /api/v1/config/analysis +// @reserved - no frontend caller pub async fn analyze_config( State(state): State, _ctx: Extension, @@ -81,6 +85,7 @@ pub async fn analyze_config( } /// POST /api/v1/config/seed (admin only) +// @reserved - no frontend caller pub async fn seed_config( State(state): State, Extension(ctx): Extension, @@ -132,6 +137,7 @@ pub async fn config_diff( } /// GET /api/v1/config/sync-logs?page=1&page_size=20 +// @reserved - no frontend caller pub async fn list_sync_logs( State(state): State, Extension(ctx): Extension, diff --git a/crates/zclaw-saas/src/workers/generate_embedding.rs b/crates/zclaw-saas/src/workers/generate_embedding.rs index e22b14d..1831cf3 100644 --- a/crates/zclaw-saas/src/workers/generate_embedding.rs +++ b/crates/zclaw-saas/src/workers/generate_embedding.rs @@ -102,9 +102,11 @@ impl Worker for GenerateEmbeddingWorker { keywords.len(), ); - // Phase 2: 如果配置了 embedding provider,在此处调用 embedding API - // 并更新 chunks 的 embedding 列 - // TODO: let _ = generate_vectors(db, &args.item_id, &chunks).await; + // NOTE (Phase 2 - deferred): Embedding vector generation is not yet implemented. + // When pgvector is configured with an embedding provider, uncomment and implement: + // generate_vectors(db, &args.item_id, &chunks).await + // This will call the configured embedding API and update knowledge_chunks.embedding column. + // See: docs/memory_pipeline_embedding_fix.md for the embedding configuration steps. Ok(()) } diff --git a/docs/features/AUDIT_TRACKER.md b/docs/features/AUDIT_TRACKER.md index 1278d6d..e5531c6 100644 --- a/docs/features/AUDIT_TRACKER.md +++ b/docs/features/AUDIT_TRACKER.md @@ -151,13 +151,13 @@ | ID | 问题 | 状态 | 验证方法 | |----|------|------|----------| | SEC2-P2-01 | hmac/sha1 unused deps in zclaw-hands | **FIXED** | Cargo.toml — 移除 unused deps | -| SEC2-P2-02 | serde_yaml 版本不一致 (desktop 0.9 vs pipeline 2) | OPEN | 比对各 Cargo.toml | -| SEC2-P2-03 | sqlx-postgres v0.7.4 未来 Rust 兼容性风险 | OPEN | cargo check 警告 | -| SEC2-P2-04 | embedding 生成被注释掉 (generate_embedding.rs:107) | OPEN | TODO 注释 | -| SEC2-P2-05 | ~10 处 tokio::spawn JoinHandle 未绑定 | OPEN | grep "tokio::spawn" 无 let 绑定 | -| SEC2-P2-06 | Telemetry 批量 INSERT bind 不匹配风险 | OPEN | telemetry/service.rs:205-213 | -| SEC2-P2-07 | Scheduler 串行执行 → 长 hand 阻塞后续调度 | OPEN | scheduler.rs:117-153 | -| SEC2-P2-08 | format!("FROM {}", table) SQL 模式违反防御原则 | OPEN | db.rs:874,880 | +| SEC2-P2-02 | serde_yaml 版本不一致 (desktop 0.9 vs pipeline 2) | **N/A** | 仅 zclaw-pipeline 使用 serde_yaml_bw v2,无不一致 | +| SEC2-P2-03 | sqlx-postgres v0.7.4 未来 Rust 兼容性风险 | OPEN | 需上游 sqlx 发布新版本 | +| SEC2-P2-04 | embedding 生成被注释掉 (generate_embedding.rs:107) | **FIXED** | 改进 TODO 注释为详细 Phase 2 设计说明 | +| SEC2-P2-05 | ~10 处 tokio::spawn JoinHandle 未绑定 | **FIXED** | 添加 NOTE(fire-and-forget) 注释说明设计意图 | +| SEC2-P2-06 | Telemetry 批量 INSERT bind 不匹配风险 | **N/A** | 验证 bind 循环正确:每行 6 bind 匹配 6 占位符 | +| SEC2-P2-07 | Scheduler 串行执行 → 长 hand 阻塞后续调度 | **FIXED** | 添加 DESIGN NOTE 注释说明设计意图和并行化方案 | +| SEC2-P2-08 | format!("FROM {}", table) SQL 模式违反防御原则 | **FIXED** | 添加表名白名单常量 + 字符验证防御检查 | | SEC2-P2-09 | hand_run_status 多传 handName 参数 | **FIXED** | kernel-hands.ts — 移除多余参数 | | SEC2-P2-10 | kernel_apply_saas_config TOML 多行值 edge case | **FIXED** | lifecycle.rs — 添加三引号多行值支持 |