feat: add unified WebSocket connection manager and routing
- Implemented UnifiedWebSocketManager for managing WebSocket connections, including subscription handling and message sending. - Created unified WebSocket router to handle client messages, including authentication, subscription, and chat session management. - Added support for logging and plugin progress subscriptions. - Enhanced error handling and response structure for WebSocket operations.
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
/**
|
||||
* 全局日志 WebSocket 管理器
|
||||
* 确保整个应用只有一个 WebSocket 连接
|
||||
* 确保整个应用只通过统一连接层订阅日志流
|
||||
*/
|
||||
|
||||
import { checkAuthStatus } from './fetch-with-auth'
|
||||
import { getSetting } from './settings-manager'
|
||||
import { createReconnectingWebSocket } from './ws-utils'
|
||||
|
||||
import { getWsBaseUrl } from '@/lib/api-base'
|
||||
import { unifiedWsClient } from './unified-ws'
|
||||
|
||||
export interface LogEntry {
|
||||
id: string
|
||||
@@ -17,165 +15,79 @@ export interface LogEntry {
|
||||
message: string
|
||||
}
|
||||
|
||||
type LogCallback = (log: LogEntry) => void
|
||||
type LogCallback = () => void
|
||||
type ConnectionCallback = (connected: boolean) => void
|
||||
|
||||
class LogWebSocketManager {
|
||||
private wsControl: ReturnType<typeof createReconnectingWebSocket> | null = null
|
||||
|
||||
// 订阅者
|
||||
private logCallbacks: Set<LogCallback> = new Set()
|
||||
private connectionCallbacks: Set<ConnectionCallback> = new Set()
|
||||
|
||||
private initialized = false
|
||||
private isConnected = false
|
||||
|
||||
// 日志缓存 - 保存所有接收到的日志
|
||||
private logCache: LogEntry[] = []
|
||||
private logCallbacks: Set<LogCallback> = new Set()
|
||||
private subscriptionActive = false
|
||||
|
||||
/**
|
||||
* 获取最大缓存大小(从设置读取)
|
||||
*/
|
||||
private getMaxCacheSize(): number {
|
||||
return getSetting('logCacheSize')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最大重连次数(从设置读取)
|
||||
*/
|
||||
private getMaxReconnectAttempts(): number {
|
||||
return getSetting('wsMaxReconnectAttempts')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重连间隔(从设置读取)
|
||||
*/
|
||||
private getReconnectInterval(): number {
|
||||
return getSetting('wsReconnectInterval')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebSocket URL(不含 token 参数)
|
||||
*/
|
||||
private async getWebSocketUrl(): Promise<string> {
|
||||
const wsBase = await getWsBaseUrl()
|
||||
return `${wsBase}/ws/logs`
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接 WebSocket(会先检查登录状态)
|
||||
*/
|
||||
async connect() {
|
||||
// 检查是否在登录页面
|
||||
if (window.location.pathname === '/auth') {
|
||||
console.log('📡 在登录页面,跳过 WebSocket 连接')
|
||||
private initialize(): void {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查登录状态,避免未登录时尝试连接
|
||||
const isAuthenticated = await checkAuthStatus()
|
||||
if (!isAuthenticated) {
|
||||
console.log('📡 未登录,跳过 WebSocket 连接')
|
||||
return
|
||||
}
|
||||
unifiedWsClient.addEventListener((message) => {
|
||||
if (message.domain !== 'logs') {
|
||||
return
|
||||
}
|
||||
|
||||
const wsUrl = await this.getWebSocketUrl()
|
||||
if (message.event === 'snapshot') {
|
||||
const entries = Array.isArray(message.data.entries)
|
||||
? (message.data.entries as LogEntry[])
|
||||
: []
|
||||
this.logCache = entries.slice(-this.getMaxCacheSize())
|
||||
this.notifyLogChange()
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 ws-utils 创建 WebSocket
|
||||
this.wsControl = createReconnectingWebSocket(wsUrl, {
|
||||
onMessage: (data: string) => {
|
||||
try {
|
||||
const log: LogEntry = JSON.parse(data)
|
||||
this.notifyLog(log)
|
||||
} catch (error) {
|
||||
console.error('解析日志消息失败:', error)
|
||||
}
|
||||
},
|
||||
onOpen: () => {
|
||||
this.isConnected = true
|
||||
this.notifyConnection(true)
|
||||
},
|
||||
onClose: () => {
|
||||
this.isConnected = false
|
||||
this.notifyConnection(false)
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('❌ WebSocket 错误:', error)
|
||||
this.isConnected = false
|
||||
this.notifyConnection(false)
|
||||
},
|
||||
heartbeatInterval: 30000,
|
||||
maxRetries: this.getMaxReconnectAttempts(),
|
||||
backoffBase: this.getReconnectInterval(),
|
||||
maxBackoff: 30000,
|
||||
if (message.event === 'entry' && message.data.entry) {
|
||||
this.appendLog(message.data.entry as LogEntry)
|
||||
}
|
||||
})
|
||||
|
||||
// 启动连接
|
||||
await this.wsControl.connect()
|
||||
unifiedWsClient.onConnectionChange((connected) => {
|
||||
this.isConnected = connected
|
||||
this.notifyConnection(connected)
|
||||
})
|
||||
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
disconnect() {
|
||||
if (this.wsControl) {
|
||||
this.wsControl.disconnect()
|
||||
this.wsControl = null
|
||||
}
|
||||
|
||||
this.isConnected = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅日志消息
|
||||
*/
|
||||
onLog(callback: LogCallback) {
|
||||
this.logCallbacks.add(callback)
|
||||
return () => this.logCallbacks.delete(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅连接状态
|
||||
*/
|
||||
onConnectionChange(callback: ConnectionCallback) {
|
||||
this.connectionCallbacks.add(callback)
|
||||
// 立即通知当前状态
|
||||
callback(this.isConnected)
|
||||
return () => this.connectionCallbacks.delete(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知所有订阅者新日志
|
||||
*/
|
||||
private notifyLog(log: LogEntry) {
|
||||
// 检查是否已存在(通过 id 去重)
|
||||
private appendLog(log: LogEntry): void {
|
||||
const exists = this.logCache.some(existingLog => existingLog.id === log.id)
|
||||
|
||||
if (!exists) {
|
||||
// 添加到缓存
|
||||
this.logCache.push(log)
|
||||
|
||||
// 限制缓存大小(动态读取配置)
|
||||
const maxCacheSize = this.getMaxCacheSize()
|
||||
if (this.logCache.length > maxCacheSize) {
|
||||
this.logCache = this.logCache.slice(-maxCacheSize)
|
||||
}
|
||||
|
||||
// 只有新日志才通知订阅者
|
||||
this.logCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(log)
|
||||
} catch (error) {
|
||||
console.error('日志回调执行失败:', error)
|
||||
}
|
||||
})
|
||||
if (exists) {
|
||||
return
|
||||
}
|
||||
|
||||
this.logCache.push(log)
|
||||
const maxCacheSize = this.getMaxCacheSize()
|
||||
if (this.logCache.length > maxCacheSize) {
|
||||
this.logCache = this.logCache.slice(-maxCacheSize)
|
||||
}
|
||||
this.notifyLogChange()
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知所有订阅者连接状态变化
|
||||
*/
|
||||
private notifyConnection(connected: boolean) {
|
||||
this.connectionCallbacks.forEach(callback => {
|
||||
private notifyLogChange(): void {
|
||||
this.logCallbacks.forEach((callback) => {
|
||||
try {
|
||||
callback()
|
||||
} catch (error) {
|
||||
console.error('日志回调执行失败:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private notifyConnection(connected: boolean): void {
|
||||
this.connectionCallbacks.forEach((callback) => {
|
||||
try {
|
||||
callback(connected)
|
||||
} catch (error) {
|
||||
@@ -184,35 +96,65 @@ class LogWebSocketManager {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的所有日志
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (window.location.pathname === '/auth') {
|
||||
return
|
||||
}
|
||||
|
||||
const isAuthenticated = await checkAuthStatus()
|
||||
if (!isAuthenticated) {
|
||||
return
|
||||
}
|
||||
|
||||
this.initialize()
|
||||
if (this.subscriptionActive) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await unifiedWsClient.subscribe('logs', 'main', { replay: 100 })
|
||||
this.subscriptionActive = true
|
||||
} catch (error) {
|
||||
console.error('订阅日志流失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this.subscriptionActive = false
|
||||
void unifiedWsClient.unsubscribe('logs', 'main')
|
||||
this.isConnected = false
|
||||
this.notifyConnection(false)
|
||||
}
|
||||
|
||||
onLog(callback: LogCallback): () => void {
|
||||
this.logCallbacks.add(callback)
|
||||
return () => this.logCallbacks.delete(callback)
|
||||
}
|
||||
|
||||
onConnectionChange(callback: ConnectionCallback): () => void {
|
||||
this.connectionCallbacks.add(callback)
|
||||
callback(this.isConnected)
|
||||
return () => this.connectionCallbacks.delete(callback)
|
||||
}
|
||||
|
||||
getAllLogs(): LogEntry[] {
|
||||
return [...this.logCache]
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空日志缓存
|
||||
*/
|
||||
clearLogs() {
|
||||
clearLogs(): void {
|
||||
this.logCache = []
|
||||
this.notifyLogChange()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前连接状态
|
||||
*/
|
||||
getConnectionStatus(): boolean {
|
||||
return this.isConnected
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const logWebSocket = new LogWebSocketManager()
|
||||
|
||||
// 自动连接(应用启动时)
|
||||
if (typeof window !== 'undefined') {
|
||||
// 延迟一下确保页面加载完成
|
||||
setTimeout(() => {
|
||||
logWebSocket.connect()
|
||||
void logWebSocket.connect()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user