256 lines
15 KiB
Markdown
256 lines
15 KiB
Markdown
# Gitea Webhook 驱动 ccweb 工作流 PRD
|
||
|
||
> 文档状态:已确认需求基线(MVP)
|
||
> 适用范围:单个 Gitea 实例、一个全局 `ccweb-bot` 账号
|
||
> 相关设计:[ARCHITECTURE.md](./ARCHITECTURE.md)
|
||
|
||
## 1. 背景与目标
|
||
|
||
本功能把 Gitea Issue/PR 的普通评论作为 ccweb 的任务入口。用户在评论中
|
||
提及 `@ccweb-bot` 后,cc-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 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-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、启动会话、确认回帖、重试和恢复 |
|
||
|
||
典型链路:
|
||
|
||
```text
|
||
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 状态不变量
|
||
|
||
1. 只有 `preparing/running/aborting` 持有仓库锁;同一 `repoKey` 同时最多一个
|
||
运行中任务。
|
||
2. 终态任务不得创建新 turn、状态评论或重试;重复 Webhook 只能返回原任务。
|
||
3. `verifying_reply` 必须引用本轮 `turnId`;`failed_reply` 不代表代码执行必然
|
||
失败,必须在审计中区分 `executionError` 与 `replyError`。
|
||
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`、幂等和回帖
|
||
确认约束。
|