This commit is contained in:
cc
2026-05-06 00:05:11 +08:00
parent 3f908a4dd3
commit abde85a900
9 changed files with 799 additions and 642 deletions

View File

@@ -2530,7 +2530,7 @@ class ChatService {
const rawRows = result.messages as Record<string, any>[]
const hasMore = rawRows.length > pageLimit
const selectedRows = hasMore ? rawRows.slice(0, pageLimit) : rawRows
const mapped = this.mapRowsToMessages(selectedRows)
const mapped = this.mapRowsToMessages(selectedRows, sessionId)
const visible = mapped.filter((msg) => this.isMessageVisibleForSession(sessionId, msg))
const outputMessages = (visible.length === 0 && mapped.length > 0)
? mapped
@@ -2541,6 +2541,7 @@ class ChatService {
const normalized = this.normalizeMessageOrder(outputMessages)
if (normalized.length > 0) {
await this.repairEmojiMessages(normalized)
await this.resolveQuotedMessages(normalized, sessionId)
}
return {
@@ -2597,7 +2598,7 @@ class ChatService {
}
// 转换为 Message 对象
const messages = this.mapRowsToMessages(res.messages as Record<string, any>[])
const messages = this.mapRowsToMessages(res.messages as Record<string, any>[], sessionId)
const normalized = this.normalizeMessageOrder(messages)
// 并发检查并修复缺失 CDN URL 的表情包
@@ -2834,7 +2835,7 @@ class ChatService {
const rowsToProcess = queuedRows
queuedRows = []
const mappedMessages = this.mapRowsToMessages(rowsToProcess)
const mappedMessages = this.mapRowsToMessages(rowsToProcess, sessionId)
for (let index = 0; index < mappedMessages.length; index += 1) {
const msg = mappedMessages[index]
rawRowsConsumed += 1
@@ -4825,8 +4826,8 @@ class ChatService {
/**
* HTTP API 复用消息解析逻辑,确保和应用内展示一致。
*/
mapRowsToMessagesForApi(rows: Record<string, any>[]): Message[] {
return this.mapRowsToMessages(rows)
mapRowsToMessagesForApi(rows: Record<string, any>[], sessionId: string): Message[] {
return this.mapRowsToMessages(rows, sessionId)
}
mapRowsToMessagesLiteForApi(rows: Record<string, any>[]): Message[] {
@@ -4880,7 +4881,7 @@ class ChatService {
return messages
}
private mapRowsToMessages(rows: Record<string, any>[]): Message[] {
private mapRowsToMessages(rows: Record<string, any>[], sessionId: string): Message[] {
const myWxid = this.configService.get('myWxid')
const messages: Message[] = []
@@ -5000,11 +5001,23 @@ class ChatService {
encrypVer = imageInfo.encrypVer
cdnThumbUrl = imageInfo.cdnThumbUrl
imageDatName = this.parseImageDatNameFromRow(row)
// 解析图片消息中的引用信息
const quoteInfo = this.parseMediaQuoteMessage(content, sessionId)
if (quoteInfo.content) quotedContent = quoteInfo.content
if (quoteInfo.sender) quotedSender = quoteInfo.sender
} else if (localType === 43) {
// 视频消息:优先从 packed_info_data 提取真实文件名32位十六进制再回退 XML
videoMd5 = this.parseVideoFileNameFromRow(row, content)
// 解析视频消息中的引用信息
const quoteInfo = this.parseMediaQuoteMessage(content, sessionId)
if (quoteInfo.content) quotedContent = quoteInfo.content
if (quoteInfo.sender) quotedSender = quoteInfo.sender
} else if (localType === 34 && content) {
voiceDurationSeconds = this.parseVoiceDurationSeconds(content)
// 解析语音消息中的引用信息
const quoteInfo = this.parseMediaQuoteMessage(content, sessionId)
if (quoteInfo.content) quotedContent = quoteInfo.content
if (quoteInfo.sender) quotedSender = quoteInfo.sender
} else if (localType === 42 && content) {
// 名片消息
const cardInfo = this.parseCardInfo(content)
@@ -5760,6 +5773,116 @@ class ChatService {
}
}
/**
* 解析媒体消息(图片/视频/语音)中的引用信息
* 这些消息的引用信息在 <extcommoninfo><refermsg> 中
*/
private parseMediaQuoteMessage(content: string, sessionId: string): { content?: string; sender?: string } {
try {
const normalizedContent = this.decodeHtmlEntities(content || '')
const referMsgStart = normalizedContent.indexOf('<refermsg>')
const referMsgEnd = normalizedContent.indexOf('</refermsg>')
if (referMsgStart === -1 || referMsgEnd === -1) {
return {}
}
const referMsgXml = normalizedContent.substring(referMsgStart, referMsgEnd + 11)
const svrid = this.extractXmlValue(referMsgXml, 'svrid')
console.log('[DEBUG] parseMediaQuoteMessage - svrid:', svrid)
if (!svrid) {
return {}
}
// 简化方案:返回 svrid 标记
console.log('[DEBUG] parseMediaQuoteMessage - 返回标记:', `__SVRID__${svrid}__`)
return { content: `__SVRID__${svrid}__` }
} catch {
return {}
}
}
async resolveQuotedMessages(messages: Message[], sessionId: string): Promise<void> {
console.log('[DEBUG] resolveQuotedMessages - 开始解析,消息数量:', messages.length)
const svridsToResolve: Array<{ msg: Message; svrid: string }> = []
for (const msg of messages) {
if (msg.quotedContent && msg.quotedContent.startsWith('__SVRID__')) {
const match = msg.quotedContent.match(/__SVRID__(.+?)__/)
if (match) {
console.log('[DEBUG] resolveQuotedMessages - 找到需要解析的svrid:', match[1])
svridsToResolve.push({ msg, svrid: match[1] })
}
}
}
console.log('[DEBUG] resolveQuotedMessages - 需要解析的数量:', svridsToResolve.length)
if (svridsToResolve.length === 0) return
const results = await Promise.allSettled(
svridsToResolve.map(({ svrid }) => {
console.log('[DEBUG] resolveQuotedMessages - 查询svrid:', svrid, 'sessionId:', sessionId)
return wcdbService.getMessageByServerId(sessionId, svrid)
})
)
console.log('[DEBUG] resolveQuotedMessages - 查询结果数量:', results.length)
for (let i = 0; i < results.length; i++) {
const result = results[i]
const { msg, svrid } = svridsToResolve[i]
console.log('[DEBUG] resolveQuotedMessages - 处理结果', i, ':', {
status: result.status,
success: result.status === 'fulfilled' ? result.value.success : false,
hasRow: result.status === 'fulfilled' && result.value.row ? true : false,
error: result.status === 'fulfilled' ? result.value.error : undefined,
svrid
})
if (result.status === 'fulfilled' && result.value.success && result.value.row) {
const localType = parseInt(result.value.row.local_type || '0', 10)
const rawMessageContent = result.value.row.message_content
const rawCompressContent = result.value.row.compress_content
console.log('[DEBUG] resolveQuotedMessages - 原始数据:', {
hasMessageContent: !!rawMessageContent,
hasCompressContent: !!rawCompressContent,
messageContentType: typeof rawMessageContent,
messageContentLength: rawMessageContent ? rawMessageContent.length : 0
})
const content = this.decodeMessageContent(rawMessageContent, rawCompressContent)
console.log('[DEBUG] resolveQuotedMessages - 解码后:', { localType, contentLength: content.length, contentPreview: content.substring(0, 50) })
if (localType === 1) {
msg.quotedContent = this.sanitizeQuotedContent(content)
} else if (localType === 3) {
msg.quotedContent = '[图片]'
} else if (localType === 34) {
msg.quotedContent = '[语音]'
} else if (localType === 43) {
msg.quotedContent = '[视频]'
} else if (localType === 47) {
msg.quotedContent = '[动画表情]'
} else if (localType === 49) {
msg.quotedContent = '[链接]'
} else {
msg.quotedContent = '[消息]'
}
console.log('[DEBUG] resolveQuotedMessages - 更新后的quotedContent:', msg.quotedContent)
} else {
msg.quotedContent = '[引用消息]'
console.log('[DEBUG] resolveQuotedMessages - 查询失败,使用占位符')
}
}
console.log('[DEBUG] resolveQuotedMessages - 完成')
}
private extractPreferredQuotedText(referMsgXml: string): string {
if (!referMsgXml) return ''
@@ -8792,7 +8915,7 @@ class ChatService {
return { success: false, error: result.error || '查询语音消息失败' }
}
let allVoiceMessages: Message[] = this.mapRowsToMessages(result.rows as Record<string, any>[])
let allVoiceMessages: Message[] = this.mapRowsToMessages(result.rows as Record<string, any>[], sessionId)
// 按 createTime 降序排序
allVoiceMessages.sort((a, b) => b.createTime - a.createTime)
@@ -8835,7 +8958,7 @@ class ChatService {
return { success: false, error: result.error || '查询图片消息失败' }
}
const mapped = this.mapRowsToMessages(result.rows as Record<string, any>[])
const mapped = this.mapRowsToMessages(result.rows as Record<string, any>[], sessionId)
let allImages: Array<{ imageMd5?: string; imageDatName?: string; createTime?: number }> = mapped
.filter(msg => msg.localType === 3)
.map(msg => ({
@@ -8960,7 +9083,7 @@ class ChatService {
if (!result.success || !Array.isArray(result.rows) || result.rows.length === 0) continue
if (result.rows.length >= perTypeFetch) maybeHasMore = true
const mapped = this.mapRowsToMessages(result.rows as Record<string, any>[])
const mapped = this.mapRowsToMessages(result.rows as Record<string, any>[], sessionId)
for (const message of mapped) {
const resourceType = this.resolveResourceType(message)
if (!resourceType || !typeSet.has(resourceType)) continue

View File

@@ -3536,7 +3536,49 @@ class ExportService {
return result
}
private parseQuoteMessage(content: string): { content?: string; sender?: string; type?: string } {
private async resolveQuotedMessagesForExport(messages: any[], sessionId: string): Promise<void> {
const svridsToResolve: Array<{ msg: any; svrid: string }> = []
for (const msg of messages) {
if (msg.replyToMessageId && msg.quotedContent === '[消息]') {
svridsToResolve.push({ msg, svrid: msg.replyToMessageId })
}
}
if (svridsToResolve.length === 0) return
const results = await Promise.allSettled(
svridsToResolve.map(({ svrid }) => wcdbService.getMessageByServerId(sessionId, svrid))
)
for (let i = 0; i < results.length; i++) {
const result = results[i]
const { msg } = svridsToResolve[i]
if (result.status === 'fulfilled' && result.value.success && result.value.row) {
const localType = parseInt(result.value.row.local_type || '0', 10)
const rawMessageContent = result.value.row.message_content
const rawCompressContent = result.value.row.compress_content
const content = chatService['decodeMessageContent'](rawMessageContent, rawCompressContent)
if (localType === 1) {
msg.quotedContent = chatService['sanitizeQuotedContent'](content)
} else if (localType === 3) {
msg.quotedContent = '[图片]'
} else if (localType === 34) {
msg.quotedContent = '[语音]'
} else if (localType === 43) {
msg.quotedContent = '[视频]'
} else if (localType === 47) {
msg.quotedContent = '[动画表情]'
} else if (localType === 49) {
msg.quotedContent = '[链接]'
}
}
}
}
private parseQuoteMessage(content: string): { content?: string; sender?: string; type?: string; svrid?: string } {
try {
const normalized = this.normalizeAppMessageContent(content || '')
const referMsgStart = normalized.indexOf('<refermsg>')
@@ -3553,6 +3595,7 @@ class ExportService {
const referContent = this.extractXmlValue(referMsgXml, 'content')
const referType = this.extractXmlValue(referMsgXml, 'type')
const svrid = this.extractXmlValue(referMsgXml, 'svrid')
let displayContent = referContent
switch (referType) {
@@ -3775,6 +3818,7 @@ class ExportService {
if (quoteInfo.content) meta.quotedContent = quoteInfo.content
if (quoteInfo.sender) meta.quotedSender = quoteInfo.sender
if (quoteInfo.type) meta.quotedType = quoteInfo.type
if (quoteInfo.svrid) meta.quotedSvrid = quoteInfo.svrid
}
if (appMsgKind === 'link') {
@@ -6935,6 +6979,9 @@ class ExportService {
await this.hydrateEmojiCaptionsForMessages(sessionId, collected.rows, control)
// 解析引用消息
await this.resolveQuotedMessagesForExport(collected.rows, sessionId)
const voiceMessages = options.exportVoiceAsText
? collected.rows.filter(msg => msg.localType === 34)
: []
@@ -7139,7 +7186,8 @@ class ExportService {
rawMyWxid,
myDisplayName: myInfo.displayName || cleanedMyWxid
})
if (quotedReplyDisplay) {
// 对于媒体消息,不要让引用信息覆盖媒体路径
if (quotedReplyDisplay && !mediaItem) {
content = this.buildQuotedReplyText(quotedReplyDisplay)
}
@@ -7674,6 +7722,9 @@ class ExportService {
await this.hydrateEmojiCaptionsForMessages(sessionId, collected.rows, control)
// 解析引用消息
await this.resolveQuotedMessagesForExport(collected.rows, sessionId)
const voiceMessages = options.exportVoiceAsText
? collected.rows.filter(msg => msg.localType === 34)
: []
@@ -8552,6 +8603,9 @@ class ExportService {
await this.hydrateEmojiCaptionsForMessages(sessionId, collected.rows, control)
// 解析引用消息
await this.resolveQuotedMessagesForExport(collected.rows, sessionId)
const voiceMessages = options.exportVoiceAsText
? collected.rows.filter(msg => msg.localType === 34)
: []

View File

@@ -38,6 +38,7 @@ export class WcdbCore {
private wcdbMarkAllSessionsRead: any = null
private wcdbGetMessages: any = null
private wcdbGetMessageCount: any = null
private wcdbGetMessageByServerId: any = null
private wcdbGetDisplayNames: any = null
private wcdbGetAvatarUrls: any = null
private wcdbGetGroupMemberCount: any = null
@@ -824,6 +825,9 @@ export class WcdbCore {
// wcdb_status wcdb_get_message_count(wcdb_handle handle, const char* username, int32_t* out_count)
this.wcdbGetMessageCount = this.lib.func('int32 wcdb_get_message_count(int64 handle, const char* username, _Out_ int32* outCount)')
// wcdb_status wcdb_get_message_by_svrid(wcdb_handle handle, const char* session_id, const char* svrid, char** out_json)
this.wcdbGetMessageByServerId = this.lib.func('int32 wcdb_get_message_by_svrid(int64 handle, const char* sessionId, const char* svrid, _Out_ void** outJson)')
// wcdb_status wcdb_get_display_names(wcdb_handle handle, const char* usernames_json, char** out_json)
this.wcdbGetDisplayNames = this.lib.func('int32 wcdb_get_display_names(int64 handle, const char* usernamesJson, _Out_ void** outJson)')
@@ -1807,6 +1811,30 @@ export class WcdbCore {
}
}
async getMessageByServerId(sessionId: string, svrid: string): Promise<{ success: boolean; row?: any; error?: string }> {
if (!this.ensureReady()) {
return { success: false, error: 'WCDB 未连接' }
}
try {
const outPtr = [null as any]
const result = this.wcdbGetMessageByServerId(this.handle, sessionId, svrid, outPtr)
if (result !== 0) {
return { success: false, error: `查询消息失败: ${result}` }
}
const jsonStr = this.decodeJsonPtr(outPtr[0])
if (!jsonStr) {
return { success: true, row: null }
}
const parsed = JSON.parse(jsonStr)
if (!parsed || Object.keys(parsed).length === 0) {
return { success: true, row: null }
}
return { success: true, row: parsed }
} catch (e) {
return { success: false, error: String(e) }
}
}
async getMessageCounts(sessionIds: string[]): Promise<{ success: boolean; counts?: Record<string, number>; error?: string }> {
if (!this.ensureReady()) {
return { success: false, error: 'WCDB 未连接' }

View File

@@ -229,6 +229,13 @@ export class WcdbService {
return this.callWorker('getMessageCount', { sessionId })
}
/**
* 根据 server_id 查询单条消息
*/
async getMessageByServerId(sessionId: string, svrid: string): Promise<{ success: boolean; row?: any; error?: string }> {
return this.callWorker('getMessageByServerId', { sessionId, svrid })
}
async getMessageCounts(sessionIds: string[]): Promise<{ success: boolean; counts?: Record<string, number>; error?: string }> {
return this.callWorker('getMessageCounts', { sessionIds })
}

View File

@@ -62,6 +62,9 @@ if (parentPort) {
case 'getMessageCount':
result = await core.getMessageCount(payload.sessionId)
break
case 'getMessageByServerId':
result = await core.getMessageByServerId(payload.sessionId, payload.svrid)
break
case 'getMessageCounts':
result = await core.getMessageCounts(payload.sessionIds)
break