3.6版本

This commit is contained in:
2026-01-12 16:08:42 +08:00
parent 55542dd212
commit 2e77f12e56
16 changed files with 400 additions and 580 deletions

353
main.js
View File

@@ -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('<h3>正在等待 iproxy.exe 启动…</h3>'));
}
// ✅ 不再自动检测 iproxy3 秒后直接进入业务页面
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)
}
}
// ======== 虚拟机检测相关工具函数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<boolean>
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;
}