Files
YMhut-box-C-/box-old/main.js
T
QWQLwToo 46a3674381
build-winui / winui (push) Has been cancelled
Add legacy Electron app
2026-06-26 13:29:02 +08:00

1448 lines
67 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 };
}
});