client: Windows 客户端(WinUI3 壳 + agent-core;调试用 Electron 壳存档);localserver token 改为常数时间比较
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user