websocket.ts 4.8 KB

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