Files
ele-HeiXiu/build/build.mjs
YoungestSongMo 337d6c1205 feat: 构建系统重构 + VCHSM 架构迁移 + 右键菜单修复 + 尺寸参数统一
## 构建、打包与分发系统重构 (build/)

- 构建系统收敛至 build/ 目录:build.mjs + electron-builder.js + 6 个脚本

- electron-builder 配置从 package.json 提取为 build/electron-builder.js

- 新增 --edition (personal/enterprise) 和 --channel (stable/beta/alpha) 参数

- OSS 路径按渠道隔离

- 新增 pre-build-check.mjs:Git/CHANGELOG/版本/环境变量校验

- 新增 bump-version.mjs:版本号管理 + CHANGELOG 自动生成

- updater.ts 发送 channel/edition/deviceFingerprint

- 新增 src/shared/utils/rollout.ts 灰度测试工具

- 新增 10 条 NPM 脚本

## VCHSM 五层架构迁移

- 7 个业务模块 + ~500 处导入路径同步

## 修复

- MediaCardContextMenu 改用共享 ContextMenu 组件

- ComboBox 尺寸参数显示统一 (size-format.ts)

## 文档

- UpdateA.md / build/README.md / README.md / .env.example 更新

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 18:41:07 +08:00

432 lines
16 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.
// ============================================================
// build.mjs — 构建总管
//
// 用法node build/build.mjs [选项]
//
// 平台(必选其一):
// --win Windows
// --mac macOS
// --linux Linux
//
// 版本 & 渠道:
// --edition <e> 版本类型personal默认| enterprise
// --channel <c> 发布渠道stable默认| beta | alpha
// --version <v> 覆盖 package.json 中的版本号
//
// 操作(可选组合,默认只执行 --build
// --build 仅构建(默认)
// --clean 构建前清理 release/ dist/ dist-electron/
// --sign 代码签名
// --upload 上传安装包到 OSS
// --publish 发布版本信息到后端 API
// --all 全流程clean + build + sign + upload + publish
//
// 签名(仅 --sign 时可用):
// --cert <path> 证书文件路径 (.pfx)
// --cert-password <p> 证书密码
//
// OSS 路径规则:
// Stable → releases/{version}/ (向后兼容,无渠道后缀)
// Beta → releases/beta/{version}/ (独立生命周期,可设自动过期策略)
// Alpha → releases/{version}/alpha/ (版本优先)
//
// 示例:
// node build/build.mjs --win
// node build/build.mjs --win --channel beta --upload --publish
// node build/build.mjs --win --edition enterprise --all
// node build/build.mjs --win --sign --cert ./cert.pfx --cert-password xxx
// ============================================================
import { spawn, execSync } from 'node:child_process';
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const SCRIPTS = join(__dirname, 'scripts');
// ANSI 颜色
const c = (code, text) => `\x1b[${code}m${text}\x1b[0m`;
const dim = (t) => c(2, t);
const bold = (t) => c(1, t);
const cyan = (t) => c(36, t);
const green = (t) => c(32, t);
const yellow = (t) => c(33, t);
// ---------- 参数解析 ----------
const args = process.argv.slice(2);
const opts = {
win: false,
mac: false,
linux: false,
clean: false,
build: false,
sign: false,
upload: false,
publish: false,
cert: '',
certPassword: '',
edition: 'personal',
channel: 'stable',
version: '',
};
let i = 0;
while (i < args.length) {
const a = args[i];
switch (a) {
case '--win': opts.win = true; break;
case '--mac': opts.mac = true; break;
case '--linux': opts.linux = true; break;
case '--clean': opts.clean = true; break;
case '--build': opts.build = true; break;
case '--sign': opts.sign = true; break;
case '--upload': opts.upload = true; break;
case '--publish': opts.publish = true; break;
case '--all':
opts.clean = opts.build = opts.sign = opts.upload = opts.publish = true;
break;
case '--cert':
opts.cert = args[++i] || '';
break;
case '--cert-password':
opts.certPassword = args[++i] || '';
break;
case '--edition':
opts.edition = args[++i] || 'personal';
break;
case '--channel':
opts.channel = args[++i] || 'stable';
break;
case '--version':
opts.version = args[++i] || '';
break;
case '--help': // fallthrough
case '-h':
printHelp();
process.exit(0);
break;
default:
break;
}
i++;
}
// ---------- 校验 ----------
const platformCount = [opts.win, opts.mac, opts.linux].filter(Boolean).length;
if (platformCount !== 1) {
console.error('错误: 请指定且仅指定一个平台(--win / --mac / --linux');
process.exit(1);
}
if (!['personal', 'enterprise'].includes(opts.edition)) {
console.error('错误: --edition 必须是 personal 或 enterprise');
process.exit(1);
}
if (!['stable', 'beta', 'alpha'].includes(opts.channel)) {
console.error('错误: --channel 必须是 stable、beta 或 alpha');
process.exit(1);
}
// 默认执行 build
if (!opts.clean && !opts.build && !opts.sign && !opts.upload && !opts.publish) {
opts.build = true;
}
const platform = opts.win ? 'win32' : opts.mac ? 'darwin' : 'linux';
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
const version = opts.version || pkg.version;
// ---------- Git 元数据 ----------
let gitCommit = '';
let gitBranch = '';
try {
gitCommit = execSync('git rev-parse --short HEAD', { cwd: ROOT, encoding: 'utf-8' }).trim();
gitBranch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: ROOT, encoding: 'utf-8' }).trim();
} catch {
// 非 git 环境 — 留空
}
const buildTimestamp = new Date().toISOString();
// ---------- OSS 路径计算 ----------
/**
* OSS 对象路径前缀:
* - Beta: releases/beta/{version}/ (独立生命周期,可设 OSS 自动过期策略)
* - Alpha: releases/{version}/alpha/ (方案 B版本优先
* - Stable: releases/{version}/ (无渠道后缀,向后兼容现有布局)
*/
function getOssPrefix() {
if (opts.channel === 'beta') return `releases/beta/${version}`;
if (opts.channel === 'alpha') return `releases/${version}/alpha`;
return `releases/${version}`;
}
// electron-builder 输出目录名(与 OSS 路径解耦,加渠道后缀防冲突)
function getReleaseDirName() {
if (opts.channel === 'stable') return version;
return `${version}-${opts.channel}`;
}
// ---------- 工具函数 ----------
function run(cmd, args, runOpts = {}) {
return new Promise((resolve, reject) => {
const child = spawn(cmd, args, {
cwd: ROOT,
stdio: 'inherit',
shell: true,
...runOpts,
env: { ...process.env, ...(runOpts.env || {}) },
});
child.on('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`${cmd} ${args.join(' ')} 退出码: ${code}`));
});
child.on('error', reject);
});
}
function runScript(name, ...scriptArgs) {
const scriptPath = join(SCRIPTS, `${name}.mjs`);
return run('node', [scriptPath, ...scriptArgs]);
}
function step(title) {
console.log(`\n${'='.repeat(60)}`);
console.log(` ${title}`);
console.log(`${'='.repeat(60)}\n`);
}
// ---------- 主流程 ----------
async function main() {
const electronBuilderPlatform = opts.win ? '--win' : opts.mac ? '--mac' : '--linux';
const editionTag = opts.edition === 'enterprise' ? '企业版' : '个人版';
const channelTag = opts.channel === 'stable' ? '' : ` · ${opts.channel} 渠道`;
const ossPrefix = getOssPrefix();
const releaseDirName = getReleaseDirName();
console.log('┌──────────────────────────────────────────────┐');
console.log(`│ HeiXiu 构建 v${version} ${platform} ${editionTag}`.padEnd(51) + '│');
if (channelTag) {
console.log(`${channelTag.trim()}`.padEnd(51) + '│');
}
if (gitCommit) {
console.log(`│ git: ${gitBranch}@${gitCommit}`.padEnd(51) + '│');
}
console.log('└──────────────────────────────────────────────┘');
console.log(`操作: ${[
opts.clean && '清理',
opts.build && '构建',
opts.sign && '签名',
opts.upload && '上传',
opts.publish && '发布',
].filter(Boolean).join(' → ')}`);
console.log(`本地产物: release/${releaseDirName}/`);
console.log(`OSS 路径: ${ossPrefix}/`);
// ── Step 0: 构建前校验 ──
if (opts.build) {
step('0/ 构建前校验');
try {
await runScript('pre-build-check', platform, version, opts.channel);
} catch (err) {
console.error(`\n❌ 构建前校验失败: ${err.message}`);
console.error('如需跳过校验,请使用 --skip-check 参数(或注释 build.mjs 中对应行)');
process.exit(1);
}
}
// ── Step 1: 清理 ──
if (opts.clean) {
step('1/ 清理构建产物');
await runScript('clean');
}
// ── Step 2: 构建 ──
if (opts.build) {
step('2/ 构建应用');
// 签名环境变量
const signEnv = {};
if (opts.sign) {
if (opts.cert) signEnv.CSC_LINK = opts.cert;
if (opts.certPassword) signEnv.CSC_KEY_PASSWORD = opts.certPassword;
console.log(opts.cert ? `签名证书: ${opts.cert}` : '签名: 使用系统证书存储');
}
// 写入构建元数据
const metaPath = join(ROOT, 'release', '.build-meta.json');
const metaDir = join(ROOT, 'release');
if (!existsSync(metaDir)) mkdirSync(metaDir, { recursive: true });
writeFileSync(metaPath, JSON.stringify({
version,
platform,
edition: opts.edition,
channel: opts.channel,
gitCommit,
gitBranch,
buildTimestamp,
ossPrefix,
releaseDirName,
}, null, 2), 'utf-8');
// 2a. TypeScript 编译
console.log('[tsc] 类型检查...');
await run('npx', ['tsc']);
// 2b. Vite 构建 + 注入构建元数据
console.log('[vite] 打包前端 + Electron...');
await run('npx', ['vite', 'build'], {
env: {
VITE_BUILD_VERSION: version,
VITE_BUILD_CHANNEL: opts.channel,
VITE_BUILD_EDITION: opts.edition,
VITE_BUILD_COMMIT: gitCommit,
VITE_BUILD_TIMESTAMP: buildTimestamp,
},
});
// 2c. electron-builder 打包
console.log(`[electron-builder] ${electronBuilderPlatform} 打包 (${editionTag}${channelTag})...`);
await run('npx', [
'electron-builder',
electronBuilderPlatform,
'--config', 'build/electron-builder.js',
], {
env: {
...signEnv,
HEIXIU_EDITION: opts.edition,
HEIXIU_CHANNEL: opts.channel,
HEIXIU_RELEASE_DIR_NAME: releaseDirName,
},
});
// 2d. 生成 update-info.json
console.log('[info] 生成 update-info.json...');
await runScript('generate-update-info', platform, version, opts.channel, opts.edition, ossPrefix, releaseDirName);
}
// ── Step 3: 上传 OSS ──
if (opts.upload) {
step('3/ 上传 OSS');
await runScript('upload-oss', platform, version, opts.channel, ossPrefix, releaseDirName);
}
// ── Step 4: 发布到 API ──
if (opts.publish) {
step('4/ 发布版本到 API');
await runScript('publish-release', platform, version, opts.channel, opts.edition, ossPrefix, releaseDirName);
}
console.log('\n✅ 全部完成');
// 构建元数据摘要
console.log(dim(`\n构建摘要:`));
console.log(dim(` 版本: ${version}`));
console.log(dim(` 平台: ${platform}`));
console.log(dim(` 版本类型: ${opts.edition}`));
console.log(dim(` 渠道: ${opts.channel}`));
console.log(dim(` OSS: ${ossPrefix}/`));
if (gitCommit) console.log(dim(` Commit: ${gitCommit} (${gitBranch})`));
console.log(dim(` 时间: ${buildTimestamp}`));
}
// ---------- 帮助 ----------
function printHelp() {
const box = (title) => {
const W = 62;
const pad = Math.max(0, W - 4 - title.length);
console.log(`\n${cyan('┌')}${cyan('─'.repeat(W - 2))}${cyan('┐')}`);
console.log(`${cyan('│')} ${bold(title)}${' '.repeat(pad)}${cyan('│')}`);
console.log(`${cyan('└')}${cyan('─'.repeat(W - 2))}${cyan('┘')}`);
};
const row = (flag, desc) => {
console.log(` ${green(flag.padEnd(24))}${dim(desc)}`);
};
const hdr = (t) => console.log(`\n ${bold(yellow(t))}`);
console.log('');
console.log(cyan(' ╔') + cyan('═══════════════════════════════════════════════════') + cyan('╗'));
console.log(cyan(' ║') + bold(' 船长·HeiXiu — 构建工具') + ' ' + cyan('║'));
console.log(cyan(' ╚') + cyan('═══════════════════════════════════════════════════') + cyan('╝'));
box('用法');
console.log(` ${bold('node build/build.mjs')} ${dim('<平台> [选项...]')}`);
console.log(` ${bold('npm run build:win')} ${dim('— 等效快捷命令')}`);
hdr('平台(必选其一)');
row('--win', 'Windows');
row('--mac', 'macOS');
row('--linux', 'Linux');
hdr('版本 & 渠道');
row('--edition <personal|enterprise>', '版本类型(默认 personal');
row('--channel <stable|beta|alpha>', '发布渠道(默认 stable');
row('--version <x.y.z>', '覆盖 package.json 版本号');
hdr('操作(可选组合,不指定则默认 --build');
row('--build', '构建应用(校验 → tsc → vite → electron-builder → info');
row('--clean', '构建前清理 release/ dist/ dist-electron/');
row('--sign', '代码签名(需配合 --cert 或环境变量)');
row('--upload', '上传安装包 + blockmap 到 OSS');
row('--publish', 'POST 版本信息到后端 API');
row('--all', '全流程 = clean + build + sign + upload + publish');
hdr('签名参数(配合 --sign');
row('--cert <path>', '证书文件路径(.pfx / .p12');
row('--cert-password <p>', '证书密码');
console.log(` ${dim('也可通过 CSC_LINK / CSC_KEY_PASSWORD 环境变量设置')}`);
hdr('灰度发布示例');
console.log(` ${dim('# Beta 渠道打包 + 上传 + 发布')}`);
console.log(` ${bold('node build/build.mjs --win --channel beta --upload --publish')}`);
console.log(` ${dim('# Alpha 渠道仅打包测试')}`);
console.log(` ${bold('node build/build.mjs --win --channel alpha')}`);
console.log(` ${dim('# 企业版全流程')}`);
console.log(` ${bold('node build/build.mjs --win --edition enterprise --all')}`);
box('OSS 路径规则');
console.log(` ${yellow('Stable')} ${dim('→ releases/{version}/ (向后兼容)')}`);
console.log(` ${yellow('Beta')} ${dim('→ releases/beta/{version}/ (独立生命周期)')}`);
console.log(` ${yellow('Alpha')} ${dim('→ releases/{version}/alpha/ (版本优先)')}`);
box('npm 快捷命令');
console.log(` ${bold('npm run build:win')} ${dim('→ node build/build.mjs --win')}`);
console.log(` ${bold('npm run build:win:enterprise')} ${dim('→ node build/build.mjs --win --edition enterprise')}`);
console.log(` ${bold('npm run build:win:beta')} ${dim('→ node build/build.mjs --win --channel beta')}`);
console.log(` ${bold('npm run build:mac')} ${dim('→ node build/build.mjs --mac')}`);
console.log(` ${bold('npm run build:linux')} ${dim('→ node build/build.mjs --linux')}`);
console.log(` ${bold('npm run build:clean')} ${dim('→ node build/scripts/clean.mjs')}`);
console.log(` ${bold('npm run version:patch')} ${dim('→ node build/scripts/bump-version.mjs patch')}`);
box('环境变量');
console.log(` ${yellow('UPDATE_API_URL')} ${dim('版本检查 API 基地址')}`);
console.log(` ${yellow('UPDATE_API_KEY')} ${dim('发布 API 鉴权 Key')}`);
console.log(` ${yellow('UPDATE_DOWNLOAD_BASE')} ${dim('OSS 下载基地址')}`);
console.log(` ${yellow('OSS_ENDPOINT')} ${dim('阿里云 OSS endpoint')}`);
console.log(` ${yellow('OSS_BUCKET')} ${dim('阿里云 OSS bucket')}`);
console.log(` ${yellow('OSS_ACCESS_KEY_ID')} ${dim('阿里云 AccessKey ID')}`);
console.log(` ${yellow('OSS_ACCESS_KEY_SECRET')} ${dim('阿里云 AccessKey Secret')}`);
console.log(` ${yellow('CSC_LINK')} ${dim('签名证书路径')}`);
console.log(` ${yellow('CSC_KEY_PASSWORD')} ${dim('签名证书密码')}`);
console.log('');
}
// ---------- 入口 ----------
main().catch((err) => {
console.error(`\n❌ 构建失败: ${err.message}`);
process.exit(1);
});