diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index ee499cd..f2f508d 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -234,6 +234,8 @@ class ChatService { // 缓存会话表信息,避免每次查询 private sessionTablesCache = new Map>() private messageTableColumnsCache = new Map; updatedAt: number }>() + private messageName2IdTableCache = new Map() + private messageSenderIdCache = new Map() private readonly sessionTablesCacheTtl = 300000 // 5分钟 private readonly messageTableColumnsCacheTtlMs = 30 * 60 * 1000 private sessionMessageCountCache = new Map() @@ -1990,6 +1992,62 @@ class ChatService { return [lowerRaw] } + private resolveMessageIsSend(rawIsSend: number | null, senderUsername?: string | null): { + isSend: number | null + selfMatched: boolean + correctedBySelfIdentity: boolean + } { + const normalizedRawIsSend = Number.isFinite(rawIsSend as number) ? rawIsSend : null + const senderKeys = this.buildIdentityKeys(String(senderUsername || '')) + if (senderKeys.length === 0) { + return { + isSend: normalizedRawIsSend, + selfMatched: false, + correctedBySelfIdentity: false + } + } + + const myWxid = String(this.configService.get('myWxid') || '').trim() + const selfKeys = this.buildIdentityKeys(myWxid) + if (selfKeys.length === 0) { + return { + isSend: normalizedRawIsSend, + selfMatched: false, + correctedBySelfIdentity: false + } + } + + const selfMatched = senderKeys.some(senderKey => + selfKeys.some(selfKey => + senderKey === selfKey || + senderKey.startsWith(selfKey + '_') || + selfKey.startsWith(senderKey + '_') + ) + ) + + if (selfMatched && normalizedRawIsSend !== 1) { + return { + isSend: 1, + selfMatched: true, + correctedBySelfIdentity: true + } + } + + if (normalizedRawIsSend === null) { + return { + isSend: selfMatched ? 1 : 0, + selfMatched, + correctedBySelfIdentity: false + } + } + + return { + isSend: normalizedRawIsSend, + selfMatched, + correctedBySelfIdentity: false + } + } + private extractGroupMemberUsername(member: any): string { if (!member) return '' if (typeof member === 'string') return member.trim() @@ -3048,9 +3106,6 @@ class ChatService { private mapRowsToMessages(rows: Record[]): Message[] { const myWxid = this.configService.get('myWxid') - const cleanedWxid = myWxid ? this.cleanAccountDirName(myWxid) : null - const myWxidLower = myWxid ? myWxid.toLowerCase() : null - const cleanedWxidLower = cleanedWxid ? cleanedWxid.toLowerCase() : null const messages: Message[] = [] for (const row of rows) { @@ -3075,30 +3130,14 @@ class ChatService { const content = this.decodeMessageContent(rawMessageContent, rawCompressContent); const localType = this.getRowInt(row, ['local_type', 'localType', 'type', 'msg_type', 'msgType', 'WCDB_CT_local_type'], 1) const isSendRaw = this.getRowField(row, ['computed_is_send', 'computedIsSend', 'is_send', 'isSend', 'WCDB_CT_is_send']) - let isSend = isSendRaw === null ? null : parseInt(isSendRaw, 10) + const parsedRawIsSend = isSendRaw === null ? null : parseInt(isSendRaw, 10) const senderUsername = this.getRowField(row, ['sender_username', 'senderUsername', 'sender', 'WCDB_CT_sender_username']) || this.extractSenderUsernameFromContent(content) || null + const { isSend } = this.resolveMessageIsSend(parsedRawIsSend, senderUsername) const createTime = this.getRowInt(row, ['create_time', 'createTime', 'createtime', 'msg_create_time', 'msgCreateTime', 'msg_time', 'msgTime', 'time', 'WCDB_CT_create_time'], 0) - if (senderUsername && (myWxidLower || cleanedWxidLower)) { - const senderLower = String(senderUsername).toLowerCase() - const expectedIsSend = ( - senderLower === myWxidLower || - senderLower === cleanedWxidLower || - // 兼容非 wxid 开头的账号(如果文件夹名带后缀,如 custom_backup,而 sender 是 custom) - (myWxidLower && myWxidLower.startsWith(senderLower + '_')) || - (cleanedWxidLower && cleanedWxidLower.startsWith(senderLower + '_')) - ) ? 1 : 0 - if (isSend === null) { - isSend = expectedIsSend - // [DEBUG] Issue #34: 记录 isSend 推断过程 - if (expectedIsSend === 0 && localType === 1) { - // 仅在被判为接收且是文本消息时记录,避免刷屏 - // - } - } - } else if (senderUsername && !myWxid) { + if (senderUsername && !myWxid) { // [DEBUG] Issue #34: 未配置 myWxid,无法判断是否发送 if (messages.length < 5) { console.warn(`[ChatService] Warning: myWxid not set. Cannot determine if message is sent by me. sender=${senderUsername}`) @@ -4421,6 +4460,75 @@ class ChatService { return result.rows[0]?.name || null } + private async resolveMessageName2IdTableName(dbPath: string): Promise { + const normalizedDbPath = String(dbPath || '').trim() + if (!normalizedDbPath) return null + if (this.messageName2IdTableCache.has(normalizedDbPath)) { + return this.messageName2IdTableCache.get(normalizedDbPath) || null + } + + const result = await wcdbService.execQuery( + 'message', + normalizedDbPath, + "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Name2Id%' ORDER BY name DESC LIMIT 1" + ) + const tableName = result.success && result.rows && result.rows.length > 0 + ? String(result.rows[0]?.name || '').trim() || null + : null + this.messageName2IdTableCache.set(normalizedDbPath, tableName) + return tableName + } + + private async resolveMessageSenderUsernameById(dbPath: string, senderId: unknown): Promise { + const normalizedDbPath = String(dbPath || '').trim() + const numericSenderId = Number.parseInt(String(senderId ?? '').trim(), 10) + if (!normalizedDbPath || !Number.isFinite(numericSenderId) || numericSenderId <= 0) { + return null + } + + const cacheKey = `${normalizedDbPath}::${numericSenderId}` + if (this.messageSenderIdCache.has(cacheKey)) { + return this.messageSenderIdCache.get(cacheKey) || null + } + + const name2IdTable = await this.resolveMessageName2IdTableName(normalizedDbPath) + if (!name2IdTable) { + this.messageSenderIdCache.set(cacheKey, null) + return null + } + + const escapedTableName = String(name2IdTable).replace(/"/g, '""') + const result = await wcdbService.execQuery( + 'message', + normalizedDbPath, + `SELECT user_name FROM "${escapedTableName}" WHERE rowid = ${numericSenderId} LIMIT 1` + ) + const username = result.success && result.rows && result.rows.length > 0 + ? String(result.rows[0]?.user_name || result.rows[0]?.userName || '').trim() || null + : null + this.messageSenderIdCache.set(cacheKey, username) + return username + } + + private async resolveSenderUsernameForMessageRow( + row: Record, + rawContent: string + ): Promise { + const directSender = this.getRowField(row, ['sender_username', 'senderUsername', 'sender', 'WCDB_CT_sender_username']) + || this.extractSenderUsernameFromContent(rawContent) + if (directSender) { + return directSender + } + + const dbPath = this.getRowField(row, ['db_path', 'dbPath', '_db_path']) + const realSenderId = this.getRowField(row, ['real_sender_id', 'realSenderId']) + if (!dbPath || realSenderId === null || realSenderId === undefined || String(realSenderId).trim() === '') { + return null + } + + return this.resolveMessageSenderUsernameById(String(dbPath), realSenderId) + } + /** * 判断是否像 wxid */ @@ -6690,7 +6798,7 @@ class ChatService { db_path: dbPath, table_name: tableName } - const message = this.parseMessage(row) + const message = await this.parseMessage(row, { source: 'detail', sessionId }) if (message.localId !== 0) { return { success: true, message } @@ -6711,7 +6819,45 @@ class ChatService { if (!result.success || !result.messages) { return { success: false, error: result.error || '搜索失败' } } - const messages = result.messages.map((row: any) => this.parseMessage(row)).filter(Boolean) as Message[] + const messages: Message[] = [] + const isGroupSearch = Boolean(String(sessionId || '').trim().endsWith('@chatroom')) + + for (const row of result.messages) { + let message = await this.parseMessage(row, { source: 'search', sessionId }) + const needsDetailHydration = isGroupSearch && + Boolean(sessionId) && + message.localId > 0 && + (!message.senderUsername || message.isSend === null) + + if (needsDetailHydration && sessionId) { + const detail = await this.getMessageById(sessionId, message.localId) + if (detail.success && detail.message) { + message = { + ...message, + ...detail.message, + parsedContent: message.parsedContent || detail.message.parsedContent, + rawContent: message.rawContent || detail.message.rawContent, + content: message.content || detail.message.content + } + } + } + + if (isGroupSearch && (needsDetailHydration || message.isSend === 1)) { + console.info('[ChatService][GroupSearchHydratedHit]', { + sessionId, + localId: message.localId, + senderUsername: message.senderUsername, + isSend: message.isSend, + senderDisplayName: message.senderDisplayName, + senderAvatarUrl: message.senderAvatarUrl, + usedDetailHydration: needsDetailHydration, + parsedContent: message.parsedContent + }) + } + + messages.push(message) + } + return { success: true, messages } } catch (e) { console.error('ChatService: searchMessages 失败:', e) @@ -6719,7 +6865,7 @@ class ChatService { } } - private parseMessage(row: any): Message { + private async parseMessage(row: any, options?: { source?: 'search' | 'detail'; sessionId?: string }): Promise { const sourceInfo = this.getMessageSourceInfo(row) const rawContent = this.decodeMessageContent( this.getRowField(row, [ @@ -6746,9 +6892,9 @@ class ChatService { const localType = this.getRowInt(row, ['local_type', 'localType', 'type', 'msg_type', 'msgType', 'WCDB_CT_local_type'], 0) const createTime = this.getRowInt(row, ['create_time', 'createTime', 'createtime', 'msg_create_time', 'msgCreateTime', 'msg_time', 'msgTime', 'time', 'WCDB_CT_create_time'], 0) const sortSeq = this.getRowInt(row, ['sort_seq', 'sortSeq', 'seq', 'sequence', 'WCDB_CT_sort_seq'], createTime) - const senderUsername = this.getRowField(row, ['sender_username', 'senderUsername', 'sender', 'WCDB_CT_sender_username']) - || this.extractSenderUsernameFromContent(rawContent) - || null + const rawIsSend = this.getRowField(row, ['computed_is_send', 'computedIsSend', 'is_send', 'isSend', 'WCDB_CT_is_send']) + const senderUsername = await this.resolveSenderUsernameForMessageRow(row, rawContent) + const sendState = this.resolveMessageIsSend(rawIsSend === null ? null : parseInt(rawIsSend, 10), senderUsername) const msg: Message = { messageKey: this.buildMessageKey({ localId, @@ -6764,7 +6910,7 @@ class ChatService { localType, createTime, sortSeq, - isSend: this.getRowInt(row, ['computed_is_send', 'computedIsSend', 'is_send', 'isSend', 'WCDB_CT_is_send'], 0), + isSend: sendState.isSend, senderUsername, rawContent: rawContent, content: rawContent, // 添加原始内容供视频MD5解析使用 @@ -6785,6 +6931,19 @@ class ChatService { }) } + if (options?.source === 'search' && String(options.sessionId || '').endsWith('@chatroom') && sendState.selfMatched) { + console.info('[ChatService][GroupSearchSelfHit]', { + sessionId: options.sessionId, + localId, + createTime, + senderUsername, + rawIsSend, + resolvedIsSend: sendState.isSend, + correctedBySelfIdentity: sendState.correctedBySelfIdentity, + rowKeys: Object.keys(row) + }) + } + // 图片/语音解析逻辑 (简化示例,实际应调用现有解析方法) if (msg.localType === 3) { // Image const imgInfo = this.parseImageInfo(rawContent) diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 6132a0a..36e784b 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -116,6 +116,40 @@ function resolveSearchSenderUsernameFallback(value?: string | null): string | un return normalized } +function buildSearchIdentityCandidates(value?: string | null): string[] { + const normalized = normalizeSearchIdentityText(value) + if (!normalized) return [] + const lower = normalized.toLowerCase() + const candidates = new Set([lower]) + if (lower.startsWith('wxid_')) { + const match = lower.match(/^(wxid_[^_]+)/i) + if (match?.[1]) { + candidates.add(match[1]) + } + } + return [...candidates] +} + +function isCurrentUserSearchIdentity( + senderUsername?: string | null, + myWxid?: string | null +): boolean { + const senderCandidates = buildSearchIdentityCandidates(senderUsername) + const selfCandidates = buildSearchIdentityCandidates(myWxid) + if (senderCandidates.length === 0 || selfCandidates.length === 0) { + return false + } + + for (const sender of senderCandidates) { + for (const self of selfCandidates) { + if (sender === self) return true + if (sender.startsWith(self + '_')) return true + if (self.startsWith(sender + '_')) return true + } + } + return false +} + interface XmlField { key: string; value: string; @@ -2764,6 +2798,7 @@ function ChatPage(props: ChatPageProps) { const { normalizedSessionId, isDirectSearchSession, + isGroupSearchSession, resolvedSessionDisplayName, resolvedSessionAvatarUrl } = resolveSearchSessionContext(sessionId) @@ -2771,6 +2806,7 @@ function ChatPage(props: ChatPageProps) { return sortedMessages.map((message) => { const senderUsername = normalizeSearchIdentityText(message.senderUsername) || message.senderUsername + const inferredSelfFromSender = isGroupSearchSession && isCurrentUserSearchIdentity(senderUsername, myWxid) const senderDisplayName = resolveSearchSenderDisplayName( message.senderDisplayName, senderUsername, @@ -2778,7 +2814,8 @@ function ChatPage(props: ChatPageProps) { ) const senderUsernameFallback = resolveSearchSenderUsernameFallback(senderUsername) const senderAvatarUrl = normalizeSearchAvatarUrl(message.senderAvatarUrl) - const nextSenderDisplayName = message.isSend === 1 + const nextIsSend = inferredSelfFromSender ? 1 : message.isSend + const nextSenderDisplayName = nextIsSend === 1 ? (senderDisplayName || '我') : ( senderDisplayName || @@ -2787,12 +2824,29 @@ function ChatPage(props: ChatPageProps) { (isDirectSearchSession ? resolvedSessionUsernameFallback : undefined) || '未知' ) - const nextSenderAvatarUrl = message.isSend === 1 + const nextSenderAvatarUrl = nextIsSend === 1 ? (senderAvatarUrl || myAvatarUrl) : (senderAvatarUrl || (isDirectSearchSession ? resolvedSessionAvatarUrl : undefined)) + if (inferredSelfFromSender) { + console.info('[InSessionSearch][GroupSelfHit][hydrate]', { + sessionId: normalizedSessionId, + localId: message.localId, + senderUsername, + rawIsSend: message.isSend, + nextIsSend, + rawSenderDisplayName: message.senderDisplayName, + nextSenderDisplayName, + rawSenderAvatarUrl: message.senderAvatarUrl, + nextSenderAvatarUrl, + myWxid, + hasMyAvatarUrl: Boolean(myAvatarUrl) + }) + } + if ( senderUsername === message.senderUsername && + nextIsSend === message.isSend && nextSenderDisplayName === message.senderDisplayName && nextSenderAvatarUrl === message.senderAvatarUrl ) { @@ -2801,12 +2855,13 @@ function ChatPage(props: ChatPageProps) { return { ...message, + isSend: nextIsSend, senderUsername, senderDisplayName: nextSenderDisplayName, senderAvatarUrl: nextSenderAvatarUrl } }) - }, [currentSessionId, myAvatarUrl, resolveSearchSessionContext]) + }, [currentSessionId, myAvatarUrl, myWxid, resolveSearchSessionContext]) const enrichMessagesWithSenderProfiles = useCallback(async (rawMessages: Message[], sessionId?: string) => { let messages = hydrateInSessionSearchResults(rawMessages, sessionId) @@ -2962,6 +3017,7 @@ function ChatPage(props: ChatPageProps) { return messages.map((message) => { const sender = normalizeSearchIdentityText(message.senderUsername) const profile = sender ? profileMap.get(sender) : undefined + const inferredSelfFromSender = isGroupSearchSession && isCurrentUserSearchIdentity(sender, myWxid) const profileDisplayName = resolveSearchSenderDisplayName( profile?.displayName, sender, @@ -2975,7 +3031,8 @@ function ChatPage(props: ChatPageProps) { const senderUsernameFallback = resolveSearchSenderUsernameFallback(sender) const sessionUsernameFallback = resolveSearchSenderUsernameFallback(normalizedSessionId) const currentSenderAvatarUrl = normalizeSearchAvatarUrl(message.senderAvatarUrl) - const nextSenderDisplayName = message.isSend === 1 + const nextIsSend = inferredSelfFromSender ? 1 : message.isSend + const nextSenderDisplayName = nextIsSend === 1 ? (currentSenderDisplayName || profileDisplayName || '我') : ( profileDisplayName || @@ -2985,7 +3042,7 @@ function ChatPage(props: ChatPageProps) { (isDirectSearchSession ? sessionUsernameFallback : undefined) || '未知' ) - const nextSenderAvatarUrl = message.isSend === 1 + const nextSenderAvatarUrl = nextIsSend === 1 ? (currentSenderAvatarUrl || myAvatarUrl || normalizeSearchAvatarUrl(profile?.avatarUrl)) : ( currentSenderAvatarUrl || @@ -2993,8 +3050,27 @@ function ChatPage(props: ChatPageProps) { (isDirectSearchSession ? resolvedSessionAvatarUrl : undefined) ) + if (inferredSelfFromSender) { + console.info('[InSessionSearch][GroupSelfHit][enrich]', { + sessionId: normalizedSessionId, + localId: message.localId, + senderUsername: sender, + rawIsSend: message.isSend, + nextIsSend, + profileDisplayName, + currentSenderDisplayName, + nextSenderDisplayName, + profileAvatarUrl: normalizeSearchAvatarUrl(profile?.avatarUrl), + currentSenderAvatarUrl, + nextSenderAvatarUrl, + myWxid, + hasMyAvatarUrl: Boolean(myAvatarUrl) + }) + } + if ( sender === message.senderUsername && + nextIsSend === message.isSend && nextSenderDisplayName === message.senderDisplayName && nextSenderAvatarUrl === message.senderAvatarUrl ) { @@ -3003,6 +3079,7 @@ function ChatPage(props: ChatPageProps) { return { ...message, + isSend: nextIsSend, senderUsername: sender || message.senderUsername, senderDisplayName: nextSenderDisplayName, senderAvatarUrl: nextSenderAvatarUrl @@ -3012,6 +3089,7 @@ function ChatPage(props: ChatPageProps) { currentSessionId, hydrateInSessionSearchResults, myAvatarUrl, + myWxid, resolveSearchSessionContext ])