From 5367b3b4d92588c4e76728e6bd4ad6aae0cbb967 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 23 七月 2026 13:05:25 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_pro2.0' into dev_pro2.0
---
src/views/im/home/store/websocketStore.ts | 1259 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 1,259 insertions(+), 0 deletions(-)
diff --git a/src/views/im/home/store/websocketStore.ts b/src/views/im/home/store/websocketStore.ts
new file mode 100644
index 0000000..f948bcd
--- /dev/null
+++ b/src/views/im/home/store/websocketStore.ts
@@ -0,0 +1,1259 @@
+import type {
+ Group,
+ ImGroupMessageNotification,
+ ImMessageReadNotification,
+ ImMessageReceiptNotification,
+ ImNoConversationNotification,
+ ImNotificationWebSocketDTO,
+ ImPrivateMessageNotification,
+ Message,
+ WebSocketFrame
+} from '../types'
+
+import type { ImChannelMessageApi } from '#/api/im/message/channel'
+
+import { acceptHMRUpdate, defineStore } from 'pinia'
+
+import { readChannelMessages as apiReadChannelMessages } from '#/api/im/message/channel'
+import { readGroupMessages as apiReadGroupMessages } from '#/api/im/message/group'
+import { readPrivateMessages as apiReadPrivateMessages } from '#/api/im/message/private'
+import { getCurrentUserId, getRefreshToken } from '#/views/im/utils/auth'
+
+import { buildChannelConversationStub } from '../../utils/channel'
+import {
+ MESSAGE_GROUP_READ_ENABLED,
+ MESSAGE_PRIVATE_READ_ENABLED,
+ WS_RECONNECT_BASE_MS,
+ WS_RECONNECT_JITTER_MS,
+ WS_RECONNECT_MAX_MS
+} from '../../utils/config'
+import {
+ ImContentType,
+ ImConversationType,
+ ImMessageReceiptStatus,
+ ImMessageStatus,
+ ImRtcCallMediaType,
+ ImRtcParticipantStatus,
+ ImWebSocketMessageType,
+ isFriendChatTip,
+ isFriendNotification,
+ isGroupRequestNotification,
+ isNormalMessage
+} from '../../utils/constants'
+import {
+ getPrivateMessagePeerId,
+ parseRtcCallPayload,
+ playAudioTip,
+ resolveCallEndReasonText
+} from '../../utils/message'
+import { getFriendDisplayName, getGroupDisplayName } from '../../utils/user'
+import { useConversationStore } from './conversationStore'
+import { type FriendNotificationPayload, useFriendStore } from './friendStore'
+import { useGroupRequestStore } from './groupRequestStore'
+import { useGroupStore } from './groupStore'
+import { useMessageStore } from './messageStore'
+import {
+ type ImRtcCallEndNotification,
+ type ImRtcCallNotification,
+ type ImRtcParticipantConnectedNotification,
+ type ImRtcParticipantDisconnectedNotification,
+ useRtcStore
+} from './rtcStore'
+
+/** FRIEND_DELETE 甯� payload 鏄惁甯� clear=true锛歝lear 璇箟鏄竻浼氳瘽鏈韩锛岃烦杩囨皵娉℃覆鏌� */
+const isFriendDeleteWithClear = (frame: ImPrivateMessageNotification): boolean => {
+ if (frame.type !== ImContentType.FRIEND_DELETE) {
+ return false
+ }
+ try {
+ const payload = JSON.parse(frame.content || '{}') as { clear?: boolean }
+ return payload.clear === true
+ } catch {
+ return false
+ }
+}
+
+/** 浠庣鑱婃秷鎭抚瑙f瀽濂藉弸閫氱煡 payload */
+const parseFriendNotificationPayload = (
+ frame: ImPrivateMessageNotification
+): FriendNotificationPayload => JSON.parse(frame.content || '{}') as FriendNotificationPayload
+
+/** 绉佽亰娑堟伅甯ф槸鍚﹀彲鎺ㄦ柇濂藉弸瀵圭 */
+const isPrivateMessageNotification = (
+ frame: ImNoConversationNotification | ImPrivateMessageNotification
+): frame is ImPrivateMessageNotification => 'senderId' in frame && 'receiverId' in frame
+
+const RTC_LIVEKIT_PROTOCOLS = new Set(['http:', 'https:', 'ws:', 'wss:'])
+const RTC_MEDIA_TYPES = new Set<number>(Object.values(ImRtcCallMediaType))
+
+/** 蹇界暐鏅�氬疄鏃跺抚鎸佷箙鍖栧け璐� */
+function ignoreRealtimePersistError(promise: Promise<void>): void {
+ void promise.catch(() => undefined)
+}
+
+interface WebSocketListenerSet {
+ close: (event: CloseEvent) => void
+ error: (event: Event) => void
+ message: (event: MessageEvent) => void
+ open: (event: Event) => void
+}
+
+const websocketListenerMap = new WeakMap<WebSocket, WebSocketListenerSet>()
+
+/** 缁戝畾 WebSocket 浜嬩欢 */
+function bindWebSocketListeners(socket: WebSocket, listeners: WebSocketListenerSet): void {
+ websocketListenerMap.set(socket, listeners)
+ socket.addEventListener('open', listeners.open)
+ socket.addEventListener('message', listeners.message)
+ socket.addEventListener('close', listeners.close)
+ socket.addEventListener('error', listeners.error)
+}
+
+/** 瑙g粦 WebSocket 浜嬩欢 */
+function unbindWebSocketListeners(socket: WebSocket): void {
+ const listeners = websocketListenerMap.get(socket)
+ if (!listeners) {
+ return
+ }
+ socket.removeEventListener('open', listeners.open)
+ socket.removeEventListener('message', listeners.message)
+ socket.removeEventListener('close', listeners.close)
+ socket.removeEventListener('error', listeners.error)
+ websocketListenerMap.delete(socket)
+}
+
+/** 鏍¢獙 LiveKit 杩炴帴鍦板潃 */
+function isValidLiveKitUrl(url?: string): boolean {
+ if (!url) {
+ return false
+ }
+ try {
+ return RTC_LIVEKIT_PROTOCOLS.has(new URL(url).protocol)
+ } catch {
+ return false
+ }
+}
+
+/** 鏍¢獙鏉ョ數淇′护杞借嵎 */
+function isValidRtcInvitePayload(payload: ImRtcCallNotification): boolean {
+ if (!payload.room || !payload.token || !isValidLiveKitUrl(payload.livekitUrl)) {
+ return false
+ }
+ if (!RTC_MEDIA_TYPES.has(payload.mediaType) || !payload.inviterUserId) {
+ return false
+ }
+ if (payload.conversationType === ImConversationType.PRIVATE) {
+ return true
+ }
+ return payload.conversationType === ImConversationType.GROUP && !!payload.groupId
+}
+
+/**
+ * WebSocket 绉佽亰 DTO -> 鍓嶇 Message锛泃argetId 鏄細璇濅富閿紙瀵圭 userId锛�
+ * 涓嶅啓鍙戦�佷汉鍚嶅瓧娈碉細娓叉煋灞傝蛋 utils/user 瀹炴椂绠楋紙澶囨敞 / 缇ゆ樀绉板彉鏇村悗鍘嗗彶娑堟伅鑷姩鍒锋柊锛�
+ */
+const convertPrivateMessage = (
+ websocketMessage: ImPrivateMessageNotification,
+ currentUserId: number
+): Message => ({
+ id: websocketMessage.id,
+ clientMessageId: websocketMessage.clientMessageId,
+ type: websocketMessage.type,
+ content: websocketMessage.content,
+ status: websocketMessage.status,
+ receiptStatus: websocketMessage.receiptStatus,
+ sendTime: new Date(websocketMessage.sendTime).getTime(),
+ senderId: websocketMessage.senderId,
+ targetId: getPrivateMessagePeerId(websocketMessage, currentUserId),
+ selfSend: websocketMessage.senderId === currentUserId
+})
+
+/**
+ * WebSocket 缇よ亰 DTO -> 鍓嶇 Message
+ * 甯� atUserIds / receiverUserIds 缁� @ 鏍囪鍜屽畾鍚戞帴鏀剁敤锛�
+ * receiptStatus / readCount 璁╁绔悓姝ユ敹鍒拌嚜宸卞彂鐨勭兢娑堟伅鏃跺洖鎵� UI 绔嬪埢灏辨湁鏁版嵁
+ */
+const convertGroupMessage = (
+ websocketMessage: ImGroupMessageNotification,
+ currentUserId: number
+): Message => ({
+ id: websocketMessage.id,
+ clientMessageId: websocketMessage.clientMessageId,
+ type: websocketMessage.type,
+ content: websocketMessage.content,
+ status: websocketMessage.status,
+ sendTime: new Date(websocketMessage.sendTime).getTime(),
+ senderId: websocketMessage.senderId,
+ targetId: websocketMessage.groupId,
+ selfSend: websocketMessage.senderId === currentUserId,
+ atUserIds: websocketMessage.atUserIds || [],
+ receiverUserIds: websocketMessage.receiverUserIds || [],
+ receiptStatus: websocketMessage.receiptStatus,
+ readCount: websocketMessage.readCount
+})
+
+/**
+ * IM WebSocket Store
+ *
+ * 鑱岃矗锛堜笉鍙槸杩為�氫俊锛屼篃鏄悗绔� IM 浜嬩欢鐨勭粺涓�鍏ュ彛 鈫� 鑱斿姩 conversationStore / friendStore / groupStore锛夛細
+ *
+ * 1. 閾捐矾绠$悊锛氬缓杩� / 鏂繛 / 蹇冭烦淇濇椿 / 鑷姩閲嶈繛
+ * 2. 甯у垎鍙戯細dispatchFrame 鈫� dispatchPrivateFrame / dispatchGroupFrame锛屾寜浼氳瘽鍜屽唴瀹圭被鍨嬪垎娴�
+ * 3. 缂撳啿锛氬垵濮嬪寲鍔犺浇鏈燂紙conversationStore.loading=true锛夋殏瀛樻秷鎭紝绛� pull 瀹屾垚鍚庣敱 useMessagePuller 璋� flushBuffer 鍥炴斁
+ * 4. 浜嬩欢澶勭悊锛堟寜绫诲瀷鍒嗗彂鍒板搴� handle*锛岃仈鍔� conversation / friend / group store锛夛細
+ * - 鏅�氭秷鎭紙TEXT / IMAGE / FILE / VOICE / VIDEO锛夛細鍏ュ簱 + 褰撳墠浼氳瘽鑷姩宸茶 / 鎻愮ず闊�
+ * - 宸茶 / 鍥炴墽锛圧EAD / RECEIPT锛夛細澶氱宸茶鍚屾銆佸鏂硅鍚庡洖鎵�
+ * - 濂藉弸鍙樻洿锛團RIEND_*锛夛細鍚屾 friendStore + 绾ц仈鍒锋柊绉佽亰浼氳瘽锛汧RIEND_ADD / FRIEND_DELETE 棰濆鎻掑叆浼氳瘽姘旀场
+ * - 缇や釜浜轰俊鍙凤紙GROUP_MEMBER_SETTING_UPDATE锛夛細鍚屾 groupStore + 绾ц仈鍒锋柊缇よ亰浼氳瘽
+ * - 缇ゆ垚鍛樻樀绉板彉鏇达紙GROUP_MEMBER_NICKNAME_UPDATE锛夛細鍚屾 groupStore锛屼笉鎻掑叆娑堟伅鍒楄〃
+ * - 缇ゅ箍鎾簨浠讹紙GROUP_*锛夛細璧� handleGroupMessage + applyGroupNotification 鏃佽矾锛堝惈 DISSOLVE / QUIT / KICK 鑷垽娓呯兢锛�
+ */
+export const useImWebSocketStore = defineStore('imWebSocketStore', {
+ state: () => ({
+ socket: null as null | WebSocket,
+ isConnected: false,
+ reconnectTimer: null as null | ReturnType<typeof setTimeout>,
+ /** 杩炵画閲嶈繛澶辫触娆℃暟锛沷nopen 鎴愬姛 / disconnect 涓诲姩鏂紑鍚庢竻闆讹紝鐢ㄤ簬鎸囨暟閫�閬� */
+ reconnectAttempts: 0,
+ heartbeatTimer: null as null | ReturnType<typeof setInterval>,
+ messageBuffer: [] as Array<
+ | { conversationType: typeof ImConversationType.CHANNEL; payload: ImChannelMessageApi.ChannelMessageRespVO }
+ | {
+ conversationType: typeof ImConversationType.GROUP
+ payload: ImGroupMessageNotification
+ }
+ | {
+ conversationType: typeof ImConversationType.PRIVATE
+ payload: ImPrivateMessageNotification
+ }
+ > // 鍒濆鍖栧姞杞芥湡鍐咃紝鍏堟妸鏅�氭秷鎭涪杩涚紦鍐插尯锛宲ull 瀹屾垚鍚庡啀涓�娆℃�у洖鏀�
+ }),
+
+ actions: {
+ /**
+ * 鍙栧嚭缂撳啿鍖烘秷鎭苟娓呯┖锛堢敱 useMessagePuller 鍦� pull 瀹屾垚鍚庤皟鐢紝缁熶竴鍥炴斁缁� conversationStore锛�
+ * 閰嶅悎 messageBuffer 瀹炵幇锛氬湪 conversationStore.loading 鏈熼棿鏀跺埌鐨� WS 娑堟伅鍏堟殏瀛橈紝閬垮厤鍜� pull 鐨� minId 娓告爣鎵撴灦
+ */
+ flushBuffer() {
+ const msgs = [...this.messageBuffer]
+ this.messageBuffer = []
+ return msgs
+ },
+
+ /** 鐩存帴涓㈠純缂撳啿甯т笉鍥炴斁锛坈ancelPull / 绂诲紑 IM 璋冪敤锛岄槻姝笅娆¤繘 IM 鎶婃棫 session 甯у洖鏀捐繘鏂� store锛� */
+ discardBuffer() {
+ this.messageBuffer = []
+ },
+
+ /**
+ * 杩炴帴 WebSocket
+ * 澶嶇敤 yudao 鍐呯疆 /infra/ws 閫氶亾锛屽悗绔�氳繃 sendObject(type, content) 涓嬪彂
+ *
+ * 璋冪敤濂戠害锛氬垏璐﹀彿 / token 鍒锋柊鍓嶅繀椤诲厛 `disconnect()` 鍐� `connect()`锛�
+ * 鏈柟娉曚笉鎰熺煡 token 鍙樺寲锛屾棫 socket 鍦� CONNECTING / OPEN 鐘舵�佷細鐩存帴澶嶇敤鏃� token锛屽彲鑳芥嬁鍒伴敊璇韩浠�
+ */
+ connect() {
+ // 閴存潈鐢� refreshToken锛堢敓鍛藉懆鏈熸洿闀匡紱access token 杩囨湡鍚庢湇鍔$浼氶�氳繃 frame 閫氱煡閲嶇櫥锛�
+ const refreshToken = getRefreshToken()
+ if (!refreshToken) {
+ console.warn('[IM WS] refreshToken 涓虹┖锛岃烦杩囪繛鎺�')
+ return
+ }
+ // 鏃� socket 杩樺湪 CONNECTING / OPEN 鐩存帴澶嶇敤锛岄伩鍏嶅彔鍔犲浠� onmessage 鐩戝惉瀵艰嚧閲嶅娑堟伅 / 鎻愮ず闊� / 宸茶涓婃姤
+ const existingSocket = this.socket
+ if (
+ existingSocket &&
+ (existingSocket.readyState === WebSocket.OPEN ||
+ existingSocket.readyState === WebSocket.CONNECTING)
+ ) {
+ return
+ }
+ // 鏃� socket 宸� CLOSING / CLOSED锛氳В缁戝洖璋� + 娓呭紩鐢ㄥ啀 new锛岄伩鍏嶈�� handler 浠嶆寔鏈� store 寮曠敤闃荤 GC
+ if (existingSocket) {
+ unbindWebSocketListeners(existingSocket)
+ this.socket = null
+ }
+ const url = `${this.buildWsUrl()}/infra/ws?token=${refreshToken}`
+ const socket = new WebSocket(url)
+ this.socket = socket
+
+ bindWebSocketListeners(socket, {
+ // 杩炴帴寤虹珛锛氭爣璁颁笂绾� + 鍚姩蹇冭烦淇濇椿锛涢噸杩為��閬胯鏁板綊闆�
+ open: () => {
+ this.isConnected = true
+ this.reconnectAttempts = 0
+ console.log('[IM WS] connected')
+ this.startHeartbeat()
+ },
+
+ // 鏀跺埌甯э細'pong' 鏄績璺冲簲绛旂洿鎺ュ悶鎺夛紱鍏朵綑鎸� WebSocketFrame 瑙f瀽鍚庝氦缁� dispatchFrame 鍒嗘祦
+ message: (event) => {
+ if (event.data === 'pong') {
+ return
+ }
+ try {
+ const frame = JSON.parse(event.data) as WebSocketFrame
+ this.dispatchFrame(frame)
+ } catch (error) {
+ console.error('[IM WS] message parse error:', error)
+ }
+ },
+
+ // 鏈嶅姟绔叧闂� / 缃戠粶鏂細鏍囪涓嬬嚎锛屾寜鎸囨暟閫�閬胯嚜鍔ㄩ噸杩�
+ close: () => {
+ this.isConnected = false
+ console.log('[IM WS] disconnected')
+ this.reconnect()
+ },
+
+ // 寮傚父鏃朵笉涓诲姩 reconnect锛屼富鍔� close() 璁� close 鎴愪负鍞竴閲嶈繛鍏ュ彛
+ error: (error) => {
+ console.error('[IM WS] error:', error)
+ this.isConnected = false
+ this.socket?.close()
+ }
+ })
+ },
+
+ /** 鎷兼帴 WebSocket 鍩虹鍦板潃 */
+ buildWsUrl(): string {
+ // VITE_BASE_URL 鍙兘鏄� http:// 鎴� https:// 寮�澶达紝鏇挎崲鎴� ws:// 鎴� wss://锛涘鏋滄病閰嶇疆锛屽氨鐢ㄥ綋鍓嶉〉闈㈢殑鍗忚 + host
+ const baseUrl = (import.meta as any).env?.VITE_BASE_URL as string | undefined
+ if (baseUrl && baseUrl.length > 0) {
+ return baseUrl.replace(/^http/, 'ws')
+ }
+ // 褰撳墠椤甸潰鍗忚 + host锛堝 http://localhost:8080锛夛紝鏇挎崲鎴� ws://localhost:8080
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
+ const host = window.location.host
+ return `${protocol}//${host}`
+ },
+
+ /**
+ * 鎸� IM 閫氱煡甯у垎鍙�
+ */
+ dispatchFrame(frame: WebSocketFrame) {
+ if (frame.type !== ImWebSocketMessageType.NOTIFICATION) {
+ console.debug('[IM WS] 鏈瘑鍒簨浠�', frame)
+ return
+ }
+
+ const notification = this.safeParse(frame.content) as ImNotificationWebSocketDTO | null
+ if (!notification?.payload || !notification.contentType) {
+ return
+ }
+ const payload = {
+ ...notification.payload,
+ type: notification.contentType
+ }
+ switch (notification.conversationType) {
+ case ImConversationType.CHANNEL: {
+ this.dispatchChannelFrame(payload as ImChannelMessageApi.ChannelMessageRespVO)
+ break
+ }
+ case ImConversationType.GROUP: {
+ this.dispatchGroupFrame(payload as ImGroupMessageNotification)
+ break
+ }
+ case ImConversationType.NONE: {
+ this.dispatchNoConversationFrame(payload as ImNoConversationNotification)
+ break
+ }
+ case ImConversationType.PRIVATE: {
+ this.dispatchPrivateFrame(payload as ImPrivateMessageNotification)
+ break
+ }
+ default: {
+ console.debug('[IM WS] 鏈瘑鍒�氱煡', notification)
+ }
+ }
+ },
+
+ /**
+ * 鏃犱細璇濋�氱煡鍒嗗彂
+ */
+ dispatchNoConversationFrame(websocketMessage: ImNoConversationNotification) {
+ if (isFriendNotification(websocketMessage.type)) {
+ this.handleFriendNotification(websocketMessage)
+ return
+ }
+ if (isGroupRequestNotification(websocketMessage.type)) {
+ this.handleGroupRequestNotification(websocketMessage)
+ return
+ }
+ switch (websocketMessage.type) {
+ case ImContentType.RTC_CALL:
+ case ImContentType.RTC_PARTICIPANT_CONNECTED:
+ case ImContentType.RTC_PARTICIPANT_DISCONNECTED: {
+ this.handleRtcSignaling(websocketMessage)
+ break
+ }
+ default: {
+ console.debug('[IM WS] 鏈瘑鍒棤浼氳瘽閫氱煡', websocketMessage)
+ }
+ }
+ },
+
+ /**
+ * 棰戦亾甯у垎鍙戯細鎸� payload.type 鍒嗗埌 READ锛堝绔凡璇诲悓姝ワ級鎴栨櫘閫氱礌鏉愭帹閫�
+ */
+ dispatchChannelFrame(websocketMessage: ImChannelMessageApi.ChannelMessageRespVO) {
+ if (websocketMessage.type === ImContentType.READ) {
+ this.handleChannelRead(websocketMessage)
+ return
+ }
+ ignoreRealtimePersistError(this.handleChannelMessage(websocketMessage))
+ },
+
+ /** 棰戦亾 READ锛氳嚜宸卞叾瀹冪粓绔湪鏌愰閬撻噷鏍囦负宸茶锛屾湰绔悓姝ユ竻闆惰棰戦亾鏈 */
+ handleChannelRead(websocketMessage: ImChannelMessageApi.ChannelMessageRespVO) {
+ void useConversationStore()
+ .applyConversationReadList([
+ {
+ id: websocketMessage.id,
+ conversationType: ImConversationType.CHANNEL,
+ targetId: websocketMessage.channelId,
+ messageId: websocketMessage.id
+ }
+ ])
+ .catch((error) => console.warn('[IM WS] 棰戦亾宸茶鍚屾澶辫触', error))
+ },
+
+ /**
+ * 棰戦亾娑堟伅瀹炴椂鍏ヤ細璇濓紱棰戦亾娑堟伅鍗曞悜 + 鏃犵姸鎬佹満锛岀洿鎺� insertMessage 鍗冲彲
+ * pull 涓� WS 鎷垮埌鍚屼竴鏉� id 鏃讹紝messageStore.insertMessage 鍐呴儴鎸� id 鍘婚噸锛屼笉浼氶噸澶�
+ */
+ handleChannelMessage(websocketMessage: ImChannelMessageApi.ChannelMessageRespVO): Promise<void> {
+ const conversationStore = useConversationStore()
+ const messageStore = useMessageStore()
+ // 绂荤嚎鍔犺浇鏈熼棿鍏堢紦鍐诧紝绛� pull 瀹屾垚鍚庡啀缁熶竴鍥炴斁锛岄伩鍏嶉噸澶嶆垨椤哄簭閿欎贡
+ if (conversationStore.loading) {
+ this.messageBuffer.push({
+ conversationType: ImConversationType.CHANNEL,
+ payload: websocketMessage
+ })
+ return Promise.resolve()
+ }
+ const sendTimeMs =
+ typeof websocketMessage.sendTime === 'number'
+ ? websocketMessage.sendTime
+ : new Date(websocketMessage.sendTime).getTime()
+ const conversation = conversationStore.getConversation(
+ ImConversationType.CHANNEL,
+ websocketMessage.channelId
+ )
+ const isActive =
+ conversationStore.activeConversation?.type === ImConversationType.CHANNEL &&
+ conversationStore.activeConversation?.targetId === websocketMessage.channelId
+ // 棰戦亾鍗曞悜璁㈤槄锛宺eceiptStatus 琛ㄨ揪銆屾垜鏄惁宸茶杩欐潯銆嶏細浼氳瘽鎵撳紑鍗冲凡璇� DONE锛屽惁鍒� PENDING锛堜笌 pull 鍙e緞涓�鑷达級
+ const persistPromise = messageStore.insertMessage(
+ buildChannelConversationStub(websocketMessage.channelId),
+ {
+ id: websocketMessage.id,
+ clientMessageId: '',
+ type: websocketMessage.type,
+ content: websocketMessage.content,
+ status: ImMessageStatus.NORMAL,
+ receiptStatus: isActive ? ImMessageReceiptStatus.DONE : ImMessageReceiptStatus.PENDING,
+ sendTime: sendTimeMs,
+ senderId: 0,
+ targetId: websocketMessage.channelId,
+ selfSend: false,
+ materialId: websocketMessage.materialId
+ }
+ )
+ if (isActive) {
+ // 绐楀彛鎵撳紑 = 宸茶锛氭湰绔竻鏈 + 涓婃姤鏈嶅姟绔浣嶇疆锛岄伩鍏嶈浣嶇疆婊炲悗
+ const readReported = conversationStore.isReportedReadPositionCovered(
+ ImConversationType.CHANNEL,
+ websocketMessage.channelId,
+ websocketMessage.id
+ )
+ conversationStore.markConversationRead(
+ ImConversationType.CHANNEL,
+ websocketMessage.channelId,
+ websocketMessage.id
+ )
+ if (!readReported) {
+ apiReadChannelMessages(websocketMessage.channelId, websocketMessage.id)
+ .then(() =>
+ conversationStore.markConversationReadReported(
+ ImConversationType.CHANNEL,
+ websocketMessage.channelId,
+ websocketMessage.id
+ )
+ )
+ .catch((error) => {
+ console.warn(
+ '[IM WS] 棰戦亾鑷姩宸茶涓婃姤澶辫触',
+ {
+ conversationType: ImConversationType.CHANNEL,
+ channelId: websocketMessage.channelId,
+ messageId: websocketMessage.id
+ },
+ error
+ )
+ })
+ }
+ } else if (!conversation?.silent && isNormalMessage(websocketMessage.type)) {
+ // 闈炲綋鍓嶄細璇濅笖鏈厤鎵撴壈锛氬搷涓�涓嬫彁绀洪煶
+ playAudioTip()
+ }
+ return persistPromise
+ },
+
+ /** content 鏃㈠彲鑳藉凡鏄璞′篃鍙兘鏄� JSON 瀛楃涓诧紙鍚庣鐢� Map 搴忓垪鍖栦笅鍙戯級 */
+ safeParse(raw: unknown): null | Record<string, any> {
+ if (!raw) {
+ return null
+ }
+ if (typeof raw === 'object') {
+ return raw as Record<string, any>
+ }
+ try {
+ return JSON.parse(raw as string)
+ } catch (error) {
+ console.error('[IM WS] content 瑙f瀽澶辫触', error)
+ return null
+ }
+ },
+
+ // ==================== 鏅�氭秷鎭� ====================
+
+ /**
+ * 绉佽亰缁熶竴甯у垎鍙戯細鎸� payload.type锛圛mContentType锛夊垎鍒板凡璇� / 鍥炴墽 / 濂藉弸閫氱煡 / 鏅�氭秷鎭�
+ *
+ * 娑堟伅閫氱煡銆佸凡璇婚�氱煡銆佸洖鎵ч�氱煡鐢卞灞� contentType 缁熶竴鍒嗗彂
+ */
+ dispatchPrivateFrame(websocketMessage: ImPrivateMessageNotification) {
+ try {
+ switch (websocketMessage.type) {
+ case ImContentType.READ: {
+ this.handlePrivateRead(websocketMessage as ImMessageReadNotification)
+ break
+ }
+ case ImContentType.RECEIPT: {
+ this.handlePrivateReceipt(websocketMessage as ImMessageReceiptNotification)
+ break
+ }
+ case ImContentType.RTC_CALL_END: {
+ // 鍏ュ簱 + 鍏抽棴閫氳瘽绐� + 娓叉煋鑱婂ぉ tip锛堢鑱婂満鏅級
+ this.handleRtcCallEnd(websocketMessage)
+ ignoreRealtimePersistError(this.handlePrivateMessage(websocketMessage))
+ break
+ }
+ default: {
+ if (isFriendChatTip(websocketMessage.type)) {
+ this.handleFriendNotification(websocketMessage)
+ // FRIEND_DELETE 鐨� clear=true 璇箟鏄竻浼氳瘽鏈韩锛岃烦杩囨皵娉¢伩鍏嶅湪宸叉竻浼氳瘽閲屽啓鍏ヨ櫄鎷熸秷鎭�
+ if (!isFriendDeleteWithClear(websocketMessage)) {
+ ignoreRealtimePersistError(this.handlePrivateMessage(websocketMessage))
+ }
+ } else {
+ // TEXT / IMAGE / FILE / VOICE / VIDEO 绛夋櫘閫氭秷鎭�
+ ignoreRealtimePersistError(this.handlePrivateMessage(websocketMessage))
+ }
+ }
+ }
+ } catch (error) {
+ // 鍗曟潯甯х殑澶勭悊寮傚父涓嶅簲闃绘柇鍚庣画甯э紱鎵撳嵃瀹屾暣 websocketMessage 渚夸簬鎺掓煡
+ console.warn('[IM WS] dispatchPrivateFrame 澶勭悊澶辫触', websocketMessage, error)
+ }
+ },
+
+ /**
+ * 缇よ亰缁熶竴甯у垎鍙戯細鎸� payload.type锛圛mContentType锛夊垎鍒板凡璇� / 鍥炴墽 / 缇や釜浜轰俊鍙� / 鏅�氭秷鎭�
+ *
+ * GROUP_MEMBER_SETTING_UPDATE / GROUP_MEMBER_NICKNAME_UPDATE 鏄垚鍛樿祫鏂欏悓姝ヤ俊鍙凤紱鍏跺畠缇ゅ箍鎾簨浠惰蛋 handleGroupMessage 鍏ュ簱 + 瑙﹀彂 applyGroupNotification 鏃佽矾
+ */
+ dispatchGroupFrame(websocketMessage: ImGroupMessageNotification) {
+ try {
+ switch (websocketMessage.type) {
+ case ImContentType.GROUP_MEMBER_NICKNAME_UPDATE: {
+ this.handleGroupMemberNicknameUpdate(websocketMessage)
+ break
+ }
+ case ImContentType.GROUP_MEMBER_SETTING_UPDATE: {
+ this.handleGroupMemberSettingUpdate(websocketMessage)
+ break
+ }
+ case ImContentType.READ: {
+ this.handleGroupRead(websocketMessage as ImMessageReadNotification)
+ break
+ }
+ case ImContentType.RECEIPT: {
+ this.handleGroupReceipt(websocketMessage as ImMessageReceiptNotification)
+ break
+ }
+ case ImContentType.RTC_CALL_END: {
+ // 鍏ュ簱 + 绉婚櫎鑳跺泭鏉� + 鍏抽棴閫氳瘽绐楋紙濡傛灉褰撳墠鍦ㄨ缇ら�氳瘽鍐咃級
+ this.handleRtcCallEnd(websocketMessage)
+ ignoreRealtimePersistError(this.handleGroupMessage(websocketMessage))
+ break
+ }
+ case ImContentType.RTC_CALL_START: {
+ // 鍏ュ簱 + 娓叉煋鑱婂ぉ tip锛涘悓鏃剁敤 START payload 鍏堢敓鎴愭渶灏忚兌鍥婃潯锛屽悗缁� getActiveCall / 鍙備笌鑰呬簨浠跺啀琛ラ綈鎴愬憳
+ this.handleRtcCallStart(websocketMessage)
+ ignoreRealtimePersistError(this.handleGroupMessage(websocketMessage))
+ break
+ }
+ default: {
+ // TEXT / IMAGE / FILE / VOICE / VIDEO + GROUP_* 缇ゅ箍鎾簨浠�
+ ignoreRealtimePersistError(this.handleGroupMessage(websocketMessage))
+ }
+ }
+ } catch (error) {
+ // 鍗曟潯甯х殑澶勭悊寮傚父涓嶅簲闃绘柇鍚庣画甯э紱鎵撳嵃瀹屾暣 websocketMessage 渚夸簬鎺掓煡
+ console.warn('[IM WS] dispatchGroupFrame 澶勭悊澶辫触', websocketMessage, error)
+ }
+ },
+
+ /**
+ * 绉佽亰鏅�氭秷鎭紙TEXT / IMAGE / FILE / VOICE / VIDEO锛夊叆搴� + 鑷姩宸茶
+ *
+ * 娴佺▼锛�
+ * 1. 绂荤嚎鍔犺浇鏈熺紦鍐诧紙閬垮紑涓� pull 鍥炲~鐨勭珵鎬侊級
+ * 2. 璁$畻 selfSend / peerId 缁村害锛屾媺濂藉弸淇℃伅鍥炲~灞曠ず瀛楁
+ * 3. 鎾ゅ洖 TIP 鐩存帴杞蛋 recallMessage锛屼笉杩涙秷鎭垪琛�
+ * 4. 鏋勯�犲墠绔� Message锛屾彃鍏ュ埌瀵瑰簲绉佽亰浼氳瘽
+ * 5. 褰撳墠浼氳瘽婵�娲绘椂鑷姩涓婃姤宸茶锛涘惁鍒欓潪鍏嶆墦鎵板搷鎻愮ず闊�
+ */
+ handlePrivateMessage(websocketMessage: ImPrivateMessageNotification): Promise<void> {
+ const conversationStore = useConversationStore()
+ const friendStore = useFriendStore()
+ const currentUserId = getCurrentUserId()
+
+ // 0. 闃插尽灞傦細senderId / receiverId 鍧囦笉鍚綋鍓嶇敤鎴风殑绉佽亰甯х洿鎺ヤ涪寮冿紝閬垮厤鍚庣璺敱 / 澶氱涓插彿姹℃煋浼氳瘽
+ // 锛團RIEND_* 绛夌郴缁熼�氱煡涔熻蛋杩欐潯閫氶亾锛屼絾 fromUserId=senderId銆乼oUserId=receiverId 浠嶆槸褰撳墠鐢ㄦ埛瑙嗚锛�
+ if (
+ currentUserId &&
+ websocketMessage.senderId !== currentUserId &&
+ websocketMessage.receiverId !== currentUserId
+ ) {
+ console.warn('[IM WS] 涓㈠純涓嶅睘浜庡綋鍓嶇敤鎴风殑绉佽亰甯�', websocketMessage)
+ return Promise.resolve()
+ }
+
+ // 1. 绂荤嚎鍔犺浇鏈熼棿鍏堢紦鍐诧紝绛� pull 瀹屾垚鍚庡啀缁熶竴鍥炴斁锛岄伩鍏嶉噸澶嶆垨椤哄簭閿欎贡
+ if (conversationStore.loading) {
+ this.messageBuffer.push({
+ conversationType: ImConversationType.PRIVATE,
+ payload: websocketMessage
+ })
+ return Promise.resolve()
+ }
+
+ // 2. selfSend / peerId锛氳嚜宸卞彂鐨勬秷鎭睘浜庛�屽彂缁� receiverId 鐨勪細璇濄�嶏紝鍒汉鍙戠殑灞炰簬銆屽彂閫佽�呯殑浼氳瘽銆�
+ const selfSend = websocketMessage.senderId === currentUserId
+ const peerId = getPrivateMessagePeerId(websocketMessage, currentUserId)
+ // 鏈煡瀵圭锛堥檶鐢熶汉鍔犲ソ鍙嬪墠鍏堟敹鍒版秷鎭瓑鍦烘櫙锛夛細寮傛琛ユ媺涓�娆★紝涓嬫鍐嶆覆鏌撳氨鏈� name/avatar
+ const friend = friendStore.getFriend(peerId)
+ if (!friend) {
+ friendStore.fetchFriendInfo(peerId).catch(() => undefined)
+ }
+ // 浼氳瘽鏍囬姘歌繙璺熴�屽绔�嶈蛋锛堜笉绠¤皝鍙戠殑娑堟伅锛夛紱杩欓噷鍙畻涓�娆$粰 insertMessage 鐢�
+ const peerDisplayName = friend ? getFriendDisplayName(friend) : ''
+
+ // 3. 鍚庣鎾ゅ洖锛氫笅鍙戜竴鏉� RECALL 娑堟伅锛宑ontent 涓� `{"messageId": xxx}`锛堝榻� ImContentTypeEnum.RECALL 鈫� RecallMessage锛�
+ // 杩欓噷鎷︽埅涓嬫潵鏀硅蛋 recallMessage锛堟妸鍘熸秷鎭洿鏂颁负 RECALL 鎬侊級锛屼笉璁╁畠浣滀负鏂版秷鎭繘鍒楄〃
+ if (websocketMessage.type === ImContentType.RECALL) {
+ return useMessageStore().recallMessage(
+ ImConversationType.PRIVATE,
+ peerId,
+ websocketMessage.content
+ )
+ }
+
+ // 4. 鍚庣 DTO 鈫� 鍓嶇 Message锛氬彂閫佷汉鍚嶆覆鏌撴椂瀹炴椂绠楋紝涓嶅啓鍏ユ秷鎭瓧娈�
+ const message = convertPrivateMessage(websocketMessage, currentUserId)
+ const persistPromise = useMessageStore().insertMessage(
+ {
+ type: ImConversationType.PRIVATE,
+ targetId: peerId,
+ name: peerDisplayName || String(peerId),
+ avatar: friend?.avatar || '',
+ silent: friend?.silent
+ },
+ message
+ )
+
+ // 5. 浠呭鏂规秷鎭墠璧般�岃嚜鍔ㄥ凡璇� / 鎻愮ず闊炽�嶅垎鏀細鑷繁鍙戠殑涓嶄細瑙﹀彂
+ if (!selfSend) {
+ const conversation = conversationStore.getConversation(ImConversationType.PRIVATE, peerId)
+ const isActive =
+ conversationStore.activeConversation?.type === ImConversationType.PRIVATE &&
+ conversationStore.activeConversation?.targetId === peerId
+ if (isActive) {
+ // 鑱婂ぉ绐楀彛鎵撳紑 = 瀹為檯鐪嬪埌浜嗭細鏈娓呮湭璇伙紱绉佽亰宸茶寮�鍚椂鍐嶄笂鎶ュ悗绔紝璁╁鏂� UI 绔嬪埢鍒囧埌"宸茶"
+ // 宸茶浣嶇疆鐩存帴鐢ㄥ垰鍒扮殑娑堟伅 id锛堣繖鏉″氨鏄綋鍓嶄細璇濇渶澶� id锛�
+ const readReported = conversationStore.isReportedReadPositionCovered(
+ ImConversationType.PRIVATE,
+ peerId,
+ websocketMessage.id
+ )
+ conversationStore.markConversationRead(
+ ImConversationType.PRIVATE,
+ peerId,
+ websocketMessage.id
+ )
+ if (MESSAGE_PRIVATE_READ_ENABLED && !readReported) {
+ apiReadPrivateMessages(peerId, websocketMessage.id)
+ .then(() =>
+ conversationStore.markConversationReadReported(
+ ImConversationType.PRIVATE,
+ peerId,
+ websocketMessage.id
+ )
+ )
+ .catch((error) => {
+ console.warn(
+ '[IM WS] 绉佽亰鑷姩宸茶涓婃姤澶辫触',
+ {
+ conversationType: ImConversationType.PRIVATE,
+ peerId,
+ messageId: websocketMessage.id
+ },
+ error
+ )
+ })
+ }
+ } else if (!conversation?.silent && isNormalMessage(websocketMessage.type)) {
+ // 闈炲綋鍓嶄細璇濅笖鏈厤鎵撴壈锛氬搷涓�涓嬫彁绀洪煶锛堝甫鑺傛祦锛岃瑙� playAudioTip锛夛紱FRIEND_* 绛夌郴缁熶簨浠朵笉鍝�
+ playAudioTip()
+ }
+ }
+ return persistPromise
+ },
+
+ /** 绉佽亰 READ 浜嬩欢锛氳嚜宸辩殑鍏跺畠缁堢鍦ㄥ鏂逛細璇濋噷鏍囦负宸茶锛屾湰绔悓姝ユ竻闆舵湭璇伙紱绉佽亰宸茶鍏抽棴鏃跺厹搴曞拷鐣� */
+ handlePrivateRead(websocketMessage: ImMessageReadNotification) {
+ if (!MESSAGE_PRIVATE_READ_ENABLED) {
+ return
+ }
+ if (!websocketMessage.id || !websocketMessage.receiverId) {
+ return
+ }
+ void useConversationStore()
+ .applyConversationReadList([
+ {
+ id: websocketMessage.id,
+ conversationType: ImConversationType.PRIVATE,
+ targetId: websocketMessage.receiverId,
+ messageId: websocketMessage.id
+ }
+ ])
+ .catch((error) => console.warn('[IM WS] 绉佽亰宸茶鍚屾澶辫触', error))
+ },
+
+ /**
+ * 绉佽亰 RECEIPT 浜嬩欢锛氬鏂硅浜嗘垜鐨勬秷鎭紝鎶婂拰瀵规柟浼氳瘽閲岃嚜宸卞彂鐨勬秷鎭爣涓哄凡璇�
+ * 鍚庣灏� maxReadId 缂栫爜鍦ㄩ�氱煡鐨� id 瀛楁锛�
+ * 杩欓噷鎹鍗¤竟鐣岋紝閬垮厤鎶�"鍥炴墽鍦ㄨ矾涓婃椂鍒氬彂鐨勬秷鎭�"璇爣涓哄凡璇伙紱绉佽亰宸茶鍏抽棴鏃跺厹搴曞拷鐣�
+ */
+ handlePrivateReceipt(websocketMessage: ImMessageReceiptNotification) {
+ if (!MESSAGE_PRIVATE_READ_ENABLED) {
+ return
+ }
+ if (!websocketMessage.id) {
+ return
+ }
+ if (!websocketMessage.senderId) {
+ return
+ }
+ useMessageStore().applyMessageReadReceipt({
+ conversationType: ImConversationType.PRIVATE,
+ targetId: websocketMessage.senderId,
+ privateReadMaxId: websocketMessage.id
+ })
+ },
+
+ /**
+ * 缇よ亰鏅�氭秷鎭叆搴� + 鑷姩宸茶锛堢粨鏋勪笌 handlePrivateMessage 瀵圭О锛�
+ *
+ * 娴佺▼锛�
+ * 1. 绂荤嚎鍔犺浇鏈熺紦鍐�
+ * 2. 鏈煡缇ゆ椂鎷夌兢璇︽儏鍏滃簳
+ * 3. 鎾ゅ洖 TIP 鐩存帴杞蛋
+ * 4. 鏋勯�� Message + at 瀛楁锛屾彃鍏ュ埌瀵瑰簲缇よ亰浼氳瘽锛堝彂閫佷汉鍚嶆覆鏌撴椂瀹炴椂绠楋級
+ * 5. 褰撳墠浼氳瘽婵�娲绘椂鑷姩涓婃姤宸茶锛堝甫 lastMessageId锛夛紱鍚﹀垯闈炲厤鎵撴壈鍝嶆彁绀洪煶
+ */
+ handleGroupMessage(websocketMessage: ImGroupMessageNotification): Promise<void> {
+ const conversationStore = useConversationStore()
+ const groupStore = useGroupStore()
+ const currentUserId = getCurrentUserId()
+ const selfSend = websocketMessage.senderId === currentUserId
+
+ // 0. 闃插尽灞傦細瀹氬悜缇ゆ秷鎭� receiverUserIds 闈炵┖涓旀湭鍖呭惈褰撳墠鐢ㄦ埛鏃朵涪寮�
+ // 鑷繁鍙戠殑锛坰elfSend锛夊缁堥�氳繃锛涘叏鍛樺彲瑙侊紙receiverUserIds 涓虹┖ / 缂哄け锛変篃閫氳繃
+ const receiverUserIds = websocketMessage.receiverUserIds
+ if (
+ currentUserId &&
+ !selfSend &&
+ Array.isArray(receiverUserIds) &&
+ receiverUserIds.length > 0 &&
+ !receiverUserIds.includes(currentUserId)
+ ) {
+ console.warn('[IM WS] 涓㈠純涓嶅睘浜庡綋鍓嶇敤鎴风殑瀹氬悜缇ゆ秷鎭�', websocketMessage)
+ return Promise.resolve()
+ }
+
+ // 1. 绂荤嚎鍔犺浇鏈熺紦鍐诧紙涓庣鑱婂绉帮級
+ if (conversationStore.loading) {
+ this.messageBuffer.push({
+ conversationType: ImConversationType.GROUP,
+ payload: websocketMessage
+ })
+ return Promise.resolve()
+ }
+
+ // 2. 鏈煡缇ゆ椂鑷姩鎷夌兢璇︽儏 + 鎴愬憳锛堣鎷夊叆缇や絾杩樻病鏀跺埌 GROUP_CREATE 鏃剁殑鍏滃簳锛�
+ const group = groupStore.getGroup(websocketMessage.groupId)
+ if (!group) {
+ groupStore.fetchGroupInfo(websocketMessage.groupId).catch(() => undefined)
+ }
+
+ // 3. 鍚庣鎾ゅ洖锛氫笅鍙戜竴鏉� RECALL 娑堟伅锛宑ontent 涓� `{"messageId": xxx}`
+ // 杩欓噷鎷︽埅涓嬫潵鏀硅蛋 recallMessage锛堟妸鍘熸秷鎭洿鏂颁负 RECALL 鎬侊級
+ if (websocketMessage.type === ImContentType.RECALL) {
+ return useMessageStore().recallMessage(
+ ImConversationType.GROUP,
+ websocketMessage.groupId,
+ websocketMessage.content
+ )
+ }
+
+ // 4. 鍚庣 DTO 鈫� 鍓嶇 Message锛氬彂閫佷汉鍚嶆覆鏌撴椂瀹炴椂绠楋紝涓嶅啓鍏ユ秷鎭瓧娈�
+ const message = convertGroupMessage(websocketMessage, currentUserId)
+ const persistPromise = useMessageStore().insertMessage(
+ {
+ type: ImConversationType.GROUP,
+ targetId: websocketMessage.groupId,
+ name: group ? getGroupDisplayName(group) : String(websocketMessage.groupId),
+ avatar: group?.avatar || '',
+ silent: group?.silent
+ },
+ message
+ )
+
+ // 5. 浠呭鏂规秷鎭墠璧般�岃嚜鍔ㄥ凡璇� / 鎻愮ず闊炽�嶏紙涓庣鑱婂绉帮級
+ if (!selfSend) {
+ const conversation = conversationStore.getConversation(
+ ImConversationType.GROUP,
+ websocketMessage.groupId
+ )
+ const isActive =
+ conversationStore.activeConversation?.type === ImConversationType.GROUP &&
+ conversationStore.activeConversation?.targetId === websocketMessage.groupId
+ if (isActive) {
+ // 缇ゅ凡璇讳笂鎶ラ渶瑕佸甫 messageId锛堢兢娑堟伅浠�"璇诲埌绗嚑鏉�"鐨勬父鏍囦负鍑嗭紝鍖哄埆浜庣鑱婂彧鏍� receiverId锛夛紱缇ゅ凡璇诲叧闂椂浠呮湰鍦版竻闆�
+ const readReported = conversationStore.isReportedReadPositionCovered(
+ ImConversationType.GROUP,
+ websocketMessage.groupId,
+ websocketMessage.id
+ )
+ conversationStore.markConversationRead(
+ ImConversationType.GROUP,
+ websocketMessage.groupId,
+ websocketMessage.id
+ )
+ if (MESSAGE_GROUP_READ_ENABLED && !readReported) {
+ apiReadGroupMessages(websocketMessage.groupId, websocketMessage.id)
+ .then(() =>
+ conversationStore.markConversationReadReported(
+ ImConversationType.GROUP,
+ websocketMessage.groupId,
+ websocketMessage.id
+ )
+ )
+ .catch((error) => {
+ console.warn(
+ '[IM WS] 缇よ亰鑷姩宸茶涓婃姤澶辫触',
+ {
+ conversationType: ImConversationType.GROUP,
+ groupId: websocketMessage.groupId,
+ messageId: websocketMessage.id
+ },
+ error
+ )
+ })
+ }
+ } else if (!conversation?.silent && isNormalMessage(websocketMessage.type)) {
+ // GROUP_* 缇ゅ箍鎾簨浠剁瓑绯荤粺娑堟伅涓嶅搷鎻愮ず闊�
+ playAudioTip()
+ }
+ }
+ return persistPromise
+ },
+
+ // ==================== 缇よ亰宸茶 / 鍥炴墽 ====================
+
+ /** 缇よ亰 READ锛氳嚜宸卞叾瀹冪粓绔湪鏌愮兢閲屾爣涓哄凡璇伙紝鏈鍚屾娓呴浂璇ョ兢鏈 + @ 绾㈠瓧锛涚兢宸茶鍏抽棴鏃跺厹搴曞拷鐣� */
+ handleGroupRead(websocketMessage: ImMessageReadNotification) {
+ if (!MESSAGE_GROUP_READ_ENABLED) {
+ return
+ }
+ const readMessageId = websocketMessage.readId || websocketMessage.id
+ if (!readMessageId || !websocketMessage.groupId) {
+ return
+ }
+ void useConversationStore()
+ .applyConversationReadList([
+ {
+ id: readMessageId,
+ conversationType: ImConversationType.GROUP,
+ targetId: websocketMessage.groupId,
+ messageId: readMessageId
+ }
+ ])
+ .catch((error) => console.warn('[IM WS] 缇よ亰宸茶鍚屾澶辫触', error))
+ },
+
+ /** 缇よ亰 RECEIPT锛氭洿鏂版煇鏉$兢娑堟伅鐨� readCount / receiptStatus锛涚兢宸茶鍏抽棴鏃跺厹搴曞拷鐣� */
+ handleGroupReceipt(websocketMessage: ImMessageReceiptNotification) {
+ if (!MESSAGE_GROUP_READ_ENABLED) {
+ return
+ }
+ if (!websocketMessage.id || !websocketMessage.groupId) {
+ return
+ }
+ useMessageStore().applyMessageReadReceipt({
+ conversationType: ImConversationType.GROUP,
+ targetId: websocketMessage.groupId,
+ groupMessageId: websocketMessage.id,
+ readCount: websocketMessage.readCount,
+ receiptStatus: websocketMessage.receiptStatus
+ })
+ },
+
+ // ==================== 濂藉弸閫氱煡锛�1201-1210 娈典綅锛� ====================
+
+ /**
+ * 绠� FRIEND_ADD / FRIEND_DELETE 甯х殑銆屽绔� userId銆嶏細
+ * becomeFriends 鍗曟潯鍏ュ簱鍚庡弻鏂规敹鍒板悓涓�浠� payload锛宲ayload.friendUserId 鍥哄畾鏄� toUserId锛屾湰绔湡姝g殑瀵圭瑕佷粠甯� sender / receiver 鍙嶆帹
+ */
+ computeFriendPeerId(frame: ImPrivateMessageNotification): number {
+ const currentUserId = getCurrentUserId()
+ return getPrivateMessagePeerId(frame, currentUserId)
+ },
+
+ /**
+ * 濂藉弸閫氱煡缁熶竴鍏ュ彛锛氭寜 type 鍒嗗彂鍒� friendStore 鍐呴儴 dispatcher
+ */
+ handleFriendNotification(
+ websocketMessage: ImNoConversationNotification | ImPrivateMessageNotification
+ ) {
+ const payload = isPrivateMessageNotification(websocketMessage)
+ ? parseFriendNotificationPayload(websocketMessage)
+ : (websocketMessage as unknown as FriendNotificationPayload)
+ const friendStore = useFriendStore()
+ switch (websocketMessage.type) {
+ case ImContentType.FRIEND_ADD: {
+ friendStore.applyFriendAddNotification(
+ payload,
+ isPrivateMessageNotification(websocketMessage)
+ ? this.computeFriendPeerId(websocketMessage)
+ : payload.friendUserId
+ )
+ break
+ }
+ case ImContentType.FRIEND_BLOCK: {
+ friendStore.applyFriendBlockNotification(payload)
+ break
+ }
+ case ImContentType.FRIEND_DELETE: {
+ friendStore.applyFriendDeleteNotification(
+ payload,
+ isPrivateMessageNotification(websocketMessage)
+ ? this.computeFriendPeerId(websocketMessage)
+ : payload.friendUserId
+ )
+ break
+ }
+ case ImContentType.FRIEND_INFO_UPDATED: {
+ friendStore.applyFriendInfoUpdatedNotification(payload)
+ break
+ }
+ case ImContentType.FRIEND_REQUEST_APPROVED: {
+ friendStore.applyFriendRequestApprovedNotification(payload)
+ break
+ }
+ case ImContentType.FRIEND_REQUEST_RECEIVED: {
+ friendStore.applyFriendRequestReceivedNotification(payload)
+ break
+ }
+ case ImContentType.FRIEND_REQUEST_REJECTED: {
+ friendStore.applyFriendRequestRejectedNotification(payload)
+ break
+ }
+ case ImContentType.FRIEND_UNBLOCK: {
+ friendStore.applyFriendUnblockNotification(payload)
+ break
+ }
+ case ImContentType.FRIEND_UPDATE: {
+ friendStore.applyFriendUpdateNotification(payload)
+ break
+ }
+ default: {
+ console.debug('[IM WS] 鏈瘑鍒ソ鍙嬮�氱煡', websocketMessage)
+ }
+ }
+ },
+
+ // ==================== 鍔犵兢鐢宠閫氱煡锛�1503 / 1505 / 1506锛� ====================
+
+ /**
+ * 鍔犵兢鐢宠閫氱煡缁熶竴鍏ュ彛锛氬垎鍙戝埌 groupRequestStore锛岄┍鍔ㄦí骞� + Drawer 鍚屾
+ */
+ handleGroupRequestNotification(websocketMessage: ImNoConversationNotification) {
+ const payload = websocketMessage as { requestId?: number }
+ if (!payload.requestId) {
+ return
+ }
+ const groupRequestStore = useGroupRequestStore()
+ switch (websocketMessage.type) {
+ case ImContentType.GROUP_REQUEST_APPROVED:
+ case ImContentType.GROUP_REQUEST_REJECTED: {
+ groupRequestStore.removeGroupRequestById(payload.requestId)
+ break
+ }
+ case ImContentType.GROUP_REQUEST_RECEIVED: {
+ groupRequestStore.addGroupRequestById(payload.requestId).catch(() => undefined)
+ break
+ }
+ default: {
+ break
+ }
+ }
+ },
+
+ // ==================== 缇ゅ叧绯讳簨浠讹紙鎵胯浇浜庣兢鑱婇�氶亾锛屾寜 inner type 鍒嗘祦锛� ====================
+
+ /**
+ * GROUP_MEMBER_SETTING_UPDATE锛氬绔悓姝ユ垚鍛樹釜浜鸿缃彉鏇达紙silent / groupRemark锛�
+ *
+ * payload 鎼哄甫鍙樻洿瀛楁锛屾寜闈� null 瀛楁鐩存帴灞�閮ㄦ洿鏂帮紱鐪佷竴娆� fetchGroupMemberList 鎺ュ彛
+ */
+ handleGroupMemberSettingUpdate(websocketMessage: ImGroupMessageNotification) {
+ // content 瑙f瀽澶辫触鐢卞灞� dispatchGroupFrame 鐨� try-catch 鍏滃簳锛堝惈 websocketMessage 鎵撳嵃锛夛紝涓嶉噸澶� catch
+ const payload: { groupRemark?: string; silent?: boolean; } = JSON.parse(
+ websocketMessage.content || '{}'
+ )
+ const groupStore = useGroupStore()
+ const group = groupStore.getGroup(websocketMessage.groupId)
+ if (!group) {
+ return
+ }
+ const fields: Partial<Group> = {}
+ if (payload.silent != null) {
+ fields.silent = payload.silent
+ }
+ if (payload.groupRemark != null) {
+ fields.groupRemark = payload.groupRemark
+ }
+ if (Object.keys(fields).length > 0) {
+ groupStore.updateGroupFields(websocketMessage.groupId, fields)
+ }
+ },
+
+ /** GROUP_MEMBER_NICKNAME_UPDATE锛氬悓姝ユ垚鍛樺湪缇ら噷鐨勬樀绉� */
+ handleGroupMemberNicknameUpdate(websocketMessage: ImGroupMessageNotification) {
+ useGroupStore().applyGroupNotification(
+ websocketMessage.groupId,
+ websocketMessage.type,
+ websocketMessage.content
+ )
+ },
+
+ // ==================== 蹇冭烦 / 閲嶈繛 ====================
+
+ /** 蹇冭烦鍖咃細绾枃鏈� 'ping'锛屽搴旀湇鍔$ 'pong'锛堝悗绔繖灞傜敤绾瓧绗︿覆绾﹀畾锛岄伩鍏� JSON 瑙f瀽寮�閿�锛� */
+ sendHeartBeat() {
+ if (this.socket && this.isConnected) {
+ this.socket.send('ping')
+ }
+ },
+
+ /** 涓诲姩鏂紑锛堝垏鎹㈢敤鎴� / 閫�鍑虹櫥褰曟椂鐢級锛氬叧 socket + 鍋滃績璺� + 鍙栨秷寰呴噸杩� */
+ disconnect() {
+ if (this.socket) {
+ // close() 寮傛瑙﹀彂 onclose / onerror锛屽洖璋冮噷浼氭棤鏉′欢 reconnect锛�
+ // 涓诲姩鍏抽棴璺緞蹇呴』鍏堝叏閮ㄨВ缁戯紝鍚﹀垯 onclose 浼氬紩鍙戣嚜鍔ㄩ噸杩烇紝CONNECTING 鏈熼棿鐨� in-flight message 涔熷彲鑳借鑰� onmessage 鎶曢�掑埌 stale 涓婁笅鏂�
+ unbindWebSocketListeners(this.socket)
+ this.socket.close()
+ this.socket = null
+ }
+ // onclose 宸茶瑙g粦锛屼笉浼氬啀甯垜浠 isConnected=false锛岃繖閲屾墜鍔ㄥ浣�
+ this.isConnected = false
+ this.stopHeartbeat()
+ if (this.reconnectTimer) {
+ clearTimeout(this.reconnectTimer)
+ this.reconnectTimer = null
+ }
+ // 涓诲姩鏂紑锛堝垏璐﹀彿 / 閫�鍑猴級锛氭竻闆堕��閬胯鏁帮紝涓嬫 connect 閲嶆柊浠庢渶鐭棿闅旇捣绠�
+ this.reconnectAttempts = 0
+ },
+
+ /**
+ * 鑷姩閲嶈繛锛氭寚鏁伴��閬� base * 2^attempt锛堝皝椤� max锛�+ 0~jitter ms 闅忔満鍋忕Щ
+ *
+ * onclose 鏄敮涓�鍏ュ彛锛沷nerror 涓嶅啀璋冩湰鏂规硶锛堟祻瑙堝櫒瑙勮寖涓よ�呭繀鍚屾椂瑙﹀彂锛岄伩鍏嶈鏁� +2锛�
+ * 涓嶈娆℃暟涓婇檺锛岄鐜囧皝椤跺湪 WS_RECONNECT_MAX_MS锛堢害 30s锛夋寔缁噸璇曪紝鐩村埌閾捐矾鎭㈠鎴栦富鍔� disconnect
+ */
+ reconnect() {
+ this.stopHeartbeat()
+ if (this.reconnectTimer) {
+ clearTimeout(this.reconnectTimer)
+ this.reconnectTimer = null
+ }
+ const backoff = Math.min(
+ WS_RECONNECT_BASE_MS * 2 ** this.reconnectAttempts,
+ WS_RECONNECT_MAX_MS
+ )
+ const delay = backoff + Math.floor(Math.random() * WS_RECONNECT_JITTER_MS)
+ this.reconnectAttempts++
+ console.log(`[IM WS] reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`)
+ this.reconnectTimer = setTimeout(() => {
+ this.connect()
+ }, delay)
+ },
+
+ /** 蹇冭烦 5 绉掍竴娆★紝淇濇椿 + 鎺㈡椿锛堥摼璺柇浜� onclose 浼氳Е鍙戯紝鐢� reconnect 鍏滃簳锛� */
+ startHeartbeat() {
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer)
+ this.heartbeatTimer = setInterval(() => {
+ if (this.socket && this.isConnected) {
+ this.sendHeartBeat()
+ }
+ }, 5000)
+ },
+
+ /** 鍋滃績璺筹細disconnect / 閲嶈繛鍓嶈皟锛岄伩鍏嶈�� timer 鍦ㄦ柊 socket 涓婄户缁Е鍙� sendHeartBeat */
+ stopHeartbeat() {
+ if (this.heartbeatTimer) {
+ clearInterval(this.heartbeatTimer)
+ this.heartbeatTimer = null
+ }
+ },
+
+ // ==================== 瀹炴椂閫氳瘽淇′护鍒嗗彂 ====================
+
+ /**
+ * 閫氳瘽淇′护鍒嗗彂锛�1601 RTC_CALL锛堟寜 status 鍖哄垎 INVITING / JOINED / REJECTED / NO_ANSWER / LEFT锛�+ 1602 / 1603 鍙備笌鑰呭姞鍏� / 绂诲紑
+ * <p>
+ * 鍗曚竴 dispatcher锛屾寜 type 鍒嗗彂鍒� rtcStore
+ */
+ handleRtcSignaling(websocketMessage: ImNoConversationNotification) {
+ const rtcStore = useRtcStore()
+ switch (websocketMessage.type) {
+ case ImContentType.RTC_CALL: {
+ const payload = websocketMessage as unknown as ImRtcCallNotification
+ switch (payload.status) {
+ case ImRtcParticipantStatus.INVITING: {
+ if (!isValidRtcInvitePayload(payload)) {
+ console.warn('[IM WS] RTC_CALL invite payload 涓嶅悎娉�', payload)
+ return
+ }
+ // 褰撳墠宸插湪閫氳瘽涓細蹇界暐鏂版潵鐢碉紱鍚庣灞傞潰涔熶細鎷掔粷锛岃繖閲屾槸鍏滃簳
+ if (!rtcStore.isActive) {
+ rtcStore.showIncoming(payload)
+ }
+ break
+ }
+ case ImRtcParticipantStatus.JOINED:
+ case ImRtcParticipantStatus.LEFT: {
+ // ACCEPT / HUNGUP 鏆備笉闇�瑕佹湰绔澶栧搷搴旓紱rtcStore 鐘舵�佺敱 1602/1603 + END 缁存姢
+ break
+ }
+ case ImRtcParticipantStatus.NO_ANSWER: {
+ // 缇ら�氳瘽鍗曚汉鎸搩瓒呮椂锛涗俊浠ょ嫭绔嬩繚鐣欒涔夛紝澶勭悊涓� REJECTED 涓�鑷�
+ rtcStore.applyParticipantNoAnswer(payload)
+ break
+ }
+ case ImRtcParticipantStatus.REJECTED: {
+ rtcStore.applyParticipantRejected(payload)
+ break
+ }
+ default: {
+ console.warn('[IM WS] 鏈瘑鍒殑 RTC_CALL status', payload)
+ }
+ }
+ return
+ }
+ case ImContentType.RTC_PARTICIPANT_CONNECTED: {
+ const payload = websocketMessage as unknown as ImRtcParticipantConnectedNotification
+ if (payload?.room && payload.userId) {
+ rtcStore.applyParticipantConnected(payload)
+ }
+ return
+ }
+ case ImContentType.RTC_PARTICIPANT_DISCONNECTED: {
+ const payload = websocketMessage as unknown as ImRtcParticipantDisconnectedNotification
+ if (payload?.room && payload.userId) {
+ rtcStore.applyParticipantDisconnected(payload)
+ }
+ }
+ }
+ },
+
+ /** RTC_CALL_START 閫氳瘽寮�濮� */
+ handleRtcCallStart(websocketMessage: ImGroupMessageNotification) {
+ const payload = parseRtcCallPayload(websocketMessage.content)
+ if (!payload?.room || !payload.mediaType || !payload.inviterUserId) {
+ console.warn('[IM WS] RTC_CALL_START payload 涓嶅悎娉�', {
+ groupId: websocketMessage.groupId,
+ messageId: websocketMessage.id,
+ contentLength: websocketMessage.content?.length ?? 0
+ })
+ return
+ }
+ useRtcStore().setGroupCall({
+ room: payload.room,
+ groupId: websocketMessage.groupId,
+ mediaType: payload.mediaType,
+ inviterId: payload.inviterUserId,
+ joinedUserIds: [payload.inviterUserId],
+ inviteeIds: []
+ })
+ },
+
+ /**
+ * RTC_CALL_END 閫氳瘽缁撴潫锛涚鑱� + 缇よ亰閮借蛋杩欎竴鏉★紱payload 鎼哄甫 conversationType 鍖哄垎
+ * <p>
+ * 绉佽亰锛氬叧闂綋鍓嶉�氳瘽绐�
+ * 缇よ亰锛氱Щ闄よ兌鍥婃潯锛涘鏈鍦ㄨ缇ら�氳瘽鍐呭垯鍏抽棴閫氳瘽绐�
+ */
+ handleRtcCallEnd(
+ websocketMessage: ImGroupMessageNotification | ImPrivateMessageNotification
+ ) {
+ const payload = this.safeParse(websocketMessage.content) as ImRtcCallEndNotification | null
+ if (!payload?.room) {
+ return
+ }
+ const rtcStore = useRtcStore()
+ const isGroup = payload.conversationType === ImConversationType.GROUP
+ // 缇ら�氳瘽锛氱Щ闄ゅ搴旀埧闂寸殑鑳跺泭鏉�
+ const groupId = (websocketMessage as ImGroupMessageNotification).groupId
+ if (isGroup && groupId) {
+ rtcStore.removeGroupCall(groupId, payload.room)
+ }
+ // 閫氳瘽绐� / 鏉ョ數绐楁寚鍚戝悓涓� room 鏃跺叧闂細
+ // RUNNING / INVITING 闃舵瀵规瘮 call.room锛汭NCOMING 闃舵瀵规瘮 incomingPayload.room
+ const matchCall = rtcStore.call?.room === payload.room
+ const matchIncoming = rtcStore.incomingPayload?.room === payload.room
+ if (rtcStore.isActive && (matchCall || matchIncoming)) {
+ const reasonText = resolveCallEndReasonText(payload.endReason)
+ console.info('[Call] end:', reasonText)
+ rtcStore.reset()
+ }
+ }
+ }
+})
+
+export const useImWebSocketStoreWithOut = () => {
+ return useImWebSocketStore()
+}
+
+// dev: 璁� Pinia 鐨� actions / state 鏀瑰姩鏀寔 HMR锛岄伩鍏嶆瘡娆℃敼 store 閮藉緱纭埛
+// 鍚﹀垯 Vite 鎶婃柊妯″潡鎺ㄤ笅鏉ュ悗锛岃�� store 瀹炰緥鐨� action 闂寘浠嶆寚鍚戞棫鍑芥暟浣�
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useImWebSocketStore, import.meta.hot))
+}
--
Gitblit v1.9.3