首页
/ Bruno 跨平台代码评审机制:cross-platform reviewer 如何在 macOS、Windows、Linux 三端守住一致性底线

Bruno 跨平台代码评审机制:cross-platform reviewer 如何在 macOS、Windows、Linux 三端守住一致性底线

2026-09-05 10:43:24作者:曹令琨Iris

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-appbruno-electronbruno-clibruno-langbruno-filestorebruno-schema(-types)bruno-converters
reviewers/e2e-tests.md Playwright E2E 测试 tests/**

cross-platform 镜头的关键特征有两点:

  1. Scope 是 **/* —— 即所有变更文件。SKILL.md 明确规定:"Never skip the lenses scoped to all files"(范围覆盖全部文件的镜头永不跳过),因此无论 diff 只改了 UI 组件还是只改了 CLI 工具,跨平台检查都会执行;
  2. 它不是凭空检查,而是要求评审者先读取并对照 .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 要求逐条报告:

  1. 硬编码的路径分隔符:在拼接路径时直接写死 /\,而不是用 path.join / path.resolve。例如在字符串模板里写 path/to/fileC:\data\xxx,在另一类操作系统上会被解析为单个文件名。
  2. 硬编码的用户目录路径:直接写死 /home/C:\Users\~/,而不是用 os.homedir() 或 Electron 的 app.getPath()
  3. 平台专属 shell / child_process 用法且没有回退:典型如用 which 找可执行文件(Windows 上是 where),或在 Windows 上 spawn 命令类脚本(npm 实为 npm.cmd)却缺少 shell: true 的适配。
  4. Unix-only 信号:只监听 SIGINT/SIGTERM 而假设它们在 Windows 上可靠送达。
  5. 使用 /tmp 代替 os.tmpdir():临时目录必须经 Node 的 os.tmpdir() 解析,不能假设 POSIX 路径存在。

3.2 suggestion —— 尚非具体故障、但可移植性脆弱的模式

这一级针对"现在还能跑,但埋着雷"的写法:

  1. 大小写敏感性假设:把路径当作大小写敏感处理,而 Windows 文件系统大小写不敏感;CRLF/LF 处理不一致:同一处换行处理逻辑在不同平台文件下行为不同;fs.chmod / fs.access 假设 Unix 权限位:在 Windows 上这些接口的语义与 POSIX 不同,依赖其返回值做权限判断是脆弱模式。
  2. 多行 .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.rmSyncforce: true 只抑制 ENOENT,不抑制 EPERM/EBUSY。Windows 对文件锁非常激进(杀软、索引、打开句柄都会锁文件),删除目录时应始终配合 maxRetriesretryDelay 使用。
  • 所有路径必须用 path.join() / path.resolve(),绝不硬编码 / 分隔符。
  • Windows 路径大小写不敏感;比较 collection/workspace 路径时使用 normalizePath()(来自 utils/common/path)。
  • app.getPath() 返回平台相关目录(userDatadocuments 等),不要假设 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-langv2/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/pathnormalizePath() 比较路径,实现位于 packages/bruno-app/src/utils/common/path.js

const normalizePath = (p) => {
  if (!p) return '';
  return p.replace(/\\/g, '/').replace(/\/+$/, '');
};

即"反斜杠统一转正斜杠 + 去掉末尾斜杠",使 macOS 与 Windows 写出的路径字符串可以直接比较。同一文件还揭示了 Bruno 处理"路径要进版本库"这一跨平台难题的整体策略:

  • posixifystr.replace(/\\/g, '/')):bruno.json 等提交进 git 的配置文件里,相对路径统一存成正斜杠格式——Windows 原生兼容正斜杠,Unix 天然识别,从而避免 Windows 用户提交的 certs\\client.pem 在 Unix 同事机器上解析失败(文件头部注释完整论述了这个设计动机与收益:减少 git 冲突、无需手工转换路径)。
  • getRelativePath / getAbsoluteFilePath / getRelativePathWithinBasePath 都带 shouldPosixify 参数,并基于 const brunoPath = isWindowsOS() ? path.win32 : path.posixpath.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#L1067workspace-watcher.js#L312apiSpecsWatcher.js#L136),由主进程统一以 Promise.allSettled 编排关闭。而 ELECTRON_USER_DATA_PATH 的 dev-only 限制就在同文件 index.js#L21-L25if (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 的编排流程,在仓库本地运行这套评审的完整步骤是:

  1. 取 diff(只评审变更部分,绝不评审未触碰的代码):
    • 已提交范围(默认):git diff main...HEAD,基线分支为 mainrelease/*。评审前先 git fetch 确认本地基线是最新的——过期的基线会把已合入的无关变更灌进 diff,白白放大一次 fan-out。
    • 未提交的工作区:树可能在评审中途变化,需一次性冻结 diff 到临时文件再分发给所有评审者:git add -N . && git diff HEAD > "$SCRATCH/review.diff"(先 git add -N . 让新增未跟踪文件也可见,可用 git reset 撤销)。
  2. 枚举变更文件git diff --name-only main...HEAD,据此跳过作用域未命中的镜头(如没改 packages/bruno-app/** 就跳过 react.md);凡 scope 为全部文件的镜头(含 cross-platform)永不跳过
  3. 并行分发:单条消息内为每个在范围内的 reviewer 启动一个子代理,告知其 diff 来源、自己负责的文件 glob,并要求其先读 _contract.md 与对应 reviewer 文件及其指向的规则文件,只按该镜头、在该镜头指定的严重度上评审。评审者是只读且互相独立的,镜头之间允许重复发现,由编排者在合并阶段去重。
  4. 合并与报告:收集全部发现,去掉完全重复项;两个镜头命中同一 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 的跨平台保障不靠口头约定,而是三层咬合:

对贡献者而言,最直接的行动指南就是三句话:路径永远走 path.join/path.resolveos.homedir()/app.getPath();临时目录用 os.tmpdir();解析 .bru 文本块用 /\r\n|\r|\n/ 切分。 守住这三条,绝大多数 blocker 级的跨平台问题在提交前就能被自己和评审镜头同时拦下。

登录后查看全文
热门项目推荐
相关项目推荐