Merge branch 'feature/multi-process' into feature/ui

This commit is contained in:
geekgeekrun
2026-02-11 23:12:12 +08:00
100 changed files with 6186 additions and 2906 deletions
+4 -4
View File
@@ -1,7 +1,7 @@
{
"version": "0.14.1",
"buildVersion": 31,
"buildTime": 1769428838471,
"buildHash": "a92b9ce0403a8fc816568cf3eb106a89a6afddf0",
"version": "0.14.2",
"buildVersion": 32,
"buildTime": 1770299394728,
"buildHash": "1d5fdb9d54c569cb53035dfef0f6157c899ccb06",
"name": "geekgeekrun-ui"
}
+1
View File
@@ -1 +1,2 @@
export const SINGLE_ITEM_DEFAULT_SERVE_WEIGHT = 1
export const EXPECT_CHROMIUM_BUILD_ID = '139.0.7258.154'
@@ -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 {
@@ -18,3 +17,9 @@ export enum RECHAT_LLM_FALLBACK {
SEND_LOOK_FORWARD_EMOTION = 1,
EXIT_REMINDER_PROGRAM = 2
}
export enum RUNNING_STATUS_ENUM {
RUNNING = 0,
NORMAL_EXITED = 1,
ERROR_EXITED = 2
}
@@ -0,0 +1,18 @@
export const getAutoStartChatSteps = () => [
{
id: 'worker-launch',
describe: '启动子进程'
},
{
id: 'puppeteer-executable-check',
describe: 'Puppeteer 可执行程序检查'
},
{
id: 'basic-cookie-check',
describe: 'Cookie 格式检查'
},
{
id: 'login-status-check',
describe: '登录状态检查'
}
]
+4
View File
@@ -0,0 +1,4 @@
import path from 'node:path'
import os from 'node:os'
export const cacheDir = path.join(os.homedir(), '.geekgeekrun', 'cache')
@@ -0,0 +1,26 @@
import { ipcMain } from 'electron'
import {
createBrowserAssistantWindow,
browserAssistantWindow
} from '../window/browserAssistantWindow'
export async function configWithBrowserAssistant({ windowOption, autoFind } = {}) {
return new Promise((resolve, reject) => {
createBrowserAssistantWindow({ ...windowOption }, { autoFind })
let processDone = false
function handler() {
processDone = true
browserAssistantWindow.close()
}
ipcMain.once('browser-config-saved', handler)
browserAssistantWindow.once('closed', () => {
ipcMain.off('browser-config-saved', handler)
if (processDone) {
resolve(true)
} else {
reject(new Error('USER_CANCELLED_CONFIG_BROWSER'))
}
})
})
}
@@ -0,0 +1,93 @@
import { sendToDaemon } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon'
import minimist from 'minimist'
import { loginWithCookieAssistant } from './login-with-cookie-assistant'
import { checkCookieListFormat } from '../../common/utils/cookie'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { readStorageFile } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
export class CookieInvalidHandlePlugin {
apply(hooks) {
hooks.cookieWillSet.tapPromise('CookieInvalidHandlePlugin', async (cookies) => {
let isValid = checkCookieListFormat(cookies)
while (!isValid) {
try {
// popup login dialog, then update login status
await loginWithCookieAssistant()
await sleep(2000)
const newCookies = readStorageFile('boss-cookies.json')
isValid = checkCookieListFormat(newCookies)
if (isValid) {
cookies.length = 0
for (const cookie of newCookies) {
cookies.push(cookie)
}
}
} catch (e) {
if (e?.message === 'USER_CANCELLED_LOGIN') {
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('LOGIN_STATUS_INVALID')
}
}
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'fulfilled'
},
runRecordId
}
})
})
hooks.userInfoResponse.tapPromise('CookieInvalidHandlePlugin', async (userInfoResponse) => {
if (userInfoResponse.code === 0) {
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'fulfilled'
},
runRecordId
}
})
return
}
try {
// popup login dialog, then update login status
await loginWithCookieAssistant()
} catch (e) {
if (e?.message === 'USER_CANCELLED_LOGIN') {
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('LOGIN_STATUS_INVALID')
}
}
// throw new Error('THROW_FOR_RETRY')
return Promise.reject(new Error('THROW_FOR_RETRY'))
})
}
}
@@ -3,6 +3,11 @@ import os from 'os'
import path from 'path'
import buildInfo from '../../common/build-info.json'
import { ensureStorageFileExist } from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import {
createFirstLaunchNoticeWindow,
firstLaunchNoticeWindow
} from '../window/firstLaunchNoticeWindow'
import { ipcMain } from 'electron'
export const firstLaunchNoticeApproveFlagPath = path.join(
os.homedir(),
@@ -10,8 +15,28 @@ export const firstLaunchNoticeApproveFlagPath = path.join(
'ui-first-launch-notice-flag'
)
export const isFirstLaunchNoticeApproveFlagExist = () => fs.existsSync(firstLaunchNoticeApproveFlagPath)
export const isFirstLaunchNoticeApproveFlagExist = () =>
fs.existsSync(firstLaunchNoticeApproveFlagPath)
export const createFirstLaunchNoticeApproveFlag = () => {
ensureStorageFileExist()
fs.writeFileSync(firstLaunchNoticeApproveFlagPath, buildInfo.version)
}
export async function waitForUserApproveAgreement({ windowOption } = {}) {
return new Promise((resolve, reject) => {
createFirstLaunchNoticeWindow({ ...windowOption })
let processDone = false
function handler() {
processDone = true
firstLaunchNoticeWindow.close()
}
ipcMain.once('first-launch-notice-approve', handler)
firstLaunchNoticeWindow.once('closed', () => {
ipcMain.off('first-launch-notice-approve', handler)
if (processDone) {
resolve(true)
} else {
reject(new Error('USER_CANCELLED'))
}
})
})
}
@@ -0,0 +1,23 @@
import { ipcMain } from 'electron'
import { createCookieAssistantWindow, cookieAssistantWindow } from '../window/cookieAssistantWindow';
export async function loginWithCookieAssistant({ windowOption } = {}) {
return new Promise((resolve, reject) => {
createCookieAssistantWindow({ ...windowOption })
let processDone = false
function handler() {
processDone = true
cookieAssistantWindow.close()
}
ipcMain.once('cookie-saved', handler)
cookieAssistantWindow.once('closed', () => {
ipcMain.off('cookie-saved', handler)
if (processDone) {
resolve(true)
} else {
reject(new Error('USER_CANCELLED_LOGIN'))
}
})
})
}
@@ -0,0 +1,28 @@
import { ipcMain } from 'electron'
import {
createBrowserDownloadProgressWindow,
browserDownloadProgressWindow
} from '../window/browserDownloadProgressWindow'
export async function openBrowserDownloadWindow({ windowOption } = {}) {
return new Promise((resolve, reject) => {
createBrowserDownloadProgressWindow({ ...windowOption })
let processDone = false
let pathOfDownloadedBrowser = null
function handler(_, executablePath) {
pathOfDownloadedBrowser = executablePath
processDone = true
browserDownloadProgressWindow.close()
}
ipcMain.once('browser-download-done', handler)
browserDownloadProgressWindow.once('closed', () => {
ipcMain.off('browser-download-done', handler)
if (processDone) {
resolve(pathOfDownloadedBrowser)
} else {
reject(new Error('USER_CANCELLED_CONFIG_BROWSER'))
}
})
})
}
@@ -0,0 +1,74 @@
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../common/enums/auto-start-chat'
import { daemonEE, sendToDaemon } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon'
import { saveAndGetCurrentRunRecord } from '../flow/OPEN_SETTING_WINDOW/utils/db'
import minimist from 'minimist'
export async function runCommon({ mode }) {
await sendToDaemon(
{
type: 'user-process-register'
},
{
needCallback: true
}
)
const taskList = (
await sendToDaemon(
{
type: 'get-status'
},
{
needCallback: true
}
)
)?.workers
const runningTask = taskList?.find((it) => it.workerId === mode)
if (runningTask) {
const commandlineArgs = minimist(runningTask.args ?? [])
const runRecordId = Number(commandlineArgs['run-record-id'])
console.log('任务已在运行中')
return {
runRecordId,
isAlreadyRunning: true
}
}
const currentRunRecord = (await saveAndGetCurrentRunRecord())?.data
const subProcessEnv = {
...process.env,
GEEKGEEKRUND_NO_AUTO_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(',')
}
const args =
process.env.NODE_ENV === 'development'
? [process.argv[1], `--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`]
: [`--mode=${mode}`, `--run-record-id=${currentRunRecord?.id || 0}`]
await sendToDaemon(
{
type: 'start-worker',
workerId: mode,
command: process.argv[0],
args,
env: subProcessEnv
},
{
needCallback: true
}
)
daemonEE.on('message', (message) => {
if (message.type === 'worker-exited') {
if (
message.workerId === mode &&
!message.restarting &&
globalThis.GEEKGEEKRUN_PROCESS_ROLE !== 'ui'
) {
process.exit(0)
}
}
})
return {
runRecordId: currentRunRecord?.id
}
}
@@ -9,7 +9,7 @@ export enum DOWNLOAD_ERROR_EXIT_CODE {
DOWNLOAD_ERROR = 80
}
export const checkAndDownloadDependenciesForInit = async () => {
export const downloadDependenciesForInit = async () => {
process.on('disconnect', () => app.exit())
process.on('uncaughtException', () => app.exit(DOWNLOAD_ERROR_EXIT_CODE.DOWNLOAD_ERROR))
app.dock?.hide()
@@ -50,7 +50,7 @@ export const checkAndDownloadDependenciesForInit = async () => {
timeoutTimer = setTimeout(() => {
// will encounter this when network disconnected when downloading
promiseWithResolver.reject(new Error('PROGRESS_NOT_CHANGED_TOO_LONG'))
}, 5 * 1000)
}, 10 * 1000)
} else {
clearTimeout(throttleProgressTimer)
throttleProgressTimer = null
@@ -73,7 +73,7 @@ export const checkAndDownloadDependenciesForInit = async () => {
)
throttleProgressTimer = setTimeout(() => {
throttleProgressTimer = null
}, 2500)
}, 500)
}
}
})
@@ -94,11 +94,13 @@ export const checkAndDownloadDependenciesForInit = async () => {
pipe,
JSON.stringify({
type: 'PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR',
...err instanceof Error ? {
name: err.name,
message: err.message,
stack: err.stack
} : null
...(err instanceof Error
? {
name: err.name,
message: err.message,
stack: err.stack
}
: null)
}) + '\r\n'
)
await sleep(1000)
@@ -8,7 +8,7 @@ export interface BrowserInfo {
executablePath: string
}
const CONFIG_VSERION = 2
const CONFIG_VERSION = 2
const runtimeFolderPath = path.join(os.homedir(), '.geekgeekrun')
export const lastUsedBrowserRecordFilePath = path.join(
@@ -35,18 +35,11 @@ export const getLastUsedAndAvailableBrowser = async (): Promise<BrowserInfo | nu
!path ||
!fs.existsSync(path) ||
!Number(configVersion) ||
Number(configVersion) < CONFIG_VSERION
Number(configVersion) < CONFIG_VERSION
) {
await removeLastUsedAndAvailableBrowserPath()
return null
}
// blacklist browser
if (path.includes(`Microsoft\\Edge\\Application\\msedge.exe`)) {
await removeLastUsedAndAvailableBrowserPath()
return null
}
return {
executablePath: path,
browser
@@ -64,7 +57,7 @@ export const saveLastUsedAndAvailableBrowserInfo = async (browserInfo: BrowserIn
}
await fsPromise.writeFile(
lastUsedBrowserRecordFilePath,
[browserInfo.executablePath, browserInfo.browser, CONFIG_VSERION].join('\n')
[browserInfo.executablePath, browserInfo.browser, CONFIG_VERSION].join('\n')
)
} catch {
console.warn('lastUsedBrowserRecordFile write error')
@@ -1,5 +1,3 @@
import * as path from 'node:path'
import * as os from 'node:os'
import * as fs from 'node:fs'
import type { InstalledBrowser } from '@puppeteer/browsers'
import {
@@ -9,6 +7,8 @@ import {
removeLastUsedAndAvailableBrowserPath
} from '../browser-history'
import gtag from '../../../../utils/gtag'
import { EXPECT_CHROMIUM_BUILD_ID } from '../../../../../common/constant'
import { cacheDir } from '../../../../constant'
const getPuppeteerManagerModule = async () => {
const puppeteerManager = await import('@puppeteer/browsers')
@@ -16,8 +16,6 @@ const getPuppeteerManagerModule = async () => {
return puppeteerManager
}
const EXPECT_CHROMIUM_BUILD_ID = '139.0.7258.154'
const cacheDir = path.join(os.homedir(), '.geekgeekrun', 'cache')
const getExpectCachedPuppeteerExecutable = async (): Promise<BrowserInfo> => {
const puppeteerManager = await getPuppeteerManagerModule()
@@ -85,41 +83,48 @@ export const checkAndDownloadPuppeteerExecutable = async (
})
).find((it) => it.buildId === EXPECT_CHROMIUM_BUILD_ID)!
}
await saveLastUsedAndAvailableBrowserInfo({
executablePath: installedBrowser.executablePath,
browser:
installedBrowser.browser[0].toUpperCase() +
installedBrowser.browser.slice(1) +
' ' +
EXPECT_CHROMIUM_BUILD_ID
})
// await saveLastUsedAndAvailableBrowserInfo({
// executablePath: installedBrowser.executablePath,
// browser:
// installedBrowser.browser[0].toUpperCase() +
// installedBrowser.browser.slice(1) +
// ' ' +
// EXPECT_CHROMIUM_BUILD_ID
// })
return installedBrowser
}
export const getAnyAvailablePuppeteerExecutable = async (): Promise<BrowserInfo | null> => {
const lastUsedOne = await getLastUsedAndAvailableBrowser()
if (lastUsedOne) {
return lastUsedOne
export const getAnyAvailablePuppeteerExecutable = async ({
ignoreCached = false,
noSave = false
}: {
ignoreCached?: boolean
noSave?: boolean
} = {}): Promise<BrowserInfo | null> => {
if (!ignoreCached) {
const lastUsedOne = await getLastUsedAndAvailableBrowser()
if (lastUsedOne) {
return lastUsedOne
}
}
// find existed browser - the fallback one
if (await checkCachedPuppeteerExecutable()) {
const cachedOne = await getExpectCachedPuppeteerExecutable()
!noSave && (await saveLastUsedAndAvailableBrowserInfo(cachedOne))
return cachedOne
}
// find existed browser - the one maybe actively installed by user or ship with os like Edge on windows
try {
const existedOne = await findAndLocateUserInstalledChromiumExecutableSync()
await saveLastUsedAndAvailableBrowserInfo(existedOne)
!noSave && (await saveLastUsedAndAvailableBrowserInfo(existedOne))
// save its path
return existedOne
} catch (err) {
console.error(err)
console.log('no existed browser path found')
}
// find existed browser - the fallback one
if (await checkCachedPuppeteerExecutable()) {
const cachedOne = await getExpectCachedPuppeteerExecutable()
await saveLastUsedAndAvailableBrowserInfo(cachedOne)
return cachedOne
}
// if no one available, then return null and remove last used browser
await removeLastUsedAndAvailableBrowserPath()
return 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()
@@ -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)
}
}
@@ -5,19 +5,28 @@ import {
readConfigFile,
getPublicDbFilePath
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import * as fs from 'fs'
import { pipeWriteRegardlessError } from '../utils/pipe'
import { getAnyAvailablePuppeteerExecutable } from '../CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable'
// import { pipeWriteRegardlessError } from '../utils/pipe'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { AUTO_CHAT_ERROR_EXIT_CODE } from '../../../common/enums/auto-start-chat'
import attachListenerForKillSelfOnParentExited from '../../utils/attachListenerForKillSelfOnParentExited'
import minimist from 'minimist'
import SqlitePluginModule from '@geekgeekrun/sqlite-plugin'
import gtag from '../../utils/gtag'
import GtagPlugin from '../../utils/gtag/GtagPlugin'
import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
// import { PeriodPushCurrentPageScreenshotPlugin } from '../../utils/screenshot'
import { checkShouldExit } from '../../utils/worker'
import { CookieInvalidHandlePlugin } from '../../features/cookie-invalid-handle-plugin'
import initPublicIpc from '../../utils/initPublicIpc'
import { getLastUsedAndAvailableBrowser } from '../DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { configWithBrowserAssistant } from '../../features/config-with-browser-assistant'
const { default: SqlitePlugin } = SqlitePluginModule
process.on('SIGTERM', () => {
console.log('收到SIGTERM信号,正在退出')
process.exit(0)
})
const rerunInterval = (() => {
let v = Number(process.env.MAIN_BOSSGEEKGO_RERUN_INTERVAL)
if (isNaN(v)) {
@@ -33,14 +42,54 @@ const initPlugins = (hooks) => {
new DingtalkPlugin(dingTalkAccessToken).apply(hooks)
new SqlitePlugin(getPublicDbFilePath()).apply(hooks)
new GtagPlugin().apply(hooks)
// new PeriodPushCurrentPageScreenshotPlugin().apply(hooks)
new CookieInvalidHandlePlugin().apply(hooks)
}
let isParentProcessDisconnect = false
process.once('disconnect', () => {
isParentProcessDisconnect = true
})
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
const runAutoChat = async () => {
app.dock?.hide()
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
if (!puppeteerExecutable) {
try {
await configWithBrowserAssistant({ autoFind: true })
} catch (error) {
//
}
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
}
if (!puppeteerExecutable) {
await dialog.showMessageBox({
type: `error`,
message: `未找到可用的浏览器`,
detail: `请重新运行本程序,按照提示安装、配置浏览器`
})
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'rejected'
},
runRecordId
}
})
app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
return
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'fulfilled'
},
runRecordId
}
})
process.env.PUPPETEER_EXECUTABLE_PATH = puppeteerExecutable.executablePath
const { initPuppeteer, mainLoop, closeBrowserWindow, autoStartChatEventBus } = await import(
'@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs'
)
@@ -48,43 +97,13 @@ const runAutoChat = async () => {
closeBrowserWindow()
app.exit()
})
app.dock?.hide()
let pipe: null | fs.WriteStream = null
try {
pipe = fs.createWriteStream(null, { fd: 3 })
} catch {
console.warn('pipe is not available')
}
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'INITIALIZE_PUPPETEER'
}) + '\r\n'
)
try {
await initPuppeteer()
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)
return
}
const isPuppeteerExecutable = !!(await getAnyAvailablePuppeteerExecutable())
if (!isPuppeteerExecutable) {
app.exit(AUTO_CHAT_ERROR_EXIT_CODE.PUPPETEER_IS_NOT_EXECUTABLE)
return
}
await initPuppeteer()
const hooks = {
puppeteerLaunched: new SyncHook(),
puppeteerLaunched: new SyncHook(['browser']),
pageGotten: new SyncHook(['page']),
pageLoaded: new SyncHook(),
cookieWillSet: new SyncHook(['cookies']),
cookieWillSet: new AsyncSeriesHook(['cookies']),
userInfoResponse: new AsyncSeriesHook(['userInfo']),
mainFlowWillLaunch: new AsyncSeriesHook(['args']),
jobDetailIsGetFromRecommendList: new AsyncSeriesHook(['userInfo']),
@@ -101,23 +120,11 @@ 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'
)
autoStartChatEventBus.once('LOGIN_STATUS_INVALID', () => {
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'LOGIN_STATUS_INVALID' //geek-auto-start-chat-with-boss-started
}) + '\r\n'
)
})
while (![isParentProcessDisconnect].includes(true)) {
while (true) {
try {
await mainLoop(hooks)
} catch (err) {
@@ -126,7 +133,7 @@ const runAutoChat = async () => {
await dialog.showMessageBox({
type: `error`,
message: `登录状态无效`,
detail: `请重新登录Boss直聘`
detail: `请重新登录BOSS直聘`
})
process.exit(AUTO_CHAT_ERROR_EXIT_CODE.LOGIN_STATUS_INVALID)
break
@@ -139,9 +146,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.`
)
@@ -150,14 +166,32 @@ 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)
}
export const waitForProcessHandShakeAndRunAutoChat = async () => {
await app.whenReady()
app.on('window-all-closed', (e) => {
e.preventDefault()
})
initPublicIpc()
await connectToDaemon()
await sendToDaemon(
{
type: 'ping'
},
{
needCallback: true
}
)
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'worker-launch',
status: 'fulfilled'
},
runRecordId
}
})
runAutoChat()
}
@@ -1,7 +1,12 @@
import { app } from 'electron'
import { main, loginEventBus } from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
import { app, dialog } from 'electron'
import {
main,
loginEventBus
} from '@geekgeekrun/launch-bosszhipin-login-page-with-preload-extension'
import { pipeWriteRegardlessError } from './utils/pipe'
import fs from "node:fs";
import fs from 'node:fs'
import { getLastUsedAndAvailableBrowser } from './DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { configWithBrowserAssistant } from '../features/config-with-browser-assistant'
export const launchBossZhipinLoginPageWithPreloadExtension = async () => {
process.on('disconnect', () => app.exit())
@@ -18,15 +23,26 @@ export const launchBossZhipinLoginPageWithPreloadExtension = async () => {
type: 'INITIALIZE_PUPPETEER'
}) + '\r\n'
)
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
if (!puppeteerExecutable) {
try {
await configWithBrowserAssistant({ autoFind: true })
} catch (error) {
//
}
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
}
if (!puppeteerExecutable) {
await dialog.showMessageBox({
type: `error`,
message: `未找到可用的浏览器`,
detail: `请重新运行本程序,按照提示安装、配置浏览器`
})
app.exit(1)
}
const { initPuppeteer } = await import('@geekgeekrun/geek-auto-start-chat-with-boss/index.mjs')
try {
await initPuppeteer()
pipeWriteRegardlessError(
pipe,
JSON.stringify({
type: 'PUPPETEER_INITIALIZE_SUCCESSFULLY'
}) + '\r\n'
)
} catch (err) {
console.error(err)
app.exit(1)
@@ -0,0 +1,5 @@
;(async () => {
await import('@geekgeekrun/pm/daemon.js')
})()
export {}
@@ -0,0 +1,145 @@
import { randomUUID } from 'node:crypto'
import { EventEmitter } from 'node:events'
import { tmpdir } from 'node:os'
import path from 'node:path'
import net from 'node:net'
import split2 from 'split2'
let daemonClient = null
export const daemonEE = new EventEmitter()
const waitForCallbackTaskMap = new Map()
export function getSocketPath(socketName) {
const ipcSocketPath =
process.platform === 'win32'
? `\\\\.\\pipe\\${socketName}`
: path.join(tmpdir(), `${socketName}.sock`)
return ipcSocketPath
}
// 连接到守护进程
export async function connectToDaemon() {
daemonClient = new net.Socket()
let isConnected = false
await new Promise((resolve, reject) => {
const ipcSocketName = process.env.GEEKGEEKRUND_PIPE_NAME
const ipcSocketPath = getSocketPath(ipcSocketName)
daemonClient.connect(ipcSocketPath, 'localhost', () => {
isConnected = true
console.log('已连接到守护进程')
daemonEE.emit('connect')
// 使用 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)
daemonEE.emit('message', message)
// FIXME:
// 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('close', () => {
if (!isConnected) {
return
}
console.log('守护进程连接已关闭')
daemonEE.emit('close')
})
resolve(true)
// 通知渲染进程连接成功
// if (mainWindow) {
// mainWindow.webContents.send('daemon-connected');
// }
})
daemonClient.on('close', () => {
if (isConnected) {
return
}
reject(new Error('连接到守护进程超时'))
})
daemonClient.on('error', (err) => {
console.error('守护进程连接错误:', err)
// daemonEE.emit('error', err)
reject(err)
})
})
}
// 向守护进程发送消息
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 });
// });
export function closeDaemonClient() {
daemonClient?.destroy()
}
@@ -5,6 +5,8 @@ import './app-menu'
import initIpc from './ipc'
import gtag from '../../utils/gtag'
import initPublicIpc from '../../utils/initPublicIpc'
import { sendToDaemon, closeDaemonClient } from './connect-to-daemon'
export function openSettingWindow() {
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
if (!app.requestSingleInstanceLock()) {
@@ -65,4 +67,25 @@ export function openSettingWindow() {
globalShortcut.unregister('Command+Option+Shift+/')
})
})
whenReadyPromise.then(async () => {
await sendToDaemon(
{
type: 'ping'
},
{
needCallback: true
}
)
await sendToDaemon(
{
type: 'user-process-register'
},
{
needCallback: true
}
)
})
app.on('window-all-closed', closeDaemonClient)
app.on('before-quit', closeDaemonClient)
}
@@ -1,22 +1,18 @@
import { ipcMain, shell, app } from 'electron'
import { ipcMain, shell, app, dialog, BrowserWindow } from 'electron'
import path from 'path'
import * as childProcess from 'node:child_process'
import {
ensureConfigFileExist,
ensureStorageFileExist,
configFileNameList,
readConfigFile,
writeConfigFile,
readStorageFile,
writeStorageFile,
storageFilePath
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
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 { getAnyAvailablePuppeteerExecutable } from '../../DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable/index'
import { mainWindow } from '../../../window/mainWindow'
import {
getAutoStartChatRecord,
@@ -36,11 +32,11 @@ import { createResumeEditorWindow, resumeEditorWindow } from '../../../window/re
import {
getValidTemplate,
requestNewMessageContent
} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
import {
autoReminderPromptTemplateFileName,
writeDefaultAutoRemindPrompt
} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
} from '../../READ_NO_REPLY_AUTO_REMINDER_MAIN/boss-operation'
import {
checkIsResumeContentValid,
resumeContentEnoughDetect
@@ -52,6 +48,16 @@ import {
import { RequestSceneEnum } from '../../../features/llm-request-log'
import { checkUpdateForUi } from '../../../features/updater'
import gtag from '../../../utils/gtag'
import { daemonEE, sendToDaemon } from '../connect-to-daemon'
import { runCommon } from '../../../features/run-common'
import { loginWithCookieAssistant } from '../../../features/login-with-cookie-assistant'
import { configWithBrowserAssistant } from '../../../features/config-with-browser-assistant'
import {
createFirstLaunchNoticeApproveFlag,
isFirstLaunchNoticeApproveFlagExist,
waitForUserApproveAgreement
} from '../../../features/first-launch-notice-window'
import { getLastUsedAndAvailableBrowser } from '../../DOWNLOAD_DEPENDENCIES/utils/browser-history'
export default function initIpc() {
ipcMain.handle('fetch-config-file-content', async () => {
@@ -175,6 +181,12 @@ export default function initIpc() {
if (hasOwn(payload, 'sageTimePauseMinute')) {
bossConfig.sageTimePauseMinute = payload.sageTimePauseMinute
}
if (hasOwn(payload, 'blockCompanyNameRegExpStr')) {
bossConfig.blockCompanyNameRegExpStr = payload.blockCompanyNameRegExpStr
}
if (hasOwn(payload, 'blockCompanyNameRegMatchStrategy')) {
bossConfig.blockCompanyNameRegMatchStrategy = payload.blockCompanyNameRegMatchStrategy
}
promiseArr.push(writeConfigFile('boss.json', bossConfig))
@@ -187,262 +199,111 @@ export default function initIpc() {
return await Promise.all(promiseArr)
})
ipcMain.handle('read-storage-file', async (ev, payload) => {
ensureStorageFileExist()
return await readStorageFile(payload.fileName)
})
ipcMain.handle('write-storage-file', async (ev, payload) => {
ensureStorageFileExist()
return await writeStorageFile(payload.fileName, JSON.parse(payload.data))
})
// 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 () => {
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
}
subProcessOfPuppeteer = childProcess.spawn(
process.argv[0],
[
process.argv[1],
`--mode=geekAutoStartWithBossDaemon`,
`--mode-to-daemon=geekAutoStartWithBossMain`
],
{
env: subProcessEnv,
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc']
ipcMain.handle('run-geek-auto-start-chat-with-boss', async (ev) => {
const mode = 'geekAutoStartWithBossMain'
const { runRecordId } = await runCommon({ mode })
daemonEE.on('message', function handler(message) {
if (message.workerId !== mode) {
return
}
if (message.type === 'worker-exited') {
mainWindow?.webContents.send('worker-exited', message)
}
)
// 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:
return { runRecordId }
})
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
}
subProcessOfPuppeteer = childProcess.spawn(
process.argv[0],
[process.argv[1], `--mode=readNoReplyAutoReminder`],
{
env: subProcessEnv,
stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'ipc']
const mode = 'readNoReplyAutoReminderMain'
const { runRecordId } = await runCommon({ mode })
daemonEE.on('message', function handler(message) {
if (message.workerId !== mode) {
return
}
)
// 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)
mainWindow?.webContents.send('check-boss-zhipin-cookie-file')
return
}
case 'ERR_INTERNET_DISCONNECTED': {
mainWindow?.webContents.send('toast-message', {
type: 'error',
message: '联网失败,请检查网络连接'
})
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')
}
})
resolve(true)
})
// TODO:
})
ipcMain.handle('check-dependencies', async () => {
const [anyAvailablePuppeteerExecutable] = await Promise.all([
getAnyAvailablePuppeteerExecutable()
])
return {
puppeteerExecutableAvailable: !!anyAvailablePuppeteerExecutable
}
})
let subProcessOfCheckAndDownloadDependencies: ChildProcess | null = null
ipcMain.handle('setup-dependencies', async () => {
if (subProcessOfCheckAndDownloadDependencies) {
return
}
subProcessOfCheckAndDownloadDependencies = childProcess.spawn(
process.argv[0],
[process.argv[1], `--mode=checkAndDownloadDependenciesForInit`],
{
stdio: [null, null, null, 'pipe', 'ipc']
if (message.type === 'worker-exited') {
mainWindow?.webContents.send('worker-exited', message)
}
)
return new Promise((resolve, reject) => {
subProcessOfCheckAndDownloadDependencies!.stdio[3]!.pipe(JSONStream.parse()).on(
'data',
(raw) => {
const data = raw
switch (data.type) {
case 'NEED_RESETUP_DEPENDENCIES':
case 'PUPPETEER_DOWNLOAD_PROGRESS': {
mainWindow?.webContents.send(data.type, data)
break
}
case 'PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR': {
console.error(data)
break
}
default: {
return
}
}
}
)
subProcessOfCheckAndDownloadDependencies!.once('exit', (exitCode) => {
switch (exitCode) {
case 0: {
resolve(exitCode)
break
}
default: {
reject('PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR')
break
}
}
subProcessOfCheckAndDownloadDependencies = null
})
})
return { runRecordId }
})
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 {
//
}
}, 1000)
})
let subProcessOfBossZhipinLoginPageWithPreloadExtension: ChildProcess | null = null
ipcMain.on('launch-bosszhipin-login-page-with-preload-extension', async () => {
try {
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
} catch {
//
}
const subProcessEnv = {
...process.env,
PUPPETEER_EXECUTABLE_PATH: (await getAnyAvailablePuppeteerExecutable())!.executablePath
}
subProcessOfBossZhipinLoginPageWithPreloadExtension = childProcess.spawn(
process.argv[0],
[process.argv[1], `--mode=launchBossZhipinLoginPageWithPreloadExtension`],
{
env: subProcessEnv,
stdio: [null, null, null, 'pipe', 'ipc']
}
)
subProcessOfBossZhipinLoginPageWithPreloadExtension!.stdio[3]!.pipe(JSONStream.parse()).on(
'data',
(raw) => {
const data = raw
switch (data.type) {
case 'BOSS_ZHIPIN_COOKIE_COLLECTED': {
mainWindow?.webContents.send(data.type, data)
break
}
default: {
return
}
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
}
)
subProcessOfBossZhipinLoginPageWithPreloadExtension!.once('exit', () => {
mainWindow?.webContents.send('BOSS_ZHIPIN_LOGIN_PAGE_CLOSED')
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
})
await p
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopped')
})
ipcMain.on('kill-bosszhipin-login-page-with-preload-extension', async () => {
try {
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
} catch {
//
} finally {
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
}
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 !== 'readNoReplyAutoReminderMain') {
return
}
if (message.type === 'worker-exited') {
daemonEE.off('message', handler)
resolve(undefined)
}
})
})
await sendToDaemon(
{
type: 'stop-worker',
workerId: 'readNoReplyAutoReminderMain'
},
{
needCallback: true
}
)
await p
mainWindow?.webContents.send('read-no-reply-auto-reminder-stopped')
})
ipcMain.handle('get-task-manager-list', async () => {
const result = await sendToDaemon(
{
type: 'get-status'
},
{
needCallback: true
}
)
return result
})
// IPC处理:停止工具进程
ipcMain.handle('stop-task', async (_, workerId) => {
await sendToDaemon(
{
type: 'stop-worker',
workerId
},
{
needCallback: true
}
)
})
ipcMain.handle('check-boss-zhipin-cookie-file', () => {
@@ -473,7 +334,7 @@ export default function initIpc() {
let subProcessOfOpenBossSiteDefer: null | PromiseWithResolvers<ChildProcess> = null
let subProcessOfOpenBossSite: null | ChildProcess = null
ipcMain.handle('open-site-with-boss-cookie', async (_, data) => {
ipcMain.handle('open-site-with-boss-cookie', async (ev, data) => {
const url = data.url
if (
!subProcessOfOpenBossSiteDefer ||
@@ -481,7 +342,31 @@ export default function initIpc() {
subProcessOfOpenBossSite.killed
) {
subProcessOfOpenBossSiteDefer = Promise.withResolvers()
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
if (!puppeteerExecutable) {
try {
const parent = BrowserWindow.fromWebContents(ev.sender) || undefined
await configWithBrowserAssistant({
autoFind: true,
windowOption: {
parent,
modal: !!parent,
show: true
}
})
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
} catch (error) {
//
}
}
if (!puppeteerExecutable) {
await dialog.showMessageBox({
type: `error`,
message: `未找到可用的浏览器`,
detail: `请重新运行本程序,按照提示安装、配置浏览器`
})
return
}
const subProcessEnv = {
...process.env,
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable!.executablePath
@@ -666,6 +551,60 @@ export default function initIpc() {
const newRelease = await checkUpdateForUi()
return newRelease
})
ipcMain.handle('login-with-cookie-assistant', async () => {
return await loginWithCookieAssistant({
windowOption: {
parent: mainWindow!,
modal: true,
show: true
}
})
})
ipcMain.handle('config-with-browser-assistant', async () => {
return await configWithBrowserAssistant({
windowOption: {
parent: mainWindow!,
modal: true,
show: true
}
})
})
ipcMain.handle('pre-enter-setting-ui', async () => {
if (!isFirstLaunchNoticeApproveFlagExist()) {
try {
await waitForUserApproveAgreement({
windowOption: {
parent: mainWindow!,
modal: true,
show: true
}
})
createFirstLaunchNoticeApproveFlag()
} catch {
app.exit(0)
return
}
}
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
if (!puppeteerExecutable) {
const lastBrowser = await getLastUsedAndAvailableBrowser()
if (!lastBrowser) {
try {
await configWithBrowserAssistant({
windowOption: {
parent: mainWindow!,
modal: true,
show: true
},
autoFind: true
})
} catch (err) {
void err
}
}
}
})
ipcMain.handle('exit-app-immediately', () => {
app.exit(0)
@@ -0,0 +1,91 @@
import { spawn } from 'child_process'
import {
ensureStorageFileExist,
writeStorageFile,
readStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
import { randomUUID } from 'node:crypto'
import { connectToDaemon } from './connect-to-daemon'
const isUiDev = process.env.NODE_ENV === 'development'
export async function ensureIpcPipeName({ isReset } = {}) {
if (isReset) {
await writeStorageFile('ipc-pipe-name', '', { isJson: false })
}
let ipcPipeName = readStorageFile('ipc-pipe-name', { isJson: false })
if (!ipcPipeName) {
ipcPipeName = `geekgeekrun-d_${randomUUID()}`
ensureStorageFileExist()
await writeStorageFile('ipc-pipe-name', ipcPipeName, { isJson: false })
}
process.env.GEEKGEEKRUND_PIPE_NAME = ipcPipeName
return ipcPipeName
}
export async function launchDaemon() {
let daemonProcess
async function startDaemon() {
console.log('启动守护进程...')
// 添加参数使守护进程在后台运行,不显示 UI
daemonProcess = spawn(
process.argv[0],
isUiDev ? [process.argv[1], `--mode=launchDaemon`] : [`--mode=launchDaemon`],
{
stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
detached: true,
env: {
...process.env
}
}
)
daemonProcess.stdout.on('data', (data) => {
console.log(`守护进程输出: ${data}`)
})
daemonProcess.stderr.on('data', (data) => {
console.error(`守护进程错误: ${data}`)
})
return new Promise((resolve, reject) => {
daemonProcess.stdio[3].on('data', (rawData) => {
let data
try {
data = JSON.parse(rawData.toString())
if (data.type === 'DAEMON_READY') {
resolve(true)
} else if (data.type === 'DAEMON_FATAL') {
reject(new Error(data.error))
}
} catch (err) {
console.error('', err)
}
})
})
}
await ensureIpcPipeName()
try {
await connectToDaemon()
} catch (err) {
let isDaemonLaunched = false
console.log('cannot connect to daemon, try to launch it', err)
// 启动守护进程
try {
await startDaemon()
isDaemonLaunched = true
} catch (err) {
console.log('cannot launch to daemon, try to change port', err)
daemonProcess?.kill('SIGKILL')
await ensureIpcPipeName({ isReset: true })
try {
await startDaemon()
isDaemonLaunched = true
} catch (err) {
console.log('cannot launch to daemon, try to change port failed', err)
}
}
if (isDaemonLaunched) {
await connectToDaemon()
}
}
}
@@ -20,7 +20,7 @@ export const initDbWorker = () => {
// attach more event
worker?.off('message', handler)
} else if (data.type === 'DB_INIT_FAIL') {
reject(undefined)
reject(data.error)
worker?.terminate()
worker?.off('message', handler)
worker = null
@@ -102,3 +102,10 @@ export const getJobHistoryByEncryptId = async (encryptJobId) => {
})
return res
}
export const saveAndGetCurrentRunRecord = async () => {
const res = await createWorkerPromise({
type: 'saveAndGetCurrentRunRecord'
})
return res
}
@@ -11,6 +11,7 @@ import { VMarkAsNotSuitLog } from '@geekgeekrun/sqlite-plugin/dist/entity/VMarkA
import { measureExecutionTime } from '../../../../../../common/utils/performance'
import { PageReq, PagedRes } from '../../../../../../common/types/pagination'
import { JobInfoChangeLog } from '@geekgeekrun/sqlite-plugin/dist/entity/JobInfoChangeLog'
import { AutoStartChatRunRecord } from '@geekgeekrun/sqlite-plugin/dist/entity/AutoStartChatRunRecord'
const dbInitPromise = initDb(getPublicDbFilePath())
let dataSource: DataSource | null = null
@@ -161,6 +162,13 @@ const payloadHandler = {
})
)
return data
},
async saveAndGetCurrentRunRecord() {
const autoStartChatRunRecord = new AutoStartChatRunRecord()
autoStartChatRunRecord.date = new Date()
const autoStartChatRunRecordRepository = dataSource!.getRepository(AutoStartChatRunRecord)
const result = await autoStartChatRunRecordRepository.save(autoStartChatRunRecord)
return result
}
}
@@ -7,8 +7,6 @@ import { setDomainLocalStorage } from '@geekgeekrun/utils/puppeteer/local-storag
const localStoragePageUrl = `https://www.zhipin.com/desktop/`
const bossChatUiUrl = `https://www.zhipin.com/web/geek/chat`
const bossCookies = readStorageFile('boss-cookies.json')
const bossLocalStorage = readStorageFile('boss-local-storage.json')
export async function bootstrap() {
const { puppeteer } = await initPuppeteer()
@@ -28,6 +26,8 @@ export async function bootstrap() {
export async function launchBoss(browser: Browser) {
const page = (await browser.pages())[0]
const bossCookies = readStorageFile('boss-cookies.json')
const bossLocalStorage = readStorageFile('boss-local-storage.json')
//set cookies
for (let i = 0; i < bossCookies.length; i++) {
await page.setCookie(bossCookies[i])
@@ -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 {
@@ -17,16 +16,35 @@ import {
getJobHireStatusRecord,
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 {
writeStorageFile,
readStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
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'
// import { pushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS } from '../../utils/screenshot'
import { checkShouldExit } from '../../utils/worker'
import minimist from 'minimist'
import { checkCookieListFormat } from '../../../common/utils/cookie'
import { loginWithCookieAssistant } from '../../features/login-with-cookie-assistant'
import initPublicIpc from '../../utils/initPublicIpc'
import { getLastUsedAndAvailableBrowser } from '../DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { configWithBrowserAssistant } from '../../features/config-with-browser-assistant'
process.on('SIGTERM', () => {
console.log('收到SIGTERM信号,正在退出')
process.exit(0)
})
const throttleIntervalMinutes =
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
@@ -42,7 +60,23 @@ const rechatLlmFallback =
const expectJobTypeRegExpStr = readConfigFile('boss.json').expectJobTypeRegExpStr
const onlyRemindBossWithExpectJobType =
readConfigFile('boss.json').autoReminder?.onlyRemindBossWithExpectJobType ?? !!expectJobTypeRegExpStr
readConfigFile('boss.json').autoReminder?.onlyRemindBossWithExpectJobType ??
!!expectJobTypeRegExpStr
const blockCompanyNameRegExpStr = readConfigFile('boss.json').blockCompanyNameRegExpStr ?? ''
const blockCompanyNameRegExp = (() => {
if (!blockCompanyNameRegExpStr?.trim()) {
return null
}
try {
return new RegExp(blockCompanyNameRegExpStr, 'im')
} catch {
return null
}
})()
const onlyRemindBossWithoutBlockCompanyName =
readConfigFile('boss.json').autoReminder?.onlyRemindBossWithoutBlockCompanyName ??
!!blockCompanyNameRegExp
const dbInitPromise = initDb(getPublicDbFilePath())
@@ -50,6 +84,30 @@ export const pageMapByName: {
boss?: Page | null
} = {}
// async function periodPushCurrentPageScreenshot () {
// try {
// if (pageMapByName.boss?.isClosed()) {
// return
// }
// const shouldExit = await checkShouldExit()
// if (shouldExit) {
// return
// }
// try {
// await pushCurrentPageScreenshot(pageMapByName.boss)
// }
// catch (err) {
// if (err?.message?.includes(`PAGE_CLOSED`)) {
// return
// }
// }
// setTimeout(periodPushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS)
// }
// catch {}
// }
// periodPushCurrentPageScreenshot()
async function saveCurrentChatRecord(page) {
const userInfo = await page.evaluate(
'document.querySelector(".main-wrap").__vue__.$store.state.userInfo'
@@ -114,7 +172,8 @@ async function saveCurrentChatRecord(page) {
async function checkJobIsClosed() {
const encryptJobId = await pageMapByName.boss!.evaluate(() => {
return document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$.encryptJobId
return document.querySelector('.chat-conversation .chat-im.chat-editor')?.__vue__?.conversation$
.encryptJobId
})
if (!encryptJobId) {
return false
@@ -214,6 +273,44 @@ const mainLoop = async () => {
browser = null
}
}
let bossCookies = readStorageFile('boss-cookies.json')
let cookieCheckResult = checkCookieListFormat(bossCookies)
while (!cookieCheckResult) {
try {
await loginWithCookieAssistant()
bossCookies = readStorageFile('boss-cookies.json')
cookieCheckResult = checkCookieListFormat(bossCookies)
} catch (err) {
await dialog.showMessageBox({
type: `error`,
message: `登录状态无效`,
detail: `请重新登录BOSS直聘`
})
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('LOGIN_STATUS_INVALID')
}
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'basic-cookie-check',
status: 'fulfilled'
},
runRecordId
}
})
const canNotConfirmIfHasReadMsgTemplateList = [
'Boss还没查看你的消息',
'你与该职位竞争者PK情况',
@@ -232,12 +329,45 @@ const mainLoop = async () => {
// #region
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/')) {
writeStorageFile('boss-cookies.json', [])
throw new Error('LOGIN_STATUS_INVALID')
try {
// popup login dialog, then update login status
await loginWithCookieAssistant()
} catch (err) {
await dialog.showMessageBox({
type: `error`,
message: `登录状态无效`,
detail: `请重新登录BOSS直聘`
})
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('LOGIN_STATUS_INVALID')
}
throw new Error('THROW_FOR_RETRY')
}
if (
currentPageUrl.startsWith('https://www.zhipin.com/web/common/403.html') ||
currentPageUrl.startsWith('https://www.zhipin.com/web/common/error.html')
) {
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('ACCESS_IS_DENIED')
}
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/safe/verify-slider')) {
@@ -260,9 +390,31 @@ const mainLoop = async () => {
})
if (validateRes.code === 0) {
await storeStorage(pageMapByName.boss)
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error('CAPTCHA_PASSED_AND_NEED_RESTART')
}
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: 'fulfilled'
},
runRecordId
}
})
// #endregion
// check set security question tip modal
let setSecurityQuestionTipModelProxy = await pageMapByName.boss!.$(
@@ -296,6 +448,9 @@ const mainLoop = async () => {
const toCheckItemAtIndex = friendListData.findIndex((it, index) => {
return (
index >= cursorToContinueFind &&
(onlyRemindBossWithoutBlockCompanyName && blockCompanyNameRegExp
? !blockCompanyNameRegExp.test(it.brandName)
: true) &&
(rechatLimitDay && it.updateTime
? +new Date() - it.updateTime < rechatLimitDay * 24 * 60 * 60 * 1000
: true) &&
@@ -452,10 +607,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)) {
@@ -465,19 +616,75 @@ const rerunInterval = (() => {
return v
})()
let pipe
try {
pipe = fs.createWriteStream(null, { fd: 3 })
} catch {
console.warn('pipe is not available')
}
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
export async function runEntry() {
process.on('disconnect', () => {
app.exit()
})
app.dock?.hide()
while (!isParentProcessDisconnect) {
await app.whenReady()
app.on('window-all-closed', (e) => {
e.preventDefault()
})
initPublicIpc()
await connectToDaemon()
await sendToDaemon(
{
type: 'ping'
},
{
needCallback: true
}
)
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'worker-launch',
status: 'fulfilled'
},
runRecordId
}
})
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
if (!puppeteerExecutable) {
try {
await configWithBrowserAssistant({ autoFind: true })
} catch (error) {
//
}
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
}
if (!puppeteerExecutable) {
await dialog.showMessageBox({
type: `error`,
message: `未找到可用的浏览器`,
detail: `请重新运行本程序,按照提示安装、配置浏览器`
})
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'rejected'
},
runRecordId
}
})
throw new Error(`PUPPETEER_IS_NOT_EXECUTABLE`)
}
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'puppeteer-executable-check',
status: 'fulfilled'
},
runRecordId
}
})
process.env.PUPPETEER_EXECUTABLE_PATH = puppeteerExecutable.executablePath
while (true) {
try {
await mainLoop()
} catch (err) {
@@ -487,31 +694,44 @@ 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')) {
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(`PUPPETEER_IS_NOT_EXECUTABLE`) ||
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
@@ -522,8 +742,6 @@ export async function runEntry() {
process.exit(0)
}
attachListenerForKillSelfOnParentExited()
process.once('uncaughtException', (error) => {
console.error('uncaughtException', error)
process.exit(1)
@@ -533,6 +751,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(),
+47 -15
View File
@@ -1,12 +1,25 @@
import minimist from 'minimist'
import { runCommon } from './features/run-common'
import { launchDaemon } from './flow/OPEN_SETTING_WINDOW/launch-daemon'
import { app } from 'electron'
// 捕获未处理的 EPIPE 错误
process.on('uncaughtException', (err) => {
if (err?.code === 'EPIPE' || err?.code === 'ERR_STREAM_DESTROYED') {
return
}
throw err
})
const isUiDev = process.env.NODE_ENV === 'development'
const commandlineArgs = minimist(isUiDev ? process.argv.slice(2) : process.argv.slice(1))
console.log(commandlineArgs)
const runMode = commandlineArgs['mode'];
const runMode = commandlineArgs['mode']
;(async () => {
switch (runMode) {
// #region internal use
case 'geekAutoStartWithBossMain': {
const { waitForProcessHandShakeAndRunAutoChat } = await import(
'./flow/GEEK_AUTO_START_CHAT_WITH_BOSS_MAIN/index'
@@ -14,18 +27,9 @@ 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'
)
checkAndDownloadDependenciesForInit()
case 'downloadDependenciesForInit': {
const { downloadDependenciesForInit } = await import('./flow/DOWNLOAD_DEPENDENCIES/index')
downloadDependenciesForInit()
break
}
case 'launchBossZhipinLoginPageWithPreloadExtension': {
@@ -40,15 +44,43 @@ const runMode = commandlineArgs['mode'];
launchBossSite()
break
}
case 'readNoReplyAutoReminder': {
const { runEntry } = await import('./flow/READ_NO_REPLY_AUTO_REMINDER/index')
case 'readNoReplyAutoReminderMain': {
const { runEntry } = await import('./flow/READ_NO_REPLY_AUTO_REMINDER_MAIN/index')
runEntry()
break
}
case 'launchDaemon': {
await import('./flow/LAUNCH_DAEMON')
break
}
// #endregion
// #region user entry
case 'geekAutoStartWithBoss': {
app.dock?.hide()
await launchDaemon()
const { isAlreadyRunning } = await runCommon({ mode: 'geekAutoStartWithBossMain' })
if (isAlreadyRunning) {
process.exit(0)
}
break
}
case 'readNoReplyAutoReminder': {
app.dock?.hide()
await launchDaemon()
const { isAlreadyRunning } = await runCommon({ mode: 'readNoReplyAutoReminderMain' })
if (isAlreadyRunning) {
process.exit(0)
}
break
}
default: {
globalThis.GEEKGEEKRUN_PROCESS_ROLE = 'ui'
await launchDaemon()
const { openSettingWindow } = await import('./flow/OPEN_SETTING_WINDOW/index')
openSettingWindow()
break
}
// #region
}
})()
+55 -1
View File
@@ -1,7 +1,13 @@
import { BrowserWindow, ipcMain, shell } from 'electron'
import { BrowserWindow, dialog, ipcMain, shell } from 'electron'
import gtag from './gtag'
import buildInfo from '../../common/build-info.json'
import os from 'node:os'
import fs from 'node:fs'
import {
ensureStorageFileExist,
readStorageFile,
writeStorageFile
} from '@geekgeekrun/geek-auto-start-chat-with-boss/runtime-file-utils.mjs'
export default function initPublicIpc() {
ipcMain.on(
@@ -56,4 +62,52 @@ export default function initPublicIpc() {
}
)
})
ipcMain.handle('read-storage-file', async (ev, payload) => {
ensureStorageFileExist()
return await readStorageFile(payload.fileName)
})
ipcMain.handle('write-storage-file', async (ev, payload) => {
ensureStorageFileExist()
return await writeStorageFile(payload.fileName, JSON.parse(payload.data))
})
ipcMain.handle('get-os-platform', () => {
return os.platform()
})
ipcMain.handle('choose-file', (ev, { fileChooserConfig }) => {
if (!fileChooserConfig) {
fileChooserConfig = {}
}
const win = BrowserWindow.fromWebContents(ev.sender)
if (!win) {
return dialog.showOpenDialog(fileChooserConfig)
} else {
return dialog.showOpenDialog(win, fileChooserConfig)
}
})
ipcMain.handle('check-executable-file', (ev, filePath: string) => {
if (!filePath?.trim()) {
return {
message: '文件名无效'
}
}
if (!fs.existsSync(filePath)) {
return {
message: '文件不存在'
}
}
if (!fs.statSync(filePath).isFile()) {
const messageSeg = ['文件不是可执行文件']
if (os.platform() === 'darwin') {
messageSeg.push(
'macOS 平台,可执行文件位于“App包.app/Contents/MacOS/ 文件夹下”,而不是“App包.app”文件夹整体'
)
}
return {
message: messageSeg.join('')
}
}
return null
})
}
+67
View File
@@ -0,0 +1,67 @@
import { sendToDaemon } from "../flow/OPEN_SETTING_WINDOW/connect-to-daemon"
import { checkShouldExit } from "./worker"
export const SCREENSHOT_INTERVAL_MS = 2500
export async function pushCurrentPageScreenshot (page) {
try {
if (!page) {
return
}
// 尝试截图当前页面(压缩为 jpeg + base64,避免文件写盘)
const screenshotBase64 = await page.screenshot({
type: 'jpeg',
quality: 60,
encoding: 'base64',
fullPage: false
})
const screenshotAt = Date.now()
await sendToDaemon({
type: 'worker-screenshot',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
data: {
screenshot: `data:image/jpeg;base64,${screenshotBase64}`,
screenshotAt,
pageUrl: page.url?.() ?? null
}
})
} catch (err) {
if (err?.message?.includes('Session closed')) {
throw new Error(`PAGE_CLOSED`)
}
// 截图失败不应影响主流程
console.warn('[pushCurrentPageScreenshot] error', err)
}
}
export class PeriodPushCurrentPageScreenshotPlugin {
apply(hooks) {
hooks.pageGotten.tap(
'PeriodPushCurrentPageScreenshotPlugin',
(page) => {
async function periodPushCurrentPageScreenshot () {
try {
if (page.isClosed()) {
return
}
const shouldExit = await checkShouldExit()
if (shouldExit) {
return
}
try {
await pushCurrentPageScreenshot(page)
}
catch (err) {
if (err?.message?.includes(`PAGE_CLOSED`)) {
return
}
}
setTimeout(periodPushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS)
}
catch {}
}
periodPushCurrentPageScreenshot()
}
)
}
}
+14
View File
@@ -0,0 +1,14 @@
import { sendToDaemon } from "../flow/OPEN_SETTING_WINDOW/connect-to-daemon"
export async function checkShouldExit () {
const shouldExitResponse = await sendToDaemon(
{
type: 'check-should-exit',
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
},
{
needCallback: true
}
)
return shouldExitResponse?.shouldExit
}
@@ -0,0 +1,100 @@
import { BrowserWindow, ipcMain } from 'electron'
import path from 'path'
import { getAnyAvailablePuppeteerExecutable } from '../flow/DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable'
import {
getLastUsedAndAvailableBrowser,
saveLastUsedAndAvailableBrowserInfo
} from '../flow/DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { openBrowserDownloadWindow } from '../features/open-browser-download-window'
export let browserAssistantWindow: BrowserWindow | null = null
const registerHandleWithWindow = (
win: BrowserWindow,
...args: Parameters<typeof ipcMain.handle>
) => {
const [channel, handler] = args
ipcMain.handle(channel, handler)
win.once('closed', () => ipcMain.removeHandler(channel))
}
export function createBrowserAssistantWindow(
opt?: Electron.BrowserWindowConstructorOptions,
{ autoFind } = {}
): BrowserWindow {
// Create the browser window.
if (browserAssistantWindow) {
browserAssistantWindow!.close()
}
browserAssistantWindow = new BrowserWindow({
width: 800,
minWidth: 800,
height: 400,
resizable: true,
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
},
...opt
})
browserAssistantWindow.on('ready-to-show', () => {
browserAssistantWindow!.show()
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
let routePath = '#/browserAssistant'
if (autoFind) {
routePath = '#/browserAutoFind'
}
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
browserAssistantWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + routePath)
} else {
browserAssistantWindow.loadURL(
'file://' + path.join(__dirname, '../renderer/index.html') + routePath
)
}
browserAssistantWindow!.once('closed', () => {
browserAssistantWindow = null
})
registerHandleWithWindow(
browserAssistantWindow,
'get-any-available-puppeteer-executable',
async (_, { ignoreCached, noSave } = {}) => {
return await getAnyAvailablePuppeteerExecutable({ ignoreCached, noSave })
}
)
registerHandleWithWindow(
browserAssistantWindow,
'get-last-used-and-available-browser',
async () => {
return await getLastUsedAndAvailableBrowser()
}
)
registerHandleWithWindow(
browserAssistantWindow,
'save-last-used-and-available-browser-info',
async (_, payload) => {
return await saveLastUsedAndAvailableBrowserInfo(payload)
}
)
registerHandleWithWindow(browserAssistantWindow, 'download-browser-with-downloader', async () => {
return await openBrowserDownloadWindow({
windowOption: {
parent: browserAssistantWindow!,
modal: true,
show: true
}
})
})
return browserAssistantWindow!
}
@@ -0,0 +1,124 @@
import { ChildProcess } from 'child_process'
import { BrowserWindow, ipcMain } from 'electron'
import path from 'path'
import * as childProcess from 'node:child_process'
import * as JSONStream from 'JSONStream'
import * as fs from 'node:fs'
import { cacheDir } from '../constant'
import { EXPECT_CHROMIUM_BUILD_ID } from '../../common/constant'
import * as puppeteerManager from '@puppeteer/browsers'
export let browserDownloadProgressWindow: BrowserWindow | null = null
const registerHandleWithWindow = (
win: BrowserWindow,
...args: Parameters<typeof ipcMain.handle>
) => {
const [channel, handler] = args
ipcMain.handle(channel, handler)
win.once('closed', () => ipcMain.removeHandler(channel))
}
export function createBrowserDownloadProgressWindow(
opt?: Electron.BrowserWindowConstructorOptions
): BrowserWindow {
// Create the browser window.
if (browserDownloadProgressWindow) {
browserDownloadProgressWindow!.close()
}
browserDownloadProgressWindow = new BrowserWindow({
width: 600,
height: 200,
resizable: false,
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
},
...opt
})
browserDownloadProgressWindow.on('ready-to-show', () => {
browserDownloadProgressWindow!.show()
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
browserDownloadProgressWindow.loadURL(
process.env['ELECTRON_RENDERER_URL'] + '#/browserDownloadProgress'
)
} else {
browserDownloadProgressWindow.loadURL(
'file://' + path.join(__dirname, '../renderer/index.html') + '#/browserDownloadProgress'
)
}
let subProcessOfCheckAndDownloadDependencies: ChildProcess | null = null
registerHandleWithWindow(browserDownloadProgressWindow, 'setup-dependencies', async () => {
if (subProcessOfCheckAndDownloadDependencies) {
return
}
subProcessOfCheckAndDownloadDependencies = childProcess.spawn(
process.argv[0],
[process.argv[1], `--mode=downloadDependenciesForInit`],
{
stdio: [null, null, null, 'pipe', 'ipc']
}
)
return new Promise((resolve, reject) => {
subProcessOfCheckAndDownloadDependencies!.stdio[3]!.pipe(JSONStream.parse()).on(
'data',
(raw) => {
const data = raw
switch (data.type) {
case 'NEED_RESETUP_DEPENDENCIES':
case 'PUPPETEER_DOWNLOAD_PROGRESS': {
browserDownloadProgressWindow?.webContents.send(data.type, data)
break
}
case 'PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR': {
console.error(data)
break
}
default: {
return
}
}
}
)
subProcessOfCheckAndDownloadDependencies!.once('exit', (exitCode) => {
const executablePath = puppeteerManager.computeExecutablePath({
browser: puppeteerManager.Browser.CHROME,
cacheDir,
buildId: EXPECT_CHROMIUM_BUILD_ID
})
if (exitCode === 0 && fs.existsSync(executablePath)) {
resolve(executablePath)
} else {
reject('PUPPETEER_DOWNLOAD_ENCOUNTER_ERROR')
}
subProcessOfCheckAndDownloadDependencies = null
})
})
})
const killHandler = async () => {
try {
subProcessOfCheckAndDownloadDependencies?.kill()
} catch {
//
} finally {
subProcessOfCheckAndDownloadDependencies = null
}
}
browserDownloadProgressWindow.once('closed', () => {
killHandler()
})
browserDownloadProgressWindow.once('closed', () => {
browserDownloadProgressWindow = null
})
return browserDownloadProgressWindow!
}
@@ -0,0 +1,132 @@
import { ChildProcess } from 'child_process'
import { BrowserWindow, dialog, ipcMain } from 'electron'
import path from 'path'
import * as childProcess from 'node:child_process'
import * as JSONStream from 'JSONStream'
import { getLastUsedAndAvailableBrowser } from '../flow/DOWNLOAD_DEPENDENCIES/utils/browser-history'
import { configWithBrowserAssistant } from '../features/config-with-browser-assistant'
export let cookieAssistantWindow: BrowserWindow | null = null
export function createCookieAssistantWindow(
opt?: Electron.BrowserWindowConstructorOptions
): BrowserWindow {
// Create the browser window.
if (cookieAssistantWindow) {
cookieAssistantWindow!.show()
}
cookieAssistantWindow = new BrowserWindow({
width: 960,
height: 720,
resizable: true,
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
},
...opt
})
cookieAssistantWindow.on('ready-to-show', () => {
cookieAssistantWindow!.show()
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
cookieAssistantWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/cookieAssistant')
} else {
cookieAssistantWindow.loadURL(
'file://' + path.join(__dirname, '../renderer/index.html') + '#/cookieAssistant'
)
}
cookieAssistantWindow!.once('closed', () => {
cookieAssistantWindow = null
})
let subProcessOfBossZhipinLoginPageWithPreloadExtension: ChildProcess | null = null
const launchHandler = async (ev) => {
try {
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
} catch {
//
}
let puppeteerExecutable = await getLastUsedAndAvailableBrowser()
if (!puppeteerExecutable) {
try {
const parent = BrowserWindow.fromWebContents(ev.sender) || undefined
await configWithBrowserAssistant({
autoFind: true,
windowOption: {
parent,
modal: !!parent,
show: true
}
})
} catch (error) {
//
}
puppeteerExecutable = await getLastUsedAndAvailableBrowser()
}
if (!puppeteerExecutable) {
await dialog.showMessageBox({
type: `error`,
message: `未找到可用的浏览器`,
detail: `请重新运行本程序,按照提示安装、配置浏览器`
})
return
}
const subProcessEnv = {
...process.env,
PUPPETEER_EXECUTABLE_PATH: puppeteerExecutable.executablePath
}
subProcessOfBossZhipinLoginPageWithPreloadExtension = childProcess.spawn(
process.argv[0],
[process.argv[1], `--mode=launchBossZhipinLoginPageWithPreloadExtension`],
{
env: subProcessEnv,
stdio: [null, null, null, 'pipe', 'ipc']
}
)
subProcessOfBossZhipinLoginPageWithPreloadExtension!.stdio[3]!.pipe(JSONStream.parse()).on(
'data',
(raw) => {
const data = raw
switch (data.type) {
case 'BOSS_ZHIPIN_COOKIE_COLLECTED': {
cookieAssistantWindow?.webContents.send(data.type, data)
break
}
default: {
return
}
}
}
)
subProcessOfBossZhipinLoginPageWithPreloadExtension!.once('exit', () => {
cookieAssistantWindow?.webContents.send('BOSS_ZHIPIN_LOGIN_PAGE_CLOSED')
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
})
}
ipcMain.on('launch-bosszhipin-login-page-with-preload-extension', launchHandler)
const killHandler = async () => {
try {
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
} catch {
//
} finally {
subProcessOfBossZhipinLoginPageWithPreloadExtension = null
}
}
ipcMain.on('kill-bosszhipin-login-page-with-preload-extension', killHandler)
cookieAssistantWindow.on('closed', () => {
subProcessOfBossZhipinLoginPageWithPreloadExtension?.kill()
ipcMain.off('launch-bosszhipin-login-page-with-preload-extension', launchHandler)
ipcMain.off('kill-bosszhipin-login-page-with-preload-extension', killHandler)
})
return cookieAssistantWindow!
}
@@ -1,6 +1,5 @@
import { BrowserWindow, ipcMain } from 'electron'
import { BrowserWindow } from 'electron'
import path from 'path'
import { createFirstLaunchNoticeApproveFlag } from '../features/first-launch-notice-window'
export let firstLaunchNoticeWindow: BrowserWindow | null = null
export function createFirstLaunchNoticeWindow(
@@ -41,11 +40,3 @@ export function createFirstLaunchNoticeWindow(
return firstLaunchNoticeWindow!
}
export const initIpc = () => {
ipcMain.handle('first-launch-notice-approve', () => {
createFirstLaunchNoticeApproveFlag()
firstLaunchNoticeWindow?.close()
})
}
initIpc()
+9 -10
View File
@@ -1,8 +1,7 @@
import { BrowserWindow, shell } from 'electron'
import path from 'path'
import { openDevTools } from '../commands'
import { createFirstLaunchNoticeWindow } from './firstLaunchNoticeWindow'
import { isFirstLaunchNoticeApproveFlagExist } from '../features/first-launch-notice-window'
import { daemonEE } from '../flow/OPEN_SETTING_WINDOW/connect-to-daemon'
export let mainWindow: BrowserWindow | null = null
export function createMainWindow(): BrowserWindow {
@@ -27,14 +26,6 @@ export function createMainWindow(): BrowserWindow {
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.on('ready-to-show', async () => {
!isFirstLaunchNoticeApproveFlagExist() &&
createFirstLaunchNoticeWindow({
parent: mainWindow!,
modal: true,
show: true
})
})
mainWindow.on('ready-to-show', async () => {
process.env.NODE_ENV === 'development' &&
setTimeout(() => {
@@ -58,5 +49,13 @@ export function createMainWindow(): BrowserWindow {
mainWindow!.once('closed', () => {
mainWindow = null
})
daemonEE.on('message', (message) => {
if (message.type === 'worker-to-gui-message') {
mainWindow?.webContents?.send('worker-to-gui-message', message)
}
})
daemonEE.on('error', (err) => {
console.log(err)
})
return mainWindow!
}
@@ -11,7 +11,7 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { random } from 'lodash-es'
import { random } from 'lodash'
const rowCount = 4
const colCount = 6
@@ -5,7 +5,7 @@
<el-col :span="6"><el-form-item label="公司">{{ jobInfo.companyName }}</el-form-item></el-col>
<el-col :span="6"><el-form-item label="职位名称">{{ jobInfo.jobName }}</el-form-item></el-col>
<el-col :span="6"><el-form-item label="职位分类">{{ jobInfo.positionName }}</el-form-item></el-col>
<el-col :span="6"><el-form-item label="Boss及其身份">{{ jobInfo.bossName }} {{ jobInfo.bossTitle }}</el-form-item></el-col>
<el-col :span="6"><el-form-item label="BOSS及其身份">{{ jobInfo.bossName }} {{ jobInfo.bossTitle }}</el-form-item></el-col>
</el-row>
</el-form>
<el-divider content-position="left">变更记录</el-divider>
@@ -0,0 +1,233 @@
<template>
<el-dialog
modal-class="running-overlay__modal"
:model-value="isDialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
width="400px"
@closed="fillEmptySteps"
>
<div flex flex-col flex-items-center>
<div class="dialog-header" w-full>
<div
h160px
w-full
:style="{
backgroundImage: 'linear-gradient(#666, #666)'
}"
></div>
</div>
<div class="dialog-main" w-full mt--20px>
<!-- v-if="stepsForRender.some(it => ['todo', 'pending', 'rejected'].includes(it.status))" -->
<div>
<ul m0 pl0>
<li
v-for="(item, index) in stepsForRender"
:key="index"
list-style-none
flex
justify-start
pt4px
pb4px
>
<div>
<span v-if="item.status === 'todo'">🕐</span>
<span v-if="item.status === 'pending'">👉</span>
<span v-if="item.status === 'fulfilled'"></span>
<span v-if="item.status === 'rejected'"></span>
</div>
<span ml8px>{{ item.describe }}</span>
</li>
</ul>
</div>
<div flex justify-between items-center w-full>
<div>
{{ runningStatusTextMapByCode[currentRunningStatus] }}
</div>
<div>
<slot name="op-buttons" :current-running-status="currentRunningStatus" />
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script lang="ts" setup>
// import { useTaskManagerStore } from '@renderer/store'
import { getAutoStartChatSteps } from '../../../../common/prerequisite-step-by-step-check'
import { computed, onUnmounted, ref, watch } from 'vue'
import {
AUTO_CHAT_ERROR_EXIT_CODE,
RUNNING_STATUS_ENUM
} from '../../../../common/enums/auto-start-chat'
import { gtagRenderer } from '@renderer/utils/gtag'
const props = defineProps({
workerId: {
type: String
},
runRecordId: {
type: Number
}
})
// const taskManagerStore = useTaskManagerStore()
// const runningTaskInfo = computed(() => {
// return taskManagerStore.runningTasks?.find((it) => {
// return it.workerId === props.workerId
// })
// })
const steps = ref([])
const stepsForRender = computed(() => {
const clonedSteps = JSON.parse(JSON.stringify(steps.value))
if (clonedSteps.some((it) => it.status === 'rejected')) {
return clonedSteps
}
const lastFulfilledIndex = clonedSteps.findLastIndex((it) => it.status === 'fulfilled')
if (lastFulfilledIndex + 1 < clonedSteps.length) {
clonedSteps[lastFulfilledIndex + 1].status = 'pending'
}
return clonedSteps
})
const runningStatusTextMapByCode = {
[RUNNING_STATUS_ENUM.RUNNING]: '正在运行中',
[RUNNING_STATUS_ENUM.NORMAL_EXITED]: '程序已正常退出',
[RUNNING_STATUS_ENUM.ERROR_EXITED]: '程序异常退出'
}
const currentRunningStatus = ref(RUNNING_STATUS_ENUM.RUNNING)
function fillEmptySteps() {
const arr = getAutoStartChatSteps()
arr.forEach((it) => (it.status = 'todo'))
steps.value = arr
currentRunningStatus.value = RUNNING_STATUS_ENUM.RUNNING
}
watch(() => props.runRecordId, fillEmptySteps, {
immediate: true
})
watch(
() => stepsForRender.value,
(v) => {
const rejectedItems = v?.filter((it) => it.status === 'rejected')
if (!rejectedItems.length) {
return
}
gtagRenderer('running_overlay_rejected', {
stepId: rejectedItems.map((it) => it.id).join(','),
workerId: props.workerId
})
},
{ deep: true }
)
const { ipcRenderer } = electron
function messageHandler(ev, { data }) {
if (
data.type !== 'prerequisite-step-by-step-checkstep-by-step-check' ||
data.runRecordId !== props.runRecordId
) {
return
}
const { id: stepId, status: stepStatus } = data.step
const targetStep = steps.value.find((it) => it.id === stepId)
if (!targetStep) {
return
}
targetStep.status = stepStatus
}
const unListenMessage = ipcRenderer.on('worker-to-gui-message', messageHandler)
onUnmounted(unListenMessage)
const isDialogVisible = ref(false)
const show = () => {
isDialogVisible.value = true
}
const hide = () => {
isDialogVisible.value = false
}
watch(
() => isDialogVisible.value,
(newVal) => {
if (!newVal) {
gtagRenderer('running_overlay_shown')
} else {
gtagRenderer('running_overlay_hidden')
}
}
)
defineExpose({
show,
hide
})
ipcRenderer.on('worker-exited', (ev, payload) => {
const { workerId, code } = payload
if (
workerId !== props.workerId
// || runRecordId !== props.runRecordId
) {
return
}
if (code !== AUTO_CHAT_ERROR_EXIT_CODE.NORMAL) {
currentRunningStatus.value = RUNNING_STATUS_ENUM.ERROR_EXITED
gtagRenderer('running_overlay_error_exited', {
exitCode: code,
workerId: props.workerId
})
} else {
currentRunningStatus.value = RUNNING_STATUS_ENUM.NORMAL_EXITED
gtagRenderer('running_overlay_normal_exited', {
exitCode: code,
workerId: props.workerId
})
}
})
</script>
<style lang="scss">
.el-overlay.running-overlay__modal {
position: absolute;
width: 100%;
height: 100%;
backdrop-filter: blur(3px);
background-color: transparent;
background-image: radial-gradient(transparent 1px, #fff 1px);
background-size: 4px 4px;
.el-overlay-dialog {
position: absolute;
pointer-events: all;
}
.el-dialog {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.el-dialog__header {
display: none;
}
.el-dialog__body {
// overflow: hidden;
}
.dialog-header {
display: none;
// display: flex;
// justify-content: center;
// border-radius: 20px 20px 0 0;
// overflow: hidden;
}
background-color: transparent;
box-shadow: none;
padding: 0;
border-radius: 0;
.dialog-main {
box-sizing: border-box;
background: var(--el-dialog-bg-color);
box-shadow: var(--el-dialog-box-shadow);
padding: var(--el-dialog-padding-primary);
//border-radius: 0 0 20px 20px;
border-radius: 20px;
}
}
}
</style>
@@ -0,0 +1,86 @@
<template>
<el-card class="task-item">
<div>
<div flex flex-col position-relative>
<div>
<el-button type="danger" size="small" @click="stopTask(task.workerId)">结束任务</el-button>
</div>
<img block :src="task.screenshot" height="190" width="360" />
<div position-absolute bottom-0 right-0 font-size-12px :style="{
backgroundColor: 'rgba(0,0,0,0.7)',
color: '#fff',
padding: '2px 4px 2px 6px',
borderRadius: '8px 0 0 0'
}">{{ task.screenshotAt ? dayjs(task.screenshotAt).format('YYYY-MM-DD HH:mm:ss') : ' - ' }}</div>
</div>
</div>
<div ml-30px>
<dl>
<dt>workerId</dt>
<dd>{{ task.workerId }}</dd>
</dl>
<dl>
<dt>status</dt>
<dd>{{ task.status }}</dd>
</dl>
<dl>
<dt>重启次数</dt>
<dd>{{ task.restartCount }}</dd>
</dl>
<dl>
<dt>已运行时间</dt>
<dd>{{ task.uptime ?? '-' }} 毫秒</dd>
</dl>
<dl>
<dt>命令行</dt>
<dd>{{ task.command }} {{ task.args.join(' ') }}</dd>
</dl>
<dl>
<dt>PID</dt>
<dd>{{ task.pid }}</dd>
</dl>
</div>
</el-card>
</template>
<script lang="ts" setup>
import dayjs from 'dayjs'
import { PropType } from 'vue'
defineProps({
task: {
type: Object as PropType<any>
}
})
const { ipcRenderer } = electron
const stopTask = async (workerId: string) => {
await ipcRenderer.invoke('stop-task', workerId)
}
</script>
<style lang="scss" scoped>
.task-item {
width: 1000px;
margin: 0 auto;
font-size: 14px;
overflow: hidden;
::v-deep(.el-card__body) {
display: flex;
}
dl {
margin: 0;
display: flex;
padding-top: 4px;
padding-bottom: 4px;
border-bottom: 1px solid #eee;
dt {
width: 6em;
flex: 0 0 6em;
}
dd {
word-break: break-all;
}
}
}
</style>
@@ -0,0 +1,18 @@
<template>
<div>
<div v-for="task in runningTasks" :key="task.workerId">
<div>
<TaskManagerItem :task="task" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useTaskManagerStore } from '@renderer/store'
import { computed } from 'vue'
import TaskManagerItem from './Item.vue'
const taskManagerStore = useTaskManagerStore()
const runningTasks = computed(() => taskManagerStore.runningTasks || [])
</script>
@@ -4,56 +4,37 @@
<img
class="block"
:class="{
'animate__animated animate__bounce animate__repeat-3':
Object.values(checkDependenciesResult).includes(false)
'animate__animated animate__bounce animate__repeat-3': true
}"
:width="256"
src="@renderer/../../../resources/icon.png"
/>
</div>
<div mt24px>愿你薪想事成</div>
<div class="h60px mt14px">
<RouterView
class="h100%"
:dependencies-status="checkDependenciesResult"
:process-waitee="downloadProcessWaitee"
></RouterView>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { onMounted, ref } from 'vue'
import { onMounted } from 'vue'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { gtagRenderer } from '@renderer/utils/gtag'
const router = useRouter()
const checkDependenciesResult = ref({})
const downloadProcessWaitee = ref(null)
// const checkDependenciesResult = ref({})
// const downloadProcessWaitee = ref(null)
onMounted(async () => {
gtagRenderer('bootstrap_mounted')
checkDependenciesResult.value = await electron.ipcRenderer.invoke('check-dependencies')
downloadProcessWaitee.value = Promise.withResolvers()
if (Object.values(checkDependenciesResult.value).includes(false)) {
gtagRenderer('dependencies_need_download')
router.replace('/downloadingDependencies')
} else {
downloadProcessWaitee.value!.resolve()
await sleep(1500)
try {
await electron.ipcRenderer.invoke('pre-enter-setting-ui')
} catch (err) {
console.log('pre-enter-setting-ui error', err)
} finally {
await sleep(500)
router.replace('/main-layout')
}
downloadProcessWaitee.value!.promise.then(async () => {
const isCookieFileValid = await electron.ipcRenderer.invoke('check-boss-zhipin-cookie-file')
if (!isCookieFileValid) {
gtagRenderer('found_cookie_invalid_when_bootstrap')
router.replace('/cookieAssistant')
} else {
await sleep(1000)
router.replace('/main-layout')
}
})
})
</script>
@@ -1,107 +0,0 @@
<template>
<div class="flex flex-col flex-items-start flex-justify-start" v-if="!dependenciesStatus.puppeteerExecutableAvailable">
<div mb14px>正在下载兼容的浏览器</div>
<el-progress
:percentage="browserDownloadPercentage"
:format="(n) => `${n.toFixed(1)}%`"
:stroke-width="10"
class="w400px"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, onUnmounted, PropType, h } from 'vue'
import { ElMessageBox } from 'element-plus'
import { gtagRenderer } from '@renderer/utils/gtag'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import FailMessage from './FailMessage.vue'
const props = defineProps({
dependenciesStatus: {
type: Object as PropType<Record<string, boolean>>,
default: () => ({})
},
processWaitee: Object
})
const browserDownloadPercentage = ref(0)
const handleBrowserDownloadProgress = (ev, { downloadedBytes, totalBytes }) => {
browserDownloadPercentage.value = (downloadedBytes / totalBytes) * 100
}
electron.ipcRenderer.on('PUPPETEER_DOWNLOAD_PROGRESS', handleBrowserDownloadProgress)
onUnmounted(() =>
electron.ipcRenderer.removeListener('PUPPETEER_DOWNLOAD_PROGRESS', handleBrowserDownloadProgress)
)
const downloadProcessExitCode = ref(0)
const processDownloadBrowser = async () => {
downloadProcessExitCode.value = 0
browserDownloadPercentage.value = 0
let restRetriedTime = 2
while (restRetriedTime > 0) {
try {
try {
await electron.ipcRenderer.invoke('setup-dependencies')
browserDownloadPercentage.value = 100
} catch (err) {
downloadProcessExitCode.value = 1
throw err
}
break
} catch (err) {
restRetriedTime--
if (restRetriedTime === 0) {
throw err
}
await sleep(5000)
}
}
}
const promiseList: Array<Promise<void>> = []
const processTasks = async () => {
if (!props.dependenciesStatus.puppeteerExecutableAvailable) {
gtagRenderer('start_download_puppeteer')
const p = processDownloadBrowser()
promiseList.push(p)
p.then(() => {
gtagRenderer('puppeteer_download_success')
props.dependenciesStatus.puppeteerExecutableAvailable = true
})
}
while (promiseList.length) {
const p = promiseList.shift()!
try {
p.then(() => {
if (!promiseList.length) {
props.processWaitee?.resolve?.()
}
})
await p
} catch {
gtagRenderer('encounter_error_when_download_deps')
await ElMessageBox.confirm(h(FailMessage), {
closeOnClickModal: false,
closeOnPressEscape: false,
showClose: false,
type: 'error',
cancelButtonText: '退出程序',
confirmButtonText: '重试'
})
.then(() => {
gtagRenderer('start_retry_download_deps')
processTasks()
})
.catch(() => {
gtagRenderer('cancel_download_deps_and_exit')
promiseList.length = 0
electron.ipcRenderer.invoke('exit-app-immediately')
})
}
}
}
processTasks()
</script>
@@ -0,0 +1,420 @@
<template>
<div class="h-screen of-hidden flex flex-col flex-items-center flex-justify-between">
<div flex-1 of-hidden w-full>
<el-form ref="formRef" :model="formData" :rules="rules" flex flex-col of-hidden h-full>
<div class="bg-#f6f6f6" flex-0>
<el-form-item
class="w-90%"
label="浏览器可执行文件路径"
label-position="top"
prop="browserPath"
pt30px
pb30px
ml-auto
mr-auto
mb-0
>
<div flex flex-1>
<el-input v-model="formData.browserPath" />
<el-button
type="primary"
:loading="isAutoDetectLoading"
@click="autoDetectPuppeteerExecutable"
>自动检测</el-button
>
<el-button :style="{ marginLeft: 0 }" @click="chooseExecutableFile"
>手动选择</el-button
>
</div>
</el-form-item>
</div>
<div flex-1 of-auto font-size-14px line-height-1.5em>
<div mt10px ml-auto mr-auto class="w-90%">
<div>常见问题</div>
<div ref="faqMainRef" class="faq-main">
<details class="faq-item" data-faq-id="cannot-auto-find-executable">
<summary>自动检测点击后提示未检测到可用浏览器的可执行文件</summary>
<div class="faq-answer">
请尝试如下方案之一来处理
<ul pl1em m0>
<li>
方案一通过本程序下载 Google Chrome for Testing
{{ EXPECT_CHROMIUM_BUILD_ID }} -
<a href="javascript:;" @click="handleClickLaunchBrowserDownloader">点击此处</a
>即可下载这个浏览器仅供本程序使用不会影响到当前 Google Chrome
安装本程序开发过程中主要是使用这个浏览器测试的<span color-orange
>可以保证兼容性</span
>网络波动有一定概率下载失败如多次尝试后确实不能下载成功请尝试方案二<span
color-orange
>推荐</span
>
</li>
<li>
方案二手动安装 Google Chrome 最新版本 -
<a href="javascript:;" @click="handleOpenChromeDownloadPage">点击此处</a>打开
Google Chrome
官方网站找到浏览器下载页面来下载安装程序下载完毕后执行安装程序安装完成后点击上方<a
href="javascript:;"
:loading="isAutoDetectLoading"
@click="autoDetectPuppeteerExecutable"
>自动检测</a
>按钮再次尝试截至本程序开发时2026.2.7Google Chrome 最新版本为
144.0.7559.133
多数情况下本程序都可以正常工作但由于浏览器会自动升级版本不固定可能存在<span
color-orange
>浏览器升级后某些功能不兼容导致本程序不能正确运行</span
>的问题如果您确实遇到不能正常运行的问题<a
href="javascript:;"
@click="handleFeedbackClick"
>提交 Issue</a
>来反馈同时请再尝试方案一
</li>
</ul>
</div>
</details>
<details class="faq-item" data-faq-id="manual-select-browser-prerequisite">
<summary>
如果要手动选择浏览器可选择的浏览器有哪些对于浏览器有什么要求
</summary>
<div class="faq-answer">
<div>
截至本程序开发时2026.2.7已确定支持的各操作系统下的浏览器及版本包括
</div>
<ul>
<li>
<div>macOS</div>
<ul>
<li>Google Chrome for Testing {{ EXPECT_CHROMIUM_BUILD_ID }}</li>
<li>Google Chrome 144.0.7559.133</li>
<li>Microsoft Edge 144.0.3719.115</li>
</ul>
</li>
<li>
<div>Windows</div>
<ul>
<li>Google Chrome for Testing {{ EXPECT_CHROMIUM_BUILD_ID }}</li>
<li>Google Chrome 144.0.7559.133</li>
<li>Microsoft Edge 144.0.3719.93</li>
<li>Opera 127.0.5778.14基于 Chromium 143.0.7499.194</li>
<li>Yandex Browser 25.12.3.1126 基于 Chromium 142.0.7444.1126</li>
</ul>
</li>
<li>
<div>Linux (Ubuntu)</div>
<ul>
<li>Google Chrome for Testing {{ EXPECT_CHROMIUM_BUILD_ID }}</li>
</ul>
</li>
</ul>
<div>
下列浏览器可以使用但由于Chromium内核版本低于本程序设置的版本可能存在潜在问题不一定能完全支持所有功能你可以尝试进行配置
</div>
<ul>
<li>
<div>Windows</div>
<ul>
<li>360 安全浏览器 16.1.2552.64 基于 Chromium 132.0.6834.83</li>
<li>360 极速浏览器X 23.1.1187.64 基于 Chromium 132.0.6805.0</li>
<li>夸克 6.4.0.728 基于 Chromium 130.0.6723.44</li>
</ul>
</li>
</ul>
<div>下列浏览器经过测试已明确不可用</div>
<ul>
<li>
<div>Windows</div>
<ul>
<li>QQ 浏览器 20.1.0 基于 Chromium 116.0.5845.97</li>
<li>搜狗高速浏览器 13.8 基于 Chromium 116.0.5845.97</li>
<li>猎豹浏览器基于 Chromium 112.0.5615.138</li>
<li>Brave 1.86.148 基于 Chromium 144.0.7559.133</li>
</ul>
</li>
</ul>
<div>
本程序目前仅支持 Chromium 内核浏览器因此 SafariFirefoxWindows Internet
Explorer旧版 Microsoft Edge <span color-orange
>非Chromium内核浏览器无法使用</span
>同时对于 Chromium 内核浏览器本程序<span color-orange
>仅支持
{{ EXPECT_CHROMIUM_BUILD_ID }}
或更高内核版本</span
>你可以打开在你想要尝试的浏览器访问 chrome://version” 找到 “用户代理” /
User Agent 行来查看当前浏览器的内核版本<br />
目前大部分中国大陆厂商发布的基于Chromium内核的浏览器均由于版本过低不被本程序支持
</div>
</div>
</details>
<details
class="faq-item"
data-faq-id="what-will-happen-when-select-unsupported-browser"
>
<summary>如果我选择了一个不支持的浏览器会发生什么</summary>
<div class="faq-answer">
<div>可能会发生的情况</div>
<ul>
<li>浏览器将会启动但会开启一个空白页面之后浏览器不会做任何事</li>
<li>浏览器启动失败你不会看到任何界面</li>
<li>浏览器会每隔一段时间打开一个新的浏览器窗口</li>
</ul>
</div>
</details>
<details class="faq-item" data-faq-id="what-will-happen-when-select-other-file">
<summary>如果我选择了一个不是浏览器的可执行文件会发生什么</summary>
<div class="faq-answer">
<div>可能会发生的情况</div>
<ul>
<li>可执行文件将会启动但不会受到本程序控制</li>
<li>可执行文件闪退并报错闪退后会自动重启继续报错</li>
<li>可执行文件会不断运行多个实例</li>
</ul>
</div>
</details>
</div>
</div>
</div>
</el-form>
</div>
<div class="bg-#f8f8f8 pb10px pt10px w-full flex-0">
<div
:style="{
display: 'flex',
justifyContent: 'end',
width: '90%',
margin: '0 auto',
paddingLeft: '',
paddingRight: ''
}"
>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSave">确定</el-button>
</div>
</div>
</div>
<!-- <div class="h60px mt14px">
<RouterView
class="h100%"
:dependencies-status="checkDependenciesResult"
:process-waitee="downloadProcessWaitee"
></RouterView>
</div> -->
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { nextTick, onMounted, ref } from 'vue'
import debounce from 'lodash/debounce'
import { ElMessage } from 'element-plus'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import { EXPECT_CHROMIUM_BUILD_ID } from '../../../../common/constant'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
const { ipcRenderer } = electron
useRouter()
// const checkDependenciesResult = ref({})
// const downloadProcessWaitee = ref(null)
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'browser-assistant',
...params
})
}
const handleOpenChromeDownloadPage = debounce(
async () => {
gtagRenderer('open_chrome_download_page_clicked')
ipcRenderer.send('open-external-link', 'https://www.google.cn/chrome/')
},
1000,
{ leading: true, trailing: false }
)
const formData = ref({
browserPath: ''
})
const rules = {
browserPath: {
validator: async (_, value, callback) => {
if (!value?.trim()) {
callback(new Error('请输入浏览器可执行文件路径'))
return
}
const err = await ipcRenderer.invoke('check-executable-file', value)
if (err) {
callback(err?.message ?? '文件无效 - 未知原因')
} else {
callback()
}
},
trigger: 'blur'
}
}
const isAutoDetectLoading = ref(false)
async function autoDetectPuppeteerExecutable() {
gtagRenderer('auto_detect_pptr_exe_clicked')
isAutoDetectLoading.value = true
await sleep(50)
try {
const result = await ipcRenderer.invoke('get-any-available-puppeteer-executable', {
ignoreCached: true,
noSave: true
})
if (!result) {
gtagRenderer('auto_detect_pptr_exe_not_found')
ElMessage({
message: '未检测到可用浏览器的可执行文件',
type: 'warning',
grouping: true
})
return
}
gtagRenderer('auto_detect_pptr_exe_done', {
isUseCached: !!(
result.executablePath?.includes(`cache`) && result.executablePath?.includes(`.geekgeekrun`)
),
executableName: result.executablePath?.split(/\/|\\/).pop() ?? ''
})
formData.value.browserPath = result.executablePath
ElMessage({
message: '已找到可用浏览器,可执行文件路径已填入输入框',
type: 'success',
grouping: true
})
await nextTick()
await formRef.value.validateField()
} finally {
isAutoDetectLoading.value = false
}
}
async function chooseExecutableFile() {
gtagRenderer('choose_pptr_exe_clicked')
const chooseResult = await ipcRenderer.invoke('choose-file', {
fileChooserConfig: {
properties: ['openFile', 'treatPackageAsDirectory'],
filters: [
{
name: '可执行文件',
extensions: (await ipcRenderer.invoke('get-os-platform')) === 'win32' ? ['exe'] : ['']
},
{ name: '所有文件', extensions: ['*'] }
]
}
})
if (chooseResult.canceled || !chooseResult.filePaths?.length) {
gtagRenderer('choose_pptr_exe_cancelled')
return
}
formData.value.browserPath = chooseResult.filePaths[0]
gtagRenderer('choose_pptr_exe_done', {
executableName: chooseResult.filePaths[0]?.split(/\/|\\/).pop() ?? ''
})
await nextTick()
await formRef.value.validateField()
}
ipcRenderer.invoke('get-last-used-and-available-browser').then((res) => {
formData.value.browserPath = res?.executablePath ?? ''
})
function handleCancel() {
gtagRenderer('cancel_clicked')
window.close()
}
const formRef = ref()
async function handleSave() {
gtagRenderer('save_clicked', {
executablePath: formData.value.browserPath,
executableName: formData.value.browserPath?.split(/\/|\\/).pop() ?? ''
})
try {
await formRef.value.validate()
await ipcRenderer.invoke('save-last-used-and-available-browser-info', {
executablePath: formData.value.browserPath,
browser: ''
})
await ipcRenderer.send('browser-config-saved')
gtagRenderer('save_done', {
executablePath: formData.value.browserPath,
executableName: formData.value.browserPath?.split(/\/|\\/).pop() ?? ''
})
} catch (err) {
gtagRenderer('save_validate_failed', {
error: err?.message ?? '',
executablePath: formData.value.browserPath,
executableName: formData.value.browserPath?.split(/\/|\\/).pop() ?? ''
})
}
}
const handleFeedbackClick = () => {
gtagRenderer('goto_feedback_for_ba_clicked')
electron.ipcRenderer.send('send-feed-back-to-github-issue')
}
const handleClickLaunchBrowserDownloader = async () => {
gtagRenderer('launch_browser_downloader_clicked')
let downloadedBrowserPath
try {
downloadedBrowserPath = await electron.ipcRenderer.invoke('download-browser-with-downloader')
if (downloadedBrowserPath) {
formData.value.browserPath = downloadedBrowserPath
ElMessage({
message: '浏览器下载成功,可执行文件路径已填入输入框',
type: 'success',
grouping: true
})
gtagRenderer('browser_downloader_done_with_path')
} else {
ElMessage({
message:
'浏览器下载成功,但未返回可执行文件路径。请点击自动检测,或手动选择~/.geekgeekrun/cache/chrome文件夹下的文件,或重新下载',
type: 'success',
grouping: true
})
gtagRenderer('browser_downloader_done_without_path')
}
} catch (err) {
gtagRenderer('browser_downloader_cancelled')
}
}
const faqMainRef = ref()
onMounted(() => {
const faqItemEls = faqMainRef?.value?.querySelectorAll(`details`) ?? []
for (const el of faqItemEls) {
el.addEventListener('toggle', () => {
const isOpen = el.open
gtagRenderer('faq_item_toggled', {
faqId: el.dataset.faqId,
isOpen
})
})
}
})
</script>
<style lang="scss" scoped>
a:link,
a:visited,
a:hover,
a:active {
color: #409eff;
}
.faq-main {
.faq-item {
summary {
padding: 4px 0;
}
.faq-answer {
color: #666;
margin-left: 12px;
ul {
margin-top: 0;
margin-bottom: 0;
padding-left: 1em;
}
}
}
}
</style>
@@ -0,0 +1,63 @@
<template>
<div class="h-screen of-hidden flex flex-col flex-items-center flex-justify-center">
<div>
<div>
由于您是首次使用本程序或者您之前配置的浏览器被卸载/被删除/被移动/被更新/版本太旧因此需要重新配置浏览器
</div>
<div>
首先将尝试自动配置自动配置成功后本对话框将自动关闭如自动配置失败请在下个页面中手动配置
</div>
<div>正在尝试自动配置请稍等...</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
const { ipcRenderer } = electron
const router = useRouter()
// const checkDependenciesResult = ref({})
// const downloadProcessWaitee = ref(null)
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'browser-auto-find',
...params
})
}
async function autoDetectPuppeteerExecutable() {
const result = await ipcRenderer.invoke('get-any-available-puppeteer-executable')
if (!result) {
gtagRenderer('first-run-auto-detect-pptr-exe-fail')
ElMessage({
message: '未找到可用浏览器的可执行文件,请尝试手动配置',
type: 'warning',
grouping: true
})
router.replace({
path: '/browserAssistant',
query: {
firstRun: 1
}
})
return
}
gtagRenderer('first-run-auto-detect-pptr-exe-success')
await ipcRenderer.send('browser-config-saved')
}
autoDetectPuppeteerExecutable()
</script>
<style lang="scss" scoped>
a:link,
a:visited,
a:hover,
a:active {
color: #409eff;
}
</style>
@@ -1,7 +1,7 @@
<template>
<div>
<p>核心组件下载失败请重试</p>
<br />
<p>浏览器下载失败请重试或使用其他方式下载</p>
<!-- <br />
<br />
<p>
<b text-orange>提示</b>由于网络颠簸如果多次重试仍然失败&nbsp;<el-button
@@ -12,23 +12,23 @@
>点击此处</el-button
>&nbsp;下载最新版本 Google Chrome
浏览器安装完毕后重新打开本程序程序会自动检测该浏览器并使用它
</p>
</p> -->
</div>
</template>
<script lang="ts" setup>
import { gtagRenderer } from '@renderer/utils/gtag'
import debounce from 'lodash-es/debounce'
const { ipcRenderer } = electron
// import { gtagRenderer } from '@renderer/utils/gtag'
// import debounce from 'lodash/debounce'
// const { ipcRenderer } = electron
const handleOpenChromeDownloadPage = debounce(
async () => {
gtagRenderer('open_chrome_download_page_clicked')
ipcRenderer.send('open-external-link', 'https://www.google.cn/chrome/')
},
1000,
{ leading: true, trailing: false }
)
// const handleOpenChromeDownloadPage = debounce(
// async () => {
// gtagRenderer('open_chrome_download_page_clicked')
// ipcRenderer.send('open-external-link', 'https://www.google.cn/chrome/')
// },
// 1000,
// { leading: true, trailing: false }
// )
</script>
<style scoped></style>
@@ -0,0 +1,100 @@
<template>
<!-- flex-items- -->
<div class="flex flex-col flex-justify-center w500px h-full ml-auto mr-auto">
<div font-size-14px>正在下载 Google Chrome for Testing {{ EXPECT_CHROMIUM_BUILD_ID }}</div>
<el-progress
:percentage="browserDownloadPercentage"
:format="(n) => `${n.toFixed(1)}%`"
:stroke-width="10"
class="w500px"
mt10px
/>
<div mt10px>
<el-button @click="handleCancelDownload">取消下载</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onUnmounted, h } from 'vue'
import { ElMessageBox } from 'element-plus'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import FailMessage from './FailMessage.vue'
import { EXPECT_CHROMIUM_BUILD_ID } from '../../../../common/constant'
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'browser-download-progress',
...params
})
}
const browserDownloadPercentage = ref(0)
const handleBrowserDownloadProgress = (ev, { downloadedBytes, totalBytes }) => {
browserDownloadPercentage.value = (downloadedBytes / totalBytes) * 100
}
electron.ipcRenderer.on('PUPPETEER_DOWNLOAD_PROGRESS', handleBrowserDownloadProgress)
onUnmounted(() =>
electron.ipcRenderer.removeListener('PUPPETEER_DOWNLOAD_PROGRESS', handleBrowserDownloadProgress)
)
const downloadProcessExitCode = ref(0)
let executablePath
const processDownloadBrowser = async () => {
downloadProcessExitCode.value = 0
browserDownloadPercentage.value = 0
let restRetriedTime = 2
while (restRetriedTime > 0) {
try {
try {
executablePath = await electron.ipcRenderer.invoke('setup-dependencies')
browserDownloadPercentage.value = 100
} catch (err) {
downloadProcessExitCode.value = 1
throw err
}
break
} catch (err) {
restRetriedTime--
if (restRetriedTime === 0) {
throw err
}
await sleep(5000)
}
}
}
const processTasks = async () => {
try {
await processDownloadBrowser()
electron.ipcRenderer.send('browser-download-done', executablePath)
gtagRenderer('download_deps_done')
} catch (err) {
gtagRenderer('encounter_error_when_download_deps')
await ElMessageBox.confirm(h(FailMessage), {
closeOnClickModal: false,
closeOnPressEscape: false,
showClose: false,
type: 'error',
cancelButtonText: '取消',
confirmButtonText: '重试'
})
.then(() => {
gtagRenderer('start_retry_download_deps')
processTasks()
})
.catch(() => {
gtagRenderer('cancel_download_deps_from_err_dialog')
window.close()
})
}
}
processTasks()
function handleCancelDownload() {
gtagRenderer('cancel_download_deps_from_cancel_btn')
window.close()
}
</script>
@@ -1,16 +1,11 @@
<template>
<div class="cookie-assistant-page">
<div ml1em mt1em mb1em font-size-16px>Boss 登录助手</div>
<el-alert
v-if="cookieInvalid"
type="warning"
:closable="false"
title="需要获取您的Boss直聘Cookie才能继续"
>
由于您是首次使用本程序或者您之前使用的Boss直聘账号登录状态失效因此您需要重新获取登录凭证
<div ml1em mt1em mb1em font-size-16px>BOSS登录助手</div>
<el-alert v-if="cookieInvalid" type="warning" :closable="false">
由于您是首次使用本程序或者您之前使用的BOSS直聘账号登录状态失效因此您需要重新获取登录凭证
</el-alert>
<div ml1em mt1em line-height-normal>
如果您了解如何获取Cookie了解有效的Cookie格式可以直接在下方输入框中进行编辑由于手动编辑较为麻烦建议您打开已登录过Boss直聘的浏览器使用<a
如果您了解如何获取Cookie了解有效的Cookie格式可以直接在下方输入框中进行编辑由于手动编辑较为麻烦建议您打开已登录过BOSS直聘的浏览器使用<a
class="color-blue! decoration-none"
href="javascript:void(0)"
@click.prevent="handleEditThisCookieExtensionStoreLinkClick"
@@ -29,14 +24,14 @@
>
启动浏览器
</li>
<li>按照正常流程通过 <b>短信验证码/二维码/微信小程序</b> 登录您的Boss直聘账号</li>
<li>按照正常流程通过 <b>短信验证码/二维码/微信小程序</b> 登录您的BOSS直聘账号</li>
<li>接下来将自动进行一些页面跳转最终将会停留在首页</li>
<li>
登录后预计5-10秒内具体取决于您的网速您的Cookie将被自动填入下方输入框
<details>
<summary color-orange cursor-pointer>我已完成登录但Cookie一直没出现</summary>
<div ml-2em max-h-200px of-auto>
如果您确实已经在打开浏览器中看到您已登录了Boss直聘请尝试按照如图所示方式复制Cookie
如果您确实已经在打开浏览器中看到您已登录了BOSS直聘请尝试按照如图所示方式复制Cookie
<figure>
<figcaption>依次点击浏览器右上角扩展程序图标EditThisCookie图标</figcaption>
<img block max-w-full src="./resources/copy-cookie-step-1.png" />
@@ -98,7 +93,7 @@
</el-form-item>
</el-form>
<footer flex mt20px pb20px flex-justify-end>
<el-button v-if="!cookieInvalid" @click="handleCancel">取消</el-button>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</footer>
</div>
@@ -201,7 +196,7 @@ const handleEditThisCookieExtensionStoreLinkClick = () => {
const handleCancel = () => {
gtagRenderer('cancel_clicked')
router.replace('/main-layout')
window.close()
}
const handleSubmit = async () => {
gtagRenderer('save_clicked')
@@ -210,9 +205,10 @@ const handleSubmit = async () => {
fileName: 'boss-cookies.json',
data: formContent.value.collectedCookies
})
ElMessage.success('Boss直聘 Cookie 保存成功')
ElMessage.success('BOSS直聘 Cookie 保存成功')
gtagRenderer('save_cookie_done')
router.replace('/main-layout')
window.electron.ipcRenderer.send('cookie-saved')
}
const handleBossZhipinLoginPageClosed = () => {
@@ -22,8 +22,8 @@
gtagRenderer('view_boss_agreement_clicked')
}
"
>Boss直聘用户协议</el-link
>2023年3月版相关条款相违背您在注册Boss直聘时已签署过这一条款根据该条款
>BOSS直聘用户协议</el-link
>2023年3月版相关条款相违背您在注册BOSS直聘时已签署过这一条款根据该条款
<i>用户的平台使用义务</i><i>违约责任</i>
章节如果一些非正常用户行为被风控监测到您需要承受包括不仅限于<b class="color-red"
>账号被强制退出登录账号被限制使用账号被封禁</b
@@ -31,10 +31,10 @@
>且如果相关风险发生<b class="color-red">您需要自行承担相关后果本程序不负责</b>
</ElCheckbox>
<ElCheckbox :label="1" :class="[unreadItemsAfterClickSubmit[1] ? 'unread' : '']">
本程序需要存储您的登录凭据即Cookie来模拟您在Boss直聘上开聊Boss的行为本程序仅会把您的Cookie存储在本地并在您访问Boss直聘时将其传输到Boss直聘<b
本程序需要存储您的登录凭据即Cookie来模拟您在BOSS直聘上开聊BOSS的行为本程序仅会把您的Cookie存储在本地并在您访问BOSS直聘时将其传输到BOSS直聘<b
class="color-red"
>不会泄露给第三方</b
>也不会进行除自动开聊Boss以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b
>也不会进行除自动开聊BOSS以外的行为<b class="color-red">请勿向他人泄漏您的Cookie</b
>
</ElCheckbox>
<ElCheckbox :label="2" :class="[unreadItemsAfterClickSubmit[2] ? 'unread' : '']">
@@ -42,11 +42,11 @@
class="color-red"
>注意节制</b
>建议当天开聊次数用尽后隔几天再使用建议您<b class="color-red"
>注册一个本程序专用的新的Boss直聘账号</b
>注册一个本程序专用的新的BOSS直聘账号</b
>进行求职
</ElCheckbox>
<ElCheckbox :label="3" :class="[unreadItemsAfterClickSubmit[3] ? 'unread' : '']">
本程序原理是模拟用户在Boss直聘网页上寻找关键元素并进行点击操作Boss直聘网站经常<b>发生改版</b>且有可能<b>包含A/B实验</b>这将导致本程序相关脚本失效典型表现为本程序运行到某一步骤后<b
本程序原理是模拟用户在BOSS直聘网页上寻找关键元素并进行点击操作BOSS直聘网站经常<b>发生改版</b>且有可能<b>包含A/B实验</b>这将导致本程序相关脚本失效典型表现为本程序运行到某一步骤后<b
class="color-red"
>浏览器重复闪退重新启动</b
>如果您在使用过程中遇上程序未按照预期执行的情况请点击程序左下角进行反馈
@@ -81,16 +81,16 @@
</ElCheckbox>
<ElCheckbox :label="7" :class="[unreadItemsAfterClickSubmit[7] ? 'unread' : '']">
本程序<b class="color-red">不对您的求职过程与结果负责</b
>为您开聊的职位均在Boss直聘上发布职位信息真实性由Boss直聘负责<b
>为您开聊的职位均在BOSS直聘上发布职位信息真实性由BOSS直聘负责<b
class="color-red"
>自行甄别为您开聊的公司认真决定是否参加面试慎重选择Offer</b
>
</ElCheckbox>
<ElCheckbox :label="8" :class="[unreadItemsAfterClickSubmit[8] ? 'unread' : '']">
请在Boss直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b
>如果您不希望您当前公司其它具有招聘账号的员工看到您在Boss直聘上活跃<b
请在BOSS直聘上自行<b class="color-red">屏蔽您不期望投递的公司</b
>如果您不希望您当前公司其它具有招聘账号的员工看到您在BOSS直聘上活跃<b
class="color-red"
>在Boss直聘上屏蔽当前公司及与之关联的公司</b
>在BOSS直聘上屏蔽当前公司及与之关联的公司</b
>
</ElCheckbox>
<ElCheckbox :label="9" :class="[unreadItemsAfterClickSubmit[9] ? 'unread' : '']">
@@ -129,7 +129,7 @@ const readmeItemCheckStatusList = ref<number[]>([])
const handleCancel = async () => {
gtagRenderer('cancel_clicked')
await sleep(500)
electron.ipcRenderer.invoke('exit-app-immediately')
window.close()
}
const unreadItemsAfterClickSubmit = ref<Record<number, true>>({})
@@ -150,7 +150,7 @@ const handleSubmit = () => {
}
return
}
electron.ipcRenderer.invoke('first-launch-notice-approve')
electron.ipcRenderer.send('first-launch-notice-approve')
gtagRenderer('submit_done')
}
const handleReadmeItemCheckStatusListChange = (value: number[]) => {
@@ -3,7 +3,7 @@
<FlyingCompanyLogoList class="flying-company-logo-list" />
<div class="tip">
<article>
<h1>👋 BOSS炸弹正在运行</h1>
<h1>👋 自动开聊正在运行</h1>
<p>💬 正在为你开聊BOSS请静候佳音</p>
<p>📱 你可以在<b>手机</b> / <b>平板电脑</b>使用BOSS直聘App与为你开聊的BOSS聊天</p>
<p>🍀 祝你求职顺利</p>
@@ -1,9 +1,9 @@
<template>
<div class="geek-auto-start-chat-with-boss__running-status">
<div class="read-no-reply-auto-reminder__running-status">
<FlyingCompanyLogoList class="flying-company-logo-list" />
<div class="tip">
<article>
<h1>👋 已读不回提醒器正在运行</h1>
<h1>👋 已读不回自动复聊正在运行</h1>
<p>🍀 祝你求职顺利</p>
</article>
<el-button :disabled="isStopping" @click="handleStopButtonClick">停止开聊</el-button>
@@ -23,7 +23,7 @@ const router = useRouter()
const handleStopButtonClick = async () => {
gtagRenderer('rnrr_stop_button_clicked')
ipcRenderer.invoke('stop-geek-auto-start-chat-with-boss')
ipcRenderer.invoke('stop-read-no-reply-auto-reminder')
}
const isStopping = ref(false)
@@ -31,23 +31,24 @@ const handleStopping = () => {
gtagRenderer('rnrr_become_stopping')
isStopping.value = true
}
ipcRenderer.once('geek-auto-start-chat-with-boss-stopping', handleStopping)
ipcRenderer.once('read-no-reply-auto-reminder-stopping', handleStopping)
const handleStopped = () => {
gtagRenderer('rnrr_become_stopped')
router.replace('/main-layout/ReadNoReplyReminder')
}
ipcRenderer.once('geek-auto-start-chat-with-boss-stopped', handleStopped)
ipcRenderer.once('read-no-reply-auto-reminder-stopped', handleStopped)
onUnmounted(() => {
ipcRenderer.removeListener('geek-auto-start-chat-with-boss-stopped', handleStopped)
ipcRenderer.removeListener('geek-auto-start-chat-with-boss-stopping', handleStopping)
ipcRenderer.removeListener('read-no-reply-auto-reminder-stopped', handleStopped)
ipcRenderer.removeListener('read-no-reply-auto-reminder-stopping', handleStopping)
})
onMounted(async () => {
try {
await electron.ipcRenderer.invoke('run-read-no-reply-auto-reminder')
} catch (err) {
}
catch (err) {
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
gtagRenderer('rnrr_cannot_run_for_corrupt')
ElMessage.error({
@@ -62,7 +63,7 @@ onMounted(async () => {
</script>
<style scoped lang="scss">
.geek-auto-start-chat-with-boss__running-status {
.read-no-reply-auto-reminder__running-status {
width: 100%;
height: 100%;
overflow: hidden;
@@ -1,54 +1,29 @@
<template><RouterView :status="currentStatus" /></template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
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'
switch (route.query.flow) {
case 'geek-auto-start-chat-with-boss': {
router.replace({
path: '/geekAutoStartChatWithBoss/runningStatus'
})
break
}
case 'read-no-reply-reminder': {
router.replace({
path: '/geekAutoStartChatWithBoss/runningStatusForReadNoReplyReminder'
})
break
}
default: {
router.replace('/')
}
}
electron.ipcRenderer.once('locating-puppeteer-executable', handleLocatingPuppeteerExecutable)
onUnmounted(() => {
electron.ipcRenderer.removeListener(
'locating-puppeteer-executable',
handleLocatingPuppeteerExecutable
)
})
promise
.then(() => {
switch (route.query.flow) {
case 'geek-auto-start-chat-with-boss': {
router.replace({
path: '/geekAutoStartChatWithBoss/runningStatus'
})
break
}
case 'read-no-reply-reminder': {
router.replace({
path: '/geekAutoStartChatWithBoss/runningStatusForReadNoReplyReminder'
})
break
}
default: {
router.replace('/')
}
}
})
.catch(async (err) => {
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
ElMessage.error({
message: `核心组件损坏,正在尝试修复`
})
router.replace('/')
}
console.error(err)
})
})
</script>
File diff suppressed because it is too large Load Diff
@@ -50,6 +50,10 @@
<strong>{{ markReasonTopicMap[row.markReason] }}</strong>
<pre class="m-0 of-auto">{{ formatMarkReason(row) }}</pre>
</template>
<template v-if="row.markReason === MarkAsNotSuitReason.COMPANY_NAME_NOT_SUIT">
<strong>{{ markReasonTopicMap[row.markReason] }}</strong>
<pre class="m-0 of-auto">{{ formatMarkReason(row) }}</pre>
</template>
</template>
</ElTableColumn>
<ElTableColumn prop="experienceName" label="工作经验" />
@@ -214,12 +218,13 @@ function handleViewJobSnapshotButtonClick(record: VMarkAsNotSuitLog) {
}
const markReasonTopicMap = {
[MarkAsNotSuitReason.BOSS_INACTIVE]: 'Boss不活跃',
[MarkAsNotSuitReason.BOSS_INACTIVE]: 'BOSS不活跃',
[MarkAsNotSuitReason.USER_MANUAL_OPERATION_WITH_UNKNOWN_REASON]: '手动标记不合适',
[MarkAsNotSuitReason.JOB_NOT_SUIT]: '职位不合适',
[MarkAsNotSuitReason.JOB_CITY_NOT_SUIT]: '工作地不合适',
[MarkAsNotSuitReason.JOB_WORK_EXP_NOT_SUIT]: '工作经验不合适',
[MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT]: '薪资不合适'
[MarkAsNotSuitReason.JOB_SALARY_NOT_SUIT]: '薪资不合适',
[MarkAsNotSuitReason.COMPANY_NAME_NOT_SUIT]: '公司名称不匹配'
}
function formatMarkReason(row: VMarkAsNotSuitLog) {
@@ -233,8 +238,8 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
}
})()
return [
extInfo?.bossActiveTimeDesc && `Boss活跃情况:${extInfo.bossActiveTimeDesc}`,
extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`
extInfo?.bossActiveTimeDesc && `BOSS活跃情况:${extInfo.bossActiveTimeDesc}`,
extInfo?.chosenReasonInUi?.text && `BOSS选项内容:${extInfo.chosenReasonInUi.text}`
]
.filter(Boolean)
.join('\n')
@@ -247,7 +252,7 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
return null
}
})()
return [extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`]
return [extInfo?.chosenReasonInUi?.text && `BOSS选项内容:${extInfo.chosenReasonInUi.text}`]
.filter(Boolean)
.join('\n')
}
@@ -260,7 +265,7 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
return null
}
})()
return [extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`]
return [extInfo?.chosenReasonInUi?.text && `BOSS选项内容:${extInfo.chosenReasonInUi.text}`]
.filter(Boolean)
.join('\n')
}
@@ -274,11 +279,23 @@ function formatMarkReason(row: VMarkAsNotSuitLog) {
})()
return [
extInfo?.salaryDesc && `薪资:${extInfo.salaryDesc}`,
extInfo?.chosenReasonInUi?.text && `Boss选项内容:${extInfo.chosenReasonInUi.text}`
extInfo?.chosenReasonInUi?.text && `BOSS选项内容:${extInfo.chosenReasonInUi.text}`
]
.filter(Boolean)
.join('\n')
}
case MarkAsNotSuitReason.COMPANY_NAME_NOT_SUIT: {
const extInfo = (() => {
try {
return JSON.parse(row.extInfo)
} catch {
return null
}
})()
return [extInfo?.chosenReasonInUi?.text && `BOSS选项内容:${extInfo.chosenReasonInUi.text}`]
.filter(Boolean)
.join('\n')
}
default: {
return ''
}
@@ -1,225 +1,248 @@
<template>
<div class="form-wrap">
<el-form
ref="formRef"
:rules="formRules"
:model="formContent.autoReminder"
label-position="top"
>
<el-form-item label="BOSS直聘 Cookie">
<el-button size="small" type="primary" @click="handleClickLaunchLogin"
>编辑Cookie</el-button
>
</el-form-item>
<el-form-item>
<div>
<el-checkbox v-if="!expectJobTypeRegExpStr?.trim()" :model-value="false" disabled>
发送提醒消息前先按照Boss炸弹-职位类型正则校验正在与Boss沟通的岗位是否满足期望校验通过后再提醒
</el-checkbox>
<template v-else>
<el-checkbox v-model="formContent.autoReminder.onlyRemindBossWithExpectJobType">
发送提醒消息前先按照Boss炸弹-职位类型正则校验正在与Boss沟通的岗位是否满足期望校验通过后再提醒
</el-checkbox>
<div ml1.5em color-gray>
<div>当前职位类型正则{{ expectJobTypeRegExpStr?.trim() }}</div>
<template
v-if="
formContent.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
>
<div>当前简历中填写的期望职位:{{ resumeContent?.expectJob ?? '-' }}</div>
<div color-orange>请确保上方二者信息匹配</div>
</template>
</div>
</template>
</div>
</el-form-item>
<el-form-item class="mb0" label="跟进话术 - 当发现已读不回的Boss时,将要向Boss发出:">
<el-radio-group v-model="formContent.autoReminder.rechatContentSource">
<div class="read-no-reply-reminder__wrap">
<div class="form-wrap">
<el-form
ref="formRef"
:rules="formRules"
:model="formContent.autoReminder"
label-position="top"
>
<el-form-item>
<div>
<el-tooltip
effect="light"
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_about_lfr_emotion_figure')"
>
<template #content>
<img block h-100px src="./resources/look-forward-reply-emotion.gif" />
</template>
<el-radio :label="RECHAT_CONTENT_SOURCE.LOOK_FORWARD_EMOTION">
“[盼回复]” 表情
</el-radio>
</el-tooltip>
<br />
<el-radio :label="RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT">
由大语言模型(根据简历及当前聊天上下文)生成的内容
</el-radio>
</div>
</el-radio-group>
</el-form-item>
<div class="ml-30px">
<template
v-if="
formContent.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
>
<el-form-item class="mb4px">
<div>
<el-button size="small" type="primary" @click="handleClickConfigLlm">
配置大语言模型
</el-button>
<div class="font-size-12px color-#666">
支持
<span
class="pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
style="background-color: #3c4efd"
>DeepSeek-V3</span
>
<span
class="ml4px pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
style="background-color: #000000"
>GPT-4o mini</span
>
<span
class="ml4px pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
style="background-color: #462ac4"
>Qwen2.5</span
>
模型;支持多个“服务商-模型”组合按权重搭配使用
</div>
</div>
</el-form-item>
<el-form-item class="mb4px">
<div>
<el-button size="small" type="primary" @click="handleClickEditResume">
编辑简历
</el-button>
<div class="font-size-12px color-#666">
简历内容将提交给大语言模型,以用于生成已读不回提醒消息;提交内容及生成消息中不会包含期望薪资
</div>
</div>
</el-form-item>
<el-form-item class="mb4px">
<div>
<div>
<el-button size="small" type="primary" @click="handleClickEditPrompt">
使用外部编辑器编辑提示词模板 (Markdown)
</el-button>
<el-button
size="small"
type="primary"
@click="
() => {
gtagRenderer('reset_template_clicked_in_main_form')
restoreDefaultTemplate()
}
<el-checkbox v-if="!expectJobTypeRegExpStr?.trim()" :model-value="false" disabled>
发送提醒消息前先按照自动开聊-职位类型正则校验正在与BOSS沟通的岗位是否满足期望校验通过后再提醒
</el-checkbox>
<template v-else>
<el-checkbox v-model="formContent.autoReminder.onlyRemindBossWithExpectJobType">
发送提醒消息前先按照自动开聊-职位类型正则校验正在与BOSS沟通的岗位是否满足期望校验通过后再提醒
</el-checkbox>
<div ml1.5em color-gray>
<div>当前职位类型正则{{ expectJobTypeRegExpStr?.trim() }}</div>
<template
v-if="
formContent.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
>
还原默认提示词模板
</el-button>
<div>当前简历中填写的期望职位:{{ resumeContent?.expectJob ?? '-' }}</div>
<div color-orange>请确保上方二者信息匹配</div>
</template>
</div>
<div class="font-size-12px color-#666">
对生成效果不够满意?可在此查看、编辑提示词模板。请在模板中需要插入简历的位置插入
__REPLACE_REAL_RESUME_HERE__
</template>
</div>
</el-form-item>
<el-form-item>
<div>
<el-checkbox v-if="!blockCompanyNameRegExpStr?.trim()" :model-value="false" disabled>
发送提醒消息前,先按照“自动开聊-不期望投递公司正则”校验正在与BOSS沟通的岗位是否归属于不期望投递的公司,如果是,则不提醒
</el-checkbox>
<template v-else>
<el-checkbox v-model="formContent.autoReminder.onlyRemindBossWithoutBlockCompanyName">
发送提醒消息前,先按照“自动开聊-不期望投递公司正则”校验正在与BOSS沟通的岗位是否归属于不期望投递的公司,如果是,则不提醒
</el-checkbox>
<div ml1.5em color-gray>
<div>当前不期望投递公司正则:{{ blockCompanyNameRegExpStr?.trim() }}</div>
</div>
</div>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
</template>
</div>
</el-form-item>
<el-form-item class="mb0" label="跟进话术 - 当发现已读不回的BOSS时,将要向BOSS发出:">
<el-radio-group v-model="formContent.autoReminder.rechatContentSource">
<div>
携带最近
<el-input-number
v-model="formContent.autoReminder.recentMessageQuantityForLlm"
class="w-120px"
:min="8"
:max="20"
:precision="0"
:step="1"
></el-input-number>
次聊天内容作为上下文生成新消息
</div>
</el-form-item>
<el-form-item>
<el-button size="small" type="primary" @click="handleTestEffectClicked"
>使用当前配置模拟已读不回复聊过程</el-button
>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div class="flex flex-items-center">
<span class="whitespace-nowrap">当所有模型均不可使用时&nbsp;</span>
<el-select
v-model="formContent.autoReminder.rechatLlmFallback"
class="w200px"
label="name"
<el-tooltip
effect="light"
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_about_lfr_emotion_figure')"
>
<el-option
v-for="option in rechatLlmFallbackOptions"
:key="option.value"
:value="option.value"
:label="option.name"
/>
</el-select>
<template #content>
<img block h-100px src="./resources/look-forward-reply-emotion.gif" />
</template>
<el-radio :label="RECHAT_CONTENT_SOURCE.LOOK_FORWARD_EMOTION">
“[盼回复]” 表情
</el-radio>
</el-tooltip>
<br />
<el-radio :label="RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT">
由大语言模型(根据简历及当前聊天上下文)生成的内容
</el-radio>
</div>
</el-form-item>
</template>
</div>
<el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes">
<el-input-number
v-model="formContent.autoReminder.throttleIntervalMinutes"
class="w-150px"
:min="3"
:precision="1"
:step="0.5"
@blur="handleThrottleIntervalMinutesBlur"
/>&nbsp;分钟内不多次跟进同一Boss
</el-form-item>
<el-form-item label="跟进时限(天)" prop="rechatLimitDay" mb-0>
<div>
<div><el-checkbox v-model="enableRechatLimit" />&nbsp;启用</div>
</el-radio-group>
</el-form-item>
<div class="ml-30px">
<template
v-if="
formContent.autoReminder.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
"
>
<el-form-item class="mb4px">
<div>
<el-button size="small" type="primary" @click="handleClickEditResume">
编辑简历
</el-button>
<div class="font-size-12px color-#666">
简历内容将提交给大语言模型,以用于生成已读不回提醒消息;提交内容及生成消息中不会包含期望薪资
</div>
</div>
</el-form-item>
<el-form-item class="mb4px">
<div>
<div>
<el-button size="small" type="primary" @click="handleClickEditPrompt">
使用外部编辑器编辑提示词模板 (Markdown)
</el-button>
<el-button
size="small"
type="primary"
@click="
() => {
gtagRenderer('reset_template_clicked_in_main_form')
restoreDefaultTemplate()
}
"
>
还原默认提示词模板
</el-button>
</div>
<div class="font-size-12px color-#666">
对生成效果不够满意?可在此查看、编辑提示词模板。请在模板中需要插入简历的位置插入
__REPLACE_REAL_RESUME_HERE__
</div>
</div>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div>
携带最近
<el-input-number
v-model="formContent.autoReminder.recentMessageQuantityForLlm"
class="w-120px"
:min="8"
:max="20"
:precision="0"
:step="1"
></el-input-number>
次聊天内容作为上下文生成新消息
</div>
</el-form-item>
<el-form-item>
<el-button size="small" type="primary" @click="handleTestEffectClicked"
>使用当前配置模拟已读不回自动复聊过程</el-button
>
</el-form-item>
<el-form-item prop="recentMessageQuantityForLlm">
<div class="flex flex-items-center">
<span class="whitespace-nowrap">当所有模型均不可使用时&nbsp;</span>
<el-select
v-model="formContent.autoReminder.rechatLlmFallback"
class="w200px"
label="name"
>
<el-option
v-for="option in rechatLlmFallbackOptions"
:key="option.value"
:value="option.value"
:label="option.name"
/>
</el-select>
</div>
</el-form-item>
</template>
</div>
<el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes">
<el-input-number
v-model="formContent.autoReminder.rechatLimitDay"
v-model="formContent.autoReminder.throttleIntervalMinutes"
class="w-150px"
:min="0"
:min="3"
:precision="1"
:step="0.5"
:disabled="!enableRechatLimit"
/>&nbsp;天<br />
<div v-if="enableRechatLimit">
不再跟进&nbsp;<span class="text-orange">{{ rechatLimitDateString }}</span
>)之前列表中没有进展的聊天
@blur="handleThrottleIntervalMinutesBlur"
/>&nbsp;分钟内不多次跟进同一BOSS
</el-form-item>
<el-form-item label="跟进时限(天)" prop="rechatLimitDay" mb-0>
<div>
<div><el-checkbox v-model="enableRechatLimit" />&nbsp;启用</div>
<el-input-number
v-model="formContent.autoReminder.rechatLimitDay"
class="w-150px"
:min="0"
:precision="1"
:step="0.5"
:disabled="!enableRechatLimit"
/>&nbsp;天<br />
<div v-if="enableRechatLimit">
不再跟进&nbsp;<span class="text-orange">{{ rechatLimitDateString }}</span
>)之前列表中没有进展的聊天
</div>
<div v-else>这将会跟进列表中所有聊天(<span class="text-orange">不建议</span></div>
</div>
<div v-else>这将会跟进列表中所有聊天(<span class="text-orange">不建议</span></div>
</div>
</el-form-item>
<el-form-item>
<el-tooltip
effect="light"
placement="bottom-start"
@show="gtagRenderer('tooltip_show_about_stop_trace_one_boss')"
>
<template #content>
<ul m0 line-height-1.5em w-300px pl2em>
<li>
请向你不想继续提醒的Boss发送任意消息,发送后立即撤回的这条消息即可。
<br />
<br />
对于PC端Boss直聘,鼠标移动到要撤回的消息,点按鼠标右键调出菜单,再鼠标左键点击菜单中的“撤回”。如图所示:
<br />
<img block w-full src="./resources/withdraw-message-guide.png" />
</li>
</ul>
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span
>我不想持续提醒某个Boss了,如何处理?</el-button
</el-form-item>
<el-form-item>
<el-tooltip
effect="light"
placement="bottom-start"
@show="gtagRenderer('tooltip_show_about_stop_trace_one_boss')"
>
</el-tooltip>
</el-form-item>
<el-form-item class="last-form-item">
<el-button type="primary" @click="handleSubmit">开始提醒</el-button>
</el-form-item>
</el-form>
<template #content>
<ul m0 line-height-1.5em w-300px pl2em>
<li>
请向你不想继续提醒的Boss发送任意消息,发送后立即撤回的这条消息即可。
<br />
<br />
对于PC端BOSS直聘,鼠标移动到要撤回的消息,点按鼠标右键调出菜单,再鼠标左键点击菜单中的“撤回”。如图所示:
<br />
<img block w-full src="./resources/withdraw-message-guide.png" />
</li>
</ul>
</template>
<el-button type="text" font-size-12px
><span><QuestionFilled w-1em h-1em mr2px /></span
>我不想持续提醒某个BOSS了,如何处理?</el-button
>
</el-tooltip>
</el-form-item>
<el-form-item class="last-form-item" flex>
<el-button type="primary" @click="handleSubmit">开始提醒</el-button>
</el-form-item>
</el-form>
</div>
<div
class="running-overlay__wrap"
:style="{
pointerEvents: 'none'
}"
>
<RunningOverlay
ref="runningOverlayRef"
worker-id="readNoReplyAutoReminderMain"
:run-record-id="runRecordId"
>
<template #op-buttons="{ currentRunningStatus }">
<div>
<template v-if="currentRunningStatus === RUNNING_STATUS_ENUM.RUNNING">
<el-button
type="danger"
plain
:loading="isStopButtonLoading"
@click="handleStopButtonClick"
>结束任务</el-button
>
</template>
<template v-else>
<el-button
type="primary"
@click="
() => {
runningOverlayRef?.hide?.()
}
"
>关闭</el-button
>
</template>
</div>
</template>
</RunningOverlay>
</div>
</div>
</template>
@@ -229,11 +252,13 @@ import { dayjs, ElForm, ElMessage, ElMessageBox, ElSelect, ElOption } from 'elem
import { useRouter } from 'vue-router'
import {
RECHAT_CONTENT_SOURCE,
RECHAT_LLM_FALLBACK
RECHAT_LLM_FALLBACK,
RUNNING_STATUS_ENUM
} from '../../../../common/enums/auto-start-chat'
import { gtagRenderer as baseGtagRenderer } from '@renderer/utils/gtag'
import mittBus from '../../utils/mitt'
import { QuestionFilled } from '@element-plus/icons-vue'
import RunningOverlay from '@renderer/features/RunningOverlay/index.vue'
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'rnrr-config',
@@ -248,7 +273,8 @@ const formContent = ref({
rechatContentSource: 1,
recentMessageQuantityForLlm: 8,
rechatLlmFallback: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION,
onlyRemindBossWithExpectJobType: true
onlyRemindBossWithExpectJobType: true,
onlyRemindBossWithoutBlockCompanyName: true
}
})
@@ -286,15 +312,17 @@ electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
})
const expectJobTypeRegExpStr = ref('')
async function fetchExpectJobTypeRegExpStr() {
const blockCompanyNameRegExpStr = ref('')
async function fetchAutoStartChatConfig() {
await electron.ipcRenderer.invoke('fetch-config-file-content').then((res) => {
expectJobTypeRegExpStr.value = res.config['boss.json']?.expectJobTypeRegExpStr
blockCompanyNameRegExpStr.value = res.config['boss.json']?.blockCompanyNameRegExpStr
})
}
fetchExpectJobTypeRegExpStr()
mittBus.on('auto-start-chat-with-boss-config-saved', fetchExpectJobTypeRegExpStr)
fetchAutoStartChatConfig()
mittBus.on('auto-start-chat-with-boss-config-saved', fetchAutoStartChatConfig)
onUnmounted(() => {
mittBus.off('auto-start-chat-with-boss-config-saved', fetchExpectJobTypeRegExpStr)
mittBus.off('auto-start-chat-with-boss-config-saved', fetchAutoStartChatConfig)
})
const resumeContent = ref(null)
@@ -434,7 +462,8 @@ async function checkIsCanRun() {
return true
}
const runRecordId = ref(null)
const runningOverlayRef = ref(null)
const handleSubmit = async () => {
gtagRenderer('run_read_no_reply_reminder_clicked', {
throttle_interval_minutes: formContent.value.autoReminder.throttleIntervalMinutes,
@@ -471,10 +500,29 @@ const handleSubmit = async () => {
}
}
gtagRenderer('run_read_no_reply_reminder_launched')
router.replace({
path: '/geekAutoStartChatWithBoss/prepareRun',
query: { flow: 'read-no-reply-reminder' }
})
try {
runningOverlayRef.value?.show()
const { runRecordId: rrId } = await electron.ipcRenderer.invoke(
'run-read-no-reply-auto-reminder'
)
runRecordId.value = rrId
} catch (err) {
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
gtagRenderer('rnrr_cannot_run_for_corrupt')
ElMessage.error({
message: `核心组件损坏,正在尝试修复`
})
router.replace('/')
}
console.error(err)
gtagRenderer('rnrr_cannot_run_for_unknown_error', { err })
}
// {
// path: '/geekAutoStartChatWithBoss/prepareRun',
// query: { flow: 'read-no-reply-reminder' }
// }
}
function handleThrottleIntervalMinutesBlur() {
if (formContent.value.autoReminder.throttleIntervalMinutes < 3) {
@@ -493,11 +541,6 @@ const restoreDefaultTemplate = async () => {
})
}
const handleClickLaunchLogin = () => {
gtagRenderer('launch_login_clicked')
router.replace('/cookieAssistant')
}
const currentStamp = ref(new Date())
let timer = 0
function updateCurrentStamp() {
@@ -515,15 +558,6 @@ const rechatLimitDateString = computed(() => {
).format('YYYY-MM-DD HH:mm:ss')
})
const handleClickConfigLlm = async () => {
gtagRenderer('config_llm_clicked')
try {
await electron.ipcRenderer.invoke('llm-config')
} catch (err) {
console.log(err)
}
}
const handleClickEditResume = async () => {
gtagRenderer('edit_resume_clicked')
try {
@@ -545,7 +579,7 @@ const rechatLlmFallbackOptions = [
value: RECHAT_LLM_FALLBACK.SEND_LOOK_FORWARD_EMOTION
},
{
name: '退出已读不回提醒器',
name: '退出已读不回自动复聊',
value: RECHAT_LLM_FALLBACK.EXIT_REMINDER_PROGRAM
}
]
@@ -559,24 +593,54 @@ async function handleTestEffectClicked() {
autoReminderConfig: JSON.parse(JSON.stringify(formContent.value.autoReminder))
})
}
const needToCheckRuntimeDependenciesHandler = () => {
router.replace('/')
}
electron.ipcRenderer.on('need-to-check-runtime-dependencies', needToCheckRuntimeDependenciesHandler)
onUnmounted(() => {
electron.ipcRenderer.removeListener(
'need-to-check-runtime-dependencies',
needToCheckRuntimeDependenciesHandler
)
})
const isStopButtonLoading = ref(false)
const handleStopButtonClick = async () => {
gtagRenderer('rnrr_stop_button_clicked')
isStopButtonLoading.value = true
try {
electron.ipcRenderer.invoke('stop-read-no-reply-auto-reminder')
runningOverlayRef.value?.hide()
} finally {
isStopButtonLoading.value = false
}
}
</script>
<style scoped lang="scss">
.form-wrap {
max-height: 100vh;
overflow: auto;
padding-left: 20px;
padding-right: 20px;
:deep(.el-form) {
margin: 0 auto;
max-width: 1000px;
padding-top: 8px;
}
.last-form-item {
:deep(.el-form-item__content) {
margin-top: 0px;
justify-content: flex-end;
<style lang="scss">
.read-no-reply-reminder__wrap {
position: relative;
.form-wrap {
max-height: 100vh;
overflow: auto;
padding-left: 20px;
padding-right: 20px;
.el-form {
margin: 0 auto;
max-width: 1000px;
padding-top: 8px;
}
.last-form-item {
.el-form-item__content {
margin-top: 0px;
justify-content: flex-end;
}
}
}
.running-overlay__wrap {
position: absolute;
inset: 0;
}
}
</style>
@@ -0,0 +1,7 @@
<template>
<TaskManagerList />
</template>
<script lang="ts" setup>
import TaskManagerList from '../../features/TaskManager/List.vue'
</script>
@@ -1,99 +1,155 @@
<template>
<div class="flex h100vh">
<div class="flex flex-col min-w200px w200px pt30px pl30px aside-nav of-hidden">
<div class="nav-list flex-1 of-auto">
<RouterLink to="./GeekAutoStartChatWithBoss">
Boss炸弹
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_nav_boss_b_entry')"
>
<template #content>
<div w-480px>
<div>扩列神器按照你所设置的求职偏好自动开聊推荐职位列表中的匹配的Boss</div>
<br />
<div>匹配步骤</div>
<ol m0 pl2em>
<li>
按照公司名称查找职位查找到目标职位后自动点击这个职位右侧将会展示职位详情
</li>
<li>
检查Boss活跃度
<ul pl2em>
<div class="nav-list flex-1 of-auto pl20px ml--20px">
<RouterLink v-show="false" to="./TaskManager">任务管理</RouterLink>
<div class="group-item">
<div class="group-title">BOSS直聘</div>
<div flex flex-col class="link-list">
<RouterLink to="./GeekAutoStartChatWithBoss">
自动开聊
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_nav_boss_b_entry')"
>
<template #content>
<div w-480px>
<div>扩列神器按照你所设置的求职偏好自动开聊推荐职位列表中的匹配的BOSS</div>
<br />
<div>匹配步骤</div>
<ol m0 pl2em>
<li>
如果Boss活跃度为本月活跃或更往前的时间则会把职位标记为不合适一段时间内你将不会在Boss上看到这个职位且将会推荐新职位置换这个职位
按照公司名称查找职位查找到目标职位后自动点击这个职位右侧将会展示职位详情
</li>
<li>
检查BOSS活跃度
<ul pl2em>
<li>
如果BOSS活跃度为本月活跃或更往前的时间则会把职位标记为不合适一段时间内你将不会在BOSS上看到这个职位且将会推荐新职位置换这个职位
</li>
</ul>
</li>
<li>
对职位名称职位类型职位描述进行匹配
<ul pl2em>
<li>如果匹配则自动点击开聊按钮</li>
<li>
不匹配则标记这个职位为不合适一段时间内你将不会在BOSS上看到这个职位且将会推荐新职位置换这个职位
</li>
</ul>
</li>
</ol>
<br />
<div>异常情况</div>
<ol m0 pl2em>
<li>
当前页面筛选条件下如果没有更多职位则自动切换备选筛选条件以获取更多新职位
</li>
<li>
如当天开聊次数用完本程序会暂停运行60分钟之后尝试继续重新运行如重新运行时间已在第二天则将会继续开聊
</li>
</ol>
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
</RouterLink>
<RouterLink to="./ReadNoReplyReminder">
已读不回自动复聊
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_rnrr_entry')"
>
<template #content>
<div w-480px>
<div>
BOSS不明原因已读不回简历就是投不出去<br />
已读不回自动复聊提醒一下已读不回的 BOSS助力把握每次机会
</div>
<br />
<div>匹配逻辑</div>
<div>在聊天列表中查找对你消息已读不回的BOSS再发一条消息多次复聊同时</div>
<ul m0 pl2em>
<li>如果设置了跟进时限那么在这个时间之前活跃的聊天将不会被检查</li>
<li>
如果设置了跟进间隔且再次检查时发现BOSS已读不回且距离上次提醒时间间隔小于这个时间那么聊天将暂时不会跟进直到下次检查时距离上次提醒时间间隔大于这个时间
</li>
</ul>
</li>
<li>
对职位名称职位类型职位描述进行匹配
<ul pl2em>
<li>如果匹配则自动点击开聊按钮</li>
<li>
不匹配则标记这个职位为不合适一段时间内你将不会在Boss上看到这个职位且将会推荐新职位置换这个职位
</li>
<br />
<div>发送内容</div>
<ul m0 pl2em>
<li>[盼回复]表情</li>
<li>由大语言模型根据简历及当前聊天上下文生成的内容</li>
</ul>
</li>
</ol>
<br />
<div>异常情况</div>
<ol m0 pl2em>
<li>
当前页面筛选条件下如果没有更多职位则自动切换备选筛选条件以获取更多新职位
</li>
<li>
如当天开聊次数用完本程序会暂停运行60分钟之后尝试继续重新运行如重新运行时间已在第二天则将会继续开聊
</li>
</ol>
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
</RouterLink>
<a href="javascript:void(0)" @click="handleClickLaunchBossLogin">
编辑登录凭据<TopRight w-1em h-1em mr10px />
</a>
<a href="javascript:void(0)" @click="handleLaunchBossSite">
手动逛<TopRight w-1em h-1em mr10px />
</a>
</div>
</div>
<hr class="group-divider" />
<div class="group-item">
<div class="group-title">全局设置</div>
<div flex flex-col class="link-list">
<a href="javascript:void(0)" @click="handleClickBrowserSetting">
编辑浏览器偏好<TopRight w-1em h-1em mr10px />
</a>
<a href="javascript:void(0)" @click="handleClickConfigLlm">
配置大语言模型
<div>
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_rnrr_entry')"
>
<template #content>
<div class="font-size-12px">
支持
<span
class="pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
style="background-color: #3c4efd"
>DeepSeek-V3</span
>
<span
class="ml4px pl6px pr6px pt4px pb2px color-black border-rd-full font-size-0.8em"
style="background-color: #fff"
>GPT-4o mini</span
>
<span
class="ml4px pl6px pr6px pt4px pb2px color-white border-rd-full font-size-0.8em"
style="background-color: #462ac4"
>Qwen2.5</span
>
模型<br />支持多个服务商-模型组合按权重搭配使用
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
<TopRight w-1em h-1em mr10px />
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
</RouterLink>
<RouterLink to="./ReadNoReplyReminder">
已读不回提醒器
<el-tooltip
placement="right"
:enterable="false"
@show="gtagRenderer('tooltip_show_for_rnrr_entry')"
>
<template #content>
<div w-480px>
<div>
Boss不明原因已读不回简历就是投不出去<br />
已读不回提醒器有事没事提醒一下已读不回的 Ta助力把握每次机会
</div>
<br />
<div>匹配逻辑</div>
<div>在聊天列表中查找对你消息已读不回的Boss再发一条消息多次复聊同时</div>
<ul m0 pl2em>
<li>如果设置了跟进时限那么在这个时间之前活跃的聊天将不会被检查</li>
<li>
如果设置了跟进间隔且再次检查时发现Boss已读不回且距离上次提醒时间间隔小于这个时间那么聊天将暂时不会跟进直到下次检查时距离上次提醒时间间隔大于这个时间
</li>
</ul>
<br />
<div>发送内容</div>
<ul m0 pl2em>
<li>[盼回复]表情</li>
<li>由大语言模型根据简历及当前聊天上下文生成的内容</li>
</ul>
</div>
</template>
<QuestionFilled w-1em h-1em mr10px />
</el-tooltip>
</RouterLink>
<hr w180px />
<a href="javascript:void(0)" @click="handleLaunchBossSite">
手动逛Boss<TopRight w-1em h-1em mr10px />
</a>
<hr w180px />
<RouterLink to="./StartChatRecord">开聊记录</RouterLink>
<RouterLink to="./MarkAsNotSuitRecord">标记不合适记录</RouterLink>
<RouterLink to="./JobLibrary">职位库</RouterLink>
<RouterLink to="./BossLibrary">Boss库</RouterLink>
<RouterLink to="./CompanyLibrary">公司库</RouterLink>
</a>
</div>
</div>
<hr class="group-divider" />
<div class="group-item">
<div class="group-title">运行数据</div>
<div flex flex-col class="link-list">
<RouterLink to="./StartChatRecord">开聊记录</RouterLink>
<RouterLink to="./MarkAsNotSuitRecord">标记不合适记录</RouterLink>
<RouterLink to="./JobLibrary">职位库</RouterLink>
<RouterLink to="./BossLibrary">BOSS库</RouterLink>
<RouterLink to="./CompanyLibrary">公司库</RouterLink>
</div>
</div>
</div>
<div class="pt-16px pb-16px flex-0 font-size-12px">
<div v-if="updateStore.availableNewRelease" mb16px>
@@ -136,56 +192,26 @@
</div>
</div>
</div>
<RouterView v-slot="{ Component }" class="flex-1">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</RouterView>
<div class="router-view-wrap">
<RouterView v-slot="{ Component }" class="flex-1 of-hidden">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</RouterView>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { TopRight, QuestionFilled } from '@element-plus/icons-vue'
import useBuildInfo from '@renderer/hooks/useBuildInfo'
import { debounce } from 'lodash-es'
import { debounce } from 'lodash'
import { gtagRenderer } from '@renderer/utils/gtag'
import { useUpdateStore } from '../../store/index'
import { useUpdateStore, useTaskManagerStore } from '../../store/index'
import { ElMessage } from 'element-plus'
const router = useRouter()
const unmountedCbs: Array<InstanceType<typeof Function>> = []
onUnmounted(() => {
while (unmountedCbs.length) {
const fn = unmountedCbs.shift()!
try {
fn()
} catch {}
}
})
const goToCheckBossZhipinCookieFile = () => router.replace('/cookieAssistant')
onMounted(() => {
electron.ipcRenderer.on('check-boss-zhipin-cookie-file', goToCheckBossZhipinCookieFile)
})
onUnmounted(() => {
electron.ipcRenderer.removeListener(
'check-boss-zhipin-cookie-file',
goToCheckBossZhipinCookieFile
)
})
;(async () => {
const checkDependenciesResult = await electron.ipcRenderer.invoke('check-dependencies')
if (Object.values(checkDependenciesResult).includes(false)) {
router.replace('/')
return
}
const isCookieFileValid = await electron.ipcRenderer.invoke('check-boss-zhipin-cookie-file')
if (!isCookieFileValid) {
router.replace('/cookieAssistant')
return
}
})()
useRouter()
const { buildInfo } = useBuildInfo()
const handleFeedbackClick = () => {
@@ -217,34 +243,85 @@ function handleViewNewReleaseClick() {
gtagRenderer('click_view_release_form_nav')
electron.ipcRenderer.send('open-external-link', updateStore.availableNewRelease!.releasePageUrl)
}
const taskManagerStore = useTaskManagerStore()
void taskManagerStore
const handleClickLaunchBossLogin = async () => {
gtagRenderer('launch_login_clicked')
try {
await electron.ipcRenderer.invoke('login-with-cookie-assistant')
ElMessage({
type: 'success',
message: '登录凭据保存成功'
})
} catch {
//
}
}
const handleClickBrowserSetting = async () => {
gtagRenderer('browser_setting_clicked')
try {
await electron.ipcRenderer.invoke('config-with-browser-assistant')
ElMessage({
type: 'success',
message: '浏览器偏好保存成功'
})
} catch {
//
}
}
const handleClickConfigLlm = async () => {
gtagRenderer('config_llm_clicked')
try {
await electron.ipcRenderer.invoke('llm-config')
} catch (err) {
console.log(err)
}
}
</script>
<style lang="scss" scoped>
.aside-nav {
background-image: linear-gradient(45deg, #eaf4f1, #dcf6f2);
.nav-list {
> a {
display: flex;
align-items: center;
justify-content: space-between;
height: 2.5em;
box-sizing: border-box;
padding-left: 2em;
&.router-link-active {
background-color: #fff;
font-weight: 700;
color: #2faa9e;
border-radius: 9999px 0 0 9999px;
}
}
> hr {
hr.group-divider {
width: 100%;
border: 0 solid;
height: 1px;
background-color: #b3c8c3;
margin-top: 0;
margin-bottom: 0;
margin-top: 4px;
margin-bottom: 4px;
margin-right: 0;
}
.group-item {
.group-title {
color: #849492;
font-size: 12px;
padding: 0.25em 0;
}
.link-list {
a {
display: flex;
align-items: center;
justify-content: space-between;
height: 2em;
box-sizing: border-box;
padding-left: 1em;
font-size: 14px;
&.router-link-active {
background-color: #fff;
font-weight: 700;
color: #2faa9e;
border-radius: 9999px 0 0 9999px;
position: relative;
box-shadow: 0px 0px 10px rgba(50, 114, 108, 0.187);
}
}
}
}
}
.feedback-button-area,
.update-button-area {
@@ -255,4 +332,10 @@ function handleViewNewReleaseClick() {
}
}
}
.router-view-wrap {
display: flex;
flex: 1;
height: 100%;
box-shadow: -4px 1px 20px rgb(50 114 108 / 29%);
}
</style>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 KiB

After

Width:  |  Height:  |  Size: 232 KiB

+38 -19
View File
@@ -14,7 +14,28 @@ const routes: Array<RouteRecordRaw> = [
path: '/cookieAssistant',
component: () => import('@renderer/page/CookieAssistant/index.vue'),
meta: {
title: 'Boss 登录助手'
title: 'BOSS登录助手'
}
},
{
path: '/browserAssistant',
component: () => import('@renderer/page/BrowserAssistant/index.vue'),
meta: {
title: '浏览器助手'
}
},
{
path: '/browserAutoFind',
component: () => import('@renderer/page/BrowserAutoFind/index.vue'),
meta: {
title: '浏览器助手 - 自动查找浏览器'
}
},
{
path: '/browserDownloadProgress',
component: () => import('@renderer/page/BrowserDownloadProgress/index.vue'),
meta: {
title: '正在下载浏览器'
}
},
{
@@ -35,7 +56,7 @@ const routes: Array<RouteRecordRaw> = [
path: '/readNoReplyReminderLlmMock',
component: () => import('@renderer/page/ReadNoReplyReminderLlmMock/index.vue'),
meta: {
title: '已读不回提醒器 大语言模型测试'
title: '已读不回自动复聊 大语言模型测试'
}
},
{
@@ -43,18 +64,25 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('@renderer/page/MainLayout/index.vue'),
redirect: '/main-layout/GeekAutoStartChatWithBoss',
children: [
{
path: 'taskManager',
component: () => import('@renderer/page/MainLayout/TaskManager.vue'),
meta: {
title: '任务管理'
}
},
{
path: 'GeekAutoStartChatWithBoss',
component: () => import('@renderer/page/MainLayout/GeekAutoStartChatWithBoss/index.vue'),
meta: {
title: 'BOSS炸弹'
title: '自动开聊'
}
},
{
path: 'ReadNoReplyReminder',
component: () => import('@renderer/page/MainLayout/ReadNoReplyReminder.vue'),
meta: {
title: '已读不回提醒器'
title: '已读不回自动复聊'
}
},
{
@@ -82,7 +110,7 @@ const routes: Array<RouteRecordRaw> = [
path: 'BossLibrary',
component: () => import('@renderer/page/MainLayout/BossLibrary.vue'),
meta: {
title: 'Boss库'
title: 'BOSS库'
}
},
{
@@ -102,14 +130,14 @@ const routes: Array<RouteRecordRaw> = [
path: 'prepareRun',
component: () => import('@renderer/page/GeekAutoStartChatWithBoss/PrepareRun.vue'),
meta: {
title: 'BOSS炸弹 正在预热'
title: '自动开聊 正在预热'
}
},
{
path: 'runningStatus',
component: () => import('@renderer/page/GeekAutoStartChatWithBoss/RunningStatus.vue'),
meta: {
title: 'BOSS炸弹 正在为你开聊BOSS'
title: '自动开聊 正在为你开聊BOSS'
}
},
{
@@ -119,7 +147,7 @@ const routes: Array<RouteRecordRaw> = [
'@renderer/page/GeekAutoStartChatWithBoss/RunningStatusForReadNoReplyReminder.vue'
),
meta: {
title: '已读不回提醒器 正在为你开聊BOSS'
title: '已读不回自动复聊 正在为你开聊BOSS'
}
}
]
@@ -128,17 +156,8 @@ const routes: Array<RouteRecordRaw> = [
path: '/',
component: BootstrapSplash,
meta: {
title: '薪想事成'
},
children: [
{
path: '/downloadingDependencies',
component: () => import('@renderer/page/BootstrapSplash/page/DownloadingDependencies.vue'),
meta: {
title: '正在下载核心组件'
}
}
]
title: '你的职场大机密'
}
}
]
@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { NewReleaseInfo } from '../../../common/types/update'
import { ref } from 'vue'
import { throttle } from 'lodash'
export const useUpdateStore = defineStore('update', () => {
const availableNewRelease = ref<NewReleaseInfo | null>(null)
@@ -18,3 +19,16 @@ export const useUpdateStore = defineStore('update', () => {
setInterval(checkUpdate, 30 * 30 * 1000)
return { availableNewRelease }
})
export const useTaskManagerStore = defineStore('taskManager', () => {
const runningTasks = ref<unknown[]>([])
function getRunningTasks() {
const { ipcRenderer } = electron
ipcRenderer.invoke('get-task-manager-list').then(res => {
runningTasks.value = res.workers ?? []
})
}
const throttledGetRunningTasks = throttle(getRunningTasks, 2000)
setInterval(throttledGetRunningTasks, 2 * 1000)
return { runningTasks, getRunningTasks: throttledGetRunningTasks }
})