Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ jobs:
cache: 'npm'
cache-dependency-path: client/package-lock.json
- run: cd client && npm ci
# 进程环回原生模块(ADR-001):windows-latest 自带 MSVC,针对 Electron ABI 编译。
# 进程环回原生模块(ADR-001):windows-latest 自带 VS 2022。
# 必须走 native:build 脚本(本地与 CI 同一入口):直接用 client 下的
# electron-rebuild 会命中旧版 node-gyp,在 CI 报
# "Could not find any Visual Studio installation"(它仍在找 VS 2013/2015)。
# continue-on-error:它是可选增强(运行时未加载到则降级为端点环回),
# 不应因其编译失败而阻断整条发布流水线
- name: Build native process-audio module
Expand Down
52 changes: 52 additions & 0 deletions client/native/process-audio/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 进程环回原生模块构建脚本(本地与 CI 共用)
//
// @ai-context: 必须本地与 CI 用同一入口——曾因 CI 使用 client 下的旧版
// node-gyp(仍在查找 VS 2013/2015)而报 "Could not find any Visual Studio
// installation",本地却用得好,属典型"本地能跑 CI 挂"。故此处固定使用本
// 目录自带的新版 node-gyp(支持 VS 2022),并显式传入 Electron 的 ABI 目标。
// @ai-context: 必须针对 Electron 的 Node ABI 编译(--target + --dist-url),
// 否则主进程 require 时报 NODE_MODULE_VERSION 不匹配而加载失败。

import { spawnSync } from 'node:child_process';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const here = path.dirname(fileURLToPath(import.meta.url));

/** 从 client 的实际安装解析 Electron 版本(比读 package.json 的 ^范围更准) */
function resolveElectronVersion() {
const clientRequire = createRequire(path.join(here, '..', '..', 'package.json'));
try {
return clientRequire('electron/package.json').version;
} catch {
return null;
}
}

const electronVersion = resolveElectronVersion();
if (!electronVersion) {
console.error('[native-build] 未能解析 Electron 版本,请先在 client 目录执行 npm ci');
process.exit(1);
}

const args = [
'rebuild',
`--target=${electronVersion}`,
'--arch=x64',
'--dist-url=https://electronjs.org/headers',
];

console.log(`[native-build] 针对 Electron ${electronVersion} 编译 process_audio.node`);

const result = spawnSync('npx', ['node-gyp', ...args], {
cwd: here,
stdio: 'inherit',
shell: true,
});

if (result.status !== 0) {
console.error(`[native-build] 编译失败(exit=${result.status})`);
process.exit(result.status ?? 1);
}
console.log('[native-build] 编译完成');
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"preview": "vite preview",
"electron:dev": "concurrently -k -n vite,electron \"cross-env ELECTRON_BUILD=1 vite --mode test\" \"wait-on http://localhost:5173 && tsc -p electron/tsconfig.json && cross-env ELECTRON_BUILD=1 NODE_ENV=development VITE_AI_GATEWAY_URL=http://101.37.70.235:8000 electron .\"",
"electron:build": "cross-env ELECTRON_BUILD=1 vite build && tsc -p electron/tsconfig.json && electron-builder",
"native:build": "electron-rebuild --module-dir native/process-audio --only @entropydecrease/process-audio",
"native:build": "node native/process-audio/build.mjs",
"native:install": "npm --prefix native/process-audio install --no-audit --no-fund",
"test": "vitest run",
"test:watch": "vitest",
Expand Down
98 changes: 98 additions & 0 deletions docs/knowledge/bugs/2026-07-ci-node-gyp-vs2022-not-found.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 知识卡片 · 踩坑记录

## 基本信息

| 字段 | 内容 |
|------|------|
| 标题 | CI 编译原生模块报「Could not find any Visual Studio installation」——旧版 node-gyp 找不到 VS 2022 |
| 日期 | 2026-07-31 |
| 类型 | 踩坑记录 |
| 标签 | #CI #原生模块 #node-gyp #electron-rebuild #本地能跑CI挂 |

---

## 症状

首次在 CI 编译自研原生 addon(`client/native/process-audio`,见 ADR-001)时,
`windows-latest` 上 `electron-rebuild` 失败:

```
Error: Could not find any Visual Studio installation to use
at VisualStudioFinder.findVisualStudio2013 (client/node_modules/node-gyp/lib/find-visualstudio.js:380)
at VisualStudioFinder.findVisualStudio2015 (...:364)
```

关键陷阱:

- **本地(VS 2022 Community)用同一条命令完全正常**,只有 CI 失败
- runner 明确自带 VS 2022,报错却说"找不到任何 VS 安装"
- 堆栈里仍在尝试 `findVisualStudio2013/2015`——暴露了真实原因
- 该步骤配了 `continue-on-error`,所以**发布流水线全绿、版本照常发出**,
安装包只是静默缺少原生模块(自动降级为端点环回),极易被忽略

## 环境

| 项目 | 版本/信息 |
|------|----------|
| CI | GitHub Actions `windows-latest`(自带 VS 2022 + MSVC) |
| 构建 | `@electron/rebuild` + `client/node_modules` 内的 node-gyp |
| 本地 | VS 2022 Community、Python 3.14、node-gyp ^11(addon 目录自带) |

## 排查过程

1. release.yml 三 job 全绿、v0.30.0 正常发出,但功能未生效 → 先怀疑运行时加载路径
2. 抓 `Build native process-audio module` 步骤完整日志(而非只看 job 结论),
发现 `native:install` 成功、`native:build` 在 **8 秒内**失败——耗时过短说明
根本没进入编译阶段
3. 读完整堆栈:路径为 `client\node_modules\node-gyp\...`,且函数名是
`findVisualStudio2013/2015` → 用的是**旧版 node-gyp**,其 VS 探测逻辑不支持 VS 2022
4. 反问"为什么 better-sqlite3 的 rebuild 在同一 CI 上成功?"→ 因为它有
**预编译二进制**,从不真正调用 MSVC。**CI 上从未编译过任何原生代码**,
这是首次暴露

## 根因

`electron-rebuild` 使用的是**调用它的项目(client)内的 node-gyp**,版本较旧,
VS 探测逻辑只覆盖到 VS 2015/2017;而 addon 目录自带的新版 node-gyp(^11,
支持 VS 2022)根本没被用上。本地之所以正常,是因为本地曾直接在 addon 目录
执行 `npx node-gyp rebuild`(用的是新版)。

## 解决方案

不只改 CI(否则仍是两套路径),而是让**本地与 CI 共用同一构建入口**:

新增 `client/native/process-audio/build.mjs`:

- 固定使用 addon 目录自带的新版 node-gyp
- 从 client 的实际安装解析 Electron 版本(`require('electron/package.json').version`,
比读 `^35.7.5` 这类范围更准)
- 显式传 `--target=<electron> --dist-url=https://electronjs.org/headers`
保证按 Electron 的 Node ABI 编译(否则主进程 require 时报
NODE_MODULE_VERSION 不匹配)

`package.json` 的 `native:build` 指向该脚本,CI 与本地都只调这一个命令。

## 教训

- **「本地能跑 CI 挂」的第三次同类事故**(前两次:Git LFS 指针未拉取、
`.env.production` 被 gitignore)。规律是:**本地存在而 CI 不具备的隐式前提**
——已 checkout 的 LFS 文件、gitignore 掉的配置、能被解析到的新版工具链。
新增构建步骤时应主动自问:这一步依赖的东西,CI 上真的存在且版本一致吗?
- **本地与 CI 必须共用同一构建入口**。两套命令等于两套隐式前提,差异只会在
发版时暴露。
- **`continue-on-error` 是双刃剑**:它正确地保护了发布流水线(可选增强失败
不该阻断发版),但也让失败变得静默。必须配套**产物存在性检查 + `::warning::`
标注**,否则"绿灯发版但功能没进去"会被忽略数个版本。
- **排查工具链失败要看完整堆栈而非结论**:本例中 `findVisualStudio2013` 这个
函数名直接指向了根因;只看 "Could not find any Visual Studio" 会误判为
runner 缺少 VS 而白费力气去装编译器。
- **反问"为什么相邻的同类步骤是成功的"**:better-sqlite3 能 rebuild 却帮不到
自研模块,是因为它走预编译。这个对比直接缩小了范围。

## 参考

- 决策背景:`docs/adr/ADR-001-audio-capture-process-loopback.md`
- 构建脚本:`client/native/process-audio/build.mjs`
- CI 步骤:`.github/workflows/release.yml`(Build native process-audio module)
- 同类卡片:[Git LFS 图标未在 CI 拉取](./2026-07-git-lfs-icon-electron-builder-ci-failure.md)、
[`.env.production` 被 gitignore](./2026-07-ci-env-production-gitignore-supabase-placeholder.md)
4 changes: 3 additions & 1 deletion docs/knowledge/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

| 日期 | 标题 | 标签 |
|------|------|------|
| 2026-07-31 | [CI 编译原生模块报「Could not find any Visual Studio installation」——旧版 node-gyp 找不到 VS 2022](./bugs/2026-07-ci-node-gyp-vs2022-not-found.md) | #CI #原生模块 #node-gyp #本地能跑CI挂 |
| 2026-07-31 | [课堂助手精细采集三症状:视觉抓页面元数据、ASR 静音幻觉、截断 JSON 泄漏 UI](./bugs/2026-07-classroom-capture-asr-hallucination-json-leak.md) | #课堂助手 #多模态 #ASR幻觉 #prompt工程 |
| 2026-07-31 | [番茄钟"跳过"退化为"取消":一个 onClose 回调承载两种意图,空目标番茄无法启动](./bugs/2026-07-pomodoro-goal-skip-acts-as-cancel.md) | #React #番茄钟 #弹窗交互 #回调语义 #意图区分 |
| 2026-07-31 | [登录失败后持续要求登录:AuthGuard 与“跳过登录”的模式降级缺口 + session-expired 事件风暴](./bugs/2026-07-login-loop-authguard-mode-gap.md) | #认证 #AuthGuard #路由守卫 #模式管理 #事件去重 #死循环 |
| 2026-07-31 | [Tailwind v3 对 `var()` 令牌色的 `/透明度` 修饰符静默失效,明亮主题弹窗背景全透明](./bugs/2026-07-tailwind-var-alpha-modifier-silent-drop.md) | #Tailwind #CSS #主题 #DesignTokens #color-mix |
| 2026-07-31 | [番茄钟计数异常:无重置路径的周期计数 + 跨模式状态残留 + store/hook 副作用双重执行](./bugs/2026-07-pomodoro-count-reset-and-duplicate-side-effects.md) | #Zustand #状态管理 #副作用 #番茄钟 #数据统计 |
Expand All @@ -31,4 +33,4 @@ _(暂无)_

- **技术**:#CSS #a11y #React #Vite #Tailwind #Zustand #CI #GitLFS #electron-builder #GitHubActions #CDN #阿里云 #环境变量 #Supabase #DesignTokens #color-mix #认证 #AuthGuard
- **类型**:#bug #方案 #学习 #复盘
- **模块**:#启动仪式 #reduced-motion #animation #发布 #安装包 #性能诊断 #测量方法 #番茄钟 #主题 #状态管理 #副作用 #数据统计 #路由守卫 #模式管理 #事件去重
- **模块**:#启动仪式 #reduced-motion #animation #发布 #安装包 #性能诊断 #测量方法 #番茄钟 #主题 #状态管理 #副作用 #数据统计 #路由守卫 #模式管理 #事件去重 #弹窗交互 #回调语义 #意图区分
Loading