From e12193aa40cf75b90bc7c1df27d9a3d9d1d5cba7 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, 22 Mar 2026 14:17:19 +0800 Subject: [PATCH] =?UTF-8?q?=20=20feat:=20=E5=AF=BC=E5=87=BA=E8=81=94?= =?UTF-8?q?=E7=B3=BB=E4=BA=BA=E6=A0=87=E7=AD=BE=E5=92=8C=E8=AF=A6=E7=BB=86?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 扩展联系人读取与导出链路,新增 labels 和 detailDescription 字段的兼容提取,并同步更新通讯录缓存、详情展示与 JSON/CSV/VCF 导出。 Close #402 --- electron/services/chatService.ts | 59 +++++++++++++++++++++++ electron/services/contactExportService.ts | 18 +++++-- src/pages/ContactsPage.tsx | 6 +++ src/services/config.ts | 10 ++++ src/types/models.ts | 2 + 5 files changed, 91 insertions(+), 4 deletions(-) diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index 4a71e88..0dfd3d9 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -153,6 +153,8 @@ export interface ContactInfo { remark?: string nickname?: string alias?: string + labels?: string[] + detailDescription?: string avatarUrl?: string type: 'friend' | 'group' | 'official' | 'former_friend' | 'other' } @@ -1321,6 +1323,8 @@ class ChatService { continue } + const labels = this.parseContactLabels(row) + const detailDescription = this.getContactDetailDescription(row) const displayName = row.remark || row.nick_name || row.alias || username contacts.push({ @@ -1329,6 +1333,8 @@ class ChatService { remark: row.remark || undefined, nickname: row.nick_name || undefined, alias: row.alias || undefined, + labels: labels.length > 0 ? labels : undefined, + detailDescription: detailDescription || undefined, avatarUrl: undefined, type, lastContactTime: lastContactTimeMap.get(username) || 0 @@ -1880,6 +1886,59 @@ class ChatService { return Number.isFinite(parsed) ? parsed : fallback } + private parseContactLabels(row: Record): string[] { + const raw = this.getRowField(row, [ + 'label_list', 'labelList', 'labels', 'label_names', 'labelNames', 'tags', 'tag_list', 'tagList' + ]) + const normalizedFromValue = (value: unknown): string[] => { + if (Array.isArray(value)) { + return Array.from(new Set(value.map((item) => String(item || '').trim()).filter(Boolean))) + } + const text = String(value || '').trim() + if (!text) return [] + return Array.from(new Set( + text + .replace(/[;;、|]+/g, ',') + .split(',') + .map((item) => item.trim()) + .filter(Boolean) + )) + } + + const direct = normalizedFromValue(raw) + if (direct.length > 0) return direct + + for (const [key, value] of Object.entries(row)) { + const normalizedKey = key.toLowerCase() + if (!normalizedKey.includes('label') && !normalizedKey.includes('tag')) continue + if (normalizedKey.includes('img') || normalizedKey.includes('head')) continue + const fallback = normalizedFromValue(value) + if (fallback.length > 0) return fallback + } + + return [] + } + + private getContactDetailDescription(row: Record): string { + const value = this.getRowField(row, [ + 'detail_description', 'detailDescription', 'description', 'desc', 'contact_description', 'contactDescription', + 'profile', 'introduction', 'phone', 'mobile', 'telephone', 'tel', 'vcard', 'card_info', 'cardInfo' + ]) + const direct = String(value || '').trim() + if (direct) return direct + + for (const [key, rawValue] of Object.entries(row)) { + const normalizedKey = key.toLowerCase() + const isCandidate = normalizedKey.includes('detail') || normalizedKey.includes('desc') || normalizedKey.includes('description') || normalizedKey.includes('profile') || normalizedKey.includes('intro') || normalizedKey.includes('phone') || normalizedKey.includes('mobile') || normalizedKey.includes('tel') || normalizedKey.includes('vcard') || normalizedKey.includes('card') + if (!isCandidate) continue + if (normalizedKey.includes('avatar') || normalizedKey.includes('img') || normalizedKey.includes('head')) continue + const text = String(rawValue || '').trim() + if (text) return text + } + + return '' + } + private normalizeUnsignedIntegerToken(raw: any): string | undefined { if (raw === undefined || raw === null || raw === '') return undefined diff --git a/electron/services/contactExportService.ts b/electron/services/contactExportService.ts index 11efee5..3134313 100644 --- a/electron/services/contactExportService.ts +++ b/electron/services/contactExportService.ts @@ -93,6 +93,9 @@ class ContactExportService { displayName: c.displayName, remark: c.remark, nickname: c.nickname, + alias: c.alias, + labels: Array.isArray(c.labels) ? c.labels : [], + detailDescription: c.detailDescription, type: c.type })) } @@ -103,12 +106,15 @@ class ContactExportService { * 导出为CSV格式 */ private async exportToCSV(contacts: any[], outputPath: string): Promise { - const headers = ['用户名', '显示名称', '备注', '昵称', '类型'] + const headers = ['用户名', '显示名称', '备注', '昵称', '微信号', '标签', '详细描述', '类型'] const rows = contacts.map(c => [ c.username || '', c.displayName || '', c.remark || '', c.nickname || '', + c.alias || '', + Array.isArray(c.labels) ? c.labels.join(' | ') : '', + c.detailDescription || '', this.getTypeLabel(c.type) ]) @@ -137,9 +143,13 @@ class ContactExportService { lines.push(`NICKNAME:${c.nickname}`) } - // 备注 - if (c.remark) { - lines.push(`NOTE:${c.remark}`) + const noteParts = [ + c.remark ? String(c.remark) : '', + Array.isArray(c.labels) && c.labels.length > 0 ? `标签: ${c.labels.join(', ')}` : '', + c.detailDescription ? `详细描述: ${c.detailDescription}` : '' + ].filter(Boolean) + if (noteParts.length > 0) { + lines.push(`NOTE:${noteParts.join('\\n')}`) } // 微信ID diff --git a/src/pages/ContactsPage.tsx b/src/pages/ContactsPage.tsx index 35e5a1d..e93588f 100644 --- a/src/pages/ContactsPage.tsx +++ b/src/pages/ContactsPage.tsx @@ -397,6 +397,9 @@ function ContactsPage() { displayName: contact.displayName, remark: contact.remark, nickname: contact.nickname, + alias: contact.alias, + labels: contact.labels, + detailDescription: contact.detailDescription, type: contact.type })) ).catch((error) => { @@ -1110,6 +1113,9 @@ function ContactsPage() {
用户名{selectedContact.username}
昵称{selectedContact.nickname || selectedContact.displayName}
{selectedContact.remark &&
备注{selectedContact.remark}
} + {selectedContact.alias &&
微信号{selectedContact.alias}
} + {selectedContact.labels && selectedContact.labels.length > 0 &&
标签{selectedContact.labels.join('、')}
} + {selectedContact.detailDescription &&
详细描述{selectedContact.detailDescription}
}
类型{getContactTypeName(selectedContact.type)}
{selectedContactSupportsSns && (
diff --git a/src/services/config.ts b/src/services/config.ts index 37c404f..3e88455 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -663,6 +663,8 @@ export interface ContactsListCacheContact { remark?: string nickname?: string alias?: string + labels?: string[] + detailDescription?: string type: 'friend' | 'group' | 'official' | 'former_friend' | 'other' } @@ -1176,6 +1178,10 @@ export async function getContactsListCache(scopeKey: string): Promise String(label || '').trim()).filter(Boolean))) + : undefined, + detailDescription: typeof item.detailDescription === 'string' ? item.detailDescription : undefined, type: (type === 'friend' || type === 'group' || type === 'official' || type === 'former_friend' || type === 'other') ? type : 'other' @@ -1210,6 +1216,10 @@ export async function setContactsListCache(scopeKey: string, contacts: ContactsL remark: contact?.remark ? String(contact.remark) : undefined, nickname: contact?.nickname ? String(contact.nickname) : undefined, alias: contact?.alias ? String(contact.alias) : undefined, + labels: Array.isArray(contact?.labels) + ? Array.from(new Set(contact.labels.map((label) => String(label || '').trim()).filter(Boolean))) + : undefined, + detailDescription: contact?.detailDescription ? String(contact.detailDescription) : undefined, type }) } diff --git a/src/types/models.ts b/src/types/models.ts index 74e81dd..334332f 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -37,6 +37,8 @@ export interface ContactInfo { remark?: string nickname?: string alias?: string + labels?: string[] + detailDescription?: string avatarUrl?: string type: 'friend' | 'group' | 'official' | 'former_friend' | 'other' }