diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e4040b1..214087bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/client/native/process-audio/build.mjs b/client/native/process-audio/build.mjs new file mode 100644 index 00000000..57a3da6c --- /dev/null +++ b/client/native/process-audio/build.mjs @@ -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] 编译完成'); diff --git a/client/package.json b/client/package.json index 83986d35..fe96571f 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/docs/knowledge/bugs/2026-07-ci-node-gyp-vs2022-not-found.md b/docs/knowledge/bugs/2026-07-ci-node-gyp-vs2022-not-found.md new file mode 100644 index 00000000..c2e7f407 --- /dev/null +++ b/docs/knowledge/bugs/2026-07-ci-node-gyp-vs2022-not-found.md @@ -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= --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) diff --git a/docs/knowledge/index.md b/docs/knowledge/index.md index 1ca556d4..dfabf60d 100644 --- a/docs/knowledge/index.md +++ b/docs/knowledge/index.md @@ -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 #状态管理 #副作用 #番茄钟 #数据统计 | @@ -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 #发布 #安装包 #性能诊断 #测量方法 #番茄钟 #主题 #状态管理 #副作用 #数据统计 #路由守卫 #模式管理 #事件去重 #弹窗交互 #回调语义 #意图区分