三栏布局 - ResizeObserver 等比例分配替代固定默认值恢复 - 修复拖拽+窗口调整竞态卡死(needsRecalcRef 延迟补算) - containerRef.current 动态读取 DOM 替代闭包 el Seedance2Content - 从占位文本重构为内容编排控件(文本+图片/视频/音频上传) - 根据 spec.image_files/video_files/audio_files 动态渲染上传区域 - 值 JSON 序列化 → 提交时转 ContentItem[](text/image_url/video_url/audio_url) 图片上传兼容视频文件 - buildAccept 根据 file_filter 区分 image/video 前缀 - createMediaBeforeUpload 动态放行 video/* MIME - 公共模块 media-upload-utils.ts 本地存储基建 - sql.js (WASM SQLite) + safeStorage 加密持久化 - 8个IPC文件操作通道 + resolveSafe 路径穿越防护 - <userData>/heixiu-data/ 沙箱 + JWT用户隔离 - 7表DDL + 游标分页 + LRU媒体淘汰 其他 - 修复 errorMessageRef 渲染期写入警告 - 媒体预览回退直接URL(<img>无CORS限制)+ output_type类型补充 - CSP wasm-unsafe-eval + https: CDN媒体源
182 lines
6.0 KiB
TypeScript
182 lines
6.0 KiB
TypeScript
import {app, BrowserWindow, ipcMain, dialog, session} 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, parseEdition} 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} from './main/logger';
|
|
import {registerLogIpcHandlers} from './main/log-ipc';
|
|
import {registerSafeStorageIpcHandlers} from './main/safe-storage-ipc';
|
|
import {registerStorageIpcHandlers} from './main/storage-ipc';
|
|
import {BIDIRECTIONAL} 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();
|
|
const currentEdition = parseEdition(process.env.EDITION);
|
|
const WINDOW_TITLE = buildWindowTitle(currentPlatform, currentEdition);
|
|
|
|
// ============================================================
|
|
// 全局未捕获异常(主进程兜底记录)
|
|
// ============================================================
|
|
|
|
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;
|
|
|
|
// ============================================================
|
|
// 主窗口
|
|
// ============================================================
|
|
|
|
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;
|
|
}
|
|
|
|
// ============================================================
|
|
// 应用生命周期
|
|
// ============================================================
|
|
|
|
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(() => {
|
|
initLogger();
|
|
registerLogIpcHandlers();
|
|
registerSafeStorageIpcHandlers();
|
|
registerStorageIpcHandlers();
|
|
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) => {
|
|
if (!mainWindow) return {canceled: true, filePaths: []};
|
|
return dialog.showOpenDialog(mainWindow, {
|
|
title: options?.title || '选择文件夹',
|
|
defaultPath: options?.defaultPath || app.getPath('home'),
|
|
properties: options?.properties || ['openDirectory'],
|
|
});
|
|
});
|
|
|
|
// 始终打开主窗口
|
|
// 登录/注册由渲染进程内的 Modal 弹层处理,不再新开窗口
|
|
const win = createMainWindow();
|
|
initUpdater(win);
|
|
});
|