Files
ele-HeiXiu/electron/main.ts
YoungestSongMo 337d6c1205 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>
2026-06-29 18:41:07 +08:00

355 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {app, BrowserWindow, ipcMain, dialog, session, protocol, net} from 'electron';
import {createRequire} from 'node:module';
import {fileURLToPath} from 'node:url';
import path from 'node:path';
import {getPlatform, isMacOS} from './main/utils/platform';
import {getWindowIconPath} from './main/utils/logo';
import {buildWindowTitle} from '../shared/constants/app';
import {loadEnvFile} from './main/load-env';
import {initUpdater, registerUpdateIpcHandlers} from './main/updater';
import {setupAppMenu} from './main/menu';
import {initLogger, flushLogger, logger, setLogDirResolver} from './main/logger';
import {registerLogIpcHandlers} from './main/ipc/log-ipc';
import {registerSafeStorageIpcHandlers} from './main/ipc/safe-storage-ipc';
import {registerStorageIpcHandlers, setDataDirResolver} from './main/ipc/storage-ipc';
import {registerCanvasIpcHandlers} from './main/ipc/canvas-ipc';
import {BIDIRECTIONAL, RENDERER_TO_MAIN, MAIN_TO_RENDERER} from '../shared/constants/ipc-channels';
createRequire(import.meta.url);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
process.env.APP_ROOT = path.join(__dirname, '..');
// 加载 .env 文件中的环境变量到 process.env
// 必须在任何读取 process.env.UPDATE_API_URL 的模块之前调用
loadEnvFile();
export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'];
export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron');
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist');
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
? path.join(process.env.APP_ROOT, 'public')
: RENDERER_DIST;
// ---------- 平台 & 版本信息 ----------
const currentPlatform = getPlatform();
/** 当前版控后缀(登录后通过 IPC 更新) */
let currentEdition: string | null = null;
/** 基础标题(无版控后缀),登录后通过 IPC 追加 */
const WINDOW_TITLE = buildWindowTitle(currentPlatform);
// ============================================================
// 全局未捕获异常(主进程兜底记录)
// ============================================================
process.on('uncaughtException', (error) => {
try {
logger.error('app', 'Uncaught exception (main process)', error);
} catch {
/* logger 自身异常 — 最后防线 */
}
});
process.on('unhandledRejection', (reason) => {
try {
const error = reason instanceof Error ? reason : new Error(String(reason));
logger.error('app', 'Unhandled rejection (main process)', error);
} catch {
/* logger 自身异常 — 最后防线 */
}
});
// ============================================================
// 窗口引用
// ============================================================
let mainWindow: BrowserWindow | null = null;
let canvasWindow: BrowserWindow | null = null;
// ============================================================
// 主窗口
// ============================================================
function createMainWindow(): BrowserWindow {
const iconPath = getWindowIconPath(app.getAppPath());
mainWindow = new BrowserWindow({
title: WINDOW_TITLE,
icon: iconPath,
width: 1280,
height: 800,
minWidth: 960,
minHeight: 600,
show: false,
...(isMacOS() ? {titleBarStyle: 'hiddenInset' as const} : {}),
webPreferences: {
preload: path.join(__dirname, 'preload.mjs'),
contextIsolation: true,
nodeIntegration: false,
},
});
mainWindow.setTitle(WINDOW_TITLE);
if (VITE_DEV_SERVER_URL) {
mainWindow.webContents.openDevTools();
}
// macOS 全屏事件
if (isMacOS()) {
mainWindow.on('enter-full-screen', () =>
mainWindow?.webContents.send('window-fullscreen-changed', true),
);
mainWindow.on('leave-full-screen', () =>
mainWindow?.webContents.send('window-fullscreen-changed', false),
);
}
mainWindow.webContents.on('did-finish-load', () => {
mainWindow?.webContents.send('main-process-message', new Date().toLocaleString());
mainWindow?.webContents.send('platform-info', {
platform: currentPlatform,
edition: currentEdition,
});
});
// ready-to-show 后显示,避免白屏闪烁
mainWindow.once('ready-to-show', () => {
mainWindow?.show();
});
if (VITE_DEV_SERVER_URL) {
mainWindow.loadURL(VITE_DEV_SERVER_URL);
} else {
mainWindow.loadFile(path.join(RENDERER_DIST, 'index.html'));
}
return mainWindow;
}
// ============================================================
// 画布窗口
// ============================================================
function createCanvasWindow(): BrowserWindow {
const iconPath = getWindowIconPath(app.getAppPath());
logger.info('canvas-window', '创建画布窗口', {
isDev: !!VITE_DEV_SERVER_URL,
url: VITE_DEV_SERVER_URL
? VITE_DEV_SERVER_URL + '#/canvas'
: path.join(RENDERER_DIST, 'index.html') + '#/canvas',
});
canvasWindow = new BrowserWindow({
title: '无限画布 - HeiXiu',
icon: iconPath,
width: 1400,
height: 900,
minWidth: 800,
minHeight: 600,
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.mjs'),
contextIsolation: true,
nodeIntegration: false,
},
});
if (VITE_DEV_SERVER_URL) {
canvasWindow.loadURL(VITE_DEV_SERVER_URL + '#/canvas');
canvasWindow.webContents.openDevTools();
logger.info('canvas-window', '画布窗口 DevTools 已自动打开(开发模式)');
} else {
canvasWindow.loadFile(path.join(RENDERER_DIST, 'index.html'), {
hash: '/canvas',
});
}
// 禁用浏览器默认页面缩放Ctrl/Cmd+滚轮 和 触控板捏合)。
// 画布使用 CSS transform 实现独立缩放,不允许 Chromium 页面级缩放干扰。
// before-input-event 在 Chromium 处理输入之前触发,比 JS event listener 更早。
canvasWindow.webContents.on('before-input-event', (event, input) => {
if (input.type === 'mouseWheel' && (input.control || input.meta)) {
event.preventDefault();
}
});
canvasWindow.once('ready-to-show', () => {
logger.info('canvas-window', '画布窗口 ready-to-show即将显示');
canvasWindow?.show();
});
canvasWindow.on('closed', () => {
logger.info('canvas-window', '画布窗口已关闭');
canvasWindow = null;
});
return canvasWindow;
}
// ============================================================
// 自定义协议local-media — 安全加载本地文件
// 必须在 app.whenReady 之前注册为特权 scheme
// ============================================================
protocol.registerSchemesAsPrivileged([
{
scheme: 'local-media',
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
bypassCSP: true,
stream: true,
},
},
]);
// ============================================================
// 应用生命周期
// ============================================================
app.on('window-all-closed', () => {
if (!isMacOS()) {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow();
}
});
app.on('before-quit', () => {
flushLogger();
});
app.whenReady().then(() => {
// ── 存储路径配置 ──
// 修改以下两行即可将日志/数据存储到任意位置
setLogDirResolver(() => path.join(app.getPath('userData'), 'logs'));
setDataDirResolver(() => path.join(app.getPath('userData'), 'heixiu-data'));
initLogger();
registerLogIpcHandlers();
registerSafeStorageIpcHandlers();
registerStorageIpcHandlers();
registerCanvasIpcHandlers();
protocol.handle('local-media', (request) => {
const urlObj = new URL(request.url);
let filePath: string;
if (urlObj.hostname) {
// UNC 路径local-media://server/share/path → \\server\share\path
filePath = '\\\\' + urlObj.hostname + decodeURIComponent(urlObj.pathname).replace(/\//g, '\\');
} else {
// 本地路径local-media:///C:/path/to/file → C:/path/to/file
filePath = decodeURIComponent(urlObj.pathname.slice(1));
}
const fwd = filePath.replace(/\\/g, '/');
let fileUrl: string;
if (fwd.startsWith('//')) {
fileUrl = 'file:' + fwd;
} else {
fileUrl = 'file:///' + fwd;
}
// 只转发 Range 头,确保音视频 seek 时返回 206 Partial Content。
// ⚠️ 不能转发所有 headersOrigin/Referer/Sec-Fetch-* 等),
// 否则会干扰 Chromium 内部的 file:// 协议处理,导致请求失败。
const rangeHeader = request.headers.get('Range');
if (rangeHeader) {
return net.fetch(fileUrl, {headers: {Range: rangeHeader}});
}
return net.fetch(fileUrl);
});
setupAppMenu();
app.setName(WINDOW_TITLE);
registerUpdateIpcHandlers();
const fakeUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36';
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
const url = details.url;
if (url.includes("rh-images-1252422369.cos.ap-beijing.myqcloud.com")) {
details.requestHeaders['User-Agent'] = fakeUserAgent;
}
callback({
requestHeaders: details.requestHeaders
});
});
// 文件对话框 IPC 处理(供渲染进程选择文件夹)
ipcMain.handle(BIDIRECTIONAL.FILE_DIALOG, async (_event, options: Electron.OpenDialogOptions) => {
try {
const parentWindow = mainWindow && !mainWindow.isDestroyed() ? mainWindow : canvasWindow;
if (!parentWindow) return {canceled: true, filePaths: []};
return dialog.showOpenDialog(parentWindow, {
title: options?.title || '选择文件夹',
defaultPath: options?.defaultPath || app.getPath('home'),
properties: options?.properties || ['openDirectory'],
});
} catch (err) {
logger.warn('ipc', '文件对话框打开失败', err instanceof Error ? err : undefined);
return {canceled: true, filePaths: []};
}
});
// 窗口标题版控后缀(渲染进程登录/登出后调用)
ipcMain.on(RENDERER_TO_MAIN.SET_WINDOW_TITLE, (_event, edition: string | null) => {
try {
currentEdition = edition;
if (!mainWindow) return;
const title = edition
? buildWindowTitle(currentPlatform, undefined, edition as 'personal' | 'enterprise')
: buildWindowTitle(currentPlatform);
mainWindow.setTitle(title);
} catch (err) {
logger.warn('ipc', '设置窗口标题失败', err instanceof Error ? err : undefined);
}
});
// 打开无限画布窗口(渲染进程请求 → 主进程创建新 BrowserWindow
ipcMain.on(RENDERER_TO_MAIN.OPEN_CANVAS_WINDOW, (_event, payload?: { projectPath?: string }) => {
try {
const existingAlive = canvasWindow && !canvasWindow.isDestroyed();
logger.info('canvas-window', '收到 OPEN_CANVAS_WINDOW IPC', {
projectPath: payload?.projectPath || '(未指定)',
existingAlive,
});
if (existingAlive) {
logger.info('canvas-window', '画布窗口已存在,聚焦现有窗口');
canvasWindow?.focus();
} else {
logger.info('canvas-window', '画布窗口不存在或已销毁,创建新窗口');
canvasWindow = createCanvasWindow();
}
// 画布页面加载完成后,发送初始化参数
if (payload?.projectPath) {
const readyHandler = () => {
logger.info('canvas-window', '画布页面 did-finish-load发送 CANVAS_READY', {
projectPath: payload.projectPath,
});
canvasWindow?.webContents.send(MAIN_TO_RENDERER.CANVAS_READY, {
projectPath: payload.projectPath,
});
};
canvasWindow?.webContents.on('did-finish-load', readyHandler);
}
} catch (err) {
logger.warn('canvas-window', '打开画布窗口失败', err instanceof Error ? err : undefined);
}
});
// 始终打开主窗口
// 登录/注册由渲染进程内的 Modal 弹层处理,不再新开窗口
const win = createMainWindow();
initUpdater(win);
});