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) => ( + + ))} +
)}