Bruno 跨平台代码评审机制:cross-platform reviewer 如何在 macOS、Windows、Linux 三端守住一致性底线
Bruno 是一个同时在 macOS、Windows 和 Linux 上发布的 Electron 应用,其 .claude/skills/code-review/ 目录下内置了一套"多镜头并行"的 AI 代码评审技能,其中 cross-platform reviewer 专门负责在每次 diff 评审中拦截跨平台隐患。本文以该 reviewer 的定义文件为主体,完整梳理它的评审范围、blocker/suggestion 两级检查清单与输出契约,并结合 .claude/rules/cross-platform.md 规则文件及仓库源码(路径工具、DSL 解析器、Electron 主进程、安装脚本),说明每一条检查项背后对应的真实代码实践。读完本文,你可以理解"什么改动会在三端某一边炸掉",以及如何按 Bruno 的既定契约运行这套跨平台评审。
一、定位:一个"永不跳过"的全局评审镜头
Bruno 的评审技能由 .claude/skills/code-review/SKILL.md 编排:评审不是一次性通读,而是拆成多个聚焦镜头(lens)并行执行,每个镜头有自己的检查清单文件,编排者负责取 diff、分发、合并结果。镜头清单如下:
| Reviewer 文件 | 镜头 | 作用范围 |
|---|---|---|
reviewers/correctness.md |
正确性与根因 | 全部源码(除 tests/**) |
reviewers/architecture.md |
架构与依赖边界 | packages/** |
reviewers/conventions.md |
编码规范与可读性 | 全部文件 |
reviewers/react.md |
React(app 层) | packages/bruno-app/** |
reviewers/cross-platform.md |
跨平台(macOS/Windows/Linux) | 全部文件 |
reviewers/security.md |
安全与数据安全 | 全部源码(除 tests/**) |
reviewers/dsl-changes.md |
磁盘 DSL 与序列化(向后兼容) | bruno-app、bruno-electron、bruno-cli、bruno-lang、bruno-filestore、bruno-schema(-types)、bruno-converters |
reviewers/e2e-tests.md |
Playwright E2E 测试 | tests/** |
cross-platform 镜头的关键特征有两点:
- Scope 是
**/*—— 即所有变更文件。SKILL.md 明确规定:"Never skip the lenses scoped to all files"(范围覆盖全部文件的镜头永不跳过),因此无论 diff 只改了 UI 组件还是只改了 CLI 工具,跨平台检查都会执行; - 它不是凭空检查,而是要求评审者先读取并对照
.claude/rules/cross-platform.md规则文件(下文第四节逐条展开),再按该规则报告违规项。
二、评审输出契约:一行一条发现
.claude/skills/code-review/reviewers/_contract.md 定义了所有 reviewer 共享的人格与输出契约。cross-platform reviewer 开头即声明:"Adopt the reviewer persona and return findings in the output contract defined in _contract.md"。
契约要点:
- 评审者人格:TypeScript、JavaScript、Node.js、Electron 企业团队上的资深评审者,表述简洁(每条发现一句清晰的话);以仓库实际代码为准——当文档、指南或注释与代码不一致时,信任代码;不得引用未经验证的行号或虚构示例值;不因作者资历而放松严重度。
- 输出格式:扁平列表,一行一条发现:
<blocker|suggestion|nit> | <file>:<line> | <one-sentence finding>
- 范围干净时只返回
no findings,不得为了凑数而编造 nit。
这意味着 cross-platform reviewer 的产物是可以被 CI 或人工直接解析的结构化清单,每条都带 file:line 可回溯定位。
三、检查清单详解:blocker 与 suggestion 两级
.claude/skills/code-review/reviewers/cross-platform.md 把违规项分为两个严重度。以下清单是 reviewer 文件原文的完整继承,并补充了每项的判定要点。
3.1 blocker —— 会让某个平台直接坏掉的问题
blocker 级问题的共同特征是"三端中至少一端会实际崩溃或行为错误",reviewer 要求逐条报告:
- 硬编码的路径分隔符:在拼接路径时直接写死
/或\,而不是用path.join/path.resolve。例如在字符串模板里写path/to/file或C:\data\xxx,在另一类操作系统上会被解析为单个文件名。 - 硬编码的用户目录路径:直接写死
/home/、C:\Users\、~/,而不是用os.homedir()或 Electron 的app.getPath()。 - 平台专属 shell /
child_process用法且没有回退:典型如用which找可执行文件(Windows 上是where),或在 Windows 上spawn命令类脚本(npm实为npm.cmd)却缺少shell: true的适配。 - Unix-only 信号:只监听
SIGINT/SIGTERM而假设它们在 Windows 上可靠送达。 - 使用
/tmp代替os.tmpdir():临时目录必须经 Node 的os.tmpdir()解析,不能假设 POSIX 路径存在。
3.2 suggestion —— 尚非具体故障、但可移植性脆弱的模式
这一级针对"现在还能跑,但埋着雷"的写法:
- 大小写敏感性假设:把路径当作大小写敏感处理,而 Windows 文件系统大小写不敏感;CRLF/LF 处理不一致:同一处换行处理逻辑在不同平台文件下行为不同;
fs.chmod/fs.access假设 Unix 权限位:在 Windows 上这些接口的语义与 POSIX 不同,依赖其返回值做权限判断是脆弱模式。 - 多行
.bru/ 文本块解析时按\n切分:reviewer 明确指向规则文件的 Line Endings 一节——解析多行内容必须用 CRLF-aware 的正则/\r\n|\r|\n/,而不能只按\n切。这一条与 Bruno 的 DSL 解析直接相关(见第五节源码印证)。
一个值得注意的细节:规则文件 .claude/rules/cross-platform.md 的 frontmatter 将其规则路径限定在 scripts/** 与 packages/bruno-electron/**(即这些目录下的代码自动受该规则约束),而 reviewer 文件把同样的检查标准放宽到所有变更文件——因为跨平台隐患(尤其是 .bru 解析)同样出现在 bruno-lang 等包中。
四、规则底座:.claude/rules/cross-platform.md 全文要点
reviewer 文件只有十几行,它的"知识库"是 .claude/rules/cross-platform.md。该规则开篇声明总前提:
"This is an Electron app targeting macOS, Windows, and Linux. All code touching the filesystem, child processes, signals, or paths must work on all three platforms."
以下按规则文件的七个板块逐一展开。
4.1 Filesystem(文件系统)
fs.rmSync带force: true只抑制ENOENT,不抑制EPERM/EBUSY。Windows 对文件锁非常激进(杀软、索引、打开句柄都会锁文件),删除目录时应始终配合maxRetries和retryDelay使用。- 所有路径必须用
path.join()/path.resolve(),绝不硬编码/分隔符。 - Windows 路径大小写不敏感;比较 collection/workspace 路径时使用
normalizePath()(来自utils/common/path)。 app.getPath()返回平台相关目录(userData、documents等),不要假设 POSIX 风格位置。- 文件监听器(chokidar)在不同平台可能发出不同的事件顺序,不要依赖特定的 add/change/unlink 序列。
4.2 Child Processes(子进程)
spawn('npm', [...])在 Windows 上需要shell: true,因为 Windows 上的npm实际是npm.cmd批处理文件,不是可直接 exec 的原生二进制。- 当使用
shell: true时,child.kill()在 Windows 上只能杀掉 shell 包装层(cmd.exe),子进程树会成为孤儿进程;要杀掉整棵进程树需用taskkill /pid <pid> /T /F。 execSync/spawn的命令要避开 Unix-only 语法:&&链在 cmd.exe 中可用,但管道与重定向的行为不同。
4.3 Signals & Shutdown(信号与退出)
SIGINT/SIGTERM在 Windows 且shell: true时不可靠,还应把SIGHUP作为回退一并处理。- 应用退出必须关闭所有文件监听器:每个 watcher 类各自实现
closeAllWatchers(),由index.js中的closeAllWatchers()统一编排(源码印证见 5.3 节)。
4.4 Line Endings(换行符)
这是与 Bruno 核心 DSL 最相关的一条:
Windows 上编写的文件使用 CRLF。逐行解析多行
.bru/文本块时,必须用 CRLF-aware 正则(/\r\n|\r|\n/)切分,绝不能只按\n切——否则行尾的\r会泄漏进解析出的值,造成虚假的 dirty 状态和 diff。参考模式是bruno-lang的v2/src/envToJson.js中的.split(/\r\n|\r|\n/),新写的解析器必须与其保持一致。
4.5 stdout vs stderr
开发工具(rsbuild、webpack、electron-builder)在 Windows 上可能把启动输出路由到 stderr 而不是 stdout。因此凡是通过进程输出检测模式(如判断构建是否就绪)的逻辑,必须同时检查两个流。
4.6 Platform-Specific Dependencies(平台专属依赖)
- scripts/setup.js 中的
forceInstallPlatformDeps()负责安装平台专属原生模块,例如@lydell/node-pty-{platform}-{arch}系列(源码印证见 5.2 节)。 - Electron builder 配置按
mac/win/linux目标分别处理打包。
4.7 Path Separators in Collection/Workspace Stores(存储层路径分隔符)
- electron-store 按原样持久化路径:macOS 上打开的 workspace 存的是
/Users/...,Windows 上是C:\Users\...。跨机器共享/比较这些路径时必须归一化。 - 环境变量
ELECTRON_USER_DATA_PATH仅在isDev为 true 时生效(对应index.js第 21 行的判断,源码印证见 5.3 节)——生产包里改这个变量不会改变userData位置。
五、源码印证:规则不是纸面条款,仓库里处处有对应实现
规则文件里点名的每一处"参考模式",在仓库中都能找到真实代码。以下逐一核对。
5.1 normalizePath() 与路径工具
规则要求用 utils/common/path 的 normalizePath() 比较路径,实现位于 packages/bruno-app/src/utils/common/path.js:
const normalizePath = (p) => {
if (!p) return '';
return p.replace(/\\/g, '/').replace(/\/+$/, '');
};
即"反斜杠统一转正斜杠 + 去掉末尾斜杠",使 macOS 与 Windows 写出的路径字符串可以直接比较。同一文件还揭示了 Bruno 处理"路径要进版本库"这一跨平台难题的整体策略:
- posixify(
str.replace(/\\/g, '/')):bruno.json等提交进 git 的配置文件里,相对路径统一存成正斜杠格式——Windows 原生兼容正斜杠,Unix 天然识别,从而避免 Windows 用户提交的certs\\client.pem在 Unix 同事机器上解析失败(文件头部注释完整论述了这个设计动机与收益:减少 git 冲突、无需手工转换路径)。 - getRelativePath / getAbsoluteFilePath / getRelativePathWithinBasePath 都带
shouldPosixify参数,并基于const brunoPath = isWindowsOS() ? path.win32 : path.posix(path.js#L44)在运行时选用对应平台的 path API 计算——这正是 reviewer 对"硬编码分隔符"报 blocker 时,仓库内部给出的正确写法范式。
5.2 平台专属依赖:forceInstallPlatformDeps()
规则点名的函数在 scripts/setup.js:
function forceInstallPlatformDeps() {
// Note: make sure to hard pin deps and only add deps that have been checked
// for sec vuln already since the following will be force installed.
const deps = {
darwin: ['@lydell/node-pty-darwin-arm64@1.1.0', '@lydell/node-pty-darwin-x64@1.1.0'],
win32: ['@lydell/node-pty-win32-arm64@1.1.0', '@@lydell/node-pty-win32-x64@1.1.0'],
linux: ['@lydell/node-pty-linux-arm64@1.1.0', '@lydell/node-pty-linux-x64@1.1.0']
};
...
const toInstall = deps[process.platform];
execCommand(
`npm i --legacy-peer-deps --no-save --force ${toInstall.join(' ')}`,
'Installing platform specific dependencies'
);
}
按 process.platform 分派硬钉版本(注释强调"force install 的依赖必须已做过安全审计并锁死版本"),安装 node-pty 的平台二进制——终端能力依赖原生模块,是典型的"必须三端分别处理"的依赖。该函数在 setup() 的 npm i 之后被调用(setup.js#L101)。
5.3 监听器编排与 dev-only 的 userData 重定向
packages/bruno-electron/src/index.js 中,规则"App shutdown must close all file watchers"的落点清晰可见:
const closeAllWatchers = () => Promise.allSettled([
collectionWatcher.closeAllWatchers(),
workspaceWatcher.closeAllWatchers(),
apiSpecWatcher.closeAllWatchers()
]);
三个 watcher 类各自实现了 closeAllWatchers()(如 collection-watcher.js#L1067、workspace-watcher.js#L312、apiSpecsWatcher.js#L136),由主进程统一以 Promise.allSettled 编排关闭。而 ELECTRON_USER_DATA_PATH 的 dev-only 限制就在同文件 index.js#L21-L25:if (isDev && process.env.ELECTRON_USER_DATA_PATH) 成立时才执行 app.setPath('userData', ...)——与规则 4.7 节"only applies when isDev is true"逐字对应。
5.4 CRLF-aware 切分的参考模式
规则指认的"参考模式"在 packages/bruno-lang/v2/src/envToJson.js 的多行文本块语义中:
multilinetextblock(_1, content, _2) {
return content.ast
.split(/\r\n|\r|\n/)
.map((line) => line.slice(indentLevel)) // Remove 4-space indentation
.join('\n')
.trim();
},
环境文件(.bru env)支持三引号多行文本块(如内嵌 PEM 公钥,见该文件头部注释),切分必须兼容 CRLF/LF/CR 三种换行,否则 Windows 写出的文件解析后每行尾部多一个 \r,触发规则警告的"spurious dirty-state and diffs"。同一文件的 ohm 文法里 nl = "\\r"? "\\n"(envToJson.js#L27)也在语法层面把 CRLF 纳入换行定义,两处配合保证了 DSL 解析的跨平台一致性。
5.5 Windows 进程树终止的真实用例
规则中 taskkill /pid <pid> /T /F 的建议在测试基建里有真实落地:tests/ssl/client-certs/server/helpers/platform.js 在 Windows 分支上用 taskkill /F /PID ${pid} 终止测试服务端,并专门处理了"PID 已退出时 taskkill 返回非零"的边界;tests/ssl/custom-ca-certs/server/helpers/platform.js 也按平台分派了 kill 逻辑。这类 helper 正是"Unix 信号在 Windows 不可靠"这一条规则在工程中的具体解法。
六、实操:如何按 Bruno 的契约跑一次跨平台评审
结合 SKILL.md 的编排流程,在仓库本地运行这套评审的完整步骤是:
- 取 diff(只评审变更部分,绝不评审未触碰的代码):
- 已提交范围(默认):
git diff main...HEAD,基线分支为main或release/*。评审前先git fetch确认本地基线是最新的——过期的基线会把已合入的无关变更灌进 diff,白白放大一次 fan-out。 - 未提交的工作区:树可能在评审中途变化,需一次性冻结 diff 到临时文件再分发给所有评审者:
git add -N . && git diff HEAD > "$SCRATCH/review.diff"(先git add -N .让新增未跟踪文件也可见,可用git reset撤销)。
- 已提交范围(默认):
- 枚举变更文件:
git diff --name-only main...HEAD,据此跳过作用域未命中的镜头(如没改packages/bruno-app/**就跳过react.md);凡 scope 为全部文件的镜头(含 cross-platform)永不跳过。 - 并行分发:单条消息内为每个在范围内的 reviewer 启动一个子代理,告知其 diff 来源、自己负责的文件 glob,并要求其先读
_contract.md与对应 reviewer 文件及其指向的规则文件,只按该镜头、在该镜头指定的严重度上评审。评审者是只读且互相独立的,镜头之间允许重复发现,由编排者在合并阶段去重。 - 合并与报告:收集全部发现,去掉完全重复项;两个镜头命中同一
file:line时保留较高严重度;最终按文件分组,每条发现标注 blocker / suggestion / nit 与file:line。没有问题就简短说明"无发现",不制造 nit。
对照这套流程,cross-platform reviewer 的典型产出形如:
blocker | packages/bruno-electron/src/app/xxx.js:120 | Uses hardcoded '/tmp' instead of os.tmpdir()
suggestion | packages/bruno-lang/v2/src/yyy.js:45 | Multiline .bru block is split on '\n' only; use /\r\n|\r|\n/ per the Line Endings rule
七、小结
Bruno 的跨平台保障不靠口头约定,而是三层咬合:
- 规则层
.claude/rules/cross-platform.md定义了文件系统、子进程、信号、换行符、stdout/stderr、平台依赖、存储路径七个板块的硬性要求,每一条都有仓库内的参考实现; - 评审层 .claude/skills/code-review/reviewers/cross-platform.md 把规则压缩为"全文件范围 + blocker/suggestion 两级清单"的可执行检查视角,并按
_contract.md的<severity> | <file>:<line> | <finding>契约输出可机器解析的结论; - 实现层 的 路径工具、DSL 解析器、Electron 主进程 与 安装脚本 提供了这些规则可回溯的落地证据。
对贡献者而言,最直接的行动指南就是三句话:路径永远走 path.join/path.resolve 与 os.homedir()/app.getPath();临时目录用 os.tmpdir();解析 .bru 文本块用 /\r\n|\r|\n/ 切分。 守住这三条,绝大多数 blocker 级的跨平台问题在提交前就能被自己和评审镜头同时拦下。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00