feat(websocket): 实现WebSocket消息监听和调试功能

- 添加WebSocketMessageListener组件用于全局消息监听- 新增WebSocketDebugPanel调试面板(仅开发环境显示)
- 在多个组件中实现基于subscriptionId的WebSocket连接管理
-为MAA和通用配置添加更可靠的连接生命周期控制
- 引入消息测试页面用于调试WebSocket消息处理- 更新调度器逻辑以支持新的WebSocket订阅机制
- 优化WebSocket连接的创建、维护和断开流程
- 添加开发环境下的调度中心调试工具导入- 重构WebSocket相关组件的导入和注册方式
- 移除冗余的电源倒计时相关状态和方法
This commit is contained in:
MoeSnowyFox
2025-10-01 17:13:31 +08:00
parent a72ce489bd
commit 1eae8c80e5
18 changed files with 1860 additions and 566 deletions

View File

@@ -44,7 +44,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
saveConfig: (config: any) => ipcRenderer.invoke('save-config', config),
loadConfig: () => ipcRenderer.invoke('load-config'),
resetConfig: () => ipcRenderer.invoke('reset-config'),
// 托盘设置实时更新
updateTraySettings: (uiSettings: any) => ipcRenderer.invoke('update-tray-settings', uiSettings),
@@ -54,7 +54,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
getLogs: (lines?: number, fileName?: string) => ipcRenderer.invoke('get-logs', lines, fileName),
clearLogs: (fileName?: string) => ipcRenderer.invoke('clear-logs', fileName),
cleanOldLogs: (daysToKeep?: number) => ipcRenderer.invoke('clean-old-logs', daysToKeep),
// 保留原有方法以兼容现有代码
saveLogsToFile: (logs: string) => ipcRenderer.invoke('save-logs-to-file', logs),
loadLogsFromFile: () => ipcRenderer.invoke('load-logs-from-file'),

View File

@@ -9,6 +9,8 @@ import TitleBar from './components/TitleBar.vue'
import UpdateModal from './components/UpdateModal.vue'
import DevDebugPanel from './components/DevDebugPanel.vue'
import GlobalPowerCountdown from './components/GlobalPowerCountdown.vue'
import WebSocketMessageListener from './components/WebSocketMessageListener.vue'
import WebSocketDebugPanel from './components/WebSocketDebugPanel.vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
import { logger } from '@/utils/logger'
@@ -53,6 +55,12 @@ onMounted(() => {
<!-- 全局电源倒计时弹窗 -->
<GlobalPowerCountdown />
<!-- WebSocket 消息监听组件 -->
<WebSocketMessageListener />
<!-- WebSocket 调试面板 (仅开发环境) -->
<WebSocketDebugPanel v-if="$route.query.debug === 'true'" />
</ConfigProvider>
</template>

View File

@@ -102,7 +102,7 @@
ghost-class="user-ghost"
chosen-class="user-chosen"
drag-class="user-drag"
@end="evt => onUserDragEnd(evt, script)"
@end="(evt: any) => onUserDragEnd(evt, script)"
class="users-list"
>
<template #item="{ element: user }">
@@ -326,7 +326,7 @@ import { message } from 'ant-design-vue'
interface Props {
scripts: Script[]
activeConnections: Map<string, string>
activeConnections: Map<string, { subscriptionId: string; websocketId: string }>
}
interface Emits {
@@ -464,7 +464,7 @@ const onScriptDragEnd = async () => {
}
// 处理用户拖拽结束
const onUserDragEnd = async (evt: any, script: Script) => {
const onUserDragEnd = async (_evt: any, script: Script) => {
try {
const userIds = script.users?.map(user => user.id) || []
await Service.reorderUserApiScriptsUserOrderPost({

View File

@@ -0,0 +1,275 @@
<template>
<div class="websocket-debug">
<h3>WebSocket 调试面板</h3>
<div class="debug-section">
<h4>连接状态</h4>
<p>状态: {{ wsStatus }}</p>
<p>订阅数量: {{ subscriberCount }}</p>
</div>
<div class="debug-section">
<h4>测试消息</h4>
<button @click="testQuestionMessage" class="test-btn">测试 Question 消息</button>
<button @click="testNormalMessage" class="test-btn">测试普通消息</button>
<button @click="testMalformedMessage" class="test-btn">测试格式错误消息</button>
</div>
<div class="debug-section">
<h4>最近接收的消息</h4>
<div class="message-log">
<div v-for="(msg, index) in recentMessages" :key="index" class="message-item">
<div class="message-timestamp">{{ msg.timestamp }}</div>
<div class="message-content">{{ JSON.stringify(msg.data, null, 2) }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket'
import { logger } from '@/utils/logger'
const { subscribe, unsubscribe, sendRaw, getConnectionInfo } = useWebSocket()
// 状态
const wsStatus = ref('')
const subscriberCount = ref(0)
const recentMessages = ref<Array<{timestamp: string, data: any}>>([])
// 订阅ID
let debugSubscriptionId: string
// 更新状态
const updateStatus = () => {
const connInfo = getConnectionInfo()
wsStatus.value = connInfo.status
subscriberCount.value = connInfo.subscriberCount
}
// 处理接收到的消息
const handleDebugMessage = (message: WebSocketBaseMessage) => {
logger.info('[WebSocket调试] 收到消息:', message)
// 添加到最近消息列表
recentMessages.value.unshift({
timestamp: new Date().toLocaleTimeString(),
data: message
})
// 保持最近10条消息
if (recentMessages.value.length > 10) {
recentMessages.value = recentMessages.value.slice(0, 10)
}
updateStatus()
}
// 测试发送Question消息
const testQuestionMessage = () => {
const message = {
id: "debug_test_" + Date.now(),
type: "message",
data: {
type: "Question",
message_id: "q_" + Date.now(),
title: "调试测试问题",
message: "这是一个来自调试面板的测试问题,请选择是否继续?"
}
}
logger.info('[WebSocket调试] 发送Question消息:', message)
sendRaw('message', message.data)
}
// 测试发送普通消息
const testNormalMessage = () => {
const message = {
id: "debug_normal_" + Date.now(),
type: "message",
data: {
action: "test_action",
status: "running",
content: "这是一个来自调试面板的普通消息"
}
}
logger.info('[WebSocket调试] 发送普通消息:', message)
sendRaw('message', message.data)
}
// 测试发送格式错误的消息
const testMalformedMessage = () => {
const message = {
id: "debug_malformed_" + Date.now(),
type: "message",
data: {
type: "Question",
// 故意缺少 message_id
title: "格式错误的问题",
message: "这个消息缺少message_id字段测试容错处理"
}
}
logger.info('[WebSocket调试] 发送格式错误消息:', message)
sendRaw('message', message.data)
}
// 组件挂载
onMounted(() => {
logger.info('[WebSocket调试] 调试面板挂载')
// 订阅所有类型的消息进行调试
debugSubscriptionId = subscribe({}, handleDebugMessage)
updateStatus()
// 定期更新状态
const statusTimer = setInterval(updateStatus, 2000)
// 组件卸载时清理定时器
onUnmounted(() => {
clearInterval(statusTimer)
})
})
// 组件卸载
onUnmounted(() => {
logger.info('[WebSocket调试] 调试面板卸载')
if (debugSubscriptionId) {
unsubscribe(debugSubscriptionId)
}
})
</script>
<style scoped>
.websocket-debug {
position: fixed;
top: 80px;
right: 20px;
width: 350px;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
font-size: 12px;
max-height: 600px;
overflow-y: auto;
}
.debug-section {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.debug-section:last-child {
border-bottom: none;
}
h3 {
margin: 0 0 15px 0;
font-size: 14px;
color: #333;
}
h4 {
margin: 0 0 8px 0;
font-size: 12px;
color: #666;
font-weight: 600;
}
p {
margin: 4px 0;
color: #555;
}
.test-btn {
background: #007bff;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
margin: 2px;
}
.test-btn:hover {
background: #0056b3;
}
.message-log {
max-height: 200px;
overflow-y: auto;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 8px;
}
.message-item {
margin-bottom: 8px;
padding: 6px;
background: white;
border-radius: 3px;
border-left: 3px solid #007bff;
}
.message-item:last-child {
margin-bottom: 0;
}
.message-timestamp {
font-size: 10px;
color: #666;
margin-bottom: 4px;
}
.message-content {
font-family: monospace;
font-size: 10px;
color: #333;
white-space: pre-wrap;
word-break: break-word;
}
/* 暗色主题适配 */
@media (prefers-color-scheme: dark) {
.websocket-debug {
background: #2a2a2a;
border-color: #444;
color: white;
}
.debug-section {
border-bottom-color: #444;
}
h3, h4 {
color: #e8e8e8;
}
p {
color: #ccc;
}
.message-log {
background: #333;
border-color: #555;
}
.message-item {
background: #444;
}
.message-content {
color: #e8e8e8;
}
}
</style>

View File

@@ -0,0 +1,359 @@
<template>
<div style="display: none">
<!-- 这是一个隐藏的监听组件不需要UI -->
</div>
<!-- 简单的自定义对话框 -->
<div v-if="showDialog" class="dialog-overlay" @click.self="showDialog = false">
<div class="dialog-container">
<div class="dialog-header">
<h3>{{ dialogData.title }}</h3>
</div>
<div class="dialog-content">
<p>{{ dialogData.message }}</p>
</div>
<div class="dialog-actions">
<button
v-for="(option, index) in dialogData.options"
:key="index"
class="dialog-button"
@click="handleChoice(index)"
>
{{ option }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, nextTick } from 'vue'
import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket'
import { logger } from '@/utils/logger'
// WebSocket hook
const { subscribe, unsubscribe, sendRaw } = useWebSocket()
// 对话框状态
const showDialog = ref(false)
const dialogData = ref({
title: '',
message: '',
options: ['确定', '取消'],
messageId: ''
})
// 存储订阅ID用于取消订阅
let subscriptionId: string
// 发送用户选择结果到后端
const sendResponse = (messageId: string, choice: boolean) => {
const response = {
message_id: messageId,
choice: choice,
}
logger.info('[WebSocket消息监听器] 发送用户选择结果:', response)
// 发送响应消息到后端
sendRaw('Response', response)
}
// 处理用户选择
const handleChoice = (choiceIndex: number) => {
const choice = choiceIndex === 0 // 第一个选项为true其他为false
sendResponse(dialogData.value.messageId, choice)
showDialog.value = false
}
// 显示问题对话框
const showQuestion = (questionData: any) => {
const title = questionData.title || '操作提示'
const message = questionData.message || ''
const options = questionData.options || ['确定', '取消']
const messageId = questionData.message_id || 'fallback_' + Date.now()
logger.info('[WebSocket消息监听器] 显示自定义对话框:', questionData)
// 设置对话框数据
dialogData.value = {
title,
message,
options,
messageId
}
showDialog.value = true
// 在下一个tick自动聚焦第一个按钮
nextTick(() => {
const firstButton = document.querySelector('.dialog-button:first-child') as HTMLButtonElement
if (firstButton) {
firstButton.focus()
}
})
}
// 消息处理函数
const handleMessage = (message: WebSocketBaseMessage) => {
try {
logger.info('[WebSocket消息监听器] 收到Message类型消息:', message)
logger.info('[WebSocket消息监听器] 消息详情 - type:', message.type, 'id:', message.id, 'data:', message.data)
// 解析消息数据
if (message.data) {
console.log('[WebSocket消息监听器] 消息数据:', message.data)
// 根据具体的消息内容进行处理
if (typeof message.data === 'object') {
// 处理对象类型的数据
handleObjectMessage(message.data)
} else if (typeof message.data === 'string') {
// 处理字符串类型的数据
handleStringMessage(message.data)
} else {
// 处理其他类型的数据
handleOtherMessage(message.data)
}
} else {
logger.warn('[WebSocket消息监听器] 收到空数据的消息')
}
// 这里可以添加具体的业务逻辑
// 例如:更新状态、触发事件、显示通知等
} catch (error) {
logger.error('[WebSocket消息监听器] 处理消息时发生错误:', error)
}
}
// 处理对象类型的消息
const handleObjectMessage = (data: any) => {
logger.info('[WebSocket消息监听器] 处理对象消息:', data)
// 检查是否为Question类型的消息
logger.info('[WebSocket消息监听器] 检查消息类型 - data.type:', data.type, 'data.message_id:', data.message_id)
if (data.type === 'Question') {
logger.info('[WebSocket消息监听器] 发现Question类型消息')
if (data.message_id) {
logger.info('[WebSocket消息监听器] message_id存在显示选择弹窗')
showQuestion(data)
return
} else {
logger.warn('[WebSocket消息监听器] Question消息缺少message_id字段:', data)
// 即使缺少message_id也尝试显示弹窗使用当前时间戳作为ID
const fallbackId = 'fallback_' + Date.now()
logger.info('[WebSocket消息监听器] 使用备用ID显示弹窗:', fallbackId)
showQuestion({
...data,
message_id: fallbackId
})
return
}
}
// 根据对象的属性进行不同处理
if (data.action) {
logger.info('[WebSocket消息监听器] 消息动作:', data.action)
}
if (data.status) {
logger.info('[WebSocket消息监听器] 消息状态:', data.status)
}
if (data.content) {
logger.info('[WebSocket消息监听器] 消息内容:', data.content)
}
// 可以根据具体需求添加更多处理逻辑
}
// 处理字符串类型的消息
const handleStringMessage = (data: string) => {
logger.info('[WebSocket消息监听器] 处理字符串消息:', data)
try {
// 尝试解析JSON字符串
const parsed = JSON.parse(data)
logger.info('[WebSocket消息监听器] 解析后的JSON:', parsed)
handleObjectMessage(parsed)
} catch (error) {
// 不是JSON格式作为普通字符串处理
logger.info('[WebSocket消息监听器] 普通字符串消息:', data)
}
}
// 处理其他类型的消息
const handleOtherMessage = (data: any) => {
logger.info('[WebSocket消息监听器] 处理其他类型消息:', typeof data, data)
}
// 组件挂载时订阅消息
onMounted(() => {
logger.info('[WebSocket消息监听器~~] 组件挂载开始监听Message类型的消息')
// 使用新的 subscribe API订阅 Message 类型的消息注意大写M
subscriptionId = subscribe({ type: 'Message' }, handleMessage)
logger.info('[WebSocket消息监听器~~] 订阅ID:', subscriptionId)
logger.info('[WebSocket消息监听器~~] 订阅过滤器:', { type: 'Message' })
})
// 组件卸载时取消订阅
onUnmounted(() => {
logger.info('[WebSocket消息监听器~~] 组件卸载停止监听Message类型的消息')
// 使用新的 unsubscribe API
if (subscriptionId) {
unsubscribe(subscriptionId)
}
})
</script>
<style scoped>
/* 对话框遮罩层 */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
/* 对话框容器 */
.dialog-container {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
min-width: 300px;
max-width: 500px;
width: 90%;
animation: dialogAppear 0.2s ease-out;
}
/* 对话框头部 */
.dialog-header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.dialog-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
/* 对话框内容 */
.dialog-content {
padding: 20px;
}
.dialog-content p {
margin: 0;
font-size: 14px;
line-height: 1.5;
color: #666;
word-break: break-word;
}
/* 按钮区域 */
.dialog-actions {
padding: 12px 20px 20px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* 按钮样式 */
.dialog-button {
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
color: #333;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
min-width: 60px;
}
.dialog-button:hover {
background: #f5f5f5;
border-color: #999;
}
.dialog-button:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
.dialog-button:first-child {
background: #007bff;
color: white;
border-color: #007bff;
}
.dialog-button:first-child:hover {
background: #0056b3;
border-color: #0056b3;
}
/* 出现动画 */
@keyframes dialogAppear {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* 暗色主题适配 */
@media (prefers-color-scheme: dark) {
.dialog-container {
background: #2d2d2d;
color: #fff;
}
.dialog-header {
border-bottom-color: #444;
}
.dialog-header h3 {
color: #fff;
}
.dialog-content p {
color: #ccc;
}
.dialog-button {
background: #444;
color: #fff;
border-color: #555;
}
.dialog-button:hover {
background: #555;
border-color: #666;
}
.dialog-button:first-child {
background: #0d6efd;
border-color: #0d6efd;
}
.dialog-button:first-child:hover {
background: #0b5ed7;
border-color: #0b5ed7;
}
}
</style>

View File

@@ -0,0 +1 @@
.log-entry.error .log-message {

View File

@@ -0,0 +1,654 @@
<template>
<div class="test-page">
<h3 class="page-title">🔧 消息弹窗测试</h3>
<div class="test-section">
<h4>测试消息弹窗</h4>
<div class="test-controls">
<button class="test-btn primary" @click="triggerQuestionModal" :disabled="isTesting">
{{ isTesting ? '测试中...' : '触发Question弹窗' }}
</button>
<button class="test-btn secondary" @click="triggerCustomModal" :disabled="isTesting">
自定义消息测试
</button>
<button class="test-btn warning" @click="directTriggerModal" :disabled="isTesting">
直接触发测试
</button>
</div>
<div class="test-info">
<p>点击按钮测试全屏消息选择弹窗功能</p>
<p>最后响应: {{ lastResponse || '暂无' }}</p>
<p>连接状态: <span :class="connectionStatusClass">{{ connectionStatus }}</span></p>
</div>
</div>
<div class="test-section">
<h4>自定义测试消息</h4>
<div class="custom-form">
<div class="form-group">
<label>标题:</label>
<input
v-model="customMessage.title"
type="text"
placeholder="请输入弹窗标题"
class="form-input"
/>
</div>
<div class="form-group">
<label>消息内容:</label>
<textarea
v-model="customMessage.message"
placeholder="请输入消息内容"
class="form-textarea"
rows="3"
></textarea>
</div>
<button
class="test-btn primary"
@click="sendCustomMessage"
:disabled="!customMessage.title || !customMessage.message"
>
发送自定义消息
</button>
</div>
</div>
<div class="test-section">
<h4>测试历史</h4>
<div class="test-history">
<div v-for="(test, index) in testHistory" :key="index" class="history-item">
<div class="history-time">{{ test.time }}</div>
<div class="history-content">{{ test.title }} - {{ test.result }}</div>
</div>
<div v-if="testHistory.length === 0" class="no-history">暂无测试历史</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import { useWebSocket } from '@/composables/useWebSocket'
import { logger } from '@/utils/logger'
const { subscribe, unsubscribe, sendRaw, getConnectionInfo } = useWebSocket()
// 测试状态
const isTesting = ref(false)
const lastResponse = ref('')
const testHistory = ref<Array<{ time: string; title: string; result: string }>>([])
const connectionStatus = ref('检查中...')
const connectionStatusClass = ref('status-checking')
// 自定义消息
const customMessage = ref({
title: '操作确认',
message: '请确认是否继续执行此操作?',
})
// 更新连接状态
const updateConnectionStatus = () => {
try {
const connInfo = getConnectionInfo()
connectionStatus.value = connInfo.status
switch (connInfo.status) {
case '已连接':
connectionStatusClass.value = 'status-connected'
break
case '连接中':
connectionStatusClass.value = 'status-connecting'
break
case '已断开':
connectionStatusClass.value = 'status-disconnected'
break
case '连接错误':
connectionStatusClass.value = 'status-error'
break
default:
connectionStatusClass.value = 'status-unknown'
}
} catch (error) {
connectionStatus.value = '获取失败'
connectionStatusClass.value = 'status-error'
}
}
// 存储订阅ID用于监听响应
let responseSubscriptionId: string
// 生成唯一ID
const generateId = () => {
return 'test-' + Math.random().toString(36).substr(2, 9)
}
// 格式化时间
const formatTime = () => {
return new Date().toLocaleTimeString()
}
// 添加测试历史
const addTestHistory = (title: string, result: string) => {
testHistory.value.unshift({
time: formatTime(),
title,
result,
})
// 保持最多10条历史记录
if (testHistory.value.length > 10) {
testHistory.value = testHistory.value.slice(0, 10)
}
}
// 直接触发弹窗(备用方法)
const directTriggerModal = () => {
isTesting.value = true
try {
// 直接触发浏览器的confirm对话框作为备用测试
const result = confirm('这是直接触发的测试弹窗。\n\n如果WebSocket消息弹窗无法正常工作这个方法可以用来验证基本功能。\n\n点击"确定"继续,点击"取消"退出。')
lastResponse.value = result ? '用户选择: 确认 (直接触发)' : '用户选择: 取消 (直接触发)'
addTestHistory('直接触发测试', result ? '确认' : '取消')
logger.info('[调试工具] 直接触发测试完成,结果:', result)
} catch (error: any) {
logger.error('[调试工具] 直接触发测试失败:', error)
lastResponse.value = '直接触发失败: ' + (error?.message || '未知错误')
}
setTimeout(() => {
isTesting.value = false
}, 1000)
}
// 发送WebSocket消息来模拟接收消息
const simulateMessage = (messageData: any) => {
logger.info('[调试工具] 发送模拟消息:', messageData)
// 检查连接状态
const connInfo = getConnectionInfo()
if (connInfo.status !== '已连接') {
logger.warn('[调试工具] WebSocket未连接无法发送消息')
lastResponse.value = '发送失败: WebSocket未连接'
return
}
try {
// 使用sendRaw直接发送Message类型的消息
sendRaw('Message', messageData)
logger.info('[调试工具] 消息已发送到WebSocket')
lastResponse.value = '消息已发送,等待弹窗显示...'
} catch (error: any) {
logger.error('[调试工具] 发送消息失败:', error)
lastResponse.value = '发送失败: ' + (error?.message || '未知错误')
}
}
// 触发标准Question弹窗
const triggerQuestionModal = () => {
isTesting.value = true
const testMessageData = {
message_id: generateId(),
type: 'Question',
title: '测试提示',
message: '这是一个测试消息,请选择您的操作。',
}
logger.info('[调试工具] 发送测试Question消息:', testMessageData)
// 直接模拟接收消息
simulateMessage(testMessageData)
lastResponse.value = '已发送测试Question消息'
addTestHistory('标准Question测试', '已发送')
setTimeout(() => {
isTesting.value = false
}, 1000)
}
// 触发自定义弹窗
const triggerCustomModal = () => {
isTesting.value = true
const testMessageData = {
message_id: generateId(),
type: 'Question',
title: '自定义测试',
message:
'这是一个自定义的测试消息,用于验证弹窗的不同内容显示。您可以测试长文本、特殊字符等情况。',
}
logger.info('[调试工具] 发送自定义测试消息:', testMessageData)
simulateMessage(testMessageData)
lastResponse.value = '已发送自定义测试消息'
addTestHistory('自定义内容测试', '已发送')
setTimeout(() => {
isTesting.value = false
}, 1000)
}
// 发送完全自定义的消息
const sendCustomMessage = () => {
if (!customMessage.value.title || !customMessage.value.message) {
return
}
isTesting.value = true
const testMessageData = {
message_id: generateId(),
type: 'Question',
title: customMessage.value.title,
message: customMessage.value.message,
}
logger.info('[调试工具] 发送用户自定义消息:', testMessageData)
simulateMessage(testMessageData)
lastResponse.value = `已发送自定义消息: ${customMessage.value.title}`
addTestHistory(`自定义: ${customMessage.value.title}`, '已发送')
setTimeout(() => {
isTesting.value = false
}, 1000)
}
// 监听响应消息
const handleResponseMessage = (message: any) => {
logger.info('[调试工具] 收到响应消息:', message)
if (message.data && message.data.choice !== undefined) {
const choice = message.data.choice ? '确认' : '取消'
lastResponse.value = `用户选择: ${choice}`
addTestHistory('用户响应', choice)
}
}
// 组件挂载时订阅响应消息
onMounted(() => {
logger.info('[调试工具] 初始化消息测试页面')
// 订阅Response类型的消息来监听用户的选择结果
responseSubscriptionId = subscribe({ type: 'Response' }, handleResponseMessage)
// 初始化连接状态
updateConnectionStatus()
// 定期更新连接状态
const statusTimer = setInterval(updateConnectionStatus, 2000)
logger.info('[调试工具] 已订阅Response消息订阅ID:', responseSubscriptionId)
// 清理定时器
onUnmounted(() => {
clearInterval(statusTimer)
})
})
// 组件卸载时清理订阅
onUnmounted(() => {
if (responseSubscriptionId) {
unsubscribe(responseSubscriptionId)
logger.info('[调试工具] 已取消Response消息订阅')
}
})
</script>
<style scoped>
.test-page {
color: #fff;
}
.page-title {
margin: 0 0 16px 0;
font-size: 14px;
color: #4caf50;
}
.test-section {
margin-bottom: 20px;
padding: 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.test-section h4 {
margin: 0 0 12px 0;
font-size: 12px;
color: #e0e0e0;
}
.test-controls {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.test-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
transition: all 0.2s ease;
min-width: 80px;
}
.test-btn.primary {
background: #4caf50;
color: white;
}
.test-btn.primary:hover:not(:disabled) {
background: #45a049;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
}
.test-btn.secondary {
background: #2196f3;
color: white;
}
.test-btn.secondary:hover:not(:disabled) {
background: #1976d2;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.3);
}
.test-btn.warning {
background: #ff9800;
color: white;
}
.test-btn.warning:hover:not(:disabled) {
background: #f57c00;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(255, 152, 0, 0.3);
}
.test-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.test-btn:active:not(:disabled) {
transform: translateY(0);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}
.test-info {
font-size: 10px;
color: #bbb;
}
.test-info p {
margin: 4px 0;
}
/* 连接状态样式 */
.status-connected {
color: #4caf50;
font-weight: 600;
}
.status-connecting {
color: #ff9800;
font-weight: 600;
}
.status-disconnected {
color: #f44336;
font-weight: 600;
}
.status-error {
color: #e91e63;
font-weight: 600;
}
.status-checking,
.status-unknown {
color: #9e9e9e;
font-weight: 600;
}
.custom-form {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.form-group label {
font-size: 10px;
color: #ccc;
font-weight: 500;
}
.form-input,
.form-textarea {
padding: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
background: rgba(255, 255, 255, 0.08);
color: #fff;
font-size: 11px;
transition: all 0.2s ease;
}
.form-input:focus,
.form-textarea:focus {
outline: none;
border-color: #4caf50;
background: rgba(255, 255, 255, 0.12);
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
.form-input::placeholder,
.form-textarea::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.form-textarea {
resize: vertical;
min-height: 60px;
font-family: inherit;
}
.test-history {
max-height: 120px;
overflow-y: auto;
border-radius: 4px;
}
.test-history::-webkit-scrollbar {
width: 4px;
}
.test-history::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 2px;
}
.test-history::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
}
.test-history::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
.history-item {
display: flex;
justify-content: space-between;
padding: 6px 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
font-size: 10px;
transition: background-color 0.2s ease;
border-radius: 3px;
margin-bottom: 2px;
}
.history-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.history-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.history-time {
color: #888;
min-width: 60px;
font-weight: 500;
}
.history-content {
color: #ccc;
flex: 1;
margin-left: 8px;
}
.no-history {
text-align: center;
color: #666;
font-size: 10px;
padding: 16px 0;
font-style: italic;
}
/* 暗色主题专用样式增强 */
@media (prefers-color-scheme: dark) {
.test-page {
color: #e8e8e8;
}
.page-title {
color: #66bb6a;
text-shadow: 0 0 8px rgba(102, 187, 106, 0.3);
}
.test-section {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(8px);
}
.test-section h4 {
color: #f0f0f0;
}
.test-btn.primary {
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%);
border: 1px solid rgba(76, 175, 80, 0.3);
}
.test-btn.primary:hover:not(:disabled) {
background: linear-gradient(135deg, #45a049 0%, #5cb85c 100%);
border-color: rgba(76, 175, 80, 0.5);
}
.test-btn.secondary {
background: linear-gradient(135deg, #2196f3 0%, #42a5f5 100%);
border: 1px solid rgba(33, 150, 243, 0.3);
}
.test-btn.secondary:hover:not(:disabled) {
background: linear-gradient(135deg, #1976d2 0%, #1e88e5 100%);
border-color: rgba(33, 150, 243, 0.5);
}
.form-input,
.form-textarea {
background: rgba(0, 0, 0, 0.2);
border-color: rgba(255, 255, 255, 0.15);
}
.form-input:focus,
.form-textarea:focus {
background: rgba(0, 0, 0, 0.3);
border-color: #66bb6a;
box-shadow: 0 0 0 2px rgba(102, 187, 106, 0.2);
}
.history-item {
background: rgba(255, 255, 255, 0.02);
border-bottom-color: rgba(255, 255, 255, 0.06);
}
.history-item:hover {
background: rgba(255, 255, 255, 0.08);
}
.history-time {
color: #aaa;
}
.history-content {
color: #ddd;
}
}
/* 高对比度模式适配 */
@media (prefers-contrast: high) {
.test-section {
border-width: 2px;
border-color: rgba(255, 255, 255, 0.3);
}
.test-btn {
border: 2px solid currentColor;
font-weight: 600;
}
.form-input,
.form-textarea {
border-width: 2px;
}
.form-input:focus,
.form-textarea:focus {
border-width: 2px;
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.3);
}
}
/* 减少动画模式适配 */
@media (prefers-reduced-motion: reduce) {
.test-btn,
.form-input,
.form-textarea,
.history-item {
transition: none;
}
.test-btn:hover:not(:disabled) {
transform: none;
}
.page-title {
text-shadow: none;
}
}
</style>

View File

@@ -43,12 +43,14 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import RouteInfoPage from './RouteInfoPage.vue'
import EnvironmentPage from './EnvironmentPage.vue'
import QuickNavPage from './QuickNavPage.vue'
import MessageTestPage from './MessageTestPage.vue'
// 调试页面配置
const tabs = [
{ key: 'route', title: '路由', icon: '🛣️', component: RouteInfoPage },
{ key: 'env', title: '环境', icon: '⚙️', component: EnvironmentPage },
{ key: 'nav', title: '导航', icon: '🚀', component: QuickNavPage },
{ key: 'message', title: '消息', icon: '💬', component: MessageTestPage },
]
// 开发环境检测

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@ import { OpenAPI } from '@/api'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
@@ -15,11 +14,13 @@ import { logger } from '@/utils/logger'
// 导入镜像管理器
import { mirrorManager } from '@/utils/mirrorManager'
// 导入WebSocket消息监听组件
import WebSocketMessageListener from '@/components/WebSocketMessageListener.vue'
import { API_ENDPOINTS } from '@/config/mirrors'
// 配置dayjs中文本地化
dayjs.locale('zh-cn')
import { API_ENDPOINTS } from '@/config/mirrors'
// 配置API基础URL
OpenAPI.BASE = API_ENDPOINTS.local
@@ -28,11 +29,14 @@ logger.info('前端应用开始初始化')
logger.info(`API基础URL: ${OpenAPI.BASE}`)
// 初始化镜像管理器(异步)
mirrorManager.initialize().then(() => {
logger.info('镜像管理器初始化完成')
}).catch((error) => {
logger.error('镜像管理器初始化失败:', error)
})
mirrorManager
.initialize()
.then(() => {
logger.info('镜像管理器初始化完成')
})
.catch(error => {
logger.error('镜像管理器初始化失败:', error)
})
// 创建应用实例
const app = createApp(App)
@@ -66,4 +70,7 @@ app.config.errorHandler = (err, instance, info) => {
// 挂载应用
app.mount('#app')
// 注册WebSocket消息监听组件
app.component('WebSocketMessageListener', WebSocketMessageListener)
logger.info('前端应用初始化完成')

View File

@@ -0,0 +1,77 @@
// 调度中心调试工具
export function debugScheduler() {
console.log('=== 调度中心调试信息 ===')
// 检查WebSocket连接状态
const wsStorage = (window as any)[Symbol.for('GLOBAL_WEBSOCKET_PERSISTENT')]
if (wsStorage) {
console.log('WebSocket状态:', wsStorage.status.value)
console.log('WebSocket连接ID:', wsStorage.connectionId)
console.log('订阅数量:', wsStorage.subscriptions.value.size)
console.log('缓存标记数量:', wsStorage.cacheMarkers.value.size)
console.log('缓存消息数量:', wsStorage.cachedMessages.value.length)
// 列出所有订阅
console.log('当前订阅:')
wsStorage.subscriptions.value.forEach((sub, id) => {
console.log(` - ${id}: type=${sub.filter.type}, id=${sub.filter.id}`)
})
} else {
console.log('WebSocket存储未初始化')
}
// 检查调度中心状态
const scheduler = document.querySelector('[data-scheduler-debug]')
if (scheduler) {
console.log('调度中心组件已挂载')
} else {
console.log('调度中心组件未找到')
}
}
// 测试WebSocket连接
export function testWebSocketConnection() {
console.log('=== 测试WebSocket连接 ===')
try {
const ws = new WebSocket('ws://localhost:36163/api/core/ws')
ws.onopen = () => {
console.log('✅ WebSocket连接成功')
ws.send(JSON.stringify({
type: 'Signal',
data: { Connect: true, connectionId: 'test-connection' }
}))
}
ws.onmessage = (event) => {
const message = JSON.parse(event.data)
console.log('📩 收到消息:', message)
}
ws.onerror = (error) => {
console.log('❌ WebSocket错误:', error)
}
ws.onclose = (event) => {
console.log('🔌 WebSocket连接关闭:', event.code, event.reason)
}
// 5秒后关闭测试连接
setTimeout(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.close()
console.log('🔌 测试连接已关闭')
}
}, 5000)
} catch (error) {
console.log('❌ 无法创建WebSocket连接:', error)
}
}
// 在控制台中暴露调试函数
if (typeof window !== 'undefined') {
(window as any).debugScheduler = debugScheduler;
(window as any).testWebSocketConnection = testWebSocketConnection;
}

View File

@@ -16,7 +16,11 @@
</a-breadcrumb>
</div>
<a-space size="middle">
// 如果已有连接,先断开
if (generalSubscriptionId.value) {
unsubscribe(generalSubscriptionId.value)
generalSubscriptionId.value = null
generalWebsocketId.value = nulla-space size="middle">
<a-button
type="primary"
ghost
@@ -390,6 +394,7 @@ const scriptName = ref('')
// 通用配置相关
const generalConfigLoading = ref(false)
const generalSubscriptionId = ref<string | null>(null)
const generalWebsocketId = ref<string | null>(null)
const showGeneralConfigMask = ref(false)
let generalConfigTimeout: number | null = null
@@ -599,12 +604,14 @@ const handleGeneralConfig = async () => {
console.debug('订阅 websocketId:', wsId)
// 订阅 websocket
subscribe(wsId, {
onMessage: (wsMessage: any) => {
const subscriptionId = subscribe(
{ id: wsId },
(wsMessage: any) => {
if (wsMessage.type === 'error') {
console.error(`用户 ${formData.userName} 通用配置错误:`, wsMessage.data)
message.error(`通用配置连接失败: ${wsMessage.data}`)
unsubscribe(wsId)
unsubscribe(subscriptionId)
generalSubscriptionId.value = null
generalWebsocketId.value = null
showGeneralConfigMask.value = false
return
@@ -612,13 +619,15 @@ const handleGeneralConfig = async () => {
if (wsMessage.data && wsMessage.data.Accomplish) {
message.success(`用户 ${formData.userName} 的配置已完成`)
unsubscribe(wsId)
unsubscribe(subscriptionId)
generalSubscriptionId.value = null
generalWebsocketId.value = null
showGeneralConfigMask.value = false
}
},
})
}
)
generalSubscriptionId.value = subscriptionId
generalWebsocketId.value = wsId
showGeneralConfigMask.value = true
message.success(`已开始配置用户 ${formData.userName} 的通用设置`)
@@ -626,9 +635,9 @@ const handleGeneralConfig = async () => {
// 设置 30 分钟超时自动断开
generalConfigTimeout = window.setTimeout(
() => {
if (generalWebsocketId.value) {
const id = generalWebsocketId.value
unsubscribe(id)
if (generalSubscriptionId.value) {
unsubscribe(generalSubscriptionId.value)
generalSubscriptionId.value = null
generalWebsocketId.value = null
showGeneralConfigMask.value = false
message.info(`用户 ${formData.userName} 的配置会话已超时断开`)
@@ -658,7 +667,10 @@ const handleSaveGeneralConfig = async () => {
const response = await Service.stopTaskApiDispatchStopPost({ taskId: websocketId })
if (response && response.code === 200) {
unsubscribe(websocketId)
if (generalSubscriptionId.value) {
unsubscribe(generalSubscriptionId.value)
generalSubscriptionId.value = null
}
generalWebsocketId.value = null
showGeneralConfigMask.value = false
if (generalConfigTimeout) {
@@ -720,8 +732,9 @@ const handleWebhookChange = () => {
}
const handleCancel = () => {
if (generalWebsocketId.value) {
unsubscribe(generalWebsocketId.value)
if (generalSubscriptionId.value) {
unsubscribe(generalSubscriptionId.value)
generalSubscriptionId.value = null
generalWebsocketId.value = null
showGeneralConfigMask.value = false
if (generalConfigTimeout) {

View File

@@ -166,6 +166,7 @@ const scriptName = ref('')
// MAA配置相关
const maaConfigLoading = ref(false)
const maaSubscriptionId = ref<string | null>(null)
const maaWebsocketId = ref<string | null>(null)
const showMAAConfigMask = ref(false)
let maaConfigTimeout: number | null = null
@@ -763,8 +764,9 @@ const handleMAAConfig = async () => {
maaConfigLoading.value = true
// 如果已有连接,先断开
if (maaWebsocketId.value) {
unsubscribe(maaWebsocketId.value)
if (maaSubscriptionId.value) {
unsubscribe(maaSubscriptionId.value)
maaSubscriptionId.value = null
maaWebsocketId.value = null
showMAAConfigMask.value = false
if (maaConfigTimeout) {
@@ -783,15 +785,17 @@ const handleMAAConfig = async () => {
const wsId = response.websocketId
// 订阅 websocket
subscribe(wsId, {
onMessage: (wsMessage: any) => {
const subscriptionId = subscribe(
{ id: wsId },
(wsMessage: any) => {
if (wsMessage.type === 'error') {
console.error(
`用户 ${formData.Info?.Name || formData.userName} MAA配置错误:`,
wsMessage.data
)
message.error(`MAA配置连接失败: ${wsMessage.data}`)
unsubscribe(wsId)
unsubscribe(subscriptionId)
maaSubscriptionId.value = null
maaWebsocketId.value = null
showMAAConfigMask.value = false
return
@@ -799,13 +803,15 @@ const handleMAAConfig = async () => {
if (wsMessage.data && wsMessage.data.Accomplish) {
message.success(`用户 ${formData.Info?.Name || formData.userName} 的配置已完成`)
unsubscribe(wsId)
unsubscribe(subscriptionId)
maaSubscriptionId.value = null
maaWebsocketId.value = null
showMAAConfigMask.value = false
}
},
})
}
)
maaSubscriptionId.value = subscriptionId
maaWebsocketId.value = wsId
showMAAConfigMask.value = true
message.success(`已开始配置用户 ${formData.Info?.Name || formData.userName} 的MAA设置`)
@@ -813,9 +819,9 @@ const handleMAAConfig = async () => {
// 设置 30 分钟超时自动断开
maaConfigTimeout = window.setTimeout(
() => {
if (maaWebsocketId.value) {
const id = maaWebsocketId.value
unsubscribe(id)
if (maaSubscriptionId.value) {
unsubscribe(maaSubscriptionId.value)
maaSubscriptionId.value = null
maaWebsocketId.value = null
showMAAConfigMask.value = false
message.info(`用户 ${formData.Info?.Name || formData.userName} 的配置会话已超时断开`)
@@ -845,7 +851,10 @@ const handleSaveMAAConfig = async () => {
const response = await Service.stopTaskApiDispatchStopPost({ taskId: websocketId })
if (response && response.code === 200) {
unsubscribe(websocketId)
if (maaSubscriptionId.value) {
unsubscribe(maaSubscriptionId.value)
maaSubscriptionId.value = null
}
maaWebsocketId.value = null
showMAAConfigMask.value = false
if (maaConfigTimeout) {
@@ -970,8 +979,9 @@ const addCustomStageRemain = (stageName: string) => {
}
const handleCancel = () => {
if (maaWebsocketId.value) {
unsubscribe(maaWebsocketId.value)
if (maaSubscriptionId.value) {
unsubscribe(maaSubscriptionId.value)
maaSubscriptionId.value = null
maaWebsocketId.value = null
}
router.push('/scripts')

View File

@@ -41,7 +41,7 @@
<!-- 空状态 -->
<!-- 增加 loadedOnce 条件避免初始渲染时闪烁 -->
<div v-if="!loading && loadedOnce && scripts.length === 0" class="empty-state">
<div v-if="!addLoading && loadedOnce && scripts.length === 0" class="empty-state">
<div class="empty-content">
<div class="empty-image-container">
<img src="@/assets/NoData.png" alt="暂无数据" class="empty-image" />
@@ -287,7 +287,7 @@ const showMAAConfigMask = ref(false) // 控制MAA配置遮罩层的显示
const currentConfigScript = ref<Script | null>(null) // 当前正在配置的脚本
// WebSocket连接管理
const activeConnections = ref<Map<string, string>>(new Map()) // scriptId -> websocketId
const activeConnections = ref<Map<string, { subscriptionId: string; websocketId: string }>>(new Map()) // scriptId -> { subscriptionId, websocketId }
// 解析模板描述的markdown
const parseMarkdown = (text: string) => {
@@ -548,8 +548,9 @@ const handleStartMAAConfig = async (script: Script) => {
currentConfigScript.value = script
// 订阅WebSocket消息
subscribe(response.websocketId, {
onMessage: (wsMessage: any) => {
const subscriptionId = subscribe(
{ id: response.websocketId },
(wsMessage: any) => {
// 处理错误消息
if (wsMessage.type === 'error') {
console.error(`脚本 ${script.name} 连接错误:`, wsMessage.data)
@@ -569,20 +570,23 @@ const handleStartMAAConfig = async (script: Script) => {
showMAAConfigMask.value = false
currentConfigScript.value = null
}
},
})
}
)
// 记录连接和websocketId
activeConnections.value.set(script.id, response.websocketId)
// 记录连接和subscriptionId
activeConnections.value.set(script.id, {
subscriptionId,
websocketId: response.websocketId
})
message.success(`已启动 ${script.name} 的MAA配置`)
// 设置自动断开连接的定时器30分钟后
setTimeout(
() => {
if (activeConnections.value.has(script.id)) {
const wsId = activeConnections.value.get(script.id)
if (wsId) {
unsubscribe(wsId)
const connection = activeConnections.value.get(script.id)
if (connection) {
unsubscribe(connection.subscriptionId)
}
activeConnections.value.delete(script.id)
// 超时时隐藏遮罩
@@ -604,20 +608,20 @@ const handleStartMAAConfig = async (script: Script) => {
const handleSaveMAAConfig = async (script: Script) => {
try {
const websocketId = activeConnections.value.get(script.id)
if (!websocketId) {
const connection = activeConnections.value.get(script.id)
if (!connection) {
message.error('未找到活动的配置会话')
return
}
// 调用停止配置任务API
const response = await Service.stopTaskApiDispatchStopPost({
taskId: websocketId,
taskId: connection.websocketId,
})
if (response.code === 200) {
// 取消订阅
unsubscribe(websocketId)
unsubscribe(connection.subscriptionId)
activeConnections.value.delete(script.id)
// 隐藏遮罩

View File

@@ -50,7 +50,7 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { ref, watch } from 'vue'
import { PlayCircleOutlined, StopOutlined } from '@ant-design/icons-vue'
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
import type { ComboBoxItem } from '@/api/models/ComboBoxItem'
@@ -88,11 +88,6 @@ const localSelectedMode = ref(props.selectedMode)
// 模式选项
const modeOptions = TASK_MODE_OPTIONS
// 计算属性
const canStart = computed(() => {
return !!(localSelectedTaskId.value && localSelectedMode.value) && !props.disabled
})
// 监听 props 变化,同步到本地状态
watch(
() => props.selectedTaskId,

View File

@@ -120,7 +120,6 @@
import { onMounted, onUnmounted } from 'vue'
import { LockOutlined } from '@ant-design/icons-vue'
import {
getPowerActionText,
POWER_ACTION_TEXT,
TAB_STATUS_COLOR,
} from './schedulerConstants'
@@ -137,8 +136,6 @@ const {
taskOptionsLoading,
taskOptions,
powerAction,
powerCountdownVisible,
powerCountdownData,
messageModalVisible,
currentMessage,
messageResponse,
@@ -160,7 +157,6 @@ const {
// 电源操作
onPowerActionChange,
cancelPowerAction,
// 消息操作
sendMessageResponse,
@@ -189,6 +185,13 @@ const onSchedulerTabEdit = (targetKey: string | MouseEvent, action: 'add' | 'rem
onMounted(() => {
initialize() // 初始化TaskManager订阅
loadTaskOptions()
// 开发环境下导入调试工具
if (process.env.NODE_ENV === 'development') {
import('@/utils/scheduler-debug').then(() => {
console.log('调度中心调试工具已加载,使用 debugScheduler() 和 testWebSocketConnection() 进行调试')
})
}
})
onUnmounted(() => {

View File

@@ -78,6 +78,7 @@ export interface SchedulerTab {
selectedTaskId: string | null
selectedMode: TaskCreateIn.mode | null
websocketId: string | null
subscriptionId?: string | null
taskQueue: QueueItem[]
userQueue: QueueItem[]
logs: LogEntry[]

View File

@@ -230,8 +230,8 @@ export function useSchedulerLogic() {
if (idx === -1) return
// 清理 WebSocket 订阅
if (tab.websocketId) {
ws.unsubscribe(tab.websocketId)
if (tab.subscriptionId) {
ws.unsubscribe(tab.subscriptionId)
}
// 清理日志引用
@@ -310,9 +310,13 @@ export function useSchedulerLogic() {
const subscribeToTask = (tab: SchedulerTab) => {
if (!tab.websocketId) return
ws.subscribe(tab.websocketId, {
onMessage: (message) => handleWebSocketMessage(tab, message)
})
const subscriptionId = ws.subscribe(
{ id: tab.websocketId },
(message) => handleWebSocketMessage(tab, message)
)
// 将订阅ID保存到tab中以便后续取消订阅
tab.subscriptionId = subscriptionId
}
const handleWebSocketMessage = (tab: SchedulerTab, wsMessage: any) => {
@@ -548,8 +552,12 @@ export function useSchedulerLogic() {
console.log('[Scheduler] 已强制更新schedulerTabs当前tabs状态:', schedulerTabs.value)
}
if (tab.subscriptionId) {
ws.unsubscribe(tab.subscriptionId)
tab.subscriptionId = null
}
if (tab.websocketId) {
ws.unsubscribe(tab.websocketId)
tab.websocketId = null
}
@@ -823,8 +831,8 @@ export function useSchedulerLogic() {
// 不再清理或重置导出的处理函数,保持使用者注册的处理逻辑永久有效
schedulerTabs.value.forEach(tab => {
if (tab.websocketId) {
ws.unsubscribe(tab.websocketId)
if (tab.subscriptionId) {
ws.unsubscribe(tab.subscriptionId)
}
})
saveTabsToStorage(schedulerTabs.value)