Files
happy-life-star/web/src/services/stomp-websocket.ts
T
peanut 89fc42819d feat: AI 场景路由、ASR 服务及前后端全链路同步
- 新增 AI 场景路由控制器和管理接口
- 新增 ASR 语音识别服务及前后端集成
- 同步 AI Runtime 客户端到 Web/小程序/Life-Script
- 完善 AI 配置测试修复和管理后台路由配置
- 新增数据库迁移脚本

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 13:25:21 +08:00

374 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void> {
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<string, string> {
const headers: Record<string, string> = {}
// 添加用户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_tokenWebSocket将以访客身份连接')
}
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