3.1.0稳定版
This commit is contained in:
@@ -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
120
main.js
@@ -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;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "YOLO-ios-ai",
|
"name": "YOLO-ios-ai",
|
||||||
"productName": "YOLO(AI助手ios)",
|
"productName": "YOLO(AI助手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",
|
||||||
|
|||||||
Reference in New Issue
Block a user