Files
git-codereview/README.md
T
kgod ed172bf369 feat: Gitea 自动代码审查服务
基于 OpenCodeReview 的 webhook 服务:监听 Gitea 的 push 与 Pull Request 事件,
调用 OCR 审查 diff,把结果发布回 Gitea,并按阻断阈值决定是否自动合并。

主要能力:
- Push / PR 事件触发,支持分支 glob 过滤与 PR-only / push-only 范围
- PR 内联评论(按 diff 行号定位)、汇总评论、Issue 生命周期、提交状态
- 可配置阻断阈值(严重级别 / 类别 / 任意意见)
- 无阻断问题时自动合并,审查覆盖不完整时拒绝合并
- 内置 Web 后台:仓库配置、任务日志、失败重跑、连通性自检
- SQLite 持久化,worker 重启回收卡死任务,失败自动重试

实现为独立服务而非 Gitea Action:本机 act_runner 指向的实例不可达,
且后台配置与任务历史需要独立进程承载。
2026-09-20 12:09:46 +08:00

219 lines
10 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 自动代码审查
基于 [OpenCodeReview](https://github.com/alibaba/open-code-review)(OCR)的 Gitea 自动代码审查服务。
监听仓库推送和 Pull Request,调用 OCR 分析 diff,把结果发布回 Gitea,并按规则决定是否自动合并。
## 它做什么
```text
Push / Pull Request 事件
↓ Gitea Webhook(HMAC 签名)
gitea-codereview
↓ git fetch + OCR 审查
OpenCodeReview CLI → LLM
↓ 结构化 JSON(文件 / 行号 / 类别 / 严重级别)
Gitea:内联评论 + 汇总评论 + Issue + 提交状态
↓ 无阻断问题且开启自动合并
自动合并 / 等待检查通过后合并
```
## 功能
**审查触发**
- 监听分支推送,自动对比上次提交或与目标分支的 merge-base
- 监听 Pull Request 的 opened / synchronize / reopened / ready_for_review
- 按分支 glob 过滤(`*`、`release/*`、`main`),可选只审 PR 或只审 push
- 同一 commit 在队列中只入队一次;webhook 按 delivery id 去重
**发布结果**
- PR 内联评论,带 `[category · severity]` 标记和 `suggestion` 代码块
- 每条内联评论都校验是否落在本次 diff 的行上,落不到的汇总进审查总结
- 提交状态 `code-review/ocr`(success / failure / error),可配成必需检查
- 同一仓库同一分支只维护一个 Issue:有问题时创建或更新,修好后自动关闭
- 可配置阻断阈值:按严重级别、按类别,或任何意见都算
**自动合并**
- 仅在无阻断问题、审查覆盖完整、提交状态为 success 时才触发
- 两种模式:`when_checks_succeed`(等分支保护检查通过)和 `immediate`
- 合并方式、是否删除分支可配
- PR head 已变化或 PR 不可合并时自动放弃
**运维**
- 内置 Web 后台:仓库配置、任务列表、实时日志、重跑、连通性自检
- 每次审查的完整日志和结果 JSON 落库
- worker 重启后自动回收卡住的任务,失败任务自动重试一次
- 全局或按仓库覆盖 Gitea token、LLM 端点 / 模型 / Key、排除路径、并发数
## 目录
```text
gitea-codereview/
├── app/
│ ├── server.js HTTP 服务:webhook、REST API、静态后台
│ ├── lib/
│ │ ├── db.js SQLite 结构与查询
│ │ ├── gitea.js Gitea REST 客户端
│ │ ├── ocr.js 调用 OCR CLI,解析 JSON
│ │ ├── diff.js 解析 unified diff,定位内联评论锚点
│ │ ├── review.js 审查流水线:发布、Issue、提交状态、自动合并
│ │ └── queue.js 串行任务队列
│ └── static/ 后台前端
├── tests/core.test.js
├── Dockerfile
└── docker-compose.yml
```
## 部署
### 1. 准备 Gitea
**创建访问 Token**:Gitea → 用户设置 → 应用 → 生成令牌,勾选 `repo` 与 `issue` 权限。
**允许 Gitea 向内网投递 Webhook**(Gitea 默认拦截内网地址)。在 Gitea 的 `app.ini` 或容器环境变量中加:
```yaml
GITEA__webhook__ALLOWED_HOST_LIST: "192.168.31.51"
```
多个地址用逗号分隔,也可写 CIDR(`192.168.31.0/24`)或内置名(`private`、`loopback`)。
### 2. 准备 LLM 端点
OCR 需要一个 OpenAI 兼容或 Anthropic 兼容的接口,填进 `.env` 的 `CR_LLM_URL` / `CR_LLM_TOKEN` / `CR_LLM_MODEL`。
用 `CR_LLM_PROTOCOL=openai` 或 `anthropic` 指定协议。
### 3. 配置并启动
```bash
cd gitea-codereview
cp .env.example .env
# 填入 CR_GITEA_TOKEN、CR_WEBHOOK_SECRET、CR_ADMIN_TOKEN、CR_LLM_*
docker compose build
docker compose up -d
docker compose ps
curl -fsS http://localhost:8090/api/health
```
打开后台 `http://<服务器>:8090`:
0. 如果设置了 `CR_ADMIN_TOKEN`,页面会先要求输入该 Token(保存在浏览器本地,可随时「清除访问 Token」)
1. 在「设置」里确认 Gitea 地址、LLM 端点,点「测试 Gitea」和「测试 LLM」验证连通
2. 在「仓库」里新增要审查的仓库,配置分支过滤、阻断阈值、是否自动合并
3. 在 Gitea 仓库 Settings → Webhooks 添加 Webhook:
- 目标 URL:`http://<服务器>:8090/webhook/gitea`
- 内容类型:`application/json`
- 密钥:填 `.env` 里的 `CR_WEBHOOK_SECRET`
- 事件:Push 与 Pull Request
也可以只配 `.env` 不打开后台;后台的修改会持久化到 SQLite 卷。
## 配置项
### 全局(`.env` 或后台「设置」)
| 变量 | 说明 |
| --- | --- |
| `CR_GITEA_URL` | Gitea 根地址,如 `http://192.168.31.51` |
| `CR_GITEA_TOKEN` | 发布评论、Issue、提交状态用的 Token |
| `CR_WEBHOOK_SECRET` | Webhook HMAC 密钥;设置后拒绝签名不符的请求 |
| `CR_ADMIN_TOKEN` | 保护后台和 REST API 的 Bearer Token;设置后打开后台需先输入;留空则不校验 |
| `CR_LLM_URL` / `CR_LLM_TOKEN` / `CR_LLM_MODEL` | LLM 端点与凭据 |
| `CR_LLM_PROTOCOL` | `openai` 或 `anthropic` |
| `CR_LLM_AUTH_HEADER` | 自定义认证头名,默认由协议决定(如 `x-api-key`) |
| `CR_LLM_EXTRA_HEADERS` | 附加请求头,`K=V,K=V` |
| `CR_LLM_TIMEOUT` | 单次 LLM 请求超时秒数,默认 `180` |
| `CR_RULE_PATH` | 自定义 OCR 规则 JSON 的容器内路径 |
| `CR_HOST_PORT` | 后台映射到宿主机的端口,默认 `8090` |
| `CR_CONCURRENCY` | 单个审查任务的并发文件数 |
| `CR_REVIEW_TIMEOUT_MS` | 单次审查超时,默认 45 分钟 |
| `CR_MAX_TOKENS_BUDGET` | 单次审查 token 上限,`0` 为不限 |
| `CR_OCR_COMMAND` | OCR 可执行文件名,默认 `ocr` |
| `CR_DATA_DIR` / `CR_DB_PATH` | 数据目录与 SQLite 路径,默认容器内 `/data` |
| `CR_POLL_MS` / `CR_MAX_ATTEMPTS` | 队列轮询间隔与失败重试次数 |
### 按仓库(后台「仓库」)
| 字段 | 说明 |
| --- | --- |
| `branch_patterns` | 监听的分支 glob,逗号分隔;`*` 为全部 |
| `review_scope` | `both` / `pr` / `push` |
| `base_branch` | 对比基准分支,也是无 PR 时 push 审查的目标 |
| `block_severity` | 阻断级别阈值,如 `critical,high`;留空则不看级别 |
| `block_categories` | 额外按类别阻断,如 `security` |
| `fail_on_findings` | 打开后任何意见都视为阻断 |
| `auto_merge` | 无阻断问题时自动合并 |
| `auto_merge_mode` | `when_checks_succeed` / `immediate` |
| `merge_method` | `squash` / `merge` / `rebase` / `rebase-merge` / `fast-forward-only` |
| `delete_branch` | 合并后删除源分支 |
| `publish_mode` | `inline`(PR 内联评论)/ `issue-only` |
| `create_issue` | 是否创建 / 更新 Issue |
| `issue_labels` | Issue 标签,不存在时自动创建 |
| `excludes` | OCR 排除路径,gitignore 风格,逗号分隔 |
| `max_comments` | 单次最多发布多少条意见 |
| `gitea_token` / `llm_token` / `llm_model` | 覆盖全局配置 |
## REST API
所有 `/api/*` 接口在设置 `CR_ADMIN_TOKEN` 后需要 `Authorization: Bearer <token>`。
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| `GET` | `/api/health` | 健康检查与队列统计(免鉴权) |
| `GET` `PUT` | `/api/settings` | 读写全局设置 |
| `GET` `POST` | `/api/repos` | 列出 / 新增仓库配置 |
| `GET` `PATCH` `DELETE` | `/api/repos/:id` | 读取 / 修改 / 删除 |
| `POST` | `/api/repos/:id/discover` | 校验连接,返回默认分支与分支列表 |
| `GET` | `/api/jobs` | 任务列表(`?repo_id=`、`?limit=`) |
| `GET` | `/api/jobs/:id` | 任务详情,含日志 |
| `POST` | `/api/jobs/:id/retry` | 重跑任务 |
| `POST` | `/api/review` | 手动排队一次审查 |
| `POST` | `/api/gitea/test` | 测试 Gitea 连接 |
| `POST` | `/api/selftest` | 测试 OCR 与 LLM 连通性 |
手动触发一次审查:
```bash
curl -X POST http://localhost:8090/api/review \
-H "Authorization: Bearer $CR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"owner":"kgod","name":"myrepo","ref":"main","sha":"<commit-sha>"}'
```
## 行为说明
**审查范围**:PR 事件用 `base.sha..head.sha`;push 事件用 webhook 里的 `before..after`,如果 `before` 是新建分支的全零值,则退回到与 `base_branch` 的 merge-base。
**阻断判定**:一条意见命中任一条件即为阻断 —— 严重级别 ≥ `block_severity` 中任一项,类别在 `block_categories` 中,或打开了 `fail_on_findings`。阻断会创建 Issue、把提交状态置为 `failure`,并阻止自动合并。
**审查不完整时**:OCR 报告 `partial` / `failed`,或存在文件级警告,或 token 预算被截断时,仍会发布已有意见,但不会自动合并,提交状态也不会是 success。宁可漏合,不可错合。
**Issue 生命周期**:标题格式 `[OCR] <owner>/<repo> · <branch> 存在阻断级代码问题`。同一分支再次出现阻断问题时更新该 Issue 并追加一条说明;该分支复查通过后自动评论并关闭。`create_issue` 关闭时不新建 Issue,但仍会关闭此前开过的。
**重复保护**:同一 commit 已在队列中或正在审查时不重复入队;Gitea 重投的同一 delivery id 会被忽略。
## 开发
```bash
node --test tests/core.test.js # 单元测试
node app/server.js # 本地直接运行(需先装 ocr CLI)
docker compose build # 构建镜像
```
本地运行需要 Node ≥ 22.5(用到内置 `node:sqlite`)、`git` ≥ 2.41,以及 `npm i -g @alibaba-group/open-code-review`。
## 故障排查
**Webhook 投递失败,提示 `webhook can only call allowed HTTP servers`**
Gitea 拦截了内网地址。按上文给 `[webhook] ALLOWED_HOST_LIST` 加上本服务地址,重启 Gitea。
**后台打开后要求输入访问 Token**
这是 `CR_ADMIN_TOKEN` 在生效。输入 `.env` 里的值即可,Token 只存在浏览器本地。想免登录就把它留空并重启容器(仅限不对外暴露的网络)。
**任务一直停在 `reviewing`**
LLM 慢或不可达。在后台「设置」点「测试 LLM」,或在「任务」页查看日志;调大 `CR_REVIEW_TIMEOUT_MS`。
**内联评论变成了汇总里的条目**
OCR 给的行号不在本次 diff 内(常见于问题定位到未改动的上下文行)。服务会保留意见并汇总展示,不丢结果。
**自动合并没有发生**
看任务日志的 `auto-merge skipped` 原因:存在阻断问题、审查覆盖不完整、提交状态非 success、PR head 已变化或 PR 不可合并。Gitea 侧还需该 Token 有合并权限。