From 81e40230d19de2587839dc37658b067c41fa850f Mon Sep 17 00:00:00 2001 From: geekgeekrun Date: Sun, 4 Jan 2026 10:32:43 +0800 Subject: [PATCH] WIP: migrate to mulit process manager --- packages/pm/daemon.js | 75 ++++--- .../index.ts | 66 +++--- .../flow/OPEN_SETTING_WINDOW/attach-daemon.ts | 194 ------------------ .../OPEN_SETTING_WINDOW/connect-to-daemon.ts | 145 +++++++++++++ .../flow/OPEN_SETTING_WINDOW/daemon-config.ts | 1 + .../main/flow/OPEN_SETTING_WINDOW/index.ts | 3 +- .../flow/OPEN_SETTING_WINDOW/ipc/index.ts | 119 ++++++----- .../flow/OPEN_SETTING_WINDOW/launch-daemon.ts | 94 +++++++++ .../page/GeekAutoStartChatWithBoss/index.vue | 13 +- 9 files changed, 391 insertions(+), 319 deletions(-) delete mode 100644 packages/ui/src/main/flow/OPEN_SETTING_WINDOW/attach-daemon.ts create mode 100644 packages/ui/src/main/flow/OPEN_SETTING_WINDOW/connect-to-daemon.ts create mode 100644 packages/ui/src/main/flow/OPEN_SETTING_WINDOW/daemon-config.ts create mode 100644 packages/ui/src/main/flow/OPEN_SETTING_WINDOW/launch-daemon.ts diff --git a/packages/pm/daemon.js b/packages/pm/daemon.js index 3527fa5..f489a3c 100644 --- a/packages/pm/daemon.js +++ b/packages/pm/daemon.js @@ -54,13 +54,13 @@ const server = net.createServer((socket) => { } catch (parseError) { console.error('解析JSON消息失败:', parseError.message); 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) => { console.error('split2 流处理错误:', err); - sendResponse(socket, { error: '流处理失败' }); + sendResponse(socket, message, { error: '流处理失败' }); }); socket.on('error', (err) => { @@ -92,7 +92,7 @@ function handleMessage(socket, message) { // 检查是否在停止列表中(防止竞态条件) if (stoppedWorkers.has(workerId)) { console.log(`工具进程 ${workerId} 尝试注册,但已被标记为停止,拒绝注册`); - sendResponse(socket, { + sendResponse(socket, message, { error: `工具进程 ${workerId} 已被停止`, shouldExit: true // 通知子进程应该退出 }); @@ -103,7 +103,7 @@ function handleMessage(socket, message) { if (workerInfo) { workerInfo.socket = socket; console.log(`工具进程 ${workerId} 已注册TCP连接`); - sendResponse(socket, { + sendResponse(socket, message, { success: true, type: 'worker-registered', message: `工具进程 ${workerId} 连接已注册` @@ -120,7 +120,7 @@ function handleMessage(socket, message) { if (!stoppedWorkers.has(workerId)) { console.log(`工具进程 ${workerId} 尝试注册,但workerInfo不存在`); } - sendResponse(socket, { + sendResponse(socket, message, { error: `工具进程 ${workerId} 未找到`, shouldExit: true }); @@ -133,7 +133,7 @@ function handleMessage(socket, message) { const workerId = message.workerId; const shouldExit = stoppedWorkers.has(workerId) || !workers.has(workerId); - sendResponse(socket, { + sendResponse(socket, message, { type: 'check-should-exit-response', workerId: workerId, shouldExit: shouldExit @@ -164,7 +164,7 @@ function handleMessage(socket, message) { workerInfo.lastHeartbeat = Date.now(); } } else { - sendResponse(socket, { error: '未注册的工具进程连接' }); + sendResponse(socket, message, { error: '未注册的工具进程连接' }); } return; } @@ -177,8 +177,19 @@ function handleMessage(socket, message) { switch (message.type) { case 'start-worker': - startWorker(message.workerId); - sendResponse(socket, { + const { + workerId, + command, + args, + env + } = message + startWorker({ + workerId, + command, + args, + env + }); + sendResponse(socket, message, { success: true, message: `工具进程 ${message.workerId} 已启动`, workerId: message.workerId @@ -187,7 +198,7 @@ function handleMessage(socket, message) { case 'stop-worker': stopWorker(message.workerId); - sendResponse(socket, { + sendResponse(socket, message, { success: true, message: `工具进程 ${message.workerId} 已停止`, workerId: message.workerId @@ -196,7 +207,7 @@ function handleMessage(socket, message) { case 'get-status': const status = getWorkersStatus(); - sendResponse(socket, { + sendResponse(socket, message, { success: true, type: 'status', workers: status @@ -204,36 +215,26 @@ function handleMessage(socket, message) { break; 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)) { console.log(`工具进程 ${workerId} 已在运行`); return; } console.log(`启动工具进程: ${workerId}${restartCount > 0 ? ` (重启第${restartCount}次)` : ''}`); - - // 使用 Electron 可执行程序路径,从环境变量获取,如果没有则回退到 node - const electronPath = process.env.ELECTRON_EXEC_PATH || 'node'; - console.log(`使用执行程序路径: ${electronPath}`); - // 添加参数使工具进程在后台运行,不显示 UI - const workerProcess = spawn(electronPath, [ - '--no-sandbox', - '--disable-gpu', - '--disable-dev-shm-usage', - path.join(__dirname, 'worker.js'), - `--worker-id=${workerId}`, - `--restart-count=${restartCount.toString()}` - ], { + const workerProcess = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], 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(() => { // 再次检查:确保worker不在停止列表中,且当前没有运行 if (!workers.has(workerId) && !stoppedWorkers.has(workerId)) { - startWorker(workerId, restartCount); + startWorker({ workerId, command, args, env }, restartCount); } else if (stoppedWorkers.has(workerId)) { console.log(`工具进程 ${workerId} 在重启前被标记为停止,取消重启`); // 从停止列表中移除,因为已经处理完毕 @@ -302,13 +303,20 @@ function startWorker(workerId, restartCount = 0) { } }); + workerProcess.on('error', (err) => { + console.log(err) + }) + workers.set(workerId, { process: workerProcess, status: 'running', startTime: Date.now(), restartCount, // 使用传入的重启次数 socket: null, // 工具进程的TCP连接,稍后由工具进程注册 - lastHeartbeat: null + lastHeartbeat: null, + command, + env, + workerId, }); // 定期发送状态更新 @@ -385,7 +393,7 @@ function broadcastToGUI(message) { guiClients.forEach(socket => { if (!socket.destroyed) { try { - sendResponse(socket, message); + sendResponse(socket, null, message); } catch (e) { console.error('广播消息失败:', e); guiClients.delete(socket); @@ -395,9 +403,12 @@ function broadcastToGUI(message) { } // 发送响应 -function sendResponse(socket, response) { +function sendResponse(socket, request, response) { try { - socket.write(JSON.stringify(response) + '\n'); + socket.write(JSON.stringify({ + ...response, + _callbackUuid: request?._callbackUuid + }) + '\n'); } catch (e) { console.error('发送响应失败:', e); } diff --git a/packages/ui/src/main/flow/GEEK_AUTO_START_CHAT_WITH_BOSS_MAIN/index.ts b/packages/ui/src/main/flow/GEEK_AUTO_START_CHAT_WITH_BOSS_MAIN/index.ts index 1387f1f..462c00f 100644 --- a/packages/ui/src/main/flow/GEEK_AUTO_START_CHAT_WITH_BOSS_MAIN/index.ts +++ b/packages/ui/src/main/flow/GEEK_AUTO_START_CHAT_WITH_BOSS_MAIN/index.ts @@ -7,7 +7,7 @@ import { } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs' 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 { sleep } from '@geekgeekrun/utils/sleep.mjs' 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 gtag from '../../utils/gtag' import GtagPlugin from '../../utils/gtag/GtagPlugin' +import { connectToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon' const { default: SqlitePlugin } = SqlitePluginModule const rerunInterval = (() => { @@ -55,20 +56,20 @@ const runAutoChat = async () => { } catch { console.warn('pipe is not available') } - pipeWriteRegardlessError( - pipe, - JSON.stringify({ - type: 'INITIALIZE_PUPPETEER' - }) + '\r\n' - ) + // pipeWriteRegardlessError( + // pipe, + // JSON.stringify({ + // type: 'INITIALIZE_PUPPETEER' + // }) + '\r\n' + // ) try { await initPuppeteer() - pipeWriteRegardlessError( - pipe, - JSON.stringify({ - type: 'PUPPETEER_INITIALIZE_SUCCESSFULLY' - }) + '\r\n' - ) + // pipeWriteRegardlessError( + // pipe, + // JSON.stringify({ + // type: 'PUPPETEER_INITIALIZE_SUCCESSFULLY' + // }) + '\r\n' + // ) } catch (err) { console.error(err) app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE) @@ -101,20 +102,20 @@ const runAutoChat = async () => { initPlugins(hooks) gtag('run_auto_chat_with_boss_main_ready') - pipeWriteRegardlessError( - pipe, - JSON.stringify({ - type: 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED' //geek-auto-start-chat-with-boss-started - }) + '\r\n' - ) + // pipeWriteRegardlessError( + // pipe, + // JSON.stringify({ + // type: 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED' //geek-auto-start-chat-with-boss-started + // }) + '\r\n' + // ) autoStartChatEventBus.once('LOGIN_STATUS_INVALID', () => { - pipeWriteRegardlessError( - pipe, - JSON.stringify({ - type: 'LOGIN_STATUS_INVALID' //geek-auto-start-chat-with-boss-started - }) + '\r\n' - ) + // pipeWriteRegardlessError( + // pipe, + // JSON.stringify({ + // type: 'LOGIN_STATUS_INVALID' //geek-auto-start-chat-with-boss-started + // }) + '\r\n' + // ) }) while (![isParentProcessDisconnect].includes(true)) { @@ -151,13 +152,14 @@ const runAutoChat = async () => { } export const waitForProcessHandShakeAndRunAutoChat = () => { - let pipe: null | fs.WriteStream = null - try { - pipe = fs.createWriteStream(null, { fd: 3 }) - } catch { - console.error('pipe is not available') - app.exit(1) - } + // let pipe: null | fs.WriteStream = null + // try { + // pipe = fs.createWriteStream(null, { fd: 3 }) + // } catch { + // console.error('pipe is not available') + // app.exit(1) + // } + connectToDaemon() runAutoChat() } diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/attach-daemon.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/attach-daemon.ts deleted file mode 100644 index e3b6564..0000000 --- a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/attach-daemon.ts +++ /dev/null @@ -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() -} diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/connect-to-daemon.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/connect-to-daemon.ts new file mode 100644 index 0000000..2b404ff --- /dev/null +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/connect-to-daemon.ts @@ -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(); + } +}); \ No newline at end of file diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/daemon-config.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/daemon-config.ts new file mode 100644 index 0000000..254ac66 --- /dev/null +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/daemon-config.ts @@ -0,0 +1 @@ +export const DAEMON_PORT = 12345; diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/index.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/index.ts index 3c80e36..bd4869e 100644 --- a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/index.ts +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/index.ts @@ -5,7 +5,8 @@ import './app-menu' import initIpc from './ipc' import gtag from '../../utils/gtag' 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' export function openSettingWindow() { // TODO: singleton lock; how can we check if there is another process should run as singleton with arguments? diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts index 5b30682..ea7c8fb 100644 --- a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts @@ -52,6 +52,7 @@ import { import { RequestSceneEnum } from '../../../features/llm-request-log' import { checkUpdateForUi } from '../../../features/updater' import gtag from '../../../utils/gtag' +import { sendToDaemon } from '../connect-to-daemon' export default function initIpc() { ipcMain.handle('fetch-config-file-content', async () => { @@ -200,17 +201,9 @@ export default function initIpc() { // const currentExecutablePath = app.getPath('exe') // 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 - ipcMain.handle('run-geek-auto-start-chat-with-boss', async () => { + ipcMain.handle('run-geek-auto-start-chat-with-boss', async (ev) => { if (subProcessOfPuppeteer) { return } @@ -222,49 +215,79 @@ export default function initIpc() { ...process.env, PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath } - subProcessOfPuppeteer = childProcess.spawn( - process.argv[0], - [ - process.argv[1], - `--mode=geekAutoStartWithBossDaemon`, - `--mode-to-daemon=geekAutoStartWithBossMain` - ], + // ipcMain.emit( + // 'start-worker', + // ev, + // { + // workerId: 'geekAutoStartWithBossMain', + // command: process.argv[0], + // args: [ + // process.argv[1], + // `--mode=geekAutoStartWithBossMain` + // ], + // env: subProcessEnv + // } + // ) + + await sendToDaemon( { - env: subProcessEnv, - stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc'] + type: 'start-worker', + 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 = 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: + // subProcessOfPuppeteer = childProcess.spawn( + // process.argv[0], + // [ + // process.argv[1], + // `--mode=geekAutoStartWithBossDaemon`, + // `--mode-to-daemon=geekAutoStartWithBossMain` + // ], + // { + // env: subProcessEnv, + // stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc'] + // } + // ) + // // 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 () => { diff --git a/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/launch-daemon.ts b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/launch-daemon.ts new file mode 100644 index 0000000..c250a0a --- /dev/null +++ b/packages/ui/src/main/flow/OPEN_SETTING_WINDOW/launch-daemon.ts @@ -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(); + }); +} + diff --git a/packages/ui/src/renderer/src/page/GeekAutoStartChatWithBoss/index.vue b/packages/ui/src/renderer/src/page/GeekAutoStartChatWithBoss/index.vue index 4b2a1f2..16e2ef9 100644 --- a/packages/ui/src/renderer/src/page/GeekAutoStartChatWithBoss/index.vue +++ b/packages/ui/src/renderer/src/page/GeekAutoStartChatWithBoss/index.vue @@ -9,18 +9,7 @@ const route = useRoute() const currentStatus = ref('') onMounted(() => { - const promise = electron.ipcRenderer.invoke('prepare-run-geek-auto-start-chat-with-boss') - const handleLocatingPuppeteerExecutable = () => { - currentStatus.value = 'locating-puppeteer-executable' - } - electron.ipcRenderer.once('locating-puppeteer-executable', handleLocatingPuppeteerExecutable) - onUnmounted(() => { - electron.ipcRenderer.removeListener( - 'locating-puppeteer-executable', - handleLocatingPuppeteerExecutable - ) - }) - + const promise = Promise.resolve() promise .then(() => { switch (route.query.flow) {