visualize prerequisite check steps

This commit is contained in:
geekgeekrun
2026-01-18 03:43:58 +08:00
parent ce32dc4c2f
commit 0df3174ad9
11 changed files with 285 additions and 47 deletions

View File

@@ -149,10 +149,10 @@ function handleMessage(socket, message) {
}
return
}
case 'worker-message': {
case 'worker-to-gui-message': {
// 转发工具进程消息到GUI客户端
broadcastToGUI({
type: 'worker-message',
type: 'worker-to-gui-message',
workerId: workerId,
data: message.data || message,
timestamp: Date.now()

View File

@@ -0,0 +1,17 @@
export const getAutoStartChatSteps = () => [{
id: 'worker-launch',
describe: '启动子进程',
},
// {
// id: 'basic-cookie-check',
// describe: 'Cookie 格式检查',
// },
{
id: 'puppeteer-executable-check',
describe: 'Puppeteer 可执行程序检查',
},
{
id: 'login-status-check',
describe: '登录状态检查',
}
]

View File

@@ -0,0 +1,29 @@
import { sendToDaemon } from "../flow/OPEN_SETTING_WINDOW/connect-to-daemon"
import minimist from 'minimist'
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
export function pushUserInfoValidStatus (userInfoResponse) {
sendToDaemon({
type: 'worker-to-gui-message',
data: {
type: 'prerequisite-step-by-step-checkstep-by-step-check',
step: {
id: 'login-status-check',
status: userInfoResponse.code === 0 ? 'fulfilled' : 'rejected'
},
runRecordId
}
})
}
export class UserResponseInfoPlugin {
apply(hooks) {
hooks.userInfoResponse.tapPromise(
"UserResponseInfoPlugin",
(userInfoResponse) => {
pushUserInfoValidStatus(userInfoResponse)
return Promise.resolve()
}
)
}
}

View File

@@ -3,33 +3,37 @@ import { sendToDaemon } from "../flow/OPEN_SETTING_WINDOW/connect-to-daemon"
import { saveAndGetCurrentRunRecord } from "../flow/OPEN_SETTING_WINDOW/utils/db"
export async function runCommon ({ mode }) {
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 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
}
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
}
)
)
return {
runRecordId: currentRunRecord?.id
}
}

View File

@@ -10,13 +10,14 @@ import { getAnyAvailablePuppeteerExecutable } from '../CHECK_AND_DOWNLOAD_DEPEND
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 { UserResponseInfoPlugin } from '../../features/boss-user-info-response-plugin'
const { default: SqlitePlugin } = SqlitePluginModule
const rerunInterval = (() => {
@@ -35,15 +36,39 @@ const initPlugins = (hooks) => {
new SqlitePlugin(getPublicDbFilePath()).apply(hooks)
new GtagPlugin().apply(hooks)
new PeriodPushCurrentPageScreenshotPlugin().apply(hooks)
new UserResponseInfoPlugin().apply(hooks)
}
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
const runAutoChat = async () => {
app.dock?.hide()
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
if (!puppeteerExecutable) {
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'
@@ -131,6 +156,17 @@ export const waitForProcessHandShakeAndRunAutoChat = async () => {
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()
}

View File

@@ -202,7 +202,7 @@ export default function initIpc() {
ipcMain.handle('run-geek-auto-start-chat-with-boss', async (ev) => {
const mode = 'geekAutoStartWithBossMain'
await runCommon({ mode })
const { runRecordId } = await runCommon({ mode })
daemonEE.on('message', function handler (message) {
if (message.workerId !== mode) {
return
@@ -226,11 +226,12 @@ export default function initIpc() {
}
}
})
return { runRecordId }
})
ipcMain.handle('run-read-no-reply-auto-reminder', async () => {
const mode = 'readNoReplyAutoReminderMain'
await runCommon({ mode })
const { runRecordId } = await runCommon({ mode })
daemonEE.on('message', function handler (message) {
if (message.workerId !== mode) {
return
@@ -254,6 +255,7 @@ export default function initIpc() {
}
}
})
return { runRecordId }
})
ipcMain.handle('check-dependencies', async () => {

View File

@@ -28,6 +28,7 @@ import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to
import { pushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS } from '../../utils/screenshot'
import { checkShouldExit } from '../../utils/worker'
import { getAnyAvailablePuppeteerExecutable } from '../CHECK_AND_DOWNLOAD_DEPENDENCIES/utils/puppeteer-executable'
import minimist from 'minimist'
const throttleIntervalMinutes =
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
@@ -249,12 +250,34 @@ const mainLoop = async () => {
// #region
if (currentPageUrl.startsWith('https://www.zhipin.com/web/user/')) {
writeStorageFile('boss-cookies.json', [])
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')
}
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')) {
@@ -277,9 +300,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!.$(
@@ -478,19 +523,53 @@ const rerunInterval = (() => {
return v
})()
const runRecordId = minimist(process.argv.slice(2))['run-record-id'] ?? null
export async function runEntry() {
app.dock?.hide()
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
if (!puppeteerExecutable) {
throw new Error(`PUPPETEER_IS_NOT_EXECUTABLE`)
}
process.env.PUPPETEER_EXECUTABLE_PATH = puppeteerExecutable.executablePath
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
}
})
const puppeteerExecutable = await getAnyAvailablePuppeteerExecutable()
if (!puppeteerExecutable) {
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()

View File

@@ -3,6 +3,7 @@ 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 {
@@ -58,5 +59,10 @@ 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)
}
})
return mainWindow!
}

View File

@@ -6,8 +6,22 @@
:close-on-press-escape="false"
:show-close="false"
>
<!-- v-if="stepsForRender.some(it => ['todo', 'pending', 'rejected'].includes(it.status))" -->
<div>
<ul m0 pl0>
<li list-style-none v-for="item in stepsForRender" 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>任务运行中!</div>
<div>任务运行中</div>
<div>
<slot name="op-buttons" />
</div>
@@ -17,20 +31,68 @@
<script lang="ts" setup>
import { useTaskManagerStore } from '@renderer/store'
import { computed } from 'vue'
import { getAutoStartChatSteps } from '../../../../common/prerequisite-step-by-step-check'
import { computed, onUnmounted, ref, watch } from 'vue'
const props = defineProps({
workerId: {
type: String
},
runRecordId: {
type: Number
}
})
const taskManagerStore = useTaskManagerStore()
const runingTaskInfo = 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
})
function fillEmptySteps () {
const arr = getAutoStartChatSteps()
arr.forEach(it => it.status = 'todo')
steps.value = arr
}
watch(
() => props.runRecordId,
fillEmptySteps,
{
immediate: 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)
</script>
<style lang="scss">

View File

@@ -1062,7 +1062,7 @@
pointerEvents: 'none'
}"
>
<RuningOverlay worker-id="geekAutoStartWithBossMain">
<RuningOverlay worker-id="geekAutoStartWithBossMain" :run-record-id="runRecordId">
<template #op-buttons>
<el-button @click="handleStopButtonClick">结束任务</el-button>
</template>
@@ -1431,6 +1431,7 @@ const formRules = {
}
const formRef = ref<InstanceType<typeof ElForm>>()
const runRecordId = ref(null)
const handleSubmit = async () => {
gtagRenderer('save_config_and_launch_clicked', {
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken,
@@ -1469,7 +1470,8 @@ const handleSubmit = async () => {
})
try {
await electron.ipcRenderer.invoke('run-geek-auto-start-chat-with-boss')
const { runRecordId: rrId } = await electron.ipcRenderer.invoke('run-geek-auto-start-chat-with-boss')
runRecordId.value = rrId
} catch (err) {
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
gtagRenderer('gascwb_cannot_run_for_corrupt')

View File

@@ -228,7 +228,7 @@
pointerEvents: 'none'
}"
>
<RuningOverlay worker-id="readNoReplyAutoReminderMain">
<RuningOverlay worker-id="readNoReplyAutoReminderMain" :run-record-id="runRecordId">
<template #op-buttons>
<el-button @click="handleStopButtonClick">结束任务</el-button>
</template>
@@ -449,7 +449,7 @@ async function checkIsCanRun() {
return true
}
const runRecordId = ref(null)
const handleSubmit = async () => {
gtagRenderer('run_read_no_reply_reminder_clicked', {
throttle_interval_minutes: formContent.value.autoReminder.throttleIntervalMinutes,
@@ -488,7 +488,8 @@ const handleSubmit = async () => {
gtagRenderer('run_read_no_reply_reminder_launched')
try {
await electron.ipcRenderer.invoke('run-read-no-reply-auto-reminder')
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')