feat: 完善多账号连接诊断与聊天媒体导出

- 新增微信账号发现、环境诊断和分步数据库连接引导
- 支持按账号安全保存数据库密钥及快速切换账号
- 完善 WCDB 历史消息分片读取和分页状态提示
- 支持导出图片、视频和语音,提供原图优先及缩略图回退
- 更新安装指引、兼容版本说明和相关自动化测试
This commit is contained in:
Wxw-Gu
2026-08-03 10:43:14 +08:00
parent 224308f0e0
commit 08e1294e5d
37 changed files with 2011 additions and 255 deletions
+99 -33
View File
@@ -58,13 +58,25 @@ import * as chat from './services/chat-service'
import { apiServer } from './http-server'
import { skillResourceService } from './services/skill-resource-service'
import { testLocalApiRequest } from './services/local-api-test-service'
import { isWindowsWechatRunning } from './services/wechat-process-status'
import { isWechatRunning } from './services/wechat-process-status'
import {
inspectImageDecryptionStatus,
testImageDecryption
} from './services/image-decryption-status-service'
import type { SaveImageKeyRequest, TestImageDecryptionRequest } from '../shared/image-decryption'
import { loadSettings, saveSettings, getSettingsPath, AppSettings } from './services/settings-store'
import {
loadSettings,
saveSettings,
getSettingsPath,
AppSettings,
validateDbRoot
} from './services/settings-store'
import {
detectDataStructureVersion,
detectWechatVersion,
getOsVersionLabel
} from './services/connection-diagnostics'
import { buildSafeDiagnosticSummary } from '../shared/connection-diagnostics'
import {
flushBootstrapCacheWritesSync,
getBootstrapCache,
@@ -88,6 +100,7 @@ import { configureRecallArchive, RecallArchiveMonitor } from './services/recall-
import { VideoAssetService } from './video-asset-service'
import { cancelExport, revealExport, runExport } from './export-service'
import type { ExportRequest } from '../shared/export'
import { discoverAccounts } from './services/account-discovery'
// electron-vite can close the child's stdout/stderr after spawning Electron.
// Plain console.error then throws EPIPE on a closed pipe and crashes the IPC
@@ -479,7 +492,7 @@ app.whenReady().then(async () => {
return clearCache(scope)
})
ipcMain.handle('db:init', async (_, key: string) => {
ipcMain.handle('db:init', async (_, key: string, accountRoot?: string) => {
if (dbInitInFlight) return dbInitInFlight
dbInitInFlight = (async () => {
@@ -489,17 +502,35 @@ app.whenReady().then(async () => {
const trimmedKey = String(key || '').trim()
console.log(`db:init build=${BUILD_MARK} keyLength=${trimmedKey.length}`)
const settings = loadSettings()
const selectedRoot = String(accountRoot || settings.dbRoot || '').trim()
const rootValidation = validateDbRoot(selectedRoot)
if (!rootValidation.valid) {
return {
success: false,
code: 'ROOT_UNAVAILABLE',
error: rootValidation.error,
monitoring: false
}
}
if (!existsSync(join(selectedRoot, 'db_storage'))) {
return {
success: false,
code: 'ACCOUNT_SELECTION_REQUIRED',
error: '请先明确选择一个微信账号',
monitoring: false
}
}
if (
chat.isReady() &&
chat.getCurrentKey().replace(/^0x/i, '').trim() === trimmedKey.replace(/^0x/i, '') &&
(!settings.dbRoot || chat.getCurrentAccountRoot() === settings.dbRoot)
chat.getCurrentAccountRoot() === selectedRoot
) {
console.log('[WCDB4] db:init reuse current connection')
return { success: true, monitoring: true }
}
const nextWechatDb = await WechatDb.create(key, settings.dbRoot)
const nextWechatDb = await WechatDb.create(key, selectedRoot)
const resolvedRoot = nextWechatDb.getWcdb4Client().getAccountRoot()
if (resolvedRoot && resolvedRoot !== settings.dbRoot) {
if (resolvedRoot) {
// 同步更新 imageKeyRoot,避免自动获取图片密钥时扫描到错误目录
saveSettings({
...settings,
@@ -543,19 +574,43 @@ app.whenReady().then(async () => {
return dbInitInFlight
})
ipcMain.handle('key:getSavedDbKey', async () => databaseKeyStore.load())
ipcMain.handle('accounts:discover', (_, inputPath: string) =>
discoverAccounts(inputPath, databaseKeyStore, chat.getCurrentAccountRoot())
)
ipcMain.handle('key:getSavedDbKey', async (_, accountRoot: string) => {
const selectedRoot = String(accountRoot || '').trim()
const scoped = await databaseKeyStore.load(selectedRoot)
if (scoped.saved || !selectedRoot) return scoped
const legacy = await databaseKeyStore.loadLegacy()
if (!legacy.success || !legacy.key) return scoped
const validation = await chat.testConnection(legacy.key, selectedRoot)
if (!validation.success) return scoped
const migrated = await databaseKeyStore.save(selectedRoot, legacy.key)
if (migrated.success) await databaseKeyStore.clearLegacy()
return migrated
})
ipcMain.handle('key:getEnvironment', async () => {
const storage = await databaseKeyStore.getStatus()
const storage = await databaseKeyStore.getStatus(
chat.getCurrentAccountRoot() || loadSettings().dbRoot
)
const self = chat.getSelfAccountInfo()
return {
const settings = loadSettings()
const environment = {
platform: process.platform,
osVersion: getOsVersionLabel(),
appVersion: `v${app.getVersion()}`,
wechatVersion: await detectWechatVersion(),
dataStructureVersion: detectDataStructureVersion(settings.dbRoot),
dataDirectoryDetected: validateDbRoot(settings.dbRoot).valid,
autoDetectSupported: process.platform === 'win32',
wechatRunning: await isWindowsWechatRunning(),
wechatRunning: await isWechatRunning(),
accountIdentified: Boolean(self?.wxid),
dbConnected: chat.isReady(),
encryptionAvailable: storage.encryptionAvailable
}
return { ...environment, diagnosticSummary: buildSafeDiagnosticSummary(environment) }
})
ipcMain.handle('key:readClipboardDbKey', () => {
@@ -566,36 +621,47 @@ app.whenReady().then(async () => {
}
})
ipcMain.handle('key:pasteAndSaveDbKey', async () => {
ipcMain.handle('key:pasteAndSaveDbKey', async (_, accountRoot: string) => {
const clipboardKey = clipboard.readText().trim()
return databaseKeyStore.save(clipboardKey)
return databaseKeyStore.save(String(accountRoot || ''), clipboardKey)
})
ipcMain.handle('key:saveDbKey', async (_, key: string) =>
databaseKeyStore.save(String(key || ''))
ipcMain.handle('key:saveDbKey', async (_, accountRoot: string, key: string) =>
databaseKeyStore.save(String(accountRoot || ''), String(key || ''))
)
ipcMain.handle('key:clearSavedDbKey', async () => databaseKeyStore.clear())
ipcMain.handle('key:clearSavedDbKey', async (_, accountRoot: string) =>
databaseKeyStore.clear(String(accountRoot || ''))
)
ipcMain.handle('key:autoGetDbKey', async (event, options?: { save?: boolean }) => {
const onStatus = (message: string): void => {
if (!event.sender.isDestroyed()) event.sender.send('key:dbKeyStatus', { message })
ipcMain.handle(
'key:autoGetDbKey',
async (event, accountRoot: string, options?: { save?: boolean }) => {
const onStatus = (message: string): void => {
if (!event.sender.isDestroyed()) event.sender.send('key:dbKeyStatus', { message })
}
const result =
process.platform === 'win32'
? await keyServiceWin.autoGetDbKey(60_000, onStatus)
: await keyServiceMac.autoGetDbKey(onStatus)
if (!result.success || !result.key) return result
const selectedRoot = String(accountRoot || '').trim()
if (!selectedRoot) return { success: false, error: '请先选择微信账号' }
const validation = await chat.testConnection(result.key, selectedRoot)
if (!validation.success) {
return { ...result, success: false, key: undefined, error: '获取到的密钥不属于所选账号' }
}
if (options?.save === false) return result
const saved = await databaseKeyStore.save(selectedRoot, result.key)
return {
...result,
saved: saved.success,
warning: saved.success ? undefined : saved.error
}
}
const result =
process.platform === 'win32'
? await keyServiceWin.autoGetDbKey(60_000, onStatus)
: await keyServiceMac.autoGetDbKey(onStatus)
if (!result.success || !result.key) return result
if (options?.save === false) return result
const saved = await databaseKeyStore.save(result.key)
return {
...result,
saved: saved.success,
warning: saved.success ? undefined : saved.error
}
})
)
ipcMain.handle('key:autoGetImageKey', async (event, options?: { save?: boolean }) => {
const settings = loadSettings()