From 325e7ccb8e8b923278286e42e00aecd7e6820c28 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 2 Aug 2025 10:32:28 +0800 Subject: [PATCH 1/2] refactor: add throttle and cache for provider model loading --- src/main/presenter/configPresenter/index.ts | 65 +++++++++++++++++-- .../llmProviderPresenter/baseProvider.ts | 11 ++-- .../settings/ModelProviderSettingsDetail.vue | 33 ++++++++-- 3 files changed, 90 insertions(+), 19 deletions(-) diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index d7a4fdb6f..1b3e14b1b 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -82,6 +82,8 @@ export class ConfigPresenter implements IConfigPresenter { private mcpConfHelper: McpConfHelper // 使用MCP配置助手 private modelConfigHelper: ModelConfigHelper // 模型配置助手 private knowledgeConfHelper: KnowledgeConfHelper // 知识配置助手 + // Model status memory cache for high-frequency read/write operations + private modelStatusCache: Map = new Map() constructor() { this.userDataPath = app.getPath('userData') @@ -318,30 +320,63 @@ export class ConfigPresenter implements IConfigPresenter { return `${MODEL_STATUS_KEY_PREFIX}${providerId}_${formattedModelId}` } - // 获取模型启用状态 + // 获取模型启用状态 (带内存缓存优化) getModelStatus(providerId: string, modelId: string): boolean { const statusKey = this.getModelStatusKey(providerId, modelId) + + // First check memory cache + if (this.modelStatusCache.has(statusKey)) { + return this.modelStatusCache.get(statusKey)! + } + + // Cache miss: read from settings and cache the result const status = this.getSetting(statusKey) - // 如果状态不是布尔值,则返回 true - return typeof status === 'boolean' ? status : true + const finalStatus = typeof status === 'boolean' ? status : true + this.modelStatusCache.set(statusKey, finalStatus) + + return finalStatus } - // 批量获取模型启用状态 + // 批量获取模型启用状态 (带内存缓存优化) getBatchModelStatus(providerId: string, modelIds: string[]): Record { const result: Record = {} + const uncachedKeys: string[] = [] + const uncachedModelIds: string[] = [] + + // First pass: check cache for all models for (const modelId of modelIds) { const statusKey = this.getModelStatusKey(providerId, modelId) + if (this.modelStatusCache.has(statusKey)) { + result[modelId] = this.modelStatusCache.get(statusKey)! + } else { + uncachedKeys.push(statusKey) + uncachedModelIds.push(modelId) + } + } + + // Second pass: fetch uncached values from settings and cache them + for (let i = 0; i < uncachedModelIds.length; i++) { + const modelId = uncachedModelIds[i] + const statusKey = uncachedKeys[i] const status = this.getSetting(statusKey) - // 如果状态不是布尔值,则返回 true - result[modelId] = typeof status === 'boolean' ? status : true + const finalStatus = typeof status === 'boolean' ? status : true + + // Cache the result and add to return object + this.modelStatusCache.set(statusKey, finalStatus) + result[modelId] = finalStatus } + return result } - // 设置模型启用状态 + // 设置模型启用状态 (同步更新内存缓存) setModelStatus(providerId: string, modelId: string, enabled: boolean): void { const statusKey = this.getModelStatusKey(providerId, modelId) + + // Update both settings and memory cache synchronously this.setSetting(statusKey, enabled) + this.modelStatusCache.set(statusKey, enabled) + // 触发模型状态变更事件(需要通知所有标签页) eventBus.sendToRenderer( CONFIG_EVENTS.MODEL_STATUS_CHANGED, @@ -362,6 +397,22 @@ export class ConfigPresenter implements IConfigPresenter { this.setModelStatus(providerId, modelId, false) } + // 清理模型状态缓存 (用于配置重载或重置场景) + clearModelStatusCache(): void { + this.modelStatusCache.clear() + } + + // 清理特定 provider 的模型状态缓存 + clearProviderModelStatusCache(providerId: string): void { + const keysToDelete: string[] = [] + for (const key of this.modelStatusCache.keys()) { + if (key.startsWith(`${MODEL_STATUS_KEY_PREFIX}${providerId}_`)) { + keysToDelete.push(key) + } + } + keysToDelete.forEach((key) => this.modelStatusCache.delete(key)) + } + // 批量设置模型状态 batchSetModelStatus(providerId: string, modelStatusMap: Record): void { for (const [modelId, enabled] of Object.entries(modelStatusMap)) { diff --git a/src/main/presenter/llmProviderPresenter/baseProvider.ts b/src/main/presenter/llmProviderPresenter/baseProvider.ts index 236fed82a..d7ccac177 100644 --- a/src/main/presenter/llmProviderPresenter/baseProvider.ts +++ b/src/main/presenter/llmProviderPresenter/baseProvider.ts @@ -146,11 +146,12 @@ export abstract class BaseLLMProvider { */ public async fetchModels(): Promise { try { - const models = await this.fetchProviderModels() - console.log('Fetched models:', models?.length, this.provider.id) - this.models = models - this.configPresenter.setProviderModels(this.provider.id, models) - return models + return this.fetchProviderModels().then((models) => { + console.log('Fetched models:', models?.length, this.provider.id) + this.models = models + this.configPresenter.setProviderModels(this.provider.id, models) + return models + }) } catch (e) { console.error('Failed to fetch models:', e) if (!this.models) { diff --git a/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue b/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue index 0c87001fd..bbd69f5f6 100644 --- a/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue +++ b/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue @@ -76,6 +76,7 @@ import ProviderDialogContainer from './ProviderDialogContainer.vue' import { useModelCheckStore } from '@/stores/modelCheck' import { levelToValueMap, safetyCategories } from '@/lib/gemini' import type { SafetyCategoryKey, SafetySettingValue } from '@/lib/gemini' +import { useThrottleFn } from '@vueuse/core' interface ProviderWebsites { official: string @@ -164,7 +165,8 @@ const validateApiKey = async () => { } } -const initData = async () => { +// Original initData implementation without debouncing +const _initData = async () => { console.log('initData for provider:', props.provider.id) const providerData = settingsStore.allProviderModels.find( (p) => p.providerId === props.provider.id @@ -223,12 +225,29 @@ const initData = async () => { } } +// Debounced version of initData to reduce frequent calls within 1 second +// Ensures the final call is always executed +const initData = useThrottleFn(_initData, 1000, true, true) + +// Immediate version for scenarios that require instant initialization +const initDataImmediate = _initData + +// Flag to track if this is the first initialization +let isFirstInit = true + watch( () => props.provider, async () => { apiKey.value = props.provider.apiKey || '' apiHost.value = props.provider.baseUrl || '' - await initData() // Ensure initData completes + + // Use immediate version for first initialization, debounced version for subsequent changes + if (isFirstInit) { + await initDataImmediate() + isFirstInit = false + } else { + initData() // Use debounced version for frequent changes + } }, { immediate: true } // Removed deep: true as provider object itself changes ) @@ -331,8 +350,8 @@ const handleSafetySettingChange = async (key: SafetyCategoryKey, level: number) // Handler for OAuth success const handleOAuthSuccess = async () => { console.log('OAuth authentication successful') - // OAuth成功后刷新provider数据 - await initData() + // OAuth成功后立即刷新provider数据 (使用立即版本以快速显示结果) + await initDataImmediate() // 可以自动验证一次 await validateApiKey() } @@ -344,9 +363,9 @@ const handleOAuthError = (error: string) => { } // Handler for config changes -const handleConfigChanged = async () => { - // 模型配置变更后重新初始化数据 - await initData() +const handleConfigChanged = () => { + // 模型配置变更后重新初始化数据 (使用防抖版本) + initData() } const openModelCheckDialog = () => { From ce7af4461898bc63db0e609289fae02de76c8f0e Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 2 Aug 2025 10:48:53 +0800 Subject: [PATCH 2/2] fix: search provider settings --- src/renderer/src/stores/settings.ts | 54 +++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/stores/settings.ts b/src/renderer/src/stores/settings.ts index 7d4062d93..758db5fce 100644 --- a/src/renderer/src/stores/settings.ts +++ b/src/renderer/src/stores/settings.ts @@ -370,6 +370,9 @@ export const useSettingsStore = defineStore('settings', () => { await initOrUpdateSearchAssistantModel() // 设置事件监听 setupProviderListener() + + // 设置搜索引擎事件监听 + setupSearchEnginesListener() } catch (error) { console.error('初始化设置失败:', error) } @@ -918,10 +921,37 @@ export const useSettingsStore = defineStore('settings', () => { const setSearchEngine = async (engineId: string) => { try { - const success = await threadP.setSearchEngine(engineId) + let success = await threadP.setSearchEngine(engineId) + + // 如果第一次设置失败,可能是后端搜索引擎列表还没更新,强制刷新后重试 + if (!success) { + console.log('第一次设置搜索引擎失败,尝试刷新搜索引擎列表后重试') + await refreshSearchEngines() + success = await threadP.setSearchEngine(engineId) + } + if (success) { - // 获取最新的引擎列表 - activeSearchEngine.value = searchEngines.value.find((e) => e.id === engineId) || null + // 先尝试从当前列表中查找 + let engine = searchEngines.value.find((e) => e.id === engineId) + + // 如果找不到,可能是新添加的自定义引擎,从后端重新获取 + if (!engine) { + try { + const customEngines = await configP.getCustomSearchEngines() + if (customEngines) { + engine = customEngines.find((e) => e.id === engineId) + } + } catch (error) { + console.warn('获取自定义搜索引擎失败:', error) + } + } + + activeSearchEngine.value = engine || null + + // 同时保存到配置中 + await configP.setSetting('searchEngine', engineId) + } else { + console.error('设置搜索引擎失败,engineId:', engineId) } } catch (error) { console.error('设置搜索引擎失败', error) @@ -1272,6 +1302,8 @@ export const useSettingsStore = defineStore('settings', () => { // 清理可能的事件监听器 const cleanup = () => { removeOllamaEventListeners() + // 清理搜索引擎事件监听器 + window.electron?.ipcRenderer?.removeAllListeners(CONFIG_EVENTS.SEARCH_ENGINES_UPDATED) } // 添加设置notificationsEnabled的方法 @@ -1308,12 +1340,22 @@ export const useSettingsStore = defineStore('settings', () => { window.electron.ipcRenderer.on(CONFIG_EVENTS.SEARCH_ENGINES_UPDATED, async () => { try { const customEngines = await configP.getCustomSearchEngines() + // 移除已有的自定义搜索引擎(避免重复) + searchEngines.value = searchEngines.value.filter((e) => !e.isCustom) + // 添加自定义搜索引擎(如果存在的话) if (customEngines && customEngines.length > 0) { - // 移除已有的自定义搜索引擎(避免重复) - searchEngines.value = searchEngines.value.filter((e) => !e.isCustom) - // 添加自定义搜索引擎 searchEngines.value.push(...customEngines) } + + // 刷新活跃搜索引擎状态,确保后端和前端状态同步 + const currentActiveEngineId = await configP.getSetting('searchEngine') + if (currentActiveEngineId) { + const engine = searchEngines.value.find((e) => e.id === currentActiveEngineId) + if (engine) { + activeSearchEngine.value = engine + await threadP.setActiveSearchEngine(currentActiveEngineId) + } + } } catch (error) { console.error('更新自定义搜索引擎失败:', error) }