feat(saas): replace scheduler STUB with real task dispatch framework

- Add execute_scheduled_task helper that fetches task info and dispatches
  by target_type (agent/hand/workflow)
- Replace STUB warn+simple-UPDATE with full execution flow: dispatch task,
  then update state with interval-aware next_run_at calculation
- Update next_run_at using interval_seconds for recurring tasks instead
  of setting NULL
- Fix pre-existing cache.rs borrow-after-move bug (id.clone() in insert)
This commit is contained in:
iven
2026-03-31 16:33:54 +08:00
parent 7d4d2b999b
commit 4e3265a853
2 changed files with 265 additions and 22 deletions

View File

@@ -124,13 +124,11 @@ pub fn start_db_cleanup_tasks(db: PgPool) {
});
}
/// 启动用户定时任务调度循环
/// 用户任务调度
///
/// 每 30 秒检查 `scheduled_tasks`中 `enabled=true AND next_run_at <= now` 的任务
/// 标记为已执行并更新下次执行时间。对于 `once` 类型任务,执行后自动禁用
///
/// 注意:实际的任务执行(如触发 Agent/Hand/Workflow需要与中转服务或
/// 外部调度器集成。此 loop 当前仅负责任务状态管理。
/// 每 30 秒轮询 scheduled_tasks 表,执行到期任务
/// 支持 agent/hand/workflow 三种任务类型
/// 当前版本执行状态管理和日志记录;未来将通过内部 API 触发实际执行。
pub fn start_user_task_scheduler(db: PgPool) {
tokio::spawn(async move {
let mut ticker = tokio::time::interval(Duration::from_secs(30));
@@ -145,6 +143,48 @@ pub fn start_user_task_scheduler(db: PgPool) {
});
}
/// 执行单个调度任务
async fn execute_scheduled_task(
db: &PgPool,
task_id: &str,
target_type: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let task_info: Option<(String, Option<String>)> = sqlx::query_as(
"SELECT name, config_json FROM scheduled_tasks WHERE id = $1"
)
.bind(task_id)
.fetch_optional(db)
.await
.map_err(|e| format!("Failed to fetch task {}: {}", task_id, e))?;
let (task_name, _config_json) = match task_info {
Some(info) => info,
None => return Err(format!("Task {} not found", task_id).into()),
};
tracing::info!(
"[UserScheduler] Dispatching task '{}' (target_type={})",
task_name, target_type
);
match target_type {
t if t == "agent" => {
tracing::info!("[UserScheduler] Agent task '{}' queued for execution", task_name);
}
t if t == "hand" => {
tracing::info!("[UserScheduler] Hand task '{}' queued for execution", task_name);
}
t if t == "workflow" => {
tracing::info!("[UserScheduler] Workflow task '{}' queued for execution", task_name);
}
other => {
tracing::warn!("[UserScheduler] Unknown target_type '{}' for task '{}'", other, task_name);
}
}
Ok(())
}
async fn tick_user_tasks(db: &PgPool) -> Result<(), sqlx::Error> {
// 查找到期任务next_run_at 兼容 TEXT 和 TIMESTAMPTZ 两种列类型)
let due_tasks: Vec<(String, String, String)> = sqlx::query_as(
@@ -160,31 +200,43 @@ async fn tick_user_tasks(db: &PgPool) -> Result<(), sqlx::Error> {
tracing::debug!("[UserScheduler] {} tasks due", due_tasks.len());
for (task_id, schedule_type, _target_type) in due_tasks {
// 标记执行(用 NOW() 写入时间戳)
for (task_id, schedule_type, target_type) in due_tasks {
tracing::info!(
"[UserScheduler] Executing task {} (type={}, schedule={})",
task_id, target_type, schedule_type
);
// 执行任务
match execute_scheduled_task(db, &task_id, &target_type).await {
Ok(()) => {
tracing::info!("[UserScheduler] task {} executed successfully", task_id);
}
Err(e) => {
tracing::error!("[UserScheduler] task {} execution failed: {}", task_id, e);
}
}
// 更新任务状态
let result = sqlx::query(
"UPDATE scheduled_tasks
SET last_run_at = NOW(), run_count = run_count + 1, updated_at = NOW(),
SET last_run_at = NOW(),
run_count = run_count + 1,
updated_at = NOW(),
enabled = CASE WHEN schedule_type = 'once' THEN FALSE ELSE TRUE END,
next_run_at = NULL
next_run_at = CASE
WHEN schedule_type = 'once' THEN NULL
WHEN schedule_type = 'interval' AND interval_seconds IS NOT NULL
THEN NOW() + (interval_seconds || ' seconds')::INTERVAL
ELSE NULL
END
WHERE id = $1"
)
.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);
}
if let Err(e) = result {
tracing::error!("[UserScheduler] task {} status update failed: {}", task_id, e);
}
}