Files
cc-web/.planning/2026-08-23-gitea-workflow/protocol-research.md

17 KiB
Raw Blame History

Gitea Webhook 与官方 gitea-mcp 协议研究

研究性质:只读核验;未修改 server.js、lib/、public/。

核验时间:2026-08-24(Asia/Shanghai)。

版本口径:Gitea 文档采用 versioned 1.24 文档;Gitea 源码采用 GitHub go-gitea/gitea 提交 4852091e851060d2233fcbf789727b1c5dba4bfc (2026-08-23);gitea-mcp 当前 main 为 815a0e26aab26c2df0963f7fca172a6f5b4a2e41(2026-08-23),最新正式发行版 为 v1.6.0,其提交为 290d06b40b9fd225e53ec9fa4be1431dc787d0ff (2026-07-30)。当前 main 已切换到官方 MCP Go SDK,和 v1.6.0 的依赖/实现 不完全相同;部署时应固定一个已验证的二进制或提交,不要隐式跟随 main。

1. 官方来源

Gitea

gitea-mcp

2. Gitea Webhook 协议

2.1 请求、事件头和签名

Gitea Webhook 通常向目标 URL 发送 JSON POST。官方文档的示例明确要求 POST、Content-Type: application/json,并给出以下头:

X-Gitea-Delivery: <UUID>
X-Gitea-Event: <event>
X-Gitea-Signature: <hex SHA-256 HMAC>

当前 Gitea 源码 services/webhook/deliver.go 还发送:

X-Gitea-Event-Type: <internal event type>
X-Gitea-Hook-Installation-Target-Type: <system|repository|organization|user|default>
X-Gogs-*                         # 兼容 Gogs
X-Hub-Signature: sha1=<hex>
X-Hub-Signature-256: sha256=<hex>
X-GitHub-*                       # 兼容 GitHub

事件名有两个层次,不能混淆:

  • Issue 普通评论:X-Gitea-Event: issue_comment,内部类型也是 issue_comment。
  • PR 普通评论:X-Gitea-Event: issue_comment,但当前源码的 X-Gitea-Event-Type 为 pull_request_comment。

因此接收端应优先使用 X-Gitea-Event 判断大类,再结合 payload 的 is_pull/pull_request 或 X-Gitea-Event-Type 区分 Issue 与 PR;不要只依赖 某个新版本才出现的扩展头。

配置 Secret 后,当前发送端对原始请求 body 字节计算:

hex(HMAC-SHA256(secret, body))

X-Gitea-Signature 是不带 sha256= 前缀的小写十六进制串;源码同时生成 GitHub 兼容的 sha256=<hex> 形式。接收端必须在 JSON 解析或重新格式化之前读取 原始 body,并使用常量时间比较(如 Node timingSafeEqual)。文档中的 PHP 示例 先 trim 后计算,只是示例代码;对本项目应以实际收到的 body 字节为准,不能依赖 JSON 等价性或 trim 后再验签。

Webhook payload 中历史 secret 字段自 Gitea 1.13 起已弃用,文档标注将移除; 不能把 payload 的 secret 当作认证依据。应使用独立配置的 HMAC Secret。

实现约束: Webhook 路由应限制 body 大小和 application/json,先验签、再解析, 验签失败返回 4xx;合法请求在持久化 delivery 与任务后尽快返回 2xx,不能把整个 Codex turn 放在 HTTP 请求内同步执行。

2.2 Delivery UUID 与去重

X-Gitea-Delivery 是 Gitea 为每次发送生成的 UUID。官方文档介绍了 Webhook 设置页的 Recent Deliveries,但没有规定接收方的去重窗口、重试次数、重试退避、 投递顺序或“同一 delivery 是否只会出现一次”。Gitea 源码的 HookTask.UUID 在 发送端数据库中唯一,但那是 Gitea 自己的出站任务记录,不是对外接收方的幂等协议。

本项目必须自行实现:

  1. 在验签之后、入队之前,以 (Gitea 实例标识, X-Gitea-Delivery) 建立持久唯一键。
  2. 采用原子“插入/已存在”语义;重复请求直接返回成功,不重复 clone、启动 turn 或 回帖。
  3. delivery 记录应包含原始 headers/body 摘要、接收时间、解析结果和最终任务状态, 便于审计与恢复。
  4. 由于官方未承诺顺序,队列按仓库串行化;不要按 delivery UUID 推断先后。

2.3 Issue/PR 普通评论 payload

当前 Gitea modules/structs/hook.go 定义的 IssueCommentPayload 为:

{
  "action": "created|edited|deleted",
  "issue": { "number": 123, "title": "...", "body": "...", "user": {}, "repository": {} },
  "pull_request": { "number": 123, "title": "...", "user": {}, "base": {}, "head": {} },
  "comment": {
    "id": 456,
    "html_url": "...",
    "pull_request_url": "...",
    "issue_url": "...",
    "user": {},
    "body": "评论正文",
    "assets": [],
    "created_at": "...",
    "updated_at": "..."
  },
  "changes": { "body": { "from": "旧正文" } },
  "repository": { "id": 1, "name": "repo", "full_name": "owner/repo", "clone_url": "..." },
  "sender": { "id": 2, "login": "user" },
  "is_pull": true
}

字段语义:

  • action=created 是新增普通评论;edited/deleted 是评论编辑/删除。
  • Issue 评论:is_pull=false,通常没有 pull_request 对象。
  • PR 普通评论:is_pull=true,当前官方测试确认 pull_request_comment 事件的 payload 类型仍是 IssueCommentPayload,并带 pull_request 对象;评论入口仍是 PR 所对应的 Issue number。
  • comment.body 是 Markdown 文本;comment.id 是回查/审计评论的稳定标识。
  • changes.body.from 只对编辑动作有意义。
  • repository.full_name 是最适合用作工作流仓库键的字段,但仍应做格式校验并拒绝 路径穿越/不合法 owner 或 repo。

本项目触发过滤建议: MVP 只消费 action=created,并在 comment.body 中按 明确规则识别 @ccweb-bot;编辑/删除不应重新触发任务。还必须在入队前过滤 sender 为本项目 Bot 的自评论,否则“最终回帖 → Webhook → 新任务”会形成回路。 Bot 判断应使用稳定用户 ID/登录名配置,而不是只匹配正文。

3. Gitea REST/API 回执协议

3.1 普通评论写入

官方 API 1.24 文档的新增评论接口为:

POST /api/v1/repos/{owner}/{repo}/issues/{index}/comments
Authorization: token <personal-access-token>
Content-Type: application/json

{"body":"评论正文"}

返回的是 Comment 对象。PR 普通评论同样使用该 Issue 评论接口(index 为 PR 编号);PR review/diff inline comment 是另一套 review API,不能用来代替普通状态 回帖。gitea-mcp 的 issue_write 中 method=add_comment 正是这个普通评论路径, 而 pull_request_review_write 的 reply_comment 针对 review comment 线程。

Gitea API 文档说明支持 Authorization: token ...,OAuth token 也接受 Authorization: bearer ...;URL query token 虽受支持,但本项目禁止把 token 放在 URL、日志或评论中。Bot Token 与 Webhook HMAC Secret 必须分离。

3.2 分页、响应上限和错误

  • Gitea 默认 [api].MAX_RESPONSE_ITEMS=50;可由实例配置改变。
  • API 使用 page 与 limit 分页,并在有更多页时返回 Link;还可返回 x-total-count。
  • gitea-mcp 对外参数多使用 page/per_page,SDK/API 请求会被 Gitea 的 MAX_RESPONSE_ITEMS 静默截断;不能把 per_page 当作无上限。
  • Gitea 文档没有给出适用于所有实例的固定速率限制数字。反向代理、云服务或实例 配置可能另行限流;实现应对 429/5xx 做有上限的退避,并记录 Retry-After,不要 假设一定存在该头。
  • gitea-mcp 源码的 REST helper 使用 60 秒 HTTP client timeout;没有看到自动 重试逻辑。工具调用失败通常编码为 MCP tools/call 结果的 isError=true,而 malformed request/server fault 才是 JSON-RPC error。Codex App 监听时必须同时处理 这两类失败。

实现影响: 正常回执优先由 MCP issue_write/add_comment 发送;cc-web 必须通过 API 查询或 Webhook 回流确认 comment ID/正文确实出现。若 turn 完成但没有真实回帖, 只允许同一会话发起一次“只补发回执、不重复修改”的 turn;仍失败时才用上面的 REST endpoint 兜底,并把 HTTP 状态、响应摘要和是否产生 comment ID 写入审计。

4. 官方 gitea-mcp(stdio)

4.1 启动与配置

官方 README 给出的 stdio 形状是:

gitea-mcp -t stdio -H https://gitea.example.com

通过环境传 Token(推荐,避免出现在进程列表):

GITEA_HOST=https://gitea.example.com
GITEA_ACCESS_TOKEN=<token>

当前 cmd/cmd.go 支持的关键参数/环境变量:

用途 参数 环境变量/默认值
传输 -t, --transport MCP_MODE;默认 stdio
Gitea 地址 -H, --host GITEA_HOST;空时 https://gitea.com
Token -T, --token GITEA_ACCESS_TOKEN;也支持 GITEA_ACCESS_TOKEN_FILE
只读 -r, --read-only GITEA_READONLY=true
工具白名单 -O, --tools GITEA_TOOLS
Scope 白名单 -S, --scope GITEA_SCOPES
调试/TLS -d, -k GITEA_DEBUG=true、GITEA_INSECURE=true
HTTP 备用传输 -b, -p 默认 bind 全接口、port 8080
附件内联上限 --max-inline-attachment-bytes GITEA_MAX_INLINE_ATTACHMENT_BYTES;默认 5 MiB

参数优先级以源码为准:显式 token 参数优先于环境;没有显式 token 时读取 GITEA_ACCESS_TOKEN,再尝试 GITEA_ACCESS_TOKEN_FILE。项目应使用固定版本的本地 二进制作为 stdio command,并将 GITEA_HOST/GITEA_ACCESS_TOKEN 放在线程级 MCP 进程环境,不要放到全局长驻 app-server 环境或命令行 -T。当前 main 的 go.mod 要求 Go 1.26.0/toolchain 1.26.6;因此不建议在本项目运行时用未锁定的 go run ...@latest(本机旧 Go 或上游变更都会导致不可复现)。

stdio 模式日志写入 $HOME/.gitea-mcp/gitea-mcp.log(目录 0700、滚动保留);源码 只在 HTTP 模式把日志复制到 stdout。不要把任何调试输出写入 stdio 的协议 stdout, 否则会污染 MCP JSON-RPC 流。

建议的线程级配置形状(概念示例):

{
  "command": "/opt/cc-web/bin/gitea-mcp-v1.6.0",
  "args": ["-t", "stdio", "-H", "https://gitea.example.com"],
  "env": { "GITEA_ACCESS_TOKEN": "<线程专用 Bot Token>" }
}

4.2 工具和写操作

当前 README 工具表共 54 个工具;未指定 -r、-S、-O 时全部暴露。-r 会隐藏 所有 Write 工具;-S 与 -O 是并集筛选。动作型工具通过 method 参数再选择 具体操作。

主要只读工具包括:用户/组织(get_me、get_user_orgs)、搜索(search_*)、 通知读取、标签/里程碑/wiki/计时/包读取、Issue 列表/读取/附件读取、PR 列表/读取、 Actions 配置/运行读取、仓库列表/tree、文件/目录读取、分支/标签/提交/Release 读取、 版本查询。

Write 工具及其源码声明的实际操作如下:

工具 写操作
notification_write mark_read、mark_all_read
label_write repo/org label 的 create、edit、delete
milestone_write create、update/edit、delete
wiki_write create、update、delete 页面
timetracking_write start/stop/delete stopwatch、add/delete time
package_write 删除 package version(不可逆)
issue_write create、update、add_comment、edit_comment、add/remove/replace/clear labels
pull_request_write create、update、close、reopen、merge、update_branch、add/remove reviewers
pull_request_review_write create/submit/delete/dismiss review、reply_comment、resolve/unresolve thread
actions_config_write Actions secret/variable 的 upsert/create/update/delete(repo/org)
actions_run_write dispatch、cancel、rerun workflow run
create_repo/fork_repo 创建/派生仓库
create_or_update_file/delete_file 创建、更新、删除仓库文件
create_branch/delete_branch 创建、删除分支
create_tag/delete_tag 创建、删除 tag
create_release/delete_release 创建、删除 Release

这远超“回帖”所需权限。当前产品决策明确要求 MVP 开放官方 gitea-mcp 全部写权限, 因此必须同时具备全局暂停、仓库停用、排队取消、turn 中止和完整审计;不能把“工具 调用成功”当作安全授权。生产环境仍应为 Bot Token 配置最小可用仓库/组织权限,并在 管理面明确显示高风险工具可用。

4.3 HTTP 备用模式的限制(不是本 MVP 主路径)

当前 main 的 HTTP transport 使用 /mcp,只接受 POST,启用 Origin 校验,且始终 无状态:不使用 Mcp-Session-Id、独立 SSE 或 Last-Event-ID 恢复。反向代理必须原样 转发 Mcp-Protocol-Version、Mcp-Method、Mcp-Name。HTTP 请求可用 Authorization: Bearer <token> 或 Authorization: token <token>,这是逐请求的 Gitea credential passthrough,不是 MCP OAuth。HTTP MCP 默认请求体上限由源码提高到 32 MiB。若未来不用 stdio 而改 HTTP,必须额外处理这些协议与反代约束。

5. 对 cc-web 工作流的落地影响

  1. Webhook 接收顺序固定为: 限制请求 → 读取原始 body → HMAC 验签 → 校验 X-Gitea-Delivery/事件头 → JSON 解析 → Bot 自评论过滤 → action=created 与 @ccweb-bot 过滤 → 持久化唯一 delivery → 快速 2xx → 入队。
  2. 会话键: 使用 gitea + repository.full_name + issue/PR 类型 + number;PR 普通评论仍使用 issue_write/add_comment,不是 review reply。
  3. 回执确认: MCP 最终输出不等于 Gitea 已写入;必须查询评论列表/回流 Webhook 确认正文和 comment ID。缺失时最多一次隐藏补发 turn,最后 REST 兜底。
  4. 凭据隔离: HMAC Secret 只用于入站验签,Bot Token 只用于 MCP/REST/Git;Token 通过线程级 env 注入,禁止写入 remote URL、任务正文、日志和 URL query。
  5. 并发和恢复: delivery 去重、任务状态、仓库锁和回执审计必须持久化;同仓库 串行、不同仓库按全局上限并行。进程重启后 queued 恢复,running 按既定最多一次重试, waiting_user 保留。
  6. API 稳定性: 对 429、5xx、网络超时做有限退避;尊重 Gitea 的分页上限和 MAX_RESPONSE_ITEMS;MCP 的 isError=true 与 JSON-RPC error 均视为失败路径。

6. 不确定项与需集成验收的内容

  • 官方文档没有承诺 Webhook 的重试次数、退避策略、投递顺序或接收端去重语义;需在 实际 Gitea 版本通过失败投递/Recent Deliveries 做黑盒验收。
  • X-Gitea-Event-Type、部分 payload 字段和旧版本兼容头可能随 Gitea 版本变化;接收 端必须允许缺失扩展头,并以稳定字段/大类事件降级。
  • API 速率限制取决于 Gitea 版本、实例配置、云服务或反向代理;当前官方资料未提供 可直接套用的全局数值。
  • Token 的精确 scope/仓库权限由目标 Gitea 实例决定;需要用目标 Bot Token 实测 issue_write/add_comment、读取评论、读取 PR/文件和必要的仓库操作。
  • 当前 gitea-mcp main 要求 Go 1.26,最新发行版与 main 的 MCP SDK/HTTP 实现 不同;必须在 CI/部署机锁定并做 stdio initialize、tools/list、真实 issue_write 和错误返回回归。
  • Gitea Webhook 文档示例 PHP 使用 trim 后验签,与当前发送源码“对原始 body 签名” 的实现细节存在示例层面的歧义;本项目应以原始字节验签,并用目标实例实际 delivery 样本校验。