定时任务
Cell 内的定时 Run 调度器,支持按 cron 表达式或延迟触发 Agent Run。
Cron 概念
每个 Cell 拥有独立的定时任务调度器。Cron 任务在指定时间触发 Agent Run,常用于:
- 定时报告生成
- 周期性数据检查
- 自动化日常流程
Cron 是 per-cell 装配——部门 A 的定时任务对部门 B 物理不可见。
CellSpec.Cron 声明
CellSpec.Cron 是出生声明(INV-SEED-01):
- 只在 Cell 首次 boot 播种进
wes_cron_jobs表 - 此后存储是唯一权威
- 声明不再被读
spec := wesgine.CellSpec{
Cron: []cron.JobSpec{
{
Name: "weekly-report",
Schedule: "0 9 * * 1",
AgentID: "reporter",
Prompt: "生成本周工作报告",
},
},
}
为什么这样设计? 如果每次 boot 都重新播种,用户运行时删掉的任务下次 boot 会复活——"删除"静默地只等于"到重启为止"。标记在探测成功后无条件写,含声明为空。
Cron API
基础 CRUD
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /cron | 定时任务列表(含 states) |
POST | /cron | 添加定时任务 |
DELETE | /cron/{id} | 删除任务 |
PATCH | /cron/{id} | 更新任务 |
调度控制
| 方法 | 路径 | 说明 |
|---|---|---|
POST | /cron/{id}/trigger | 立即触发 |
POST | /cron/{id}/enable | 启用 |
POST | /cron/{id}/disable | 禁用 |
观测
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /cron/{id}/runs | 运行记录 |
GET | /cron/stats | 调度器统计 |
SDK 调用
// 添加
err := cell.Cron().Add(ctx, actor, cron.Job{
Name: "daily-check",
Schedule: "0 8 * * *",
AgentID: "checker",
Prompt: "检查今日待办事项",
})
// 列出
jobs, err := cell.Cron().ListAllStates(ctx, actor)
// 触发
err := cell.Cron().Trigger(ctx, actor, jobID)
// 启用/禁用
err := cell.Cron().Enable(ctx, actor, jobID)
err := cell.Cron().Disable(ctx, actor, jobID)
Actor 边界(INV-CRON-10)
Cron 是 Actor 边界的第四个面——泄漏的不是数据而是执行身份。
executeJob 把 entry.Actor 抄进 JobRequest,job 以所有者身份跑。一次不查主的 Trigger 不是"改了别人的配置",是以别人的身份跑代码。
判据设计
| 概念 | 实现 |
|---|---|
| 可见性单点 | jobVisible(actor) |
| by-ID 单点 | ownedJob(ctx, id, actor) |
| 不存在与不属于 | 同返 ErrJobNotFound |
| 无主 job | 仅 admin 可见 |
| 工具侧 | 全 action 取 tc.Actor |
| HTTP 侧 | 一律 actorFromRequestOrContext(req) |
// Manager 每个方法都收 actor
manager.Add(ctx, actor, job)
manager.Remove(ctx, actor, jobID)
manager.Trigger(ctx, actor, jobID)
manager.Update(ctx, actor, jobID, patch)
ClaimOwnerless — 认领无主任务
升级面:actor 列是 DEFAULT '' 加的,本条生效前写入的每一行都无主。
无主行的症状:
- UI 列不出(fail-closed 过滤掉了)
- by-ID 全
ErrJobNotFound - 却每到点照跑照发消息(
ListDue不带 actor)
解决:boot 期 ClaimOwnerless(ctx, actor) 认领。
约束
| 规则 | 原因 |
|---|---|
| 只给单 actor 部署用 | 多 actor 盲调 = 把某员工的任务判给另一个 |
| actor 必须是持久身份 | 占位 actor 只是把幽灵搬个家 |
| 拿不到持久身份就跳过 | wesclaw 未登录时的占位名不可用 |
// 桌面产品 boot 时调用
if uid := authSvc.OwnerID(); uid != "" {
cell.Cron().ClaimOwnerless(ctx, uid)
}
配额(INV-QUOTA-05)
MaxCronJobs 的判据位置遵循外设配额统一规则:
| 规则 | 说明 |
|---|---|
| 唯一增长点判定 | Scheduler.Add 是唯一能让集合变大的地方 |
| QuotaFn 提问 | 适配器不知道 cap 数值,只收 QuotaFn(current, delta) |
| delta 按行存不存在 | 新增 delta=1,覆盖已有名字 delta=0 |
| 问与长同临界区 | 防止两个写者各读到 cap-1 各加一 |
创建时由 validateSeedDeclarationCaps 拒绝装不下自己的声明。