fix: 调度中心ws订阅的相关方法在初始化时暴露

This commit is contained in:
DLmaster361
2025-09-24 21:43:54 +08:00
parent bd58a512c9
commit 34df37c040
5 changed files with 257 additions and 24 deletions

View File

@@ -1,4 +1,10 @@
import { ref, type Ref } from 'vue'
// 为了在调度中心 UI 未加载时仍能使用具体逻辑,直接同步导入 schedulerHandlers 的默认实现
// schedulerHandlers 不再依赖 useWebSocket因此同步导入不会产生致命循环依赖
import schedulerHandlers from '@/views/scheduler/schedulerHandlers'
let _defaultHandlersLoaded = true
let _defaultTaskManagerHandler: (m: any) => void = schedulerHandlers.handleTaskManagerMessage
let _defaultMainHandler: (m: any) => void = schedulerHandlers.handleMainMessage
import { Modal } from 'ant-design-vue'
// 基础配置
@@ -576,18 +582,34 @@ window.addEventListener('beforeunload', () => {
// 保持连接
})
// 全局订阅处理函数 - 供调度台逻辑调用
let globalTaskManagerHandler: ((message: any) => void) | null = null
let globalMainMessageHandler: ((message: any) => void) | null = null
// 设置TaskManager消息处理函数
export const setTaskManagerHandler = (handler: (message: any) => void) => {
globalTaskManagerHandler = handler
}
// 设置Main消息处理函数
export const setMainMessageHandler = (handler: (message: any) => void) => {
globalMainMessageHandler = handler
// 导出一个长期存在的可变对象,供外部通过 import 直接赋值/替换处理函数
// 这样可以避免通过 window 或全局函数暴露,同时保证导入方始终能获取到该对象并设置回调,且不会被内部清理
export const ExternalWSHandlers: {
mainMessage: (message: any) => void
taskManagerMessage: (message: any) => void
} = {
mainMessage: (message: any) => {
// 如果默认实现已加载,则调用之;否则保持空实现
try {
if (_defaultHandlersLoaded && typeof _defaultMainHandler === 'function') {
_defaultMainHandler(message)
return
}
} catch (e) {
console.warn('[ExternalWSHandlers] default main handler error:', e)
}
// 未加载默认实现时保持空实现
},
taskManagerMessage: (message: any) => {
try {
if (_defaultHandlersLoaded && typeof _defaultTaskManagerHandler === 'function') {
_defaultTaskManagerHandler(message)
return
}
} catch (e) {
console.warn('[ExternalWSHandlers] default taskManager handler error:', e)
}
},
}
// 初始化全局订阅
@@ -595,8 +617,11 @@ const initializeGlobalSubscriptions = () => {
// 订阅TaskManager消息
_subscribe('TaskManager', {
onMessage: (message) => {
if (globalTaskManagerHandler) {
globalTaskManagerHandler(message)
try {
ExternalWSHandlers.taskManagerMessage(message)
} catch (e) {
// 防御性处理,确保调用方异常不会影响消息通道
console.warn('[WebSocket] External taskManagerMessage handler error:', e)
}
}
})
@@ -633,9 +658,11 @@ const initializeGlobalSubscriptions = () => {
}
}
// 调用外部设置的Main消息处理函数
if (globalMainMessageHandler) {
globalMainMessageHandler(message)
// 调用外部导入的 Main 消息处理函数(由使用者通过 import 并赋值给 ExternalWSHandlers
try {
ExternalWSHandlers.mainMessage(message)
} catch (e) {
console.warn('[WebSocket] External mainMessage handler error:', e)
}
}
})

View File

@@ -37,6 +37,23 @@ mirrorManager.initialize().then(() => {
// 创建应用实例
const app = createApp(App)
// 提前初始化调度中心逻辑(使 handler 在前端初始化阶段就被注册)
try {
// 动态导入以避免循环引用问题
const { useSchedulerLogic } = await import('./views/scheduler/useSchedulerLogic')
try {
const scheduler = useSchedulerLogic()
// 初始化并加载任务选项(不阻塞主流程,但希望尽早完成)
scheduler.initialize()
logger.info('Scheduler logic initialized at app startup')
} catch (e) {
logger.warn('Scheduler logic init failed at startup:', e)
}
} catch (e) {
// 如果导入失败(例如构建/路径问题),记录并继续,避免阻塞应用启动
logger.warn('Failed to pre-import scheduler logic:', e)
}
// 注册插件
app.use(Antd)
app.use(router)

View File

@@ -1,5 +1,7 @@
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router'
// 同步导入调度中心,保证其模块级导出(如 handler 注册点)在应用初始化时可用
import SchedulerView from '../views/scheduler/index.vue'
import { isAppInitialized } from '@/utils/config'
let needInitLanding = true
@@ -80,7 +82,7 @@ const routes: RouteRecordRaw[] = [
{
path: '/scheduler',
name: 'Scheduler',
component: () => import('../views/scheduler/index.vue'),
component: SchedulerView,
meta: { title: '调度中心' },
},
{

View File

@@ -0,0 +1,136 @@
// schedulerHandlers.ts
// 提供在调度中心 UI 未加载前也能工作的消息处理实现。
// 核心策略:将重要事件保存到 localStorage或内存队列并暴露注册点供 UI 在挂载时接收并回放。
// no types needed here to avoid circular/unused imports
const PENDING_TABS_KEY = 'scheduler-pending-tabs'
const PENDING_COUNTDOWN_KEY = 'scheduler-pending-countdown'
const POWER_ACTION_KEY = 'scheduler-power-action'
type UIHooks = {
onNewTab?: (tab: any) => void
onCountdown?: (data: any) => void
}
let uiHooks: UIHooks = {}
export function registerSchedulerUI(hooks: UIHooks) {
uiHooks = { ...uiHooks, ...hooks }
}
// helper: push pending tab id to localStorage
function pushPendingTab(taskId: string) {
try {
const raw = localStorage.getItem(PENDING_TABS_KEY)
const arr = raw ? JSON.parse(raw) : []
if (!arr.includes(taskId)) {
arr.push(taskId)
localStorage.setItem(PENDING_TABS_KEY, JSON.stringify(arr))
}
} catch (e) {
// ignore
}
}
function popPendingTabs(): string[] {
try {
const raw = localStorage.getItem(PENDING_TABS_KEY)
if (!raw) return []
localStorage.removeItem(PENDING_TABS_KEY)
return JSON.parse(raw)
} catch (e) {
return []
}
}
function storePendingCountdown(data: any) {
try {
localStorage.setItem(PENDING_COUNTDOWN_KEY, JSON.stringify(data))
} catch (e) {
// ignore
}
}
function consumePendingCountdownAndClear(): any | null {
try {
const raw = localStorage.getItem(PENDING_COUNTDOWN_KEY)
if (!raw) return null
localStorage.removeItem(PENDING_COUNTDOWN_KEY)
return JSON.parse(raw)
} catch (e) {
return null
}
}
function savePowerAction(value: string) {
try {
localStorage.setItem(POWER_ACTION_KEY, value)
} catch (e) {
// ignore
}
}
// 导出:供 useWebSocket 在模块加载时就能使用的处理函数
export function handleTaskManagerMessage(wsMessage: any) {
if (!wsMessage || typeof wsMessage !== 'object') return
const { type, data } = wsMessage
try {
if (type === 'Signal' && data && data.newTask) {
const taskId = String(data.newTask)
// 将任务 ID 写入 pending 队列UI 在挂载时会回放
pushPendingTab(taskId)
// 如果 UI 已注册回调,则立即通知
if (uiHooks.onNewTab) {
try {
uiHooks.onNewTab({ title: `调度台自动-${taskId}`, websocketId: taskId })
} catch (e) {
console.warn('[SchedulerHandlers] onNewTab handler error:', e)
}
}
}
} catch (e) {
console.warn('[SchedulerHandlers] handleTaskManagerMessage error:', e)
}
}
export function handleMainMessage(wsMessage: any) {
if (!wsMessage || typeof wsMessage !== 'object') return
const { type, data } = wsMessage
try {
if (type === 'Message' && data && data.type === 'Countdown') {
// 存储倒计时消息,供 UI 回放
storePendingCountdown(data)
if (uiHooks.onCountdown) {
try {
uiHooks.onCountdown(data)
} catch (e) {
console.warn('[SchedulerHandlers] onCountdown handler error:', e)
}
}
} else if (type === 'Update' && data && data.PowerSign !== undefined) {
// 保存电源操作显示值(供 UI 加载时读取)
savePowerAction(String(data.PowerSign))
}
} catch (e) {
console.warn('[SchedulerHandlers] handleMainMessage error:', e)
}
}
// UI 在挂载时调用,消费并回放 pending 数据
export function consumePendingTabIds(): string[] {
return popPendingTabs()
}
export function consumePendingCountdown(): any | null {
return consumePendingCountdownAndClear()
}
export default {
handleTaskManagerMessage,
handleMainMessage,
registerSchedulerUI,
consumePendingTabIds,
consumePendingCountdown,
}

View File

@@ -3,7 +3,8 @@ import { message, Modal, notification } from 'ant-design-vue'
import { Service } from '@/api/services/Service'
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
import { PowerIn } from '@/api/models/PowerIn'
import { useWebSocket, setTaskManagerHandler, setMainMessageHandler } from '@/composables/useWebSocket'
import { useWebSocket, ExternalWSHandlers } from '@/composables/useWebSocket'
import schedulerHandlers from './schedulerHandlers'
import type { ComboBoxItem } from '@/api/models/ComboBoxItem'
import type { QueueItem, Script } from './schedulerConstants'
import {
@@ -184,7 +185,7 @@ export function useSchedulerLogic() {
title: options?.title || `调度台${tabCounter}`,
closable: true,
status: validStatus,
selectedTaskId: null,
selectedTaskId: options?.websocketId || null,
selectedMode: TaskCreateIn.mode.AutoMode,
websocketId: options?.websocketId || null,
taskQueue: [],
@@ -767,10 +768,61 @@ export function useSchedulerLogic() {
// 初始化函数
const initialize = () => {
// 设置全局WebSocket的消息处理函数
setTaskManagerHandler(handleTaskManagerMessage)
setMainMessageHandler(handleMainMessage)
// 通过 import 的 ExternalWSHandlers 直接注册处理函数,保证导入方能够永久引用并调用
ExternalWSHandlers.taskManagerMessage = handleTaskManagerMessage
ExternalWSHandlers.mainMessage = handleMainMessage
console.log('[Scheduler] 已设置全局WebSocket消息处理函数')
// 注册 UI hooks 到 schedulerHandlers使其能在 schedulerHandlers 检测到 pending 时回放到当前 UI
try {
schedulerHandlers.registerSchedulerUI({
onNewTab: (tab) => {
try {
// 创建并订阅新调度台
const newTab = addSchedulerTab({ title: tab.title, status: '运行', websocketId: tab.websocketId })
subscribeToTask(newTab)
saveTabsToStorage(schedulerTabs.value)
} catch (e) {
console.warn('[Scheduler] registerSchedulerUI onNewTab error:', e)
}
},
onCountdown: (data) => {
try {
// 直接启动前端倒计时
startPowerCountdown(data)
} catch (e) {
console.warn('[Scheduler] registerSchedulerUI onCountdown error:', e)
}
}
})
// 回放 pending tabs如果有的话
const pending = schedulerHandlers.consumePendingTabIds()
if (pending && pending.length > 0) {
pending.forEach((taskId: string) => {
try {
const newTab = addSchedulerTab({ title: `调度台${taskId}`, status: '运行', websocketId: taskId })
subscribeToTask(newTab)
} catch (e) {
console.warn('[Scheduler] replay pending tab error:', e)
}
})
saveTabsToStorage(schedulerTabs.value)
}
// 回放 pending countdown如果有的话
const pendingCountdown = schedulerHandlers.consumePendingCountdown()
if (pendingCountdown) {
try {
startPowerCountdown(pendingCountdown)
} catch (e) {
console.warn('[Scheduler] replay pending countdown error:', e)
}
}
} catch (e) {
console.warn('[Scheduler] schedulerHandlers registration failed:', e)
}
// 新增:为已有的“运行中”标签恢复 WebSocket 订阅,防止路由切换返回后不再更新
try {
schedulerTabs.value.forEach(tab => {
@@ -811,8 +863,7 @@ export function useSchedulerLogic() {
}
// 清理全局WebSocket的消息处理函数
setTaskManagerHandler(() => {})
setMainMessageHandler(() => {})
// 不再清理或重置导出的处理函数,保持使用者注册的处理逻辑永久有效
schedulerTabs.value.forEach(tab => {
if (tab.websocketId) {