第 17 章:Autopilot、调度器与 Webhook
17.1 Autopilot 是持久规则,不是 Cron 包装
一条 Autopilot 同时定义:
- 做什么;
- 由哪个 Agent/Squad 做;
- 结果创建 Issue 还是直接 Run;
- 什么时候或被什么事件触发;
- 谁发布并对规则负责;
- 每次运行产生了什么下游对象;
- 暂停、归档、失败和重试怎样表现。
因此它横跨配置、权限、调度、任务队列和审计。
17.2 核心数据模型
查询入口在 server/pkg/db/queries/autopilot.sql,业务服务在 server/internal/service/autopilot.go。
主要对象:
- Autopilot:规则主体、目标、状态、执行模式;
- Autopilot Trigger:Schedule/Webhook 等触发配置;
- Autopilot Rule Version:不可变发布快照与 Publisher;
- Autopilot Run:一次触发的持久结果;
- Collaborator:规则写权限;
- Webhook Delivery:外部投递、去重、租约和尝试。
17.3 两种 Execution Mode
| 模式 | 下游对象 | 适用场景 |
|---|---|---|
create_issue | 先创建并分配 Issue,再由正常任务流执行 | 需要人类可见工作项、讨论、Review |
run_only | 直接创建 Agent Task,结果写回 Autopilot Run | 巡检、同步、汇总、无需 Issue 的动作 |
两者不能只用“是否填 Issue ID”区分;Admission、Prompt、终态同步与失败恢复都有不同路径。
17.4 create_issue 的审计优先
create_issue 的主要契约是留下一个持久、可见的工作项。即便 Runtime 暂时 Offline,系统也可以允许 Issue 创建:
- Issue 记录为什么产生;
- Assignee 与 Project 已确定;
- Runtime 恢复后普通任务机制可继续;
- 人类不会看不到一次计划触发。
run_only 没有这个中间载体,Agent 不可运行时通常应记录 Skipped/Failed,而不是生成一个永远无人消费的 Task。
17.5 Run 状态
Autopilot Run 大致经历:
实际字段以 Migration/SQL Check 为准。重点是 Run 持久化连接 Trigger、Issue/Task、Source、Payload、Planned At 和 Failure Reason。
17.6 四类 Source
服务接受:
- manual;
- schedule;
- webhook;
- api。
Source 不只是标签,它影响:
- 是否有直接 Human Actor;
- Attribution 取 Manual Clicker 还是 Rule/Trigger Owner;
- 是否有 Canonical Planned Time;
- 是否关联 Webhook Delivery;
- HTTP 是否需要同步返回可读 Outcome。
17.7 Admission Gate
Dispatch 前检查:
- Autopilot 是否 Active;
- Trigger 是否 Enabled;
- Assignee Agent/Squad Leader 是否可解析;
- Agent 是否 Archived;
- Runtime Binding/Readiness;
- Manual Actor 或 Rule Owner 是否有 Invoke 权;
- Private Agent 是否只由允许的 Owner 使用;
- Project/Workspace 关系是否一致。
Skipped 也是一个 Run,带明确 Reason;不能静默 No-op,否则 Scheduler 会把“没有执行”误当成功。
17.8 Manual 与自动化归因
Manual Run 有当前 Member:
originator_user_id是 Clicker;- Accountable User 也优先是这位直接发起者;
- 权限按 Clicker 实时检查。
Schedule/Webhook/API 没有现场 Human:
- 优先用 Trigger Publisher;
- 无法解析时降级到 Active Rule Version Publisher;
- 再按明确策略使用 Owner Fallback 或拒绝;
- 不能伪造一个当前用户。
17.9 Rule Version 为什么不可变
Autopilot 的 Title、Description、Target、Mode 以后会变化。若 Run 只指向当前 Row,半年后无法回答“当时是谁发布了怎样的规则”。
autopilot_rule_version 保存:
- Autopilot/Workspace;
- Published By Type/ID;
- 有效配置摘要;
- 发布时间。
创建、Enable/Resume、Target/Instructions/Mode 变更、Trigger 变更、Archive 等实质性发布会追加新版本。
17.10 Trigger Publisher 的更细归因
同一 Autopilot 可以有多个 Trigger。编辑某个 Cron/Filter/Webhook Security 时,只应转移该 Trigger 的责任。
所以 Trigger 自己也有 published_by_*:
- 单 Trigger 实质编辑 → 只 Re-stamp 该 Trigger;
- Autopilot 级实质编辑 → Re-stamp 全部 Trigger;
- 单纯展示性改动可不转移问责。
Run 同时保留 Originator、Accountable User 和 Rule Version ID。
17.11 写权限与问责权限不同
Autopilot Creator、Workspace Owner/Admin、显式 Collaborator 可以拥有写权限;但只有 Creator/Owner/Admin 能管理 Collaborator 列表。
Collaborator 修改规则后,会成为后续对应 Trigger/Rule 的 Publisher。权限不是匿名共享,编辑动作会转移未来运行的责任。
17.12 通用 DB-backed Scheduler
调度器位于 server/internal/scheduler。它不是 Autopilot 专用,核心表 sys_cron_executions 同时充当:
- Distributed Lock;
- Execution Audit Log;
- Retry State;
- Lease/Heartbeat;
- Catch-up Watermark。
所有 Server Replica 都可以 Tick,同一 (job, scope, plan_time) 只有一个 Claim Winner。
17.13 JobSpec
- 稳定 Job Name;
- Cadence 或自定义 PlansForScope;
- Scope Provider;
- Schedule Delay;
- Catch-up Mode/Window;
- 每 Tick 最大 Plans;
- Run/Stale/Heartbeat Timeout;
- 是否允许 Stale Reentry;
- Max Attempts 与 Retry Backoff;
- Handler。
校验要求 RunTimeout < StaleTimeout、HeartbeatInterval < StaleTimeout,否则健康 Handler 也会被其他节点误偷。
17.14 Scheduler Claim 协议
旧 Owner 在 Lease 被 Steal 后的 Terminal Update 会因 Token 不匹配被拒绝,不能覆盖新 Owner。
17.15 Catch-up 模式
通用 Scheduler 支持:
latest_only:只处理最近到期 Plan;every_plan:按从旧到新逐个补偿,受 Window/Max 限制;- 自定义
PlansForScope:适合不规则 Cron Occurrence。
Autopilot Schedule 使用第三种,并实现 Latest-only 意图。
17.16 Autopilot Schedule Job 参数
jobs_autopilot.go 当前配置:
- Job Name:
autopilot_schedule_dispatch; - 每个 Enabled Schedule Trigger 是一个 Scope;
- Run Timeout 2 分钟;
- Stale Timeout 5 分钟;
- Heartbeat 30 秒;
- 允许 Stale Reentry;
- 最多 3 次尝试;
- Backoff 1、5、15 分钟;
- Catch-up Window 24 小时;
- 每 Tick 最多 5 个 Plan(保护上限)。
参数是当前快照的实现细节,不是外部 SLA。
17.17 Cron 与 Timezone
service/cron.go 在 Trigger Timezone 中解析表达式,再返回 Canonical UTC Occurrence。
规则:
- Timezone 必须是有效 IANA 名称;
- 缺失默认 UTC;
- Preview 与执行使用同一解析逻辑;
- Plan Time 以 UTC 持久化;
- 人类可见描述按 Trigger Timezone 格式化;
- Schedule Trigger 才接受 Cron/Timezone,Webhook 明确拒绝这些字段。
17.18 Latest-only 与 Lateness
Planner 枚举 (lastPlan, dbNow] 中的 Cron Occurrences,只保留最新一个;如果它比计划时间晚超过 5 分钟,则不在任意恢复时刻补跑。
意义:
- Server 停机数小时后不会一次创建几十个重复日报;
- 新启用的旧 Trigger 不会立刻补发历史任务;
- 正常几十秒 Scheduler Jitter 仍可接受。
这是一种业务选择:Autopilot Schedule 偏“下一次保持新鲜”,不是财务流水那种每个 Bucket 必须重放。
17.19 双层幂等
一次 Schedule Occurrence 有两层键:
sys_cron_executions(job, scope, plan_time)防多个 Scheduler 同时跑;autopilot_run(trigger_id, planned_at)Partial Unique 防 Dispatch Crash Window 重复生成 Run。
如果先创建 Run、后创建下游 Issue/Task 时崩溃:
- 已有完整下游链接 → 复用原 Run;
- 只有半成品 Run → 标 Failed,清空 Planned At 释放唯一槽;
- 新尝试创建干净 Run;
- 旧失败记录保留供审计。
17.20 Stale Lease
Manager 每 Tick 先清理遗留 Running Lease:
- Heartbeat 超时 → 标 Failed/stale_timeout;
- 允许重入且尝试未耗尽 → 同 Plan 可再次 Claim;
- Steal 会轮换 Lease Token;
- 旧 Runner 的 Heartbeat/Finish 失效。
Autopilot Planner 必须在 Latest Row 是 Retry-eligible Failed 时返回 同一个 Plan Time;若直接向新 Cron Occurrence 前进,这次失败重试会永久搁浅。
17.21 Webhook Token 与 Secret
Webhook Trigger 的公开路径含 Bearer Token:
- 前缀
awt_; - 32 随机字节做 URL-safe Base64;
- Token 可旋转,旧 Token 立即失效;
- 只有有权管理 Autopilot 的成员才能看到完整 Token/URL。
可选 Signing Secret 额外验证 HMAC-SHA256。无 Secret 是显式 Bearer-only 模式,不代表跳过所有认证。
17.22 Webhook Ingress
autopilot_webhook.go 位于普通登录路由之外,路径 Token 就是 Credential。
固定顺序:
- IP 绝对上限与 Bad-credential Debt;
- Token Lookup;
- 256 KiB Body Cap;
- Autopilot/Workspace Cross-check;
- JSON Envelope Normalize;
- Provider Dedupe Key 与 Signature;
- 先插 Webhook Delivery;
- Invalid Signature 标 Rejected;
- Disabled/Paused/Archived 标 Ignored;
- 幂等 Admit Run;
- 唤醒持久 Worker;
- 同步返回 Accepted/Skipped/Duplicate。
未知 Token 一律 404,不泄露“差一点正确”的信息。
17.23 为什么先持久化 Delivery
Webhook Provider 会重试,Server 也会重启。若在 HTTP Handler 里直接创建 Issue:
- 超时后 Provider 不知道是否成功;
- Server Crash 产生不确定副作用;
- 重放容易重复;
- 慢 Agent Dispatch 占住 Ingress。
Delivery Row 保存 Raw Body、精选 Header、Signature Status、Dedupe Key、Response 与 Run Link,使 HTTP 可以快速确认,Worker 再异步执行。
Signature Header 只保存“是否存在”,不保存 HMAC 值。
17.24 Webhook Dedup
Provider-specific Header 或 Idempotency Key 归一成 (trigger_id, dedupe_key)。
碰撞时:
- 不新建 Delivery;
- 增加 Attempt Count;
- 返回原 Delivery ID;
- 若已有 Run,返回 Run ID;
- 原 Delivery 仍 Queued 时再次 Wake Worker。
重复请求返回 200,避免 Provider 因错误码继续无意义重试。
17.25 Webhook Worker
webhook_delivery_worker.go 使用 PostgreSQL 队列:
- 单进程并发 4;
- Recovery Poll 每秒;
- 本地 Notify 只是低延迟提示;
SKIP LOCKED跨 Replica 分配;- Lease Token 保护 Complete/Retry;
- 最多 5 次 Dispatch;
- 指数 Backoff;
- Trigger Rate Limit 可把 Delivery 释放到未来 Available At。
进程崩溃后其他 Replica 可回收过期 Lease。
17.26 Filter 与 Admission 时点
Ingress 同步 Admit 后,决定已持久化。Worker 不应因为管理员在 200 Response 之后立刻 Pause 就把已接受 Run 悄悄丢掉。
只有恢复“尚未 Admit 的 Crash Window”时,Worker 才重新检查:
- Trigger Enabled;
- Autopilot Active;
- Event Filter;
- Ownership 一致。
这让外部调用方收到的 Accepted 具有稳定含义。
17.27 Run 怎样收尾
- create_issue:Issue/Linked Task 状态通过
SyncRunFromIssue与SyncRunFromLinkedIssueTask更新; - run_only:Task Terminal 通过
SyncRunFromTask更新; - Skipped/Admission Error:Dispatch 当场终结;
- Webhook Delivery 另行记录 Dispatched/Failed/Ignored;
- Event Bus 发布 Run Started/Done,前端失效 Query。
Run 状态不是靠 Scheduler 猜 Agent 是否完成。
17.28 排障矩阵
| 症状 | 先查 |
|---|---|
| Cron 没触发 | Trigger Active、Cron/Timezone、Scope、sys_cron_executions |
| 同一时间触发两次 | 两层唯一键、planned_at 是否为空/不规范 |
| Run 卡 pending | Dispatch Crash Window、下游 Issue/Task Link |
| Schedule 失败后不重试 | Planner 是否返回 Retry-eligible 的旧 Plan |
| Webhook 200 但没任务 | Delivery、Admitted Run、Worker Lease、Admission Reason |
| Webhook 重复创建 | Dedupe Key 提取与 Delivery Unique Constraint |
| 别人编辑后仍归原 Owner | Rule Version/Trigger Publisher 未 Re-stamp |
| Squad Autopilot 不运行 | Leader 解析、Leader Runtime、Private Invoke Gate |
17.29 本章结论
Autopilot 的可靠性来自三组持久事实:
- Rule Version/Trigger Publisher 说明“谁发布了什么”;
- Scheduler Lease/Plan Time 说明“什么时候由谁执行”;
- Run/Delivery/Task Link 说明“实际产生了什么”。
Cron、Webhook 和 Agent Task 都可能重试;只有把幂等、归因和恢复写进数据库,自动化才不是一组脆弱的后台 Goroutine。