// main.cjs —— 光湖桥聊天 · Electron 壳 // 集成方式:spawn 光湖桥节点子进程(stdio JSON 模式),IPC 转发事件 const { app, BrowserWindow, ipcMain } = require('electron'); const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); // ── 配置:节点号 / 会合点 / node 可执行路径 ── const cfgPath = path.join(__dirname, 'config.json'); let cfg = {}; try { cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); } catch { /* 用默认值 */ } const NODE_ID = process.env.GHB_NODE_ID || cfg.nodeId || 'GHB-ELECTRON'; const RELAY = process.env.GHB_RELAY || cfg.relay || '127.0.0.1:9701'; const NODE_BIN = process.env.GHB_NODE_BIN || cfg.nodeBin || 'node'; // 打包态下 bridge 脚本被 unpack 到 app.asar.unpacked(外部 node 读不了 asar 内文件) const BRIDGE_MJS = [ path.join(__dirname, '..', 'app.asar.unpacked', 'bridge', 'bridge-node.mjs'), path.join(__dirname, 'bridge', 'bridge-node.mjs'), ].find((p) => fs.existsSync(p)); let win = null; let bridge = null; let winReady = false; const evQueue = []; // 窗口就绪前到达的事件先缓存,就绪后补发 function forward(ev) { if (!winReady || !win || win.isDestroyed()) { evQueue.push(ev); return; } win.webContents.send('ghb:event', ev); } function startBridge() { const dataDir = path.join(app.getPath('userData'), 'bridge-node'); const args = [BRIDGE_MJS, 'node', '--stdio', '--id', NODE_ID, '--relay', RELAY, '--state-dir', dataDir]; bridge = spawn(NODE_BIN, args, { stdio: ['pipe', 'pipe', 'pipe'] }); let buf = ''; bridge.stdout.on('data', (chunk) => { buf += chunk.toString('utf8'); let i; while ((i = buf.indexOf('\n')) >= 0) { const line = buf.slice(0, i).trim(); buf = buf.slice(i + 1); if (!line) continue; try { const ev = JSON.parse(line); forward(ev); } catch { /* 非 JSON 行忽略 */ } } }); bridge.stderr.on('data', (d) => console.error(`[bridge] ${d.toString().trim()}`)); bridge.on('exit', (code) => { console.error(`[bridge] 子进程退出 code=${code}`); if (win && !win.isDestroyed()) win.webContents.send('ghb:event', { event: 'error', message: `桥接进程退出(${code})` }); }); } function createWindow() { win = new BrowserWindow({ width: 920, height: 660, title: `光湖桥 · 聊天 · ${NODE_ID}`, backgroundColor: '#0b1220', webPreferences: { preload: path.join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, }, }); win.loadFile(path.join(__dirname, 'chat.html')); win.webContents.once('did-finish-load', () => { winReady = true; for (const ev of evQueue.splice(0)) win.webContents.send('ghb:event', ev); // 补发早到事件 }); } app.whenReady().then(() => { startBridge(); createWindow(); // 自检钩子:GHB_AUTOSEND="对端|文本|延迟秒" 自动发一条;GHB_SHOT=/path.png 延迟截屏取证 const auto = process.env.GHB_AUTOSEND; if (auto) { const [to, text, delay] = auto.split('|'); setTimeout(() => { // 走渲染进程的 UI 发送路径,让气泡/状态行真实渲染出来 if (win && !win.isDestroyed()) { win.webContents.send('ghb:event', { event: '__autosend', to, text }); console.log(`[autotest] 已触发 UI 自动发送 → ${to}`); } }, (Number(delay) || 3) * 1000); } const shot = process.env.GHB_SHOT; if (shot) { setTimeout(async () => { try { const img = await win.capturePage(); fs.writeFileSync(shot, img.toPNG()); console.log(`[autotest] 截屏已存 ${shot}`); } catch (e) { console.error(`[autotest] 截屏失败:${e.message}`); } }, (Number(process.env.GHB_SHOT_DELAY) || 8) * 1000); } app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); ipcMain.on('ghb:send', (_e, to, text) => { if (bridge && bridge.stdin.writable) { bridge.stdin.write(JSON.stringify({ cmd: 'send', to, text }) + '\n'); } }); app.on('window-all-closed', () => { if (bridge) bridge.kill('SIGTERM'); if (process.platform !== 'darwin') app.quit(); else app.quit(); }); app.on('quit', () => { if (bridge) bridge.kill('SIGTERM'); });