Initial clean project import

This commit is contained in:
cat-shark
2026-06-19 18:41:41 +08:00
commit a13b804c7a
1306 changed files with 220568 additions and 0 deletions
+309
View File
@@ -0,0 +1,309 @@
#!/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: 'electron/resources/build', 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, 'electron/resources/build');
if (fs.existsSync(buildDir)) {
fs.copyFileSync(sourceLogo, path.join(buildDir, 'logo.png'));
console.log('[OK] Copied logo.png to electron/resources/build/');
}
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, 'electron/resources/build');
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');