Files
WYF-koubo/apply-oem.cjs
T
2026-06-20 18:37:23 +08:00

310 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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');