feat: 重构更新流程 — 无更新不抛异常、用户决定下载、更新详情展示(md文件中的)

【核心重构】
  - getLatestVersion() 不再 throw Error,无更新时返回 { version: APP_VERSION }
  - checkForUpdates() 先自行预检 → 无更新直发 IPC,有更新才交 electron-updater
  - 移除 NO_UPDATE_MESSAGE 常量及所有字符串匹配拦截代码
  - HeiXiuProvider 增加 500ms TTL 缓存,避免预检+electron-updater 重复请求 API

  【用户体验】
  - 检查到新版本后不再自动下载,展示更新内容供用户决定
  - 新增"下载更新"按钮(与"安装更新"分离为两步操作)
  - 设置页新增"查看详情"弹窗,Markdown 格式化渲染更新日志
  - available / downloading / downloaded 三个状态 UI 独立展示

  【Bug 修复】
  - 测试服务器版本排序:字符串序 → 语义版本序(_version_key),0.0.10 正确 > 0.0.9
  - 版本比较:!= → <(_version_key),防止高版本误判为有更新
  - get_best_release fallback 同样改为语义版本排序
  - Windows cmd set 命令尾部空格 → Invalid URL(.trim() 修复)
  - electron-updater 'error' 事件中拦截 NO_UPDATE_MESSAGE → UPDATE_NOT_AVAILABLE

  【文档更新】
  - CHANGELOG.md、PROJECT.md、UpdateA.md 同步更新架构图和流程说明
This commit is contained in:
2026-06-04 18:31:52 +08:00
parent b55c2fdd2e
commit 6204c14b88
37 changed files with 1908 additions and 648 deletions

View File

@@ -1,5 +1,5 @@
// ============================================================
// 上传安装包到阿里云 OSS
// 上传安装包 + blockmap 文件到阿里云 OSS
//
// 用法:
// node scripts/upload-oss.mjs <platform> <edition> [version]
@@ -54,7 +54,10 @@ 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} — 未找到安装包`);
if (!installerName) {
console.error(`错误: ${releaseDir} — 未找到安装包`);
process.exit(1);
}
} catch (err) {
console.error(`错误: ${releaseDir}${err.message}`);
process.exit(1);
@@ -62,6 +65,20 @@ try {
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 {
@@ -74,56 +91,78 @@ const {
const basePath = (process.env.OSS_BASE_PATH || 'releases').replace(/^\/+|\/+$/g, '');
const objectKey = `${basePath}/${version}/${installerName}`;
// ---------- 模式 A无 OSS 配置 → 打印手动命令 ----------
// ============================================================
// 模式 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}/${objectKey}`;
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('📦 上传安装包(手动)');
console.log('📦 上传安装包 + blockmap(手动)');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
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}/${objectKey}\n`);
console.log(` ossutil cp "${installerPath}" oss://${exampleBucket}/${objectKey}`);
if (blockmapPath) {
const blockmapKey = `${basePath}/${version}/${blockmapName}`;
console.log(` ossutil cp "${blockmapPath}" oss://${exampleBucket}/${blockmapKey}`);
}
console.log('');
console.log('方式 2 — 阿里云控制台:');
console.log(` 登录 OSS 控制台 → ${exampleBucket} → 上传 → 目标路径: ${objectKey}\n`);
console.log(` 登录 OSS 控制台 → ${exampleBucket} → 上传 → 目标路径: ${objectKey}`);
if (blockmapPath) {
console.log(` 同时上传 ${blockmapName} 到相同目录`);
}
console.log('');
const blockmapUrl = blockmapPath
? `https://${exampleBucket}.${exampleEndpoint}/${basePath}/${version}/${blockmapName}`
: '';
console.log('上传后执行以下命令,用实际 URL 重新生成 info');
console.log(` node scripts/generate-update-info.mjs ${platform} ${edition} ${version} --url "${downloadUrl}"\n`);
console.log('或直接发布到后端 API');
console.log(` node scripts/publish-release.mjs ${platform} ${edition} ${version} --url "${downloadUrl}"`);
if (blockmapUrl) {
console.log(` node scripts/generate-update-info.mjs ${platform} ${edition} ${version} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`);
console.log('');
console.log('或直接发布到后端 API');
console.log(` node scripts/publish-release.mjs ${platform} ${edition} ${version} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`);
} else {
console.log(` node scripts/generate-update-info.mjs ${platform} ${edition} ${version} --url "${downloadUrl}"`);
console.log('');
console.log('或直接发布到后端 API');
console.log(` node scripts/publish-release.mjs ${platform} ${edition} ${version} --url "${downloadUrl}"`);
}
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
process.exit(0);
}
// ---------- 模式 BNode.js 直传 OSS ----------
// ============================================================
// 模式 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();
// 阿里云 OSS Authorization 签名
const stringToSign = [
'PUT',
'',
contentType,
date,
`/${OSS_BUCKET}/${objectKey}`,
'PUT', '', contentType, date, `/${OSS_BUCKET}/${objectKey}`,
].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}/${objectKey}`;
@@ -141,21 +180,69 @@ try {
},
body: fileBuffer,
});
if (response.ok) {
console.log(`✅ 上传成功`);
console.log(`下载 URL: ${downloadUrl}\n`);
console.log('接下来:');
console.log(` # 重新生成 update-info.json`);
console.log(` node scripts/generate-update-info.mjs ${platform} ${edition} ${version} --url "${downloadUrl}"`);
console.log(`\n # 或直接发布到后端:`);
console.log(` node scripts/publish-release.mjs ${platform} ${edition} ${version} --url "${downloadUrl}"`);
} else {
if (!response.ok) {
const body = await response.text();
console.error(`❌ 上传失败 HTTP ${response.status}: ${body}`);
console.error(`安装包上传失败 HTTP ${response.status}: ${body}`);
process.exit(1);
}
console.log(`✅ 安装包上传成功`);
console.log(`下载 URL: ${downloadUrl}`);
} catch (err) {
console.error(`❌ 上传失败: ${err.message}`);
console.error(`安装包上传失败: ${err.message}`);
process.exit(1);
}
// 上传 blockmap 文件(如果存在)
let blockmapUrl = null;
if (blockmapPath) {
const blockmapBuffer = readFileSync(blockmapPath);
const blockmapKey = `${basePath}/${version}/${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 scripts/generate-update-info.mjs ${platform} ${edition} ${version} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`);
console.log('\n或直接发布到后端 API');
console.log(` node scripts/publish-release.mjs ${platform} ${edition} ${version} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`);
} else {
console.log(` node scripts/generate-update-info.mjs ${platform} ${edition} ${version} --url "${downloadUrl}"`);
console.log('\n或直接发布到后端 API');
console.log(` node scripts/publish-release.mjs ${platform} ${edition} ${version} --url "${downloadUrl}"`);
}