From f7610a3570e03daa56603c5e088dddeae51f0978 Mon Sep 17 00:00:00 2001
From: xuncha <1658671838@qq.com>
Date: Fri, 13 Mar 2026 19:38:17 +0800
Subject: [PATCH 1/8] =?UTF-8?q?=E5=9B=BE=E7=89=87=E8=A7=A3=E5=AF=86?=
=?UTF-8?q?=E5=A4=B1=E8=B4=A5=E7=9A=84=E6=97=B6=E5=80=99=20=E5=8F=AF?=
=?UTF-8?q?=E4=BB=A5=E5=AF=BC=E5=87=BA=E7=BC=A9=E7=95=A5=E5=9B=BE=20html?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
electron/services/exportService.ts | 20 +++++++++++++++-----
1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts
index 20864a5..d4b36ea 100644
--- a/electron/services/exportService.ts
+++ b/electron/services/exportService.ts
@@ -2157,12 +2157,22 @@ class ExportService {
imageMd5,
imageDatName
})
- if (!thumbResult.success || !thumbResult.localPath) {
- console.log(`[Export] 缩略图也获取失败 (localId=${msg.localId}): error=${thumbResult.error || '未知'} → 将显示 [图片] 占位符`)
- return null
+ if (thumbResult.success && thumbResult.localPath) {
+ console.log(`[Export] 使用缩略图替代 (localId=${msg.localId}): ${thumbResult.localPath}`)
+ result.localPath = thumbResult.localPath
+ } else {
+ console.log(`[Export] 缩略图也获取失败 (localId=${msg.localId}): error=${thumbResult.error || '未知'}`)
+ // 最后尝试:直接从 imageStore 获取缓存的缩略图 data URL
+ const { imageStore } = await import('../main')
+ const cachedThumb = imageStore?.getCachedImage(sessionId, imageMd5, imageDatName)
+ if (cachedThumb) {
+ console.log(`[Export] 从 imageStore 获取到缓存缩略图 (localId=${msg.localId})`)
+ result.localPath = cachedThumb
+ } else {
+ console.log(`[Export] 所有方式均失败 → 将显示 [图片] 占位符`)
+ return null
+ }
}
- console.log(`[Export] 使用缩略图替代 (localId=${msg.localId}): ${thumbResult.localPath}`)
- result.localPath = thumbResult.localPath
}
// 为每条消息生成稳定且唯一的文件名前缀,避免跨日期/消息发生同名覆盖
From 1c5cacf1ce188c1cc7e0d772d40cd742fa85c78b Mon Sep 17 00:00:00 2001
From: xuncha <1658671838@qq.com>
Date: Fri, 13 Mar 2026 20:08:42 +0800
Subject: [PATCH 2/8] =?UTF-8?q?=E7=BB=99=E5=88=AB=E7=9A=84=E6=A0=BC?=
=?UTF-8?q?=E5=BC=8F=E5=90=8C=E6=AD=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
electron/services/exportService.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts
index d4b36ea..e789187 100644
--- a/electron/services/exportService.ts
+++ b/electron/services/exportService.ts
@@ -3467,9 +3467,13 @@ class ExportService {
// 确定消息内容
let content: string | null
+ const mediaKey = `${msg.localType}_${msg.localId}`
+ const mediaItem = mediaCache.get(mediaKey)
if (msg.localType === 34 && options.exportVoiceAsText) {
// 使用预先转写的文字
content = voiceTranscriptMap.get(msg.localId) || '[语音消息 - 转文字失败]'
+ } else if (mediaItem && msg.localType === 3) {
+ content = mediaItem.relativePath
} else {
content = this.parseMessageContent(
msg.content,
From d7f7139f36cc875c191c332b31cf15a664bbc24e Mon Sep 17 00:00:00 2001
From: xuncha <1658671838@qq.com>
Date: Fri, 13 Mar 2026 20:21:56 +0800
Subject: [PATCH 3/8] =?UTF-8?q?=E5=B0=9D=E8=AF=95=E4=BF=AE=E5=A4=8D=20http?=
=?UTF-8?q?s://github.com/hicccc77/WeFlow/issues/378=20=E4=B8=AD=E7=9A=84?=
=?UTF-8?q?=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
electron/services/chatService.ts | 21 +++++-
electron/services/httpService.ts | 111 +++++++++++++++++++++++++------
2 files changed, 107 insertions(+), 25 deletions(-)
diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts
index 470933a..6f104fd 100644
--- a/electron/services/chatService.ts
+++ b/electron/services/chatService.ts
@@ -2974,7 +2974,9 @@ class ChatService {
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 senderUsername = this.getRowField(row, ['sender_username', 'senderUsername', 'sender', 'WCDB_CT_sender_username']) || null
+ const senderUsername = this.getRowField(row, ['sender_username', 'senderUsername', 'sender', 'WCDB_CT_sender_username'])
+ || this.extractSenderUsernameFromContent(content)
+ || null
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)) {
@@ -4385,7 +4387,18 @@ class ChatService {
}
private stripSenderPrefix(content: string): string {
- return content.replace(/^[\s]*([a-zA-Z0-9_-]+):(?!\/\/)\s*/, '')
+ return content.replace(/^[\s]*([a-zA-Z0-9_@-]+):(?!\/\/)(?:\s*(?:\r?\n|
)\s*|\s*)/i, '')
+ }
+
+ private extractSenderUsernameFromContent(content: string): string | null {
+ if (!content) return null
+
+ const normalized = this.cleanUtf16(this.decodeHtmlEntities(String(content)))
+ const match = /^\s*([a-zA-Z0-9_@-]{4,}):(?!\/\/)\s*(?:\r?\n|
)/i.exec(normalized)
+ if (!match?.[1]) return null
+
+ const candidate = match[1].trim()
+ return candidate || null
}
private decodeHtmlEntities(content: string): string {
@@ -6594,7 +6607,9 @@ class ChatService {
createTime: this.getRowInt(row, ['create_time', 'createTime', 'createtime', 'msg_create_time', 'msgCreateTime', 'msg_time', 'msgTime', 'time', 'WCDB_CT_create_time'], 0),
sortSeq: this.getRowInt(row, ['sort_seq', 'sortSeq', 'seq', 'sequence', 'WCDB_CT_sort_seq'], this.getRowInt(row, ['create_time', 'createTime', 'createtime', 'msg_create_time', 'msgCreateTime', 'msg_time', 'msgTime', 'time', 'WCDB_CT_create_time'], 0)),
isSend: this.getRowInt(row, ['computed_is_send', 'computedIsSend', 'is_send', 'isSend', 'WCDB_CT_is_send'], 0),
- senderUsername: this.getRowField(row, ['sender_username', 'senderUsername', 'sender', 'WCDB_CT_sender_username']) || null,
+ senderUsername: this.getRowField(row, ['sender_username', 'senderUsername', 'sender', 'WCDB_CT_sender_username'])
+ || this.extractSenderUsernameFromContent(rawContent)
+ || null,
rawContent: rawContent,
content: rawContent, // 添加原始内容供视频MD5解析使用
parsedContent: this.parseMessageContent(rawContent, this.getRowInt(row, ['local_type', 'localType', 'type', 'msg_type', 'msgType', 'WCDB_CT_local_type'], 0))
diff --git a/electron/services/httpService.ts b/electron/services/httpService.ts
index 86ea6c9..bfb8ce1 100644
--- a/electron/services/httpService.ts
+++ b/electron/services/httpService.ts
@@ -340,6 +340,7 @@ class HttpService {
const trimmedRows = allRows.slice(0, limit)
const finalHasMore = hasMore || allRows.length > limit
const messages = chatService.mapRowsToMessagesForApi(trimmedRows)
+ await this.backfillMissingSenderUsernames(talker, messages)
return { success: true, messages, hasMore: finalHasMore }
} finally {
await wcdbService.closeMessageCursor(cursor)
@@ -359,6 +360,41 @@ class HttpService {
return Math.min(Math.max(parsed, min), max)
}
+ private async backfillMissingSenderUsernames(talker: string, messages: Message[]): Promise {
+ if (!talker.endsWith('@chatroom')) return
+
+ const targets = messages.filter((msg) => !String(msg.senderUsername || '').trim())
+ if (targets.length === 0) return
+
+ const myWxid = (this.configService.get('myWxid') || '').trim()
+ for (const msg of targets) {
+ const localId = Number(msg.localId || 0)
+ if (Number.isFinite(localId) && localId > 0) {
+ try {
+ const detail = await wcdbService.getMessageById(talker, localId)
+ if (detail.success && detail.message) {
+ const hydrated = chatService.mapRowsToMessagesForApi([detail.message])[0]
+ if (hydrated?.senderUsername) {
+ msg.senderUsername = hydrated.senderUsername
+ }
+ if ((msg.isSend === null || msg.isSend === undefined) && hydrated?.isSend !== undefined) {
+ msg.isSend = hydrated.isSend
+ }
+ if (!msg.rawContent && hydrated?.rawContent) {
+ msg.rawContent = hydrated.rawContent
+ }
+ }
+ } catch (error) {
+ console.warn('[HttpService] backfill sender failed:', error)
+ }
+ }
+
+ if (!msg.senderUsername && msg.isSend === 1 && myWxid) {
+ msg.senderUsername = myWxid
+ }
+ }
+ }
+
private parseBooleanParam(url: URL, keys: string[], defaultValue: boolean = false): boolean {
for (const key of keys) {
const raw = url.searchParams.get(key)
@@ -778,6 +814,49 @@ class HttpService {
return {}
}
+ private lookupGroupNickname(groupNicknamesMap: Map, sender: string): string {
+ if (!sender) return ''
+ return groupNicknamesMap.get(sender) || groupNicknamesMap.get(sender.toLowerCase()) || ''
+ }
+
+ private resolveChatLabSenderInfo(
+ msg: Message,
+ talkerId: string,
+ talkerName: string,
+ myWxid: string,
+ isGroup: boolean,
+ senderNames: Record,
+ groupNicknamesMap: Map
+ ): { sender: string; accountName: string; groupNickname?: string } {
+ let sender = String(msg.senderUsername || '').trim()
+ let usedUnknownPlaceholder = false
+ const sameAsMe = sender && myWxid && sender.toLowerCase() === myWxid.toLowerCase()
+ const isSelf = msg.isSend === 1 || sameAsMe
+
+ if (!sender && isSelf && myWxid) {
+ sender = myWxid
+ }
+
+ if (!sender) {
+ if (msg.localType === 10000 || msg.localType === 266287972401) {
+ sender = talkerId
+ } else {
+ sender = `unknown_sender_${msg.localId || msg.createTime || 0}`
+ usedUnknownPlaceholder = true
+ }
+ }
+
+ const groupNickname = isGroup ? this.lookupGroupNickname(groupNicknamesMap, sender) : ''
+ const displayName = senderNames[sender] || groupNickname || (usedUnknownPlaceholder ? '' : sender)
+ const accountName = isSelf ? '我' : (displayName || '未知发送者')
+
+ return {
+ sender,
+ accountName,
+ groupNickname: groupNickname || undefined
+ }
+ }
+
/**
* 转换为 ChatLab 格式
*/
@@ -817,36 +896,24 @@ class HttpService {
// 构建成员列表
const memberMap = new Map()
for (const msg of messages) {
- const sender = msg.senderUsername || ''
- if (sender && !memberMap.has(sender)) {
- const displayName = senderNames[sender] || sender
- const isSelf = sender === myWxid || sender.toLowerCase() === myWxid.toLowerCase()
- // 获取群昵称(尝试多种方式)
- const groupNickname = isGroup
- ? (groupNicknamesMap.get(sender) || groupNicknamesMap.get(sender.toLowerCase()) || '')
- : ''
- memberMap.set(sender, {
- platformId: sender,
- accountName: isSelf ? '我' : displayName,
- groupNickname: groupNickname || undefined
+ const senderInfo = this.resolveChatLabSenderInfo(msg, talkerId, talkerName, myWxid, isGroup, senderNames, groupNicknamesMap)
+ if (!memberMap.has(senderInfo.sender)) {
+ memberMap.set(senderInfo.sender, {
+ platformId: senderInfo.sender,
+ accountName: senderInfo.accountName,
+ groupNickname: senderInfo.groupNickname
})
}
}
// 转换消息
const chatLabMessages: ChatLabMessage[] = messages.map(msg => {
- const sender = msg.senderUsername || ''
- const isSelf = msg.isSend === 1 || sender === myWxid
- const accountName = isSelf ? '我' : (senderNames[sender] || sender)
- // 获取该发送者的群昵称
- const groupNickname = isGroup
- ? (groupNicknamesMap.get(sender) || groupNicknamesMap.get(sender.toLowerCase()) || '')
- : ''
+ const senderInfo = this.resolveChatLabSenderInfo(msg, talkerId, talkerName, myWxid, isGroup, senderNames, groupNicknamesMap)
return {
- sender,
- accountName,
- groupNickname: groupNickname || undefined,
+ sender: senderInfo.sender,
+ accountName: senderInfo.accountName,
+ groupNickname: senderInfo.groupNickname,
timestamp: msg.createTime,
type: this.mapMessageType(msg.localType, msg),
content: this.getMessageContent(msg),
From 4c551a8c91cb48df7d3ba967ab44416bcd061f9d Mon Sep 17 00:00:00 2001
From: xuncha <1658671838@qq.com>
Date: Fri, 13 Mar 2026 20:40:16 +0800
Subject: [PATCH 4/8] =?UTF-8?q?=E5=9B=BE=E7=89=87=E8=A7=A3=E5=AF=86?=
=?UTF-8?q?=E9=80=BB=E8=BE=91=E4=BC=98=E5=8C=96https://github.com/hicccc77?=
=?UTF-8?q?/WeFlow/issues/408#issuecomment-4053026902?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
electron/imageSearchWorker.ts | 74 ++++---
electron/services/imageDecryptService.ts | 246 ++++++++++++++---------
2 files changed, 187 insertions(+), 133 deletions(-)
diff --git a/electron/imageSearchWorker.ts b/electron/imageSearchWorker.ts
index 56826a2..429a00f 100644
--- a/electron/imageSearchWorker.ts
+++ b/electron/imageSearchWorker.ts
@@ -10,7 +10,7 @@ type WorkerPayload = {
thumbOnly: boolean
}
-type Candidate = { score: number; path: string; isThumb: boolean; hasX: boolean }
+type Candidate = { score: number; path: string; isThumb: boolean }
const payload = workerData as WorkerPayload
@@ -18,16 +18,26 @@ function looksLikeMd5(value: string): boolean {
return /^[a-fA-F0-9]{16,32}$/.test(value)
}
+function stripDatVariantSuffix(base: string): string {
+ const lower = base.toLowerCase()
+ const suffixes = ['_thumb', '.thumb', '_hd', '.hd', '_h', '.h', '_t', '.t', '_c', '.c']
+ for (const suffix of suffixes) {
+ if (lower.endsWith(suffix)) {
+ return lower.slice(0, -suffix.length)
+ }
+ }
+ if (/[._][a-z]$/.test(lower)) {
+ return lower.slice(0, -2)
+ }
+ return lower
+}
+
function hasXVariant(baseLower: string): boolean {
- return /[._][a-z]$/.test(baseLower)
+ return stripDatVariantSuffix(baseLower) !== baseLower
}
function hasImageVariantSuffix(baseLower: string): boolean {
- return /[._][a-z]$/.test(baseLower)
-}
-
-function isLikelyImageDatBase(baseLower: string): boolean {
- return hasImageVariantSuffix(baseLower) || looksLikeMd5(baseLower)
+ return stripDatVariantSuffix(baseLower) !== baseLower
}
function normalizeDatBase(name: string): string {
@@ -35,10 +45,17 @@ function normalizeDatBase(name: string): string {
if (base.endsWith('.dat') || base.endsWith('.jpg')) {
base = base.slice(0, -4)
}
- while (/[._][a-z]$/.test(base)) {
- base = base.slice(0, -2)
+ while (true) {
+ const stripped = stripDatVariantSuffix(base)
+ if (stripped === base) {
+ return base
+ }
+ base = stripped
}
- return base
+}
+
+function isLikelyImageDatBase(baseLower: string): boolean {
+ return hasImageVariantSuffix(baseLower) || looksLikeMd5(normalizeDatBase(baseLower))
}
function matchesDatName(fileName: string, datName: string): boolean {
@@ -47,25 +64,23 @@ function matchesDatName(fileName: string, datName: string): boolean {
const normalizedBase = normalizeDatBase(base)
const normalizedTarget = normalizeDatBase(datName.toLowerCase())
if (normalizedBase === normalizedTarget) return true
- const pattern = new RegExp(`^${datName}(?:[._][a-z])?\\.dat$`)
- if (pattern.test(lower)) return true
- return lower.endsWith('.dat') && lower.includes(datName)
+ return lower.endsWith('.dat') && lower.includes(normalizedTarget)
}
function scoreDatName(fileName: string): number {
- if (fileName.includes('.t.dat') || fileName.includes('_t.dat')) return 1
- if (fileName.includes('.c.dat') || fileName.includes('_c.dat')) return 1
- return 2
+ const lower = fileName.toLowerCase()
+ const baseLower = lower.endsWith('.dat') ? lower.slice(0, -4) : lower
+ if (baseLower.endsWith('_h') || baseLower.endsWith('.h')) return 600
+ if (!hasXVariant(baseLower)) return 500
+ if (baseLower.endsWith('_hd') || baseLower.endsWith('.hd')) return 450
+ if (baseLower.endsWith('_c') || baseLower.endsWith('.c')) return 400
+ if (isThumbnailDat(lower)) return 100
+ return 350
}
function isThumbnailDat(fileName: string): boolean {
- return fileName.includes('.t.dat') || fileName.includes('_t.dat')
-}
-
-function isHdDat(fileName: string): boolean {
const lower = fileName.toLowerCase()
- const base = lower.endsWith('.dat') ? lower.slice(0, -4) : lower
- return base.endsWith('_hd') || base.endsWith('_h')
+ return lower.includes('.t.dat') || lower.includes('_t.dat') || lower.includes('_thumb.dat')
}
function walkForDat(
@@ -105,20 +120,15 @@ function walkForDat(
if (!lower.endsWith('.dat')) continue
const baseLower = lower.slice(0, -4)
if (!isLikelyImageDatBase(baseLower)) continue
- if (!hasXVariant(baseLower)) continue
if (!matchesDatName(lower, datName)) continue
- // 排除高清图片格式 (_hd, _h)
- if (isHdDat(lower)) continue
matchedBases.add(baseLower)
const isThumb = isThumbnailDat(lower)
if (!allowThumbnail && isThumb) continue
if (thumbOnly && !isThumb) continue
- const score = scoreDatName(lower)
candidates.push({
- score,
+ score: scoreDatName(lower),
path: entryPath,
- isThumb,
- hasX: hasXVariant(baseLower)
+ isThumb
})
}
}
@@ -126,10 +136,8 @@ function walkForDat(
return { path: null, matchedBases: Array.from(matchedBases).slice(0, 20) }
}
- const withX = candidates.filter((item) => item.hasX)
- const basePool = withX.length ? withX : candidates
- const nonThumb = basePool.filter((item) => !item.isThumb)
- const finalPool = thumbOnly ? basePool : (nonThumb.length ? nonThumb : basePool)
+ const nonThumb = candidates.filter((item) => !item.isThumb)
+ const finalPool = thumbOnly ? candidates : (nonThumb.length ? nonThumb : candidates)
let best: { score: number; path: string } | null = null
for (const item of finalPool) {
diff --git a/electron/services/imageDecryptService.ts b/electron/services/imageDecryptService.ts
index 13dce67..a78b7ed 100644
--- a/electron/services/imageDecryptService.ts
+++ b/electron/services/imageDecryptService.ts
@@ -414,23 +414,33 @@ export class ImageDecryptService {
if (!skipResolvedCache) {
if (imageMd5) {
const cached = this.resolvedCache.get(imageMd5)
- if (cached && existsSync(cached)) return cached
+ if (cached && existsSync(cached)) {
+ const preferred = this.getPreferredDatVariantPath(cached, allowThumbnail)
+ this.cacheDatPath(accountDir, imageMd5, preferred)
+ if (imageDatName) this.cacheDatPath(accountDir, imageDatName, preferred)
+ return preferred
+ }
}
if (imageDatName) {
const cached = this.resolvedCache.get(imageDatName)
- if (cached && existsSync(cached)) return cached
+ if (cached && existsSync(cached)) {
+ const preferred = this.getPreferredDatVariantPath(cached, allowThumbnail)
+ this.cacheDatPath(accountDir, imageDatName, preferred)
+ if (imageMd5) this.cacheDatPath(accountDir, imageMd5, preferred)
+ return preferred
+ }
}
}
// 1. 通过 MD5 快速定位 (MsgAttach 目录)
if (imageMd5) {
- const res = await this.fastProbabilisticSearch(accountDir, imageMd5, allowThumbnail)
+ const res = await this.fastProbabilisticSearch(join(accountDir, 'msg', 'attach'), imageMd5, allowThumbnail)
if (res) return res
}
// 2. 如果 imageDatName 看起来像 MD5,也尝试快速定位
if (!imageMd5 && imageDatName && this.looksLikeMd5(imageDatName)) {
- const res = await this.fastProbabilisticSearch(accountDir, imageDatName, allowThumbnail)
+ const res = await this.fastProbabilisticSearch(join(accountDir, 'msg', 'attach'), imageDatName, allowThumbnail)
if (res) return res
}
@@ -439,16 +449,17 @@ export class ImageDecryptService {
this.logInfo('[ImageDecrypt] hardlink lookup (md5)', { imageMd5, sessionId })
const hardlinkPath = await this.resolveHardlinkPath(accountDir, imageMd5, sessionId)
if (hardlinkPath) {
- const isThumb = this.isThumbnailPath(hardlinkPath)
+ const preferredPath = this.getPreferredDatVariantPath(hardlinkPath, allowThumbnail)
+ const isThumb = this.isThumbnailPath(preferredPath)
if (allowThumbnail || !isThumb) {
- this.logInfo('[ImageDecrypt] hardlink hit', { imageMd5, path: hardlinkPath })
- this.cacheDatPath(accountDir, imageMd5, hardlinkPath)
- if (imageDatName) this.cacheDatPath(accountDir, imageDatName, hardlinkPath)
- return hardlinkPath
+ this.logInfo('[ImageDecrypt] hardlink hit', { imageMd5, path: preferredPath })
+ this.cacheDatPath(accountDir, imageMd5, preferredPath)
+ if (imageDatName) this.cacheDatPath(accountDir, imageDatName, preferredPath)
+ return preferredPath
}
// hardlink 找到的是缩略图,但要求高清图
// 尝试在同一目录下查找高清图变体(快速查找,不遍历)
- const hdPath = this.findHdVariantInSameDir(hardlinkPath)
+ const hdPath = this.findHdVariantInSameDir(preferredPath)
if (hdPath) {
this.cacheDatPath(accountDir, imageMd5, hdPath)
if (imageDatName) this.cacheDatPath(accountDir, imageDatName, hdPath)
@@ -462,16 +473,19 @@ export class ImageDecryptService {
this.logInfo('[ImageDecrypt] hardlink fallback (datName)', { imageDatName, sessionId })
const fallbackPath = await this.resolveHardlinkPath(accountDir, imageDatName, sessionId)
if (fallbackPath) {
- const isThumb = this.isThumbnailPath(fallbackPath)
+ const preferredPath = this.getPreferredDatVariantPath(fallbackPath, allowThumbnail)
+ const isThumb = this.isThumbnailPath(preferredPath)
if (allowThumbnail || !isThumb) {
- this.logInfo('[ImageDecrypt] hardlink hit (datName)', { imageMd5: imageDatName, path: fallbackPath })
- this.cacheDatPath(accountDir, imageDatName, fallbackPath)
- return fallbackPath
+ this.logInfo('[ImageDecrypt] hardlink hit (datName)', { imageMd5: imageDatName, path: preferredPath })
+ this.cacheDatPath(accountDir, imageDatName, preferredPath)
+ if (imageMd5) this.cacheDatPath(accountDir, imageMd5, preferredPath)
+ return preferredPath
}
// 找到缩略图但要求高清图,尝试同目录查找高清图变体
- const hdPath = this.findHdVariantInSameDir(fallbackPath)
+ const hdPath = this.findHdVariantInSameDir(preferredPath)
if (hdPath) {
this.cacheDatPath(accountDir, imageDatName, hdPath)
+ if (imageMd5) this.cacheDatPath(accountDir, imageMd5, hdPath)
return hdPath
}
return null
@@ -484,14 +498,15 @@ export class ImageDecryptService {
this.logInfo('[ImageDecrypt] hardlink lookup (datName)', { imageDatName, sessionId })
const hardlinkPath = await this.resolveHardlinkPath(accountDir, imageDatName, sessionId)
if (hardlinkPath) {
- const isThumb = this.isThumbnailPath(hardlinkPath)
+ const preferredPath = this.getPreferredDatVariantPath(hardlinkPath, allowThumbnail)
+ const isThumb = this.isThumbnailPath(preferredPath)
if (allowThumbnail || !isThumb) {
- this.logInfo('[ImageDecrypt] hardlink hit', { imageMd5: imageDatName, path: hardlinkPath })
- this.cacheDatPath(accountDir, imageDatName, hardlinkPath)
- return hardlinkPath
+ this.logInfo('[ImageDecrypt] hardlink hit', { imageMd5: imageDatName, path: preferredPath })
+ this.cacheDatPath(accountDir, imageDatName, preferredPath)
+ return preferredPath
}
// hardlink 找到的是缩略图,但要求高清图
- const hdPath = this.findHdVariantInSameDir(hardlinkPath)
+ const hdPath = this.findHdVariantInSameDir(preferredPath)
if (hdPath) {
this.cacheDatPath(accountDir, imageDatName, hdPath)
return hdPath
@@ -510,9 +525,10 @@ export class ImageDecryptService {
if (!skipResolvedCache) {
const cached = this.resolvedCache.get(imageDatName)
if (cached && existsSync(cached)) {
- if (allowThumbnail || !this.isThumbnailPath(cached)) return cached
+ const preferred = this.getPreferredDatVariantPath(cached, allowThumbnail)
+ if (allowThumbnail || !this.isThumbnailPath(preferred)) return preferred
// 缓存的是缩略图,尝试找高清图
- const hdPath = this.findHdVariantInSameDir(cached)
+ const hdPath = this.findHdVariantInSameDir(preferred)
if (hdPath) return hdPath
}
}
@@ -801,7 +817,8 @@ export class ImageDecryptService {
const key = `${accountDir}|${datName}`
const cached = this.resolvedCache.get(key)
if (cached && existsSync(cached)) {
- if (allowThumbnail || !this.isThumbnailPath(cached)) return cached
+ const preferred = this.getPreferredDatVariantPath(cached, allowThumbnail)
+ if (allowThumbnail || !this.isThumbnailPath(preferred)) return preferred
}
const root = join(accountDir, 'msg', 'attach')
@@ -810,7 +827,7 @@ export class ImageDecryptService {
// 优化1:快速概率性查找
// 包含:1. 基于文件名的前缀猜测 (旧版)
// 2. 基于日期的最近月份扫描 (新版无索引时)
- const fastHit = await this.fastProbabilisticSearch(root, datName)
+ const fastHit = await this.fastProbabilisticSearch(root, datName, allowThumbnail)
if (fastHit) {
this.resolvedCache.set(key, fastHit)
return fastHit
@@ -830,33 +847,28 @@ export class ImageDecryptService {
* 包含:1. 微信旧版结构 filename.substr(0, 2)/...
* 2. 微信新版结构 msg/attach/{hash}/{YYYY-MM}/Img/filename
*/
- private async fastProbabilisticSearch(root: string, datName: string, _allowThumbnail?: boolean): Promise {
+ private async fastProbabilisticSearch(root: string, datName: string, allowThumbnail = true): Promise {
const { promises: fs } = require('fs')
const { join } = require('path')
try {
// --- 策略 A: 旧版路径猜测 (msg/attach/xx/yy/...) ---
const lowerName = datName.toLowerCase()
- let baseName = lowerName
- if (baseName.endsWith('.dat')) {
- baseName = baseName.slice(0, -4)
- if (baseName.endsWith('_t') || baseName.endsWith('.t') || baseName.endsWith('_hd')) {
- baseName = baseName.slice(0, -3)
- } else if (baseName.endsWith('_thumb')) {
- baseName = baseName.slice(0, -6)
- }
- }
+ const baseName = this.normalizeDatBase(lowerName)
+ const targetNames = this.buildPreferredDatNames(baseName, allowThumbnail)
const candidates: string[] = []
if (/^[a-f0-9]{32}$/.test(baseName)) {
const dir1 = baseName.substring(0, 2)
const dir2 = baseName.substring(2, 4)
- candidates.push(
- join(root, dir1, dir2, datName),
- join(root, dir1, dir2, 'Img', datName),
- join(root, dir1, dir2, 'mg', datName),
- join(root, dir1, dir2, 'Image', datName)
- )
+ for (const targetName of targetNames) {
+ candidates.push(
+ join(root, dir1, dir2, targetName),
+ join(root, dir1, dir2, 'Img', targetName),
+ join(root, dir1, dir2, 'mg', targetName),
+ join(root, dir1, dir2, 'Image', targetName)
+ )
+ }
}
for (const path of candidates) {
@@ -883,13 +895,6 @@ export class ImageDecryptService {
months.push(mStr)
}
- const targetNames = [datName]
- if (baseName !== lowerName) {
- targetNames.push(`${baseName}.dat`)
- targetNames.push(`${baseName}_t.dat`)
- targetNames.push(`${baseName}_thumb.dat`)
- }
-
const batchSize = 20
for (let i = 0; i < sessionDirs.length; i += batchSize) {
const batch = sessionDirs.slice(i, i + batchSize)
@@ -919,36 +924,13 @@ export class ImageDecryptService {
/**
* 在同一目录下查找高清图变体
- * 缩略图 xxx_t.dat -> 高清图 xxx_h.dat 或 xxx.dat
+ * 优先 `_h`,再回退其他非缩略图变体
*/
private findHdVariantInSameDir(thumbPath: string): string | null {
try {
const dir = dirname(thumbPath)
- const fileName = basename(thumbPath).toLowerCase()
-
- // 提取基础名称(去掉 _t.dat 或 .t.dat)
- let baseName = fileName
- if (baseName.endsWith('_t.dat')) {
- baseName = baseName.slice(0, -6)
- } else if (baseName.endsWith('.t.dat')) {
- baseName = baseName.slice(0, -6)
- } else {
- return null
- }
-
- // 尝试查找高清图变体
- const variants = [
- `${baseName}_h.dat`,
- `${baseName}.h.dat`,
- `${baseName}.dat`
- ]
-
- for (const variant of variants) {
- const variantPath = join(dir, variant)
- if (existsSync(variantPath)) {
- return variantPath
- }
- }
+ const fileName = basename(thumbPath)
+ return this.findPreferredDatVariantInDir(dir, fileName, false)
} catch { }
return null
}
@@ -998,7 +980,86 @@ export class ImageDecryptService {
void worker.terminate()
resolve(null)
})
- })
+ })
+ }
+
+ private stripDatVariantSuffix(base: string): string {
+ const lower = base.toLowerCase()
+ const suffixes = ['_thumb', '.thumb', '_hd', '.hd', '_h', '.h', '_t', '.t', '_c', '.c']
+ for (const suffix of suffixes) {
+ if (lower.endsWith(suffix)) {
+ return lower.slice(0, -suffix.length)
+ }
+ }
+ if (/[._][a-z]$/.test(lower)) {
+ return lower.slice(0, -2)
+ }
+ return lower
+ }
+
+ private getDatVariantPriority(name: string): number {
+ const lower = name.toLowerCase()
+ const baseLower = lower.endsWith('.dat') || lower.endsWith('.jpg') ? lower.slice(0, -4) : lower
+ if (baseLower.endsWith('_h') || baseLower.endsWith('.h')) return 600
+ if (!this.hasXVariant(baseLower)) return 500
+ if (baseLower.endsWith('_hd') || baseLower.endsWith('.hd')) return 450
+ if (baseLower.endsWith('_c') || baseLower.endsWith('.c')) return 400
+ if (this.isThumbnailDat(lower)) return 100
+ return 350
+ }
+
+ private buildPreferredDatNames(baseName: string, allowThumbnail: boolean): string[] {
+ if (!baseName) return []
+ const names = [
+ `${baseName}_h.dat`,
+ `${baseName}.h.dat`,
+ `${baseName}.dat`,
+ `${baseName}_hd.dat`,
+ `${baseName}.hd.dat`,
+ `${baseName}_c.dat`,
+ `${baseName}.c.dat`
+ ]
+ if (allowThumbnail) {
+ names.push(
+ `${baseName}_thumb.dat`,
+ `${baseName}.thumb.dat`,
+ `${baseName}_t.dat`,
+ `${baseName}.t.dat`
+ )
+ }
+ return Array.from(new Set(names))
+ }
+
+ private findPreferredDatVariantInDir(dirPath: string, baseName: string, allowThumbnail: boolean): string | null {
+ let entries: string[]
+ try {
+ entries = readdirSync(dirPath)
+ } catch {
+ return null
+ }
+ const target = this.normalizeDatBase(baseName.toLowerCase())
+ let bestPath: string | null = null
+ let bestScore = Number.NEGATIVE_INFINITY
+ for (const entry of entries) {
+ const lower = entry.toLowerCase()
+ if (!lower.endsWith('.dat')) continue
+ if (!allowThumbnail && this.isThumbnailDat(lower)) continue
+ const baseLower = lower.slice(0, -4)
+ if (this.normalizeDatBase(baseLower) !== target) continue
+ const score = this.getDatVariantPriority(lower)
+ if (score > bestScore) {
+ bestScore = score
+ bestPath = join(dirPath, entry)
+ }
+ }
+ return bestPath
+ }
+
+ private getPreferredDatVariantPath(datPath: string, allowThumbnail: boolean): string {
+ const lower = datPath.toLowerCase()
+ if (!lower.endsWith('.dat')) return datPath
+ const preferred = this.findPreferredDatVariantInDir(dirname(datPath), basename(datPath), allowThumbnail)
+ return preferred || datPath
}
private normalizeDatBase(name: string): string {
@@ -1006,18 +1067,21 @@ export class ImageDecryptService {
if (base.endsWith('.dat') || base.endsWith('.jpg')) {
base = base.slice(0, -4)
}
- while (/[._][a-z]$/.test(base)) {
- base = base.slice(0, -2)
+ for (;;) {
+ const stripped = this.stripDatVariantSuffix(base)
+ if (stripped === base) {
+ return base
+ }
+ base = stripped
}
- return base
}
private hasImageVariantSuffix(baseLower: string): boolean {
- return /[._][a-z]$/.test(baseLower)
+ return this.stripDatVariantSuffix(baseLower) !== baseLower
}
private isLikelyImageDatBase(baseLower: string): boolean {
- return this.hasImageVariantSuffix(baseLower) || this.looksLikeMd5(baseLower)
+ return this.hasImageVariantSuffix(baseLower) || this.looksLikeMd5(this.normalizeDatBase(baseLower))
}
@@ -1206,24 +1270,7 @@ export class ImageDecryptService {
}
private findNonThumbnailVariantInDir(dirPath: string, baseName: string): string | null {
- let entries: string[]
- try {
- entries = readdirSync(dirPath)
- } catch {
- return null
- }
- const target = this.normalizeDatBase(baseName.toLowerCase())
- for (const entry of entries) {
- const lower = entry.toLowerCase()
- if (!lower.endsWith('.dat')) continue
- if (this.isThumbnailDat(lower)) continue
- const baseLower = lower.slice(0, -4)
- // 只排除没有 _x 变体后缀的文件(允许 _hd、_h 等所有带变体的)
- if (!this.hasXVariant(baseLower)) continue
- if (this.normalizeDatBase(baseLower) !== target) continue
- return join(dirPath, entry)
- }
- return null
+ return this.findPreferredDatVariantInDir(dirPath, baseName, false)
}
private isNonThumbnailVariantDat(datPath: string): boolean {
@@ -1231,8 +1278,7 @@ export class ImageDecryptService {
if (!lower.endsWith('.dat')) return false
if (this.isThumbnailDat(lower)) return false
const baseLower = lower.slice(0, -4)
- // 只检查是否有 _x 变体后缀(允许 _hd、_h 等所有带变体的)
- return this.hasXVariant(baseLower)
+ return this.isLikelyImageDatBase(baseLower)
}
private emitImageUpdate(payload: { sessionId?: string; imageMd5?: string; imageDatName?: string }, cacheKey: string): void {
@@ -1858,7 +1904,7 @@ export class ImageDecryptService {
private hasXVariant(base: string): boolean {
const lower = base.toLowerCase()
- return lower.endsWith('_h') || lower.endsWith('_hd') || lower.endsWith('_thumb') || lower.endsWith('_t')
+ return this.stripDatVariantSuffix(lower) !== lower
}
private isHdPath(p: string): boolean {
From 7697f382ef41ddab5590a68a4804b50ff9b59bfe Mon Sep 17 00:00:00 2001
From: xuncha <1658671838@qq.com>
Date: Fri, 13 Mar 2026 20:49:53 +0800
Subject: [PATCH 5/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B6=88=E6=81=AF?=
=?UTF-8?q?=E7=AA=97=E5=8F=A3=E6=8B=96=E5=8A=A8=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/pages/ChatPage.scss | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss
index a21cafb..4b02936 100644
--- a/src/pages/ChatPage.scss
+++ b/src/pages/ChatPage.scss
@@ -1605,6 +1605,7 @@
align-items: center;
gap: 12px;
border-bottom: 1px solid var(--border-color);
+ -webkit-app-region: drag;
.session-avatar {
width: 40px;
@@ -1638,6 +1639,7 @@
display: flex;
align-items: center;
gap: 8px;
+ -webkit-app-region: no-drag;
.jump-calendar-anchor {
position: relative;
From 6e371d75c8aaa5b0f3044cb96e09647c970d8e6f Mon Sep 17 00:00:00 2001
From: xuncha <1658671838@qq.com>
Date: Fri, 13 Mar 2026 20:50:08 +0800
Subject: [PATCH 6/8] =?UTF-8?q?=E6=AF=8F=E6=AC=A1=E7=82=B9=E5=87=BB?=
=?UTF-8?q?=E5=9B=BE=E7=89=87=E7=9A=84=E6=97=B6=E5=80=99=20=E9=83=BD?=
=?UTF-8?q?=E8=A7=A3=E5=AF=86=E4=B8=80=E9=81=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/pages/ChatPage.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx
index 0eec7d4..09f33db 100644
--- a/src/pages/ChatPage.tsx
+++ b/src/pages/ChatPage.tsx
@@ -5464,8 +5464,9 @@ function MessageBubble({
let finalImagePath = imageLocalPath
let finalLiveVideoPath = imageLiveVideoPath || undefined
- // If current cache is a thumbnail, wait for a silent force-HD decrypt before opening viewer.
- if (imageHasUpdate) {
+ // Every explicit preview click re-runs the forced HD search/decrypt path so
+ // users don't need to re-enter the session after WeChat materializes a new original image.
+ if (message.imageMd5 || message.imageDatName) {
try {
const upgraded = await requestImageDecrypt(true, true)
if (upgraded?.success && upgraded.localPath) {
@@ -5497,7 +5498,6 @@ function MessageBubble({
void window.electronAPI.window.openImageViewerWindow(finalImagePath, finalLiveVideoPath)
}, [
- imageHasUpdate,
imageLiveVideoPath,
imageLocalPath,
imageCacheKey,
From b9e0535f63be6c368bfd060858986fdb677ce334 Mon Sep 17 00:00:00 2001
From: xuncha <1658671838@qq.com>
Date: Fri, 13 Mar 2026 21:13:45 +0800
Subject: [PATCH 7/8] =?UTF-8?q?https://github.com/hicccc77/WeFlow/issues/3?=
=?UTF-8?q?72=E5=AF=BC=E5=87=BA=E6=97=B6=E7=BB=99=E6=AF=8F=E4=B8=AA?=
=?UTF-8?q?=E6=A0=BC=E5=BC=8F=E9=83=BD=E6=8E=A5=E5=85=A5=E4=BA=86=E7=BE=A4?=
=?UTF-8?q?=E6=88=90=E5=91=98=E6=98=BE=E7=A4=BA=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
electron/services/exportService.ts | 236 ++++++++++++++++++++++-------
1 file changed, 182 insertions(+), 54 deletions(-)
diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts
index e789187..dcf3956 100644
--- a/electron/services/exportService.ts
+++ b/electron/services/exportService.ts
@@ -107,6 +107,15 @@ interface MediaExportItem {
posterDataUrl?: string
}
+interface ExportDisplayProfile {
+ wxid: string
+ nickname: string
+ remark: string
+ alias: string
+ groupNickname: string
+ displayName: string
+}
+
type MessageCollectMode = 'full' | 'text-fast' | 'media-fast'
type MediaContentType = 'voice' | 'image' | 'video' | 'emoji'
@@ -860,6 +869,50 @@ class ExportService {
}
}
+ private async resolveExportDisplayProfile(
+ wxid: string,
+ preference: ExportOptions['displayNamePreference'],
+ getContact: (username: string) => Promise<{ success: boolean; contact?: any; error?: string }>,
+ groupNicknamesMap: Map,
+ fallbackDisplayName = '',
+ extraGroupNicknameCandidates: Array = []
+ ): Promise {
+ const resolvedWxid = String(wxid || '').trim() || String(fallbackDisplayName || '').trim() || 'unknown'
+ const contactResult = resolvedWxid ? await getContact(resolvedWxid) : { success: false as const }
+ const contact = contactResult.success ? contactResult.contact : null
+ const nickname = String(contact?.nickName || contact?.nick_name || fallbackDisplayName || resolvedWxid)
+ const remark = String(contact?.remark || '')
+ const alias = String(contact?.alias || '')
+ const groupNickname = this.resolveGroupNicknameByCandidates(
+ groupNicknamesMap,
+ [
+ resolvedWxid,
+ contact?.username,
+ contact?.userName,
+ contact?.encryptUsername,
+ contact?.encryptUserName,
+ alias,
+ ...extraGroupNicknameCandidates
+ ]
+ ) || ''
+ const displayName = this.getPreferredDisplayName(
+ resolvedWxid,
+ nickname,
+ remark,
+ groupNickname,
+ preference || 'remark'
+ )
+
+ return {
+ wxid: resolvedWxid,
+ nickname,
+ remark,
+ alias,
+ groupNickname,
+ displayName
+ }
+ }
+
/**
* 从转账消息 XML 中提取并解析 "谁转账给谁" 描述
* @param content 原始消息内容 XML
@@ -3282,8 +3335,19 @@ class ExportService {
const cleanedMyWxid = conn.cleanedWxid
const isGroup = sessionId.includes('@chatroom')
+ const rawMyWxid = String(this.configService.get('myWxid') || '').trim()
const sessionInfo = await this.getContactInfo(sessionId)
+ const myInfo = await this.getContactInfo(cleanedMyWxid)
+ const contactCache = new Map()
+ const getContactCached = async (username: string) => {
+ if (contactCache.has(username)) {
+ return contactCache.get(username)!
+ }
+ const result = await wcdbService.getContact(username)
+ contactCache.set(username, result)
+ return result
+ }
onProgress?.({
current: 0,
@@ -3319,6 +3383,18 @@ class ExportService {
await this.ensureVoiceModel(onProgress)
}
+ const senderUsernames = new Set()
+ let senderScanIndex = 0
+ for (const msg of allMessages) {
+ if ((senderScanIndex++ & 0x7f) === 0) {
+ this.throwIfStopRequested(control)
+ }
+ if (msg.senderUsername) senderUsernames.add(msg.senderUsername)
+ }
+ senderUsernames.add(sessionId)
+ senderUsernames.add(cleanedMyWxid)
+ await this.preloadContacts(senderUsernames, contactCache)
+
if (isGroup) {
this.throwIfStopRequested(control)
await this.mergeGroupMembers(sessionId, collected.memberSet, options.exportAvatars === true)
@@ -3449,6 +3525,7 @@ class ExportService {
})
const chatLabMessages: ChatLabMessage[] = []
+ const senderProfileMap = new Map()
let messageIndex = 0
for (const msg of allMessages) {
if ((messageIndex++ & 0x7f) === 0) {
@@ -3464,6 +3541,26 @@ class ExportService {
const groupNickname = memberInfo.groupNickname
|| (isGroup ? this.resolveGroupNicknameByCandidates(groupNicknamesMap, [msg.senderUsername]) : '')
|| ''
+ const senderProfile = isGroup
+ ? await this.resolveExportDisplayProfile(
+ msg.senderUsername || cleanedMyWxid,
+ options.displayNamePreference,
+ getContactCached,
+ groupNicknamesMap,
+ msg.isSend ? (myInfo.displayName || cleanedMyWxid) : (memberInfo.accountName || msg.senderUsername || ''),
+ msg.isSend ? [rawMyWxid, cleanedMyWxid] : []
+ )
+ : {
+ wxid: msg.senderUsername || cleanedMyWxid,
+ nickname: memberInfo.accountName || msg.senderUsername || '',
+ remark: '',
+ alias: '',
+ groupNickname,
+ displayName: memberInfo.accountName || msg.senderUsername || ''
+ }
+ if (senderProfile.wxid && !senderProfileMap.has(senderProfile.wxid)) {
+ senderProfileMap.set(senderProfile.wxid, senderProfile)
+ }
// 确定消息内容
let content: string | null
@@ -3504,8 +3601,8 @@ class ExportService {
const message: ChatLabMessage = {
sender: msg.senderUsername,
- accountName: memberInfo.accountName,
- groupNickname: groupNickname || undefined,
+ accountName: senderProfile.displayName || memberInfo.accountName,
+ groupNickname: (senderProfile.groupNickname || groupNickname) || undefined,
timestamp: msg.createTime,
type: this.convertMessageType(msg.localType, msg.content),
content: content
@@ -3621,10 +3718,27 @@ class ExportService {
: new Map()
const sessionAvatar = avatarMap.get(sessionId)
- const members = Array.from(collected.memberSet.values()).map((info) => {
+ const members = await Promise.all(Array.from(collected.memberSet.values()).map(async (info) => {
+ const profile = isGroup
+ ? (senderProfileMap.get(info.member.platformId) || await this.resolveExportDisplayProfile(
+ info.member.platformId,
+ options.displayNamePreference,
+ getContactCached,
+ groupNicknamesMap,
+ info.member.accountName || info.member.platformId,
+ this.isSameWxid(info.member.platformId, cleanedMyWxid) ? [rawMyWxid, cleanedMyWxid] : []
+ ))
+ : null
+ const member = profile
+ ? {
+ ...info.member,
+ accountName: profile.displayName || info.member.accountName,
+ groupNickname: profile.groupNickname || info.member.groupNickname
+ }
+ : info.member
const avatar = avatarMap.get(info.member.platformId)
- return avatar ? { ...info.member, avatar } : info.member
- })
+ return avatar ? { ...member, avatar } : member
+ }))
const { chatlab, meta } = this.getExportMeta(sessionId, sessionInfo, isGroup, sessionAvatar)
@@ -3697,6 +3811,7 @@ class ExportService {
const cleanedMyWxid = conn.cleanedWxid
const isGroup = sessionId.includes('@chatroom')
+ const rawMyWxid = String(this.configService.get('myWxid') || '').trim()
const sessionInfo = await this.getContactInfo(sessionId)
const myInfo = await this.getContactInfo(cleanedMyWxid)
@@ -4512,13 +4627,14 @@ class ExportService {
}
// 预加载群昵称 (仅群聊且完整列模式)
- const groupNicknameCandidates = (isGroup && !useCompactColumns)
+ const groupNicknameCandidates = isGroup
? this.buildGroupNicknameIdCandidates([
...collected.rows.map(msg => msg.senderUsername),
- cleanedMyWxid
+ cleanedMyWxid,
+ rawMyWxid
])
: []
- const groupNicknamesMap = (isGroup && !useCompactColumns)
+ const groupNicknamesMap = isGroup
? await this.getGroupNicknamesForRoom(sessionId, groupNicknameCandidates)
: new Map()
@@ -4637,30 +4753,26 @@ class ExportService {
let senderRemark: string = ''
let senderGroupNickname: string = '' // 群昵称
-
- if (msg.isSend) {
+ if (isGroup) {
+ const senderProfile = await this.resolveExportDisplayProfile(
+ msg.isSend ? cleanedMyWxid : (msg.senderUsername || cleanedMyWxid),
+ options.displayNamePreference,
+ getContactCached,
+ groupNicknamesMap,
+ msg.isSend ? (myInfo.displayName || cleanedMyWxid) : (msg.senderUsername || ''),
+ msg.isSend ? [rawMyWxid, cleanedMyWxid] : []
+ )
+ senderWxid = senderProfile.wxid
+ senderNickname = senderProfile.nickname
+ senderRemark = senderProfile.remark
+ senderGroupNickname = senderProfile.groupNickname
+ senderRole = senderProfile.displayName
+ } else if (msg.isSend) {
// 我发送的消息
senderRole = '我'
senderWxid = cleanedMyWxid
senderNickname = myInfo.displayName || cleanedMyWxid
senderRemark = ''
- } else if (isGroup && msg.senderUsername) {
- // 群消息
- senderWxid = msg.senderUsername
-
- // 用 getContact 获取联系人详情,分别取昵称和备注
- const contactDetail = await getContactCached(msg.senderUsername)
- if (contactDetail.success && contactDetail.contact) {
- // nickName 才是真正的昵称
- senderNickname = contactDetail.contact.nickName || msg.senderUsername
- senderRemark = contactDetail.contact.remark || ''
- // 身份:有备注显示备注,没有显示昵称
- senderRole = senderRemark || senderNickname
- } else {
- senderNickname = msg.senderUsername
- senderRemark = ''
- senderRole = msg.senderUsername
- }
} else {
// 单聊对方消息 - 用 getContact 获取联系人详情
senderWxid = sessionId
@@ -4676,12 +4788,6 @@ class ExportService {
}
}
- // 获取群昵称 (仅群聊且完整列模式)
- if (isGroup && !useCompactColumns && senderWxid) {
- senderGroupNickname = this.resolveGroupNicknameByCandidates(groupNicknamesMap, [senderWxid])
- }
-
-
const row = worksheet.getRow(currentRow)
row.height = 24
@@ -4857,6 +4963,7 @@ class ExportService {
const cleanedMyWxid = conn.cleanedWxid
const isGroup = sessionId.includes('@chatroom')
+ const rawMyWxid = String(this.configService.get('myWxid') || '').trim()
const sessionInfo = await this.getContactInfo(sessionId)
const myInfo = await this.getContactInfo(cleanedMyWxid)
@@ -4919,7 +5026,8 @@ class ExportService {
? this.buildGroupNicknameIdCandidates([
...Array.from(senderUsernames.values()),
...collected.rows.map(msg => msg.senderUsername),
- cleanedMyWxid
+ cleanedMyWxid,
+ rawMyWxid
])
: []
const groupNicknamesMap = isGroup
@@ -5077,21 +5185,23 @@ class ExportService {
let senderNickname: string
let senderRemark = ''
- if (msg.isSend) {
+ if (isGroup) {
+ const senderProfile = await this.resolveExportDisplayProfile(
+ msg.isSend ? cleanedMyWxid : (msg.senderUsername || cleanedMyWxid),
+ options.displayNamePreference,
+ getContactCached,
+ groupNicknamesMap,
+ msg.isSend ? (myInfo.displayName || cleanedMyWxid) : (msg.senderUsername || ''),
+ msg.isSend ? [rawMyWxid, cleanedMyWxid] : []
+ )
+ senderWxid = senderProfile.wxid
+ senderNickname = senderProfile.nickname
+ senderRemark = senderProfile.remark
+ senderRole = senderProfile.displayName
+ } else if (msg.isSend) {
senderRole = '我'
senderWxid = cleanedMyWxid
senderNickname = myInfo.displayName || cleanedMyWxid
- } else if (isGroup && msg.senderUsername) {
- senderWxid = msg.senderUsername
- const contactDetail = await getContactCached(msg.senderUsername)
- if (contactDetail.success && contactDetail.contact) {
- senderNickname = contactDetail.contact.nickName || msg.senderUsername
- senderRemark = contactDetail.contact.remark || ''
- senderRole = senderRemark || senderNickname
- } else {
- senderNickname = msg.senderUsername
- senderRole = msg.senderUsername
- }
} else {
senderWxid = sessionId
const contactDetail = await getContactCached(sessionId)
@@ -5163,6 +5273,7 @@ class ExportService {
const cleanedMyWxid = conn.cleanedWxid
const isGroup = sessionId.includes('@chatroom')
+ const rawMyWxid = String(this.configService.get('myWxid') || '').trim()
const sessionInfo = await this.getContactInfo(sessionId)
const myInfo = await this.getContactInfo(cleanedMyWxid)
@@ -5214,7 +5325,8 @@ class ExportService {
? this.buildGroupNicknameIdCandidates([
...Array.from(senderUsernames.values()),
...collected.rows.map(msg => msg.senderUsername),
- cleanedMyWxid
+ cleanedMyWxid,
+ rawMyWxid
])
: []
const groupNicknamesMap = isGroup
@@ -5344,7 +5456,17 @@ class ExportService {
}
let talker = myInfo.displayName || '我'
- if (!msg.isSend) {
+ if (isGroup) {
+ const senderProfile = await this.resolveExportDisplayProfile(
+ msg.isSend ? cleanedMyWxid : senderWxid,
+ options.displayNamePreference,
+ getContactCached,
+ groupNicknamesMap,
+ msg.isSend ? (myInfo.displayName || cleanedMyWxid) : senderWxid,
+ msg.isSend ? [rawMyWxid, cleanedMyWxid] : []
+ )
+ talker = senderProfile.displayName
+ } else if (!msg.isSend) {
const contactDetail = await getContactCached(senderWxid)
const senderNickname = contactDetail.success && contactDetail.contact
? (contactDetail.contact.nickName || senderWxid)
@@ -5584,7 +5706,8 @@ class ExportService {
? this.buildGroupNicknameIdCandidates([
...Array.from(senderUsernames.values()),
...collected.rows.map(msg => msg.senderUsername),
- cleanedMyWxid
+ cleanedMyWxid,
+ rawMyWxid
])
: []
const groupNicknamesMap = isGroup
@@ -5792,11 +5915,16 @@ class ExportService {
const isSenderMe = msg.isSend
const senderInfo = collected.memberSet.get(msg.senderUsername)?.member
- const senderName = isSenderMe
- ? (myInfo.displayName || '我')
- : (isGroup
- ? (senderInfo?.groupNickname || senderInfo?.accountName || msg.senderUsername)
- : (sessionInfo.displayName || sessionId))
+ const senderName = isGroup
+ ? (await this.resolveExportDisplayProfile(
+ isSenderMe ? cleanedMyWxid : (msg.senderUsername || cleanedMyWxid),
+ options.displayNamePreference,
+ getContactCached,
+ groupNicknamesMap,
+ isSenderMe ? (myInfo.displayName || cleanedMyWxid) : (senderInfo?.accountName || msg.senderUsername || ''),
+ isSenderMe ? [rawMyWxid, cleanedMyWxid] : []
+ )).displayName
+ : (isSenderMe ? (myInfo.displayName || '我') : (sessionInfo.displayName || sessionId))
const avatarHtml = getAvatarHtml(isSenderMe ? cleanedMyWxid : msg.senderUsername, senderName)
From bdc7f8a8a841e409f03b5cca0799557add00ed95 Mon Sep 17 00:00:00 2001
From: xuncha <1658671838@qq.com>
Date: Sat, 14 Mar 2026 15:40:44 +0800
Subject: [PATCH 8/8] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=89=93=E5=8C=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/release.yml | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 58b8b58..477da32 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -8,6 +8,9 @@ on:
permissions:
contents: write
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
+
jobs:
release-mac-arm64:
runs-on: macos-14
@@ -21,7 +24,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
- node-version: 22.12
+ node-version: 24
cache: "npm"
- name: Install Dependencies
@@ -73,7 +76,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
- node-version: 22.12
+ node-version: 24
cache: 'npm'
- name: Install Dependencies