diff --git a/js/preload.js b/js/preload.js index c6b49dd..c442006 100644 --- a/js/preload.js +++ b/js/preload.js @@ -7,6 +7,8 @@ contextBridge.exposeInMainWorld('electronAPI', { manualGc: () => ipcRenderer.invoke('manual-gc'), mqSend: (arg) => ipcRenderer.invoke('mq-send', arg), startMq: (tendid, id) => ipcRenderer.invoke('start-mq', tendid, id), + // 新增 toggle 接口 + toggleMq: (type, enabled) => ipcRenderer.invoke('mq-toggle', type, enabled), fileExists: (url) => ipcRenderer.invoke('file-exists', url), isiproxy: (url) => ipcRenderer.invoke('isiproxy', url), getVersion: () => ipcRenderer.invoke('getVersion'), diff --git a/main.js b/main.js index 2f1b22a..ba12c36 100644 --- a/main.js +++ b/main.js @@ -2,7 +2,6 @@ const { app, globalShortcut, BrowserWindow, net, dialog, ipcMain } = require('el const { startSSE } = require('./js/sse-server'); const { createBurstBroadcaster } = require('./js/burst-broadcast'); const mq = require('./js/rabbitmq-service'); -const https = require('https') const axios = require('axios'); const os = require('os'); const fsp = require('node:fs/promises') @@ -28,10 +27,6 @@ const LOCAL_HTTPS_WHITELIST = [ ] let userData = { tenantId: null, userId: null } -let mqEnabled = true; -let mqActive = false; -const mqQueueEnabled = { crawler: true, boss: true }; -const mqConsumers = new Map(); const { exec, spawn, execFile } = require('child_process'); // 如果你用 exec 启动 scrcpy,保留此行 // app.commandLine.appendSwitch('remote-debugging-port', '9222'); //远程控制台端口F12 @@ -314,75 +309,132 @@ function dumpAllMem() { // 在函数外定义计数器(或者放在函数内部,用闭包封装) let consumeCount = 0; -function syncMqEnabled() { - mqEnabled = Boolean(mqQueueEnabled.crawler || mqQueueEnabled.boss); -} +// 存储活跃的消费者 { key: configObject } +const activeConsumers = new Map(); +// 辅助函数:生成消费者 key +const getConsumerKey = (type, tenantId) => `${type}:${tenantId}`; -function getQueueNameByType(type, tenantId) { - if (type === 'crawler') return `q.tenant.${tenantId}`; - if (type === 'boss') return `b.tenant.${tenantId}`; - return null; -} +/** + * 启动指定类型的 MQ 消费者 + * @param {string} type 'crawler' (q.tenant.*) | 'boss' (b.tenant.*) + * @param {string} tenantId + * @param {Function} emitMessage 广播函数 + */ +async function startConsumerByType(type, tenantId, emitMessage) { + const key = getConsumerKey(type, tenantId); -async function startMQConsumer(queueName, emitMessage) { - if (!queueName || mqConsumers.has(queueName)) return; - const consumer = await mq.startConsumer( - queueName, - async (msg) => { - const payload = msg.json ?? msg.text; - consumeCount++; - - const isBurstQueue = queueName.startsWith('b.'); - const meta = isBurstQueue ? 2 : 1; - - console.log( - `[MQ] [${queueName}]`, - payload?.hostsId, - payload?.country, - 'count=', - consumeCount - ); - - const wrapped = { - ...payload, - _mqMeta: meta - }; - - emitMessage(wrapped); - }, - { prefetch: 1, requeueOnError: false, durable: true, assertQueue: true } - ); - mqConsumers.set(queueName, consumer); -} - -async function stopMQConsumer(queueName) { - const consumer = mqConsumers.get(queueName); - if (!consumer) { - console.warn('[MQ] stopMQConsumer: no consumer for', queueName); + // 防止重复启动 + if (activeConsumers.has(key)) { + console.log(`[MQ] ${type} 消费者已在运行,跳过启动`); return; } - if (consumer?.stop) { - await consumer.stop().catch(() => { }); + + let qName = ''; + // 根据类型决定队列名 + if (type === 'crawler') { + qName = `q.tenant.${tenantId}`; + } else if (type === 'boss') { + qName = `b.tenant.${tenantId}`; + } else { + console.warn(`[MQ] 未知消费者类型: ${type}`); + return; + } + + console.log(`[MQ] 正在启动消费者: ${type} -> ${qName}`); + + try { + const consumer = await mq.startConsumer( + qName, + async (msg) => { + const payload = msg.json ?? msg.text; // 原始业务数据 + consumeCount++; // 所有队列共用计数器 + + // 标记来源类型:爬虫=1 (q.tenant.*) / 大哥=2 (b.tenant.*) + const meta = (type === 'boss') ? 2 : 1; + + console.log( + `[MQ消费] [${qName}]`, + payload?.hostsId, + payload?.country, + 'Start' // 简单标记一下 + ); + + // ⚠️ 关键:在原有 payload 的基础上,增加 _mqMeta 字段 + const wrapped = { + ...payload, + _mqMeta: meta + }; + + // 广播到前端 + emitMessage(wrapped); + // 成功返回会在 mq 客户端内部自动 ack + }, + { prefetch: 1, requeueOnError: false, durable: true, assertQueue: true } + ); + console.log('启动成功') + // 记录下来,以便后续关闭 + activeConsumers.set(key, { + type, + tenantId, + qName, + consumer // 包含 .stop() 方法 + }); + + } catch (err) { + console.error(`[MQ] 启动消费者失败 (${type}):`, err); } - mqConsumers.delete(queueName); } -async function setupMQConsumerAndPublisher(emitMessage, tenantId) { - syncMqEnabled(); - if (!mqEnabled) return; - for (const type of Object.keys(mqQueueEnabled)) { - if (!mqQueueEnabled[type]) continue; - const qName = getQueueNameByType(type, tenantId); - await startMQConsumer(qName, emitMessage); +/** + * 停止指定类型的 MQ 消费者 + */ +async function stopConsumerByType(type, tenantId) { + const key = getConsumerKey(type, tenantId); + const item = activeConsumers.get(key); + + if (!item) { + // console.log(`[MQ] ${type} 消费者未运行,无需停止`); + return; } - mqActive = mqConsumers.size > 0; + console.log(`[MQ] 正在停止消费者: ${type} -> ${item.qName}`); + try { + if (item.consumer && typeof item.consumer.stop === 'function') { + await item.consumer.stop(); + console.log(`[MQ] 停止消费者成功: ${type} -> ${item.qName}`); + } + } catch (e) { + console.warn(`[MQ] 停止消费者异常 (${type}):`, e); + } finally { + activeConsumers.delete(key); + } +} + +/** + * 初始化 MQ 管理器(替代原来的 setupMQConsumerAndPublisher) + * 现在它主要负责初始化“发送端”,并注册 toggle 监听 + */ +async function setupMQManager(emitMessage, tenantId) { + // 供渲染进程发送消息到队列(保持原来的 q.tenant.* 不变) ipcMain.removeHandler('mq-send'); ipcMain.handle('mq-send', async (_event, user) => { console.log('消息已发送', user); await mq.publishToQueue(`q.tenant.${tenantId}`, user); return { ok: true }; }); + + // 注册切换监听 + ipcMain.removeHandler('mq-toggle'); + ipcMain.handle('mq-toggle', async (_event, type, enabled) => { + // type: 'crawler' | 'boss' + console.log(`[MQ-Toggle] ${type} -> ${enabled}`); + if (enabled) { + await startConsumerByType(type, tenantId, emitMessage); + } else { + await stopConsumerByType(type, tenantId); + } + return { ok: true }; + }); } @@ -437,10 +489,8 @@ function createWindow() { // 自动判断环境使用不同的页面地址 const isProd = app.isPackaged; - // const targetURL = isProd ? 'https://iosaitest.yolozs.com' : 'http://192.168.2.128:8081'; + // const targetURL = isProd ? 'https://iosaitest.yolozs.com' : 'http://192.168.1.128:8080'; const targetURL = isProd ? 'https://iosai.yolozs.com' : 'http://192.168.2.128:8080'; - // const targetURL = 'https://iosai.yolozs.com'; - // const targetURL = 'http://192.168.2.128:8080'; console.log('[页面加载] 使用地址:', targetURL); let retryIntervalId = null; @@ -532,10 +582,7 @@ function createWindow() { } }); - ipcMain.handle('getVersion', async (_event, opts = {}) => { - return app.getVersion(); - }); // 仅检查固定路径:C:\Users\Administrator\IOSAI\aiConfig.json ipcMain.handle('file-exists', async () => { @@ -635,19 +682,10 @@ function createWindow() { // 如果没有把加载页文件放到指定位置,给个兜底 win.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent('

正在等待 iproxy.exe 启动…

')); } - // 🆕 先在等待页面做虚拟机检测:如果是虚拟机 → 弹窗 + 退出 - (async () => { - const isVM = await detectVMAndHandle(win); - if (isVM) { - // detectVMAndHandle 里已经 app.quit(),这里直接返回 - return; - } - - // 非虚拟机:3 秒后正常进入业务页面 - setTimeout(() => { - tryNavigate('delay-3s-autogo'); - }, 3000); - })(); + // ✅ 不再自动检测 iproxy,3 秒后直接进入业务页面 + setTimeout(() => { + tryNavigate('delay-3s-autogo'); + }, 3000); // // 开始检测 iproxy,检测到后再跳转 targetURL // waitForIproxyAndNavigate(win, targetURL, { // intervalMs: 2000, @@ -673,88 +711,9 @@ function createWindow() { ipcMain.handle('start-mq', async (event, tentId, id) => { userData.tenantId = tentId; userData.userId = id; - syncMqEnabled(); - if (!mqEnabled) { - console.log('[MQ] start-mq skipped because disabled'); - return { ok: true, enabled: false }; - } + //启动mq管理器(但不立即启动消费,等待前端发指令) + setupMQManager(emitMessage, tentId) - if (mq.open) { - await mq.open(); - } - await setupMQConsumerAndPublisher(emitMessage, tentId); - mqActive = mqConsumers.size > 0; - return { ok: true, enabled: mqActive }; - }); - - ipcMain.handle('open-mq', async (_event, payload) => { - console.log('[MQ] open-mq', payload, 'tenantId=', userData.tenantId, 'consumers=', [...mqConsumers.keys()]); - - if (typeof payload == 'boolean') { - mqQueueEnabled.crawler = payload; - mqQueueEnabled.boss = payload; - syncMqEnabled(); - - if (payload) { - if (!userData.tenantId) { - return { ok: false, error: 'tenantId missing' }; - } - if (mq.open) { - await mq.open(); - } - await setupMQConsumerAndPublisher(emitMessage, userData.tenantId); - mqActive = mqConsumers.size > 0; - return { ok: true, enabled: mqEnabled }; - } - - await mq.close().catch((err) => { - console.warn('[MQ] close failed', err); - }); - mqConsumers.clear(); - mqActive = false; - return { ok: true, enabled: false }; - } - - const type = payload?.type; - const shouldEnable = payload?.enable === true; - if (!type) { - return { ok: false, error: 'type missing' }; - } - if (type !== 'crawler' && type !== 'boss') { - return { ok: false, error: 'type invalid' }; - } - if (payload?.enable !== true && payload?.enable !== false) { - return { ok: false, error: 'enable invalid' }; - } - - mqQueueEnabled[type] = shouldEnable; - syncMqEnabled(); - - if (!userData.tenantId) { - return { ok: false, error: 'tenantId missing' }; - } - - const qName = getQueueNameByType(type, userData.tenantId); - if (shouldEnable) { - if (mq.open) { - await mq.open(); - } - await startMQConsumer(qName, emitMessage); - } else { - await stopMQConsumer(qName); - } - - if (!mqEnabled) { - await mq.close().catch((err) => { - console.warn('[MQ] close failed', err); - }); - mqConsumers.clear(); - mqActive = false; - } else { - mqActive = mqConsumers.size > 0; - } - - return { ok: true, enabled: mqEnabled, queue: type, queueEnabled: mqQueueEnabled[type] }; }); // 可选:开发阶段打开 DevTools F12 // win.webContents.openDevTools(); @@ -901,25 +860,18 @@ function startIOSAIExecutable() { async function killIOSAI() { console.log('尝试关闭IOSAI', userData); // console.log('axios', axios); - const httpsAgent = new https.Agent({ - family: 4 // 强制使用 IPv4 - }) - await axios.post( - 'https://crawlclient.api.yolozs.com/api/user/aiChat-logout', - { userId: userData.userId, tenantId: userData.tenantId }, - { - headers: { - 'Content-Type': 'application/json', - 'vvtoken': userData.tokenValue - }, - httpsAgent, - timeout: 5000 // 建议加个超时时间,避免死卡 + await axios.post('https://crawlclient.api.yolozs.com/api/user/aiChat-logout', { userId: userData.userId, tenantId: userData.tenantId }, { + headers: { + 'Content-Type': 'application/json', // 设置请求头 + 'vvtoken': userData.tokenValue } - ).then(res => { - console.log('发送登出请求成功') - }).catch(err => { - console.log('发送登出请求错误', err) + }).then(response => { + console.log("发送登出请求成功") + }).catch(error => { + console.log("发送登出请求错误", error) + }).finally(error => { + // console.log("发送") }) try { @@ -1024,125 +976,4 @@ function normalizePath(targetPath, baseDir) { return path.resolve(base, targetPath) } return path.resolve(targetPath) -} - - -// ======== 虚拟机检测相关工具函数(Windows 为主)======== - -// CPU 型号检测(同步) -function isVmByCpu() { - try { - const cpus = os.cpus(); - if (!cpus || !cpus.length) return false; - const model = (cpus[0].model || '').toLowerCase(); - const vmKeywords = [ - 'virtualbox', - 'vmware', - 'kvm', - 'qemu', - 'hyper-v', - 'xen', - 'parallels' - ]; - return vmKeywords.some(k => model.includes(k)); - } catch (e) { - console.warn('[VM检测] CPU 检测失败:', e); - return false; - } -} - -// MAC 前缀检测(同步) -function isVmByMac() { - try { - const ifaces = os.networkInterfaces(); - const vmMacPrefixes = [ - '00:05:69', '00:0c:29', '00:1c:14', '00:50:56', // VMware - '08:00:27', // VirtualBox - '00:15:5d', // Hyper-V - ]; - for (const name in ifaces) { - for (const detail of ifaces[name]) { - const mac = (detail.mac || '').toLowerCase(); - if (!mac || mac === '00:00:00:00:00:00') continue; - if (vmMacPrefixes.some(p => mac.startsWith(p))) { - return true; - } - } - } - return false; - } catch (e) { - console.warn('[VM检测] MAC 检测失败:', e); - return false; - } -} - -// WMI 读取制造商(异步,仅 Windows) -function isVmByWMI() { - return new Promise((resolve) => { - if (process.platform !== 'win32') return resolve(false); - exec('wmic computersystem get manufacturer', { windowsHide: true }, (err, stdout = '') => { - if (err) { - console.warn('[VM检测] WMI 检测失败:', err); - return resolve(false); - } - const txt = stdout.toLowerCase(); - const vmKeywords = [ - 'vmware', - 'virtualbox', - 'microsoft corporation', // Hyper-V 通常是这个 - 'qemu', - 'xen', - 'parallels' - ]; - const hit = vmKeywords.some(k => txt.includes(k)); - resolve(hit); - }); - }); -} - -// 综合判断:返回 Promise -async function isVirtualMachine() { - try { - const byCpu = isVmByCpu(); - const byMac = isVmByMac(); - const byWmi = await isVmByWMI(); - - const hits = [byCpu, byMac, byWmi].filter(Boolean).length; - - // 规则:命中 2 项以上,或者 WMI 单独命中,就认为是虚拟机 - const isVM = hits >= 2 || byWmi; - - console.log('[VM检测] 结果:', { byCpu, byMac, byWmi, hits, isVM }); - return isVM; - } catch (e) { - console.warn('[VM检测] 检测异常,放行运行:', e); - return false; - } -} - -// 检测并处理:如果是虚拟机 → 弹窗 + 退出 -async function detectVMAndHandle(win) { - // 只在 Windows 做严格限制,其他平台按需放行 - if (process.platform !== 'win32') return false; - - const isVM = await isVirtualMachine(); - if (!isVM) return false; - - console.warn('[VM检测] 检测到虚拟机环境,准备退出应用'); - - try { - await dialog.showMessageBox(win || null, { - type: 'error', - title: '运行环境异常', - message: '检测到程序运行于虚拟机环境,出于安全策略,本程序将退出。', - buttons: ['确定'], - defaultId: 0, - noLink: true - }); - } catch (e) { - console.warn('[VM检测] 弹窗失败,直接退出:', e); - } - - app.quit(); - return true; -} +} \ No newline at end of file diff --git a/package.json b/package.json index f73c288..b01d8cd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "YOLO-ios-ai", "productName": "YOLO(AI助手ios)", - "version": "3.6.0", + "version": "3.6.2", "description": "Vue3 + WS 控制台", "author": "yourname", "main": "main.js",