diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index bfeaf4a..270b4dc 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -4486,15 +4486,16 @@ class ChatService { */ private parseQuoteMessage(content: string): { content?: string; sender?: string } { try { + const normalizedContent = this.decodeHtmlEntities(content || '') // 提取 refermsg 部分 - const referMsgStart = content.indexOf('') - const referMsgEnd = content.indexOf('') + const referMsgStart = normalizedContent.indexOf('') + const referMsgEnd = normalizedContent.indexOf('') if (referMsgStart === -1 || referMsgEnd === -1) { return {} } - const referMsgXml = content.substring(referMsgStart, referMsgEnd + 11) + const referMsgXml = normalizedContent.substring(referMsgStart, referMsgEnd + 11) // 提取发送者名称 let displayName = this.extractXmlValue(referMsgXml, 'displayname') @@ -4511,8 +4512,8 @@ class ChatService { let displayContent = referContent switch (referType) { case '1': - // 文本消息,清理可能的 wxid - displayContent = this.sanitizeQuotedContent(referContent) + // 文本消息优先取“部分引用”字段,缺失时再回退到完整 content + displayContent = this.extractPreferredQuotedText(referMsgXml) break case '3': displayContent = '[图片]' @@ -4552,6 +4553,76 @@ class ChatService { } } + private extractPreferredQuotedText(referMsgXml: string): string { + if (!referMsgXml) return '' + + const sources = [this.decodeHtmlEntities(referMsgXml)] + const rawMsgSource = this.extractXmlValue(referMsgXml, 'msgsource') + if (rawMsgSource) { + const decodedMsgSource = this.decodeHtmlEntities(rawMsgSource) + if (decodedMsgSource) { + sources.push(decodedMsgSource) + } + } + + const fullContent = this.sanitizeQuotedContent(this.extractXmlValue(sources[0] || referMsgXml, 'content')) + const partialText = this.extractPartialQuotedText(sources[0] || referMsgXml, fullContent) + if (partialText) return partialText + + const candidateTags = [ + 'selectedcontent', + 'selectedtext', + 'selectcontent', + 'selecttext', + 'quotecontent', + 'quotetext', + 'partcontent', + 'parttext', + 'excerpt', + 'summary', + 'preview' + ] + + for (const source of sources) { + for (const tag of candidateTags) { + const value = this.sanitizeQuotedContent(this.extractXmlValue(source, tag)) + if (value) return value + } + } + + return fullContent + } + + private extractPartialQuotedText(xml: string, fullContent: string): string { + if (!xml || !fullContent) return '' + + const startChar = this.extractXmlValue(xml, 'start') + const endChar = this.extractXmlValue(xml, 'end') + const startIndexRaw = this.extractXmlValue(xml, 'startindex') + const endIndexRaw = this.extractXmlValue(xml, 'endindex') + const startIndex = Number.parseInt(startIndexRaw, 10) + const endIndex = Number.parseInt(endIndexRaw, 10) + + if (startChar && endChar) { + const startPos = fullContent.indexOf(startChar) + if (startPos !== -1) { + const endPos = fullContent.indexOf(endChar, startPos + startChar.length - 1) + if (endPos !== -1 && endPos >= startPos) { + const sliced = fullContent.slice(startPos, endPos + endChar.length).trim() + if (sliced) return sliced + } + } + } + + if (Number.isFinite(startIndex) && Number.isFinite(endIndex) && endIndex >= startIndex) { + const chars = Array.from(fullContent) + const sliced = chars.slice(startIndex, endIndex + 1).join('').trim() + if (sliced) return sliced + } + + return '' + } + /** * 解析名片消息 * 格式: diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index 95800b6..d03af2b 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -2254,7 +2254,7 @@ class ExportService { const referMsgXml = normalized.substring(referMsgStart, referMsgEnd + 11) const quoteInfo = this.parseQuoteMessage(normalized) const replyText = this.stripSenderPrefix(this.extractXmlValue(normalized, 'title') || '') - const quotedPreview = this.formatQuotedReferencePreview( + const quotedPreview = quoteInfo.content || this.formatQuotedReferencePreview( this.extractXmlValue(referMsgXml, 'content'), this.extractXmlValue(referMsgXml, 'type') ) @@ -2960,7 +2960,7 @@ class ExportService { switch (referType) { case '1': - displayContent = this.sanitizeQuotedContent(referContent) + displayContent = this.extractPreferredQuotedText(referMsgXml) break case '3': displayContent = '[图片]' @@ -3001,6 +3001,76 @@ class ExportService { } } + private extractPreferredQuotedText(referMsgXml: string): string { + if (!referMsgXml) return '' + + const sources = [this.decodeHtmlEntities(referMsgXml)] + const rawMsgSource = this.extractXmlValue(referMsgXml, 'msgsource') + if (rawMsgSource) { + const decodedMsgSource = this.decodeHtmlEntities(rawMsgSource) + if (decodedMsgSource) { + sources.push(decodedMsgSource) + } + } + + const fullContent = this.sanitizeQuotedContent(this.extractXmlValue(sources[0] || referMsgXml, 'content')) + const partialText = this.extractPartialQuotedText(sources[0] || referMsgXml, fullContent) + if (partialText) return partialText + + const candidateTags = [ + 'selectedcontent', + 'selectedtext', + 'selectcontent', + 'selecttext', + 'quotecontent', + 'quotetext', + 'partcontent', + 'parttext', + 'excerpt', + 'summary', + 'preview' + ] + + for (const source of sources) { + for (const tag of candidateTags) { + const value = this.sanitizeQuotedContent(this.extractXmlValue(source, tag)) + if (value) return value + } + } + + return fullContent + } + + private extractPartialQuotedText(xml: string, fullContent: string): string { + if (!xml || !fullContent) return '' + + const startChar = this.extractXmlValue(xml, 'start') + const endChar = this.extractXmlValue(xml, 'end') + const startIndexRaw = this.extractXmlValue(xml, 'startindex') + const endIndexRaw = this.extractXmlValue(xml, 'endindex') + const startIndex = Number.parseInt(startIndexRaw, 10) + const endIndex = Number.parseInt(endIndexRaw, 10) + + if (startChar && endChar) { + const startPos = fullContent.indexOf(startChar) + if (startPos !== -1) { + const endPos = fullContent.indexOf(endChar, startPos + startChar.length - 1) + if (endPos !== -1 && endPos >= startPos) { + const sliced = fullContent.slice(startPos, endPos + endChar.length).trim() + if (sliced) return sliced + } + } + } + + if (Number.isFinite(startIndex) && Number.isFinite(endIndex) && endIndex >= startIndex) { + const chars = Array.from(fullContent) + const sliced = chars.slice(startIndex, endIndex + 1).join('').trim() + if (sliced) return sliced + } + + return '' + } + private extractChatLabReplyToMessageId(content: string): string | undefined { try { const normalized = this.normalizeAppMessageContent(content || '') diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 36b7cc1..5e86cc5 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -8695,6 +8695,28 @@ function MessageBubble({ appMsgTextCache.set(selector, value) return value }, [appMsgDoc, appMsgTextCache]) + const queryPreferredQuotedContent = useCallback((): string => { + if (message.quotedContent) return message.quotedContent + const candidates = [ + 'refermsg > selectedcontent', + 'refermsg > selectedtext', + 'refermsg > selectcontent', + 'refermsg > selecttext', + 'refermsg > quotecontent', + 'refermsg > quotetext', + 'refermsg > partcontent', + 'refermsg > parttext', + 'refermsg > excerpt', + 'refermsg > summary', + 'refermsg > preview', + 'refermsg > content' + ] + for (const selector of candidates) { + const value = queryAppMsgText(selector) + if (value) return value + } + return '' + }, [message.quotedContent, queryAppMsgText]) const appMsgThumbRawCandidate = useMemo(() => ( message.linkThumb || message.appMsgThumbUrl || @@ -8712,7 +8734,7 @@ function MessageBubble({ queryAppMsgText('refermsg > fromusr'), queryAppMsgText('refermsg > chatusr') ) - const quotedContent = message.quotedContent || queryAppMsgText('refermsg > content') || '' + const quotedContent = queryPreferredQuotedContent() const quotedSenderFallbackName = useMemo( () => resolveQuotedSenderFallbackDisplayName( session.username, @@ -9262,7 +9284,7 @@ function MessageBubble({ // type 57: 引用回复消息,解析 refermsg 渲染为引用样式 if (xmlType === '57') { const replyText = q('title') || cleanedParsedContent || '' - const referContent = q('refermsg > content') || '' + const referContent = queryPreferredQuotedContent() const referType = q('refermsg > type') || '' // 根据被引用消息类型渲染对应内容 @@ -9385,7 +9407,7 @@ function MessageBubble({ if (kind === 'quote') { // 引用回复消息(appMsgKind='quote',xmlType=57) const replyText = message.linkTitle || q('title') || cleanedParsedContent || '' - const referContent = message.quotedContent || q('refermsg > content') || '' + const referContent = queryPreferredQuotedContent() return ( renderBubbleWithQuote( renderQuotedMessageBlock(renderTextWithEmoji(cleanMessageContent(referContent))), @@ -9576,7 +9598,7 @@ function MessageBubble({ // 引用回复消息 (type=57),防止被误判为链接 if (appMsgType === '57') { const replyText = parsedDoc?.querySelector('title')?.textContent?.trim() || cleanedParsedContent || '' - const referContent = parsedDoc?.querySelector('refermsg > content')?.textContent?.trim() || '' + const referContent = queryPreferredQuotedContent() const referType = parsedDoc?.querySelector('refermsg > type')?.textContent?.trim() || '' const renderReferContent2 = () => {