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

92
electron/main/load-env.ts Normal file
View File

@@ -0,0 +1,92 @@
// 主进程 .env 加载器:.env.{mode} → .env.local → OS 环境变量(后者覆盖前者)
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
// ESM 中 __dirname 不可用,手动计算
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* 解析单个 .env 文件,将 key=value 写入 process.env。
* 已存在的 process.env 值不会被覆盖OS 环境变量优先)。
* 返回成功加载的变量数。
*/
function parseEnvFile(filePath: string, fileName: string): number {
try {
const content = readFileSync(filePath, 'utf-8');
let count = 0;
for (const line of content.split('\n')) {
const trimmed = line.trim();
// 跳过空行和注释
if (!trimmed || trimmed.startsWith('#')) continue;
// 解析 key=value
const eqIdx = trimmed.indexOf('=');
if (eqIdx === -1) continue;
const key = trimmed.slice(0, eqIdx).trim();
let value = trimmed.slice(eqIdx + 1).trim();
// 去除引号(支持 "value" 和 'value'
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
// 不覆盖已在 OS 环境变量中设置的值
if (key && !process.env[key]) {
process.env[key] = value;
count++;
}
}
if (count > 0) {
console.log(`[load-env] 从 ${fileName} 加载了 ${count} 个环境变量`);
}
return count;
} catch (err) {
// 文件不存在 — 静默跳过(打包后 .env.* 不在 ASAR 中,这是正常情况)
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
return 0;
}
// 其他错误(权限、编码等)— 警告但不中断
console.warn(`[load-env] 读取 ${fileName} 失败:`, (err as Error).message);
return 0;
}
}
/**
* 加载环境变量文件到 process.env。
* 开发模式加载 .env.development生产模式加载 .env.production
* 之后再加载 .env.local 作为覆盖层。
*/
export function loadEnvFile(): void {
// 项目根目录
// APP_ROOT 在 main.ts 中设置dist-electron/.. = 项目根)
// fallbackdist-electron/main/load-env.js → __dirname/../.. = 项目根
const projectRoot = process.env.APP_ROOT || join(__dirname, '..', '..');
// 确定当前模式
const mode =
!process.env.NODE_ENV || process.env.NODE_ENV === 'development'
? 'development'
: 'production';
console.log(
`[load-env] 模式: ${mode}, projectRoot: ${projectRoot}`,
);
// ① 基础环境文件(按模式选择)
const baseFile = `.env.${mode}`;
parseEnvFile(join(projectRoot, baseFile), baseFile);
// ② 本地覆盖文件(不进 git用于打包后连测试服务器等特殊场景
const localFile = '.env.local';
parseEnvFile(join(projectRoot, localFile), localFile);
}