Files
LangBot/web/src/app/infra/websocket/WebSocketClient.ts
T
2026-07-19 09:58:59 +08:00

334 lines
8.9 KiB
TypeScript

/**
* WebSocket客户端类
* 用于管理WebSocket连接和消息处理
*/
import { getActiveWorkspaceUuid } from '@/app/infra/http/workspaceContext';
export interface WebSocketMessage {
id: number;
role: 'user' | 'assistant';
content: string;
message_chain: Array<{ type: string; text?: string; target?: string }>;
timestamp: string;
is_final?: boolean;
connection_id?: string;
}
export interface WebSocketResponse {
type:
| 'connected'
| 'response'
| 'user_message'
| 'pong'
| 'broadcast'
| 'error';
connection_id?: string;
pipeline_uuid?: string;
session_type?: string;
timestamp?: string;
data?: WebSocketMessage;
message?: string;
}
export class WebSocketClient {
private ws: WebSocket | null = null;
private connectionId: string | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 3000; // 3秒重连间隔
private heartbeatInterval: NodeJS.Timeout | null = null;
private heartbeatIntervalMs = 30000; // 30秒
private isConnecting = false; // 防止重复连接
// 事件回调
private onConnectedCallback?: (data: WebSocketResponse) => void;
private onMessageCallback?: (data: WebSocketMessage) => void;
private onErrorCallback?: (error: Error) => void;
private onCloseCallback?: () => void;
private onBroadcastCallback?: (message: string) => void;
constructor(
private pipelineId: string,
private sessionType: 'person' | 'group' = 'person',
private token?: string,
) {}
/**
* 连接到WebSocket服务器
*/
public connect(): Promise<string> {
return new Promise((resolve, reject) => {
try {
// 防止重复连接
if (
this.isConnecting ||
(this.ws && this.ws.readyState === WebSocket.CONNECTING)
) {
console.warn('WebSocket正在连接中,忽略重复连接请求');
reject(new Error('Connection already in progress'));
return;
}
// 如果已经连接,直接返回
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
console.warn('WebSocket已连接,忽略重复连接请求');
resolve(this.connectionId || '');
return;
}
this.isConnecting = true;
// 构建WebSocket URL
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// extract host from import.meta.env.VITE_API_BASE_URL
// If env var is undefined, use current page host (for production)
const host =
import.meta.env.VITE_API_BASE_URL?.split('://')[1] ||
window.location.host;
const url = `${protocol}//${host}/api/v1/pipelines/${this.pipelineId}/ws/connect?session_type=${this.sessionType}`;
this.ws = new WebSocket(url);
// 连接打开
this.ws.onopen = () => {
this.reconnectAttempts = 0;
this.isConnecting = false;
const token = this.token || localStorage.getItem('token');
const workspaceUuid = getActiveWorkspaceUuid();
if (!token || !workspaceUuid) {
const error = new Error('WebSocket认证信息缺失');
this.onErrorCallback?.(error);
this.ws?.close();
reject(error);
return;
}
this.ws?.send(
JSON.stringify({
type: 'authenticate',
token,
workspace_uuid: workspaceUuid,
}),
);
};
// 接收消息
this.ws.onmessage = (event) => {
try {
const data: WebSocketResponse = JSON.parse(event.data);
this.handleMessage(data);
// 第一次连接成功
if (data.type === 'connected' && data.connection_id) {
this.connectionId = data.connection_id;
this.startHeartbeat();
resolve(data.connection_id);
}
} catch (error) {
console.error('解析WebSocket消息失败:', error);
this.onErrorCallback?.(error as Error);
}
};
// 连接关闭
this.ws.onclose = () => {
this.isConnecting = false;
this.stopHeartbeat();
this.onCloseCallback?.();
// 自动重连
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
setTimeout(() => {
this.connect().catch(console.error);
}, this.reconnectDelay * this.reconnectAttempts);
}
};
// 连接错误
this.ws.onerror = (event) => {
console.error('WebSocket错误:', event);
this.isConnecting = false;
const error = new Error('WebSocket连接失败');
this.onErrorCallback?.(error);
reject(error);
};
} catch (error) {
this.isConnecting = false;
reject(error);
}
});
}
/**
* 处理接收到的消息
*/
private handleMessage(data: WebSocketResponse) {
switch (data.type) {
case 'connected':
this.onConnectedCallback?.(data);
break;
case 'response':
// 检查 session_type 是否匹配 - 如果消息没有 session_type 或者不匹配当前session,都忽略
if (!data.session_type || data.session_type !== this.sessionType) {
// 忽略不匹配的 session_type 消息
console.debug(
`忽略不匹配的消息: 当前session=${this.sessionType}, 消息session=${data.session_type}`,
);
break;
}
if (data.data) {
this.onMessageCallback?.(data.data);
}
break;
case 'user_message':
// 检查 session_type 是否匹配 - 如果消息没有 session_type 或者不匹配当前session,都忽略
if (!data.session_type || data.session_type !== this.sessionType) {
// 忽略不匹配的 session_type 消息
console.debug(
`忽略不匹配的用户消息: 当前session=${this.sessionType}, 消息session=${data.session_type}`,
);
break;
}
// 用户消息广播(包括自己发送的消息)
if (data.data) {
this.onMessageCallback?.(data.data);
}
break;
case 'pong':
// 心跳响应
break;
case 'broadcast':
if (data.message) {
this.onBroadcastCallback?.(data.message);
}
break;
case 'error':
const error = new Error(data.message || '未知错误');
this.onErrorCallback?.(error);
break;
default:
console.warn('未知消息类型:', data);
}
}
/**
* 发送消息
*/
public sendMessage(
messageChain: Array<{ type: string; text?: string; target?: string }>,
stream: boolean = true,
) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
throw new Error('WebSocket未连接');
}
const message = {
type: 'message',
message: messageChain,
stream: stream,
};
this.ws.send(JSON.stringify(message));
}
/**
* 发送心跳
*/
private sendHeartbeat() {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return;
}
this.ws.send(JSON.stringify({ type: 'ping' }));
}
/**
* 启动心跳
*/
private startHeartbeat() {
this.stopHeartbeat();
this.heartbeatInterval = setInterval(() => {
this.sendHeartbeat();
}, this.heartbeatIntervalMs);
}
/**
* 停止心跳
*/
private stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
/**
* 断开连接
*/
public disconnect() {
if (this.ws) {
this.stopHeartbeat();
// 停止自动重连
this.reconnectAttempts = this.maxReconnectAttempts;
// 发送断开消息
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'disconnect' }));
}
this.ws.close();
this.ws = null;
this.connectionId = null;
this.isConnecting = false;
}
}
/**
* 获取连接ID
*/
public getConnectionId(): string | null {
return this.connectionId;
}
/**
* 获取连接状态
*/
public isConnected(): boolean {
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
}
// ===== 事件回调设置 =====
public onConnected(callback: (data: WebSocketResponse) => void) {
this.onConnectedCallback = callback;
return this;
}
public onMessage(callback: (data: WebSocketMessage) => void) {
this.onMessageCallback = callback;
return this;
}
public onError(callback: (error: Error) => void) {
this.onErrorCallback = callback;
return this;
}
public onClose(callback: () => void) {
this.onCloseCallback = callback;
return this;
}
public onBroadcast(callback: (message: string) => void) {
this.onBroadcastCallback = callback;
return this;
}
}