From b64a0deae5b5d33f9e20671a68936b27f0b9b00b Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 21 七月 2026 18:03:03 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_pro2.0' into dev_pro2.0

---
 src/views/im/home/composables/useMessagePuller.ts |  464 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 464 insertions(+), 0 deletions(-)

diff --git a/src/views/im/home/composables/useMessagePuller.ts b/src/views/im/home/composables/useMessagePuller.ts
new file mode 100644
index 0000000..95cf36d
--- /dev/null
+++ b/src/views/im/home/composables/useMessagePuller.ts
@@ -0,0 +1,464 @@
+import type { Message } from '../types'
+
+import type { ImChannelMessageApi } from '#/api/im/message/channel'
+import type { ImGroupMessageApi } from '#/api/im/message/group'
+import type { ImPrivateMessageApi } from '#/api/im/message/private'
+
+import { watch } from 'vue'
+
+import { pullChannelMessageList as apiPullChannelMessageList } from '#/api/im/message/channel'
+import { pullGroupMessageList as apiPullGroupMessageList } from '#/api/im/message/group'
+import { getPrivateMaxReadMessageId as apiGetPrivateMaxReadMessageId, pullPrivateMessageList as apiPullPrivateMessageList } from '#/api/im/message/private'
+import { getCurrentUserId } from '#/views/im/utils/auth'
+
+import { buildChannelConversationStub } from '../../utils/channel'
+import {
+  MESSAGE_GROUP_PULL_SIZE,
+  MESSAGE_PRIVATE_PULL_SIZE,
+  MESSAGE_PRIVATE_READ_ENABLED
+} from '../../utils/config'
+import {
+  ImContentType,
+  ImConversationType,
+  ImMessageStatus,
+  isFriendChatTip,
+  isFriendNotification
+} from '../../utils/constants'
+import { generateClientMessageId, getPrivateMessagePeerId } from '../../utils/message'
+import { runMinIdPull } from '../../utils/pull'
+import { getFriendDisplayName, getGroupDisplayName } from '../../utils/user'
+import { useConversationStore } from '../store/conversationStore'
+import { useFriendStore } from '../store/friendStore'
+import { useGroupRequestStore } from '../store/groupRequestStore'
+import { useGroupStore } from '../store/groupStore'
+import { type PulledMessage, useMessageStore } from '../store/messageStore'
+import { useRtcStore } from '../store/rtcStore'
+import { useImWebSocketStore } from '../store/websocketStore'
+
+/** 涓夌被娑堟伅 pull 鎺ュ彛杩斿洖鐨勫師濮� VO 鑱斿悎绫诲瀷锛況unMinIdPull 鍙渶 id 鎺ㄨ繘娓告爣锛屽叿浣撳垎鍙戝湪 applyPage 鍐呮寜绫诲瀷 cast */
+type PulledRawMessage = ImChannelMessageApi.ChannelMessageRespVO | ImGroupMessageApi.GroupMessageRespVO | ImPrivateMessageApi.PrivateMessageRespVO
+
+/**
+ * 娑堟伅澧為噺鎷夊彇锛氱櫥褰曞悗鍒嗛〉鎷夊彇绂荤嚎鏈熼棿鐨勬柊娑堟伅
+ *
+ * 璁捐瑕佺偣锛�
+ * 1. 鍚屾椂鎷夊彇绉佽亰 + 缇よ亰锛屼娇鐢ㄥ悇鑷殑 `minId` 娓告爣锛坧rivateMessageMaxId / groupMessageMaxId锛�
+ * 2. 鍚庣涓�娆℃渶澶氳繑鍥� size 鏉★紱鍓嶇鎸� minId 鎸佺画缈婚〉锛岀洿鍒版帴鍙h繑鍥炵┖鍒楄〃涓烘
+ * 3. 鎷夊彇鏈熼棿 conversationStore.loading=true锛�
+ *    - conversationStore 璺宠繃鎵归噺鎸佷箙鍖栵紝閬垮厤棰戠箒鍐欏叆鍗¢】
+ *    - websocketStore 鎶婃柊鏉ョ殑 WS 鏅�氭秷鎭涪杩涚紦鍐插尯锛岀瓑寰幆缁撴潫鍚庣粺涓�鍥炴斁
+ * 4. WebSocket 閲嶈繛鍚庝細鍐嶈Е鍙戜竴娆℃媺鍙栵紝琛ラ綈鏂綉鏈熼棿閿欒繃鐨勬秷鎭�
+ */
+export const useMessagePuller = () => {
+  const conversationStore = useConversationStore()
+  const messageStore = useMessageStore()
+  const wsStore = useImWebSocketStore()
+  const friendStore = useFriendStore()
+  const groupStore = useGroupStore()
+  const groupRequestStore = useGroupRequestStore()
+  const rtcStore = useRtcStore()
+  const currentUserId = getCurrentUserId()
+
+  /** 鍒ゆ柇璇锋眰鏄惁琚富鍔ㄥ彇娑� */
+  const isAbortError = (e: unknown): boolean => {
+    const error = e as { code?: string; message?: string; name?: string; }
+    return (
+      error?.name === 'CanceledError' ||
+      error?.code === 'ERR_CANCELED' ||
+      error?.message === 'canceled'
+    )
+  }
+
+  /** 绉佽亰浼氳瘽褰掑睘锛氳嚜宸卞彂鐨勭畻"鍙戠粰 receiverId 鐨勪細璇�"锛屽惁鍒欑畻"鍙戦�佹柟鐨勪細璇�"锛沜urry currentUserId 杩涢棴鍖呭噺灏� 3 澶勮皟鐢ㄦ柟鐨勬牱鏉� */
+  const getPrivatePeerId = (message: ImPrivateMessageApi.PrivateMessageRespVO) =>
+    getPrivateMessagePeerId(message, currentUserId)
+
+  /** 鏈嶅姟绔鑱婃秷鎭� -> 鏈湴 Message锛歵argetId 鏄細璇濅富閿紙瀵圭 userId锛� */
+  const convertPrivateMessage = (message: ImPrivateMessageApi.PrivateMessageRespVO): Message => {
+    return {
+      id: message.id,
+      clientMessageId: message.clientMessageId || generateClientMessageId(),
+      type: message.type,
+      content: message.content,
+      status: message.status,
+      receiptStatus: message.receiptStatus,
+      sendTime: new Date(message.sendTime).getTime(),
+      senderId: message.senderId,
+      targetId: getPrivatePeerId(message),
+      selfSend: message.senderId === currentUserId
+    }
+  }
+
+  /** 鏈嶅姟绔兢鑱婃秷鎭� -> 鏈湴 Message */
+  const convertGroupMessage = (message: ImGroupMessageApi.GroupMessageRespVO): Message => {
+    return {
+      id: message.id,
+      clientMessageId: message.clientMessageId || generateClientMessageId(),
+      type: message.type,
+      content: message.content,
+      status: message.status,
+      sendTime: new Date(message.sendTime).getTime(),
+      senderId: message.senderId,
+      targetId: message.groupId,
+      selfSend: message.senderId === currentUserId,
+      atUserIds: message.atUserIds || [],
+      receiverUserIds: message.receiverUserIds || [],
+      receiptStatus: message.receiptStatus,
+      readCount: message.readCount
+    }
+  }
+
+  /** 鏈嶅姟绔閬撴秷鎭� -> 鏈湴 Message */
+  const convertChannelMessage = (message: ImChannelMessageApi.ChannelMessageRespVO): Message => {
+    return {
+      id: message.id,
+      clientMessageId: message.clientMessageId || generateClientMessageId(),
+      type: message.type,
+      content: message.content,
+      status: ImMessageStatus.NORMAL, // 棰戦亾鏃犳挙鍥烇紝鎭掍负姝e父
+      receiptStatus: message.receiptStatus, // 棰戦亾宸茶鎬侊細DONE 宸茶 / PENDING 鏈
+      sendTime: new Date(message.sendTime).getTime(),
+      senderId: 0, // 绯荤粺涓嬪彂锛屾棤鍙戦�佷汉
+      targetId: message.channelId, // 浼氳瘽褰掑睘鍒伴閬撶紪鍙�
+      selfSend: false,
+      materialId: message.materialId // 璇︽儏椤垫媺瀵屾枃鏈敤
+    }
+  }
+
+  /** 棰戦亾锛氫細璇濆綊灞炲埌 channelId锛沶ame / avatar 鏆傜敤鍗犱綅锛屽皢鏉ユ帴鍏� channelStore 鍚庡啀濉湡鍊� */
+  const convertChannelConversation = (message: ImChannelMessageApi.ChannelMessageRespVO) =>
+    buildChannelConversationStub(message.channelId)
+
+  /** 绉佽亰锛氫細璇濆綊灞炲埌瀵圭 userId */
+  const convertPrivateConversation = (message: ImPrivateMessageApi.PrivateMessageRespVO) => {
+    const targetId = getPrivatePeerId(message)
+    const friend = friendStore.getFriend(targetId)
+    return {
+      type: ImConversationType.PRIVATE,
+      targetId,
+      name: friend ? getFriendDisplayName(friend) : String(targetId), // 浼氳瘽鍒楄〃 / 椤堕儴鏍囬灞曠ず锛氬ソ鍙嬪娉� > 鐪熷疄鏄电О
+      avatar: friend?.avatar || '',
+      silent: friend?.silent
+    }
+  }
+
+  /** 缇よ亰锛氫細璇濆綊灞炲埌 groupId */
+  const convertGroupConversation = (message: ImGroupMessageApi.GroupMessageRespVO) => {
+    const group = groupStore.getGroup(message.groupId)
+    return {
+      type: ImConversationType.GROUP,
+      targetId: message.groupId,
+      name: group ? getGroupDisplayName(group) : String(message.groupId),
+      avatar: group?.avatar || '',
+      silent: group?.silent
+    }
+  }
+
+  /**
+   * 鍒嗙被鍨嬫媺鍙栫绾挎秷鎭細缈婚〉 / minId 娓告爣鎺ㄨ繘 / 绌洪〉鍋滅敱 runMinIdPull 璐熻矗锛岃繖閲屽彧鍋氭帴鍙e垎鏀� + 閫愭潯涓氬姟鍒嗗彂
+   * 锛堟挙鍥� / 濂藉弸閫氱煡 / 鏅�氭秷鎭級+ 鍏ュ簱銆�
+   *
+   * 鍙栨秷璇箟涓ゅ眰瀹堝崼锛岀粡 isActive 浼犲叆 runMinIdPull锛屼换涓�涓嶇瓑鍗充涪寮冩湰鎵逛笉鍏ュ簱銆佸仠姝㈢炕椤碉紝閬垮厤鏃� session 鍝嶅簲钀藉埌鏂� store锛�
+   * 1. startEpoch锛歝ancelPull() 閫掑 pullEpoch锛涚寮� IM / 鍒囪处鍙锋椂璺冲嚭
+   * 2. startUserId锛氭瘡鎵� await 鍚庢瘮瀵瑰綋鍓嶇櫥褰� userId锛涢槻寰� logout / 澶� tab 涓嬬敤鎴峰凡鍒囦絾 cancelPull 鏈Е鍙�
+   */
+  const pullByType = async (
+    conversationType: number,
+    startMinId: number,
+    startEpoch: number,
+    startUserId: number,
+    signal: AbortSignal
+  ) => {
+    // 绉佽亰 / 缇よ亰 / 棰戦亾鍚勮嚜涓�濂楁帴鍙o紱鎸� conversationType 鍒嗘敮璋冨害銆傜炕椤垫満鍒讹紙minId 娓告爣 / 绌洪〉鍒ゆ柇 / 闃叉缈伙級浜ょ粰 runMinIdPull
+    const isPrivate = conversationType === ImConversationType.PRIVATE
+    const isChannel = conversationType === ImConversationType.CHANNEL
+    const size = isPrivate ? MESSAGE_PRIVATE_PULL_SIZE : MESSAGE_GROUP_PULL_SIZE
+    const isStillValid = () =>
+      !signal.aborted && pullEpoch === startEpoch && getCurrentUserId() === startUserId
+    await runMinIdPull<PulledRawMessage>({
+      initialMinId: startMinId,
+      pageSize: size,
+      isActive: isStillValid,
+      fetchPage: ({ minId, size }) => {
+        if (isPrivate) {
+          return apiPullPrivateMessageList({ minId, size }, signal)
+        }
+        if (isChannel) {
+          return apiPullChannelMessageList({ minId, size }, signal)
+        }
+        return apiPullGroupMessageList({ minId, size }, signal)
+      },
+      applyPage: async (list, nextMinId) => {
+        const pulledMessages: PulledMessage[] = []
+        // 閫愭潯 dispatch锛氬師娑堟伅璧版壒閲� insert锛汻ECALL 淇″彿璧版壒閲� recall 鎶婂悓鎵瑰唴宸� insert 鐨勫師娑堟伅鏇存柊涓烘挙鍥炴彁绀恒��
+        // 鍚庣鎸� id 鍗囧簭杩斿洖锛屼笖淇″彿 id 涓�瀹� > 鍘熸秷鎭� id锛堝厛鏇存柊 status 鍐嶆彃淇″彿锛夛紝鎵�浠ュ師娑堟伅涓�瀹氬厛鍒般�乺ecallMessage 鎵惧緱鍒�
+        for (const raw of list) {
+          if (isChannel) {
+            const message = raw as ImChannelMessageApi.ChannelMessageRespVO
+            pulledMessages.push({
+              kind: 'insert',
+              conversationInfo: convertChannelConversation(message),
+              message: convertChannelMessage(message)
+            })
+            continue
+          }
+          if (isPrivate) {
+            const message = raw as ImPrivateMessageApi.PrivateMessageRespVO
+            // 鐗规畩锛氭挙鍥炴秷鎭殑澶勭悊
+            if (message.type === ImContentType.RECALL) {
+              pulledMessages.push({
+                kind: 'recall',
+                conversationType: ImConversationType.PRIVATE,
+                targetId: getPrivatePeerId(message),
+                recallSignalContent: message.content
+              })
+              continue
+            }
+            // 鐗规畩锛氬巻鍙插ソ鍙嬩簨浠跺彧杩樺師鑱婂ぉ姘旀场锛涘ソ鍙嬩富鏁版嵁鐢卞ソ鍙嬪閲忚ˉ鍋垮悓姝�
+            // 浠� FRIEND_ADD / FRIEND_DELETE 鎵嶄綔涓轰細璇濇皵娉″叆娑堟伅鍒楄〃
+            if (isFriendNotification(message.type) && !isFriendChatTip(message.type)) {
+              continue
+            }
+            // 鍏跺畠娑堟伅姝e父鍏ヤ細璇濇秷鎭垪琛�
+            pulledMessages.push({
+              kind: 'insert',
+              conversationInfo: convertPrivateConversation(message),
+              message: convertPrivateMessage(message)
+            })
+          } else {
+            const message = raw as ImGroupMessageApi.GroupMessageRespVO
+            // 鐗规畩锛氭挙鍥炴秷鎭殑澶勭悊
+            if (message.type === ImContentType.RECALL) {
+              pulledMessages.push({
+                kind: 'recall',
+                conversationType: ImConversationType.GROUP,
+                targetId: message.groupId,
+                recallSignalContent: message.content
+              })
+              continue
+            }
+            pulledMessages.push({
+              kind: 'insert',
+              conversationInfo: convertGroupConversation(message),
+              message: convertGroupMessage(message)
+            })
+          }
+        }
+        // 鍏ュ簱 + 鎺ㄨ繘 messageMaxId锛沶extMinId 涓虹┖锛堟湰鎵规棤鏈夋晥 id锛夋椂涓嶆帹杩涙父鏍囷紝涓庢棫閫昏緫涓�鑷�
+        await messageStore.applyPulledMessageList(pulledMessages, conversationType, nextMinId)
+      }
+    })
+  }
+
+  /** 鍚屼竴鏃跺埢鍙厑璁镐竴娆� pull锛歩ndex.vue 鐨勬墜鍔ㄨ皟鐢ㄤ笌閲嶈繛 watch 瑙﹀彂鍙兘骞跺彂锛屽叡鐢ㄥ悓涓�涓� promise 鍗冲彲鍘婚噸 */
+  let pullPromise: null | Promise<void> = null
+  let pullAbortController: AbortController | null = null
+
+  /**
+   * 棣栨 pull 鏄惁宸插畬鎴愩�備粎鍦ㄧ疆 true 鍚庯紝isConnected watch 鎵嶄細瑙﹀彂 pull銆�
+   * 闃叉 socket onopen 姣� friendStore/groupStore 棰勬媺鍏堝埌杈炬椂锛寃atcher 鎶㈣窇閫犳垚娑堟伅鎻掑叆鏃╀簬浼氳瘽鍏冩暟鎹彲瑙�
+   */
+  let initialPulled = false
+
+  /**
+   * pull 杞璁℃暟锛涘垏璐﹀彿 / 绂诲紑 IM 鏃� cancelPull() 閫掑锛屾棫 pullByType 寰幆鎸� epoch 鑷鍚庤烦鍑�
+   * 閬垮厤鏃� session 鐨勬帴鍙e搷搴斿湪鏂� session 钀藉湴锛岄�犳垚璺ㄨ处鍙锋秷鎭硠婕�
+   *
+   * 娉ㄦ剰锛氭櫘閫氭柇杩烇紙WS 鐭柇锛変笉鍙栨秷 pull鈥斺�旂綉缁滄姈鍔� / 鏈嶅姟绔噸鍚兘灞炰簬鏈处鍙锋甯哥敓鍛藉懆鏈燂紝
+   * 鍙栨秷浼氬鑷撮鎷夎涓柇鍚� initialPulled 姘歌繙鍋滃湪 false锛屽悗缁噸杩� watcher 涓嶅啀琛ユ媺
+   */
+  let pullEpoch = 0
+
+  /** 鏄惧紡鍙栨秷锛氫粎鐢� index.vue onUnmounted锛堢寮� IM / 鍒囪处鍙� / 璺敱璺冲嚭锛夎皟鐢� */
+  const cancelPull = () => {
+    pullEpoch++
+    pullAbortController?.abort()
+    pullAbortController = null
+    // 鏃� promise 浠嶅湪 finally 闃舵璺戯紝浣� epoch 瀹堝崼宸查樆鏂悗缁壇浣滅敤锛涜繖閲岀珛鍒昏 pullPromise = null 璁╂柊涓�杞彲閲嶅叆
+    pullPromise = null
+    // 鍚屾涓㈠純 WS 缂撳啿甯э紱鏃� pull 宸蹭笉浼� flushBuffer锛岃嫢涓嶆竻涓嬫杩� IM 绗竴娆� pullOnce 浼氭妸鏃� session 鐨勫抚鍥炴斁杩涙柊 store
+    wsStore.discardBuffer()
+  }
+
+  /**
+   * 鐘舵�佷簨浠惰ˉ鍋匡細濂藉弸 / 濂藉弸鐢宠璧板閲忥紱缇ゅ垪琛ㄥ拰缇ょ敵璇风孩鐐硅蛋蹇収鍒锋柊
+   *
+   * 棣栫櫥涓绘暟鎹敱 index.vue 椹卞姩锛岄噸杩炴椂鍚� store 宸插氨浣嶏紝澶氳矾 allSettled 骞跺彂浜掍笉褰卞搷锛屽崟璺け璐ヤ粎璁版棩蹇椼��
+   * 缇ゆ垚鍛樹笉鍋氬叏灞�澧為噺鍚屾锛岄噸杩炲彧鏍囪鏈湴缇ゆ垚鍛� cache 杩囨湡锛岃繘鍏ョ兢浼氳瘽鎴栨垚鍛樺垪琛ㄦ椂鍐嶆寜 groupId 鍒锋柊銆�
+   */
+  const pullStateEvents = async (): Promise<void> => {
+    // 1. 娓呯悊杩炴帴绾х紦瀛�
+    messageStore.clearPrivateReadMaxIdCache()
+    rtcStore.clearGroupCallCache()
+    groupStore.markAllGroupActiveCallsExpired()
+    groupStore.markAllGroupInfoExpired()
+    groupStore.markAllGroupMembersExpired()
+    // 2. 骞跺彂琛ュ伩杩滅鐘舵��
+    const results = await Promise.allSettled([
+      friendStore.pullFriends(),
+      friendStore.pullFriendRequests(),
+      conversationStore.pullConversationReads(),
+      groupStore.fetchGroupList(true),
+      groupRequestStore.pullGroupRequests(),
+      groupRequestStore.fetchUnhandledGroupRequestList()
+    ])
+    for (const result of results) {
+      if (result.status === 'rejected') {
+        console.warn('[IM] 鐘舵�佷簨浠跺閲忚ˉ鍋垮け璐�', result.reason)
+      }
+    }
+  }
+
+  /** 鎵ц涓�娆″叏閲忓閲忔媺鍙栵紙閲嶅叆瀹夊叏锛氳繘琛屼腑鍐嶆璋冪敤澶嶇敤鍚屼竴涓� promise锛� */
+  const pullOnce = (): Promise<void> => {
+    if (!currentUserId) {
+      return Promise.resolve()
+    }
+    if (pullPromise) {
+      return pullPromise
+    }
+    const startEpoch = pullEpoch
+    // 鍚姩鏃剁殑鐢ㄦ埛蹇収锛沺ullByType 姣忔壒 await 鍚庢瘮瀵瑰綋鍓嶇櫥褰曠敤鎴凤紝璐﹀彿鍙樹簡绔嬪埢涓㈠純
+    const startUserId = currentUserId
+    const abortController = new AbortController()
+    pullAbortController = abortController
+    // 鏈疆 pull 浠嶅睘浜庡綋鍓� session锛歟poch 鏈紓 + 鐢ㄦ埛鏈垏锛涗换浣曞姩鏂� store 鐘舵�佺殑鍓綔鐢ㄩ兘瑕佸厛杩囪繖閬撳叧
+    const isCurrentPull = () =>
+      !abortController.signal.aborted &&
+      pullEpoch === startEpoch &&
+      getCurrentUserId() === startUserId
+    pullPromise = (async () => {
+      try {
+        // 鏃� puller 鍦� cancelPull 鏈Е鍙戠殑寮傚父璺緞涓婂啀杩涙潵鏃讹紝鍏堜簬浠讳綍鍓綔鐢ㄩ��鍑猴紝閬垮厤姹℃煋鏂� session 鐨� loading
+        if (!isCurrentPull()) {
+          return
+        }
+        conversationStore.loading = true
+        let messagePullSucceeded = false
+        try {
+          // 骞跺彂鎷夊彇绉佽亰 + 缇よ亰 + 棰戦亾娑堟伅锛岄檷浣庡垵濮嬪姞杞借�楁椂
+          await Promise.all([
+            pullByType(
+              ImConversationType.PRIVATE,
+              messageStore.privateMessageMaxId,
+              startEpoch,
+              startUserId,
+              abortController.signal
+            ),
+            pullByType(
+              ImConversationType.GROUP,
+              messageStore.groupMessageMaxId,
+              startEpoch,
+              startUserId,
+              abortController.signal
+            ),
+            pullByType(
+              ImConversationType.CHANNEL,
+              messageStore.channelMessageMaxId,
+              startEpoch,
+              startUserId,
+              abortController.signal
+            )
+          ])
+          messagePullSucceeded = true
+        } catch (error) {
+          if (isAbortError(error)) {
+            return
+          }
+          console.error('[IM] 鎷夊彇绂荤嚎娑堟伅澶辫触:', error)
+        } finally {
+          // 浠嶅睘鏈疆鎵嶅浣� loading锛涙棫杞 cancel / 鍒囪处鍙锋椂鐢辨柊涓�杞嚜绠★紝閬垮厤瑕嗙洊鏂� session 鐨� true
+          if (isCurrentPull()) {
+            conversationStore.loading = false
+          }
+        }
+
+        // 鍙栨秷 / 鍒囪处鍙峰悗璺宠繃 flushBuffer / 鎺掑簭 / 宸茶浣嶇疆琛ラ綈
+        if (!isCurrentPull()) {
+          return
+        }
+        if (!messagePullSucceeded) {
+          return
+        }
+
+        // 鍥炴斁 WebSocket 鍦� loading 鏈熼棿鏀跺埌鐨勭紦鍐叉秷鎭�
+        const buffered = wsStore.flushBuffer()
+        const replayPersistPromises: Promise<void>[] = []
+        for (const item of buffered) {
+          if (item.conversationType === ImConversationType.PRIVATE) {
+            replayPersistPromises.push(wsStore.handlePrivateMessage(item.payload))
+          } else if (item.conversationType === ImConversationType.CHANNEL) {
+            replayPersistPromises.push(wsStore.handleChannelMessage(item.payload))
+          } else {
+            replayPersistPromises.push(wsStore.handleGroupMessage(item.payload))
+          }
+        }
+        await Promise.all(replayPersistPromises)
+
+        // pull + replay 閮藉畬鎴愬悗鍐嶆帓搴忥紝閬垮厤鍥炴斁娑堟伅鎵撲贡椤哄簭
+        conversationStore.sortConversationList()
+
+        // 閲嶈繛 / 鍐峰惎鍔ㄥ悗琛ラ綈褰撳墠婵�娲荤鑱婁細璇濈殑銆屽鏂瑰凡璇讳綅缃��
+        // 绂荤嚎鏈熼棿閿欒繃鐨� RECEIPT 鎺ㄩ�佷細琚繖閲岃ˉ鍥烇紱鍏朵粬绉佽亰浼氳瘽绛夌敤鎴风偣寮�鏃剁敱 index.vue 鐨� watch 瑙﹀彂
+        // 绉佽亰宸茶鍏抽棴鏃惰烦杩囷紝閬垮厤鎵撳埌宸茬鐢ㄦ帴鍙hЕ鍙戦敊璇棩蹇�
+        const active = conversationStore.activeConversation
+        if (MESSAGE_PRIVATE_READ_ENABLED && active && active.type === ImConversationType.PRIVATE) {
+          try {
+            const maxReadId = await apiGetPrivateMaxReadMessageId(
+              active.targetId,
+              abortController.signal
+            )
+            if (!isCurrentPull()) {
+              return
+            }
+            messageStore.updatePrivateReadMaxId(active.targetId, maxReadId)
+            if (maxReadId) {
+              messageStore.applyMessageReadReceipt({
+                conversationType: ImConversationType.PRIVATE,
+                targetId: active.targetId,
+                privateReadMaxId: maxReadId
+              })
+            }
+          } catch (error) {
+            if (isAbortError(error)) {
+              return
+            }
+            console.warn('[IM] 鎷夊彇瀵规柟宸茶浣嶇疆澶辫触', error)
+          }
+        }
+      } finally {
+        // 浠嶅睘鏈疆锛氭甯稿畬鎴愰鎷夛紱epoch 绛変絾 userId 鍒囦簡锛氭竻 pullPromise 闃插崱姝汇�佷笉鏍囬鎷夛紱epoch 婕傦細cancelPull 宸叉竻锛宯o-op
+        if (isCurrentPull()) {
+          pullPromise = null
+          initialPulled = true
+          if (pullAbortController === abortController) {
+            pullAbortController = null
+          }
+        } else if (pullEpoch === startEpoch) {
+          pullPromise = null
+          if (pullAbortController === abortController) {
+            pullAbortController = null
+          }
+        }
+      }
+    })()
+    return pullPromise
+  }
+
+  /**
+   * 鏂綉鏈熼棿 WS 鏀朵笉鍒版帹閫侊細閲嶈繛鍚庢棦瑕佹寜 minId 琛ラ綈娑堟伅锛屼篃瑕佹寜 update_time + id 琛ラ綈濂藉弸 / 缇� / 缇ょ敵璇风姸鎬併��
+   * 棣栨杩炴帴鐢� index.vue 鏄惧紡椹卞姩锛坧ullOnce 鎷夋秷鎭� + 鍚� store 棣栨媺锛夛紝杩欓噷浠呰鐩栦箣鍚庣殑閲嶈繛銆�
+   * 閲嶈繛鏃� store 宸插氨浣嶏紝pullStateEvents 涓� pullOnce 骞跺彂鍗冲彲锛屾棤闇�銆屽厛灏变綅鍐嶆媺娑堟伅銆嶇殑棣栫櫥椤哄簭绾︽潫銆�
+   */
+  watch(
+    () => wsStore.isConnected,
+    (isConnected) => {
+      if (isConnected && initialPulled) {
+        void pullOnce()
+        void pullStateEvents()
+      }
+    }
+  )
+
+  return { pullOnce, cancelPull, convertPrivateMessage, convertGroupMessage }
+}

--
Gitblit v1.9.3