Files

15 KiB
Raw Permalink Blame History

Gitea Webhook 驱动 ccweb 工作流 PRD

文档状态已确认需求基线MVP
适用范围:单个 Gitea 实例、一个全局 ccweb-bot 账号
相关设计:ARCHITECTURE.md

1. 背景与目标

本功能把 Gitea Issue/PR 的普通评论作为 ccweb 的任务入口。用户在评论中 提及 @ccweb-botcc-web 验证 Webhook、准备持久工作区、复用该 Issue/PR 的独立 Codex App 会话,并通过官方 gitea-mcp 完成代码研究、修改 和回帖。Gitea 是唯一主交互入口cc-web 页面只提供镜像、运维和审计能力。

目标:

  1. 让用户无需离开 Gitea 即可驱动一次或连续多轮工程任务。
  2. 保证同一仓库的工作区和任务严格串行不同仓库可并行MVP 不设置额外的 全局并发上限。
  3. 在进程重启、网络抖动、MCP 回帖失败时,任务状态、会话和回执仍可恢复、 重试且不重复执行或重复回帖。
  4. 对 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 Appcodexapp+ yolo,不在 Gitea 交互中暴露模式切换
MCP 官方 gitea-mcpstdio 传输;线程级注入 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-mcp stdio 配置和 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_commentpull_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,其中 kindissuepull_request;同一资源永远复用同一个 threadId
  • 每个新任务创建一个 turnId;持久化 threadId/turnId、启动时间、最后事件 序号和重试次数,支持断线后恢复监听。
  • 启动参数固定为 codexappyolomodelreasoning_effortdeveloper_instructions 放入 collaborationMode.settings(若运行时启用 collaborationMode不得在顶层重复传递。
  • thread/start.config.mcp_servers.gitea 线程级注入官方 gitea-mcp stdio每 个线程使用对应实例的 host/token不使用进程全局来源会话变量替代。
  • Prompt 必须包含仓库/资源标识、评论上下文、工作区路径、预期回帖格式和安全 边界;评论正文视为不可信输入,不得覆盖系统约束。

FR-06 Gitea 交互与回帖确认

  • 合法任务只发送一条起始 giteabot: 已收到 回执;运行中、等待用户和成功状态 不再额外发送评论,避免污染 Issue/PR 讨论串。回执仍带不可见关联标记 taskIdstate、可选 turnId)用于去重和审计。
  • 正常最终答案必须由 gitea-mcp 的 Issue/PR comment 工具发送cc-web 记录 MCP 返回的 commentId 或工具调用结果,并通过 Gitea 查询确认实际存在。
  • 确认必须同时匹配 resourceKeytaskIdturnId(或等价不可见标记)和 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 queuedabort running turn 必须幂等,响应包含当前状态和操作者。
  • 审计事件至少包含 eventId、taskId、sessionKey、actor、action、fromState、 toState、deliveryId、turnId、commentId、errorCode、timestamp;支持按仓库、 资源、任务和时间范围查询。

6. 状态机

6.1 任务状态

状态 含义 可转移
received 已验签并完成事件规范化 queuedignoredduplicaterejected
queued 等待仓库锁和全局槽位 preparingcancelled
preparing 正在登记仓库、clone/fetch、构造线程 runningblocked_workspaceretry_waitfailed
running Codex App turn 执行中 waiting_userverifying_replyretry_waitabortingfailed
waiting_user Agent 需要用户补充,保留会话 queued(新评论)、cancelledfailed
verifying_reply 已收到 turn 完成,等待确认最终评论 succeededretry_waitfailed_reply
retry_wait 到达退避时间,等待有限次重试 preparingrunningverifying_replyfailed
blocked_workspace 工作区脏/冲突/权限问题 queued(管理员解除后)、cancelled
aborting 已请求取消,等待 Codex App 确认 abortedfailed
succeeded 最终回帖已确认 终态
succeeded_with_rest_fallback 使用 REST 兜底回帖成功 终态
failed / failed_reply 执行或回帖不可恢复失败 终态
cancelled / aborted 排队取消或运行中止 终态
ignored / duplicate / rejected 未触发、重复或安全拒绝 终态

6.2 状态不变量

  1. 只有 preparing/running/aborting 持有仓库锁;同一 repoKey 同时最多一个 运行中任务。
  2. 终态任务不得创建新 turn、状态评论或重试重复 Webhook 只能返回原任务。
  3. verifying_reply 必须引用本轮 turnIdfailed_reply 不代表代码执行必然 失败,必须在审计中区分 executionErrorreplyError
  4. 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、幂等和回帖 确认约束。