feat:新增了切换账号的功能

This commit is contained in:
xuncha
2026-01-24 12:39:20 +08:00
parent eb2f90e605
commit 4d647a9467
13 changed files with 300 additions and 86 deletions

View File

@@ -8,6 +8,7 @@ interface ConfigSchema {
onboardingDone: boolean onboardingDone: boolean
imageXorKey: number imageXorKey: number
imageAesKey: string imageAesKey: string
wxidConfigs: Record<string, { decryptKey?: string; imageXorKey?: number; imageAesKey?: string; updatedAt?: number }>
// 缓存相关 // 缓存相关
cachePath: string cachePath: string
@@ -40,6 +41,7 @@ export class ConfigService {
onboardingDone: false, onboardingDone: false,
imageXorKey: 0, imageXorKey: 0,
imageAesKey: '', imageAesKey: '',
wxidConfigs: {},
cachePath: '', cachePath: '',
lastOpenedDb: '', lastOpenedDb: '',
lastSession: '', lastSession: '',

View File

@@ -1,6 +1,6 @@
{ {
"name": "weflow", "name": "weflow",
"version": "1.3.1", "version": "1.4.0",
"description": "WeFlow", "description": "WeFlow",
"main": "dist-electron/main.js", "main": "dist-electron/main.js",
"author": "cc", "author": "cc",

View File

@@ -185,9 +185,15 @@ function App() {
const decryptKey = await configService.getDecryptKey() const decryptKey = await configService.getDecryptKey()
const wxid = await configService.getMyWxid() const wxid = await configService.getMyWxid()
const onboardingDone = await configService.getOnboardingDone() const onboardingDone = await configService.getOnboardingDone()
const wxidConfig = wxid ? await configService.getWxidConfig(wxid) : null
const effectiveDecryptKey = wxidConfig?.decryptKey || decryptKey
if (wxidConfig?.decryptKey && wxidConfig.decryptKey !== decryptKey) {
await configService.setDecryptKey(wxidConfig.decryptKey)
}
// 如果配置完整,自动测试连接 // 如果配置完整,自动测试连接
if (dbPath && decryptKey && wxid) { if (dbPath && effectiveDecryptKey && wxid) {
if (!onboardingDone) { if (!onboardingDone) {
await configService.setOnboardingDone(true) await configService.setOnboardingDone(true)
} }

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useCallback } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { Users, Clock, MessageSquare, Send, Inbox, Calendar, Loader2, RefreshCw, User, Medal } from 'lucide-react' import { Users, Clock, MessageSquare, Send, Inbox, Calendar, Loader2, RefreshCw, User, Medal } from 'lucide-react'
import ReactECharts from 'echarts-for-react' import ReactECharts from 'echarts-for-react'
@@ -16,7 +16,7 @@ function AnalyticsPage() {
const themeMode = useThemeStore((state) => state.themeMode) const themeMode = useThemeStore((state) => state.themeMode)
const { statistics, rankings, timeDistribution, isLoaded, setStatistics, setRankings, setTimeDistribution, markLoaded } = useAnalyticsStore() const { statistics, rankings, timeDistribution, isLoaded, setStatistics, setRankings, setTimeDistribution, markLoaded } = useAnalyticsStore()
const loadData = async (forceRefresh = false) => { const loadData = useCallback(async (forceRefresh = false) => {
if (isLoaded && !forceRefresh) return if (isLoaded && !forceRefresh) return
setIsLoading(true) setIsLoading(true)
setError(null) setError(null)
@@ -55,14 +55,22 @@ function AnalyticsPage() {
setIsLoading(false) setIsLoading(false)
if (removeListener) removeListener() if (removeListener) removeListener()
} }
} }, [isLoaded, markLoaded, setRankings, setStatistics, setTimeDistribution])
const location = useLocation() const location = useLocation()
useEffect(() => { useEffect(() => {
const force = location.state?.forceRefresh === true const force = location.state?.forceRefresh === true
loadData(force) loadData(force)
}, [location.state]) }, [location.state, loadData])
useEffect(() => {
const handleChange = () => {
loadData(true)
}
window.addEventListener('wxid-changed', handleChange as EventListener)
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
}, [loadData])
const handleRefresh = () => loadData(true) const handleRefresh = () => loadData(true)

View File

@@ -245,6 +245,38 @@ function ChatPage(_props: ChatPageProps) {
} }
}, [loadMyAvatar]) }, [loadMyAvatar])
const handleAccountChanged = useCallback(async () => {
senderAvatarCache.clear()
senderAvatarLoading.clear()
preloadImageKeysRef.current.clear()
lastPreloadSessionRef.current = null
setSessionDetail(null)
setCurrentSession(null)
setSessions([])
setFilteredSessions([])
setMessages([])
setSearchKeyword('')
setConnectionError(null)
setConnected(false)
setConnecting(false)
setHasMoreMessages(true)
setHasMoreLater(false)
await connect()
}, [
connect,
setConnected,
setConnecting,
setConnectionError,
setCurrentSession,
setFilteredSessions,
setHasMoreLater,
setHasMoreMessages,
setMessages,
setSearchKeyword,
setSessionDetail,
setSessions
])
// 加载会话列表(优化:先返回基础数据,异步加载联系人信息) // 加载会话列表(优化:先返回基础数据,异步加载联系人信息)
const loadSessions = async (options?: { silent?: boolean }) => { const loadSessions = async (options?: { silent?: boolean }) => {
if (options?.silent) { if (options?.silent) {
@@ -842,6 +874,14 @@ function ChatPage(_props: ChatPageProps) {
} }
}, []) }, [])
useEffect(() => {
const handleChange = () => {
void handleAccountChanged()
}
window.addEventListener('wxid-changed', handleChange as EventListener)
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
}, [handleAccountChanged])
useEffect(() => { useEffect(() => {
const nextSet = new Set<string>() const nextSet = new Set<string>()
for (const msg of messages) { for (const msg of messages) {

View File

@@ -16,6 +16,11 @@ function DataManagementPage() {
setWxid(id) setWxid(id)
} }
loadConfig() loadConfig()
const handleChange = () => {
loadConfig()
}
window.addEventListener('wxid-changed', handleChange as EventListener)
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
}, []) }, [])
return ( return (

View File

@@ -157,6 +157,19 @@ function ExportPage() {
loadExportDefaults() loadExportDefaults()
}, [loadSessions, loadExportPath, loadExportDefaults]) }, [loadSessions, loadExportPath, loadExportDefaults])
useEffect(() => {
const handleChange = () => {
setSelectedSessions(new Set())
setSearchKeyword('')
setExportResult(null)
setSessions([])
setFilteredSessions([])
loadSessions()
}
window.addEventListener('wxid-changed', handleChange as EventListener)
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
}, [loadSessions])
useEffect(() => { useEffect(() => {
const removeListener = window.electronAPI.export.onProgress?.((payload) => { const removeListener = window.electronAPI.export.onProgress?.((payload) => {
setExportProgress({ setExportProgress({

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react' import { useState, useEffect, useRef, useCallback } from 'react'
import { Users, BarChart3, Clock, Image, Loader2, RefreshCw, User, Medal, Search, X, ChevronLeft, Copy, Check } from 'lucide-react' import { Users, BarChart3, Clock, Image, Loader2, RefreshCw, User, Medal, Search, X, ChevronLeft, Copy, Check } from 'lucide-react'
import { Avatar } from '../components/Avatar' import { Avatar } from '../components/Avatar'
import ReactECharts from 'echarts-for-react' import ReactECharts from 'echarts-for-react'
@@ -56,7 +56,7 @@ function GroupAnalyticsPage() {
useEffect(() => { useEffect(() => {
loadGroups() loadGroups()
}, []) }, [loadGroups])
useEffect(() => { useEffect(() => {
if (searchQuery) { if (searchQuery) {
@@ -93,7 +93,7 @@ function GroupAnalyticsPage() {
} }
}, [dateRangeReady]) }, [dateRangeReady])
const loadGroups = async () => { const loadGroups = useCallback(async () => {
setIsLoading(true) setIsLoading(true)
try { try {
const result = await window.electronAPI.groupAnalytics.getGroupChats() const result = await window.electronAPI.groupAnalytics.getGroupChats()
@@ -106,7 +106,23 @@ function GroupAnalyticsPage() {
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
} }, [])
useEffect(() => {
const handleChange = () => {
setGroups([])
setFilteredGroups([])
setSelectedGroup(null)
setSelectedFunction(null)
setMembers([])
setRankings([])
setActiveHours({})
setMediaStats(null)
void loadGroups()
}
window.addEventListener('wxid-changed', handleChange as EventListener)
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
}, [loadGroups])
const handleGroupSelect = (group: GroupChatInfo) => { const handleGroupSelect = (group: GroupChatInfo) => {
if (selectedGroup?.username !== group.username) { if (selectedGroup?.username !== group.username) {

View File

@@ -1156,7 +1156,6 @@
input { input {
flex: 1; flex: 1;
padding-right: 36px;
} }
} }

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from 'react' import { useState, useEffect, useRef } from 'react'
import { useAppStore } from '../stores/appStore' import { useAppStore } from '../stores/appStore'
import { useChatStore } from '../stores/chatStore'
import { useThemeStore, themes } from '../stores/themeStore' import { useThemeStore, themes } from '../stores/themeStore'
import { useAnalyticsStore } from '../stores/analyticsStore' import { useAnalyticsStore } from '../stores/analyticsStore'
import { dialog } from '../services/ipc' import { dialog } from '../services/ipc'
@@ -28,7 +29,8 @@ interface WxidOption {
} }
function SettingsPage() { function SettingsPage() {
const { setDbConnected, setLoading, reset } = useAppStore() const { isDbConnected, setDbConnected, setLoading, reset } = useAppStore()
const resetChatStore = useChatStore((state) => state.reset)
const { currentTheme, themeMode, setTheme, setThemeMode } = useThemeStore() const { currentTheme, themeMode, setTheme, setThemeMode } = useThemeStore()
const clearAnalyticsStoreCache = useAnalyticsStore((state) => state.clearCache) const clearAnalyticsStoreCache = useAnalyticsStore((state) => state.clearCache)
@@ -40,7 +42,6 @@ function SettingsPage() {
const [wxid, setWxid] = useState('') const [wxid, setWxid] = useState('')
const [wxidOptions, setWxidOptions] = useState<WxidOption[]>([]) const [wxidOptions, setWxidOptions] = useState<WxidOption[]>([])
const [showWxidSelect, setShowWxidSelect] = useState(false) const [showWxidSelect, setShowWxidSelect] = useState(false)
const wxidDropdownRef = useRef<HTMLDivElement>(null)
const [showExportFormatSelect, setShowExportFormatSelect] = useState(false) const [showExportFormatSelect, setShowExportFormatSelect] = useState(false)
const [showExportDateRangeSelect, setShowExportDateRangeSelect] = useState(false) const [showExportDateRangeSelect, setShowExportDateRangeSelect] = useState(false)
const [showExportExcelColumnsSelect, setShowExportExcelColumnsSelect] = useState(false) const [showExportExcelColumnsSelect, setShowExportExcelColumnsSelect] = useState(false)
@@ -92,9 +93,6 @@ function SettingsPage() {
useEffect(() => { useEffect(() => {
const handleClickOutside = (e: MouseEvent) => { const handleClickOutside = (e: MouseEvent) => {
const target = e.target as Node const target = e.target as Node
if (showWxidSelect && wxidDropdownRef.current && !wxidDropdownRef.current.contains(target)) {
setShowWxidSelect(false)
}
if (showExportFormatSelect && exportFormatDropdownRef.current && !exportFormatDropdownRef.current.contains(target)) { if (showExportFormatSelect && exportFormatDropdownRef.current && !exportFormatDropdownRef.current.contains(target)) {
setShowExportFormatSelect(false) setShowExportFormatSelect(false)
} }
@@ -107,7 +105,7 @@ function SettingsPage() {
} }
document.addEventListener('mousedown', handleClickOutside) document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside)
}, [showWxidSelect, showExportFormatSelect, showExportDateRangeSelect, showExportExcelColumnsSelect]) }, [showExportFormatSelect, showExportDateRangeSelect, showExportExcelColumnsSelect])
useEffect(() => { useEffect(() => {
const removeDb = window.electronAPI.key.onDbKeyStatus((payload) => { const removeDb = window.electronAPI.key.onDbKeyStatus((payload) => {
@@ -142,14 +140,24 @@ function SettingsPage() {
const savedExportDefaultVoiceAsText = await configService.getExportDefaultVoiceAsText() const savedExportDefaultVoiceAsText = await configService.getExportDefaultVoiceAsText()
const savedExportDefaultExcelCompactColumns = await configService.getExportDefaultExcelCompactColumns() const savedExportDefaultExcelCompactColumns = await configService.getExportDefaultExcelCompactColumns()
if (savedKey) setDecryptKey(savedKey)
if (savedPath) setDbPath(savedPath) if (savedPath) setDbPath(savedPath)
if (savedWxid) setWxid(savedWxid) if (savedWxid) setWxid(savedWxid)
if (savedCachePath) setCachePath(savedCachePath) if (savedCachePath) setCachePath(savedCachePath)
if (savedImageXorKey != null) {
setImageXorKey(`0x${savedImageXorKey.toString(16).toUpperCase().padStart(2, '0')}`) const wxidConfig = savedWxid ? await configService.getWxidConfig(savedWxid) : null
const decryptKeyToUse = wxidConfig?.decryptKey ?? savedKey ?? ''
const imageXorKeyToUse = typeof wxidConfig?.imageXorKey === 'number'
? wxidConfig.imageXorKey
: savedImageXorKey
const imageAesKeyToUse = wxidConfig?.imageAesKey ?? savedImageAesKey ?? ''
setDecryptKey(decryptKeyToUse)
if (typeof imageXorKeyToUse === 'number') {
setImageXorKey(`0x${imageXorKeyToUse.toString(16).toUpperCase().padStart(2, '0')}`)
} else {
setImageXorKey('')
} }
if (savedImageAesKey) setImageAesKey(savedImageAesKey) setImageAesKey(imageAesKeyToUse)
setLogEnabled(savedLogEnabled) setLogEnabled(savedLogEnabled)
setAutoTranscribeVoice(savedAutoTranscribe) setAutoTranscribeVoice(savedAutoTranscribe)
setTranscribeLanguages(savedTranscribeLanguages) setTranscribeLanguages(savedTranscribeLanguages)
@@ -254,6 +262,103 @@ function SettingsPage() {
setTimeout(() => setMessage(null), 3000) setTimeout(() => setMessage(null), 3000)
} }
type WxidKeys = {
decryptKey: string
imageXorKey: number | null
imageAesKey: string
}
const formatImageXorKey = (value: number) => `0x${value.toString(16).toUpperCase().padStart(2, '0')}`
const parseImageXorKey = (value: string) => {
if (!value) return null
const parsed = parseInt(value.replace(/^0x/i, ''), 16)
return Number.isNaN(parsed) ? null : parsed
}
const buildKeysFromState = (): WxidKeys => ({
decryptKey: decryptKey || '',
imageXorKey: parseImageXorKey(imageXorKey),
imageAesKey: imageAesKey || ''
})
const buildKeysFromConfig = (wxidConfig: configService.WxidConfig | null): WxidKeys => ({
decryptKey: wxidConfig?.decryptKey || '',
imageXorKey: typeof wxidConfig?.imageXorKey === 'number' ? wxidConfig.imageXorKey : null,
imageAesKey: wxidConfig?.imageAesKey || ''
})
const applyKeysToState = (keys: WxidKeys) => {
setDecryptKey(keys.decryptKey)
if (typeof keys.imageXorKey === 'number') {
setImageXorKey(formatImageXorKey(keys.imageXorKey))
} else {
setImageXorKey('')
}
setImageAesKey(keys.imageAesKey)
}
const syncKeysToConfig = async (keys: WxidKeys) => {
await configService.setDecryptKey(keys.decryptKey)
await configService.setImageXorKey(typeof keys.imageXorKey === 'number' ? keys.imageXorKey : 0)
await configService.setImageAesKey(keys.imageAesKey)
}
const applyWxidSelection = async (
selectedWxid: string,
options?: { preferCurrentKeys?: boolean; showToast?: boolean; toastText?: string }
) => {
if (!selectedWxid) return
const currentWxid = wxid
const isSameWxid = currentWxid === selectedWxid
if (currentWxid && currentWxid !== selectedWxid) {
const currentKeys = buildKeysFromState()
await configService.setWxidConfig(currentWxid, {
decryptKey: currentKeys.decryptKey,
imageXorKey: typeof currentKeys.imageXorKey === 'number' ? currentKeys.imageXorKey : 0,
imageAesKey: currentKeys.imageAesKey
})
}
const preferCurrentKeys = options?.preferCurrentKeys ?? false
const keys = preferCurrentKeys
? buildKeysFromState()
: buildKeysFromConfig(await configService.getWxidConfig(selectedWxid))
setWxid(selectedWxid)
applyKeysToState(keys)
await configService.setMyWxid(selectedWxid)
await syncKeysToConfig(keys)
await configService.setWxidConfig(selectedWxid, {
decryptKey: keys.decryptKey,
imageXorKey: typeof keys.imageXorKey === 'number' ? keys.imageXorKey : 0,
imageAesKey: keys.imageAesKey
})
setShowWxidSelect(false)
if (isDbConnected) {
try {
await window.electronAPI.chat.close()
const result = await window.electronAPI.chat.connect()
setDbConnected(result.success, dbPath || undefined)
if (!result.success && result.error) {
showMessage(result.error, false)
}
} catch (e) {
showMessage(`切换账号后重新连接失败: ${e}`, false)
setDbConnected(false)
}
}
if (!isSameWxid) {
clearAnalyticsStoreCache()
resetChatStore()
window.dispatchEvent(new CustomEvent('wxid-changed', { detail: { wxid: selectedWxid } }))
}
if (options?.showToast ?? true) {
showMessage(options?.toastText || `已选择账号:${selectedWxid}`, true)
}
}
const handleAutoDetectPath = async () => { const handleAutoDetectPath = async () => {
if (isDetectingPath) return if (isDetectingPath) return
setIsDetectingPath(true) setIsDetectingPath(true)
@@ -267,11 +372,10 @@ function SettingsPage() {
const wxids = await window.electronAPI.dbPath.scanWxids(result.path) const wxids = await window.electronAPI.dbPath.scanWxids(result.path)
setWxidOptions(wxids) setWxidOptions(wxids)
if (wxids.length === 1) { if (wxids.length === 1) {
setWxid(wxids[0].wxid) await applyWxidSelection(wxids[0].wxid, {
await configService.setMyWxid(wxids[0].wxid) toastText: `已检测到账号:${wxids[0].wxid}`
showMessage(`已检测到账号:${wxids[0].wxid}`, true) })
} else if (wxids.length > 1) { } else if (wxids.length > 1) {
// 多账号时弹出选择对话框
setShowWxidSelect(true) setShowWxidSelect(true)
} }
} else { } else {
@@ -296,7 +400,10 @@ function SettingsPage() {
} }
} }
const handleScanWxid = async (silent = false) => { const handleScanWxid = async (
silent = false,
options?: { preferCurrentKeys?: boolean; showDialog?: boolean }
) => {
if (!dbPath) { if (!dbPath) {
if (!silent) showMessage('请先选择数据库目录', false) if (!silent) showMessage('请先选择数据库目录', false)
return return
@@ -304,12 +411,14 @@ function SettingsPage() {
try { try {
const wxids = await window.electronAPI.dbPath.scanWxids(dbPath) const wxids = await window.electronAPI.dbPath.scanWxids(dbPath)
setWxidOptions(wxids) setWxidOptions(wxids)
const allowDialog = options?.showDialog ?? !silent
if (wxids.length === 1) { if (wxids.length === 1) {
setWxid(wxids[0].wxid) await applyWxidSelection(wxids[0].wxid, {
await configService.setMyWxid(wxids[0].wxid) preferCurrentKeys: options?.preferCurrentKeys ?? false,
if (!silent) showMessage(`已检测到账号:${wxids[0].wxid}`, true) showToast: !silent,
} else if (wxids.length > 1) { toastText: `已检测到账号:${wxids[0].wxid}`
// 多账号时弹出选择对话框 })
} else if (wxids.length > 1 && allowDialog) {
setShowWxidSelect(true) setShowWxidSelect(true)
} else { } else {
if (!silent) showMessage('未检测到账号目录,请检查路径', false) if (!silent) showMessage('未检测到账号目录,请检查路径', false)
@@ -320,10 +429,7 @@ function SettingsPage() {
} }
const handleSelectWxid = async (selectedWxid: string) => { const handleSelectWxid = async (selectedWxid: string) => {
setWxid(selectedWxid) await applyWxidSelection(selectedWxid)
await configService.setMyWxid(selectedWxid)
setShowWxidSelect(false)
showMessage(`已选择账号:${selectedWxid}`, true)
} }
const handleSelectCachePath = async () => { const handleSelectCachePath = async () => {
@@ -396,7 +502,7 @@ function SettingsPage() {
setDecryptKey(result.key) setDecryptKey(result.key)
setDbKeyStatus('密钥获取成功') setDbKeyStatus('密钥获取成功')
showMessage('已自动获取解密密钥', true) showMessage('已自动获取解密密钥', true)
await handleScanWxid(true) await handleScanWxid(true, { preferCurrentKeys: true, showDialog: false })
} else { } else {
if (result.error?.includes('未找到微信安装路径') || result.error?.includes('启动微信失败')) { if (result.error?.includes('未找到微信安装路径') || result.error?.includes('启动微信失败')) {
setIsManualStartPrompt(true) setIsManualStartPrompt(true)
@@ -482,19 +588,14 @@ function SettingsPage() {
await configService.setDbPath(dbPath) await configService.setDbPath(dbPath)
await configService.setMyWxid(wxid) await configService.setMyWxid(wxid)
await configService.setCachePath(cachePath) await configService.setCachePath(cachePath)
if (imageXorKey) { const parsedXorKey = parseImageXorKey(imageXorKey)
const parsed = parseInt(imageXorKey.replace(/^0x/i, ''), 16) await configService.setImageXorKey(typeof parsedXorKey === 'number' ? parsedXorKey : 0)
if (!Number.isNaN(parsed)) { await configService.setImageAesKey(imageAesKey || '')
await configService.setImageXorKey(parsed) await configService.setWxidConfig(wxid, {
} decryptKey,
} else { imageXorKey: typeof parsedXorKey === 'number' ? parsedXorKey : 0,
await configService.setImageXorKey(0) imageAesKey
} })
if (imageAesKey) {
await configService.setImageAesKey(imageAesKey)
} else {
await configService.setImageAesKey('')
}
await configService.setWhisperModelDir(whisperModelDir) await configService.setWhisperModelDir(whisperModelDir)
await configService.setAutoTranscribeVoice(autoTranscribeVoice) await configService.setAutoTranscribeVoice(autoTranscribeVoice)
await configService.setTranscribeLanguages(transcribeLanguages) await configService.setTranscribeLanguages(transcribeLanguages)
@@ -687,37 +788,13 @@ function SettingsPage() {
<div className="form-group"> <div className="form-group">
<label> wxid</label> <label> wxid</label>
<span className="form-hint"></span> <span className="form-hint"></span>
<div className="wxid-input-wrapper" ref={wxidDropdownRef}> <div className="wxid-input-wrapper">
<input <input
type="text" type="text"
placeholder="例如: wxid_xxxxxx" placeholder="例如: wxid_xxxxxx"
value={wxid} value={wxid}
onChange={(e) => setWxid(e.target.value)} onChange={(e) => setWxid(e.target.value)}
/> />
<button
type="button"
className={`wxid-dropdown-btn ${showWxidSelect ? 'open' : ''}`}
onClick={() => wxidOptions.length > 0 ? setShowWxidSelect(!showWxidSelect) : handleScanWxid()}
title={wxidOptions.length > 0 ? "选择已检测到的账号" : "扫描账号"}
>
<ChevronDown size={16} />
</button>
{showWxidSelect && wxidOptions.length > 0 && (
<div className="wxid-dropdown">
{wxidOptions.map((opt) => (
<div
key={opt.wxid}
className={`wxid-option ${opt.wxid === wxid ? 'active' : ''}`}
onClick={() => handleSelectWxid(opt.wxid)}
>
<span className="wxid-value">{opt.wxid}</span>
<span className="wxid-time">
{new Date(opt.modifiedTime).toLocaleDateString()}
</span>
</div>
))}
</div>
)}
</div> </div>
<button className="btn btn-secondary btn-sm" onClick={() => handleScanWxid()}><Search size={14} /> wxid</button> <button className="btn btn-secondary btn-sm" onClick={() => handleScanWxid()}><Search size={14} /> wxid</button>
</div> </div>

View File

@@ -111,7 +111,7 @@ export default function SnsPage() {
}, [offset, selectedUsernames, searchKeyword, startDate, endDate]) }, [offset, selectedUsernames, searchKeyword, startDate, endDate])
// 获取联系人列表 // 获取联系人列表
const loadContacts = async () => { const loadContacts = useCallback(async () => {
setContactsLoading(true) setContactsLoading(true)
try { try {
const result = await window.electronAPI.chat.getSessions() const result = await window.electronAPI.chat.getSessions()
@@ -171,11 +171,26 @@ export default function SnsPage() {
} finally { } finally {
setContactsLoading(false) setContactsLoading(false)
} }
} }, [])
useEffect(() => { useEffect(() => {
loadContacts() loadContacts()
}, []) }, [loadContacts])
useEffect(() => {
const handleChange = () => {
setPosts([])
setHasMore(true)
setHasNewer(false)
setSelectedUsernames([])
setSearchKeyword('')
setJumpTargetDate(null)
loadContacts()
loadPosts({ reset: true })
}
window.addEventListener('wxid-changed', handleChange as EventListener)
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
}, [loadContacts, loadPosts])
useEffect(() => { useEffect(() => {
loadPosts(true) loadPosts(true)

View File

@@ -269,15 +269,14 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
await configService.setDecryptKey(decryptKey) await configService.setDecryptKey(decryptKey)
await configService.setMyWxid(wxid) await configService.setMyWxid(wxid)
await configService.setCachePath(cachePath) await configService.setCachePath(cachePath)
if (imageXorKey) { const parsedXorKey = imageXorKey ? parseInt(imageXorKey.replace(/^0x/i, ''), 16) : null
const parsed = parseInt(imageXorKey.replace(/^0x/i, ''), 16) await configService.setImageXorKey(typeof parsedXorKey === 'number' && !Number.isNaN(parsedXorKey) ? parsedXorKey : 0)
if (!Number.isNaN(parsed)) { await configService.setImageAesKey(imageAesKey || '')
await configService.setImageXorKey(parsed) await configService.setWxidConfig(wxid, {
} decryptKey,
} imageXorKey: typeof parsedXorKey === 'number' && !Number.isNaN(parsedXorKey) ? parsedXorKey : 0,
if (imageAesKey) { imageAesKey
await configService.setImageAesKey(imageAesKey) })
}
await configService.setOnboardingDone(true) await configService.setOnboardingDone(true)
setDbConnected(true, dbPath) setDbConnected(true, dbPath)

View File

@@ -6,6 +6,7 @@ export const CONFIG_KEYS = {
DECRYPT_KEY: 'decryptKey', DECRYPT_KEY: 'decryptKey',
DB_PATH: 'dbPath', DB_PATH: 'dbPath',
MY_WXID: 'myWxid', MY_WXID: 'myWxid',
WXID_CONFIGS: 'wxidConfigs',
THEME: 'theme', THEME: 'theme',
THEME_ID: 'themeId', THEME_ID: 'themeId',
LAST_SESSION: 'lastSession', LAST_SESSION: 'lastSession',
@@ -30,6 +31,13 @@ export const CONFIG_KEYS = {
EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns' EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns'
} as const } as const
export interface WxidConfig {
decryptKey?: string
imageXorKey?: number
imageAesKey?: string
updatedAt?: number
}
// 获取解密密钥 // 获取解密密钥
export async function getDecryptKey(): Promise<string | null> { export async function getDecryptKey(): Promise<string | null> {
const value = await config.get(CONFIG_KEYS.DECRYPT_KEY) const value = await config.get(CONFIG_KEYS.DECRYPT_KEY)
@@ -63,6 +71,32 @@ export async function setMyWxid(wxid: string): Promise<void> {
await config.set(CONFIG_KEYS.MY_WXID, wxid) await config.set(CONFIG_KEYS.MY_WXID, wxid)
} }
export async function getWxidConfigs(): Promise<Record<string, WxidConfig>> {
const value = await config.get(CONFIG_KEYS.WXID_CONFIGS)
if (value && typeof value === 'object') {
return value as Record<string, WxidConfig>
}
return {}
}
export async function getWxidConfig(wxid: string): Promise<WxidConfig | null> {
if (!wxid) return null
const configs = await getWxidConfigs()
return configs[wxid] || null
}
export async function setWxidConfig(wxid: string, configValue: WxidConfig): Promise<void> {
if (!wxid) return
const configs = await getWxidConfigs()
const previous = configs[wxid] || {}
configs[wxid] = {
...previous,
...configValue,
updatedAt: Date.now()
}
await config.set(CONFIG_KEYS.WXID_CONFIGS, configs)
}
// 获取主题 // 获取主题
export async function getTheme(): Promise<'light' | 'dark'> { export async function getTheme(): Promise<'light' | 'dark'> {
const value = await config.get(CONFIG_KEYS.THEME) const value = await config.get(CONFIG_KEYS.THEME)