17 KiB
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): https://gitea.com/gitea/docs/raw/branch/main/versioned_docs/version-1.24/usage/webhooks.md (渲染页:https://docs.gitea.com/1.24/usage/webhooks/)。
- API 使用说明(1.24): https://gitea.com/gitea/docs/raw/branch/main/versioned_docs/version-1.24/development/api-usage.md。
- API 1.24.7 “Add a comment to an issue”: https://docs.gitea.com/api/1.24/operations/issue-create-comment/。
- Webhook 发送逻辑: https://github.com/go-gitea/gitea/blob/4852091e851060d2233fcbf789727b1c5dba4bfc/services/webhook/deliver.go
- Webhook payload 定义: https://github.com/go-gitea/gitea/blob/4852091e851060d2233fcbf789727b1c5dba4bfc/modules/structs/hook.go
- Issue/PR/Comment 结构: https://github.com/go-gitea/gitea/blob/4852091e851060d2233fcbf789727b1c5dba4bfc/modules/structs/issue.go、 https://github.com/go-gitea/gitea/blob/4852091e851060d2233fcbf789727b1c5dba4bfc/modules/structs/pull.go、 https://github.com/go-gitea/gitea/blob/4852091e851060d2233fcbf789727b1c5dba4bfc/modules/structs/issue_comment.go。
- 官方集成测试(普通 Issue/PR 评论):
https://github.com/go-gitea/gitea/blob/4852091e851060d2233fcbf789727b1c5dba4bfc/tests/integration/repo_webhook_test.go
(
Test_WebhookIssueComment、Test_WebhookPullRequestComment)。
gitea-mcp
- 仓库:https://gitea.com/gitea/gitea-mcp。
- 当前 README(启动、工具表、HTTP 说明): https://gitea.com/gitea/gitea-mcp/src/branch/main/README.md。
- 当前启动参数与环境变量源码: https://gitea.com/gitea/gitea-mcp/src/branch/main/cmd/cmd.go。
- 当前 MCP/HTTP 运行与限制: https://gitea.com/gitea/gitea-mcp/src/branch/main/operation/operation.go。
- 当前 Gitea API 客户端、超时、重定向和认证: https://gitea.com/gitea/gitea-mcp/src/branch/main/pkg/gitea/gitea.go、 https://gitea.com/gitea/gitea-mcp/src/branch/main/pkg/gitea/rest.go。
- 最新发行版:https://gitea.com/gitea/gitea-mcp/releases/tag/v1.6.0。
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 自己的出站任务记录,不是对外接收方的幂等协议。
本项目必须自行实现:
- 在验签之后、入队之前,以
(Gitea 实例标识, X-Gitea-Delivery)建立持久唯一键。 - 采用原子“插入/已存在”语义;重复请求直接返回成功,不重复 clone、启动 turn 或 回帖。
- delivery 记录应包含原始 headers/body 摘要、接收时间、解析结果和最终任务状态, 便于审计与恢复。
- 由于官方未承诺顺序,队列按仓库串行化;不要按 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;没有看到自动 重试逻辑。工具调用失败通常编码为 MCPtools/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 工作流的落地影响
- Webhook 接收顺序固定为: 限制请求 → 读取原始 body → HMAC 验签 → 校验
X-Gitea-Delivery/事件头 → JSON 解析 → Bot 自评论过滤 →action=created与@ccweb-bot过滤 → 持久化唯一 delivery → 快速 2xx → 入队。 - 会话键: 使用
gitea + repository.full_name + issue/PR 类型 + number;PR 普通评论仍使用issue_write/add_comment,不是 review reply。 - 回执确认: MCP 最终输出不等于 Gitea 已写入;必须查询评论列表/回流 Webhook 确认正文和 comment ID。缺失时最多一次隐藏补发 turn,最后 REST 兜底。
- 凭据隔离: HMAC Secret 只用于入站验签,Bot Token 只用于 MCP/REST/Git;Token 通过线程级 env 注入,禁止写入 remote URL、任务正文、日志和 URL query。
- 并发和恢复: delivery 去重、任务状态、仓库锁和回执审计必须持久化;同仓库 串行、不同仓库按全局上限并行。进程重启后 queued 恢复,running 按既定最多一次重试, waiting_user 保留。
- 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-mcpmain要求 Go 1.26,最新发行版与main的 MCP SDK/HTTP 实现 不同;必须在 CI/部署机锁定并做 stdioinitialize、tools/list、真实issue_write和错误返回回归。 - Gitea Webhook 文档示例 PHP 使用
trim后验签,与当前发送源码“对原始 body 签名” 的实现细节存在示例层面的歧义;本项目应以原始字节验签,并用目标实例实际 delivery 样本校验。