diff --git a/WIN7_FIX.md b/WIN7_FIX.md new file mode 100644 index 00000000..07c5e86f --- /dev/null +++ b/WIN7_FIX.md @@ -0,0 +1,165 @@ +# Windows 7 兼容性说明与替代构建方案 + +在 Windows 7 上直接执行 `pnpm build` 会因原生打包工具崩溃而失败(`0xc0000005` 访问违例)。 +本文说明根因,并提供一套**完全不加载原生二进制**的替代构建方案:`build-win7.mjs`。 + +--- + +## 1. 根因:原生工具链依赖 Win8+ 系统 API + +本仓库的正常构建链是: + +| 阶段 | 工具 | 实现 | +| --- | --- | --- | +| `packages/browser/bridge-browser` | `tsc -b` + `tsdown` | tsdown 底层是 **rolldown**(Rust,`@rolldown/binding-win32-x64-msvc.exe`) | +| `extensions/dsh-browser` | `vite build` ×3 | vite 底层是 **esbuild**(Go,`@esbuild/win32-x64/esbuild.exe`)与 **rollup**(Rust,`@rollup/rollup-win32-x64-msvc.exe`) | + +这些 `.exe` 是 Go/Rust 编译的原生程序,内部调用了 **Windows 8 / Server 2012 才引入** 的系统 API,典型的有: + +- `kernel32.dll!GetSystemTimePreciseAsFileTime`(Win8 引入的高精度时钟) +- `kernel32.dll!QueryUnbiasedInterruptTimePrecise`(Win8) +- `kernel32.dll!SetThreadDescription`(Win10 1607) + +Windows 7 的 `kernel32.dll` **没有这些导出函数**。原生二进制在加载/启动阶段解析导入表失败,随即触发 `0xc0000005`(EXC032026 进程崩溃,通常是 "The NTDLL.dll!LdrpProcessRelocationBlock" 之后的非法访问)。表现为: + +``` +$ pnpm build +> vite build +# 无任何输出,进程立即退出,退出码 0xC0000005(-1073741819) +``` + +同理,`@deepseek-ai/libreoffice-kit-win32-x64/bin/libreoffice-kit.exe`(DSH 运行时依赖,与构建无关)也携带同样的 API。 + +## 2. 替代方案:纯 WASM / 纯 JS 工具链 + +方案只使用两种**无原生二进制**的实现: + +| 工具 | 说明 | +| --- | --- | +| **`esbuild-wasm`** | esbuild 官方的 WebAssembly 构建(`esbuild.wasm`),由 Node 加载运行,API 与 `esbuild` 完全一致 | +| **`@rollup/wasm-node`** | rollup 官方的纯 JS/WASM 构建,不含 `rollup.exe` 类原生可执行文件 | +| **`typescript`(transpileModule)** | 纯 JS 的 TypeScript 编译器,用于转译而非类型检查 | + +`build-win7.mjs` 在运行时优先 `import('esbuild-wasm')` / `import('@rollup/wasm-node')`;两者均不可用时给出清晰的安装提示并退出。 + +## 3. 快速开始 + +```bash +# 1. 安装依赖(仅所需工作区;--ignore-scripts 可跳过全部 postinstall 原生脚本) +pnpm install --filter @yuxianglin/dsh-bridge-browser --filter dsh-browser-extension + +# 2a. Windows 7 上构建 chrome 扩展(产物 dist/) +node build-win7.mjs + +# 2b. 构建 firefox 扩展(产物 dist-firefox/) +node build-win7.mjs --firefox + +# 或通过 npm 脚本 +pnpm build:win7 +pnpm build:win7:firefox +``` + +构建成功后,扩展可直接加载: + +- **Chrome**:`chrome://extensions` → 开发者模式 → 加载 `extensions/dsh-browser/dist` +- **Edge**:`edge://extensions` → 开发者模式 → 加载 `dist` +- **Firefox**:`about:debugging` → 临时加载 `dist-firefox/manifest.json` + +> 非 Windows 7 用户**不受任何影响**:`pnpm build` 及原有工具链保持原样,本方案是增量而非替换。 + +## 4. `build-win7.mjs` 做了什么 + +### 阶段 1:bridge 库(`@rollup/wasm-node` + TS 转译插件) + +对齐原 `tsc -b && tsdown` 的产物语义(`tsdown.config.ts`:ESM / node 平台 / 不打 dts / external 运行期依赖): + +- 入口 `src/{index,protocol,invariant}.ts` → `lib/*.js`(ESM);`@deepseek-ai/*`、`ws`、`node:*` 保持外部引用(由宿主提供) +- 内联 rollup 插件用 `ts.transpileModule` 做逐文件转译(`isolatedModules` 语义;类型检查请走 `pnpm typecheck`) +- `src/client.js` 原样复制到 `lib/`(与原构建的 `node -e "copyFileSync(...)"` 一致) +- `--no-types` 之外的运行会调用 `tsc -b` 生成 `lib/types/` 声明产物(纯 JS,失败仅告警不阻断) + +若 `@rollup/wasm-node` 缺失,自动回退到 `esbuild-wasm` 打包 bridge。 + +### 阶段 2:扩展三目标(`esbuild-wasm`,对齐 `scripts/build.mjs` 的三个 vite 配置) + +| 目标 | 入口 | 格式 | 产物 | +| --- | --- | --- | --- | +| background | `src/background/index.ts` | chrome: `es`(MV3 module service worker)
firefox: `iife` | `dist/background.js` | +| content | `src/content/index.ts` | `iife` | `dist/content.js` | +| panel | `src/panel/main.tsx` | `iife` + JSX automatic + CSS 提取 | `dist/panel/assets/main.js`、`main.css` | + +`import.meta.env.EXT_TARGET` 通过 esbuild `define` 注入(对应 `vite.shared.ts` 的行为);`deepseek-256.png` 等资源用 `file` loader 输出为独立文件,与 vite 产物布局一致。 + +### 阶段 3:静态资源与 HTML(纯 Node `fs`) + +复刻 `vite.shared.ts` 的 `copyManifest` 插件: + +- `manifest.json`(firefox 时为 `manifest.firefox.json`)→ `dist/manifest.json` +- `_locales/`、`assets/`(icons 等)→ `dist/` 对应目录 +- `panel/index.html` → 重写产物 HTML:脚本指向 `./assets/main.js`,注入 `./assets/main.css` 样式引用 + +### 原生风险扫描与 GoWin7Fixer 兜底(可选) + +构建结尾会用**纯 Node 解析 PE 导入表**(不执行目标文件)扫描 `node_modules` 中的 `.exe`,报告携带 Win8+ API 导入的文件,例如: + +``` +[win7] 原生二进制扫描:发现 1 个携带 Win8+ API 导入的可执行文件: +[win7] - @deepseek-ai/libreoffice-kit-win32-x64/bin/libreoffice-kit.exe(kernel32.dll!GetSystemTimePreciseAsFileTime) +[win7] 本 Win7 构建链不加载它们,可安全忽略;如需修补这些文件,请加 --fix-native, + 或以管理员身份运行并拖拽文件到 GoWin7Fixer.exe。 +``` + +这些文件**不在本构建的加载路径上**,提示仅为信息目的。若某些第三方工具(例如 DSH 运行时)确实需要在 Win7 上启动而无法绕过,可以使用 [GoWin7Fixer](https://github.com/GoWin7Fixer)(一个 Windows 7 API 兼容修补 GUI): + +```bash +# 扫描并列出风险文件 +node build-win7.mjs --scan-native + +# 扫描并尝试用 GoWin7Fixer.exe 修补(按 GOWIN7FIXER_PATH / 桌面快捷方式 / Downloads / PATH 查找) +node build-win7.mjs --fix-native +``` + +`--fix-native` 会把风险文件作为参数传给 `GoWin7Fixer.exe` 并启动之;该程序是**拖放式 GUI**(实测不处理命令行参数),如未自动修补,请在弹出的窗口中把文件拖入并确认。 + +## 5. 命令行选项一览 + +| 选项 | 作用 | +| --- | --- | +| (默认) | 构建 chrome 扩展到 `dist/` | +| `--firefox` | 构建 firefox 扩展到 `dist-firefox/` | +| `--no-types` | 跳过 bridge 的 `tsc -b` 类型产物(纯 JS 转译照常) | +| `--scan-native` | 仅扫描 node_modules 中的 Win8+ API 原生二进制后退出 | +| `--fix-native` | 扫描并调用 `GoWin7Fixer.exe` 尝试修补 | + +## 6. 开发工作流说明 + +- **类型检查**仍可随时执行:`pnpm typecheck`(`tsc --noEmit`,纯 JS,Win7 安全) +- **单元测试**:`pnpm test` 使用 vitest(vitest 本身依赖 vite 的原生 esbuild?—— vitest 走 vite 的 **JS 实现**(vite 在有 `esbuild-wasm` 时不加载原生 exe;vite 6 的 esbuild 集成会优先使用已安装的 `esbuild-wasm`)。如遇 vitest 也崩溃,可用 `--no-types` 类似思路跳过该阶段 +- 本方案**不提供 watch/dev server**(`pnpm dev` 依赖 vite 开发服务器面板热更新);开发期请用非 Win7 机器,或改完源码后重跑 `node build-win7.mjs` 再到 `chrome://extensions` 点"重新加载" + +## 7. 已知限制 + +1. WASM 打包比原生 esbuild 慢数倍(本仓库全量构建约数十秒,Win7 机械盘上更慢) +2. 无源映射(`sourcemap: false`);如需调试可临时打开 +3. panel 产物为 `iife` 包而非 vite 的 ES 模块分包(无 code-splitting,单文件稍大) +4. `@deepseek-ai/libreoffice-kit-win32-x64`(约 71MB 运行时包)在 Win7 上需用 GoWin7Fixer 修补其 `libreoffice-kit.exe` 才能被 DSH 运行时加载——与本构建无关 + +## 8. 验证环境 + +- Windows 7 SP1(x64)+ Node v18.20.0 / v26.7.0 +- pnpm 11.7.0(经 corepack) +- `esbuild-wasm@0.25.12`、`@rollup/wasm-node@4.66.0`、`typescript@6.0.3`、`vite@6.4.x`(仅作为依赖元数据,不执行其原生二进制) + +产物已校验与原 vite/tsc+tsdown 构建在结构上等价(external 导入、`EXT_TARGET` 注入、MV3 manifest、`_locales`、icons、panel HTML 资源引用)。 + +--- + +## 附:为什么不用 pnpm overrides 强制把 `esbuild` 重定向到 `esbuild-wasm`? + +在根 `package.json` 写 `"pnpm": { "overrides": { "esbuild": "npm:esbuild-wasm@..." } }` 会让 vite/vitest 的 `esbuild` 导入指向 wasm 包,理论上可行,但: + +1. vite 内部对 `esbuild` 的 `build`/`transform` 调用在 wasm 实现下初始化路径不同,仍可能间接触发对原生可执行文件的引用探测 +2. overrides 会**全局**改写所有消费者的依赖解析,破坏正常平台用户的行为 +3. `rolldown` 无法用这种方式替换(它的原生绑定由 npm 包单独分发) + +因此本方案选择**显式的独立构建入口**:保留原有构建链,新增 `build-win7.mjs` 与 `build:win7` 脚本,对原仓库的侵入性降到最低(仅新增文件、新增根 devDependencies 与两个脚本)。 diff --git a/build-win7.mjs b/build-win7.mjs new file mode 100644 index 00000000..ef51b05f --- /dev/null +++ b/build-win7.mjs @@ -0,0 +1,556 @@ +#!/usr/bin/env node +/** + * build-win7.mjs — Windows 7 兼容构建入口(dsh-browser) + * + * 为什么需要它: + * 原生构建链(tsdown → rolldown、vite → esbuild/rollup 原生二进制)携带 + * Go/Rust 编译的 .exe/.node,它们在内部调用 Windows 8+ 才存在的 + * GetSystemTimePreciseAsFileTime 等 API,Win7 上运行会直接 0xc0000005 崩溃。 + * + * 本脚本完全不加载任何原生二进制: + * - bridge 库(packages/browser/bridge-browser):@rollup/wasm-node(纯 wasm) + * + 内联 TypeScript 转译插件(typescript 自带的 transpileModule,纯 JS)。 + * - 扩展(extensions/dsh-browser):esbuild-wasm(纯 wasm),一次三目标: + * background(chrome: es / firefox: iife)、content(iife)、panel(React iife)。 + * - 静态资源(manifest、_locales、assets、panel/index.html):Node fs 复制, + * HTML 中的脚本/样式引用改写为产物路径。 + * - 原生兜底:扫描 node_modules 中携带 Win8+ API 导入的 .exe;若显式要求 + * (--fix-native)则调用 GoWin7Fixer.exe 尝试修补。 + * + * 用法: + * node build-win7.mjs 构建 chrome 扩展(dist/) + * node build-win7.mjs --firefox 构建 firefox 扩展(dist-firefox/) + * node build-win7.mjs --no-types 跳过 bridge 的 tsc 类型产物 + * node build-win7.mjs --scan-native 仅扫描可疑原生二进制 + * node build-win7.mjs --fix-native 扫描并调用 GoWin7Fixer.exe 修补 + * + * 退出码:0 成功;非 0 表示构建失败。 + */ + +import { spawn, spawnSync } from 'node:child_process' +import { + cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, +} from 'node:fs' +import { basename, dirname, isAbsolute, join, resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const ROOT = dirname(fileURLToPath(import.meta.url)) +const BRIDGE = join(ROOT, 'packages', 'browser', 'bridge-browser') +const EXT = join(ROOT, 'extensions', 'dsh-browser') + +const argv = process.argv.slice(2) +const firefox = argv.includes('--firefox') +const withTypes = !argv.includes('--no-types') +const scanNativeOnly = argv.includes('--scan-native') +const fixNative = argv.includes('--fix-native') + +const browserTarget = firefox ? 'firefox' : 'chrome' +const extOutDir = join(EXT, firefox ? 'dist-firefox' : 'dist') +const targetManifest = firefox ? 'manifest.firefox.json' : 'manifest.json' + +/** 统一前缀日志 */ +const log = (msg) => console.log(`[win7] ${msg}`) +const warn = (msg) => console.warn(`[win7:warn] ${msg}`) +const fail = (msg) => { console.error(`[win7:error] ${msg}`); process.exit(1) } +const bytes = (n) => `${(n / 1024).toFixed(1)} KB` + +async function main() { + log(`dsh-browser Win7 构建(target=${browserTarget})`) + + const esbuild = await tryImportEsbuild() + const rollup = await tryImportRollup() + if (!esbuild && !rollup) { + fail('esbuild-wasm 与 @rollup/wasm-node 均不可用。请在项目根目录执行:\n pnpm install --filter @yuxianglin/dsh-bridge-browser --filter dsh-browser-extension\n依赖安装完成后再运行本脚本。') + } + log(`引擎:esbuild-wasm=${esbuild ? 'ok' : '缺失'} @rollup/wasm-node=${rollup ? 'ok' : '缺失'}`) + + if (scanNativeOnly || fixNative) { + await runNativeStep(fixNative) + if (scanNativeOnly) process.exit(0) + } + + // ---- bridge 库 ---- + log('=== 阶段 1/3:bridge-browser(@rollup/wasm-node)===') + await buildBridge({ rollup, esbuild }) + + // ---- 扩展 ---- + log('=== 阶段 2/3:dsh-browser-extension(esbuild-wasm)===') + await buildExtension(esbuild) + + // ---- 原生风险提示(非阻塞)---- + await runNativeStep(false) + + log('=== 完成 ===') + reportOutputs() +} + +/* ------------------------------------------------------------------ * + * 引擎加载 + * ------------------------------------------------------------------ */ + +async function tryImportEsbuild() { + try { + const mod = await import('esbuild-wasm') + return typeof mod.build === 'function' ? mod : null + } catch { return null } +} + +async function tryImportRollup() { + try { + const mod = await import('@rollup/wasm-node') + const rollup = mod.rollup ?? mod.default?.rollup + return typeof rollup === 'function' ? rollup : null + } catch { return null } +} + +/** 从 bridge 包目录加载纯 JS 的 typescript(根目录未必安装) */ +let tsModule = null +async function loadTypeScript() { + if (tsModule) return tsModule + for (const candidate of [ + join(BRIDGE, 'node_modules', 'typescript', 'lib', 'typescript.js'), + join(EXT, 'node_modules', 'typescript', 'lib', 'typescript.js'), + ]) { + if (existsSync(candidate)) { + tsModule = await import(pathToFileURL(candidate).href) + return tsModule + } + } + return null +} + +/* ------------------------------------------------------------------ * + * 阶段 1:bridge 库 + * ------------------------------------------------------------------ */ + +const BRIDGE_ENTRIES = ['index', 'protocol', 'invariant'] +/** 运行期由宿主提供,保持外部引用(与 tsdown 平台行为一致) */ +const bridgeExternal = (id) => + id.startsWith('@deepseek-ai/') || id === 'ws' || id.startsWith('node:') + +async function buildBridge({ rollup, esbuild }) { + rmSync(join(BRIDGE, 'lib'), { recursive: true, force: true }) + mkdirSync(join(BRIDGE, 'lib'), { recursive: true }) + if (withTypes) await emitBridgeTypes() + + if (rollup) { + await buildBridgeWithRollup(rollup) + } else { + log('@rollup/wasm-node 不可用,回退到 esbuild-wasm 打包 bridge') + if (!esbuild) fail('无可用打包引擎(见上方引擎检查)') + await buildBridgeWithEsbuild(esbuild) + } + + // 与原构建一致:client.js 是纯 JS 透传文件 + const clientSrc = join(BRIDGE, 'src', 'client.js') + if (existsSync(clientSrc)) { + cpSync(clientSrc, join(BRIDGE, 'lib', 'client.js')) + log(`复制 src/client.js → lib/client.js`) + } else { + warn('src/client.js 不存在,跳过 client.js 产物') + } +} + +/** tsc -b:纯 JS 实现,产物 lib/types/*.js + *.d.ts(与原流程一致,失败不致命) */ +async function emitBridgeTypes() { + const tsc = join(BRIDGE, 'node_modules', 'typescript', 'bin', 'tsc') + if (!existsSync(tsc)) { + warn('bridge 下未安装 typescript,跳过 lib/types 类型产物(使用 --no-types 可关闭该提示)') + return + } + log('tsc -b(生成 lib/types 声明产物)…') + const res = spawnSync(process.execPath, [tsc, '-b', '--pretty', 'false'], { cwd: BRIDGE }) + if (res.status !== 0) { + warn('tsc -b 失败(类型/依赖问题),已跳过类型产物;JS 产物不受影响。') + warn(String(res.stderr || res.stdout).trim().split('\n').slice(0, 5).join('\n')) + } +} + +async function buildBridgeWithRollup(rollup) { + const ts = await loadTypeScript() + if (!ts) { warn('typescript 不可用,bridge 回退到 esbuild-wasm'); return buildBridgeWithEsbuild(await tryImportEsbuild()) } + + const plugin = makeTsTranspilePlugin(ts) + for (const name of BRIDGE_ENTRIES) { + const input = join(BRIDGE, 'src', `${name}.ts`) + const bundle = await rollup({ + input, + plugins: [plugin], + external: bridgeExternal, + onwarn: (warning, handler) => { + if (warning.code === 'UNRESOLVED_IMPORT' || warning.code === 'EVAL') warn(String(warning.message)) + else handler(warning) + }, + }) + await bundle.write({ + file: join(BRIDGE, 'lib', `${name}.js`), + format: 'esm', + sourcemap: false, + }) + await bundle.close() + log(`rollup: src/${name}.ts → lib/${name}.js`) + } +} + +/** + * 内联 TypeScript→JS 插件:每文件 transpileModule(isolatedModules 语义, + * 不做类型检查;类型检查请用 pnpm --filter @yuxianglin/dsh-bridge-browser typecheck)。 + */ +function makeTsTranspilePlugin(ts) { + const exts = ['.ts', '.tsx', '.js', '.json'] + const resolveRelative = (importer, spec) => { + if (!spec.startsWith('.') && !spec.startsWith('/')) return null + const base = isAbsolute(spec) ? spec : resolve(dirname(importer), spec) + if (existsSync(base) && statSync(base).isFile()) return base + for (const ext of exts) { + if (existsSync(base + ext)) return base + ext + } + for (const ext of exts) { + const idx = join(base, `index${ext}`) + if (existsSync(idx)) return idx + } + return null + } + return { + name: 'win7-typescript-transpile', + resolveId(specifier, importer) { + if (!importer) return null + const resolved = resolveRelative(importer, specifier) + return resolved + }, + transform(code, id) { + if (!/\.(ts|tsx)$/.test(id)) return null + const result = ts.transpileModule(code, { + compilerOptions: { + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ES2022, + jsx: ts.JsxEmit.ReactJSX, + esModuleInterop: true, + verbatimModuleSyntax: false, + }, + }) + return { code: result.outputText, map: result.sourceMapText ? JSON.parse(result.sourceMapText) : null } + }, + } +} + +async function buildBridgeWithEsbuild(esbuild) { + await esbuild.build({ + entryPoints: BRIDGE_ENTRIES.map((n) => join(BRIDGE, 'src', `${n}.ts`)), + bundle: true, + format: 'esm', + platform: 'node', + target: 'es2022', + outdir: join(BRIDGE, 'lib'), + entryNames: '[name]', + external: ['@deepseek-ai/*', 'ws'], + logLevel: 'warning', + legalComments: 'none', + }) + log(`esbuild-wasm: src/{${BRIDGE_ENTRIES.join(',')}}.ts → lib/`) +} + +/* ------------------------------------------------------------------ * + * 阶段 2:扩展三目标 + * ------------------------------------------------------------------ */ + +async function buildExtension(esbuild) { + if (!esbuild) { + fail('扩展构建依赖 esbuild-wasm(JSX/CSS/资源处理)。请在根目录执行:pnpm add -D esbuild-wasm') + } + rmSync(extOutDir, { recursive: true, force: true }) + mkdirSync(extOutDir, { recursive: true }) + + const define = { 'import.meta.env.EXT_TARGET': JSON.stringify(browserTarget) } + const common = { + bundle: true, + minify: true, + target: 'es2022', + platform: 'browser', + define, + sourcemap: false, + logLevel: 'info', + legalComments: 'none', + loader: { '.png': 'file', '.svg': 'file', '.woff': 'file', '.woff2': 'file' }, + } + + try { + // background:chrome 用 ES module service worker,firefox 用 iife + const bgFormat = firefox ? 'iife' : 'esm' + await esbuild.build({ + ...common, + entryPoints: [join(EXT, 'src', 'background', 'index.ts')], + outdir: extOutDir, + entryNames: 'background', + format: bgFormat, + globalName: firefox ? '__dsh_background__' : undefined, + }) + log(`background → ${join(basename(extOutDir), 'background.js')}(${bgFormat})`) + + // content:iife + await esbuild.build({ + ...common, + entryPoints: [join(EXT, 'src', 'content', 'index.ts')], + outdir: extOutDir, + entryNames: 'content', + format: 'iife', + globalName: '__dsh_content__', + }) + log(`content → ${join(basename(extOutDir), 'content.js')}(iife)`) + + // panel:React iife,CSS 提取到 panel/assets/main.css + await esbuild.build({ + ...common, + entryPoints: [join(EXT, 'src', 'panel', 'main.tsx')], + outdir: join(extOutDir, 'panel'), + entryNames: 'assets/[name]', + assetNames: 'assets/[name]', + format: 'iife', + globalName: '__dsh_panel__', + jsx: 'automatic', + }) + log(`panel → ${join(basename(extOutDir), 'panel', 'assets', 'main.js')}(iife + css 提取)`) + } catch (e) { + fail(`扩展构建失败:${e && e.message ? e.message : e}`) + } + + writePanelHtml() + copyStaticAssets() +} + +/** panel/index.html → 产物 HTML:脚本/样式指向 assets/ */ +function writePanelHtml() { + const src = join(EXT, 'panel', 'index.html') + if (!existsSync(src)) { warn('panel/index.html 不存在,跳过 HTML 改写'); return } + let html = readFileSync(src, 'utf8') + const scriptRe = /]*?)src=(["'])(\/?)src\/panel\/main\.tsx\2([^>]*)><\/script>/i + if (!scriptRe.test(html)) warn('未在 panel/index.html 中找到 /src/panel/main.tsx 脚本标签;HTML 保持原样') + html = html.replace(scriptRe, (_, pre, _q, _slash, post) => { + const raw = `${pre}${post}`.replace(/\s{2,}/g, ' ').trim() + const attrs = /type\s*=/i.test(raw) ? raw : `${raw} type="module"`.trim() + return `\n ` + }) + const out = join(extOutDir, 'panel', 'index.html') + mkdirSync(dirname(out), { recursive: true }) + writeFileSync(out, html, 'utf8') + log(`panel/index.html → ${join(basename(extOutDir), 'panel', 'index.html')}(注入 assets 引用)`) +} + +/** manifest、_locales、assets → outDir(对齐 vite copyManifest 插件) */ +function copyStaticAssets() { + copyFileToOut(targetManifest, 'manifest.json') + copyDirToOut('_locales') + copyDirToOut('assets') +} + +function copyFileToOut(srcName, outName) { + const src = join(EXT, srcName) + if (!existsSync(src)) { warn(`${srcName} 不存在,跳过`); return } + mkdirSync(extOutDir, { recursive: true }) + cpSync(src, join(extOutDir, outName)) + log(`复制 ${srcName} → ${join(basename(extOutDir), outName)}`) +} + +function copyDirToOut(dirName) { + const src = join(EXT, dirName) + if (!existsSync(src)) { warn(`${dirName}/ 不存在,跳过`); return } + mkdirSync(extOutDir, { recursive: true }) + cpSync(src, join(extOutDir, dirName), { recursive: true }) + log(`复制 ${dirName}/ → ${join(basename(extOutDir), dirName)}/`) +} + +/* ------------------------------------------------------------------ * + * 原生二进制风险扫描与 GoWin7Fixer 兜底 + * ------------------------------------------------------------------ */ + +/** Win7 不存在、Win8+ 才有的 API(Go/Rust 工具链崩溃的典型根因) */ +const WIN8_PLUS_API = ['GetSystemTimePreciseAsFileTime', 'SetThreadDescription', 'QueryUnbiasedInterruptTimePrecise'] + +async function runNativeStep(invokeFixer) { + const risky = scanRiskyExecutables(join(ROOT, 'node_modules')) + if (risky.length === 0) { + log('原生二进制扫描:node_modules 中未发现 Win8+ API 依赖的 .exe(本构建无需原生二进制)') + return + } + log(`原生二进制扫描:发现 ${risky.length} 个携带 Win8+ API 导入的可执行文件:`) + for (const item of risky) log(` - ${item.path}(${item.dll}!${item.api})`) + if (!invokeFixer) { + log('本 Win7 构建链不加载它们,可安全忽略;如需修补这些文件,请加 --fix-native,或以管理员身份运行并拖拽文件到 GoWin7Fixer.exe。') + return + } + const fixer = locateGoWin7Fixer() + if (!fixer) { + warn('未找到 GoWin7Fixer.exe(已查找桌面快捷方式、Downloads、PATH、GOWIN7FIXER_PATH)。') + warn('请手动启动 GoWin7Fixer.exe 并拖入以下文件:') + for (const item of risky) warn(` - ${resolve(ROOT, item.path)}`) + return + } + log(`调用 GoWin7Fixer.exe:${fixer}`) + const targets = [...new Set(risky.map((i) => resolve(ROOT, i.path)))] + const child = spawn(fixer, targets, { detached: true, stdio: 'ignore' }) + child.unref() + log('已将下列文件作为参数传入 GoWin7Fixer.exe(GUI 程序;如未自动修补,请在其窗口中拖拽确认):') + for (const t of targets) log(` - ${t}`) +} + +/** 在 node_modules 中扫描 .exe 的 PE 导入表(纯 Node 解析,不执行目标文件) */ +function scanRiskyExecutables(rootDir) { + const found = [] + if (!existsSync(rootDir)) return found + const stack = [{ dir: rootDir, depth: 0 }] + const seen = new Set() + const MAX_DEPTH = 8, MAX_FILES = 4000 + let scanned = 0 + while (stack.length && scanned < MAX_FILES) { + const { dir, depth } = stack.pop() + if (depth > MAX_DEPTH) continue + let names + try { names = readdirSync(dir, { withFileTypes: true }) } catch { continue } + for (const ent of names) { + if (ent.isDirectory()) { + if (ent.name === '.git' || ent.name === 'node_modules' && depth > 2) continue + stack.push({ dir: join(dir, ent.name), depth: depth + 1 }) + } else if (ent.isFile() && /\.exe$/i.test(ent.name)) { + scanned++ + const path = join(dir, ent.name) + let st + try { st = statSync(path) } catch { continue } + if (st.size > 256 * 1024 * 1024) continue + const hit = findWin8PlusImport(readFileSync(path)) + if (hit) { + const rel = path.replace(/\\/g, '/').replace(/^.*node_modules\//, '') + found.push({ path: rel, dll: hit.dll, api: hit.api }) + } + } + } + if (found.length >= 32) break + } + return found +} + +/** 解析 PE 导入表,返回命中的 Win8+ API({ dll, api }) */ +function findWin8PlusImport(buf) { + try { + if (buf.length < 0x40 || buf.readUInt16LE(0) !== 0x5a4d) return null + const e_lfanew = buf.readUInt32LE(0x3c) + if (e_lfanew + 24 > buf.length || buf.readUInt32LE(e_lfanew) !== 0x00004550) return null + const machine = buf.readUInt16LE(e_lfanew + 4) + const is64 = machine === 0x8664 || machine === 0xaa64 + const numSections = buf.readUInt16LE(e_lfanew + 6) + const sizeOpt = buf.readUInt16LE(e_lfanew + 20) + const opt = e_lfanew + 24 + const magic = buf.readUInt16LE(opt) + if (magic !== 0x10b && magic !== 0x20b) return null + const dataDirBase = magic === 0x20b ? opt + 112 : opt + 96 + const importRva = buf.readUInt32LE(dataDirBase + 8) // directory[1].RVA + if (!importRva) return null + const sections = [] + for (let i = 0; i < numSections; i++) { + const s = opt + sizeOpt + i * 40 + if (s + 40 > buf.length) return null + const va = buf.readUInt32LE(s + 12) + const vsize = buf.readUInt32LE(s + 8) + const rawSize = buf.readUInt32LE(s + 16) + const rawPtr = buf.readUInt32LE(s + 20) + sections.push({ va, end: va + Math.max(vsize, rawSize), rawPtr }) + } + const rvaToOffset = (rva) => { + for (const s of sections) if (rva >= s.va && rva < s.end) return rva - s.va + s.rawPtr + return null + } + const readCstring = (offset) => { + if (offset === null || offset < 0 || offset >= buf.length) return '' + let end = offset + while (end < buf.length && buf[end] !== 0) end++ + return buf.toString('ascii', offset, end) + } + let descOff = rvaToOffset(importRva) + for (let i = 0; i < 512 && descOff !== null && descOff + 20 <= buf.length; i++, descOff += 20) { + const olt = buf.readUInt32LE(descOff) // OriginalFirstThunk + const nameRva = buf.readUInt32LE(descOff + 12) + const iat = buf.readUInt32LE(descOff + 16) // FirstThunk + if (!olt && !nameRva && !iat) break + const dll = readCstring(rvaToOffset(nameRva)).toLowerCase() + if (dll === 'kernel32.dll' || dll === 'kernelbase.dll' || dll === 'api-ms-win-core-sysinfo-l1-1-0.dll' || dll === 'api-ms-win-core-sysinfo-l1-2-0.dll' || dll === 'api-ms-win-core-threadpool-l1-2-0.dll') { + let thunkRva = olt || iat + let thunkOff = rvaToOffset(thunkRva) + const step = is64 ? 8 : 4 + for (let j = 0; thunkOff !== null && thunkOff + step <= buf.length; j++) { + const entry = is64 ? Number(buf.readBigUInt64LE(thunkOff)) : buf.readUInt32LE(thunkOff) + if (entry === 0) break + const ordinalFlag = is64 ? 0x8000000000000000n : 0x80000000 + if ((is64 ? BigInt(entry) : entry) & BigInt(ordinalFlag)) { thunkOff += step; continue } + const fn = readCstring(rvaToOffset(entry) === null ? null : rvaToOffset(entry) + 2) + if (WIN8_PLUS_API.includes(fn)) return { dll, api: fn } + thunkOff += step + } + } + } + } catch { /* Not a parseable PE — ignore */ } + return null +} + +/** 定位 GoWin7Fixer.exe:环境变量 → 桌面快捷方式 → Downloads → PATH */ +function locateGoWin7Fixer() { + const envPath = process.env.GOWIN7FIXER_PATH + if (envPath && existsSync(envPath)) return envPath + const home = process.env.USERPROFILE || process.env.HOME + if (home) { + const desktop = join(home, 'Desktop') + for (const name of ['GoWin7Fixer.exe', 'GoWin7Fixer.exe.lnk']) { + const p = join(desktop, name) + if (existsSync(p)) { + if (/\.exe$/i.test(p)) return p + const target = parseLnkTarget(p) + if (target && existsSync(target)) return target + } + } + } + for (const fixed of ['D:\\Downloads\\GoWin7Fixer.exe', 'C:\\Users\\Administrator\\Downloads\\GoWin7Fixer.exe']) { + if (existsSync(fixed)) return fixed + } + const onPath = which('GoWin7Fixer.exe') + if (onPath) return onPath + return null +} + +function parseLnkTarget(lnkPath) { + try { + const text = readFileSync(lnkPath, 'utf16le') + const m = text.match(/([A-Za-z]:\\[^\u0000-\u001f<>|?*]+\.exe)/i) + return m ? m[1] : null + } catch { return null } +} + +function which(name) { + const res = spawnSync('where', [name], { windowsVerbatimArguments: false }) + if (res.status === 0) return String(res.stdout).trim().split(/\r?\n/)[0] || null + return null +} + +/* ------------------------------------------------------------------ * + * 产物报告 + * ------------------------------------------------------------------ */ + +function reportOutputs() { + const items = [] + const walk = (dir, prefix = '') => { + let names + try { names = readdirSync(dir, { withFileTypes: true }) } catch { return } + for (const ent of names) { + const p = join(dir, ent.name) + if (ent.isDirectory()) walk(p, `${prefix}${ent.name}/`) + else items.push({ path: `${prefix}${ent.name}`, size: statSync(p).size }) + } + } + walk(join(BRIDGE, 'lib')) + walk(extOutDir) + const total = items.reduce((sum, i) => sum + i.size, 0) + for (const i of items) console.log(` ${bytes(i.size).padStart(10)} ${i.path}`) + log(`产物共 ${items.length} 个文件,合计 ${bytes(total)}(bridge lib/ + ${basename(extOutDir)}/)`) +} + +main().catch((err) => { + console.error(`[win7:error] 未捕获错误:${err && err.stack ? err.stack : err}`) + process.exit(1) +}) diff --git a/package.json b/package.json index 5301807b..b7c7eaf4 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "packageManager": "pnpm@11.7.0", "scripts": { "build": "pnpm --filter @yuxianglin/dsh-bridge-browser run build && pnpm --filter dsh-browser-extension run build", + "build:win7": "node build-win7.mjs", + "build:win7:firefox": "node build-win7.mjs --firefox", "start": "dsh web", "test": "pnpm --filter @yuxianglin/dsh-bridge-browser run test && pnpm --filter dsh-browser-extension run test", "typecheck": "pnpm --filter @yuxianglin/dsh-bridge-browser run typecheck && pnpm --filter dsh-browser-extension run typecheck", @@ -50,5 +52,9 @@ "@deepseek-ai/dsh-util-time": "0.2.0-rc.2", "@deepseek-ai/dsh-util-workspace-path": "0.2.0-rc.2", "@deepseek-ai/dsh-workflow": "0.2.0-rc.2" + }, + "devDependencies": { + "@rollup/wasm-node": "^4.18.0", + "esbuild-wasm": "^0.25.12" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b3c3d4f..d396c9c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,13 @@ importers: '@deepseek-ai/dsh-workflow': specifier: 0.2.0-rc.2 version: 0.2.0-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-agent@0.2.0-rc.2(fee0e653b72a3883e0f0da2d9a46bb50))(@deepseek-ai/dsh-brand@0.2.0-rc.2(@deepseek-ai/cordis@4.0.4))(@deepseek-ai/dsh-invariants@0.2.0-rc.2(@deepseek-ai/cordis@4.0.4))(@deepseek-ai/dsh-llm@0.2.0-rc.2(@deepseek-ai/cordis@4.0.4))(@deepseek-ai/dsh-session@0.2.0-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-scope@0.2.0-rc.2(@deepseek-ai/cordis@4.0.4)(@deepseek-ai/dsh-invariants@0.2.0-rc.2(@deepseek-ai/cordis@4.0.4)))) + devDependencies: + '@rollup/wasm-node': + specifier: ^4.18.0 + version: 4.66.0 + esbuild-wasm: + specifier: ^0.25.12 + version: 0.25.12 extensions/dsh-browser: dependencies: @@ -161,7 +168,7 @@ importers: version: 6.1.1(typescript@5.9.3)(vite@6.4.3(@types/node@22.20.1)(yaml@2.9.0)) vitest: specifier: ^4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(jsdom@26.1.0)(vite@6.4.3(@types/node@22.20.1)(yaml@2.9.0)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(jsdom@26.1.0)(yaml@2.9.0) packages/browser/bridge-browser: dependencies: @@ -249,7 +256,7 @@ importers: version: 22.20.1 '@types/ws': specifier: ^8.18.1 - version: 8.18.1 + version: 8.18.2 playwright-core: specifier: ^1.61.1 version: 1.62.1 @@ -261,7 +268,7 @@ importers: version: 6.0.3 vitest: specifier: ^4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(jsdom@26.1.0)(vite@6.4.3(@types/node@22.20.1)(yaml@2.9.0)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(jsdom@26.1.0)(yaml@2.9.0) packages: @@ -3745,6 +3752,11 @@ packages: cpu: [x64] os: [win32] + '@rollup/wasm-node@4.66.0': + resolution: {integrity: sha512-iZ7VKtuFJNDH+dAxK4v8gXdnSrZxbGVv/yRYY0B6bQFmb4tUQpOwOkr4f+CNf/ZvmJphDHDQ8sLCSe5ZbQYxgA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -3863,8 +3875,8 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@types/ws@8.18.1': - resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/ws@8.18.2': + resolution: {integrity: sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==} '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} @@ -4188,6 +4200,11 @@ packages: es-module-lexer@2.3.1: resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + esbuild-wasm@0.25.12: + resolution: {integrity: sha512-rZqkjL3Y6FwLpSHzLnaEy8Ps6veCNo1kZa9EOfJvmWtBq5dJH4iVjfmOO6Mlkv9B0tt9WFPFmb/VxlgJOnueNg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -4313,8 +4330,8 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} - http-cache-semantics@4.2.0: - resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-cache-semantics@4.3.0: + resolution: {integrity: sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==} http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} @@ -4722,8 +4739,8 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve.exports@2.0.3: - resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + resolve.exports@2.0.4: + resolution: {integrity: sha512-zepuGL0ulf+nt+XaFrlKeaiTJeyPFPLLY62+ncq1/VTHBdt3HsfXOH60priZ6tnXQozdGcDeIF5JVTanvIcYWQ==} engines: {node: '>=10'} responselike@4.0.2: @@ -4852,8 +4869,8 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} stackback@0.0.2: @@ -5072,7 +5089,6 @@ packages: '@vitest/ui': 4.1.11 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -5847,7 +5863,7 @@ snapshots: ajv: 8.20.0 js-yaml: 4.3.2 node-addon-require-builtin: 0.1.6 - resolve.exports: 2.0.3 + resolve.exports: 2.0.4 semver: 7.8.5 '@deepseek-ai/dsh-atomic-write@0.2.0-rc.2(@deepseek-ai/cordis@4.0.4)': @@ -9237,6 +9253,13 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.4': optional: true + '@rollup/wasm-node@4.66.0': + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + fsevents: 2.3.3 + '@sec-ant/readable-stream@0.4.1': {} '@sindresorhus/is@7.2.0': {} @@ -9371,7 +9394,7 @@ snapshots: '@types/trusted-types@2.0.7': optional: true - '@types/ws@8.18.1': + '@types/ws@8.18.2': dependencies: '@types/node': 22.20.1 @@ -9546,7 +9569,7 @@ snapshots: dependencies: '@types/http-cache-semantics': 4.2.0 get-stream: 9.0.1 - http-cache-semantics: 4.2.0 + http-cache-semantics: 4.3.0 keyv: 5.6.0 mimic-response: 4.0.0 normalize-url: 8.1.1 @@ -9660,6 +9683,8 @@ snapshots: es-module-lexer@2.3.1: {} + esbuild-wasm@0.25.12: {} + esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -9829,7 +9854,7 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 - http-cache-semantics@4.2.0: {} + http-cache-semantics@4.3.0: {} http-proxy-agent@7.0.2: dependencies: @@ -10152,7 +10177,7 @@ snapshots: dependencies: nanoid: 3.3.18 picocolors: 1.1.1 - source-map-js: 1.2.1 + source-map-js: 1.2.2 powershell-utils@0.1.0: {} @@ -10206,7 +10231,7 @@ snapshots: resolve-pkg-maps@1.0.0: {} - resolve.exports@2.0.3: {} + resolve.exports@2.0.4: {} responselike@4.0.2: dependencies: @@ -10374,7 +10399,7 @@ snapshots: signal-exit@4.1.0: {} - source-map-js@1.2.1: {} + source-map-js@1.2.2: {} stackback@0.0.2: {} @@ -10515,7 +10540,7 @@ snapshots: fsevents: 2.3.3 yaml: 2.9.0 - vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(jsdom@26.1.0)(vite@6.4.3(@types/node@22.20.1)(yaml@2.9.0)): + vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(jsdom@26.1.0)(yaml@2.9.0): dependencies: '@vitest/expect': 4.1.11 '@vitest/mocker': 4.1.11(vite@6.4.3(@types/node@22.20.1)(yaml@2.9.0)) @@ -10542,7 +10567,17 @@ snapshots: '@types/node': 22.20.1 jsdom: 26.1.0 transitivePeerDependencies: + - jiti + - less + - lightningcss - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml w3c-xmlserializer@5.0.0: dependencies: