From e67640a4c4cdd29b4a60fd6e4e5ac50f262bf90e Mon Sep 17 00:00:00 2001 From: ILoveBingLu Date: Tue, 7 Apr 2026 11:25:55 +0800 Subject: [PATCH] feat: add multi-account storage support --- electron/main.ts | 76 +++++- electron/preload.ts | 16 +- electron/services/cacheService.ts | 114 +++++---- electron/services/config.ts | 358 +++++++++++++++++++++++++++- src/pages/SettingsPage.tsx | 380 +++++++++++++++++++++++++----- src/pages/WelcomePage.tsx | 37 ++- src/services/config.ts | 29 ++- src/services/ipc.ts | 9 + src/types/account.ts | 19 ++ src/types/electron.d.ts | 13 +- 10 files changed, 915 insertions(+), 136 deletions(-) create mode 100644 src/types/account.ts diff --git a/electron/main.ts b/electron/main.ts index d66fbb7..489260c 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -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!) diff --git a/electron/preload.ts b/electron/preload.ts index d424a35..1287c7a 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -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, + getActive: () => ipcRenderer.invoke('accounts:getActive') as Promise, + setActive: (accountId: string) => ipcRenderer.invoke('accounts:setActive', accountId) as Promise, + save: (profile: Omit) => ipcRenderer.invoke('accounts:save', profile) as Promise, + update: (accountId: string, patch: Partial>) => + ipcRenderer.invoke('accounts:update', accountId, patch) as Promise, + 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: { diff --git a/electron/services/cacheService.ts b/electron/services/cacheService.ts index 27f021b..42ed059 100644 --- a/electron/services/cacheService.ts +++ b/electron/services/cacheService.ts @@ -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): 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 } -} \ No newline at end of file +} diff --git a/electron/services/config.ts b/electron/services/config.ts index 726d852..3a928bb 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -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(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(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, 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(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(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) diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index c1a65c7..a17db77 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -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([]) + 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() { {/* 引导窗口按钮 */}
- 使用引导窗口一步步完成配置 + 使用引导窗口一步步新增账号,不会覆盖其他已保存账号 +
+ +

账号管理

+
+
+ 当前激活账号:{accountsList.find(item => item.id === activeAccountId)?.displayName || '未设置'} +
+ {accountsList.length > 0 ? ( +
+ {accountsList.map((account) => ( + + ))} +
+ ) : ( +
当前还没有已保存账号,请先新增一个账号。
+ )} +
+ + + + +
{/* 数据库解密部分 */} @@ -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() { > 取消 - + @@ -2698,10 +2946,16 @@ function SettingsPage() { 配置信息
密钥、路径等
- - + + + +
diff --git a/src/pages/WelcomePage.tsx b/src/pages/WelcomePage.tsx index e0ca77f..39b4f97 100644 --- a/src/pages/WelcomePage.tsx +++ b/src/pages/WelcomePage.tsx @@ -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) diff --git a/src/services/config.ts b/src/services/config.ts index bdc1b4d..b590be7 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -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 { await config.set(CONFIG_KEYS.MY_WXID, wxid) } +export async function listAccounts(): Promise { + return accounts.list() +} + +export async function getActiveAccount(): Promise { + return accounts.getActive() +} + +export async function setActiveAccount(accountId: string): Promise { + return accounts.setActive(accountId) +} + +export async function saveAccount(profile: AccountProfileInput): Promise { + return accounts.save(profile) +} + +export async function updateAccount(accountId: string, patch: AccountProfilePatch): Promise { + 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) diff --git a/src/services/ipc.ts b/src/services/ipc.ts index e8b7bca..b33a10b 100644 --- a/src/services/ipc.ts +++ b/src/services/ipc.ts @@ -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[0]) => window.electronAPI.accounts.save(profile), + update: (accountId: string, patch: Parameters[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), diff --git a/src/types/account.ts b/src/types/account.ts new file mode 100644 index 0000000..a267aa6 --- /dev/null +++ b/src/types/account.ts @@ -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 + +export type AccountProfilePatch = Partial & { + displayName?: string +} diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index f870b91..d85b167 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -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 openAgreementWindow: () => Promise openPurchaseWindow: () => Promise - openWelcomeWindow: () => Promise + openWelcomeWindow: (mode?: 'default' | 'add-account') => Promise completeWelcome: () => Promise isChatWindowOpen: () => Promise closeChatWindow: () => Promise @@ -57,6 +58,14 @@ export interface ElectronAPI { getTldCache: () => Promise<{ tlds: string[]; updatedAt: number } | null> setTldCache: (tlds: string[]) => Promise } + accounts: { + list: () => Promise + getActive: () => Promise + setActive: (accountId: string) => Promise + save: (profile: Omit) => Promise + update: (accountId: string, patch: Partial>) => Promise + delete: (accountId: string, deleteLocalData?: boolean) => Promise<{ success: boolean; error?: string; deleted?: AccountProfile | null; nextActiveAccountId?: string }> + } db: { open: (dbPath: string, key?: string) => Promise query: (sql: string, params?: unknown[]) => Promise @@ -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;