Files
WYF-koubo/electron/main/index.ts
T
2026-06-19 18:45:55 +08:00

1157 lines
47 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, desktopCapturer, ipcMain, session, shell, protocol, globalShortcut } from "electron";
import { optimizer } from "@electron-toolkit/utils";
import path from "node:path"
import fs from "node:fs";
import { execSync, exec as execCallback } from "child_process";
import os from "node:os";
import { promisify } from "node:util";
/** process.js 必须位于非依赖项的顶部 */
import { isDummy } from "../lib/process";
import { AppEnv, AppRuntime } from "../mapi/env";
import { MAPI } from "../mapi/main";
import { initSystemTemplates } from "../mapi/db/initSystemTemplates";
import { initBuiltinAvatars } from "../mapi/db/initBuiltinAvatars";
import { initAppConfig, initVoicePreCache } from "../mapi/config/initAppConfig";
import { ConfigMain } from "../mapi/config/main";
import { setUpdaterWindow, default as updaterIndex } from "../mapi/updater/index";
import { WindowConfig } from "../config/window";
import { AppConfig } from "../../src/config";
import Log from "../mapi/log/main";
import { ConfigMenu } from "../config/menu";
import { ConfigLang, t } from "../config/lang";
import { ConfigContextMenu } from "../config/contextMenu";
import { preloadDefault, rendererLoadPath } from "../lib/env-main";
import { Page } from "../page";
import { ConfigTray } from "../config/tray";
import { icnsLogoPath, icoLogoPath, logoPath } from "../config/icon";
import { isDev, isMac, isPackaged, shouldEnableDevTools } from "../lib/env";
import { executeHooks } from "../lib/hooks";
import { DevToolsManager } from "../lib/devtools";
import { AppsMain } from "../mapi/app/main";
import { ServerMain } from "../mapi/server/main";
import CryptoManager from "../lib/crypto";
import AntiDebugManager from "../lib/antiDebug";
import { getCommonResourcePath, getModelPath } from "../lib/resource-path";
import { ensurePythonSetup, getMacOSPythonPath } from "../lib/python-setup";
import Store from "../lib/store";
import { checkResourceBundles } from "../mapi/resourceManager";
import { fetchElectronSystemConfig } from "../mapi/systemConfig";
// 窗口状态保存路径
const windowStateFile = () => path.join(app.getPath("userData"), "window-state.json");
// 加载窗口状态
function loadWindowState() {
try {
if (fs.existsSync(windowStateFile())) {
const data = fs.readFileSync(windowStateFile(), "utf-8");
return JSON.parse(data);
}
} catch (error) {
Log.error("加载窗口状态失败:", error);
}
return null;
}
// 保存窗口状态
function saveWindowState(window: BrowserWindow) {
try {
const bounds = window.getBounds();
const state = {
width: bounds.width,
height: bounds.height,
x: bounds.x,
y: bounds.y,
isMaximized: window.isMaximized(),
};
fs.writeFileSync(windowStateFile(), JSON.stringify(state, null, 2));
} catch (error) {
Log.error("保存窗口状态失败:", error);
}
}
const isDummyNew = isDummy;
// 临时屏蔽防破解,用于调试
// if (process.env["ELECTRON_ENV_PROD"]) {
// DevToolsManager.setEnable(false);
// }
process.on("uncaughtException", reason => {
let error: any = reason;
if (error instanceof Error) {
error = [error.message, error.stack].join("\n");
}
Log.error("UncaughtException", error);
});
process.on("unhandledRejection", reason => {
Log.error("UnhandledRejection", reason);
let error: any = reason;
if (error instanceof Error) {
error = [error.message, error.stack].join("\n");
}
Log.error("UnhandledRejection", error);
});
app.disableHardwareAcceleration();
// 注册自定义协议为特权协议(必须在 app.whenReady() 之前)
protocol.registerSchemesAsPrivileged([
{
scheme: 'font',
privileges: {
bypassCSP: true,
supportFetchAPI: true,
corsEnabled: true,
stream: true
}
}
]);
// Set application name for Windows 10+ notifications
if (process.platform === "win32") app.setAppUserModelId(app.getName());
if (!app.requestSingleInstanceLock()) {
app.quit();
process.exit(0);
}
// 🔧 便携版(绿色版)检测和配置
// 便携版应该把数据存储在应用本地目录,而不是 AppData
const isPortable = (() => {
// 🟢 默认启用便携模式:所有数据存储在安装目录
console.log('[Portable Detection] 便携模式已默认启用');
return true;
/*
// 原检测逻辑(已禁用)
try {
// 方法1: electron-builder 的便携版会设置这个环境变量
if (process.env.PORTABLE_EXECUTABLE_DIR || process.env.PORTABLE_EXECUTABLE_FILE) {
return true;
}
// 方法2: 检查可执行文件名是否包含 portable
if (process.execPath && process.execPath.toLowerCase().includes('portable')) {
return true;
}
// 方法3: 检查应用根目录是否存在 portable.txt 标记文件
if (process.env.APP_ROOT) {
const portableMarker = path.join(process.env.APP_ROOT, 'portable.txt');
if (fs.existsSync(portableMarker)) {
return true;
}
}
} catch (error) {
console.error('[Portable Detection] 检测失败:', error);
}
return false;
*/
})();
const resolveInstallRoot = () => {
if (isDev) {
return process.cwd();
}
return path.dirname(process.execPath);
};
const resolveResourceBundleRoot = (installRoot: string) => {
if (!isDev) {
const packagedBundleRoot = path.join(process.resourcesPath, 'resources-bundles');
if (fs.existsSync(packagedBundleRoot)) {
return packagedBundleRoot;
}
}
return path.join(installRoot, 'resources-bundles');
};
const configurePortablePaths = () => {
const installRoot = resolveInstallRoot();
const dataRoot = path.join(installRoot, 'data');
const userDataRoot = path.join(dataRoot, 'userData');
const sessionDataRoot = path.join(dataRoot, 'session');
const cacheRoot = path.join(dataRoot, 'cache');
const tempRoot = path.join(dataRoot, 'temp');
const resourceStateRoot = path.join(dataRoot, 'resource-state');
const resourceBundleRoot = resolveResourceBundleRoot(installRoot);
for (const dir of [dataRoot, userDataRoot, sessionDataRoot, cacheRoot, tempRoot, resourceStateRoot, resourceBundleRoot]) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
AppEnv.installRoot = installRoot;
AppEnv.dataRoot = dataRoot;
AppEnv.userData = userDataRoot;
AppEnv.sessionData = sessionDataRoot;
AppEnv.cacheRoot = cacheRoot;
AppEnv.tempRoot = tempRoot;
AppEnv.resourceStateRoot = resourceStateRoot;
AppEnv.resourceBundleRoot = resourceBundleRoot;
app.setPath('userData', userDataRoot);
app.setPath('sessionData', sessionDataRoot);
try {
app.setPath('temp', tempRoot);
} catch (error) {
console.warn('[Portable Mode] temp 路径设置失败:', error);
}
try {
app.setPath('cache', cacheRoot);
} catch (error) {
console.warn('[Portable Mode] cache 路径设置失败:', error);
}
process.env.TMPDIR = tempRoot;
process.env.TEMP = tempRoot;
process.env.TMP = tempRoot;
process.env.PLAYWRIGHT_BROWSERS_PATH = resourceBundleRoot;
console.log('[Portable Mode] 🟢 便携版模式已启用');
console.log('[Portable Mode] 安装目录:', installRoot);
console.log('[Portable Mode] 用户数据目录:', userDataRoot);
console.log('[Portable Mode] 资源目录:', resourceBundleRoot);
};
if (isPortable) {
try {
configurePortablePaths();
} catch (error) {
console.error('[Portable Mode] 设置失败:', error);
}
}
const hasSplashWindow = false;
// 🔧 初始化 AppEnv - 确保所有路径都有效(支持中文路径)
try {
// 多重降级方案确保 appRoot 总是有效
let appRoot = process.env.APP_ROOT;
if (!appRoot || typeof appRoot !== 'string') {
// 🔧 修复:process.resourcesPath 是 resources 目录本身
// 如果 AppEnv.appRoot 意为应用根目录(包含 resources 的目录),则需要取父目录
appRoot = path.dirname(process.resourcesPath);
console.log('[AppEnv] 使用 resourcesPath 的父目录:', appRoot);
}
if (!appRoot || typeof appRoot !== 'string') {
appRoot = app.getAppPath();
console.log('[AppEnv] 使用 getAppPath:', appRoot);
}
if (!appRoot || typeof appRoot !== 'string') {
// 最终降级:使用可执行文件目录
appRoot = path.dirname(process.execPath);
console.log('[AppEnv] 使用 execPath dirname:', appRoot);
}
AppEnv.appRoot = appRoot;
console.log('[AppEnv] ✅ appRoot 已设置:', AppEnv.appRoot);
} catch (error) {
console.error('[AppEnv] 设置 appRoot 失败:', error);
AppEnv.appRoot = path.dirname(process.execPath);
}
try {
AppEnv.appData = app.getPath("appData");
AppEnv.userData = AppEnv.userData || app.getPath("userData");
AppEnv.sessionData = AppEnv.sessionData || app.getPath("sessionData");
console.log('[AppEnv] ✅ userData 已设置:', AppEnv.userData);
if (!AppEnv.installRoot) {
AppEnv.installRoot = resolveInstallRoot();
}
if (!AppEnv.dataRoot) {
if (AppEnv.userData && typeof AppEnv.userData === 'string') {
if (AppEnv.userData.endsWith('/userData') || AppEnv.userData.endsWith('\\userData')) {
AppEnv.dataRoot = path.dirname(AppEnv.userData);
} else if (AppEnv.userData.endsWith('/data') || AppEnv.userData.endsWith('\\data')) {
AppEnv.dataRoot = AppEnv.userData;
} else {
AppEnv.dataRoot = path.join(AppEnv.userData, "data");
}
} else {
AppEnv.dataRoot = path.join(app.getPath("temp"), "zhenqianba-data");
console.warn('[AppEnv] userData 不可用,使用临时目录:', AppEnv.dataRoot);
}
}
AppEnv.cacheRoot = AppEnv.cacheRoot || path.join(AppEnv.dataRoot, 'cache');
AppEnv.tempRoot = AppEnv.tempRoot || path.join(AppEnv.dataRoot, 'temp');
AppEnv.resourceStateRoot = AppEnv.resourceStateRoot || path.join(AppEnv.dataRoot, 'resource-state');
AppEnv.resourceBundleRoot = AppEnv.resourceBundleRoot || resolveResourceBundleRoot(AppEnv.installRoot);
console.log('[AppEnv] ✅ dataRoot 已设置:', AppEnv.dataRoot);
} catch (error) {
console.error('[AppEnv] 设置路径失败:', error);
throw error;
}
for (const dir of [
AppEnv.dataRoot,
path.join(AppEnv.dataRoot, "logs"),
path.join(AppEnv.dataRoot, "storage"),
AppEnv.cacheRoot,
AppEnv.tempRoot,
AppEnv.resourceStateRoot,
AppEnv.resourceBundleRoot,
AppEnv.sessionData,
]) {
if (dir && !fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
if (isPackaged) {
checkResourceBundles({ installMissing: false }).then((result) => {
if (result.pending.length > 0) {
Log.warn('[ResourceManager] bundles pending', result.pending);
}
if (result.installed.length > 0) {
Log.info('[ResourceManager] bundles installed', result.installed);
}
if (result.missingRequired.length > 0) {
Log.warn('[ResourceManager] required bundles missing', result.missingRequired);
}
}).catch((error) => {
Log.warn('[ResourceManager] initial check failed', error);
});
}
AppEnv.isInit = true;
// 🔧 设置 U2NET_HOME 环境变量,指向打包的模型目录
// ⚠️ 只使用程序自带的 U2-Net 模型,不依赖用户目录下载
const modelsPath = getModelPath('u2net.onnx');
process.env.U2NET_HOME = path.dirname(modelsPath);
Log.info("U2NET_HOME 设置完成", { U2NET_HOME: process.env.U2NET_HOME });
ConfigContextMenu.init();
Log.info("Starting");
Log.info("LaunchInfo", {
isPackaged,
userData: AppEnv.userData,
dataRoot: AppEnv.dataRoot,
});
async function createWindow() {
let icon = logoPath;
if (process.platform === "win32") {
icon = icoLogoPath;
} else if (process.platform === "darwin") {
icon = icnsLogoPath;
}
if (hasSplashWindow) {
AppRuntime.splashWindow = new BrowserWindow({
title: AppConfig.title,
width: 600,
height: 350,
transparent: true,
frame: false,
alwaysOnTop: true,
hasShadow: true,
skipTaskbar: true,
});
rendererLoadPath(AppRuntime.splashWindow, "splash.html");
}
// 加载保存的窗口状态
const savedState = loadWindowState();
const windowOptions: any = {
show: !hasSplashWindow,
title: AppConfig.title,
...(!isPackaged ? { icon } : {}),
frame: false,
transparent: false,
hasShadow: true,
center: !savedState, // 如果有保存的状态,不居中
minWidth: WindowConfig.minWidth,
minHeight: WindowConfig.minHeight,
width: savedState?.width || WindowConfig.initWidth,
height: savedState?.height || WindowConfig.initHeight,
backgroundColor: await AppsMain.defaultDarkModeBackgroundColor(),
titleBarStyle: "hidden",
trafficLightPosition: { x: 10, y: 11 },
webPreferences: {
preload: preloadDefault,
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
nodeIntegration: true,
webSecurity: false,
webviewTag: true,
// Consider using contextBridge.exposeInMainWorld
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
contextIsolation: false,
// sandbox: false,
},
};
// 如果有保存的位置,设置窗口位置
if (savedState?.x !== undefined && savedState?.y !== undefined) {
windowOptions.x = savedState.x;
windowOptions.y = savedState.y;
}
AppRuntime.mainWindow = new BrowserWindow(windowOptions);
setUpdaterWindow(AppRuntime.mainWindow);
// 如果之前是最大化状态,恢复最大化
if (savedState?.isMaximized) {
AppRuntime.mainWindow.maximize();
}
// 监听窗口大小和位置变化,保存状态
let saveTimeout: NodeJS.Timeout | null = null;
const scheduleSave = () => {
if (saveTimeout) {
clearTimeout(saveTimeout);
}
saveTimeout = setTimeout(() => {
if (AppRuntime.mainWindow && !AppRuntime.mainWindow.isDestroyed()) {
saveWindowState(AppRuntime.mainWindow);
}
}, 500); // 延迟500ms保存,避免频繁写入
};
AppRuntime.mainWindow.on("resize", scheduleSave);
AppRuntime.mainWindow.on("move", scheduleSave);
AppRuntime.mainWindow.on("maximize", scheduleSave);
AppRuntime.mainWindow.on("unmaximize", scheduleSave);
AppRuntime.mainWindow.on("closed", () => {
if (saveTimeout) {
clearTimeout(saveTimeout);
}
AppRuntime.mainWindow = null;
});
AppRuntime.mainWindow.on("show", async () => {
await executeHooks(AppRuntime.mainWindow, "Show");
});
AppRuntime.mainWindow.on("hide", async () => {
await executeHooks(AppRuntime.mainWindow, "Hide");
});
if (isMac) {
AppRuntime.mainWindow.on("close", event => {
// @ts-ignore
if (!app.quitForce && !isDev) {
executeHooks(AppRuntime.mainWindow, "ShowQuitConfirmDialog");
event.preventDefault();
}
});
}
AppRuntime.mainWindow.on("enter-full-screen", () => {
executeHooks(AppRuntime.mainWindow, "EnterFullScreen");
});
AppRuntime.mainWindow.on("leave-full-screen", () => {
executeHooks(AppRuntime.mainWindow, "LeaveFullScreen");
});
rendererLoadPath(AppRuntime.mainWindow, "index.html");
DevToolsManager.register("Main", AppRuntime.mainWindow);
AppRuntime.mainWindow.webContents.on("did-finish-load", async () => {
try {
const mapiCheck = await AppRuntime.mainWindow!.webContents.executeJavaScript(
`JSON.stringify({ hasMapi: !!window['$mapi'], hasIpcRenderer: !!window['ipcRenderer'], hasPage: !!window['__page'], windowDollarKeys: Object.keys(window).filter(k => k.startsWith('$')) })`
);
console.log('[Main:did-finish-load] preload检查:', mapiCheck);
} catch(e: any) {
console.error('[Main:did-finish-load] preload检查失败:', e.message);
}
if (hasSplashWindow) {
AppRuntime.mainWindow?.show();
setTimeout(() => {
try {
AppRuntime.splashWindow?.close();
AppRuntime.splashWindow = null;
// AppRuntime.mainWindow.webContents.openDevTools({
// mode: 'detach',
// })
} catch (e) { }
}, 1000);
}
// 🔧 延迟调用 Page.ready,直到 MAPI 初始化完成
// 等待 MAPI.init() 完成再通知前端
console.log('[Main:did-finish-load] 页面加载完成,等待 MAPI 初始化...');
// 给 MAPI 初始化一个机会完成
// 如果 MAPI 已经初始化,这会立即执行
// 否则等待最多 5 秒钟
let mapiInitCheck = 0;
const mapiCheckInterval = setInterval(() => {
mapiInitCheck++;
if (AppRuntime.mapiInitialized || mapiInitCheck > 50) { // 5 秒超时
clearInterval(mapiCheckInterval);
if (!AppRuntime.mapiInitialized) {
console.warn('[Main:did-finish-load] ⚠️ MAPI 初始化超时,但继续启动页面');
}
console.log('[Main:did-finish-load] 调用 Page.ready("main")');
Page.ready("main");
DevToolsManager.autoShow(AppRuntime.mainWindow);
if (isPackaged) {
setTimeout(async () => {
try {
const checkAtLaunch = ConfigMain.get("updaterCheckAtLaunch", "yes");
console.log('[Main] auto check update, setting:', checkAtLaunch);
if (checkAtLaunch !== "no") {
await updaterIndex.checkForUpdate();
}
} catch (e: any) {
console.error('[Main] auto check update failed:', e.message);
}
}, 8000);
}
}
}, 100);
// 🔐 启用防破解保护(生产环境且非调试模式时强制启用)
if (!shouldEnableDevTools || process.env.ELECTRON_ENABLE_ANTI_DEBUG === 'true') {
console.log('[Main] 启用防破解保护...');
AntiDebugManager.enableFullProtection(AppRuntime.mainWindow, {
disableDevTools: true,
disableShortcuts: true,
disableContextMenu: true,
disableMenuBar: !shouldEnableDevTools, // 调试模式保留菜单栏
interceptHotkeys: true
});
} else {
console.log('[Main] 调试模式:防破解保护已禁用');
console.log('[Main] 提示:生产环境中设置环境变量 ELECTRON_DEBUG_MODE=true 可启用DevTools');
}
// 🆕 隐藏调试快捷键 Ctrl+Shift+Alt+D(即使在生产环境也能启用DevTools)
globalShortcut.register('Ctrl+Shift+Alt+D', () => {
console.log('[Main] 隐藏调试快捷键触发');
if (AppRuntime.mainWindow && !AppRuntime.mainWindow.webContents.isDevToolsOpened()) {
// 使用 AntiDebugManager 的 restoreDevTools 方法,绕过防护
AntiDebugManager.restoreDevTools(AppRuntime.mainWindow);
console.log('[Main] ✅ DevTools 已通过隐藏快捷键打开');
} else {
AppRuntime.mainWindow?.webContents.closeDevTools();
console.log('[Main] DevTools 已关闭');
}
});
});
AppRuntime.mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith("https:")) shell.openExternal(url);
return { action: "deny" };
});
}
app.whenReady()
.then(() => {
// 注册自定义 font:// 协议
protocol.registerFileProtocol('font', (request, callback) => {
try {
// 从 URL 中提取文件路径
// font://path/to/font.ttf -> path/to/font.ttf
const url = request.url.substring(7); // 移除 'font://'
const fontPath = decodeURIComponent(url);
Log.info('[FontProtocol] Loading font:', fontPath);
// 验证文件存在
if (!fs.existsSync(fontPath)) {
Log.error('[FontProtocol] Font file not found:', fontPath);
callback({ error: -6 }); // FILE_NOT_FOUND
return;
}
// 返回文件路径
callback({ path: fontPath });
} catch (error) {
Log.error('[FontProtocol] Error loading font:', error);
callback({ error: -2 }); // FAILED
}
});
session.defaultSession.setDisplayMediaRequestHandler((request, callback) => {
desktopCapturer.getSources({ types: ["screen"] }).then(sources => {
// Grant access to the first screen found.
callback({ video: sources[0], audio: "loopback" });
});
});
})
.then(ConfigLang.readyAsync)
.then(async () => {
// 初始化加密管理器
try {
console.log("[Main:init] 初始化加密管理器...");
await CryptoManager.initializeKeys();
console.log("[Main:init] ✅ 加密管理器初始化完成");
} catch (error) {
console.error("[Main:init] 加密管理器初始化失败:", error);
Log.error("加密管理器初始化失败:", error);
}
// 在创建窗口前先初始化MAPI(包括数据库)
try {
console.log("[Main:init] Starting MAPI initialization");
console.log("[Main:init] AppEnv.isInit =", AppEnv.isInit);
console.log("[Main:init] AppEnv.dataRoot =", AppEnv.dataRoot);
console.log("[Main:init] AppEnv.userData =", AppEnv.userData);
await MAPI.init();
console.log("[Main:init] MAPI initialized successfully");
Log.info("MAPI initialized successfully");
// 🔧 标记 MAPI 已初始化,前端可以开始使用 API
AppRuntime.mapiInitialized = true;
console.log('[Main:init] 设置 AppRuntime.mapiInitialized = true');
// 初始化系统默认模板
console.log("[Main:init] Initializing system templates...");
await initSystemTemplates();
console.log("[Main:init] System templates initialized");
console.log("[Main:init] Initializing builtin avatars...");
await initBuiltinAvatars();
console.log("[Main:init] Builtin avatars initialized");
// 初始化应用配置(字幕模板等)
console.log("[Main:init] Initializing app config...");
await initAppConfig();
console.log("[Main:init] App config initialized");
console.log("[Main:init] Pre-fetching system config...");
try {
await fetchElectronSystemConfig();
console.log("[Main:init] System config fetched");
} catch(e: any) {
console.warn("[Main:init] System config fetch failed:", e.message);
}
console.log("[Main:init] Initializing voice preview cache...");
await initVoicePreCache();
console.log("[Main:init] Voice preview cache initialized");
if (process.platform !== 'win32') {
console.log("[Main:init] Starting macOS/Linux Python setup (background)...");
ensurePythonSetup().then((pythonPath) => {
if (pythonPath) {
console.log("[Main:init] ✅ Python setup complete:", pythonPath);
} else {
console.warn("[Main:init] Python setup returned null");
}
}).catch((err) => {
console.warn("[Main:init] Python setup failed (non-fatal):", err.message);
});
}
} catch (error) {
console.error("[Main:init] MAPI initialization error:", error);
Log.error("MAPI initialization failed:", error);
// 🔧 即使失败也标记为已尝试初始化,允许前端继续(为了诊断)
AppRuntime.mapiInitialized = true;
// 不抛出错误,允许应用继续(为了诊断)
}
})
.then(() => {
MAPI.ready();
ConfigMenu.ready();
ConfigTray.ready();
// 🔐 加密相关 IPC 处理器
ipcMain.handle("Crypto:getPublicKey", () => {
try {
return { success: true, publicKey: CryptoManager.getPublicKey() };
} catch (error: any) {
return { success: false, error: error.message };
}
});
ipcMain.handle("Crypto:encryptRSA", (event, data: string) => {
try {
return { success: true, encrypted: CryptoManager.encryptRSA(data) };
} catch (error: any) {
return { success: false, error: error.message };
}
});
ipcMain.handle("Crypto:decryptRSA", (event, encryptedData: string) => {
try {
return { success: true, decrypted: CryptoManager.decryptRSA(encryptedData) };
} catch (error: any) {
return { success: false, error: error.message };
}
});
ipcMain.handle("Crypto:encryptAES", (event, data: string, key: string) => {
try {
return { success: true, encrypted: CryptoManager.encryptAES(data, key) };
} catch (error: any) {
return { success: false, error: error.message };
}
});
ipcMain.handle("Crypto:decryptAES", (event, encryptedData: string, key: string) => {
try {
return { success: true, decrypted: CryptoManager.decryptAES(encryptedData, key) };
} catch (error: any) {
return { success: false, error: error.message };
}
});
ipcMain.handle("Crypto:sha256", (event, data: string) => {
try {
return { success: true, hash: CryptoManager.sha256(data) };
} catch (error: any) {
return { success: false, error: error.message };
}
});
ipcMain.handle("Crypto:generateRandomKey", (event, length: number = 32) => {
try {
return { success: true, key: CryptoManager.generateRandomKey(length) };
} catch (error: any) {
return { success: false, error: error.message };
}
});
ipcMain.handle("Crypto:sign", (event, data: string) => {
try {
return { success: true, signature: CryptoManager.sign(data) };
} catch (error: any) {
return { success: false, error: error.message };
}
});
ipcMain.handle("Crypto:verify", (event, data: string, signature: string) => {
try {
return { success: true, valid: CryptoManager.verify(data, signature) };
} catch (error: any) {
return { success: false, error: error.message };
}
});
// 🔧 System 相关 IPC 处理器 - 获取硬件信息用于设备指纹
const execAsync = promisify(execCallback);
ipcMain.handle("System:getSystemInfo", async () => {
try {
console.log('[System:getSystemInfo] IPC 被调用');
// 获取磁盘序列号(Windows
let diskSerial = 'unknown';
try {
if (process.platform === 'win32') {
console.log('[System:getSystemInfo] 执行 wmic diskdrive 命令...');
const { stdout } = await execAsync('wmic diskdrive get SerialNumber');
console.log('[System:getSystemInfo] wmic 输出:', stdout);
const lines = stdout.trim().split('\n');
if (lines.length > 1) {
diskSerial = lines[1].trim();
console.log('[System:getSystemInfo] 获得的 diskSerial:', diskSerial);
}
} else if (process.platform === 'darwin') {
const { stdout } = await execAsync('ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformSerialNumber');
const match = stdout.match(/"IOPlatformSerialNumber"\s*=\s*"([^"]+)"/);
if (match) {
diskSerial = match[1];
}
}
} catch (e) {
console.warn('[System:getSystemInfo] 获取磁盘序列号失败:', e);
Log.warn('[System:getSystemInfo] 获取磁盘序列号失败:', e);
}
const systemInfo = {
hostname: os.hostname(),
platform: os.platform(),
arch: os.arch(),
cpuModel: os.cpus()[0]?.model || 'unknown',
diskSerial
};
console.log('[System:getSystemInfo] 返回系统信息:', systemInfo);
return systemInfo;
} catch (error: any) {
console.error("[System:getSystemInfo] 失败:", error);
Log.error("[System:getSystemInfo] 失败:", error);
return null;
}
});
ipcMain.handle("System:getHardwareSerial", async () => {
try {
console.log('[System:getHardwareSerial] IPC 被调用');
const hardwareSerial: any = {
motherboard: 'unknown',
bios: 'unknown'
};
if (process.platform === 'win32') {
// 获取主板序列号
try {
console.log('[System:getHardwareSerial] 执行 wmic baseboard 命令...');
const { stdout: mbStdout } = await execAsync('wmic baseboard get SerialNumber');
console.log('[System:getHardwareSerial] baseboard 输出:', mbStdout);
const mbLines = mbStdout.trim().split('\n');
if (mbLines.length > 1) {
hardwareSerial.motherboard = mbLines[1].trim();
}
} catch (e) {
console.warn('[System:getHardwareSerial] 获取主板序列号失败:', e);
Log.warn('[System:getHardwareSerial] 获取主板序列号失败:', e);
}
// 获取BIOS序列号
try {
console.log('[System:getHardwareSerial] 执行 wmic bios 命令...');
const { stdout: biosStdout } = await execAsync('wmic bios get SerialNumber');
console.log('[System:getHardwareSerial] bios 输出:', biosStdout);
const biosLines = biosStdout.trim().split('\n');
if (biosLines.length > 1) {
hardwareSerial.bios = biosLines[1].trim();
}
} catch (e) {
console.warn('[System:getHardwareSerial] 获取BIOS序列号失败:', e);
Log.warn('[System:getHardwareSerial] 获取BIOS序列号失败:', e);
}
} else if (process.platform === 'darwin') {
try {
const { stdout } = await execAsync('ioreg -rd1 -c IOPlatformExpertDevice');
const serialMatch = stdout.match(/"IOPlatformSerialNumber"\s*=\s*"([^"]+)"/);
if (serialMatch) {
hardwareSerial.motherboard = serialMatch[1];
hardwareSerial.bios = serialMatch[1];
}
} catch (e) {
console.warn('[System:getHardwareSerial] macOS 获取序列号失败:', e);
}
}
console.log('[System:getHardwareSerial] 返回硬件序列号:', hardwareSerial);
return hardwareSerial;
} catch (error: any) {
console.error("[System:getHardwareSerial] 失败:", error);
Log.error("[System:getHardwareSerial] 失败:", error);
return null;
}
});
// 🔧 IPC处理器:杀死模型进程
ipcMain.handle("killAllModelProcesses", async (event, data: any) => {
try {
let enableGpuCleanup = false;
try {
const raw = ConfigMain.getSync('ipAgentSettings');
if (raw) {
const s = typeof raw === 'string' ? JSON.parse(raw) : raw;
enableGpuCleanup = !!s.enableGpuCleanup;
}
} catch {}
if (!enableGpuCleanup) {
Log.info("[IPC] 本地模型进程优化未开启,跳过进程清理");
return { success: true, message: "未开启,跳过" };
}
const isWin = process.platform === 'win32';
const defaultProcs = isWin
? ["launcher.exe", "python.exe", "ffmpeg.exe", "ffprobe.exe"]
: ["python3", "ffmpeg", "ffprobe"];
const processes = data?.processes || defaultProcs;
const protectedCommands = ['api_server.py', 'LstmSync'];
Log.info(`[IPC] 开始杀死进程: ${processes.join(", ")}`);
for (const proc of processes) {
try {
if (isWin) {
if (proc === 'python.exe') {
const output = execSync(`powershell -Command "Get-CimInstance Win32_Process -Filter \\"Name = '${proc}'\\" | Select-Object ProcessId, CommandLine | ConvertTo-Json"`, {
encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore']
});
let procs = [];
try { procs = JSON.parse(output); if (!Array.isArray(procs)) procs = [procs]; } catch { procs = []; }
for (const p of procs) {
if (!p || !p.ProcessId) continue;
const cmd = (p.CommandLine || '').toLowerCase();
if (protectedCommands.some(kw => cmd.includes(kw.toLowerCase()))) {
Log.info(`[IPC] 🛡️ 跳过保护进程 PID ${p.ProcessId}: ${cmd.substring(0, 100)}`);
continue;
}
try {
execSync(`taskkill /f /pid ${p.ProcessId} /t`, { stdio: 'ignore', shell: 'cmd.exe' });
Log.info(`[IPC] ✓ 已杀死: ${proc} PID ${p.ProcessId}`);
} catch {}
}
} else {
execSync(`taskkill /f /im ${proc} /t`, { stdio: 'ignore', shell: 'cmd.exe' });
Log.info(`[IPC] ✓ 已杀死: ${proc}`);
}
} else {
execSync(`pkill -f "${proc}" 2>/dev/null || true`, { stdio: 'ignore', shell: '/bin/bash' });
Log.info(`[IPC] ✓ 已杀死: ${proc}`);
}
} catch (err: any) {
Log.debug(`[IPC] ${proc} 不存在或已关闭`);
}
}
Log.info("[IPC] ✓ 进程清理完成");
return { success: true, message: "进程已杀死" };
} catch (error: any) {
Log.error("[IPC] 杀死进程失败:", error.message);
return { success: false, error: error.message };
}
});
// 🔧 IPC处理器:手动触发GPU清理(用于测试)
ipcMain.handle("testGPUCleanup", async (event) => {
try {
console.log("[TestGPUCleanup] ========== 手动触发GPU清理 ==========");
// 检查 GPU 进程
let gpuProcs = '';
try {
gpuProcs = execSync(`nvidia-smi --query-compute-apps=pid --format=csv,noheader`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
console.log(`[TestGPUCleanup] GPU 进程: ${gpuProcs}`);
} catch (e) {
console.log(`[TestGPUCleanup] 没有GPU进程或nvidia-smi失败`);
}
if (gpuProcs) {
const pidLines = gpuProcs.split('\n');
const pids = pidLines.map(p => p.trim()).filter(p => p && !isNaN(Number(p)));
console.log(`[TestGPUCleanup] 找到 ${pids.length} 个GPU进程`);
const targetServers = ['indextts', 'heygem'];
const targetPids: string[] = [];
for (const pid of pids) {
try {
const cmdline = execSync(
`powershell -Command "Get-CimInstance Win32_Process -Filter \\"ProcessId = ${pid}\\" | Select-Object -ExpandProperty CommandLine"`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }
).toLowerCase();
if (targetServers.some(server => cmdline.includes(server))) {
console.log(`[TestGPUCleanup] 🎯 发现目标进程 PID ${pid}`);
targetPids.push(pid);
}
} catch (e) {
console.log(`[TestGPUCleanup] 检查PID ${pid} 失败`);
}
}
if (targetPids.length > 0) {
const pidList = targetPids.join(',');
console.log(`[TestGPUCleanup] 🔨 清理 ${targetPids.length} 个进程: ${pidList}`);
execSync(
`powershell -Command "Stop-Process -Id ${pidList} -Force -ErrorAction SilentlyContinue"`,
{ stdio: 'ignore' }
);
console.log(`[TestGPUCleanup] ✓ 已清理 ${targetPids.length} 个进程`);
}
}
// 调用清理脚本
try {
const appRoot = process.env.APP_ROOT;
if (appRoot) {
const cleanupScript = path.join(appRoot, 'scripts', 'cleanup_gpu.py');
if (fs.existsSync(cleanupScript)) {
const pythonExe = process.platform === 'win32' ? 'python.exe' : 'python3';
const pythonPath = path.join(getCommonResourcePath('python'), pythonExe);
if (fs.existsSync(pythonPath)) {
const result = execSync(`"${pythonPath}" "${cleanupScript}"`, {
encoding: 'utf-8',
timeout: 10000,
stdio: ['pipe', 'pipe', 'ignore']
});
console.log(`[TestGPUCleanup] ✓ GPU显存清理完成`);
if (result) {
console.log(`[cleanup_gpu.py] ${result}`);
}
}
}
}
} catch (e) {
console.warn(`[TestGPUCleanup] GPU清理脚本失败: ${e}`);
}
console.log("[TestGPUCleanup] ========== GPU清理完成 ==========");
return { success: true, message: "GPU清理完成,请查看控制台输出" };
} catch (error: any) {
console.error("[TestGPUCleanup] 错误:", error);
return { success: false, error: error.message };
}
});
// 添加智能体登录窗口打开处理器
ipcMain.handle("Agent.OpenLoginWindow", async (event, options) => {
try {
const region = options.region || '07';
await Page.open("agentLogin", {
region: region,
onGetQRCode: async (region: string) => {
// 这里调用智能体登录二维码生成
const result = await MAPI.invoke("Agent.Login", "getQRCode", { region });
return result;
},
onCheckStatus: async (region: string) => {
// 这里检查登录状态
const result = await MAPI.invoke("Agent.Login", "checkStatus", { region });
return result;
},
onClose: () => {
console.log("智能体登录窗口已关闭");
}
});
return { success: true };
} catch (error) {
Log.error("打开智能体登录窗口失败:", error);
return { success: false, error: error.message };
}
});
app.on("browser-window-created", (_, window) => {
optimizer.watchWindowShortcuts(window);
});
createWindow().then();
});
// 标记应用是否正在退出
let isQuitting = false;
app.on("before-quit", async (event) => {
// 更新时立即退出,不执行清理(清理已在 quitAndInstall 中完成)
if ((global as any).__isUpdating) {
console.log("[AppShutdown] 更新模式,跳过清理,立即退出");
Log.info("[AppShutdown] 更新模式,跳过清理,立即退出");
return;
}
// 防止多次触发
if (!isQuitting) {
event.preventDefault();
isQuitting = true;
console.log("[AppShutdown] 应用关闭,开始清理资源...");
Log.info("[AppShutdown] 应用关闭,开始清理资源...");
// 设置清理超时(最多8秒,给安装程序留缓冲时间)
const cleanupTimeout = setTimeout(() => {
console.log("[AppShutdown] ⚠️ 清理超时,强制退出");
Log.info("[AppShutdown] 清理超时,强制退出");
process.exit(0);
}, 8000);
try {
// 1. 停止所有运行中的服务器(最多3秒)
console.log("[AppShutdown] 停止所有服务器...");
await Promise.race([
ServerMain.stopAllServers(),
new Promise(resolve => setTimeout(resolve, 3000))
]);
console.log("[AppShutdown] ✓ 服务器已停止");
// 2. 杀死残留的模型进程(Windows)- 总是清理,确保更新/卸载时干净
if (process.platform === "win32") {
const processesToKill = ["launcher.exe", "python.exe", "ffmpeg.exe", "ffprobe.exe"];
const protectedCommands = ['api_server.py', 'LstmSync'];
console.log("[AppShutdown] 清理残留进程...");
for (const proc of processesToKill) {
try {
if (proc === 'python.exe') {
// 使用超时保护,避免 PowerShell 查询卡住
const output = execSync(`powershell -Command "$ErrorActionPreference='SilentlyContinue'; Get-CimInstance Win32_Process -Filter \\"Name = '${proc}'\\" | Select-Object ProcessId, CommandLine | ConvertTo-Json"`, {
encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'], timeout: 2000
});
let procs = [];
try { procs = JSON.parse(output); if (!Array.isArray(procs)) procs = [procs]; } catch { procs = []; }
for (const p of procs) {
if (!p || !p.ProcessId) continue;
const cmd = (p.CommandLine || '').toLowerCase();
if (protectedCommands.some(kw => cmd.includes(kw.toLowerCase()))) {
console.log(`[AppShutdown] 🛡️ 跳过保护进程 PID ${p.ProcessId}`);
continue;
}
try {
execSync(`taskkill /f /pid ${p.ProcessId} /t`, { stdio: 'ignore', shell: 'cmd.exe', timeout: 1000 });
console.log(`[AppShutdown] ✓ 已杀死: ${proc} PID ${p.ProcessId}`);
} catch {}
}
} else {
execSync(`taskkill /f /im ${proc} /t`, { stdio: 'ignore', shell: 'cmd.exe', timeout: 1000 });
console.log(`[AppShutdown] ✓ 已杀死: ${proc}`);
}
} catch (err: any) {
}
}
console.log("[AppShutdown] ✓ 残留进程清理完成");
}
} catch (error) {
console.error("[AppShutdown] 清理过程出错:", error);
Log.error("[AppShutdown] 清理过程出错:", error);
} finally {
clearTimeout(cleanupTimeout);
}
console.log("[AppShutdown] ✓ 资源清理完成,退出应用");
Log.info("[AppShutdown] ✓ 资源清理完成,退出应用");
// 退出应用
app.quit();
}
});
app.on("will-quit", () => {
// 应用即将退出,不执行清理操作
// 操作系统会自动回收所有资源
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});
app.on("second-instance", () => {
if (AppRuntime.mainWindow) {
if (AppRuntime.mainWindow.isMinimized()) {
AppRuntime.mainWindow.restore();
}
AppRuntime.mainWindow.show();
AppRuntime.mainWindow.focus();
}
});
app.on("activate", () => {
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length) {
if (!AppRuntime.mainWindow.isVisible()) {
AppRuntime.mainWindow.show();
}
AppRuntime.mainWindow.focus();
} else {
createWindow().then();
}
});