#!/usr/bin/env node /** * OEM配置自动应用脚本 * 使用方法:node apply-oem.cjs */ // 强制使用 UTF-8 输出(解决 Windows 控制台中文乱码) if (process.platform === 'win32') { try { require('child_process').execSync('chcp 65001', { stdio: 'ignore' }); } catch (e) { } } const fs = require('fs'); const path = require('path'); const { syncVersions } = require('./scripts/sync-version.cjs'); // 读取OEM配置 const oemConfig = require('./oem-config.cjs'); function findFirstLevelDirsWith(relativeFile) { const dirs = ['.']; for (const entry of fs.readdirSync(__dirname, { withFileTypes: true })) { if (!entry.isDirectory()) continue; if (entry.name === 'node_modules' || entry.name === 'tmp' || entry.name.startsWith('.')) continue; const candidate = path.join(__dirname, entry.name, relativeFile); if (fs.existsSync(candidate)) { dirs.push(entry.name); } } return dirs; } console.log('========================================'); console.log('OEM Config Apply Start'); console.log('Brand:', oemConfig.brandName); console.log('Slogan:', oemConfig.brandSlogan); console.log('API:', oemConfig.apiBaseUrl); console.log('========================================\n'); const configuredVersion = oemConfig.appVersion || process.env.APP_VERSION || process.env.OEM_VERSION; if (configuredVersion) { const result = syncVersions(configuredVersion, { silent: true }); console.log('[OK] Synced app package versions:', result.version, '(' + result.packageCount + ' apps)'); if (result.changed.length > 0) { result.changed.forEach(file => console.log(' [VERSION] ' + file)); } console.log(''); } // 通用替换函数(带调试信息) function replaceInFile(filePath, replacements) { if (!fs.existsSync(filePath)) return false; let content = fs.readFileSync(filePath, 'utf8'); let changed = false; for (const [pattern, replacement] of replacements) { const before = content; content = content.replace(pattern, replacement); if (content !== before) { changed = true; console.log(' [CHANGED] pattern matched: ' + pattern); } else { console.log(' [SKIP] pattern not matched: ' + pattern); } } if (changed) { fs.writeFileSync(filePath, content, 'utf8'); } return changed; } function quoted(value) { return JSON.stringify(String(value || '')); } function templateQuoted(value) { return '`' + String(value || '').replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${') + '`'; } // ========== 1. 更新所有 config.ts 文件 ========== const configDirs = findFirstLevelDirsWith(path.join('src', 'config.ts')) .map(dir => dir === '.' ? 'src' : path.join(dir, 'src')); const apiUrl = oemConfig.apiBaseUrl || ''; const brandName = oemConfig.brandName || ''; const brandSlogan = oemConfig.brandSlogan || ''; let configUpdated = 0; configDirs.forEach(dir => { const configPath = path.join(__dirname, dir, 'config.ts'); if (!fs.existsSync(configPath)) return; console.log('\nProcessing: ' + configPath); const replacements = [ // name: "任意内容" → name: "新品牌名" [/name:\s*"[^"]*"/, 'name: ' + quoted(brandName)], // title: "任意内容" → title: "新品牌名" [/title:\s*"[^"]*"/, 'title: ' + quoted(brandName)], // slogan: "任意内容" → slogan: "新标语" [/slogan:\s*"[^"]*"/, 'slogan: ' + quoted(brandSlogan)], ]; if (apiUrl) { // apiBaseUrl: `任意内容` → apiBaseUrl: `新地址`(反引号格式) replacements.push([/apiBaseUrl:\s*`[^`]*`/, 'apiBaseUrl: ' + templateQuoted(apiUrl)]); // apiBaseUrl: "任意内容" → apiBaseUrl: "新地址"(双引号格式兜底) replacements.push([/apiBaseUrl:\s*"[^"]*"/, 'apiBaseUrl: ' + quoted(apiUrl)]); } const ok = replaceInFile(configPath, replacements); if (ok) { configUpdated++; console.log(' [OK] Updated: ' + configPath); } else { console.log(' [WARN] Nothing changed in: ' + configPath); } }); console.log('\nTotal config.ts files updated: ' + configUpdated + '\n'); // ========== 1.5 更新 electron 端的认证服务器地址 ========== if (apiUrl) { const authConfigPath = path.join(__dirname, 'electron/mapi/auth/config.ts'); // 从 apiBaseUrl 提取基础地址(去掉末尾的 /api) const baseServerUrl = apiUrl.replace(/\/api\/?$/, ''); console.log('Processing auth config: ' + authConfigPath); const authConfigContent = fs.existsSync(authConfigPath) ? fs.readFileSync(authConfigPath, 'utf8') : ''; if (authConfigContent.includes('AppConfig.apiBaseUrl')) { console.log(' [OK] AUTH_SERVER_URL is derived from AppConfig.apiBaseUrl -> ' + baseServerUrl + '\n'); } else { const ok = replaceInFile(authConfigPath, [ [/AUTH_SERVER_URL\s*=\s*process\.env\.AUTH_SERVER_URL\s*\|\|\s*'[^']*'/, 'AUTH_SERVER_URL = process.env.AUTH_SERVER_URL || ' + quoted(baseServerUrl)], [/AUTH_SERVER_URL\s*=\s*process\.env\.AUTH_SERVER_URL\s*\|\|\s*"[^"]*"/, 'AUTH_SERVER_URL = process.env.AUTH_SERVER_URL || ' + quoted(baseServerUrl)], ]); if (ok) { console.log(' [OK] AUTH_SERVER_URL -> ' + baseServerUrl + '\n'); } else { console.log(' [WARN] AUTH_SERVER_URL pattern not matched\n'); } } } // ========== 2. 复制二维码图片 ========== const qrcodeDirs = findFirstLevelDirsWith(path.join('src', 'assets', 'image')) .map(dir => dir === '.' ? path.join('src', 'assets', 'image') : path.join(dir, 'src', 'assets', 'image')); const sourceQrcode = path.join(__dirname, oemConfig.qrcodePath); if (fs.existsSync(sourceQrcode)) { let qrcodeUpdated = 0; qrcodeDirs.forEach(dir => { const targetDir = path.join(__dirname, dir); if (fs.existsSync(targetDir)) { const targetPath = path.join(targetDir, 'qrcode-wish.png'); fs.copyFileSync(sourceQrcode, targetPath); qrcodeUpdated++; console.log('[OK] Copied qrcode to: ' + targetPath); } }); console.log('\nTotal qrcode files copied: ' + qrcodeUpdated + '\n'); } else { console.error('[WARN] Qrcode file not found: ' + sourceQrcode); } // ========== 3. 复制 LOGO 图片 ========== const logoDirs = [ { src: 'public', file: 'logo.svg' }, { src: 'packaging/build-resources', file: 'logo.png' }, ]; const sourceLogo = oemConfig.logoPath ? path.join(__dirname, oemConfig.logoPath) : null; if (sourceLogo && fs.existsSync(sourceLogo)) { const publicDir = path.join(__dirname, 'public'); if (fs.existsSync(publicDir)) { fs.copyFileSync(sourceLogo, path.join(publicDir, 'logo.png')); console.log('[OK] Copied logo.png to public/'); } const buildDir = path.join(__dirname, 'packaging/build-resources'); if (fs.existsSync(buildDir)) { fs.copyFileSync(sourceLogo, path.join(buildDir, 'logo.png')); console.log('[OK] Copied logo.png to packaging/build-resources/'); } const allPublicDirs = fs.readdirSync(__dirname).filter(d => { const sub = path.join(__dirname, d, 'public'); return fs.existsSync(sub) && d !== 'node_modules'; }); allPublicDirs.forEach(d => { const target = path.join(__dirname, d, 'public', 'logo.png'); try { fs.copyFileSync(sourceLogo, target); console.log('[OK] Copied logo to ' + d + '/public/'); } catch(e) {} }); console.log(''); } else { console.log('[INFO] No logoPath configured or file not found, skipping logo copy\n'); } // ========== 3.5 复制图标(ICO/ICNS) ========== const sourceIcon = oemConfig.iconPath ? path.join(__dirname, oemConfig.iconPath) : null; if (sourceIcon && fs.existsSync(sourceIcon)) { const buildDir = path.join(__dirname, 'packaging/build-resources'); if (fs.existsSync(buildDir)) { const sharp = require('sharp'); const iconBuf = fs.readFileSync(sourceIcon); (async () => { try { await sharp(iconBuf).resize(256, 256).png().toFile(path.join(buildDir, 'logo.png')); console.log('[OK] Generated logo.png (256x256)'); const png256 = await sharp(iconBuf).resize(256, 256).png().toBuffer(); const png128 = await sharp(iconBuf).resize(128, 128).png().toBuffer(); const png64 = await sharp(iconBuf).resize(64, 64).png().toBuffer(); const png48 = await sharp(iconBuf).resize(48, 48).png().toBuffer(); const png32 = await sharp(iconBuf).resize(32, 32).png().toBuffer(); const png16 = await sharp(iconBuf).resize(16, 16).png().toBuffer(); // ICO format: header(6) + 1 entry(16) + PNG data const icoHeader = Buffer.alloc(6); icoHeader.writeUInt16LE(0, 0); icoHeader.writeUInt16LE(1, 2); icoHeader.writeUInt16LE(1, 4); const icoEntry = Buffer.alloc(16); icoEntry[0] = 0; icoEntry[1] = 0; icoEntry.writeUInt16LE(1, 2); icoEntry.writeUInt16LE(32, 4); icoEntry.writeUInt32LE(png256.length, 8); icoEntry.writeUInt32LE(22, 12); const ico = Buffer.concat([icoHeader, icoEntry, png256]); fs.writeFileSync(path.join(buildDir, 'logo.ico'), ico); console.log('[OK] Generated logo.ico'); // ICNS: simple icon resource with PNG payloads const icnsIcon = Buffer.alloc(4); icnsIcon.write('icns', 0); const icnsRsrc24 = Buffer.alloc(8); icnsRsrc24.write('ic24', 0); icnsRsrc24.writeUInt32BE(png256.length + 8, 4); const icnsRsrc12 = Buffer.alloc(8); icnsRsrc12.write('ic12', 0); icnsRsrc12.writeUInt32BE(png128.length + 8, 4); const icnsRsrc08 = Buffer.alloc(8); icnsRsrc08.write('ic08', 0); icnsRsrc08.writeUInt32BE(png256.length + 8, 4); const totalSize = 4 + 4 + (8 + png256.length) + (8 + png128.length) + (8 + png256.length); const icnsSizeBuf = Buffer.alloc(4); icnsSizeBuf.writeUInt32BE(totalSize, 0); const icns = Buffer.concat([icnsIcon, icnsSizeBuf, icnsRsrc24, png256, icnsRsrc12, png128, icnsRsrc08, png256]); fs.writeFileSync(path.join(buildDir, 'logo.icns'), icns); console.log('[OK] Generated logo.icns'); // tray icon: 16x16 PNG fs.writeFileSync(path.join(buildDir, 'tray.png'), png16); fs.writeFileSync(path.join(buildDir, 'tray.ico'), Buffer.concat([ (() => { const h = Buffer.alloc(6); h.writeUInt16LE(0,0); h.writeUInt16LE(1,2); h.writeUInt16LE(1,4); return h; })(), (() => { const e = Buffer.alloc(16); e.writeUInt16LE(1,2); e.writeUInt16LE(32,4); e.writeUInt32BE(png16.length,8); e.writeUInt32BE(22,12); return e; })(), png16 ])); console.log('[OK] Generated tray.png and tray.ico'); } catch(err) { console.warn('[WARN] sharp not available, copying icon.png as-is. Install sharp for auto ICO/ICNS conversion:', err.message); fs.copyFileSync(sourceIcon, path.join(buildDir, 'logo.png')); } })(); } console.log(''); } else { console.log('[INFO] No iconPath configured or file not found, skipping icon generation\n'); } // ========== 3.6 复制登录页背景图 ========== const sourceLoginBg = oemConfig.loginBgPath ? path.join(__dirname, oemConfig.loginBgPath) : null; if (sourceLoginBg && fs.existsSync(sourceLoginBg)) { const loginBgTarget = path.join(__dirname, 'public', 'login-bg.png'); fs.copyFileSync(sourceLoginBg, loginBgTarget); console.log('[OK] Copied login-bg.png to public/'); const allPublicDirs2 = fs.readdirSync(__dirname).filter(d => { const sub = path.join(__dirname, d, 'public'); return fs.existsSync(sub) && d !== 'node_modules'; }); allPublicDirs2.forEach(d => { const target = path.join(__dirname, d, 'public', 'login-bg.png'); try { fs.copyFileSync(sourceLoginBg, target); console.log('[OK] Copied login-bg to ' + d + '/public/'); } catch(e) {} }); console.log(''); } else { console.log('[INFO] No loginBgPath configured or file not found, skipping login background\n'); } // ========== 4. 更新 electron-builder.json5 ========== const builderConfigPath = path.join(__dirname, 'electron-builder.json5'); if (fs.existsSync(builderConfigPath)) { console.log('Processing: ' + builderConfigPath); const ok = replaceInFile(builderConfigPath, [ [/"appId":\s*"[^"]*"/, '"appId": ' + quoted(oemConfig.brandShortName)], [/"productName":\s*"[^"]*"/, '"productName": ' + quoted(oemConfig.brandName)], [/"identityName":\s*"[^"]*"/, '"identityName": ' + quoted(oemConfig.brandShortName)], [/"publisherDisplayName":\s*"[^"]*"/, '"publisherDisplayName": ' + quoted(oemConfig.brandDisplayName)], [/"maintainer":\s*"[^"]*"/, '"maintainer": ' + quoted(oemConfig.brandDisplayName)], ]); if (ok) console.log('[OK] electron-builder.json5 updated\n'); } console.log('========================================'); console.log('OEM Apply Done!'); console.log('NEXT STEP: run npm run build to rebuild'); console.log('========================================\n');