diff --git a/README.md b/README.md index 20bb661..c25ed5c 100644 --- a/README.md +++ b/README.md @@ -466,6 +466,33 @@ skills 配置示例: } ``` +tools 配置示例: + +```json +{ + "tools": { + "disabled": ["bash", "http_request", "web_fetch"] + } +} +``` + +可用工具名称: +- calculator - 数学计算器 +- get_time - 获取当前时间 +- file_read - 读取文件 +- file_write - 写入文件 +- file_edit - 编辑文件 +- memory_search - 搜索长期记忆 +- memory_manage - 管理长期记忆 +- session_send - 发送会话消息 +- scheduler_manage - 管理定时任务 +- skill_activate - 激活技能 +- skill_list - 列出技能 +- skill_manage - 管理技能 +- bash - 执行 shell 命令 +- http_request - HTTP 请求 +- web_fetch - 网页抓取 + ## 8. 工具机制 PicoBot 的 Agent 是围绕工具调用构建的。当前默认注册的工具包括: @@ -724,6 +751,7 @@ CLI 中已实现的交互命令包括: - scheduler:调度器开关、worker 队列容量、误触发策略和任务列表 - channels:飞书等通道配置 - skills:技能来源与索引限制 +- tools:工具启用/禁用配置(通过 disabled 列表指定禁用的工具) - time.timezone:时区,默认应使用 IANA 时区名,例如 Asia/Shanghai ## 12. 快速开始 diff --git a/src/config/mod.rs b/src/config/mod.rs index aa0f992..d2ecf7e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -25,6 +25,8 @@ pub struct Config { pub channels: HashMap, #[serde(default)] pub skills: SkillsConfig, + #[serde(default)] + pub tools: ToolsConfig, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -98,6 +100,25 @@ impl Default for SkillsConfig { } } +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ToolsConfig { + #[serde(default)] + pub disabled: Vec, +} + +impl Default for ToolsConfig { + fn default() -> Self { + Self { disabled: Vec::new() } + } +} + +impl ToolsConfig { + /// Check if a tool is disabled + pub fn is_disabled(&self, tool_name: &str) -> bool { + self.disabled.iter().any(|name| name == tool_name) + } +} + #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(untagged)] pub enum ChannelConfig { diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 2e61079..951251f 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -78,6 +78,7 @@ impl GatewayState { provider_configs, skills, Arc::new(BusSessionMessageSender::new(bus.clone())), + std::collections::HashSet::new(), )?; Ok(Self { diff --git a/src/gateway/runtime.rs b/src/gateway/runtime.rs index b3596d3..6bae7c4 100644 --- a/src/gateway/runtime.rs +++ b/src/gateway/runtime.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use crate::agent::AgentError; use crate::config::LLMProviderConfig; +use crate::gateway::tool_registry_factory::ToolRegistryFactory; use crate::skills::SkillRuntime; use crate::storage::{ ConversationRepository, MemoryRepository, PromptInjectionRepository, SchedulerJobRepository, @@ -20,7 +21,6 @@ use super::session::{SessionManager, SessionManagerServices}; use super::session_factory::SessionFactory; use super::session_lifecycle::SessionLifecycleService; use super::session_message_service::SessionMessageService; -use super::tool_registry_factory::ToolRegistryFactory; pub(crate) fn build_session_manager( session_ttl_hours: u64, @@ -30,6 +30,7 @@ pub(crate) fn build_session_manager( provider_config: LLMProviderConfig, provider_configs: HashMap, skills: Arc, + disabled_tools: HashSet, ) -> Result { build_session_manager_with_sender( session_ttl_hours, @@ -40,6 +41,7 @@ pub(crate) fn build_session_manager( provider_configs, skills, Arc::new(NoopSessionMessageSender), + disabled_tools, ) } @@ -52,6 +54,7 @@ pub(crate) fn build_session_manager_with_sender( provider_configs: HashMap, skills: Arc, session_message_sender: Arc, + disabled_tools: HashSet, ) -> Result { let store = Arc::new( SessionStore::new() @@ -78,6 +81,7 @@ pub(crate) fn build_session_manager_with_sender( session_message_sender, known_agents, default_timezone, + disabled_tools, ) .build(), ); diff --git a/src/gateway/session.rs b/src/gateway/session.rs index 12663d0..ec2d558 100644 --- a/src/gateway/session.rs +++ b/src/gateway/session.rs @@ -378,6 +378,7 @@ impl SessionManager { provider_config: LLMProviderConfig, provider_configs: HashMap, skills: Arc, + disabled_tools: std::collections::HashSet, ) -> Result { super::runtime::build_session_manager( session_ttl_hours, @@ -387,6 +388,7 @@ impl SessionManager { provider_config, provider_configs, skills, + disabled_tools, ) } @@ -536,8 +538,9 @@ mod tests { store.clone(), store.clone(), Arc::new(NoopSessionMessageSender), - HashSet::new(), + HashSet::new(), "Asia/Shanghai".to_string(), + HashSet::new(), ) .build(), ); @@ -581,8 +584,9 @@ mod tests { store.clone(), store.clone(), Arc::new(NoopSessionMessageSender), - HashSet::new(), + HashSet::new(), "Asia/Shanghai".to_string(), + HashSet::new(), ) .build(), ); @@ -787,6 +791,7 @@ mod tests { provider_config.clone(), HashMap::from([("default".to_string(), provider_config)]), Arc::new(SkillRuntime::default()), + HashSet::new(), ) .unwrap(); @@ -835,6 +840,7 @@ mod tests { ("planner".to_string(), planner_provider), ]), Arc::new(SkillRuntime::default()), + HashSet::new(), ) .unwrap(); @@ -899,6 +905,7 @@ mod tests { provider_config.clone(), HashMap::from([("default".to_string(), provider_config)]), Arc::new(SkillRuntime::default()), + HashSet::new(), ) .unwrap(); @@ -980,6 +987,7 @@ mod tests { provider_config.clone(), HashMap::from([("default".to_string(), provider_config)]), Arc::new(SkillRuntime::default()), + HashSet::new(), ) .unwrap(); @@ -1066,6 +1074,7 @@ mod tests { provider_config.clone(), HashMap::from([("default".to_string(), provider_config)]), Arc::new(SkillRuntime::default()), + HashSet::new(), ) .unwrap(); @@ -1142,6 +1151,7 @@ mod tests { provider_config.clone(), HashMap::from([("default".to_string(), provider_config)]), Arc::new(SkillRuntime::default()), + HashSet::new(), ) .unwrap(); @@ -1205,6 +1215,7 @@ mod tests { provider_config.clone(), HashMap::from([("default".to_string(), provider_config)]), Arc::new(SkillRuntime::default()), + HashSet::new(), ) .unwrap(); @@ -1278,6 +1289,7 @@ mod tests { provider_config.clone(), HashMap::from([("default".to_string(), provider_config)]), Arc::new(SkillRuntime::default()), + HashSet::new(), ) .unwrap(); @@ -1335,6 +1347,7 @@ mod tests { provider_config.clone(), HashMap::from([("default".to_string(), provider_config)]), Arc::new(SkillRuntime::default()), + HashSet::new(), ) .unwrap(); @@ -1508,8 +1521,9 @@ mod tests { store.clone(), store.clone(), Arc::new(NoopSessionMessageSender), - HashSet::new(), + HashSet::new(), "Asia/Shanghai".to_string(), + HashSet::new(), ) .build(), ); @@ -1546,8 +1560,9 @@ mod tests { store.clone(), store.clone(), Arc::new(NoopSessionMessageSender), - HashSet::new(), + HashSet::new(), "Asia/Shanghai".to_string(), + HashSet::new(), ) .build(), ); @@ -1612,8 +1627,9 @@ mod tests { store.clone(), store.clone(), Arc::new(NoopSessionMessageSender), - HashSet::new(), + HashSet::new(), "Asia/Shanghai".to_string(), + HashSet::new(), ) .build(), ); @@ -1662,6 +1678,7 @@ mod tests { Arc::new(NoopSessionMessageSender), HashSet::new(), "Asia/Shanghai".to_string(), + HashSet::new(), ) .build(); diff --git a/src/gateway/tool_registry_factory.rs b/src/gateway/tool_registry_factory.rs index 61b3c65..8bbb047 100644 --- a/src/gateway/tool_registry_factory.rs +++ b/src/gateway/tool_registry_factory.rs @@ -18,6 +18,7 @@ pub(crate) struct ToolRegistryFactory { session_message_sender: Arc, known_agents: HashSet, default_timezone: String, + disabled_tools: HashSet, } impl ToolRegistryFactory { @@ -29,6 +30,7 @@ impl ToolRegistryFactory { session_message_sender: Arc, known_agents: HashSet, default_timezone: String, + disabled_tools: HashSet, ) -> Self { Self { skills, @@ -38,37 +40,74 @@ impl ToolRegistryFactory { session_message_sender, known_agents, default_timezone, + disabled_tools, } } + fn is_enabled(&self, tool_name: &str) -> bool { + !self.disabled_tools.contains(tool_name) + } + pub(crate) fn build(&self) -> ToolRegistry { let mut registry = ToolRegistry::new(); - registry.register(CalculatorTool::new()); - registry.register(TimeTool::new(self.default_timezone.clone())); - registry.register(FileReadTool::new()); - registry.register(FileWriteTool::new()); - registry.register(FileEditTool::new()); - registry.register(MemorySearchTool::new(self.memories.clone())); - registry.register(MemoryManageTool::new(self.memories.clone())); - registry.register(SessionSendTool::new(self.session_message_sender.clone())); - registry.register(SchedulerManageTool::new( - self.scheduler_jobs.clone(), - self.known_agents.clone(), - )); - registry.register(SkillActivateTool::new( - self.skills.clone(), - self.skill_events.clone(), - )); - registry.register(SkillListTool::new(self.skills.clone())); - registry.register(SkillManageTool::new(self.skills.clone())); - registry.register(BashTool::new()); - registry.register(HttpRequestTool::new( - vec!["*".to_string()], - 1_000_000, - 30, - false, - )); - registry.register(WebFetchTool::new(50_000, 30)); + + if self.is_enabled("calculator") { + registry.register(CalculatorTool::new()); + } + if self.is_enabled("get_time") { + registry.register(TimeTool::new(self.default_timezone.clone())); + } + if self.is_enabled("file_read") { + registry.register(FileReadTool::new()); + } + if self.is_enabled("file_write") { + registry.register(FileWriteTool::new()); + } + if self.is_enabled("file_edit") { + registry.register(FileEditTool::new()); + } + if self.is_enabled("memory_search") { + registry.register(MemorySearchTool::new(self.memories.clone())); + } + if self.is_enabled("memory_manage") { + registry.register(MemoryManageTool::new(self.memories.clone())); + } + if self.is_enabled("session_send") { + registry.register(SessionSendTool::new(self.session_message_sender.clone())); + } + if self.is_enabled("scheduler_manage") { + registry.register(SchedulerManageTool::new( + self.scheduler_jobs.clone(), + self.known_agents.clone(), + )); + } + if self.is_enabled("skill_activate") { + registry.register(SkillActivateTool::new( + self.skills.clone(), + self.skill_events.clone(), + )); + } + if self.is_enabled("skill_list") { + registry.register(SkillListTool::new(self.skills.clone())); + } + if self.is_enabled("skill_manage") { + registry.register(SkillManageTool::new(self.skills.clone())); + } + if self.is_enabled("bash") { + registry.register(BashTool::new()); + } + if self.is_enabled("http_request") { + registry.register(HttpRequestTool::new( + vec!["*".to_string()], + 1_000_000, + 30, + false, + )); + } + if self.is_enabled("web_fetch") { + registry.register(WebFetchTool::new(50_000, 30)); + } + registry } } diff --git a/src/scheduler/mod.rs b/src/scheduler/mod.rs index 821ea48..352e5ca 100644 --- a/src/scheduler/mod.rs +++ b/src/scheduler/mod.rs @@ -655,10 +655,90 @@ fn parse_scheduler_cron(expression: &str) -> anyhow::Result { fn normalize_cron_expression(expression: &str) -> String { let parts: Vec<&str> = expression.split_whitespace().collect(); - if parts.len() == 5 { + let expression_with_seconds = if parts.len() == 5 { format!("0 {}", expression.trim()) } else { expression.trim().to_string() + }; + + // 转换星期字段为标准 cron 到 cron crate 格式 + // 标准 cron: 0=周日, 1=周一, ..., 6=周六, 7=周日 + // cron crate: 1=周日, 2=周一, ..., 6=周五, 7=周六 + convert_weekday_field(&expression_with_seconds) +} + +/// 将标准 cron 的星期字段转换为 cron crate 格式 +/// 标准: 0/7=周日, 1=周一, 2=周二, 3=周三, 4=周四, 5=周五, 6=周六 +/// crate: 1=周日, 2=周一, 3=周二, 4=周三, 5=周四, 6=周五, 7=周六 +fn convert_weekday_field(expression: &str) -> String { + let parts: Vec<&str> = expression.split_whitespace().collect(); + if parts.len() != 6 { + return expression.to_string(); + } + + let weekday_field = parts[5]; + let converted = convert_cron_weekday(weekday_field); + + format!("{} {} {} {} {} {}", parts[0], parts[1], parts[2], parts[3], parts[4], converted) +} + +/// 转换星期表达式中的数字 +fn convert_cron_weekday(field: &str) -> String { + if field == "*" || field == "?" { + return field.to_string(); + } + + // 处理列表(逗号分隔) + let items: Vec<&str> = field.split(',').collect(); + let converted_items: Vec = items.iter().map(|item| { + convert_weekday_item(item.trim()) + }).collect(); + + converted_items.join(",") +} + +/// 转换单个星期项(可能是范围、步长或单个值) +fn convert_weekday_item(item: &str) -> String { + // 检查是否有步长 + if let Some(pos) = item.find('/') { + let base = &item[..pos]; + let step = &item[pos + 1..]; + let converted_base = convert_weekday_range_or_value(base); + return format!("{}/{}", converted_base, step); + } + + // 检查是否是范围 + if item.contains('-') { + return convert_weekday_range_or_value(item); + } + + // 单个值 + convert_single_weekday(item) +} + +/// 转换范围或单个值 +fn convert_weekday_range_or_value(item: &str) -> String { + let parts: Vec<&str> = item.split('-').collect(); + if parts.len() == 2 { + let start = convert_single_weekday(parts[0].trim()); + let end = convert_single_weekday(parts[1].trim()); + format!("{}-{}", start, end) + } else { + convert_single_weekday(item) + } +} + +/// 转换单个星期数字 +fn convert_single_weekday(day: &str) -> String { + match day { + "0" | "7" => "1".to_string(), // 周日 -> 1 + "1" => "2".to_string(), // 周一 -> 2 + "2" => "3".to_string(), // 周二 -> 3 + "3" => "4".to_string(), // 周三 -> 4 + "4" => "5".to_string(), // 周四 -> 5 + "5" => "6".to_string(), // 周五 -> 6 + "6" => "7".to_string(), // 周六 -> 7 + _ => day.to_string(), // 其他(如字母)保持不变 } } @@ -1070,6 +1150,7 @@ impl TryFrom for SchedulerJobTarget { #[cfg(test)] mod tests { use super::*; + use chrono::{Datelike, Timelike}; use crate::bus::MessageBus; use crate::config::BUILTIN_MEMORY_MAINTENANCE_JOB_ID; use crate::storage::{SchedulerJobUpsert, SessionStore}; @@ -1495,4 +1576,127 @@ mod tests { .unwrap() ); } + + + #[test] + fn debug_cron_weekday_definitions() { + // 重大发现:cron crate 的星期定义是反常规的! + // 1 = 周日, 2 = 周一, ..., 6 = 周五, 7 = 周六 + // 0 是无效的! + let test_cases = vec![ + ("0 9 * * 1", "1=周日"), + ("0 9 * * 2", "2=周一"), + ("0 9 * * 3", "3=周二"), + ("0 9 * * 4", "4=周三"), + ("0 9 * * 5", "5=周四"), + ("0 9 * * 6", "6=周五"), + ("0 9 * * 7", "7=周六"), + ("0 9 * * 1-5", "1-5=周日到周四"), + ("0 9 * * 2-6", "2-6=周一到周五(这才是正确的工作日)"), + ]; + + // 从周六(2026-04-25)开始测试 + let saturday = Utc.with_ymd_and_hms(2026, 4, 25, 10, 0, 0).single().unwrap(); + let shanghai_saturday = saturday.with_timezone(&chrono_tz::Asia::Shanghai); + println!("\n=== 从周六 {} ({:?}) 开始测试 ===", shanghai_saturday, shanghai_saturday.weekday()); + + for (expr, desc) in &test_cases { + let schedule = parse_scheduler_cron(expr).unwrap(); + let next = schedule.after(&shanghai_saturday).next().unwrap(); + println!("{}: 下次执行 {} (星期: {:?})", desc, next, next.weekday()); + } + + // 验证正确的工作日表达式 + println!("\n=== 验证正确的工作日表达式 1-5(标准 cron)==="); + let schedule_workday = parse_scheduler_cron("0 9 * * 1-5").unwrap(); + + let sat_next = schedule_workday.after(&shanghai_saturday).next().unwrap(); + println!("周六 -> 1-5 下次执行: {} (星期: {:?})", sat_next, sat_next.weekday()); + assert_eq!(sat_next.weekday(), chrono::Weekday::Mon, "1-5 应该从周六跳到周一"); + + // 从周日开始 + let sunday = Utc.with_ymd_and_hms(2026, 4, 26, 10, 0, 0).single().unwrap(); + let shanghai_sunday = sunday.with_timezone(&chrono_tz::Asia::Shanghai); + let sun_next = schedule_workday.after(&shanghai_sunday).next().unwrap(); + println!("周日 -> 1-5 下次执行: {} (星期: {:?})", sun_next, sun_next.weekday()); + assert_eq!(sun_next.weekday(), chrono::Weekday::Mon, "1-5 应该从周日跳到周一"); + + // 从周一早上7点开始 + let shanghai_monday = chrono_tz::Asia::Shanghai.with_ymd_and_hms(2026, 4, 27, 7, 0, 0).single().unwrap(); + println!("周一早上7点 -> 1-5 下次执行: {} (星期: {:?})", + schedule_workday.after(&shanghai_monday).next().unwrap(), + schedule_workday.after(&shanghai_monday).next().unwrap().weekday()); + } + + /// 测试标准 cron 星期转换功能 + /// 现在可以使用标准 cron 语法: + /// - 0 或 7 = 周日 + /// - 1 = 周一 + /// - ... + /// - 6 = 周六 + /// 工作日(周一到周五)应该使用 1-5 + #[test] + fn standard_cron_weekday_conversion() { + // 测试:标准 cron 的 1-5 应该表示周一到周五 + let saturday = Utc.with_ymd_and_hms(2026, 4, 25, 10, 0, 0).single().unwrap(); + let shanghai_saturday = saturday.with_timezone(&chrono_tz::Asia::Shanghai); + + // 现在使用标准 cron:1-5 表示周一到周五 + let schedule_std = parse_scheduler_cron("0 9 * * 1-5").unwrap(); + + let sat_next = schedule_std.after(&shanghai_saturday).next().unwrap(); + println!("周六 -> 标准 cron 1-5 下次执行: {} (星期: {:?})", sat_next, sat_next.weekday()); + assert_eq!(sat_next.weekday(), chrono::Weekday::Mon, "标准 cron 1-5 应该从周六跳到周一"); + + // 从周日开始 + let sunday = Utc.with_ymd_and_hms(2026, 4, 26, 10, 0, 0).single().unwrap(); + let shanghai_sunday = sunday.with_timezone(&chrono_tz::Asia::Shanghai); + let sun_next = schedule_std.after(&shanghai_sunday).next().unwrap(); + println!("周日 -> 标准 cron 1-5 下次执行: {} (星期: {:?})", sun_next, sun_next.weekday()); + assert_eq!(sun_next.weekday(), chrono::Weekday::Mon, "标准 cron 1-5 应该从周日跳到周一"); + + // 从周一开始(上海时间周一早上7点) + let shanghai_monday = chrono_tz::Asia::Shanghai.with_ymd_and_hms(2026, 4, 27, 7, 0, 0).single().unwrap(); + let mon_next = schedule_std.after(&shanghai_monday).next().unwrap(); + println!("周一早上 -> 标准 cron 1-5 下次执行: {} (星期: {:?})", mon_next, mon_next.weekday()); + assert_eq!(mon_next.weekday(), chrono::Weekday::Mon, "标准 cron 1-5 应该当天执行"); + assert_eq!(mon_next.hour(), 9, "应该是上海时间9点"); + + // 从周五开始(应该下周周一) + let friday = Utc.with_ymd_and_hms(2026, 5, 1, 10, 0, 0).single().unwrap(); // 周五 + let shanghai_friday = friday.with_timezone(&chrono_tz::Asia::Shanghai); + let fri_next = schedule_std.after(&shanghai_friday).next().unwrap(); + println!("周五 -> 标准 cron 1-5 下次执行: {} (星期: {:?})", fri_next, fri_next.weekday()); + assert_eq!(fri_next.weekday(), chrono::Weekday::Mon, "标准 cron 1-5 应该从周五跳到下周一"); + } + + /// 测试转换辅助函数 + #[test] + fn test_weekday_conversion_helper() { + // 测试单个值 + assert_eq!(convert_single_weekday("0"), "1"); // 周日 + assert_eq!(convert_single_weekday("1"), "2"); // 周一 + assert_eq!(convert_single_weekday("5"), "6"); // 周五 + assert_eq!(convert_single_weekday("6"), "7"); // 周六 + assert_eq!(convert_single_weekday("7"), "1"); // 周日(标准 cron 兼容写法) + + // 测试范围 + assert_eq!(convert_weekday_range_or_value("1-5"), "2-6"); // 周一到周五 + assert_eq!(convert_weekday_range_or_value("0-6"), "1-7"); // 周日到周六 + assert_eq!(convert_weekday_range_or_value("0-7"), "1-1"); // 周日(循环) + + // 测试列表 + assert_eq!(convert_cron_weekday("1,3,5"), "2,4,6"); // 周一、三、五 + assert_eq!(convert_cron_weekday("0,6"), "1,7"); // 周日和周六 + + // 测试步长 + assert_eq!(convert_weekday_item("*/2"), "*/2"); // 步长保持不变 + + // 测试混合 + assert_eq!(convert_cron_weekday("1-5,7"), "2-6,1"); // 周一到周五 + 周日 + + // 测试特殊字符 + assert_eq!(convert_cron_weekday("*"), "*"); + assert_eq!(convert_cron_weekday("?"), "?"); + } } diff --git a/src/tools/scheduler_manage.rs b/src/tools/scheduler_manage.rs index 5022228..50e2978 100644 --- a/src/tools/scheduler_manage.rs +++ b/src/tools/scheduler_manage.rs @@ -31,7 +31,7 @@ impl Tool for SchedulerManageTool { } fn description(&self) -> &str { - "Manage repository-backed scheduled jobs. Supports actions: list, get, put, delete, pause, resume. Jobs are persisted by the configured scheduler job repository and executed by the scheduler runtime. When creating agent_task or silent_agent_task jobs, keep prompt/system_prompt focused on the work to perform; do not restate execution times unless the task logic truly depends on them, because the trigger already controls timing." + "Manage repository-backed scheduled jobs. Supports actions: list, get, put, delete, pause, resume. Jobs are persisted by the configured scheduler job repository and executed by the scheduler runtime. When creating agent_task or silent_agent_task jobs, keep prompt/system_prompt focused on the work to perform; do not restate execution times unless the task logic truly depends on them, because the trigger already controls timing. For cron schedules, standard cron syntax is supported: use 1-5 for Monday-Friday, 0 or 7 for Sunday." } fn parameters_schema(&self) -> serde_json::Value {