// 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 }; } });