feat: 增强设置和欢迎页面的账号验证功能,优化用户体验

This commit is contained in:
ILoveBingLu
2026-03-03 02:15:18 +08:00
parent 4a5f51bed7
commit e0d2712393
8 changed files with 260 additions and 58 deletions

1
.gitignore vendored
View File

@@ -53,3 +53,4 @@ native-dlls
MyCoolInstaller
resources/whisper
xkey
skills

View File

@@ -79,6 +79,27 @@ export interface AnnualReportData {
class AnnualReportService {
private configService: ConfigService
private messageDbCache: Map<string, Database.Database> = new Map()
private readonly systemAccounts = new Set([
'medianote',
'floatbottle',
'qmessage',
'qqmail',
'fmessage',
'weixin',
'newsapp',
'notification_messages',
'weixinreminder',
'masssendapp',
'qqsync',
'facebookapp',
'feedsapp',
'voip',
'blogapp',
'gmailapp',
'linkedinplugin',
'appbrand_notify_message',
'appbrandcustomerservicemsg'
])
constructor() {
this.configService = new ConfigService()
@@ -211,6 +232,32 @@ class AnnualReportService {
return crypto.createHash('md5').update(username).digest('hex')
}
private shouldExcludeAnnualSession(username: string): boolean {
if (!username) return true
const u = username.toLowerCase().trim()
if (!u) return true
// 群聊、公众号、文件传输助手
if (u.includes('@chatroom')) return true
if (u.startsWith('gh_')) return true
if (u === 'filehelper') return true
// 已知系统账号
if (this.systemAccounts.has(u)) return true
// 邮箱/提醒类
if (u.includes('qqmail') || u.includes('mail')) return true
if (u.includes('reminder') || u.includes('notify')) return true
return false
}
private extractTableHash(tableName: string): string | null {
const match = tableName.match(/msg_([0-9a-f]{32})/i)
if (match?.[1]) return match[1].toLowerCase()
return null
}
private hasName2IdTable(db: Database.Database): boolean {
try {
const result = db.prepare(
@@ -270,7 +317,7 @@ class AnnualReportService {
const tables = db.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name LIKE 'Msg_%'
WHERE type='table' AND lower(name) LIKE 'msg_%'
`).all() as { name: string }[]
for (const { name: tableName } of tables) {
@@ -345,17 +392,19 @@ class AnnualReportService {
.map(s => s.username)
.filter(u => {
const uLower = u.toLowerCase()
return uLower !== wxidLower && uLower !== cleanedWxidLower
return uLower !== wxidLower && uLower !== cleanedWxidLower && !this.shouldExcludeAnnualSession(u)
})
// 构建 hash -> username 映射
const hashToUsername = new Map<string, string>()
for (const username of privateUsernames) {
hashToUsername.set(this.getTableHash(username), username)
hashToUsername.set(this.getTableHash(username).toLowerCase(), username)
}
const startTime = Math.floor(new Date(year, 0, 1).getTime() / 1000)
const endTime = Math.floor(new Date(year, 11, 31, 23, 59, 59).getTime() / 1000)
const isAllTime = year <= 0
const reportYear = isAllTime ? 0 : year
const startTime = isAllTime ? 0 : Math.floor(new Date(year, 0, 1).getTime() / 1000)
const endTime = isAllTime ? Math.floor(Date.now() / 1000) : Math.floor(new Date(year, 11, 31, 23, 59, 59).getTime() / 1000)
// 统计数据
let totalMessages = 0
@@ -387,12 +436,13 @@ class AnnualReportService {
const tables = db.prepare(`
SELECT name FROM sqlite_master
WHERE type='table' AND name LIKE 'Msg_%'
WHERE type='table' AND lower(name) LIKE 'msg_%'
`).all() as { name: string }[]
for (const { name: tableName } of tables) {
// 从表名提取 hash查找对应的 sessionId
const tableHash = tableName.replace('Msg_', '')
const tableHash = this.extractTableHash(tableName)
if (!tableHash) continue
const sessionId = hashToUsername.get(tableHash)
if (!sessionId) continue // 不是私聊表
@@ -803,7 +853,7 @@ class AnnualReportService {
.map(([phrase, count]) => ({ phrase, count }))
const reportData: AnnualReportData = {
year,
year: reportYear,
totalMessages,
totalFriends: contactStats.size,
coreFriends,

View File

@@ -840,22 +840,44 @@ class ChatService extends EventEmitter {
return hash
}
/**
* 从消息表名中提取会话 hash兼容大小写与后缀
*/
private extractTableHash(tableName: string): string | null {
const match = tableName.match(/msg_([0-9a-f]{32})/i)
if (match?.[1]) return match[1].toLowerCase()
return null
}
/**
* 在消息数据库中查找会话的消息表(带缓存)
*/
private findMessageTable(db: Database.Database, sessionId: string): string | null {
try {
const tables = db.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Msg_%'"
"SELECT name FROM sqlite_master WHERE type='table' AND lower(name) LIKE 'msg_%'"
).all() as any[]
const hash = this.getTableNameHash(sessionId)
const hash = this.getTableNameHash(sessionId).toLowerCase()
for (const table of tables) {
const name = table.name as string
if (name.includes(hash)) {
// 优先精确提取 hash 匹配
const tableHash = this.extractTableHash(name)
if (tableHash && tableHash === hash) {
return name
}
// 兜底兼容:历史表名规则可能不完全一致,采用大小写无关包含匹配
if (name.toLowerCase().includes(hash)) {
return name
}
}
if (tables.length > 0) {
const sample = tables.slice(0, 8).map(t => t.name).join(', ')
console.warn(`[ChatService] 未匹配到消息表: session=${sessionId}, hash=${hash}, tables=${tables.length}, sample=[${sample}]`)
}
} catch { }
@@ -3386,9 +3408,9 @@ class ChatService extends EventEmitter {
if (!db) continue
// 查找所有消息表
const tables = db.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Msg_%'"
).all() as any[]
const tables = db.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND lower(name) LIKE 'msg_%'"
).all() as any[]
for (const table of tables) {
const tableName = table.name as string

View File

@@ -1,6 +1,6 @@
{
"name": "ciphertalk",
"version": "2.2.9",
"version": "2.2.10",
"description": "密语 - 微信聊天记录查看工具",
"author": "ILoveBingLu",
"license": "CC-BY-NC-SA-4.0",

View File

@@ -2,9 +2,11 @@ import { useState, useEffect } from 'react'
import { Calendar, Loader2, Sparkles } from 'lucide-react'
import './AnnualReportPage.scss'
type YearOption = number | 'all'
function AnnualReportPage() {
const [availableYears, setAvailableYears] = useState<number[]>([])
const [selectedYear, setSelectedYear] = useState<number | null>(null)
const [selectedYear, setSelectedYear] = useState<YearOption | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [isGenerating, setIsGenerating] = useState(false)
@@ -31,7 +33,8 @@ function AnnualReportPage() {
if (!selectedYear) return
setIsGenerating(true)
try {
await window.electronAPI.window.openAnnualReportWindow(selectedYear)
const yearParam = selectedYear === 'all' ? 0 : selectedYear
await window.electronAPI.window.openAnnualReportWindow(yearParam)
} catch (e) {
console.error('生成报告失败:', e)
} finally {
@@ -58,6 +61,15 @@ function AnnualReportPage() {
)
}
const yearOptions: YearOption[] = availableYears.length > 0
? ['all', ...availableYears]
: []
const getYearLabel = (value: YearOption | null) => {
if (!value) return ''
return value === 'all' ? '全部时间' : `${value}`
}
return (
<div className="annual-report-page">
<Sparkles size={32} className="header-icon" />
@@ -65,14 +77,14 @@ function AnnualReportPage() {
<p className="page-desc"></p>
<div className="year-grid">
{availableYears.map(year => (
{yearOptions.map(year => (
<div
key={year}
className={`year-card ${selectedYear === year ? 'selected' : ''}`}
onClick={() => setSelectedYear(year)}
>
<span className="year-number">{year}</span>
<span className="year-label"></span>
<span className="year-number">{year === 'all' ? '全部' : year}</span>
<span className="year-label">{year === 'all' ? '时间' : '年'}</span>
</div>
))}
</div>
@@ -90,7 +102,7 @@ function AnnualReportPage() {
) : (
<>
<Sparkles size={20} />
<span> {selectedYear} </span>
<span> {getYearLabel(selectedYear)} </span>
</>
)}
</button>

View File

@@ -361,6 +361,11 @@ function AnnualReportWindow() {
return `${Math.round(seconds / 3600)}小时`
}
const formatYearLabel = (value: number, withSuffix: boolean = true) => {
if (value === 0) return '全部时间'
return withSuffix ? `${value}` : `${value}`
}
// 获取可用的板块列表
const getAvailableSections = (): SectionInfo[] => {
if (!reportData) return []
@@ -623,7 +628,8 @@ function AnnualReportWindow() {
const finalDataUrl = outputCanvas.toDataURL('image/png')
const link = document.createElement('a')
link.download = `${reportData?.year}年度报告.png`
const yearFilePrefix = reportData ? formatYearLabel(reportData.year, false) : ''
link.download = `${yearFilePrefix}年度报告.png`
link.href = finalDataUrl
document.body.appendChild(link)
link.click()
@@ -669,22 +675,24 @@ function AnnualReportWindow() {
// 单张图片直接下载,多张打包成 zip
if (exportedImages.length === 1) {
const link = document.createElement('a')
link.download = `${reportData?.year}年度报告_${exportedImages[0].name}.png`
const yearFilePrefix = reportData ? formatYearLabel(reportData.year, false) : ''
link.download = `${yearFilePrefix}年度报告_${exportedImages[0].name}.png`
link.href = exportedImages[0].data
link.click()
} else {
setExportProgress('正在打包...')
const zip = new JSZip()
const yearFilePrefix = reportData ? formatYearLabel(reportData.year, false) : ''
for (const img of exportedImages) {
// 从 data URL 提取 base64 数据
const base64Data = img.data.split(',')[1]
zip.file(`${reportData?.year}年度报告_${img.name}.png`, base64Data, { base64: true })
zip.file(`${yearFilePrefix}年度报告_${img.name}.png`, base64Data, { base64: true })
}
const blob = await zip.generateAsync({ type: 'blob' })
const link = document.createElement('a')
link.download = `${reportData?.year}年度报告_分模块.zip`
link.download = `${yearFilePrefix}年度报告_分模块.zip`
link.href = URL.createObjectURL(blob)
link.click()
URL.revokeObjectURL(link.href)
@@ -753,6 +761,7 @@ function AnnualReportWindow() {
}
const { year, totalMessages, totalFriends, coreFriends, monthlyTopFriends, peakDay, longestStreak, activityHeatmap, midnightKing, selfAvatarUrl, mutualFriend, socialInitiative, responseSpeed, topPhrases } = reportData
const yearTitle = formatYearLabel(year)
const topFriend = coreFriends[0]
const mostActive = getMostActiveTime(activityHeatmap.data)
@@ -855,10 +864,10 @@ function AnnualReportWindow() {
{/* 封面 */}
<section className="section cover-section" ref={sectionRefs.cover}>
<div className="label-text">CipherTalk · ANNUAL REPORT</div>
<div className="cover-year">{year}</div>
<div className="cover-year">{year === 0 ? 'ALL' : year}</div>
<h1 className="hero-title"></h1>
<hr className="divider" />
<p className="hero-desc"><br /></p>
<p className="hero-desc"><br /></p>
</section>
{/* 年度概览 */}
@@ -895,7 +904,7 @@ function AnnualReportWindow() {
{/* 月度好友 */}
<section className="section" ref={sectionRefs.monthlyFriends}>
<div className="label-text"></div>
<h2 className="hero-title">{year}</h2>
<h2 className="hero-title">{yearTitle}</h2>
<p className="hero-desc">12<br /></p>
<div className="monthly-orbit">
{monthlyTopFriends.map((m, i) => (
@@ -1036,9 +1045,9 @@ function AnnualReportWindow() {
{topPhrases && topPhrases.length > 0 && (
<section className="section" ref={sectionRefs.topPhrases}>
<div className="label-text"></div>
<h2 className="hero-title">{year}</h2>
<h2 className="hero-title">{yearTitle}</h2>
<p className="hero-desc">
<br />
<span className="hl" style={{ fontSize: '20px' }}>
{topPhrases.slice(0, 3).map(p => p.phrase).join('、')}
@@ -1103,7 +1112,7 @@ function AnnualReportWindow() {
<br />
</p>
<div className="ending-year">{year}</div>
<div className="ending-year">{year === 0 ? 'ALL' : year}</div>
<div className="ending-brand">-CipherTalk</div>
</section>
</div>

View File

@@ -82,6 +82,8 @@ function SettingsPage() {
const [wxidOptions, setWxidOptions] = useState<string[]>([])
const [showWxidDropdown, setShowWxidDropdown] = useState(false)
const [isScanningWxid, setIsScanningWxid] = useState(false)
const [isAccountVerified, setIsAccountVerified] = useState(false)
const [isVerifyingAccount, setIsVerifyingAccount] = useState(false)
const [cachePath, setCachePath] = useState('')
const [imageXorKey, setImageXorKey] = useState('')
const [imageAesKey, setImageAesKey] = useState('')
@@ -636,21 +638,21 @@ function SettingsPage() {
setIsScanningWxid(true)
try {
const wxids = await window.electronAPI.dbPath.scanWxids(dbPath)
setIsAccountVerified(false)
if (wxids.length === 0) {
showMessage('未检测到账号目录(需包含 db_storage 文件夹)', false)
setWxidOptions([])
} else if (wxids.length === 1) {
// 只有一个账号,直接设置
setWxid(wxids[0])
await configService.setMyWxid(wxids[0])
showMessage(`已检测到账号:${wxids[0]}`, true)
showMessage(`已检测到候选账号目录:${wxids[0]}(待验证)`, true)
setWxidOptions([])
setShowWxidDropdown(false)
} else {
// 多个账号,显示选择下拉框
setWxidOptions(wxids)
setShowWxidDropdown(true)
showMessage(`检测到 ${wxids.length}账号,请选择`, true)
showMessage(`检测到 ${wxids.length}候选账号目录,请选择后验证`, true)
}
} catch (e) {
showMessage(`扫描失败: ${e}`, false)
@@ -662,16 +664,41 @@ function SettingsPage() {
// 选择 wxid
const handleSelectWxid = async (selectedWxid: string) => {
setWxid(selectedWxid)
await configService.setMyWxid(selectedWxid)
setIsAccountVerified(false)
setShowWxidDropdown(false)
showMessage(`已选择账号${selectedWxid}`, true)
showMessage(`已选择候选账号目录${selectedWxid}(待验证)`, true)
}
const handleVerifyAccountDirectory = async () => {
if (!dbPath) { showMessage('请先选择数据库目录', false); return }
if (!decryptKey || decryptKey.length !== 64) { showMessage('请先配置64位解密密钥', false); return }
if (!wxid) { showMessage('请先选择账号目录', false); return }
setIsVerifyingAccount(true)
try {
const result = await window.electronAPI.wcdb.testConnection(dbPath, decryptKey, wxid)
if (result.success) {
setIsAccountVerified(true)
await configService.setMyWxid(wxid)
showMessage(`账号目录验证成功:${wxid}`, true)
} else {
setIsAccountVerified(false)
showMessage(result.error || '账号目录验证失败,请更换目录重试', false)
}
} catch (e) {
setIsAccountVerified(false)
showMessage(`账号目录验证失败: ${e}`, false)
} finally {
setIsVerifyingAccount(false)
}
}
const handleTestConnection = async () => {
if (!dbPath) { showMessage('请先选择数据库目录', false); return }
if (!decryptKey) { showMessage('请先输入解密密钥', false); return }
if (decryptKey.length !== 64) { showMessage('密钥长度必须为64个字符', false); return }
if (!wxid) { showMessage('请先输入或扫描 wxid', false); return }
if (!wxid) { showMessage('请先选择账号目录', false); return }
if (!isAccountVerified) { showMessage('请先验证账号目录', false); return }
setIsTesting(true)
try {
@@ -734,7 +761,7 @@ function SettingsPage() {
await configService.setAiMessageLimit(aiMessageLimit)
// 如果数据库配置完整,尝试设置已连接状态(不进行耗时测试,仅标记)
if (decryptKey && dbPath && wxid && decryptKey.length === 64) {
if (decryptKey && dbPath && wxid && decryptKey.length === 64 && isAccountVerified) {
setDbConnected(true, dbPath)
}
@@ -1056,19 +1083,26 @@ function SettingsPage() {
</div>
<div className="form-group">
<label> wxid</label>
<span className="form-hint"> db_storage </span>
<label></label>
<span className="form-hint"></span>
<input
type="text"
placeholder="例如: wxid_xxxxxx"
placeholder="例如: wxid_xxxxxx 或其他账号目录名"
value={wxid}
onChange={(e) => setWxid(e.target.value)}
onChange={(e) => {
setWxid(e.target.value)
setIsAccountVerified(false)
}}
/>
<div className="btn-row">
<button className="btn btn-secondary" onClick={handleScanWxid} disabled={isScanningWxid}>
<Search size={16} /> {isScanningWxid ? '扫描中...' : '扫描 wxid'}
<Search size={16} /> {isScanningWxid ? '扫描中...' : '扫描账号目录'}
</button>
<button className="btn btn-secondary" onClick={handleVerifyAccountDirectory} disabled={isVerifyingAccount || !wxid || decryptKey.length !== 64}>
<Check size={16} /> {isVerifyingAccount ? '验证中...' : '验证账号目录'}
</button>
</div>
<span className="form-hint">{isAccountVerified ? '✅ 已验证' : '⚠️ 未验证'}</span>
{/* 多账号选择列表 */}
{showWxidDropdown && wxidOptions.length > 1 && (

View File

@@ -39,6 +39,8 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
const [cachePath, setCachePath] = useState('')
const [wxid, setWxid] = useState('')
const [wxidOptions, setWxidOptions] = useState<string[]>([])
const [isAccountVerified, setIsAccountVerified] = useState(false)
const [isVerifyingAccount, setIsVerifyingAccount] = useState(false)
const [error, setError] = useState('')
const [isScanningWxid, setIsScanningWxid] = useState(false)
@@ -134,10 +136,38 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
useEffect(() => {
setWxidOptions([])
setIsAccountVerified(false)
// 注意:不要清空 wxid因为它可能是从缓存加载的
// setWxid('')
}, [dbPath])
const verifyAccountDirectory = async (candidateWxid: string, key: string, silent = false) => {
if (!dbPath || !candidateWxid || key.length !== 64) {
setIsAccountVerified(false)
return false
}
setIsVerifyingAccount(true)
try {
const result = await window.electronAPI.wcdb.testConnection(dbPath, key, candidateWxid)
if (result.success) {
setIsAccountVerified(true)
if (!silent) setDbKeyStatus(`账号目录验证成功:${candidateWxid}`)
return true
}
setIsAccountVerified(false)
if (!silent) setError(result.error || '账号目录验证失败,请重新选择')
return false
} catch (e) {
setIsAccountVerified(false)
if (!silent) setError(`账号目录验证失败: ${e}`)
return false
} finally {
setIsVerifyingAccount(false)
}
}
// 保存配置到缓存
useEffect(() => {
const config = {
@@ -223,8 +253,9 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
try {
const wxids = await window.electronAPI.dbPath.scanWxids(dbPath)
setWxidOptions(wxids)
setIsAccountVerified(false)
if (wxids.length > 0) {
// 优先选择以 wxid_ 开头的账号
// 密钥前仅做候选识别,默认优先 wxid_ 前缀目录
const wxidAccount = wxids.find(id => id.startsWith('wxid_'))
const selectedWxid = wxidAccount || wxids[0]
setWxid(selectedWxid)
@@ -250,16 +281,37 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
const result = await window.electronAPI.wxKey.startGetKey(wechatPath)
if (result.success && result.key) {
setDecryptKey(result.key)
setDbKeyStatus('密钥获取成功,正在识别账号...')
setDbKeyStatus('密钥获取成功,正在验证账号目录...')
setError('')
setShowWechatPathPrompt(false)
// 先尝试当前登录账号检测(强信号)
let accountInfo: { wxid: string; dbPath: string } | null = null
if (dbPath) {
accountInfo = await window.electronAPI.wxKey.detectCurrentAccount(dbPath, 10)
if (!accountInfo) {
accountInfo = await window.electronAPI.wxKey.detectCurrentAccount(dbPath, 60)
}
}
if (accountInfo) {
setWxid(accountInfo.wxid)
const ok = await verifyAccountDirectory(accountInfo.wxid, result.key, true)
if (ok) {
setDbKeyStatus(`密钥获取成功,已验证账号目录: ${accountInfo.wxid}`)
return
}
}
const wxids = await handleScanWxid(true)
if (wxids.length > 1) {
setDbKeyStatus(`密钥获取成功,识别到 ${wxids.length} 个账号,请选择`)
// 多账号时仅作为候选,等待用户选择后再验证
setDbKeyStatus(`密钥获取成功,识别到 ${wxids.length} 个候选账号目录,请选择后验证`)
} else if (wxids.length === 1) {
setDbKeyStatus('密钥获取成功,已自动识别账号')
const ok = await verifyAccountDirectory(wxids[0], result.key, true)
setDbKeyStatus(ok ? '密钥获取成功,已自动识别并验证账号目录' : '密钥获取成功,请手动确认账号目录')
} else {
setDbKeyStatus('密钥获取成功')
setDbKeyStatus('密钥获取成功,请手动选择并验证账号目录')
}
} else {
if (result.needManualPath) {
@@ -352,8 +404,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
if (currentStep.id === 'intro') return true
if (currentStep.id === 'db') return Boolean(dbPath)
if (currentStep.id === 'cache') return Boolean(cachePath)
if (currentStep.id === 'key') return decryptKey.length === 64 && Boolean(wxid)
if (currentStep.id === 'key') return decryptKey.length === 64 && Boolean(wxid)
if (currentStep.id === 'key') return decryptKey.length === 64 && Boolean(wxid) && isAccountVerified
if (currentStep.id === 'image') return true
if (currentStep.id === 'security') return true
if (currentStep.id === 'decrypt') return false // 最后一步,不能下一步
@@ -366,7 +417,8 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
if (currentStep.id === 'cache' && !cachePath) setError('请填写缓存目录')
if (currentStep.id === 'key') {
if (decryptKey.length !== 64) setError('密钥长度必须为 64 个字符')
else if (!wxid) setError('未能自动识别 wxid请尝试重新获取或检查目录')
else if (!wxid) setError('请先选择账号目录')
else if (!isAccountVerified) setError('账号目录尚未验证,请先验证后继续')
}
return
}
@@ -381,7 +433,8 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
const handleStartDecrypt = async () => {
if (!dbPath) { setError('请先选择数据库目录'); return }
if (!wxid) { setError('请填写微信ID'); return }
if (!wxid) { setError('请先选择账号目录'); return }
if (!isAccountVerified) { setError('账号目录尚未验证,请先验证'); return }
if (!decryptKey || decryptKey.length !== 64) { setError('请填写 64 位解密密钥'); return }
setIsDecrypting(true)
@@ -770,13 +823,16 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
{currentStep.id === 'key' && (
<div className="setup-body">
<label className="field-label"> wxid</label>
<label className="field-label"></label>
<input
type="text"
className="field-input"
placeholder="获取密钥后将自动填充"
value={wxid}
onChange={(e) => setWxid(e.target.value)}
onChange={(e) => {
setWxid(e.target.value)
setIsAccountVerified(false)
}}
/>
{wxidOptions.length > 0 && (
<div className="wxid-options">
@@ -784,13 +840,31 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
<button
key={id}
className={`wxid-option ${wxid === id ? 'is-selected' : ''}`}
onClick={() => setWxid(id)}
onClick={async () => {
setWxid(id)
setIsAccountVerified(false)
if (decryptKey.length === 64) {
await verifyAccountDirectory(id, decryptKey)
}
}}
>
<div className="wxid-option-name">{id}</div>
</button>
))}
</div>
)}
<div className="button-row">
<button
className="btn btn-secondary btn-inline"
onClick={() => verifyAccountDirectory(wxid, decryptKey)}
disabled={isVerifyingAccount || !wxid || decryptKey.length !== 64}
>
{isVerifyingAccount ? '验证中...' : '验证账号目录'}
</button>
</div>
<div className="field-hint">
{isAccountVerified ? '✅ 已验证' : '⚠️ 未验证(密钥前只能识别候选目录)'}
</div>
<label className="field-label"></label>
<div className="field-with-toggle">
<input
@@ -832,7 +906,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
)}
{dbKeyStatus && <div className="field-hint status-text">{dbKeyStatus}</div>}
<div className="field-hint"></div>
<div className="field-hint"></div>
<div className="field-hint"><span style={{ color: 'red' }}>hook安装成功</span></div>
</div>
)}
@@ -938,8 +1012,8 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
<span className="summary-value">{cachePath || '未设置'}</span>
</div>
<div className="summary-item">
<span className="summary-label"></span>
<span className="summary-value">{wxid || '未设置'}</span>
<span className="summary-label"></span>
<span className="summary-value">{wxid ? `${wxid}${isAccountVerified ? '(已验证)' : '(未验证)'}` : '未设置'}</span>
</div>
<div className="summary-item">
<span className="summary-label"></span>