[Bug]: 群聊导出中的邀请记录无法正常查看

Fixes #877
This commit is contained in:
xuncha
2026-05-02 07:59:54 +08:00
parent 318b553d0e
commit e61930107a
2 changed files with 68 additions and 10 deletions

View File

@@ -6652,17 +6652,35 @@ class ChatService {
} }
private cleanSystemMessage(content: string): string { private cleanSystemMessage(content: string): string {
if (!content) return '[系统消息]'
const normalized = this.cleanUtf16(this.decodeHtmlEntities(String(content)))
const readableSysmsg = this.extractReadableSystemMessageText(normalized)
if (readableSysmsg) {
return readableSysmsg
}
// 移除 XML 声明 // 移除 XML 声明
let cleaned = content.replace(/<\?xml[^?]*\?>/gi, '') let cleaned = normalized.replace(/<\?xml[^?]*\?>/gi, '')
// 移除所有 XML/HTML 标签 // 移除所有 XML/HTML 标签
cleaned = cleaned.replace(/<[^>]+>/g, '') cleaned = cleaned.replace(/<[^>]+>/g, '')
// 移除尾部的数字(如撤回消息后的时间戳) // 移除尾部的数字(如撤回消息后的时间戳)
cleaned = cleaned.replace(/\d+\s*$/, '') cleaned = cleaned.replace(/\d+\s*$/, '')
// 清理多余空白 // 清理多余空白
cleaned = cleaned.replace(/\s+/g, ' ').trim() cleaned = this.stripSenderPrefix(cleaned).replace(/\s+/g, ' ').trim()
return cleaned || '[系统消息]' return cleaned || '[系统消息]'
} }
private extractReadableSystemMessageText(content: string): string {
const sysmsgMatch = /<sysmsg\b[^>]*>([\s\S]*?)<\/sysmsg>/i.exec(content)
const source = sysmsgMatch?.[1] || content
const text =
this.extractXmlValue(source, 'plain') ||
this.extractXmlValue(source, 'text') ||
''
return this.stripSenderPrefix(text).replace(/\s+/g, ' ').trim()
}
private stripSenderPrefix(content: string): string { private stripSenderPrefix(content: string): string {
return content.replace(/^[\s]*([a-zA-Z0-9_@-]+):(?!\/\/)(?:\s*(?:\r?\n|<br\s*\/?>)\s*|\s*)/i, '') return content.replace(/^[\s]*([a-zA-Z0-9_@-]+):(?!\/\/)(?:\s*(?:\r?\n|<br\s*\/?>)\s*|\s*)/i, '')
} }

View File

@@ -2178,6 +2178,10 @@ class ExportService {
*/ */
private convertMessageType(localType: number, content: string): number { private convertMessageType(localType: number, content: string): number {
const normalized = this.normalizeAppMessageContent(content || '') const normalized = this.normalizeAppMessageContent(content || '')
if (this.isReadableSystemMessage(localType, normalized)) {
return 80
}
const xmlTypeRaw = this.extractAppMessageType(normalized) const xmlTypeRaw = this.extractAppMessageType(normalized)
const xmlType = xmlTypeRaw ? Number.parseInt(xmlTypeRaw, 10) : null const xmlType = xmlTypeRaw ? Number.parseInt(xmlTypeRaw, 10) : null
const looksLikeAppMessage = localType === 49 || normalized.includes('<appmsg') || normalized.includes('<msg>') const looksLikeAppMessage = localType === 49 || normalized.includes('<appmsg') || normalized.includes('<msg>')
@@ -2201,6 +2205,12 @@ class ExportService {
return MESSAGE_TYPE_MAP[localType] ?? 99 // 未知类型 -> OTHER return MESSAGE_TYPE_MAP[localType] ?? 99 // 未知类型 -> OTHER
} }
private isReadableSystemMessage(localType: number, content: string): boolean {
if (localType === 10000) return true
const normalized = this.normalizeAppMessageContent(content || '')
return /<sysmsg\b/i.test(this.stripSenderPrefix(normalized))
}
/** /**
* 解码消息内容 * 解码消息内容
*/ */
@@ -2627,6 +2637,10 @@ class ExportService {
emojiCaption?: string emojiCaption?: string
): string { ): string {
const safeContent = content || '' const safeContent = content || ''
const readableSystemText = this.extractReadableSystemMessageText(safeContent)
if (readableSystemText && this.isReadableSystemMessage(localType, safeContent)) {
return readableSystemText
}
if (localType === 3) return '[图片]' if (localType === 3) return '[图片]'
if (localType === 1) return this.stripSenderPrefix(safeContent) if (localType === 1) return this.stripSenderPrefix(safeContent)
@@ -3075,6 +3089,18 @@ class ExportService {
.trim() || '[系统消息]' .trim() || '[系统消息]'
} }
private extractReadableSystemMessageText(content: string): string {
if (!content) return ''
const normalized = this.normalizeAppMessageContent(content)
const sysmsgMatch = /<sysmsg\b[^>]*>([\s\S]*?)<\/sysmsg>/i.exec(this.stripSenderPrefix(normalized))
const source = sysmsgMatch?.[1] || normalized
const text =
this.extractXmlValue(source, 'plain') ||
this.extractXmlValue(source, 'text') ||
''
return this.stripSenderPrefix(text).replace(/\s+/g, ' ').trim()
}
/** /**
* 解析通话消息 * 解析通话消息
* 格式: <voipmsg type="VoIPBubbleMsg"><VoIPBubbleMsg><msg><![CDATA[...]]></msg><room_type>0/1</room_type>...</VoIPBubbleMsg></voipmsg> * 格式: <voipmsg type="VoIPBubbleMsg"><VoIPBubbleMsg><msg><![CDATA[...]]></msg><room_type>0/1</room_type>...</VoIPBubbleMsg></voipmsg>
@@ -3139,6 +3165,9 @@ class ExportService {
// 检查 XML 中的 type 标签(支持大 localType 的情况) // 检查 XML 中的 type 标签(支持大 localType 的情况)
if (content) { if (content) {
const normalized = this.normalizeAppMessageContent(content) const normalized = this.normalizeAppMessageContent(content)
if (this.isReadableSystemMessage(localType, normalized)) {
return '系统消息'
}
const xmlType = this.extractAppMessageType(normalized) const xmlType = this.extractAppMessageType(normalized)
if (xmlType) { if (xmlType) {
@@ -3936,6 +3965,11 @@ class ExportService {
} }
if (!content) return '' if (!content) return ''
const readableSystemText = this.extractReadableSystemMessageText(content)
if (readableSystemText && this.isReadableSystemMessage(localType, content)) {
return readableSystemText
}
if (localType === 1) { if (localType === 1) {
return this.stripSenderPrefix(content) return this.stripSenderPrefix(content)
} }
@@ -6585,6 +6619,9 @@ class ExportService {
msg.emojiCaption msg.emojiCaption
) )
} }
if (this.isReadableSystemMessage(msg.localType, msg.content)) {
content = this.extractReadableSystemMessageText(msg.content) || content
}
// 转账消息:追加 "谁转账给谁" 信息 // 转账消息:追加 "谁转账给谁" 信息
if (content && this.isTransferExportContent(content) && msg.content) { if (content && this.isTransferExportContent(content) && msg.content) {
@@ -7086,6 +7123,9 @@ class ExportService {
msg.emojiCaption msg.emojiCaption
) )
} }
if (this.isReadableSystemMessage(msg.localType, msg.content)) {
content = this.extractReadableSystemMessageText(msg.content) || content
}
const quotedReplyDisplay = await this.resolveQuotedReplyDisplayWithNames({ const quotedReplyDisplay = await this.resolveQuotedReplyDisplayWithNames({
content: msg.content, content: msg.content,
@@ -7141,7 +7181,7 @@ class ExportService {
localId: allMessages.length + 1, localId: allMessages.length + 1,
createTime: msg.createTime, createTime: msg.createTime,
formattedTime: this.formatTimestamp(msg.createTime), formattedTime: this.formatTimestamp(msg.createTime),
type: this.getMessageTypeName(msg.localType), type: this.getMessageTypeName(msg.localType, msg.content),
localType: msg.localType, localType: msg.localType,
content, content,
isSend: msg.isSend ? 1 : 0, isSend: msg.isSend ? 1 : 0,
@@ -8052,20 +8092,20 @@ class ExportService {
worksheet.getCell(currentRow, 2).value = this.formatTimestamp(msg.createTime) worksheet.getCell(currentRow, 2).value = this.formatTimestamp(msg.createTime)
if (useCompactColumns) { if (useCompactColumns) {
worksheet.getCell(currentRow, 3).value = senderRole worksheet.getCell(currentRow, 3).value = senderRole
worksheet.getCell(currentRow, 4).value = this.getMessageTypeName(msg.localType) worksheet.getCell(currentRow, 4).value = this.getMessageTypeName(msg.localType, msg.content)
} else if (includeGroupNicknameColumn) { } else if (includeGroupNicknameColumn) {
worksheet.getCell(currentRow, 3).value = senderNickname worksheet.getCell(currentRow, 3).value = senderNickname
worksheet.getCell(currentRow, 4).value = senderWxid worksheet.getCell(currentRow, 4).value = senderWxid
worksheet.getCell(currentRow, 5).value = senderRemark worksheet.getCell(currentRow, 5).value = senderRemark
worksheet.getCell(currentRow, 6).value = senderGroupNickname worksheet.getCell(currentRow, 6).value = senderGroupNickname
worksheet.getCell(currentRow, 7).value = senderRole worksheet.getCell(currentRow, 7).value = senderRole
worksheet.getCell(currentRow, 8).value = this.getMessageTypeName(msg.localType) worksheet.getCell(currentRow, 8).value = this.getMessageTypeName(msg.localType, msg.content)
} else { } else {
worksheet.getCell(currentRow, 3).value = senderNickname worksheet.getCell(currentRow, 3).value = senderNickname
worksheet.getCell(currentRow, 4).value = senderWxid worksheet.getCell(currentRow, 4).value = senderWxid
worksheet.getCell(currentRow, 5).value = senderRemark worksheet.getCell(currentRow, 5).value = senderRemark
worksheet.getCell(currentRow, 6).value = senderRole worksheet.getCell(currentRow, 6).value = senderRole
worksheet.getCell(currentRow, 7).value = this.getMessageTypeName(msg.localType) worksheet.getCell(currentRow, 7).value = this.getMessageTypeName(msg.localType, msg.content)
} }
contentCell.value = enrichedContentValue contentCell.value = enrichedContentValue
if (!quotedReplyDisplay) { if (!quotedReplyDisplay) {
@@ -8338,7 +8378,7 @@ class ExportService {
i + 1, i + 1,
this.formatTimestamp(msg.createTime), this.formatTimestamp(msg.createTime),
senderRole, senderRole,
this.getMessageTypeName(msg.localType), this.getMessageTypeName(msg.localType, msg.content),
enrichedContentValue enrichedContentValue
] ]
: includeGroupNicknameColumn : includeGroupNicknameColumn
@@ -8350,7 +8390,7 @@ class ExportService {
senderRemark, senderRemark,
senderGroupNickname, senderGroupNickname,
senderRole, senderRole,
this.getMessageTypeName(msg.localType), this.getMessageTypeName(msg.localType, msg.content),
enrichedContentValue enrichedContentValue
] ]
: [ : [
@@ -8360,7 +8400,7 @@ class ExportService {
senderWxid, senderWxid,
senderRemark, senderRemark,
senderRole, senderRole,
this.getMessageTypeName(msg.localType), this.getMessageTypeName(msg.localType, msg.content),
enrichedContentValue enrichedContentValue
]) ])
if (!quotedReplyDisplay) { if (!quotedReplyDisplay) {
@@ -9635,7 +9675,7 @@ class ExportService {
const avatarHtml = getAvatarHtml(isSenderMe ? cleanedMyWxid : msg.senderUsername, resolvedSenderName) const avatarHtml = getAvatarHtml(isSenderMe ? cleanedMyWxid : msg.senderUsername, resolvedSenderName)
const timeText = this.formatTimestamp(msg.createTime) const timeText = this.formatTimestamp(msg.createTime)
const typeName = this.getMessageTypeName(msg.localType) const typeName = this.getMessageTypeName(msg.localType, msg.content)
const quotedReplyDisplay = await this.resolveQuotedReplyDisplayWithNames({ const quotedReplyDisplay = await this.resolveQuotedReplyDisplayWithNames({
content: msg.content, content: msg.content,
isGroup, isGroup,