Files

256 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`、幂等和回帖
确认约束。