websocket.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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. // 获取 API 基地址
  47. const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || "http://127.0.0.1:8080";
  48. // 将 http:// 替换为 ws://,将 https:// 替换为 wss://
  49. let wsUrl =
  50. apiBaseUrl.replace(/^http/, "ws") +
  51. `/ws?conversation_id=${this.conversationId}&is_visitor=${this.isVisitor}`;
  52. // 如果是客服连接,添加 agent_id 参数
  53. if (!this.isVisitor && this.agentId) {
  54. wsUrl += `&agent_id=${this.agentId}`;
  55. }
  56. try {
  57. this.ws = new WebSocket(wsUrl);
  58. this.ws.onopen = () => {
  59. this.reconnectAttempts = 0; // 重置重连次数
  60. };
  61. this.ws.onmessage = (event) => {
  62. try {
  63. const message: WSMessage<T> = JSON.parse(event.data);
  64. if (this.onMessage) {
  65. this.onMessage(message);
  66. }
  67. } catch (error) {
  68. console.error(
  69. `❌ 解析 WebSocket 消息失败: 对话ID=${this.conversationId}`,
  70. error
  71. );
  72. }
  73. };
  74. this.ws.onerror = (error) => {
  75. const state = this.ws?.readyState;
  76. const stateText =
  77. state === WebSocket.CONNECTING
  78. ? "连接中"
  79. : state === WebSocket.OPEN
  80. ? "已连接"
  81. : state === WebSocket.CLOSING
  82. ? "关闭中"
  83. : state === WebSocket.CLOSED
  84. ? "已关闭"
  85. : "未知";
  86. const url = this.ws?.url || wsUrl;
  87. console.error(
  88. `❌ WebSocket 错误: 对话ID=${this.conversationId}, 状态=${stateText}, URL=${url}`,
  89. error
  90. );
  91. if (this.onError) {
  92. this.onError(error);
  93. }
  94. };
  95. this.ws.onclose = (event) => {
  96. this.ws = null;
  97. if (this.onClose) {
  98. this.onClose();
  99. }
  100. // 只有在非正常关闭时才尝试重连(避免在开发模式下频繁重连)
  101. const code = event.code;
  102. const wasClean = event.wasClean;
  103. if (!wasClean && code !== 1000) {
  104. this.attemptReconnect();
  105. }
  106. };
  107. } catch (error) {
  108. console.error(
  109. `❌ 创建 WebSocket 连接失败: 对话ID=${this.conversationId}, URL=${wsUrl}`,
  110. error
  111. );
  112. if (this.onError) {
  113. // 创建一个错误事件对象
  114. const errorEvent = new Event("error");
  115. this.onError(errorEvent);
  116. }
  117. }
  118. }
  119. // 尝试重连
  120. private attemptReconnect() {
  121. if (this.reconnectAttempts >= this.maxReconnectAttempts) {
  122. console.error(`❌ WebSocket 重连次数已达上限,停止重连: 对话ID=${this.conversationId}`);
  123. return;
  124. }
  125. this.reconnectAttempts++;
  126. this.reconnectTimer = setTimeout(() => {
  127. this.connect();
  128. }, this.reconnectDelay);
  129. }
  130. // 断开连接
  131. disconnect() {
  132. // 取消重连
  133. if (this.reconnectTimer) {
  134. clearTimeout(this.reconnectTimer);
  135. this.reconnectTimer = null;
  136. }
  137. // 关闭 WebSocket 连接
  138. if (this.ws) {
  139. // 设置标志,避免重连
  140. this.reconnectAttempts = this.maxReconnectAttempts;
  141. // 关闭连接
  142. if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
  143. this.ws.close();
  144. }
  145. this.ws = null;
  146. }
  147. }
  148. // 检查是否已连接
  149. isConnected(): boolean {
  150. return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
  151. }
  152. }