/** * 聊天 WebSocket 处理器 * * 原始 Dart: lib/core/net/websocket_handler.dart * * 浏览器方案:使用原生 `WebSocket` API,封装连接管理 + 重连 + 事件回调。 * * 事件: * - onConnected(userId) * - onTyping(isTyping) * - onSentence(index, text, total, done) ← AI 逐句推送 * - onVoiceReady(index, audioUrl) * - onIntimacyUpdate(intimacyChange, newIntimacy, relationshipStage) * - onMoodUpdate(mood) * - onError(errorMessage) */ import { getApiConfig } from "@/core/net/config/api_config"; export interface SentencePayload { index: number; text: string; total: number; done: boolean; } export interface IntimacyPayload { intimacyChange?: number; newIntimacy?: number; relationshipStage?: string; } export class ChatWebSocket { private ws: WebSocket | null = null; private reconnectTimer: ReturnType | null = null; private disposed = false; onConnected: ((userId: string) => void) | null = null; onTyping: ((isTyping: boolean) => void) | null = null; onSentence: ((payload: SentencePayload) => void) | null = null; onVoiceReady: ((index: number, audioUrl: string) => void) | null = null; onIntimacyUpdate: ((payload: IntimacyPayload) => void) | null = null; onMoodUpdate: ((mood: string) => void) | null = null; onError: ((errorMessage: string) => void) | null = null; constructor( private readonly serverUrl: string, private readonly token: string, ) {} get isConnected(): boolean { return this.ws?.readyState === WebSocket.OPEN; } connect(): void { if (this.disposed) return; if (this.ws && this.ws.readyState !== WebSocket.CLOSED) return; try { const url = `${this.serverUrl}?token=${encodeURIComponent(this.token)}`; this.ws = new WebSocket(url); this.ws.onopen = () => { // 连接成功;userId 由后端在第一条消息中下发 }; this.ws.onmessage = (e) => this.handleMessage(e.data); this.ws.onerror = () => this.onError?.("WebSocket error"); this.ws.onclose = () => { if (!this.disposed) { this.reconnectTimer = setTimeout(() => this.connect(), 3000); } }; } catch (e) { this.onError?.(e instanceof Error ? e.message : String(e)); } } sendMessage(content: string): boolean { if (!this.isConnected) return false; this.ws?.send(JSON.stringify({ type: "message", content })); return true; } disconnect(): void { this.disposed = true; if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } this.ws?.close(); this.ws = null; } private handleMessage(data: unknown): void { if (typeof data !== "string") return; let payload: { type?: string; userId?: string; isTyping?: boolean; index?: number; text?: string; total?: number; done?: boolean; audioUrl?: string; intimacyChange?: number; newIntimacy?: number; relationshipStage?: string; mood?: string; error?: string; }; try { payload = JSON.parse(data); } catch { return; } switch (payload.type) { case "connected": if (payload.userId) this.onConnected?.(payload.userId); break; case "typing": this.onTyping?.(payload.isTyping ?? false); break; case "sentence": this.onSentence?.({ index: payload.index ?? 0, text: payload.text ?? "", total: payload.total ?? 0, done: payload.done ?? false, }); break; case "voice_ready": this.onVoiceReady?.(payload.index ?? 0, payload.audioUrl ?? ""); break; case "intimacy_update": this.onIntimacyUpdate?.({ intimacyChange: payload.intimacyChange, newIntimacy: payload.newIntimacy, relationshipStage: payload.relationshipStage, }); break; case "mood_update": if (payload.mood) this.onMoodUpdate?.(payload.mood); break; case "error": this.onError?.(payload.error ?? "Unknown error"); break; } } } /** 默认创建实例:从 env 读取 WS URL。 */ export function createChatWebSocket(token: string): ChatWebSocket { const base = getApiConfig().wsUrl; return new ChatWebSocket(base, token); }