diff --git a/electron/services/wcdbCore.ts b/electron/services/wcdbCore.ts index a5167b3..d6124dd 100644 --- a/electron/services/wcdbCore.ts +++ b/electron/services/wcdbCore.ts @@ -155,63 +155,7 @@ export class WcdbCore { return this.startMonitor(callback) } - /** - * 获取指定时间之后的新消息(增量更新) - */ - getNewMessages(sessionId: string, minTime: number, limit: number = 1000): { success: boolean; messages?: any[]; error?: string } { - if (!this.handle || !this.wcdbOpenMessageCursorLite || !this.wcdbFetchMessageBatch || !this.wcdbCloseMessageCursor) { - return { success: false, error: 'Database not handled or functions missing' } - } - // 1. Open Cursor - const cursorPtr = Buffer.alloc(8) // int64* - // wcdb_open_message_cursor_lite(handle, sessionId, batchSize, ascending, beginTime, endTime, outCursor) - // ascending=1 (ASC) to get messages AFTER minTime ordered by time - // beginTime = minTime + 1 (to avoid duplicate of the last message) - // Actually, let's use minTime, user logic might handle duplication or we just pass strictly greater - // C++ logic: create_time >= beginTimestamp. So if we want new messages, passing lastTimestamp + 1 is safer. - const openRes = this.wcdbOpenMessageCursorLite(this.handle, sessionId, limit, 1, minTime, 0, cursorPtr) - - if (openRes !== 0) { - return { success: false, error: `Open cursor failed: ${openRes}` } - } - - // Read int64 from buffer - const cursor = cursorPtr.readBigInt64LE(0) - - // 2. Fetch Batch - const outJsonPtr = Buffer.alloc(8) // void** - const outHasMorePtr = Buffer.alloc(4) // int32* - - // fetch_message_batch(handle, cursor, outJson, outHasMore) - const fetchRes = this.wcdbFetchMessageBatch(this.handle, cursor, outJsonPtr, outHasMorePtr) - - let messages: any[] = [] - if (fetchRes === 0) { - const jsonPtr = outJsonPtr.readBigInt64LE(0) // void* address - if (jsonPtr !== 0n) { - // koffi decode string - const jsonStr = this.koffi.decode(jsonPtr, 'string') - this.wcdbFreeString(jsonPtr) // Must free - if (jsonStr) { - try { - messages = JSON.parse(jsonStr) - } catch (e) { - console.error('Parse messages failed', e) - } - } - } - } - - // 3. Close Cursor - this.wcdbCloseMessageCursor(this.handle, cursor) - - if (fetchRes !== 0) { - return { success: false, error: `Fetch batch failed: ${fetchRes}` } - } - - return { success: true, messages } - } /** * 获取 DLL 路径 @@ -999,6 +943,37 @@ export class WcdbCore { } } + /** + * 获取指定时间之后的新消息 + */ + async getNewMessages(sessionId: string, minTime: number, limit: number = 1000): Promise<{ success: boolean; messages?: any[]; error?: string }> { + if (!this.ensureReady()) { + return { success: false, error: 'WCDB 未连接' } + } + try { + // 1. 打开游标 (使用 Ascending=1 从指定时间往后查) + const openRes = await this.openMessageCursorLite(sessionId, limit, true, minTime, 0) + if (!openRes.success || !openRes.cursor) { + return { success: false, error: openRes.error } + } + + const cursor = openRes.cursor + try { + // 2. 获取批次 + const fetchRes = await this.fetchMessageBatch(cursor) + if (!fetchRes.success) { + return { success: false, error: fetchRes.error } + } + return { success: true, messages: fetchRes.rows } + } finally { + // 3. 关闭游标 + await this.closeMessageCursor(cursor) + } + } catch (e) { + return { success: false, error: String(e) } + } + } + async getMessageCount(sessionId: string): Promise<{ success: boolean; count?: number; error?: string }> { if (!this.ensureReady()) { return { success: false, error: 'WCDB 未连接' } diff --git a/resources/wcdb_api.dll b/resources/wcdb_api.dll index 8c4fb39..9d0741f 100644 Binary files a/resources/wcdb_api.dll and b/resources/wcdb_api.dll differ diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index a5c8b3d..f5e99e2 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -192,6 +192,7 @@ function ChatPage(_props: ChatPageProps) { const isLoadingMessagesRef = useRef(false) const isLoadingMoreRef = useRef(false) const isConnectedRef = useRef(false) + const isRefreshingRef = useRef(false) const searchKeywordRef = useRef('') const preloadImageKeysRef = useRef>(new Set()) const lastPreloadSessionRef = useRef(null) @@ -320,6 +321,7 @@ function ChatPage(_props: ChatPageProps) { } setSessions(nextSessions) + sessionsRef.current = nextSessions // 立即启动联系人信息加载,不再延迟 500ms void enrichSessionsContactInfo(nextSessions) } else { @@ -566,10 +568,13 @@ function ChatPage(_props: ChatPageProps) { * (由用户建议:记住上一条消息时间,自动取之后的并渲染,然后后台兜底全量同步) */ const handleIncrementalRefresh = async () => { - if (!currentSessionId || isRefreshingMessages) return + if (!currentSessionId || isRefreshingRef.current) return + isRefreshingRef.current = true + setIsRefreshingMessages(true) - // 找出当前已渲染消息中的最大时间戳 - const lastMsg = messages[messages.length - 1] + // 找出当前已渲染消息中的最大时间戳(使用 getState 获取最新状态,避免闭包过时导致重复) + const currentMessages = useChatStore.getState().messages + const lastMsg = currentMessages[currentMessages.length - 1] const minTime = lastMsg?.createTime || 0 // 1. 优先执行增量查询并渲染(第一步) @@ -581,8 +586,9 @@ function ChatPage(_props: ChatPageProps) { } if (result.success && result.messages && result.messages.length > 0) { - // 过滤去重 - const existingKeys = new Set(messages.map(getMessageKey)) + // 过滤去重:必须对比实时的状态,防止在 handleRefreshMessages 运行期间导致的冲突 + const latestMessages = useChatStore.getState().messages + const existingKeys = new Set(latestMessages.map(getMessageKey)) const newOnes = result.messages.filter(m => !existingKeys.has(getMessageKey(m))) if (newOnes.length > 0) { @@ -598,18 +604,19 @@ function ChatPage(_props: ChatPageProps) { } } catch (e) { console.warn('[IncrementalRefresh] 失败,将依赖全量同步兜底:', e) + } finally { + isRefreshingRef.current = false + setIsRefreshingMessages(false) } - - // 2. 后台兜底:执行之前的完整游标刷新,确保没有遗漏(比如跨库的消息) - void handleRefreshMessages() } const handleRefreshMessages = async () => { - if (!currentSessionId || isRefreshingMessages) return + if (!currentSessionId || isRefreshingRef.current) return setJumpStartTime(0) setJumpEndTime(0) setHasMoreLater(false) setIsRefreshingMessages(true) + isRefreshingRef.current = true try { // 获取最新消息并增量添加 const result = await window.electronAPI.chat.getLatestMessages(currentSessionId, 50) as { @@ -620,13 +627,17 @@ function ChatPage(_props: ChatPageProps) { if (!result.success || !result.messages) { return } - const existing = new Set(messages.map(getMessageKey)) - const lastMsg = messages[messages.length - 1] + // 使用实时状态进行去重对比 + const latestMessages = useChatStore.getState().messages + const existing = new Set(latestMessages.map(getMessageKey)) + const lastMsg = latestMessages[latestMessages.length - 1] const lastTime = lastMsg?.createTime ?? 0 + const newMessages = result.messages.filter((msg) => { const key = getMessageKey(msg) if (existing.has(key)) return false - if (lastTime > 0 && msg.createTime < lastTime) return false + // 这里的 lastTime 仅作参考过滤,主要的去重靠 key + if (lastTime > 0 && msg.createTime < lastTime - 3600) return false // 仅过滤 1 小时之前的冗余请求 return true }) if (newMessages.length > 0) { @@ -642,6 +653,7 @@ function ChatPage(_props: ChatPageProps) { } catch (e) { console.error('刷新消息失败:', e) } finally { + isRefreshingRef.current = false setIsRefreshingMessages(false) } } diff --git a/src/stores/chatStore.ts b/src/stores/chatStore.ts index 561f568..0e166c9 100644 --- a/src/stores/chatStore.ts +++ b/src/stores/chatStore.ts @@ -80,11 +80,23 @@ export const useChatStore = create((set, get) => ({ setMessages: (messages) => set({ messages }), - appendMessages: (newMessages, prepend = false) => set((state) => ({ - messages: prepend - ? [...newMessages, ...state.messages] - : [...state.messages, ...newMessages] - })), + appendMessages: (newMessages, prepend = false) => set((state) => { + // 强制去重逻辑 + const getMsgKey = (m: Message) => { + if (m.localId && m.localId > 0) return `l:${m.localId}` + return `t:${m.createTime}:${m.sortSeq || 0}:${m.serverId || 0}` + } + const existingKeys = new Set(state.messages.map(getMsgKey)) + const filtered = newMessages.filter(m => !existingKeys.has(getMsgKey(m))) + + if (filtered.length === 0) return state + + return { + messages: prepend + ? [...filtered, ...state.messages] + : [...state.messages, ...filtered] + } + }), setLoadingMessages: (loading) => set({ isLoadingMessages: loading }), setLoadingMore: (loading) => set({ isLoadingMore: loading }),