Add File
This commit is contained in:
226
src/elderly/services/websocketService.ts
Normal file
226
src/elderly/services/websocketService.ts
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
/**
|
||||||
|
* WebSocket service for connecting to dialog engine (port 10002)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface WebSocketMessage {
|
||||||
|
Topic?: string;
|
||||||
|
Data: {
|
||||||
|
Key: 'text' | 'audio' | 'question' | 'log';
|
||||||
|
Value?: string;
|
||||||
|
Text?: string;
|
||||||
|
IsFirst?: number;
|
||||||
|
IsEnd?: number;
|
||||||
|
};
|
||||||
|
Username?: string;
|
||||||
|
robot?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebSocketServiceOptions {
|
||||||
|
url: string;
|
||||||
|
onMessage?: (message: WebSocketMessage) => void;
|
||||||
|
onConnect?: () => void;
|
||||||
|
onDisconnect?: () => void;
|
||||||
|
onError?: (error: Event) => void;
|
||||||
|
maxReconnectAttempts?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
|
||||||
|
const INITIAL_RECONNECT_DELAY = 1000;
|
||||||
|
const MAX_RECONNECT_DELAY = 30000;
|
||||||
|
|
||||||
|
export class WebSocketService {
|
||||||
|
private ws: WebSocket | null = null;
|
||||||
|
private url: string;
|
||||||
|
private reconnectAttempts = 0;
|
||||||
|
private maxReconnectAttempts: number;
|
||||||
|
private reconnectTimer: number | null = null;
|
||||||
|
private isConnecting = false;
|
||||||
|
private onMessageCallback?: (message: WebSocketMessage) => void;
|
||||||
|
private onConnectCallback?: () => void;
|
||||||
|
private onDisconnectCallback?: () => void;
|
||||||
|
private onErrorCallback?: (error: Event) => void;
|
||||||
|
|
||||||
|
constructor(options: WebSocketServiceOptions) {
|
||||||
|
this.url = options.url;
|
||||||
|
this.onMessageCallback = options.onMessage;
|
||||||
|
this.onConnectCallback = options.onConnect;
|
||||||
|
this.onDisconnectCallback = options.onDisconnect;
|
||||||
|
this.onErrorCallback = options.onError;
|
||||||
|
this.maxReconnectAttempts = options.maxReconnectAttempts || DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(): void {
|
||||||
|
if (this.isConnecting) {
|
||||||
|
console.log('[Fay] 已有连接正在进行中,跳过');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||||
|
console.error('[Fay] 达到最大重连次数限制');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isConnecting = true;
|
||||||
|
console.log('[Fay] 正在连接到', this.url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.cleanup(false);
|
||||||
|
this.ws = new WebSocket(this.url);
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
console.log('[Fay] 已连接到数字人接口');
|
||||||
|
this.isConnecting = false;
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
|
||||||
|
// 发送初始化消息
|
||||||
|
const initMsg = { Username: 'User', Output: false };
|
||||||
|
console.log('[Fay] 发送初始化消息:', initMsg);
|
||||||
|
this.send(initMsg);
|
||||||
|
|
||||||
|
this.onConnectCallback?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
console.log('[Fay] 收到消息:', event.data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message: WebSocketMessage = JSON.parse(event.data);
|
||||||
|
console.log('[Fay] 解析后:', JSON.stringify(message, null, 2));
|
||||||
|
|
||||||
|
if (!message || !message.Data || !message.Data.Key) {
|
||||||
|
console.warn('[Fay] 消息格式不完整:', message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理文本消息
|
||||||
|
if (message.Data.Key === 'text') {
|
||||||
|
const text = message.Data.Value || '';
|
||||||
|
const isFirst = message.Data.IsFirst === 1;
|
||||||
|
const isEnd = message.Data.IsEnd === 1;
|
||||||
|
|
||||||
|
if (text && text.trim()) {
|
||||||
|
console.log('[xmov] 收到文本消息:', text, { is_start: isFirst, is_end: isEnd });
|
||||||
|
this.onMessageCallback?.(message);
|
||||||
|
} else {
|
||||||
|
console.warn('[Fay] 收到空文本消息');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理音频消息
|
||||||
|
else if (message.Data.Key === 'audio') {
|
||||||
|
const text = message.Data.Text || '';
|
||||||
|
const isFirst = message.Data.IsFirst === 1;
|
||||||
|
const isEnd = message.Data.IsEnd === 1;
|
||||||
|
|
||||||
|
if (text && text.trim()) {
|
||||||
|
console.log('[xmov] 收到音频消息:', text, { is_start: isFirst, is_end: isEnd });
|
||||||
|
this.onMessageCallback?.(message);
|
||||||
|
} else {
|
||||||
|
console.warn('[Fay] 收到空音频文本');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理日志消息(思考中等状态提示)
|
||||||
|
else if (message.Data.Key === 'log') {
|
||||||
|
const logText = message.Data.Value || '';
|
||||||
|
console.log('[Fay] 收到日志消息:', logText);
|
||||||
|
this.onMessageCallback?.(message);
|
||||||
|
}
|
||||||
|
// 处理问题消息
|
||||||
|
else if (message.Data.Key === 'question') {
|
||||||
|
console.log('[Fay] 用户问题:', message.Data.Value);
|
||||||
|
} else {
|
||||||
|
console.log('[Fay] 未知消息类型:', message.Data.Key);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Fay] 处理消息出错:', error, '原始数据:', event.data);
|
||||||
|
}
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = (error) => {
|
||||||
|
console.error('[Fay] WebSocket错误:', error);
|
||||||
|
this.isConnecting = false;
|
||||||
|
this.onErrorCallback?.(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onclose = (event) => {
|
||||||
|
console.log('[Fay] WebSocket连接已关闭, 代码:', event.code, '原因:', event.reason);
|
||||||
|
this.isConnecting = false;
|
||||||
|
this.onDisconnectCallback?.();
|
||||||
|
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer);
|
||||||
|
this.reconnectTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用指数退避策略重连
|
||||||
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
const delay = this.calculateReconnectDelay();
|
||||||
|
console.log(`[Fay] ${delay}ms后尝试第${this.reconnectAttempts}次重连...`);
|
||||||
|
|
||||||
|
this.reconnectTimer = window.setTimeout(() => {
|
||||||
|
this.connect();
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
console.error('[Fay] 已达到最大重连次数');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Fay] 创建WebSocket连接失败:', error);
|
||||||
|
this.isConnecting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data: any): void {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
this.ws.send(JSON.stringify(data));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Fay] 发送消息失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(): void {
|
||||||
|
this.cleanup(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanup(resetReconnectAttempts: boolean): void {
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer);
|
||||||
|
this.reconnectTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.ws) {
|
||||||
|
try {
|
||||||
|
this.ws.onclose = null;
|
||||||
|
this.ws.onerror = null;
|
||||||
|
this.ws.onmessage = null;
|
||||||
|
this.ws.onopen = null;
|
||||||
|
this.ws.close();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[Fay] 清理WebSocket时出错:', error);
|
||||||
|
}
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isConnecting = false;
|
||||||
|
if (resetReconnectAttempts) {
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateReconnectDelay(): number {
|
||||||
|
const exponentialDelay = Math.min(
|
||||||
|
INITIAL_RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts - 1),
|
||||||
|
MAX_RECONNECT_DELAY
|
||||||
|
);
|
||||||
|
const jitter = exponentialDelay * 0.2 * (Math.random() * 2 - 1);
|
||||||
|
return Math.floor(exponentialDelay + jitter);
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected(): boolean {
|
||||||
|
return this.ws?.readyState === WebSocket.OPEN;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user