diff --git a/docs/development/local-startup-troubleshooting.md b/docs/development/local-startup-troubleshooting.md index 8bc02b6..372a976 100644 --- a/docs/development/local-startup-troubleshooting.md +++ b/docs/development/local-startup-troubleshooting.md @@ -38,25 +38,24 @@ go version `electron-vite dev` 报 `Electron uninstall`,或 Electron 安装器报 `fetch failed`,通常表示 `node_modules/electron/dist` 中的 Electron 二进制缺失或下载未完成。这不是应用业务代码的启动错误。 -项目的 [`.npmrc`](../../.npmrc) 已设置: +**先看根因,别急着删 `node_modules` 重装。** `electron@43` 的 npm 包**不再声明 `postinstall`**(其 `package.json` 里 `scripts` 是空对象),下载改为「首次 `require('electron')` 时的懒加载」。因此: -```ini -electron_mirror=https://npmmirror.com/mirrors/electron/ -``` +- `package.json` 里的 `pnpm.onlyBuiltDependencies: ["electron"]` 对它不起作用——上游没有脚本可执行,pnpm 无从下手; +- `pnpm install` 跑完不会有任何二进制被下载,**只重装依赖解决不了这个问题**。 -pnpm 会把该值传给 Electron 安装器,令其从镜像下载与 `package.json` 锁定版本匹配的二进制文件,避免默认 GitHub 下载源在受限网络中不可访问。 +项目已自动兜住这条路径:`scripts/ensure-electron-binary.cjs` 挂在 `postinstall` 与 `predev` 上,校验 `path.txt` 指向的可执行文件是否真的存在(只有 `path.txt` 而没有 `dist/` 同样算没装好),缺失时就地补下载。它读取 `.npmrc` 的 `electron_mirror`(当前为 `https://npmmirror.com/mirrors/electron/`),失败后再兜底重试一次该镜像。 -依赖安装被中断或 Electron 目录不完整时,删除不完整的 `node_modules` 后重新安装: +正常情况下你不需要做任何事。只有当自动步骤没有执行时(例如安装时带了 `--ignore-scripts`),才需要手动补一次: ```bash -pnpm install --frozen-lockfile +node scripts/ensure-electron-binary.cjs ``` -单次安装需要使用其他镜像时,可以临时覆盖项目默认值。PowerShell 示例: +要换用别的镜像时,显式设置环境变量(优先于 `.npmrc`)。PowerShell 示例: ```powershell $env:ELECTRON_MIRROR = 'https://your-electron-mirror.example/' -pnpm install --frozen-lockfile +node scripts/ensure-electron-binary.cjs ``` 该环境变量只影响当前终端,不会改写仓库中的 `.npmrc`。镜像地址必须保留末尾的 `/`,并提供与 Electron 版本对应的目录结构。 diff --git a/electron-builder.win.yml b/electron-builder.win.yml index a7d3131..bade454 100644 --- a/electron-builder.win.yml +++ b/electron-builder.win.yml @@ -63,4 +63,6 @@ publish: provider: github owner: Wxw-Gu repo: TraceMemo - releaseType: release + # 一律先上传为草稿 再到 GitHub 上手动 Publish。 + # 需要预发布时用 `pnpm release:beta`(EP_PRE_RELEASE 会覆盖这里的 draft)。 + releaseType: draft diff --git a/electron-builder.yml b/electron-builder.yml index 728991f..c556c13 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -71,4 +71,6 @@ publish: provider: github owner: Wxw-Gu repo: TraceMemo - releaseType: release + # 一律先上传为草稿 再到 GitHub 上手动 Publish。 + # 需要预发布时用 `pnpm release:beta`(EP_PRE_RELEASE 会覆盖这里的 draft)。 + releaseType: draft diff --git a/package.json b/package.json index bfca444..a448251 100644 --- a/package.json +++ b/package.json @@ -35,10 +35,12 @@ "cp:env": "node scripts/ensure-env.cjs", "prepare:env": "node scripts/ensure-env.cjs", "prepare:ffmpeg:win": "node scripts/prepare-electron-runtime.cjs --platform win32 --arch x64", + "prepare:ffmpeg:mac:arm64": "node scripts/prepare-electron-runtime.cjs --platform darwin --arch arm64", + "prepare:ffmpeg:mac:x64": "node scripts/prepare-electron-runtime.cjs --platform darwin --arch x64", "prepare:win-runtime": "node scripts/prepare-win-runtime.cjs && npm run prepare:ffmpeg:win", "prepare:wechat-personal": "node scripts/prepare-wechat-chatter-runtime.cjs", "start": "electron-vite preview", - "predev": "node -e \"require('electron')\"", + "predev": "node scripts/ensure-electron-binary.cjs", "dev": "node scripts/ensure-env.cjs && node scripts/build-wechat-connector.cjs && electron-vite dev", "dev:update": "cross-env TRACEMEMO_UPDATE_SIMULATION=true pnpm dev", "poc:query-agent": "electron-vite build && node scripts/run-query-agent-poc.cjs", @@ -59,15 +61,18 @@ "build:wechat-connector:mac": "node scripts/build-wechat-connector.cjs --platform darwin --arch arm64", "build:native-services": "npm run build:wechat-connector", "build": "npm run typecheck && npm run build:native-services && electron-vite build", - "postinstall": "electron-builder install-app-deps && node scripts/prepare-electron-runtime.cjs", + "postinstall": "electron-builder install-app-deps && node scripts/prepare-electron-runtime.cjs && node scripts/ensure-electron-binary.cjs", "build:unpack": "npm run build && electron-builder --config electron-builder.yml --dir", "build:win": "npm run typecheck && npm run build:wechat-connector:win && npm run prepare:win-runtime && electron-vite build && electron-builder --config electron-builder.win.yml --win --x64", - "build:mac:arm64": "npm run typecheck && node scripts/build-wechat-connector.cjs --platform darwin --arch arm64 && electron-vite build && electron-builder --config electron-builder.yml --mac --arm64", + "build:mac:arm64": "npm run typecheck && node scripts/build-wechat-connector.cjs --platform darwin --arch arm64 && npm run prepare:ffmpeg:mac:arm64 && electron-vite build && electron-builder --config electron-builder.yml --mac --arm64", + "build:mac:x64": "npm run typecheck && node scripts/build-wechat-connector.cjs --platform darwin --arch x64 && npm run prepare:ffmpeg:mac:x64 && electron-vite build && electron-builder --config electron-builder.yml --mac --x64", "release": "npm run release:mac && npm run release:win", - "release:mac": "npm run typecheck && npm run build:wechat-connector:mac && electron-vite build && electron-builder --config electron-builder.yml --mac --arm64 --publish always", + "release:mac": "npm run typecheck && node scripts/build-wechat-connector.cjs --platform darwin --arch arm64,x64 && electron-vite build && npm run release:mac:arm64 && npm run release:mac:x64", + "release:mac:arm64": "npm run prepare:ffmpeg:mac:arm64 && electron-builder --config electron-builder.yml --mac --arm64 --publish always", + "release:mac:x64": "npm run prepare:ffmpeg:mac:x64 && electron-builder --config electron-builder.yml --mac --x64 --publish always", "release:win": "npm run typecheck && npm run build:wechat-connector:win && npm run prepare:win-runtime && electron-vite build && electron-builder --config electron-builder.win.yml --win --x64 --publish always", - "release:beta": "cross-env RELEASE_TYPE=prerelease npm run release", - "release:stable": "cross-env RELEASE_TYPE=release npm run release", + "release:beta": "cross-env EP_PRE_RELEASE=true npm run release", + "release:stable": "npm run release", "build:linux": "electron-vite build && electron-builder --config electron-builder.yml --linux" }, "dependencies": { diff --git a/scripts/after-pack.cjs b/scripts/after-pack.cjs index be0dff6..d64d1d3 100644 --- a/scripts/after-pack.cjs +++ b/scripts/after-pack.cjs @@ -1,8 +1,9 @@ /* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/explicit-function-return-type */ -const { chmodSync, existsSync } = require('node:fs') +const { chmodSync, existsSync, readdirSync, rmSync } = require('node:fs') const { execFileSync } = require('node:child_process') const path = require('node:path') const asar = require('@electron/asar') +const { readBinaryArchitectures } = require('./binary-arch.cjs') const REQUIRED_RUNTIME_PACKAGES = [ '@electron-toolkit/preload', @@ -82,6 +83,43 @@ function normalizeBuilderArch(arch) { return { 0: 'ia32', 1: 'x64', 2: 'armv7l', 3: 'arm64', 4: 'universal' }[arch] || String(arch) } +/** + * A foreign-architecture binary only fails once the user touches the feature + * that needs it, so verify the ones whose filename is shared across + * architectures (ffmpeg-static keeps a single "ffmpeg" per platform) and fail + * the build instead of shipping a broken bundle. + */ +function validateRuntimeBinaryArchitecture(filePath, platform, arch, label) { + if (platform !== 'darwin' && platform !== 'win32') return + if (arch === 'universal') return + const architectures = readBinaryArchitectures(filePath) + if (!architectures.length || architectures.includes(arch)) return + throw new Error( + `${label} is ${architectures.join('/')} but this bundle targets ${arch}: ${filePath}` + ) +} + +/** + * The Intel Mac key helper is an x86_64 executable that only the x64 (or + * universal) macOS bundle can run. Every other target — Apple Silicon macOS, + * Windows, Linux — would otherwise ship a ~34MB binary it can never execute, + * so it is dropped from those bundles. x64/universal builds fail fast instead + * of silently shipping an Intel Mac app that cannot read keys. + */ +function pruneIntelMacKeyTool(runtimeResources, platform, arch) { + const keyToolDirectory = path.join(runtimeResources, 'resources', 'macos-key-tool') + const usable = platform === 'darwin' && (arch === 'x64' || arch === 'universal') + if (!usable) { + rmSync(keyToolDirectory, { recursive: true, force: true }) + return null + } + const helperPath = path.join(keyToolDirectory, 'intel_mac_key_helper') + if (!existsSync(helperPath)) { + throw new Error(`Missing Intel Mac key helper in a ${arch} bundle: ${helperPath}`) + } + return helperPath +} + function validateAsarRuntimeDependencies(runtimeResources) { const asarPath = path.join(runtimeResources, 'app.asar') if (!existsSync(asarPath)) throw new Error(`Missing packaged application archive: ${asarPath}`) @@ -107,17 +145,87 @@ function validateReaderSkillRuntime(runtimeResources) { return skillPath } +/** + * Native runtime packages are published once per platform-arch pair, and pnpm + * installs all of them, so every bundle ends up carrying the native libraries + * of every platform (measured: ~129MB of speech models plus ~16MB of koffi). + * The loaders pick their package from process.platform/arch, so the siblings + * are dead weight — drop them. + */ +const NATIVE_RUNTIME_PACKAGES = [ + { + modules: [], + prefix: 'sherpa-onnx', + platformName: (platform) => (platform === 'win32' ? 'win' : platform) + }, + { + modules: ['@koromix'], + prefix: 'koffi', + platformName: (platform) => platform + } +] + +function pruneForeignArchNativeRuntimes(runtimeResources, platform, arch) { + if (arch === 'universal') return [] + const unpackedRoot = path.join(runtimeResources, 'app.asar.unpacked', 'node_modules') + if (!existsSync(unpackedRoot)) return [] + const removed = [] + for (const runtime of NATIVE_RUNTIME_PACKAGES) { + const modulesRoot = path.join(unpackedRoot, ...runtime.modules) + if (!existsSync(modulesRoot)) continue + const expected = `${runtime.prefix}-${runtime.platformName(platform)}-${arch}` + const foreign = new RegExp(`^${runtime.prefix}-[a-z0-9]+-(arm64|x64|ia32|loong64|riscv64)$`) + for (const entry of readdirSync(modulesRoot, { withFileTypes: true })) { + if (!entry.isDirectory() || entry.name === expected || !foreign.test(entry.name)) continue + rmSync(path.join(modulesRoot, entry.name), { recursive: true, force: true }) + removed.push(runtime.modules.length ? `${runtime.modules.join('/')}/${entry.name}` : entry.name) + } + } + return removed +} + +/** + * Bundled native directories under resources/connectors are named + * "-". Cross-building both macOS architectures leaves both on + * disk, but a bundle can only execute its own, so drop the foreign ones + * instead of shipping every connector twice. + */ +function pruneForeignArchConnectors(runtimeResources, platform, arch) { + if (arch === 'universal') return [] + const connectorsRoot = path.join(runtimeResources, 'resources', 'connectors') + if (!existsSync(connectorsRoot)) return [] + const expected = `${platform}-${arch}` + const removed = [] + for (const packageEntry of readdirSync(connectorsRoot, { withFileTypes: true })) { + if (!packageEntry.isDirectory()) continue + const packageRoot = path.join(connectorsRoot, packageEntry.name) + for (const targetEntry of readdirSync(packageRoot, { withFileTypes: true })) { + if (!targetEntry.isDirectory() || targetEntry.name === expected) continue + if (!/^[a-z0-9]+-(arm64|x64|ia32)$/.test(targetEntry.name)) continue + rmSync(path.join(packageRoot, targetEntry.name), { recursive: true, force: true }) + removed.push(`${packageEntry.name}/${targetEntry.name}`) + } + } + return removed +} + exports.default = async function afterPack(context) { const runtimeResources = getRuntimeResources(context) + const arch = normalizeBuilderArch(context.arch) validateAsarRuntimeDependencies(runtimeResources) validateReaderSkillRuntime(runtimeResources) validateSilkWasmRuntime(runtimeResources) const ffmpegPath = validateFfmpegRuntime(runtimeResources, context.electronPlatformName) - validateSherpaRuntime( - runtimeResources, + validateRuntimeBinaryArchitecture( + ffmpegPath, context.electronPlatformName, - normalizeBuilderArch(context.arch) + arch, + 'Bundled ffmpeg' ) + validateSherpaRuntime(runtimeResources, context.electronPlatformName, arch) + pruneIntelMacKeyTool(runtimeResources, context.electronPlatformName, arch) + pruneForeignArchConnectors(runtimeResources, context.electronPlatformName, arch) + pruneForeignArchNativeRuntimes(runtimeResources, context.electronPlatformName, arch) if (context.electronPlatformName === 'darwin') { execFileSync('/usr/bin/codesign', ['--force', '--sign', '-', ffmpegPath], { @@ -150,3 +258,7 @@ exports.validateReaderSkillRuntime = validateReaderSkillRuntime exports.validateFfmpegRuntime = validateFfmpegRuntime exports.validateSilkWasmRuntime = validateSilkWasmRuntime exports.validateSherpaRuntime = validateSherpaRuntime +exports.pruneIntelMacKeyTool = pruneIntelMacKeyTool +exports.pruneForeignArchConnectors = pruneForeignArchConnectors +exports.pruneForeignArchNativeRuntimes = pruneForeignArchNativeRuntimes +exports.validateRuntimeBinaryArchitecture = validateRuntimeBinaryArchitecture diff --git a/scripts/binary-arch.cjs b/scripts/binary-arch.cjs new file mode 100644 index 0000000..72c0c9e --- /dev/null +++ b/scripts/binary-arch.cjs @@ -0,0 +1,83 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +const fs = require('node:fs') + +const MACHO_MAGIC_32 = 0xfeedface +const MACHO_MAGIC_64 = 0xfeedfacf +const FAT_MAGIC = 0xcafebabe +const FAT_MAGIC_64 = 0xcafebabf +const PE_SIGNATURE = 0x00004550 +const PE_MACHINE_X64 = 0x8664 +const PE_MACHINE_ARM64 = 0xaa64 + +const CPU_TYPE_IA32 = 0x00000007 +const CPU_TYPE_X86_64 = 0x01000007 +const CPU_TYPE_ARM64 = 0x0100000c + +/** Only the headers are needed; native binaries can be tens of megabytes. */ +const HEADER_BYTES = 64 * 1024 + +function readHeader(filePath) { + const descriptor = fs.openSync(filePath, 'r') + try { + const buffer = Buffer.alloc(HEADER_BYTES) + const bytesRead = fs.readSync(descriptor, buffer, 0, HEADER_BYTES, 0) + return buffer.subarray(0, bytesRead) + } finally { + fs.closeSync(descriptor) + } +} + +function normalizeMachoCpuType(cpuType) { + if (cpuType === CPU_TYPE_X86_64) return 'x64' + if (cpuType === CPU_TYPE_ARM64) return 'arm64' + if (cpuType === CPU_TYPE_IA32) return 'ia32' + return '' +} + +/** + * Returns every architecture contained in a Mach-O or PE binary, as + * electron-builder arch names ("x64", "arm64"). Universal binaries report both. + * Returns an empty array for anything that is not a native executable (scripts, + * wasm), so callers can treat "unknown" separately from "wrong architecture". + */ +function readBinaryArchitectures(filePath) { + let buffer + try { + buffer = readHeader(filePath) + } catch { + return [] + } + if (buffer.length < 8) return [] + + const fatMagic = buffer.readUInt32BE(0) + if (fatMagic === FAT_MAGIC || fatMagic === FAT_MAGIC_64) { + const entrySize = fatMagic === FAT_MAGIC_64 ? 32 : 20 + const count = Math.min(buffer.readUInt32BE(4), 32) + const architectures = [] + for (let index = 0; index < count; index += 1) { + const entryOffset = 8 + index * entrySize + if (entryOffset + 4 > buffer.length) break + const name = normalizeMachoCpuType(buffer.readUInt32BE(entryOffset)) + if (name && !architectures.includes(name)) architectures.push(name) + } + return architectures + } + + const thinMagic = buffer.readUInt32LE(0) + if (thinMagic === MACHO_MAGIC_32 || thinMagic === MACHO_MAGIC_64) { + const name = normalizeMachoCpuType(buffer.readUInt32LE(4)) + return name ? [name] : [] + } + + if (buffer.readUInt16LE(0) === 0x5a4d) { + const peOffset = buffer.readUInt32LE(0x3c) + if (peOffset + 6 > buffer.length || buffer.readUInt32LE(peOffset) !== PE_SIGNATURE) return [] + const machine = buffer.readUInt16LE(peOffset + 4) + if (machine === PE_MACHINE_X64) return ['x64'] + if (machine === PE_MACHINE_ARM64) return ['arm64'] + } + + return [] +} + +module.exports = { readBinaryArchitectures } diff --git a/scripts/ensure-electron-binary.cjs b/scripts/ensure-electron-binary.cjs new file mode 100644 index 0000000..64e812f --- /dev/null +++ b/scripts/ensure-electron-binary.cjs @@ -0,0 +1,103 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +const fs = require('node:fs') +const { execFileSync } = require('node:child_process') +const path = require('node:path') + +/** + * electron@43 的 npm 包不再声明 postinstall(它自己的 package.json 里 scripts 是空的), + * 下载被改成「首次 require('electron') 时的懒加载」。后果是 `pnpm install` 之后 + * node_modules/electron 里只有一个空壳:dist/ 与 path.txt 都不在,而 + * package.json 里的 pnpm.onlyBuiltDependencies: ["electron"] 对着空气发号施令 —— + * 上游没有脚本可跑,pnpm 自然什么也不做。新 clone 于是直到 `pnpm dev` 才炸。 + * 这里显式补上下载,并把它挂在 postinstall / predev 上。 + */ +const MIRROR_FALLBACK = 'https://npmmirror.com/mirrors/electron/' +const projectRoot = path.resolve(__dirname, '..') + +function resolveElectronPackageRoot() { + try { + return path.dirname(require.resolve('electron/package.json')) + } catch { + return '' + } +} + +/** + * .npmrc 的 electron_mirror 只有在 npm/pnpm 执行脚本时才会变成 npm_config_* 环境变量; + * 直接 `node node_modules/electron/install.js` 时读不到,于是会绕开镜像去打 GitHub。 + * 这里显式读出来当 ELECTRON_MIRROR 传下去。显式设置的环境变量优先。 + */ +function resolveMirror() { + if (process.env.ELECTRON_MIRROR) return process.env.ELECTRON_MIRROR + try { + const npmrc = fs.readFileSync(path.join(projectRoot, '.npmrc'), 'utf8') + const match = npmrc.match(/^\s*electron_mirror\s*=\s*(\S+)\s*$/m) + return match ? match[1] : '' + } catch { + return '' + } +} + +/** + * path.txt 只是 electron 写下的相对路径,光有它不算装好 —— 指向的可执行文件 + * 必须真的存在,否则仍会在启动时报「Electron failed to install correctly」。 + */ +function isElectronBinaryInstalled(packageRoot) { + if (!packageRoot) return false + try { + const executable = fs.readFileSync(path.join(packageRoot, 'path.txt'), 'utf8').trim() + return executable !== '' && fs.existsSync(path.join(packageRoot, 'dist', executable)) + } catch { + return false + } +} + +function runInstaller(packageRoot) { + const installer = path.join(packageRoot, 'install.js') + if (!fs.existsSync(installer)) { + throw new Error(`[ensure-electron] missing ${installer}; run pnpm install first`) + } + const mirror = resolveMirror() + const attempts = mirror + ? [{ label: mirror, env: { ELECTRON_MIRROR: mirror } }] + : [{ label: 'default source', env: {} }] + // 配的镜像本身不是 npmmirror 时,再兜一层:镜像挂掉时不至于完全没退路。 + if (!mirror.includes('npmmirror.com')) { + attempts.push({ label: MIRROR_FALLBACK, env: { ELECTRON_MIRROR: MIRROR_FALLBACK } }) + } + let lastError + for (let index = 0; index < attempts.length; index += 1) { + const attempt = attempts[index] + console.log(`[ensure-electron] downloading from ${attempt.label}`) + try { + execFileSync(process.execPath, [installer], { + stdio: 'inherit', + env: { ...process.env, ...attempt.env } + }) + return + } catch (error) { + lastError = error + const next = attempts[index + 1] + if (next) { + console.warn(`[ensure-electron] download failed, retrying from ${next.label}`) + } + } + } + throw lastError +} + +function ensureElectronBinary() { + const packageRoot = resolveElectronPackageRoot() + if (isElectronBinaryInstalled(packageRoot)) return false + console.log('[ensure-electron] Electron binary is missing; downloading it now') + runInstaller(packageRoot) + if (!isElectronBinaryInstalled(packageRoot)) { + throw new Error('[ensure-electron] Electron binary is still missing after installing') + } + console.log('[ensure-electron] Electron binary is ready') + return true +} + +if (require.main === module) ensureElectronBinary() + +module.exports = { ensureElectronBinary, isElectronBinaryInstalled, resolveElectronPackageRoot } diff --git a/scripts/prepare-electron-runtime.cjs b/scripts/prepare-electron-runtime.cjs index cac6d0a..5670adf 100644 --- a/scripts/prepare-electron-runtime.cjs +++ b/scripts/prepare-electron-runtime.cjs @@ -1,6 +1,7 @@ const fs = require('node:fs') const { execFileSync } = require('node:child_process') const path = require('node:path') +const { readBinaryArchitectures } = require('./binary-arch.cjs') const runtimeNames = ['msvcp140.dll', 'msvcp140_1.dll', 'vcruntime140.dll', 'vcruntime140_1.dll'] @@ -24,6 +25,31 @@ function readOption(name, fallback) { return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback } +function ffmpegExecutableName(targetPlatform) { + return targetPlatform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg' +} + +/** + * ffmpeg-static keeps a single binary per platform ("ffmpeg" everywhere except + * Windows), so an arm64 and an x64 macOS checkout cannot coexist in + * node_modules. Its installer also exits early whenever the file already + * exists, so a binary left over from the other architecture would be packed + * silently. Check the real architecture and drop the file when it differs, so + * the caller re-downloads the requested one. + */ +function ensureFfmpegArchitecture(ffmpegPath, targetPlatform, targetArch) { + if (!fs.existsSync(ffmpegPath)) return 'missing' + const architectures = readBinaryArchitectures(ffmpegPath) + if (architectures.includes(targetArch)) return 'match' + console.log( + `[prepare-electron-runtime] ffmpeg-static is ${ + architectures.join('/') || 'not a native binary' + } but ${targetPlatform}-${targetArch} was requested; replacing it` + ) + fs.rmSync(ffmpegPath, { force: true }) + return 'replaced' +} + function prepareFfmpegRuntime(targetPlatform = process.platform, targetArch = process.arch) { let packageRoot = '' try { @@ -31,10 +57,11 @@ function prepareFfmpegRuntime(targetPlatform = process.platform, targetArch = pr } catch { return } - const executable = targetPlatform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg' + const executable = ffmpegExecutableName(targetPlatform) const ffmpegPath = path.join(packageRoot, executable) + const architectureState = ensureFfmpegArchitecture(ffmpegPath, targetPlatform, targetArch) - if (!fs.existsSync(ffmpegPath)) { + if (architectureState !== 'match') { const installScript = path.join(packageRoot, 'install.js') console.log( `[prepare-electron-runtime] downloading ffmpeg-static for ${targetPlatform}-${targetArch}` @@ -83,4 +110,6 @@ function main() { } } -main() +if (require.main === module) main() + +module.exports = { ensureFfmpegArchitecture, ffmpegExecutableName, prepareFfmpegRuntime } diff --git a/src/main/services/personal-wechat-runtime-manager.ts b/src/main/services/personal-wechat-runtime-manager.ts index aaf59c9..98451bb 100644 --- a/src/main/services/personal-wechat-runtime-manager.ts +++ b/src/main/services/personal-wechat-runtime-manager.ts @@ -390,9 +390,11 @@ export class PersonalWechatRuntimeManager { return this.buildStatus( 'unsupported', 0, - process.platform === 'win32' - ? 'Windows 暂不支持个人微信发送组件' - : `当前系统暂不支持个人微信发送组件:${process.platform} ${process.arch}` + process.platform === 'darwin' + ? 'Intel Mac 不支持个人微信发送组件' + : process.platform === 'win32' + ? 'Windows 暂不支持个人微信发送组件' + : '当前系统不支持个人微信发送组件' ) } if (this.downloadPromise) return this.buildStatus('downloading', this.downloadedBytes) diff --git a/src/main/services/personal-wechat-send-service.ts b/src/main/services/personal-wechat-send-service.ts index e309c45..3f03874 100644 --- a/src/main/services/personal-wechat-send-service.ts +++ b/src/main/services/personal-wechat-send-service.ts @@ -1630,7 +1630,10 @@ export class PersonalWechatSendService { status: { ...base, state: 'unsupported_platform', - message: '个人微信发送当前仅支持 Apple Silicon Mac' + message: + process.platform === 'darwin' + ? 'Intel Mac 不支持个人微信发送' + : '当前系统不支持个人微信发送' } } } diff --git a/src/renderer/src/features/settings/pages/PersonalWechatSendPage.tsx b/src/renderer/src/features/settings/pages/PersonalWechatSendPage.tsx index 8885d4a..a4f5460 100644 --- a/src/renderer/src/features/settings/pages/PersonalWechatSendPage.tsx +++ b/src/renderer/src/features/settings/pages/PersonalWechatSendPage.tsx @@ -414,11 +414,13 @@ export function PersonalWechatSendPage({ 个人微信 {pageMessage}

- {pageStatus === 'unsupported' && !isWindows - ? '微信消息发送目前仅支持 macOS 和 Windows。' - : isWindows - ? 'Windows 通过本机微信发送接口工作,请先配置并检测接口端口。' - : '档案中的文字、图片和语音发送,以及定时日报发送,都会使用这项能力。'} + {pageStatus === 'unsupported' && isMac + ? 'Intel Mac 不支持个人微信发送。' + : pageStatus === 'unsupported' && !isWindows + ? '微信消息发送目前仅支持 macOS 和 Windows。' + : isWindows + ? 'Windows 通过本机微信发送接口工作,请先配置并检测接口端口。' + : '档案中的文字、图片和语音发送,以及定时日报发送,都会使用这项能力。'}