add browser assistant entry on ui

This commit is contained in:
geekgeekrun
2026-02-07 20:04:49 +08:00
parent 9f97622da9
commit ebf512c86a
13 changed files with 466 additions and 170 deletions

View File

@@ -1 +1,2 @@
export const SINGLE_ITEM_DEFAULT_SERVE_WEIGHT = 1
export const EXPECT_CHROMIUM_BUILD_ID = '139.0.7258.154'

View File

@@ -0,0 +1,26 @@
import { ipcMain } from 'electron'
import {
createBrowserAssistantWindow,
browserAssistantWindow
} from '../window/browserAssistantWindow'
export async function configWithBrowserAssistant({ windowOption } = {}) {
return new Promise((resolve, reject) => {
createBrowserAssistantWindow({ ...windowOption })
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'))
}
})
})
}

View File

@@ -6,11 +6,13 @@ export async function loginWithCookieAssistant({ windowOption } = {}) {
createCookieAssistantWindow({ ...windowOption })
let processDone = false
ipcMain.once('cookie-saved', function handler() {
function handler() {
processDone = true
cookieAssistantWindow.close()
})
}
ipcMain.once('cookie-saved', handler)
cookieAssistantWindow.once('closed', () => {
ipcMain.off('cookie-saved', handler)
if (processDone) {
resolve(true)
} else {

View File

@@ -40,13 +40,6 @@ export const getLastUsedAndAvailableBrowser = async (): Promise<BrowserInfo | nu
await removeLastUsedAndAvailableBrowserPath()
return null
}
// blacklist browser
if (path.includes(`Microsoft\\Edge\\Application\\msedge.exe`)) {
await removeLastUsedAndAvailableBrowserPath()
return null
}
return {
executablePath: path,
browser

View File

@@ -9,6 +9,7 @@ import {
removeLastUsedAndAvailableBrowserPath
} from '../browser-history'
import gtag from '../../../../utils/gtag'
import { EXPECT_CHROMIUM_BUILD_ID } from '../../../../../common/constant'
const getPuppeteerManagerModule = async () => {
const puppeteerManager = await import('@puppeteer/browsers')
@@ -16,7 +17,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> => {
@@ -97,29 +97,36 @@ export const checkAndDownloadPuppeteerExecutable = async (
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

View File

@@ -53,6 +53,7 @@ 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'
export default function initIpc() {
ipcMain.handle('fetch-config-file-content', async () => {
@@ -246,64 +247,6 @@ export default function initIpc() {
return { runRecordId }
})
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']
}
)
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
})
})
})
ipcMain.handle('stop-geek-auto-start-chat-with-boss', async () => {
mainWindow?.webContents.send('geek-auto-start-chat-with-boss-stopping')
const p = new Promise((resolve) => {
@@ -613,6 +556,15 @@ export default function initIpc() {
}
})
})
ipcMain.handle('config-with-browser-assistant', async () => {
return await configWithBrowserAssistant({
windowOption: {
parent: mainWindow!,
modal: true,
show: true
}
})
})
ipcMain.handle('exit-app-immediately', () => {
app.exit(0)

View File

@@ -1,4 +1,4 @@
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'
@@ -71,4 +71,18 @@ export default function initPublicIpc() {
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)
}
})
}

View File

@@ -0,0 +1,149 @@
import { ChildProcess } from 'child_process'
import { BrowserWindow, ipcMain } from 'electron'
import path from 'path'
import { getAnyAvailablePuppeteerExecutable } from '../flow/CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable'
import * as childProcess from 'node:child_process'
import * as JSONStream from 'JSONStream'
import {
getLastUsedAndAvailableBrowser,
saveLastUsedAndAvailableBrowserInfo
} from '../flow/CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/browser-history'
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
): 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.
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
browserAssistantWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/browserAssistant')
} else {
browserAssistantWindow.loadURL(
'file://' + path.join(__dirname, '../renderer/index.html') + '#/browserAssistant'
)
}
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)
}
)
let subProcessOfCheckAndDownloadDependencies: ChildProcess | null = null
registerHandleWithWindow(browserAssistantWindow, 'setup-dependencies', async () => {
if (subProcessOfCheckAndDownloadDependencies) {
return
}
subProcessOfCheckAndDownloadDependencies = childProcess.spawn(
process.argv[0],
[process.argv[1], `--mode=checkAndDownloadDependenciesForInit`],
{
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': {
browserAssistantWindow?.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
})
})
})
const killHandler = async () => {
try {
subProcessOfCheckAndDownloadDependencies?.kill()
} catch {
//
} finally {
subProcessOfCheckAndDownloadDependencies = null
}
}
browserAssistantWindow.once('closed', () => {
killHandler()
})
return browserAssistantWindow!
}

View File

@@ -27,25 +27,7 @@ const router = useRouter()
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()
// }
// 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(2000)
router.replace('/main-layout')
// }
// })
})
</script>

View File

@@ -1,58 +1,200 @@
<template>
<div class="h-screen flex flex-col flex-items-center flex-justify-center">
<div>
<img
class="block"
:class="{
'animate__animated animate__bounce animate__repeat-3':
Object.values(checkDependenciesResult).includes(false)
}"
:width="256"
src="@renderer/../../../resources/icon.png"
/>
<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-80%"
label="浏览器可执行文件路径"
prop="browserPath"
pt50px
pb50px
ml-auto
mr-auto
mb-0
>
<div flex flex-1>
<el-input v-model="formData.browserPath" />
<el-button type="primary" @click="autoDetectPuppeteerExecutable">自动检测</el-button>
<el-button :style="{ marginLeft: 0 }" @click="browserExecutableFile">浏览</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-80%">
<div>常见问题</div>
<div>
<details>
<summary>不能自动检测到浏览器</summary>
<div ml12px class="color-#666">
请尝试如下方案之一来处理
<ul pl1em m0>
<li>
方案一通过本程序下载 Google Chrome for Testing
{{ EXPECT_CHROMIUM_BUILD_ID }} -
<a href="javascript:;">点击此处</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:;"
@click="autoDetectPuppeteerExecutable"
>自动检测</a
>按钮再次尝试目前2026.2.7已知 Chrome 最新版本为 144.0.7559.133
多数情况下本程序都可以正常工作但由于浏览器会自动升级版本不固定可能存在<span color-orange
>浏览器升级后某些功能不兼容导致本程序不能正确运行</span
>的问题您可以<a href="javascript:;" @click="handleFeedbackClick"
>提交 Issue</a
>来反馈新版本浏览器不能正常运行的问题同时请再尝试方法一
</li>
</ul>
</div>
</details>
</div>
</div>
</div>
</el-form>
</div>
<div mt24px>愿你薪想事成</div>
<div class="h60px mt14px">
<div class="bg-#f8f8f8 pb10px pt10px w-full flex-0">
<div
:style="{
display: 'flex',
justifyContent: 'end',
width: '80%',
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>
</div>
</div> -->
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { onMounted, ref } from 'vue'
import { sleep } from '@geekgeekrun/utils/sleep.mjs'
import { gtagRenderer } from '@renderer/utils/gtag'
import { 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'
const router = useRouter()
const { ipcRenderer } = electron
useRouter()
// const checkDependenciesResult = ref({})
// const downloadProcessWaitee = ref(null)
const checkDependenciesResult = ref({})
const downloadProcessWaitee = ref(null)
const gtagRenderer = (name, params?: object) => {
return baseGtagRenderer(name, {
scene: 'cookie-assistant',
...params
})
}
onMounted(async () => {
checkDependenciesResult.value = await electron.ipcRenderer.invoke('check-dependencies')
downloadProcessWaitee.value = Promise.withResolvers()
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 }
)
if (Object.values(checkDependenciesResult.value).includes(false)) {
gtagRenderer('dependencies_need_download')
router.replace('/downloadingDependencies')
} else {
downloadProcessWaitee.value!.resolve()
const formData = ref({
browserPath: ''
})
const rules = {
browserPath: {
validator: (_, value, callback) => {
if (!value?.trim()) {
callback(new Error('请输入浏览器可执行文件路径'))
}
// TODO: 检查文件是否存在
else {
callback()
}
}
}
}
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')
async function autoDetectPuppeteerExecutable() {
const result = await ipcRenderer.invoke('get-any-available-puppeteer-executable', {
ignoreCached: true,
noSave: true
})
if (!result) {
ElMessage.warning({
message: '未检测到可用浏览器的可执行文件',
type: 'warning'
})
return
}
formData.value.browserPath = result.executablePath
}
async function browserExecutableFile() {
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) {
return
}
formData.value.browserPath = chooseResult.filePaths[0]
}
ipcRenderer.invoke('get-last-used-and-available-browser').then((res) => {
formData.value.browserPath = res?.executablePath ?? ''
})
function handleCancel() {
gtagRenderer('cancel_clicked')
window.close()
}
async function handleSave() {
await ipcRenderer.invoke('save-last-used-and-available-browser-info', {
executablePath: formData.value.browserPath,
browser: ''
})
await ipcRenderer.send('browser-config-saved')
}
const handleFeedbackClick = () => {
gtagRenderer('goto_feedback_for_ba_clicked')
electron.ipcRenderer.send('send-feed-back-to-github-issue')
}
</script>
<style lang="scss" scoped>
a:link,
a:visited,
a:hover,
a:active {
color: #409eff;
}
</style>

View File

@@ -10,16 +10,27 @@
<div class="form-wrap geek-auto-start-run-with-boss">
<el-form ref="formRef" :model="formContent" label-position="top" :rules="formRules">
<el-card class="config-section">
<el-form-item>
<div>
<div font-size-16px>BOSS直聘 Cookie</div>
<el-button size="small" type="primary" @click="handleClickLaunchLogin"
>编辑Cookie</el-button
>
</div>
</el-form-item>
<div flex>
<el-form-item>
<div>
<div font-size-14px>BOSS直聘 Cookie</div>
<el-button size="small" type="primary" @click="handleClickLaunchLogin"
>编辑Cookie</el-button
>
</div>
</el-form-item>
<div w1px class="bg-#dee1e8" ml16px mr16px />
<el-form-item>
<div>
<div font-size-14px>浏览器</div>
<el-button size="small" type="primary" @click="handleClickBrowserSetting"
>编辑浏览器设置</el-button
>
</div>
</el-form-item>
</div>
<div>
<div font-size-16px>
<div font-size-14px>
摸鱼模式
<el-tooltip
effect="light"
@@ -51,7 +62,7 @@
启用摸鱼模式
</el-checkbox>
</div>
<div pl-1.5em font-size-14px>
<div pl-1.5em font-size-12px>
<div
:style="{
color: formContent.isSageTimeEnabled ? '' : '#aaa'
@@ -1602,6 +1613,20 @@ const handleClickLaunchLogin = async () => {
//
}
}
const handleClickBrowserSetting = async () => {
gtagRenderer('browser_setting_clicked')
try {
await electron.ipcRenderer.invoke('config-with-browser-assistant')
ElMessage({
type: 'success',
message: '浏览器设置保存成功'
})
} catch {
//
}
}
const expectCompanyTemplateList = [
{
name: '默认值',

View File

@@ -7,11 +7,19 @@
: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>
<div flex>
<el-form-item label="BOSS直聘 Cookie">
<el-button size="small" type="primary" @click="handleClickLaunchLogin"
>编辑Cookie</el-button
>
</el-form-item>
<div w1px class="bg-#dee1e8" ml16px mr16px />
<el-form-item label="浏览器">
<el-button size="small" type="primary" @click="handleClickBrowserSetting"
>编辑浏览器设置</el-button
>
</el-form-item>
</div>
<el-form-item>
<div>
<el-checkbox v-if="!expectJobTypeRegExpStr?.trim()" :model-value="false" disabled>
@@ -538,6 +546,18 @@ const handleClickLaunchLogin = async () => {
//
}
}
const handleClickBrowserSetting = async () => {
gtagRenderer('browser_setting_clicked')
try {
await electron.ipcRenderer.invoke('config-with-browser-assistant')
ElMessage({
type: 'success',
message: '浏览器设置保存成功'
})
} catch {
//
}
}
const currentStamp = ref(new Date())
let timer = 0

View File

@@ -146,7 +146,6 @@
</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'
@@ -154,23 +153,7 @@ import { debounce } from 'lodash'
import { gtagRenderer } from '@renderer/utils/gtag'
import { useUpdateStore, useTaskManagerStore } from '../../store/index'
const router = useRouter()
const unmountedCbs: Array<InstanceType<typeof Function>> = []
onUnmounted(() => {
while (unmountedCbs.length) {
const fn = unmountedCbs.shift()!
try {
fn()
} catch {}
}
})
;(async () => {
const checkDependenciesResult = await electron.ipcRenderer.invoke('check-dependencies')
if (Object.values(checkDependenciesResult).includes(false)) {
router.replace('/')
return
}
})()
useRouter()
const { buildInfo } = useBuildInfo()
const handleFeedbackClick = () => {