mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-24 21:36:06 +00:00
334 lines
8.9 KiB
TypeScript
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;
|
|
}
|
|
}
|