优化
This commit is contained in:
@@ -7,6 +7,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
manualGc: () => ipcRenderer.invoke('manual-gc'),
|
manualGc: () => ipcRenderer.invoke('manual-gc'),
|
||||||
mqSend: (arg) => ipcRenderer.invoke('mq-send', arg),
|
mqSend: (arg) => ipcRenderer.invoke('mq-send', arg),
|
||||||
startMq: (tendid, id) => ipcRenderer.invoke('start-mq', tendid, id),
|
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),
|
fileExists: (url) => ipcRenderer.invoke('file-exists', url),
|
||||||
isiproxy: (url) => ipcRenderer.invoke('isiproxy', url),
|
isiproxy: (url) => ipcRenderer.invoke('isiproxy', url),
|
||||||
getVersion: () => ipcRenderer.invoke('getVersion'),
|
getVersion: () => ipcRenderer.invoke('getVersion'),
|
||||||
|
|||||||
375
main.js
375
main.js
@@ -2,7 +2,6 @@ const { app, globalShortcut, BrowserWindow, net, dialog, ipcMain } = require('el
|
|||||||
const { startSSE } = require('./js/sse-server');
|
const { startSSE } = require('./js/sse-server');
|
||||||
const { createBurstBroadcaster } = require('./js/burst-broadcast');
|
const { createBurstBroadcaster } = require('./js/burst-broadcast');
|
||||||
const mq = require('./js/rabbitmq-service');
|
const mq = require('./js/rabbitmq-service');
|
||||||
const https = require('https')
|
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const fsp = require('node:fs/promises')
|
const fsp = require('node:fs/promises')
|
||||||
@@ -28,10 +27,6 @@ const LOCAL_HTTPS_WHITELIST = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
let userData = { tenantId: null, userId: null }
|
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,保留此行
|
const { exec, spawn, execFile } = require('child_process'); // 如果你用 exec 启动 scrcpy,保留此行
|
||||||
|
|
||||||
// app.commandLine.appendSwitch('remote-debugging-port', '9222'); //远程控制台端口F12
|
// app.commandLine.appendSwitch('remote-debugging-port', '9222'); //远程控制台端口F12
|
||||||
@@ -314,75 +309,132 @@ function dumpAllMem() {
|
|||||||
|
|
||||||
// 在函数外定义计数器(或者放在函数内部,用闭包封装)
|
// 在函数外定义计数器(或者放在函数内部,用闭包封装)
|
||||||
let consumeCount = 0;
|
let consumeCount = 0;
|
||||||
function syncMqEnabled() {
|
// 存储活跃的消费者 { key: configObject }
|
||||||
mqEnabled = Boolean(mqQueueEnabled.crawler || mqQueueEnabled.boss);
|
const activeConsumers = new Map();
|
||||||
|
// 辅助函数:生成消费者 key
|
||||||
|
const getConsumerKey = (type, tenantId) => `${type}:${tenantId}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动指定类型的 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);
|
||||||
|
|
||||||
|
// 防止重复启动
|
||||||
|
if (activeConsumers.has(key)) {
|
||||||
|
console.log(`[MQ] ${type} 消费者已在运行,跳过启动`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQueueNameByType(type, tenantId) {
|
let qName = '';
|
||||||
if (type === 'crawler') return `q.tenant.${tenantId}`;
|
// 根据类型决定队列名
|
||||||
if (type === 'boss') return `b.tenant.${tenantId}`;
|
if (type === 'crawler') {
|
||||||
return null;
|
qName = `q.tenant.${tenantId}`;
|
||||||
|
} else if (type === 'boss') {
|
||||||
|
qName = `b.tenant.${tenantId}`;
|
||||||
|
} else {
|
||||||
|
console.warn(`[MQ] 未知消费者类型: ${type}`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startMQConsumer(queueName, emitMessage) {
|
console.log(`[MQ] 正在启动消费者: ${type} -> ${qName}`);
|
||||||
if (!queueName || mqConsumers.has(queueName)) return;
|
|
||||||
|
try {
|
||||||
const consumer = await mq.startConsumer(
|
const consumer = await mq.startConsumer(
|
||||||
queueName,
|
qName,
|
||||||
async (msg) => {
|
async (msg) => {
|
||||||
const payload = msg.json ?? msg.text;
|
const payload = msg.json ?? msg.text; // 原始业务数据
|
||||||
consumeCount++;
|
consumeCount++; // 所有队列共用计数器
|
||||||
|
|
||||||
const isBurstQueue = queueName.startsWith('b.');
|
// 标记来源类型:爬虫=1 (q.tenant.*) / 大哥=2 (b.tenant.*)
|
||||||
const meta = isBurstQueue ? 2 : 1;
|
const meta = (type === 'boss') ? 2 : 1;
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[MQ] [${queueName}]`,
|
`[MQ消费] [${qName}]`,
|
||||||
payload?.hostsId,
|
payload?.hostsId,
|
||||||
payload?.country,
|
payload?.country,
|
||||||
'count=',
|
'Start' // 简单标记一下
|
||||||
consumeCount
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ⚠️ 关键:在原有 payload 的基础上,增加 _mqMeta 字段
|
||||||
const wrapped = {
|
const wrapped = {
|
||||||
...payload,
|
...payload,
|
||||||
_mqMeta: meta
|
_mqMeta: meta
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 广播到前端
|
||||||
emitMessage(wrapped);
|
emitMessage(wrapped);
|
||||||
|
// 成功返回会在 mq 客户端内部自动 ack
|
||||||
},
|
},
|
||||||
{ prefetch: 1, requeueOnError: false, durable: true, assertQueue: true }
|
{ prefetch: 1, requeueOnError: false, durable: true, assertQueue: true }
|
||||||
);
|
);
|
||||||
mqConsumers.set(queueName, consumer);
|
console.log('启动成功')
|
||||||
|
// 记录下来,以便后续关闭
|
||||||
|
activeConsumers.set(key, {
|
||||||
|
type,
|
||||||
|
tenantId,
|
||||||
|
qName,
|
||||||
|
consumer // 包含 .stop() 方法
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[MQ] 启动消费者失败 (${type}):`, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stopMQConsumer(queueName) {
|
/**
|
||||||
const consumer = mqConsumers.get(queueName);
|
* 停止指定类型的 MQ 消费者
|
||||||
if (!consumer) {
|
*/
|
||||||
console.warn('[MQ] stopMQConsumer: no consumer for', queueName);
|
async function stopConsumerByType(type, tenantId) {
|
||||||
|
const key = getConsumerKey(type, tenantId);
|
||||||
|
const item = activeConsumers.get(key);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
// console.log(`[MQ] ${type} 消费者未运行,无需停止`);
|
||||||
return;
|
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)) {
|
console.log(`[MQ] 正在停止消费者: ${type} -> ${item.qName}`);
|
||||||
if (!mqQueueEnabled[type]) continue;
|
try {
|
||||||
const qName = getQueueNameByType(type, tenantId);
|
if (item.consumer && typeof item.consumer.stop === 'function') {
|
||||||
await startMQConsumer(qName, emitMessage);
|
await item.consumer.stop();
|
||||||
|
console.log(`[MQ] 停止消费者成功: ${type} -> ${item.qName}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[MQ] 停止消费者异常 (${type}):`, e);
|
||||||
|
} finally {
|
||||||
|
activeConsumers.delete(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mqActive = mqConsumers.size > 0;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 MQ 管理器(替代原来的 setupMQConsumerAndPublisher)
|
||||||
|
* 现在它主要负责初始化“发送端”,并注册 toggle 监听
|
||||||
|
*/
|
||||||
|
async function setupMQManager(emitMessage, tenantId) {
|
||||||
|
// 供渲染进程发送消息到队列(保持原来的 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 };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 注册切换监听
|
||||||
|
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 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 = 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);
|
console.log('[页面加载] 使用地址:', targetURL);
|
||||||
|
|
||||||
let retryIntervalId = null;
|
let retryIntervalId = null;
|
||||||
@@ -532,10 +582,7 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('getVersion', async (_event, opts = {}) => {
|
|
||||||
return app.getVersion();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// 仅检查固定路径:C:\Users\Administrator\IOSAI\aiConfig.json
|
// 仅检查固定路径:C:\Users\Administrator\IOSAI\aiConfig.json
|
||||||
ipcMain.handle('file-exists', async () => {
|
ipcMain.handle('file-exists', async () => {
|
||||||
@@ -635,19 +682,10 @@ function createWindow() {
|
|||||||
// 如果没有把加载页文件放到指定位置,给个兜底
|
// 如果没有把加载页文件放到指定位置,给个兜底
|
||||||
win.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent('<h3>正在等待 iproxy.exe 启动…</h3>'));
|
win.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent('<h3>正在等待 iproxy.exe 启动…</h3>'));
|
||||||
}
|
}
|
||||||
// 🆕 先在等待页面做虚拟机检测:如果是虚拟机 → 弹窗 + 退出
|
// ✅ 不再自动检测 iproxy,3 秒后直接进入业务页面
|
||||||
(async () => {
|
|
||||||
const isVM = await detectVMAndHandle(win);
|
|
||||||
if (isVM) {
|
|
||||||
// detectVMAndHandle 里已经 app.quit(),这里直接返回
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 非虚拟机:3 秒后正常进入业务页面
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
tryNavigate('delay-3s-autogo');
|
tryNavigate('delay-3s-autogo');
|
||||||
}, 3000);
|
}, 3000);
|
||||||
})();
|
|
||||||
// // 开始检测 iproxy,检测到后再跳转 targetURL
|
// // 开始检测 iproxy,检测到后再跳转 targetURL
|
||||||
// waitForIproxyAndNavigate(win, targetURL, {
|
// waitForIproxyAndNavigate(win, targetURL, {
|
||||||
// intervalMs: 2000,
|
// intervalMs: 2000,
|
||||||
@@ -673,88 +711,9 @@ function createWindow() {
|
|||||||
ipcMain.handle('start-mq', async (event, tentId, id) => {
|
ipcMain.handle('start-mq', async (event, tentId, id) => {
|
||||||
userData.tenantId = tentId;
|
userData.tenantId = tentId;
|
||||||
userData.userId = id;
|
userData.userId = id;
|
||||||
syncMqEnabled();
|
//启动mq管理器(但不立即启动消费,等待前端发指令)
|
||||||
if (!mqEnabled) {
|
setupMQManager(emitMessage, tentId)
|
||||||
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
|
// 可选:开发阶段打开 DevTools F12
|
||||||
// win.webContents.openDevTools();
|
// win.webContents.openDevTools();
|
||||||
@@ -901,25 +860,18 @@ function startIOSAIExecutable() {
|
|||||||
async function killIOSAI() {
|
async function killIOSAI() {
|
||||||
console.log('尝试关闭IOSAI', userData);
|
console.log('尝试关闭IOSAI', userData);
|
||||||
// console.log('axios', axios);
|
// console.log('axios', axios);
|
||||||
const httpsAgent = new https.Agent({
|
|
||||||
family: 4 // 强制使用 IPv4
|
|
||||||
})
|
|
||||||
|
|
||||||
await axios.post(
|
await axios.post('https://crawlclient.api.yolozs.com/api/user/aiChat-logout', { userId: userData.userId, tenantId: userData.tenantId }, {
|
||||||
'https://crawlclient.api.yolozs.com/api/user/aiChat-logout',
|
|
||||||
{ userId: userData.userId, tenantId: userData.tenantId },
|
|
||||||
{
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json', // 设置请求头
|
||||||
'vvtoken': userData.tokenValue
|
'vvtoken': userData.tokenValue
|
||||||
},
|
|
||||||
httpsAgent,
|
|
||||||
timeout: 5000 // 建议加个超时时间,避免死卡
|
|
||||||
}
|
}
|
||||||
).then(res => {
|
}).then(response => {
|
||||||
console.log('发送登出请求成功')
|
console.log("发送登出请求成功")
|
||||||
}).catch(err => {
|
}).catch(error => {
|
||||||
console.log('发送登出请求错误', err)
|
console.log("发送登出请求错误", error)
|
||||||
|
}).finally(error => {
|
||||||
|
// console.log("发送")
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -1025,124 +977,3 @@ function normalizePath(targetPath, baseDir) {
|
|||||||
}
|
}
|
||||||
return path.resolve(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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "YOLO-ios-ai",
|
"name": "YOLO-ios-ai",
|
||||||
"productName": "YOLO(AI助手ios)",
|
"productName": "YOLO(AI助手ios)",
|
||||||
"version": "3.6.0",
|
"version": "3.6.2",
|
||||||
"description": "Vue3 + WS 控制台",
|
"description": "Vue3 + WS 控制台",
|
||||||
"author": "yourname",
|
"author": "yourname",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
|
|||||||
Reference in New Issue
Block a user