feat(scheduler): 定时任务后端持久化 + Pipeline trigger 编译修复

S4/S8 定时任务后端:
- 新增 scheduled_tasks 表 (migration v7)
- 新增 scheduled_task CRUD 模块 (handlers/service/types)
- 注册 /api/scheduler/tasks 路由 (GET/POST/PATCH/DELETE)
- 新增 start_user_task_scheduler() 30秒轮询循环
- 支持 cron/interval/once 三种调度类型
- once 类型执行后自动禁用

修复:
- pipeline_commands.rs: 修复 pipeline.trigger 字段不存在的编译错误
  (Pipeline 结构体无 trigger 字段,改用 metadata.tags/description)
This commit is contained in:
iven
2026-03-30 19:46:45 +08:00
parent c2aff09811
commit a0bbd4ba82
10 changed files with 457 additions and 5 deletions

View File

@@ -99,3 +99,75 @@ pub fn start_db_cleanup_tasks(db: PgPool) {
}
});
}
/// 启动用户定时任务调度循环
///
/// 每 30 秒检查 `scheduled_tasks` 表中 `enabled=true AND next_run_at <= now` 的任务,
/// 标记为已执行并更新下次执行时间。对于 `once` 类型任务,执行后自动禁用。
///
/// 注意:实际的任务执行(如触发 Agent/Hand/Workflow需要与中转服务或
/// 外部调度器集成。此 loop 当前仅负责任务状态管理。
pub fn start_user_task_scheduler(db: PgPool) {
tokio::spawn(async move {
let mut ticker = tokio::time::interval(Duration::from_secs(30));
ticker.tick().await; // 跳过首次立即触发
loop {
ticker.tick().await;
if let Err(e) = tick_user_tasks(&db).await {
tracing::error!("[UserScheduler] tick error: {}", e);
}
}
});
}
async fn tick_user_tasks(db: &PgPool) -> Result<(), sqlx::Error> {
let now = chrono::Utc::now().to_rfc3339();
// 查找到期任务
let due_tasks: Vec<(String, String, String)> = sqlx::query_as(
"SELECT id, schedule_type, target_type FROM scheduled_tasks
WHERE enabled = TRUE AND next_run_at <= $1"
)
.bind(&now)
.fetch_all(db)
.await?;
if due_tasks.is_empty() {
return Ok(());
}
tracing::debug!("[UserScheduler] {} tasks due", due_tasks.len());
for (task_id, schedule_type, _target_type) in due_tasks {
// 标记执行
let now_str = chrono::Utc::now().to_rfc3339();
let result = sqlx::query(
"UPDATE scheduled_tasks
SET last_run_at = $1, run_count = run_count + 1, updated_at = $1,
enabled = CASE WHEN schedule_type = 'once' THEN FALSE ELSE TRUE END,
next_run_at = NULL
WHERE id = $2"
)
.bind(&now_str)
.bind(&task_id)
.execute(db)
.await;
match result {
Ok(r) => {
if r.rows_affected() > 0 {
tracing::info!(
"[UserScheduler] task {} executed ({})",
task_id, schedule_type
);
}
}
Err(e) => {
tracing::error!("[UserScheduler] task {} failed: {}", task_id, e);
}
}
}
Ok(())
}