diff --git a/src/main/key-service-mac.ts b/src/main/key-service-mac.ts index 11a6d56..9664899 100644 --- a/src/main/key-service-mac.ts +++ b/src/main/key-service-mac.ts @@ -5,6 +5,7 @@ import path from 'path' import { promisify } from 'util' import { isValidDatabaseKey } from './database-key-store' import crypto from 'crypto' +import { findResource, getResourceCandidates } from './resource-paths' const execFileAsync = promisify(execFile) @@ -25,33 +26,10 @@ export interface ImageKeyResult { export class KeyServiceMac { private getHelperPath(): string { - // 多 candidate fallback:覆盖 extraResources、asarUnpack、dev 三种场景 - // (extraResources → Contents/Resources/resources/;asarUnpack 同路径;dev → cwd 或 app.getAppPath) - const candidates = [ - // 1) extraResources 标准位置(electron-builder.yml 配的就是这个) - path.join(process.resourcesPath, 'resources', 'xkey_helper'), - // 2) process.resourcesPath 直接(防止 extraResources 没复制成功) - path.join(process.resourcesPath, 'xkey_helper'), - // 3) asarUnpack 路径(如果在 asar 内的 resources/ 被解包到 app.asar.unpacked) - path.join(app.getAppPath(), 'app.asar.unpacked', 'resources', 'xkey_helper'), - // 4) dev 模式 + 打包后某些版本 app.getAppPath() 也指向 .app 根目录 - path.join(app.getAppPath(), 'resources', 'xkey_helper'), - // 5) dev 模式:cwd - path.join(process.cwd(), 'resources', 'xkey_helper') - ].filter((p, idx, arr) => arr.indexOf(p) === idx) // 去重 - - // 诊断:即使命中也打 log,方便排查"装了但找不到"的问题(translocation / quarantine) - const statusList = candidates.map((candidate) => ({ - path: candidate, - exists: fs.existsSync(candidate) - })) - console.log('[KeyServiceMac] xkey_helper candidates:', JSON.stringify(statusList)) - const helperPath = candidates.find((candidate) => fs.existsSync(candidate)) + const helperPath = findResource('xkey_helper') if (!helperPath) { throw new Error( - `找不到 xkey_helper(尝试 ${candidates.length} 个路径;` + - ` app.isPackaged=${app.isPackaged} resourcesPath=${process.resourcesPath} ` + - ` appPath=${app.getAppPath()} cwd=${process.cwd()})` + `找不到 xkey_helper(已检查:${getResourceCandidates('xkey_helper').join(';')})` ) } return helperPath @@ -163,7 +141,7 @@ export class KeyServiceMac { 'return "ERR::" & errNum & "::" & errMsg', 'end try' ] - onStatus?.('授权后请保持微信已登录并活动...') + onStatus?.('授权后请保持微信登录界面 并点击登录微信...') const { stdout } = await execFileAsync( '/usr/bin/osascript', scriptLines.flatMap((line) => ['-e', line]), @@ -207,7 +185,8 @@ export class KeyServiceMac { const orderedWxids: string[] = [] this.pushAccountIdCandidates(orderedWxids, path.basename(candidateAccountPath)) - for (const candidate of wxidCandidates) this.pushAccountIdCandidates(orderedWxids, candidate) + for (const candidate of wxidCandidates) + this.pushAccountIdCandidates(orderedWxids, candidate) onStatus?.(`正在校验候选 wxid(${orderedWxids.length} 个)...`) for (const candidateWxid of orderedWxids) { @@ -255,20 +234,33 @@ export class KeyServiceMac { private getKvcommCandidates(accountPath?: string): string[] { const home = app.getPath('home') const candidates = new Set([ - path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Documents/app_data/net/kvcomm'), - path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/xwechat/net/kvcomm'), - path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/net/kvcomm'), + path.join( + home, + 'Library/Containers/com.tencent.xinWeChat/Data/Documents/app_data/net/kvcomm' + ), + path.join( + home, + 'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/xwechat/net/kvcomm' + ), + path.join( + home, + 'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/net/kvcomm' + ), path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat/net/kvcomm') ]) - const normalized = String(accountPath || '').replace(/\\/g, '/').replace(/\/+$/, '') + const normalized = String(accountPath || '') + .replace(/\\/g, '/') + .replace(/\/+$/, '') const marker = '/xwechat_files' const markerIndex = normalized.indexOf(marker) if (markerIndex >= 0) { candidates.add(`${normalized.slice(0, markerIndex)}/app_data/net/kvcomm`) } - const newPathMatch = normalized.match(/^(.*\/com\.tencent\.xinWeChat\/(?:\d+\.\d+b\d+\.\d+|\d+\.\d+\.\d+))/) + const newPathMatch = normalized.match( + /^(.*\/com\.tencent\.xinWeChat\/(?:\d+\.\d+b\d+\.\d+|\d+\.\d+\.\d+))/ + ) if (newPathMatch) { candidates.add(`${newPathMatch[1]}/net/kvcomm`) candidates.add(`${newPathMatch[1]}/xwechat/net/kvcomm`) @@ -281,7 +273,9 @@ export class KeyServiceMac { const candidates: string[] = [] this.pushAccountIdCandidates(candidates, wxidParam) - const normalized = String(accountPath || '').replace(/\\/g, '/').replace(/\/+$/, '') + const normalized = String(accountPath || '') + .replace(/\\/g, '/') + .replace(/\/+$/, '') if (normalized) { this.pushAccountIdCandidates(candidates, path.basename(normalized)) const root = this.resolveXwechatRootFromPath(normalized) @@ -290,7 +284,8 @@ export class KeyServiceMac { for (const entry of fs.readdirSync(root, { withFileTypes: true })) { if (!entry.isDirectory()) continue const entryPath = path.join(root, entry.name) - if (this.isAccountDirPath(entryPath)) this.pushAccountIdCandidates(candidates, entry.name) + if (this.isAccountDirPath(entryPath)) + this.pushAccountIdCandidates(candidates, entry.name) } } catch { // Ignore unreadable directories. @@ -316,7 +311,8 @@ export class KeyServiceMac { for (const entry of fs.readdirSync(root, { withFileTypes: true })) { if (!entry.isDirectory()) continue const entryPath = path.join(root, entry.name) - if (this.isAccountDirPath(entryPath) && this.isReasonableAccountId(entry.name)) push(entryPath) + if (this.isAccountDirPath(entryPath) && this.isReasonableAccountId(entry.name)) + push(entryPath) } } catch { // Ignore unreadable directories. @@ -326,12 +322,16 @@ export class KeyServiceMac { } private resolveXwechatRootFromPath(accountPath?: string): string | null { - const normalized = String(accountPath || '').replace(/\\/g, '/').replace(/\/+$/, '') + const normalized = String(accountPath || '') + .replace(/\\/g, '/') + .replace(/\/+$/, '') if (!normalized) return null const marker = '/xwechat_files' const markerIndex = normalized.indexOf(marker) if (markerIndex >= 0) return normalized.slice(0, markerIndex + marker.length) - const newPathMatch = normalized.match(/^(.*\/com\.tencent\.xinWeChat\/(?:\d+\.\d+b\d+\.\d+|\d+\.\d+\.\d+))(\/|$)/) + const newPathMatch = normalized.match( + /^(.*\/com\.tencent\.xinWeChat\/(?:\d+\.\d+b\d+\.\d+|\d+\.\d+\.\d+))(\/|$)/ + ) return newPathMatch ? newPathMatch[1] : null } @@ -366,7 +366,9 @@ export class KeyServiceMac { } private isReasonableAccountId(value: string): boolean { - const lowered = String(value || '').trim().toLowerCase() + const lowered = String(value || '') + .trim() + .toLowerCase() if (!lowered || lowered.includes('/') || lowered.includes('\\')) return false return !['xwechat_files', 'all_users', 'backup', 'wmpf', 'app_data'].includes(lowered) } diff --git a/src/main/key-service-win.ts b/src/main/key-service-win.ts index 4f0d90c..e5bd6b4 100644 --- a/src/main/key-service-win.ts +++ b/src/main/key-service-win.ts @@ -1,13 +1,13 @@ // @ts-nocheck // Windows native bridge adapted from WeFlow. Koffi Win32 callbacks and optional // helpers are intentionally dynamic; keep this file isolated from strict TS. -import { app } from 'electron' import { join, dirname, delimiter } from 'path' import { existsSync, copyFileSync, mkdirSync } from 'fs' import { execFile, spawn } from 'child_process' import { promisify } from 'util' import os from 'os' import crypto from 'crypto' +import { getResourceRoots as getSharedResourceRoots } from './resource-paths' const execFileAsync = promisify(execFile) @@ -69,14 +69,7 @@ export class KeyService { private readonly DB_KEY_PROCESS_CHECK_INTERVAL_MS = 1000 private getResourceRoots(): string[] { - const roots = [ - join(process.cwd(), 'resources'), - join(process.cwd(), 'resources', 'resources'), - join(process.resourcesPath || '', 'resources'), - process.resourcesPath || '', - join(app.getAppPath(), 'resources') - ] - return Array.from(new Set(roots.filter((root) => root && existsSync(root)))) + return getSharedResourceRoots() } private getDllPath(): string { diff --git a/src/main/resource-paths.ts b/src/main/resource-paths.ts new file mode 100644 index 0000000..03c5f79 --- /dev/null +++ b/src/main/resource-paths.ts @@ -0,0 +1,35 @@ +import { app } from 'electron' +import { existsSync } from 'fs' +import { dirname, join } from 'path' + +function unique(values: string[]): string[] { + return Array.from(new Set(values.filter(Boolean))) +} + +export function getResourceRoots(): string[] { + const appPath = app.getAppPath() + const appPathDir = dirname(appPath) + const execDir = dirname(process.execPath) + + return unique([ + process.env.WECHATEXPLORER_RESOURCES_PATH || '', + join(process.cwd(), 'resources'), + join(process.cwd(), 'resources', 'resources'), + join(process.resourcesPath || '', 'resources'), + process.resourcesPath || '', + join(appPath, 'resources'), + join(appPathDir, 'resources'), + appPathDir, + join(execDir, 'resources'), + join(dirname(execDir), 'Resources', 'resources'), + join(dirname(execDir), 'Resources') + ]).filter((root) => existsSync(root)) +} + +export function getResourceCandidates(relativePath: string): string[] { + return unique(getResourceRoots().map((root) => join(root, relativePath))) +} + +export function findResource(relativePath: string): string | null { + return getResourceCandidates(relativePath).find((candidate) => existsSync(candidate)) || null +} diff --git a/src/main/wcdb4-client.ts b/src/main/wcdb4-client.ts index 40cc14f..6388b0f 100644 --- a/src/main/wcdb4-client.ts +++ b/src/main/wcdb4-client.ts @@ -4,6 +4,7 @@ import os from 'os' import crypto from 'crypto' import { createRequire } from 'module' import { createConnection, Socket } from 'net' +import { getResourceRoots } from './resource-paths' export interface Wcdb4Session { username: string @@ -81,14 +82,14 @@ export function bootstrapWcdbNative(libPath?: string, libDirOverride?: string): resourcePath: string ) => number const resourceRoots = Array.from( - new Set([ - libDir, - path.dirname(libDir), - process.env.WCDB_RESOURCES_PATH || '', - path.join(process.resourcesPath || process.cwd(), 'resources'), - process.resourcesPath || process.cwd(), - path.join(process.cwd(), 'resources') - ]) + new Set( + [ + libDir, + path.dirname(libDir), + process.env.WCDB_RESOURCES_PATH || '', + ...getResourceRoots() + ].filter(Boolean) + ) ) let lastCode = -1 let initOk = false @@ -1330,14 +1331,14 @@ export class Wcdb4Client { ) => number const resourceRoots = Array.from( - new Set([ - libDir, - path.dirname(libDir), - process.env.WCDB_RESOURCES_PATH || '', - path.join(process.resourcesPath || process.cwd(), 'resources'), - process.resourcesPath || process.cwd(), - path.join(process.cwd(), 'resources') - ]) + new Set( + [ + libDir, + path.dirname(libDir), + process.env.WCDB_RESOURCES_PATH || '', + ...getResourceRoots() + ].filter(Boolean) + ) ) let lastCode = -1 @@ -1373,19 +1374,21 @@ export class Wcdb4Client { ? 'libwcdb_api.so' : 'wcdb_api.dll' const platformDir = - process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'win32' : process.platform + process.platform === 'darwin' + ? 'macos' + : process.platform === 'win32' + ? 'win32' + : process.platform const archDir = process.arch === 'arm64' ? 'arm64' : 'x64' - const resourcesPath = process.resourcesPath || process.cwd() + const resourceRoots = getResourceRoots() const candidates = [ process.env.WCDB_DLL_PATH, - path.join(resourcesPath, 'resources', 'wcdb', platformDir, archDir, libName), - path.join(resourcesPath, 'resources', 'wcdb', platformDir, 'x64', libName), - path.join(process.cwd(), 'resources', 'wcdb', platformDir, archDir, libName), - path.join(process.cwd(), 'resources', 'wcdb', platformDir, 'x64', libName), - path.join(resourcesPath, 'resources', platformDir, libName), - path.join(resourcesPath, 'resources', libName), - path.join(process.cwd(), 'resources', platformDir, libName), - path.join(process.cwd(), 'resources', libName) + ...resourceRoots.flatMap((root) => [ + path.join(root, 'wcdb', platformDir, archDir, libName), + path.join(root, 'wcdb', platformDir, 'x64', libName), + path.join(root, platformDir, libName), + path.join(root, libName) + ]) ].filter(Boolean) as string[] const found = candidates.find((candidate) => fs.existsSync(candidate)) @@ -1393,9 +1396,13 @@ export class Wcdb4Client { throw new Error(`找不到 WCDB native 库: ${candidates.join(', ')}`) } if (process.platform === 'win32') { - const runtimeDir = path.join(resourcesPath, 'resources', 'runtime', 'win32') + const runtimeDir = resourceRoots + .map((root) => path.join(root, 'runtime', 'win32')) + .find((candidate) => fs.existsSync(candidate)) const dllDir = path.dirname(found) - process.env.PATH = [dllDir, runtimeDir, process.env.PATH || ''].filter(Boolean).join(path.delimiter) + process.env.PATH = [dllDir, runtimeDir || '', process.env.PATH || ''] + .filter(Boolean) + .join(path.delimiter) } return found } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index ff40922..986803f 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -624,6 +624,25 @@ function App(): React.ReactElement { setDbKeyStatusKind('normal') } + const handleReturnToLogin = (): void => { + setIsAuthenticated(false) + setIsDatabaseConnected(false) + setBootState('login') + setActivePage('archive') + setSettingsCategory('database-key') + setSelectedContact(null) + setMessages([]) + setContacts([]) + setFilteredContacts([]) + setSelfInfo(null) + setIsMessagesLoading(false) + setIsNativeMonitorActive(false) + setReportNotice('') + setDbKeyStatus('已断开当前连接,可重新输入或获取数据库密钥') + setDbKeyStatusKind('normal') + setStartupProgress(null) + } + const getDateRangeParams = ( range: string ): { startTime: number | undefined; endTime: number | undefined } => { @@ -1140,6 +1159,7 @@ function App(): React.ReactElement { onSelfInfoChange={setSelfInfo} onContactsChange={setContacts} onFilteredContactsChange={setFilteredContacts} + onReturnToLogin={handleReturnToLogin} onAIRuntimeChange={(config: AIRuntimeModelConfig) => setAiModelConfig(config)} onNotice={setReportNotice} onOpenSettings={openSettings} diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 12bdb32..bef8aa4 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -3825,7 +3825,7 @@ body { .database-key-auto-error { display:grid; gap:4px; margin-top:16px; padding:12px 14px; border-left:3px solid #c68635; background:#fff8ec; color:#79501f; font-size:12px; }.database-key-auto-error span { overflow-wrap:anywhere; }.database-key-auto-error p { color:#79501f; }.database-key-auto-error button { justify-self:start; border:0; padding:4px 0; background:transparent; color:#247a63; cursor:pointer; font:600 12px inherit; } .database-key-security-info { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1px; overflow:hidden; border:1px solid #e3e8e5; border-radius:9px; background:#e3e8e5; }.database-key-security-info span { display:grid; gap:6px; padding:15px; background:#f5f7f6; color:#66706b; font-size:12px; line-height:1.5; }.database-key-security-info strong { color:#35403b; font-size:13px; } .database-key-diagnostics { margin-top:22px; border:1px solid #dde3e0; border-radius:9px; background:#fff; }.database-key-diagnostics summary { padding:14px 16px; color:#46514c; cursor:pointer; font-size:13px; font-weight:600; }.database-key-diagnostics dl { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:14px 28px; margin:0; padding:2px 16px 16px; }.database-key-diagnostics button { margin:0 16px 16px; border:0; padding:0; background:transparent; color:#247a63; cursor:pointer; font:600 12px inherit; } -.database-key-danger { margin-top:30px; padding-bottom:1px; }.database-key-danger>h2 { margin:0 0 14px; color:#c85a5a; font-size:15px; }.database-key-danger>div { display:flex; align-items:center; justify-content:space-between; gap:20px; padding:16px 20px; border:1px solid #efcdcd; background:#fff7f7; }.database-key-danger>div:first-of-type { border-radius:9px 9px 0 0; }.database-key-danger>div:last-of-type { border-top:0; border-radius:0 0 9px 9px; }.database-key-danger span { display:grid; gap:4px; }.database-key-danger strong { color:#4a3c3c; font-size:13px; }.database-key-danger small { color:#786868; font-size:12px; }.database-key-danger button { min-height:34px; flex:0 0 auto; border:1px solid #e8baba; border-radius:8px; padding:0 13px; background:#fff; color:#c85a5a; cursor:pointer; }.database-key-danger button:disabled { cursor:not-allowed; opacity:.45; } +.database-key-connection-actions { margin-top:30px; }.database-key-connection-actions>h2 { margin:0 0 14px; color:#3d4742; font-size:15px; }.database-key-connection-actions>div { display:flex; align-items:center; justify-content:space-between; gap:20px; padding:16px 20px; border:1px solid #dde3e0; border-radius:9px; background:#fff; }.database-key-connection-actions span { display:grid; gap:4px; }.database-key-connection-actions strong { color:#35403b; font-size:13px; }.database-key-connection-actions small { color:#66706b; font-size:12px; }.database-key-connection-actions button { min-height:34px; flex:0 0 auto; border:1px solid #247a63; border-radius:8px; padding:0 13px; background:#fff; color:#247a63; cursor:pointer; }.database-key-connection-actions button:hover:not(:disabled) { background:#f0f7f4; }.database-key-danger { margin-top:30px; padding-bottom:1px; }.database-key-danger>h2 { margin:0 0 14px; color:#c85a5a; font-size:15px; }.database-key-danger>div { display:flex; align-items:center; justify-content:space-between; gap:20px; padding:16px 20px; border:1px solid #efcdcd; background:#fff7f7; }.database-key-danger>div:first-of-type { border-radius:9px 9px 0 0; }.database-key-danger>div:last-of-type { border-top:0; border-radius:0 0 9px 9px; }.database-key-danger span { display:grid; gap:4px; }.database-key-danger strong { color:#4a3c3c; font-size:13px; }.database-key-danger small { color:#786868; font-size:12px; }.database-key-danger button { min-height:34px; flex:0 0 auto; border:1px solid #e8baba; border-radius:8px; padding:0 13px; background:#fff; color:#c85a5a; cursor:pointer; }.database-key-danger button:disabled { cursor:not-allowed; opacity:.45; } .database-key-confirm-backdrop { position:fixed; z-index:30; inset:0; display:grid; place-items:center; padding:24px; background:rgba(25,32,29,.34); }.database-key-confirm { width:min(100%,430px); padding:22px; border-radius:12px; background:#fff; box-shadow:0 18px 50px rgba(24,35,30,.2); }.database-key-confirm h2 { margin:0; color:#202724; font-size:17px; }.database-key-confirm p { margin:12px 0 22px; color:#66706b; font-size:13px; line-height:1.7; }.database-key-confirm>div { display:flex; justify-content:flex-end; gap:8px; }.database-key-confirm button { min-height:34px; border:1px solid #dde3e0; border-radius:8px; padding:0 14px; background:#fff; color:#46514c; cursor:pointer; }.database-key-confirm button.danger { border-color:#c85a5a; background:#c85a5a; color:#fff; } /* SETTINGS-03: image decryption */ .image-decryption-content { padding-bottom:48px; } diff --git a/src/renderer/src/features/settings/SettingsWorkspace.tsx b/src/renderer/src/features/settings/SettingsWorkspace.tsx index 64c2e83..b13f4ff 100644 --- a/src/renderer/src/features/settings/SettingsWorkspace.tsx +++ b/src/renderer/src/features/settings/SettingsWorkspace.tsx @@ -20,6 +20,7 @@ export function SettingsWorkspace({ onSelfInfoChange, onContactsChange, onFilteredContactsChange, + onReturnToLogin, onAIRuntimeChange, onNotice, onOpenSettings @@ -34,6 +35,7 @@ export function SettingsWorkspace({ onSelfInfoChange: (info: SettingsSelfInfo | null) => void onContactsChange: (contacts: Contact[]) => void onFilteredContactsChange: (contacts: Contact[]) => void + onReturnToLogin: () => void onAIRuntimeChange: (config: AIRuntimeModelConfig) => void onNotice: (message: string) => void onOpenSettings: () => void @@ -64,6 +66,7 @@ export function SettingsWorkspace({ onSelfInfoChange={onSelfInfoChange} onContactsChange={onContactsChange} onFilteredContactsChange={onFilteredContactsChange} + onReturnToLogin={onReturnToLogin} onNotice={onNotice} /> ) : selectedCategory === 'image-key' ? ( diff --git a/src/renderer/src/features/settings/database-key/DatabaseKeyDangerZone.tsx b/src/renderer/src/features/settings/database-key/DatabaseKeyDangerZone.tsx index 4c3af9d..c10533d 100644 --- a/src/renderer/src/features/settings/database-key/DatabaseKeyDangerZone.tsx +++ b/src/renderer/src/features/settings/database-key/DatabaseKeyDangerZone.tsx @@ -3,15 +3,30 @@ import { useState } from 'react' export function DatabaseKeyDangerZone({ disabled, onClear, - onReplace + onReplace, + onReturnToLogin }: { disabled: boolean onClear: () => void onReplace: () => void + onReturnToLogin: () => void }): React.ReactElement { const [confirming, setConfirming] = useState(false) + const [confirmingReturn, setConfirmingReturn] = useState(false) return ( <> +
+

连接管理

+
+ + 返回登录界面 + 断开当前数据库连接,回到密钥输入界面。不会删除已保存密钥或微信数据。 + + +
+

密钥管理

@@ -69,6 +84,41 @@ export function DatabaseKeyDangerZone({
)} + {confirmingReturn && ( +
setConfirmingReturn(false)} + > +
event.stopPropagation()} + > +

返回登录界面?

+

+ WechatExplorer + 将断开当前数据库连接并回到密钥输入界面。已保存的数据库密钥和微信原始数据不会被删除。 +

+
+ + +
+
+
+ )} ) } diff --git a/src/renderer/src/features/settings/database-key/types.ts b/src/renderer/src/features/settings/database-key/types.ts index 7d37929..8da01b2 100644 --- a/src/renderer/src/features/settings/database-key/types.ts +++ b/src/renderer/src/features/settings/database-key/types.ts @@ -59,6 +59,7 @@ export interface DatabaseKeyController { saveKey: () => Promise autoDetectKey: () => Promise clearSavedKey: () => Promise + returnToLogin: () => Promise copyDiagnostics: () => Promise refreshEnvironment: () => Promise } diff --git a/src/renderer/src/features/settings/database-key/useDatabaseKeyController.ts b/src/renderer/src/features/settings/database-key/useDatabaseKeyController.ts index abb8f91..0dbbb08 100644 --- a/src/renderer/src/features/settings/database-key/useDatabaseKeyController.ts +++ b/src/renderer/src/features/settings/database-key/useDatabaseKeyController.ts @@ -14,6 +14,7 @@ export function useDatabaseKeyController({ onSelfInfoChange, onContactsChange, onFilteredContactsChange, + onReturnToLogin, onNotice }: { dbKey: string @@ -24,6 +25,7 @@ export function useDatabaseKeyController({ onSelfInfoChange: (info: SettingsSelfInfo | null) => void onContactsChange: (contacts: Contact[]) => void onFilteredContactsChange: (contacts: Contact[]) => void + onReturnToLogin: () => void onNotice: (message: string) => void }): DatabaseKeyController { const [state, dispatch] = useReducer(databaseKeyReducer, initialDatabaseKeyState) @@ -188,6 +190,26 @@ export function useDatabaseKeyController({ refreshEnvironment ]) + const returnToLogin = useCallback(async (): Promise => { + const result = await window.api.disconnectDb() + if (!result.success) { + onNotice(result.error || '断开数据库连接失败') + return + } + onDatabaseConnectionChange(false) + onSelfInfoChange(null) + onContactsChange([]) + onFilteredContactsChange([]) + onReturnToLogin() + }, [ + onContactsChange, + onDatabaseConnectionChange, + onFilteredContactsChange, + onNotice, + onReturnToLogin, + onSelfInfoChange + ]) + const copyDiagnostics = useCallback(async (): Promise => { const result = await window.api.copyText( buildDatabaseKeyDiagnostics(state, dbKey, selfInfo, dbReady) @@ -214,6 +236,7 @@ export function useDatabaseKeyController({ saveKey, autoDetectKey, clearSavedKey, + returnToLogin, copyDiagnostics, refreshEnvironment } diff --git a/src/renderer/src/features/settings/pages/DatabaseKeyPage.tsx b/src/renderer/src/features/settings/pages/DatabaseKeyPage.tsx index 8e4bf8d..362c6bc 100644 --- a/src/renderer/src/features/settings/pages/DatabaseKeyPage.tsx +++ b/src/renderer/src/features/settings/pages/DatabaseKeyPage.tsx @@ -24,6 +24,7 @@ export function DatabaseKeyPage({ onSelfInfoChange, onContactsChange, onFilteredContactsChange, + onReturnToLogin, onNotice }: { dbKey: string @@ -34,6 +35,7 @@ export function DatabaseKeyPage({ onSelfInfoChange: (info: SettingsSelfInfo | null) => void onContactsChange: (contacts: Contact[]) => void onFilteredContactsChange: (contacts: Contact[]) => void + onReturnToLogin: () => void onNotice: (message: string) => void }): React.ReactElement { const controller = useDatabaseKeyController({ @@ -45,6 +47,7 @@ export function DatabaseKeyPage({ onSelfInfoChange, onContactsChange, onFilteredContactsChange, + onReturnToLogin, onNotice }) @@ -132,6 +135,7 @@ export function DatabaseKeyPage({ disabled={controller.isBusy || !controller.state.saved} onClear={() => void controller.clearSavedKey()} onReplace={scrollToEditor} + onReturnToLogin={() => void controller.returnToLogin()} />