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>
This commit is contained in:
431
build/build.mjs
Normal file
431
build/build.mjs
Normal file
@@ -0,0 +1,431 @@
|
||||
// ============================================================
|
||||
// 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);
|
||||
});
|
||||
78
build/electron-builder.js
Normal file
78
build/electron-builder.js
Normal file
@@ -0,0 +1,78 @@
|
||||
// ============================================================
|
||||
// electron-builder 配置
|
||||
//
|
||||
// 从 package.json "build" 字段提取,通过环境变量动态注入参数。
|
||||
// build.mjs 负责设置 HEIXIU_* 环境变量后调用 electron-builder。
|
||||
//
|
||||
// 用法:
|
||||
// HEIXIU_EDITION=enterprise HEIXIU_CHANNEL=beta npx electron-builder --win --config build/electron-builder.js
|
||||
// ============================================================
|
||||
|
||||
import { readFileSync } 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 pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
||||
|
||||
const edition = process.env.HEIXIU_EDITION || 'personal';
|
||||
const channel = process.env.HEIXIU_CHANNEL || 'stable';
|
||||
const releaseDirName = process.env.HEIXIU_RELEASE_DIR_NAME || pkg.version;
|
||||
|
||||
const editionLabel = edition === 'enterprise' ? '企业版' : '';
|
||||
const productName = editionLabel
|
||||
? `船长·HeiXiu·${editionLabel}`
|
||||
: '船长·HeiXiu';
|
||||
|
||||
/** @type {import('electron-builder').Configuration} */
|
||||
const config = {
|
||||
appId: edition === 'enterprise'
|
||||
? 'com.heixiu.enterprise'
|
||||
: 'com.heixiu.electron',
|
||||
productName,
|
||||
asar: true,
|
||||
directories: {
|
||||
output: `release/${releaseDirName}`,
|
||||
},
|
||||
files: [
|
||||
'dist',
|
||||
'dist-electron',
|
||||
],
|
||||
electronDownload: {
|
||||
mirror: 'https://npmmirror.com/mirrors/electron/',
|
||||
},
|
||||
// generic provider 仅作为 electron-updater 的 fallback,
|
||||
// 实际版本检查通过自定义 HeiXiuProvider → API
|
||||
publish: {
|
||||
provider: 'generic',
|
||||
url: 'https://update.heixiu.com',
|
||||
},
|
||||
win: {
|
||||
icon: 'src/assets/logo/heixiu.ico',
|
||||
target: [{ target: 'NSIS', arch: ['x64'] }],
|
||||
artifactName: '${productName}-Windows-${version}-Setup.${ext}',
|
||||
},
|
||||
mac: {
|
||||
icon: 'src/assets/logo/heixiu.icns',
|
||||
target: ['dmg'],
|
||||
artifactName: '${productName}-Mac-${version}-Installer.${ext}',
|
||||
},
|
||||
linux: {
|
||||
icon: 'src/assets/logo/HX.png',
|
||||
target: ['AppImage'],
|
||||
artifactName: '${productName}-Linux-${version}.${ext}',
|
||||
},
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
perMachine: false,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
deleteAppDataOnUninstall: false,
|
||||
differentialPackage: true,
|
||||
installerIcon: 'src/assets/logo/heixiu.ico',
|
||||
uninstallerIcon: 'src/assets/logo/heixiu.ico',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
153
build/scripts/bump-version.mjs
Normal file
153
build/scripts/bump-version.mjs
Normal file
@@ -0,0 +1,153 @@
|
||||
// ============================================================
|
||||
// bump-version.mjs — 版本号管理与 CHANGELOG 模板生成
|
||||
//
|
||||
// 用法:
|
||||
// node build/scripts/bump-version.mjs <major|minor|patch> [--no-changelog]
|
||||
// node build/scripts/bump-version.mjs set <x.y.z>
|
||||
// node build/scripts/bump-version.mjs pre <beta|alpha>
|
||||
//
|
||||
// 示例:
|
||||
// node build/scripts/bump-version.mjs patch # 0.0.28 → 0.0.29
|
||||
// node build/scripts/bump-version.mjs minor # 0.0.28 → 0.1.0
|
||||
// node build/scripts/bump-version.mjs set 1.0.0 # 直接设置版本
|
||||
// node build/scripts/bump-version.mjs pre beta # 0.0.29 → 0.0.29-beta.1
|
||||
// ============================================================
|
||||
|
||||
import { readFileSync, writeFileSync } 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 args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
const arg1 = args[1];
|
||||
const noChangelog = args.includes('--no-changelog');
|
||||
|
||||
if (!command) {
|
||||
console.error('用法: node build/scripts/bump-version.mjs <major|minor|patch|set|pre> [value]');
|
||||
console.error('示例: node build/scripts/bump-version.mjs patch');
|
||||
console.error(' node build/scripts/bump-version.mjs set 1.0.0');
|
||||
console.error(' node build/scripts/bump-version.mjs pre beta');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pkgPath = join(ROOT, 'package.json');
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
||||
const oldVersion = pkg.version;
|
||||
|
||||
// ---------- 版本计算 ----------
|
||||
|
||||
function parseSemver(v) {
|
||||
const match = v.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
|
||||
if (!match) throw new Error(`无效的版本号格式: ${v}`);
|
||||
return {
|
||||
major: parseInt(match[1]),
|
||||
minor: parseInt(match[2]),
|
||||
patch: parseInt(match[3]),
|
||||
pre: match[4] || null,
|
||||
};
|
||||
}
|
||||
|
||||
let newVersion;
|
||||
|
||||
switch (command) {
|
||||
case 'major':
|
||||
case 'minor':
|
||||
case 'patch': {
|
||||
const sv = parseSemver(oldVersion);
|
||||
if (command === 'major') {
|
||||
sv.major++;
|
||||
sv.minor = 0;
|
||||
sv.patch = 0;
|
||||
} else if (command === 'minor') {
|
||||
sv.minor++;
|
||||
sv.patch = 0;
|
||||
} else {
|
||||
sv.patch++;
|
||||
}
|
||||
sv.pre = null;
|
||||
newVersion = `${sv.major}.${sv.minor}.${sv.patch}`;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'set': {
|
||||
if (!arg1) {
|
||||
console.error('用法: node build/scripts/bump-version.mjs set <x.y.z>');
|
||||
process.exit(1);
|
||||
}
|
||||
// 基本格式校验
|
||||
if (!/^\d+\.\d+\.\d+(-.+)?$/.test(arg1)) {
|
||||
console.error(`无效的版本号: ${arg1}`);
|
||||
process.exit(1);
|
||||
}
|
||||
newVersion = arg1;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'pre': {
|
||||
if (!arg1 || !['beta', 'alpha'].includes(arg1)) {
|
||||
console.error('用法: node build/scripts/bump-version.mjs pre <beta|alpha>');
|
||||
process.exit(1);
|
||||
}
|
||||
const sv = parseSemver(oldVersion);
|
||||
|
||||
// 如果当前版本已有预发布后缀,递增进位号
|
||||
if (sv.pre) {
|
||||
const match = sv.pre.match(new RegExp(`^${arg1}\\.(\\d+)$`));
|
||||
if (match) {
|
||||
const num = parseInt(match[1]) + 1;
|
||||
newVersion = `${sv.major}.${sv.minor}.${sv.patch}-${arg1}.${num}`;
|
||||
} else {
|
||||
// 不同预发布类型,重新开始
|
||||
newVersion = `${sv.major}.${sv.minor}.${sv.patch}-${arg1}.1`;
|
||||
}
|
||||
} else {
|
||||
// 纯 semver → 首次预发布:自动升 patch 再加后缀
|
||||
sv.patch++;
|
||||
newVersion = `${sv.major}.${sv.minor}.${sv.patch}-${arg1}.1`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.error(`未知命令: ${command}。支持: major | minor | patch | set <ver> | pre <beta|alpha>`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ---------- 更新 package.json ----------
|
||||
|
||||
pkg.version = newVersion;
|
||||
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
||||
console.log(`✅ package.json: ${oldVersion} → ${newVersion}`);
|
||||
|
||||
// ---------- 更新 shared/constants/version.ts(由 import pkg 动态读取,无需修改) ----------
|
||||
console.log(' shared/constants/version.ts 自动读取 package.json,无需手动同步');
|
||||
|
||||
// ---------- 更新 CHANGELOG.md ----------
|
||||
|
||||
if (!noChangelog) {
|
||||
const changelogPath = join(ROOT, 'CHANGELOG.md');
|
||||
try {
|
||||
let changelog = readFileSync(changelogPath, 'utf-8');
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const entry = `## ${newVersion}(${today})\n\n**更新内容待补充**\n\n---\n\n`;
|
||||
|
||||
// 在第一个 ## 之前插入新版本条目
|
||||
const firstHeaderMatch = changelog.match(/^##\s/m);
|
||||
if (firstHeaderMatch) {
|
||||
const insertAt = firstHeaderMatch.index;
|
||||
changelog = changelog.slice(0, insertAt) + entry + changelog.slice(insertAt);
|
||||
writeFileSync(changelogPath, changelog, 'utf-8');
|
||||
console.log(`✅ CHANGELOG.md: 已添加 v${newVersion} 模板条目`);
|
||||
} else {
|
||||
console.warn('⚠️ CHANGELOG.md 格式异常,未找到 ## 版本标题');
|
||||
}
|
||||
} catch {
|
||||
console.warn('⚠️ CHANGELOG.md 不存在,跳过更新');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n版本已更新: ${oldVersion} → ${newVersion}`);
|
||||
25
build/scripts/clean.mjs
Normal file
25
build/scripts/clean.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
// ============================================================
|
||||
// 清理所有构建产物
|
||||
// npm run build:clean
|
||||
// ============================================================
|
||||
|
||||
import { rmSync, existsSync } 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 dirs = ['release', 'dist', 'dist-electron'];
|
||||
|
||||
for (const dir of dirs) {
|
||||
const p = join(ROOT, dir);
|
||||
if (existsSync(p)) {
|
||||
rmSync(p, { recursive: true, force: true });
|
||||
console.log(`已清理: ${dir}/`);
|
||||
} else {
|
||||
console.log(`跳过(不存在): ${dir}/`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n清理完成。');
|
||||
202
build/scripts/generate-update-info.mjs
Normal file
202
build/scripts/generate-update-info.mjs
Normal file
@@ -0,0 +1,202 @@
|
||||
// ============================================================
|
||||
// 打包后生成 update-info.json(含渠道/版本类型/SHA512/blockmap)
|
||||
//
|
||||
// 用法(由 build.mjs 调用):
|
||||
// node build/scripts/generate-update-info.mjs <platform> <version> <channel> <edition> <ossPrefix> <releaseDirName>
|
||||
//
|
||||
// 也支持手动调用:
|
||||
// node build/scripts/generate-update-info.mjs <platform> <version> [channel] [edition] [ossPrefix] [releaseDirName] [--url <url>] [--blockmap-url <url>]
|
||||
//
|
||||
// 环境变量(可选):
|
||||
// UPDATE_DOWNLOAD_BASE — OSS 下载基地址
|
||||
// UPDATE_FORCE — 是否强制更新,默认 false
|
||||
// UPDATE_MIN_VERSION — 最低兼容版本,默认 0.0.0
|
||||
// UPDATE_CHANGELOG — 覆盖 CHANGELOG.md 中的日志内容
|
||||
// ============================================================
|
||||
|
||||
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, '..', '..');
|
||||
|
||||
// ---------- 参数解析 ----------
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
let urlOverride = '';
|
||||
let blockmapUrlOverride = '';
|
||||
const positional = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--url' && args[i + 1]) {
|
||||
urlOverride = args[++i];
|
||||
} else if (args[i] === '--blockmap-url' && args[i + 1]) {
|
||||
blockmapUrlOverride = args[++i];
|
||||
} else if (!args[i].startsWith('--')) {
|
||||
positional.push(args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const platform = positional[0];
|
||||
const version = positional[1];
|
||||
const channel = positional[2] || 'stable';
|
||||
const edition = positional[3] || 'personal';
|
||||
const ossPrefix = positional[4] || `releases/${version}`;
|
||||
const releaseDirName = positional[5] || version;
|
||||
|
||||
if (!platform || !version) {
|
||||
console.error('用法: node build/scripts/generate-update-info.mjs <platform> <version> [channel] [edition] [ossPrefix] [releaseDirName]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const productName = '船长·HeiXiu';
|
||||
|
||||
// ---------- 查找安装包 ----------
|
||||
|
||||
const releaseDir = join(ROOT, 'release', releaseDirName);
|
||||
|
||||
let installerPath;
|
||||
try {
|
||||
const files = readdirSync(releaseDir);
|
||||
const exts = platform === 'win32' ? ['.exe'] : platform === 'darwin' ? ['.dmg'] : ['.AppImage'];
|
||||
const match = files.find((f) => exts.some((ext) => f.endsWith(ext)));
|
||||
if (!match) {
|
||||
console.error(`未在 ${releaseDir} 中找到安装包文件`);
|
||||
process.exit(1);
|
||||
}
|
||||
installerPath = join(releaseDir, match);
|
||||
console.log(`找到安装包: ${match}`);
|
||||
} catch {
|
||||
console.error(`目录不存在: ${releaseDir}\n请先执行构建(如 npm run build:win)`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ---------- 计算文件信息 ----------
|
||||
|
||||
const fileBuffer = readFileSync(installerPath);
|
||||
const stats = statSync(installerPath);
|
||||
|
||||
const sha512 = createHash('sha512').update(fileBuffer).digest('base64').replace(/\r?\n/g, '');
|
||||
const fileSize = stats.size;
|
||||
|
||||
console.log(`文件大小: ${(fileSize / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`SHA512: ${sha512.slice(0, 32)}...`);
|
||||
|
||||
// ---------- 查找 blockmap 文件 ----------
|
||||
|
||||
let blockMapUrl;
|
||||
let blockMapSize;
|
||||
|
||||
try {
|
||||
const blockmapFile = readdirSync(releaseDir).find((f) => f.endsWith('.blockmap'));
|
||||
if (blockmapFile) {
|
||||
const blockmapPath = join(releaseDir, blockmapFile);
|
||||
const blockmapStats = statSync(blockmapPath);
|
||||
blockMapSize = blockmapStats.size;
|
||||
|
||||
if (blockmapUrlOverride) {
|
||||
blockMapUrl = blockmapUrlOverride;
|
||||
} else {
|
||||
const downloadBase = process.env.UPDATE_DOWNLOAD_BASE || 'https://heixiu.oss-cn-hangzhou.aliyuncs.com';
|
||||
blockMapUrl = `${downloadBase.replace(/\/+$/, '')}/${ossPrefix}/${encodeURI(blockmapFile)}`;
|
||||
}
|
||||
|
||||
console.log(`Blockmap: ${blockmapFile} (${(blockMapSize / 1024).toFixed(2)} KB)`);
|
||||
console.log(`Blockmap URL: ${blockMapUrl}`);
|
||||
}
|
||||
} catch {
|
||||
blockMapUrl = undefined;
|
||||
blockMapSize = undefined;
|
||||
}
|
||||
|
||||
// ---------- 构建下载 URL ----------
|
||||
|
||||
const fileName = installerPath.split(/[/\\]/).pop();
|
||||
|
||||
let downloadUrl;
|
||||
if (urlOverride) {
|
||||
downloadUrl = urlOverride;
|
||||
console.log(`下载 URL(手动指定): ${downloadUrl}`);
|
||||
} else {
|
||||
const downloadBase = process.env.UPDATE_DOWNLOAD_BASE || 'https://heixiu.oss-cn-hangzhou.aliyuncs.com';
|
||||
downloadUrl = `${downloadBase.replace(/\/+$/, '')}/${ossPrefix}/${encodeURI(fileName)}`;
|
||||
console.log(`下载 URL(自动拼接): ${downloadUrl}`);
|
||||
}
|
||||
|
||||
// ---------- 读取更新日志 ----------
|
||||
|
||||
let changelog;
|
||||
if (process.env.UPDATE_CHANGELOG) {
|
||||
changelog = process.env.UPDATE_CHANGELOG;
|
||||
} else {
|
||||
const changelogPath = join(ROOT, 'CHANGELOG.md');
|
||||
try {
|
||||
const changelogContent = readFileSync(changelogPath, 'utf-8');
|
||||
// 精确匹配当前版本块
|
||||
const escapedVersion = version.replace(/\./g, '\\.');
|
||||
const versionSection = changelogContent.match(
|
||||
new RegExp(`^##\\s+${escapedVersion}[^\\n]*\\n([\\s\\S]*?)(?=^##\\s|\\Z)`, 'm'),
|
||||
);
|
||||
if (versionSection) {
|
||||
changelog = versionSection[0].trim();
|
||||
console.log(`更新日志: 已从 CHANGELOG.md 提取 v${version} 条目`);
|
||||
} else {
|
||||
changelog = `## ${version}\n\n更新内容待补充`;
|
||||
console.warn(`⚠️ CHANGELOG.md 中未找到 v${version} 条目,使用占位内容`);
|
||||
}
|
||||
} catch {
|
||||
changelog = `## ${version}\n\n更新内容待补充`;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 其他配置 ----------
|
||||
|
||||
const forceUpdate = process.env.UPDATE_FORCE === 'true';
|
||||
const minimumVersion = process.env.UPDATE_MIN_VERSION || '0.0.0';
|
||||
const releaseDate = new Date().toISOString().slice(0, 10);
|
||||
|
||||
// ---------- 生成 JSON ----------
|
||||
|
||||
const updateInfo = {
|
||||
// --- 必填:API 返回字段 ---
|
||||
hasUpdate: true,
|
||||
latestVersion: version,
|
||||
downloadUrl,
|
||||
fileSize,
|
||||
sha512,
|
||||
changelog,
|
||||
releaseDate,
|
||||
forceUpdate,
|
||||
minimumVersion,
|
||||
|
||||
// --- 渠道与版本类型(灰度测试关键字段)---
|
||||
channel,
|
||||
edition,
|
||||
|
||||
// --- 增量更新 ---
|
||||
...(blockMapUrl && { blockMapUrl }),
|
||||
...(blockMapSize != null && { blockMapSize }),
|
||||
|
||||
// --- 附加元数据 ---
|
||||
_meta: {
|
||||
platform,
|
||||
channel,
|
||||
edition,
|
||||
fileName,
|
||||
ossPrefix,
|
||||
blockmap: blockMapUrl ? { url: blockMapUrl, size: blockMapSize } : null,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
const outputPath = join(
|
||||
releaseDir,
|
||||
`${productName}-${platform}-v${version}-update-info.json`,
|
||||
);
|
||||
|
||||
writeFileSync(outputPath, JSON.stringify(updateInfo, null, 2), 'utf-8');
|
||||
|
||||
console.log(`\n✅ 已生成: ${outputPath}`);
|
||||
console.log(JSON.stringify(updateInfo, null, 2));
|
||||
135
build/scripts/pre-build-check.mjs
Normal file
135
build/scripts/pre-build-check.mjs
Normal file
@@ -0,0 +1,135 @@
|
||||
// ============================================================
|
||||
// pre-build-check.mjs — 构建前校验
|
||||
//
|
||||
// 用法:
|
||||
// node build/scripts/pre-build-check.mjs <platform> <version> <channel>
|
||||
//
|
||||
// 检查项:
|
||||
// 1. Git 工作区是否干净(有未提交变更时警告,非阻断)
|
||||
// 2. CHANGELOG.md 是否包含当前版本条目
|
||||
// 3. package.json 版本号与传入版本号一致
|
||||
// 4. 渠道版本号格式校验(beta/alpha 必须含预发布后缀)
|
||||
// ============================================================
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, '..', '..');
|
||||
|
||||
const platform = process.argv[2];
|
||||
const version = process.argv[3];
|
||||
const channel = process.argv[4] || 'stable';
|
||||
|
||||
if (!platform || !version) {
|
||||
console.error('用法: node build/scripts/pre-build-check.mjs <platform> <version> [channel]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let hasError = false;
|
||||
|
||||
// ---------- 1. Git 工作区检查 ----------
|
||||
|
||||
console.log('▸ 检查 Git 工作区状态...');
|
||||
try {
|
||||
const status = execSync('git status --porcelain', { cwd: ROOT, encoding: 'utf-8' }).trim();
|
||||
if (status) {
|
||||
console.warn('⚠️ 警告: Git 工作区有未提交的变更:');
|
||||
const lines = status.split('\n').slice(0, 10);
|
||||
lines.forEach((l) => console.warn(` ${l}`));
|
||||
if (status.split('\n').length > 10) {
|
||||
console.warn(` ... 还有 ${status.split('\n').length - 10} 个文件`);
|
||||
}
|
||||
console.warn(' (不阻断构建,但建议先提交)\n');
|
||||
} else {
|
||||
console.log(' ✅ Git 工作区干净\n');
|
||||
}
|
||||
} catch {
|
||||
console.warn(' ⚠️ 非 Git 环境,跳过工作区检查\n');
|
||||
}
|
||||
|
||||
// ---------- 2. CHANGELOG 版本条目检查 ----------
|
||||
|
||||
console.log('▸ 检查 CHANGELOG.md 版本条目...');
|
||||
const changelogPath = join(ROOT, 'CHANGELOG.md');
|
||||
try {
|
||||
const changelogContent = readFileSync(changelogPath, 'utf-8');
|
||||
const versionPattern = new RegExp(
|
||||
`^##\\s+${version.replace(/\./g, '\\.')}\\b`,
|
||||
'm',
|
||||
);
|
||||
if (versionPattern.test(changelogContent)) {
|
||||
console.log(` ✅ CHANGELOG.md 已包含 v${version} 条目\n`);
|
||||
} else {
|
||||
console.error(` ❌ CHANGELOG.md 中未找到 v${version} 的更新日志条目`);
|
||||
console.error(` 请在 CHANGELOG.md 顶部添加: ## ${version}(日期)\n`);
|
||||
hasError = true;
|
||||
}
|
||||
} catch {
|
||||
console.warn(' ⚠️ CHANGELOG.md 不存在,跳过版本条目检查\n');
|
||||
}
|
||||
|
||||
// ---------- 3. package.json 版本号一致性 ----------
|
||||
|
||||
console.log('▸ 检查 package.json 版本一致性...');
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
||||
if (pkg.version === version) {
|
||||
console.log(` ✅ package.json 版本号一致: ${version}\n`);
|
||||
} else {
|
||||
console.error(` ❌ 版本号不一致: 传入 ${version},package.json 为 ${pkg.version}`);
|
||||
console.error(' 请先执行: node build/scripts/bump-version.mjs <major|minor|patch>\n');
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
// ---------- 4. 渠道版本号格式校验 ----------
|
||||
|
||||
console.log('▸ 检查渠道版本号格式...');
|
||||
if (channel === 'stable') {
|
||||
// stable 版本号必须是纯 semver(如 0.0.28)
|
||||
if (/^\d+\.\d+\.\d+$/.test(version)) {
|
||||
console.log(' ✅ Stable 渠道版本号格式正确\n');
|
||||
} else {
|
||||
console.error(` ❌ Stable 渠道版本号必须为纯 semver 格式 (x.y.z),当前: ${version}\n`);
|
||||
hasError = true;
|
||||
}
|
||||
} else {
|
||||
// beta/alpha 版本号必须含预发布后缀(如 0.0.29-beta.1)
|
||||
if (/-/.test(version)) {
|
||||
console.log(` ✅ ${channel} 渠道版本号含预发布后缀\n`);
|
||||
} else {
|
||||
console.warn(` ⚠️ ${channel} 渠道建议使用预发布版本号(如 ${version}-${channel}.1)`);
|
||||
console.warn(' 当前版本号不含预发布后缀,可能覆盖 stable 版本的 OSS 文件\n');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 5. 环境变量检查(仅 upload/publish 时有效,此处仅提示) ----------
|
||||
|
||||
console.log('▸ 环境变量就绪检查...');
|
||||
const uploadVars = ['OSS_ENDPOINT', 'OSS_BUCKET', 'OSS_ACCESS_KEY_ID', 'OSS_ACCESS_KEY_SECRET'];
|
||||
const missingUpload = uploadVars.filter((v) => !process.env[v]);
|
||||
if (missingUpload.length > 0) {
|
||||
console.warn(` ⚠️ OSS 相关环境变量未设置: ${missingUpload.join(', ')}`);
|
||||
console.warn(' 如需上传 OSS,请配置这些环境变量(或在 .env.local 中设置)');
|
||||
} else {
|
||||
console.log(' ✅ OSS 环境变量已配置');
|
||||
}
|
||||
|
||||
if (!process.env.UPDATE_API_KEY) {
|
||||
console.warn(' ⚠️ UPDATE_API_KEY 未设置,发布到 API 可能失败');
|
||||
} else {
|
||||
console.log(' ✅ UPDATE_API_KEY 已配置');
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// ---------- 结果 ----------
|
||||
|
||||
if (hasError) {
|
||||
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.error('❌ 构建前校验未通过,请修复以上问题后重试');
|
||||
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✅ 构建前校验全部通过');
|
||||
149
build/scripts/publish-release.mjs
Normal file
149
build/scripts/publish-release.mjs
Normal file
@@ -0,0 +1,149 @@
|
||||
// ============================================================
|
||||
// 发布版本信息到后端 API
|
||||
//
|
||||
// 用法(由 build.mjs 调用):
|
||||
// node build/scripts/publish-release.mjs <platform> <version> <channel> <edition> <ossPrefix> <releaseDirName>
|
||||
//
|
||||
// 也支持手动调用:
|
||||
// node build/scripts/publish-release.mjs <platform> <version> [channel] [edition] [ossPrefix] [--url <url>] [--json <json>]
|
||||
//
|
||||
// 环境变量:
|
||||
// UPDATE_API_URL — API 基地址(默认 https://www.heixiu.com)
|
||||
// UPDATE_API_KEY — API 鉴权 Key(通过 Authorization: Bearer 头传递)
|
||||
// ============================================================
|
||||
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import axios from 'axios';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, '..', '..');
|
||||
|
||||
// ---------- 参数 ----------
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const positional = [];
|
||||
let urlOverride = '';
|
||||
let jsonOverride = '';
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--url' && args[i + 1]) {
|
||||
urlOverride = args[++i];
|
||||
} else if (args[i] === '--json' && args[i + 1]) {
|
||||
jsonOverride = args[++i];
|
||||
} else if (!args[i].startsWith('--')) {
|
||||
positional.push(args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const platform = positional[0];
|
||||
const version = positional[1];
|
||||
const channel = positional[2] || 'stable';
|
||||
const edition = positional[3] || 'personal';
|
||||
const ossPrefix = positional[4] || `releases/${version}`;
|
||||
const releaseDirName = positional[5] || version;
|
||||
|
||||
if (!platform || !version) {
|
||||
console.error('用法: node build/scripts/publish-release.mjs <platform> <version> [channel] [edition] [ossPrefix] [releaseDirName]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ---------- 读取 update-info.json ----------
|
||||
|
||||
let updateInfo;
|
||||
|
||||
if (jsonOverride) {
|
||||
try {
|
||||
updateInfo = JSON.parse(jsonOverride);
|
||||
} catch {
|
||||
console.error('错误: --json 参数不是有效的 JSON');
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
const releaseDir = join(ROOT, 'release', releaseDirName);
|
||||
let infoFile;
|
||||
try {
|
||||
const all = readdirSync(releaseDir);
|
||||
infoFile = all.find((f) =>
|
||||
f.includes(platform) && f.endsWith('-update-info.json'),
|
||||
);
|
||||
if (!infoFile) {
|
||||
console.error(`未找到 ${platform} 的 update-info.json(在 ${releaseDir} 中)`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`错误: ${releaseDir} — ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
updateInfo = JSON.parse(readFileSync(join(releaseDir, infoFile), 'utf-8'));
|
||||
if (urlOverride) updateInfo.downloadUrl = urlOverride;
|
||||
}
|
||||
|
||||
// ---------- 发送到 API ----------
|
||||
|
||||
const apiBase = (process.env.UPDATE_API_URL || 'https://www.heixiu.com').replace(/\/+$/, '');
|
||||
const apiKey = process.env.UPDATE_API_KEY || '';
|
||||
|
||||
const payload = {
|
||||
platform,
|
||||
channel,
|
||||
edition,
|
||||
latestVersion: updateInfo.latestVersion || version,
|
||||
downloadUrl: updateInfo.downloadUrl,
|
||||
fileSize: updateInfo.fileSize,
|
||||
sha512: updateInfo.sha512,
|
||||
changelog: updateInfo.changelog,
|
||||
releaseDate: updateInfo.releaseDate,
|
||||
forceUpdate: updateInfo.forceUpdate || false,
|
||||
minimumVersion: updateInfo.minimumVersion || '0.0.0',
|
||||
// 增量更新
|
||||
...(updateInfo.blockMapUrl && { blockMapUrl: updateInfo.blockMapUrl }),
|
||||
...(updateInfo.blockMapSize != null && { blockMapSize: updateInfo.blockMapSize }),
|
||||
// 元数据
|
||||
_meta: {
|
||||
ossPrefix,
|
||||
releaseDirName,
|
||||
publishedAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
const endpoint = `${apiBase}/api/v1/releases`;
|
||||
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('📡 发布版本到 API');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log(`POST ${endpoint}`);
|
||||
console.log(`版本: ${version}`);
|
||||
console.log(`平台: ${platform}`);
|
||||
console.log(`渠道: ${channel}`);
|
||||
console.log(`版本类型: ${edition}`);
|
||||
console.log(`URL: ${payload.downloadUrl}\n`);
|
||||
|
||||
try {
|
||||
const response = await axios.post(endpoint, payload, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
console.log('✅ 发布成功');
|
||||
console.log(`响应: ${JSON.stringify(response.data, null, 2)}`);
|
||||
} catch (err) {
|
||||
const status = err.response?.status;
|
||||
const data = err.response?.data;
|
||||
|
||||
if (status) {
|
||||
console.error(`❌ 发布失败 HTTP ${status}: ${JSON.stringify(data)}`);
|
||||
} else {
|
||||
console.error(`❌ 请求失败: ${err.message}`);
|
||||
}
|
||||
|
||||
console.log('\n💡 手动备选 — 把以下 JSON 发给后端:');
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
242
build/scripts/upload-oss.mjs
Normal file
242
build/scripts/upload-oss.mjs
Normal file
@@ -0,0 +1,242 @@
|
||||
// ============================================================
|
||||
// 上传安装包 + blockmap 文件到阿里云 OSS
|
||||
//
|
||||
// 用法(由 build.mjs 调用):
|
||||
// node build/scripts/upload-oss.mjs <platform> <version> <channel> <ossPrefix> <releaseDirName>
|
||||
//
|
||||
// 也支持手动调用:
|
||||
// node build/scripts/upload-oss.mjs <platform> <version> [channel] [ossPrefix]
|
||||
//
|
||||
// 环境变量(使用 Node.js 直传时需要):
|
||||
// OSS_ENDPOINT — OSS endpoint(如 oss-cn-hangzhou.aliyuncs.com)
|
||||
// OSS_BUCKET — Bucket 名称
|
||||
// OSS_ACCESS_KEY_ID — AccessKey ID
|
||||
// OSS_ACCESS_KEY_SECRET — AccessKey Secret
|
||||
//
|
||||
// 不传 OSS 环境变量时 → 打印手动上传命令
|
||||
// ============================================================
|
||||
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createHmac } from 'node:crypto';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, '..', '..');
|
||||
|
||||
// ---------- 参数 ----------
|
||||
|
||||
const platform = process.argv[2];
|
||||
const version = process.argv[3];
|
||||
const channel = process.argv[4] || 'stable';
|
||||
const ossPrefix = process.argv[5] || `releases/${version}`;
|
||||
const releaseDirName = process.argv[6] || version;
|
||||
|
||||
if (!platform || !version) {
|
||||
console.error('用法: node build/scripts/upload-oss.mjs <platform> <version> [channel] [ossPrefix] [releaseDirName]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ---------- 查找安装包 ----------
|
||||
|
||||
const releaseDir = join(ROOT, 'release', releaseDirName);
|
||||
|
||||
let installerName;
|
||||
try {
|
||||
const all = readdirSync(releaseDir);
|
||||
const exts = platform === 'win32' ? ['.exe'] : platform === 'darwin' ? ['.dmg'] : ['.AppImage'];
|
||||
installerName = all.find((f) => exts.some((e) => f.endsWith(e)));
|
||||
if (!installerName) {
|
||||
console.error(`错误: ${releaseDir} — 未找到安装包`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`错误: ${releaseDir} — ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const installerPath = join(releaseDir, installerName);
|
||||
|
||||
// ---------- 查找 blockmap 文件 ----------
|
||||
|
||||
let blockmapName = null;
|
||||
let blockmapPath = null;
|
||||
try {
|
||||
const all = readdirSync(releaseDir);
|
||||
blockmapName = all.find((f) => f.endsWith('.blockmap'));
|
||||
if (blockmapName) {
|
||||
blockmapPath = join(releaseDir, blockmapName);
|
||||
}
|
||||
} catch {
|
||||
// blockmap 不存在不影响主流程
|
||||
}
|
||||
|
||||
// ---------- OSS 配置 ----------
|
||||
|
||||
const {
|
||||
OSS_ENDPOINT,
|
||||
OSS_BUCKET,
|
||||
OSS_ACCESS_KEY_ID,
|
||||
OSS_ACCESS_KEY_SECRET,
|
||||
} = process.env;
|
||||
|
||||
const installerObjectKey = `${ossPrefix}/${installerName}`;
|
||||
|
||||
// ============================================================
|
||||
// 模式 A:无 OSS 配置 → 打印手动命令
|
||||
// ============================================================
|
||||
|
||||
if (!OSS_ENDPOINT || !OSS_BUCKET || !OSS_ACCESS_KEY_ID) {
|
||||
const exampleBucket = OSS_BUCKET || '<bucket>';
|
||||
const exampleEndpoint = OSS_ENDPOINT || '<endpoint>';
|
||||
const downloadUrl = `https://${exampleBucket}.${exampleEndpoint}/${installerObjectKey}`;
|
||||
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('📦 上传安装包 + blockmap(手动)');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log(`渠道: ${channel}`);
|
||||
console.log(`OSS 路径: ${ossPrefix}/`);
|
||||
console.log(`文件: ${installerPath}`);
|
||||
console.log(`大小: ${(readFileSync(installerPath).length / 1024 / 1024).toFixed(2)} MB\n`);
|
||||
|
||||
if (blockmapPath) {
|
||||
console.log(`Blockmap: ${blockmapPath}`);
|
||||
console.log(`Blockmap 大小: ${(readFileSync(blockmapPath).length / 1024).toFixed(2)} KB\n`);
|
||||
}
|
||||
|
||||
console.log('方式 1 — ossutil CLI(推荐):');
|
||||
console.log(` ossutil cp "${installerPath}" oss://${exampleBucket}/${installerObjectKey}`);
|
||||
if (blockmapPath) {
|
||||
const blockmapKey = `${ossPrefix}/${blockmapName}`;
|
||||
console.log(` ossutil cp "${blockmapPath}" oss://${exampleBucket}/${blockmapKey}`);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
console.log('方式 2 — 阿里云控制台:');
|
||||
console.log(` 登录 OSS 控制台 → ${exampleBucket} → 上传 → 目标路径: ${installerObjectKey}`);
|
||||
if (blockmapPath) {
|
||||
console.log(` 同时上传 ${blockmapName} 到相同目录`);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
const blockmapUrl = blockmapPath
|
||||
? `https://${exampleBucket}.${exampleEndpoint}/${ossPrefix}/${blockmapName}`
|
||||
: '';
|
||||
|
||||
console.log('上传后执行以下命令,用实际 URL 重新生成 info:');
|
||||
if (blockmapUrl) {
|
||||
console.log(` node build/scripts/generate-update-info.mjs ${platform} ${version} ${channel} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`);
|
||||
console.log('');
|
||||
console.log('或直接发布到后端 API:');
|
||||
console.log(` node build/scripts/publish-release.mjs ${platform} ${version} ${channel} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`);
|
||||
} else {
|
||||
console.log(` node build/scripts/generate-update-info.mjs ${platform} ${version} ${channel} --url "${downloadUrl}"`);
|
||||
console.log('');
|
||||
console.log('或直接发布到后端 API:');
|
||||
console.log(` node build/scripts/publish-release.mjs ${platform} ${version} ${channel} --url "${downloadUrl}"`);
|
||||
}
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 模式 B:Node.js 直传 OSS(安装包 + blockmap)
|
||||
// ============================================================
|
||||
|
||||
const fileBuffer = readFileSync(installerPath);
|
||||
const contentType = platform === 'win32'
|
||||
? 'application/vnd.microsoft.portable-executable'
|
||||
: 'application/octet-stream';
|
||||
|
||||
// 上传安装包
|
||||
const date = new Date().toUTCString();
|
||||
const stringToSign = [
|
||||
'PUT', '', contentType, date, `/${OSS_BUCKET}/${installerObjectKey}`,
|
||||
].join('\n');
|
||||
const signature = createHmac('sha1', OSS_ACCESS_KEY_SECRET)
|
||||
.update(stringToSign)
|
||||
.digest('base64');
|
||||
const authHeader = `OSS ${OSS_ACCESS_KEY_ID}:${signature}`;
|
||||
const downloadUrl = `https://${OSS_BUCKET}.${OSS_ENDPOINT}/${installerObjectKey}`;
|
||||
|
||||
console.log(`上传中: ${installerName} (${(fileBuffer.length / 1024 / 1024).toFixed(2)} MB)`);
|
||||
console.log(`渠道: ${channel}`);
|
||||
console.log(`目标: oss://${OSS_BUCKET}/${installerObjectKey}`);
|
||||
|
||||
try {
|
||||
const response = await fetch(downloadUrl, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Content-Length': String(fileBuffer.length),
|
||||
'Date': date,
|
||||
'Authorization': authHeader,
|
||||
},
|
||||
body: fileBuffer,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
console.error(`❌ 安装包上传失败 HTTP ${response.status}: ${body}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✅ 安装包上传成功');
|
||||
console.log(`下载 URL: ${downloadUrl}`);
|
||||
} catch (err) {
|
||||
console.error(`❌ 安装包上传失败: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 上传 blockmap 文件
|
||||
let blockmapUrl = null;
|
||||
if (blockmapPath) {
|
||||
const blockmapBuffer = readFileSync(blockmapPath);
|
||||
const blockmapKey = `${ossPrefix}/${blockmapName}`;
|
||||
blockmapUrl = `https://${OSS_BUCKET}.${OSS_ENDPOINT}/${blockmapKey}`;
|
||||
|
||||
const bmDate = new Date().toUTCString();
|
||||
const bmStringToSign = [
|
||||
'PUT', '', 'application/octet-stream', bmDate, `/${OSS_BUCKET}/${blockmapKey}`,
|
||||
].join('\n');
|
||||
const bmSignature = createHmac('sha1', OSS_ACCESS_KEY_SECRET)
|
||||
.update(bmStringToSign)
|
||||
.digest('base64');
|
||||
|
||||
console.log(`\n上传 blockmap: ${blockmapName} (${(blockmapBuffer.length / 1024).toFixed(2)} KB)`);
|
||||
|
||||
try {
|
||||
const bmResponse = await fetch(blockmapUrl, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Length': String(blockmapBuffer.length),
|
||||
'Date': bmDate,
|
||||
'Authorization': `OSS ${OSS_ACCESS_KEY_ID}:${bmSignature}`,
|
||||
},
|
||||
body: blockmapBuffer,
|
||||
});
|
||||
|
||||
if (bmResponse.ok) {
|
||||
console.log('✅ Blockmap 上传成功');
|
||||
console.log(`Blockmap URL: ${blockmapUrl}`);
|
||||
} else {
|
||||
console.warn(`⚠️ Blockmap 上传失败(不影响主流程),HTTP ${bmResponse.status}`);
|
||||
blockmapUrl = null;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Blockmap 上传失败(不影响主流程): ${err.message}`);
|
||||
blockmapUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 后续步骤提示
|
||||
console.log('\n接下来 — 重新生成 update-info.json:');
|
||||
if (blockmapUrl) {
|
||||
console.log(` node build/scripts/generate-update-info.mjs ${platform} ${version} ${channel} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`);
|
||||
console.log('\n或直接发布到后端 API:');
|
||||
console.log(` node build/scripts/publish-release.mjs ${platform} ${version} ${channel} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`);
|
||||
} else {
|
||||
console.log(` node build/scripts/generate-update-info.mjs ${platform} ${version} ${channel} --url "${downloadUrl}"`);
|
||||
console.log('\n或直接发布到后端 API:');
|
||||
console.log(` node build/scripts/publish-release.mjs ${platform} ${version} ${channel} --url "${downloadUrl}"`);
|
||||
}
|
||||
Reference in New Issue
Block a user