开源代码审查工具open-code-review:规则引擎与LLM双通道架构实践

去年我把一个内部用了很久的代码审查工具整理成了开源项目,起了个名字叫open-code-review。本意很简单:代码审查这件事,不应该只靠人肉盯着 diff 来回看,也不应该被商业工具锁死。它在 GitHub 上挂了几个月,反馈比我预期的好,不少人问部署方式、架构设计,还有怎么接入现有 CI。今天就把这个项目的完整思路、核心实现和实操过程一次说清楚。

这个项目适合谁?如果你团队还在用“拉分支→群里催review→人工过diff→改完再看一遍”的纯人力流程,或者你正打算引入 AI 辅助代码审查但从何下手,再或者你想自己搭一套内网可用的审查系统,那我建议你花十几分钟把这篇读完。里面包含了我从零到一搭建这套体系的全部关键决策和踩坑记录。

1. 项目整体设计与思路拆解

1.1 为什么选择自己做开源代码审查工具

先说下为什么要造这个轮子。

团队之前用过的方案大概分两类:一类是 SaaS 代码托管平台自带的基础审查功能,能看 diff、留评论、提交检查清单,但它只是个流程壳子,不会帮你分析代码质量问题。另一类是商业代码质量管理工具,功能确实全,一套下来License费用不低,而且很多规则粒度不是你想要的样子。

真正让我下定决心自己做的触发点是:团队引入 AI 辅助编程之后,代码产量上来了,但是 review 变成了瓶颈。每次 MR 动辄几百上千行,靠人一遍遍翻累不说,效果还很差。我需要一个能自动分析 diff、跑规则检查、给出审查意见、还能跟现有代码托管平台打通的工具。

于是 open-code-review 的定位就清楚了:

  • 开源的、可私有化部署的代码审查助手
  • 能自动分析合并请求里改动的代码
  • 内置规则引擎处理确定性检查项
  • 支持接入大模型做语义层面的智能分析
  • 审查结果能通过插件推送到代码托管平台、IM 工具

它不是一个替代人工 review 的方案,而是一个把人工 review 效率拉高的辅助系统。**人还是要做最终的判断,但重复性的、确定性的、低层次的问题,机器先帮你扫掉。**这一点从一开始就明确下来,也决定了后面整个架构的走向。

1.2 规则引擎加 AI 双通道的架构决策

项目最早期的原型只有一个规则引擎:解析 diff 文件,根据预先定义的规则模式匹配代码片段,命中就输出一条问题记录。这套东西跑起来以后效果是有的,但暴露出一个明显短板——规则只能识别“写的模式不对”,识别不了“这段代码是不是真正缺了一个边界判断”。前者是语法层面的,后者是语义层面的。

所以在 1.0 版本之后,我引入了第二条分析通道:LLM 通道。diff 内容经过裁剪和格式化之后发送给大模型,让模型从设计、安全性、可维护性几个维度输出审查意见。两条通道的结果在报告聚合层合并,去重之后统一格式化输出。

之所以不做成纯 LLM 方案,有一个很现实的考虑:审查成本和响应速度。规则引擎是本地执行的,毫秒级出结果,这是任何模型调用都比不了的。团队里每天几十个合并请求,如果全部走模型,成本完全不受控。让规则引擎先把低级问题筛掉,把剩下的值得“思考”的片段交给模型,这才是合理的资源分配方案。

2. 核心技术细节与实现要点

2.1 Diff 解析的细节处理和补丁归一化

解析 diff 是整个项目的根基,最开始我直接用字符串匹配去读 diff,很快发现不可行。不同代码托管平台返回的 diff 格式有细微差异,比如 GitHub 的补丁格式里包含了index行和--- a/xxx、+++ b/xxx的行前缀,GitLab 在某些场景下返回的格式略有不同。另外行尾处理必须统一,否则带 CRLF 的 Windows 项目能让你解析结果错位。

经过几个版本的迭代,我把 diff 解析抽成了独立模块,核心规则如下:

  • 统一将换行符归一化为\n,统一使用 UTF-8 编码读取文件内容
  • 按文件单元切分大 diff,每个文件内部再按 hunk 切分
  • 解析@@ -start,count +start,count @@头,建立新旧行号的映射关系
  • 删除行(-开头)和新增行(+开头)分别记录,上下文行保留供后续分析

行号映射这个点特别重要。规则引擎最后要输出“哪个文件的第几行有问题”,如果行号对不上,审查结果就是废的。GitHub 和 GitLab 对评论行号的传递机制也不一样,需要针对不同平台做适配层。

2.2 规则引擎的设计思路

规则引擎的核心是一组预定义的规则函数。每个规则函数接收一个上下文对象,上下文里包含当前文件路径、语言类型、hunk 内容、行号映射表等。规则函数返回一个或多个问题对象。

我选了 Python 作为项目主语言,所以规则就是普通的 Python 函数,用装饰器注册到规则中心。比如一个典型的防止调试代码残留的规则:

@rule_register.register( rule_id="PY001", name="debug_breakpoint_check", file_types=["*.py"], severity="warning", ) def check_breakpoint(ctx): issues = [] for line_no, line in ctx.new_lines: stripped = line.strip() if stripped.startswith("breakpoint()") or stripped.startswith("pdb.set_trace()"): issues.append(Issue( rule_id="PY001", line=line_no, message="检测到调试断点残留,请在提交前移除。", suggestion="删除调试语句,或改用日志方式输出调试信息。" )) return issues

定义规则机制的花样很多,关键不是写规则本身,而是让规则“好写、好测、好维护”。每一条规则都对应一个独立的测试用例,构造最小的 diff 片段来验证命中与不命中两种情况,所有规则必须覆盖这两个分支。这个设计让后来新增规则的成本变得很低——复制一个已有规则文件,改逻辑、加用例,跑一遍测试就完事。到目前为止项目内置了覆盖 Python、JavaScript、Java、Go 四门语言的四十多条基础规则。

2.3 LLM 通道的上下文裁剪策略

LLM 通道是项目最复杂也最容易出错的地方。

最开始我尝试过把整个 MR 的 diff 全量塞给模型,结果很快就遇到了两个问题:一是大 MR 的 diff 能有好几万 token,模型输入长度直接爆掉;二是上下文太长之后模型对重点的聚焦能力明显下降,给出的意见开始泛泛而谈。

后来我采用了分层裁剪策略。第一步在文件级别做裁剪,把超过设定阈值的大文件过滤掉,只保留最值得审查的改动文件。第二步在文件内部做裁剪,只保留新增行和带有上下文的关键片段,删除大量纯移动代码的冗余行。第三步在对话级别做裁剪,给模型的任务描述保持精简,让它的注意力集中在代码本身。

当前版本我用的请求模板分为三个部分:

  • 系统角色定义:说明你是资深代码审查专家,输出格式必须为 JSON
  • 代码变更输入:裁剪后的 diff 内容
  • 输出约束:必须返回包含问题列表、行号、严重级别、建议修复方案的 JSON 结构

模型选型上,私有化部署用本地模型,追求效果可以直接接入商用模型 API。无论哪种,都要在请求参数中把temperature调低,我一般设置在 0.2 到 0.3 之间,确保输出稳定性优先。

2.4 审查报告与去重合并机制

两条通道的结果出来之后,不能直接拼在一起就完事,因为很可能出现同一处问题被两边重复报。合并去重逻辑我做了两个层面。

第一层是锚点去重:以“文件路径 + 行号区间”为锚点,如果规则引擎和 LLM 命中了同一个锚点区间,优先保留规则引擎的结果(它更精确,且附带规则文档链接)。

第二层是语义去重:这个问题难一点,因为模型报出来的行号和实际行号可能有偏差。我实现了一个模糊匹配算法,将 LLM 结果中的行号与规则引擎命中的行号做邻近匹配,偏差在三行以内视为同一条问题,再进行内容相似度比对,相似度超过阈值的合并为一条。

报告最终的输出格式我定为 Markdown。之所以不用 JSON 或 XML,是因为这份报告最终要展示给人看,Markdown 最自然,既能在终端里直接读,也能转成 HTML。

3. 实操过程与核心环节实现

3.1 整体架构与模块划分

写到这里,有必要把整个项目的架构做一个总览。open-code-review 是一个典型的管道式架构,数据流是单向的:代码托管平台事件 → 拉取 diff → 解析归一化 → 双通道分析 → 合并去重 → 报告生成 → 插件分发。

项目目录结构大致如下:

open-code-review/ ├── core/ │ ├── diff_parser.py │ ├── rule_engine.py │ ├── llm_channel.py │ ├── report_merger.py │ └── models.py ├── rules/ │ ├── python_rules.py │ ├── javascript_rules.py │ ├── java_rules.py │ ├── go_rules.py │ └── common_rules.py ├── integrations/ │ ├── cli.py │ ├── github_app.py │ ├── gitlab_webhook.py │ ├── local_git.py │ └── webhook_server.py ├── plugins/ │ ├── console_output.py │ ├── markdown_report.py │ ├── github_comment.py │ ├── gitlab_note.py │ └── feishu_sender.py ├── config/ │ ├── default_config.yaml │ └── rule_catalog.yaml └── tests/ ├── test_diff_parser.py ├── test_rules_python.py ├── test_report_merger.py └── test_plugins.py

这个划分方式的思路是:核心分析逻辑和外部对接彻底分离。换平台只动integrations/目录,加审查能力只动core/和rules/,改通知方式只动plugins/。后来接的几个平台都验证了这个决策是对的,改动范围完全被限制住了。

3.2 最小完整可复现的本地接入步骤

如果你把这个仓库拉下来,想最快速度跑通一次完整的代码审查,我的建议是先不要接任何远程平台,直接用本地 Git 仓库做测试。这种方式排除掉一切网络和环境干扰,方便验证核心逻辑。

操作流程如下:

  1. 创建虚拟环境并安装依赖:python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt

  2. 准备一个测试用的本地 Git 仓库,在里面创建几个故意包含问题的文件,比如在 Python 文件里写一个breakpoint(),在 JavaScript 文件里不用===而用==,在 Go 文件里把错误忽略掉

  3. 修改配置中心的local_git段,填入仓库路径和待检查的最新提交号

  4. 执行python -m open_code_review.cli --platform local --repo-path ./demo-repo --commit HEAD

执行完之后,CLI 会输出一份文本形式的审查报告。这份报告大致长这样:

审查目标: demo-repo (commit 3f2a1c0) 审查时间: 2025-01-15 14:32:08 审查文件数: 6 发现问题总数: 5 ├─ 严重问题: 1 ├─ 警告: 2 └─ 提示: 2 [PY001] 检测到调试断点残留 src/utils/logger.py:42 建议: 删除断点语句,改用 logging.debug() 输出关键变量 [JS002] 使用了宽松相等运算符 web/component.js:88 建议: 使用 === 替代 ==,避免隐式类型转换

看到这个输出格式,就说明核心分析链路已经通了。本地模式下输出的报告和 GitHub 模式下输出的报告,内容上完全一致,只是分发途径不同。这个设计是为了保证插件层的可替换性,审查逻辑从分发逻辑中彻底解耦。

3.3 接入 GitHub 和 GitLab 的完整流程

本地跑通之后,下一步就是接入真实代码托管平台。我做了一套统一的 Webhook 服务,兼容 GitHub 和 GitLab 的事件推送。

GitHub 接入过程需要注意几个细节。在 GitHub 设置里添加 Webhook 时,Payload URL 指向你的服务地址,Content type 必须选择application/json,触发事件要勾选Pull requests和Issue comments。GitHub 会发送X-GitHub-Event请求头标识事件类型,服务端需要校验这个头,只处理pull_request事件且 action 为opened、synchronize、reopened的情况。

GitLab 的接入大同小异,事件头变成了X-Gitlab-Event,事件类型是Merge Request Hook。部署在内网的 GitLab 需要注意一个问题:Webhook 回调的目标地址必须能被 GitLab 服务器访问到,如果 GitLab 和审查服务不在同一内网网段,需要在防火墙放行。

接入步骤我整理成了如下清单:

  • 在你的服务机器上启动 webhook 服务:python -m open_code_review.webhook_server --host 0.0.0.0 --port 8123

  • 在代码托管平台上创建 Webhook,URL 填写http://你的服务器IP:8123/webhook/github或http://你的服务器IP:8123/webhook/gitlab

  • 如果平台在公网,审查服务在内网,需要做内网穿透或者反向代理,建议直接用 Nginx 搭配proxy_pass处理

  • 仓库中创建.open-code-review.yml配置文件,按需求启用或禁用规则,设置各严重级别是否阻断合并

Webhook 服务内部的处理逻辑并不复杂:收到事件请求后,提取仓库地址、分支、MR/PR 编号信息 → 调用对应的平台 API 获取合并请求详情和改动的文件列表 → 依次拉取每个文件的 diff 内容 → 交给核心分析管道。关键点在于和代码托管平台的 API 认证,GitHub 用 Personal Access Token 或者 GitHub App 安装令牌,GitLab 用 Personal Access Token 或者 Project Access Token。我这里建议用专用机器账号的 Token,不要直接用个人的,否则账号变动会导致整个审查服务失效。

3.4 配置体系的组织方式

这个项目的配置体系经历了多次重构,最终稳定为“全局配置 + 仓库配置 + 命令行覆盖”的三层结构。

全局配置文件default_config.yaml设置了所有项目的默认行为,包括模型 API 地址和密钥(部署在服务器环境变量里)、审查开关、通知渠道等。仓库配置.open-code-review.yml放在被审查的代码仓库根目录,允许项目之间有不同的规则启停、严重级别阈值、豁免路径等。命令行覆盖则是最高优先级,适合临时调试。

仓库配置文件的示例:

review_rules: enabled: true rule_level: PY001: error PY002: warning JS001: off llm_review: enabled: true model: gpt-4o-mini max_files: 20 max_file_size: 2048 report: output_format: markdown send_to: ["github_comment", "feishu"] ignore_paths: - "build/" - "dist/" - "node_modules/"

配置体系里最容易被忽略的是ignore_paths配置。没有它,依赖锁定文件(比如package-lock.json)、自动生成的代码文件会大量涌入审查管道,把规则引擎和 LLM 的调用次数顶上去,浪费算力还污染报告。我在实战中把yarn.lock、poetry.lock、vendor/等路径全部加进了默认忽略列表,审查信号质量立刻明显提升。

3.5 插件系统的扩展方式

插件系统让这个工具从“自己用”变成了“团队用”。

最初版本的报告只在终端里打印,后来自用的时候加了 Markdown 报告,再后来接 GitHub 评论、GitLab 讨论、飞书通知。每加一个渠道,改动一次主程序,这样搞了几次之后我意识到必须把分发逻辑抽象出来。

插件接口的定义很简单,任何插件只需要实现一个函数,接收一份统一的报告对象,返回一个布尔值表示是否发送成功:

class ReviewPlugin(Protocol): def send_report(self, report: ReviewReport) -> bool: ... def plugin_name(self) -> str: ...

内置的三个官方插件分别覆盖了三种典型场景:

  • console_output:终端输出,适合本地调试
  • github_comment:审查完成后在 GitHub 对应 PR 下自动发表评论,按“严重问题/警告/提示”三个区块组织内容
  • feishu_sender:异步推送到飞书群机器人,适合团队成员不常看 GitHub 通知的场景

有个细节值得说:审查结果的评论里,我特意在每条问题后面加了一个很简短的“为什么”说明,比如此写法会导致隐式类型转换,可能隐藏字符串与数字比较的bug。这个设计并非出于教育目的,而是实践验证出来的——当审查意见带有解释时,开发者修改代码的意愿明显更强,而且可以减少评论区的质疑和来回讨论。

4. 常见问题与排查技巧实录

4.1 Diff 解析错位问题

这是开发过程中遇到最多的一类问题。现象是规则引擎报的行号和实际代码位置对不上,有明显的系统性偏移。

排查过程比较典型。一开始我怀疑是行号映射算法写错了,反复检查@@ a,b +c,d @@的解析逻辑,还专门加了单元测试覆盖各种 diff 边界情况,但问题依旧。后来打日志发现,某些文件的二进制内容被 Git 误判为文本,在 diff 里出现大量不可见字符,行数统计全乱。另外文件末尾没有换行符也会导致解析结果偏行。

解决办法是双管齐下:一是在 diff 解析前增加git diff --numstat预判,如果文件改动行数异常大或包含 NUL 字符,直接判定为二进制文件跳过;二是文件末尾缺失换行符的边界情况在解析器里做了标记,不参与行号偏移计算。这两个修复上线后再没有出现过系统性错位。

4.2 LLM 返回内容不稳定问题

LLM 通道刚上线那段时间,模型经常返回格式不完整的内容。有时候 JSON 解析到一半断了,有时候多了一个外层对象,有时候返回了一堆解释性文字而不是纯 JSON。

网上很多解决方案是“换一个大模型的提示词试试”,但我的结论是:靠提示词约束响应格式,稳定性永远不如工程手段。我最终的做法是三层防护:

  • 第一层,提示词尾部强制要求“只输出 JSON,不要包含任何解释性文字”
  • 第二层,调用后尝试json.loads()解析,失败后进入兜底逻辑,从响应文本里提取最外层大括号包含的内容重新解析
  • 第三层,针对</think>:open_code_review是那种很典型的“看名字简单,做起来全是细节”的项目。我最初决定自己写一套代码审查工具,不是因为业内没有现成方案,而是因为团队的实际痛点一直没有被解决:合并请求越来越多、代码评审越来越赶、低级问题反复漏到线上。与其反复抱怨流程不顺,不如直接做一个能嵌入现有流程的审查管道。

这个项目至今保持着规则引擎本地快速筛 + LLM 语义深度分析的双通道架构。简单说,规则引擎用毫秒级的速度扫出确定的、可枚举的问题,比如调试断点残留、相等操作符误用、未处理异常等;LLM 通道则专门负责需要“看懂”代码才能发现的逻辑问题,比如边界条件缺失、并发隐患、设计层面的不合理之处。两条通道的结果经过合并去重后,统一输出为一份 Markdown 审查报告。

这篇内容适合两类人读。一类是团队里正在为代码评审效率发愁,想自建或者选型开源审查工具的技术负责人;另一类是本身就在做 AI 辅助编程工具,想了解代码审查管道如何设计、规则引擎和 LLM 怎么协同的开发者。我会把这个项目从架构选型到落地接入的完整过程,包括踩过的坑和最后的解决办法,一次说清楚。

1. 从“要不要做”到“做成什么样”:项目的定位逻辑

1.1 为什么代码审查工具不能只靠人肉

代码评审这件事,流程上各家都差不多:提交合并请求、指定评审人、评审人翻代码、提出改动意见、修改后合入。听起来没毛病,但规模一上来就出问题。我见过的一个实际场景是,一个后端仓库平均每天合并 15 到 20 个 MR,每个 MR 改动 300 到 800 行。也就是说,光靠人工评审,团队每天需要处理接近一万行代码的阅读量。这还不算通知、回复评论、反复 review 的循环耗时。

人肉 review 还有一个更难解决的问题:注意力衰减。评审人看前 200 行的时候是新鲜的、认真的,看到第 800 行的时候精力基本到底了。一个藏在改动文件后部的breakpoint()残留,或者一个被误改的并发参数,非常容易被漏掉。这些漏网之鱼到了线上,就是生产事故或者调试悲剧。

所以我的判断是:代码审查一定要有自动化工具打底。机器负责把确定性的、一眼能看出来的问题全部扫掉,把漏网率降下来;人把精力集中在机器给不了结论的地方,比如架构设计、业务语义、未来扩展性。这不是替代人工,而是给人工腾出时间和注意力。

1.2 商用软件 vs 自研开源,为什么我选了后者

市面上其实有不少代码审查和代码质量工具。商用软件功能齐全,开箱即用,但我在选型时遇到了几个绕不开的问题:一是费用按席位和组织规模计算,团队稍微大一点就是一笔不小的长期开销;二是规则体系是封闭的,内置规则的粒度很难精确匹配团队自己的编码规范。团队规定“所有对外接口的入参必须做空值校验”,这种高度定制化的规则在商业工具里配置成本非常高,有些甚至根本不支持。

开源社区也有现成方案,但大多数停留在“静态代码分析”层面,对合并请求的 diff 没有天然的集成能力。我要的是一个能挂在 CI/CD 管道里、每个 MR 自动触发审查、把结果直接反馈到代码托管平台评论区的工具,这需要和平台 API 深度耦合。与其拿通用工具做大量二次开发,不如直接按自己的需求写一套。

当然了,自研也有一条明确的红线:不重复造基础轮子。diff 解析、Git 交互、Webhook 接收这类通用能力,能借助成熟库就借助;真正投入精力的是审查规则、LLM 集成方式、平台对接插件,这些才是这个项目的价值所在。

1.3 项目设计的三个核心原则

定下自研路线之后,我给自己写了三条设计原则,后面所有决策都围绕这三条来。

原则一:审查能力要分层。规则引擎是确定性检查,LLM 是智能分析,两者并行而不是取代关系。规则引擎能快速、稳定、免费地处理大量低级别问题,LLM 再对剩余的可疑片段做深度判断。这样一来,算力成本可控,响应速度也有保障。

原则二:接入成本要趋近于零。团队引入新工具最大的阻力是流程变复杂。如果审查工具需要开发者改变日常提交习惯,或者要额外登录一个平台看报告,推广起来一定会失败。所以 open-code-review 必须被动嵌入:MR 被创建时自动触发,结果自动发回 MR 评论区。开发者打开 MR,评论就在那儿了,不需要跳转任何系统。

原则三:规则和渠道都可插拔。项目最初的版本把规则写死在主程序里,接渠道的时候也直接改主函数,结果每加一个新规则就要提一次 PR,每接一个通知渠道就要梳理一遍全局逻辑。后来重构成了现在的样子:规则用装饰器注册,通知渠道做成插件接口,各管各的,互不干扰。

2. 核心模块拆解:Diff 解析、规则引擎与报告合并

2.1 Diff 解析模块:整个项目最容易翻车的地方

如果以为 diff 解析就是把文件内容按行读进来,那就太天真了。真正处理起来才会发现,diff 是高度格式敏感的。GitHub 返回的补丁格式里面有index行、--- a/路径和+++ b/路径的行前缀,每次 hunk 还有行号范围的头部说明。GitLab 某些接口返回的格式有细微差别,要是用同一套字符串解析逻辑去硬套,早晚要翻车。

我的 diff 解析模块核心做了这几件事。

换行符统一。Windows 项目常见的 CRLF 行尾如果不转成\n,解析出来的行内容会带一个\r,规则匹配时会出现莫名其妙的不命中。

按 hunk 切分。每个@@ -start,count +start,count @@头部下面是一组上下文和变更行。我需要做的是把新旧行号的映射关系建好,否则后面规则引擎报的行号全是乱的。

新增行和删除行分别处理。这对代码审查来说很重要——删除行不是我们要找的问题(除非是某段不该删的代码被删了),新增行才是引入问题的主要来源。上下文行的作用则是给 LLM 提供“看懂代码”的线索。

行号映射是最容易踩坑的部分。GitHub 的评论接口要求传具体行号,GitLab 的讨论接口传法又不一样。我在解析阶段就统一维护一份“新文件行号 ↔ 评论锚定行号”的映射表,插件层直接用这份映射,避免每个平台各写一套行号换算。

2.2 规则引擎:让规则真正“好写、好测、好维护”

规则引擎不是一堆 if-else 堆起来的。它的核心是一个注册中心和一批规则函数。注册中心维护了一个规则目录,记录每条规则的 ID、名称、适用文件类型、严重级别、是否启用。规则函数则通过装饰器注册到中心。

@rule_register.register( rule_id="PY001", name="debug_breakpoint_check", file_types=["*.py"], severity="warning", ) def check_breakpoint(ctx): issues = [] for line_no, line in ctx.new_lines: stripped = line.strip() if stripped.startswith("breakpoint()") or stripped.startswith("pdb.set_trace()"): issues.append(Issue( rule_id="PY001", line=line_no, message="检测到调试断点残留,请在提交前移除。", suggestion="删除调试语句,或改用日志方式输出调试信息。" )) return issues

上下文对象ctx封装了当前文件路径、语言类型、hunk 内容、新旧行号映射等。每条规则的逻辑都独立在一个函数里,只依赖上下文对象,不访问任何全局状态。这样做的好处一是测试方便,二是规则之间不会互相污染。

写规则这件事,逻辑本身往往不难,难的是确定规则边界。一条规则的命中率太高,会产生大量噪音,开发者看多了就麻木了;命中率太低,说明规则没抓住问题本质。业界常说的“规则要有明确意图”,做起来比听起来难。我的经验是:能枚举出 3 个以上真实故障案例的问题,才值得写一条规则;如果是“理论上可能有问题但实际没踩过坑”的情况,先写成 warning 观察一段时间再升级。

规则文件按语言分包管理,目前内置覆盖 Python、JavaScript、Java、Go 四门语言的 40 多条规则。随着项目迭代,新语言规则通过复制现有规则文件结构来扩展即可,测试用例也可以对照已有模板来写。

2.3 LLM 审查通道:怎么把大模型“塞”进审查管道

LLM 通道是这个项目里工程挑战最大的一部分。

最开始我天真地把 MR 的整个 diff 全部塞给大模型,结果很快碰壁。大型 MR 的 diff 动辄上万行,token 数直接超模型上下文限制;即便不超,上下文太长也会严重分散模型的注意力,给出的意见开始变得空泛,停留在一堆“建议增强代码可读性”的正确废话上。

后来我设计了一套分层裁剪策略。

文件级裁剪。改动文件数量超过阈值时,按“新增行数降序”排序,只取前 N 个文件进入 LLM 审查。这个依据是:新增行数越多,问题的出现概率越高。

文件内裁剪。对每个文件,只保留新增行和与新增行相邻的少量上下文行,纯删除的代码基本不影响语义判断。

消息模板精简。给模型的系统提示词只保留必要的身份定义和输出格式约束,不写一大堆无关的“你应该是一个资深专家”这种冗余话术。经过精简的提示词反而更稳定,因为模型聚焦在具体任务上而非角色演绎上。

模型输出格式的统一走了不少弯路。最初提示词里要求“以 JSON 格式输出”,但模型有时候会给出 Markdown 代码块包裹的 JSON,有时候里面夹杂解释文字,解析直接失败。后来我在提示词末尾强制加上“只输出 JSON 对象,不要包含任何其他文字”,同时在代码侧做了解析兜底:提取响应文本里最外层大括号之间的内容进行json.loads(),如果仍然失败则只记录错误日志,不阻塞整个审查管道。

模型选型上并没有绑定任何一家。open-code-review 在配置里要求用户提供 OpenAI 兼容的 API 地址和密钥。这样一套配置,既能接商用模型,也能接本地部署的开源模型。团队如果对安全要求高,数据不能出内网,那就在内网起一个模型服务,把地址指向内网即可。

2.4 报告合并去重:规则引擎和 LLM 的结果如何不打架

双通道跑完之后,最重要的一步是合并。如果不做处理,两条通道可能会针对同一个问题各报一条,报告里出现两条内容相似但行号略有差异的条目,开发者看起来会觉得这个工具连自己都没协调好。

去重逻辑我做了两层。

第一层是锚点去重。以“文件路径 + 行号区间”为锚点,如果规则引擎和 LLM 命中了同一个锚点,保留规则引擎的结果。原因很直接:规则引擎的行号是绝对精确的,而且附带规则文档链接,对开发者更有参考价值;LLM 的行号存在一定的估计误差,作为补充参考就够了。

第二层是语义去重。当 LLM 报告的行号和规则引擎命中的行号相差在三行以内时,对两条问题的描述文本做相似度计算。这里用的是简单的 Jaccard 相似度,在去掉停用词之后计算两个问题描述之间的单词交集比例,超过阈值判定为同一条问题。语义去重不追求完美,目的是在快准之间找一个平衡点,把明显重复的合并掉。

合并完成后,报告对象里每条问题都带上了规则 ID、严重级别、行号、描述和修改建议,给到插件系统做分发。

3. 接入实操:从本地仓库跑通到接上 GitHub、GitLab

3.1 本地模式:最快验证核心链路的方法

如果你想快速验证这个项目,我强烈建议先跑本地模式。它不依赖任何代码托管平台,不涉及 Webhook,不需要公网地址,只用一个本地 Git 仓库就能完成一次完整的代码审查。

准备好一个测试仓库,在里面故意制造一些典型问题:Python 文件里放一个breakpoint(),JavaScript 文件里用==而不是===,Go 文件里忽略错误返回值。这些都会命中内置规则。

然后在项目根目录执行:

python3 -m venv venv source venv/bin/activate pip install -r requirements.txt python -m open_code_review.cli \ --platform local \ --repo-path ./demo-repo \ --commit HEAD \ --config config/default_config.yaml

命令行里--platform local的意思是走本地 Git 分析通道,不走 Webhook 事件触发。--commit HEAD指定审查当前最新提交的改动。

执行完的终端输出大致长这样:

审查目标: demo-repo (commit 3f2a1c0) 审查时间: 2025-01-15 14:32:08 审查文件数: 6 发现问题总数: 5 ├─ 严重问题: 1 ├─ 警告: 2 └─ 提示: 2 [PY001] 检测到调试断点残留 src/utils/logger.py:42 建议: 删除断点语句,改用 logging.debug() 输出关键变量 [JS002] 使用了宽松相等运算符 web/component.js:88 建议: 使用 === 替代 ==,避免隐式类型转换

看到这个输出,就说明核心链路已经通了,剩下的事情都是锦上添花。

3.2 接入 GitHub:Webhook 配置与事件解析

GitHub 接入流程的核心是 Webhook。GitHub 仓库的 Settings → Webhooks → Add webhook,在 Payload URL 填上你的审查服务地址,Content type 选 JSON,触发事件勾选 Pull requests 和 Issue comments。

服务端收到请求后,需要根据X-GitHub-Event请求头来判断事件类型,只处理pull_request事件里 action 为opened、synchronize、reopened的情况。synchronize对应的是开发者推了新代码到 PR 分支,这个也必须触发重新审查,否则审查结果永远停在第一次分析的状态。

GitHub 的 API 认证用 Personal Access Token 最简单,权限只需要repo范围的读取和评论写入。如果要做得更正规,建议用 GitHub App 的形式,安装到组织层面,权限按组织统一管理,但这种做法的开发成本高一些。我的项目里两种认证方式都支持,配置文件里切换即可。

审查完成后,插件系统里的github_comment插件会自动在 PR 下面发一条评论,按照严重级别分区块展示。评论内容里每条问题都带了文件路径和行号,GitHub 会把这些行号渲染成可点击的锚点,开发者点过去就能直接看到对应代码。

3.3 接入 GitLab:兼容性处理和鉴权差异

GitLab 的接入方式整体和 GitHub 类似,但有些细节差异需要处理。

GitLab 用X-Gitlab-Event请求头标识事件类型,Merle Request Hook 的事件类型是Merge Request Hook。在 GitLab 系统设置或项目设置里创建 Webhook 时,同样填写 URL 和 Secret Token。

如果团队用的是自建 GitLab,Webhook 回调地址必须是 GitLab 服务器可以访问到的内网地址或公网地址。这个很容易被忽略——审查服务部署在 A 服务器,GitLab 在 B 服务器,A 和 B 之间的网络如果不通,GitLab 推事件直接失败。

GitLab API 的鉴权用 Personal Access Token 即可,权限范围勾选read_api和write_repository。用机器账号而不是个人账号创建 Token,避免某位同事离职后审查服务突然失效。

3.4 后启动审查服务:一条命令的事

无论接入 GitHub 还是 GitLab,审查服务本体就是一个 Python Web 服务,监听在指定的端口上。

python -m open_code_review.webhook_server \ --host 0.0.0.0 \ --port 8123 \ --config config/default_config.yaml

启动之后最好先做几件事来验证接入是否成功。

看看服务日志里有没有收到平台推送的事件记录。如果连事件都收不到,问题大概率在 Webhook 配置或网络连通性,审查逻辑都还没执行。

用curl -X POST http://localhost:8123/webhook/github -H "Content-Type: application/json" -d @test_event.json这种方式模拟发送一个测试事件,检查服务端能否正确解析并触发完整流程。

强制触发一次审查命令,确认核心分析管道在服务模式下依然正常工作。

3.5 配置体系:全局配置、仓库配置和命令行覆盖

项目里我设计了一套三层配置结构,用来应对“不同仓库有不同审查需求”这个实际场景。

全局配置default_config.yaml位于项目部署目录,保存了模型 API 地址、Token、默认开关、默认严重级别、默认通知渠道等。这些是兜底配置,任何仓库没有特殊指定时,使用全局配置。

仓库配置.open-code-review.yml放在被审查代码仓库的根目录。这个文件的优势是它可以跟着仓库走,不同团队可以按自己的规范调整规则启停和级别,不需要改审查服务端的任何配置。

review_rules: enabled: true rule_level: PY001: error PY002: warning JS001: off llm_review: enabled: true model: gpt-4o-mini max_files: 20 max_file_size: 2048 report: output_format: markdown send_to: ["github_comment", "feishu"] ignore_paths: - "build/" - "dist/" - "node_modules/"

ignore_paths值得单独说几句。没有它的时候,锁文件、构建产物、自动生成的代码会像洪水一样涌入审查管道,规则的命中率被稀释,LLM 的 token 消耗蹭蹭上涨,报告里充满噪音。加了这层忽略配置,审查信号干净多了。

在这个配置里还做了一件重要的事:把规则级别的维度拆分。同一个规则 ID 在不同仓库可以定义为 error 或 warning,甚至可以直接关闭。比如PY001调试断点残留,在公共工具库是 error,在其他业务仓库可能只是 warning,具体的取舍由仓库维护者自己决定。

4. 项目管理与协作中的踩坑经验

4.1 审查速度慢到被开发者吐槽的 4 个原因

LLM 通道刚上线时,工具响应慢的问题一度被团队吐槽得很厉害。一个 MR 创建之后,开发者等了五六分钟还看不到审查评论,体验非常糟糕。

复盘下来,慢的原因主要有四类。

第一类是串行调用 LLM。早期实现是“文件 A 分析完,再分析文件 B”,每个文件平均耗时 15 秒,五个文件就是 75 秒,加上排队网络延迟,能不慢吗?后来改成异步并发调用,用一个线程池控制并发数,5 个文件并发跑,总耗时压缩到 20 秒左右。

第二类是 diff 里的文件数量太多。一个 MR 改了 200 个文件,每个文件都去做完整分析,哪怕并发也顶不住。后来给 LLM 通道加了max_files限制,超过数量只取增删行数最多的前 20 个文件,其余走规则引擎兜底。

第三类是模型网络请求超时时间设置太长。默认 60 秒的超时时间,模型一旦响应慢,整条链路就卡在等待上。现在把超时压到 30 秒,并且加了重试机制,首次超时后换一个浅层模型再跑一次。

第四类是报告生成和插件分发串行执行。发送 GitHub 评论和发送飞书通知是独立的,没有理由串行。这里同样改为并发执行,分发环节延迟明显下降。

4.2 怎样让开发者真正愿意看审查意见

技术问题解决完之后,还有一个更难的问题:开发者不爱看审查意见。

一开始报告直接扔到 MR 评论区,长列表的格式很劝退。后来我调整了报告的排版和分发方式,效果提升很明显。

报告里每条意见增加了“为什么”字段。同一句话,只写“检测到调试断点残留”和补充一句“删除调试语句,避免测试代码跑到生产环境”带来的开发者行动率是两码事。

严重级别的信息前置。评论的顶部先用一句话总结问题分布:3 个严重问题,5 个警告。然后才按文件分类详情。开发者扫一眼就知道有没有需要必须处理的。

工具消息和人工评论分开。审查机器人以特殊账号身份发言,不带表情符号,不用感叹号,不表现得像一个试图证明自己有用的人。语气越接近客观描述,开发者越愿意看。

4.3 报告语气和规则表述的自我约束

在写规则的过程中,我很注意避免一种倾向:把代码审查工具变成一个“judge”工具。规则描述里不能出现“这段代码写得很差”“这个写法很低级”这类带有主观评价的措辞。每条表述都围绕事实展开——这个写法会触发什么问题,不修的话可能带来什么后果,建议用什么方式修改。

伤害最小化原则可能听起来很软,但实际效果非常硬。工具给出的意见如果让开发者感到被指责,他们就会开始质疑工具的每一个结论,即使工具的结论是对的。保持客观、事实化、建议式的表述,配合后面的协作代码建议功能,开发者对工具结论的接受度会稳定得多。

工具本身越谦逊,团队越愿意用它。

5. 项目复现的核心要点与后续规划

5.1 复现项目时,先抓住这几个关键环节

如果你想把这个项目重新实现一遍,或者说拿去内部部署,我的建议是不要一上来就铺所有功能。按依赖关系排序,先做这几个核心环节。

第一步,diff 解析。把 Git 补丁格式处理对,能稳定输出“文件路径 + 新增行号 + 行内容”的结构化数据。这一步没过关,后面全是空中楼阁。

第二步,规则引擎扫描。不用急着做配置体系,先写死 5 条规则跑通整条链路。能看到输出报告,这个项目的骨架就有了。

第三步,报告输出插件。先实现最简单的终端输出,把报告内容格式调好,后面接什么平台都是改发送方式而已。

第四步,LLM 通道。把裁剪策略做对,这一步最花时间,所以放到基础链路通了之后再投入精力优化。

第五步,Webhook 接入。等平台事件触发的逻辑完善了,再根据需要使用 GitHub 或 GitLab 的 API 将结果回传。

这些步骤里,每一步都可以独立使用。比如只做前两步,就是一个本地 diff 快速扫描工具;做到第三步,就能输出好看的报告;做到第四步,就成了一个带 AI 分析的审查工具。

5.2 这个项目后续可以扩展的方向

open-code-review 目前已经覆盖了基础的代码审查场景,但我觉得它还有几个值得探索的方向。

规则的自学习。通过反馈数据来调整规则的阈值和优先级。某些规则在特定仓库命中率很低,说明规则和业务场景不匹配,这些规则会根据历史数据自动降级。如果用户点击“误报”按钮,反馈会直接进入规则统计库,用于后续调优。

代码建议一键应用。目前工具只给建议,不给实现。后续可以支持对简单的代码问题直接在 MR 分支上生成修复补丁,生成基于规则功能的修改代码,减少“从建议到实现”的切换成本。这一步的难点在于修改代码的安全性——必须保证自动生成的补丁不改变原有逻辑。

多模型协同。不同模型在不同语言上的表现有差异,后续可以针对不同文件类型路由到不同的模型实现上。这个想法可以先从实验开始,等效果稳定后再集成到正式构建版本。

与本地开发工具联动。现在审查是提交之后的事。后续可以在开发者的本地 IDE 里差量提前运行同一套规则,让问题在提交前就被拦截。同样的规则既能在本地跑又能跑在 CI 上,是完善审查体验的方向之一。

5.3 我的几点实操体会

如果回到项目最开始,让我做一次选择,我还是会选择走自研这条路。不是因为现成工具不好,而是这个过程教会了我太多代码仓库之外的东西:怎么设计一套可插拔的架构,怎么在规则粒度上平衡精准度和覆盖率,怎么通过工程手段让一个智能模型变得稳定可用。这些经验在用它解决代码审查问题之前其实是很难获得的。

一个项目从“能用”到“好用”,差距往往不在于核心算法多高级,而在于细节是否被反复打磨。审查消息的语气、报告排版的可读性、规则描述的准确性、失败时的重试策略,所有这些加在一起,才构成了一个团队愿意每天使用的工具。我不主张上来就把架构做得特别复杂,从一个真实痛点出发,做成最小可用的闭环,再逐步迭代功能和性能,这个路径是我觉得最稳妥的。

最后说一个我在实际运营这个项目时的小经验:不要试图让自动化工具解决所有问题。代码审查本质上还是人的协作过程,工具可以放大人的效率,但不能取代人的判断。好的代码审查工具,应该像一个可靠的后台助手,安静地准备好所有材料,让做决策的人看得更清楚、更从容。

相关新闻

开放式代码评审机制:基于GitLab原生能力打造高效研发流程

开放式代码评审机制:基于GitLab原生能力打造高效研发流程

做技术这行&#xff0c;代码评审这四个字谁都绕不开。但我发现一个很有意思的现象&#xff1a;很多团队把 Code Review 当成发布流程里的一个“过场”&#xff0c;MR 一提交&#xff0c;象征性点个赞、回个“LGTM”&#xff0c;然后合入、上线、出事故、回滚&#xff0c;下一轮…

2026/10/19 17:11:36 阅读更多 →
Open Code Review:让代码审查从形式主义走向自动化闭环

Open Code Review:让代码审查从形式主义走向自动化闭环

先说结论&#xff1a;多数团队的 code review 没有真正产生价值&#xff0c;不是因为大家不认真&#xff0c;而是因为流程没有把“认真”变成默认选项。参与过几个从零搭建的团队模块之后&#xff0c;我越发确定这一点。open-code-review 这个名字听起来像一个开源库&#xff0…

2026/10/19 17:11:36 阅读更多 →
大模型微调实战指南:从LoRA选型到数据准备与效果验收

大模型微调实战指南:从LoRA选型到数据准备与效果验收

模型给自己的回答总是四平八稳&#xff0c;业务上却总差着一口气。你要它按企业内部流程输出&#xff0c;它给你一篇放之四海皆准的通用文案&#xff1b;你要它从一堆工单里提取关键信息&#xff0c;它宁可跟你绕圈子也不肯直接给结论——这就是为什么我一直建议身边搞技术的朋…

2026/10/19 17:11:36 阅读更多 →

最新新闻

研究生论文写作效率提升:8个亲测好用的AI辅助工具

研究生论文写作效率提升:8个亲测好用的AI辅助工具

我读研究生那几年&#xff0c;论文写作占掉了差不多三分之二的精力。最折磨人的不是“想不出写什么”&#xff0c;而是大量时间耗在重复劳动里&#xff1a;文献翻了半天找不到一句合适的话、段落写完读着像白开水、参考文献格式改到半夜、最后降重又降得面目全非。后来我把市面…

2026/10/19 17:51:01 阅读更多 →
本科毕业论文AI工具实测:10个网站覆盖选题到排版全流程

本科毕业论文AI工具实测:10个网站覆盖选题到排版全流程

本科毕业论文&#xff0c;大概是很多人学生生涯里第一次真正意义上的学术写作。选题、开题、查文献、写综述、出初稿、降重、排版……每一步都能把人磨到崩溃&#xff0c;偏偏这些环节学校里没人系统教过&#xff0c;全靠自己硬扛。这两年AI工具爆发式出现&#xff0c;几乎每周…

2026/10/19 17:51:01 阅读更多 →
Android 11 SystemServer源码解析:启动流程与Binder通信机制

Android 11 SystemServer源码解析:启动流程与Binder通信机制

坦白讲&#xff0c;Android系统源码里真正值得逐行去啃的入口类并不多&#xff0c;而SystemServer绝对算一个。它是系统启动流程中第一个纯 Java 层的大型进程&#xff0c;是Zygotefork 出来的核心子进程&#xff0c;承载了ActivityManagerService、PackageManagerService、Win…

2026/10/19 17:51:01 阅读更多 →
artcraft:从审美到工艺,手作产品可复现的实操方法论

artcraft:从审美到工艺,手作产品可复现的实操方法论

1. 从“artcraft”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“artcraft”这个标题&#xff0c;我脑子里蹦出来的第一反应是&#xff1a;这大概率不是一个单纯的绘画工具&#xff0c;也不是一个纯粹的手工教程合集。它更像是一个把“艺术&#xff08;art&#xf…

2026/10/19 17:51:01 阅读更多 →
Windows原生C/C++ BLE开发:绕过WinRT直调GATT API

Windows原生C/C++ BLE开发:绕过WinRT直调GATT API

简介&#xff1a;本资源是一套面向Windows平台C/C开发者的BLE&#xff08;低功耗蓝牙&#xff09;通信实践代码&#xff0c;适用于具备基础WinRT编程经验的中高级开发者&#xff0c;解决在桌面端调用系统API完成BLE设备扫描、连接、GATT服务发现与数据读写等核心操作的技术落地…

2026/10/19 17:51:01 阅读更多 →
Artcraft:打通数字创意与实体制造的全链路实践方法论

Artcraft:打通数字创意与实体制造的全链路实践方法论

1. 项目概述&#xff1a;当“艺术”与“工艺”在数字时代重新握手“artcraft”这个词乍看像一个生造词&#xff0c;但拆开来看——art&#xff08;艺术&#xff09; craft&#xff08;工艺&#xff09;&#xff0c;它精准击中了当下创作者群体最真实的一类实践状态&#xff1a;…

2026/10/19 17:50:00 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介&#xff1a;本资源为电源技术方向的学术研究文档&#xff0c;聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略&#xff0c;面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址&#xff1a; https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集&#xff0c;共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →