add page to show running task

This commit is contained in:
geekgeekrun
2026-01-14 17:44:42 +08:00
parent 45397bcc74
commit 69ec879967
12 changed files with 254 additions and 72 deletions

View File

@@ -162,10 +162,10 @@ function handleMessage(socket, message) {
timestamp: Date.now()
});
// 如果是心跳,更新最后心跳时间
if (message.type === 'worker-heartbeat') {
workerInfo.lastHeartbeat = Date.now();
}
// // 如果是心跳,更新最后心跳时间
// if (message.type === 'worker-heartbeat') {
// workerInfo.lastHeartbeat = Date.now();
// }
} else {
sendResponse(socket, _callbackUuid, { error: '未注册的工具进程连接' });
}
@@ -328,9 +328,10 @@ function startWorker({ workerId, command, args, env }, restartCount = 0) {
status: 'running',
startTime: Date.now(),
restartCount, // 使用传入的重启次数
socket: null, // 工具进程的TCP连接稍后由工具进程注册
lastHeartbeat: null,
// socket: null, // 工具进程的TCP连接稍后由工具进程注册
// lastHeartbeat: null,
command,
args,
env,
workerId,
}
@@ -388,8 +389,11 @@ function getWorkersStatus() {
status: workerInfo.status,
uptime: Date.now() - workerInfo.startTime,
restartCount: workerInfo.restartCount || 0,
connected: workerInfo.socket !== null && !workerInfo.socket.destroyed,
lastHeartbeat: workerInfo.lastHeartbeat
// connected: workerInfo.socket !== null && !workerInfo.socket.destroyed,
// lastHeartbeat: workerInfo.lastHeartbeat,
command: workerInfo.command,
args: workerInfo.args,
pid: workerInfo.process?.pid
});
}
return status;

View File

@@ -129,16 +129,6 @@ export function sendToDaemon(message, {
// sendToDaemon({ type: 'start-worker', workerId, command, args, env });
// });
// // IPC处理停止工具进程
// ipcMain.on('stop-worker', (event, workerId) => {
// sendToDaemon({ type: 'stop-worker', workerId });
// });
// // IPC处理获取所有工具进程状态
// ipcMain.on('get-workers-status', () => {
// sendToDaemon({ type: 'get-status' });
// });
app.on('window-all-closed', () => {
if (daemonClient) {
daemonClient.destroy();
@@ -149,4 +139,4 @@ app.on('before-quit', () => {
if (daemonClient) {
daemonClient.destroy();
}
});
});

View File

@@ -423,6 +423,31 @@ export default function initIpc() {
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
}
)
})
let subProcessOfBossZhipinLoginPageWithPreloadExtension: ChildProcess | null = null
ipcMain.on('launch-bosszhipin-login-page-with-preload-extension', async () => {
try {

View File

@@ -0,0 +1,69 @@
<template>
<div class="task-item" flex>
<div>
<img height="160" width="256" />
</div>
<div ml-40px>
<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>
<el-button type="danger" @click="stopTask(task.workerId)">结束任务</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
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 {
font-size: 14px;
overflow: hidden;
dl {
margin: 0;
display: flex;
padding-top: 4px;
padding-bottom: 4px;
border-bottom: 1px solid #eee;
dt {
width: 6em;
flex: 0 0 6em;
}
dd {
}
}
}
</style>

View File

@@ -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?.workers || [])
</script>

View File

@@ -1,53 +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 { ipcRenderer } = electron
const currentStatus = ref('')
onMounted(() => {
const promise = Promise.resolve()
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)
})
})
const needToCheckRuntimeDependenciesHandler = () => {
router.replace('/')
}
ipcRenderer.on('need-to-check-runtime-dependencies', needToCheckRuntimeDependenciesHandler)
onUnmounted(() => {
ipcRenderer.removeListener('need-to-check-runtime-dependencies', needToCheckRuntimeDependenciesHandler)
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('/')
}
}
})
</script>

View File

@@ -1049,12 +1049,15 @@
<el-button @click="handleSave">仅保存配置</el-button>
<el-button type="primary" @click="handleSubmit"> 保存配置,并开始求职! </el-button>
</div>
<div>
<el-button @click="handleStopButtonClick">结束任务</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch, nextTick } from 'vue'
import { computed, onBeforeUnmount, ref, watch, nextTick, onUnmounted } from 'vue'
import { ElForm, ElMessage } from 'element-plus'
import { QuestionFilled } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
@@ -1445,13 +1448,28 @@ const handleSubmit = async () => {
delete clonedFormContent.__jobSourceList
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(clonedFormContent))
mittBus.emit('auto-start-chat-with-boss-config-saved')
router.replace({
path: '/geekAutoStartChatWithBoss/prepareRun',
query: { flow: 'geek-auto-start-chat-with-boss' }
})
gtagRenderer('config_saved_and_launch_auto_start_chat', {
has_dingtalk_robot_token: !!formContent.value?.dingtalkRobotAccessToken
})
try {
await electron.ipcRenderer.invoke('run-geek-auto-start-chat-with-boss')
} catch (err) {
if (err instanceof Error && err.message.includes('NEED_TO_CHECK_RUNTIME_DEPENDENCIES')) {
gtagRenderer('gascwb_cannot_run_for_corrupt')
ElMessage.error({
message: `核心组件损坏,正在尝试修复`
})
router.replace('/')
}
console.error(err)
gtagRenderer('gascwb_cannot_run_for_unknown_error', { err })
}
// {
// path: '/geekAutoStartChatWithBoss/prepareRun',
// query: { flow: 'geek-auto-start-chat-with-boss' }
// }
}
const handleSave = async () => {
gtagRenderer('save_config_clicked', {
@@ -1803,6 +1821,22 @@ const combineRecommendJobFilterTypeOptions = [
value: 2
}
]
const needToCheckRuntimeDependenciesHandler = () => {
router.replace('/')
}
electron.ipcRenderer.on('need-to-check-runtime-dependencies', needToCheckRuntimeDependenciesHandler)
onUnmounted(() => {
electron.ipcRenderer.removeListener(
'need-to-check-runtime-dependencies',
needToCheckRuntimeDependenciesHandler
)
})
const handleStopButtonClick = async () => {
gtagRenderer('gascwb_stop_button_clicked')
electron.ipcRenderer.invoke('stop-geek-auto-start-chat-with-boss')
}
</script>
<style scoped lang="scss">

View File

@@ -219,6 +219,9 @@
<el-form-item class="last-form-item">
<el-button type="primary" @click="handleSubmit">开始提醒</el-button>
</el-form-item>
<div>
<el-button @click="handleStopButtonClick">结束任务</el-button>
</div>
</el-form>
</div>
</template>
@@ -471,10 +474,25 @@ const handleSubmit = async () => {
}
}
gtagRenderer('run_read_no_reply_reminder_launched')
router.replace({
path: '/geekAutoStartChatWithBoss/prepareRun',
query: { flow: 'read-no-reply-reminder' }
})
try {
await electron.ipcRenderer.invoke('run-read-no-reply-auto-reminder')
} 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) {
@@ -559,6 +577,22 @@ 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 handleStopButtonClick = async () => {
gtagRenderer('rnrr_stop_button_clicked')
electron.ipcRenderer.invoke('stop-read-no-reply-auto-reminder')
}
</script>
<style scoped lang="scss">

View File

@@ -0,0 +1,7 @@
<template>
<TaskManagerList />
</template>
<script lang="ts" setup>
import TaskManagerList from '../../features/TaskManager/List.vue'
</script>

View File

@@ -2,6 +2,7 @@
<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="./TaskManager">任务管理</RouterLink>
<RouterLink to="./GeekAutoStartChatWithBoss">
Boss炸弹
<el-tooltip
@@ -136,7 +137,7 @@
</div>
</div>
</div>
<RouterView v-slot="{ Component }" class="flex-1">
<RouterView v-slot="{ Component }" class="flex-1 of-hidden">
<KeepAlive>
<component :is="Component" />
</KeepAlive>
@@ -151,7 +152,7 @@ import { TopRight, QuestionFilled } from '@element-plus/icons-vue'
import useBuildInfo from '@renderer/hooks/useBuildInfo'
import { debounce } from 'lodash-es'
import { gtagRenderer } from '@renderer/utils/gtag'
import { useUpdateStore } from '../../store/index'
import { useUpdateStore, useTaskManagerStore } from '../../store/index'
const router = useRouter()
const unmountedCbs: Array<InstanceType<typeof Function>> = []
@@ -217,6 +218,9 @@ function handleViewNewReleaseClick() {
gtagRenderer('click_view_release_form_nav')
electron.ipcRenderer.send('open-external-link', updateStore.availableNewRelease!.releasePageUrl)
}
const taskManagerStore = useTaskManagerStore()
void taskManagerStore
</script>
<style lang="scss" scoped>

View File

@@ -43,6 +43,13 @@ 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'),

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { NewReleaseInfo } from '../../../common/types/update'
import { ref } from 'vue'
import { throttle } from 'lodash-es'
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
})
}
const throttledGetRunningTasks = throttle(getRunningTasks, 2000)
setInterval(throttledGetRunningTasks, 2 * 1000)
return { runningTasks, getRunningTasks: throttledGetRunningTasks }
})