This commit is contained in:
cc
2026-05-02 19:08:07 +08:00
parent 318b553d0e
commit 1e3a496021
15 changed files with 251 additions and 86 deletions

View File

@@ -2208,11 +2208,21 @@ function registerIpcHandlers() {
// WCDB 数据库相关 // WCDB 数据库相关
ipcMain.handle('wcdb:testConnection', async (_, dbPath: string, hexKey: string, wxid: string) => { ipcMain.handle('wcdb:testConnection', async (_, dbPath: string, hexKey: string, wxid: string) => {
return wcdbService.testConnection(dbPath, hexKey, wxid) const cfg = configService || new ConfigService()
const accountDir = cfg.getAccountDir(dbPath, wxid)
if (!accountDir) {
return { success: false, error: '未找到账号目录' }
}
return wcdbService.testConnection(accountDir, hexKey)
}) })
ipcMain.handle('wcdb:open', async (_, dbPath: string, hexKey: string, wxid: string) => { ipcMain.handle('wcdb:open', async (_, dbPath: string, hexKey: string, wxid: string) => {
return wcdbService.open(dbPath, hexKey, wxid) const cfg = configService || new ConfigService()
const accountDir = cfg.getAccountDir(dbPath, wxid)
if (!accountDir) {
return false
}
return wcdbService.open(accountDir, hexKey)
}) })
ipcMain.handle('wcdb:close', async () => { ipcMain.handle('wcdb:close', async () => {

View File

@@ -131,9 +131,13 @@ class AnalyticsService {
if (!dbPath) return { success: false, error: '未配置数据库路径' } if (!dbPath) return { success: false, error: '未配置数据库路径' }
if (!decryptKey) return { success: false, error: '未配置解密密钥' } if (!decryptKey) return { success: false, error: '未配置解密密钥' }
const cleanedWxid = this.cleanAccountDirName(wxid) const accountDir = this.configService.getAccountDir(dbPath, wxid)
const ok = await wcdbService.open(dbPath, decryptKey, cleanedWxid) if (!accountDir) return { success: false, error: '未找到账号目录' }
const ok = await wcdbService.open(accountDir, decryptKey)
if (!ok) return { success: false, error: 'WCDB 打开失败' } if (!ok) return { success: false, error: 'WCDB 打开失败' }
const cleanedWxid = this.cleanAccountDirName(wxid)
return { success: true, cleanedWxid } return { success: true, cleanedWxid }
} }

View File

@@ -1,5 +1,6 @@
import { parentPort } from 'worker_threads' import { parentPort } from 'worker_threads'
import { wcdbService } from './wcdbService' import { wcdbService } from './wcdbService'
import { ConfigService } from './config'
export interface TopContact { export interface TopContact {
username: string username: string
@@ -158,9 +159,14 @@ class AnnualReportService {
if (!dbPath) return { success: false, error: '未配置数据库路径' } if (!dbPath) return { success: false, error: '未配置数据库路径' }
if (!decryptKey) return { success: false, error: '未配置解密密钥' } if (!decryptKey) return { success: false, error: '未配置解密密钥' }
const cleanedWxid = this.cleanAccountDirName(wxid) const configService = ConfigService.getInstance()
const ok = await wcdbService.open(dbPath, decryptKey, cleanedWxid) const accountDir = configService.getAccountDir(dbPath, wxid)
if (!accountDir) return { success: false, error: '未找到账号目录' }
const ok = await wcdbService.open(accountDir, decryptKey)
if (!ok) return { success: false, error: 'WCDB 打开失败' } if (!ok) return { success: false, error: 'WCDB 打开失败' }
const cleanedWxid = this.cleanAccountDirName(wxid)
return { success: true, cleanedWxid, rawWxid: wxid } return { success: true, cleanedWxid, rawWxid: wxid }
} }

View File

@@ -454,14 +454,14 @@ export class BackupService {
if (!wxid || !dbPath) return { success: false, error: '请先配置数据库路径和微信账号' } if (!wxid || !dbPath) return { success: false, error: '请先配置数据库路径和微信账号' }
if (!decryptKey) return { success: false, error: '请先配置数据库解密密钥' } if (!decryptKey) return { success: false, error: '请先配置数据库解密密钥' }
const accountDir = this.resolveAccountDir(dbPath, wxid) // 使用 ConfigService 统一解析账号目录
const accountDir = this.configService.getAccountDir(dbPath, wxid)
if (!accountDir) return { success: false, error: `未在配置的 dbPath 下找到账号目录:${wxid}` } if (!accountDir) return { success: false, error: `未在配置的 dbPath 下找到账号目录:${wxid}` }
const dbStorage = join(accountDir, 'db_storage') const dbStorage = join(accountDir, 'db_storage')
if (!existsSync(dbStorage)) return { success: false, error: '未找到 db_storage 目录' } if (!existsSync(dbStorage)) return { success: false, error: '未找到 db_storage 目录' }
const accountDirName = basename(accountDir)
const opened = await withTimeout( const opened = await withTimeout(
wcdbService.open(dbPath, decryptKey, accountDirName), wcdbService.open(accountDir, decryptKey),
15000, 15000,
'连接目标账号数据库超时,请检查数据库路径、密钥是否正确' '连接目标账号数据库超时,请检查数据库路径、密钥是否正确'
) )

View File

@@ -553,8 +553,13 @@ class ChatService {
return { success: false, error: '请先在设置页面配置解密密钥' } return { success: false, error: '请先在设置页面配置解密密钥' }
} }
const cleanedWxid = this.cleanAccountDirName(wxid) // 使用 ConfigService 统一解析账号目录
const openOk = await wcdbService.open(dbPath, decryptKey, cleanedWxid) const accountDir = this.configService.getAccountDir(dbPath, wxid)
if (!accountDir) {
return { success: false, error: '未找到账号目录请检查数据库路径和微信ID配置' }
}
const openOk = await wcdbService.open(accountDir, decryptKey)
if (!openOk) { if (!openOk) {
const detailedError = this.toCodeOnlyMessage(await wcdbService.getLastInitError()) const detailedError = this.toCodeOnlyMessage(await wcdbService.getLastInitError())
await this.maybeShowInitFailureDialog(detailedError) await this.maybeShowInitFailureDialog(detailedError)

View File

@@ -1,4 +1,5 @@
import { join } from 'path' import { join } from 'path'
import { existsSync, readdirSync, statSync } from 'fs'
import { app, safeStorage } from 'electron' import { app, safeStorage } from 'electron'
import crypto from 'crypto' import crypto from 'crypto'
import Store from 'electron-store' import Store from 'electron-store'
@@ -145,6 +146,9 @@ export class ConfigService {
private unlockedKeys: Map<string, any> = new Map() private unlockedKeys: Map<string, any> = new Map()
private unlockPassword: string | null = null private unlockPassword: string | null = null
// 账号目录缓存
private accountDirCache: Map<string, string> = new Map()
static getInstance(): ConfigService { static getInstance(): ConfigService {
if (!ConfigService.instance) { if (!ConfigService.instance) {
ConfigService.instance = new ConfigService() ConfigService.instance = new ConfigService()
@@ -839,6 +843,99 @@ export class ConfigService {
} }
} }
/**
* 清理账号目录名称(移除后缀)
*/
private cleanAccountDirName(dirName: string): string {
const trimmed = dirName.trim()
if (!trimmed) return trimmed
// wxid_ 开头的特殊处理
if (trimmed.toLowerCase().startsWith('wxid_')) {
const match = trimmed.match(/^(wxid_[^_]+)/i)
if (match) return match[1]
return trimmed
}
// 移除4位后缀
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
if (suffixMatch) return suffixMatch[1]
return trimmed
}
/**
* 检查是否是目录
*/
private isDirectory(path: string): boolean {
try {
return statSync(path).isDirectory()
} catch {
return false
}
}
/**
* 获取账号目录路径
* 统一的账号目录解析方法,所有服务应该使用此方法而不是自己实现
*
* @param dbPath 数据库根目录(可选,默认从配置读取)
* @param wxid 微信ID可选默认从配置读取
* @returns 账号目录的完整路径,如果找不到返回 null
*/
getAccountDir(dbPath?: string, wxid?: string): string | null {
const actualDbPath = dbPath || this.get('dbPath')
const actualWxid = wxid || this.get('myWxid')
if (!actualDbPath || !actualWxid) return null
const cleanedWxid = this.cleanAccountDirName(actualWxid)
const normalized = actualDbPath.replace(/[\\/]+$/, '')
const cacheKey = `${normalized}|${cleanedWxid.toLowerCase()}`
// 检查缓存
const cached = this.accountDirCache.get(cacheKey)
if (cached && existsSync(cached)) return cached
if (cached && !existsSync(cached)) {
this.accountDirCache.delete(cacheKey)
}
// 尝试直接路径(非 wxid_ 开头的账号)
const lowerWxid = cleanedWxid.toLowerCase()
if (!lowerWxid.startsWith('wxid_')) {
const direct = join(normalized, cleanedWxid)
if (existsSync(direct) && this.isDirectory(direct)) {
this.accountDirCache.set(cacheKey, direct)
return direct
}
}
// 扫描目录查找匹配的账号目录
try {
const entries = readdirSync(normalized)
for (const entry of entries) {
const entryPath = join(normalized, entry)
if (!this.isDirectory(entryPath)) continue
const lowerEntry = entry.toLowerCase()
const isExactMatch = lowerEntry === lowerWxid
const isSuffixMatch = lowerEntry.startsWith(`${lowerWxid}_`)
// wxid_ 开头只接受带后缀的目录;其他账号精确匹配或带后缀都可以
const shouldMatch = lowerWxid.startsWith('wxid_')
? isSuffixMatch
: (isExactMatch || isSuffixMatch)
if (shouldMatch) {
this.accountDirCache.set(cacheKey, entryPath)
return entryPath
}
}
} catch { }
return null
}
private getUserDataPath(): string { private getUserDataPath(): string {
const workerUserDataPath = String(process.env.WEFLOW_USER_DATA_PATH || process.env.WEFLOW_CONFIG_CWD || '').trim() const workerUserDataPath = String(process.env.WEFLOW_USER_DATA_PATH || process.env.WEFLOW_CONFIG_CWD || '').trim()
if (workerUserDataPath) { if (workerUserDataPath) {

View File

@@ -160,6 +160,16 @@ export class DbPathService {
// 检查是否有有效账号目录结构 // 检查是否有有效账号目录结构
if (this.isAccountDir(entryPath)) { if (this.isAccountDir(entryPath)) {
// 过滤掉不带后缀的 wxid_ 目录
const lowerEntry = entry.toLowerCase()
if (lowerEntry.startsWith('wxid_')) {
// wxid_ 开头的目录必须带后缀wxid_xxx_yyyy 格式)
const parts = entry.split('_')
if (parts.length <= 2) {
// wxid_xxx 格式,跳过
continue
}
}
accounts.push(entry) accounts.push(entry)
} }
} }
@@ -232,6 +242,16 @@ export class DbPathService {
const lower = entry.toLowerCase() const lower = entry.toLowerCase()
if (lower === 'all_users') continue if (lower === 'all_users') continue
if (!entry.includes('_')) continue if (!entry.includes('_')) continue
// 过滤掉不带后缀的 wxid_ 目录
if (lower.startsWith('wxid_')) {
const parts = entry.split('_')
if (parts.length <= 2) {
// wxid_xxx 格式,跳过
continue
}
}
wxids.push({ wxid: entry, modifiedTime: stat.mtimeMs }) wxids.push({ wxid: entry, modifiedTime: stat.mtimeMs })
} }
} }

View File

@@ -514,50 +514,11 @@ export class ImageDecryptService {
} }
private resolveAccountDir(dbPath: string, wxid: string): string | null { private resolveAccountDir(dbPath: string, wxid: string): string | null {
const cleanedWxid = this.cleanAccountDirName(wxid) return this.configService.getAccountDir(dbPath, wxid)
const normalized = dbPath.replace(/[\\/]+$/, '')
const cacheKey = `${normalized}|${cleanedWxid.toLowerCase()}`
const cached = this.accountDirCache.get(cacheKey)
if (cached && existsSync(cached)) return cached
if (cached && !existsSync(cached)) {
this.accountDirCache.delete(cacheKey)
}
const direct = join(normalized, cleanedWxid)
if (existsSync(direct)) {
this.accountDirCache.set(cacheKey, direct)
return direct
}
if (this.isAccountDir(normalized)) {
this.accountDirCache.set(cacheKey, normalized)
return normalized
}
try {
const entries = readdirSync(normalized)
const lowerWxid = cleanedWxid.toLowerCase()
for (const entry of entries) {
const entryPath = join(normalized, entry)
if (!this.isDirectory(entryPath)) continue
const lowerEntry = entry.toLowerCase()
if (lowerEntry === lowerWxid || lowerEntry.startsWith(`${lowerWxid}_`)) {
if (this.isAccountDir(entryPath)) {
this.accountDirCache.set(cacheKey, entryPath)
return entryPath
}
}
}
} catch { }
return null
} }
private resolveCurrentAccountDir(): string | null { private resolveCurrentAccountDir(): string | null {
const wxid = this.getConfiguredMyWxid() return this.configService.getAccountDir()
const dbPath = this.getConfiguredDbPath()
if (!wxid || !dbPath) return null
return this.resolveAccountDir(dbPath, wxid)
} }
/** /**

View File

@@ -131,6 +131,14 @@ class VideoService {
if (dbPathContainsWxid) { if (dbPathContainsWxid) {
return join(dbPath, 'msg', 'video') return join(dbPath, 'msg', 'video')
} }
// 使用 ConfigService 的统一账号目录解析
const accountDir = this.configService.getAccountDir(dbPath, wxid)
if (accountDir) {
return join(accountDir, 'msg', 'video')
}
// 回退到原始逻辑
return join(dbPath, wxid, 'msg', 'video') return join(dbPath, wxid, 'msg', 'video')
} }
@@ -144,6 +152,13 @@ class VideoService {
return [join(dbPath, 'db_storage', 'hardlink', 'hardlink.db')] return [join(dbPath, 'db_storage', 'hardlink', 'hardlink.db')]
} }
// 使用 ConfigService 的统一账号目录解析
const accountDir = this.configService.getAccountDir(dbPath, wxid)
if (accountDir) {
return [join(accountDir, 'db_storage', 'hardlink', 'hardlink.db')]
}
// 回退到原始逻辑
return [ return [
join(dbPath, wxid, 'db_storage', 'hardlink', 'hardlink.db'), join(dbPath, wxid, 'db_storage', 'hardlink', 'hardlink.db'),
join(dbPath, cleanedWxid, 'db_storage', 'hardlink', 'hardlink.db') join(dbPath, cleanedWxid, 'db_storage', 'hardlink', 'hardlink.db')

View File

@@ -1260,13 +1260,12 @@ export class WcdbCore {
/** /**
* 测试数据库连接 * 测试数据库连接
*/ */
async testConnection(dbPath: string, hexKey: string, wxid: string): Promise<{ success: boolean; error?: string; sessionCount?: number }> { async testConnection(accountDir: string, hexKey: string): Promise<{ success: boolean; error?: string; sessionCount?: number }> {
try { try {
// 如果当前已经有相同参数的活动连接,直接返回成功 // 如果当前已经有相同参数的活动连接,直接返回成功
if (this.handle !== null && if (this.handle !== null &&
this.currentPath === dbPath && this.currentPath === accountDir &&
this.currentKey === hexKey && this.currentKey === hexKey) {
this.currentWxid === wxid) {
return { success: true, sessionCount: 0 } return { success: true, sessionCount: 0 }
} }
@@ -1284,9 +1283,9 @@ export class WcdbCore {
} }
} }
// 构建 db_storage 目录路径 // 直接使用账号目录
const dbStoragePath = this.resolveDbStoragePath(dbPath, wxid) const dbStoragePath = join(accountDir, 'db_storage')
this.writeLog(`testConnection dbPath=${dbPath} wxid=${wxid} dbStorage=${dbStoragePath || 'null'}`) this.writeLog(`testConnection accountDir=${accountDir} dbStorage=${dbStoragePath}`)
if (!dbStoragePath || !existsSync(dbStoragePath)) { if (!dbStoragePath || !existsSync(dbStoragePath)) {
return { success: false, error: this.formatInitProtectionError(-3001) } return { success: false, error: this.formatInitProtectionError(-3001) }
@@ -1329,9 +1328,9 @@ export class WcdbCore {
} }
// 恢复测试前的连接(如果之前有活动连接) // 恢复测试前的连接(如果之前有活动连接)
if (hadActiveConnection && prevPath && prevKey && prevWxid) { if (hadActiveConnection && prevPath && prevKey) {
try { try {
await this.open(prevPath, prevKey, prevWxid) await this.open(prevPath, prevKey)
} catch { } catch {
// 恢复失败则保持断开,由调用方处理 // 恢复失败则保持断开,由调用方处理
} }
@@ -1536,7 +1535,7 @@ export class WcdbCore {
/** /**
* 打开数据库 * 打开数据库
*/ */
async open(dbPath: string, hexKey: string, wxid: string): Promise<boolean> { async open(accountDir: string, hexKey: string): Promise<boolean> {
try { try {
lastDllInitError = null lastDllInitError = null
if (!this.initialized) { if (!this.initialized) {
@@ -1546,9 +1545,8 @@ export class WcdbCore {
// 检查是否已经是当前连接的参数,如果是则直接返回成功,实现"始终保持链接" // 检查是否已经是当前连接的参数,如果是则直接返回成功,实现"始终保持链接"
if (this.handle !== null && if (this.handle !== null &&
this.currentPath === dbPath && this.currentPath === accountDir &&
this.currentKey === hexKey && this.currentKey === hexKey) {
this.currentWxid === wxid) {
return true return true
} }
@@ -1560,12 +1558,12 @@ export class WcdbCore {
if (!initOk) return false if (!initOk) return false
} }
const dbStoragePath = this.resolveDbStoragePath(dbPath, wxid) const dbStoragePath = join(accountDir, 'db_storage')
this.writeLog(`open dbPath=${dbPath} wxid=${wxid} dbStorage=${dbStoragePath || 'null'}`, true) this.writeLog(`open accountDir=${accountDir} dbStorage=${dbStoragePath}`, true)
if (!dbStoragePath || !existsSync(dbStoragePath)) { if (!dbStoragePath || !existsSync(dbStoragePath)) {
console.error('数据库目录不存在:', dbPath) console.error('数据库目录不存在:', accountDir)
this.writeLog(`open failed: dbStorage not found for ${dbPath}`) this.writeLog(`open failed: dbStorage not found for ${accountDir}`)
lastDllInitError = this.formatInitProtectionError(-3001) lastDllInitError = this.formatInitProtectionError(-3001)
return false return false
} }
@@ -1596,8 +1594,11 @@ export class WcdbCore {
return false return false
} }
// 从账号目录路径中提取 wxid目录名
const wxid = basename(accountDir)
this.handle = handle this.handle = handle
this.currentPath = dbPath this.currentPath = accountDir
this.currentKey = hexKey this.currentKey = hexKey
this.currentWxid = wxid this.currentWxid = wxid
this.currentDbStoragePath = dbStoragePath this.currentDbStoragePath = dbStoragePath
@@ -1615,7 +1616,7 @@ export class WcdbCore {
} }
this.writeLog(`open ok handle=${handle}`, true) this.writeLog(`open ok handle=${handle}`, true)
await this.dumpDbStatus('open') await this.dumpDbStatus('open')
await this.runPostOpenDiagnostics(dbPath, dbStoragePath, sessionDbPath, wxid) await this.runPostOpenDiagnostics(accountDir, dbStoragePath, sessionDbPath, wxid)
return true return true
} catch (e) { } catch (e) {
console.error('打开数据库异常:', e) console.error('打开数据库异常:', e)

View File

@@ -154,15 +154,17 @@ export class WcdbService {
/** /**
* 测试数据库连接 * 测试数据库连接
*/ */
async testConnection(dbPath: string, hexKey: string, wxid: string): Promise<{ success: boolean; error?: string; sessionCount?: number }> { async testConnection(accountDir: string, hexKey: string): Promise<{ success: boolean; error?: string; sessionCount?: number }> {
return this.callWorker('testConnection', { dbPath, hexKey, wxid }) return this.callWorker('testConnection', { accountDir, hexKey })
} }
/** /**
* 打开数据库 * 打开数据库
* @param accountDir 账号目录的完整路径
* @param hexKey 解密密钥
*/ */
async open(dbPath: string, hexKey: string, wxid: string): Promise<boolean> { async open(accountDir: string, hexKey: string): Promise<boolean> {
return this.callWorker('open', { dbPath, hexKey, wxid }) return this.callWorker('open', { accountDir, hexKey })
} }
async getLastInitError(): Promise<string | null> { async getLastInitError(): Promise<string | null> {

View File

@@ -32,10 +32,10 @@ if (parentPort) {
break break
} }
case 'testConnection': case 'testConnection':
result = await core.testConnection(payload.dbPath, payload.hexKey, payload.wxid) result = await core.testConnection(payload.accountDir, payload.hexKey)
break break
case 'open': case 'open':
result = await core.open(payload.dbPath, payload.hexKey, payload.wxid) result = await core.open(payload.accountDir, payload.hexKey)
break break
case 'getLastInitError': case 'getLastInitError':
result = core.getLastInitError() result = core.getLastInitError()

View File

@@ -2245,11 +2245,28 @@
box-shadow: 0 0 0 2px var(--primary-light); box-shadow: 0 0 0 2px var(--primary-light);
} }
.image-unavailable.error {
background: rgba(239, 68, 68, 0.08);
border: 1px solid rgba(239, 68, 68, 0.2);
svg {
color: rgba(239, 68, 68, 0.8);
}
}
.image-unavailable:disabled { .image-unavailable:disabled {
cursor: default; cursor: default;
opacity: 0.7; opacity: 0.7;
} }
.image-error-reason {
font-size: 11px;
color: rgba(239, 68, 68, 0.9);
max-width: 140px;
word-break: break-word;
line-height: 1.3;
}
.image-action { .image-action {
font-size: 11px; font-size: 11px;
color: var(--text-quaternary); color: var(--text-quaternary);

View File

@@ -8370,6 +8370,8 @@ function MessageBubble({
// State variables... // State variables...
const [imageError, setImageError] = useState(false) const [imageError, setImageError] = useState(false)
const [imageErrorReason, setImageErrorReason] = useState<string | undefined>(undefined)
const [imageFailureKind, setImageFailureKind] = useState<'not_found' | 'decrypt_failed' | undefined>(undefined)
const [imageLoading, setImageLoading] = useState(false) const [imageLoading, setImageLoading] = useState(false)
const [imageLoaded, setImageLoaded] = useState(false) const [imageLoaded, setImageLoaded] = useState(false)
const [imageStageLockHeight, setImageStageLockHeight] = useState<number | null>(null) const [imageStageLockHeight, setImageStageLockHeight] = useState<number | null>(null)
@@ -8757,7 +8759,11 @@ function MessageBubble({
if (result.success && result.localPath) { if (result.success && result.localPath) {
const renderPath = toRenderableImageSrc(result.localPath) const renderPath = toRenderableImageSrc(result.localPath)
if (!renderPath) { if (!renderPath) {
if (!silent) setImageError(true) if (!silent) {
setImageError(true)
setImageErrorReason('路径无效')
setImageFailureKind('decrypt_failed')
}
return { success: false } return { success: false }
} }
imageDataUrlCache.set(imageCacheKey, renderPath) imageDataUrlCache.set(imageCacheKey, renderPath)
@@ -8769,6 +8775,10 @@ function MessageBubble({
setImageHasUpdate(false) setImageHasUpdate(false)
if (result.liveVideoPath) setImageLiveVideoPath(result.liveVideoPath) if (result.liveVideoPath) setImageLiveVideoPath(result.liveVideoPath)
return { ...result, localPath: renderPath } return { ...result, localPath: renderPath }
} else if (!silent && result.error) {
setImageError(true)
setImageErrorReason(result.error)
setImageFailureKind(result.failureKind)
} }
} }
@@ -8785,9 +8795,17 @@ function MessageBubble({
setImageHasUpdate(false) setImageHasUpdate(false)
return { success: true, localPath: dataUrl } return { success: true, localPath: dataUrl }
} }
if (!silent) setImageError(true) if (!silent) {
} catch { setImageError(true)
if (!silent) setImageError(true) setImageErrorReason('图片数据获取失败')
setImageFailureKind('not_found')
}
} catch (e) {
if (!silent) {
setImageError(true)
setImageErrorReason(e instanceof Error ? e.message : '解密异常')
setImageFailureKind('decrypt_failed')
}
} finally { } finally {
if (!silent) setImageLoading(false) if (!silent) setImageLoading(false)
imageDecryptPendingRef.current = false imageDecryptPendingRef.current = false
@@ -9636,14 +9654,15 @@ function MessageBubble({
</div> </div>
) : imageError || !imageLocalPath ? ( ) : imageError || !imageLocalPath ? (
<button <button
className={`image-unavailable ${imageClicked ? 'clicked' : ''}`} className={`image-unavailable ${imageClicked ? 'clicked' : ''} ${imageError ? 'error' : ''}`}
onClick={handleImageClick} onClick={handleImageClick}
disabled={imageLoading} disabled={imageLoading}
type="button" type="button"
> >
<ImageIcon size={24} /> <ImageIcon size={24} />
<span></span> <span>{imageError ? '解密失败' : '图片未解密'}</span>
<span className="image-action">{imageClicked ? '已点击…' : '点击解密'}</span> {imageErrorReason && <span className="image-error-reason">{imageErrorReason}</span>}
<span className="image-action">{imageClicked ? '已点击…' : '点击重试'}</span>
</button> </button>
) : ( ) : (
<> <>
@@ -9659,6 +9678,8 @@ function MessageBubble({
onLoad={() => { onLoad={() => {
setImageLoaded(true) setImageLoaded(true)
setImageError(false) setImageError(false)
setImageErrorReason(undefined)
setImageFailureKind(undefined)
stabilizeImageScrollAfterResize() stabilizeImageScrollAfterResize()
releaseImageStageLock() releaseImageStageLock()
}} }}

View File

@@ -1038,7 +1038,13 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
className="field-input" className="field-input"
placeholder="64 位十六进制密钥" placeholder="64 位十六进制密钥"
value={decryptKey} value={decryptKey}
onChange={(e) => setDecryptKey(e.target.value.trim())} onChange={(e) => {
const value = e.target.value.trim()
setDecryptKey(value)
if (value.length === 64) {
setHasReacquiredDbKey(true)
}
}}
/> />
<button type="button" className="toggle-btn" onClick={() => setShowDecryptKey(!showDecryptKey)}> <button type="button" className="toggle-btn" onClick={() => setShowDecryptKey(!showDecryptKey)}>
{showDecryptKey ? <EyeOff size={16} /> : <Eye size={16} />} {showDecryptKey ? <EyeOff size={16} /> : <Eye size={16} />}
@@ -1171,7 +1177,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
)} )}
<div className="field-hint" style={{ marginTop: '8px' }}> <div className="field-hint" style={{ marginTop: '8px' }}>
+ 使 "缓存计算 + 本地校验通过"使
</div> </div>
{isImageKeyVerified && ( {isImageKeyVerified && (
<div className="status-message is-success" style={{ marginTop: '8px' }}> <div className="status-message is-success" style={{ marginTop: '8px' }}>