diff --git a/js/preload.js b/js/preload.js
index f6bc374..c6b49dd 100644
--- a/js/preload.js
+++ b/js/preload.js
@@ -9,6 +9,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
startMq: (tendid, id) => ipcRenderer.invoke('start-mq', tendid, id),
fileExists: (url) => ipcRenderer.invoke('file-exists', url),
isiproxy: (url) => ipcRenderer.invoke('isiproxy', url),
+ getVersion: () => ipcRenderer.invoke('getVersion'),
})
contextBridge.exposeInMainWorld('appUpdater', {
@@ -27,4 +28,4 @@ window.addEventListener('unload', () => {
ipcRenderer.removeAllListeners('update:progress');
ipcRenderer.removeAllListeners('update:downloaded');
ipcRenderer.removeAllListeners('update:error');
-});
\ No newline at end of file
+});
diff --git a/js/rabbitmq-service.js b/js/rabbitmq-service.js
index 31ed719..8b89304 100644
--- a/js/rabbitmq-service.js
+++ b/js/rabbitmq-service.js
@@ -16,6 +16,7 @@ const CFG = {
};
let conn = null;
+let connectionLock = null; // 🔒 连接锁,防止并发创建
let pubCh = null; // 发布 Confirm Channel
let conCh = null; // 消费 Channel
const emitter = new EventEmitter();
@@ -37,54 +38,80 @@ function toBuffer(payload) {
// —— 内部:建立连接(含心跳、keepalive、事件)
async function createConnection() {
- const connection = await amqp.connect({
- protocol: CFG.protocol,
- hostname: CFG.host,
- port: CFG.port,
- username: CFG.user,
- password: CFG.pass,
- vhost: CFG.vhost,
- heartbeat: CFG.heartbeat,
- frameMax: CFG.frameMax > 0 ? CFG.frameMax : undefined,
- // 也可用 URL 形式:`amqp://u:p@host:5672/vhost?heartbeat=60`
- });
+ // 1. 如果已有连接,直接返回
+ if (conn) return conn;
- // 打开 TCP keepalive,降低 NAT/空闲超时断开的概率
- try {
- const stream = connection.stream || connection.socket;
- if (stream?.setKeepAlive) stream.setKeepAlive(true, 15_000); // 15s
- } catch (_) { }
+ // 2. 如果正在连接中,等待它完成
+ if (connectionLock) {
+ return connectionLock;
+ }
- connection.on('error', (e) => {
- // 心跳超时常见,避免重复噪音
- const msg = e?.message || String(e);
- if (msg && /heartbeat/i.test(msg)) {
- console.error('[AMQP] 连接错误 (心跳):', msg);
- } else {
- console.error('[AMQP] 连接错误:', msg);
+ // 3. 开始新连接,加锁
+ connectionLock = (async () => {
+ try {
+ console.log(`[AMQP] 开始连接 ${CFG.host}...`);
+ const connection = await amqp.connect({
+ protocol: CFG.protocol,
+ hostname: CFG.host,
+ port: CFG.port,
+ username: CFG.user,
+ password: CFG.pass,
+ vhost: CFG.vhost,
+ heartbeat: CFG.heartbeat,
+ frameMax: CFG.frameMax > 0 ? CFG.frameMax : undefined,
+ });
+
+ // 如果在连接过程中被要求关闭(race condition),则立刻关闭并抛错
+ if (closing) {
+ console.warn('[AMQP] 连接刚建立但 Detected closing=true, closing now...');
+ connection.close().catch(() => { });
+ throw new Error('Connection aborted (closing)');
+ }
+
+ // 打开 TCP keepalive
+ try {
+ const stream = connection.stream || connection.socket;
+ if (stream?.setKeepAlive) stream.setKeepAlive(true, 15_000);
+ } catch (_) { }
+
+ connection.on('error', (e) => {
+ const msg = e?.message || String(e);
+ if (msg && /heartbeat/i.test(msg)) {
+ console.error('[AMQP] 连接错误 (心跳):', msg);
+ } else {
+ console.error('[AMQP] 连接错误:', msg);
+ }
+ emitter.emit('error', e);
+ });
+
+ connection.on('close', () => {
+ if (closing) return;
+ console.error('[AMQP] 连接已关闭');
+ conn = null; pubCh = null; conCh = null;
+ scheduleReconnect();
+ });
+
+ connection.on('blocked', (reason) => {
+ console.warn('[AMQP] 连接被代理阻塞::', reason);
+ emitter.emit('blocked', reason);
+ });
+ connection.on('unblocked', () => {
+ console.log('[AMQP] 链接解锁');
+ emitter.emit('unblocked');
+ });
+
+ console.log(`[AMQP] 已连接到 ${CFG.host} (hb=${CFG.heartbeat}s)`);
+ conn = connection; // ✅ 赋值给全局变量
+ return connection;
+ } catch (err) {
+ console.error('[AMQP] createConnection failed:', err.message);
+ throw err;
+ } finally {
+ connectionLock = null; // 🔓 解锁
}
- emitter.emit('error', e);
- });
+ })();
- connection.on('close', () => {
- if (closing) return; // 正在关闭时不重连
- console.error('[AMQP] 连接已关闭');
- conn = null; pubCh = null; conCh = null;
- scheduleReconnect();
- });
-
- // Broker 侧内存/磁盘压力会 block 连接
- connection.on('blocked', (reason) => {
- console.warn('[AMQP] 连接被代理阻塞::', reason);
- emitter.emit('blocked', reason);
- });
- connection.on('unblocked', () => {
- console.log('[AMQP] 链接解锁');
- emitter.emit('unblocked');
- });
-
- console.log(`[AMQP] 已连接到 ${CFG.host} (hb=${CFG.heartbeat}s)`);
- return connection;
+ return connectionLock;
}
// —— 内部:确保连接和通道存在
@@ -252,6 +279,7 @@ async function publishToExchange(exchange, routingKey, payload, options = {}) {
persistent = true,
headers = {},
mandatory = false,
+ // mandatory = false,
} = options;
if (assertExchange) {
@@ -267,6 +295,9 @@ async function publishToExchange(exchange, routingKey, payload, options = {}) {
async function reconnectNow() {
if (closing) return;
if (reconnecting) return;
+ // 如果正在连接,也视为正在重连/忙碌,稍后
+ if (connectionLock) return;
+
try {
if (pubCh) await pubCh.close().catch(() => { });
if (conCh) await conCh.close().catch(() => { });
@@ -281,6 +312,18 @@ async function reconnectNow() {
async function close() {
closing = true;
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
+
+ // 关键修复:如果正在建立连接,必须等待它完成(或失败),再执行关闭
+ // 否则 amqplib 的 "socket closed abruptly" 错误会频发
+ if (connectionLock) {
+ try {
+ console.log('[AMQP] close() called while connecting, waiting...');
+ await connectionLock;
+ } catch (_) {
+ // 忽略连接失败
+ }
+ }
+
try {
// 取消所有消费者
for (const [q, c] of consumers.entries()) {
@@ -298,6 +341,14 @@ async function close() {
}
// —— 进程信号(可选)
+async function open() {
+ closing = false;
+ reconnecting = false;
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
+ backoff = 1000;
+ await ensureChannels();
+}
+
process.once('SIGINT', async () => { try { await close(); } finally { process.exit(0); } });
process.once('SIGTERM', async () => { try { await close(); } finally { process.exit(0); } });
@@ -305,6 +356,7 @@ module.exports = {
startConsumer,
publishToQueue,
publishToExchange,
+ open,
reconnectNow,
close,
emitter, // 可订阅 'message' / 'handlerError' / 'reconnected' / 'error' / 'blocked' / 'unblocked'
diff --git a/main.js b/main.js
index ead011c..2f1b22a 100644
--- a/main.js
+++ b/main.js
@@ -2,6 +2,7 @@ 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')
@@ -27,6 +28,10 @@ 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
@@ -309,46 +314,69 @@ function dumpAllMem() {
// 在函数外定义计数器(或者放在函数内部,用闭包封装)
let consumeCount = 0;
-async function setupMQConsumerAndPublisher(emitMessage, tenantId) {
- const queueNames = [
- `q.tenant.${tenantId}`,
- `b.tenant.${tenantId}`, // 新增队列
- ];
+function syncMqEnabled() {
+ mqEnabled = Boolean(mqQueueEnabled.crawler || mqQueueEnabled.boss);
+}
- for (const qName of queueNames) {
- await mq.startConsumer(
- qName,
- async (msg) => {
- const payload = msg.json ?? msg.text; // 原始业务数据
- consumeCount++; // 所有队列共用计数器
+function getQueueNameByType(type, tenantId) {
+ if (type === 'crawler') return `q.tenant.${tenantId}`;
+ if (type === 'boss') return `b.tenant.${tenantId}`;
+ return null;
+}
- // 标记来源类型:普通队列 / burst 队列
- const isBurstQueue = qName.startsWith('b.');
- const meta = isBurstQueue ? 2 : 1;
+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++;
- console.log(
- `[MQ消费] [${qName}]`,
- payload?.hostsId,
- payload?.country,
- '共' + consumeCount + '条数据'
- );
+ const isBurstQueue = queueName.startsWith('b.');
+ const meta = isBurstQueue ? 2 : 1;
- // ⚠️ 关键:在原有 payload 的基础上,增加 _mqMeta 字段
- // 这样你之前前端用 payload.hostsId / payload.country 的地方完全不用改
- const wrapped = {
- ...payload,
- _mqMeta: meta
- };
+ console.log(
+ `[MQ] [${queueName}]`,
+ payload?.hostsId,
+ payload?.country,
+ 'count=',
+ consumeCount
+ );
- // 广播到前端
- emitMessage(wrapped);
- // 成功返回会在 mq 客户端内部自动 ack
- },
- { prefetch: 1, requeueOnError: false, durable: true, assertQueue: true }
- );
+ 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);
+ return;
}
+ if (consumer?.stop) {
+ await consumer.stop().catch(() => { });
+ }
+ 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);
+ }
+ mqActive = mqConsumers.size > 0;
- // 供渲染进程发送消息到队列(保持原来的 q.tenant.* 不变)
ipcMain.removeHandler('mq-send');
ipcMain.handle('mq-send', async (_event, user) => {
console.log('消息已发送', user);
@@ -409,8 +437,10 @@ function createWindow() {
// 自动判断环境使用不同的页面地址
const isProd = app.isPackaged;
- // 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://iosaitest.yolozs.com' : 'http://192.168.2.128:8081';
+ 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;
@@ -502,7 +532,10 @@ function createWindow() {
}
});
+ ipcMain.handle('getVersion', async (_event, opts = {}) => {
+ return app.getVersion();
+ });
// 仅检查固定路径:C:\Users\Administrator\IOSAI\aiConfig.json
ipcMain.handle('file-exists', async () => {
@@ -602,10 +635,19 @@ function createWindow() {
// 如果没有把加载页文件放到指定位置,给个兜底
win.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent('
正在等待 iproxy.exe 启动…
'));
}
- // ✅ 不再自动检测 iproxy,3 秒后直接进入业务页面
- setTimeout(() => {
- tryNavigate('delay-3s-autogo');
- }, 3000);
+ // 🆕 先在等待页面做虚拟机检测:如果是虚拟机 → 弹窗 + 退出
+ (async () => {
+ const isVM = await detectVMAndHandle(win);
+ if (isVM) {
+ // detectVMAndHandle 里已经 app.quit(),这里直接返回
+ return;
+ }
+
+ // 非虚拟机:3 秒后正常进入业务页面
+ setTimeout(() => {
+ tryNavigate('delay-3s-autogo');
+ }, 3000);
+ })();
// // 开始检测 iproxy,检测到后再跳转 targetURL
// waitForIproxyAndNavigate(win, targetURL, {
// intervalMs: 2000,
@@ -631,9 +673,88 @@ function createWindow() {
ipcMain.handle('start-mq', async (event, tentId, id) => {
userData.tenantId = tentId;
userData.userId = id;
- //启动mq
- setupMQConsumerAndPublisher(emitMessage, tentId)
+ syncMqEnabled();
+ if (!mqEnabled) {
+ console.log('[MQ] start-mq skipped because disabled');
+ return { ok: true, enabled: false };
+ }
+ 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();
@@ -780,18 +901,25 @@ 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
+ 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 // 建议加个超时时间,避免死卡
}
- }).then(response => {
- console.log("发送登出请求成功")
- }).catch(error => {
- console.log("发送登出请求错误", error)
- }).finally(error => {
- // console.log("发送")
+ ).then(res => {
+ console.log('发送登出请求成功')
+ }).catch(err => {
+ console.log('发送登出请求错误', err)
})
try {
@@ -896,4 +1024,125 @@ function normalizePath(targetPath, baseDir) {
return path.resolve(base, targetPath)
}
return path.resolve(targetPath)
-}
\ No newline at end of file
+}
+
+
+// ======== 虚拟机检测相关工具函数(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;
+}
diff --git a/main/index.js b/main/index.js
deleted file mode 100644
index be927ba..0000000
--- a/main/index.js
+++ /dev/null
@@ -1,61 +0,0 @@
-// main/index.js
-const { app, globalShortcut, BrowserWindow } = require('electron')
-const path = require('path')
-
-const { registerUpdater, updaterState } = require('./updater')
-const { createMainWindow } = require('./window')
-const { registerSystemIpc } = require('./ipc/system')
-const { registerFileIpc } = require('./ipc/files')
-const { registerMqIpc } = require('./ipc/mq')
-const { startIOSAIExecutable, killIOSAI } = require('./services/iosai')
-const { startSSE } = require('./services/sse')
-const { setupGuards } = require('./utils/guard')
-const { dumpAllMem } = require('./utils/mem')
-
-setupGuards()
-app.commandLine.appendSwitch('enable-precise-memory-info')
-app.commandLine.appendSwitch('js-flags', '--expose-gc --max-old-space-size=4096')
-app.commandLine.appendSwitch('enable-experimental-web-platform-features')
-app.commandLine.appendSwitch('enable-features', 'WebCodecs,MediaStreamInsertableStreams')
-
-let sse = null
-
-app.on('ready', async () => {
- // 更新
- registerUpdater()
-
- // 启动外部服务
- startIOSAIExecutable()
-
- // IPC
- registerSystemIpc()
- registerFileIpc()
- registerMqIpc((payload) => sse?.broadcast('message', payload)) // 渲染用
-
- // SSE
- sse = startSSE()
-
- // 窗口
- const win = createMainWindow({ updaterState })
-
- // 快捷键
- globalShortcut.register('Control+Shift+I', () => {
- const w = BrowserWindow.getFocusedWindow()
- if (!w) return
- const wc = w.webContents
- wc.isDevToolsOpened() ? wc.closeDevTools() : wc.openDevTools({ mode: 'detach' })
- })
-
- // 内存Dump(可选)
- setInterval(dumpAllMem, 5000)
-})
-
-app.on('will-quit', () => globalShortcut.unregisterAll())
-app.on('before-quit', async () => { await killIOSAI() })
-app.on('window-all-closed', async () => {
- await killIOSAI()
- if (process.platform !== 'darwin') app.quit()
-})
-app.on('activate', () => {
- if (BrowserWindow.getAllWindows().length === 0) createMainWindow({ updaterState })
-})
diff --git a/main/ipc/files.js b/main/ipc/files.js
deleted file mode 100644
index 53eab6f..0000000
--- a/main/ipc/files.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// main/ipc/files.js
-const { ipcMain } = require('electron')
-const fsp = require('node:fs/promises')
-const { normalizePath } = require('../utils/paths')
-
-function registerFileIpc() {
- ipcMain.removeHandler('file-exists')
- ipcMain.handle('file-exists', async (_evt, targetPath, baseDir) => {
- try {
- const full = normalizePath(targetPath, baseDir)
- const stat = await fsp.stat(full).catch(err => (err?.code === 'ENOENT' ? null : Promise.reject(err)))
- if (!stat) return { ok: true, exists: false, path: full }
- return {
- ok: true, exists: true, path: full,
- isFile: stat.isFile(), isDirectory: stat.isDirectory(),
- size: stat.size, mtimeMs: stat.mtimeMs
- }
- } catch (e) {
- return { ok: false, error: e.message || String(e) }
- }
- })
-}
-
-module.exports = { registerFileIpc }
diff --git a/main/ipc/mq.js b/main/ipc/mq.js
deleted file mode 100644
index 0c88014..0000000
--- a/main/ipc/mq.js
+++ /dev/null
@@ -1,30 +0,0 @@
-// main/ipc/mq.js
-const { ipcMain } = require('electron')
-const mq = require('../../js/rabbitmq-service') // 复用你的文件
-let currentTenantId = null
-
-async function startConsumer(emitMessage, tenantId) {
- await mq.startConsumer(
- `q.tenant.${tenantId}`,
- (msg) => emitMessage(msg.json ?? msg.text),
- { prefetch: 1, requeueOnError: false, durable: true, assertQueue: true }
- )
-}
-
-function registerMqIpc(emitMessage) {
- ipcMain.removeHandler('start-mq')
- ipcMain.handle('start-mq', async (_event, tenantId, userId) => {
- currentTenantId = tenantId
- await startConsumer(emitMessage, tenantId)
- return { ok: true }
- })
-
- ipcMain.removeHandler('mq-send')
- ipcMain.handle('mq-send', async (_event, payload) => {
- if (!currentTenantId) return { ok: false, error: 'tenant not set' }
- await mq.publishToQueue(`q.tenant.${currentTenantId}`, payload)
- return { ok: true }
- })
-}
-
-module.exports = { registerMqIpc }
diff --git a/main/ipc/system.js b/main/ipc/system.js
deleted file mode 100644
index ab8ff9a..0000000
--- a/main/ipc/system.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// main/ipc/system.js
-const { ipcMain, dialog } = require('electron')
-
-function registerSystemIpc() {
- ipcMain.removeHandler('manual-gc')
- ipcMain.handle('manual-gc', () => {
- if (global.gc) {
- global.gc()
- console.log('🧹 手动触发 GC 成功')
- } else {
- console.warn('⚠️ global.gc 不存在,请确认 --expose-gc')
- }
- })
- // 你也可以把 select-file 放这里(如果不想放 window.js)
-}
-
-module.exports = { registerSystemIpc }
diff --git a/main/preload.js b/main/preload.js
deleted file mode 100644
index f6bc374..0000000
--- a/main/preload.js
+++ /dev/null
@@ -1,30 +0,0 @@
-// preload.js
-const { contextBridge, ipcRenderer } = require('electron')
-
-contextBridge.exposeInMainWorld('electronAPI', {
- selectApkFile: () => ipcRenderer.invoke('select-apk-file'),
- selectFile: () => ipcRenderer.invoke('select-file'),
- manualGc: () => ipcRenderer.invoke('manual-gc'),
- mqSend: (arg) => ipcRenderer.invoke('mq-send', arg),
- startMq: (tendid, id) => ipcRenderer.invoke('start-mq', tendid, id),
- fileExists: (url) => ipcRenderer.invoke('file-exists', url),
- isiproxy: (url) => ipcRenderer.invoke('isiproxy', url),
-})
-
-contextBridge.exposeInMainWorld('appUpdater', {
- onAvailable: (cb) => ipcRenderer.on('update:available', (_e, info) => cb(info)),
- onProgress: (cb) => ipcRenderer.on('update:progress', (_e, p) => cb(p)),
- onDownloaded: (cb) => ipcRenderer.on('update:downloaded', (_e, info) => cb(info)),
- onError: (cb) => ipcRenderer.on('update:error', (_e, err) => cb(err)),
- // 主动触发
- checkNow: () => ipcRenderer.invoke('update:checkNow'),
- quitAndInstallNow: () => ipcRenderer.invoke('update:quitAndInstallNow'),
-});
-
-// 页面卸载时清理监听(可选)
-window.addEventListener('unload', () => {
- ipcRenderer.removeAllListeners('update:available');
- ipcRenderer.removeAllListeners('update:progress');
- ipcRenderer.removeAllListeners('update:downloaded');
- ipcRenderer.removeAllListeners('update:error');
-});
\ No newline at end of file
diff --git a/main/services/iosai.js b/main/services/iosai.js
deleted file mode 100644
index acfebda..0000000
--- a/main/services/iosai.js
+++ /dev/null
@@ -1,78 +0,0 @@
-// main/services/iosai.js
-const { app } = require('electron')
-const path = require('path')
-const fs = require('fs')
-const { exec } = require('child_process')
-const axios = require('axios')
-
-let iosAIProcess = null
-let userData = { tenantId: null, userId: null, tokenValue: null } // 如果需要共享,请提供 setter
-
-function resolveIOSAIPath() {
- const candidates = [
- path.join(path.dirname(process.execPath), 'iOSAI', 'IOSAI.exe'),
- path.join(process.resourcesPath || '', 'iOSAI', 'IOSAI.exe'),
- path.join(__dirname, '..', 'iOSAI', 'IOSAI.exe'),
- path.join(process.cwd(), 'iOSAI', 'IOSAI.exe'),
- ]
- for (const p of candidates) {
- try { if (fs.existsSync(p)) return p } catch { }
- }
- return null
-}
-
-function startIOSAIExecutable() {
- if (process.platform !== 'win32') {
- console.warn('[IOSAI] 非 Windows,跳过')
- return
- }
- const exePath = resolveIOSAIPath()
- if (!exePath) return console.error('[IOSAI] 未找到 IOSAI.exe')
-
- const exeDir = path.dirname(exePath)
- const exeFile = path.basename(exePath)
- const cmd = `start "" /D "${exeDir}" "${exeFile}"`
-
- try {
- exec(cmd, { cwd: exeDir, windowsHide: false }, (err) => {
- if (err) console.error('[IOSAI] 启动失败:', err)
- else console.log('[IOSAI] 启动命令已执行')
- })
- } catch (e) { console.error('[IOSAI] 启动异常:', e) }
-}
-
-async function killIOSAI() {
- try {
- if (userData.userId && userData.tenantId) {
- 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 } }
- ).catch(e => console.log('[IOSAI] 登出失败', e?.message))
- }
- if (iosAIProcess?.pid) {
- exec(`taskkill /PID ${iosAIProcess.pid} /T /F`, (err) => {
- if (err) {
- exec('taskkill /IM IOSAI.exe /F')
- }
- })
- } else {
- exec('taskkill /IM IOSAI.exe /F')
- exec('taskkill /IM iproxy.exe /F')
- exec('taskkill /IM tidevice.exe /F')
- }
- } catch { }
- iosAIProcess = null
-}
-
-function isProcessRunningWin(exeName) {
- return new Promise((resolve) => {
- if (process.platform !== 'win32') return resolve(true)
- const cmd = `tasklist /FI "IMAGENAME eq ${exeName}" /FO CSV /NH`
- exec(cmd, { windowsHide: true }, (err, stdout) => {
- if (err || !stdout) return resolve(false)
- resolve(stdout.toLowerCase().includes(`"${exeName.toLowerCase()}"`))
- })
- })
-}
-
-module.exports = { startIOSAIExecutable, killIOSAI, isProcessRunningWin }
diff --git a/main/services/sse.js b/main/services/sse.js
deleted file mode 100644
index f18837d..0000000
--- a/main/services/sse.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// main/services/sse.js
-const { startSSE } = require('../../js/sse-server')
-const { createBurstBroadcaster } = require('../../js/burst-broadcast')
-
-function start() {
- const sseServer = startSSE()
- const broadcast = createBurstBroadcaster(sseServer.broadcast, {
- event: 'message',
- idleMs: 10_000,
- startPayload: 'start',
- startOnFirst: true
- })
- // 返回一个统一接口
- return {
- broadcast: (event, payload) => sseServer.broadcast(event, payload),
- burst: broadcast,
- }
-}
-
-module.exports = { startSSE: start }
diff --git a/main/updater.js b/main/updater.js
deleted file mode 100644
index 0a63a1b..0000000
--- a/main/updater.js
+++ /dev/null
@@ -1,69 +0,0 @@
-// main/updater.js
-const { app, BrowserWindow, ipcMain } = require('electron')
-const { autoUpdater } = require('electron-updater')
-const log = require('electron-log')
-Object.assign(console, log.functions)
-
-const updaterState = {
- updateInProgress: false,
- updateDownloaded: false,
- pendingNavs: [],
-}
-function flushPendingNavs() {
- const fns = updaterState.pendingNavs.slice()
- updaterState.pendingNavs = []
- for (const fn of fns) try { fn() } catch (e) { console.warn('[Nav defer err]', e) }
-}
-function broadcast(channel, payload) {
- BrowserWindow.getAllWindows().forEach(w => !w.isDestroyed() && w.webContents.send(channel, payload))
-}
-
-function registerUpdater() {
- autoUpdater.logger = log
- autoUpdater.logger.transports.file.level = 'info'
- autoUpdater.autoDownload = true
- autoUpdater.autoInstallOnAppQuit = false
-
- autoUpdater.on('checking-for-update', () => console.log('[updater] checking...'))
- autoUpdater.on('update-available', (info) => {
- updaterState.updateInProgress = true
- console.log('[updater] available', info.version)
- broadcast('update:available', info)
- })
- autoUpdater.on('update-not-available', () => {
- console.log('[updater] not-available')
- updaterState.updateInProgress = false
- flushPendingNavs()
- })
- let lastSend = 0
- autoUpdater.on('download-progress', (p) => {
- updaterState.updateInProgress = true
- const now = Date.now()
- if (now - lastSend > 150) {
- lastSend = now
- broadcast('update:progress', p)
- }
- const win = BrowserWindow.getAllWindows()[0]
- if (win) win.setProgressBar(p.percent / 100)
- })
- autoUpdater.on('update-downloaded', (info) => {
- console.log('[updater] downloaded')
- updaterState.updateInProgress = true
- updaterState.updateDownloaded = true
- broadcast('update:downloaded', info)
- // 想要自动安装:autoUpdater.quitAndInstall(false, true)
- })
- autoUpdater.on('error', (err) => {
- console.error('[updater] error', err)
- broadcast('update:error', { message: String(err) })
- updaterState.updateInProgress = false
- flushPendingNavs()
- })
-
- ipcMain.handle('update:quitAndInstall', () => autoUpdater.quitAndInstall(false, true))
- ipcMain.handle('update:checkNow', () => autoUpdater.checkForUpdates())
-
- autoUpdater.checkForUpdates()
-}
-
-module.exports = { registerUpdater, updaterState }
diff --git a/main/utils/guard.js b/main/utils/guard.js
deleted file mode 100644
index 7480824..0000000
--- a/main/utils/guard.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// main/utils/guard.js
-const { app } = require('electron')
-
-function setupGuards() {
- process.on('uncaughtException', (error) => console.error('uncaughtException:', error))
- process.on('unhandledRejection', (reason) => console.error('unhandledRejection:', reason))
- app.on('web-contents-created', (_, contents) => {
- contents.on('render-process-gone', (_e, details) => console.error('渲染崩溃:', details))
- })
-}
-module.exports = { setupGuards }
diff --git a/main/utils/mem.js b/main/utils/mem.js
deleted file mode 100644
index 94735c2..0000000
--- a/main/utils/mem.js
+++ /dev/null
@@ -1,25 +0,0 @@
-// main/utils/mem.js
-const { app } = require('electron')
-
-function toMB(v) {
- if (!v || v <= 0) return 0
- const kbToMB = v / 1024
- if (kbToMB > 1) return Number(kbToMB.toFixed(1))
- return Number(v.toFixed(1))
-}
-function dumpAllMem() {
- try {
- const metrics = app.getAppMetrics()
- const report = metrics.map(m => {
- const mem = m.memory || {}
- const workingSetMB = toMB(mem.workingSetSize ?? mem.workingSet ?? 0)
- const privateMB = toMB(mem.privateBytes ?? mem.private ?? 0)
- const sharedMB = toMB(mem.shared ?? 0)
- return { pid: m.pid, type: m.type, workingSetMB, privateMB, sharedMB }
- })
- // console.log(report)
- } catch (e) {
- console.warn('getAppMetrics error:', e)
- }
-}
-module.exports = { toMB, dumpAllMem }
diff --git a/main/utils/paths.js b/main/utils/paths.js
deleted file mode 100644
index a960dc6..0000000
--- a/main/utils/paths.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// main/utils/paths.js
-const path = require('path')
-const { fileURLToPath } = require('node:url')
-const { app } = require('electron')
-
-function normalizePath(targetPath, baseDir) {
- if (typeof targetPath !== 'string' || !targetPath.trim()) throw new Error('无效的路径参数')
- if (targetPath.startsWith('file://')) return fileURLToPath(new URL(targetPath))
- if (!path.isAbsolute(targetPath)) {
- const base = baseDir && typeof baseDir === 'string' ? baseDir : process.cwd()
- return path.resolve(base, targetPath)
- }
- return path.resolve(targetPath)
-}
-
-function resolveResource(relPath) {
- const base = app.isPackaged ? process.resourcesPath : path.resolve(__dirname, '..')
- return path.join(base, relPath)
-}
-
-module.exports = { normalizePath, resolveResource }
diff --git a/main/window.js b/main/window.js
deleted file mode 100644
index 2b5e5bf..0000000
--- a/main/window.js
+++ /dev/null
@@ -1,96 +0,0 @@
-// main/window.js
-const { app, BrowserWindow, dialog, ipcMain } = require('electron')
-const path = require('path')
-const fs = require('fs')
-const { isProcessRunningWin } = require('./services/iosai')
-
-function safeLoadURL(win, url, onFail) {
- if (!win || win.isDestroyed()) return
- win.loadURL(url).catch(err => { console.warn('[loadURL fail]', err); onFail?.(err) })
-}
-
-function createMainWindow({ updaterState }) {
- const win = new BrowserWindow({
- width: 1920,
- height: 1080,
- title: `YOLO(AI助手ios) v${app.getVersion()}`,
- frame: true,
- webPreferences: {
- preload: path.join(__dirname, 'preload.js'),
- contextIsolation: true,
- nodeIntegration: false,
- sandbox: true,
- webSecurity: true,
- backgroundThrottling: false,
- enableRemoteModule: false,
- offscreen: false,
- experimentalFeatures: true,
- autoplayPolicy: 'no-user-gesture-required',
- devTools: true,
- }
- })
- win.setMenu(null)
- win.maximize()
-
- const isProd = app.isPackaged
- const targetURL = isProd ? 'https://iosai.yolozs.com' : 'http://192.168.1.128:8080'
- console.log('[page] target:', targetURL)
-
- const tryNavigate = (reason = '') => {
- const ts = Date.now()
- const go = () => safeLoadURL(win, `${targetURL}?t=${ts}`)
- if (updaterState.updateInProgress) {
- console.log(`[Nav] blocked (${reason}): updating...`)
- updaterState.pendingNavs.push(go)
- return
- }
- go()
- }
-
- // 等待页
- const loadingFile = path.join(__dirname, '..', 'waiting.html')
- if (process.platform === 'win32') {
- if (fs.existsSync(loadingFile)) {
- win.loadFile(loadingFile).catch(() => win.loadURL('data:text/html,正在等待 iproxy.exe 启动…
'))
- } else {
- win.loadURL('data:text/html,正在等待 iproxy.exe 启动…
')
- }
- waitForProcessAndNavigate(win, targetURL, { exeName: 'iproxy.exe' }, tryNavigate)
- } else {
- tryNavigate('non-win-first')
- }
-
- // 选择文件对话框(放这也行,但我在 system.js 里也提供了封装)
- ipcMain.handle('select-apk-file', async () => {
- const r = await dialog.showOpenDialog({ title: '选择 APK 文件', filters: [{ name: 'APK', extensions: ['apk'] }], properties: ['openFile'] })
- return r.canceled ? null : r.filePaths[0]
- })
- ipcMain.handle('select-file', async () => {
- const r = await dialog.showOpenDialog({ title: '选择文件', properties: ['openFile'] })
- return r.canceled ? null : r.filePaths[0]
- })
-
- return win
-}
-
-function waitForProcessAndNavigate(win, targetURL, { intervalMs = 2000, timeoutMs = 999999, exeName = 'iproxy.exe' } = {}, tryNavigate) {
- let elapsed = 0
- win.setProgressBar(2, { mode: 'indeterminate' })
- const timer = setInterval(async () => {
- const ok = await isProcessRunningWin(exeName)
- if (ok) {
- clearInterval(timer)
- win.setProgressBar(-1)
- console.log(`[iproxy] running, go -> ${targetURL}`)
- tryNavigate('iproxy-ok')
- return
- }
- elapsed += intervalMs
- if (elapsed >= timeoutMs) {
- console.warn(`[iproxy] wait timeout ${timeoutMs / 1000}s`)
- win.setTitle('YOLO(AI助手ios) - 正在等待 iproxy.exe 启动…(可检查连接/杀软/权限)')
- }
- }, intervalMs)
-}
-
-module.exports = { createMainWindow, safeLoadURL }
diff --git a/package.json b/package.json
index 21943e6..f73c288 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "YOLO-ios-ai",
"productName": "YOLO(AI助手ios)",
- "version": "3.2.0",
+ "version": "3.6.0",
"description": "Vue3 + WS 控制台",
"author": "yourname",
"main": "main.js",