feat: 添加工具配置示例,支持工具启用/禁用功能;更新调度器管理工具描述以支持标准 cron 语法
This commit is contained in:
parent
5989b817b4
commit
33e6b78267
28
README.md
28
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. 快速开始
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -78,6 +78,7 @@ impl GatewayState {
|
||||
provider_configs,
|
||||
skills,
|
||||
Arc::new(BusSessionMessageSender::new(bus.clone())),
|
||||
std::collections::HashSet::new(),
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
|
||||
@ -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(),
|
||||
);
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
// 现在使用标准 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("?"), "?");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user