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