diff --git a/src/main/index.ts b/src/main/index.ts index 8b99f78..7a33d62 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -486,6 +486,7 @@ app.whenReady().then(async () => { if (dbInitInFlight) return dbInitInFlight dbInitInFlight = (async () => { + const startedAt = Date.now() try { if (wcdbBootstrapPromise) await wcdbBootstrapPromise const trimmedKey = String(key || '').trim() @@ -511,7 +512,7 @@ app.whenReady().then(async () => { } chat.setChatDb(nextWechatDb) const wcdb4Client = nextWechatDb.getWcdb4Client() - const sessions = await wcdb4Client.getSessionsAsync() + const sessions = await wcdb4Client.getSessionsAsync({ hydrateDisplayNames: false }) configureRecallProtection(wcdb4Client, resolvedRoot, settings.recallProtectionEnabled) voiceService = new VoiceService(wcdb4Client) stickerService = new StickerService(wcdb4Client) @@ -530,6 +531,9 @@ app.whenReady().then(async () => { .catch((error) => console.warn('[WCDB4] message cursor warmup failed:', error)) } imageDecryptService = null + console.log( + `[WCDB4] db:init ready sessions=${sessions.length} monitoring=${monitoring} cost=${Date.now() - startedAt}ms` + ) return { success: true, monitoring } } catch (error) { console.error('Failed to init DB:', error) diff --git a/src/main/services/chat-service.ts b/src/main/services/chat-service.ts index 12420a8..ecd5e68 100644 --- a/src/main/services/chat-service.ts +++ b/src/main/services/chat-service.ts @@ -174,7 +174,7 @@ export function listContacts(filter?: string): FormattedContact[] { export async function listContactsAsync(filter?: string): Promise { if (!dbRef) return [] - await dbRef.getWcdb4Client().getSessionsAsync() + await dbRef.getWcdb4Client().getSessionsAsync({ hydrateDisplayNames: false }) return listContacts(filter) } diff --git a/src/main/wcdb4-client.ts b/src/main/wcdb4-client.ts index 9d6ba01..a48c863 100644 --- a/src/main/wcdb4-client.ts +++ b/src/main/wcdb4-client.ts @@ -32,6 +32,10 @@ export interface Wcdb4MessageQueryOptions { limit?: number } +export interface Wcdb4SessionQueryOptions { + hydrateDisplayNames?: boolean +} + type Wcdb4MessageStore = { tableName: string dbPath: string @@ -242,6 +246,8 @@ export class Wcdb4Client { private cachedSessions: Wcdb4Session[] | null = null private cachedChatTables: { name: string; db_number: string }[] | null = null private sessionsInFlight: Promise | null = null + private sessionDisplayNamesInFlight: Promise | null = null + private sessionDisplayNamesHydrated = false private sessionCacheGeneration = 0 private wcdbShutdown: (() => number) | null = null @@ -558,6 +564,7 @@ export class Wcdb4Client { this.handle = null this.cachedSessions = null + this.sessionDisplayNamesHydrated = false this.displayNameCache.clear() this.avatarCache.clear() this.groupNicknameCache.clear() @@ -722,13 +729,22 @@ export class Wcdb4Client { ...session, nickname: this.displayNameCache.get(session.username) || session.nickname || session.username })) + this.sessionDisplayNamesHydrated = true return this.cachedSessions } - async getSessionsAsync(): Promise { - if (this.cachedSessions) return this.cachedSessions - if (this.sessionsInFlight) return this.sessionsInFlight + async getSessionsAsync(options: Wcdb4SessionQueryOptions = {}): Promise { + const hydrateDisplayNames = options.hydrateDisplayNames !== false + if (this.cachedSessions) { + if (hydrateDisplayNames) await this.ensureSessionDisplayNamesAsync() + return this.cachedSessions + } + if (this.sessionsInFlight) { + await this.sessionsInFlight + if (hydrateDisplayNames) await this.ensureSessionDisplayNamesAsync() + return this.cachedSessions || [] + } if (!this.wcdbGetSessions) return [] const generation = this.sessionCacheGeneration @@ -739,25 +755,17 @@ export class Wcdb4Client { const sessions = (Array.isArray(rows) ? rows : []) .map((row) => this.normalizeSession(row)) .filter((session) => session.username) - await this.hydrateDisplayNamesAsync( - sessions - .filter((session) => this.shouldHydrateSessionDisplayName(session)) - .map((session) => session.username) - ) - const hydrated = sessions.map((session) => ({ - ...session, - nickname: - this.displayNameCache.get(session.username) || session.nickname || session.username - })) - if (generation === this.sessionCacheGeneration) this.cachedSessions = hydrated - return hydrated + if (generation === this.sessionCacheGeneration) this.cachedSessions = sessions + return sessions })() this.sessionsInFlight = request try { - return await request + await request } finally { if (this.sessionsInFlight === request) this.sessionsInFlight = null } + if (hydrateDisplayNames) await this.ensureSessionDisplayNamesAsync() + return this.cachedSessions || [] } invalidateSessionCache(): void { @@ -765,6 +773,7 @@ export class Wcdb4Client { this.cachedSessions = null this.cachedChatTables = null this.sessionsInFlight = null + this.sessionDisplayNamesHydrated = false } getChatTables(): { name: string; db_number: string }[] { @@ -2214,6 +2223,34 @@ export class Wcdb4Client { } } + private async ensureSessionDisplayNamesAsync(): Promise { + if (this.sessionDisplayNamesHydrated || !this.cachedSessions) return + if (this.sessionDisplayNamesInFlight) return this.sessionDisplayNamesInFlight + + const generation = this.sessionCacheGeneration + const sessions = this.cachedSessions + const request = (async (): Promise => { + await this.hydrateDisplayNamesAsync( + sessions + .filter((session) => this.shouldHydrateSessionDisplayName(session)) + .map((session) => session.username) + ) + if (generation !== this.sessionCacheGeneration || this.cachedSessions !== sessions) return + this.cachedSessions = sessions.map((session) => ({ + ...session, + nickname: + this.displayNameCache.get(session.username) || session.nickname || session.username + })) + this.sessionDisplayNamesHydrated = true + })() + this.sessionDisplayNamesInFlight = request + try { + await request + } finally { + if (this.sessionDisplayNamesInFlight === request) this.sessionDisplayNamesInFlight = null + } + } + private hydrateAvatarUrls(usernames: string[]): void { const missing = this.uniq(usernames).filter((username) => !this.avatarCache.has(username)) if (missing.length === 0) return diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 2aa7772..e57df39 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -210,6 +210,7 @@ const mergeMessagePages = (older: Message[], current: Message[]): Message[] => { function App(): React.ReactElement { const [isAuthenticated, setIsAuthenticated] = useState(false) const [isDatabaseConnected, setIsDatabaseConnected] = useState(false) + const [isDatabaseConnecting, setIsDatabaseConnecting] = useState(false) const [dbKey, setDbKey] = useState(getDevelopmentDatabaseKey) const [contacts, setContacts] = useState([]) const [selectedContact, setSelectedContact] = useState(null) @@ -582,6 +583,7 @@ function App(): React.ReactElement { if (!autoLoginEnabled) return try { const startupCacheReady = await loadStartupCache() + setIsDatabaseConnecting(true) const initPromise = window.api.initDb(key) if (startupCacheReady) { setIsAuthenticated(true) @@ -606,6 +608,9 @@ function App(): React.ReactElement { console.warn('[Startup] background database init failed:', error) setDbKeyStatusKind('error') }) + .finally(() => { + if (active) setIsDatabaseConnecting(false) + }) return } const result = await initPromise @@ -638,6 +643,8 @@ function App(): React.ReactElement { setDbKeyStatus(`自动连接失败: ${message}`) setDbKeyStatusKind('error') setBootState('login') + } finally { + if (active) setIsDatabaseConnecting(false) } } void attemptAutoConnect() @@ -663,6 +670,7 @@ function App(): React.ReactElement { const keyToUse = keyInput || dbKey if (!keyToUse) return setBootState('connecting') + setIsDatabaseConnecting(true) // 持久化用户手动指定的微信聊天文件路径,供 db:init 读取 settings.dbRoot const trimmedRoot = dbRootInput.trim() if (trimmedRoot) { @@ -746,6 +754,8 @@ function App(): React.ReactElement { setBootState('login') setStartupProgress(null) alert('Error connecting to database') + } finally { + setIsDatabaseConnecting(false) } } @@ -918,6 +928,7 @@ function App(): React.ReactElement { const handleReturnToLogin = (): void => { setIsAuthenticated(false) setIsDatabaseConnected(false) + setIsDatabaseConnecting(false) setBootState('login') setDatabaseConnectionMode(dbKey ? 'manual' : 'automatic') setActivePage('archive') @@ -1425,6 +1436,7 @@ function App(): React.ReactElement { width={sidebarWidth} selfInfo={selfInfo} dbReady={isDatabaseConnected} + dbConnecting={isDatabaseConnecting} onOpenSettings={openSettings} />
@@ -1454,6 +1466,7 @@ function App(): React.ReactElement { selectedReportId={selectedReportId} selfInfo={selfInfo} dbReady={isDatabaseConnected} + dbConnecting={isDatabaseConnecting} onSelectReport={openReport} onCreateReport={openReportConfigure} onDeleteReport={handleDeleteReport} @@ -1476,6 +1489,7 @@ function App(): React.ReactElement { selectedContact={reportSourceContact} selfInfo={selfInfo} dbReady={isDatabaseConnected} + dbConnecting={isDatabaseConnecting} onSelectContact={handleSelectReportSource} onOpenSettings={openSettings} /> @@ -1546,6 +1560,7 @@ function App(): React.ReactElement { onCategoryChange={setSettingsCategory} selfInfo={selfInfo} dbReady={isDatabaseConnected} + dbConnecting={isDatabaseConnecting} dbKey={dbKey} onDbKeyChange={setDbKey} onDatabaseConnectionChange={setIsDatabaseConnected} @@ -1692,6 +1707,7 @@ function App(): React.ReactElement { activePage={activePage} selfInfo={selfInfo} dbReady={isDatabaseConnected} + dbConnecting={isDatabaseConnecting} onPageChange={handlePageChange} onOpenSettings={openSettings} onOpenGuide={openFirstUseGuide} diff --git a/src/renderer/src/components/account/AccountSummary.tsx b/src/renderer/src/components/account/AccountSummary.tsx index 3f121c5..06d905d 100644 --- a/src/renderer/src/components/account/AccountSummary.tsx +++ b/src/renderer/src/components/account/AccountSummary.tsx @@ -10,6 +10,7 @@ interface SelfInfo { interface AccountSummaryProps { selfInfo: SelfInfo | null dbReady: boolean + dbConnecting?: boolean compact?: boolean onClick?: () => void } @@ -17,13 +18,20 @@ interface AccountSummaryProps { export function AccountSummary({ selfInfo, dbReady, + dbConnecting = false, compact = false, onClick }: AccountSummaryProps): React.ReactElement { + const showAccount = Boolean(selfInfo && (dbReady || dbConnecting)) const displayName = - dbReady && selfInfo ? selfInfo.nickname || selfInfo.wxid || '当前账号' : '未连接' - const subtitle = dbReady && selfInfo ? selfInfo.wxid : '打开设置' - const statusText = dbReady ? '数据库已连接' : '数据库未连接' + showAccount && selfInfo ? selfInfo.nickname || selfInfo.wxid || '当前账号' : '未连接' + const subtitle = showAccount && selfInfo ? selfInfo.wxid : '打开设置' + const statusText = dbReady + ? '数据库已连接' + : dbConnecting + ? '正在连接数据库' + : '数据库未连接' + const statusClass = dbReady ? 'ready' : dbConnecting ? 'connecting' : '' const initial = (displayName || '?').charAt(0) const title = `${displayName}\n${subtitle}` const avatar = ( @@ -33,7 +41,7 @@ export function AccountSummary({ ) : ( initial )} - + ) @@ -52,7 +60,7 @@ export function AccountSummary({ {displayName} {subtitle} - + {statusText} diff --git a/src/renderer/src/components/conversation/ConversationSidebar.tsx b/src/renderer/src/components/conversation/ConversationSidebar.tsx index c55f116..c870794 100644 --- a/src/renderer/src/components/conversation/ConversationSidebar.tsx +++ b/src/renderer/src/components/conversation/ConversationSidebar.tsx @@ -21,6 +21,7 @@ export interface ConversationSidebarProps { width: number selfInfo: SelfInfo | null dbReady: boolean + dbConnecting?: boolean onOpenSettings: () => void } @@ -37,6 +38,7 @@ export function ConversationSidebar({ width, selfInfo, dbReady, + dbConnecting = false, onOpenSettings }: ConversationSidebarProps): React.ReactElement { const [searchTerm, setSearchTerm] = useState('') @@ -124,7 +126,12 @@ export function ConversationSidebar({
- +
) diff --git a/src/renderer/src/components/layout/AppShell.tsx b/src/renderer/src/components/layout/AppShell.tsx index 46fddc0..af4e284 100644 --- a/src/renderer/src/components/layout/AppShell.tsx +++ b/src/renderer/src/components/layout/AppShell.tsx @@ -15,6 +15,7 @@ interface AppShellProps { activePage: AppPage selfInfo: SelfInfo | null dbReady: boolean + dbConnecting?: boolean onPageChange: (page: AppPage) => void onOpenSettings: () => void onOpenGuide: () => void @@ -35,6 +36,7 @@ export function AppShell({ activePage, selfInfo, dbReady, + dbConnecting = false, onPageChange, onOpenSettings, onOpenGuide, @@ -63,7 +65,13 @@ export function AppShell({ 新手引导
- +
diff --git a/src/renderer/src/components/reports/ReportHistorySidebar.tsx b/src/renderer/src/components/reports/ReportHistorySidebar.tsx index a87f23f..2aaaca4 100644 --- a/src/renderer/src/components/reports/ReportHistorySidebar.tsx +++ b/src/renderer/src/components/reports/ReportHistorySidebar.tsx @@ -14,6 +14,7 @@ interface ReportHistorySidebarProps { selectedReportId: string | null selfInfo: SelfInfo | null dbReady: boolean + dbConnecting?: boolean onSelectReport: (reportId: string) => void onCreateReport: () => void onDeleteReport: (reportId: string) => Promise<{ success: boolean; error?: string }> @@ -80,6 +81,7 @@ export function ReportHistorySidebar({ selectedReportId, selfInfo, dbReady, + dbConnecting = false, onSelectReport, onCreateReport, onDeleteReport, @@ -202,7 +204,12 @@ export function ReportHistorySidebar({ )}
- +
{pendingDelete && (
diff --git a/src/renderer/src/components/reports/ReportSourceSidebar.tsx b/src/renderer/src/components/reports/ReportSourceSidebar.tsx index 1a47b32..b041d10 100644 --- a/src/renderer/src/components/reports/ReportSourceSidebar.tsx +++ b/src/renderer/src/components/reports/ReportSourceSidebar.tsx @@ -14,6 +14,7 @@ interface ReportSourceSidebarProps { selectedContact: Contact | null selfInfo: SelfInfo | null dbReady: boolean + dbConnecting?: boolean onSelectContact: (contact: Contact) => void onOpenSettings: () => void } @@ -23,6 +24,7 @@ export function ReportSourceSidebar({ selectedContact, selfInfo, dbReady, + dbConnecting = false, onSelectContact, onOpenSettings }: ReportSourceSidebarProps): React.ReactElement { @@ -95,6 +97,7 @@ export function ReportSourceSidebar({
diff --git a/src/renderer/src/features/settings/SettingsWorkspace.tsx b/src/renderer/src/features/settings/SettingsWorkspace.tsx index 9554370..ebdd020 100644 --- a/src/renderer/src/features/settings/SettingsWorkspace.tsx +++ b/src/renderer/src/features/settings/SettingsWorkspace.tsx @@ -19,6 +19,7 @@ export function SettingsWorkspace({ onCategoryChange, selfInfo, dbReady, + dbConnecting = false, dbKey, onDbKeyChange, onDatabaseConnectionChange, @@ -35,6 +36,7 @@ export function SettingsWorkspace({ onCategoryChange: (id: SettingsCategoryId) => void selfInfo: SettingsSelfInfo | null dbReady: boolean + dbConnecting?: boolean dbKey: string onDbKeyChange: (key: string) => void onDatabaseConnectionChange: (connected: boolean) => void @@ -47,6 +49,54 @@ export function SettingsWorkspace({ onOpenSettings: () => void onAppearanceChange: (settings: { theme: 'system' | 'light' | 'dark'; compactMode: boolean }) => void }): React.ReactElement { + const renderSelectedPage = (): React.ReactElement => { + switch (selectedCategory) { + case 'account-database': + return ( + + ) + case 'database-key': + return ( + + ) + case 'image-key': + return + case 'ai-model': + return + case 'recall-protection': + return + case 'advanced': + return + case 'cache-cleanup': + return + case 'appearance': + return ( + + ) + case 'about': + return + default: + return + } + } + return (
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
- {![ - 'account-database', - 'database-key', - 'image-key', - 'ai-model', - 'recall-protection', - 'advanced', - 'cache-cleanup', - 'appearance', - 'about' - ].includes(selectedCategory) && ( -
- -
- )} +
{renderSelectedPage()}
) } diff --git a/src/renderer/src/features/settings/account-database/useAccountDatabaseController.ts b/src/renderer/src/features/settings/account-database/useAccountDatabaseController.ts index 739955a..6365ba3 100644 --- a/src/renderer/src/features/settings/account-database/useAccountDatabaseController.ts +++ b/src/renderer/src/features/settings/account-database/useAccountDatabaseController.ts @@ -13,11 +13,13 @@ import type { ConnectionCheckState } from './types' export function useAccountDatabaseController({ dbKey, dbReady, + dbConnecting = false, selfInfo, onNotice }: { dbKey: string dbReady: boolean + dbConnecting?: boolean selfInfo: SettingsSelfInfo | null onNotice: (message: string) => void }) { @@ -28,7 +30,7 @@ export function useAccountDatabaseController({ useEffect(() => { let active = true void window.api - .getImageDecryptionStatus() + .getImageKeyConfig() .then((result) => { if (active) setHasImageKey(result.configured) }) @@ -58,8 +60,11 @@ export function useAccountDatabaseController({ [checkState, dbKey, dbReady, hasImageKey, selfInfo] ) const connectionStatus = useMemo( - () => getConnectionOverviewStatus({ dbReady, checkState, diagnostics }), - [checkState, dbReady, diagnostics] + () => + dbConnecting + ? ('checking' as const) + : getConnectionOverviewStatus({ dbReady, checkState, diagnostics }), + [checkState, dbConnecting, dbReady, diagnostics] ) const lastCheckedLabel = formatConnectionCheckedAt(checkState, clock) @@ -121,7 +126,7 @@ export function useAccountDatabaseController({ diagnostics, connectionStatus, checkState, - isChecking: checkState.status === 'checking', + isChecking: dbConnecting || checkState.status === 'checking', lastCheckedLabel, testConnection, openAccountDirectory, diff --git a/src/renderer/src/features/settings/components/SettingsSidebar.tsx b/src/renderer/src/features/settings/components/SettingsSidebar.tsx index 7c90c99..24da1ee 100644 --- a/src/renderer/src/features/settings/components/SettingsSidebar.tsx +++ b/src/renderer/src/features/settings/components/SettingsSidebar.tsx @@ -8,12 +8,14 @@ export function SettingsSidebar({ onSelect, selfInfo, dbReady, + dbConnecting = false, onOpenSettings }: { selectedId: SettingsCategoryId onSelect: (id: SettingsCategoryId) => void selfInfo: SettingsSelfInfo | null dbReady: boolean + dbConnecting?: boolean onOpenSettings: () => void }): React.ReactElement { const [keyword, setKeyword] = useState('') @@ -57,7 +59,12 @@ export function SettingsSidebar({ ))}
- +
) diff --git a/src/renderer/src/features/settings/pages/AccountDatabasePage.tsx b/src/renderer/src/features/settings/pages/AccountDatabasePage.tsx index 0eaed00..a974662 100644 --- a/src/renderer/src/features/settings/pages/AccountDatabasePage.tsx +++ b/src/renderer/src/features/settings/pages/AccountDatabasePage.tsx @@ -17,15 +17,23 @@ const STATUS_LABELS: Record = { export function AccountDatabasePage({ dbKey, dbReady, + dbConnecting = false, selfInfo, onNotice }: { dbKey: string dbReady: boolean + dbConnecting?: boolean selfInfo: SettingsSelfInfo | null onNotice: (message: string) => void }): React.ReactElement { - const controller = useAccountDatabaseController({ dbKey, dbReady, selfInfo, onNotice }) + const controller = useAccountDatabaseController({ + dbKey, + dbReady, + dbConnecting, + selfInfo, + onNotice + }) const [autoLogin, setAutoLogin] = useState(false) useEffect(() => { diff --git a/src/renderer/src/styles/foundation.scss b/src/renderer/src/styles/foundation.scss index 161dc5a..dddc6e7 100644 --- a/src/renderer/src/styles/foundation.scss +++ b/src/renderer/src/styles/foundation.scss @@ -253,6 +253,10 @@ body { &.ready { background: var(--wxex-success); } + + &.connecting { + background: var(--wxex-brand); + } } .account-summary:not(.compact) .account-summary-avatar .account-summary-status { @@ -306,6 +310,10 @@ body { &.ready { background: var(--wxex-success); } + + &.connecting { + background: var(--wxex-brand); + } } .account-summary-settings {