moki 3 هفته پیش
والد
کامیت
8435be0f7a
3فایلهای تغییر یافته به همراه36 افزوده شده و 3 حذف شده
  1. 8 1
      src/composables/useWebSocket.ts
  2. 1 1
      src/types/index.ts
  3. 27 1
      src/views/Dashboard.vue

+ 8 - 1
src/composables/useWebSocket.ts

@@ -9,12 +9,18 @@ export function useWebSocket(url: string) {
   let reconnectDelay = 1000
 
   const listeners = new Set<(msg: StatusMessage) => void>()
+  const disconnectListeners = new Set<() => void>()
 
   function onMessage(cb: (msg: StatusMessage) => void) {
     listeners.add(cb)
     return () => listeners.delete(cb)
   }
 
+  function onDisconnect(cb: () => void) {
+    disconnectListeners.add(cb)
+    return () => disconnectListeners.delete(cb)
+  }
+
   function connect() {
     if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return
 
@@ -36,6 +42,7 @@ export function useWebSocket(url: string) {
 
     ws.onclose = () => {
       connected.value = false
+      disconnectListeners.forEach((cb) => cb())
       scheduleReconnect()
     }
 
@@ -67,5 +74,5 @@ export function useWebSocket(url: string) {
 
   onUnmounted(disconnect)
 
-  return { connected, messages, onMessage, disconnect, connect }
+  return { connected, messages, onMessage, onDisconnect, disconnect, connect }
 }

+ 1 - 1
src/types/index.ts

@@ -52,7 +52,7 @@ export interface PortState {
 
 export const STATUS_CONFIG: Record<StatusCode, { color: string; label: string }> = {
   idle: { color: 'green', label: '空闲' },
-  busy: { color: 'blue', label: '忙碌' },
+  busy: { color: 'blue', label: '工作中' },
   retry: { color: 'orange', label: '重试中' },
   pending: { color: 'gold', label: '修改中' },
   reasoning: { color: 'purple', label: '思考中' },

+ 27 - 1
src/views/Dashboard.vue

@@ -5,11 +5,32 @@ import { getClients } from '@/api/client'
 import type { PortState, StatusMessage } from '@/types'
 import PortStatusCard from '@/components/PortStatusCard.vue'
 
-const { connected, onMessage } = useWebSocket(`${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/ws`)
+const { connected, onMessage, onDisconnect } = useWebSocket(`${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/ws`)
 
 const ports = reactive<Map<number, PortState>>(new Map())
 
+let notified = new Set<number>()
+
+function sendNotification(port: number, status: string) {
+  if (!('Notification' in window)) return
+  if (Notification.permission === 'granted') {
+    new Notification(`端口 ${port}`, { body: status })
+  } else if (Notification.permission === 'default') {
+    Notification.requestPermission().then((perm) => {
+      if (perm === 'granted') new Notification(`端口 ${port}`, { body: status })
+    })
+  }
+}
+
 const removeListener = onMessage((msg: StatusMessage) => {
+  const prev = ports.get(msg.port)
+  if (prev && msg.code === 'session_completed' && prev.code !== 'session_completed' && !notified.has(msg.port)) {
+    notified.add(msg.port)
+    sendNotification(msg.port, msg.status)
+  }
+  if (msg.code === 'idle') {
+    notified.delete(msg.port)
+  }
   ports.set(msg.port, {
     port: msg.port,
     status: msg.status,
@@ -31,6 +52,10 @@ onMounted(async () => {
   }
 })
 
+const removeDisconnectListener = onDisconnect(() => {
+  ports.clear()
+})
+
 const portList = computed(() =>
   Array.from(ports.values()).sort((a, b) => a.port - b.port),
 )
@@ -46,6 +71,7 @@ const errorCount = computed(() => portList.value.filter((p) => p.code === 'error
 const permissionCount = computed(() => portList.value.filter((p) => p.code === 'permission').length)
 
 onUnmounted(removeListener)
+onUnmounted(removeDisconnectListener)
 </script>
 
 <template>