From 579b63b0366c221e4c55ed8dde3e81ed985327a5 Mon Sep 17 00:00:00 2001 From: H3CoF6 <1707889225@qq.com> Date: Mon, 16 Mar 2026 07:30:08 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E8=A7=A3=E6=9E=90mmkv=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=EF=BC=8C=E4=BC=98=E5=8C=96=E8=B4=A6=E5=8F=B7=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/services/dbPathService.ts | 120 ++++++++++++++++++++++++++--- src/pages/SettingsPage.scss | 62 ++++++++++++++- src/pages/SettingsPage.tsx | 30 +++++--- src/pages/WelcomePage.scss | 42 ++++++++++ src/pages/WelcomePage.tsx | 49 ++++++++---- 5 files changed, 264 insertions(+), 39 deletions(-) diff --git a/electron/services/dbPathService.ts b/electron/services/dbPathService.ts index b199b85..b6fb973 100644 --- a/electron/services/dbPathService.ts +++ b/electron/services/dbPathService.ts @@ -1,13 +1,90 @@ import { join, basename } from 'path' -import { existsSync, readdirSync, statSync } from 'fs' +import { existsSync, readdirSync, statSync, readFileSync } from 'fs' import { homedir } from 'os' +import { createDecipheriv } from 'crypto' export interface WxidInfo { wxid: string modifiedTime: number + nickname?: string + avatarUrl?: string } export class DbPathService { + private readVarint(buf: Buffer, offset: number): { value: number, length: number } { + let value = 0; + let length = 0; + let shift = 0; + while (offset < buf.length && shift < 32) { + const b = buf[offset++]; + value |= (b & 0x7f) << shift; + length++; + if ((b & 0x80) === 0) break; + shift += 7; + } + return { value, length }; + } + + private extractMmkvString(buf: Buffer, keyName: string): string { + const keyBuf = Buffer.from(keyName, 'utf8'); + const idx = buf.indexOf(keyBuf); + if (idx === -1) return ''; + + try { + let offset = idx + keyBuf.length; + const v1 = this.readVarint(buf, offset); + offset += v1.length; + const v2 = this.readVarint(buf, offset); + offset += v2.length; + + // 合理性检查 + if (v2.value > 0 && v2.value <= 10000 && offset + v2.value <= buf.length) { + return buf.toString('utf8', offset, offset + v2.value); + } + } catch { } + return ''; + } + + private parseGlobalConfig(rootPath: string): { wxid: string, nickname: string, avatarUrl: string } | null { + try { + const configPath = join(rootPath, 'all_users', 'config', 'global_config'); + if (!existsSync(configPath)) return null; + + const fullData = readFileSync(configPath); + if (fullData.length <= 4) return null; + const encryptedData = fullData.subarray(4); + + const key = Buffer.alloc(16, 0); + Buffer.from('xwechat_crypt_key').copy(key); // 直接硬编码,iv更是不重要 + const iv = Buffer.alloc(16, 0); + + const decipher = createDecipheriv('aes-128-cfb', key, iv); + decipher.setAutoPadding(false); + const decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]); + + const wxid = this.extractMmkvString(decrypted, 'mmkv_key_user_name'); + const nickname = this.extractMmkvString(decrypted, 'mmkv_key_nick_name'); + let avatarUrl = this.extractMmkvString(decrypted, 'mmkv_key_head_img_url'); + + if (!avatarUrl && decrypted.includes('http')) { + const httpIdx = decrypted.indexOf('http'); + const nullIdx = decrypted.indexOf(0x00, httpIdx); + if (nullIdx !== -1) { + avatarUrl = decrypted.toString('utf8', httpIdx, nullIdx); + } + } + + if (wxid || nickname) { + return { wxid, nickname, avatarUrl }; + } + return null; + } catch (e) { + console.error('解析 global_config 失败:', e); + return null; + } + } + + /** * 自动检测微信数据库根目录 */ @@ -135,21 +212,16 @@ export class DbPathService { for (const entry of entries) { const entryPath = join(rootPath, entry) let stat: ReturnType - try { - stat = statSync(entryPath) - } catch { - continue - } - + try { stat = statSync(entryPath) } catch { continue } if (!stat.isDirectory()) continue const lower = entry.toLowerCase() if (lower === 'all_users') continue if (!entry.includes('_')) continue - wxids.push({ wxid: entry, modifiedTime: stat.mtimeMs }) } } + if (wxids.length === 0) { const rootName = basename(rootPath) if (rootName.includes('_') && rootName.toLowerCase() !== 'all_users') { @@ -159,12 +231,25 @@ export class DbPathService { } } catch { } - return wxids.sort((a, b) => { + const sorted = wxids.sort((a, b) => { if (b.modifiedTime !== a.modifiedTime) return b.modifiedTime - a.modifiedTime return a.wxid.localeCompare(b.wxid) - }) + }); + + const globalInfo = this.parseGlobalConfig(rootPath); + if (globalInfo) { + for (const w of sorted) { + if (w.wxid.startsWith(globalInfo.wxid) || sorted.length === 1) { + w.nickname = globalInfo.nickname; + w.avatarUrl = globalInfo.avatarUrl; + } + } + } + + return sorted; } + /** * 扫描 wxid 列表 */ @@ -187,10 +272,21 @@ export class DbPathService { } } catch { } - return wxids.sort((a, b) => { + const sorted = wxids.sort((a, b) => { if (b.modifiedTime !== a.modifiedTime) return b.modifiedTime - a.modifiedTime return a.wxid.localeCompare(b.wxid) - }) + }); + + const globalInfo = this.parseGlobalConfig(rootPath); + if (globalInfo) { + for (const w of sorted) { + if (w.wxid.startsWith(globalInfo.wxid) || sorted.length === 1) { + w.nickname = globalInfo.nickname; + w.avatarUrl = globalInfo.avatarUrl; + } + } + } + return sorted; } /** diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index 10b6730..6b8bf8c 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -1705,7 +1705,7 @@ .wxid-dialog-item { display: flex; - flex-direction: column; + align-items: center; gap: 4px; padding: 14px 16px; border-radius: 10px; @@ -1743,6 +1743,66 @@ justify-content: flex-end; } +.wxid-profile-row { + display: flex; + align-items: center; + gap: 12px; + + .wxid-avatar { + width: 38px; + height: 38px; + border-radius: 8px; + object-fit: cover; + } + + .wxid-avatar-fallback { + width: 38px; + height: 38px; + border-radius: 8px; + background: var(--bg-tertiary); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-tertiary); + } + + .wxid-info-col { + display: flex; + flex-direction: column; + gap: 2px; + } +} + +.wxid-profile-mini { + display: flex; + align-items: center; + gap: 10px; + + .wxid-avatar { + width: 26px; + height: 26px; + border-radius: 6px; + object-fit: cover; + } + + .wxid-avatar-fallback { + width: 26px; + height: 26px; + border-radius: 6px; + background: var(--bg-tertiary); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-tertiary); + } + + .wxid-info-col { + display: flex; + flex-direction: column; + align-items: flex-start; + } +} + // 通知过滤双列表容器 .notification-filter-container { display: grid; diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index db817e2..6aff733 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -10,7 +10,7 @@ import { Eye, EyeOff, FolderSearch, FolderOpen, Search, Copy, RotateCcw, Trash2, Plug, Check, Sun, Moon, Monitor, Palette, Database, HardDrive, Info, RefreshCw, ChevronDown, Download, Mic, - ShieldCheck, Fingerprint, Lock, KeyRound, Bell, Globe, BarChart2, X + ShieldCheck, Fingerprint, Lock, KeyRound, Bell, Globe, BarChart2, X, UserRound } from 'lucide-react' import { Avatar } from '../components/Avatar' import './SettingsPage.scss' @@ -34,6 +34,8 @@ const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [ interface WxidOption { wxid: string modifiedTime: number + nickname?: string + avatarUrl?: string } interface SettingsPageProps { @@ -2132,14 +2134,24 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
{wxidOptions.map((opt) => ( -
handleSelectWxid(opt.wxid)} - > - {opt.wxid} - 最后修改 {new Date(opt.modifiedTime).toLocaleString()} -
+
handleSelectWxid(opt.wxid)} + > +
+ {opt.avatarUrl ? ( + avatar + ) : ( +
+ )} +
+ {opt.nickname || opt.wxid} + {opt.nickname && {opt.wxid}} +
+
+ 最后修改 {new Date(opt.modifiedTime).toLocaleString()} +
))}
diff --git a/src/pages/WelcomePage.scss b/src/pages/WelcomePage.scss index ad8358a..fb5012d 100644 --- a/src/pages/WelcomePage.scss +++ b/src/pages/WelcomePage.scss @@ -488,6 +488,48 @@ white-space: nowrap; } +.wxid-profile { + display: flex; + align-items: center; + gap: 10px; +} + +.wxid-avatar { + width: 32px; + height: 32px; + border-radius: 6px; + object-fit: cover; +} + +.wxid-avatar-fallback { + width: 32px; + height: 32px; + border-radius: 6px; + background: var(--bg-tertiary); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-tertiary); +} + +.wxid-info { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; +} + +.wxid-nickname { + font-weight: 600; + font-size: 13px; + color: var(--text-primary); +} + +.wxid-sub { + font-size: 11px; + color: var(--text-tertiary); +} + .field-with-toggle { position: relative; } diff --git a/src/pages/WelcomePage.tsx b/src/pages/WelcomePage.tsx index 2a3745c..185b23b 100644 --- a/src/pages/WelcomePage.tsx +++ b/src/pages/WelcomePage.tsx @@ -47,7 +47,12 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { const [imageAesKey, setImageAesKey] = useState('') const [cachePath, setCachePath] = useState('') const [wxid, setWxid] = useState('') - const [wxidOptions, setWxidOptions] = useState>([]) + const [wxidOptions, setWxidOptions] = useState>([]) const [showWxidSelect, setShowWxidSelect] = useState(false) const wxidSelectRef = useRef(null) const [error, setError] = useState('') @@ -688,22 +693,32 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { onChange={(e) => setWxid(e.target.value)} /> {showWxidSelect && wxidOptions.length > 0 && ( -
- {wxidOptions.map((opt) => ( - - ))} -
+
+ {wxidOptions.map((opt) => ( + + ))} +
)}
From 6beefb9fc01ac24f65e0e9758bce1e977064af0b Mon Sep 17 00:00:00 2001 From: H3CoF6 <1707889225@qq.com> Date: Mon, 16 Mar 2026 07:40:16 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20sidebar=20?= =?UTF-8?q?=E7=9A=84=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Sidebar.tsx | 44 +++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index e6d0147..6b14cb4 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -6,6 +6,7 @@ import { useChatStore } from '../stores/chatStore' import { useAnalyticsStore } from '../stores/analyticsStore' import * as configService from '../services/config' import { onExportSessionStatus, requestExportSessionStatus } from '../services/exportBridge' +import { UserRound } from 'lucide-react' import './Sidebar.scss' @@ -35,6 +36,7 @@ interface AccountProfilesCache { interface WxidOption { wxid: string modifiedTime: number + nickname?: string displayName?: string avatarUrl?: string } @@ -280,26 +282,28 @@ function Sidebar({ collapsed }: SidebarProps) { const accountsCache = readAccountProfilesCache() console.log('[切换账号] 账号缓存:', accountsCache) - const enrichedWxids = wxids.map(option => { + const enrichedWxids = wxids.map((option: WxidOption) => { const normalizedWxid = normalizeAccountId(option.wxid) const cached = accountsCache[option.wxid] || accountsCache[normalizedWxid] + let displayName = option.nickname || option.wxid + let avatarUrl = option.avatarUrl + if (option.wxid === userProfile.wxid || normalizedWxid === userProfile.wxid) { - return { - ...option, - displayName: userProfile.displayName, - avatarUrl: userProfile.avatarUrl - } + displayName = userProfile.displayName || displayName + avatarUrl = userProfile.avatarUrl || avatarUrl } - if (cached) { - console.log('[切换账号] 使用缓存:', option.wxid, cached) - return { - ...option, - displayName: cached.displayName, - avatarUrl: cached.avatarUrl - } + + else if (cached) { + displayName = cached.displayName || displayName + avatarUrl = cached.avatarUrl || avatarUrl + } + + return { + ...option, + displayName, + avatarUrl } - return { ...option, displayName: option.wxid } }) setWxidOptions(enrichedWxids) @@ -553,11 +557,17 @@ function Sidebar({ collapsed }: SidebarProps) { type="button" >
- {option.avatarUrl ? : {getAvatarLetter(option.displayName || option.wxid)}} + {option.avatarUrl ? ( + + ) : ( +
+ +
+ )}
-
{option.displayName || option.wxid}
-
{option.wxid}
+
{option.displayName}
+ {option.displayName !== option.wxid &&
{option.wxid}
}
{userProfile.wxid === option.wxid && 当前} From 7bcdecaceb4cdd28ca4beda8f0c565ebd0d761f0 Mon Sep 17 00:00:00 2001 From: H3CoF6 <1707889225@qq.com> Date: Mon, 16 Mar 2026 08:35:24 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E6=90=9C=E7=B4=A2=E6=97=B6=E4=B8=8D?= =?UTF-8?q?=E6=B1=A1=E6=9F=93=E5=8E=9F=E5=AF=B9=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/ChatPage.tsx | 80 ++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index eaad286..6e3800d 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -3338,24 +3338,32 @@ function ChatPage(props: ChatPageProps) { return } const lower = searchKeyword.toLowerCase() - setFilteredSessions(visible.filter(s => { - const matchedByName = s.displayName?.toLowerCase().includes(lower) - const matchedByUsername = s.username.toLowerCase().includes(lower) - const matchedByAlias = s.alias?.toLowerCase().includes(lower) + setFilteredSessions(visible + .filter(s => { + const matchedByName = s.displayName?.toLowerCase().includes(lower) + const matchedByUsername = s.username.toLowerCase().includes(lower) + const matchedByAlias = s.alias?.toLowerCase().includes(lower) + return matchedByName || matchedByUsername || matchedByAlias + }) + .map(s => { + const matchedByName = s.displayName?.toLowerCase().includes(lower) + const matchedByUsername = s.username.toLowerCase().includes(lower) + const matchedByAlias = s.alias?.toLowerCase().includes(lower) - if (matchedByUsername && !matchedByName && !matchedByAlias) { - s.matchedField = 'wxid' - } else if (matchedByAlias && !matchedByName && !matchedByUsername) { - s.matchedField = 'alias' - } else if (matchedByName && !matchedByUsername && !matchedByAlias) { - (s as any).matchedField = 'name' - console.log('设置 matchedField=name:', s.displayName) - } else { - s.matchedField = undefined - } + let matchedField: 'wxid' | 'alias' | 'name' | undefined = undefined + + if (matchedByUsername && !matchedByName && !matchedByAlias) { + matchedField = 'wxid' + } else if (matchedByAlias && !matchedByName && !matchedByUsername) { + matchedField = 'alias' + } else if (matchedByName && !matchedByUsername && !matchedByAlias) { + matchedField = 'name' + } - return matchedByName || matchedByUsername || matchedByAlias - })) + // ✅ 关键点:返回一个新对象,解耦全局状态 + return { ...s, matchedField } + }) + ) }, [sessions, searchKeyword, setFilteredSessions]) // 折叠群列表(独立计算,供折叠 panel 使用) @@ -3364,22 +3372,34 @@ function ChatPage(props: ChatPageProps) { const folded = sessions.filter(s => s.isFolded) if (!searchKeyword.trim() || !foldedView) return folded const lower = searchKeyword.toLowerCase() - return folded.filter(s => { - const matchedByName = s.displayName?.toLowerCase().includes(lower) - const matchedByUsername = s.username.toLowerCase().includes(lower) - const matchedByAlias = s.alias?.toLowerCase().includes(lower) - const matchedBySummary = s.summary.toLowerCase().includes(lower) + return folded + // 1. 先过滤 + .filter(s => { + const matchedByName = s.displayName?.toLowerCase().includes(lower) + const matchedByUsername = s.username.toLowerCase().includes(lower) + const matchedByAlias = s.alias?.toLowerCase().includes(lower) + const matchedBySummary = s.summary?.toLowerCase().includes(lower) // 注意:这里有个 summary - if (matchedByUsername && !matchedByName && !matchedBySummary && !matchedByAlias) { - s.matchedField = 'wxid' - } else if (matchedByAlias && !matchedByName && !matchedBySummary && !matchedByUsername) { - s.matchedField = 'alias' - } else { - s.matchedField = undefined - } + return matchedByName || matchedByUsername || matchedByAlias || matchedBySummary + }) + // 2. 后映射 + .map(s => { + const matchedByName = s.displayName?.toLowerCase().includes(lower) + const matchedByUsername = s.username.toLowerCase().includes(lower) + const matchedByAlias = s.alias?.toLowerCase().includes(lower) + const matchedBySummary = s.summary?.toLowerCase().includes(lower) - return matchedByName || matchedByUsername || matchedByAlias || matchedBySummary - }) + let matchedField: 'wxid' | 'alias' | 'name' | undefined = undefined + + if (matchedByUsername && !matchedByName && !matchedBySummary && !matchedByAlias) { + matchedField = 'wxid' + } else if (matchedByAlias && !matchedByName && !matchedBySummary && !matchedByUsername) { + matchedField = 'alias' + } + + // ✅ 同样返回新对象 + return { ...s, matchedField } + }) }, [sessions, searchKeyword, foldedView]) const hasSessionRecords = Array.isArray(sessions) && sessions.length > 0