websocket.ts 6.3 KB

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