云端 goal/merge/review 三事件:goal 循环 + PR 事件通知

此前 goal-unmet/merge-failed/review-requested 只有允许列表,
云端没有生产者(概念只在本地 host.py)。本轮给三者接真生产者:
- goal:POST /tasks 接受 goal/verify_command/goal_max_rounds
  (迁移补列,SQLite 与 Postgres 两支);带 verify_command 的
  任务走 host.py goal 循环(Local/Docker 执行器 goal_task,
  turn→验测→喂回重试,缺省 3 轮);达标 completed,未达标
  failed 且只发 goal-unmet、不重复发 failed;goal 模式下 worker
  级重试/fallback 关闭,同任务 /retry 会撞 host 任务名已存在
  (已在文档写明,须新建任务);CloudExecutor 无 goal 概念,
  如实失败不降级
- review-requested:POST /tasks/{id}/pr 建 PR 成功时发
  (payload 含 pr_url/pr_number)
- merge-failed:PR 落盘(push/建 PR)失败 502 时发(payload
  含 step/error),任务保持 merged、可重调 /pr;跳过时不发
测试:host 29、cloud 469 全过。
This commit is contained in:
host committed 2026-10-26 18:11:12 +08:00
1 parent 26e73124fb
commit 5ee9d291dd
8 files changed
+965 -27

No files matched your search

+18 -4
View File
@@ -227,8 +227,10 @@ webhook 实测)。
webhook 投 JSON(event_type、project_id、task_id、payload),
slack 投 Slack incoming webhook 格式的一行文本(任务 id +
事件 + 项目,失败附 error 摘要)。任务终态(completed /
failed)由 worker 在 run_one 的终态写回后自动通知订阅者;
手动分发走 `POST /admin/push/dispatch`,同样经内置发送端。
failed / goal-unmet)由 worker 在 run_one 的终态写回后自动
通知订阅者;PR 端点在建 PR 成功时发 review-requested、
落盘失败时发 merge-failed;手动分发走
`POST /admin/push/dispatch`,同样经内置发送端。
显式注入的 sender 优先于内置端。
- webpush:需要 VAPID 密钥对(私钥绝不进仓库),且需要
浏览器端订阅(endpoint + p256dh + auth,现有订阅模型只有
@@ -364,7 +366,7 @@ psycopg,写明“需安装 psycopg”),绝不静默回落到 SQLite,
| `GET /health` | 无需认证 | `200 {"ok": true, "service": "cloud-control-plane"}` |
| `POST /projects` | 认证即可 | body `{name, id?}`;无 id 则生成 uuid;调用者记为 owner(调 `AuthStore.create_project`,同时写 projects 表并记审计 `create_project`);返回 `201` + 项目 |
| `GET /projects` | 认证即可 | 只返回调用者是成员的项目:`{"projects": [...]}` |
| `POST /tasks` | `create_task` | body `{project_id, prompt, depends_on?, priority?, idempotency_key?}`;无权限 `403`;同 `project_id + idempotency_key` 已存在则返回已有任务(`200`,不建重复);否则建 `queued` 任务并记审计 `create_task`,返回 `201` |
| `POST /tasks` | `create_task` | body `{project_id, prompt, depends_on?, priority?, idempotency_key?, executor?, goal?, verify_command?, goal_max_rounds?}`;无权限 `403`;同 `project_id + idempotency_key` 已存在则返回已有任务(`200`,不建重复);否则建 `queued` 任务并记审计 `create_task`,返回 `201`;`verify_command` 非空即 goal 模式(见下文“goal 模式”),`goal_max_rounds` 缺省 3、须 ≥1 |
| `GET /tasks?project_id=...` | 该项目 `read` | 返回该项目任务列表:`{"tasks": [...]}`;无权限 `403` |
| `GET /tasks/{id}` | 该任务所属项目 `read` | 无权限 `403`;不存在 `404` |
| `POST /tasks/{id}/cancel` | 该项目 `create_task` | `queued` / `running` 改为 `interrupted` 并记审计 `cancel_task`;已是终态(completed / failed / interrupted / merged / merge-failed)返回 `400`;不存在 `404` |
@@ -386,7 +388,7 @@ psycopg,写明“需安装 psycopg”),绝不静默回落到 SQLite,
| `POST /projects/{id}/push/subscriptions` | 该项目 `create_task` | 记订阅 body `{channel, target, preference?}`,channel 为 webhook/slack/webpush/apns/fcm,preference 为 instant/digest/silent,返回 `201` |
| `GET /projects/{id}/push/subscriptions` | 该项目 `read` | 只列调用者自己的订阅(不泄露他人 target) |
| `DELETE /projects/{id}/push/subscriptions/{sid}` | 该项目 `create_task` | 只能删自己的订阅,项目 owner 可删任意;不存在 `404`,他人订阅 `403` |
| `POST /admin/push/dispatch` | 该项目 `read` | body `{event_type, project_id, payload?}`;只发状态变化事件(completed/failed/goal-unmet/merge-failed/review-requested);sender=None 时用内置真实发送端(webhook/slack 真 POST,webpush/apns/fcm 报缺凭证),都不静默成功;worker 在任务终态(completed/failed)时自动调分发,不依赖手动调本端点 |
| `POST /admin/push/dispatch` | 该项目 `read` | body `{event_type, project_id, payload?}`;只发状态变化事件(completed/failed/goal-unmet/merge-failed/review-requested);sender=None 时用内置真实发送端(webhook/slack 真 POST,webpush/apns/fcm 报缺凭证),都不静默成功;自动触发:worker 在任务终态时调分发(completed/failed/goal-unmet,见“goal 模式”),PR 端点在建 PR 成功时发 review-requested、落盘失败时发 merge-failed,不依赖手动调本端点 |
| `POST /webhooks/github` | GitHub 签名 | 见“webhook 端点” |
| `POST /webhooks/slack` | Slack 签名 | 见“webhook 端点” |
@@ -823,6 +825,18 @@ interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳
rowcount == 1);认领日志与审计里含 executor。注意
`_executor_name` 的修正:CloudExecutor 以前会被算成 local,
现在明确返回 cloud,否则 cloud worker 会认领 local 池的任务。
- goal 模式(`POST /tasks` 带 `verify_command` 即开启,本地
host.py `goal` 循环的云端版):worker 不走普通 dispatch,
改调执行器的 goal_task(local/Docker 均已接,CloudExecutor
没有 goal 概念、goal 任务派给它会如实失败,不静默降级);
循环内每轮 turn 后跑验收命令、失败输出喂回下一轮,最多
`goal_max_rounds` 轮(缺省 3,与 host.py 默认一致),goal
文本缺省用 prompt。达标时任务 completed;未达标时任务
failed(错误文本为最后一轮验收输出),且只发 goal-unmet
推送、不再重复发 failed。goal 模式下 worker 级重试与模型
fallback 关闭(循环内部已重试,再叠加只会污染错误文本);
goal 任务失败后不要用 `/retry` 同任务重跑——host 任务名由
任务 id 派生,重跑会撞“任务已存在”而失败,应新建任务。
- CloudExecutor 通用 runner 接口(厂商无关,`cloud/sandbox.py`):
本轮不假装接通某个云厂商(ECS、Cloud Run 等专属适配明确不在
范围)。CloudExecutor 接受一个 runner endpoint(显式参数或