mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-09-08 00:47:15 +08:00
WIP: migrate to mulit process manager
This commit is contained in:
+43
-32
@@ -54,13 +54,13 @@ const server = net.createServer((socket) => {
|
|||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.error('解析JSON消息失败:', parseError.message);
|
console.error('解析JSON消息失败:', parseError.message);
|
||||||
console.error('原始数据:', trimmedLine.substring(0, 100)); // 只打印前100个字符
|
console.error('原始数据:', trimmedLine.substring(0, 100)); // 只打印前100个字符
|
||||||
sendResponse(socket, { error: '无效的JSON格式', details: parseError.message });
|
sendResponse(socket, message, { error: '无效的JSON格式', details: parseError.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
splitStream.on('error', (err) => {
|
splitStream.on('error', (err) => {
|
||||||
console.error('split2 流处理错误:', err);
|
console.error('split2 流处理错误:', err);
|
||||||
sendResponse(socket, { error: '流处理失败' });
|
sendResponse(socket, message, { error: '流处理失败' });
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('error', (err) => {
|
socket.on('error', (err) => {
|
||||||
@@ -92,7 +92,7 @@ function handleMessage(socket, message) {
|
|||||||
// 检查是否在停止列表中(防止竞态条件)
|
// 检查是否在停止列表中(防止竞态条件)
|
||||||
if (stoppedWorkers.has(workerId)) {
|
if (stoppedWorkers.has(workerId)) {
|
||||||
console.log(`工具进程 ${workerId} 尝试注册,但已被标记为停止,拒绝注册`);
|
console.log(`工具进程 ${workerId} 尝试注册,但已被标记为停止,拒绝注册`);
|
||||||
sendResponse(socket, {
|
sendResponse(socket, message, {
|
||||||
error: `工具进程 ${workerId} 已被停止`,
|
error: `工具进程 ${workerId} 已被停止`,
|
||||||
shouldExit: true // 通知子进程应该退出
|
shouldExit: true // 通知子进程应该退出
|
||||||
});
|
});
|
||||||
@@ -103,7 +103,7 @@ function handleMessage(socket, message) {
|
|||||||
if (workerInfo) {
|
if (workerInfo) {
|
||||||
workerInfo.socket = socket;
|
workerInfo.socket = socket;
|
||||||
console.log(`工具进程 ${workerId} 已注册TCP连接`);
|
console.log(`工具进程 ${workerId} 已注册TCP连接`);
|
||||||
sendResponse(socket, {
|
sendResponse(socket, message, {
|
||||||
success: true,
|
success: true,
|
||||||
type: 'worker-registered',
|
type: 'worker-registered',
|
||||||
message: `工具进程 ${workerId} 连接已注册`
|
message: `工具进程 ${workerId} 连接已注册`
|
||||||
@@ -120,7 +120,7 @@ function handleMessage(socket, message) {
|
|||||||
if (!stoppedWorkers.has(workerId)) {
|
if (!stoppedWorkers.has(workerId)) {
|
||||||
console.log(`工具进程 ${workerId} 尝试注册,但workerInfo不存在`);
|
console.log(`工具进程 ${workerId} 尝试注册,但workerInfo不存在`);
|
||||||
}
|
}
|
||||||
sendResponse(socket, {
|
sendResponse(socket, message, {
|
||||||
error: `工具进程 ${workerId} 未找到`,
|
error: `工具进程 ${workerId} 未找到`,
|
||||||
shouldExit: true
|
shouldExit: true
|
||||||
});
|
});
|
||||||
@@ -133,7 +133,7 @@ function handleMessage(socket, message) {
|
|||||||
const workerId = message.workerId;
|
const workerId = message.workerId;
|
||||||
const shouldExit = stoppedWorkers.has(workerId) || !workers.has(workerId);
|
const shouldExit = stoppedWorkers.has(workerId) || !workers.has(workerId);
|
||||||
|
|
||||||
sendResponse(socket, {
|
sendResponse(socket, message, {
|
||||||
type: 'check-should-exit-response',
|
type: 'check-should-exit-response',
|
||||||
workerId: workerId,
|
workerId: workerId,
|
||||||
shouldExit: shouldExit
|
shouldExit: shouldExit
|
||||||
@@ -164,7 +164,7 @@ function handleMessage(socket, message) {
|
|||||||
workerInfo.lastHeartbeat = Date.now();
|
workerInfo.lastHeartbeat = Date.now();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sendResponse(socket, { error: '未注册的工具进程连接' });
|
sendResponse(socket, message, { error: '未注册的工具进程连接' });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -177,8 +177,19 @@ function handleMessage(socket, message) {
|
|||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'start-worker':
|
case 'start-worker':
|
||||||
startWorker(message.workerId);
|
const {
|
||||||
sendResponse(socket, {
|
workerId,
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
env
|
||||||
|
} = message
|
||||||
|
startWorker({
|
||||||
|
workerId,
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
env
|
||||||
|
});
|
||||||
|
sendResponse(socket, message, {
|
||||||
success: true,
|
success: true,
|
||||||
message: `工具进程 ${message.workerId} 已启动`,
|
message: `工具进程 ${message.workerId} 已启动`,
|
||||||
workerId: message.workerId
|
workerId: message.workerId
|
||||||
@@ -187,7 +198,7 @@ function handleMessage(socket, message) {
|
|||||||
|
|
||||||
case 'stop-worker':
|
case 'stop-worker':
|
||||||
stopWorker(message.workerId);
|
stopWorker(message.workerId);
|
||||||
sendResponse(socket, {
|
sendResponse(socket, message, {
|
||||||
success: true,
|
success: true,
|
||||||
message: `工具进程 ${message.workerId} 已停止`,
|
message: `工具进程 ${message.workerId} 已停止`,
|
||||||
workerId: message.workerId
|
workerId: message.workerId
|
||||||
@@ -196,7 +207,7 @@ function handleMessage(socket, message) {
|
|||||||
|
|
||||||
case 'get-status':
|
case 'get-status':
|
||||||
const status = getWorkersStatus();
|
const status = getWorkersStatus();
|
||||||
sendResponse(socket, {
|
sendResponse(socket, message, {
|
||||||
success: true,
|
success: true,
|
||||||
type: 'status',
|
type: 'status',
|
||||||
workers: status
|
workers: status
|
||||||
@@ -204,36 +215,26 @@ function handleMessage(socket, message) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
sendResponse(socket, { error: '未知的消息类型' });
|
sendResponse(socket, message, { error: '未知的消息类型' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动工具进程
|
// 启动工具进程
|
||||||
function startWorker(workerId, restartCount = 0) {
|
function startWorker({ workerId, command, args, env }, restartCount = 0) {
|
||||||
if (workers.has(workerId)) {
|
if (workers.has(workerId)) {
|
||||||
console.log(`工具进程 ${workerId} 已在运行`);
|
console.log(`工具进程 ${workerId} 已在运行`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`启动工具进程: ${workerId}${restartCount > 0 ? ` (重启第${restartCount}次)` : ''}`);
|
console.log(`启动工具进程: ${workerId}${restartCount > 0 ? ` (重启第${restartCount}次)` : ''}`);
|
||||||
|
|
||||||
// 使用 Electron 可执行程序路径,从环境变量获取,如果没有则回退到 node
|
|
||||||
const electronPath = process.env.ELECTRON_EXEC_PATH || 'node';
|
|
||||||
console.log(`使用执行程序路径: ${electronPath}`);
|
|
||||||
|
|
||||||
// 添加参数使工具进程在后台运行,不显示 UI
|
// 添加参数使工具进程在后台运行,不显示 UI
|
||||||
const workerProcess = spawn(electronPath, [
|
const workerProcess = spawn(command, args, {
|
||||||
'--no-sandbox',
|
|
||||||
'--disable-gpu',
|
|
||||||
'--disable-dev-shm-usage',
|
|
||||||
path.join(__dirname, 'worker.js'),
|
|
||||||
`--worker-id=${workerId}`,
|
|
||||||
`--restart-count=${restartCount.toString()}`
|
|
||||||
], {
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
ELECTRON_EXEC_PATH: electronPath // 继续传递给子进程(如果需要)
|
...env,
|
||||||
|
GEEKGEEKRUND_WORKER_ID: workerId,
|
||||||
|
GEEKGEEKRUND_RESTART_COUNT: restartCount.toString(),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -281,7 +282,7 @@ function startWorker(workerId, restartCount = 0) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 再次检查:确保worker不在停止列表中,且当前没有运行
|
// 再次检查:确保worker不在停止列表中,且当前没有运行
|
||||||
if (!workers.has(workerId) && !stoppedWorkers.has(workerId)) {
|
if (!workers.has(workerId) && !stoppedWorkers.has(workerId)) {
|
||||||
startWorker(workerId, restartCount);
|
startWorker({ workerId, command, args, env }, restartCount);
|
||||||
} else if (stoppedWorkers.has(workerId)) {
|
} else if (stoppedWorkers.has(workerId)) {
|
||||||
console.log(`工具进程 ${workerId} 在重启前被标记为停止,取消重启`);
|
console.log(`工具进程 ${workerId} 在重启前被标记为停止,取消重启`);
|
||||||
// 从停止列表中移除,因为已经处理完毕
|
// 从停止列表中移除,因为已经处理完毕
|
||||||
@@ -302,13 +303,20 @@ function startWorker(workerId, restartCount = 0) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
workerProcess.on('error', (err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
workers.set(workerId, {
|
workers.set(workerId, {
|
||||||
process: workerProcess,
|
process: workerProcess,
|
||||||
status: 'running',
|
status: 'running',
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
restartCount, // 使用传入的重启次数
|
restartCount, // 使用传入的重启次数
|
||||||
socket: null, // 工具进程的TCP连接,稍后由工具进程注册
|
socket: null, // 工具进程的TCP连接,稍后由工具进程注册
|
||||||
lastHeartbeat: null
|
lastHeartbeat: null,
|
||||||
|
command,
|
||||||
|
env,
|
||||||
|
workerId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 定期发送状态更新
|
// 定期发送状态更新
|
||||||
@@ -385,7 +393,7 @@ function broadcastToGUI(message) {
|
|||||||
guiClients.forEach(socket => {
|
guiClients.forEach(socket => {
|
||||||
if (!socket.destroyed) {
|
if (!socket.destroyed) {
|
||||||
try {
|
try {
|
||||||
sendResponse(socket, message);
|
sendResponse(socket, null, message);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('广播消息失败:', e);
|
console.error('广播消息失败:', e);
|
||||||
guiClients.delete(socket);
|
guiClients.delete(socket);
|
||||||
@@ -395,9 +403,12 @@ function broadcastToGUI(message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 发送响应
|
// 发送响应
|
||||||
function sendResponse(socket, response) {
|
function sendResponse(socket, request, response) {
|
||||||
try {
|
try {
|
||||||
socket.write(JSON.stringify(response) + '\n');
|
socket.write(JSON.stringify({
|
||||||
|
...response,
|
||||||
|
_callbackUuid: request?._callbackUuid
|
||||||
|
}) + '\n');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('发送响应失败:', e);
|
console.error('发送响应失败:', e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
|
||||||
|
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import { pipeWriteRegardlessError } from '../utils/pipe'
|
// import { pipeWriteRegardlessError } from '../utils/pipe'
|
||||||
import { getAnyAvailablePuppeteerExecutable } from '../CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable'
|
import { getAnyAvailablePuppeteerExecutable } from '../CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable'
|
||||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||||
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../common/enums/auto-start-chat'
|
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../common/enums/auto-start-chat'
|
||||||
@@ -16,6 +16,7 @@ import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerF
|
|||||||
import SqlitePluginModule from '@geekgeekrun/sqlite-plugin'
|
import SqlitePluginModule from '@geekgeekrun/sqlite-plugin'
|
||||||
import gtag from '../../utils/gtag'
|
import gtag from '../../utils/gtag'
|
||||||
import GtagPlugin from '../../utils/gtag/GtagPlugin'
|
import GtagPlugin from '../../utils/gtag/GtagPlugin'
|
||||||
|
import { connectToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
|
||||||
const { default: SqlitePlugin } = SqlitePluginModule
|
const { default: SqlitePlugin } = SqlitePluginModule
|
||||||
|
|
||||||
const rerunInterval = (() => {
|
const rerunInterval = (() => {
|
||||||
@@ -55,20 +56,20 @@ const runAutoChat = async () => {
|
|||||||
} catch {
|
} catch {
|
||||||
console.warn('pipe is not available')
|
console.warn('pipe is not available')
|
||||||
}
|
}
|
||||||
pipeWriteRegardlessError(
|
// pipeWriteRegardlessError(
|
||||||
pipe,
|
// pipe,
|
||||||
JSON.stringify({
|
// JSON.stringify({
|
||||||
type: 'INITIALIZE_PUPPETEER'
|
// type: 'INITIALIZE_PUPPETEER'
|
||||||
}) + '\r\n'
|
// }) + '\r\n'
|
||||||
)
|
// )
|
||||||
try {
|
try {
|
||||||
await initPuppeteer()
|
await initPuppeteer()
|
||||||
pipeWriteRegardlessError(
|
// pipeWriteRegardlessError(
|
||||||
pipe,
|
// pipe,
|
||||||
JSON.stringify({
|
// JSON.stringify({
|
||||||
type: 'PUPPETEER_INITIALIZE_SUCCESSFULLY'
|
// type: 'PUPPETEER_INITIALIZE_SUCCESSFULLY'
|
||||||
}) + '\r\n'
|
// }) + '\r\n'
|
||||||
)
|
// )
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
|
app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
|
||||||
@@ -101,20 +102,20 @@ const runAutoChat = async () => {
|
|||||||
initPlugins(hooks)
|
initPlugins(hooks)
|
||||||
|
|
||||||
gtag('run_auto_chat_with_boss_main_ready')
|
gtag('run_auto_chat_with_boss_main_ready')
|
||||||
pipeWriteRegardlessError(
|
// pipeWriteRegardlessError(
|
||||||
pipe,
|
// pipe,
|
||||||
JSON.stringify({
|
// JSON.stringify({
|
||||||
type: 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED' //geek-auto-start-chat-with-boss-started
|
// type: 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED' //geek-auto-start-chat-with-boss-started
|
||||||
}) + '\r\n'
|
// }) + '\r\n'
|
||||||
)
|
// )
|
||||||
|
|
||||||
autoStartChatEventBus.once('LOGIN_STATUS_INVALID', () => {
|
autoStartChatEventBus.once('LOGIN_STATUS_INVALID', () => {
|
||||||
pipeWriteRegardlessError(
|
// pipeWriteRegardlessError(
|
||||||
pipe,
|
// pipe,
|
||||||
JSON.stringify({
|
// JSON.stringify({
|
||||||
type: 'LOGIN_STATUS_INVALID' //geek-auto-start-chat-with-boss-started
|
// type: 'LOGIN_STATUS_INVALID' //geek-auto-start-chat-with-boss-started
|
||||||
}) + '\r\n'
|
// }) + '\r\n'
|
||||||
)
|
// )
|
||||||
})
|
})
|
||||||
|
|
||||||
while (![isParentProcessDisconnect].includes(true)) {
|
while (![isParentProcessDisconnect].includes(true)) {
|
||||||
@@ -151,13 +152,14 @@ const runAutoChat = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const waitForProcessHandShakeAndRunAutoChat = () => {
|
export const waitForProcessHandShakeAndRunAutoChat = () => {
|
||||||
let pipe: null | fs.WriteStream = null
|
// let pipe: null | fs.WriteStream = null
|
||||||
try {
|
// try {
|
||||||
pipe = fs.createWriteStream(null, { fd: 3 })
|
// pipe = fs.createWriteStream(null, { fd: 3 })
|
||||||
} catch {
|
// } catch {
|
||||||
console.error('pipe is not available')
|
// console.error('pipe is not available')
|
||||||
app.exit(1)
|
// app.exit(1)
|
||||||
}
|
// }
|
||||||
|
connectToDaemon()
|
||||||
runAutoChat()
|
runAutoChat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
const { app, ipcMain } = require('electron');
|
|
||||||
const path = require('path');
|
|
||||||
const { spawn } = require('child_process');
|
|
||||||
const net = require('net');
|
|
||||||
const split2 = require('split2');
|
|
||||||
|
|
||||||
const isUiDev = process.env.NODE_ENV === 'development'
|
|
||||||
const DAEMON_PORT = 12345;
|
|
||||||
|
|
||||||
export function launchDaemon() {
|
|
||||||
let daemonProcess = null;
|
|
||||||
|
|
||||||
// 所有窗口关闭时
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
// if (process.platform !== 'darwin') {
|
|
||||||
// 关闭守护进程
|
|
||||||
if (daemonProcess) {
|
|
||||||
daemonProcess.kill();
|
|
||||||
}
|
|
||||||
// app.quit();
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 应用退出前清理
|
|
||||||
app.on('before-quit', () => {
|
|
||||||
if (daemonProcess) {
|
|
||||||
daemonProcess.kill();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 启动守护进程
|
|
||||||
function startDaemon() {
|
|
||||||
console.log('启动守护进程...');
|
|
||||||
// 使用 Electron 可执行程序路径,如果没有则回退到 node
|
|
||||||
const electronPath = process.execPath;
|
|
||||||
console.log(`使用 Electron 路径: ${electronPath}`);
|
|
||||||
|
|
||||||
// 添加参数使守护进程在后台运行,不显示 UI
|
|
||||||
daemonProcess = spawn(
|
|
||||||
process.argv[0],
|
|
||||||
isUiDev
|
|
||||||
? [process.argv[1], `--mode=launchDaemon`]
|
|
||||||
: [`--mode=launchDaemon`],
|
|
||||||
{
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
|
||||||
detached: false,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// daemonProcess = spawn(electronPath, [
|
|
||||||
// '--no-sandbox',
|
|
||||||
// '--disable-dev-shm-usage',
|
|
||||||
// path.join(__dirname, 'daemon.js')
|
|
||||||
// ], {
|
|
||||||
// stdio: ['ignore', 'pipe', 'pipe'],
|
|
||||||
// detached: false,
|
|
||||||
// env: {
|
|
||||||
// ...process.env,
|
|
||||||
// ELECTRON_EXEC_PATH: electronPath // 传递给守护进程,用于启动 worker
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
daemonProcess.stdout.on('data', (data) => {
|
|
||||||
console.log(`守护进程输出: ${data}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
daemonProcess.stderr.on('data', (data) => {
|
|
||||||
console.error(`守护进程错误: ${data}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
daemonProcess.on('exit', (code) => {
|
|
||||||
console.log(`守护进程退出,代码: ${code}`);
|
|
||||||
// 如果守护进程意外退出,尝试重启
|
|
||||||
if (code !== 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('尝试重启守护进程...');
|
|
||||||
startDaemon();
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 等待守护进程启动后连接
|
|
||||||
setTimeout(() => {
|
|
||||||
connectToDaemon();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用准备就绪
|
|
||||||
return app.whenReady().then(() => {
|
|
||||||
startDaemon();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function connectToDaemon() {
|
|
||||||
let daemonClient = null;
|
|
||||||
// 连接到守护进程
|
|
||||||
function _connectToDaemon() {
|
|
||||||
daemonClient = new net.Socket();
|
|
||||||
daemonClient.connect(DAEMON_PORT, 'localhost', () => {
|
|
||||||
console.log('已连接到守护进程');
|
|
||||||
// 通知渲染进程连接成功
|
|
||||||
// if (mainWindow) {
|
|
||||||
// mainWindow.webContents.send('daemon-connected');
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
// 使用 split2 按行分割流式数据,处理 JSONL 格式(每行一个 JSON)
|
|
||||||
const splitStream = split2();
|
|
||||||
daemonClient.pipe(splitStream).on('data', (line) => {
|
|
||||||
const trimmedLine = line.toString().trim();
|
|
||||||
if (!trimmedLine) {
|
|
||||||
return; // 跳过空行
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const message = JSON.parse(trimmedLine);
|
|
||||||
console.log('收到守护进程消息:', message);
|
|
||||||
// 转发消息到渲染进程
|
|
||||||
// if (mainWindow) {
|
|
||||||
// mainWindow.webContents.send('daemon-message', message);
|
|
||||||
// }
|
|
||||||
} catch (parseError) {
|
|
||||||
console.error('解析守护进程消息失败:', parseError.message);
|
|
||||||
console.error('原始数据:', trimmedLine.substring(0, 100));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
splitStream.on('error', (err) => {
|
|
||||||
console.error('split2 流处理错误:', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
daemonClient.on('error', (err) => {
|
|
||||||
console.error('守护进程连接错误:', err);
|
|
||||||
// 尝试重连
|
|
||||||
setTimeout(() => {
|
|
||||||
if (daemonClient.destroyed) {
|
|
||||||
_connectToDaemon();
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
|
|
||||||
daemonClient.on('close', () => {
|
|
||||||
console.log('守护进程连接已关闭');
|
|
||||||
// 尝试重连
|
|
||||||
setTimeout(() => {
|
|
||||||
_connectToDaemon();
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 向守护进程发送消息
|
|
||||||
function sendToDaemon(message) {
|
|
||||||
if (daemonClient && !daemonClient.destroyed) {
|
|
||||||
daemonClient.write(JSON.stringify(message) + '\n');
|
|
||||||
} else {
|
|
||||||
console.error('守护进程未连接');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPC处理:从渲染进程接收消息并转发到守护进程
|
|
||||||
ipcMain.on('send-to-daemon', (event, message) => {
|
|
||||||
sendToDaemon(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
// IPC处理:启动工具进程
|
|
||||||
ipcMain.on('start-worker', (event, workerId) => {
|
|
||||||
sendToDaemon({ type: 'start-worker', workerId });
|
|
||||||
});
|
|
||||||
|
|
||||||
// IPC处理:停止工具进程
|
|
||||||
ipcMain.on('stop-worker', (event, workerId) => {
|
|
||||||
sendToDaemon({ type: 'stop-worker', workerId });
|
|
||||||
});
|
|
||||||
|
|
||||||
// IPC处理:获取所有工具进程状态
|
|
||||||
ipcMain.on('get-workers-status', () => {
|
|
||||||
sendToDaemon({ type: 'get-status' });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (daemonClient) {
|
|
||||||
daemonClient.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
|
||||||
if (daemonClient) {
|
|
||||||
daemonClient.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_connectToDaemon()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { DAEMON_PORT } from "./daemon-config";
|
||||||
|
|
||||||
|
const net = require('net');
|
||||||
|
const split2 = require('split2');
|
||||||
|
const { app } = require('electron');
|
||||||
|
|
||||||
|
let daemonClient = null;
|
||||||
|
const waitForCallbackTaskMap = new Map()
|
||||||
|
|
||||||
|
// 连接到守护进程
|
||||||
|
export function connectToDaemon() {
|
||||||
|
daemonClient = new net.Socket();
|
||||||
|
daemonClient.connect(DAEMON_PORT, 'localhost', () => {
|
||||||
|
console.log('已连接到守护进程');
|
||||||
|
// 通知渲染进程连接成功
|
||||||
|
// if (mainWindow) {
|
||||||
|
// mainWindow.webContents.send('daemon-connected');
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
// 使用 split2 按行分割流式数据,处理 JSONL 格式(每行一个 JSON)
|
||||||
|
const splitStream = split2();
|
||||||
|
daemonClient.pipe(splitStream).on('data', (line) => {
|
||||||
|
const trimmedLine = line.toString().trim();
|
||||||
|
if (!trimmedLine) {
|
||||||
|
return; // 跳过空行
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(trimmedLine);
|
||||||
|
console.log('收到守护进程消息:', message);
|
||||||
|
if (
|
||||||
|
message._callbackUuid
|
||||||
|
) {
|
||||||
|
const callbackInfo = waitForCallbackTaskMap.get(message._callbackUuid)
|
||||||
|
if (callbackInfo) {
|
||||||
|
const isError = message._isError
|
||||||
|
if (isError) {
|
||||||
|
callbackInfo.reject(message)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
callbackInfo.resolve(message)
|
||||||
|
}
|
||||||
|
waitForCallbackTaskMap.delete(message._callbackUuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 转发消息到渲染进程
|
||||||
|
// if (mainWindow) {
|
||||||
|
// mainWindow.webContents.send('daemon-message', message);
|
||||||
|
// }
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('解析守护进程消息失败:', parseError.message);
|
||||||
|
console.error('原始数据:', trimmedLine.substring(0, 100));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
splitStream.on('error', (err) => {
|
||||||
|
console.error('split2 流处理错误:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
daemonClient.on('error', (err) => {
|
||||||
|
console.error('守护进程连接错误:', err);
|
||||||
|
// 尝试重连
|
||||||
|
setTimeout(() => {
|
||||||
|
if (daemonClient.destroyed) {
|
||||||
|
connectToDaemon();
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
daemonClient.on('close', () => {
|
||||||
|
console.log('守护进程连接已关闭');
|
||||||
|
// 尝试重连
|
||||||
|
setTimeout(() => {
|
||||||
|
connectToDaemon();
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向守护进程发送消息
|
||||||
|
export function sendToDaemon(message, {
|
||||||
|
needCallback = false,
|
||||||
|
timeout = undefined
|
||||||
|
} = {}) {
|
||||||
|
const _callbackUuid = randomUUID()
|
||||||
|
if (daemonClient && !daemonClient.destroyed) {
|
||||||
|
daemonClient.write(JSON.stringify({
|
||||||
|
...message,
|
||||||
|
_callbackUuid
|
||||||
|
}) + '\n');
|
||||||
|
if (needCallback) {
|
||||||
|
let resolve, reject
|
||||||
|
const promise = new Promise((_resolve, _reject) => {
|
||||||
|
resolve = _resolve
|
||||||
|
reject = _reject
|
||||||
|
})
|
||||||
|
waitForCallbackTaskMap.set(_callbackUuid, { resolve, reject })
|
||||||
|
promise.finally(() => waitForCallbackTaskMap.delete(_callbackUuid))
|
||||||
|
let timeoutTimer
|
||||||
|
if (!isNaN(parseInt(timeout))) {
|
||||||
|
timeoutTimer = setTimeout(() => {
|
||||||
|
reject(new Error(`Callback timeout after ${timeout}ms`))
|
||||||
|
}, timeout)
|
||||||
|
}
|
||||||
|
promise.finally(() => {
|
||||||
|
clearTimeout(timeoutTimer)
|
||||||
|
})
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('守护进程未连接');
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// // IPC处理:从渲染进程接收消息并转发到守护进程
|
||||||
|
// ipcMain.on('send-to-daemon', (event, message) => {
|
||||||
|
// sendToDaemon(message);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // IPC处理:启动工具进程
|
||||||
|
// ipcMain.on('start-worker', (event, { workerId, command, args, env }) => {
|
||||||
|
// sendToDaemon({ type: 'start-worker', workerId, command, args, env });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // IPC处理:停止工具进程
|
||||||
|
// ipcMain.on('stop-worker', (event, workerId) => {
|
||||||
|
// sendToDaemon({ type: 'stop-worker', workerId });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // IPC处理:获取所有工具进程状态
|
||||||
|
// ipcMain.on('get-workers-status', () => {
|
||||||
|
// sendToDaemon({ type: 'get-status' });
|
||||||
|
// });
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (daemonClient) {
|
||||||
|
daemonClient.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('before-quit', () => {
|
||||||
|
if (daemonClient) {
|
||||||
|
daemonClient.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const DAEMON_PORT = 12345;
|
||||||
@@ -5,7 +5,8 @@ import './app-menu'
|
|||||||
import initIpc from './ipc'
|
import initIpc from './ipc'
|
||||||
import gtag from '../../utils/gtag'
|
import gtag from '../../utils/gtag'
|
||||||
import initPublicIpc from '../../utils/initPublicIpc'
|
import initPublicIpc from '../../utils/initPublicIpc'
|
||||||
import { connectToDaemon, launchDaemon } from './attach-daemon'
|
import { launchDaemon } from './launch-daemon'
|
||||||
|
import { connectToDaemon } from './connect-to-daemon'
|
||||||
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
|
||||||
export function openSettingWindow() {
|
export function openSettingWindow() {
|
||||||
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
|
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
import { RequestSceneEnum } from '../../../features/llm-request-log'
|
import { RequestSceneEnum } from '../../../features/llm-request-log'
|
||||||
import { checkUpdateForUi } from '../../../features/updater'
|
import { checkUpdateForUi } from '../../../features/updater'
|
||||||
import gtag from '../../../utils/gtag'
|
import gtag from '../../../utils/gtag'
|
||||||
|
import { sendToDaemon } from '../connect-to-daemon'
|
||||||
|
|
||||||
export default function initIpc() {
|
export default function initIpc() {
|
||||||
ipcMain.handle('fetch-config-file-content', async () => {
|
ipcMain.handle('fetch-config-file-content', async () => {
|
||||||
@@ -200,17 +201,9 @@ export default function initIpc() {
|
|||||||
|
|
||||||
// const currentExecutablePath = app.getPath('exe')
|
// const currentExecutablePath = app.getPath('exe')
|
||||||
// console.log(currentExecutablePath)
|
// console.log(currentExecutablePath)
|
||||||
ipcMain.handle('prepare-run-geek-auto-start-chat-with-boss', async () => {
|
|
||||||
mainWindow?.webContents.send('locating-puppeteer-executable')
|
|
||||||
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
|
|
||||||
if (!puppeteerExecutable) {
|
|
||||||
return Promise.reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')
|
|
||||||
}
|
|
||||||
mainWindow?.webContents.send('puppeteer-executable-is-located')
|
|
||||||
})
|
|
||||||
|
|
||||||
let subProcessOfPuppeteer: ChildProcess | null = null
|
let subProcessOfPuppeteer: ChildProcess | null = null
|
||||||
ipcMain.handle('run-geek-auto-start-chat-with-boss', async () => {
|
ipcMain.handle('run-geek-auto-start-chat-with-boss', async (ev) => {
|
||||||
if (subProcessOfPuppeteer) {
|
if (subProcessOfPuppeteer) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -222,49 +215,79 @@ export default function initIpc() {
|
|||||||
...process.env,
|
...process.env,
|
||||||
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath
|
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath
|
||||||
}
|
}
|
||||||
subProcessOfPuppeteer = childProcess.spawn(
|
// ipcMain.emit(
|
||||||
process.argv[0],
|
// 'start-worker',
|
||||||
[
|
// ev,
|
||||||
process.argv[1],
|
// {
|
||||||
`--mode=geekAutoStartWithBossDaemon`,
|
// workerId: 'geekAutoStartWithBossMain',
|
||||||
`--mode-to-daemon=geekAutoStartWithBossMain`
|
// command: process.argv[0],
|
||||||
],
|
// args: [
|
||||||
|
// process.argv[1],
|
||||||
|
// `--mode=geekAutoStartWithBossMain`
|
||||||
|
// ],
|
||||||
|
// env: subProcessEnv
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
await sendToDaemon(
|
||||||
{
|
{
|
||||||
env: subProcessEnv,
|
type: 'start-worker',
|
||||||
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc']
|
workerId: 'geekAutoStartWithBossMain',
|
||||||
|
command: process.argv[0],
|
||||||
|
args: [
|
||||||
|
process.argv[1],
|
||||||
|
`--mode=geekAutoStartWithBossMain`
|
||||||
|
],
|
||||||
|
env: subProcessEnv
|
||||||
|
},
|
||||||
|
{
|
||||||
|
needCallback: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// console.log(subProcessOfPuppeteer)
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
subProcessOfPuppeteer!.stdio[3]!.pipe(JSONStream.parse()).on('data', async (raw) => {
|
|
||||||
const data = raw
|
|
||||||
switch (data.type) {
|
|
||||||
case 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED': {
|
|
||||||
resolve(data)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'LOGIN_STATUS_INVALID': {
|
|
||||||
await sleep(500)
|
|
||||||
mainWindow?.webContents.send('check-boss-zhipin-cookie-file')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
subProcessOfPuppeteer!.once('exit', (exitCode) => {
|
// subProcessOfPuppeteer = childProcess.spawn(
|
||||||
subProcessOfPuppeteer = null
|
// process.argv[0],
|
||||||
if (exitCode === AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE) {
|
// [
|
||||||
// means cannot find downloaded puppeteer
|
// process.argv[1],
|
||||||
reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')
|
// `--mode=geekAutoStartWithBossDaemon`,
|
||||||
} else {
|
// `--mode-to-daemon=geekAutoStartWithBossMain`
|
||||||
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopped')
|
// ],
|
||||||
}
|
// {
|
||||||
})
|
// env: subProcessEnv,
|
||||||
})
|
// stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc']
|
||||||
// TODO:
|
// }
|
||||||
|
// )
|
||||||
|
// // console.log(subProcessOfPuppeteer)
|
||||||
|
// return new Promise((resolve, reject) => {
|
||||||
|
// subProcessOfPuppeteer!.stdio[3]!.pipe(JSONStream.parse()).on('data', async (raw) => {
|
||||||
|
// const data = raw
|
||||||
|
// switch (data.type) {
|
||||||
|
// case 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED': {
|
||||||
|
// resolve(data)
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// case 'LOGIN_STATUS_INVALID': {
|
||||||
|
// await sleep(500)
|
||||||
|
// mainWindow?.webContents.send('check-boss-zhipin-cookie-file')
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// default: {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// subProcessOfPuppeteer!.once('exit', (exitCode) => {
|
||||||
|
// subProcessOfPuppeteer = null
|
||||||
|
// if (exitCode === AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE) {
|
||||||
|
// // means cannot find downloaded puppeteer
|
||||||
|
// reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')
|
||||||
|
// } else {
|
||||||
|
// mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopped')
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// // TODO:
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('run-read-no-reply-auto-reminder', async () => {
|
ipcMain.handle('run-read-no-reply-auto-reminder', async () => {
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { connectToDaemon } from "./connect-to-daemon";
|
||||||
|
|
||||||
|
const { app } = require('electron');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
|
const isUiDev = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
export function launchDaemon() {
|
||||||
|
let daemonProcess = null;
|
||||||
|
|
||||||
|
// 所有窗口关闭时
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
// if (process.platform !== 'darwin') {
|
||||||
|
// 关闭守护进程
|
||||||
|
if (daemonProcess) {
|
||||||
|
daemonProcess.kill();
|
||||||
|
}
|
||||||
|
// app.quit();
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 应用退出前清理
|
||||||
|
app.on('before-quit', () => {
|
||||||
|
if (daemonProcess) {
|
||||||
|
daemonProcess.kill();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动守护进程
|
||||||
|
function startDaemon() {
|
||||||
|
console.log('启动守护进程...');
|
||||||
|
// 使用 Electron 可执行程序路径,如果没有则回退到 node
|
||||||
|
const electronPath = process.execPath;
|
||||||
|
console.log(`使用 Electron 路径: ${electronPath}`);
|
||||||
|
|
||||||
|
// 添加参数使守护进程在后台运行,不显示 UI
|
||||||
|
daemonProcess = spawn(
|
||||||
|
process.argv[0],
|
||||||
|
isUiDev
|
||||||
|
? [process.argv[1], `--mode=launchDaemon`]
|
||||||
|
: [`--mode=launchDaemon`],
|
||||||
|
{
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
detached: false,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// daemonProcess = spawn(electronPath, [
|
||||||
|
// '--no-sandbox',
|
||||||
|
// '--disable-dev-shm-usage',
|
||||||
|
// path.join(__dirname, 'daemon.js')
|
||||||
|
// ], {
|
||||||
|
// stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
// detached: false,
|
||||||
|
// env: {
|
||||||
|
// ...process.env,
|
||||||
|
// ELECTRON_EXEC_PATH: electronPath // 传递给守护进程,用于启动 worker
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
daemonProcess.stdout.on('data', (data) => {
|
||||||
|
console.log(`守护进程输出: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
daemonProcess.stderr.on('data', (data) => {
|
||||||
|
console.error(`守护进程错误: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
daemonProcess.on('exit', (code) => {
|
||||||
|
console.log(`守护进程退出,代码: ${code}`);
|
||||||
|
// 如果守护进程意外退出,尝试重启
|
||||||
|
if (code !== 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('尝试重启守护进程...');
|
||||||
|
startDaemon();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 等待守护进程启动后连接
|
||||||
|
setTimeout(() => {
|
||||||
|
connectToDaemon();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用准备就绪
|
||||||
|
return app.whenReady().then(() => {
|
||||||
|
startDaemon();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -9,18 +9,7 @@ const route = useRoute()
|
|||||||
|
|
||||||
const currentStatus = ref('')
|
const currentStatus = ref('')
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const promise = electron.ipcRenderer.invoke('prepare-run-geek-auto-start-chat-with-boss')
|
const promise = Promise.resolve()
|
||||||
const handleLocatingPuppeteerExecutable = () => {
|
|
||||||
currentStatus.value = 'locating-puppeteer-executable'
|
|
||||||
}
|
|
||||||
electron.ipcRenderer.once('locating-puppeteer-executable', handleLocatingPuppeteerExecutable)
|
|
||||||
onUnmounted(() => {
|
|
||||||
electron.ipcRenderer.removeListener(
|
|
||||||
'locating-puppeteer-executable',
|
|
||||||
handleLocatingPuppeteerExecutable
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
promise
|
promise
|
||||||
.then(() => {
|
.then(() => {
|
||||||
switch (route.query.flow) {
|
switch (route.query.flow) {
|
||||||
|
|||||||
Reference in New Issue
Block a user