From bd12bdb62b8bfe60cc3c35824325951aa98e4af8 Mon Sep 17 00:00:00 2001 From: iven Date: Wed, 15 Apr 2026 10:02:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(chat):=20=E5=AE=9A=E6=97=B6=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=AE=A1=E8=AE=A1=E4=BF=AE=E5=A4=8D=20=E2=80=94=20?= =?UTF-8?q?=E6=B6=88=E9=99=A4=E9=87=8D=E5=A4=8D=E8=A7=A3=E6=9E=90=20+=20ID?= =?UTF-8?q?=E7=A2=B0=E6=92=9E=20+=20=E8=BE=93=E5=85=A5=E8=A1=A5=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审计发现修复: - H-01: 存储 ParsedSchedule 避免重复 parse_nl_schedule 调用 - H-03: trigger ID 追加 UUID 片段防止高并发碰撞 - C-02: execute_trigger 验证错误信息明确系统 Hand 必须注册 - M-02: SchedulerService 传递 trigger_name 作为 task_description - M-01: 添加拦截路径跳过 post_hook 的设计注释 --- crates/zclaw-kernel/src/scheduler.rs | 8 ++-- crates/zclaw-kernel/src/trigger_manager.rs | 3 +- desktop/src-tauri/src/kernel_commands/chat.rs | 37 ++++++++++--------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/crates/zclaw-kernel/src/scheduler.rs b/crates/zclaw-kernel/src/scheduler.rs index 385afd4..f0ade2f 100644 --- a/crates/zclaw-kernel/src/scheduler.rs +++ b/crates/zclaw-kernel/src/scheduler.rs @@ -77,7 +77,7 @@ impl SchedulerService { kernel_lock: &Arc>>, ) -> Result<()> { // Collect due triggers under lock - let to_execute: Vec<(String, String, String)> = { + let to_execute: Vec<(String, String, String, String)> = { let kernel_guard = kernel_lock.lock().await; let kernel = match kernel_guard.as_ref() { Some(k) => k, @@ -103,7 +103,8 @@ impl SchedulerService { .filter_map(|t| { if let zclaw_hands::TriggerType::Schedule { ref cron } = t.config.trigger_type { if Self::should_fire_cron(cron, &now) { - Some((t.config.id.clone(), t.config.hand_id.clone(), cron.clone())) + // (trigger_id, hand_id, cron_expr, trigger_name) + Some((t.config.id.clone(), t.config.hand_id.clone(), cron.clone(), t.config.name.clone())) } else { None } @@ -123,7 +124,7 @@ impl SchedulerService { // 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 { + for (trigger_id, hand_id, cron_expr, trigger_name) in to_execute { tracing::info!( "[Scheduler] Firing scheduled trigger '{}' → hand '{}' (cron: {})", trigger_id, hand_id, cron_expr @@ -138,6 +139,7 @@ impl SchedulerService { let input = serde_json::json!({ "trigger_id": trigger_id, "trigger_type": "schedule", + "task_description": trigger_name, "cron": cron_expr, "fired_at": now.to_rfc3339(), }); diff --git a/crates/zclaw-kernel/src/trigger_manager.rs b/crates/zclaw-kernel/src/trigger_manager.rs index 8f926fb..d6c1a8e 100644 --- a/crates/zclaw-kernel/src/trigger_manager.rs +++ b/crates/zclaw-kernel/src/trigger_manager.rs @@ -305,9 +305,10 @@ impl TriggerManager { }; // Get hand (outside of our lock to avoid potential deadlock with hand_registry) + // System hands (prefixed with '_') must be registered at boot — same rule as create_trigger. let hand = self.hand_registry.get(&hand_id).await .ok_or_else(|| zclaw_types::ZclawError::InvalidInput( - format!("Hand '{}' not found", hand_id) + format!("Hand '{}' not found (system hands must be registered at boot)", hand_id) ))?; // Update state before execution diff --git a/desktop/src-tauri/src/kernel_commands/chat.rs b/desktop/src-tauri/src/kernel_commands/chat.rs index 09c60dc..9f0e749 100644 --- a/desktop/src-tauri/src/kernel_commands/chat.rs +++ b/desktop/src-tauri/src/kernel_commands/chat.rs @@ -221,7 +221,7 @@ pub async fn agent_chat_stream( // If the user's message contains a schedule intent (e.g. "每天早上9点提醒我查房"), // parse it with NlScheduleParser, create a trigger, and return confirmation // directly without calling the LLM. - let mut schedule_intercepted = false; + let mut captured_parsed: Option = None; if zclaw_runtime::nl_schedule::has_schedule_intent(&message) { let parse_result = zclaw_runtime::nl_schedule::parse_nl_schedule(&message, &id); @@ -233,7 +233,12 @@ pub async fn agent_chat_stream( // Try to create a schedule trigger let kernel_lock = state.lock().await; if let Some(kernel) = kernel_lock.as_ref() { - let trigger_id = format!("sched_{}", chrono::Utc::now().timestamp_millis()); + // Use UUID fragment to avoid collision under high concurrency + let trigger_id = format!( + "sched_{}_{}", + chrono::Utc::now().timestamp_millis(), + &uuid::Uuid::new_v4().to_string()[..8] + ); let trigger_config = zclaw_hands::TriggerConfig { id: trigger_id.clone(), name: parsed.task_description.clone(), @@ -242,6 +247,7 @@ pub async fn agent_chat_stream( cron: parsed.cron_expression.clone(), }, enabled: true, + // 60/hour = once per minute max, reasonable for scheduled tasks max_executions_per_hour: 60, }; @@ -251,11 +257,11 @@ pub async fn agent_chat_stream( "[agent_chat_stream] Schedule trigger created: {} (cron: {})", trigger_id, parsed.cron_expression ); - schedule_intercepted = true; + captured_parsed = Some(parsed.clone()); } Err(e) => { tracing::warn!( - "[agent_chat_stream] Failed to create schedule trigger: {}", + "[agent_chat_stream] Failed to create schedule trigger, falling through to LLM: {}", e ); } @@ -272,20 +278,17 @@ pub async fn agent_chat_stream( } // Get the streaming receiver while holding the lock, then release it - let (mut rx, llm_driver) = if schedule_intercepted { + // NOTE: When schedule_intercepted, llm_driver is None so post_conversation_hook + // (memory extraction, heartbeat, reflection) is intentionally skipped — + // schedule confirmations are system messages, not user conversations. + let (mut rx, llm_driver) = if let Some(parsed) = captured_parsed { // Schedule was intercepted — build confirmation message directly - let confirm_msg = { - let parsed = match zclaw_runtime::nl_schedule::parse_nl_schedule(&message, &id) { - zclaw_runtime::nl_schedule::ScheduleParseResult::Exact(p) => p, - _ => unreachable!("schedule_intercepted is only true for Exact results"), - }; - format!( - "已为您设置定时任务:\n\n- **任务**:{}\n- **时间**:{}\n- **Cron**:`{}`\n\n任务已激活,将在设定时间自动执行。", - parsed.task_description, - parsed.natural_description, - parsed.cron_expression, - ) - }; + let confirm_msg = format!( + "已为您设置定时任务:\n\n- **任务**:{}\n- **时间**:{}\n- **Cron**:`{}`\n\n任务已激活,将在设定时间自动执行。", + parsed.task_description, + parsed.natural_description, + parsed.cron_expression, + ); let (tx, rx) = tokio::sync::mpsc::channel(32); let _ = tx.send(zclaw_runtime::LoopEvent::Delta(confirm_msg)).await;