mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-09-05 15:38:46 +08:00
Merge branch 'feature/multi-process' into feature/ui
This commit is contained in:
@@ -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 +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: '登录状态检查'
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+10
-8
@@ -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)
|
||||
+3
-10
@@ -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')
|
||||
+30
-25
@@ -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()
|
||||
-10
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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])
|
||||
+271
-49
@@ -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(),
|
||||
@@ -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
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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.7)Google 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 内核浏览器,因此 Safari、Firefox、Windows 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>
|
||||
+14
-14
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>核心组件下载失败,请重试。</p>
|
||||
<br />
|
||||
<p>浏览器下载失败,请重试,或使用其他方式下载</p>
|
||||
<!-- <br />
|
||||
<br />
|
||||
<p>
|
||||
<b text-orange>提示:</b>由于网络颠簸,如果多次重试仍然失败,请 <el-button
|
||||
@@ -12,23 +12,23 @@
|
||||
>点击此处</el-button
|
||||
> 下载最新版本 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>
|
||||
|
||||
+10
-9
@@ -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>
|
||||
|
||||
+1242
-996
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">当所有模型均不可使用时 </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"
|
||||
/> 分钟内不多次跟进同一Boss
|
||||
</el-form-item>
|
||||
<el-form-item label="跟进时限(天)" prop="rechatLimitDay" mb-0>
|
||||
<div>
|
||||
<div><el-checkbox v-model="enableRechatLimit" /> 启用</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">当所有模型均不可使用时 </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"
|
||||
/> 天<br />
|
||||
<div v-if="enableRechatLimit">
|
||||
不再跟进 (<span class="text-orange">{{ rechatLimitDateString }}</span
|
||||
>)之前列表中没有进展的聊天
|
||||
@blur="handleThrottleIntervalMinutesBlur"
|
||||
/> 分钟内不多次跟进同一BOSS
|
||||
</el-form-item>
|
||||
<el-form-item label="跟进时限(天)" prop="rechatLimitDay" mb-0>
|
||||
<div>
|
||||
<div><el-checkbox v-model="enableRechatLimit" /> 启用</div>
|
||||
<el-input-number
|
||||
v-model="formContent.autoReminder.rechatLimitDay"
|
||||
class="w-150px"
|
||||
:min="0"
|
||||
:precision="1"
|
||||
:step="0.5"
|
||||
:disabled="!enableRechatLimit"
|
||||
/> 天<br />
|
||||
<div v-if="enableRechatLimit">
|
||||
不再跟进 (<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 |
@@ -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 }
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user