178 lines
5.9 KiB
JavaScript
178 lines
5.9 KiB
JavaScript
const { app, BrowserWindow, Tray, Menu, ipcMain, shell, nativeImage } = require('electron');
|
|
const { spawn } = require('child_process');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const http = require('http');
|
|
|
|
const isWin = process.platform === 'win32';
|
|
const isSmoke = process.argv.includes('--smoke');
|
|
const minimized = process.argv.includes('--minimized');
|
|
|
|
let win = null;
|
|
let tray = null;
|
|
let coreProc = null;
|
|
let corePort = 0;
|
|
let coreToken = '';
|
|
let localJsonTimer = null;
|
|
let quitting = false;
|
|
|
|
const dataDir = path.join(app.getPath('userData'), 'data');
|
|
const coreBin = isWin ? 'agent-core.exe' : 'agent-core';
|
|
const coreExe = app.isPackaged
|
|
? path.join(process.resourcesPath, coreBin)
|
|
: path.join(__dirname, '..', 'dist', coreBin);
|
|
|
|
function startCore() {
|
|
if (!fs.existsSync(coreExe)) {
|
|
console.error('[core] binary not found: ' + coreExe);
|
|
win?.webContents.send('core-error', '核心程序缺失:' + coreExe);
|
|
return;
|
|
}
|
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
try { coreProc?.kill(); } catch (e) { /* ignore */ }
|
|
coreProc = spawn(coreExe, ['-dir', dataDir], { windowsHide: true });
|
|
coreProc.on('exit', (code) => {
|
|
coreProc = null;
|
|
win?.webContents.send('core-exit', code);
|
|
});
|
|
pollLocalJson();
|
|
}
|
|
|
|
function pollLocalJson() {
|
|
if (localJsonTimer) clearInterval(localJsonTimer);
|
|
localJsonTimer = setInterval(() => {
|
|
try {
|
|
const info = JSON.parse(fs.readFileSync(path.join(dataDir, 'local.json'), 'utf8'));
|
|
if (info.port && info.token) {
|
|
corePort = info.port;
|
|
coreToken = info.token;
|
|
clearInterval(localJsonTimer);
|
|
win?.webContents.send('core-ready');
|
|
}
|
|
} catch (e) { /* 核心尚未就绪 */ }
|
|
}, 800);
|
|
}
|
|
|
|
function coreRequest(method, p, body) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!corePort || !coreToken) { reject(new Error('核心未就绪')); return; }
|
|
const data = body ? JSON.stringify(body) : null;
|
|
const req = http.request({
|
|
host: '127.0.0.1',
|
|
port: corePort,
|
|
path: p,
|
|
method: method,
|
|
headers: {
|
|
'X-Agent-Token': coreToken,
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': data ? Buffer.byteLength(data) : 0,
|
|
},
|
|
}, (res) => {
|
|
let raw = '';
|
|
res.on('data', (c) => { raw += c; });
|
|
res.on('end', () => {
|
|
try { resolve(JSON.parse(raw)); } catch (e) { resolve({ raw }); }
|
|
});
|
|
});
|
|
req.on('error', reject);
|
|
req.setTimeout(8000, () => { req.destroy(new Error('核心请求超时')); });
|
|
if (data) req.write(data);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
// ---- IPC(渲染进程 → 核心/系统) ----
|
|
ipcMain.handle('core:status', () => coreRequest('GET', '/status'));
|
|
ipcMain.handle('core:pair', (e, code, name) => coreRequest('POST', '/pair', { code, name }));
|
|
ipcMain.handle('core:challenges', () => coreRequest('GET', '/challenges'));
|
|
ipcMain.handle('core:solve', (e, id, value) => coreRequest('POST', '/challenges/' + encodeURIComponent(id), { value }));
|
|
ipcMain.handle('core:get-config', () => readConfig());
|
|
ipcMain.handle('core:save-config', (e, cfg) => { writeConfig(cfg); startCore(); return true; });
|
|
ipcMain.handle('core:restart', () => { startCore(); return true; });
|
|
ipcMain.handle('app:open-data-dir', () => shell.openPath(dataDir));
|
|
ipcMain.handle('app:open-web', (e, url) => shell.openExternal(url));
|
|
ipcMain.handle('app:set-autostart', (e, on) => {
|
|
app.setLoginItemSettings({ openAtLogin: !!on });
|
|
return app.getLoginItemSettings().openAtLogin;
|
|
});
|
|
ipcMain.handle('app:get-autostart', () => app.getLoginItemSettings().openAtLogin);
|
|
ipcMain.handle('app:quit', () => { quitting = true; app.quit(); });
|
|
|
|
function readConfig() {
|
|
try { return JSON.parse(fs.readFileSync(path.join(dataDir, 'config.json'), 'utf8')); }
|
|
catch (e) { return { server: '', fakeAll: false }; }
|
|
}
|
|
function writeConfig(cfg) {
|
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
fs.writeFileSync(path.join(dataDir, 'config.json'), JSON.stringify(cfg, null, 2));
|
|
}
|
|
|
|
function createTray() {
|
|
let img = nativeImage.createFromPath(path.join(__dirname, 'icons', 'app.png'));
|
|
if (img.isEmpty()) img = nativeImage.createEmpty();
|
|
tray = new Tray(img);
|
|
tray.setToolTip('EveryPublish 客户端');
|
|
tray.setContextMenu(Menu.buildFromTemplate([
|
|
{ label: '打开主窗口', click: showWin },
|
|
{ type: 'separator' },
|
|
{ label: '退出', click: () => { quitting = true; app.quit(); } },
|
|
]));
|
|
tray.on('click', showWin);
|
|
}
|
|
|
|
function showWin() {
|
|
if (win) { win.show(); win.focus(); }
|
|
}
|
|
|
|
function createWindow() {
|
|
win = new BrowserWindow({
|
|
width: 980, height: 660,
|
|
minWidth: 780, minHeight: 540,
|
|
title: 'EveryPublish 客户端',
|
|
autoHideMenuBar: true,
|
|
webPreferences: {
|
|
preload: path.join(__dirname, 'preload.js'),
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
},
|
|
});
|
|
win.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
|
win.on('close', (e) => {
|
|
if (!quitting) { e.preventDefault(); win.hide(); }
|
|
});
|
|
}
|
|
|
|
app.whenReady().then(() => {
|
|
app.setAppUserModelId('com.everypublish.client');
|
|
if (isSmoke) {
|
|
// 冒烟模式:启动 core → 等就绪 → 查状态 → 退出(无窗口)
|
|
runSmoke();
|
|
return;
|
|
}
|
|
createTray();
|
|
createWindow();
|
|
if (minimized) win?.hide();
|
|
startCore();
|
|
});
|
|
|
|
async function runSmoke() {
|
|
startCore();
|
|
const deadline = Date.now() + 30000;
|
|
while (!coreToken && Date.now() < deadline) {
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
}
|
|
if (!coreToken) { console.log('SMOKE_RESULT ' + JSON.stringify({ error: 'core 未就绪' })); app.exit(1); return; }
|
|
try {
|
|
const status = await coreRequest('GET', '/status');
|
|
console.log('SMOKE_RESULT ' + JSON.stringify(status));
|
|
app.exit(0);
|
|
} catch (e) {
|
|
console.log('SMOKE_RESULT ' + JSON.stringify({ error: String(e) }));
|
|
app.exit(1);
|
|
}
|
|
}
|
|
|
|
app.on('before-quit', () => { quitting = true; try { coreProc?.kill(); } catch (e) { /* ignore */ } });
|
|
app.on('window-all-closed', () => { /* 驻留托盘不退出 */ });
|
|
app.on('activate', showWin);
|