fix(auth): avoid logout on export-only clear and harden db key auto-fetch

This commit is contained in:
tisonhuang
2026-03-05 11:15:44 +08:00
parent 459f23bbd6
commit c5a6d765ee
5 changed files with 126 additions and 24 deletions

View File

@@ -1347,27 +1347,29 @@ function registerIpcHandlers() {
} }
} }
try { if (clearCache) {
const wxidConfigsRaw = cfg.get('wxidConfigs') as Record<string, any> | undefined try {
if (wxidConfigsRaw && typeof wxidConfigsRaw === 'object') { const wxidConfigsRaw = cfg.get('wxidConfigs') as Record<string, any> | undefined
const nextConfigs: Record<string, any> = { ...wxidConfigsRaw } if (wxidConfigsRaw && typeof wxidConfigsRaw === 'object') {
for (const key of Object.keys(nextConfigs)) { const nextConfigs: Record<string, any> = { ...wxidConfigsRaw }
if (isMatchedAccountName(key) || normalizeAccountId(key) === normalizedWxid) { for (const key of Object.keys(nextConfigs)) {
delete nextConfigs[key] if (isMatchedAccountName(key) || normalizeAccountId(key) === normalizedWxid) {
delete nextConfigs[key]
}
} }
cfg.set('wxidConfigs' as any, nextConfigs as any)
} }
cfg.set('wxidConfigs' as any, nextConfigs as any) cfg.set('myWxid' as any, '')
cfg.set('decryptKey' as any, '')
cfg.set('imageXorKey' as any, 0)
cfg.set('imageAesKey' as any, '')
cfg.set('dbPath' as any, '')
cfg.set('lastOpenedDb' as any, '')
cfg.set('onboardingDone' as any, false)
cfg.set('lastSession' as any, '')
} catch (error) {
warnings.push(`清理账号配置失败: ${String(error)}`)
} }
cfg.set('myWxid' as any, '')
cfg.set('decryptKey' as any, '')
cfg.set('imageXorKey' as any, 0)
cfg.set('imageAesKey' as any, '')
cfg.set('dbPath' as any, '')
cfg.set('lastOpenedDb' as any, '')
cfg.set('onboardingDone' as any, false)
cfg.set('lastSession' as any, '')
} catch (error) {
warnings.push(`清理账号配置失败: ${String(error)}`)
} }
return { return {
@@ -2172,7 +2174,7 @@ function registerIpcHandlers() {
// 密钥获取 // 密钥获取
ipcMain.handle('key:autoGetDbKey', async (event) => { ipcMain.handle('key:autoGetDbKey', async (event) => {
return keyService.autoGetDbKey(60_000, (message, level) => { return keyService.autoGetDbKey(180_000, (message, level) => {
event.sender.send('key:dbKeyStatus', { message, level }) event.sender.send('key:dbKeyStatus', { message, level })
}) })
}) })

View File

@@ -509,6 +509,58 @@ export class KeyService {
return false return false
} }
private isLoginRelatedText(value: string): boolean {
const normalized = String(value || '').replace(/\s+/g, '').toLowerCase()
if (!normalized) return false
const keywords = [
'登录',
'扫码',
'二维码',
'请在手机上确认',
'手机确认',
'切换账号',
'wechatlogin',
'qrcode',
'scan'
]
return keywords.some((keyword) => normalized.includes(keyword))
}
private async detectWeChatLoginRequired(pid: number): Promise<boolean> {
if (!this.ensureUser32()) return false
let loginRequired = false
const enumWindowsCallback = this.koffi.register((hWnd: any, _lParam: any) => {
if (!this.IsWindowVisible(hWnd)) return true
const title = this.getWindowTitle(hWnd)
if (!this.isWeChatWindowTitle(title)) return true
const pidBuf = Buffer.alloc(4)
this.GetWindowThreadProcessId(hWnd, pidBuf)
const windowPid = pidBuf.readUInt32LE(0)
if (windowPid !== pid) return true
if (this.isLoginRelatedText(title)) {
loginRequired = true
return false
}
const children = this.collectChildWindowInfos(hWnd)
for (const child of children) {
if (this.isLoginRelatedText(child.title) || this.isLoginRelatedText(child.className)) {
loginRequired = true
return false
}
}
return true
}, this.WNDENUMPROC_PTR)
this.EnumWindows(enumWindowsCallback, 0)
this.koffi.unregister(enumWindowsCallback)
return loginRequired
}
private async waitForWeChatWindowComponents(pid: number, timeoutMs = 15000): Promise<boolean> { private async waitForWeChatWindowComponents(pid: number, timeoutMs = 15000): Promise<boolean> {
if (!this.ensureUser32()) return true if (!this.ensureUser32()) return true
const startTime = Date.now() const startTime = Date.now()
@@ -605,6 +657,7 @@ export class KeyService {
const keyBuffer = Buffer.alloc(128) const keyBuffer = Buffer.alloc(128)
const start = Date.now() const start = Date.now()
let loginRequiredDetected = false
try { try {
while (Date.now() - start < timeoutMs) { while (Date.now() - start < timeoutMs) {
@@ -624,6 +677,9 @@ export class KeyService {
const level = levelOut[0] ?? 0 const level = levelOut[0] ?? 0
if (msg) { if (msg) {
logs.push(msg) logs.push(msg)
if (this.isLoginRelatedText(msg)) {
loginRequiredDetected = true
}
onStatus?.(msg, level) onStatus?.(msg, level)
} }
} }
@@ -635,6 +691,15 @@ export class KeyService {
} catch { } } catch { }
} }
const loginRequired = loginRequiredDetected || await this.detectWeChatLoginRequired(pid)
if (loginRequired) {
return {
success: false,
error: '微信已启动但尚未完成登录,请先在微信客户端完成登录后再重试自动获取密钥。',
logs
}
}
return { success: false, error: '获取密钥超时', logs } return { success: false, error: '获取密钥超时', logs }
} }

View File

@@ -305,9 +305,14 @@ function Sidebar() {
if (result.warning) { if (result.warning) {
detailLines.push('', `注意:${result.warning}`) detailLines.push('', `注意:${result.warning}`)
} }
window.alert(`账号数据清理完成。\n\n${detailLines.join('\n')}\n\n为保障数据安全WeFlow 已清除该账号本地缓存/导出相关数据。若需再次获取数据,请手动登录微信客户端并重新在 WeFlow 完成配置。`) const followupHint = shouldClearCacheData
? '若需再次获取数据,请手动登录微信客户端并重新在 WeFlow 完成配置。'
: '你可以继续使用当前登录状态,无需重新登录。'
window.alert(`账号数据清理完成。\n\n${detailLines.join('\n')}\n\n为保障数据安全WeFlow 已清除该账号本地缓存/导出相关数据。${followupHint}`)
resetClearDialogState() resetClearDialogState()
window.location.reload() if (shouldClearCacheData) {
window.location.reload()
}
} catch (error) { } catch (error) {
console.error('清理账号数据失败:', error) console.error('清理账号数据失败:', error)
window.alert('清理失败,请稍后重试。') window.alert('清理失败,请稍后重试。')

View File

@@ -36,6 +36,18 @@ interface WxidOption {
modifiedTime: number modifiedTime: number
} }
const formatDbKeyFailureMessage = (error?: string, logs?: string[]): string => {
const base = String(error || '自动获取密钥失败').trim()
const tailLogs = Array.isArray(logs)
? logs
.map(item => String(item || '').trim())
.filter(Boolean)
.slice(-6)
: []
if (tailLogs.length === 0) return base
return `${base};最近状态:${tailLogs.join(' | ')}`
}
function SettingsPage() { function SettingsPage() {
const { const {
isDbConnected, isDbConnected,
@@ -725,7 +737,10 @@ function SettingsPage() {
setIsManualStartPrompt(true) setIsManualStartPrompt(true)
setDbKeyStatus('需要手动启动微信') setDbKeyStatus('需要手动启动微信')
} else { } else {
showMessage(result.error || '自动获取密钥失败', false) if (result.error?.includes('尚未完成登录')) {
setDbKeyStatus('请先在微信完成登录后重试')
}
showMessage(formatDbKeyFailureMessage(result.error, result.logs), false)
} }
} }
} catch (e: any) { } catch (e: any) {

View File

@@ -23,6 +23,18 @@ interface WelcomePageProps {
standalone?: boolean standalone?: boolean
} }
const formatDbKeyFailureMessage = (error?: string, logs?: string[]): string => {
const base = String(error || '自动获取密钥失败').trim()
const tailLogs = Array.isArray(logs)
? logs
.map(item => String(item || '').trim())
.filter(Boolean)
.slice(-6)
: []
if (tailLogs.length === 0) return base
return `${base};最近状态:${tailLogs.join(' | ')}`
}
function WelcomePage({ standalone = false }: WelcomePageProps) { function WelcomePage({ standalone = false }: WelcomePageProps) {
const navigate = useNavigate() const navigate = useNavigate()
const { isDbConnected, setDbConnected, setLoading } = useAppStore() const { isDbConnected, setDbConnected, setLoading } = useAppStore()
@@ -292,7 +304,10 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
setIsManualStartPrompt(true) setIsManualStartPrompt(true)
setDbKeyStatus('需要手动启动微信') setDbKeyStatus('需要手动启动微信')
} else { } else {
setError(result.error || '自动获取密钥失败') if (result.error?.includes('尚未完成登录')) {
setDbKeyStatus('请先在微信完成登录后重试')
}
setError(formatDbKeyFailureMessage(result.error, result.logs))
} }
} }
} catch (e) { } catch (e) {