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

341 lines
17 KiB
Markdown
Raw 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 与官方 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`,并给出以下头:
```text
X-Gitea-Delivery: <UUID>
X-Gitea-Event: <event>
X-Gitea-Signature: <hex SHA-256 HMAC>
```
当前 Gitea 源码 `services/webhook/deliver.go` 还发送:
```text
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 字节**计算:
```text
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` 为:
```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 <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 形状是:
```bash
gitea-mcp -t stdio -H https://gitea.example.com
```
通过环境传 Token(推荐,避免出现在进程列表):
```text
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 流。
建议的线程级配置形状(概念示例):
```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 <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
样本校验。