import { useState, useEffect, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { useAppStore } from '../stores/appStore' import { dialog } from '../services/ipc' import * as configService from '../services/config' import { ArrowLeft, ArrowRight, CheckCircle2, Database, Eye, EyeOff, FolderOpen, FolderSearch, KeyRound, ShieldCheck, Sparkles, UserRound, Wand2, Minus, X, HardDrive, RotateCcw } from 'lucide-react' import './WelcomePage.scss' const steps = [ { id: 'intro', title: '欢迎', desc: '准备开始你的本地数据探索' }, { id: 'db', title: '数据库目录', desc: '定位 xwechat_files 目录' }, { id: 'cache', title: '缓存目录', desc: '设置本地缓存存储位置(可选)' }, { id: 'key', title: '解密密钥', desc: '获取密钥与自动识别账号' }, { id: 'image', title: '图片密钥', desc: '获取 XOR 与 AES 密钥' }, { id: 'security', title: '安全防护', desc: '保护你的数据' } ] interface WelcomePageProps { 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) { const navigate = useNavigate() const { isDbConnected, setDbConnected, setLoading } = useAppStore() const [stepIndex, setStepIndex] = useState(0) const [dbPath, setDbPath] = useState('') const [decryptKey, setDecryptKey] = useState('') const [imageXorKey, setImageXorKey] = useState('') const [imageAesKey, setImageAesKey] = useState('') const [cachePath, setCachePath] = useState('') const [wxid, setWxid] = useState('') const [wxidOptions, setWxidOptions] = useState>([]) const [showWxidSelect, setShowWxidSelect] = useState(false) const wxidSelectRef = useRef(null) const [error, setError] = useState('') const [isConnecting, setIsConnecting] = useState(false) const [isDetectingPath, setIsDetectingPath] = useState(false) const [isScanningWxid, setIsScanningWxid] = useState(false) const [isFetchingDbKey, setIsFetchingDbKey] = useState(false) const [isFetchingImageKey, setIsFetchingImageKey] = useState(false) const [showDecryptKey, setShowDecryptKey] = useState(false) const [isClosing, setIsClosing] = useState(false) const [dbKeyStatus, setDbKeyStatus] = useState('') const [imageKeyStatus, setImageKeyStatus] = useState('') const [isManualStartPrompt, setIsManualStartPrompt] = useState(false) const [imageKeyPercent, setImageKeyPercent] = useState(null) // 安全相关 state const [enableAuth, setEnableAuth] = useState(false) const [authPassword, setAuthPassword] = useState('') const [authConfirmPassword, setAuthConfirmPassword] = useState('') const [enableHello, setEnableHello] = useState(false) const [helloAvailable, setHelloAvailable] = useState(false) const [isSettingHello, setIsSettingHello] = useState(false) // 检查 Hello 可用性 useEffect(() => { if (window.PublicKeyCredential) { void PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().then(setHelloAvailable) } }, []) async function sha256(message: string) { const msgBuffer = new TextEncoder().encode(message) const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer) const hashArray = Array.from(new Uint8Array(hashBuffer)) const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('') return hashHex } const handleSetupHello = async () => { setIsSettingHello(true) try { // 注册凭证 (WebAuthn) const challenge = new Uint8Array(32) window.crypto.getRandomValues(challenge) const credential = await navigator.credentials.create({ publicKey: { challenge, rp: { name: 'WeFlow', id: 'localhost' }, user: { id: new Uint8Array([1]), name: 'user', displayName: 'User' }, pubKeyCredParams: [{ alg: -7, type: 'public-key' }], authenticatorSelection: { userVerification: 'required' }, timeout: 60000 } }) if (credential) { setEnableHello(true) // 成功提示? } } catch (e: any) { if (e.name !== 'NotAllowedError') { setError('Windows Hello 设置失败: ' + e.message) } } finally { setIsSettingHello(false) } } useEffect(() => { const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => { setDbKeyStatus(payload.message) }) const removeImage = window.electronAPI.key.onImageKeyStatus((payload: { message: string, percent?: number }) => { let msg = payload.message; let pct = payload.percent; // 解析文本中的百分比 if (pct === undefined) { const match = msg.match(/\(([\d.]+)%\)/); if (match) { pct = parseFloat(match[1]); msg = msg.replace(/\s*\([\d.]+%\)/, ''); } } setImageKeyStatus(msg); if (pct !== undefined) { setImageKeyPercent(pct); } else if (msg.includes('启动多核') || msg.includes('定位') || msg.includes('准备')) { setImageKeyPercent(0); } }) return () => { removeDb?.() removeImage?.() } }, []) useEffect(() => { if (isDbConnected && !standalone) { navigate('/home') } }, [isDbConnected, standalone, navigate]) useEffect(() => { setWxidOptions([]) setWxid('') setShowWxidSelect(false) }, [dbPath]) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (!showWxidSelect) return const target = event.target as Node if (wxidSelectRef.current && !wxidSelectRef.current.contains(target)) { setShowWxidSelect(false) } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, [showWxidSelect]) const currentStep = steps[stepIndex] const rootClassName = `welcome-page${isClosing ? ' is-closing' : ''}${standalone ? ' is-standalone' : ''}` const showWindowControls = standalone const handleMinimize = () => { window.electronAPI.window.minimize() } const handleCloseWindow = () => { window.electronAPI.window.close() } const handleSelectPath = async () => { try { const result = await dialog.openFile({ title: '选择微信数据库目录', properties: ['openDirectory'] }) if (!result.canceled && result.filePaths.length > 0) { setDbPath(result.filePaths[0]) setError('') } } catch (e) { setError('选择目录失败') } } const handleAutoDetectPath = async () => { if (isDetectingPath) return setIsDetectingPath(true) setError('') try { const result = await window.electronAPI.dbPath.autoDetect() if (result.success && result.path) { setDbPath(result.path) setError('') } else { setError(result.error || '未能检测到数据库目录') } } catch (e) { setError(`自动检测失败: ${e}`) } finally { setIsDetectingPath(false) } } const handleSelectCachePath = async () => { try { const result = await dialog.openFile({ title: '选择缓存目录', properties: ['openDirectory'] }) if (!result.canceled && result.filePaths.length > 0) { setCachePath(result.filePaths[0]) setError('') } } catch (e) { setError('选择缓存目录失败') } } const handleScanWxid = async (silent = false) => { if (!dbPath) { if (!silent) setError('请先选择数据库目录') return } if (isScanningWxid) return setIsScanningWxid(true) if (!silent) setError('') try { const wxids = await window.electronAPI.dbPath.scanWxids(dbPath) setWxidOptions(wxids) if (wxids.length > 0) { // scanWxids 已经按时间排过序了,直接取第一个 setWxid(wxids[0].wxid) if (!silent) setError('') } else { if (!silent) setError('未检测到账号目录,请检查路径') } } catch (e) { if (!silent) setError(`扫描失败: ${e}`) } finally { setIsScanningWxid(false) } } const handleScanWxidCandidates = async () => { if (!dbPath) { setError('请先选择数据库目录') return } if (isScanningWxid) return setIsScanningWxid(true) setError('') try { const wxids = await window.electronAPI.dbPath.scanWxidCandidates(dbPath) setWxidOptions(wxids) setShowWxidSelect(true) if (!wxids.length) { setError('未检测到可用的账号目录,请检查路径') } } catch (e) { setError(`扫描失败: ${e}`) } finally { setIsScanningWxid(false) } } const handleAutoGetDbKey = async () => { if (isFetchingDbKey) return setIsFetchingDbKey(true) setError('') setIsManualStartPrompt(false) setDbKeyStatus('正在连接微信进程...') try { const result = await window.electronAPI.key.autoGetDbKey() if (result.success && result.key) { setDecryptKey(result.key) setDbKeyStatus('密钥获取成功') setError('') // 获取成功后自动扫描并填入 wxid await handleScanWxid(true) } else { if (result.error?.includes('未找到微信安装路径') || result.error?.includes('启动微信失败')) { setIsManualStartPrompt(true) setDbKeyStatus('需要手动启动微信') } else { if (result.error?.includes('尚未完成登录')) { setDbKeyStatus('请先在微信完成登录后重试') } setError(formatDbKeyFailureMessage(result.error, result.logs)) } } } catch (e) { setError(`自动获取密钥失败: ${e}`) } finally { setIsFetchingDbKey(false) } } const handleManualConfirm = async () => { setIsManualStartPrompt(false) handleAutoGetDbKey() } const handleAutoGetImageKey = async () => { if (isFetchingImageKey) return if (!dbPath) { setError('请先选择数据库目录'); return } setIsFetchingImageKey(true) setError('') setImageKeyPercent(0) setImageKeyStatus('正在准备获取图片密钥...') try { const accountPath = wxid ? `${dbPath}/${wxid}` : dbPath const result = await window.electronAPI.key.autoGetImageKey(accountPath, wxid) if (result.success && result.aesKey) { if (typeof result.xorKey === 'number') setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`) setImageAesKey(result.aesKey) setImageKeyStatus('已获取图片密钥') } else { setError(result.error || '自动获取图片密钥失败') } } catch (e) { setError(`自动获取图片密钥失败: ${e}`) } finally { setIsFetchingImageKey(false) } } const handleScanImageKeyFromMemory = async () => { if (isFetchingImageKey) return if (!dbPath) { setError('请先选择数据库目录'); return } setIsFetchingImageKey(true) setError('') setImageKeyPercent(0) setImageKeyStatus('正在扫描内存...') try { const accountPath = wxid ? `${dbPath}/${wxid}` : dbPath const result = await window.electronAPI.key.scanImageKeyFromMemory(accountPath) if (result.success && result.aesKey) { if (typeof result.xorKey === 'number') setImageXorKey(`0x${result.xorKey.toString(16).toUpperCase().padStart(2, '0')}`) setImageAesKey(result.aesKey) setImageKeyStatus('内存扫描成功,已获取图片密钥') } else { setError(result.error || '内存扫描获取图片密钥失败') } } catch (e) { setError(`内存扫描失败: ${e}`) } finally { setIsFetchingImageKey(false) } } const canGoNext = () => { if (currentStep.id === 'intro') return true if (currentStep.id === 'db') return Boolean(dbPath) if (currentStep.id === 'cache') return true if (currentStep.id === 'key') return decryptKey.length === 64 && Boolean(wxid) if (currentStep.id === 'image') return true if (currentStep.id === 'security') { if (enableAuth) { return authPassword.length > 0 && authPassword === authConfirmPassword } return true } return false } const handleNext = () => { if (!canGoNext()) { if (currentStep.id === 'db' && !dbPath) setError('请先选择数据库目录') if (currentStep.id === 'key') { if (decryptKey.length !== 64) setError('密钥长度必须为 64 个字符') else if (!wxid) setError('未能自动识别 wxid,请尝试重新获取或检查目录') } return } setError('') setStepIndex((prev) => Math.min(prev + 1, steps.length - 1)) } const handleBack = () => { setError('') setStepIndex((prev) => Math.max(prev - 1, 0)) } const handleConnect = async () => { if (!dbPath) { setError('请先选择数据库目录'); return } if (!wxid) { setError('请填写微信ID'); return } if (!decryptKey || decryptKey.length !== 64) { setError('请填写 64 位解密密钥'); return } setIsConnecting(true) setError('') setLoading(true, '正在连接数据库...') try { const result = await window.electronAPI.wcdb.testConnection(dbPath, decryptKey, wxid) if (!result.success) { setError(result.error || 'WCDB 连接失败') setLoading(false) return } await configService.setDbPath(dbPath) await configService.setDecryptKey(decryptKey) await configService.setMyWxid(wxid) await configService.setCachePath(cachePath) const parsedXorKey = imageXorKey ? parseInt(imageXorKey.replace(/^0x/i, ''), 16) : null await configService.setImageXorKey(typeof parsedXorKey === 'number' && !Number.isNaN(parsedXorKey) ? parsedXorKey : 0) await configService.setImageAesKey(imageAesKey || '') await configService.setWxidConfig(wxid, { decryptKey, imageXorKey: typeof parsedXorKey === 'number' && !Number.isNaN(parsedXorKey) ? parsedXorKey : 0, imageAesKey }) // 保存安全配置 if (enableAuth && authPassword) { const hash = await sha256(authPassword) await configService.setAuthEnabled(true) await configService.setAuthPassword(hash) await configService.setAuthUseHello(enableHello) } await configService.setOnboardingDone(true) setDbConnected(true, dbPath) setLoading(false) if (standalone) { setIsClosing(true) setTimeout(() => { window.electronAPI.window.completeOnboarding() }, 450) } else { navigate('/home') } } catch (e) { setError(`连接失败: ${e}`) setLoading(false) } finally { setIsConnecting(false) } } const formatModifiedTime = (time: number) => { if (!time) return '未知时间' const date = new Date(time) const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') const hours = String(date.getHours()).padStart(2, '0') const minutes = String(date.getMinutes()).padStart(2, '0') return `${year}-${month}-${day} ${hours}:${minutes}` } if (isDbConnected) { return (
{showWindowControls && (
)}
WeFlow
WeFlow Connected
本地安全存储

配置已完成

数据库已连接,你可以直接进入首页使用全部功能。

) } return (
{showWindowControls && (
)}
WeFlow
WeFlow Setup
{steps.map((step, index) => (
{index < stepIndex ? :
}
{step.title}
{step.desc}
))}
数据仅在本地处理,不上传服务器

{currentStep.title}

{currentStep.desc}

{currentStep.id === 'intro' && (
{/* 内容移至底部 */}
)} {currentStep.id === 'db' && (
setDbPath(e.target.value)} />
请选择微信-设置-存储位置对应的目录
目录路径不可包含中文,如有中文请先在微信中迁移至全英文目录
)} {currentStep.id === 'cache' && (
setCachePath(e.target.value)} />
用于头像、表情与图片缓存
)} {currentStep.id === 'key' && (
setWxid(e.target.value)} /> {showWxidSelect && wxidOptions.length > 0 && (
{wxidOptions.map((opt) => ( ))}
)}
setDecryptKey(e.target.value.trim())} />
{isManualStartPrompt ? (

未能自动启动微信,请手动启动并登录

) : ( )}
{dbKeyStatus &&
{dbKeyStatus}
}
点击自动获取后微信将重启,请留意弹窗提示
)} {currentStep.id === 'security' && (
每次启动应用时需要验证密码
{enableAuth && (
setAuthPassword(e.target.value)} />
setAuthConfirmPassword(e.target.value)} /> {authPassword && authConfirmPassword && authPassword !== authConfirmPassword && (
两次密码不一致
)}
使用面容、指纹或 PIN 码快速解锁
{enableHello ? (
已开启
) : ( )}
{!helloAvailable &&
当前设备不支持 Windows Hello 或未设置 PIN 码
}
)}
)} {currentStep.id === 'image' && (
⚠️ 快速获取方案基于本地缓存计算,可能因账号信息不匹配而不准确。若图片无法解密,请使用下方「内存扫描」方案。
setImageXorKey(e.target.value)} />
setImageAesKey(e.target.value)} />
{isFetchingImageKey ? (
{imageKeyStatus || '正在启动...'}
) : ( imageKeyStatus &&
{imageKeyStatus}
)}
内存扫描需要微信正在运行,并在微信中打开 2-3 张图片大图后再点击
)}
{error &&
{error}
} {currentStep.id === 'intro' && (

接下来的几个步骤将引导你连接本地微信数据库。

WeFlow 需要访问你的本地数据文件以提供分析与导出功能。

)}
{stepIndex < steps.length - 1 ? ( ) : ( )}
) } export default WelcomePage