Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 58 additions & 7 deletions src/main/presenter/configPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, boolean> = new Map()

constructor() {
this.userDataPath = app.getPath('userData')
Expand Down Expand Up @@ -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<boolean>(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<string, boolean> {
const result: Record<string, boolean> = {}
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<boolean>(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,
Expand All @@ -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<string, boolean>): void {
for (const [modelId, enabled] of Object.entries(modelStatusMap)) {
Expand Down
11 changes: 6 additions & 5 deletions src/main/presenter/llmProviderPresenter/baseProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,12 @@ export abstract class BaseLLMProvider {
*/
public async fetchModels(): Promise<MODEL_META[]> {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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()
}
Expand All @@ -344,9 +363,9 @@ const handleOAuthError = (error: string) => {
}

// Handler for config changes
const handleConfigChanged = async () => {
// 模型配置变更后重新初始化数据
await initData()
const handleConfigChanged = () => {
// 模型配置变更后重新初始化数据 (使用防抖版本)
initData()
}

const openModelCheckDialog = () => {
Expand Down
54 changes: 48 additions & 6 deletions src/renderer/src/stores/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,9 @@ export const useSettingsStore = defineStore('settings', () => {
await initOrUpdateSearchAssistantModel()
// 设置事件监听
setupProviderListener()

// 设置搜索引擎事件监听
setupSearchEnginesListener()
} catch (error) {
console.error('初始化设置失败:', error)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1272,6 +1302,8 @@ export const useSettingsStore = defineStore('settings', () => {
// 清理可能的事件监听器
const cleanup = () => {
removeOllamaEventListeners()
// 清理搜索引擎事件监听器
window.electron?.ipcRenderer?.removeAllListeners(CONFIG_EVENTS.SEARCH_ENGINES_UPDATED)
}

// 添加设置notificationsEnabled的方法
Expand Down Expand Up @@ -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<string>('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)
}
Expand Down