mirror of
https://github.com/ILoveBingLu/CipherTalk.git
synced 2026-05-17 17:58:54 +08:00
feat: add multi-account storage support
This commit is contained in:
+71
-5
@@ -872,7 +872,7 @@ function createAgreementWindow() {
|
||||
/**
|
||||
* 创建首次引导窗口(独立无边框透明窗口)
|
||||
*/
|
||||
function createWelcomeWindow() {
|
||||
function createWelcomeWindow(mode: 'default' | 'add-account' = 'default') {
|
||||
// 如果已存在,聚焦
|
||||
if (welcomeWindow && !welcomeWindow.isDestroyed()) {
|
||||
welcomeWindow.focus()
|
||||
@@ -905,10 +905,12 @@ function createWelcomeWindow() {
|
||||
welcomeWindow?.show()
|
||||
})
|
||||
|
||||
const welcomeHash = mode === 'add-account' ? '/welcome-window?mode=add-account' : '/welcome-window'
|
||||
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
welcomeWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/welcome-window`)
|
||||
welcomeWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}#${welcomeHash}`)
|
||||
} else {
|
||||
welcomeWindow.loadFile(join(__dirname, '../dist/index.html'), { hash: '/welcome-window' })
|
||||
welcomeWindow.loadFile(join(__dirname, '../dist/index.html'), { hash: welcomeHash })
|
||||
}
|
||||
|
||||
welcomeWindow.on('closed', () => {
|
||||
@@ -1293,6 +1295,48 @@ function registerIpcHandlers() {
|
||||
return configService?.setTldCache(tlds)
|
||||
})
|
||||
|
||||
ipcMain.handle('accounts:list', async () => {
|
||||
return configService?.listAccounts() || []
|
||||
})
|
||||
|
||||
ipcMain.handle('accounts:getActive', async () => {
|
||||
return configService?.getActiveAccount() || null
|
||||
})
|
||||
|
||||
ipcMain.handle('accounts:setActive', async (_, accountId: string) => {
|
||||
return configService?.setActiveAccount(accountId) || null
|
||||
})
|
||||
|
||||
ipcMain.handle('accounts:save', async (_, profile: any) => {
|
||||
return configService?.saveAccount(profile) || null
|
||||
})
|
||||
|
||||
ipcMain.handle('accounts:update', async (_, accountId: string, patch: any) => {
|
||||
return configService?.updateAccount(accountId, patch) || null
|
||||
})
|
||||
|
||||
ipcMain.handle('accounts:delete', async (_, accountId: string, deleteLocalData = false) => {
|
||||
if (!configService) {
|
||||
return { success: false, error: '配置服务未初始化' }
|
||||
}
|
||||
|
||||
const deleted = configService.listAccounts().find((item) => item.id === accountId) || null
|
||||
if (!deleted) {
|
||||
return { success: false, error: '账号不存在' }
|
||||
}
|
||||
|
||||
if (deleteLocalData) {
|
||||
const cacheService = new (await import('./services/cacheService')).CacheService(configService)
|
||||
const clearResult = await cacheService.clearAccountDatabases(deleted)
|
||||
if (!clearResult.success) {
|
||||
return { success: false, error: clearResult.error || '删除账号本地数据失败' }
|
||||
}
|
||||
}
|
||||
|
||||
const result = configService.deleteAccount(accountId)
|
||||
return { success: true, deleted: result.deleted, nextActiveAccountId: result.nextActiveAccountId }
|
||||
})
|
||||
|
||||
// HTTP API 管理
|
||||
ipcMain.handle('httpApi:getStatus', async () => {
|
||||
return { success: true, status: httpApiService.getUiStatus() }
|
||||
@@ -2910,8 +2954,8 @@ function registerIpcHandlers() {
|
||||
})
|
||||
|
||||
// 打开引导窗口
|
||||
ipcMain.handle('window:openWelcomeWindow', async () => {
|
||||
createWelcomeWindow()
|
||||
ipcMain.handle('window:openWelcomeWindow', async (_, mode?: 'default' | 'add-account') => {
|
||||
createWelcomeWindow(mode || 'default')
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -3116,6 +3160,28 @@ function registerIpcHandlers() {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('cache:clearCurrentAccount', async (_, deleteLocalData = false) => {
|
||||
logService?.info('Cache', '开始清除当前账号配置', { deleteLocalData })
|
||||
try {
|
||||
const cacheService = new (await import('./services/cacheService')).CacheService(configService!)
|
||||
return await cacheService.clearCurrentAccount(deleteLocalData)
|
||||
} catch (e) {
|
||||
logService?.error('Cache', '清除当前账号配置异常', { error: String(e) })
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('cache:clearAllAccountConfigs', async () => {
|
||||
logService?.info('Cache', '开始清空全部账号配置')
|
||||
try {
|
||||
const cacheService = new (await import('./services/cacheService')).CacheService(configService!)
|
||||
return await cacheService.clearAllAccountConfigs()
|
||||
} catch (e) {
|
||||
logService?.error('Cache', '清空全部账号配置异常', { error: String(e) })
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('cache:getCacheSize', async () => {
|
||||
try {
|
||||
const cacheService = new (await import('./services/cacheService')).CacheService(configService!)
|
||||
|
||||
+15
-1
@@ -1,4 +1,5 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import type { AccountProfile } from '../src/types/account'
|
||||
|
||||
function getMcpLaunchConfigSafe(): Promise<{
|
||||
command: string
|
||||
@@ -33,6 +34,17 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
setTldCache: (tlds: string[]) => ipcRenderer.invoke('config:setTldCache', tlds)
|
||||
},
|
||||
|
||||
accounts: {
|
||||
list: () => ipcRenderer.invoke('accounts:list') as Promise<AccountProfile[]>,
|
||||
getActive: () => ipcRenderer.invoke('accounts:getActive') as Promise<AccountProfile | null>,
|
||||
setActive: (accountId: string) => ipcRenderer.invoke('accounts:setActive', accountId) as Promise<AccountProfile | null>,
|
||||
save: (profile: Omit<AccountProfile, 'id' | 'createdAt' | 'updatedAt' | 'lastUsedAt'>) => ipcRenderer.invoke('accounts:save', profile) as Promise<AccountProfile | null>,
|
||||
update: (accountId: string, patch: Partial<Omit<AccountProfile, 'id' | 'createdAt' | 'updatedAt' | 'lastUsedAt'>>) =>
|
||||
ipcRenderer.invoke('accounts:update', accountId, patch) as Promise<AccountProfile | null>,
|
||||
delete: (accountId: string, deleteLocalData?: boolean) =>
|
||||
ipcRenderer.invoke('accounts:delete', accountId, deleteLocalData) as Promise<{ success: boolean; error?: string; deleted?: AccountProfile | null; nextActiveAccountId?: string }>
|
||||
},
|
||||
|
||||
// 数据库操作
|
||||
db: {
|
||||
open: (dbPath: string, key?: string) => ipcRenderer.invoke('db:open', dbPath, key),
|
||||
@@ -128,7 +140,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
openAnnualReportWindow: (year: number) => ipcRenderer.invoke('window:openAnnualReportWindow', year),
|
||||
openAgreementWindow: () => ipcRenderer.invoke('window:openAgreementWindow'),
|
||||
openPurchaseWindow: () => ipcRenderer.invoke('window:openPurchaseWindow'),
|
||||
openWelcomeWindow: () => ipcRenderer.invoke('window:openWelcomeWindow'),
|
||||
openWelcomeWindow: (mode?: 'default' | 'add-account') => ipcRenderer.invoke('window:openWelcomeWindow', mode),
|
||||
completeWelcome: () => ipcRenderer.invoke('window:completeWelcome'),
|
||||
isChatWindowOpen: () => ipcRenderer.invoke('window:isChatWindowOpen'),
|
||||
closeChatWindow: () => ipcRenderer.invoke('window:closeChatWindow'),
|
||||
@@ -413,6 +425,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
clearDatabases: () => ipcRenderer.invoke('cache:clearDatabases'),
|
||||
clearAll: () => ipcRenderer.invoke('cache:clearAll'),
|
||||
clearConfig: () => ipcRenderer.invoke('cache:clearConfig'),
|
||||
clearCurrentAccount: (deleteLocalData?: boolean) => ipcRenderer.invoke('cache:clearCurrentAccount', deleteLocalData),
|
||||
clearAllAccountConfigs: () => ipcRenderer.invoke('cache:clearAllAccountConfigs'),
|
||||
getCacheSize: () => ipcRenderer.invoke('cache:getCacheSize')
|
||||
},
|
||||
log: {
|
||||
|
||||
@@ -2,10 +2,47 @@ import { join } from 'path'
|
||||
import { existsSync, rmSync, readdirSync, statSync } from 'fs'
|
||||
import { app } from 'electron'
|
||||
import { ConfigService } from './config'
|
||||
import type { AccountProfile } from '../../src/types/account'
|
||||
|
||||
export class CacheService {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
private getEffectiveCachePathFor(cachePath?: string): string {
|
||||
if (cachePath && cachePath.trim()) return cachePath.trim()
|
||||
return this.getEffectiveCachePath()
|
||||
}
|
||||
|
||||
private async deleteAccountDatabaseFolder(wxid: string, cachePath?: string): Promise<{ success: boolean; error?: string }> {
|
||||
if (!wxid) {
|
||||
return { success: false, error: '未配置 wxid' }
|
||||
}
|
||||
|
||||
try {
|
||||
const targetCachePath = this.getEffectiveCachePathFor(cachePath)
|
||||
if (!existsSync(targetCachePath)) {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
const possibleFolderNames = [
|
||||
wxid,
|
||||
wxid.replace('wxid_', ''),
|
||||
wxid.split('_').slice(0, 2).join('_'),
|
||||
]
|
||||
|
||||
for (const folderName of possibleFolderNames) {
|
||||
const wxidFolderPath = join(targetCachePath, folderName)
|
||||
if (existsSync(wxidFolderPath)) {
|
||||
rmSync(wxidFolderPath, { recursive: true, force: true })
|
||||
return { success: true }
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (e: any) {
|
||||
return { success: false, error: e.message || String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有效的缓存路径
|
||||
* - 如果配置了 cachePath,使用配置的路径
|
||||
@@ -152,34 +189,9 @@ export class CacheService {
|
||||
if (!existsSync(cachePath)) {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// 查找并删除 wxid 文件夹(包含所有解密后的数据库)
|
||||
const possibleFolderNames = [
|
||||
wxid,
|
||||
(wxid as string).replace('wxid_', ''),
|
||||
(wxid as string).split('_').slice(0, 2).join('_'),
|
||||
]
|
||||
|
||||
let deleted = false
|
||||
for (const folderName of possibleFolderNames) {
|
||||
const wxidFolderPath = join(cachePath, folderName)
|
||||
if (existsSync(wxidFolderPath)) {
|
||||
console.log('[CacheService] 找到 wxid 文件夹,准备删除:', wxidFolderPath)
|
||||
try {
|
||||
rmSync(wxidFolderPath, { recursive: true, force: true })
|
||||
console.log('[CacheService] 成功删除 wxid 文件夹')
|
||||
deleted = true
|
||||
break
|
||||
} catch (e: any) {
|
||||
console.error('[CacheService] 删除 wxid 文件夹失败:', e)
|
||||
return { success: false, error: `删除失败: ${e.message}` }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!deleted) {
|
||||
console.warn('[CacheService] 未找到 wxid 文件夹')
|
||||
return { success: false, error: '未找到数据库缓存文件夹' }
|
||||
const deleteResult = await this.deleteAccountDatabaseFolder(wxid, cachePath)
|
||||
if (!deleteResult.success) {
|
||||
return deleteResult
|
||||
}
|
||||
|
||||
console.log('[CacheService] 数据库缓存清理完成')
|
||||
@@ -285,21 +297,41 @@ export class CacheService {
|
||||
*/
|
||||
async clearConfig(): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
// 清除所有配置项
|
||||
const configKeys = [
|
||||
'decryptKey',
|
||||
'dbPath',
|
||||
'myWxid',
|
||||
'cachePath',
|
||||
'imageXorKey',
|
||||
'imageAesKey',
|
||||
'exportPath'
|
||||
]
|
||||
this.configService.clearAllAccountsAndAccountConfig()
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of configKeys) {
|
||||
this.configService.set(key as any, '' as any)
|
||||
async clearAccountDatabases(account: Pick<AccountProfile, 'wxid' | 'cachePath'>): Promise<{ success: boolean; error?: string }> {
|
||||
return this.deleteAccountDatabaseFolder(account.wxid, account.cachePath)
|
||||
}
|
||||
|
||||
async clearCurrentAccount(deleteLocalData = false): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const active = this.configService.getActiveAccount()
|
||||
if (!active) {
|
||||
return { success: false, error: '当前没有可清除的账号' }
|
||||
}
|
||||
|
||||
if (deleteLocalData) {
|
||||
const clearResult = await this.clearAccountDatabases(active)
|
||||
if (!clearResult.success) {
|
||||
return clearResult
|
||||
}
|
||||
}
|
||||
|
||||
this.configService.clearCurrentAccount()
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
}
|
||||
}
|
||||
|
||||
async clearAllAccountConfigs(): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
this.configService.clearAllAccountsAndAccountConfig()
|
||||
return { success: true }
|
||||
} catch (e) {
|
||||
return { success: false, error: String(e) }
|
||||
@@ -483,4 +515,4 @@ export class CacheService {
|
||||
|
||||
return totalSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+347
-11
@@ -2,12 +2,33 @@ import Database from 'better-sqlite3'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { getUserDataPath } from './runtimePaths'
|
||||
import type { AccountProfile, AccountProfileInput, AccountProfilePatch } from '../../src/types/account'
|
||||
|
||||
const ACCOUNT_FIELD_KEYS = new Set([
|
||||
'dbPath',
|
||||
'decryptKey',
|
||||
'myWxid',
|
||||
'cachePath',
|
||||
'imageXorKey',
|
||||
'imageAesKey'
|
||||
])
|
||||
|
||||
const ACCOUNT_CONFIG_CLEAR_KEYS = [
|
||||
'decryptKey',
|
||||
'dbPath',
|
||||
'myWxid',
|
||||
'cachePath',
|
||||
'imageXorKey',
|
||||
'imageAesKey'
|
||||
] as const
|
||||
|
||||
interface ConfigSchema {
|
||||
// 数据库相关
|
||||
dbPath: string
|
||||
decryptKey: string
|
||||
myWxid: string
|
||||
accounts: AccountProfile[]
|
||||
activeAccountId: string
|
||||
|
||||
// 图片解密相关
|
||||
imageXorKey: string
|
||||
@@ -91,6 +112,8 @@ const defaults: ConfigSchema = {
|
||||
dbPath: '',
|
||||
decryptKey: '',
|
||||
myWxid: '',
|
||||
accounts: [],
|
||||
activeAccountId: '',
|
||||
imageXorKey: '',
|
||||
imageAesKey: '',
|
||||
emoticonUin: '',
|
||||
@@ -187,6 +210,8 @@ export class ConfigService {
|
||||
insertStmt.run(key, JSON.stringify(value))
|
||||
}
|
||||
|
||||
this.migrateLegacySingleAccount()
|
||||
|
||||
// 迁移:修复旧版本产生的空 STT 语言配置,默认为中文
|
||||
try {
|
||||
const sttRow = this.db.prepare("SELECT value FROM config WHERE key = 'sttLanguages'").get() as { value: string } | undefined
|
||||
@@ -261,16 +286,310 @@ export class ConfigService {
|
||||
}
|
||||
}
|
||||
|
||||
private getStoredValue<K extends keyof ConfigSchema>(key: K): ConfigSchema[K] {
|
||||
if (!this.db) {
|
||||
return defaults[key]
|
||||
}
|
||||
|
||||
const row = this.db.prepare('SELECT value FROM config WHERE key = ?').get(key) as { value: string } | undefined
|
||||
if (row) {
|
||||
return JSON.parse(row.value)
|
||||
}
|
||||
|
||||
return defaults[key]
|
||||
}
|
||||
|
||||
private setStoredValue<K extends keyof ConfigSchema>(key: K, value: ConfigSchema[K]): void {
|
||||
if (!this.db) return
|
||||
this.db.prepare(`
|
||||
INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)
|
||||
`).run(key, JSON.stringify(value))
|
||||
}
|
||||
|
||||
private createAccountId(): string {
|
||||
return `acct_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
|
||||
private normalizeAccountInput(profile: Partial<AccountProfileInput>, fallback?: AccountProfile): AccountProfileInput {
|
||||
const wxid = String(profile.wxid ?? fallback?.wxid ?? '').trim()
|
||||
const dbPath = String(profile.dbPath ?? fallback?.dbPath ?? '').trim()
|
||||
const decryptKey = String(profile.decryptKey ?? fallback?.decryptKey ?? '').trim()
|
||||
const cachePath = String(profile.cachePath ?? fallback?.cachePath ?? '').trim()
|
||||
const imageXorKey = String(profile.imageXorKey ?? fallback?.imageXorKey ?? '').trim()
|
||||
const imageAesKey = String(profile.imageAesKey ?? fallback?.imageAesKey ?? '').trim()
|
||||
const displayName = String(profile.displayName ?? fallback?.displayName ?? wxid || '未命名账号').trim() || wxid || '未命名账号'
|
||||
|
||||
return {
|
||||
wxid,
|
||||
dbPath,
|
||||
decryptKey,
|
||||
cachePath,
|
||||
imageXorKey,
|
||||
imageAesKey,
|
||||
displayName
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeAccountProfile(raw: any): AccountProfile {
|
||||
const now = Date.now()
|
||||
const base = this.normalizeAccountInput(raw || {})
|
||||
return {
|
||||
id: String(raw?.id || this.createAccountId()),
|
||||
...base,
|
||||
createdAt: Number(raw?.createdAt) || now,
|
||||
updatedAt: Number(raw?.updatedAt) || now,
|
||||
lastUsedAt: Number(raw?.lastUsedAt) || now
|
||||
}
|
||||
}
|
||||
|
||||
private getAccountsRaw(): AccountProfile[] {
|
||||
const accounts = this.getStoredValue('accounts')
|
||||
if (!Array.isArray(accounts)) return []
|
||||
return accounts.map((item) => this.normalizeAccountProfile(item))
|
||||
}
|
||||
|
||||
private setAccountsRaw(accounts: AccountProfile[]): void {
|
||||
this.setStoredValue('accounts', accounts)
|
||||
}
|
||||
|
||||
private getActiveAccountIdRaw(): string {
|
||||
return String(this.getStoredValue('activeAccountId') || '')
|
||||
}
|
||||
|
||||
private setActiveAccountIdRaw(accountId: string): void {
|
||||
this.setStoredValue('activeAccountId', accountId)
|
||||
}
|
||||
|
||||
private getAccountFieldValue(account: AccountProfile | null, key: keyof ConfigSchema): any {
|
||||
if (!account) return defaults[key]
|
||||
|
||||
switch (key) {
|
||||
case 'dbPath':
|
||||
return account.dbPath
|
||||
case 'decryptKey':
|
||||
return account.decryptKey
|
||||
case 'myWxid':
|
||||
return account.wxid
|
||||
case 'cachePath':
|
||||
return account.cachePath
|
||||
case 'imageXorKey':
|
||||
return account.imageXorKey
|
||||
case 'imageAesKey':
|
||||
return account.imageAesKey
|
||||
default:
|
||||
return defaults[key]
|
||||
}
|
||||
}
|
||||
|
||||
private setAccountField(account: AccountProfile, key: keyof ConfigSchema, value: any): AccountProfile {
|
||||
const next = { ...account, updatedAt: Date.now() }
|
||||
const normalized = String(value ?? '').trim()
|
||||
|
||||
switch (key) {
|
||||
case 'dbPath':
|
||||
next.dbPath = normalized
|
||||
break
|
||||
case 'decryptKey':
|
||||
next.decryptKey = normalized
|
||||
break
|
||||
case 'myWxid':
|
||||
next.wxid = normalized
|
||||
if (!next.displayName || next.displayName === account.wxid || next.displayName === '未命名账号') {
|
||||
next.displayName = normalized || '未命名账号'
|
||||
}
|
||||
break
|
||||
case 'cachePath':
|
||||
next.cachePath = normalized
|
||||
break
|
||||
case 'imageXorKey':
|
||||
next.imageXorKey = normalized
|
||||
break
|
||||
case 'imageAesKey':
|
||||
next.imageAesKey = normalized
|
||||
break
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
private ensureActiveAccount(): AccountProfile {
|
||||
const active = this.getActiveAccount()
|
||||
if (active) return active
|
||||
|
||||
const empty = this.normalizeAccountProfile({
|
||||
id: this.createAccountId(),
|
||||
displayName: '未命名账号'
|
||||
})
|
||||
const accounts = [...this.getAccountsRaw(), empty]
|
||||
this.setAccountsRaw(accounts)
|
||||
this.setActiveAccountIdRaw(empty.id)
|
||||
return empty
|
||||
}
|
||||
|
||||
private migrateLegacySingleAccount(): void {
|
||||
const accounts = this.getAccountsRaw()
|
||||
if (accounts.length > 0) return
|
||||
|
||||
const dbPath = String(this.getStoredValue('dbPath') || '').trim()
|
||||
const decryptKey = String(this.getStoredValue('decryptKey') || '').trim()
|
||||
const wxid = String(this.getStoredValue('myWxid') || '').trim()
|
||||
const cachePath = String(this.getStoredValue('cachePath') || '').trim()
|
||||
const imageXorKey = String(this.getStoredValue('imageXorKey') || '').trim()
|
||||
const imageAesKey = String(this.getStoredValue('imageAesKey') || '').trim()
|
||||
|
||||
if (!dbPath && !decryptKey && !wxid && !cachePath && !imageXorKey && !imageAesKey) {
|
||||
return
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const migrated = this.normalizeAccountProfile({
|
||||
id: this.createAccountId(),
|
||||
wxid,
|
||||
dbPath,
|
||||
decryptKey,
|
||||
cachePath,
|
||||
imageXorKey,
|
||||
imageAesKey,
|
||||
displayName: wxid || '未命名账号',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
lastUsedAt: now
|
||||
})
|
||||
|
||||
this.setAccountsRaw([migrated])
|
||||
this.setActiveAccountIdRaw(migrated.id)
|
||||
}
|
||||
|
||||
listAccounts(): AccountProfile[] {
|
||||
return this.getAccountsRaw()
|
||||
.sort((a, b) => (b.lastUsedAt || 0) - (a.lastUsedAt || 0) || a.displayName.localeCompare(b.displayName))
|
||||
}
|
||||
|
||||
getActiveAccount(): AccountProfile | null {
|
||||
const accounts = this.getAccountsRaw()
|
||||
if (accounts.length === 0) return null
|
||||
|
||||
const activeId = this.getActiveAccountIdRaw()
|
||||
const active = accounts.find((item) => item.id === activeId)
|
||||
return active || accounts[0]
|
||||
}
|
||||
|
||||
setActiveAccount(accountId: string): AccountProfile | null {
|
||||
const accounts = this.getAccountsRaw()
|
||||
const target = accounts.find((item) => item.id === accountId)
|
||||
if (!target) return null
|
||||
|
||||
const now = Date.now()
|
||||
const nextAccounts = accounts.map((item) => (
|
||||
item.id === accountId ? { ...item, lastUsedAt: now, updatedAt: now } : item
|
||||
))
|
||||
this.setAccountsRaw(nextAccounts)
|
||||
this.setActiveAccountIdRaw(accountId)
|
||||
return nextAccounts.find((item) => item.id === accountId) || null
|
||||
}
|
||||
|
||||
saveAccount(profile: AccountProfileInput): AccountProfile {
|
||||
const accounts = this.getAccountsRaw()
|
||||
const now = Date.now()
|
||||
const normalized = this.normalizeAccountInput(profile)
|
||||
const duplicate = accounts.find((item) => item.wxid === normalized.wxid && item.dbPath === normalized.dbPath)
|
||||
|
||||
if (duplicate) {
|
||||
const updated: AccountProfile = {
|
||||
...duplicate,
|
||||
...normalized,
|
||||
updatedAt: now,
|
||||
lastUsedAt: duplicate.lastUsedAt || now
|
||||
}
|
||||
const nextAccounts = accounts.map((item) => item.id === duplicate.id ? updated : item)
|
||||
this.setAccountsRaw(nextAccounts)
|
||||
return updated
|
||||
}
|
||||
|
||||
const created: AccountProfile = {
|
||||
id: this.createAccountId(),
|
||||
...normalized,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
lastUsedAt: now
|
||||
}
|
||||
this.setAccountsRaw([...accounts, created])
|
||||
if (!this.getActiveAccountIdRaw()) {
|
||||
this.setActiveAccountIdRaw(created.id)
|
||||
}
|
||||
return created
|
||||
}
|
||||
|
||||
updateAccount(accountId: string, patch: AccountProfilePatch): AccountProfile | null {
|
||||
const accounts = this.getAccountsRaw()
|
||||
const current = accounts.find((item) => item.id === accountId)
|
||||
if (!current) return null
|
||||
|
||||
const normalized = this.normalizeAccountInput(patch, current)
|
||||
const next: AccountProfile = {
|
||||
...current,
|
||||
...normalized,
|
||||
updatedAt: Date.now()
|
||||
}
|
||||
const nextAccounts = accounts.map((item) => item.id === accountId ? next : item)
|
||||
this.setAccountsRaw(nextAccounts)
|
||||
return next
|
||||
}
|
||||
|
||||
deleteAccount(accountId: string): { deleted: AccountProfile | null; nextActiveAccountId: string } {
|
||||
const accounts = this.getAccountsRaw()
|
||||
const deleted = accounts.find((item) => item.id === accountId) || null
|
||||
if (!deleted) {
|
||||
return { deleted: null, nextActiveAccountId: this.getActiveAccountIdRaw() }
|
||||
}
|
||||
|
||||
const remaining = accounts.filter((item) => item.id !== accountId)
|
||||
this.setAccountsRaw(remaining)
|
||||
|
||||
let nextActiveAccountId = this.getActiveAccountIdRaw()
|
||||
if (nextActiveAccountId === accountId) {
|
||||
nextActiveAccountId = remaining[0]?.id || ''
|
||||
this.setActiveAccountIdRaw(nextActiveAccountId)
|
||||
}
|
||||
|
||||
if (remaining.length === 0) {
|
||||
this.setActiveAccountIdRaw('')
|
||||
}
|
||||
|
||||
return { deleted, nextActiveAccountId }
|
||||
}
|
||||
|
||||
clearCurrentAccount(): AccountProfile | null {
|
||||
const active = this.getActiveAccount()
|
||||
if (!active) return null
|
||||
const next = this.updateAccount(active.id, {
|
||||
wxid: '',
|
||||
dbPath: '',
|
||||
decryptKey: '',
|
||||
cachePath: '',
|
||||
imageXorKey: '',
|
||||
imageAesKey: '',
|
||||
displayName: '未命名账号'
|
||||
})
|
||||
return next
|
||||
}
|
||||
|
||||
clearAllAccountsAndAccountConfig(): void {
|
||||
this.setAccountsRaw([])
|
||||
this.setActiveAccountIdRaw('')
|
||||
for (const key of ACCOUNT_CONFIG_CLEAR_KEYS) {
|
||||
this.setStoredValue(key, '' as any)
|
||||
}
|
||||
}
|
||||
|
||||
get<K extends keyof ConfigSchema>(key: K): ConfigSchema[K] {
|
||||
try {
|
||||
if (!this.db) {
|
||||
return defaults[key]
|
||||
if (ACCOUNT_FIELD_KEYS.has(key as string)) {
|
||||
const active = this.getActiveAccount()
|
||||
return this.getAccountFieldValue(active, key)
|
||||
}
|
||||
const row = this.db.prepare('SELECT value FROM config WHERE key = ?').get(key) as { value: string } | undefined
|
||||
if (row) {
|
||||
return JSON.parse(row.value)
|
||||
}
|
||||
return defaults[key]
|
||||
|
||||
return this.getStoredValue(key)
|
||||
} catch (e) {
|
||||
console.error(`获取配置 ${key} 失败:`, e)
|
||||
return defaults[key]
|
||||
@@ -279,10 +598,18 @@ export class ConfigService {
|
||||
|
||||
set<K extends keyof ConfigSchema>(key: K, value: ConfigSchema[K]): void {
|
||||
try {
|
||||
if (!this.db) return
|
||||
this.db.prepare(`
|
||||
INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)
|
||||
`).run(key, JSON.stringify(value))
|
||||
if (ACCOUNT_FIELD_KEYS.has(key as string)) {
|
||||
const active = this.ensureActiveAccount()
|
||||
const updated = this.setAccountField(active, key, value)
|
||||
const accounts = this.getAccountsRaw().map((item) => item.id === active.id ? updated : item)
|
||||
this.setAccountsRaw(accounts)
|
||||
if (!this.getActiveAccountIdRaw()) {
|
||||
this.setActiveAccountIdRaw(updated.id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.setStoredValue(key, value)
|
||||
} catch (e) {
|
||||
console.error(`设置配置 ${key} 失败:`, e)
|
||||
}
|
||||
@@ -300,6 +627,15 @@ export class ConfigService {
|
||||
(result as any)[row.key] = JSON.parse(row.value)
|
||||
}
|
||||
}
|
||||
const active = this.getActiveAccount()
|
||||
if (active) {
|
||||
result.dbPath = active.dbPath
|
||||
result.decryptKey = active.decryptKey
|
||||
result.myWxid = active.wxid
|
||||
result.cachePath = active.cachePath
|
||||
result.imageXorKey = active.imageXorKey
|
||||
result.imageAesKey = active.imageAesKey
|
||||
}
|
||||
return result
|
||||
} catch (e) {
|
||||
console.error('获取所有配置失败:', e)
|
||||
|
||||
+317
-63
@@ -4,6 +4,7 @@ import { useAppStore } from '../stores/appStore'
|
||||
import { useThemeStore, themes } from '../stores/themeStore'
|
||||
import { useActivationStore } from '../stores/activationStore'
|
||||
import type { UpdateDownloadProgressPayload } from '../types/electron'
|
||||
import type { AccountProfile } from '../types/account'
|
||||
import { dialog } from '../services/ipc'
|
||||
import * as configService from '../services/config'
|
||||
import AISummarySettings from '../components/ai/AISummarySettings'
|
||||
@@ -45,13 +46,16 @@ const sttModelTypeOptions = [
|
||||
function SettingsPage() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const location = useLocation()
|
||||
const { setDbConnected, setLoading } = useAppStore()
|
||||
const { setDbConnected, setLoading, setMyWxid: setCurrentWxid } = useAppStore()
|
||||
const { currentTheme, themeMode, setTheme, setThemeMode, appIcon, setAppIcon } = useThemeStore()
|
||||
const { status: activationStatus, checkStatus: checkActivationStatus } = useActivationStore()
|
||||
|
||||
const { isAuthEnabled, enableAuth, disableAuth, setupPassword, authMethod } = useAuthStore()
|
||||
const [passwordInput, setPasswordInput] = useState('')
|
||||
const [showPasswordInput, setShowPasswordInput] = useState(false)
|
||||
const [accountsList, setAccountsList] = useState<AccountProfile[]>([])
|
||||
const [activeAccountId, setActiveAccountId] = useState('')
|
||||
const [editingAccountId, setEditingAccountId] = useState('')
|
||||
|
||||
// 安全设置确认弹窗状态
|
||||
const [securityConfirm, setSecurityConfirm] = useState<{
|
||||
@@ -143,7 +147,7 @@ function SettingsPage() {
|
||||
const [closeToTray, setCloseToTray] = useState(true)
|
||||
const [showAesKey, setShowAesKey] = useState(false)
|
||||
const [showClearDialog, setShowClearDialog] = useState<{
|
||||
type: 'images' | 'emojis' | 'databases' | 'all' | 'config'
|
||||
type: 'images' | 'emojis' | 'databases' | 'all' | 'currentAccount' | 'allAccounts'
|
||||
title: string
|
||||
message: string
|
||||
} | null>(null)
|
||||
@@ -197,6 +201,41 @@ function SettingsPage() {
|
||||
const isMac = platformInfo.platform === 'darwin'
|
||||
const biometricLabel = isMac ? 'Touch ID' : 'Windows Hello'
|
||||
|
||||
const buildAccountPayload = () => ({
|
||||
wxid: wxid.trim(),
|
||||
dbPath: dbPath.trim(),
|
||||
decryptKey: decryptKey.trim(),
|
||||
cachePath: cachePath.trim(),
|
||||
imageXorKey: imageXorKey.trim(),
|
||||
imageAesKey: imageAesKey.trim(),
|
||||
displayName: wxid.trim() || '未命名账号'
|
||||
})
|
||||
|
||||
const applyAccountToForm = (account: AccountProfile | null) => {
|
||||
setEditingAccountId(account?.id || '')
|
||||
setDecryptKey(account?.decryptKey || '')
|
||||
setDbPath(account?.dbPath || '')
|
||||
setWxid(account?.wxid || '')
|
||||
setCachePath(account?.cachePath || '')
|
||||
setImageXorKey(account?.imageXorKey || '')
|
||||
setImageAesKey(account?.imageAesKey || '')
|
||||
setIsAccountVerified(Boolean(account?.decryptKey && account?.dbPath && account?.wxid))
|
||||
}
|
||||
|
||||
const refreshAccountsState = async (preferredEditingId?: string) => {
|
||||
const [accounts, activeAccount] = await Promise.all([
|
||||
configService.listAccounts(),
|
||||
configService.getActiveAccount()
|
||||
])
|
||||
setAccountsList(accounts)
|
||||
setActiveAccountId(activeAccount?.id || '')
|
||||
|
||||
const editingId = preferredEditingId || editingAccountId || activeAccount?.id || accounts[0]?.id || ''
|
||||
const editingAccount = accounts.find(item => item.id === editingId) || activeAccount || accounts[0] || null
|
||||
applyAccountToForm(editingAccount)
|
||||
return { accounts, activeAccount, editingAccount }
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig()
|
||||
loadDefaultExportPath()
|
||||
@@ -210,6 +249,7 @@ function SettingsPage() {
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const { activeAccount, editingAccount } = await refreshAccountsState()
|
||||
const savedKey = await configService.getDecryptKey()
|
||||
const savedPath = await configService.getDbPath()
|
||||
const savedWxid = await configService.getMyWxid()
|
||||
@@ -222,12 +262,13 @@ function SettingsPage() {
|
||||
const savedSkipIntegrityCheck = await configService.getSkipIntegrityCheck()
|
||||
const savedAutoUpdateDatabase = await configService.getAutoUpdateDatabase()
|
||||
|
||||
if (savedKey) setDecryptKey(savedKey)
|
||||
if (savedPath) setDbPath(savedPath)
|
||||
if (savedWxid) setWxid(savedWxid)
|
||||
if (savedCachePath) setCachePath(savedCachePath)
|
||||
if (savedXorKey) setImageXorKey(savedXorKey)
|
||||
if (savedAesKey) setImageAesKey(savedAesKey)
|
||||
if (!editingAccount && savedKey) setDecryptKey(savedKey)
|
||||
if (!editingAccount && savedPath) setDbPath(savedPath)
|
||||
if (!editingAccount && savedWxid) setWxid(savedWxid)
|
||||
if (!editingAccount && savedCachePath) setCachePath(savedCachePath)
|
||||
if (!editingAccount && savedXorKey) setImageXorKey(savedXorKey)
|
||||
if (!editingAccount && savedAesKey) setImageAesKey(savedAesKey)
|
||||
setIsAccountVerified(Boolean((editingAccount || activeAccount)?.decryptKey && (editingAccount || activeAccount)?.dbPath && (editingAccount || activeAccount)?.wxid))
|
||||
if (savedExportPath) setExportPath(savedExportPath)
|
||||
if (savedSttLanguages && savedSttLanguages.length > 0) {
|
||||
setSttLanguagesState(savedSttLanguages)
|
||||
@@ -308,7 +349,8 @@ function SettingsPage() {
|
||||
aiCustomSystemPrompt: savedAiCustomSystemPrompt,
|
||||
aiEnableThinking: savedAiEnableThinking,
|
||||
aiMessageLimit: savedAiMessageLimit,
|
||||
closeToTray: savedCloseToTray
|
||||
closeToTray: savedCloseToTray,
|
||||
editingAccountId: (editingAccount || activeAccount)?.id || ''
|
||||
})
|
||||
|
||||
} catch (e) {
|
||||
@@ -356,7 +398,8 @@ function SettingsPage() {
|
||||
aiCustomSystemPrompt,
|
||||
aiEnableThinking,
|
||||
aiMessageLimit,
|
||||
closeToTray
|
||||
closeToTray,
|
||||
editingAccountId
|
||||
}
|
||||
|
||||
// 深度比较配置是否有变化
|
||||
@@ -369,7 +412,7 @@ function SettingsPage() {
|
||||
quoteStyle, exportDefaultDateRange, exportDefaultAvatars,
|
||||
aiProvider, aiApiKey, aiModel, aiDefaultTimeRange, aiSummaryDetail,
|
||||
aiSystemPromptPreset, aiCustomSystemPrompt, aiEnableThinking, aiMessageLimit,
|
||||
closeToTray, initialConfig
|
||||
closeToTray, editingAccountId, initialConfig
|
||||
])
|
||||
|
||||
const loadAppVersion = async () => {
|
||||
@@ -604,11 +647,19 @@ function SettingsPage() {
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearConfig = () => {
|
||||
const handleClearCurrentAccount = () => {
|
||||
setShowClearDialog({
|
||||
type: 'config',
|
||||
title: '清除配置',
|
||||
message: '此操作将删除所有保存的配置信息(包括密钥、路径等),清除后无法恢复。确定要继续吗?'
|
||||
type: 'currentAccount',
|
||||
title: '清除当前账号',
|
||||
message: '此操作将清除当前账号的密钥、路径等配置,不影响其他账号。确定要继续吗?'
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearAllAccounts = () => {
|
||||
setShowClearDialog({
|
||||
type: 'allAccounts',
|
||||
title: '清空全部账号配置',
|
||||
message: '此操作将删除所有账号配置和账号级密钥/路径信息,不删除全局主题、AI、MCP、HTTP API 等通用设置。确定要继续吗?'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -617,31 +668,34 @@ function SettingsPage() {
|
||||
|
||||
try {
|
||||
let result
|
||||
switch (showClearDialog.type) {
|
||||
case 'images':
|
||||
result = await window.electronAPI.cache.clearImages()
|
||||
break
|
||||
switch (showClearDialog.type) {
|
||||
case 'images':
|
||||
result = await window.electronAPI.cache.clearImages()
|
||||
break
|
||||
case 'emojis':
|
||||
result = await window.electronAPI.cache.clearEmojis()
|
||||
break
|
||||
case 'databases':
|
||||
result = await window.electronAPI.cache.clearDatabases()
|
||||
break
|
||||
case 'all':
|
||||
result = await window.electronAPI.cache.clearAll()
|
||||
break
|
||||
case 'config':
|
||||
result = await window.electronAPI.cache.clearConfig()
|
||||
break
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
showMessage(`${showClearDialog.title}成功`, true)
|
||||
if (showClearDialog.type === 'config') {
|
||||
await loadConfig()
|
||||
} else {
|
||||
await loadCacheSize()
|
||||
case 'all':
|
||||
result = await window.electronAPI.cache.clearAll()
|
||||
break
|
||||
case 'currentAccount':
|
||||
result = await window.electronAPI.cache.clearCurrentAccount(false)
|
||||
break
|
||||
case 'allAccounts':
|
||||
result = await window.electronAPI.cache.clearAllAccountConfigs()
|
||||
break
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
showMessage(`${showClearDialog.title}成功`, true)
|
||||
if (showClearDialog.type === 'currentAccount' || showClearDialog.type === 'allAccounts') {
|
||||
await loadConfig()
|
||||
} else {
|
||||
await loadCacheSize()
|
||||
}
|
||||
} else {
|
||||
showMessage(result.error || `${showClearDialog.title}失败`, false)
|
||||
}
|
||||
@@ -697,14 +751,12 @@ function SettingsPage() {
|
||||
|
||||
if (result.success && result.key) {
|
||||
setDecryptKey(result.key)
|
||||
await configService.setDecryptKey(result.key)
|
||||
|
||||
if (dbPath) {
|
||||
const resolved = await window.electronAPI.wcdb.resolveValidWxid(dbPath, result.key)
|
||||
if (resolved.success && resolved.wxid) {
|
||||
setWxid(resolved.wxid)
|
||||
setIsAccountVerified(true)
|
||||
await configService.setMyWxid(resolved.wxid)
|
||||
showMessage(`密钥获取成功!已验证账号: ${resolved.wxid}`, true)
|
||||
setKeyStatus('')
|
||||
return
|
||||
@@ -714,7 +766,6 @@ function SettingsPage() {
|
||||
if (result.validatedWxid) {
|
||||
setWxid(result.validatedWxid)
|
||||
setIsAccountVerified(true)
|
||||
await configService.setMyWxid(result.validatedWxid)
|
||||
showMessage(`密钥获取成功!已验证账号: ${result.validatedWxid}`, true)
|
||||
setKeyStatus('')
|
||||
return
|
||||
@@ -730,7 +781,6 @@ function SettingsPage() {
|
||||
if (accountInfo) {
|
||||
setWxid(accountInfo.wxid)
|
||||
setIsAccountVerified(false)
|
||||
await configService.setMyWxid(accountInfo.wxid)
|
||||
showMessage(`密钥获取成功!已识别候选账号: ${accountInfo.wxid},请继续验证目录。`, true)
|
||||
} else {
|
||||
const wxids = await window.electronAPI.dbPath.scanWxids(dbPath)
|
||||
@@ -798,7 +848,6 @@ function SettingsPage() {
|
||||
|
||||
if (result.success && result.key) {
|
||||
setDecryptKey(result.key)
|
||||
await configService.setDecryptKey(result.key)
|
||||
|
||||
// 自动检测当前登录的微信账号
|
||||
setKeyStatus('正在检测当前登录账号...')
|
||||
@@ -813,7 +862,6 @@ function SettingsPage() {
|
||||
|
||||
if (accountInfo) {
|
||||
setWxid(accountInfo.wxid)
|
||||
await configService.setMyWxid(accountInfo.wxid)
|
||||
showMessage(`密钥获取成功!已自动绑定账号: ${accountInfo.wxid}`, true)
|
||||
} else {
|
||||
showMessage('密钥获取成功,已自动保存!(未能自动检测账号,请手动输入 wxid)', true)
|
||||
@@ -839,12 +887,152 @@ function SettingsPage() {
|
||||
|
||||
const handleOpenWelcomeWindow = async () => {
|
||||
try {
|
||||
await window.electronAPI.window.openWelcomeWindow()
|
||||
await window.electronAPI.window.openWelcomeWindow('add-account')
|
||||
} catch (e) {
|
||||
showMessage('打开引导窗口失败', false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectAccountForEdit = (account: AccountProfile) => {
|
||||
applyAccountToForm(account)
|
||||
setInitialConfig((prev: any) => prev ? {
|
||||
...prev,
|
||||
decryptKey: account.decryptKey || '',
|
||||
dbPath: account.dbPath || '',
|
||||
wxid: account.wxid || '',
|
||||
cachePath: account.cachePath || '',
|
||||
imageXorKey: account.imageXorKey || '',
|
||||
imageAesKey: account.imageAesKey || '',
|
||||
editingAccountId: account.id
|
||||
} : prev)
|
||||
setHasUnsavedChanges(false)
|
||||
}
|
||||
|
||||
const handleSwitchAccountAndReconnect = async () => {
|
||||
if (!editingAccountId || editingAccountId === activeAccountId) {
|
||||
showMessage('当前没有待切换账号', false)
|
||||
return
|
||||
}
|
||||
|
||||
if (hasUnsavedChanges) {
|
||||
showMessage('请先保存当前账号表单,再执行切换', false)
|
||||
return
|
||||
}
|
||||
|
||||
const target = accountsList.find((item) => item.id === editingAccountId)
|
||||
if (!target) {
|
||||
showMessage('待切换账号不存在', false)
|
||||
return
|
||||
}
|
||||
|
||||
if (!target.dbPath || !target.decryptKey || !target.wxid) {
|
||||
showMessage('待切换账号配置不完整,请先保存并补全账号信息', false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoadingState(true)
|
||||
setLoading(true, '正在切换账号...')
|
||||
try {
|
||||
const switched = await configService.setActiveAccount(target.id)
|
||||
if (!switched) {
|
||||
throw new Error('切换账号失败')
|
||||
}
|
||||
|
||||
const result = await window.electronAPI.wcdb.testConnection(target.dbPath, target.decryptKey, target.wxid)
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || '账号重连失败')
|
||||
}
|
||||
|
||||
await window.electronAPI.chat.close()
|
||||
await window.electronAPI.chat.refreshCache()
|
||||
await window.electronAPI.chat.connect()
|
||||
setDbConnected(true, target.dbPath)
|
||||
setCurrentWxid(target.wxid)
|
||||
await refreshAccountsState(target.id)
|
||||
showMessage(`已切换到账号:${target.displayName}`, true)
|
||||
} catch (e) {
|
||||
showMessage(`切换账号失败: ${e}`, false)
|
||||
} finally {
|
||||
setIsLoadingState(false)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteAccount = (account: AccountProfile) => {
|
||||
setSecurityConfirm({
|
||||
show: true,
|
||||
title: '删除账号',
|
||||
message: `删除账号 ${account.displayName || account.wxid}?此操作仅删除配置,不删除本地解密数据。`,
|
||||
onConfirm: async () => {
|
||||
const result = await configService.deleteAccount(account.id, false)
|
||||
if (result.success) {
|
||||
await refreshAccountsState(result.nextActiveAccountId)
|
||||
showMessage('账号已删除', true)
|
||||
} else {
|
||||
showMessage(result.error || '删除账号失败', false)
|
||||
}
|
||||
setSecurityConfirm(prev => ({ ...prev, show: false }))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteAccountWithLocalData = (account: AccountProfile) => {
|
||||
setSecurityConfirm({
|
||||
show: true,
|
||||
title: '删除账号并清理本地数据',
|
||||
message: `将删除账号 ${account.displayName || account.wxid} 的配置,并尝试删除该账号对应的本地解密数据库缓存。`,
|
||||
onConfirm: async () => {
|
||||
const result = await configService.deleteAccount(account.id, true)
|
||||
if (result.success) {
|
||||
await refreshAccountsState(result.nextActiveAccountId)
|
||||
showMessage('账号及其本地数据已删除', true)
|
||||
} else {
|
||||
showMessage(result.error || '删除账号失败', false)
|
||||
}
|
||||
setSecurityConfirm(prev => ({ ...prev, show: false }))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearCurrentAccountConfig = (deleteLocalData = false) => {
|
||||
setSecurityConfirm({
|
||||
show: true,
|
||||
title: deleteLocalData ? '清除当前账号并删除本地数据' : '清除当前账号',
|
||||
message: deleteLocalData
|
||||
? '将清除当前账号配置,并尝试删除该账号对应的本地解密数据库缓存。'
|
||||
: '将只清除当前账号配置,不影响其他账号和全局设置。',
|
||||
onConfirm: async () => {
|
||||
const result = await window.electronAPI.cache.clearCurrentAccount(deleteLocalData)
|
||||
if (result.success) {
|
||||
await refreshAccountsState(activeAccountId)
|
||||
showMessage('当前账号配置已清除', true)
|
||||
} else {
|
||||
showMessage(result.error || '清除当前账号失败', false)
|
||||
}
|
||||
setSecurityConfirm(prev => ({ ...prev, show: false }))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearAllAccountConfigs = () => {
|
||||
setSecurityConfirm({
|
||||
show: true,
|
||||
title: '清空全部账号配置',
|
||||
message: '将删除所有账号配置和账号级密钥/路径信息,不会删除主题、AI、MCP、HTTP API 等通用设置。',
|
||||
onConfirm: async () => {
|
||||
const result = await window.electronAPI.cache.clearAllAccountConfigs()
|
||||
if (result.success) {
|
||||
await refreshAccountsState()
|
||||
await loadConfig()
|
||||
showMessage('已清空全部账号配置', true)
|
||||
} else {
|
||||
showMessage(result.error || '清空全部账号配置失败', false)
|
||||
}
|
||||
setSecurityConfirm(prev => ({ ...prev, show: false }))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSelectDbPath = async () => {
|
||||
try {
|
||||
const result = await dialog.openFile({ title: '选择微信数据库根目录', properties: ['openDirectory'] })
|
||||
@@ -971,7 +1159,6 @@ function SettingsPage() {
|
||||
const result = await window.electronAPI.wcdb.testConnection(dbPath, decryptKey, wxid)
|
||||
if (result.success) {
|
||||
setIsAccountVerified(true)
|
||||
await configService.setMyWxid(wxid)
|
||||
showMessage(`账号目录验证成功:${wxid}`, true)
|
||||
} else {
|
||||
setIsAccountVerified(false)
|
||||
@@ -1013,15 +1200,20 @@ function SettingsPage() {
|
||||
|
||||
try {
|
||||
// 保存数据库相关配置
|
||||
if (decryptKey) await configService.setDecryptKey(decryptKey)
|
||||
if (dbPath) await configService.setDbPath(dbPath)
|
||||
if (wxid) await configService.setMyWxid(wxid)
|
||||
await configService.setCachePath(cachePath)
|
||||
let savedAccount: AccountProfile | null = null
|
||||
const accountPayload = buildAccountPayload()
|
||||
|
||||
if (editingAccountId) {
|
||||
savedAccount = await configService.updateAccount(editingAccountId, accountPayload)
|
||||
} else if (accountPayload.wxid || accountPayload.dbPath || accountPayload.decryptKey || accountPayload.cachePath) {
|
||||
savedAccount = await configService.saveAccount(accountPayload)
|
||||
}
|
||||
|
||||
if (savedAccount) {
|
||||
setEditingAccountId(savedAccount.id)
|
||||
}
|
||||
|
||||
// 保存图片密钥(包括空值)
|
||||
await configService.setImageXorKey(imageXorKey)
|
||||
await configService.setImageAesKey(imageAesKey)
|
||||
|
||||
// 保存导出路径
|
||||
if (exportPath) await configService.setExportPath(exportPath)
|
||||
|
||||
@@ -1060,6 +1252,8 @@ function SettingsPage() {
|
||||
setDbConnected(true, dbPath)
|
||||
}
|
||||
|
||||
await refreshAccountsState(savedAccount?.id || editingAccountId)
|
||||
|
||||
showMessage('配置保存成功', true)
|
||||
|
||||
// 保存成功后更新初始配置,重置变化状态
|
||||
@@ -1090,7 +1284,8 @@ function SettingsPage() {
|
||||
aiCustomSystemPrompt,
|
||||
aiEnableThinking,
|
||||
aiMessageLimit,
|
||||
closeToTray
|
||||
closeToTray,
|
||||
editingAccountId: savedAccount?.id || editingAccountId
|
||||
})
|
||||
setHasUnsavedChanges(false)
|
||||
} catch (e) {
|
||||
@@ -1249,9 +1444,64 @@ function SettingsPage() {
|
||||
{/* 引导窗口按钮 */}
|
||||
<div className="form-group">
|
||||
<button className="btn btn-secondary" onClick={handleOpenWelcomeWindow}>
|
||||
<Zap size={16} /> 打开配置引导窗口
|
||||
<Zap size={16} /> 新增账号引导
|
||||
</button>
|
||||
<span className="form-hint">使用引导窗口一步步完成配置</span>
|
||||
<span className="form-hint">使用引导窗口一步步新增账号,不会覆盖其他已保存账号</span>
|
||||
</div>
|
||||
|
||||
<h3 className="section-title">账号管理</h3>
|
||||
<div className="form-group">
|
||||
<div className="form-hint" style={{ marginBottom: '10px' }}>
|
||||
当前激活账号:{accountsList.find(item => item.id === activeAccountId)?.displayName || '未设置'}
|
||||
</div>
|
||||
{accountsList.length > 0 ? (
|
||||
<div className="wxid-options">
|
||||
{accountsList.map((account) => (
|
||||
<button
|
||||
key={account.id}
|
||||
className={`wxid-option ${editingAccountId === account.id ? 'is-selected' : ''}`}
|
||||
onClick={() => handleSelectAccountForEdit(account)}
|
||||
>
|
||||
<div className="wxid-option-name">
|
||||
{account.displayName}
|
||||
{account.id === activeAccountId ? '(当前激活)' : ''}
|
||||
</div>
|
||||
<div className="field-hint">{account.wxid || '未设置 wxid'}</div>
|
||||
<div className="field-hint">{account.dbPath || '未设置数据库路径'}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="form-hint">当前还没有已保存账号,请先新增一个账号。</div>
|
||||
)}
|
||||
<div className="btn-row" style={{ marginTop: '12px' }}>
|
||||
<button className="btn btn-secondary" onClick={handleSaveConfig} disabled={isLoading}>
|
||||
<Save size={16} /> 使用当前表单更新此账号
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={handleSwitchAccountAndReconnect} disabled={!editingAccountId || editingAccountId === activeAccountId || isLoading}>
|
||||
<RefreshCw size={16} /> 切换并重连
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={() => {
|
||||
const account = accountsList.find(item => item.id === editingAccountId)
|
||||
if (account) handleDeleteAccount(account)
|
||||
}}
|
||||
disabled={!editingAccountId || isLoading}
|
||||
>
|
||||
<Trash2 size={16} /> 删除账号
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={() => {
|
||||
const account = accountsList.find(item => item.id === editingAccountId)
|
||||
if (account) handleDeleteAccountWithLocalData(account)
|
||||
}}
|
||||
disabled={!editingAccountId || isLoading}
|
||||
>
|
||||
<Trash2 size={16} /> 删除并清理数据
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 数据库解密部分 */}
|
||||
@@ -1602,11 +1852,9 @@ function SettingsPage() {
|
||||
if (result.xorKey !== undefined) {
|
||||
const xorKeyHex = `0x${result.xorKey.toString(16).padStart(2, '0')}`
|
||||
setImageXorKey(xorKeyHex)
|
||||
await configService.setImageXorKey(xorKeyHex)
|
||||
}
|
||||
if (result.aesKey) {
|
||||
setImageAesKey(result.aesKey)
|
||||
await configService.setImageAesKey(result.aesKey)
|
||||
}
|
||||
showMessage('图片密钥获取成功!', true)
|
||||
setImageKeyStatus('')
|
||||
@@ -2562,12 +2810,12 @@ function SettingsPage() {
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={securityConfirm.onConfirm}
|
||||
>
|
||||
确定切换
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={securityConfirm.onConfirm}
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2698,10 +2946,16 @@ function SettingsPage() {
|
||||
<span className="cache-card-label">配置信息</span>
|
||||
</div>
|
||||
<div className="cache-card-desc">密钥、路径等</div>
|
||||
<button type="button" className="btn btn-secondary cache-card-btn" onClick={handleClearConfig}>
|
||||
<Trash2 size={14} /> 清除配置
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" className="btn btn-secondary cache-card-btn" onClick={handleClearCurrentAccount}>
|
||||
<Trash2 size={14} /> 清除当前账号
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary cache-card-btn" onClick={handleClearCurrentAccountConfig.bind(null, true)}>
|
||||
<Trash2 size={14} /> 删除当前账号并清理数据
|
||||
</button>
|
||||
<button type="button" className="btn btn-danger cache-card-btn" onClick={handleClearAllAccounts}>
|
||||
<Trash2 size={14} /> 清空全部账号配置
|
||||
</button>
|
||||
</div>
|
||||
<div className="cache-card cache-card-total">
|
||||
<div className="cache-card-header">
|
||||
<Layers size={20} className="cache-card-icon" />
|
||||
|
||||
+24
-13
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useThemeStore } from '../stores/themeStore'
|
||||
import { useAppStore } from '../stores/appStore'
|
||||
import { dialog } from '../services/ipc'
|
||||
@@ -27,7 +27,8 @@ interface WelcomePageProps {
|
||||
|
||||
function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
const navigate = useNavigate()
|
||||
const { isDbConnected, setDbConnected } = useAppStore()
|
||||
const location = useLocation()
|
||||
const { isDbConnected, setDbConnected, setMyWxid: setCurrentWxid } = useAppStore()
|
||||
const appIcon = useThemeStore(state => state.appIcon)
|
||||
const { enableAuth, disableAuth, isAuthEnabled } = useAuthStore()
|
||||
|
||||
@@ -67,6 +68,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
|
||||
const isMac = platformInfo.platform === 'darwin'
|
||||
const biometricLabel = isMac ? 'Touch ID' : 'Windows Hello'
|
||||
const isAddAccountMode = new URLSearchParams(location.search).get('mode') === 'add-account'
|
||||
|
||||
useEffect(() => {
|
||||
const removeStatus = window.electronAPI.wxKey?.onStatus?.((payload) => {
|
||||
@@ -95,6 +97,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
|
||||
// 从缓存加载配置
|
||||
const loadCachedConfig = () => {
|
||||
if (isAddAccountMode) return
|
||||
try {
|
||||
const cached = localStorage.getItem('welcomeConfig')
|
||||
if (cached) {
|
||||
@@ -144,7 +147,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
removeStatus?.()
|
||||
removeImageProgress?.()
|
||||
}
|
||||
}, [])
|
||||
}, [isAddAccountMode])
|
||||
|
||||
useEffect(() => {
|
||||
setWxidOptions([])
|
||||
@@ -182,6 +185,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
|
||||
// 保存配置到缓存
|
||||
useEffect(() => {
|
||||
if (isAddAccountMode) return
|
||||
const config = {
|
||||
dbPath,
|
||||
cachePath,
|
||||
@@ -195,7 +199,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
} catch (e) {
|
||||
console.error('保存配置到缓存失败:', e)
|
||||
}
|
||||
}, [dbPath, cachePath, wxid, decryptKey, imageXorKey, imageAesKey])
|
||||
}, [dbPath, cachePath, wxid, decryptKey, imageXorKey, imageAesKey, isAddAccountMode])
|
||||
|
||||
const currentStep = steps[stepIndex]
|
||||
const rootClassName = `welcome-page${isClosing ? ' is-closing' : ''}${standalone ? ' is-standalone' : ''}`
|
||||
@@ -548,17 +552,23 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
|
||||
try {
|
||||
// 先保存配置,因为 dataManagementService 需要从配置中读取这些信息
|
||||
await configService.setDbPath(dbPath)
|
||||
await configService.setDecryptKey(decryptKey)
|
||||
await configService.setMyWxid(wxid)
|
||||
await configService.setCachePath(cachePath)
|
||||
if (imageXorKey) {
|
||||
await configService.setImageXorKey(imageXorKey)
|
||||
}
|
||||
if (imageAesKey) {
|
||||
await configService.setImageAesKey(imageAesKey)
|
||||
const savedAccount = await configService.saveAccount({
|
||||
dbPath,
|
||||
decryptKey,
|
||||
wxid,
|
||||
cachePath,
|
||||
imageXorKey,
|
||||
imageAesKey,
|
||||
displayName: wxid || '未命名账号'
|
||||
})
|
||||
|
||||
if (!savedAccount) {
|
||||
throw new Error('保存账号配置失败')
|
||||
}
|
||||
|
||||
await configService.setActiveAccount(savedAccount.id)
|
||||
setCurrentWxid(wxid)
|
||||
|
||||
setDecryptStatus('正在测试数据库连接...')
|
||||
|
||||
const result = await window.electronAPI.wcdb.testConnection(dbPath, decryptKey, wxid)
|
||||
@@ -625,6 +635,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
||||
|
||||
// 在跳转前设置连接状态
|
||||
setDbConnected(true, dbPath)
|
||||
setCurrentWxid(wxid)
|
||||
|
||||
if (standalone) {
|
||||
setIsClosing(true)
|
||||
|
||||
+28
-1
@@ -1,5 +1,6 @@
|
||||
// 配置服务 - 封装 Electron Store
|
||||
import { config } from './ipc'
|
||||
import { accounts, config } from './ipc'
|
||||
import type { AccountProfile, AccountProfileInput, AccountProfilePatch } from '../types/account'
|
||||
|
||||
// 配置键名
|
||||
export const CONFIG_KEYS = {
|
||||
@@ -37,6 +38,8 @@ export const CONFIG_KEYS = {
|
||||
CLOSE_TO_TRAY: 'closeToTray'
|
||||
} as const
|
||||
|
||||
export type { AccountProfile, AccountProfileInput, AccountProfilePatch }
|
||||
|
||||
// 当前协议版本 - 更新协议内容时递增此版本号
|
||||
export const CURRENT_AGREEMENT_VERSION = 2
|
||||
|
||||
@@ -161,6 +164,30 @@ export async function setMyWxid(wxid: string): Promise<void> {
|
||||
await config.set(CONFIG_KEYS.MY_WXID, wxid)
|
||||
}
|
||||
|
||||
export async function listAccounts(): Promise<AccountProfile[]> {
|
||||
return accounts.list()
|
||||
}
|
||||
|
||||
export async function getActiveAccount(): Promise<AccountProfile | null> {
|
||||
return accounts.getActive()
|
||||
}
|
||||
|
||||
export async function setActiveAccount(accountId: string): Promise<AccountProfile | null> {
|
||||
return accounts.setActive(accountId)
|
||||
}
|
||||
|
||||
export async function saveAccount(profile: AccountProfileInput): Promise<AccountProfile | null> {
|
||||
return accounts.save(profile)
|
||||
}
|
||||
|
||||
export async function updateAccount(accountId: string, patch: AccountProfilePatch): Promise<AccountProfile | null> {
|
||||
return accounts.update(accountId, patch)
|
||||
}
|
||||
|
||||
export async function deleteAccount(accountId: string, deleteLocalData = false): Promise<{ success: boolean; error?: string; deleted?: AccountProfile | null; nextActiveAccountId?: string }> {
|
||||
return accounts.delete(accountId, deleteLocalData)
|
||||
}
|
||||
|
||||
// 获取主题
|
||||
export async function getTheme(): Promise<'light' | 'dark'> {
|
||||
const value = await config.get(CONFIG_KEYS.THEME)
|
||||
|
||||
@@ -6,6 +6,15 @@ export const config = {
|
||||
set: (key: string, value: unknown) => window.electronAPI.config.set(key, value)
|
||||
}
|
||||
|
||||
export const accounts = {
|
||||
list: () => window.electronAPI.accounts.list(),
|
||||
getActive: () => window.electronAPI.accounts.getActive(),
|
||||
setActive: (accountId: string) => window.electronAPI.accounts.setActive(accountId),
|
||||
save: (profile: Parameters<typeof window.electronAPI.accounts.save>[0]) => window.electronAPI.accounts.save(profile),
|
||||
update: (accountId: string, patch: Parameters<typeof window.electronAPI.accounts.update>[1]) => window.electronAPI.accounts.update(accountId, patch),
|
||||
delete: (accountId: string, deleteLocalData?: boolean) => window.electronAPI.accounts.delete(accountId, deleteLocalData)
|
||||
}
|
||||
|
||||
// 数据库
|
||||
export const db = {
|
||||
open: (dbPath: string, key?: string) => window.electronAPI.db.open(dbPath, key),
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface AccountProfile {
|
||||
id: string
|
||||
wxid: string
|
||||
dbPath: string
|
||||
decryptKey: string
|
||||
cachePath: string
|
||||
imageXorKey: string
|
||||
imageAesKey: string
|
||||
displayName: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
lastUsedAt: number
|
||||
}
|
||||
|
||||
export type AccountProfileInput = Omit<AccountProfile, 'id' | 'createdAt' | 'updatedAt' | 'lastUsedAt'>
|
||||
|
||||
export type AccountProfilePatch = Partial<AccountProfileInput> & {
|
||||
displayName?: string
|
||||
}
|
||||
Vendored
+12
-1
@@ -1,5 +1,6 @@
|
||||
import type { ChatSession, Message, Contact, ContactInfo } from './models'
|
||||
import type { SummaryResult } from './ai'
|
||||
import type { AccountProfile } from './account'
|
||||
|
||||
export interface ImageListItem {
|
||||
imagePath: string
|
||||
@@ -33,7 +34,7 @@ export interface ElectronAPI {
|
||||
openAnnualReportWindow: (year: number) => Promise<boolean>
|
||||
openAgreementWindow: () => Promise<boolean>
|
||||
openPurchaseWindow: () => Promise<boolean>
|
||||
openWelcomeWindow: () => Promise<boolean>
|
||||
openWelcomeWindow: (mode?: 'default' | 'add-account') => Promise<boolean>
|
||||
completeWelcome: () => Promise<boolean>
|
||||
isChatWindowOpen: () => Promise<boolean>
|
||||
closeChatWindow: () => Promise<boolean>
|
||||
@@ -57,6 +58,14 @@ export interface ElectronAPI {
|
||||
getTldCache: () => Promise<{ tlds: string[]; updatedAt: number } | null>
|
||||
setTldCache: (tlds: string[]) => Promise<void>
|
||||
}
|
||||
accounts: {
|
||||
list: () => Promise<AccountProfile[]>
|
||||
getActive: () => Promise<AccountProfile | null>
|
||||
setActive: (accountId: string) => Promise<AccountProfile | null>
|
||||
save: (profile: Omit<AccountProfile, 'id' | 'createdAt' | 'updatedAt' | 'lastUsedAt'>) => Promise<AccountProfile | null>
|
||||
update: (accountId: string, patch: Partial<Omit<AccountProfile, 'id' | 'createdAt' | 'updatedAt' | 'lastUsedAt'>>) => Promise<AccountProfile | null>
|
||||
delete: (accountId: string, deleteLocalData?: boolean) => Promise<{ success: boolean; error?: string; deleted?: AccountProfile | null; nextActiveAccountId?: string }>
|
||||
}
|
||||
db: {
|
||||
open: (dbPath: string, key?: string) => Promise<boolean>
|
||||
query: <T = unknown>(sql: string, params?: unknown[]) => Promise<T[]>
|
||||
@@ -812,6 +821,8 @@ export interface ElectronAPI {
|
||||
clearDatabases: () => Promise<{ success: boolean; error?: string }>
|
||||
clearAll: () => Promise<{ success: boolean; error?: string }>
|
||||
clearConfig: () => Promise<{ success: boolean; error?: string }>
|
||||
clearCurrentAccount: (deleteLocalData?: boolean) => Promise<{ success: boolean; error?: string }>
|
||||
clearAllAccountConfigs: () => Promise<{ success: boolean; error?: string }>
|
||||
getCacheSize: () => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
|
||||
Reference in New Issue
Block a user