15 KiB
15 KiB
Gitea Webhook 驱动 ccweb 工作流 PRD
文档状态:已确认需求基线(MVP)
适用范围:单个 Gitea 实例、一个全局ccweb-bot账号
相关设计:ARCHITECTURE.md
1. 背景与目标
本功能把 Gitea Issue/PR 的普通评论作为 ccweb 的任务入口。用户在评论中
提及 @ccweb-bot 后,cc-web 验证 Webhook、准备持久工作区、复用该
Issue/PR 的独立 Codex App 会话,并通过官方 gitea-mcp 完成代码研究、修改
和回帖。Gitea 是唯一主交互入口;cc-web 页面只提供镜像、运维和审计能力。
目标:
- 让用户无需离开 Gitea 即可驱动一次或连续多轮工程任务。
- 保证同一仓库的工作区和任务严格串行,不同仓库可并行;MVP 不设置额外的 全局并发上限。
- 在进程重启、网络抖动、MCP 回帖失败时,任务状态、会话和回执仍可恢复、 重试且不重复执行或重复回帖。
- 对 Webhook、Bot Token、工作区写入和每次工具调用提供可追溯审计,并允许运 维人员暂停、取消排队和中止运行中的 turn。
2. 已确认决策
| 主题 | 决策 |
|---|---|
| Gitea 实例 | MVP 只支持一个配置的 Gitea 实例,instanceId=default |
| Bot | 一个全局 ccweb-bot,用于触发识别、起始回执和 REST 兜底 |
| 触发 | Issue 或 PR 的普通评论正文包含 @ccweb-bot;评论作者不能是 Bot 自身 |
| 自动接入 | 合法触发首次发现未登记仓库时自动登记并 clone;管理员可停用 |
| 工作区 | 可配置工作区根目录下按仓库持久 checkout,后续使用 fetch 更新 |
| 并发 | 同仓库串行;跨仓库并行;MVP 不设置全局运行上限 |
| Agent | 固定 Codex App(codexapp)+ yolo,不在 Gitea 交互中暴露模式切换 |
| MCP | 官方 gitea-mcp,stdio 传输;线程级注入 Gitea host/token |
| 会话 | 每个 Issue/PR 一个独立持久会话;后续同资源评论进入同一会话 |
| 交互 | Gitea 为主;cc-web 页面只展示状态、队列和审计,不替代 Gitea 对话 |
| 回复 | 先发一条“已收到”回执;正常最终答案由 gitea-mcp 发回原 Issue/PR;缺失时一次补触发,最后 REST 兜底 |
| 安全 | Webhook HMAC、delivery 去重、Bot 自评论过滤、Token/Secret 分离、审计 |
| 恢复 | queued 重启后继续;waiting_user 保留;running 视为中断并最多重试一次 |
| 运维 | 全局暂停、仓库停用、取消排队、中止 turn、查看审计 |
3. 范围
3.1 In Scope
- Gitea Webhook 接收、原始请求 HMAC 校验和 delivery 幂等。
- Issue/PR 普通评论事件的规范化、提及解析和 Bot 自评论抑制。
- 未登记仓库自动接入、持久 clone/fetch、脏工作区保护。
- 任务队列、同仓库锁、跨仓库并行。
- Issue/PR 独立会话与 Codex App turn 生命周期管理。
- 线程级官方
gitea-mcpstdio 配置和 Gitea 主交互。 - 起始“已收到”回执、MCP 最终回帖确认、一次补触发和 REST 兜底;不发送运行中、等待用户或成功状态评论。
- 暂停、停用、取消排队、中止、重启恢复、审计和可观测字段。
- 可供前端/运维使用的只读状态和控制接口契约(实现不在本 PRD 变更范围)。
3.2 Out of Scope
- 多 Gitea 实例、跨 Forge 统一协议和 GitHub/GitLab 适配。
- 非评论触发(Push、定时任务、Issue 创建、Actions 等)。
- 让普通用户在 cc-web 中切换模型、推理强度或 yolo/plan 模式。
- 自定义 dynamicTools 代替 MCP;手动填写 MCP 工具参数的 UI。
- 自动清理用户未提交修改、
reset --hard、强制覆盖其他会话的工作区。 - 细粒度按用户/路径的写权限策略(Bot 全写权限是已确认前提);只记录风险并提供暂停/审计。
4. 角色与核心场景
| 角色 | 能力 |
|---|---|
| Gitea 用户 | 在 Issue/PR 评论 @ccweb-bot,查看状态、追问、确认结果 |
ccweb-bot |
发送起始回执、作为 MCP/REST 身份回帖;Bot 自己的评论不会再次触发 |
| 运维管理员 | 全局暂停/恢复、仓库停用/启用、取消排队、中止 turn、查审计 |
| 系统 | 验签、去重、排队、clone/fetch、启动会话、确认回帖、重试和恢复 |
典型链路:
Gitea 普通评论 @ccweb-bot
│
▼
验签 → delivery 去重 → 解析 Issue/PR → 自动接入仓库
│ │
└────────────── 持久化任务并入队 ◄──┘
│
同仓库锁串行,跨仓并行 ────┘
▼
Codex App/yolo + gitea-mcp(stdio)
│
已收到回执 → MCP 最终回帖 → turnId/评论确认
│
补触发一次 → REST 兜底(必要时)
5. 功能需求
FR-01 触发与事件规范化
- 接受 Gitea 配置的 Issue/PR 评论 Webhook;事件适配器将不同版本的事件名称
归一为
issue_comment或pull_request_comment。 - 仅当正文包含独立 mention
@ccweb-bot时触发,mention 后的文本作为任务 指令;没有有效指令时仍创建可追踪任务并要求用户补充。 - 记录原始
deliveryId、事件类型、仓库、资源类型/编号、评论 ID、作者和 接收时间。无法解析的事件进入ignored,不启动 Agent。 - Bot 自己发出的状态/最终/兜底评论必须被识别并忽略,防止自触发环路。
FR-02 Webhook 安全与幂等
- 在读取或持久化业务字段前,使用原始 body 和配置 Secret 计算 HMAC-SHA256, 采用常量时间比较;验签失败返回 401,不入队。
deliveryKey = instanceId + ":" + deliveryId唯一;已处理或处理中重复 delivery 返回 200(或约定的幂等响应)但不创建新任务。- Secret、Bot Token、MCP 启动参数不得写入评论、普通日志或审计明文;审计只 记录凭据引用/哈希指纹。
FR-03 仓库自动接入与工作区
- 通过合法 Webhook 首次发现仓库时原子创建
RepositoryRecord;未登记不因 任务排队失败而丢失事件。 - 按
workspaceRoot/<instanceId>/<owner>/<repo>建立持久目录。首次使用clone,后续任务在仓库锁内fetch;凭据通过临时环境或 credential helper 注入,不把 Token 写入 remote URL。 - 检测到未提交修改、冲突或目录被外部占用时,不执行 reset/clean/覆盖;任务
进入
blocked_workspace,写入审计并等待管理员处理。 - PR 任务必须记录 base/head ref 和源仓库信息;Issue 任务使用仓库默认分支, 除非指令明确指定已允许的分支。
FR-04 队列、锁与并发
- 调度器维护持久任务队列;同
repoKey同时最多一个preparing/running任务,跨仓库可并行。 - 不设置额外全局运行信号量;
queued任务只受全局暂停和各自仓库锁影响,暂停时不再领取新任务。 - 队列顺序默认 FIFO;同一资源的新评论可合并为下一轮输入,但不得跳过已持久 化的任务或破坏评论顺序。
- 取消排队只允许作用于尚未启动的任务;中止操作必须向 Codex App 发送取消,
并等待
aborted或超时后释放锁。
FR-05 会话与 Agent 运行时
sessionKey = instanceId:owner/repo:kind:number,其中kind为issue或pull_request;同一资源永远复用同一个threadId。- 每个新任务创建一个
turnId;持久化threadId/turnId、启动时间、最后事件 序号和重试次数,支持断线后恢复监听。 - 启动参数固定为
codexapp与yolo。model、reasoning_effort、developer_instructions放入collaborationMode.settings(若运行时启用 collaborationMode),不得在顶层重复传递。 thread/start.config.mcp_servers.gitea线程级注入官方 gitea-mcp stdio;每 个线程使用对应实例的 host/token,不使用进程全局来源会话变量替代。- Prompt 必须包含仓库/资源标识、评论上下文、工作区路径、预期回帖格式和安全 边界;评论正文视为不可信输入,不得覆盖系统约束。
FR-06 Gitea 交互与回帖确认
- 合法任务只发送一条起始
giteabot: 已收到回执;运行中、等待用户和成功状态 不再额外发送评论,避免污染 Issue/PR 讨论串。回执仍带不可见关联标记 (taskId、state、可选turnId)用于去重和审计。 - 正常最终答案必须由 gitea-mcp 的 Issue/PR comment 工具发送;cc-web 记录
MCP 返回的
commentId或工具调用结果,并通过 Gitea 查询确认实际存在。 - 确认必须同时匹配
resourceKey、taskId、turnId(或等价不可见标记)和 Bot 作者,避免把旧评论当作本轮回执。 - 仅在确认 Gitea 评论查询结果为“缺失”时,发起一次“只补发回执、不重复修改”的
隐藏 turn;补发仍缺失才以 Bot Token 经 Gitea REST 发布最终正文。查询结果为
unknown时不自动重试或覆盖,任务进入failed_reply,由终态兜底回执告知用户, 避免把已经成功落库的 MCP 回复重复发布。
FR-07 重试、恢复与状态一致性
- 网络/进程级暂时错误使用持久
retry_wait和指数退避;执行 turn 因重启被 中断时最多自动重试一次,禁止无界重试。 queued重启后恢复调度;waiting_user保留原会话等待新评论;running恢复为retry_wait并带interrupted_by_restart原因;终态不重新执行。- 所有状态变更、评论发送、MCP 工具调用和重试都追加审计事件,状态写入遵循 版本号/单调序列,防止旧事件覆盖新状态。
FR-08 运维控制与审计
- 全局
pause/resume:暂停只阻止新任务领取,已运行任务继续或由管理员另行中止。 - 仓库
disable/enable:停用后拒绝该仓库的新触发并保留历史;正在运行的任务 不强制杀死。 cancel queued、abort running turn必须幂等,响应包含当前状态和操作者。- 审计事件至少包含
eventId、taskId、sessionKey、actor、action、fromState、 toState、deliveryId、turnId、commentId、errorCode、timestamp;支持按仓库、 资源、任务和时间范围查询。
6. 状态机
6.1 任务状态
| 状态 | 含义 | 可转移 |
|---|---|---|
received |
已验签并完成事件规范化 | queued、ignored、duplicate、rejected |
queued |
等待仓库锁和全局槽位 | preparing、cancelled |
preparing |
正在登记仓库、clone/fetch、构造线程 | running、blocked_workspace、retry_wait、failed |
running |
Codex App turn 执行中 | waiting_user、verifying_reply、retry_wait、aborting、failed |
waiting_user |
Agent 需要用户补充,保留会话 | queued(新评论)、cancelled、failed |
verifying_reply |
已收到 turn 完成,等待确认最终评论 | succeeded、retry_wait、failed_reply |
retry_wait |
到达退避时间,等待有限次重试 | preparing、running、verifying_reply、failed |
blocked_workspace |
工作区脏/冲突/权限问题 | queued(管理员解除后)、cancelled |
aborting |
已请求取消,等待 Codex App 确认 | aborted、failed |
succeeded |
最终回帖已确认 | 终态 |
succeeded_with_rest_fallback |
使用 REST 兜底回帖成功 | 终态 |
failed / failed_reply |
执行或回帖不可恢复失败 | 终态 |
cancelled / aborted |
排队取消或运行中止 | 终态 |
ignored / duplicate / rejected |
未触发、重复或安全拒绝 | 终态 |
6.2 状态不变量
- 只有
preparing/running/aborting持有仓库锁;同一repoKey同时最多一个 运行中任务。 - 终态任务不得创建新 turn、状态评论或重试;重复 Webhook 只能返回原任务。
verifying_reply必须引用本轮turnId;failed_reply不代表代码执行必然 失败,必须在审计中区分executionError与replyError。waiting_user的新评论创建新taskId,但复用同一sessionKey/threadId。
7. 非功能需求与风险
| 类别 | 要求 |
|---|---|
| 安全 | HMAC 常量时间校验、凭据隔离、Bot 自触发抑制、敏感字段脱敏 |
| 一致性 | delivery/task/session/turn/comment 均有唯一键;状态变更可重放且幂等 |
| 可恢复性 | 进程重启不丢队列、会话映射和回帖确认上下文 |
| 可观测性 | 结构化日志 + 审计事件 + task/session/turn 关联 ID |
| 性能 | Webhook 验签与入队不等待 Agent 完成;并发只受仓库锁约束 |
| 数据保留 | 任务/会话/审计至少保留到管理员显式清理策略生效;工作区持久保留 |
| 风险 | gitea-mcp 全写权限意味着 Bot 能修改代码和 Gitea 内容;通过 HMAC、暂停、 |
| 中止、审计和脏工作区保护降低风险,不声称实现细粒度授权 |
8. 验收标准与需求覆盖矩阵
每个验收项都应能在集成测试、协议 mock 或只读审计中独立验证。
| ID | 验收标准 | 覆盖需求/设计 |
|---|---|---|
| AC-01 | 合法 Issue 普通评论含 @ccweb-bot 创建任务;无 mention、非评论或 Bot 自评论不创建任务 |
FR-01 |
| AC-02 | HMAC 错误返回 401 且无业务写入;相同 delivery 重放不增加任务数 | FR-02 |
| AC-03 | 首次合法触发自动登记仓库并 clone;再次触发只 fetch;脏工作区不被 reset/覆盖 | FR-03 |
| AC-04 | 同仓库两个任务严格串行;两个仓库可并行;不额外施加全局并发上限 | FR-04 |
| AC-05 | 同一 Issue/PR 多条评论复用一个 threadId,每条任务有唯一 turnId |
FR-05 |
| AC-06 | Codex App 运行时固定 codexapp+yolo;官方 gitea-mcp 以线程级 stdio 配置启动 |
FR-05 |
| AC-07 | 合法任务仅有一条“已收到”起始回执;成功正文由 MCP/REST 兜底承担,失败或控制终态最多一条结果回执,Bot 评论不会回触发 | FR-06 |
| AC-08 | MCP 最终回帖能按 taskId+turnId+resourceKey 确认;缺失只补发一次,随后 REST 兜底 |
FR-06 |
| AC-09 | 重启后 queued 继续、waiting_user 保留、running 最多重试一次;终态不重跑 | FR-07 |
| AC-10 | pause 阻止新领取;仓库 disable 拒绝新任务;cancel/abort 幂等且释放相应资源 | FR-08 |
| AC-11 | 审计可按 task/session/repo 查询,包含状态迁移、delivery、turn、comment 和错误字段 | FR-08 |
| AC-12 | PRD 与 ARCHITECTURE 的状态枚举、字段名、默认并发、回帖错误语义完全一致 | 文档一致性 |
| AC-13 | 生产挂接包含 server.js 路由、lib/gitea-workflow-* 核心模块、管理页与离线回归;不新增第三方依赖 |
交付约束 |
9. 交付边界与后续演进
当前仓库已提供 MVP 实现:POST /api/gitea/webhook、持久任务/队列、工作区维护、
Codex App 线程级 gitea-mcp 注入、回帖确认/兜底、管理 API/UI 和离线回归。实现与
测试必须继续以本 PRD 的 FR/AC 编号作为审计索引。
后续可独立评估:多实例、细粒度授权、审批门、工作区隔离容器、任务预算、
SSE/前端实时镜像和更多 Forge 适配;这些不改变 MVP 的 sessionKey、幂等和回帖
确认约束。