import { Client, IMessage } from '@stomp/stompjs' import SockJS from 'sockjs-client' import { envConfig } from '@/config/env' // WebSocket消息类型 export interface WebSocketMessage { messageId?: string conversationId?: string type: 'TEXT' | 'TYPING' | 'SYSTEM' | 'ERROR' | 'HEARTBEAT' | 'CONNECTION' | 'AI_THINKING' | 'AI_STREAM_START' | 'AI_STREAM_DELTA' | 'AI_STREAM_DONE' | 'AI_STREAM_ERROR' | 'AI_STREAM_EVENT' content: string senderId: string senderType: 'USER' | 'GUEST' | 'AI' | 'SYSTEM' status?: 'SENDING' | 'SENT' | 'DELIVERED' | 'READ' | 'FAILED' createTime?: string timestamp?: number data?: any } // 聊天请求类型 - 完全匹配后端ChatRequest export interface ChatRequest { content: string senderId: string senderType: 'USER' | 'GUEST' | 'AI' | 'SYSTEM' messageType: 'TEXT' | 'IMAGE' | 'FILE' | 'SYSTEM' | 'HEARTBEAT' conversationId?: string timestamp?: number } // 连接状态 export type ConnectionStatus = 'CONNECTING' | 'CONNECTED' | 'DISCONNECTED' | 'ERROR' // 事件回调类型 export interface WebSocketCallbacks { onMessage?: (message: WebSocketMessage) => void onConnect?: () => void onDisconnect?: () => void onError?: (error: any) => void onStatusChange?: (status: ConnectionStatus) => void } /** * STOMP WebSocket服务类 * 使用STOMP协议与后端Spring WebSocket通信 */ export class StompWebSocketService { private client: Client | null = null private callbacks: WebSocketCallbacks = {} private status: ConnectionStatus = 'DISCONNECTED' private reconnectAttempts = 0 private maxReconnectAttempts = 5 private reconnectInterval = 3000 private userId: string | null = null private conversationId: string | null = null constructor() { // 构建WebSocket URL const wsUrl = `${envConfig.apiBaseUrl}/ws/chat` console.log('STOMP WebSocket URL:', wsUrl) } /** * 连接WebSocket */ connect(userId?: string, callbacks?: WebSocketCallbacks): Promise { return new Promise((resolve, reject) => { try { this.callbacks = { ...callbacks } // 设置用户ID和类型 if (userId) { this.userId = userId console.log('🔌 使用登录用户ID:', userId) } else { this.userId = `guest_${Date.now()}` console.log('🔌 使用访客ID:', this.userId) } this.setStatus('CONNECTING') // 创建STOMP客户端 this.client = new Client({ webSocketFactory: () => { const wsUrl = `${envConfig.apiBaseUrl}/ws/chat` return new SockJS(wsUrl) }, // 连接头信息 connectHeaders: this.getConnectHeaders(), // 调试信息 debug: (str) => { console.log('STOMP Debug:', str) }, // 重连配置 reconnectDelay: this.reconnectInterval, heartbeatIncoming: 4000, heartbeatOutgoing: 4000, }) // 连接成功回调 this.client.onConnect = (frame) => { console.log('✅ STOMP WebSocket连接成功:', frame) this.setStatus('CONNECTED') this.reconnectAttempts = 0 // 订阅消息 this.subscribeToMessages() // 发送连接消息 this.sendConnectMessage() this.callbacks.onConnect?.() resolve() } // 连接错误回调 this.client.onStompError = (frame) => { console.error('❌ STOMP连接错误:', frame) this.setStatus('ERROR') this.callbacks.onError?.(frame) reject(new Error(`STOMP连接错误: ${frame.headers['message']}`)) } // WebSocket错误回调 this.client.onWebSocketError = (error) => { console.error('❌ WebSocket错误:', error) this.setStatus('ERROR') this.callbacks.onError?.(error) } // 连接关闭回调 this.client.onWebSocketClose = (event) => { console.log('🔌 WebSocket连接关闭:', event) this.setStatus('DISCONNECTED') this.callbacks.onDisconnect?.() // 自动重连 if (!event.wasClean && this.reconnectAttempts < this.maxReconnectAttempts) { this.scheduleReconnect() } } // 激活连接 this.client.activate() } catch (error) { console.error('❌ STOMP连接异常:', error) this.setStatus('ERROR') this.callbacks.onError?.(error) reject(error) } }) } /** * 断开连接 */ disconnect(): void { if (this.client) { this.client.deactivate() this.client = null } this.userId = null this.conversationId = null this.setStatus('DISCONNECTED') } /** * 发送聊天消息 */ sendChatMessage(content: string, conversationId?: string): void { if (!this.client || !this.client.connected) { const error = new Error('STOMP客户端未连接,无法发送消息') console.error('STOMP未连接') this.callbacks.onError?.({ userMessage: '连接已断开,请等待重连后再试', originalError: error }) return } if (!content.trim()) { const error = new Error('消息内容不能为空') this.callbacks.onError?.({ userMessage: '消息内容不能为空', originalError: error }) return } // 判断用户类型 const isGuest = !this.userId || this.userId.startsWith('guest_') const senderType = isGuest ? 'GUEST' : 'USER' console.log('📤 发送STOMP聊天消息,用户信息:', { userId: this.userId, senderType, isGuest, content: content.trim() }) // 创建聊天请求 - 匹配后端ChatRequest格式 const chatRequest: ChatRequest = { content: content.trim(), senderId: this.userId!, senderType, messageType: 'TEXT', conversationId: conversationId || this.conversationId || undefined, timestamp: Date.now() } console.log('📤 准备发送的聊天请求:', chatRequest) try { // 发送到后端的/app/chat/send端点(对应 @MessageMapping("/chat") + @MessageMapping("/send")) this.client.publish({ destination: '/app/chat/send', body: JSON.stringify(chatRequest) }) console.log('✅ STOMP聊天消息发送成功:', chatRequest) } catch (error) { console.error('❌ STOMP消息发送失败:', error) this.callbacks.onError?.({ userMessage: '消息发送失败,请重试', originalError: error }) } } /** * 设置会话ID */ setConversationId(conversationId: string): void { this.conversationId = conversationId console.log('设置会话ID:', conversationId) } /** * 获取连接状态 */ getStatus(): ConnectionStatus { return this.status } /** * 是否已连接 */ isConnected(): boolean { return this.status === 'CONNECTED' && this.client?.connected === true } /** * 获取连接头信息 */ private getConnectHeaders(): Record { const headers: Record = {} // 添加用户ID if (this.userId) { headers['X-User-Id'] = this.userId } // 添加JWT token - 修复:使用正确的localStorage key const token = localStorage.getItem('access_token') if (token) { headers['Authorization'] = `Bearer ${token}` console.log('🔐 添加Authorization头到STOMP连接,token预览:', token.substring(0, 20) + '...') } else { console.warn('🔐 未找到access_token,WebSocket将以访客身份连接') } return headers } /** * 订阅消息 */ private subscribeToMessages(): void { if (!this.client?.connected) return // 订阅用户私有消息 - 这是主要的消息接收频道 if (this.userId) { const userQueuePath = `/user/${this.userId}/queue/messages` console.log('📨 订阅用户私有队列:', userQueuePath) this.client.subscribe(userQueuePath, (message: IMessage) => { this.handleMessage(message) }) } // 订阅广播消息(系统通知等) this.client.subscribe('/topic/broadcast', (message: IMessage) => { this.handleMessage(message) }) // 注释掉会话特定消息订阅,避免重复接收消息 // 所有消息都通过用户私有队列接收,确保消息不重复 // if (this.conversationId) { // this.client.subscribe(`/topic/conversation/${this.conversationId}`, (message: IMessage) => { // this.handleMessage(message) // }) // } } /** * 处理收到的消息 */ private handleMessage(message: IMessage): void { try { const wsMessage: WebSocketMessage = JSON.parse(message.body) console.log('📨 收到STOMP消息:', wsMessage) this.callbacks.onMessage?.(wsMessage) } catch (error) { console.error('❌ 解析STOMP消息失败:', error, message.body) } } /** * 发送连接消息 */ private sendConnectMessage(): void { if (!this.client?.connected) return const connectRequest = { userId: this.userId, clientType: 'web', clientVersion: '1.0.0', timestamp: Date.now() } try { this.client.publish({ destination: '/app/chat/connect', body: JSON.stringify(connectRequest) }) console.log('✅ STOMP连接消息发送成功:', connectRequest) } catch (error) { console.error('❌ STOMP连接消息发送失败:', error) } } /** * 设置连接状态 */ private setStatus(status: ConnectionStatus): void { this.status = status this.callbacks.onStatusChange?.(status) } /** * 安排重连 */ private scheduleReconnect(): void { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('达到最大重连次数,停止重连') return } this.reconnectAttempts++ console.log(`${this.reconnectInterval}ms后尝试第${this.reconnectAttempts}次重连`) setTimeout(() => { if (this.status !== 'CONNECTED') { this.connect(this.userId!, this.callbacks).catch(() => { // 重连失败会自动安排下次重连 }) } }, this.reconnectInterval) // 递增重连间隔 this.reconnectInterval = Math.min(this.reconnectInterval * 1.5, 30000) } } // 创建STOMP WebSocket服务实例 export const stompWebSocketService = new StompWebSocketService() export default stompWebSocketService