feat: 初始化船长·HeiXiu 桌面应用项目(错误是svg的问题)

Electron + React 19 + TypeScript + Ant Design 6 + Tailwind CSS 4

  【核心架构】
  - Electron 主进程(窗口管理、平台检测、图标适配)
  - Vite 构建链(双产物 dist/ + dist-electron/)
  - 共享层 shared/(类型、常量、工具函数)
  - IPC 通信框架 + 安全守卫

  【主题系统】
  - 蓝紫渐变主题(亮色/暗色双模式)
  - Ant Design ConfigProvider 适配(lightAlgorithm / darkAlgorithm)
  - Tailwind CSS 4 @theme 指令 + CSS 变量
  - 设计令牌三处同步(tokens.ts / globals.css / antd-theme.ts)

  【导航栏】
  - 自定义 H5 导航栏替代系统菜单
  - 个人版/企业版双布局
  - 未登录拦截 + 事件总线驱动登录弹窗

  【登录注册】
  - Modal 弹层(Portal)+ Tabs 切换
  - 登录/注册表单动态字段渲染
  - 记住密码 / 自动登录 / 用户协议

  【更新系统】
  - 自定义 API 版本检查(替代 electron-updater generic provider)
  - 手动下载 + SHA512 校验 + spawn NSIS 安装
  - 渲染进程更新状态机 Hook(useUpdater)
  - 检查更新按钮 + 下载进度展示

  【构建工具链】
  - build.mjs 构建总管(--win/--mac/--linux --personal/--enterprise --build/--clean/--sign/--upload/--publish)
  - scripts/ 自动化脚本(clean / generate-update-info / upload-oss / publish-release)
  - electron-builder NSIS 安装器(可选路径、多版本打包)
  - update-info.json 自动生成(fileSize/sha512/changelog/releaseDate)

  【代码质量】
  - ESLint + TypeScript strict + Prettier
  - husky + lint-staged(提交前自动检查)
  - commitlint(规范提交信息)
  - Playwright E2E + Vitest 单元测试框架
  - rollup-plugin-visualizer 打包体积分析

  【文档】
  - README.md(快速开始 + 目录结构 + 命令速查)
  - UpdateA.md(发布手册:API/签名/发版流程/数据库)
  - CHANGELOG.md(更新日志)
  - .env.example(构建环境变量模板)
This commit is contained in:
2026-06-03 15:00:51 +08:00
commit 767bb8b297
95 changed files with 20814 additions and 0 deletions

316
electron/main/updater.ts Normal file
View File

@@ -0,0 +1,316 @@
// ============================================================
// 自动更新模块 — 自定义 API 版本检查 + OSS 文件下载
//
// 架构:
// 客户端 → GET /api/v1/update/check → ECS (API) → 返回 JSON
// 客户端 → GET <downloadUrl> → OSS → 下载安装包
// 下载完成 → SHA512 校验 → spawn 安装器 → app.quit()
//
// 与旧版 (electron-updater generic provider) 的区别:
// - 版本检查走自定义 API后端可做灰度/强更/统计
// - 不再依赖 latest.yml 静态文件
// - 安装包仍托管于 OSS下载地址由 API 动态返回
//
// 环境变量:
// UPDATE_API_URL — 版本检查 API 基地址(默认 https://www.heixiu.com
// ============================================================
import { BrowserWindow, net, app, ipcMain } from 'electron';
import { createWriteStream } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
import { spawn } from 'node:child_process';
import { unlink } from 'node:fs/promises';
import type { UpdateCheckResult } from '../../shared/types';
import { APP_VERSION } from '../../shared/constants/version';
// ---------- IPC 通道(对渲染进程暴露,与旧版兼容)----------
export const UPDATE_CHANNELS = {
UPDATE_AVAILABLE: 'update-available',
UPDATE_PROGRESS: 'update-progress',
UPDATE_DOWNLOADED: 'update-downloaded',
UPDATE_ERROR: 'update-error',
UPDATE_NOT_AVAILABLE: 'update-not-available',
INSTALL_UPDATE: 'install-update',
CHECK_FOR_UPDATE: 'check-for-update',
} as const;
// ---------- 配置 ----------
function getApiBaseUrl(): string {
return process.env.UPDATE_API_URL || 'https://www.heixiu.com';
}
// ---------- 状态 ----------
let updateWindow: BrowserWindow | null = null;
let updateChecked = false;
/** 当前正在下载的临时文件路径(用于安装后清理) */
let pendingInstallerPath: string | null = null;
// ============================================================
// 初始化
// ============================================================
export function initUpdater(mainWindow: BrowserWindow): void {
updateWindow = mainWindow;
if (import.meta.env.DEV) {
console.log('[Updater] 开发模式跳过自动检查更新。API:', getApiBaseUrl());
return;
}
// 启动 5 秒后静默检查
setTimeout(() => {
checkForUpdates();
}, 5000);
}
// ============================================================
// 版本检查(调用 API
// ============================================================
async function fetchUpdateInfo(): Promise<UpdateCheckResult | null> {
const platform = process.platform; // 'win32' | 'darwin' | 'linux'
const edition = process.env.EDITION || 'personal';
const apiUrl = `${getApiBaseUrl()}/api/v1/update/check?platform=${platform}&version=${APP_VERSION}&edition=${edition}`;
console.log('[Updater] 请求版本检查 API:', apiUrl);
return new Promise((resolve, reject) => {
const req = net.request({ method: 'GET', url: apiUrl });
req.on('response', (response) => {
let body = '';
response.on('data', (chunk) => (body += chunk.toString()));
response.on('end', () => {
try {
const json = JSON.parse(body);
// 兼容两种 API 响应格式:
// 格式 A: { code: 0, data: UpdateCheckResult }
// 格式 B: UpdateCheckResult 直接返回
const data: UpdateCheckResult = json.data ?? json;
if (!data.hasUpdate) {
console.log('[Updater] 当前已是最新版本');
return resolve(null);
}
console.log('[Updater] 发现新版本:', data.latestVersion);
resolve(data);
} catch (err) {
reject(new Error(`API 响应解析失败: ${(err as Error).message}`));
}
});
});
req.on('error', (err) => reject(new Error(`API 请求失败: ${err.message}`)));
req.end();
});
}
// ============================================================
// 下载安装包
// ============================================================
async function downloadInstaller(info: UpdateCheckResult): Promise<string> {
const ext =
process.platform === 'win32' ? '.exe' : process.platform === 'darwin' ? '.dmg' : '.AppImage';
const tempPath = join(tmpdir(), `HeiXiu-${info.latestVersion}-update${ext}`);
console.log('[Updater] 开始下载:', info.downloadUrl);
console.log('[Updater] 临时文件:', tempPath);
return new Promise((resolve, reject) => {
const req = net.request({ method: 'GET', url: info.downloadUrl });
const fileStream = createWriteStream(tempPath);
const hash = createHash('sha512');
const totalSize = info.fileSize || 0;
let downloadedSize = 0;
let lastReportTime = 0;
req.on('response', (response) => {
const statusCode = response.statusCode;
if (statusCode !== 200) {
fileStream.close();
unlink(tempPath).catch(() => {});
return reject(new Error(`下载失败HTTP ${statusCode}`));
}
response.on('data', (chunk: Buffer) => {
fileStream.write(chunk);
hash.update(chunk);
downloadedSize += chunk.length;
// 限频:最多每 250ms 推送一次进度
const now = Date.now();
if (totalSize > 0 && now - lastReportTime >= 250) {
lastReportTime = now;
const percent = Math.round((downloadedSize / totalSize) * 100);
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_PROGRESS, {
percent,
bytesPerSecond: 0, // 简化处理,不计算实时速度
transferred: downloadedSize,
total: totalSize,
});
}
});
response.on('end', () => {
fileStream.end();
fileStream.on('finish', () => {
// SHA512 校验
const actualSha512 = hash.digest('base64');
if (info.sha512 && actualSha512 !== info.sha512) {
unlink(tempPath).catch(() => {});
return reject(
new Error(
`SHA512 校验失败\n期望: ${info.sha512.slice(0, 16)}...\n实际: ${actualSha512.slice(0, 16)}...`,
),
);
}
console.log('[Updater] 下载完成SHA512 校验通过');
pendingInstallerPath = tempPath;
resolve(tempPath);
});
});
response.on('error', (err) => {
fileStream.close();
unlink(tempPath).catch(() => {});
reject(new Error(`下载中断: ${err.message}`));
});
});
req.on('error', (err) => {
fileStream.close();
unlink(tempPath).catch(() => {});
reject(new Error(`下载请求失败: ${err.message}`));
});
req.end();
});
}
// ============================================================
// 操作方法
// ============================================================
export async function checkForUpdates(): Promise<void> {
if (updateChecked) {
console.log('[Updater] 已检查过更新,跳过');
return;
}
if (import.meta.env.DEV) {
console.log('[Updater] 开发模式,跳过更新检查');
return;
}
try {
updateChecked = true;
const info = await fetchUpdateInfo();
if (!info) {
// 无更新,不发通知(静默)
return;
}
// 通知渲染进程:有新版本
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_AVAILABLE, {
version: info.latestVersion,
releaseDate: info.releaseDate,
releaseNotes: info.changelog,
forceUpdate: info.forceUpdate,
});
// 自动开始下载
await downloadInstaller(info);
// 下载完成 → 通知渲染进程
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_DOWNLOADED);
} catch (error) {
console.error('[Updater] 更新流程失败:', (error as Error).message);
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_ERROR, {
message: (error as Error).message || '更新检查失败',
});
updateChecked = false;
}
}
export async function checkForUpdatesManual(): Promise<void> {
try {
updateChecked = false;
const info = await fetchUpdateInfo();
if (!info) {
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_NOT_AVAILABLE);
return;
}
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_AVAILABLE, {
version: info.latestVersion,
releaseDate: info.releaseDate,
releaseNotes: info.changelog,
forceUpdate: info.forceUpdate,
});
await downloadInstaller(info);
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_DOWNLOADED);
} catch (error) {
console.error('[Updater] 手动更新失败:', (error as Error).message);
updateWindow?.webContents.send(UPDATE_CHANNELS.UPDATE_ERROR, {
message: (error as Error).message || '检查更新失败',
});
}
}
export function installUpdateNow(): void {
if (!pendingInstallerPath) {
console.error('[Updater] 没有待安装的更新文件');
return;
}
console.log('[Updater] 开始安装更新:', pendingInstallerPath);
if (process.platform === 'win32') {
// Windows: NSIS 安装器,/S 静默安装
spawn(pendingInstallerPath, ['/S'], {
detached: true,
stdio: 'ignore',
});
} else if (process.platform === 'darwin') {
// macOS: 挂载 DMG
spawn('hdiutil', ['attach', pendingInstallerPath], {
detached: true,
stdio: 'ignore',
});
} else {
// Linux: AppImagechmod +x 后执行(通常需要手动替换)
console.log('[Updater] Linux 更新请手动替换 AppImage 文件');
}
// 退出应用,让安装器接管
setImmediate(() => {
app.quit();
});
}
// ============================================================
// IPC 注册
// ============================================================
export function registerUpdateIpcHandlers(): void {
ipcMain.handle(UPDATE_CHANNELS.CHECK_FOR_UPDATE, async () => {
await checkForUpdatesManual();
});
ipcMain.on(UPDATE_CHANNELS.INSTALL_UPDATE, () => {
installUpdateNow();
});
}