3.1.0稳定版

This commit is contained in:
2025-12-01 20:04:52 +08:00
parent f7e1cad639
commit 55542dd212
3 changed files with 107 additions and 40 deletions

View File

@@ -5,7 +5,8 @@ const { EventEmitter } = require('events');
const CFG = { const CFG = {
protocol: process.env.RABBIT_PROTOCOL || 'amqp', // 'amqp' | 'amqps' protocol: process.env.RABBIT_PROTOCOL || 'amqp', // 'amqp' | 'amqps'
// host: process.env.RABBIT_HOST || '192.168.1.144', // host: process.env.RABBIT_HOST || '192.168.1.144',
host: process.env.RABBIT_HOST || 'crawlclient.api.yolozs.com', // host: process.env.RABBIT_HOST || 'crawlclient.api.yolozs.com',
host: process.env.RABBIT_HOST || '47.79.98.113',
port: Number(process.env.RABBIT_PORT || 5672), port: Number(process.env.RABBIT_PORT || 5672),
user: process.env.RABBIT_USER || 'tkdata', user: process.env.RABBIT_USER || 'tkdata',
pass: process.env.RABBIT_PASS || '6rARaRj8Z7UG3ahLzh', pass: process.env.RABBIT_PASS || '6rARaRj8Z7UG3ahLzh',
@@ -58,31 +59,31 @@ async function createConnection() {
// 心跳超时常见,避免重复噪音 // 心跳超时常见,避免重复噪音
const msg = e?.message || String(e); const msg = e?.message || String(e);
if (msg && /heartbeat/i.test(msg)) { if (msg && /heartbeat/i.test(msg)) {
console.error('[AMQP] connection error (heartbeat):', msg); console.error('[AMQP] 连接错误 (心跳):', msg);
} else { } else {
console.error('[AMQP] connection error:', msg); console.error('[AMQP] 连接错误:', msg);
} }
emitter.emit('error', e); emitter.emit('error', e);
}); });
connection.on('close', () => { connection.on('close', () => {
if (closing) return; // 正在关闭时不重连 if (closing) return; // 正在关闭时不重连
console.error('[AMQP] connection closed'); console.error('[AMQP] 连接已关闭');
conn = null; pubCh = null; conCh = null; conn = null; pubCh = null; conCh = null;
scheduleReconnect(); scheduleReconnect();
}); });
// Broker 侧内存/磁盘压力会 block 连接 // Broker 侧内存/磁盘压力会 block 连接
connection.on('blocked', (reason) => { connection.on('blocked', (reason) => {
console.warn('[AMQP] connection blocked by broker:', reason); console.warn('[AMQP] 连接被代理阻塞::', reason);
emitter.emit('blocked', reason); emitter.emit('blocked', reason);
}); });
connection.on('unblocked', () => { connection.on('unblocked', () => {
console.log('[AMQP] connection unblocked'); console.log('[AMQP] 链接解锁');
emitter.emit('unblocked'); emitter.emit('unblocked');
}); });
console.log(`[AMQP] connected to ${CFG.host} (hb=${CFG.heartbeat}s)`); console.log(`[AMQP] 已连接到 ${CFG.host} (hb=${CFG.heartbeat}s)`);
return connection; return connection;
} }
@@ -92,13 +93,13 @@ async function ensureChannels() {
if (!pubCh) { if (!pubCh) {
pubCh = await conn.createConfirmChannel(); pubCh = await conn.createConfirmChannel();
pubCh.on('error', e => console.error('[AMQP] pub channel error:', e?.message || e)); pubCh.on('error', e => console.error('[AMQP] 通道错误:', e?.message || e));
pubCh.on('close', () => { pubCh = null; if (!closing) scheduleReconnect(); }); pubCh.on('close', () => { pubCh = null; if (!closing) scheduleReconnect(); });
} }
if (!conCh) { if (!conCh) {
conCh = await conn.createChannel(); conCh = await conn.createChannel();
conCh.on('error', e => console.error('[AMQP] con channel error:', e?.message || e)); conCh.on('error', e => console.error('[AMQP] 通道错误:', e?.message || e));
conCh.on('close', () => { conCh = null; if (!closing) scheduleReconnect(); }); conCh.on('close', () => { conCh = null; if (!closing) scheduleReconnect(); });
} }
} }
@@ -117,12 +118,12 @@ function scheduleReconnect() {
reconnecting = false; reconnecting = false;
backoff = 1000; backoff = 1000;
emitter.emit('reconnected'); emitter.emit('reconnected');
console.log('[AMQP] reconnected and consumers resumed'); console.log('[AMQP] 重新连接并恢复了消费者');
} catch (e) { } catch (e) {
const base = Math.min(backoff, MAX_BACKOFF); const base = Math.min(backoff, MAX_BACKOFF);
// 加抖动,避免雪崩:在 75%~125% 之间浮动 // 加抖动,避免雪崩:在 75%~125% 之间浮动
const jitter = base * (0.75 + Math.random() * 0.5); const jitter = base * (0.75 + Math.random() * 0.5);
console.warn(`[AMQP] reconnect failed: ${e?.message || e}; retry in ${Math.round(jitter)}ms`); console.warn(`[AMQP] 重连失败: ${e?.message || e}; retry in ${Math.round(jitter)}ms`);
backoff = Math.min(backoff * 1.6, MAX_BACKOFF); backoff = Math.min(backoff * 1.6, MAX_BACKOFF);
reconnectTimer = setTimeout(attempt, jitter); reconnectTimer = setTimeout(attempt, jitter);
} }
@@ -193,7 +194,7 @@ async function startOneConsumer(queueName, onMessage, options = {}, isResume = f
consumers.set(queueName, { onMessage, options, consumerTag: consumeResult.consumerTag }); consumers.set(queueName, { onMessage, options, consumerTag: consumeResult.consumerTag });
} }
console.log(`[*] consuming "${queueName}" (prefetch=${prefetch})`); console.log(`[*] 消费 "${queueName}" (预取=${prefetch})`);
return consumeResult; return consumeResult;
} }

120
main.js
View File

@@ -17,8 +17,20 @@ Object.assign(console, log.functions)
// 存储 SSE 服务器的引用 // 存储 SSE 服务器的引用
let sseServer = null; let sseServer = null;
// ✅ 本地 HTTPS 白名单(按你的实际地址改端口)
const LOCAL_HTTPS_WHITELIST = [
'https://192.168.1.218:34567',
'https://192.168.1.231:34567',
'https://127.0.0.1:34567',
'https://localhost:34567'
]
let userData = { tenantId: null, userId: null } let userData = { tenantId: null, userId: null }
const { exec, spawn, execFile } = require('child_process'); // 如果你用 exec 启动 scrcpy保留此行 const { exec, spawn, execFile } = require('child_process'); // 如果你用 exec 启动 scrcpy保留此行
// app.commandLine.appendSwitch('remote-debugging-port', '9222'); //远程控制台端口F12
app.commandLine.appendSwitch('enable-precise-memory-info') app.commandLine.appendSwitch('enable-precise-memory-info')
app.commandLine.appendSwitch('js-flags', '--expose-gc --max-old-space-size=4096') // 以 MB 为单位,建议设置 2048-4096 app.commandLine.appendSwitch('js-flags', '--expose-gc --max-old-space-size=4096') // 以 MB 为单位,建议设置 2048-4096
@@ -45,7 +57,7 @@ const SAFE_MODE =
// (可选)允许命令行清除持久化标记:--clear-safe-mode // (可选)允许命令行清除持久化标记:--clear-safe-mode
if (process.argv.includes('--clear-safe-mode')) { if (process.argv.includes('--clear-safe-mode')) {
const c = readCompat(); delete c.safeMode; writeCompat(c); const c = readCompat(); delete c.safeMode; writeCompat(c);
console.log('[Compat] cleared safeMode flag'); console.log('[兼容性] 已清除安全模式标志');
} }
// === 兼容模式等价于 bat 里的那些开关(不含日志) === // === 兼容模式等价于 bat 里的那些开关(不含日志) ===
@@ -56,7 +68,7 @@ if (SAFE_MODE) {
app.commandLine.appendSwitch('disable-direct-composition'); // --disable-direct-composition app.commandLine.appendSwitch('disable-direct-composition'); // --disable-direct-composition
// 如需更稳再关沙箱:默认开着更安全;若你确实要关,则解除注释 // 如需更稳再关沙箱:默认开着更安全;若你确实要关,则解除注释
// app.commandLine.appendSwitch('no-sandbox'); // --no-sandbox // app.commandLine.appendSwitch('no-sandbox'); // --no-sandbox
console.log('[SAFE_MODE] enabled'); console.log('[安全模式] 已启用');
} }
@@ -66,13 +78,13 @@ if (SAFE_MODE) {
function flushPendingNavs() { function flushPendingNavs() {
const fns = pendingNavs.slice(); const fns = pendingNavs.slice();
pendingNavs = []; pendingNavs = [];
for (const fn of fns) { try { fn(); } catch (e) { console.warn('[Nav] deferred run err:', e); } } for (const fn of fns) { try { fn(); } catch (e) { console.warn('[导航] 延迟运行错误:', e); } }
} }
function safeLoadURL(win, url, onFail) { function safeLoadURL(win, url, onFail) {
if (!win || win.isDestroyed()) return; if (!win || win.isDestroyed()) return;
win.loadURL(url).catch(err => { win.loadURL(url).catch(err => {
console.warn('[loadURL 失败]', err); console.warn('[加载url 失败]', err);
onFail?.(err); onFail?.(err);
}); });
} }
@@ -90,7 +102,8 @@ app.on('web-contents-created', (_, contents) => {
console.error('⚠️ 渲染崩溃:', details); console.error('⚠️ 渲染崩溃:', details);
if (['launch-failed', 'abnorm al-exit'].includes(details.reason)) { if (['launch-failed', 'abnorm al-exit'].includes(details.reason)) {
const c = readCompat(); c.safeMode = true; writeCompat(c); const c = readCompat(); c.safeMode = true; writeCompat(c);
console.warn('[Compat] set safeMode=true (persisted), will auto-use SAFE_MODE next launch'); console.warn('[兼容性] 已设置安全模式为true已持久化下次启动时将自动使用安全模式');
} }
}); });
}); });
@@ -99,10 +112,23 @@ app.on('child-process-gone', (_e, details) => {
if ((details.type === 'GPU' || details.type === 'Renderer') && if ((details.type === 'GPU' || details.type === 'Renderer') &&
['launch-failed', 'abnormal-exit'].includes(details.reason)) { ['launch-failed', 'abnormal-exit'].includes(details.reason)) {
const c = readCompat(); c.safeMode = true; writeCompat(c); const c = readCompat(); c.safeMode = true; writeCompat(c);
console.warn('[Compat] GPU/Renderer gone, safeMode persisted'); console.warn('[兼容性] 已设置安全模式为true已持久化下次启动时将自动使用安全模式');
} }
}); });
//本地https 证书跳过校验
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
// console.warn('[certificate-error]', url, error)
// 只放行你自己的本地 HTTPS
if (LOCAL_HTTPS_WHITELIST.some(prefix => url.startsWith(prefix))) {
event.preventDefault() // 告诉 Electron我来处理这个错误
callback(true) // ✅ 忽略证书错误,照样加载
} else {
// 其他地址还是正常校验证书,防止误放行
callback(false)
}
})
app.on('ready', () => { app.on('ready', () => {
@@ -179,7 +205,8 @@ app.on('ready', () => {
// 安装前做一次业务清理:但避免误杀自身或阻塞 // 安装前做一次业务清理:但避免误杀自身或阻塞
await killIOSAI(); await killIOSAI();
} catch (e) { } catch (e) {
console.warn('[update] pre-install cleanup warning:', e); console.warn('[更新] 预安装清理警告:', e);
} }
// electron-updater 实际上传给 NSIS /S 静默参数,第二个 true 表示强制重启后运行新版本 // electron-updater 实际上传给 NSIS /S 静默参数,第二个 true 表示强制重启后运行新版本
autoUpdater.quitAndInstall(false, true); autoUpdater.quitAndInstall(false, true);
@@ -207,7 +234,8 @@ app.whenReady().then(() => {
// globalShortcut.register('F12', () => toggleDevtools(BrowserWindow.getFocusedWindow())); // globalShortcut.register('F12', () => toggleDevtools(BrowserWindow.getFocusedWindow()));
globalShortcut.register('Control+Shift+I', () => toggleDevtools(BrowserWindow.getFocusedWindow())); globalShortcut.register('Control+Shift+i', () => toggleDevtools(BrowserWindow.getFocusedWindow()));
// globalShortcut.register('Control+Alt+m', () => toggleDevtools(BrowserWindow.getFocusedWindow()));
}); });
app.on('will-quit', () => globalShortcut.unregisterAll()); app.on('will-quit', () => globalShortcut.unregisterAll());
@@ -217,7 +245,8 @@ app.on('before-quit', async () => {
if (!installingNow) { if (!installingNow) {
await killIOSAI(); await killIOSAI();
} else { } else {
console.log('[before-quit] skip killIOSAI because installingNow = true'); console.log('[退出前] 跳过 killIOSAI 因为 installingNow = true');
} }
}); });
function toggleDevtools(win) { function toggleDevtools(win) {
@@ -273,33 +302,56 @@ function dumpAllMem() {
// console.log('> [MEM] SUM Renderer workingSetMB=', sum(renderers, 'workingSetMB')); // console.log('> [MEM] SUM Renderer workingSetMB=', sum(renderers, 'workingSetMB'));
// if (gpu.length) console.log('> [MEM] GPU=', gpu); // if (gpu.length) console.log('> [MEM] GPU=', gpu);
} catch (e) { } catch (e) {
console.warn('getAppMetrics error:', e); console.warn('获取应用指标错误:', e);
} }
} }
// 在函数外定义计数器(或者放在函数内部,用闭包封装) // 在函数外定义计数器(或者放在函数内部,用闭包封装)
let consumeCount = 0; let consumeCount = 0;
async function setupMQConsumerAndPublisher(emitMessage, tenantId) { async function setupMQConsumerAndPublisher(emitMessage, tenantId) {
// 启动持续消费:来一条 -> 立刻 SSE 广播 const queueNames = [
await mq.startConsumer(
`q.tenant.${tenantId}`, `q.tenant.${tenantId}`,
async (msg) => { `b.tenant.${tenantId}`, // 新增队列
const payload = msg.json ?? msg.text; // 数据 ];
consumeCount++; // 每消费一条消息就加 1
console.log('消费到:', payload.hostsId, payload.country, '共' + consumeCount + '条数据'); for (const qName of queueNames) {
await mq.startConsumer(
qName,
async (msg) => {
const payload = msg.json ?? msg.text; // 原始业务数据
consumeCount++; // 所有队列共用计数器
// 广播到前端(事件名自定义为 'mq' // 标记来源类型:普通队列 / burst 队列
emitMessage(payload); const isBurstQueue = qName.startsWith('b.');
// 成功返回会在 mq 客户端内部自动 ack const meta = isBurstQueue ? 2 : 1;
},
{ prefetch: 1, requeueOnError: false, durable: true, assertQueue: true }
);
// 供渲染进程发送消息到队列 console.log(
`[MQ消费] [${qName}]`,
payload?.hostsId,
payload?.country,
'共' + consumeCount + '条数据'
);
// ⚠️ 关键:在原有 payload 的基础上,增加 _mqMeta 字段
// 这样你之前前端用 payload.hostsId / payload.country 的地方完全不用改
const wrapped = {
...payload,
_mqMeta: meta
};
// 广播到前端
emitMessage(wrapped);
// 成功返回会在 mq 客户端内部自动 ack
},
{ prefetch: 1, requeueOnError: false, durable: true, assertQueue: true }
);
}
// 供渲染进程发送消息到队列(保持原来的 q.tenant.* 不变)
ipcMain.removeHandler('mq-send'); ipcMain.removeHandler('mq-send');
ipcMain.handle('mq-send', async (_event, user) => { ipcMain.handle('mq-send', async (_event, user) => {
console.log('消息已发送', user) console.log('消息已发送', user);
await mq.publishToQueue(`q.tenant.${tenantId}`, user); await mq.publishToQueue(`q.tenant.${tenantId}`, user);
return { ok: true }; return { ok: true };
}); });
@@ -342,10 +394,23 @@ function createWindow() {
win.setMenu(null); // 禁用菜单栏F12 win.setMenu(null); // 禁用菜单栏F12
win.maximize(); win.maximize();
// ✅ 把渲染进程的 console.* 打进 main 的日志
win.webContents.on('console-message', (event, level, message, line, sourceId) => {
// level: 0=log,1=warn,2=error,3=info,4=debug
if (level == 1) {
console.log(
`[页面日志] ${message}`
);
}
});
// 自动判断环境使用不同的页面地址 // 自动判断环境使用不同的页面地址
const isProd = app.isPackaged; const isProd = app.isPackaged;
const targetURL = isProd ? 'https://iosaitest.yolozs.com' : 'http://192.168.1.128:8080'; // const targetURL = isProd ? 'https://iosaitest.yolozs.com' : 'http://192.168.1.128:8080';
// const targetURL = isProd ? 'https://iosai.yolozs.com' : 'http://192.168.1.128:8080'; const targetURL = isProd ? 'https://iosai.yolozs.com' : 'http://192.168.1.128:8080';
console.log('[页面加载] 使用地址:', targetURL); console.log('[页面加载] 使用地址:', targetURL);
let retryIntervalId = null; let retryIntervalId = null;
@@ -724,7 +789,7 @@ async function killIOSAI() {
}).then(response => { }).then(response => {
console.log("发送登出请求成功") console.log("发送登出请求成功")
}).catch(error => { }).catch(error => {
console.log("发送登出请求错误", error.message) console.log("发送登出请求错误", error)
}).finally(error => { }).finally(error => {
// console.log("发送") // console.log("发送")
}) })
@@ -744,6 +809,7 @@ async function killIOSAI() {
exec('taskkill /IM IOSAI.exe /F'); exec('taskkill /IM IOSAI.exe /F');
exec('taskkill /IM iproxy.exe /F'); exec('taskkill /IM iproxy.exe /F');
exec('taskkill /IM pythonw.exe /F'); exec('taskkill /IM pythonw.exe /F');
exec('taskkill /IM ios.exe /F');
} }
} catch (_) { } } catch (_) { }
iosAIProcess = null; iosAIProcess = null;

View File

@@ -1,7 +1,7 @@
{ {
"name": "YOLO-ios-ai", "name": "YOLO-ios-ai",
"productName": "YOLOAI助手ios", "productName": "YOLOAI助手ios",
"version": "2.8.0", "version": "3.2.0",
"description": "Vue3 + WS 控制台", "description": "Vue3 + WS 控制台",
"author": "yourname", "author": "yourname",
"main": "main.js", "main": "main.js",