From 318c296ee91bf28d3a56a7e8749ea1471f1d060a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A7=9C=E5=8C=97=E5=B0=98?= <2678115663@qq.com> Date: Sun, 15 Mar 2026 01:30:14 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=A1=A5=E9=BD=90=E7=BE=A4=E8=81=8A=20H?= =?UTF-8?q?TTP=20API=20=E5=AF=BC=E5=87=BA=E7=9A=84=E5=A4=B4=E5=83=8F?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 ChatLab 格式的群聊 HTTP API 导出补齐成员头像与群头像, 并兼容 wxid 清洗后的账号匹配,避免导出结果只有昵称没有头像。 Fixes #371 --- electron/services/httpService.ts | 93 ++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/electron/services/httpService.ts b/electron/services/httpService.ts index bfb8ce1..6e3423f 100644 --- a/electron/services/httpService.ts +++ b/electron/services/httpService.ts @@ -798,6 +798,20 @@ class HttpService { return 0 } + private normalizeAccountId(value: string): string { + const trimmed = String(value || '').trim() + if (!trimmed) return trimmed + + if (trimmed.toLowerCase().startsWith('wxid_')) { + const match = trimmed.match(/^(wxid_[^_]+)/i) + if (match) return match[1] + return trimmed + } + + const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/) + return suffixMatch ? suffixMatch[1] : trimmed + } + /** * 获取显示名称 */ @@ -814,6 +828,62 @@ class HttpService { return {} } + private async getAvatarUrls(usernames: string[]): Promise> { + const lookupUsernames = Array.from(new Set( + usernames.flatMap((username) => { + const normalized = String(username || '').trim() + if (!normalized) return [] + const cleaned = this.normalizeAccountId(normalized) + return cleaned && cleaned !== normalized ? [normalized, cleaned] : [normalized] + }) + )) + + if (lookupUsernames.length === 0) return {} + + try { + const result = await wcdbService.getAvatarUrls(lookupUsernames) + if (result.success && result.map) { + const avatarMap: Record = {} + for (const [username, avatarUrl] of Object.entries(result.map)) { + const normalizedUsername = String(username || '').trim() + const normalizedAvatarUrl = String(avatarUrl || '').trim() + if (!normalizedUsername || !normalizedAvatarUrl) continue + + avatarMap[normalizedUsername] = normalizedAvatarUrl + avatarMap[normalizedUsername.toLowerCase()] = normalizedAvatarUrl + + const cleaned = this.normalizeAccountId(normalizedUsername) + if (cleaned) { + avatarMap[cleaned] = normalizedAvatarUrl + avatarMap[cleaned.toLowerCase()] = normalizedAvatarUrl + } + } + return avatarMap + } + } catch (e) { + console.error('[HttpService] Failed to get avatar urls:', e) + } + + return {} + } + + private resolveAvatarUrl(avatarMap: Record, candidates: Array): string | undefined { + for (const candidate of candidates) { + const normalized = String(candidate || '').trim() + if (!normalized) continue + + const cleaned = this.normalizeAccountId(normalized) + const avatarUrl = avatarMap[normalized] + || avatarMap[normalized.toLowerCase()] + || avatarMap[cleaned] + || avatarMap[cleaned.toLowerCase()] + + if (avatarUrl) return avatarUrl + } + + return undefined + } + private lookupGroupNickname(groupNicknamesMap: Map, sender: string): string { if (!sender) return '' return groupNicknamesMap.get(sender) || groupNicknamesMap.get(sender.toLowerCase()) || '' @@ -868,6 +938,7 @@ class HttpService { ): Promise { const isGroup = talkerId.endsWith('@chatroom') const myWxid = this.configService.get('myWxid') || '' + const normalizedMyWxid = this.normalizeAccountId(myWxid).toLowerCase() // 收集所有发送者 const senderSet = new Set() @@ -906,6 +977,27 @@ class HttpService { } } + const [memberAvatarMap, myAvatarResult, sessionAvatarInfo] = await Promise.all([ + this.getAvatarUrls(Array.from(memberMap.keys()).filter((sender) => !sender.startsWith('unknown_sender_'))), + myWxid + ? chatService.getMyAvatarUrl() + : Promise.resolve<{ success: boolean; avatarUrl?: string }>({ success: true }), + isGroup ? chatService.getContactAvatar(talkerId) : Promise.resolve(null) + ]) + + for (const [sender, member] of memberMap.entries()) { + if (sender.startsWith('unknown_sender_')) continue + + const normalizedSender = this.normalizeAccountId(sender).toLowerCase() + const isSelfMember = Boolean(normalizedMyWxid && normalizedSender && normalizedSender === normalizedMyWxid) + const avatarUrl = (isSelfMember ? myAvatarResult.avatarUrl : undefined) + || this.resolveAvatarUrl(memberAvatarMap, isSelfMember ? [sender, myWxid] : [sender]) + + if (avatarUrl) { + member.avatar = avatarUrl + } + } + // 转换消息 const chatLabMessages: ChatLabMessage[] = messages.map(msg => { const senderInfo = this.resolveChatLabSenderInfo(msg, talkerId, talkerName, myWxid, isGroup, senderNames, groupNicknamesMap) @@ -933,6 +1025,7 @@ class HttpService { platform: 'wechat', type: isGroup ? 'group' : 'private', groupId: isGroup ? talkerId : undefined, + groupAvatar: isGroup ? sessionAvatarInfo?.avatarUrl : undefined, ownerId: myWxid || undefined }, members: Array.from(memberMap.values()),