mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 03:27:00 +08:00
fix: 修复登录报错key问题
This commit is contained in:
+40
-38
@@ -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<string>([
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+35
-28
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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' ? (
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<section className="database-key-connection-actions">
|
||||
<h2>连接管理</h2>
|
||||
<div>
|
||||
<span>
|
||||
<strong>返回登录界面</strong>
|
||||
<small>断开当前数据库连接,回到密钥输入界面。不会删除已保存密钥或微信数据。</small>
|
||||
</span>
|
||||
<button type="button" onClick={() => setConfirmingReturn(true)}>
|
||||
返回登录
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<section className="database-key-danger">
|
||||
<h2>密钥管理</h2>
|
||||
<div>
|
||||
@@ -69,6 +84,41 @@ export function DatabaseKeyDangerZone({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{confirmingReturn && (
|
||||
<div
|
||||
className="database-key-confirm-backdrop"
|
||||
role="presentation"
|
||||
onMouseDown={() => setConfirmingReturn(false)}
|
||||
>
|
||||
<div
|
||||
className="database-key-confirm"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="database-key-return-confirm-title"
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<h2 id="database-key-return-confirm-title">返回登录界面?</h2>
|
||||
<p>
|
||||
WechatExplorer
|
||||
将断开当前数据库连接并回到密钥输入界面。已保存的数据库密钥和微信原始数据不会被删除。
|
||||
</p>
|
||||
<div>
|
||||
<button type="button" onClick={() => setConfirmingReturn(false)}>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setConfirmingReturn(false)
|
||||
onReturnToLogin()
|
||||
}}
|
||||
>
|
||||
返回登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ export interface DatabaseKeyController {
|
||||
saveKey: () => Promise<void>
|
||||
autoDetectKey: () => Promise<void>
|
||||
clearSavedKey: () => Promise<void>
|
||||
returnToLogin: () => Promise<void>
|
||||
copyDiagnostics: () => Promise<void>
|
||||
refreshEnvironment: () => Promise<void>
|
||||
}
|
||||
|
||||
@@ -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<void> => {
|
||||
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<void> => {
|
||||
const result = await window.api.copyText(
|
||||
buildDatabaseKeyDiagnostics(state, dbKey, selfInfo, dbReady)
|
||||
@@ -214,6 +236,7 @@ export function useDatabaseKeyController({
|
||||
saveKey,
|
||||
autoDetectKey,
|
||||
clearSavedKey,
|
||||
returnToLogin,
|
||||
copyDiagnostics,
|
||||
refreshEnvironment
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user