实现数据库密钥设置页

This commit is contained in:
电摇小子
2026-07-14 11:07:35 +08:00
parent ab24185670
commit f1ceef0e5e
21 changed files with 1264 additions and 77 deletions
+48 -23
View File
@@ -1,12 +1,7 @@
import { app, safeStorage } from 'electron'
import fs from 'fs-extra'
import path from 'path'
export interface StoredKeyResult {
success: boolean
key?: string
error?: string
}
import type { DatabaseKeyStorageResult } from '../shared/database-key'
const normalizeDatabaseKey = (value: string): string => value.trim().replace(/^0x/i, '')
@@ -18,37 +13,67 @@ export class DatabaseKeyStore {
return path.join(app.getPath('userData'), 'wechat-db-key.bin')
}
async load(): Promise<StoredKeyResult> {
try {
if (!(await fs.pathExists(this.filePath))) return { success: true }
if (!safeStorage.isEncryptionAvailable()) {
return { success: false, error: '系统安全存储不可用' }
}
const encrypted = await fs.readFile(this.filePath)
const key = normalizeDatabaseKey(safeStorage.decryptString(encrypted))
if (!isValidDatabaseKey(key)) return { success: false, error: '已保存的密钥格式无效' }
return { success: true, key }
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) }
async getStatus(): Promise<{ saved: boolean; encryptionAvailable: boolean }> {
return {
saved: await fs.pathExists(this.filePath),
encryptionAvailable: safeStorage.isEncryptionAvailable()
}
}
async save(rawKey: string): Promise<StoredKeyResult> {
async load(): Promise<DatabaseKeyStorageResult> {
try {
const status = await this.getStatus()
if (!status.saved) return { success: true, ...status }
if (!status.encryptionAvailable) {
return { success: false, error: '系统安全存储不可用', ...status }
}
const encrypted = await fs.readFile(this.filePath)
const key = normalizeDatabaseKey(safeStorage.decryptString(encrypted))
if (!isValidDatabaseKey(key)) {
return { success: false, error: '已保存的密钥格式无效', ...status }
}
return { success: true, key, ...status }
} catch (error) {
const status = await this.getStatus()
return {
success: false,
error: error instanceof Error ? error.message : String(error),
...status
}
}
}
async save(rawKey: string): Promise<DatabaseKeyStorageResult> {
const key = normalizeDatabaseKey(rawKey)
if (!isValidDatabaseKey(key)) {
return { success: false, error: '密钥必须是 64 位十六进制字符' }
return {
success: false,
error: '密钥必须是 64 位十六进制字符',
saved: await fs.pathExists(this.filePath),
encryptionAvailable: safeStorage.isEncryptionAvailable()
}
}
if (!safeStorage.isEncryptionAvailable()) {
return { success: false, error: '系统安全存储不可用' }
return {
success: false,
error: '系统安全存储不可用',
saved: await fs.pathExists(this.filePath),
encryptionAvailable: false
}
}
try {
await fs.ensureDir(path.dirname(this.filePath))
await fs.writeFile(this.filePath, safeStorage.encryptString(key), { mode: 0o600 })
await fs.chmod(this.filePath, 0o600)
return { success: true, key }
return { success: true, key, saved: true, encryptionAvailable: true }
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) }
return {
success: false,
error: error instanceof Error ? error.message : String(error),
saved: await fs.pathExists(this.filePath),
encryptionAvailable: true
}
}
}
+25 -1
View File
@@ -34,6 +34,7 @@ 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 { loadSettings, saveSettings, getSettingsPath, AppSettings } from './services/settings-store'
import {
getBootstrapCache,
@@ -194,6 +195,27 @@ app.whenReady().then(async () => {
ipcMain.handle('key:getSavedDbKey', async () => databaseKeyStore.load())
ipcMain.handle('key:getEnvironment', async () => {
const storage = await databaseKeyStore.getStatus()
const self = chat.getSelfAccountInfo()
return {
platform: process.platform,
autoDetectSupported: process.platform === 'win32',
wechatRunning: await isWindowsWechatRunning(),
accountIdentified: Boolean(self?.wxid),
dbConnected: chat.isReady(),
encryptionAvailable: storage.encryptionAvailable
}
})
ipcMain.handle('key:readClipboardDbKey', () => {
try {
return { success: true, value: clipboard.readText().trim() }
} catch {
return { success: false, error: '无法读取剪贴板' }
}
})
ipcMain.handle('key:pasteAndSaveDbKey', async () => {
const clipboardKey = clipboard.readText().trim()
return databaseKeyStore.save(clipboardKey)
@@ -205,7 +227,7 @@ app.whenReady().then(async () => {
ipcMain.handle('key:clearSavedDbKey', async () => databaseKeyStore.clear())
ipcMain.handle('key:autoGetDbKey', async (event) => {
ipcMain.handle('key:autoGetDbKey', async (event, options?: { save?: boolean }) => {
const onStatus = (message: string): void => {
if (!event.sender.isDestroyed()) event.sender.send('key:dbKeyStatus', { message })
}
@@ -215,6 +237,8 @@ app.whenReady().then(async () => {
: 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,
+83 -29
View File
@@ -4,6 +4,10 @@ import {
parseMessageContent,
parseStickerMessageFromRow
} from '../message-parser'
import type {
DatabaseKeyValidationCode,
DatabaseKeyValidationResult
} from '../../shared/database-key'
export function getCurrentKey(): string {
if (!dbRef) return ''
@@ -239,11 +243,7 @@ export function listMessages(
}
}
if (
!contentData &&
typeof content === 'string' &&
/^[0-9a-fA-F]{64,}$/.test(content.trim())
) {
if (!contentData && typeof content === 'string' && /^[0-9a-fA-F]{64,}$/.test(content.trim())) {
const parsed = parseStickerMessageFromRow(msg, content)
if (parsed.type === 'sticker') {
if (!parsed.url && parsed.md5) {
@@ -324,8 +324,7 @@ export function resolveMd5(query: string): FormattedContact | null {
const partial = contacts.find(
(c) =>
c.m_nsNickName.toLowerCase().includes(lower) ||
c.m_nsUsrName.toLowerCase().includes(lower)
c.m_nsNickName.toLowerCase().includes(lower) || c.m_nsUsrName.toLowerCase().includes(lower)
)
return partial || null
}
@@ -377,32 +376,87 @@ export function getSelfAccountInfo(): SelfAccountInfo | null {
}
}
export function testConnection(
key: string,
accountRoot?: string
): { success: boolean; error?: string; accountRoot?: string; wxid?: string } {
try {
const probeKey = key.replace(/^0x/i, '').trim()
if (!probeKey) {
return { success: false, error: '密钥不能为空' }
}
const probe = accountRoot ? new WechatDb(probeKey, accountRoot) : new WechatDb(probeKey)
try {
probe.close()
} catch {
// best effort
}
return {
success: true,
accountRoot: probe.getWcdb4Client().getAccountRoot(),
wxid: (probe.getWcdb4Client().getMyUsernameCandidates?.() ?? [])[0] || ''
}
} catch (error) {
export function testConnection(key: string, accountRoot?: string): DatabaseKeyValidationResult {
const probeKey = key.replace(/^0x/i, '').trim()
if (!/^[0-9a-f]{64}$/i.test(probeKey)) {
return {
success: false,
error: error instanceof Error ? error.message : String(error)
code: 'INVALID_FORMAT',
error: '密钥格式不正确'
}
}
let probe: WechatDb | null = null
let ownsProbe = false
try {
const current = dbRef
const canReuseCurrent =
current &&
getCurrentKey().replace(/^0x/i, '').trim() === probeKey &&
(!accountRoot || getCurrentAccountRoot() === accountRoot)
const validationDb = canReuseCurrent
? current
: accountRoot
? new WechatDb(probeKey, accountRoot)
: new WechatDb(probeKey)
probe = validationDb
ownsProbe = validationDb !== current
const client = validationDb.getWcdb4Client()
const contacts = client.getSessions()
const messages = client.getChatTables()
if (contacts[0]) client.getMessages(contacts[0].username, undefined, undefined, { limit: 1 })
return {
success: true,
accountRoot: client.getAccountRoot(),
wxid: (client.getMyUsernameCandidates?.() ?? [])[0] || '',
contacts: { available: true, count: contacts.length },
messages: { available: true, count: messages.length }
}
} catch (error) {
const detail = error instanceof Error ? error.message : String(error)
const code = mapConnectionError(detail)
return {
success: false,
code,
error: DATABASE_KEY_ERROR_MESSAGES[code]
}
} finally {
try {
// macOS native shutdown is process-wide. Do not tear down the active reader
// when validating a replacement key; the new runtime connection takes over on save.
if (ownsProbe && !(process.platform === 'darwin' && dbRef)) probe?.close()
} catch {
// Validation probes are best-effort closed without exposing native details.
}
}
}
const DATABASE_KEY_ERROR_MESSAGES: Record<DatabaseKeyValidationCode, string> = {
INVALID_FORMAT: '密钥格式不正确',
DATABASE_OPEN_FAILED: '无法打开数据库',
ACCOUNT_MISMATCH: '密钥与当前账号不匹配',
ROOT_UNAVAILABLE: '当前数据库目录不可用',
DATABASE_FILE_MISSING: '数据库文件缺失',
UNKNOWN_VALIDATION_ERROR: '未知验证错误'
}
function mapConnectionError(detail: string): DatabaseKeyValidationCode {
const normalized = detail.toLowerCase()
if (normalized.includes('-1005') || normalized.includes('不匹配')) return 'ACCOUNT_MISMATCH'
if (normalized.includes('session.db') || normalized.includes('数据库文件')) {
return 'DATABASE_FILE_MISSING'
}
if (
normalized.includes('数据目录') ||
normalized.includes('账号目录') ||
normalized.includes('db_storage')
) {
return 'ROOT_UNAVAILABLE'
}
if (normalized.includes('wcdb_open_account') || normalized.includes('open')) {
return 'DATABASE_OPEN_FAILED'
}
return 'UNKNOWN_VALIDATION_ERROR'
}
export function reopenWithRoot(accountRoot: string): boolean {
@@ -0,0 +1,23 @@
import { execFile } from 'child_process'
import { promisify } from 'util'
const execFileAsync = promisify(execFile)
const WINDOWS_WECHAT_IMAGES = ['Weixin.exe', 'WeChat.exe']
export async function isWindowsWechatRunning(): Promise<boolean> {
if (process.platform !== 'win32') return false
for (const imageName of WINDOWS_WECHAT_IMAGES) {
try {
const { stdout } = await execFileAsync(
'tasklist',
['/FI', `IMAGENAME eq ${imageName}`, '/FO', 'CSV', '/NH'],
{ windowsHide: true }
)
if (stdout.toLowerCase().includes(`"${imageName.toLowerCase()}"`)) return true
} catch {
// Try the other supported WeChat executable name.
}
}
return false
}