websocket.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  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. onMessage?: (message: WSMessage<T>) => void; // 收到消息时的回调
  15. onError?: (error: Event) => void; // 连接错误时的回调
  16. onClose?: () => void; // 连接关闭时的回调
  17. }
  18. // WebSocket 客户端类
  19. export class WSClient<T = unknown> {
  20. private ws: WebSocket | null = null;
  21. private conversationId: number;
  22. private isVisitor: boolean;
  23. private agentId?: number; // 客服ID
  24. private onMessage?: (message: WSMessage<T>) => void;
  25. private onError?: (error: Event) => void;
  26. private onClose?: () => void;
  27. private reconnectTimer: NodeJS.Timeout | null = null;
  28. private reconnectAttempts = 0;
  29. private maxReconnectAttempts = 5;
  30. private reconnectDelay = 3000; // 3秒
  31. constructor(options: WSOptions<T>) {
  32. this.conversationId = options.conversationId;
  33. this.isVisitor = options.isVisitor !== undefined ? options.isVisitor : true;
  34. this.agentId = options.agentId;
  35. this.onMessage = options.onMessage;
  36. this.onError = options.onError;
  37. this.onClose = options.onClose;
  38. }
  39. // 连接 WebSocket
  40. connect() {
  41. // 如果已经连接,先断开
  42. if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
  43. this.ws.close();
  44. this.ws = null;
  45. }
  46. // 使用相对路径构建 WebSocket URL(自动适配当前域名和协议)
  47. // 根据当前页面的协议自动选择 ws:// 或 wss://
  48. const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  49. const host = typeof window !== 'undefined' ? window.location.host : '';
  50. let wsUrl = `${protocol}//${host}/ws?conversation_id=${this.conversationId}&is_visitor=${this.isVisitor}`;
  51. // 如果是客服连接,添加 agent_id 参数
  52. if (!this.isVisitor && this.agentId) {
  53. wsUrl += `&agent_id=${this.agentId}`;
  54. }
  55. try {
  56. this.ws = new WebSocket(wsUrl);
  57. this.ws.onopen = () => {
  58. this.reconnectAttempts = 0; // 重置重连次数
  59. };
  60. this.ws.onmessage = (event) => {
  61. try {
  62. const message: WSMessage<T> = JSON.parse(event.data);
  63. if (this.onMessage) {
  64. this.onMessage(message);
  65. }
  66. } catch (error) {
  67. console.error(
  68. `❌ 解析 WebSocket 消息失败: 对话ID=${this.conversationId}`,
  69. error
  70. );
  71. }
  72. };
  73. this.ws.onerror = (error) => {
  74. const state = this.ws?.readyState;
  75. const stateText =
  76. state === WebSocket.CONNECTING
  77. ? "连接中"
  78. : state === WebSocket.OPEN
  79. ? "已连接"
  80. : state === WebSocket.CLOSING
  81. ? "关闭中"
  82. : state === WebSocket.CLOSED
  83. ? "已关闭"
  84. : "未知";
  85. const url = this.ws?.url || wsUrl;
  86. console.error(
  87. `❌ WebSocket 错误: 对话ID=${this.conversationId}, 状态=${stateText}, URL=${url}`,
  88. error
  89. );
  90. if (this.onError) {
  91. this.onError(error);
  92. }
  93. };
  94. this.ws.onclose = (event) => {
  95. this.ws = null;
  96. if (this.onClose) {
  97. this.onClose();
  98. }
  99. // 只有在非正常关闭时才尝试重连(避免在开发模式下频繁重连)
  100. const code = event.code;
  101. const wasClean = event.wasClean;
  102. if (!wasClean && code !== 1000) {
  103. this.attemptReconnect();
  104. }
  105. };
  106. } catch (error) {
  107. console.error(
  108. `❌ 创建 WebSocket 连接失败: 对话ID=${this.conversationId}, URL=${wsUrl}`,
  109. error
  110. );
  111. if (this.onError) {
  112. // 创建一个错误事件对象
  113. const errorEvent = new Event("error");
  114. this.onError(errorEvent);
  115. }
  116. }
  117. }
  118. // 尝试重连
  119. private attemptReconnect() {
  120. if (this.reconnectAttempts >= this.maxReconnectAttempts) {
  121. console.error(`❌ WebSocket 重连次数已达上限,停止重连: 对话ID=${this.conversationId}`);
  122. return;
  123. }
  124. this.reconnectAttempts++;
  125. this.reconnectTimer = setTimeout(() => {
  126. this.connect();
  127. }, this.reconnectDelay);
  128. }
  129. // 断开连接
  130. disconnect() {
  131. // 取消重连
  132. if (this.reconnectTimer) {
  133. clearTimeout(this.reconnectTimer);
  134. this.reconnectTimer = null;
  135. }
  136. // 关闭 WebSocket 连接
  137. if (this.ws) {
  138. // 设置标志,避免重连
  139. this.reconnectAttempts = this.maxReconnectAttempts;
  140. // 关闭连接
  141. if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
  142. this.ws.close();
  143. }
  144. this.ws = null;
  145. }
  146. }
  147. // 检查是否已连接
  148. isConnected(): boolean {
  149. return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
  150. }
  151. }