feat: 添加工具配置示例,支持工具启用/禁用功能;更新调度器管理工具描述以支持标准 cron 语法

This commit is contained in:
ooodc 2026-05-10 13:57:47 +08:00
parent 5989b817b4
commit 33e6b78267
8 changed files with 348 additions and 34 deletions

View File

@ -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. 快速开始

View File

@ -25,6 +25,8 @@ pub struct Config {
pub channels: HashMap<String, ChannelConfig>,
#[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<String>,
}
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 {

View File

@ -78,6 +78,7 @@ impl GatewayState {
provider_configs,
skills,
Arc::new(BusSessionMessageSender::new(bus.clone())),
std::collections::HashSet::new(),
)?;
Ok(Self {

View File

@ -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<String, LLMProviderConfig>,
skills: Arc<SkillRuntime>,
disabled_tools: HashSet<String>,
) -> Result<SessionManager, AgentError> {
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<String, LLMProviderConfig>,
skills: Arc<SkillRuntime>,
session_message_sender: Arc<dyn SessionMessageSender>,
disabled_tools: HashSet<String>,
) -> Result<SessionManager, AgentError> {
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(),
);

View File

@ -378,6 +378,7 @@ impl SessionManager {
provider_config: LLMProviderConfig,
provider_configs: HashMap<String, LLMProviderConfig>,
skills: Arc<SkillRuntime>,
disabled_tools: std::collections::HashSet<String>,
) -> Result<Self, AgentError> {
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();

View File

@ -18,6 +18,7 @@ pub(crate) struct ToolRegistryFactory {
session_message_sender: Arc<dyn SessionMessageSender>,
known_agents: HashSet<String>,
default_timezone: String,
disabled_tools: HashSet<String>,
}
impl ToolRegistryFactory {
@ -29,6 +30,7 @@ impl ToolRegistryFactory {
session_message_sender: Arc<dyn SessionMessageSender>,
known_agents: HashSet<String>,
default_timezone: String,
disabled_tools: HashSet<String>,
) -> 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
}
}

View File

@ -655,10 +655,90 @@ fn parse_scheduler_cron(expression: &str) -> anyhow::Result<cron::Schedule> {
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<String> = 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<serde_json::Value> 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);
// 现在使用标准 cron1-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("?"), "?");
}
}

View File

@ -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 {