1448 lines
67 KiB
JavaScript
1448 lines
67 KiB
JavaScript
// main.js
|
||
const { app, BrowserWindow, ipcMain, dialog, shell, nativeTheme, screen, session } = require('electron');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
const https = require('https');
|
||
const { pipeline } = require('stream');
|
||
const os = require('os');
|
||
const { exec } = require('child_process');
|
||
const iconv = require('iconv-lite');
|
||
const AppDatabase = require('./config/database');
|
||
const si = require('systeminformation');
|
||
const sudo = require('sudo-prompt');
|
||
const ini = require('ini');
|
||
const { Worker } = require('worker_threads');
|
||
const apiReservations = require('./config/api-reservations');
|
||
|
||
// --- [核心路径定义] ---
|
||
const exeDir = path.dirname(process.execPath);
|
||
const rootDir = app.isPackaged ? path.resolve(exeDir, '..') : __dirname;
|
||
const langDir = app.isPackaged ? path.join(exeDir, 'lang') : path.join(__dirname, 'lang');
|
||
const langDirFallback = app.isPackaged ? path.join(exeDir, 'resources', 'lang') : path.join(__dirname, 'lang');
|
||
|
||
function determineDataPath() {
|
||
if (!app.isPackaged) {
|
||
return path.join(__dirname, 'config', 'app.db');
|
||
}
|
||
const dataDir = path.join(rootDir, 'data');
|
||
if (!fs.existsSync(dataDir)) {
|
||
try { fs.mkdirSync(dataDir, { recursive: true }); } catch (error) { }
|
||
}
|
||
return path.join(dataDir, 'app.db');
|
||
}
|
||
|
||
// --- 全局变量 ---
|
||
let currentLanguagePack = {};
|
||
let fallbackLanguagePack = {};
|
||
let appConfig = { Settings: { Language: 'auto' } };
|
||
let db;
|
||
let mainWindow;
|
||
let splashWindow;
|
||
let disclaimerWindow;
|
||
let downloadController = null;
|
||
let windowStateChangeTimer = null;
|
||
let acknowledgementsWindow = null;
|
||
let toolWindows = new Map();
|
||
const APP_VERSION = app.getVersion();
|
||
|
||
// 缓存启动配置
|
||
let pendingLaunchConfig = null;
|
||
|
||
const compressionWorkerPath = path.join(__dirname, 'src/js/workers/compressionWorker.js');
|
||
|
||
app.commandLine.appendSwitch('force-gpu-rasterization');
|
||
app.commandLine.appendSwitch('enable-features', 'WebView2');
|
||
|
||
// 处理 SSL 证书验证(对于已知的 API 域名,允许证书错误)
|
||
// 注意:这仅用于处理已知安全的 API,不应用于用户数据
|
||
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
||
// [健康检查预留位置] 允许特定域名的证书错误(这些是已知的第三方 API)
|
||
// 注意:这些域名用于工具健康检查和工具功能,不涉及用户敏感数据
|
||
const allowedDomains = [
|
||
'www.cunyuapi.top',
|
||
'cunyuapi.top',
|
||
'api.jkyai.top',
|
||
'api.pearktrue.cn',
|
||
'shanhe.kim',
|
||
'uapis.cn',
|
||
'update.ymhut.cn',
|
||
'api.suyanw.cn', // 百度热榜、B站热榜等工具使用的API
|
||
'api.ipify.org', // IP查询工具使用的API
|
||
'ipinfo.io' // IP查询工具备选API
|
||
];
|
||
|
||
try {
|
||
const hostname = new URL(url).hostname;
|
||
if (allowedDomains.some(domain => hostname === domain || hostname.endsWith('.' + domain))) {
|
||
// 对于允许的域名,忽略证书错误
|
||
event.preventDefault();
|
||
callback(true);
|
||
return;
|
||
}
|
||
} catch (e) {
|
||
// URL 解析失败,使用默认验证
|
||
}
|
||
|
||
// 对于其他域名,使用默认的证书验证
|
||
callback(false);
|
||
});
|
||
|
||
// ==========================================
|
||
// --- [核心工具函数定义] ---
|
||
// ==========================================
|
||
|
||
function simpleFetch(url) {
|
||
return new Promise((resolve) => {
|
||
const req = https.get(url, { timeout: 3000 }, (res) => {
|
||
let data = '';
|
||
res.on('data', chunk => data += chunk);
|
||
res.on('end', () => {
|
||
try { resolve(JSON.parse(data)); } catch { resolve(null); }
|
||
});
|
||
});
|
||
req.on('error', () => resolve(null));
|
||
req.on('timeout', () => { req.abort(); resolve(null); });
|
||
});
|
||
}
|
||
|
||
async function loadRemoteConfigs() {
|
||
const urls = {
|
||
media_types: 'https://update.ymhut.cn/media-types.json',
|
||
update_info: `https://update.ymhut.cn/update-info.json?r=${Date.now()}`,
|
||
tool_status: `https://update.ymhut.cn/tool-status.json?r=${Date.now()}`
|
||
};
|
||
const offlineCapableTools = ['system-tool', 'system-info', 'base64-converter', 'qr-code-generator', 'chinese-converter', 'profanity-check', 'image-processor', 'archive-tool', 'sanguosha-downloader'];
|
||
|
||
const fetchAndCache = (key, url) => new Promise((resolve, reject) => {
|
||
https.get(url, (res) => {
|
||
if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`));
|
||
let data = '';
|
||
res.on('data', chunk => data += chunk);
|
||
res.on('end', () => {
|
||
try {
|
||
const json = JSON.parse(data);
|
||
db.saveRemoteConfig(key, json);
|
||
resolve(json);
|
||
} catch (e) { reject(e); }
|
||
});
|
||
}).on('error', (err) => reject(err));
|
||
});
|
||
|
||
try {
|
||
const [mediaTypes, updateInfo, toolStatus] = await Promise.all([
|
||
fetchAndCache('media_types', urls.media_types),
|
||
fetchAndCache('update_info', urls.update_info),
|
||
fetchAndCache('tool_status', urls.tool_status)
|
||
]);
|
||
const configVersion = updateInfo.last_updated || 'unknown';
|
||
const oldConfigVersion = db.getConfig('config_version');
|
||
if (oldConfigVersion !== configVersion) {
|
||
db.setConfig('config_version', configVersion);
|
||
db.logAction({ timestamp: new Date().toISOString(), action: `远程配置已更新: ${configVersion}`, category: 'system' });
|
||
}
|
||
return { ...mediaTypes, ...updateInfo, tool_status: toolStatus, config_version: configVersion, is_offline_mode: false };
|
||
} catch (error) {
|
||
console.warn(`联网获取配置失败 (${error.message}),尝试读取数据库缓存...`);
|
||
const cachedMedia = db.getRemoteConfig('media_types');
|
||
const cachedUpdate = db.getRemoteConfig('update_info');
|
||
const cachedStatus = db.getRemoteConfig('tool_status');
|
||
|
||
if (cachedMedia && cachedUpdate && cachedStatus) {
|
||
console.log('已加载本地缓存配置 (离线工具模式)');
|
||
const modifiedStatus = { ...cachedStatus };
|
||
for (const toolId in modifiedStatus) {
|
||
if (!offlineCapableTools.includes(toolId) && !toolId.startsWith('comment')) {
|
||
modifiedStatus[toolId] = { enabled: false, message: "网络不可用,此在线工具已暂停服务 (离线模式)" };
|
||
}
|
||
}
|
||
db.logAction({ timestamp: new Date().toISOString(), action: `进入离线工具模式: ${error.message}`, category: 'system' });
|
||
return { ...cachedMedia, ...cachedUpdate, tool_status: modifiedStatus, config_version: (cachedUpdate.last_updated || 'cached') + ' (Offline)', is_offline_mode: true };
|
||
}
|
||
throw new Error(`无法连接网络且无本地缓存: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
async function checkWriteAccess() {
|
||
if (!app.isPackaged) return true;
|
||
const dataDir = path.join(rootDir, 'data');
|
||
const testFile = path.join(dataDir, `_writetest_${Date.now()}`);
|
||
try {
|
||
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
||
fs.writeFileSync(testFile, 'test');
|
||
fs.unlinkSync(testFile);
|
||
return true;
|
||
} catch (error) {
|
||
dialog.showMessageBoxSync({ type: 'error', title: '权限不足', message: `应用需要管理员权限才能在安装目录中写入数据。` });
|
||
await ipcMain.handle('check-and-relaunch-as-admin');
|
||
app.quit();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function checkAndDownloadLanguagePack() {
|
||
let targetDir = langDir;
|
||
let defaultLangFile = path.join(targetDir, 'zh-CN.json');
|
||
if (!fs.existsSync(targetDir)) {
|
||
if (fs.existsSync(langDirFallback)) targetDir = langDirFallback;
|
||
else try { fs.mkdirSync(langDir, { recursive: true }); } catch (e) { }
|
||
}
|
||
if (fs.existsSync(path.join(targetDir, 'zh-CN.json'))) return true;
|
||
|
||
const choice = dialog.showMessageBoxSync({ type: 'warning', title: '缺失核心文件', message: '检测到默认语言包丢失。', buttons: ['下载修复', '退出'], defaultId: 0, cancelId: 1 });
|
||
if (choice === 1) { app.quit(); return false; }
|
||
|
||
try {
|
||
const downloadUrl = 'https://update.ymhut.cn/lang/zh-CN.json';
|
||
const downloadTarget = path.join(langDir, 'zh-CN.json');
|
||
await new Promise((resolve, reject) => {
|
||
const file = fs.createWriteStream(downloadTarget);
|
||
https.get(downloadUrl, (res) => {
|
||
if (res.statusCode !== 200) { reject(new Error(res.statusCode)); return; }
|
||
res.pipe(file); file.on('finish', () => file.close(resolve));
|
||
}).on('error', (err) => { fs.unlink(downloadTarget, () => { }); reject(err); });
|
||
});
|
||
app.relaunch(); app.quit(); return false;
|
||
} catch (error) { return false; }
|
||
}
|
||
|
||
function loadConfigAndLanguage() {
|
||
try {
|
||
const configPath = path.join(rootDir, 'config.ini');
|
||
if (fs.existsSync(configPath)) appConfig = ini.parse(fs.readFileSync(configPath, 'utf-8'));
|
||
} catch (e) { }
|
||
let activeLangDir = langDir;
|
||
if (!fs.existsSync(path.join(activeLangDir, 'zh-CN.json')) && fs.existsSync(path.join(langDirFallback, 'zh-CN.json'))) activeLangDir = langDirFallback;
|
||
try { fallbackLanguagePack = JSON.parse(fs.readFileSync(path.join(activeLangDir, 'zh-CN.json'), 'utf-8')); } catch (e) { }
|
||
let targetLang = appConfig.Settings?.Language || 'auto';
|
||
|
||
// 改进的auto模式:检测系统语言
|
||
if (targetLang === 'auto') {
|
||
const systemLocale = app.getLocale(); // 例如: 'zh-CN', 'en-US', 'en-GB'
|
||
const localeParts = systemLocale.split('-');
|
||
const langCode = localeParts[0].toLowerCase();
|
||
|
||
// 支持的语言映射
|
||
if (langCode === 'zh') {
|
||
targetLang = 'zh-CN';
|
||
} else if (langCode === 'en') {
|
||
targetLang = 'en-US';
|
||
} else {
|
||
// 如果系统语言不在支持列表中,默认使用中文
|
||
targetLang = 'zh-CN';
|
||
}
|
||
}
|
||
|
||
if (targetLang === 'zh-CN') currentLanguagePack = fallbackLanguagePack;
|
||
else {
|
||
try { currentLanguagePack = JSON.parse(fs.readFileSync(path.join(activeLangDir, `${targetLang}.json`), 'utf-8')); } catch (e) { currentLanguagePack = fallbackLanguagePack; }
|
||
}
|
||
}
|
||
|
||
async function migrateAndInitializeDatabase() {
|
||
const newDbPath = determineDataPath();
|
||
const newDbDir = path.dirname(newDbPath);
|
||
if (fs.existsSync(newDbPath)) {
|
||
try { db = new AppDatabase(newDbPath); } catch (e) { app.quit(); return; }
|
||
} else {
|
||
try { fs.mkdirSync(newDbDir, { recursive: true }); db = new AppDatabase(newDbPath); } catch (e) { app.quit(); return; }
|
||
}
|
||
|
||
// [重构] 注册接口预留位置到数据库
|
||
await registerApiReservations();
|
||
}
|
||
|
||
/**
|
||
* [重构] 注册接口预留位置
|
||
*/
|
||
async function registerApiReservations() {
|
||
if (!db) return;
|
||
|
||
try {
|
||
Object.values(apiReservations).forEach(reservation => {
|
||
db.registerApiReservation(reservation);
|
||
});
|
||
console.log(`[API] 已注册 ${Object.keys(apiReservations).length} 个接口预留位置`);
|
||
} catch (error) {
|
||
console.error('[API] 注册接口预留位置失败:', error);
|
||
}
|
||
}
|
||
|
||
async function checkAndLogReinstall() {
|
||
const uninstallInfo = appConfig.UninstallInfo;
|
||
if (uninstallInfo && uninstallInfo.Time) {
|
||
try {
|
||
const uninstallTime = new Date(uninstallInfo.Time);
|
||
if (new Date() - uninstallTime < 86400000) db.logAction({ timestamp: new Date().toISOString(), action: `检测到快速重装/升级至 v${APP_VERSION}`, category: 'system' });
|
||
delete appConfig.UninstallInfo;
|
||
fs.writeFileSync(path.join(rootDir, 'config.ini'), ini.stringify(appConfig));
|
||
} catch (e) { }
|
||
}
|
||
}
|
||
|
||
let userPublicIp = null;
|
||
async function getPublicIP() {
|
||
if (userPublicIp) return userPublicIp;
|
||
const apis = ['https://api.ipify.org?format=json', 'https://ipinfo.io/json'];
|
||
for (const apiUrl of apis) {
|
||
try {
|
||
const response = await new Promise((resolve, reject) => { https.get(apiUrl, { timeout: 3000 }, res => resolve(res)).on('error', reject); });
|
||
if (response.statusCode === 200) {
|
||
let data = ''; for await (const chunk of response) { data += chunk; }
|
||
const jsonData = JSON.parse(data);
|
||
if (jsonData.ip) { userPublicIp = jsonData.ip; return userPublicIp; }
|
||
}
|
||
} catch (e) { }
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function fetchIpBanList() {
|
||
return new Promise((resolve) => {
|
||
https.get(`https://update.ymhut.cn/ip-ban-list.json?r=${Date.now()}`, { timeout: 4000 }, (res) => {
|
||
if (res.statusCode !== 200) return resolve([]);
|
||
let data = ''; res.on('data', chunk => data += chunk);
|
||
res.on('end', () => { try { const parsed = JSON.parse(data); resolve(Array.isArray(parsed.banned_ips) ? parsed.banned_ips : []); } catch (e) { resolve([]); } });
|
||
}).on('error', () => resolve([]));
|
||
});
|
||
}
|
||
|
||
let networkMonitor = { currentBytes: 0, lastTimestamp: Date.now() };
|
||
let networkSpeedInterval = null;
|
||
let cpuUsage = { prev: process.cpuUsage(), prevTime: process.hrtime() };
|
||
|
||
// [修复] 设置控制台输出编码为 UTF-8,解决乱码问题
|
||
if (process.platform === 'win32') {
|
||
try {
|
||
// Windows 下设置控制台编码
|
||
process.stdout.setDefaultEncoding('utf8');
|
||
process.stderr.setDefaultEncoding('utf8');
|
||
// 尝试设置控制台代码页为 UTF-8
|
||
if (process.env.PROMPT) {
|
||
exec('chcp 65001', (error) => {
|
||
if (!error) {
|
||
console.log('[System] 控制台编码已设置为 UTF-8');
|
||
}
|
||
});
|
||
}
|
||
} catch (e) {
|
||
// 忽略编码设置错误
|
||
}
|
||
}
|
||
|
||
process.on('uncaughtException', (error, origin) => {
|
||
try {
|
||
const errorMsg = error.message || String(error);
|
||
if (db) db.logAction({ timestamp: new Date().toISOString(), action: `主进程错误: ${errorMsg}`, category: 'error' });
|
||
} catch (dbError) {
|
||
console.error('[Error] 记录错误日志失败:', dbError);
|
||
}
|
||
// [修复] 确保错误信息正确输出
|
||
console.error('[UncaughtException]', error);
|
||
});
|
||
|
||
// ==========================================
|
||
// --- [窗口管理] ---
|
||
// ==========================================
|
||
|
||
function createSplashWindow() {
|
||
const themeFromConfig = db ? db.getConfig('theme') : 'dark';
|
||
splashWindow = new BrowserWindow({
|
||
width: 600, height: 300, frame: false, resizable: false, center: true, transparent: true, hasShadow: false, alwaysOnTop: true,
|
||
backgroundColor: '#00000000', roundedCorners: true, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true },
|
||
show: false, skipTaskbar: true,
|
||
});
|
||
splashWindow.loadFile(path.join(__dirname, 'src/splash.html'), { query: { "theme": themeFromConfig } });
|
||
splashWindow.on('ready-to-show', () => splashWindow.show());
|
||
splashWindow.on('closed', () => { splashWindow = null; });
|
||
}
|
||
|
||
function createDisclaimerWindow() {
|
||
const themeFromConfig = db ? db.getConfig('theme') : 'dark';
|
||
disclaimerWindow = new BrowserWindow({
|
||
width: 480, height: 600, frame: false, resizable: false, center: true, transparent: true, hasShadow: false, roundedCorners: true,
|
||
webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true },
|
||
show: false
|
||
});
|
||
disclaimerWindow.loadFile(path.join(__dirname, 'src/disclaimer.html'), { query: { "theme": themeFromConfig } });
|
||
disclaimerWindow.once('ready-to-show', () => {
|
||
disclaimerWindow.show();
|
||
if (splashWindow && !splashWindow.isDestroyed()) splashWindow.close();
|
||
});
|
||
disclaimerWindow.on('closed', () => { disclaimerWindow = null; });
|
||
}
|
||
|
||
function launchMainApp() {
|
||
if (!pendingLaunchConfig) return;
|
||
if (pendingLaunchConfig.isOffline) createMainWindow({ isOffline: true, error: pendingLaunchConfig.error });
|
||
else createMainWindow(pendingLaunchConfig.config);
|
||
}
|
||
|
||
function isWindowVisible(bounds) {
|
||
const displays = screen.getAllDisplays();
|
||
return displays.some(display => {
|
||
const dBounds = display.workArea;
|
||
return (bounds.x >= dBounds.x && bounds.y >= dBounds.y && bounds.x + bounds.width <= dBounds.x + dBounds.width && bounds.y + bounds.height <= dBounds.y + dBounds.height);
|
||
});
|
||
}
|
||
|
||
async function createMainWindow(initialConfig) {
|
||
const theme = db.getConfig('theme') || 'dark';
|
||
const volume = db.getConfig('global_volume');
|
||
const background_image = db.getConfig('background_image') || null;
|
||
const background_opacity = db.getConfig('background_opacity');
|
||
const card_opacity = db.getConfig('card_opacity');
|
||
const savedWidth = parseInt(db.getConfig('window_width'), 10);
|
||
const savedHeight = parseInt(db.getConfig('window_height'), 10);
|
||
const savedX = parseInt(db.getConfig('window_x'), 10);
|
||
const savedY = parseInt(db.getConfig('window_y'), 10);
|
||
const customFontName = db.getConfig('custom_font_name') || '';
|
||
const customFontPath = db.getConfig('custom_font_path') || '';
|
||
|
||
let windowOptions = {
|
||
width: savedWidth || 1500, height: savedHeight || 940, minWidth: 1200, minHeight: 768, frame: false, titleBarStyle: 'hidden',
|
||
webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, webviewTag: true },
|
||
show: false, icon: path.join(__dirname, 'src/assets/icon.ico'), transparent: true, hasShadow: false,
|
||
roundedCorners: true // Windows 圆角支持
|
||
};
|
||
|
||
if (!isNaN(savedX) && !isNaN(savedY)) {
|
||
if (isWindowVisible({ x: savedX, y: savedY, width: windowOptions.width, height: windowOptions.height })) {
|
||
windowOptions.x = savedX; windowOptions.y = savedY;
|
||
}
|
||
}
|
||
|
||
let cachedVersions = {};
|
||
const today = new Date().toISOString().split('T')[0];
|
||
if (appConfig.Environment && appConfig.Environment.app_version && appConfig.Environment.last_checked_date === today) {
|
||
cachedVersions = appConfig.Environment;
|
||
} else {
|
||
const detectedVersions = await si.versions('node, npm, git, docker, python, gcc, java, perl, go');
|
||
cachedVersions = { ...detectedVersions, app_version: APP_VERSION, electron: process.versions.electron, node: process.versions.node, chromium: process.versions.chrome, last_checked_date: today };
|
||
appConfig.Environment = cachedVersions;
|
||
try { fs.writeFileSync(path.join(rootDir, 'config.ini'), ini.stringify(appConfig)); } catch (e) { }
|
||
}
|
||
|
||
const finalConfig = {
|
||
...initialConfig,
|
||
dbSettings: {
|
||
theme, globalVolume: volume ? parseFloat(volume) : 0.5, backgroundImage: background_image, backgroundOpacity: background_opacity ? parseFloat(background_opacity) : 1.0, cardOpacity: card_opacity ? parseFloat(card_opacity) : 0.7, customFontName, customFontPath, versions: cachedVersions, electronVersion: process.versions.electron, nodeVersion: process.versions.node, chromeVersion: process.versions.chrome, config_version: initialConfig.config_version
|
||
}
|
||
};
|
||
|
||
mainWindow = new BrowserWindow(windowOptions);
|
||
|
||
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||
const policy = [
|
||
"default-src 'self'",
|
||
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com",
|
||
"worker-src 'self' blob:",
|
||
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com",
|
||
"font-src 'self' https://cdnjs.cloudflare.com https://s1.hdslb.com https://cdn.jsdelivr.net https://registry.npmmirror.com file: data:",
|
||
"img-src 'self' data: blob: http: https:",
|
||
"connect-src 'self' http: https:",
|
||
"media-src 'self' blob: http: https:",
|
||
"frame-src 'self' http: https:",
|
||
"upgrade-insecure-requests"
|
||
].join('; ');
|
||
|
||
try {
|
||
const contentLength = details.responseHeaders['Content-Length'] || details.responseHeaders['content-length'];
|
||
if (contentLength && contentLength.length > 0) {
|
||
const bytes = parseInt(contentLength[0], 10);
|
||
if (bytes > 0) { db.addTraffic(bytes); networkMonitor.currentBytes += bytes; }
|
||
}
|
||
} catch (e) { }
|
||
|
||
callback({ responseHeaders: { ...details.responseHeaders, 'Content-Security-Policy': [policy] } });
|
||
});
|
||
|
||
if (networkSpeedInterval) clearInterval(networkSpeedInterval);
|
||
networkSpeedInterval = setInterval(() => {
|
||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||
const now = Date.now();
|
||
const timeDiff = (now - networkMonitor.lastTimestamp) / 1000;
|
||
if (timeDiff > 0) {
|
||
const speed = networkMonitor.currentBytes / timeDiff;
|
||
mainWindow.webContents.send('network-speed-update', { speed });
|
||
networkMonitor.currentBytes = 0; networkMonitor.lastTimestamp = now;
|
||
}
|
||
}
|
||
}, 1000);
|
||
|
||
const saveWindowState = () => {
|
||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||
const bounds = mainWindow.getBounds();
|
||
db.setConfig('window_width', bounds.width.toString()); db.setConfig('window_height', bounds.height.toString());
|
||
db.setConfig('window_x', bounds.x.toString()); db.setConfig('window_y', bounds.y.toString());
|
||
};
|
||
const debouncedSave = () => { clearTimeout(windowStateChangeTimer); windowStateChangeTimer = setTimeout(saveWindowState, 500); };
|
||
mainWindow.on('resize', debouncedSave);
|
||
mainWindow.on('move', debouncedSave);
|
||
|
||
nativeTheme.themeSource = theme;
|
||
mainWindow.loadFile(path.join(__dirname, 'src/index.html'));
|
||
|
||
mainWindow.once('ready-to-show', () => {
|
||
mainWindow.webContents.send('initial-data', finalConfig);
|
||
mainWindow.show();
|
||
mainWindow.focus();
|
||
|
||
if (splashWindow && !splashWindow.isDestroyed()) splashWindow.close();
|
||
if (disclaimerWindow && !disclaimerWindow.isDestroyed()) disclaimerWindow.close();
|
||
});
|
||
mainWindow.on('closed', () => { mainWindow = null; });
|
||
}
|
||
|
||
app.on('ready', async () => {
|
||
if (app.isPackaged) { if (!(await checkWriteAccess())) return; }
|
||
if (!(await checkAndDownloadLanguagePack())) return;
|
||
loadConfigAndLanguage();
|
||
await migrateAndInitializeDatabase();
|
||
await checkAndLogReinstall();
|
||
const verCheck = db.checkAndRecordVersion(APP_VERSION);
|
||
if (verCheck.isUpgrade) db.logAction({ timestamp: new Date().toISOString(), action: `应用已更新: v${verCheck.oldVersion} -> v${APP_VERSION}`, category: 'update' });
|
||
createSplashWindow();
|
||
});
|
||
|
||
app.on('window-all-closed', () => { if (process.platform !== 'darwin') { if (networkSpeedInterval) clearInterval(networkSpeedInterval); db.close(); app.quit(); } });
|
||
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createSplashWindow(); });
|
||
|
||
// ==========================================
|
||
// --- [IPC 通信处理] ---
|
||
// ==========================================
|
||
|
||
ipcMain.handle('run-initialization', async () => {
|
||
let totalProgress = 0;
|
||
const updateStep = async (status, increment, minDuration = 500) => {
|
||
totalProgress += increment;
|
||
const targetProgress = Math.min(totalProgress, 99);
|
||
if (splashWindow && !splashWindow.isDestroyed()) {
|
||
splashWindow.webContents.send('init-progress', { status, progress: targetProgress });
|
||
}
|
||
await new Promise(r => setTimeout(r, minDuration));
|
||
};
|
||
|
||
let combinedConfig = {};
|
||
let isFatalOffline = false;
|
||
let errorMsg = '';
|
||
let initWeatherData = null;
|
||
|
||
try {
|
||
await updateStep('正在检测运行环境权限...', 10, 600);
|
||
await updateStep('读取本地配置文件...', 15, 800);
|
||
await updateStep('校验数据库与本地缓存...', 15, 800);
|
||
|
||
const startTime = Date.now();
|
||
combinedConfig = await loadRemoteConfigs();
|
||
|
||
if (!combinedConfig.is_offline_mode) {
|
||
const ipData = await simpleFetch('https://uapis.cn/api/v1/network/myip?source=commercial');
|
||
if (ipData && ipData.code === 200 && ipData.city) {
|
||
// 使用新的天气API,优先使用adcode,如果没有则使用城市名
|
||
let weatherUrl = '';
|
||
if (ipData.area_code) {
|
||
weatherUrl = `https://uapis.cn/api/v1/misc/weather?adcode=${ipData.area_code}&extended=true&indices=true&forecast=true`;
|
||
} else {
|
||
weatherUrl = `https://uapis.cn/api/v1/misc/weather?city=${encodeURIComponent(ipData.city)}&extended=true&indices=true&forecast=true`;
|
||
}
|
||
const weatherData = await simpleFetch(weatherUrl);
|
||
if (weatherData) initWeatherData = { ip: ipData, weather: weatherData };
|
||
}
|
||
}
|
||
|
||
const networkTime = Date.now() - startTime;
|
||
if (networkTime < 1500) await new Promise(r => setTimeout(r, 1500 - networkTime));
|
||
totalProgress = 70;
|
||
|
||
if (combinedConfig.is_offline_mode) await updateStep('网络受限,正在切换离线策略...', 20, 1000);
|
||
else await updateStep('解析气象卫星数据...', 20, 800);
|
||
|
||
} catch (error) {
|
||
isFatalOffline = true; errorMsg = error.message; await updateStep('初始化遇到问题...', 10, 1500);
|
||
}
|
||
await updateStep('正在构建 UI...', 5, 600);
|
||
return { success: true, config: { ...combinedConfig, initWeatherData }, isOffline: isFatalOffline, error: errorMsg };
|
||
});
|
||
|
||
ipcMain.on('initialization-complete', (event, result) => {
|
||
pendingLaunchConfig = result;
|
||
const dbAgreed = db.getConfig('user_agreement_accepted') === 'true';
|
||
let iniAgreed = false;
|
||
try {
|
||
const configPath = path.join(rootDir, 'config.ini');
|
||
if (fs.existsSync(configPath)) {
|
||
const d = ini.parse(fs.readFileSync(configPath, 'utf-8'));
|
||
if (d.Settings?.UserAgreementAccepted === 'true') iniAgreed = true;
|
||
}
|
||
} catch (e) { }
|
||
|
||
if (dbAgreed || iniAgreed) launchMainApp();
|
||
else createDisclaimerWindow();
|
||
});
|
||
|
||
// [修复点 1] 新增配置获取和保存接口,供子窗口调用
|
||
ipcMain.handle('get-app-config', () => {
|
||
// 返回渲染进程需要的配置
|
||
return {
|
||
download_path: db.getConfig('download_path'),
|
||
theme: db.getConfig('theme') || 'dark',
|
||
// 可以根据需要扩展其他配置
|
||
};
|
||
});
|
||
|
||
ipcMain.handle('set-config-value', (event, { key, value }) => {
|
||
try {
|
||
if (db) {
|
||
db.setConfig(key, value);
|
||
return { success: true };
|
||
}
|
||
return { success: false, error: 'Database not initialized' };
|
||
} catch (e) {
|
||
return { success: false, error: e.message };
|
||
}
|
||
});
|
||
|
||
|
||
// [新增] 工业级原子下载器 (支持重试、超时、锁文件处理)
|
||
ipcMain.handle('download-resource-atomic', async (event, { url, targetDir, fileName, headers }) => {
|
||
const MAX_RETRIES = 2; // 最多重试2次
|
||
const TIMEOUT = 5000; // 5秒超时
|
||
|
||
// 确保目录存在
|
||
try {
|
||
if (!fs.existsSync(targetDir)) {
|
||
fs.mkdirSync(targetDir, { recursive: true });
|
||
}
|
||
} catch (e) {
|
||
return { status: 'error', error: `创建目录失败: ${e.message}` };
|
||
}
|
||
|
||
const targetPath = path.join(targetDir, fileName);
|
||
const tempPath = targetPath + '.tmp'; // 临时文件
|
||
|
||
// 检查文件是否已存在且非空
|
||
if (fs.existsSync(targetPath)) {
|
||
try {
|
||
const stat = fs.statSync(targetPath);
|
||
if (stat.size > 0) return { status: 'skipped' };
|
||
} catch(e) {}
|
||
}
|
||
|
||
const downloadAttempt = async (attempt) => {
|
||
return new Promise((resolve, reject) => {
|
||
const request = https.get(url, { headers, timeout: TIMEOUT }, (response) => {
|
||
// 处理非 200 状态
|
||
if (response.statusCode === 404) {
|
||
response.resume(); // 消耗流
|
||
return resolve({ status: '404' });
|
||
}
|
||
if (response.statusCode !== 200) {
|
||
response.resume();
|
||
return reject(new Error(`HTTP ${response.statusCode}`));
|
||
}
|
||
|
||
const fileStream = fs.createWriteStream(tempPath);
|
||
let receivedBytes = 0;
|
||
|
||
response.on('data', (chunk) => {
|
||
receivedBytes += chunk.length;
|
||
});
|
||
|
||
// 管道流向文件
|
||
response.pipe(fileStream);
|
||
|
||
fileStream.on('finish', () => {
|
||
// [关键] 必须先关闭流,释放文件句柄,才能重命名
|
||
fileStream.close((err) => {
|
||
if (err) return reject(err);
|
||
|
||
// 原子重命名: .tmp -> .png
|
||
try {
|
||
if (fs.existsSync(targetPath)) fs.unlinkSync(targetPath);
|
||
fs.renameSync(tempPath, targetPath);
|
||
if (db) db.addTraffic(receivedBytes);
|
||
resolve({ status: 'success', size: receivedBytes });
|
||
} catch (renameErr) {
|
||
reject(new Error(`Rename Failed: ${renameErr.message}`));
|
||
}
|
||
});
|
||
});
|
||
|
||
fileStream.on('error', (err) => {
|
||
fileStream.close();
|
||
try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch(e){}
|
||
reject(err);
|
||
});
|
||
|
||
});
|
||
|
||
request.on('error', (err) => {
|
||
reject(err);
|
||
});
|
||
|
||
request.on('timeout', () => {
|
||
request.destroy();
|
||
reject(new Error('Connection Timed Out'));
|
||
});
|
||
});
|
||
};
|
||
|
||
// 重试循环
|
||
let lastError = null;
|
||
for (let i = 0; i <= MAX_RETRIES; i++) {
|
||
try {
|
||
return await downloadAttempt(i);
|
||
} catch (err) {
|
||
lastError = err;
|
||
try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch(e){}
|
||
if (err.message.includes('404')) return { status: '404' };
|
||
if (i < MAX_RETRIES) {
|
||
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
|
||
}
|
||
}
|
||
}
|
||
|
||
return { status: 'error', error: lastError ? lastError.message : 'Unknown Error' };
|
||
});
|
||
|
||
|
||
ipcMain.handle('confirm-user-agreement', () => {
|
||
try {
|
||
if (db) {
|
||
db.setConfig('user_agreement_accepted', 'true');
|
||
db.logAction({ timestamp: new Date().toISOString(), action: '用户已同意免责声明', category: 'system' });
|
||
}
|
||
const configPath = path.join(rootDir, 'config.ini');
|
||
let iniConfig = {};
|
||
if (fs.existsSync(configPath)) { try { iniConfig = ini.parse(fs.readFileSync(configPath, 'utf-8')); } catch (e) { } }
|
||
iniConfig.Settings = iniConfig.Settings || {};
|
||
iniConfig.Settings.UserAgreementAccepted = 'true';
|
||
try { fs.writeFileSync(configPath, ini.stringify(iniConfig)); } catch (e) { }
|
||
|
||
launchMainApp();
|
||
return { success: true };
|
||
} catch (e) { return { success: false, error: e.message }; }
|
||
});
|
||
|
||
ipcMain.handle('check-user-agreement', () => {
|
||
const dbValue = db ? db.getConfig('user_agreement_accepted') : 'false';
|
||
const configPath = path.join(rootDir, 'config.ini');
|
||
let iniValue = 'false';
|
||
try {
|
||
if (fs.existsSync(configPath)) {
|
||
const iniData = ini.parse(fs.readFileSync(configPath, 'utf-8'));
|
||
iniValue = iniData.Settings?.UserAgreementAccepted || 'false';
|
||
}
|
||
} catch (e) { }
|
||
return (dbValue === 'true' || iniValue === 'true');
|
||
});
|
||
|
||
ipcMain.handle('get-language-config', () => {
|
||
let lang = appConfig.Settings?.Language || 'auto';
|
||
let actualLang = lang;
|
||
|
||
// 改进的auto模式:检测系统语言
|
||
if (lang === 'auto') {
|
||
const systemLocale = app.getLocale();
|
||
const localeParts = systemLocale.split('-');
|
||
const langCode = localeParts[0].toLowerCase();
|
||
|
||
if (langCode === 'zh') {
|
||
actualLang = 'zh-CN';
|
||
} else if (langCode === 'en') {
|
||
actualLang = 'en-US';
|
||
} else {
|
||
actualLang = 'zh-CN'; // 默认中文
|
||
}
|
||
}
|
||
|
||
return {
|
||
current: lang, // 返回配置的语言(可能是'auto')
|
||
actual: actualLang, // 返回实际使用的语言
|
||
pack: currentLanguagePack,
|
||
fallback: fallbackLanguagePack
|
||
};
|
||
});
|
||
ipcMain.handle('save-language-config', (event, lang) => {
|
||
try {
|
||
const configPath = path.join(rootDir, 'config.ini');
|
||
const currentConfig = ini.parse(fs.readFileSync(configPath, 'utf-8'));
|
||
currentConfig.Settings = currentConfig.Settings || {}; currentConfig.Settings.Language = lang;
|
||
fs.writeFileSync(configPath, ini.stringify(currentConfig));
|
||
if (db) db.close(); setTimeout(() => { app.relaunch(); app.quit(); }, 1000);
|
||
return { success: true };
|
||
} catch (e) { return { success: false, error: e.message }; }
|
||
});
|
||
|
||
// 保存翻译后的语言包
|
||
ipcMain.handle('save-translated-language-pack', async (event, langCode, translatedPack) => {
|
||
try {
|
||
const activeLangDir = langDir;
|
||
const langFilePath = path.join(activeLangDir, `${langCode}.json`);
|
||
|
||
// 确保目录存在
|
||
if (!fs.existsSync(activeLangDir)) {
|
||
fs.mkdirSync(activeLangDir, { recursive: true });
|
||
}
|
||
|
||
// 保存翻译后的语言包
|
||
fs.writeFileSync(langFilePath, JSON.stringify(translatedPack, null, 2), 'utf-8');
|
||
|
||
return { success: true, path: langFilePath };
|
||
} catch (e) {
|
||
console.error('[Main] Failed to save translated language pack:', e);
|
||
return { success: false, error: e.message };
|
||
}
|
||
});
|
||
ipcMain.on('window-minimize', () => mainWindow?.minimize());
|
||
ipcMain.on('window-maximize', () => { if (mainWindow?.isMaximized()) mainWindow.unmaximize(); else mainWindow?.maximize(); });
|
||
|
||
ipcMain.on('window-close', (event) => {
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
if (win) win.close();
|
||
});
|
||
|
||
ipcMain.on('relaunch-app', () => { app.relaunch(); app.quit(); });
|
||
ipcMain.on('report-traffic', (event, bytes) => { if (typeof bytes === 'number' && bytes > 0) networkMonitor.currentBytes += bytes; });
|
||
ipcMain.handle('log-action', (event, logData) => db.logAction(logData));
|
||
ipcMain.handle('get-logs', (event, filterDate) => db.getLogs(filterDate));
|
||
ipcMain.handle('clear-logs', () => db.clearLogs());
|
||
ipcMain.handle('get-traffic-stats', () => db.getTrafficStats());
|
||
ipcMain.handle('get-traffic-history', () => db.getTrafficHistory());
|
||
ipcMain.handle('add-traffic', (event, bytes) => db.addTraffic(bytes));
|
||
ipcMain.handle('get-app-version', () => APP_VERSION);
|
||
ipcMain.handle('check-updates', async () => {
|
||
const updateURL = `https://update.ymhut.cn/update-info.json?r=${Date.now()}`;
|
||
return new Promise((resolve, reject) => {
|
||
https.get(updateURL, (res) => {
|
||
let data = ''; res.on('data', chunk => data += chunk);
|
||
res.on('end', () => { try { const info = JSON.parse(data); const hasUpdate = info.app_version > APP_VERSION; resolve({ hasUpdate, currentVersion: APP_VERSION, remoteVersion: info.app_version, updateNotes: info.update_notes, downloadUrl: info.download_url }); } catch (e) { reject(new Error('解析失败')); } });
|
||
}).on('error', (e) => reject(new Error('获取失败: ' + e.message)));
|
||
});
|
||
});
|
||
ipcMain.handle('download-update', async (event, url) => {
|
||
const filename = path.basename(new URL(url).pathname);
|
||
const downloadPath = path.join(app.getPath('downloads'), filename);
|
||
const writer = require('fs').createWriteStream(downloadPath);
|
||
return new Promise((resolve, reject) => {
|
||
const request = https.get(url, (response) => {
|
||
if (response.statusCode !== 200) return reject({ success: false, error: `Code ${response.statusCode}` });
|
||
const totalBytes = parseInt(response.headers['content-length'], 10);
|
||
let receivedBytes = 0; let lastTime = Date.now(); let lastReceivedBytes = 0;
|
||
downloadController = { abort: () => request.abort() };
|
||
response.on('data', (chunk) => {
|
||
networkMonitor.currentBytes += chunk.length; receivedBytes += chunk.length;
|
||
const now = Date.now();
|
||
if (now - lastTime > 500) {
|
||
const speed = (receivedBytes - lastReceivedBytes) / ((now - lastTime) / 1000);
|
||
mainWindow.webContents.send('download-progress', { total: totalBytes, received: receivedBytes, percent: (receivedBytes / totalBytes) * 100, speed: speed });
|
||
lastTime = now; lastReceivedBytes = receivedBytes;
|
||
}
|
||
});
|
||
response.on('aborted', () => { downloadController = null; reject({ success: false, error: 'Stop' }); });
|
||
pipeline(response, writer, async (err) => { downloadController = null; if (err) reject({ success: false, error: err.message }); else resolve({ success: true, path: downloadPath }); });
|
||
}).on('error', (err) => { downloadController = null; reject({ success: false, error: 'Network Error' }); });
|
||
});
|
||
});
|
||
ipcMain.handle('cancel-download', () => { if (downloadController) { downloadController.abort(); downloadController = null; return { success: true }; } return { success: false }; });
|
||
ipcMain.handle('install-update', async (event, filePath) => { try { shell.openPath(filePath); setTimeout(() => app.quit(), 1500); return { success: true }; } catch (e) { return { success: false, error: e.message }; } });
|
||
ipcMain.handle('open-file', (event, filePath) => shell.openPath(filePath));
|
||
ipcMain.handle('open-external-link', (event, url) => shell.openExternal(url));
|
||
ipcMain.handle('show-item-in-folder', (event, filePath) => shell.showItemInFolder(filePath));
|
||
ipcMain.handle('save-media', async (event, { buffer, defaultPath, type, name, path: fPath }) => {
|
||
if (type === 'config_font') { await db.setConfig('custom_font_name', name); await db.setConfig('custom_font_path', fPath); return { success: true }; }
|
||
const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, { title: '保存', defaultPath });
|
||
if (!canceled && filePath) { try { await fs.promises.writeFile(filePath, Buffer.from(buffer)); return { success: true, path: filePath }; } catch (error) { return { success: false, error: error.message }; } }
|
||
return { success: false, error: 'Cancel' };
|
||
});
|
||
ipcMain.handle('set-theme', async (event, theme) => { nativeTheme.themeSource = theme; return await db.setConfig('theme', theme); });
|
||
ipcMain.handle('set-global-volume', async (event, volume) => await db.setConfig('global_volume', volume.toString()));
|
||
ipcMain.handle('select-background-image', async () => {
|
||
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, { title: '选择图片', properties: ['openFile'], filters: [{ name: 'Images', extensions: ['jpg', 'png', 'webp'] }] });
|
||
if (!canceled && filePaths.length > 0) {
|
||
try {
|
||
const filePath = filePaths[0]; const dataUrl = `data:image/${path.extname(filePath).substring(1)};base64,${await fs.promises.readFile(filePath, 'base64')}`;
|
||
await db.setConfig('background_image', dataUrl); return { success: true, path: dataUrl };
|
||
} catch (error) { return { success: false, error: error.message }; }
|
||
} return { success: false, error: 'Cancel' };
|
||
});
|
||
ipcMain.handle('clear-background-image', async () => await db.setConfig('background_image', ''));
|
||
ipcMain.handle('set-background-opacity', async (event, opacity) => await db.setConfig('background_opacity', opacity.toString()));
|
||
ipcMain.handle('set-card-opacity', async (event, opacity) => await db.setConfig('card_opacity', opacity.toString()));
|
||
ipcMain.on('launch-system-tool', (event, command) => { const cmd = process.platform === 'win32' ? `start "" "${command}"` : command; exec(cmd, (error) => { if (error) dialog.showErrorBox('Fail', error.message); }); });
|
||
ipcMain.handle('show-confirmation-dialog', async (event, options) => {
|
||
// 1. 获取触发此事件的窗口 (即三国杀工具窗口)
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
|
||
// 2. 将 win 作为第一个参数传入,使其成为模态弹窗
|
||
const result = await dialog.showMessageBox(win, {
|
||
type: 'question',
|
||
buttons: ['确定', '取消'],
|
||
defaultId: 0,
|
||
cancelId: 1,
|
||
title: options.title || '确认',
|
||
message: options.message || '',
|
||
detail: options.detail || '',
|
||
noLink: true,
|
||
...options // 允许覆盖
|
||
});
|
||
|
||
return result;
|
||
});
|
||
ipcMain.handle('check-and-relaunch-as-admin', async () => {
|
||
const isAdmin = await new Promise(r => process.platform === 'win32' ? exec('net session', err => r(!err)) : r(process.getuid && process.getuid() === 0));
|
||
if (isAdmin) return { isAdmin: true };
|
||
const { response } = await dialog.showMessageBox(mainWindow, { type: 'info', buttons: ['以管理员身份重启', '取消'], title: '需要权限', message: '部分功能需要管理员权限。' });
|
||
if (response === 0) {
|
||
const cmd = app.isPackaged ? `"${process.execPath}"` : `"${process.execPath}" "${app.getAppPath()}"`;
|
||
sudo.exec(process.platform === 'win32' ? `start "" ${cmd}` : cmd, { name: 'YMhut Box' }, (err) => { if (err) dialog.showErrorBox('Fail', 'Cancel'); else app.quit(); });
|
||
return { isAdmin: false, relaunching: true };
|
||
}
|
||
return { isAdmin: false, relaunching: false };
|
||
});
|
||
ipcMain.handle('select-directory', async (event) => {
|
||
// [修复关键点] 获取触发该事件的窗口(即你的工具子窗口)
|
||
const win = BrowserWindow.fromWebContents(event.sender);
|
||
|
||
// 将 win 作为第一个参数传入 showOpenDialog
|
||
const result = await dialog.showOpenDialog(win, {
|
||
properties: ['openDirectory'],
|
||
title: '选择保存目录',
|
||
buttonLabel: '选择此文件夹',
|
||
// 防止对话框乱跑
|
||
modal: true
|
||
});
|
||
|
||
return result;
|
||
});
|
||
|
||
// ipcMain.handle('select-directory', async () => {
|
||
// const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
|
||
// title: '选择保存目录',
|
||
// properties: ['openDirectory']
|
||
// });
|
||
// if (!canceled && filePaths.length > 0) {
|
||
// return { success: true, path: filePaths[0] };
|
||
// }
|
||
// return { success: false };
|
||
// });
|
||
|
||
ipcMain.handle('clean-empty-dirs', async (event, dirPath) => {
|
||
if (!dirPath || !fs.existsSync(dirPath)) return 0;
|
||
let deletedCount = 0;
|
||
const cleanDir = (d) => {
|
||
let files = fs.readdirSync(d);
|
||
let count = files.length;
|
||
files.forEach(file => {
|
||
const fullPath = path.join(d, file);
|
||
if (fs.statSync(fullPath).isDirectory()) {
|
||
if (cleanDir(fullPath)) count--;
|
||
}
|
||
});
|
||
if (count === 0 && d !== dirPath) {
|
||
fs.rmdirSync(d);
|
||
deletedCount++;
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
try { cleanDir(dirPath); return deletedCount; }
|
||
catch (e) { console.error('Clean dirs failed:', e); throw e; }
|
||
});
|
||
ipcMain.handle('get-system-info', async () => {
|
||
try {
|
||
let timeData = null; try { timeData = si.time(); } catch (e) { }
|
||
const [osData, cpuData, memData, memLayoutData, networkData, systemData, baseboardData, biosData, diskData, userData, versionsData, processesData, chassisData, graphicsData] = await Promise.all([
|
||
safePromise(si.osInfo()), safePromise(si.cpu()), safePromise(si.mem()), safePromise(si.memLayout()), safePromise(si.networkInterfaces()), safePromise(si.system()), safePromise(si.baseboard()), safePromise(si.bios()), safePromise(si.diskLayout()), safePromise(si.users()), safePromise(si.versions('node, npm, git, docker, python, gcc, java, perl, go')), safePromise(si.processes()), safePromise(si.chassis()), safePromise(si.graphics())
|
||
]);
|
||
const displaysFromElectron = screen.getAllDisplays().map(display => ({ id: display.id, resolution: `${display.size.width}x${display.size.height}`, scaleFactor: display.scaleFactor, colorDepth: display.colorDepth, isPrimary: display.bounds.x === 0 && display.bounds.y === 0 }));
|
||
return { osInfo: osData, users: userData, time: { uptime: os.uptime(), ...timeData, locale: app.getLocale() }, cpu: cpuData, mem: { ...memData, layout: memLayoutData }, networkInterfaces: networkData, system: { ...systemData, platformRole: chassisData?.type }, baseboard: baseboardData, bios: biosData, diskLayout: diskData, graphics: { ...graphicsData, displaysFromElectron }, versions: { app: app.getVersion(), electron: process.versions.electron, node: process.versions.node, ...versionsData }, processes: processesData };
|
||
} catch (e) { console.error('Error:', e); throw new Error('Info Error'); }
|
||
});
|
||
ipcMain.handle('set-config-key', async (event, { key, value }) => {
|
||
try {
|
||
db.setConfig(key, value);
|
||
return { success: true };
|
||
} catch (e) {
|
||
console.error('Set config failed:', e);
|
||
return { success: false, error: e.message };
|
||
}
|
||
});
|
||
ipcMain.handle('get-gpu-stats', async () => { try { const g = await si.graphics(); return g?.controllers?.map(c => ({ model: c.model, vendor: c.vendor, load: c.utilizationGpu, temperature: c.temperatureGpu })) || []; } catch { return []; } });
|
||
ipcMain.handle('get-memory-update', async () => { try { const m = await si.mem(); return { free: (m.available / 1024 ** 3).toFixed(2), swapfree: (m.swapfree / 1024 ** 3).toFixed(2), usagePercentage: ((m.used / m.total) * 100).toFixed(2) }; } catch { return { free: 0, swapfree: 0, usagePercentage: 0 }; } });
|
||
ipcMain.handle('get-realtime-stats', () => { try { const cur = process.cpuUsage(cpuUsage.prev), td = process.hrtime(cpuUsage.prevTime), el = td[0] * 1e9 + td[1]; cpuUsage.prev = process.cpuUsage(); cpuUsage.prevTime = process.hrtime(); if (el === 0) return { cpu: '0.00', uptime: '00:00:00' }; const pct = Math.min(100, ((cur.user + cur.system) / 1000 / (el / 1e6)) * 100), up = process.uptime(), h = Math.floor(up / 3600), m = Math.floor((up % 3600) / 60), s = Math.floor(up % 60); return { cpu: pct.toFixed(2), uptime: `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` }; } catch { return { cpu: '0.00', uptime: '00:00:00' }; } });
|
||
ipcMain.handle('compress-files', (event, data) => new Promise((resolve) => {
|
||
dialog.showSaveDialog(mainWindow, { title: 'Save Archive', defaultPath: `archive_${Date.now()}.${data.format}`, filters: [{ name: 'Archive', extensions: [data.format] }] }).then(res => {
|
||
if (res.canceled || !res.filePath) return resolve({ success: false, error: 'Cancel' });
|
||
const w = new Worker(compressionWorkerPath);
|
||
w.on('message', (msg) => { if (msg.status === 'complete') { resolve({ success: true, path: msg.path }); w.terminate(); } else if (msg.status === 'error') { resolve({ success: false, error: msg.error }); w.terminate(); } });
|
||
w.postMessage({ type: 'compress', data: { ...data, output: res.filePath } });
|
||
});
|
||
}));
|
||
ipcMain.handle('decompress-file', (event, data) => new Promise((resolve) => {
|
||
dialog.showOpenDialog(mainWindow, { title: 'Select Folder', properties: ['openDirectory'] }).then(res => {
|
||
if (res.canceled || !res.filePaths.length) return resolve({ success: false, error: 'Cancel' });
|
||
const w = new Worker(compressionWorkerPath);
|
||
w.on('message', (msg) => { if (msg.status === 'complete') { shell.openPath(msg.path); resolve({ success: true, path: msg.path }); w.terminate(); } else if (msg.status === 'error') { resolve({ success: false, error: msg.error }); w.terminate(); } });
|
||
w.postMessage({ type: 'decompress', data: { ...data, targetDir: res.filePaths[0] } });
|
||
});
|
||
}));
|
||
|
||
|
||
|
||
function createToolWindow(viewName, toolId, theme) {
|
||
const k = `${viewName}_${toolId}`; if (toolWindows.has(k)) { const w = toolWindows.get(k); if (w) { if (w.isMinimized()) w.restore(); w.show(); w.focus(); return; } }
|
||
const w = new BrowserWindow({ width: 1024, height: 768, frame: false, show: false, transparent: true, hasShadow: false, resizable: true, roundedCorners: true, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, webviewTag: true, devTools: !app.isPackaged } });
|
||
const html = viewName === 'view-browser' ? 'src/views/view-browser.html' : 'src/views/view-tool.html';
|
||
w.loadFile(path.join(__dirname, html), { search: new URLSearchParams({ theme, tool: toolId || '' }).toString() });
|
||
w.once('ready-to-show', () => { w.show(); w.focus(); }); w.on('closed', () => toolWindows.delete(k)); toolWindows.set(k, w);
|
||
}
|
||
// --- [新增] 三国杀批量下载专用处理器 ---
|
||
ipcMain.handle('download-file-direct', async (event, { url, saveRoot, subDir, fileName }) => {
|
||
const targetDir = path.join(saveRoot, subDir);
|
||
const filePath = path.join(targetDir, fileName);
|
||
const tempPath = filePath + '.tmp';
|
||
|
||
try {
|
||
// 1. 检查目录是否存在,不存在则创建 (对应 Python: os.makedirs)
|
||
if (!fs.existsSync(targetDir)) {
|
||
fs.mkdirSync(targetDir, { recursive: true });
|
||
}
|
||
|
||
// 2. 检查文件是否已存在且不为空 (对应 Python: SKIP 逻辑)
|
||
if (fs.existsSync(filePath)) {
|
||
const stats = fs.statSync(filePath);
|
||
if (stats.size > 0) return { status: 'SKIPPED' };
|
||
}
|
||
|
||
// 3. 发起请求 (使用 Node.js 原生 https)
|
||
return new Promise((resolve) => {
|
||
const request = https.get(url, {
|
||
headers: {
|
||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||
"Referer": "https://web.sanguosha.com/",
|
||
"Connection": "keep-alive"
|
||
},
|
||
timeout: 10000 // 10秒超时
|
||
}, (response) => {
|
||
// 处理 404 (对应 Python: NOT_FOUND)
|
||
if (response.statusCode === 404) {
|
||
response.resume(); // 消耗掉数据流
|
||
resolve({ status: 'NOT_FOUND' });
|
||
return;
|
||
}
|
||
|
||
// 处理非 200 错误
|
||
if (response.statusCode !== 200) {
|
||
response.resume();
|
||
resolve({ status: 'ERROR', code: response.statusCode });
|
||
return;
|
||
}
|
||
|
||
// 4. 写入临时文件 (对应 Python: with open(temp_path))
|
||
const fileStream = fs.createWriteStream(tempPath);
|
||
|
||
response.pipe(fileStream);
|
||
|
||
fileStream.on('finish', () => {
|
||
fileStream.close(() => {
|
||
// 5. 原子重命名 (对应 Python: os.rename)
|
||
// 校验:这里可以加 Content-Length 校验,简化起见先略过
|
||
try {
|
||
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
||
fs.renameSync(tempPath, filePath);
|
||
resolve({ status: 'SUCCESS' });
|
||
} catch (err) {
|
||
resolve({ status: 'ERROR', message: err.message });
|
||
}
|
||
});
|
||
});
|
||
|
||
fileStream.on('error', (err) => {
|
||
fs.unlink(tempPath, () => {}); // 删除临时文件
|
||
resolve({ status: 'ERROR', message: err.message });
|
||
});
|
||
});
|
||
|
||
request.on('error', (err) => {
|
||
resolve({ status: 'ERROR', message: err.message });
|
||
});
|
||
|
||
request.on('timeout', () => {
|
||
request.destroy();
|
||
resolve({ status: 'TIMEOUT' });
|
||
});
|
||
});
|
||
|
||
} catch (error) {
|
||
return { status: 'ERROR', message: error.message };
|
||
}
|
||
});
|
||
ipcMain.on('open-acknowledgements-window', (e, t, v) => { if (acknowledgementsWindow) { acknowledgementsWindow.focus(); return; } acknowledgementsWindow = new BrowserWindow({ width: 400, height: 520, frame: false, resizable: false, show: false, backgroundColor: t === 'dark' ? '#1d1d1f' : '#ffffff', hasShadow: true, skipTaskbar: true, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true } }); acknowledgementsWindow.loadFile(path.join(__dirname, 'src/acknowledgements.html'), { search: new URLSearchParams({ theme: t, versions: JSON.stringify(v || {}) }).toString() }); acknowledgementsWindow.once('ready-to-show', () => acknowledgementsWindow.show()); acknowledgementsWindow.on('closed', () => acknowledgementsWindow = null); });
|
||
ipcMain.on('open-secret-window', (e, t) => createToolWindow('view-browser', 'archive', t));
|
||
ipcMain.on('open-tool-window', (e, v, i, t) => createToolWindow(v, i, t));
|
||
ipcMain.on('close-current-window', (e) => BrowserWindow.fromWebContents(e.sender)?.close());
|
||
ipcMain.on('secret-window-minimize', (e) => BrowserWindow.fromWebContents(e.sender)?.minimize());
|
||
ipcMain.on('secret-window-maximize', (e) => { const w = BrowserWindow.fromWebContents(e.sender); if (w) w.isMaximized() ? w.unmaximize() : w.maximize(); });
|
||
const iconDir = path.join(rootDir, 'data', 'icons'); if (!fs.existsSync(iconDir)) try { fs.mkdirSync(iconDir, { recursive: true }); } catch (e) { }
|
||
const downloadFile = (url, dest) => new Promise((resolve, reject) => { const f = fs.createWriteStream(dest); https.get(url, r => { if (r.statusCode !== 200) reject(new Error(r.statusCode)); r.pipe(f); f.on('finish', () => f.close(resolve)); }).on('error', e => { fs.unlink(dest, () => { }); reject(e); }); });
|
||
ipcMain.handle('get-cached-icon', async (e, id, url) => { if (!url) return null; const ext = path.extname(url) || '.svg'; const p = path.join(iconDir, `${id}${ext}`); if (fs.existsSync(p) && fs.statSync(p).size > 0) return `file://${p.replace(/\\/g, '/')}`; try { await downloadFile(url, p); return `file://${p.replace(/\\/g, '/')}`; } catch (e) { return null; } });
|
||
const fontsDir = path.join(rootDir, 'data', 'fonts'); if (!fs.existsSync(fontsDir)) try { fs.mkdirSync(fontsDir, { recursive: true }); } catch (e) { }
|
||
ipcMain.handle('download-font', async (e, { fontName, fontUrl }) => { const u = new URL(fontUrl); const ext = path.extname(u.pathname) || '.woff2'; const p = path.join(fontsDir, `${fontName.replace(/[^a-zA-Z0-9_-]/g, '_')}${ext}`); if (fs.existsSync(p) && fs.statSync(p).size > 102400) return { success: true, path: `file://${p.replace(/\\/g, '/')}`, cached: true }; try { await downloadFile(fontUrl, p); return { success: true, path: `file://${p.replace(/\\/g, '/')}`, cached: false }; } catch (e) { return { success: false, error: e.message }; } });
|
||
ipcMain.handle('check-secret-access', async () => { const b = db.getConfig('secret_ip_ban_until'); if (b && Date.now() < parseInt(b)) return { status: 'ip-banned', until: parseInt(b) }; const ip = await getPublicIP(); if (ip && (await fetchIpBanList()).includes(ip)) { const t = Date.now() + 3600000; db.setConfig('secret_ip_ban_until', t.toString()); return { status: 'ip-banned', until: t }; } const l = db.getConfig('secret_code_lockout_until'); if (l && Date.now() < parseInt(l)) return { status: 'locked', until: parseInt(l) }; return { status: 'ok' }; });
|
||
ipcMain.handle('record-secret-failure', () => { let a = parseInt(db.getConfig('secret_code_attempts') || '0') + 1; if (a >= 5) { const t = Date.now() + 3600000; db.setConfig('secret_code_lockout_until', t.toString()); db.setConfig('secret_code_attempts', '0'); return { isLocked: true, attemptsLeft: 0, lockoutUntil: t }; } db.setConfig('secret_code_attempts', a.toString()); return { isLocked: false, attemptsLeft: 5 - a }; });
|
||
ipcMain.handle('reset-secret-attempts', () => { db.setConfig('secret_code_attempts', '0'); db.setConfig('secret_code_lockout_until', '0'); db.setConfig('secret_ip_ban_until', '0'); });
|
||
ipcMain.on('request-new-window', (e, u, o) => { const p = BrowserWindow.fromWebContents(e.sender); const w = new BrowserWindow({ width: o.width || 1024, height: o.height || 768, parent: p || mainWindow, frame: true, autoHideMenuBar: true, webPreferences: { contextIsolation: true, sandbox: false, webviewTag: false } }); w.loadURL(u); w.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; }); });
|
||
// [新增] 处理网页重定向下载 (小窗模式)
|
||
ipcMain.handle('open-download-window', async (event, url) => {
|
||
// 创建一个较小的临时窗口用于网页下载
|
||
const win = new BrowserWindow({
|
||
width: 1024,
|
||
height: 768,
|
||
title: '下载中心',
|
||
autoHideMenuBar: true,
|
||
webPreferences: {
|
||
nodeIntegration: false,
|
||
contextIsolation: true,
|
||
// 允许下载
|
||
}
|
||
});
|
||
|
||
// 监听该窗口内的下载事件
|
||
win.webContents.session.on('will-download', (event, item, webContents) => {
|
||
// 这里可以选择接管下载,或者让 Electron 默认处理(弹出保存框)
|
||
// 为了简单起见,我们让用户自己选择保存位置,或者我们可以捕获它
|
||
// item.setSavePath(...)
|
||
|
||
// 监控下载状态并发送给主窗口 (可选优化)
|
||
item.on('updated', (event, state) => {
|
||
if (state === 'interrupted') {
|
||
console.log('Download is interrupted but can be resumed')
|
||
} else if (state === 'progressing') {
|
||
if (item.isPaused()) {
|
||
console.log('Download is paused')
|
||
} else {
|
||
console.log(`Received bytes: ${item.getReceivedBytes()}`)
|
||
}
|
||
}
|
||
})
|
||
item.once('done', (event, state) => {
|
||
if (state === 'completed') {
|
||
console.log('Download successfully')
|
||
// 下载完成后询问是否打开文件等逻辑
|
||
} else {
|
||
console.log(`Download failed: ${state}`)
|
||
}
|
||
})
|
||
});
|
||
|
||
win.loadURL(url);
|
||
return { success: true };
|
||
});
|
||
|
||
|
||
async function safePromise(p) { try { return await p; } catch (e) { return null; } }
|
||
|
||
// 文件哈希计算
|
||
ipcMain.handle('calculate-hash', async (event, bufferArray, algorithm) => {
|
||
try {
|
||
const crypto = require('crypto');
|
||
// bufferArray 是 Uint8Array 的数组形式,需要转换为 Buffer
|
||
const buffer = Buffer.from(bufferArray);
|
||
const hash = crypto.createHash(algorithm || 'md5');
|
||
hash.update(buffer);
|
||
return hash.digest('hex');
|
||
} catch (e) {
|
||
console.error('[Hash] Calculation error:', e);
|
||
throw new Error(e.message);
|
||
}
|
||
});
|
||
|
||
// ==========================================
|
||
// --- [重构] 接口健康度校验 IPC 处理 ---
|
||
// ==========================================
|
||
|
||
/**
|
||
* [重构] 获取接口健康度状态
|
||
*/
|
||
ipcMain.handle('get-api-health-status', async (event, apiId) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
if (apiId) {
|
||
// 获取单个接口的健康度
|
||
const latest = db.getLatestApiHealth(apiId);
|
||
const history = db.getApiHealthHistory(apiId, 10);
|
||
const stats = db.getApiCallStats(apiId);
|
||
|
||
return {
|
||
success: true,
|
||
data: {
|
||
latest,
|
||
history,
|
||
stats
|
||
}
|
||
};
|
||
} else {
|
||
// 获取所有接口的健康度
|
||
const reservations = db.getAllApiReservations();
|
||
const allHealth = {};
|
||
|
||
for (const reservation of reservations) {
|
||
const latest = db.getLatestApiHealth(reservation.api_id);
|
||
const stats = db.getApiCallStats(reservation.api_id);
|
||
allHealth[reservation.api_id] = {
|
||
reservation,
|
||
latest,
|
||
stats
|
||
};
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
data: allHealth
|
||
};
|
||
}
|
||
} catch (error) {
|
||
console.error('[API Health] Get status error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
/**
|
||
* [重构] 记录接口调用
|
||
*/
|
||
ipcMain.handle('record-api-call', async (event, callData) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
db.recordApiCall(callData);
|
||
return { success: true };
|
||
} catch (error) {
|
||
console.error('[API Call] Record error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
/**
|
||
* [重构] 记录接口健康度
|
||
*/
|
||
ipcMain.handle('record-api-health', async (event, healthData) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
db.recordApiHealth(healthData);
|
||
return { success: true };
|
||
} catch (error) {
|
||
console.error('[API Health] Record error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
/**
|
||
* [重构] 获取接口预留位置
|
||
*/
|
||
ipcMain.handle('get-api-reservations', async (event, category) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
const reservations = db.getAllApiReservations(category);
|
||
return {
|
||
success: true,
|
||
data: reservations
|
||
};
|
||
} catch (error) {
|
||
console.error('[API Reservations] Get error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
/**
|
||
* [重构] 注册新的接口预留位置
|
||
*/
|
||
ipcMain.handle('register-api-reservation', async (event, reservation) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
db.registerApiReservation(reservation);
|
||
return { success: true };
|
||
} catch (error) {
|
||
console.error('[API Reservations] Register error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
/**
|
||
* [重构] 更新接口预留位置
|
||
*/
|
||
ipcMain.handle('update-api-reservation', async (event, apiId, updates) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
db.updateApiReservation(apiId, updates);
|
||
return { success: true };
|
||
} catch (error) {
|
||
console.error('[API Reservations] Update error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
// ==========================================
|
||
// --- [重构] 工具健康检查数据库操作 IPC 处理 ---
|
||
// ==========================================
|
||
|
||
/**
|
||
* [重构] 记录工具健康检查结果
|
||
*/
|
||
ipcMain.handle('record-tool-health-check-result', async (event, resultData) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
db.recordToolHealthCheckResult(resultData);
|
||
return { success: true };
|
||
} catch (error) {
|
||
console.error('[Tool Health Check] Record result error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
/**
|
||
* [重构] 获取工具最新健康检查结果
|
||
*/
|
||
ipcMain.handle('get-latest-tool-health-check-result', async (event, toolId) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
const result = db.getLatestToolHealthCheckResult(toolId);
|
||
return {
|
||
success: true,
|
||
data: result
|
||
};
|
||
} catch (error) {
|
||
console.error('[Tool Health Check] Get latest result error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
/**
|
||
* [重构] 获取工具健康检查历史
|
||
*/
|
||
ipcMain.handle('get-tool-health-check-history', async (event, toolId, limit) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
const history = db.getToolHealthCheckHistory(toolId, limit || 100);
|
||
return {
|
||
success: true,
|
||
data: history
|
||
};
|
||
} catch (error) {
|
||
console.error('[Tool Health Check] Get history error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
/**
|
||
* [新增] 获取所有工具健康检查摘要(用于显示检查历史)
|
||
*/
|
||
ipcMain.handle('get-all-tool-health-check-summaries', async (event, limit) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
const summaries = db.getAllToolHealthCheckSummaries(limit || 50);
|
||
return {
|
||
success: true,
|
||
data: summaries
|
||
};
|
||
} catch (error) {
|
||
console.error('[Tool Health Check] Get all summaries error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
/**
|
||
* [重构] 获取指定日期的所有工具健康检查结果
|
||
*/
|
||
ipcMain.handle('get-tool-health-check-results-by-date', async (event, checkDate) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
const results = db.getToolHealthCheckResultsByDate(checkDate);
|
||
return {
|
||
success: true,
|
||
data: results
|
||
};
|
||
} catch (error) {
|
||
console.error('[Tool Health Check] Get results by date error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
/**
|
||
* [重构] 获取最后一次健康检查的日期
|
||
*/
|
||
ipcMain.handle('get-last-health-check-date', async (event) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
const lastDate = db.getLastHealthCheckDate();
|
||
return {
|
||
success: true,
|
||
data: lastDate
|
||
};
|
||
} catch (error) {
|
||
console.error('[Tool Health Check] Get last check date error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
/**
|
||
* [重构] 检查是否应该强制健康检查(5天)
|
||
*/
|
||
ipcMain.handle('should-force-health-check', async (event, days) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
const shouldForce = db.shouldForceHealthCheck(days || 5);
|
||
return {
|
||
success: true,
|
||
data: shouldForce
|
||
};
|
||
} catch (error) {
|
||
console.error('[Tool Health Check] Check force check error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
/**
|
||
* [重构] 保存工具健康检查汇总
|
||
*/
|
||
ipcMain.handle('save-tool-health-check-summary', async (event, summaryData) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
db.saveToolHealthCheckSummary(summaryData);
|
||
return { success: true };
|
||
} catch (error) {
|
||
console.error('[Tool Health Check] Save summary error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
/**
|
||
* [重构] 获取工具健康检查汇总
|
||
*/
|
||
ipcMain.handle('get-tool-health-check-summary', async (event, checkDate) => {
|
||
try {
|
||
if (!db) return { success: false, error: 'Database not initialized' };
|
||
|
||
const summary = db.getToolHealthCheckSummary(checkDate);
|
||
return {
|
||
success: true,
|
||
data: summary
|
||
};
|
||
} catch (error) {
|
||
console.error('[Tool Health Check] Get summary error:', error);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}); |