websocket.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. // WebSocket 客户端工具
  2. // 用于连接后端 WebSocket 服务,接收实时消息
  3. // WebSocket 消息类型
  4. export interface WSMessage<T = unknown> {
  5. type: string; // "new_message" | "conversation_update" 等
  6. conversation_id: number;
  7. data: T; // 消息内容(Message 对象)
  8. }
  9. // WebSocket 连接选项
  10. export interface WSOptions<T = unknown> {
  11. conversationId: number; // 对话ID
  12. isVisitor?: boolean; // 是否是访客(默认为 true)
  13. agentId?: number; // 客服ID(如果是客服连接,需要传递)
  14. wsToken?: string; // 客服 WS 令牌(登录后下发)
  15. onMessage?: (message: WSMessage<T>) => void; // 收到消息时的回调
  16. onError?: (error: Event) => void; // 连接错误时的回调
  17. onClose?: () => void; // 连接关闭时的回调
  18. }
  19. // WebSocket 客户端类
  20. export class WSClient<T = unknown> {
  21. private ws: WebSocket | null = null;
  22. private conversationId: number;
  23. private isVisitor: boolean;
  24. private agentId?: number; // 客服ID
  25. private wsToken?: string; // 客服 WS 令牌
  26. private onMessage?: (message: WSMessage<T>) => void;
  27. private onError?: (error: Event) => void;
  28. private onClose?: () => void;
  29. private reconnectTimer: NodeJS.Timeout | null = null;
  30. private reconnectAttempts = 0;
  31. private reconnectDelay = 3000; // 初始 3 秒
  32. private maxReconnectDelay = 30000; // 最长 30 秒
  33. private manualDisconnect = false;
  34. private logPrefix = "❌ WebSocket 错误";
  35. constructor(options: WSOptions<T>) {
  36. this.conversationId = options.conversationId;
  37. this.isVisitor = options.isVisitor !== undefined ? options.isVisitor : true;
  38. this.agentId = options.agentId;
  39. this.wsToken = options.wsToken;
  40. this.onMessage = options.onMessage;
  41. this.onError = options.onError;
  42. this.onClose = options.onClose;
  43. }
  44. // 连接 WebSocket
  45. connect() {
  46. this.manualDisconnect = false;
  47. // 如果已经连接,先断开
  48. if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
  49. this.ws.close();
  50. this.ws = null;
  51. }
  52. // 使用相对路径构建 WebSocket URL(自动适配当前域名和协议)
  53. // 根据当前页面的协议自动选择 ws:// 或 wss://
  54. const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  55. const host = typeof window !== 'undefined' ? window.location.host : '';
  56. let wsUrl = `${protocol}//${host}/ws?conversation_id=${this.conversationId}&is_visitor=${this.isVisitor}`;
  57. // 如果是客服连接,添加 agent_id 参数
  58. if (!this.isVisitor && this.agentId) {
  59. wsUrl += `&agent_id=${this.agentId}`;
  60. if (this.wsToken) {
  61. wsUrl += `&ws_token=${encodeURIComponent(this.wsToken)}`;
  62. }
  63. }
  64. try {
  65. this.ws = new WebSocket(wsUrl);
  66. this.ws.onopen = () => {
  67. this.reconnectAttempts = 0; // 重置重连次数
  68. this.reconnectDelay = 3000; // 连接恢复后重置退避时间
  69. };
  70. this.ws.onmessage = (event) => {
  71. try {
  72. const message: WSMessage<T> = JSON.parse(event.data);
  73. if (this.onMessage) {
  74. this.onMessage(message);
  75. }
  76. } catch (error) {
  77. console.error(
  78. `❌ 解析 WebSocket 消息失败: 对话ID=${this.conversationId}`,
  79. error
  80. );
  81. }
  82. };
  83. this.ws.onerror = (error) => {
  84. const state = this.ws?.readyState;
  85. // 主动断开或连接已进入关闭态时,浏览器仍可能触发 onerror,这属于预期行为,避免误报。
  86. if (this.manualDisconnect || state === WebSocket.CLOSING || state === WebSocket.CLOSED) {
  87. return;
  88. }
  89. const stateText =
  90. state === WebSocket.CONNECTING
  91. ? "连接中"
  92. : state === WebSocket.OPEN
  93. ? "已连接"
  94. : state === WebSocket.CLOSING
  95. ? "关闭中"
  96. : state === WebSocket.CLOSED
  97. ? "已关闭"
  98. : "未知";
  99. const url = this.ws?.url || wsUrl;
  100. console.error(
  101. `${this.logPrefix}: 对话ID=${this.conversationId}, 状态=${stateText}, URL=${url}`,
  102. error
  103. );
  104. if (this.onError) {
  105. this.onError(error);
  106. }
  107. };
  108. this.ws.onclose = (event) => {
  109. this.ws = null;
  110. if (this.onClose) {
  111. this.onClose();
  112. }
  113. // 除主动断开外,任意关闭都重连(代理空闲断开通常是 clean close)。
  114. if (!this.manualDisconnect) {
  115. this.attemptReconnect();
  116. }
  117. };
  118. } catch (error) {
  119. console.error(
  120. `❌ 创建 WebSocket 连接失败: 对话ID=${this.conversationId}, URL=${wsUrl}`,
  121. error
  122. );
  123. if (this.onError) {
  124. // 创建一个错误事件对象
  125. const errorEvent = new Event("error");
  126. this.onError(errorEvent);
  127. }
  128. }
  129. }
  130. // 尝试重连
  131. private attemptReconnect() {
  132. this.reconnectAttempts++;
  133. this.reconnectTimer = setTimeout(() => {
  134. this.connect();
  135. }, this.reconnectDelay);
  136. // 指数退避,避免网络波动时过于频繁重连
  137. this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
  138. }
  139. // 断开连接
  140. disconnect() {
  141. this.manualDisconnect = true;
  142. // 取消重连
  143. if (this.reconnectTimer) {
  144. clearTimeout(this.reconnectTimer);
  145. this.reconnectTimer = null;
  146. }
  147. // 关闭 WebSocket 连接
  148. if (this.ws) {
  149. // 关闭连接
  150. if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
  151. this.ws.close();
  152. }
  153. this.ws = null;
  154. }
  155. }
  156. // 检查是否已连接
  157. isConnected(): boolean {
  158. return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
  159. }
  160. send(type: string, data?: unknown): boolean {
  161. if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
  162. return false;
  163. }
  164. try {
  165. this.ws.send(
  166. JSON.stringify({
  167. type,
  168. conversation_id: this.conversationId,
  169. data: data ?? {},
  170. })
  171. );
  172. return true;
  173. } catch {
  174. return false;
  175. }
  176. }
  177. }