feat: add multi-account storage support

This commit is contained in:
ILoveBingLu
2026-04-07 11:25:55 +08:00
parent 892bb38105
commit e67640a4c4
10 changed files with 915 additions and 136 deletions
+71 -5
View File
@@ -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
View File
@@ -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: {
+73 -41
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+9
View File
@@ -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),
+19
View File
@@ -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
}
+12 -1
View File
@@ -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;