# 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 - Webhook 文档(1.24): (渲染页:)。 - API 使用说明(1.24): 。 - API 1.24.7 “Add a comment to an issue”: 。 - Webhook 发送逻辑: - Webhook payload 定义: - Issue/PR/Comment 结构: 、 、 。 - 官方集成测试(普通 Issue/PR 评论): (`Test_WebhookIssueComment`、`Test_WebhookPullRequestComment`)。 ### gitea-mcp - 仓库:。 - 当前 README(启动、工具表、HTTP 说明): 。 - 当前启动参数与环境变量源码: 。 - 当前 MCP/HTTP 运行与限制: 。 - 当前 Gitea API 客户端、超时、重定向和认证: 、 。 - 最新发行版:。 ## 2. Gitea Webhook 协议 ### 2.1 请求、事件头和签名 Gitea Webhook 通常向目标 URL 发送 JSON `POST`。官方文档的示例明确要求 `POST`、`Content-Type: application/json`,并给出以下头: ```text X-Gitea-Delivery: X-Gitea-Event: X-Gitea-Signature: ``` 当前 Gitea 源码 `services/webhook/deliver.go` 还发送: ```text X-Gitea-Event-Type: X-Gitea-Hook-Installation-Target-Type: X-Gogs-* # 兼容 Gogs X-Hub-Signature: sha1= X-Hub-Signature-256: sha256= 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 字节**计算: ```text hex(HMAC-SHA256(secret, body)) ``` `X-Gitea-Signature` 是不带 `sha256=` 前缀的小写十六进制串;源码同时生成 GitHub 兼容的 `sha256=` 形式。接收端必须在 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` 为: ```json { "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 文档的新增评论接口为: ```text POST /api/v1/repos/{owner}/{repo}/issues/{index}/comments Authorization: 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 形状是: ```bash gitea-mcp -t stdio -H https://gitea.example.com ``` 通过环境传 Token(推荐,避免出现在进程列表): ```text GITEA_HOST=https://gitea.example.com GITEA_ACCESS_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 流。 建议的线程级配置形状(概念示例): ```json { "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 ` 或 `Authorization: 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 样本校验。