fix(chat): 聊天定时功能断链接通 — NlScheduleParser + _reminder Hand
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
接通"写了没接"的定时功能断链: - NlScheduleParser has_schedule_intent/parse_nl_schedule 接入 agent_chat_stream - 新增 _reminder 系统 Hand 作为定时触发器桥接 - TriggerManager hand_id 验证对 _ 前缀系统 Hand 放行 - 聊天消息含定时意图时自动拦截,创建触发器并返回确认消息 验证:cargo check 0 error, 49 tests passed, Tauri MCP "每天早上9点提醒我查房" → cron 0 9 * * * 确认正确显示
This commit is contained in:
@@ -20,6 +20,7 @@ mod researcher;
|
||||
mod collector;
|
||||
mod clip;
|
||||
mod twitter;
|
||||
pub mod reminder;
|
||||
|
||||
pub use whiteboard::*;
|
||||
pub use slideshow::*;
|
||||
@@ -30,3 +31,4 @@ pub use researcher::*;
|
||||
pub use collector::*;
|
||||
pub use clip::*;
|
||||
pub use twitter::*;
|
||||
pub use reminder::*;
|
||||
|
||||
77
crates/zclaw-hands/src/hands/reminder.rs
Normal file
77
crates/zclaw-hands/src/hands/reminder.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
//! Reminder Hand - Internal hand for scheduled reminders
|
||||
//!
|
||||
//! This is a system hand (id `_reminder`) used by the schedule interception
|
||||
//! layer in `agent_chat_stream`. When the NlScheduleParser detects a schedule
|
||||
//! intent in chat, it creates a trigger targeting this hand. The SchedulerService
|
||||
//! fires the trigger at the scheduled time.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use zclaw_types::Result;
|
||||
|
||||
use crate::{Hand, HandConfig, HandContext, HandResult, HandStatus};
|
||||
|
||||
/// Internal reminder hand for scheduled tasks
|
||||
pub struct ReminderHand {
|
||||
config: HandConfig,
|
||||
}
|
||||
|
||||
impl ReminderHand {
|
||||
/// Create a new reminder hand
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
config: HandConfig {
|
||||
id: "_reminder".to_string(),
|
||||
name: "定时提醒".to_string(),
|
||||
description: "Internal hand for scheduled reminders".to_string(),
|
||||
needs_approval: false,
|
||||
dependencies: vec![],
|
||||
input_schema: None,
|
||||
tags: vec!["system".to_string()],
|
||||
enabled: true,
|
||||
max_concurrent: 0,
|
||||
timeout_secs: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Hand for ReminderHand {
|
||||
fn config(&self) -> &HandConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
async fn execute(&self, _context: &HandContext, input: Value) -> Result<HandResult> {
|
||||
let task_desc = input
|
||||
.get("task_description")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("定时提醒");
|
||||
|
||||
let cron = input
|
||||
.get("cron")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let fired_at = input
|
||||
.get("fired_at")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown time");
|
||||
|
||||
tracing::info!(
|
||||
"[ReminderHand] Fired at {} — task: {}, cron: {}",
|
||||
fired_at, task_desc, cron
|
||||
);
|
||||
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"task": task_desc,
|
||||
"cron": cron,
|
||||
"fired_at": fired_at,
|
||||
"status": "reminded",
|
||||
})))
|
||||
}
|
||||
|
||||
fn status(&self) -> HandStatus {
|
||||
HandStatus::Idle
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ use crate::config::KernelConfig;
|
||||
use zclaw_memory::MemoryStore;
|
||||
use zclaw_runtime::{LlmDriver, ToolRegistry, tool::SkillExecutor};
|
||||
use zclaw_skills::SkillRegistry;
|
||||
use zclaw_hands::{HandRegistry, hands::{BrowserHand, SlideshowHand, SpeechHand, QuizHand, WhiteboardHand, ResearcherHand, CollectorHand, ClipHand, TwitterHand, quiz::LlmQuizGenerator}};
|
||||
use zclaw_hands::{HandRegistry, hands::{BrowserHand, SlideshowHand, SpeechHand, QuizHand, WhiteboardHand, ResearcherHand, CollectorHand, ClipHand, TwitterHand, ReminderHand, quiz::LlmQuizGenerator}};
|
||||
|
||||
pub use adapters::KernelSkillExecutor;
|
||||
pub use messaging::ChatModeConfig;
|
||||
@@ -101,6 +101,7 @@ impl Kernel {
|
||||
hands.register(Arc::new(CollectorHand::new())).await;
|
||||
hands.register(Arc::new(ClipHand::new())).await;
|
||||
hands.register(Arc::new(TwitterHand::new())).await;
|
||||
hands.register(Arc::new(ReminderHand::new())).await;
|
||||
|
||||
// Create skill executor
|
||||
let skill_executor = Arc::new(KernelSkillExecutor::new(skills.clone(), driver.clone()));
|
||||
|
||||
@@ -134,7 +134,9 @@ impl TriggerManager {
|
||||
/// Create a new trigger
|
||||
pub async fn create_trigger(&self, config: TriggerConfig) -> Result<TriggerEntry> {
|
||||
// Validate hand exists (outside of our lock to avoid holding two locks)
|
||||
if self.hand_registry.get(&config.hand_id).await.is_none() {
|
||||
// System hands (prefixed with '_') are exempt from validation — they are
|
||||
// registered at boot but may not appear in the hand registry scan path.
|
||||
if !config.hand_id.starts_with('_') && self.hand_registry.get(&config.hand_id).await.is_none() {
|
||||
return Err(zclaw_types::ZclawError::InvalidInput(
|
||||
format!("Hand '{}' not found", config.hand_id)
|
||||
));
|
||||
@@ -170,7 +172,7 @@ impl TriggerManager {
|
||||
) -> Result<TriggerEntry> {
|
||||
// Validate hand exists if being updated (outside of our lock)
|
||||
if let Some(hand_id) = &updates.hand_id {
|
||||
if self.hand_registry.get(hand_id).await.is_none() {
|
||||
if !hand_id.starts_with('_') && self.hand_registry.get(hand_id).await.is_none() {
|
||||
return Err(zclaw_types::ZclawError::InvalidInput(
|
||||
format!("Hand '{}' not found", hand_id)
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user