# 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 + 提交状态 ↓ 无阻断问题且开启自动合并 自动合并 / 等待检查通过后合并 ``` ## 功能 **仓库接入** - 「从 Gitea 导入」一键列出全局 Token 可见的仓库,点「添加」即建好监控配置 - 手动新增时只填 Git 地址,自动识别 `所有者/仓库名`,旁边可「测试连接」验证 - 支持 https / ssh / scp 三种地址写法 - 保存仓库时**自动在 Gitea 里创建 Webhook**,无需手工配置;仓库列表会显示 Webhook 状态,未配置可一键「修复 Webhook」 **分支模型** - 一个「管理分支」作为唯一合并目标 - 多个「检查分支」全部纳入监控,点选即可增减 - 管理分支不存在时可「克隆创建」,从任意已有分支复制 - 审查始终以管理分支为对比基准,即「这些改动合入管理分支会怎样」 **审查触发** - 监听检查分支的推送,对比管理分支的 merge-base - 监听 Pull Request 的 opened / synchronize / reopened / ready_for_review - 可选只审 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 不可合并时自动放弃 **审查历史** - 「审查历史」页按仓库汇总每次审查的摘要,可一键跳转 PR - 每条记录保留完整上下文:触发来源、PR 链接、审查范围、阻断阈值、发布方式、 自动合并设置、排除路径、LLM 模型、token 消耗、耗时、本次填写的需求背景 - 详情弹窗列出本次全部审查意见,标注严重级别与是否阻断 **AI 摘要** - 独立的摘要模块,与代码审查的提示词完全分开,但共用同一份全局 LLM 设置 - 专用提示词把一次审查压缩成固定四节、500 字内的记录: `结论 / 范围 / 问题 / 要点` - 摘要失败不影响审查结果,可单条重新生成 **运维** - 内置 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_WEBHOOK_URL` | Gitea 回调本服务的地址;留空则自动推导为「Gitea 主机名 + 本服务端口」 | | `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` | 队列轮询间隔与失败重试次数 | ### 按仓库(后台「仓库」) | 字段 | 说明 | | --- | --- | | `repo_url` | 仓库 Git 地址,`owner` / `name` 由此自动解析 | | `managed_branch` | 主管理分支。push 事件没有对应 PR 时用它做对比基准;自动合并不限于它,任一检查分支都可作为合并目标 | | `check_branches` | 受保护的目标分支列表,逗号分隔。任何**合并进**这些分支的 PR 都会审查(功能分支不必列出),向这些分支推送也会审查 | | `review_scope` | `both` / `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 `。 | 方法 | 路径 | 说明 | | --- | --- | --- | | `GET` | `/api/health` | 健康检查与队列统计(免鉴权) | | `GET` `PUT` | `/api/settings` | 读写全局设置 | | `GET` `POST` | `/api/repos` | 列出 / 新增仓库配置 | | `GET` `PATCH` `DELETE` | `/api/repos/:id` | 读取 / 修改 / 删除 | | `POST` | `/api/repos/:id/discover` | 校验连接,返回默认分支与分支列表 | | `GET` | `/api/gitea/repos` | 列出全局 Token 可见的仓库,供一键导入 | | `POST` | `/api/repos/parse` | 解析 Git 地址,返回 `owner` / `name` | | `POST` | `/api/repos/branches` | 用 Git 地址查询分支(仓库尚未保存时使用) | | `POST` | `/api/repos/:id/branches` | 创建分支,可从指定分支克隆 | | `GET` | `/api/repos/:id/webhook` | 查询该仓库的 Webhook 是否已安装 | | `POST` | `/api/repos/:id/webhook` | 安装或修复该仓库的 Webhook | | `GET` | `/api/records` | 审查历史(`?repo_id=`、`?limit=`) | | `GET` | `/api/records/:id` | 单条记录,含参数、意见与摘要 | | `POST` | `/api/records/:id/summarise` | 重新排队生成摘要 | | `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":""}' ``` ## 行为说明 **分支与合并**:`check_branches` 是**被保护的目标分支**。任何合并进这些分支的 PR 都会被审查,所以功能分支不必列进去;向这些分支直接推送也会审查。审查基准是该改动实际要合入的分支(PR 用 PR 自己的目标分支,push 用管理分支),也就是「这次改动合进去会怎样」。 自动合并作用于目标为**任一检查分支**的 PR,因此两级流转可以直接用: ```text feature/xxx ──PR──> test ──PR──> prd ↑ 第一级:审查通过即自动合并 ↑ 第二级:同样审查通过才合并 ``` **直接推送**也走同一条链路:往 `test` 推代码(没有 PR)时,审查通过后服务会 自动开一个 `test → prd` 的 PR 并合并它,然后关闭该仓库所有审查 Issue。 自动开的晋级 PR 会带一个标记,它自身的 webhook 事件会被忽略——同一批提交在 push 时已经审过,重复审会在代码合并后凭空造出 Issue。 晋级 PR 始终使用 `merge` 而非 `squash`:squash 会把提交重写成全新提交, 两个长期共存的分支每晋级一次就多分叉一点,最终必然冲突;真正的 merge 保留共同祖先,下次晋级只携带新提交。 `managed_branch` 只决定「push 事件没有对应 PR 时,拿哪个分支做对比基准」,以及新仓库的默认值。 **Issue 生命周期**:只要发生合并(服务自动合并、或人在 Gitea 里手动合并), 该仓库所有由本服务创建的 open Issue 都会被评论并关闭——代码已经落地, 上一轮的问题描述的是不再存在的状态。下一个问题会在下一次审查时重新开 Issue。 **审查发现都会建 Issue**,与是否阻断合并无关。等级体现在 Issue 标题与标签上 (`[OCR][medium] …`,标签 `medium`),阻断项在正文里额外标注「(阻断合并)」。 这样低等级问题不会丢失,也不会挡住合并。 **合并路径由事件类型决定**:push 事件永远走「晋级分支」,PR 事件才走「合并该 PR」。 早先按「是否找到关联 PR」来判断,导致一个残留的 `test → prd` PR 会让后续 push 误判成 PR 事件、跳过晋级。关联 PR 现在只用于评论归属,不参与路径选择。 **合并失败会自动重试**:Gitea 在算出 PR 可合并性之前会返回 `405 Please try again later`, 这类暂时性失败(405/409/5xx)按指数退避重试 4 次;权限不足、真实冲突等 永久性失败立即放弃,并在 PR 上留言说明原因。若 PR 已被合并,视为成功。 **审查范围**:PR 事件优先用 PR 的目标分支作为基准;push 事件优先用 webhook 里的 `before`,当 `before` 是新建分支的全零值或缺失时,退回到与管理分支的 merge-base。 **审查背景**:仓库配置里的「审查背景 / 需求」是多行文本,既作为 OCR 的 `--background` 传给模型,也会完整记录到审查历史,方便回溯「当时是按什么需求审的」。 **阻断判定**:一条意见命中任一条件即为阻断 —— 严重级别 ≥ `block_severity` 中任一项,类别在 `block_categories` 中,或打开了 `fail_on_findings`。阻断会创建 Issue、把提交状态置为 `failure`,并阻止自动合并。 **审查不完整时**:OCR 报告 `partial` / `failed`,或存在文件级警告,或 token 预算被截断时,仍会发布已有意见,但不会自动合并,提交状态也不会是 success。宁可漏合,不可错合。 **Issue 生命周期**:标题格式 `[OCR] / · 存在阻断级代码问题`。同一分支再次出现阻断问题时更新该 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`。 ## 故障排查 **PR 提交了却没有触发审查** 先看仓库列表的 Webhook 列:显示「未配置」说明 Gitea 不会发事件,点「修复 Webhook」即可。其次确认 `check_branches` 是否包含该 PR 的**目标分支**(不是功能分支)。 **审查通过了却没有自动合并** 看任务日志里的 `auto-merge skipped` 原因。常见几类:`auto_merge` 开关没打开;PR 目标分支不在 `check_branches` 里;存在阻断问题;审查覆盖不完整(OCR 报了 partial/警告/token 预算截断);提交状态不是 success;PR head 已变化或不可合并。 **Webhook 投递失败,提示 `webhook can only call allowed HTTP servers`** Gitea 拦截了内网地址。按上文给 `[webhook] ALLOWED_HOST_LIST` 加上本服务地址,重启 Gitea。 **后台打开后要求输入访问 Token** 这是 `CR_ADMIN_TOKEN` 在生效。输入 `.env` 里的值即可,Token 只存在浏览器本地。想免登录就把它留空并重启容器(仅限不对外暴露的网络)。 **历史记录一直显示「摘要生成中…」** 摘要与审查串行执行,一次只跑一个;如果 LLM 不可达会在记录里留下失败原因,可在详情里点「重新生成摘要」。摘要失败不会影响审查结论与自动合并。 **任务一直停在 `reviewing`** LLM 慢或不可达。在后台「设置」点「测试 LLM」,或在「任务」页查看日志;调大 `CR_REVIEW_TIMEOUT_MS`。 **内联评论变成了汇总里的条目** OCR 给的行号不在本次 diff 内(常见于问题定位到未改动的上下文行)。服务会保留意见并汇总展示,不丢结果。 **自动合并没有发生** 看任务日志的 `auto-merge skipped` 原因:存在阻断问题、审查覆盖不完整、提交状态非 success、PR head 已变化或 PR 不可合并。Gitea 侧还需该 Token 有合并权限。