mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-05-06 15:09:42 +00:00
@@ -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 () => {
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
'连接目标账号数据库超时,请检查数据库路径、密钥是否正确'
|
'连接目标账号数据库超时,请检查数据库路径、密钥是否正确'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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()
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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' }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user