const cp = require('node:child_process'); const fs = require('node:fs'); const path = require('node:path'); const yaml = require('js-yaml'); const backendDir = path.resolve(__dirname, '..'); const repoDir = path.resolve(backendDir, '..', '..'); const outputDir = path.join(backendDir, 'build', 'pkg-output'); const trayProject = path.join(repoDir, 'packages', 'windows-tray', 'Neta.Tray', 'Neta.Tray.csproj'); const trayOutputDir = path.join(backendDir, 'build', 'tray-output'); const nodeRuntimeDir = path.join(backendDir, 'build', 'node-runtime'); const skillsSourceDir = path.join(backendDir, 'skills'); const skillsOutputDir = path.join(backendDir, 'build', 'skills-output'); const toolsSourceDir = path.join(backendDir, 'tools', 'win32'); const toolsOutputDir = path.join(backendDir, 'build', 'tools-output', 'win32'); const issPath = path.join(backendDir, 'installer', 'setup.iss'); const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; function copyBundledNodeRuntime() { if (process.platform !== 'win32') { console.log('Skipping bundled node.exe copy on non-Windows host.'); return; } fs.rmSync(nodeRuntimeDir, { recursive: true, force: true }); fs.mkdirSync(nodeRuntimeDir, { recursive: true }); fs.copyFileSync(process.execPath, path.join(nodeRuntimeDir, 'node.exe')); } function readSkillConfig(skillDir) { const configPath = path.join(skillDir, 'skill.config.yaml'); if (!fs.existsSync(configPath)) return null; return yaml.load(fs.readFileSync(configPath, 'utf8')) || null; } function prepareSkillsOutput() { fs.rmSync(skillsOutputDir, { recursive: true, force: true }); fs.cpSync(skillsSourceDir, skillsOutputDir, { recursive: true, filter: source => path.basename(source) !== 'node_modules', }); for (const entry of fs.readdirSync(skillsOutputDir, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const skillDir = path.join(skillsOutputDir, entry.name); const config = readSkillConfig(skillDir); const nodePackages = config?.runtime === 'node' && config?.entrypoint ? config?.dependencies?.node?.packages : null; if (!Array.isArray(nodePackages) || nodePackages.length === 0) continue; console.log(`Installing Node dependencies for skill ${entry.name}...`); cp.execFileSync( npmCmd, [ 'install', '--omit=dev', '--no-audit', '--no-fund', '--package-lock=false', ...nodePackages, ], { cwd: skillDir, stdio: 'inherit' } ); } } if (!fs.existsSync(issPath)) { throw new Error(`缺少安装器脚本: ${issPath}`); } if (!fs.existsSync(path.join(outputDir, 'backend.exe'))) { console.log('backend.exe not found, running pkg build first...'); cp.execFileSync('node', ['scripts/pkg-build.js'], { cwd: backendDir, stdio: 'inherit' }); } copyBundledNodeRuntime(); prepareSkillsOutput(); fs.rmSync(trayOutputDir, { recursive: true, force: true }); cp.execFileSync( 'dotnet', [ 'publish', trayProject, '-c', 'Release', '-r', 'win-x64', '--self-contained', 'true', '/p:PublishSingleFile=true', '/p:PublishTrimmed=false', '-o', trayOutputDir, ], { cwd: repoDir, stdio: 'inherit' } ); const publishedTrayExePath = path.join(trayOutputDir, 'Neta.Tray.exe'); if (!fs.existsSync(publishedTrayExePath)) { throw new Error(`缺少托盘产物: ${publishedTrayExePath}`); } // 架构 C: 把 tools/win32/*.ps1 拷贝到 installer stage 目录(weixin-db 的 key 抽取脚本) fs.rmSync(toolsOutputDir, { recursive: true, force: true }); fs.mkdirSync(toolsOutputDir, { recursive: true }); if (fs.existsSync(toolsSourceDir)) { fs.cpSync(toolsSourceDir, toolsOutputDir, { recursive: true }); console.log(`[installer] copied tools/win32 → ${toolsOutputDir}`); } else { console.warn(`[installer] tools/win32 not found at ${toolsSourceDir}, skip`); } const isccCandidates = [ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Inno Setup 6', 'ISCC.exe'), 'C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe', 'C:\\Program Files\\Inno Setup 6\\ISCC.exe', ]; const iscc = isccCandidates.find(p => fs.existsSync(p)) || 'iscc'; console.log('Building installer with Inno Setup...'); cp.execFileSync(iscc, [issPath], { cwd: backendDir, stdio: 'inherit' }); console.log('Installer build complete.');