diff --git a/packages/geek-auto-start-chat-with-boss/index.mjs b/packages/geek-auto-start-chat-with-boss/index.mjs index 9224f18..d92d5ae 100644 --- a/packages/geek-auto-start-chat-with-boss/index.mjs +++ b/packages/geek-auto-start-chat-with-boss/index.mjs @@ -1494,7 +1494,10 @@ export async function closeBrowserWindow () { browser?.close() const browserProcess = browser?.process() if (browserProcess) { - process.kill(browserProcess.pid) + try { + process.kill(browserProcess.pid) + } + catch {} } browser = null page = null diff --git a/packages/pm/daemon.js b/packages/pm/daemon.js index 3227382..0f8aab8 100644 --- a/packages/pm/daemon.js +++ b/packages/pm/daemon.js @@ -33,6 +33,7 @@ const PORT = 12345; const workers = new Map(); // workerId -> { process, status, restartCount, socket } const guiClients = new Set(); // GUI客户端连接集合 const stoppedWorkers = new Set(); // 被用户主动停止的workerId集合,用于防止竞态条件 +const pidToProcessInfoMap = new Map() // 创建TCP服务器 const server = net.createServer((socket) => { @@ -137,7 +138,6 @@ function handleMessage(socket, message) { const shouldExit = stoppedWorkers.has(workerId) || !workers.has(workerId); sendResponse(socket, _callbackUuid, { - type: 'check-should-exit-response', workerId: workerId, shouldExit: shouldExit }); @@ -228,6 +228,11 @@ function startWorker({ workerId, command, args, env }, restartCount = 0) { console.log(`工具进程 ${workerId} 已在运行`); return; } + const noRestartExitCodeSet = new Set([0]); + (env.GEEKGEEKRUND_NO_RESTART_EXIT_CODE ?? '') + .split(',') + .map(n => parseInt(n)) + .forEach(n => noRestartExitCodeSet.add(n)) console.log(`启动工具进程: ${workerId}${restartCount > 0 ? ` (重启第${restartCount}次)` : ''}`); // 添加参数使工具进程在后台运行,不显示 UI @@ -253,15 +258,15 @@ function startWorker({ workerId, command, args, env }, restartCount = 0) { workerProcess.on('exit', (code, signal) => { console.log(`工具进程 ${workerId} 退出,代码: ${code}, 信号: ${signal}`); - - const workerInfo = workers.get(workerId); + const workerInfo = pidToProcessInfoMap.get(workerProcess.pid) if (workerInfo) { + pidToProcessInfoMap.delete(workerProcess.pid) // 关闭工具进程的TCP连接 if (workerInfo.socket) { workerInfo.socket.destroy(); } - const shouldRestart = code !== 0 // && code !== null; + const shouldRestart = !noRestartExitCodeSet.has(code) // && code !== null; // 使用当前的 restartCount 加1,而不是从 workerInfo 中取(因为可能已经被删除) const restartCount = (workerInfo.restartCount || 0) + 1; @@ -290,6 +295,14 @@ function startWorker({ workerId, command, args, env }, restartCount = 0) { console.log(`工具进程 ${workerId} 在重启前被标记为停止,取消重启`); // 从停止列表中移除,因为已经处理完毕 stoppedWorkers.delete(workerId); + broadcastToGUI({ + type: 'worker-exited', + workerId: workerId, + code: code, + signal: signal, + restarting: false, + restartCount: restartCount + }); } }, 2000); } else if (stoppedWorkers.has(workerId)) { @@ -310,7 +323,7 @@ function startWorker({ workerId, command, args, env }, restartCount = 0) { console.log(err) }) - workers.set(workerId, { + const workerInfo = { process: workerProcess, status: 'running', startTime: Date.now(), @@ -320,8 +333,9 @@ function startWorker({ workerId, command, args, env }, restartCount = 0) { command, env, workerId, - }); - + } + workers.set(workerId, workerInfo); + pidToProcessInfoMap.set(workerProcess.pid, workerInfo); // 定期发送状态更新 broadcastStatus(); } @@ -333,7 +347,6 @@ function stopWorker(workerId) { // 无论workerInfo是否存在,都添加到停止列表,防止竞态条件 stoppedWorkers.add(workerId); console.log(`停止工具进程: ${workerId} (已添加到停止列表)`); - if (!workerInfo) { console.log(`工具进程 ${workerId} 不存在,但已标记为停止(防止重启)`); // 通知GUI客户端 diff --git a/packages/pm/worker.js b/packages/pm/worker.js index 5d40ea8..6bef35d 100644 --- a/packages/pm/worker.js +++ b/packages/pm/worker.js @@ -137,7 +137,7 @@ function sendToDaemon(message) { function handleDaemonMessage(message) { if (message.type === 'worker-registered') { console.log(`[工具进程 ${workerId}] 连接已注册到守护进程`); - } else if (message.type === 'check-should-exit-response') { + } else if (message.type === 'check-should-exit') { // 处理是否应该退出的查询响应 if (message.shouldExit) { console.log(`[工具进程 ${workerId}] 守护进程指示应该退出,正在退出...`); diff --git a/packages/ui/src/common/enums/auto-start-chat.ts b/packages/ui/src/common/enums/auto-start-chat.ts index ff0cc95..e89bf97 100644 --- a/packages/ui/src/common/enums/auto-start-chat.ts +++ b/packages/ui/src/common/enums/auto-start-chat.ts @@ -5,8 +5,7 @@ export enum AUTO_CHAT_ERROR_EXIT_CODE { ERR_INTERNET_DISCONNECTED = 83, ACCESS_IS_DENIED = 84, PUPPETEER_IS_NOT_EXECUTABLE = 85, - AUTO_START_CHAT_DAEMON_PROCESS_SUICIDE = 86, - AUTO_START_CHAT_MAIN_PROCESS_SUICIDE = 87, + LLM_UNAVAILABLE = 86, } export enum RECHAT_CONTENT_SOURCE { diff --git a/packages/ui/src/main/flow/GEEK_AUTO_START_CHAT_WITH_BOSS_DAEMON/index.ts b/packages/ui/src/main/flow/GEEK_AUTO_START_CHAT_WITH_BOSS_DAEMON/index.ts deleted file mode 100644 index 0427f07..0000000 --- a/packages/ui/src/main/flow/GEEK_AUTO_START_CHAT_WITH_BOSS_DAEMON/index.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { sleep } from '@geekgeekrun/utils/sleep.mjs' -import childProcess from 'node:child_process' -import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../common/enums/auto-start-chat' -import { app, dialog } from 'electron' -import fs, { WriteStream } from 'node:fs' -import { pipeWriteRegardlessError } from '../utils/pipe' -import * as JSONStream from 'JSONStream' -import { initPowerSaveBlocker } from './power-saver-blocker' -import gtag from '../../utils/gtag' -import { initDb } from '@geekgeekrun/sqlite-plugin' -import { getPublicDbFilePath } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs' -import { AutoStartChatRunRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/AutoStartChatRunRecord' -import minimist from 'minimist' -import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited' -const isUiDev = process.env.NODE_ENV === 'development' -const rerunInterval = (() => { - let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL) - if (isNaN(v)) { - v = 3000 - } - - return v -})() -function runWithDaemon({ runRecordId, runMode, parentProcessPipe }) { - const subProcessOfCore = childProcess.spawn( - process.argv[0], - isUiDev - ? [process.argv[1], `--run-record-id=${runRecordId}`, `--mode=${runMode}`] - : [`--run-record-id=${runRecordId}`, `--mode=${runMode}`], - { - stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc'] - } - ) - - subProcessOfCore!.stdio[3]!.pipe(JSONStream.parse()).on('data', async (raw) => { - const data = raw - switch (data.type) { - case 'GEEK_AUTO_START_CHAT_WITH_BOSS_STARTED': { - pipeWriteRegardlessError( - parentProcessPipe as WriteStream, - JSON.stringify({ - type: data.type - }) - ) - break - } - default: { - return - } - } - }) - - subProcessOfCore.once('exit', async (exitCode: number) => { - if ( - [...Object.values(AUTO_CHAT_ERROR_EXIT_CODE)] - .filter((it) => typeof it === 'number') - .includes(exitCode) - ) { - console.log( - `[Run core daemon] Child process exit with reason ${AUTO_CHAT_ERROR_EXIT_CODE[exitCode]}.` - ) - process.exit(exitCode) - return - } - console.log( - `[Run core daemon] Child process exit with code ${exitCode}, an internal error may not be caught, and will be restarted in ${rerunInterval}ms.` - ) - await sleep(rerunInterval) - runWithDaemon({ runRecordId, runMode, parentProcessPipe }) - }) -} - -export async function runAutoChatWithDaemon() { - const commandlineArgs = minimist(isUiDev ? process.argv.slice(2) : process.argv.slice(1)) - if (!['geekAutoStartWithBossMain'].includes(commandlineArgs['mode-to-daemon'])) { - await new Promise((resolve) => { - app.once('ready', () => resolve(undefined)) - }) - - dialog.showMessageBoxSync({ - type: 'error', - message: `守护进程不支持 ${commandlineArgs['mode-to-daemon'] ?? '(默认)'} 模式` - }) - app.exit() - return - } - - app.dock?.hide() - process.on('disconnect', () => { - app.exit() - }) - - let pipe: null | fs.WriteStream = null - try { - pipe = fs.createWriteStream(null, { fd: 3 }) - } catch { - console.error('pipe is not available') - app.exit(1) - } - - const disposePowerSaveBlocker = initPowerSaveBlocker() - app.once('quit', disposePowerSaveBlocker) - - process.on('SIGINT', () => { - process.exit() - }) - - const ds = await initDb(getPublicDbFilePath()) - const autoStartChatRunRecord = new AutoStartChatRunRecord() - autoStartChatRunRecord.date = new Date() - const autoStartChatRunRecordRepository = ds.getRepository(AutoStartChatRunRecord) - const result = await autoStartChatRunRecordRepository.save(autoStartChatRunRecord) - runWithDaemon({ - runRecordId: result.id, - runMode: commandlineArgs['mode-to-daemon'], - parentProcessPipe: pipe - }) - - pipeWriteRegardlessError( - pipe, - JSON.stringify({ - type: 'AUTO_START_CHAT_DAEMON_PROCESS_STARTUP' - }) - ) - - gtag('daemon_ready', { mode: commandlineArgs['mode-to-daemon'] ?? '' }) -} - -attachListenerForKillSelfOnParentExited() diff --git a/packages/ui/src/main/flow/GEEK_AUTO_START_CHAT_WITH_BOSS_DAEMON/power-saver-blocker.ts b/packages/ui/src/main/flow/GEEK_AUTO_START_CHAT_WITH_BOSS_DAEMON/power-saver-blocker.ts deleted file mode 100644 index f8940b5..0000000 --- a/packages/ui/src/main/flow/GEEK_AUTO_START_CHAT_WITH_BOSS_DAEMON/power-saver-blocker.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { powerSaveBlocker } from 'electron' - -export const initPowerSaveBlocker = ( - type: 'prevent-app-suspension' | 'prevent-display-sleep' = 'prevent-app-suspension' -) => { - const id = powerSaveBlocker.start(type) - return function disposePowerSaveBlocker() { - return powerSaveBlocker.stop(id) - } -} 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 462c00f..6c23ae7 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 @@ -16,7 +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' +import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon' const { default: SqlitePlugin } = SqlitePluginModule const rerunInterval = (() => { @@ -36,10 +36,18 @@ const initPlugins = (hooks) => { new GtagPlugin().apply(hooks) } -let isParentProcessDisconnect = false -process.once('disconnect', () => { - isParentProcessDisconnect = true -}) +async function checkShouldExit () { + const shouldExitResponse = await sendToDaemon( + { + type: 'check-should-exit', + workerId: process.env.GEEKGEEKRUND_WORKER_ID, + }, + { + needCallback: true + } + ) + return shouldExitResponse?.shouldExit +} const runAutoChat = async () => { const { initPuppeteer, mainLoop, closeBrowserWindow, autoStartChatEventBus } = await import( @@ -118,7 +126,7 @@ const runAutoChat = async () => { // ) }) - while (![isParentProcessDisconnect].includes(true)) { + while (true) { try { await mainLoop(hooks) } catch (err) { @@ -140,9 +148,18 @@ const runAutoChat = async () => { process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ACCESS_IS_DENIED) break } + if (err.message.includes(`Could not find Chrome`) || err.message.includes(`no executable was found`)) { + process.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE) + break + } } closeBrowserWindow?.() console.error(err) + const shouldExit = await checkShouldExit() + if (shouldExit) { + app.exit() + return + } console.log( `[Run core main] An internal error is caught, and browser will be restarted in ${rerunInterval}ms.` ) 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 index 2b404ff..c62e076 100644 --- 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 @@ -1,11 +1,13 @@ import { randomUUID } from "node:crypto"; import { DAEMON_PORT } from "./daemon-config"; +import { EventEmitter } from "node:events"; const net = require('net'); const split2 = require('split2'); const { app } = require('electron'); let daemonClient = null; +export const daemonEE = new EventEmitter() const waitForCallbackTaskMap = new Map() // 连接到守护进程 @@ -13,6 +15,7 @@ export function connectToDaemon() { daemonClient = new net.Socket(); daemonClient.connect(DAEMON_PORT, 'localhost', () => { console.log('已连接到守护进程'); + daemonEE.emit('connect') // 通知渲染进程连接成功 // if (mainWindow) { // mainWindow.webContents.send('daemon-connected'); @@ -27,6 +30,8 @@ export function connectToDaemon() { } try { const message = JSON.parse(trimmedLine); + daemonEE.emit('message', message) + // FIXME: console.log('收到守护进程消息:', message); if ( message._callbackUuid @@ -59,6 +64,7 @@ export function connectToDaemon() { daemonClient.on('error', (err) => { console.error('守护进程连接错误:', err); + daemonEE.emit('error', err) // 尝试重连 setTimeout(() => { if (daemonClient.destroyed) { @@ -69,6 +75,7 @@ export function connectToDaemon() { daemonClient.on('close', () => { console.log('守护进程连接已关闭'); + daemonEE.emit('close') // 尝试重连 setTimeout(() => { connectToDaemon(); 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 ea7c8fb..5a3878b 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 @@ -15,7 +15,6 @@ import { ChildProcess } from 'child_process' import * as JSONStream from 'JSONStream' import { checkCookieListFormat } from '../../../../common/utils/cookie' import { getAnyAvailablePuppeteerExecutable } from '../../../flow/CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable/index' -import { sleep } from '@geekgeekrun/utils/sleep.mjs' import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../../common/enums/auto-start-chat' import { mainWindow } from '../../../window/mainWindow' import { @@ -52,7 +51,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' +import { daemonEE, sendToDaemon } from '../connect-to-daemon' export default function initIpc() { ipcMain.handle('fetch-config-file-content', async () => { @@ -199,36 +198,20 @@ export default function initIpc() { return await writeStorageFile(payload.fileName, JSON.parse(payload.data)) }) - // const currentExecutablePath = app.getPath('exe') - // console.log(currentExecutablePath) - - let subProcessOfPuppeteer: ChildProcess | null = null ipcMain.handle('run-geek-auto-start-chat-with-boss', async (ev) => { - if (subProcessOfPuppeteer) { - return - } const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable() if (!puppeteerExecutable) { return Promise.reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES') } const subProcessEnv = { ...process.env, - PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath + PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath, + GEEKGEEKRUND_NO_RESTART_EXIT_CODE: [ + AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE, + AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID, + AUTO_CHAT_ERROR_EXIT_CODE.LLM_UNAVAILABLE + ].join(',') } - // ipcMain.emit( - // 'start-worker', - // ev, - // { - // workerId: 'geekAutoStartWithBossMain', - // command: process.argv[0], - // args: [ - // process.argv[1], - // `--mode=geekAutoStartWithBossMain` - // ], - // env: subProcessEnv - // } - // ) - await sendToDaemon( { type: 'start-worker', @@ -244,108 +227,83 @@ export default function initIpc() { needCallback: true } ) - - // 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: + daemonEE.on('message', function handler (message) { + if (message.workerId !== 'geekAutoStartWithBossMain') { + return + } + if (message.type === 'worker-exited') { + switch(message.code) { + case AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE: { + mainWindow?.webContents.send('need-to-check-runtime-dependencies') + daemonEE.off('message', handler) + break + } + case AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID: { + mainWindow?.webContents.send('check-boss-zhipin-cookie-file') + daemonEE.off('message', handler) + break + } + case AUTO_CHAT_ERROR_EXIT_CODE.NORMAL: { + daemonEE.off('message', handler) + break + } + } + } + }) }) ipcMain.handle('run-read-no-reply-auto-reminder', async () => { - if (subProcessOfPuppeteer) { - return - } const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable() if (!puppeteerExecutable) { return Promise.reject('NEED_TO_CHECK_RUNTIME_DEPENDENCIES') } const subProcessEnv = { ...process.env, - PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath + PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath, + GEEKGEEKRUND_NO_RESTART_EXIT_CODE: [ + AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE, + AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID, + AUTO_CHAT_ERROR_EXIT_CODE.LLM_UNAVAILABLE + ].join(',') } - subProcessOfPuppeteer = childProcess.spawn( - process.argv[0], - [process.argv[1], `--mode=readNoReplyAutoReminder`], + await sendToDaemon( { - env: subProcessEnv, - stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc'] + type: 'start-worker', + workerId: 'readNoReplyAutoReminder', + command: process.argv[0], + args: [ + process.argv[1], + `--mode=readNoReplyAutoReminder` + ], + 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 'LOGIN_STATUS_INVALID': { - await sleep(500) + daemonEE.on('message', function handler (message) { + if (message.workerId !== 'readNoReplyAutoReminder') { + return + } + if (message.type === 'worker-exited') { + switch(message.code) { + case AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE: { + mainWindow?.webContents.send('need-to-check-runtime-dependencies') + daemonEE.off('message', handler) + break + } + case AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID: { mainWindow?.webContents.send('check-boss-zhipin-cookie-file') - return + daemonEE.off('message', handler) + break } - case 'ERR_INTERNET_DISCONNECTED': { - mainWindow?.webContents.send('toast-message', { - type: 'error', - message: '联网失败,请检查网络连接' - }) - return - } - default: { - return + case AUTO_CHAT_ERROR_EXIT_CODE.NORMAL: { + daemonEE.off('message', handler) + break } } - }) - - 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') - } - }) - - resolve(true) + } }) - // TODO: }) ipcMain.handle('check-dependencies', async () => { @@ -408,14 +366,56 @@ export default function initIpc() { ipcMain.handle('stop-geek-auto-start-chat-with-boss', async () => { mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopping') - subProcessOfPuppeteer?.kill() - setTimeout(() => { - try { - subProcessOfPuppeteer?.kill('SIGKILL') - } catch { - // + const p = new Promise(resolve => { + daemonEE.on('message', function handler (message) { + if (message.workerId !== 'geekAutoStartWithBossMain') { + return + } + if (message.type === 'worker-exited') { + daemonEE.off('message', handler) + resolve(undefined) + } + }) + }) + await sendToDaemon( + { + type: 'stop-worker', + workerId: 'geekAutoStartWithBossMain', + }, + { + needCallback: true } - }, 1000) + ) + + await p + mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopped') + }) + + ipcMain.handle('stop-read-no-reply-auto-reminder', async () => { + mainWindow?.webContents.send('read-no-reply-auto-reminder-stopping') + const p = new Promise(resolve => { + daemonEE.on('message', function handler (message) { + if (message.workerId !== 'readNoReplyAutoReminder') { + return + } + if (message.type === 'worker-exited') { + daemonEE.off('message', handler) + resolve(undefined) + } + }) + }) + await sendToDaemon( + { + type: 'stop-worker', + workerId: 'readNoReplyAutoReminder', + }, + { + needCallback: true + } + ) + + await p + mainWindow?.webContents.send('read-no-reply-auto-reminder-stopped') }) let subProcessOfBossZhipinLoginPageWithPreloadExtension: ChildProcess | null = null diff --git a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts index d6696b9..fe6245d 100644 --- a/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts +++ b/packages/ui/src/main/flow/READ_NO_REPLY_AUTO_REMINDER/index.ts @@ -4,7 +4,6 @@ import { Browser, Page } from 'puppeteer' import { sendGptContent, sendLookForwardReplyEmotion } from './boss-operation' import { sleep, sleepWithRandomDelay } from '@geekgeekrun/utils/sleep.mjs' import { waitForPage } from '@geekgeekrun/utils/puppeteer/wait.mjs' -import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited' import { app, dialog } from 'electron' import { initDb } from '@geekgeekrun/sqlite-plugin' import { @@ -18,15 +17,14 @@ import { saveJobHireStatusRecord } from '@geekgeekrun/sqlite-plugin/dist/handlers' import { writeStorageFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs' -import * as fs from 'fs' -import { pipeWriteRegardlessError } from '../utils/pipe' import { BossInfo } from '@geekgeekrun/sqlite-plugin/dist/entity/BossInfo' import { messageForSaveFilter } from '../../../common/utils/chat-list' -import { RECHAT_CONTENT_SOURCE, RECHAT_LLM_FALLBACK } from '../../../common/enums/auto-start-chat' +import { AUTO_CHAT_ERROR_EXIT_CODE, RECHAT_CONTENT_SOURCE, RECHAT_LLM_FALLBACK } from '../../../common/enums/auto-start-chat' import gtag from '../../utils/gtag' import { JobHireStatus } from '@geekgeekrun/sqlite-plugin/dist/enums'; import dayjs from 'dayjs' import cheerio from 'cheerio' +import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon' const throttleIntervalMinutes = readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10 @@ -444,10 +442,6 @@ const mainLoop = async () => { } } -let isParentProcessDisconnect = false -process.once('disconnect', () => { - isParentProcessDisconnect = true -}) const rerunInterval = (() => { let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL) if (isNaN(v)) { @@ -457,19 +451,22 @@ const rerunInterval = (() => { return v })() -let pipe -try { - pipe = fs.createWriteStream(null, { fd: 3 }) -} catch { - console.warn('pipe is not available') +async function checkShouldExit () { + const shouldExitResponse = await sendToDaemon( + { + type: 'check-should-exit', + workerId: process.env.GEEKGEEKRUND_WORKER_ID, + }, + { + needCallback: true + } + ) + return shouldExitResponse?.shouldExit } export async function runEntry() { - process.on('disconnect', () => { - app.exit() - }) + connectToDaemon() app.dock?.hide() - - while (!isParentProcessDisconnect) { + while (true) { try { await mainLoop() } catch (err) { @@ -479,31 +476,45 @@ export async function runEntry() { } catch { // } - // handle error - if ( - err instanceof Error && - ['LOGIN_STATUS_INVALID', 'ACCESS_IS_DENIED', 'ERR_INTERNET_DISCONNECTED'].includes( - err.message - ) - ) { - pipeWriteRegardlessError( - pipe, - JSON.stringify({ - type: err.message - }) + '\r\n' - ) - process.exit(1) + const shouldExit = await checkShouldExit() + if (shouldExit) { + app.exit() + return } - if (err instanceof Error && err.message === 'CANNOT_FIND_A_USABLE_MODEL') { - gtag('cannot_find_a_usable_model') - await dialog.showMessageBox({ - type: 'error', - message: - '未找到可以使用的模型,请确定您所配置的模型均可使用。重启本程序或许可以解决这个问题', - buttons: ['退出'] - }) - process.exit(0) - break; + // handle error + if (err instanceof Error) { + if (err.message.includes('LOGIN_STATUS_INVALID')) { + await dialog.showMessageBox({ + type: `error`, + message: `登录状态无效`, + detail: `请重新登录Boss直聘` + }) + process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID) + break + } + if (err.message.includes('ERR_INTERNET_DISCONNECTED')) { + process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ERR_INTERNET_DISCONNECTED) + break + } + if (err.message.includes('ACCESS_IS_DENIED')) { + process.exit(AUTO_CHAT_ERROR_EXIT_CODE.ACCESS_IS_DENIED) + break + } + if (err.message.includes(`Could not find Chrome`) || err.message.includes(`no executable was found`)) { + process.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE) + break + } + if (err.message === 'CANNOT_FIND_A_USABLE_MODEL') { + gtag('cannot_find_a_usable_model') + await dialog.showMessageBox({ + type: 'error', + message: + '未找到可以使用的模型,请确定您所配置的模型均可使用。重启本程序或许可以解决这个问题', + buttons: ['退出'] + }) + process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LLM_UNAVAILABLE) + break; + } } } finally { pageMapByName['boss'] = null @@ -514,8 +525,6 @@ export async function runEntry() { process.exit(0) } -attachListenerForKillSelfOnParentExited() - process.once('uncaughtException', (error) => { console.error('uncaughtException', error) process.exit(1) @@ -525,6 +534,10 @@ process.once('unhandledRejection', (error) => { process.exit(1) }) +process.once('disconnect', () => { + process.exit(0) +}) + async function storeStorage(page) { const [cookies, localStorage] = await Promise.all([ page.cookies(), diff --git a/packages/ui/src/main/index.ts b/packages/ui/src/main/index.ts index 73e3eb0..834464a 100644 --- a/packages/ui/src/main/index.ts +++ b/packages/ui/src/main/index.ts @@ -14,13 +14,6 @@ const runMode = commandlineArgs['mode']; waitForProcessHandShakeAndRunAutoChat() break } - case 'geekAutoStartWithBossDaemon': { - const { runAutoChatWithDaemon } = await import( - './flow/GEEK_AUTO_START_CHAT_WITH_BOSS_DAEMON/index' - ) - runAutoChatWithDaemon() - break - } case 'checkAndDownloadDependenciesForInit': { const { checkAndDownloadDependenciesForInit } = await import( './flow/CHECK_AND_DOWNLOAD_DEPENDENCIES/index' diff --git a/packages/ui/src/renderer/src/page/GeekAutoStartChatWithBoss/RunningStatusForReadNoReplyReminder.vue b/packages/ui/src/renderer/src/page/GeekAutoStartChatWithBoss/RunningStatusForReadNoReplyReminder.vue index 7bc40b4..3871063 100644 --- a/packages/ui/src/renderer/src/page/GeekAutoStartChatWithBoss/RunningStatusForReadNoReplyReminder.vue +++ b/packages/ui/src/renderer/src/page/GeekAutoStartChatWithBoss/RunningStatusForReadNoReplyReminder.vue @@ -1,5 +1,5 @@