From 08260499f80aa90c7f1ed03df1e39ab676daff1e Mon Sep 17 00:00:00 2001 From: digua Date: Tue, 19 May 2026 22:42:00 +0800 Subject: [PATCH] feat: migrate localStorage to backend storage with declarative persist plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add backendPersist Pinia plugin for declarative preferences.json persistence - Move Preferences types to @openchatlab/shared-types (single source of truth) - Migrate sessionGapThreshold from localStorage to config.toml [ui] - Migrate chatlab_filter_history from localStorage to preferences.json - Fix stale localStorage locale read in i18n/index.ts - Simplify usePreferencesSync (172 → 75 lines) by delegating to plugin Adding a new backend-synced preference now only requires 3 changes: 1. shared-types interface 2. node-runtime DEFAULTS 3. store backendPersist.pick --- apps/cli/src/http/index.ts | 2 + apps/cli/src/http/routes/preferences.ts | 58 ++++++ apps/desktop/main/i18n/index.ts | 55 ++---- apps/desktop/main/ipc/preferences.ts | 69 +++++++ apps/desktop/main/ipcMain.ts | 2 + apps/desktop/preload/apis/preferences.ts | 30 ++++ apps/desktop/preload/index.d.ts | 11 ++ apps/desktop/preload/index.ts | 4 + packages/config/src/index.ts | 2 +- packages/config/src/schema.ts | 9 +- packages/node-runtime/src/index.ts | 11 ++ packages/node-runtime/src/preferences.ts | 169 ++++++++++++++++++ packages/shared-types/index.ts | 88 +++++++++ src/App.vue | 3 + .../MessageExport/FilterHistory.vue | 69 ++----- src/components/analysis/SessionIndexModal.vue | 6 +- .../DataStorage/SessionIndexSection.vue | 12 +- src/composables/usePreferencesSync.ts | 80 +++++++++ src/composables/useUiConfig.ts | 29 +++ src/i18n/index.ts | 28 +-- src/main.ts | 2 + src/pages/home/components/AgreementModal.vue | 10 -- src/pages/home/components/ChangelogModal.vue | 6 +- .../home/components/DemoImportButton.vue | 4 +- src/pages/home/components/ImportArea.vue | 4 +- .../home/components/LanguageSelectModal.vue | 22 +-- src/plugins/backendPersist.ts | 106 +++++++++++ src/services/index.ts | 9 + src/services/preferences/electron.ts | 27 +++ src/services/preferences/fetch.ts | 29 +++ src/services/preferences/service.ts | 6 + src/services/preferences/types.ts | 28 +++ src/services/registry.ts | 6 + src/stores/prompt.ts | 10 +- src/stores/session.ts | 23 ++- src/stores/settings.ts | 19 +- src/stores/wordFilter.ts | 5 +- 37 files changed, 857 insertions(+), 196 deletions(-) create mode 100644 apps/cli/src/http/routes/preferences.ts create mode 100644 apps/desktop/main/ipc/preferences.ts create mode 100644 apps/desktop/preload/apis/preferences.ts create mode 100644 packages/node-runtime/src/preferences.ts create mode 100644 src/composables/usePreferencesSync.ts create mode 100644 src/composables/useUiConfig.ts create mode 100644 src/plugins/backendPersist.ts create mode 100644 src/services/preferences/electron.ts create mode 100644 src/services/preferences/fetch.ts create mode 100644 src/services/preferences/service.ts create mode 100644 src/services/preferences/types.ts diff --git a/apps/cli/src/http/index.ts b/apps/cli/src/http/index.ts index a0636a70..8bbcf4a1 100644 --- a/apps/cli/src/http/index.ts +++ b/apps/cli/src/http/index.ts @@ -25,6 +25,7 @@ import { registerSessionRoutes } from './routes/sessions' import { registerWebRoutes } from './routes/web' import { registerNlpRoutes } from './routes/nlp' import { registerAiRoutes } from './routes/ai' +import { registerPreferencesRoutes } from './routes/preferences' import { initServerAiLogger, closeServerAiLogger } from '../ai/logger' import { initSync, cleanupSync } from '../sync' @@ -133,6 +134,7 @@ export async function startHttpServer(options?: HttpServerOptions): Promise<{ registerNlpRoutes(server, dbManager) registerAiRoutes(server, dbManager, convManager) registerWebRoutes(server, dbManager, { pathProvider, nativeBinding }) + registerPreferencesRoutes(server, pathProvider) initSync(server, dbManager, pathProvider, { port, host, token }) diff --git a/apps/cli/src/http/routes/preferences.ts b/apps/cli/src/http/routes/preferences.ts new file mode 100644 index 00000000..19e982d5 --- /dev/null +++ b/apps/cli/src/http/routes/preferences.ts @@ -0,0 +1,58 @@ +/** + * Preferences HTTP routes — /_web/preferences/* + * + * Read/write preferences.json and config.toml [ui] section for Web UI. + */ + +import type { FastifyInstance } from 'fastify' +import { PreferencesManager, type Preferences } from '@openchatlab/node-runtime' +import { loadConfig, writeConfigField, type UiConfig } from '@openchatlab/config' +import type { PathProvider } from '@openchatlab/core' + +let prefManager: PreferencesManager | null = null + +export function registerPreferencesRoutes(server: FastifyInstance, pathProvider: PathProvider): void { + if (!prefManager) { + prefManager = new PreferencesManager(pathProvider.getSystemDir()) + } + + server.get('/_web/preferences', async () => { + return prefManager!.load() + }) + + server.patch<{ Body: Partial }>('/_web/preferences', async (request) => { + return prefManager!.save(request.body) + }) + + server.get('/_web/preferences/ui-config', async () => { + const config = loadConfig() + return config.ui + }) + + server.patch<{ Body: Partial }>('/_web/preferences/ui-config', async (request) => { + try { + for (const [key, value] of Object.entries(request.body)) { + if (value !== undefined) { + writeConfigField('ui', key, value as string | number) + } + } + return { success: true } + } catch (err) { + return { success: false, error: err instanceof Error ? err.message : String(err) } + } + }) + + server.get('/_web/preferences/locale', async () => { + const config = loadConfig() + return { lang: config.locale.lang } + }) + + server.patch<{ Body: { lang: string } }>('/_web/preferences/locale', async (request) => { + try { + writeConfigField('locale', 'lang', request.body.lang) + return { success: true } + } catch (err) { + return { success: false, error: err instanceof Error ? err.message : String(err) } + } + }) +} diff --git a/apps/desktop/main/i18n/index.ts b/apps/desktop/main/i18n/index.ts index e8b9a146..51b900f9 100644 --- a/apps/desktop/main/i18n/index.ts +++ b/apps/desktop/main/i18n/index.ts @@ -1,45 +1,19 @@ /** - * 主进程国际化模块 + * Main process i18n module * - * 基于 i18next,提供主进程的多语言支持。 - * 语言设置持久化在 settings/locale.json 中, - * 并通过 IPC 'locale:change' 与渲染进程同步。 + * Uses i18next for multi-language support. + * Locale is persisted in config.toml [locale] lang and synced + * with the renderer process via IPC 'locale:change'. */ import i18next from 'i18next' import { app, ipcMain } from 'electron' -import * as fs from 'fs' -import * as path from 'path' -import { getSettingsDir, ensureDir } from '../paths' +import { loadConfig, writeConfigField } from '@openchatlab/config' import zhCN from './locales/zh-CN' import enUS from './locales/en-US' import zhTW from './locales/zh-TW' import jaJP from './locales/ja-JP' -const LOCALE_FILE = 'locale.json' - -/** - * 获取 locale 配置文件路径 - */ -function getLocaleFilePath(): string { - return path.join(getSettingsDir(), LOCALE_FILE) -} - -/** - * 保存语言设置到文件 - */ -function saveLocale(lng: string): void { - try { - ensureDir(getSettingsDir()) - fs.writeFileSync(getLocaleFilePath(), JSON.stringify({ locale: lng }, null, 2), 'utf-8') - } catch (err) { - console.error('[i18n] Failed to save locale:', err) - } -} - -/** - * 从系统 locale 探测应用 locale - */ function detectSystemLocale(): string { const sysLocale = app.getLocale() if (sysLocale === 'zh-TW' || sysLocale === 'zh-Hant') return 'zh-TW' @@ -48,20 +22,13 @@ function detectSystemLocale(): string { return 'en-US' } -/** - * 初始化主进程国际化 - * - * 优先级:settings/locale.json > app.getLocale() 系统检测 > en-US 默认 - * 同时注册 IPC 监听器接收渲染进程的语言切换请求 - */ export async function initLocale(): Promise { let lng = 'en-US' try { - const filePath = getLocaleFilePath() - if (fs.existsSync(filePath)) { - const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) - if (data.locale) lng = data.locale + const config = loadConfig() + if (config.locale.lang) { + lng = config.locale.lang } else { lng = detectSystemLocale() } @@ -86,7 +53,11 @@ export async function initLocale(): Promise { ipcMain.on('locale:change', async (_event, newLocale: string) => { if (newLocale !== i18next.language) { await i18next.changeLanguage(newLocale) - saveLocale(newLocale) + try { + writeConfigField('locale', 'lang', newLocale) + } catch (err) { + console.error('[i18n] Failed to persist locale:', err) + } console.log(`[i18n] Locale changed to: ${newLocale}`) } }) diff --git a/apps/desktop/main/ipc/preferences.ts b/apps/desktop/main/ipc/preferences.ts new file mode 100644 index 00000000..b24b7997 --- /dev/null +++ b/apps/desktop/main/ipc/preferences.ts @@ -0,0 +1,69 @@ +/** + * Preferences IPC handlers + * + * Read/write ~/.chatlab/preferences.json and config.toml [ui] section. + */ + +import { ipcMain } from 'electron' +import type { IpcContext } from './types' +import { PreferencesManager, type Preferences } from '@openchatlab/node-runtime' +import { loadConfig, writeConfigField, type UiConfig } from '@openchatlab/config' +import { getSystemDataDir } from '../paths' + +let prefManager: PreferencesManager | null = null + +function getManager(): PreferencesManager { + if (!prefManager) { + prefManager = new PreferencesManager(getSystemDataDir()) + } + return prefManager +} + +export function registerPreferencesHandlers(_context: IpcContext): void { + console.log('[IpcMain] Registering preferences handlers...') + + ipcMain.handle('preferences:get', (): Preferences => { + return getManager().load() + }) + + ipcMain.handle('preferences:save', (_event, partial: Partial): { success: boolean; error?: string } => { + return getManager().save(partial) + }) + + ipcMain.handle('preferences:getUiConfig', (): UiConfig => { + const config = loadConfig() + return config.ui + }) + + ipcMain.handle( + 'preferences:saveUiConfig', + (_event, partial: Partial): { success: boolean; error?: string } => { + try { + for (const [key, value] of Object.entries(partial)) { + if (value !== undefined) { + writeConfigField('ui', key, value as string | number) + } + } + return { success: true } + } catch (err) { + return { success: false, error: err instanceof Error ? err.message : String(err) } + } + } + ) + + ipcMain.handle('preferences:getLocale', (): string => { + const config = loadConfig() + return config.locale.lang + }) + + ipcMain.handle('preferences:saveLocale', (_event, lang: string): { success: boolean; error?: string } => { + try { + writeConfigField('locale', 'lang', lang) + return { success: true } + } catch (err) { + return { success: false, error: err instanceof Error ? err.message : String(err) } + } + }) + + console.log('[IpcMain] Preferences handlers registered') +} diff --git a/apps/desktop/main/ipcMain.ts b/apps/desktop/main/ipcMain.ts index 6121859b..276c5c41 100644 --- a/apps/desktop/main/ipcMain.ts +++ b/apps/desktop/main/ipcMain.ts @@ -17,6 +17,7 @@ import { registerNlpHandlers } from './ipc/nlp' import { registerAnalyticsHandlers } from './analytics' import { registerApiHandlers, initApiServer, cleanupApiServer } from './ipc/api' import { registerDemoHandlers } from './ipc/demo' +import { registerPreferencesHandlers } from './ipc/preferences' // 导入 Worker 模块(用于异步分析查询和流式导入) import * as worker from './worker/workerManager' @@ -52,6 +53,7 @@ const mainIpcMain = (win: BrowserWindow) => { registerAnalyticsHandlers() registerApiHandlers(context) registerDemoHandlers(context) + registerPreferencesHandlers(context) // 启动 ChatLab API 服务(异步,不阻塞 IPC 注册) initApiServer(context).catch((err) => { diff --git a/apps/desktop/preload/apis/preferences.ts b/apps/desktop/preload/apis/preferences.ts new file mode 100644 index 00000000..1bfa812c --- /dev/null +++ b/apps/desktop/preload/apis/preferences.ts @@ -0,0 +1,30 @@ +/** + * Preferences Preload API + */ +import { ipcRenderer } from 'electron' + +export const preferencesApi = { + get: (): Promise> => { + return ipcRenderer.invoke('preferences:get') + }, + + save: (partial: Record): Promise<{ success: boolean; error?: string }> => { + return ipcRenderer.invoke('preferences:save', partial) + }, + + getUiConfig: (): Promise> => { + return ipcRenderer.invoke('preferences:getUiConfig') + }, + + saveUiConfig: (partial: Record): Promise<{ success: boolean; error?: string }> => { + return ipcRenderer.invoke('preferences:saveUiConfig', partial) + }, + + getLocale: (): Promise => { + return ipcRenderer.invoke('preferences:getLocale') + }, + + saveLocale: (lang: string): Promise<{ success: boolean; error?: string }> => { + return ipcRenderer.invoke('preferences:saveLocale', lang) + }, +} diff --git a/apps/desktop/preload/index.d.ts b/apps/desktop/preload/index.d.ts index af2aac3a..d6925be6 100644 --- a/apps/desktop/preload/index.d.ts +++ b/apps/desktop/preload/index.d.ts @@ -1159,6 +1159,15 @@ interface SessionApi { > } +interface PreferencesApi { + get: () => Promise> + save: (partial: Record) => Promise<{ success: boolean; error?: string }> + getUiConfig: () => Promise> + saveUiConfig: (partial: Record) => Promise<{ success: boolean; error?: string }> + getLocale: () => Promise + saveLocale: (lang: string) => Promise<{ success: boolean; error?: string }> +} + declare global { interface Window { electron: ElectronAPI @@ -1175,6 +1184,7 @@ declare global { sessionApi: SessionApi nlpApi: NlpApi apiServerApi: ApiServerApi + preferencesApi: PreferencesApi } } @@ -1237,4 +1247,5 @@ export { ApiServerStatus, DataSource, ImportSession, + PreferencesApi, } diff --git a/apps/desktop/preload/index.ts b/apps/desktop/preload/index.ts index 166fdcea..409bbfa8 100644 --- a/apps/desktop/preload/index.ts +++ b/apps/desktop/preload/index.ts @@ -11,6 +11,7 @@ import { chatApi, mergeApi } from './apis/chat' import { aiApi, llmApi, agentApi, assistantApi, skillApi } from './apis/ai' import { nlpApi, networkApi, cacheApi, sessionApi } from './apis/utils' import { apiServerApi } from './apis/api-server' +import { preferencesApi } from './apis/preferences' // 为渲染进程提供统一的类型入口,避免 type-only import 指向无导出的运行时代码。 export type { PreprocessConfig } from './apis/ai' @@ -46,6 +47,7 @@ if (process.contextIsolated) { contextBridge.exposeInMainWorld('sessionApi', sessionApi) contextBridge.exposeInMainWorld('nlpApi', nlpApi) contextBridge.exposeInMainWorld('apiServerApi', apiServerApi) + contextBridge.exposeInMainWorld('preferencesApi', preferencesApi) } catch (error) { console.error(error) } @@ -78,4 +80,6 @@ if (process.contextIsolated) { window.nlpApi = nlpApi // @ts-ignore (define in dts) window.apiServerApi = apiServerApi + // @ts-ignore (define in dts) + window.preferencesApi = preferencesApi } diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index a78a7b70..6f9ba4f5 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -6,7 +6,7 @@ export { loadConfig, getConfigPath, getConfigDir, writeConfigField } from './loader' export { configSchema } from './schema' -export type { ChatLabConfig, LlmConfig, DataConfig, ApiConfig, LocaleConfig } from './schema' +export type { ChatLabConfig, LlmConfig, DataConfig, ApiConfig, LocaleConfig, UiConfig } from './schema' export { loadAuthProfiles, getApiKeyByProfile, diff --git a/packages/config/src/schema.ts b/packages/config/src/schema.ts index a31b202b..722a4b0f 100644 --- a/packages/config/src/schema.ts +++ b/packages/config/src/schema.ts @@ -23,7 +23,12 @@ export const apiConfigSchema = z.object({ }) export const localeConfigSchema = z.object({ - lang: z.string().default('zh-CN'), + lang: z.string().default(''), +}) + +export const uiConfigSchema = z.object({ + default_session_tab: z.enum(['overview', 'ai-chat']).default('overview'), + session_gap_threshold: z.number().int().min(60).max(86400).default(1800), }) export const configSchema = z.object({ @@ -31,6 +36,7 @@ export const configSchema = z.object({ data: dataConfigSchema.default({}), api: apiConfigSchema.default({}), locale: localeConfigSchema.default({}), + ui: uiConfigSchema.default({}), }) export type ChatLabConfig = z.infer @@ -38,3 +44,4 @@ export type LlmConfig = z.infer export type DataConfig = z.infer export type ApiConfig = z.infer export type LocaleConfig = z.infer +export type UiConfig = z.infer diff --git a/packages/node-runtime/src/index.ts b/packages/node-runtime/src/index.ts index 8c3a2aee..286dbedc 100644 --- a/packages/node-runtime/src/index.ts +++ b/packages/node-runtime/src/index.ts @@ -223,6 +223,17 @@ export type { MigrationDeps } from './migrations' export { migrateFromElectronIfNeeded, verifyCliDataPath, wasElectronUsed } from './migrations' export type { ElectronMigrationResult } from './migrations' +// Preferences manager (preferences.json) +export { PreferencesManager } from './preferences' +export type { + Preferences, + AIGlobalSettings, + AIPreprocessConfig, + WordFilterScheme, + KeywordTemplate, + ContextCompressionSettings, +} from './preferences' + // Merger orchestration export { checkConflictsFromSources, buildMergedOutput, serializeChatLabToJsonl } from './merger' export type { diff --git a/packages/node-runtime/src/preferences.ts b/packages/node-runtime/src/preferences.ts new file mode 100644 index 00000000..be40deeb --- /dev/null +++ b/packages/node-runtime/src/preferences.ts @@ -0,0 +1,169 @@ +/** + * PreferencesManager — manages ~/.chatlab/preferences.json + * + * Stores complex user preferences that cannot fit into config.toml + * (arrays, nested objects) and need cross-client consistency. + */ + +import * as fs from 'fs' +import * as path from 'path' +import type { + Preferences, + AIGlobalSettings, + AIPreprocessConfig, + WordFilterScheme, + KeywordTemplate, + ContextCompressionSettings, + DesensitizeRule, + FilterHistoryItem, +} from '@openchatlab/shared-types' + +export type { + Preferences, + AIGlobalSettings, + AIPreprocessConfig, + WordFilterScheme, + KeywordTemplate, + ContextCompressionSettings, + DesensitizeRule, + FilterHistoryItem, +} + +const DEFAULTS: Preferences = { + pinnedSessionIds: [], + aiPreprocessConfig: { + dataCleaning: true, + mergeConsecutive: false, + mergeWindowSeconds: 180, + blacklistKeywords: [], + denoise: false, + desensitize: false, + desensitizeRules: [], + anonymizeNames: false, + }, + aiGlobalSettings: { + maxMessagesPerRequest: 1000, + exportFormat: 'markdown', + sqlExportFormat: 'csv', + enableAutoSkill: true, + searchContextBefore: 2, + searchContextAfter: 2, + contextCompression: { + enabled: true, + tokenThresholdPercent: 75, + bufferSizePercent: 20, + maxToolResultPercent: 50, + }, + }, + customKeywordTemplates: [], + deletedPresetTemplateIds: [], + wordFilter: { + schemes: [], + defaultSchemeId: null, + sessionSchemeOverrides: {}, + }, + filterHistory: [], +} + +export class PreferencesManager { + private filePath: string + private cache: Preferences | null = null + + constructor(systemDir: string) { + this.filePath = path.join(systemDir, 'preferences.json') + } + + load(): Preferences { + if (this.cache) return this.cache + + try { + if (fs.existsSync(this.filePath)) { + const raw = fs.readFileSync(this.filePath, 'utf-8') + const parsed = JSON.parse(raw) as Partial + this.cache = this.mergeDefaults(parsed) + return this.cache + } + } catch (err) { + console.warn('[Preferences] Failed to load preferences.json:', err) + } + + this.cache = { ...DEFAULTS } + return this.cache + } + + save(partial: Partial): { success: boolean; error?: string } { + try { + const current = this.load() + const merged = this.deepMerge( + current as unknown as Record, + partial as unknown as Record + ) + this.cache = merged as unknown as Preferences + + const dir = path.dirname(this.filePath) + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + fs.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2), 'utf-8') + return { success: true } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + console.error('[Preferences] Failed to save:', msg) + return { success: false, error: msg } + } + } + + getFilePath(): string { + return this.filePath + } + + invalidateCache(): void { + this.cache = null + } + + private mergeDefaults(partial: Partial): Preferences { + return { + pinnedSessionIds: partial.pinnedSessionIds ?? DEFAULTS.pinnedSessionIds, + aiPreprocessConfig: partial.aiPreprocessConfig + ? { ...DEFAULTS.aiPreprocessConfig, ...partial.aiPreprocessConfig } + : { ...DEFAULTS.aiPreprocessConfig }, + aiGlobalSettings: partial.aiGlobalSettings + ? { + ...DEFAULTS.aiGlobalSettings, + ...partial.aiGlobalSettings, + contextCompression: { + ...DEFAULTS.aiGlobalSettings.contextCompression, + ...(partial.aiGlobalSettings.contextCompression ?? {}), + }, + } + : { ...DEFAULTS.aiGlobalSettings }, + customKeywordTemplates: partial.customKeywordTemplates ?? DEFAULTS.customKeywordTemplates, + deletedPresetTemplateIds: partial.deletedPresetTemplateIds ?? DEFAULTS.deletedPresetTemplateIds, + wordFilter: partial.wordFilter ? { ...DEFAULTS.wordFilter, ...partial.wordFilter } : { ...DEFAULTS.wordFilter }, + filterHistory: partial.filterHistory ?? DEFAULTS.filterHistory, + } + } + + /** + * Deep merge: arrays are replaced (not concatenated), objects are merged. + */ + private deepMerge(base: Record, override: Record): Record { + const result = { ...base } + for (const [key, value] of Object.entries(override)) { + if (value === undefined) continue + if ( + value !== null && + typeof value === 'object' && + !Array.isArray(value) && + typeof base[key] === 'object' && + base[key] !== null && + !Array.isArray(base[key]) + ) { + result[key] = this.deepMerge(base[key] as Record, value as Record) + } else { + result[key] = value + } + } + return result + } +} diff --git a/packages/shared-types/index.ts b/packages/shared-types/index.ts index 7b3212a9..d603c5b3 100644 --- a/packages/shared-types/index.ts +++ b/packages/shared-types/index.ts @@ -109,3 +109,91 @@ export interface ParsedMessage { content: string | null replyToMessageId?: string } + +// ==================== Preferences (跨端偏好设置) ==================== + +export interface WordFilterScheme { + id: string + name: string + words: string[] + createdAt: number +} + +export interface ContextCompressionSettings { + enabled: boolean + tokenThresholdPercent: number + bufferSizePercent: number + maxToolResultPercent: number +} + +export interface AIGlobalSettings { + maxMessagesPerRequest: number + exportFormat: 'markdown' | 'txt' + sqlExportFormat: 'csv' | 'json' + enableAutoSkill: boolean + searchContextBefore: number + searchContextAfter: number + contextCompression: ContextCompressionSettings +} + +export interface KeywordTemplate { + id: string + name: string + keywords: string[] + [key: string]: unknown +} + +export interface DesensitizeRule { + id: string + label: string + pattern: string + replacement: string + enabled: boolean + builtin: boolean + locales: string[] +} + +export interface AIPreprocessConfig { + dataCleaning: boolean + mergeConsecutive: boolean + mergeWindowSeconds: number + blacklistKeywords: string[] + denoise: boolean + desensitize: boolean + desensitizeRules: DesensitizeRule[] + anonymizeNames: boolean +} + +export interface FilterHistoryItem { + id: string + sessionId: string + createdAt: number + name: string + mode: 'condition' | 'session' + conditionFilter?: { + keywords: string[] + timeRange: { start: number; end: number } | null + senderIds: number[] + contextSize: number + } + selectedSessionIds?: number[] +} + +export interface Preferences { + pinnedSessionIds: string[] + aiPreprocessConfig: AIPreprocessConfig + aiGlobalSettings: AIGlobalSettings + customKeywordTemplates: KeywordTemplate[] + deletedPresetTemplateIds: string[] + wordFilter: { + schemes: WordFilterScheme[] + defaultSchemeId: string | null + sessionSchemeOverrides: Record + } + filterHistory: FilterHistoryItem[] +} + +export interface UiConfig { + default_session_tab: 'overview' | 'ai-chat' + session_gap_threshold: number +} diff --git a/src/App.vue b/src/App.vue index e1111cd6..b329f394 100644 --- a/src/App.vue +++ b/src/App.vue @@ -14,6 +14,7 @@ import { useLayoutStore } from '@/stores/layout' import { useSettingsStore } from '@/stores/settings' import { useLLMStore } from '@/stores/llm' import { initServices } from '@/services' +import { initPreferencesSync } from '@/composables/usePreferencesSync' const { t } = useI18n() @@ -59,6 +60,8 @@ onMounted(async () => { // 初始化 Service 层(按领域注册 Adapter),必须在其他依赖 Adapter 的逻辑之前 await initServices() + // 从后端加载偏好设置并 hydrate stores + await initPreferencesSync() // 初始化语言设置(同步 i18n 和 dayjs,异步加载脱敏规则 — 依赖 AI adapter) await settingsStore.initLocale() // 初始化 LLM 配置(预加载,避免首次使用时延迟) diff --git a/src/components/MessageExport/FilterHistory.vue b/src/components/MessageExport/FilterHistory.vue index 417a865b..7a2420bf 100644 --- a/src/components/MessageExport/FilterHistory.vue +++ b/src/components/MessageExport/FilterHistory.vue @@ -1,15 +1,18 @@