Multica Autopilots 源码地图导读:从触发链、幂等投递到权限门禁的自动化引擎全解析
Autopilot 是 Multica 中把"人类 + AI Agent 组成同一团队"落到自动化层的关键机制:它不是 Agent,而是一条把工作派发给 Agent(或 Squad 队长 Agent)的持久化规则。本文以仓库内 builtin skill 的源码地图文档为主体,结合 CLI、服务层、Handler 与测试实现,逐层拆解 Autopilot 的触发链、执行模式、Webhook 持久化与幂等恢复、调度预览、读写权限分层与协作者管理,帮助读者真正读懂"它为什么在某个时刻跑了(或没跑)",并能在命令行与 API 层面安全地创建、排查与维护自动化规则。
一、先读这份源码地图:它定位了什么
仓库中的 autopilots-source-map.md 是一份给内置技能(SKILL.md,用于创建、更新、检查、触发或调试 Autopilot)使用的"源码索引"。它的价值在于把分散在 CLI、Service、Handler、Router、迁移与前端组件里的 Autopilot 相关代码一次性串起来,形成一条可追踪的调用链。它对应的上层技能文档已经给出了 Quick Start 与调试顺序,本文则在源码地图的骨架上做纵深展开。
从整体代码结构看,Autopilot 的代码横跨三个主要层:
- CLI 层:cmd_autopilot.go 注册全部子命令;
- 服务层:autopilot.go(约 1900 行)承担核心执行逻辑、准入检查与 Squad 队长解析;
- 接口层:handler/autopilot.go、autopilot_webhook.go、autopilot_cron_preview.go 与 webhook_delivery_worker.go 负责 HTTP 路由、权限判定与投递工作器。
二、核心模型:Autopilot 不是 Agent,而是一条"规则"
源码地图开宗明义的第一条原则是:Autopilot 本身不是 Agent,它是一条把工作派发给 Agent 或 Squad 队长 Agent 的规则。这一抽象决定了后面所有设计:
- 规则持有
assignee_type与assignee_id,指向 Agent 或 Squad; - 触发后创建一行
autopilot_run(运行时记录); execution_mode决定规则的"输出形态";- 真正干活的是被派发的 Agent/运行时,Autopilot 只负责"何时派、派给谁、产出什么"。
主链:trigger → run → 执行模式 → 准入 → 派发
完整执行链为:
触发(schedule / webhook / manual)
→ autopilot_run 行落库
→ execution_mode 决定输出
→ assignee 就绪检查(准入)
→ issue/task 执行
→ run 状态同步
该链的核心入口在 service/autopilot.go:
DispatchAutopilot(约 L119)是"无成员执行者"的统一入口(定时/Webhook/API),内部通过带幂等键的dispatchAutopilot落 run 并派发;DispatchAutopilotManual/DispatchAutopilotManualWithKey(约 L141/L153)是成员手动"run now"入口,与调度/Webhook 不同,它会把 run 归属到发起成员(direct_human),并返回面向人的 typed reason code;DispatchAutopilotForPlan(约 L400)专供定时触发使用,接收触发已确定的 UTCplanned_at,与(trigger_id, planned_at)部分唯一索引一起保证"同一计划时刻不会产生两次成功 run";dispatchAutopilotRun(约 L566)在一个已持久化的 run 上执行下游副作用,并把 run 创建与副作用创建解耦——这正是 Webhook 工作器能在崩溃后无缝恢复的基础。
执行模式:create_issue 与 run_only
规则体上的 execution_mode 支持两种取值,CLI 层在创建时做了硬校验(cmd_autopilot.go 中 --mode must be create_issue or run_only):
| 模式 | 行为 | 适用场景 | run 初始状态 |
|---|---|---|---|
create_issue |
创建一条 Multica issue,run 以 issue 状态呈现 | 需要可见的审计痕迹,人工可追踪 | issue_created |
run_only |
直接创建 Agent task,不创建 issue | 后台批处理,无需 issue 记录;持久报告需由任务上下文/指令承载 | running |
对应实现为 dispatchCreateIssue(约 L641)与 dispatchRunOnly(约 L933),二者都先经过共同的准入判断 shouldSkipDispatch。
issue-title 模板的边界
源码地图特别提醒:issue-title-template 只支持 {{date}} 一个插值变量。CLI 帮助文本明确写为 {{date}} (UTC, YYYY-MM-DD),任何其他 {{...}} 令牌都会在 create 时被拒绝(见 cmd_autopilot.go 的 issue-title-template flag 描述),不要自行发明 {{trigger_id}}、{{branch}} 之类的变量。
三、CLI 命令全集:读操作与写操作的边界
multica autopilot 家族是日常操作 Autopilot 的主要入口。根据 cmd_autopilot.go 的注册与 flag 定义,可整理为下表:
| 命令 | 参数/Flags 要点 | 说明 |
|---|---|---|
autopilot list |
--status active|paused、--output table|json、--full-id |
列出工作区内的 autopilots,table 输出含 NEXT_RUN/LAST_RUN 相对时间 |
autopilot get <id> |
--output(默认 json)、--show-secrets(仅 json) |
读取详情,默认对 Webhook 凭证做脱敏 |
autopilot create |
--title、--description、--agent、--mode(均必填);--project、--issue-title-template、--subscriber(可重复) |
创建规则;agent 可传名称或 ID |
autopilot update <id> |
--title/--description/--agent/--project/--status/--mode/--issue-title-template/--subscriber/--clear-subscribers |
更新规则字段 |
autopilot delete <id> |
无 flags | 删除规则 |
autopilot trigger <id> |
--output |
手动触发一次 run(真实副作用) |
autopilot runs <id> |
--limit(默认 20)、--offset、--output |
查看运行历史与失败原因 |
autopilot trigger-add <id> |
--kind schedule|webhook、--cron、--timezone、--label |
新增定时或 Webhook 触发器 |
autopilot trigger-list <id> |
--output、--full-id |
列出触发器(拿到 trigger id 供后续命令用) |
autopilot trigger-update <id> <trigger-id> |
--enabled、--cron、--timezone、--label |
启停/修改触发器 |
autopilot trigger-delete <id> <trigger-id> |
无额外 flags | 删除触发器 |
autopilot trigger-rotate-url <id> <trigger-id> |
--yes/-y(跳过交互确认) |
轮换 Webhook URL,旧 URL 立即失效 |
CLI 内部把读写映射到 REST 接口:/api/autopilots、/api/autopilots/{id}、/api/autopilots/{id}/trigger、/api/autopilots/{id}/runs 以及触发器子路由,认证与权限则落在服务端。runs 还支持分页(--limit/--offset),适合在调试"为什么没跑"时翻历史。
敏感输出的脱敏与逃生舱
源码地图强调了一个安全细节:autopilot get 在普通 JSON 输出中会把 webhook_token、webhook_path、webhook_url 置空(null),同时报告是否存在 token(has_webhook_token)并给出不敏感的后缀提示(webhook_token_hint)。其客户端实现见 cmd_autopilot.go 的 redactAutopilotWebhookCredentials:hint 只取 token 的末尾 4 个字符(webhookTokenHint)。
当且仅当用户明确要求取回实时 Webhook 凭证时,才追加 --show-secrets。它有两个约束:
- 要求
--output json,否则直接报错--show-secrets requires --output json; - 会向 stderr 打印凭证暴露警告(源码地图称之为 "credential-exposure warning"),例如:
Warning: --show-secrets exposes live webhook credentials; keep this output out of logs and shared transcripts.
切记不要把 Webhook token 或签名材料粘贴进评论、日志、文档或 PR。
四、Webhook 触发:持久化投递 + 同步准入 + 数据库租约恢复
Webhook 是三种触发方式(schedule / webhook / manual)中最讲究可靠性的路径,源码地图给它单独画了一条前置链:
HTTP 入口持久化一条 queued webhook_delivery
→ 同步为这条 delivery 创建(或复用)幂等 run
→ 返回 200 { status: accepted|skipped, run_id }
→ 持有数据库租约的 worker 唤醒并恢复 accepted 的 run
1. 入口持久化与同步准入
server/cmd/server/router.go 对外暴露两类路由:需要认证的 /api/autopilots 系列,以及无需认证的 Webhook 公网入口 /api/webhooks/autopilots/{token}(token 本身就是凭证,这解释了上一节为何对所有读取方脱敏 token)。
handler/autopilot_webhook.go 负责把公开的 Webhook 投递持久化落库,然后同步调用 AdmitAutopilotWebhookDelivery(service/autopilot.go 约 L173)。准入阶段只负责"是否值得跑":
- 先查
GetAutopilotRunByWebhookDelivery(deliveryID)——若已存在则直接复用,保证幂等; - 若命中
shouldSkipDispatch的跳过条件,则写一条skippedrun(reason_code 一并记录); - 否则按执行模式创建
issue_created或running状态的 run,并把webhook_delivery_id持久化在 run 上,随后唤醒 worker。
整个准入是幂等 + 并发安全的:如果两个 server 副本同时处理同一投递,recoverConcurrentWebhookAdmission 会捕获唯一索引冲突(PostgreSQL 错误码 23505),失败方重新读取胜者的 run 复用之。
2. Worker 侧恢复:绝不重复 issue/task
handler/webhook_delivery_worker.go 承担排队投递的领取与执行:
- 用带过期时间的数据库租约(lease)认领
queued的 delivery; - 按触发器维度施加派发节流(per-trigger dispatch pacing);
- 通过
autopilot_run.webhook_delivery_id恢复已准入的 run 调用DispatchAutopilotForWebhookDelivery(约 L266)。
由于 delivery 与 run 之间存在部分唯一索引,进程崩溃后重新认领同一投递只会复用原始 run,而不会产生第二条 issue/task。服务层还专门封了两个崩溃窗口修复函数,源码地图之外的细节尤其值得留意:
ensureWebhookCreateIssueTask(约 L309):兜底create_issue在"issue/run 事务提交成功、但普通任务入队尚未提交"之间崩溃的窗口——只要 issue 已存在任何 task 即认为归属已移交下游,否则按原派发路径精确补一次入队(Squad 走EnqueueTaskForSquadLeader,否则EnqueueTaskForIssue);repairAutopilotRunTaskLink(约 L344):兜底run_only在"task 创建已提交、但autopilot_run.task_id未回写"之间崩溃的窗口——找到该 run 的任务即证明所有权已移交,活跃任务重新唤醒、终止任务走常规 finalizer 而非重复执行。
3. 一次恢复逻辑在定时路径的复用
isAutopilotRunComplete(约 L496)定义了"run 可安全复用"的判定:终态(completed/failed/skipped)或带有效下游引用的在途态(issue_created 且 issue_id 有效、running 且 task_id 有效)视为 complete;其余(尤其 issue_created/running 但下游引用为 NULL,或短暂的 pending)视为 partial,必须标记 failed、释放 planned_at 槽位后重新派发。DispatchAutopilotForPlan 对定时触发的 stale-steal 重入正是靠这套判定避免"调度器标记 SUCCESS 但实际从未创建 issue/task"。
五、调度预览:GET /api/autopilots/cron-preview 是纯计算接口
源码地图特别强调了一个"编辑器友好"的接口:autopilot_cron_preview.go 的 GET /api/autopilots/cron-preview?expr=&tz=。
- 响应:
200 OK返回{"next_runs": [...]},即接下来 3 次发生时刻,统一格式化为 RFC3339 UTC; - 错误:返回
400,并携带可区分的错误码——invalid_cron(表达式解析失败)与invalid_timezone(时区 tzdata 不识别)。二者在同一界面里分别指向不同的表单控件,所以用独立 code 而非笼统的 400; - 纯计算:
CronPreview只做计算,不触碰任何 autopilot 资源;仅以工作区成员身份为门槛(路由组强制),编辑器可放心在保存前调用,无需担心副作用; - 细节语义:语法合法但永不触发的表达式(如
0 9 * * *配合极端时区偏移)会返回短的或空的next_runs,而不是报错——编辑器靠状态码区分"永不运行"与"cron 写错"。
实现上调用 service.NextOccurrencesAfterUTC(expr, tz, now, 3) 计算后续时刻,tz 缺省按 UTC 处理,与 DefaultAutopilotTriggerTimezone = "UTC"(service/autopilot.go 顶部常量)保持一致——该默认时区同时被调度器用于计算 next run。
六、授权层:autopilot 级的 View/Write 分层与显式协作者
Autopilot 把访问控制拆成了两层,源码地图称其为 "autopilot-level View/Write layer":
1. 写授权归属
写/执行类操作(编辑、删除、触发、重放投递、管理 trigger 与 Webhook 密钥)由 autopilotWriteByOwnership / memberCanWriteAutopilot / requireAutopilotWrite(均在 handler/autopilot.go)把关,放行的主体是:
- autopilot 的创建者(creator);
- 工作区的 owner/admin;
- 被显式授予的 collaborator。
读操作(list/get/runs/deliveries)向任意工作区成员开放,但 GetAutopilot 对缺乏写权限的调用方仍要脱敏 webhook_token/webhook_path/webhook_url——因为仅凭 token 即可触发该 autopilot。而创建新 autopilot 依然对所有成员开放(创建者即规则 owner)。
这一层与派发期的"私有 assignee-agent 门禁"相互独立且按 AND 叠加(后者在 shouldSkipDispatch 中强制执行)。从 shouldSkipDispatch 的实现(约 L1270)可以看到更细的权限语义:手动触发(actorUserID 有效)按当前点击者的权限判定准入与归属,保证"准入主体 == 归属主体"不分裂;自动化触发(schedule/webhook/api,无人在环)退化为按 creator 判定;admin 也不能绕过其不拥有的私有 Agent。
2. 显式协作者(collaborators)
显式写授权记录在 autopilot_collaborator 表(迁移 128_autopilot_collaborator.up.sql,仅成员、无外键,删除 autopilot 时在同一删除事务中连带清理)。端点如下:
POST /api/autopilots/{id}/collaborators,body 形如{"user_id": "..."};DELETE /api/autopilots/{id}/collaborators/{userId};- 两者都返回更新后的
{collaborators}列表。
两个端点都被更窄的 requireAutopilotAccessManagement 门禁约束(仅 creator 或 workspace owner/admin)——被授权的 collaborator 拥有写/执行权,但不能再授权或撤销同伴,这从根本上防止权限提升。
与之配套,GetAutopilot 的响应内嵌 collaborators 数组,并打上两个按调用者计算的布尔标记:
can_write:门控编辑/运行/触发控件;can_manage_access:更窄,门控 "Manage access" 入口。
Web/桌面端的 "Manage access" 界面位于 manage-access-dialog.tsx。
七、派发准入与 Squad 队长解析:为什么"没跑"以及为什么该跑时必跑
调试"为什么没跑"时,真正的裁决点是派发前的准入检查 shouldSkipDispatch(约 L1270)。它按序检查:
assignee_id是否存在(不存在 → skip);- 解析实际执行者
resolveAutopilotLeader(约 L1400):assignee_type='agent'时执行者即该 agent;assignee_type='squad'时解析为 squad 的 leader_id(源码地图称此语义为 Autopilot-on-squad ≈ Autopilot-on-leader)。若 squad 已归档则直接 fail-closed(errSquadArchived,此时本应已由 DeleteSquad 的迁移把幸存 autopilot 改写为 leader agent); - 用
AgentReadiness检查该 agent 的运行时状态:- agent 已归档 / 无 runtime 绑定 → skip 并生成带 "at dispatch time" 后缀的可报警原因;
- 离线但可等待(
AgentWaitable)且模式为create_issue→ 特例放行:issue 写于服务端,run 等待笔记本/daemon 恢复后认领——因为create_issue的首要契约是持久审计轨迹,离线不应阻塞它; - runtime 不可用(无法运行)→ 拒绝,避免堆积注定失败的任务;
- autopilot 层调用门禁(MUL-3963/MUL-4525):确认触发主体对 assignee agent 有调用权限。
run_only 模式没有 create_issue 的审计轨迹豁免:若 agent 的 runtime 不在线,准入会在入队前记录一条 skipped run 并附带 failure_reason,直接返回。这是源码注释中 "触发时准入" 门禁的初衷——否则一个关机的笔记本电脑/离线 daemon 会让定时 autopilot 向 agent_task_queue 堆积成千上万条注定失败的任务。recordSkippedRun(约 L1438)会发出与正常终止一致的 WS/analytics 信号,把 skip 当作"成功但无操作"的派发处理。
八、实战调试路径与安全红线
把源码地图与上层 SKILL.md 的调试指引合并,得到一套可复制的排查顺序:
multica autopilot get <id> --output json—— 看 status、mode、assignee、triggers,以及can_write/can_manage_access;multica autopilot runs <id> --output json—— 看 run 的 status 与 failure reason(skipped+reason_code意味着准入拦截,别去查任务);- 若指派给 squad:
multica squad get <squad-id> --output json,确认队长在位——执行最终落在 leader 上; - 检查目标 agent/runtime:
multica agent get <agent-id> --output json与multica runtime list --output json; - Webhook 场景查投递状态:
queued表示 worker 尚未完成派发,failed携带 worker 错误;携带相同X-GitHub-Delivery/Idempotency-Key的 provider 重试会复用原 delivery,不会重复执行; create_issue模式若 run 记录了 issue,则去检查该 issue 的任务是否创建成功。
安全红线(源码地图与 SKILL 共同强调):
trigger、delete、trigger-delete、trigger-rotate-url以及向/api/webhooks/autopilots/{token}的任何调用都会产生真实副作用,不要拿它们"测试";- 仅在用户明确要求手动运行时才用
trigger;仅在需要轮换 Webhook URL 时才用trigger-rotate-url,旧 URL 立即作废; autopilot get --show-secrets的输出与 Webhook token 严禁进入日志、评论、文档或 PR。
九、继续深入的地图坐标
若要在源码中继续追这条链路,以下文件是权威坐标:
- 规则模型的完整派发与准入逻辑:server/internal/service/autopilot.go(
DispatchAutopilot/DispatchAutopilotForPlan/AdmitAutopilotWebhookDelivery/shouldSkipDispatch/resolveAutopilotLeader); - CLI 全命令与脱敏/提示逻辑:server/cmd/multica/cmd_autopilot.go;
- HTTP 路由暴露(含未认证 Webhook 入口):server/cmd/server/router.go;
- Webhook 落库、准入与租约 worker:server/internal/handler/autopilot_webhook.go、server/internal/handler/webhook_delivery_worker.go;
- cron 纯计算预览:server/internal/handler/autopilot_cron_preview.go;
- 协作者表结构:server/migrations/128_autopilot_collaborator.up.sql,前端入口 packages/views/autopilots/components/manage-access-dialog.tsx;
- 服务层测试(幂等、权限、配额等行为的可验证证据):server/internal/service/autopilot_test.go、server/internal/handler/autopilot_permissions_test.go、server/internal/handler/autopilot_webhook_handler_test.go。
值得强调的是,本仓库的 Autopilot 能力是内置技能的一部分,为"Agent 辅助人类运维自家自动化"提供了可被 Agent 正确消费的源码索引——这也是 SKILL.md 把"先读源码地图、再做变更"列为黄金法则的原因:理解 autopilot_run 的生命周期、webhook_delivery_id 的幂等语义与 View/Write 权限分层,是安全操作 Autopilot、并把人类与 Agent 真正拧成一股绳的前提。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00