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 数据库相关
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) => {
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 () => {

View File

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

View File

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

View File

@@ -454,14 +454,14 @@ export class BackupService {
if (!wxid || !dbPath) 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}` }
const dbStorage = join(accountDir, 'db_storage')
if (!existsSync(dbStorage)) return { success: false, error: '未找到 db_storage 目录' }
const accountDirName = basename(accountDir)
const opened = await withTimeout(
wcdbService.open(dbPath, decryptKey, accountDirName),
wcdbService.open(accountDir, decryptKey),
15000,
'连接目标账号数据库超时,请检查数据库路径、密钥是否正确'
)

View File

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

View File

@@ -1,4 +1,5 @@
import { join } from 'path'
import { existsSync, readdirSync, statSync } from 'fs'
import { app, safeStorage } from 'electron'
import crypto from 'crypto'
import Store from 'electron-store'
@@ -145,6 +146,9 @@ export class ConfigService {
private unlockedKeys: Map<string, any> = new Map()
private unlockPassword: string | null = null
// 账号目录缓存
private accountDirCache: Map<string, string> = new Map()
static getInstance(): ConfigService {
if (!ConfigService.instance) {
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 {
const workerUserDataPath = String(process.env.WEFLOW_USER_DATA_PATH || process.env.WEFLOW_CONFIG_CWD || '').trim()
if (workerUserDataPath) {

View File

@@ -160,6 +160,16 @@ export class DbPathService {
// 检查是否有有效账号目录结构
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)
}
}
@@ -232,6 +242,16 @@ export class DbPathService {
const lower = entry.toLowerCase()
if (lower === 'all_users') 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 })
}
}

View File

@@ -514,50 +514,11 @@ export class ImageDecryptService {
}
private resolveAccountDir(dbPath: string, wxid: string): string | null {
const cleanedWxid = this.cleanAccountDirName(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
return this.configService.getAccountDir(dbPath, wxid)
}
private resolveCurrentAccountDir(): string | null {
const wxid = this.getConfiguredMyWxid()
const dbPath = this.getConfiguredDbPath()
if (!wxid || !dbPath) return null
return this.resolveAccountDir(dbPath, wxid)
return this.configService.getAccountDir()
}
/**

View File

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

View File

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