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:
2026-06-29 18:41:07 +08:00
parent e0d91335c7
commit 337d6c1205
187 changed files with 5252 additions and 1702 deletions

View 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
View 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清理完成。');

View 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));

View 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('✅ 构建前校验全部通过');

View 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('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');

View 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);
}
// ============================================================
// 模式 BNode.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}"`);
}