Files
EveryPublish/client/desktop/main.js
T

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