feat: support MCP elicitation and rebuild release
This commit is contained in:
340
.planning/2026-08-23-gitea-workflow/protocol-research.md
Normal file
340
.planning/2026-08-23-gitea-workflow/protocol-research.md
Normal file
@@ -0,0 +1,340 @@
|
||||
# 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
|
||||
样本校验。
|
||||
|
||||
Reference in New Issue
Block a user