From 7a862974c256afeef90d0d8441e6c96489edaaf9 Mon Sep 17 00:00:00 2001 From: SuYao Date: Mon, 22 Dec 2025 16:13:31 +0800 Subject: [PATCH 01/24] fix(options): add support for persistent server configuration in OpenAI provider options (#12058) * fix(options): add support for persistent server configuration in OpenAI provider options * fix(options): disable storing in OpenAI provider options --- src/renderer/src/aiCore/utils/__tests__/options.test.ts | 3 ++- src/renderer/src/aiCore/utils/options.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/aiCore/utils/__tests__/options.test.ts b/src/renderer/src/aiCore/utils/__tests__/options.test.ts index 9eeeac725b..a6c9a6c95c 100644 --- a/src/renderer/src/aiCore/utils/__tests__/options.test.ts +++ b/src/renderer/src/aiCore/utils/__tests__/options.test.ts @@ -464,7 +464,8 @@ describe('options utils', () => { custom_param: 'custom_value', another_param: 123, serviceTier: undefined, - textVerbosity: undefined + textVerbosity: undefined, + store: false } }) }) diff --git a/src/renderer/src/aiCore/utils/options.ts b/src/renderer/src/aiCore/utils/options.ts index fd9bc590cd..4d8d4070e9 100644 --- a/src/renderer/src/aiCore/utils/options.ts +++ b/src/renderer/src/aiCore/utils/options.ts @@ -396,10 +396,12 @@ function buildOpenAIProviderOptions( } } + // TODO: 支持配置是否在服务端持久化 providerOptions = { ...providerOptions, serviceTier, - textVerbosity + textVerbosity, + store: false } return { From d1c93e4eae592f812bacc64046f93664e741351e Mon Sep 17 00:00:00 2001 From: SuYao Date: Tue, 23 Dec 2025 12:13:01 +0800 Subject: [PATCH 02/24] fix: update default assistant settings to disable temperature (#12069) * fix: update default assistant settings to disable temperature * fix: typecheck * fix: typecheck * refactor(settings): use DEFAULT_ASSISTANT_SETTINGS constant for reset Replace hardcoded default settings with DEFAULT_ASSISTANT_SETTINGS constant to improve maintainability * fix(AssistantService): set default maxTokens to DEFAULT_MAX_TOKENS * docs(AssistantService): add jsdoc for getAssistantSettings function * refactor(AssistantService): use default settings constants for fallback values * refactor(AssistantService): update default assistant settings type Add defaultModel field and mark settings as const satisfies AssistantSettings * refactor(AssistantService): reorder and add new default assistant settings Add reasoning_effort_cache and qwenThinkMode fields * docs(AssistantService): add jsdoc comments for default assistant settings Explain purpose of DEFAULT_ASSISTANT_SETTINGS template and clarify difference between template values and actual settings * docs(AssistantService): move default assistant settings docs to function The documentation about current settings inheritance was moved from createTranslateAssistant to the dedicated getDefaultAssistantSettings function where it belongs. This improves code organization and makes the documentation more accurate by placing it with the relevant function. * docs(AssistantService): clarify getDefaultAssistant behavior in jsdoc Explain the difference between this temporary instance and the actual default assistant from Redux store * fix: change default enableTemperature value to false The default value for enableTemperature was incorrectly set to true, which could lead to unexpected behavior. This change aligns it with the intended default behavior. --------- Co-authored-by: icarus --- .../AssistantModelSettings.tsx | 4 +- .../DefaultAssistantSettings.tsx | 18 +--- src/renderer/src/services/AssistantService.ts | 95 +++++++++++++++---- 3 files changed, 84 insertions(+), 33 deletions(-) diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx index bc594235a7..b45cecc586 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx @@ -41,7 +41,7 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA const [customParameters, setCustomParameters] = useState( assistant?.settings?.customParameters ?? [] ) - const [enableTemperature, setEnableTemperature] = useState(assistant?.settings?.enableTemperature ?? true) + const [enableTemperature, setEnableTemperature] = useState(assistant?.settings?.enableTemperature ?? false) const customParametersRef = useRef(customParameters) @@ -168,7 +168,7 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA const onReset = () => { setTemperature(DEFAULT_ASSISTANT_SETTINGS.temperature) - setEnableTemperature(DEFAULT_ASSISTANT_SETTINGS.enableTemperature ?? true) + setEnableTemperature(DEFAULT_ASSISTANT_SETTINGS.enableTemperature ?? false) setContextCount(DEFAULT_ASSISTANT_SETTINGS.contextCount) setEnableMaxTokens(DEFAULT_ASSISTANT_SETTINGS.enableMaxTokens ?? false) setMaxTokens(DEFAULT_ASSISTANT_SETTINGS.maxTokens ?? 0) diff --git a/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx b/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx index 65be642adc..0dfb121e94 100644 --- a/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx +++ b/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx @@ -4,9 +4,10 @@ import { ResetIcon } from '@renderer/components/Icons' import { HStack } from '@renderer/components/Layout' import Selector from '@renderer/components/Selector' import { TopView } from '@renderer/components/TopView' -import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant' +import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant' import { useTheme } from '@renderer/context/ThemeProvider' import { useDefaultAssistant } from '@renderer/hooks/useAssistant' +import { DEFAULT_ASSISTANT_SETTINGS } from '@renderer/services/AssistantService' import type { AssistantSettings as AssistantSettingsType } from '@renderer/types' import { getLeadingEmoji, modalConfirm } from '@renderer/utils' import { Button, Col, Divider, Flex, Input, InputNumber, Modal, Popover, Row, Slider, Switch, Tooltip } from 'antd' @@ -21,7 +22,7 @@ import { SettingContainer, SettingRow, SettingSubtitle } from '..' const AssistantSettings: FC = () => { const { defaultAssistant, updateDefaultAssistant } = useDefaultAssistant() const [temperature, setTemperature] = useState(defaultAssistant.settings?.temperature ?? DEFAULT_TEMPERATURE) - const [enableTemperature, setEnableTemperature] = useState(defaultAssistant.settings?.enableTemperature ?? true) + const [enableTemperature, setEnableTemperature] = useState(defaultAssistant.settings?.enableTemperature ?? false) const [contextCount, setContextCount] = useState(defaultAssistant.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT) const [enableMaxTokens, setEnableMaxTokens] = useState(defaultAssistant?.settings?.enableMaxTokens ?? false) const [maxTokens, setMaxTokens] = useState(defaultAssistant?.settings?.maxTokens ?? 0) @@ -81,18 +82,7 @@ const AssistantSettings: FC = () => { setToolUseMode('function') updateDefaultAssistant({ ...defaultAssistant, - settings: { - ...defaultAssistant.settings, - temperature: DEFAULT_TEMPERATURE, - enableTemperature: true, - contextCount: DEFAULT_CONTEXTCOUNT, - enableMaxTokens: false, - maxTokens: DEFAULT_MAX_TOKENS, - streamOutput: true, - topP: 1, - enableTopP: false, - toolUseMode: 'function' - } + settings: { ...DEFAULT_ASSISTANT_SETTINGS } }) } diff --git a/src/renderer/src/services/AssistantService.ts b/src/renderer/src/services/AssistantService.ts index 3983483aff..6f4ec188da 100644 --- a/src/renderer/src/services/AssistantService.ts +++ b/src/renderer/src/services/AssistantService.ts @@ -27,21 +27,51 @@ import { uuid } from '@renderer/utils' const logger = loggerService.withContext('AssistantService') +/** + * Default assistant settings configuration template. + * + * **Important**: This defines the DEFAULT VALUES for assistant settings, NOT the current settings + * of the default assistant. To get the actual settings of the default assistant, use `getDefaultAssistantSettings()`. + * + * Provides sensible defaults for all assistant settings with a focus on minimal parameter usage: + * - **Temperature disabled**: Use provider defaults by default + * - **MaxTokens disabled**: Use provider defaults by default + * - **TopP disabled**: Use provider defaults by default + * - **Streaming enabled**: Provides real-time response for better UX + * - **Standard context count**: Balanced memory usage and conversation length + */ export const DEFAULT_ASSISTANT_SETTINGS = { - temperature: DEFAULT_TEMPERATURE, - enableTemperature: true, - contextCount: DEFAULT_CONTEXTCOUNT, + maxTokens: DEFAULT_MAX_TOKENS, enableMaxTokens: false, - maxTokens: 0, - streamOutput: true, + temperature: DEFAULT_TEMPERATURE, + enableTemperature: false, topP: 1, enableTopP: false, - // It would gracefully fallback to prompt if not supported by model. - toolUseMode: 'function', + contextCount: DEFAULT_CONTEXTCOUNT, + streamOutput: true, + defaultModel: undefined, customParameters: [], - reasoning_effort: 'default' + reasoning_effort: 'default', + reasoning_effort_cache: undefined, + qwenThinkMode: undefined, + // It would gracefully fallback to prompt if not supported by model. + toolUseMode: 'function' } as const satisfies AssistantSettings +/** + * Creates a temporary default assistant instance. + * + * **Important**: This creates a NEW temporary assistant instance with DEFAULT_ASSISTANT_SETTINGS, + * NOT the actual default assistant from Redux store. This is used as a template for creating + * new assistants or as a fallback when no assistant is specified. + * + * To get the actual default assistant from Redux store (with current user settings), use: + * ```typescript + * const defaultAssistant = store.getState().assistants.defaultAssistant + * ``` + * + * @returns New temporary assistant instance with default settings + */ export function getDefaultAssistant(): Assistant { return { id: 'default', @@ -56,6 +86,14 @@ export function getDefaultAssistant(): Assistant { } } +/** + * Creates a default translate assistant. + * + * @param targetLanguage - Target language for translation + * @param text - Text to be translated + * @param _settings - Optional settings to override default assistant settings + * @returns Configured translate assistant + */ export function getDefaultTranslateAssistant( targetLanguage: TranslateLanguage, text: string, @@ -106,6 +144,17 @@ export function getDefaultTranslateAssistant( return translateAssistant } +/** + * Gets the CURRENT SETTINGS of the default assistant. + * + * **Important**: This returns the actual current settings of the default assistant (user-configured), + * NOT the DEFAULT_ASSISTANT_SETTINGS template. The settings may have been modified by the user + * from their initial default values. + * + * To get the template of default values, use DEFAULT_ASSISTANT_SETTINGS directly. + * + * @returns Current settings of the default assistant from store state + */ export function getDefaultAssistantSettings() { return store.getState().assistants.defaultAssistant.settings } @@ -165,6 +214,18 @@ export function getProviderByModelId(modelId?: string) { return providers.find((p) => p.models.find((m) => m.id === _modelId)) as Provider } +/** + * Retrieves and normalizes assistant settings with special transformation handling. + * + * **Special Transformations:** + * 1. **Context Count**: Converts `MAX_CONTEXT_COUNT` to `UNLIMITED_CONTEXT_COUNT` for internal processing + * 2. **Max Tokens**: Only returns a value when `enableMaxTokens` is true, otherwise returns `undefined` + * 3. **Max Tokens Validation**: Ensures maxTokens > 0, falls back to `DEFAULT_MAX_TOKENS` if invalid + * 4. **Fallback Defaults**: Applies system defaults for all undefined/missing settings + * + * @param assistant - The assistant instance to extract settings from + * @returns Normalized assistant settings with all transformations applied + */ export const getAssistantSettings = (assistant: Assistant): AssistantSettings => { const contextCount = assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT const getAssistantMaxTokens = () => { @@ -181,16 +242,16 @@ export const getAssistantSettings = (assistant: Assistant): AssistantSettings => return { contextCount: contextCount === MAX_CONTEXT_COUNT ? UNLIMITED_CONTEXT_COUNT : contextCount, temperature: assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE, - enableTemperature: assistant?.settings?.enableTemperature ?? true, - topP: assistant?.settings?.topP ?? 1, - enableTopP: assistant?.settings?.enableTopP ?? false, - enableMaxTokens: assistant?.settings?.enableMaxTokens ?? false, + enableTemperature: assistant?.settings?.enableTemperature ?? DEFAULT_ASSISTANT_SETTINGS.enableTemperature, + topP: assistant?.settings?.topP ?? DEFAULT_ASSISTANT_SETTINGS.topP, + enableTopP: assistant?.settings?.enableTopP ?? DEFAULT_ASSISTANT_SETTINGS.enableTopP, + enableMaxTokens: assistant?.settings?.enableMaxTokens ?? DEFAULT_ASSISTANT_SETTINGS.enableMaxTokens, maxTokens: getAssistantMaxTokens(), - streamOutput: assistant?.settings?.streamOutput ?? true, - toolUseMode: assistant?.settings?.toolUseMode ?? 'function', - defaultModel: assistant?.defaultModel ?? undefined, - reasoning_effort: assistant?.settings?.reasoning_effort ?? 'default', - customParameters: assistant?.settings?.customParameters ?? [] + streamOutput: assistant?.settings?.streamOutput ?? DEFAULT_ASSISTANT_SETTINGS.streamOutput, + toolUseMode: assistant?.settings?.toolUseMode ?? DEFAULT_ASSISTANT_SETTINGS.toolUseMode, + defaultModel: assistant?.defaultModel ?? DEFAULT_ASSISTANT_SETTINGS.defaultModel, + reasoning_effort: assistant?.settings?.reasoning_effort ?? DEFAULT_ASSISTANT_SETTINGS.reasoning_effort, + customParameters: assistant?.settings?.customParameters ?? DEFAULT_ASSISTANT_SETTINGS.customParameters } } From 6bdaba8a15899204aedcd3ab7cdca4b134d818a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=C2=B7Dong?= <98630204+GeorgeDong32@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:16:03 +0800 Subject: [PATCH 03/24] feat: add GLM-4.7 and MiniMax-M2.1 model support (#12071) --- src/renderer/src/config/models/default.ts | 12 ++++++++++++ src/renderer/src/config/models/reasoning.ts | 4 ++-- src/renderer/src/config/models/tooluse.ts | 3 ++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/config/models/default.ts b/src/renderer/src/config/models/default.ts index 37854c5749..f87293798d 100644 --- a/src/renderer/src/config/models/default.ts +++ b/src/renderer/src/config/models/default.ts @@ -617,6 +617,12 @@ export const SYSTEM_MODELS: Record = name: 'GLM-4.6', group: 'GLM-4.6' }, + { + id: 'glm-4.7', + provider: 'zhipu', + name: 'GLM-4.7', + group: 'GLM-4.7' + }, { id: 'glm-4.5', provider: 'zhipu', @@ -921,6 +927,12 @@ export const SYSTEM_MODELS: Record = provider: 'minimax', name: 'MiniMax M2 Stable', group: 'minimax-m2' + }, + { + id: 'MiniMax-M2.1', + provider: 'minimax', + name: 'MiniMax M2.1', + group: 'minimax-m2' } ], hyperbolic: [ diff --git a/src/renderer/src/config/models/reasoning.ts b/src/renderer/src/config/models/reasoning.ts index 144afc52a7..27a793bf7d 100644 --- a/src/renderer/src/config/models/reasoning.ts +++ b/src/renderer/src/config/models/reasoning.ts @@ -571,7 +571,7 @@ export const isSupportedReasoningEffortPerplexityModel = (model: Model): boolean export const isSupportedThinkingTokenZhipuModel = (model: Model): boolean => { const modelId = getLowerBaseModelName(model.id, '/') - return ['glm-4.5', 'glm-4.6'].some((id) => modelId.includes(id)) + return ['glm-4.5', 'glm-4.6', 'glm-4.7'].some((id) => modelId.includes(id)) } export const isSupportedThinkingTokenMiMoModel = (model: Model): boolean => { @@ -632,7 +632,7 @@ export const isMiniMaxReasoningModel = (model?: Model): boolean => { return false } const modelId = getLowerBaseModelName(model.id, '/') - return (['minimax-m1', 'minimax-m2'] as const).some((id) => modelId.includes(id)) + return (['minimax-m1', 'minimax-m2', 'minimax-m2.1'] as const).some((id) => modelId.includes(id)) } export function isReasoningModel(model?: Model): boolean { diff --git a/src/renderer/src/config/models/tooluse.ts b/src/renderer/src/config/models/tooluse.ts index 54d371dfda..2333db94d8 100644 --- a/src/renderer/src/config/models/tooluse.ts +++ b/src/renderer/src/config/models/tooluse.ts @@ -22,6 +22,7 @@ export const FUNCTION_CALLING_MODELS = [ 'deepseek', 'glm-4(?:-[\\w-]+)?', 'glm-4.5(?:-[\\w-]+)?', + 'glm-4.7(?:-[\\w-]+)?', 'learnlm(?:-[\\w-]+)?', 'gemini(?:-[\\w-]+)?', // 提前排除了gemini的嵌入模型 'grok-3(?:-[\\w-]+)?', @@ -30,7 +31,7 @@ export const FUNCTION_CALLING_MODELS = [ 'kimi-k2(?:-[\\w-]+)?', 'ling-\\w+(?:-[\\w-]+)?', 'ring-\\w+(?:-[\\w-]+)?', - 'minimax-m2', + 'minimax-m2(?:.1)?', 'mimo-v2-flash' ] as const From 6815ab65d124fb1aabdd674b9d107d346aa082b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Tue, 23 Dec 2025 13:21:29 +0800 Subject: [PATCH 04/24] fix(memory): fix retrieval issues and enable database backup (#12073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(memory): fix retrieval issues and enable database backup - Fix memory retrieval by storing model references instead of API client configs (baseURL was missing v1 suffix causing retrieval failures) - Move memory database to DATA_PATH/Memory for proper backup support - Add migration to convert legacy embedderApiClient/llmApiClient to model references - Simplify IPC handlers by removing unnecessary async/await wrappers - Rename and relocate MemorySettingsModal for better organization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * refactor(UserSelector): simplify user label rendering and remove unused dependencies - Update UserSelector component to directly use user IDs as labels instead of rendering them through a function. - Remove unnecessary dependency on the renderLabel function to streamline the code. * refactor(UserSelector): remove unused dependencies and simplify user avatar logic - Eliminate the getUserAvatar function and directly use user IDs for rendering. - Remove the HStack and Avatar components from the renderLabel function to streamline the UserSelector component. * refactor(ipc): simplify IPC handler for deleting all memories for a user and streamline error logging - Remove unnecessary async/await from the Memory_DeleteAllMemoriesForUser handler. - Simplify error logging in useAppInit hook for memory service configuration updates. - Update persisted reducer version from 191 to 189 in the store configuration. --------- Co-authored-by: Claude --- packages/shared/IpcChannel.ts | 1 + src/main/ipc.ts | 43 +++------- src/main/services/memory/MemoryService.ts | 51 ++++++++---- src/preload/index.ts | 3 +- .../src/aiCore/tools/MemorySearchTool.ts | 3 +- src/renderer/src/hooks/useAppInit.ts | 4 +- .../AssistantMemorySettings.tsx | 12 +-- .../MemorySettings/MemorySettings.tsx | 28 +++---- .../MemorySettings/MemorySettingsModal.tsx} | 82 +++++++------------ .../settings/MemorySettings/UserSelector.tsx | 32 ++------ src/renderer/src/services/MemoryProcessor.ts | 10 ++- src/renderer/src/services/MemoryService.ts | 29 +++++-- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/memory.ts | 2 +- src/renderer/src/store/migrate.ts | 30 +++++++ src/renderer/src/types/index.ts | 14 +--- 16 files changed, 170 insertions(+), 176 deletions(-) rename src/renderer/src/pages/{memory/settings-modal.tsx => settings/MemorySettings/MemorySettingsModal.tsx} (64%) diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index c97b258676..2ba327db93 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -318,6 +318,7 @@ export enum IpcChannel { Memory_DeleteUser = 'memory:delete-user', Memory_DeleteAllMemoriesForUser = 'memory:delete-all-memories-for-user', Memory_GetUsersList = 'memory:get-users-list', + Memory_MigrateMemoryDb = 'memory:migrate-memory-db', // TRACE TRACE_SAVE_DATA = 'trace:saveData', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 0bebb62fca..a4e0fe5c53 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -686,36 +686,19 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.KnowledgeBase_Check_Quota, KnowledgeService.checkQuota.bind(KnowledgeService)) // memory - ipcMain.handle(IpcChannel.Memory_Add, async (_, messages, config) => { - return await memoryService.add(messages, config) - }) - ipcMain.handle(IpcChannel.Memory_Search, async (_, query, config) => { - return await memoryService.search(query, config) - }) - ipcMain.handle(IpcChannel.Memory_List, async (_, config) => { - return await memoryService.list(config) - }) - ipcMain.handle(IpcChannel.Memory_Delete, async (_, id) => { - return await memoryService.delete(id) - }) - ipcMain.handle(IpcChannel.Memory_Update, async (_, id, memory, metadata) => { - return await memoryService.update(id, memory, metadata) - }) - ipcMain.handle(IpcChannel.Memory_Get, async (_, memoryId) => { - return await memoryService.get(memoryId) - }) - ipcMain.handle(IpcChannel.Memory_SetConfig, async (_, config) => { - memoryService.setConfig(config) - }) - ipcMain.handle(IpcChannel.Memory_DeleteUser, async (_, userId) => { - return await memoryService.deleteUser(userId) - }) - ipcMain.handle(IpcChannel.Memory_DeleteAllMemoriesForUser, async (_, userId) => { - return await memoryService.deleteAllMemoriesForUser(userId) - }) - ipcMain.handle(IpcChannel.Memory_GetUsersList, async () => { - return await memoryService.getUsersList() - }) + ipcMain.handle(IpcChannel.Memory_Add, (_, messages, config) => memoryService.add(messages, config)) + ipcMain.handle(IpcChannel.Memory_Search, (_, query, config) => memoryService.search(query, config)) + ipcMain.handle(IpcChannel.Memory_List, (_, config) => memoryService.list(config)) + ipcMain.handle(IpcChannel.Memory_Delete, (_, id) => memoryService.delete(id)) + ipcMain.handle(IpcChannel.Memory_Update, (_, id, memory, metadata) => memoryService.update(id, memory, metadata)) + ipcMain.handle(IpcChannel.Memory_Get, (_, memoryId) => memoryService.get(memoryId)) + ipcMain.handle(IpcChannel.Memory_SetConfig, (_, config) => memoryService.setConfig(config)) + ipcMain.handle(IpcChannel.Memory_DeleteUser, (_, userId) => memoryService.deleteUser(userId)) + ipcMain.handle(IpcChannel.Memory_DeleteAllMemoriesForUser, (_, userId) => + memoryService.deleteAllMemoriesForUser(userId) + ) + ipcMain.handle(IpcChannel.Memory_GetUsersList, () => memoryService.getUsersList()) + ipcMain.handle(IpcChannel.Memory_MigrateMemoryDb, () => memoryService.migrateMemoryDb()) // window ipcMain.handle(IpcChannel.Windows_SetMinimumSize, (_, width: number, height: number) => { diff --git a/src/main/services/memory/MemoryService.ts b/src/main/services/memory/MemoryService.ts index 3466e2c3c6..101dd54294 100644 --- a/src/main/services/memory/MemoryService.ts +++ b/src/main/services/memory/MemoryService.ts @@ -1,7 +1,9 @@ import type { Client } from '@libsql/client' import { createClient } from '@libsql/client' import { loggerService } from '@logger' +import { DATA_PATH } from '@main/config' import Embeddings from '@main/knowledge/embedjs/embeddings/Embeddings' +import { makeSureDirExists } from '@main/utils' import type { AddMemoryOptions, AssistantMessage, @@ -13,6 +15,7 @@ import type { } from '@types' import crypto from 'crypto' import { app } from 'electron' +import fs from 'fs' import path from 'path' import { MemoryQueries } from './queries' @@ -71,6 +74,21 @@ export class MemoryService { return MemoryService.instance } + /** + * Migrate the memory database from the old path to the new path + * If the old memory database exists, rename it to the new path + */ + public migrateMemoryDb(): void { + const oldMemoryDbPath = path.join(app.getPath('userData'), 'memories.db') + const memoryDbPath = path.join(DATA_PATH, 'Memory', 'memories.db') + + makeSureDirExists(path.dirname(memoryDbPath)) + + if (fs.existsSync(oldMemoryDbPath)) { + fs.renameSync(oldMemoryDbPath, memoryDbPath) + } + } + /** * Initialize the database connection and create tables */ @@ -80,11 +98,12 @@ export class MemoryService { } try { - const userDataPath = app.getPath('userData') - const dbPath = path.join(userDataPath, 'memories.db') + const memoryDbPath = path.join(DATA_PATH, 'Memory', 'memories.db') + + makeSureDirExists(path.dirname(memoryDbPath)) this.db = createClient({ - url: `file:${dbPath}`, + url: `file:${memoryDbPath}`, intMode: 'number' }) @@ -168,12 +187,13 @@ export class MemoryService { // Generate embedding if model is configured let embedding: number[] | null = null - const embedderApiClient = this.config?.embedderApiClient - if (embedderApiClient) { + const embeddingModel = this.config?.embeddingModel + + if (embeddingModel) { try { embedding = await this.generateEmbedding(trimmedMemory) logger.debug( - `Generated embedding for restored memory with dimension: ${embedding.length} (target: ${this.config?.embedderDimensions || MemoryService.UNIFIED_DIMENSION})` + `Generated embedding for restored memory with dimension: ${embedding.length} (target: ${this.config?.embeddingDimensions || MemoryService.UNIFIED_DIMENSION})` ) } catch (error) { logger.error('Failed to generate embedding for restored memory:', error as Error) @@ -211,11 +231,11 @@ export class MemoryService { // Generate embedding if model is configured let embedding: number[] | null = null - if (this.config?.embedderApiClient) { + if (this.config?.embeddingModel) { try { embedding = await this.generateEmbedding(trimmedMemory) logger.debug( - `Generated embedding with dimension: ${embedding.length} (target: ${this.config?.embedderDimensions || MemoryService.UNIFIED_DIMENSION})` + `Generated embedding with dimension: ${embedding.length} (target: ${this.config?.embeddingDimensions || MemoryService.UNIFIED_DIMENSION})` ) // Check for similar memories using vector similarity @@ -300,7 +320,7 @@ export class MemoryService { try { // If we have an embedder model configured, use vector search - if (this.config?.embedderApiClient) { + if (this.config?.embeddingModel) { try { const queryEmbedding = await this.generateEmbedding(query) return await this.hybridSearch(query, queryEmbedding, { limit, userId, agentId, filters }) @@ -497,11 +517,11 @@ export class MemoryService { // Generate new embedding if model is configured let embedding: number[] | null = null - if (this.config?.embedderApiClient) { + if (this.config?.embeddingModel) { try { embedding = await this.generateEmbedding(memory) logger.debug( - `Updated embedding with dimension: ${embedding.length} (target: ${this.config?.embedderDimensions || MemoryService.UNIFIED_DIMENSION})` + `Updated embedding with dimension: ${embedding.length} (target: ${this.config?.embeddingDimensions || MemoryService.UNIFIED_DIMENSION})` ) } catch (error) { logger.error('Failed to generate embedding for update:', error as Error) @@ -710,21 +730,22 @@ export class MemoryService { * Generate embedding for text */ private async generateEmbedding(text: string): Promise { - if (!this.config?.embedderApiClient) { + if (!this.config?.embeddingModel) { throw new Error('Embedder model not configured') } try { // Initialize embeddings instance if needed if (!this.embeddings) { - if (!this.config.embedderApiClient) { + if (!this.config.embeddingApiClient) { throw new Error('Embedder provider not configured') } this.embeddings = new Embeddings({ - embedApiClient: this.config.embedderApiClient, - dimensions: this.config.embedderDimensions + embedApiClient: this.config.embeddingApiClient, + dimensions: this.config.embeddingDimensions }) + await this.embeddings.init() } diff --git a/src/preload/index.ts b/src/preload/index.ts index 46ce84903f..d393d4a6e2 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -310,7 +310,8 @@ const api = { deleteUser: (userId: string) => ipcRenderer.invoke(IpcChannel.Memory_DeleteUser, userId), deleteAllMemoriesForUser: (userId: string) => ipcRenderer.invoke(IpcChannel.Memory_DeleteAllMemoriesForUser, userId), - getUsersList: () => ipcRenderer.invoke(IpcChannel.Memory_GetUsersList) + getUsersList: () => ipcRenderer.invoke(IpcChannel.Memory_GetUsersList), + migrateMemoryDb: () => ipcRenderer.invoke(IpcChannel.Memory_MigrateMemoryDb) }, window: { setMinimumSize: (width: number, height: number) => diff --git a/src/renderer/src/aiCore/tools/MemorySearchTool.ts b/src/renderer/src/aiCore/tools/MemorySearchTool.ts index 20064dd1b2..5028f2eb4d 100644 --- a/src/renderer/src/aiCore/tools/MemorySearchTool.ts +++ b/src/renderer/src/aiCore/tools/MemorySearchTool.ts @@ -24,7 +24,8 @@ export const memorySearchTool = () => { } const memoryConfig = selectMemoryConfig(store.getState()) - if (!memoryConfig.llmApiClient || !memoryConfig.embedderApiClient) { + + if (!memoryConfig.llmModel || !memoryConfig.embeddingModel) { return [] } diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts index 3ee9392ce5..360f8a5e2a 100644 --- a/src/renderer/src/hooks/useAppInit.ts +++ b/src/renderer/src/hooks/useAppInit.ts @@ -268,9 +268,7 @@ export function useAppInit() { // Update memory service configuration when it changes useEffect(() => { const memoryService = MemoryService.getInstance() - memoryService.updateConfig().catch((error) => { - logger.error('Failed to update memory config:', error) - }) + memoryService.updateConfig().catch((error) => logger.error('Failed to update memory config:', error)) }, [memoryConfig]) useEffect(() => { diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx index 8987d31bcd..c4d579bfbf 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantMemorySettings.tsx @@ -1,7 +1,7 @@ import { InfoCircleOutlined } from '@ant-design/icons' import { loggerService } from '@logger' import { Box } from '@renderer/components/Layout' -import MemoriesSettingsModal from '@renderer/pages/memory/settings-modal' +import MemoriesSettingsModal from '@renderer/pages/settings/MemorySettings/MemorySettingsModal' import MemoryService from '@renderer/services/MemoryService' import { selectGlobalMemoryEnabled, selectMemoryConfig } from '@renderer/store/memory' import type { Assistant, AssistantSettings } from '@renderer/types' @@ -68,7 +68,7 @@ const AssistantMemorySettings: React.FC = ({ assistant, updateAssistant, window.location.hash = '#/settings/memory' } - const isMemoryConfigured = memoryConfig.embedderApiClient && memoryConfig.llmApiClient + const isMemoryConfigured = memoryConfig.embeddingModel && memoryConfig.llmModel const isMemoryEnabled = globalMemoryEnabled && isMemoryConfigured return ( @@ -130,16 +130,16 @@ const AssistantMemorySettings: React.FC = ({ assistant, updateAssistant, {t('memory.stored_memories')}: {memoryStats.loading ? t('common.loading') : memoryStats.count} - {memoryConfig.embedderApiClient && ( + {memoryConfig.embeddingModel && (
{t('memory.embedding_model')}: - {memoryConfig.embedderApiClient.model} + {memoryConfig.embeddingModel.id}
)} - {memoryConfig.llmApiClient && ( + {memoryConfig.llmModel && (
{t('memory.llm_model')}: - {memoryConfig.llmApiClient.model} + {memoryConfig.llmModel.id}
)} diff --git a/src/renderer/src/pages/settings/MemorySettings/MemorySettings.tsx b/src/renderer/src/pages/settings/MemorySettings/MemorySettings.tsx index ecf8d74b68..6a454c5dd4 100644 --- a/src/renderer/src/pages/settings/MemorySettings/MemorySettings.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/MemorySettings.tsx @@ -5,7 +5,6 @@ import { HStack } from '@renderer/components/Layout' import TextBadge from '@renderer/components/TextBadge' import { useTheme } from '@renderer/context/ThemeProvider' import { useModel } from '@renderer/hooks/useModel' -import MemoriesSettingsModal from '@renderer/pages/memory/settings-modal' import MemoryService from '@renderer/services/MemoryService' import { selectCurrentUserId, @@ -34,6 +33,7 @@ import { SettingTitle } from '../index' import { DEFAULT_USER_ID } from './constants' +import MemorySettingsModal from './MemorySettingsModal' import UserSelector from './UserSelector' const logger = loggerService.withContext('MemorySettings') @@ -154,23 +154,17 @@ const EditMemoryModal: React.FC = ({ visible, memory, onCa open={visible} onCancel={onCancel} width={600} + centered + transitionName="animation-move-down" + okButtonProps={{ loading: loading, title: t('common.save'), onClick: () => form.submit() }} styles={{ header: { borderBottom: '0.5px solid var(--color-border)', - paddingBottom: 16 - }, - body: { - paddingTop: 24 + paddingBottom: 16, + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0 } - }} - footer={[ - , - - ]}> + }}>
{ } const memoryConfig = useSelector(selectMemoryConfig) - const embedderModel = useModel(memoryConfig.embedderApiClient?.model, memoryConfig.embedderApiClient?.provider) + const embeddingModel = useModel(memoryConfig.embeddingModel?.id, memoryConfig.embeddingModel?.provider) const handleGlobalMemoryToggle = async (enabled: boolean) => { - if (enabled && !embedderModel) { + if (enabled && !embeddingModel) { window.keyv.set('memory.wait.settings', true) return setSettingsModalVisible(true) } @@ -799,7 +793,7 @@ const MemorySettings = () => { existingUsers={[...uniqueUsers, DEFAULT_USER_ID]} /> - await handleSettingsSubmit()} onCancel={handleSettingsCancel} diff --git a/src/renderer/src/pages/memory/settings-modal.tsx b/src/renderer/src/pages/settings/MemorySettings/MemorySettingsModal.tsx similarity index 64% rename from src/renderer/src/pages/memory/settings-modal.tsx rename to src/renderer/src/pages/settings/MemorySettings/MemorySettingsModal.tsx index 996baebd1d..509e54e54a 100644 --- a/src/renderer/src/pages/memory/settings-modal.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/MemorySettingsModal.tsx @@ -1,10 +1,9 @@ import { loggerService } from '@logger' -import AiProvider from '@renderer/aiCore' import InputEmbeddingDimension from '@renderer/components/InputEmbeddingDimension' import ModelSelector from '@renderer/components/ModelSelector' import { InfoTooltip } from '@renderer/components/TooltipIcons' import { isEmbeddingModel, isRerankModel } from '@renderer/config/models' -import { useModel } from '@renderer/hooks/useModel' +import { getModel, useModel } from '@renderer/hooks/useModel' import { useProviders } from '@renderer/hooks/useProvider' import { getModelUniqId } from '@renderer/services/ModelService' import { selectMemoryConfig, updateMemoryConfig } from '@renderer/store/memory' @@ -12,12 +11,12 @@ import type { Model } from '@renderer/types' import { Flex, Form, Modal } from 'antd' import { t } from 'i18next' import type { FC } from 'react' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -const logger = loggerService.withContext('MemoriesSettingsModal') +const logger = loggerService.withContext('MemorySettingsModal') -interface MemoriesSettingsModalProps { +interface MemorySettingsModalProps { visible: boolean onSubmit: (values: any) => void onCancel: () => void @@ -26,78 +25,57 @@ interface MemoriesSettingsModalProps { type formValue = { llmModel: string - embedderModel: string - embedderDimensions: number + embeddingModel: string + embeddingDimensions: number } -const MemoriesSettingsModal: FC = ({ visible, onSubmit, onCancel, form }) => { +const MemorySettingsModal: FC = ({ visible, onSubmit, onCancel, form }) => { const { providers } = useProviders() const dispatch = useDispatch() const memoryConfig = useSelector(selectMemoryConfig) const [loading, setLoading] = useState(false) // Get all models for lookup - const allModels = useMemo(() => providers.flatMap((p) => p.models), [providers]) - const llmModel = useModel(memoryConfig.llmApiClient?.model, memoryConfig.llmApiClient?.provider) - const embedderModel = useModel(memoryConfig.embedderApiClient?.model, memoryConfig.embedderApiClient?.provider) - - const findModelById = useCallback( - (id: string | undefined) => (id ? allModels.find((m) => getModelUniqId(m) === id) : undefined), - [allModels] - ) + const llmModel = useModel(memoryConfig.llmModel?.id, memoryConfig.llmModel?.provider) + const embeddingModel = useModel(memoryConfig.embeddingModel?.id, memoryConfig.embeddingModel?.provider) // Initialize form with current memory config when modal opens useEffect(() => { if (visible && memoryConfig) { form.setFieldsValue({ llmModel: getModelUniqId(llmModel), - embedderModel: getModelUniqId(embedderModel), - embedderDimensions: memoryConfig.embedderDimensions + embeddingModel: getModelUniqId(embeddingModel), + embeddingDimensions: memoryConfig.embeddingDimensions // customFactExtractionPrompt: memoryConfig.customFactExtractionPrompt, // customUpdateMemoryPrompt: memoryConfig.customUpdateMemoryPrompt }) } - }, [visible, memoryConfig, form, llmModel, embedderModel]) + }, [embeddingModel, form, llmModel, memoryConfig, visible]) const handleFormSubmit = async (values: formValue) => { try { // Convert model IDs back to Model objects - const llmModel = findModelById(values.llmModel) - const llmProvider = providers.find((p) => p.id === llmModel?.provider) - const aiLlmProvider = new AiProvider(llmProvider!) - const embedderModel = findModelById(values.embedderModel) - const embedderProvider = providers.find((p) => p.id === embedderModel?.provider) - const aiEmbedderProvider = new AiProvider(embedderProvider!) - if (embedderModel) { + const llmModel = getModel(values.llmModel) + const embeddingModel = getModel(values.embeddingModel) + + if (embeddingModel) { setLoading(true) - const provider = providers.find((p) => p.id === embedderModel.provider) + const provider = providers.find((p) => p.id === embeddingModel.provider) if (!provider) { return } const finalDimensions = - typeof values.embedderDimensions === 'string' - ? parseInt(values.embedderDimensions) - : values.embedderDimensions + typeof values.embeddingDimensions === 'string' + ? parseInt(values.embeddingDimensions) + : values.embeddingDimensions const updatedConfig = { ...memoryConfig, - llmApiClient: { - model: llmModel?.id ?? '', - provider: llmProvider?.id ?? '', - apiKey: aiLlmProvider.getApiKey(), - baseURL: aiLlmProvider.getBaseURL(), - apiVersion: llmProvider?.apiVersion - }, - embedderApiClient: { - model: embedderModel?.id ?? '', - provider: embedderProvider?.id ?? '', - apiKey: aiEmbedderProvider.getApiKey(), - baseURL: aiEmbedderProvider.getBaseURL(), - apiVersion: embedderProvider?.apiVersion - }, - embedderDimensions: finalDimensions + llmModel, + embeddingModel, + embeddingDimensions: finalDimensions // customFactExtractionPrompt: values.customFactExtractionPrompt, // customUpdateMemoryPrompt: values.customUpdateMemoryPrompt } @@ -150,7 +128,7 @@ const MemoriesSettingsModal: FC = ({ visible, onSubm = ({ visible, onSubm prevValues.embedderModel !== currentValues.embedderModel}> + shouldUpdate={(prevValues, currentValues) => prevValues.embeddingModel !== currentValues.embeddingModel}> {({ getFieldValue }) => { - const embedderModelId = getFieldValue('embedderModel') - const embedderModel = findModelById(embedderModelId) + const embeddingModelId = getFieldValue('embeddingModel') + const embeddingModel = getModel(embeddingModelId) return ( = ({ visible, onSubm } - name="embedderDimensions" + name="embeddingDimensions" rules={[ { validator(_, value) { @@ -183,7 +161,7 @@ const MemoriesSettingsModal: FC = ({ visible, onSubm } } ]}> - + ) }} @@ -199,4 +177,4 @@ const MemoriesSettingsModal: FC = ({ visible, onSubm ) } -export default MemoriesSettingsModal +export default MemorySettingsModal diff --git a/src/renderer/src/pages/settings/MemorySettings/UserSelector.tsx b/src/renderer/src/pages/settings/MemorySettings/UserSelector.tsx index d11318c25f..2521fcad20 100644 --- a/src/renderer/src/pages/settings/MemorySettings/UserSelector.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/UserSelector.tsx @@ -1,7 +1,6 @@ -import { HStack } from '@renderer/components/Layout' -import { Avatar, Button, Select, Space, Tooltip } from 'antd' +import { Button, Select, Space, Tooltip } from 'antd' import { UserRoundPlus } from 'lucide-react' -import { useCallback, useMemo } from 'react' +import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { DEFAULT_USER_ID } from './constants' @@ -16,39 +15,18 @@ interface UserSelectorProps { const UserSelector: React.FC = ({ currentUser, uniqueUsers, onUserSwitch, onAddUser }) => { const { t } = useTranslation() - const getUserAvatar = useCallback((user: string) => { - return user === DEFAULT_USER_ID ? user.slice(0, 1).toUpperCase() : user.slice(0, 2).toUpperCase() - }, []) - - const renderLabel = useCallback( - (userId: string, userName: string) => { - return ( - - - {getUserAvatar(userId)} - - {userName} - - ) - }, - [getUserAvatar] - ) - const options = useMemo(() => { const defaultOption = { value: DEFAULT_USER_ID, - label: renderLabel(DEFAULT_USER_ID, t('memory.default_user')) + label: t('memory.default_user') } const userOptions = uniqueUsers .filter((user) => user !== DEFAULT_USER_ID) - .map((user) => ({ - value: user, - label: renderLabel(user, user) - })) + .map((user) => ({ value: user, label: user })) return [defaultOption, ...userOptions] - }, [renderLabel, t, uniqueUsers]) + }, [t, uniqueUsers]) return ( diff --git a/src/renderer/src/services/MemoryProcessor.ts b/src/renderer/src/services/MemoryProcessor.ts index 01ba5eeb77..7e9291cfd9 100644 --- a/src/renderer/src/services/MemoryProcessor.ts +++ b/src/renderer/src/services/MemoryProcessor.ts @@ -40,7 +40,7 @@ export class MemoryProcessor { try { const { memoryConfig } = config - if (!memoryConfig.llmApiClient) { + if (!memoryConfig.llmModel) { throw new Error('No LLM model configured for memory processing') } @@ -53,8 +53,9 @@ export class MemoryProcessor { const responseContent = await fetchGenerate({ prompt: systemPrompt, content: userPrompt, - model: getModel(memoryConfig.llmApiClient.model, memoryConfig.llmApiClient.provider) + model: getModel(memoryConfig.llmModel.id, memoryConfig.llmModel.provider) }) + if (!responseContent || responseContent.trim() === '') { return [] } @@ -100,9 +101,10 @@ export class MemoryProcessor { const { memoryConfig, assistantId, userId, lastMessageId } = config - if (!memoryConfig.llmApiClient) { + if (!memoryConfig.llmModel) { throw new Error('No LLM model configured for memory processing') } + const existingMemoriesResult = (window.keyv.get(`memory-search-${lastMessageId}`) as MemoryItem[]) || [] const existingMemories = existingMemoriesResult.map((memory) => ({ @@ -123,7 +125,7 @@ export class MemoryProcessor { const responseContent = await fetchGenerate({ prompt: updateMemorySystemPrompt, content: updateMemoryUserPrompt, - model: getModel(memoryConfig.llmApiClient.model, memoryConfig.llmApiClient.provider) + model: getModel(memoryConfig.llmModel.id, memoryConfig.llmModel.provider) }) if (!responseContent || responseContent.trim() === '') { return [] diff --git a/src/renderer/src/services/MemoryService.ts b/src/renderer/src/services/MemoryService.ts index 8f572194df..d7d575886c 100644 --- a/src/renderer/src/services/MemoryService.ts +++ b/src/renderer/src/services/MemoryService.ts @@ -1,14 +1,19 @@ import { loggerService } from '@logger' +import { getModel } from '@renderer/hooks/useModel' import store from '@renderer/store' import { selectMemoryConfig } from '@renderer/store/memory' import type { AddMemoryOptions, AssistantMessage, + KnowledgeBase, MemoryHistoryItem, MemoryListOptions, MemorySearchOptions, MemorySearchResult } from '@types' +import { now } from 'lodash' + +import { getKnowledgeBaseParams } from './KnowledgeService' const logger = loggerService.withContext('MemoryService') @@ -203,16 +208,24 @@ class MemoryService { } const memoryConfig = selectMemoryConfig(store.getState()) - const embedderApiClient = memoryConfig.embedderApiClient - const llmApiClient = memoryConfig.llmApiClient + const embeddingModel = memoryConfig.embeddingModel - const configWithProviders = { + // Get knowledge base params for memory + const { embedApiClient: embeddingApiClient } = getKnowledgeBaseParams({ + id: 'memory', + name: 'Memory', + model: getModel(embeddingModel?.id, embeddingModel?.provider), + dimensions: memoryConfig.embeddingDimensions, + items: [], + created_at: now(), + updated_at: now(), + version: 1 + } as KnowledgeBase) + + return window.api.memory.setConfig({ ...memoryConfig, - embedderApiClient, - llmApiClient - } - - return window.api.memory.setConfig(configWithProviders) + embeddingApiClient + }) } catch (error) { logger.warn('Failed to update memory config:', error as Error) return diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 0a079df9b5..15f45648dc 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -67,7 +67,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 188, + version: 189, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'], migrate }, diff --git a/src/renderer/src/store/memory.ts b/src/renderer/src/store/memory.ts index 808f2154af..e28b291c19 100644 --- a/src/renderer/src/store/memory.ts +++ b/src/renderer/src/store/memory.ts @@ -17,7 +17,7 @@ export interface MemoryState { // Default memory configuration to avoid undefined errors const defaultMemoryConfig: MemoryConfig = { - embedderDimensions: 1536, + embeddingDimensions: undefined, isAutoDimensions: true, customFactExtractionPrompt: factExtractionPrompt, customUpdateMemoryPrompt: updateMemorySystemPrompt diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index af789378c8..4b9d41b9e4 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -18,6 +18,7 @@ import { TRANSLATE_PROMPT } from '@renderer/config/prompts' import { SYSTEM_PROVIDERS } from '@renderer/config/providers' import { DEFAULT_SIDEBAR_ICONS } from '@renderer/config/sidebar' import db from '@renderer/databases' +import { getModel } from '@renderer/hooks/useModel' import i18n from '@renderer/i18n' import { DEFAULT_ASSISTANT_SETTINGS } from '@renderer/services/AssistantService' import { defaultPreprocessProviders } from '@renderer/store/preprocess' @@ -3068,6 +3069,35 @@ const migrateConfig = { logger.error('migrate 188 error', error as Error) return state } + }, + // 1.7.7 + '189': (state: RootState) => { + try { + window.api.memory.migrateMemoryDb() + // @ts-ignore + const memoryLlmApiClient = state?.memory?.memoryConfig?.llmApiClient + // @ts-ignore + const memoryEmbeddingApiClient = state?.memory?.memoryConfig?.embedderApiClient + + if (memoryLlmApiClient) { + state.memory.memoryConfig.llmModel = getModel(memoryLlmApiClient.model, memoryLlmApiClient.provider) + // @ts-ignore + delete state.memory.memoryConfig.llmApiClient + } + + if (memoryEmbeddingApiClient) { + state.memory.memoryConfig.embeddingModel = getModel( + memoryEmbeddingApiClient.model, + memoryEmbeddingApiClient.provider + ) + // @ts-ignore + delete state.memory.memoryConfig.embedderApiClient + } + return state + } catch (error) { + logger.error('migrate 189 error', error as Error) + return state + } } } diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index eefa380a66..126c97686e 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -915,17 +915,11 @@ export * from './tool' // Memory Service Types // ======================================================================== export interface MemoryConfig { - /** - * @deprecated use embedderApiClient instead - */ - embedderModel?: Model - embedderDimensions?: number - /** - * @deprecated use llmApiClient instead - */ + embeddingDimensions?: number + embeddingModel?: Model llmModel?: Model - embedderApiClient?: ApiClient - llmApiClient?: ApiClient + // Dynamically retrieved, not persistently stored + embeddingApiClient?: ApiClient customFactExtractionPrompt?: string customUpdateMemoryPrompt?: string /** Indicates whether embedding dimensions are automatically detected */ From 5f0006dcede235cf06bd630864f3afb13ef552bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Tue, 23 Dec 2025 13:22:02 +0800 Subject: [PATCH 05/24] refactor(websearch): redesign settings with two-column layout (#12068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactor WebSearchSettings to use two-column layout (left sidebar + right content) - Add local search provider settings with internal browser window support - Add "Set as Default" button in provider settings page - Show default indicator tag in provider list - Prevent selection of providers without API key configured - Add logos for local search providers (Google, Bing, Baidu) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- src/main/ipc.ts | 4 +- src/main/services/SearchService.ts | 40 ++- src/preload/index.ts | 2 +- .../src/assets/images/search/baidu.svg | 1 + .../src/assets/images/search/bing.svg | 1 + .../src/assets/images/search/google.svg | 1 + src/renderer/src/i18n/locales/en-us.json | 15 ++ src/renderer/src/i18n/locales/zh-cn.json | 15 ++ src/renderer/src/i18n/locales/zh-tw.json | 15 ++ src/renderer/src/i18n/translate/de-de.json | 15 ++ src/renderer/src/i18n/translate/el-gr.json | 15 ++ src/renderer/src/i18n/translate/es-es.json | 15 ++ src/renderer/src/i18n/translate/fr-fr.json | 15 ++ src/renderer/src/i18n/translate/ja-jp.json | 15 ++ src/renderer/src/i18n/translate/pt-pt.json | 21 +- src/renderer/src/i18n/translate/ru-ru.json | 15 ++ .../src/pages/settings/SettingsPage.tsx | 20 +- .../WebSearchSettings/BasicSettings.tsx | 121 +++++++++- .../WebSearchGeneralSettings.tsx | 21 ++ .../WebSearchProviderSetting.tsx | 89 +++++-- .../WebSearchProviderSettings.tsx | 26 ++ .../settings/WebSearchSettings/index.tsx | 227 ++++++++++++++---- 22 files changed, 603 insertions(+), 106 deletions(-) create mode 100644 src/renderer/src/assets/images/search/baidu.svg create mode 100644 src/renderer/src/assets/images/search/bing.svg create mode 100644 src/renderer/src/assets/images/search/google.svg create mode 100644 src/renderer/src/pages/settings/WebSearchSettings/WebSearchGeneralSettings.tsx create mode 100644 src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSettings.tsx diff --git a/src/main/ipc.ts b/src/main/ipc.ts index a4e0fe5c53..08bfbac6f8 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -858,8 +858,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ) // search window - ipcMain.handle(IpcChannel.SearchWindow_Open, async (_, uid: string) => { - await searchService.openSearchWindow(uid) + ipcMain.handle(IpcChannel.SearchWindow_Open, async (_, uid: string, show?: boolean) => { + await searchService.openSearchWindow(uid, show) }) ipcMain.handle(IpcChannel.SearchWindow_Close, async (_, uid: string) => { await searchService.closeSearchWindow(uid) diff --git a/src/main/services/SearchService.ts b/src/main/services/SearchService.ts index 8a4e42099a..6c69f80889 100644 --- a/src/main/services/SearchService.ts +++ b/src/main/services/SearchService.ts @@ -14,38 +14,36 @@ export class SearchService { return SearchService.instance } - constructor() { - // Initialize the service - } - - private async createNewSearchWindow(uid: string): Promise { + private async createNewSearchWindow(uid: string, show: boolean = false): Promise { const newWindow = new BrowserWindow({ - width: 800, - height: 600, - show: false, + width: 1280, + height: 768, + show, webPreferences: { nodeIntegration: true, contextIsolation: false, devTools: is.dev } }) - newWindow.webContents.session.webRequest.onBeforeSendHeaders({ urls: ['*://*/*'] }, (details, callback) => { - const headers = { - ...details.requestHeaders, - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' - } - callback({ requestHeaders: headers }) - }) + this.searchWindows[uid] = newWindow - newWindow.on('closed', () => { - delete this.searchWindows[uid] - }) + newWindow.on('closed', () => delete this.searchWindows[uid]) + + newWindow.webContents.userAgent = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36' + return newWindow } - public async openSearchWindow(uid: string): Promise { - await this.createNewSearchWindow(uid) + public async openSearchWindow(uid: string, show: boolean = false): Promise { + const existingWindow = this.searchWindows[uid] + + if (existingWindow) { + show && existingWindow.show() + return + } + + await this.createNewSearchWindow(uid, show) } public async closeSearchWindow(uid: string): Promise { diff --git a/src/preload/index.ts b/src/preload/index.ts index d393d4a6e2..424253f8e3 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -442,7 +442,7 @@ const api = { ipcRenderer.invoke(IpcChannel.Nutstore_GetDirectoryContents, token, path) }, searchService: { - openSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Open, uid), + openSearchWindow: (uid: string, show?: boolean) => ipcRenderer.invoke(IpcChannel.SearchWindow_Open, uid, show), closeSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Close, uid), openUrlInSearchWindow: (uid: string, url: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_OpenUrl, uid, url) }, diff --git a/src/renderer/src/assets/images/search/baidu.svg b/src/renderer/src/assets/images/search/baidu.svg new file mode 100644 index 0000000000..ead7f89822 --- /dev/null +++ b/src/renderer/src/assets/images/search/baidu.svg @@ -0,0 +1 @@ +Baidu \ No newline at end of file diff --git a/src/renderer/src/assets/images/search/bing.svg b/src/renderer/src/assets/images/search/bing.svg new file mode 100644 index 0000000000..b411a4f068 --- /dev/null +++ b/src/renderer/src/assets/images/search/bing.svg @@ -0,0 +1 @@ +Bing \ No newline at end of file diff --git a/src/renderer/src/assets/images/search/google.svg b/src/renderer/src/assets/images/search/google.svg new file mode 100644 index 0000000000..e8e0f867bd --- /dev/null +++ b/src/renderer/src/assets/images/search/google.svg @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 63d77e03bf..9528b4cd6b 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -4756,6 +4756,12 @@ }, "title": "Other Settings", "websearch": { + "api_key_required": { + "content": "{{provider}} requires an API key to work. Would you like to configure it now?", + "ok": "Configure", + "title": "API Key Required" + }, + "api_providers": "API Providers", "apikey": "API key", "blacklist": "Blacklist", "blacklist_description": "Results from the following websites will not appear in search results", @@ -4797,7 +4803,15 @@ }, "content_limit": "Content length limit", "content_limit_tooltip": "Limit the content length of the search results; content that exceeds the limit will be truncated.", + "default_provider": "Default Provider", "free": "Free", + "is_default": "Default", + "local_provider": { + "hint": "Log in to the website to get better search results and personalize your search settings.", + "open_settings": "Open {{provider}} Settings", + "settings": "Local Search Settings" + }, + "local_providers": "Local Providers", "no_provider_selected": "Please select a search service provider before checking.", "overwrite": "Override search service", "overwrite_tooltip": "Force use search service instead of LLM", @@ -4808,6 +4822,7 @@ "search_provider": "Search service provider", "search_provider_placeholder": "Choose a search service provider.", "search_with_time": "Search with dates included", + "set_as_default": "Set as Default", "subscribe": "Blacklist Subscription", "subscribe_add": "Add Subscription", "subscribe_add_failed": "Failed to add feed source", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index b3dbc9e365..524f32c338 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -4756,6 +4756,12 @@ }, "title": "其他设置", "websearch": { + "api_key_required": { + "content": "{{provider}} 需要 API 密钥才能使用。是否现在去配置?", + "ok": "去配置", + "title": "需要 API 密钥" + }, + "api_providers": "API 服务商", "apikey": "API 密钥", "blacklist": "黑名单", "blacklist_description": "在搜索结果中不会出现以下网站的结果", @@ -4797,7 +4803,15 @@ }, "content_limit": "内容长度限制", "content_limit_tooltip": "限制搜索结果的内容长度, 超过限制的内容将被截断", + "default_provider": "默认搜索引擎", "free": "免费", + "is_default": "默认搜索", + "local_provider": { + "hint": "登录网站可以获得更好的搜索结果,也可以对搜索进行个性化设置。", + "open_settings": "打开 {{provider}} 设置", + "settings": "本地搜索设置" + }, + "local_providers": "本地搜索", "no_provider_selected": "请选择搜索服务商后再检测", "overwrite": "覆盖服务商搜索", "overwrite_tooltip": "强制使用搜索服务商而不是大语言模型进行搜索", @@ -4808,6 +4822,7 @@ "search_provider": "搜索服务商", "search_provider_placeholder": "选择一个搜索服务商", "search_with_time": "搜索包含日期", + "set_as_default": "设为默认", "subscribe": "黑名单订阅", "subscribe_add": "添加订阅", "subscribe_add_failed": "订阅源添加失败", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index a2c26fa399..fe30018ac5 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -4756,6 +4756,12 @@ }, "title": "其他設定", "websearch": { + "api_key_required": { + "content": "{{provider}} 需要 API 金鑰才能運作。您現在要設定嗎?", + "ok": "設定", + "title": "需要 API 金鑰" + }, + "api_providers": "API 服務商", "apikey": "API 金鑰", "blacklist": "黑名單", "blacklist_description": "以下網站不會出現在搜尋結果中", @@ -4797,7 +4803,15 @@ }, "content_limit": "內容長度限制", "content_limit_tooltip": "限制搜尋結果的內容長度;超過限制的內容將被截斷。", + "default_provider": "預設搜尋引擎", "free": "免費", + "is_default": "[to be translated]:Default", + "local_provider": { + "hint": "登入網站以獲得更佳搜尋結果並個人化您的搜尋設定。", + "open_settings": "開啟 {{provider}} 設定", + "settings": "本地搜尋設定" + }, + "local_providers": "本地搜尋", "no_provider_selected": "請選擇搜尋供應商後再檢查", "overwrite": "覆蓋搜尋服務", "overwrite_tooltip": "強制使用搜尋服務而不是 LLM", @@ -4808,6 +4822,7 @@ "search_provider": "搜尋供應商", "search_provider_placeholder": "選擇一個搜尋供應商", "search_with_time": "搜尋包含日期", + "set_as_default": "[to be translated]:Set as Default", "subscribe": "黑名單訂閱", "subscribe_add": "新增訂閱", "subscribe_add_failed": "訂閱來源新增失敗", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index c13e174b06..e77b9dede1 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -4756,6 +4756,12 @@ }, "title": "Weitere Einstellungen", "websearch": { + "api_key_required": { + "content": "{{provider}} erfordert einen API-Schlüssel, um zu funktionieren. Möchten Sie ihn jetzt konfigurieren?", + "ok": "Konfigurieren", + "title": "API-Schlüssel erforderlich" + }, + "api_providers": "API-Anbieter", "apikey": "API-Schlüssel", "blacklist": "Schwarze Liste", "blacklist_description": "Folgende Websites werden nicht in Suchergebnissen angezeigt", @@ -4797,7 +4803,15 @@ }, "content_limit": "Inhaltslängenbegrenzung", "content_limit_tooltip": "Begrenzen Sie die Länge der Suchergebnisse, überschreitende Inhalte werden abgeschnitten", + "default_provider": "Standardanbieter", "free": "Kostenlos", + "is_default": "[to be translated]:Default", + "local_provider": { + "hint": "Melden Sie sich auf der Website an, um bessere Suchergebnisse zu erhalten und Ihre Sucheinstellungen zu personalisieren.", + "open_settings": "{{provider}}-Einstellungen öffnen", + "settings": "Lokale Sucheinstellungen" + }, + "local_providers": "Lokale Anbieter", "no_provider_selected": "Wählen Sie einen Suchanbieter aus, bevor Sie suchen", "overwrite": "Suchanbieter statt LLM für Suche erzwingen", "overwrite_tooltip": "Suchanbieter statt LLM für Suche erzwingen", @@ -4808,6 +4822,7 @@ "search_provider": "Suchanbieter", "search_provider_placeholder": "Einen Suchanbieter auswählen", "search_with_time": "Suche mit Datum", + "set_as_default": "[to be translated]:Set as Default", "subscribe": "Schwarze Liste-Abonnement", "subscribe_add": "Abonnement hinzufügen", "subscribe_add_failed": "Abonnement-Quelle hinzufügen fehlgeschlagen", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 8746eed716..1593099707 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -4756,6 +4756,12 @@ }, "title": "Ρυθμίσεις Εργαλείων", "websearch": { + "api_key_required": { + "content": "Ο {{provider}} απαιτεί κλειδί API για να λειτουργήσει. Θα θέλατε να το διαμορφώσετε τώρα;", + "ok": "Ρυθμίστε", + "title": "Απαιτείται κλειδί API" + }, + "api_providers": "Πάροχοι API", "apikey": "Κλειδί API", "blacklist": "Μαύρη Λίστα", "blacklist_description": "Τα αποτελέσματα από τους παρακάτω ιστότοπους δεν θα εμφανίζονται στα αποτελέσματα αναζήτησης", @@ -4797,7 +4803,15 @@ }, "content_limit": "Όριο μήκους περιεχομένου", "content_limit_tooltip": "Περιορίζει το μήκος του περιεχομένου των αποτελεσμάτων αναζήτησης, το περιεχόμενο πέραν του ορίου θα περικοπεί", + "default_provider": "Προεπιλεγμένος Πάροχος", "free": "Δωρεάν", + "is_default": "[to be translated]:Default", + "local_provider": { + "hint": "Συνδεθείτε στην ιστοσελίδα για να λάβετε καλύτερα αποτελέσματα αναζήτησης και να εξατομικεύσετε τις ρυθμίσεις αναζήτησής σας.", + "open_settings": "Άνοιγμα Ρυθμίσεων {{provider}}", + "settings": "Ρυθμίσεις τοπικής αναζήτησης" + }, + "local_providers": "Τοπικοί Πάροχοι", "no_provider_selected": "Παρακαλώ επιλέξτε πάροχο αναζήτησης πριν τον έλεγχο", "overwrite": "Αντικατάσταση αναζήτησης παρόχου", "overwrite_tooltip": "Εξαναγκάζει τη χρήση του παρόχου αναζήτησης αντί για μοντέλο μεγάλης γλώσσας για αναζήτηση", @@ -4808,6 +4822,7 @@ "search_provider": "Πάροχος αναζήτησης", "search_provider_placeholder": "Επιλέξτε έναν πάροχο αναζήτησης", "search_with_time": "Αναζήτηση με ημερομηνία", + "set_as_default": "[to be translated]:Set as Default", "subscribe": "Εγγραφή σε μαύρη λίστα", "subscribe_add": "Προσθήκη εγγραφής", "subscribe_add_failed": "Η προσθήκη της ροής συνδρομής απέτυχε", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index df7743694e..56f06b1b53 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -4756,6 +4756,12 @@ }, "title": "Configuración de Herramientas", "websearch": { + "api_key_required": { + "content": "{{provider}} requiere una clave de API para funcionar. ¿Te gustaría configurarla ahora?", + "ok": "Configurar", + "title": "Se requiere clave de API" + }, + "api_providers": "Proveedores de API", "apikey": "Clave API", "blacklist": "Lista negra", "blacklist_description": "Los resultados de los siguientes sitios web no aparecerán en los resultados de búsqueda", @@ -4797,7 +4803,15 @@ }, "content_limit": "Límite de longitud del contenido", "content_limit_tooltip": "Limita la longitud del contenido en los resultados de búsqueda; el contenido que exceda el límite será truncado", + "default_provider": "Proveedor Predeterminado", "free": "Gratis", + "is_default": "[to be translated]:Default", + "local_provider": { + "hint": "Inicia sesión en el sitio web para obtener mejores resultados de búsqueda y personalizar tu configuración de búsqueda.", + "open_settings": "Abrir configuración de {{provider}}", + "settings": "Configuración de búsqueda local" + }, + "local_providers": "Proveedores locales", "no_provider_selected": "Seleccione un proveedor de búsqueda antes de comprobar", "overwrite": "Sobrescribir búsqueda del proveedor", "overwrite_tooltip": "Forzar el uso del proveedor de búsqueda en lugar del modelo de lenguaje grande", @@ -4808,6 +4822,7 @@ "search_provider": "Proveedor de búsqueda", "search_provider_placeholder": "Seleccione un proveedor de búsqueda", "search_with_time": "Buscar con fecha", + "set_as_default": "[to be translated]:Set as Default", "subscribe": "Suscripción a lista negra", "subscribe_add": "Añadir suscripción", "subscribe_add_failed": "Error al agregar la fuente de suscripción", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 990c94a3c1..4e8f2ac8e6 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -4756,6 +4756,12 @@ }, "title": "Paramètres des outils", "websearch": { + "api_key_required": { + "content": "{{provider}} nécessite une clé API pour fonctionner. Souhaitez-vous la configurer maintenant ?", + "ok": "Configurer", + "title": "Clé API requise" + }, + "api_providers": "Fournisseurs d'API", "apikey": "Clé API", "blacklist": "Liste noire", "blacklist_description": "Les résultats provenant des sites suivants n'apparaîtront pas dans les résultats de recherche", @@ -4797,7 +4803,15 @@ }, "content_limit": "Limite de longueur du contenu", "content_limit_tooltip": "Limiter la longueur du contenu des résultats de recherche ; le contenu dépassant cette limite sera tronqué", + "default_provider": "Fournisseur par défaut", "free": "Gratuit", + "is_default": "[to be translated]:Default", + "local_provider": { + "hint": "Connectez-vous au site Web pour obtenir de meilleurs résultats de recherche et personnaliser vos paramètres de recherche.", + "open_settings": "Ouvrir les paramètres de {{provider}}", + "settings": "Paramètres de recherche locale" + }, + "local_providers": "Fournisseurs locaux", "no_provider_selected": "Veuillez sélectionner un fournisseur de recherche avant de vérifier", "overwrite": "Remplacer la recherche du fournisseur", "overwrite_tooltip": "Forcer l'utilisation du fournisseur de recherche au lieu du grand modèle linguistique", @@ -4808,6 +4822,7 @@ "search_provider": "Fournisseur de recherche", "search_provider_placeholder": "Sélectionnez un fournisseur de recherche", "search_with_time": "Rechercher avec date", + "set_as_default": "[to be translated]:Set as Default", "subscribe": "Abonnement à la liste noire", "subscribe_add": "Ajouter un abonnement", "subscribe_add_failed": "Échec de l'ajout de la source d'abonnement", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index d36fddc63c..58ee184061 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -4756,6 +4756,12 @@ }, "title": "その他の設定", "websearch": { + "api_key_required": { + "content": "{{provider}}はAPIキーが必要です。今すぐ設定しますか?", + "ok": "設定", + "title": "APIキーが必要" + }, + "api_providers": "APIプロバイダー", "apikey": "APIキー", "blacklist": "ブラックリスト", "blacklist_description": "以下のウェブサイトの結果は検索結果に表示されません", @@ -4797,7 +4803,15 @@ }, "content_limit": "コンテンツ制限", "content_limit_tooltip": "検索結果のコンテンツの長さを制限します。制限を超えるコンテンツは切り捨てられます。", + "default_provider": "デフォルトプロバイダー", "free": "無料", + "is_default": "[to be translated]:Default", + "local_provider": { + "hint": "ウェブサイトにログインして、より良い検索結果を得て、検索設定をパーソナライズしてください。", + "open_settings": "{{provider}}設定を開く", + "settings": "ローカル検索設定" + }, + "local_providers": "地元のプロバイダー", "no_provider_selected": "検索サービスプロバイダーを選択してから再確認してください。", "overwrite": "検索サービスを上書き", "overwrite_tooltip": "LLMの代わりに検索サービスを強制的に使用する", @@ -4808,6 +4822,7 @@ "search_provider": "検索サービスプロバイダー", "search_provider_placeholder": "検索サービスプロバイダーを選択する", "search_with_time": "日付を含む検索", + "set_as_default": "[to be translated]:Set as Default", "subscribe": "ブラックリスト購読", "subscribe_add": "購読を追加", "subscribe_add_failed": "購読ソースの追加に失敗しました", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 65783166cb..553795f6b3 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -40,7 +40,7 @@ "error": { "description": "O Git Bash é necessário para executar agentes no Windows. O agente não pode funcionar sem ele. Por favor, instale o Git para Windows a partir de", "recheck": "Reverificar a Instalação do Git Bash", - "required": "[to be translated]:Git Bash path is required on Windows", + "required": "O caminho do Git Bash é necessário no Windows", "title": "Git Bash Necessário" }, "found": { @@ -53,7 +53,7 @@ "invalidPath": "O arquivo selecionado não é um executável válido do Git Bash (bash.exe).", "title": "Selecionar executável do Git Bash" }, - "placeholder": "[to be translated]:Select bash.exe path", + "placeholder": "Selecione o caminho do bash.exe", "success": "Git Bash detectado com sucesso!", "tooltip": "O Git Bash é necessário para executar agentes no Windows. Instale-o a partir de git-scm.com, caso não esteja disponível." }, @@ -2198,7 +2198,7 @@ "collapse": "[minimizar]", "content_placeholder": "Introduza o conteúdo da nota...", "copyContent": "copiar conteúdo", - "crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}", + "crossPlatformRestoreWarning": "Configuração multiplataforma restaurada, mas o diretório de notas está vazio. Por favor, copie seus arquivos de nota para: {{path}}", "delete": "eliminar", "delete_confirm": "Tem a certeza de que deseja eliminar este {{type}}?", "delete_folder_confirm": "Tem a certeza de que deseja eliminar a pasta \"{{name}}\" e todos os seus conteúdos?", @@ -4756,6 +4756,12 @@ }, "title": "Configurações de Ferramentas", "websearch": { + "api_key_required": { + "content": "{{provider}} requer uma chave de API para funcionar. Você gostaria de configurá-la agora?", + "ok": "Configurar", + "title": "Chave de API Necessária" + }, + "api_providers": "Provedores de API", "apikey": "Chave API", "blacklist": "Lista Negra", "blacklist_description": "Os resultados dos seguintes sites não aparecerão nos resultados de pesquisa", @@ -4797,7 +4803,15 @@ }, "content_limit": "Limite de comprimento do conteúdo", "content_limit_tooltip": "Limita o comprimento do conteúdo dos resultados de pesquisa; o conteúdo excedente será truncado", + "default_provider": "Provedor Padrão", "free": "Grátis", + "is_default": "[to be translated]:Default", + "local_provider": { + "hint": "Faça login no site para obter melhores resultados de pesquisa e personalizar suas configurações de busca.", + "open_settings": "Abrir Configurações do {{provider}}", + "settings": "Configurações de Pesquisa Local" + }, + "local_providers": "Fornecedores Locais", "no_provider_selected": "Por favor, selecione um provedor de pesquisa antes de verificar", "overwrite": "Substituir busca do provedor", "overwrite_tooltip": "Força o uso do provedor de pesquisa em vez do modelo de linguagem grande", @@ -4808,6 +4822,7 @@ "search_provider": "Provedor de pesquisa", "search_provider_placeholder": "Selecione um provedor de pesquisa", "search_with_time": "Pesquisar com data", + "set_as_default": "[to be translated]:Set as Default", "subscribe": "Assinatura de lista negra", "subscribe_add": "Adicionar assinatura", "subscribe_add_failed": "Falha ao adicionar a fonte de subscrição", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 2e245f9ff5..489e8b4695 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -4756,6 +4756,12 @@ }, "title": "Другие настройки", "websearch": { + "api_key_required": { + "content": "{{provider}} требует API-ключ для работы. Хотите настроить его сейчас?", + "ok": "Настроить", + "title": "Требуется ключ API" + }, + "api_providers": "Поставщики API", "apikey": "API ключ", "blacklist": "Черный список", "blacklist_description": "Результаты из следующих веб-сайтов не будут отображаться в результатах поиска", @@ -4797,7 +4803,15 @@ }, "content_limit": "Ограничение длины контента", "content_limit_tooltip": "Ограничить длину контента в результатах поиска; контент, превышающий лимит, будет усечен.", + "default_provider": "Поставщик по умолчанию", "free": "Бесплатно", + "is_default": "[to be translated]:Default", + "local_provider": { + "hint": "Войдите на сайт, чтобы получать более точные результаты поиска и настроить параметры поиска под себя.", + "open_settings": "Открыть настройки {{provider}}", + "settings": "Настройки локального поиска" + }, + "local_providers": "Местные поставщики", "no_provider_selected": "Пожалуйста, выберите поставщика поисковых услуг, затем проверьте.", "overwrite": "Переопределить поисковый сервис", "overwrite_tooltip": "Принудительно использовать поисковый сервис вместо LLM", @@ -4808,6 +4822,7 @@ "search_provider": "поиск сервисного провайдера", "search_provider_placeholder": "Выберите поставщика поисковых услуг", "search_with_time": "Поиск, содержащий дату", + "set_as_default": "[to be translated]:Set as Default", "subscribe": "Подписка на черный список", "subscribe_add": "Добавить подписку", "subscribe_add_failed": "Не удалось добавить источник подписки", diff --git a/src/renderer/src/pages/settings/SettingsPage.tsx b/src/renderer/src/pages/settings/SettingsPage.tsx index cb5d8df32a..0f7659ddac 100644 --- a/src/renderer/src/pages/settings/SettingsPage.tsx +++ b/src/renderer/src/pages/settings/SettingsPage.tsx @@ -1,4 +1,3 @@ -import { GlobalOutlined } from '@ant-design/icons' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { McpLogo } from '@renderer/components/Icons' import Scrollbar from '@renderer/components/Scrollbar' @@ -15,6 +14,7 @@ import { NotebookPen, Package, PictureInPicture2, + Search, Server, Settings2, TextCursorInput, @@ -88,19 +88,13 @@ const SettingsPage: FC = () => { - + {t('settings.mcp.title')} - - - - {t('notes.settings.title')} - - - + {t('settings.tool.websearch.title')} @@ -122,6 +116,12 @@ const SettingsPage: FC = () => { {t('settings.tool.preprocess.title')} + + + + {t('notes.settings.title')} + + @@ -159,7 +159,7 @@ const SettingsPage: FC = () => { } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx index 95c0749126..e4db2caf22 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx @@ -1,22 +1,138 @@ +import BaiduLogo from '@renderer/assets/images/search/baidu.svg' +import BingLogo from '@renderer/assets/images/search/bing.svg' +import BochaLogo from '@renderer/assets/images/search/bocha.webp' +import ExaLogo from '@renderer/assets/images/search/exa.png' +import GoogleLogo from '@renderer/assets/images/search/google.svg' +import SearxngLogo from '@renderer/assets/images/search/searxng.svg' +import TavilyLogo from '@renderer/assets/images/search/tavily.png' +import ZhipuLogo from '@renderer/assets/images/search/zhipu.png' +import Selector from '@renderer/components/Selector' import { useTheme } from '@renderer/context/ThemeProvider' -import { useWebSearchSettings } from '@renderer/hooks/useWebSearchProviders' +import { + useDefaultWebSearchProvider, + useWebSearchProviders, + useWebSearchSettings +} from '@renderer/hooks/useWebSearchProviders' import { useAppDispatch } from '@renderer/store' import { setMaxResult, setSearchWithTime } from '@renderer/store/websearch' +import type { WebSearchProvider, WebSearchProviderId } from '@renderer/types' +import { hasObjectKey } from '@renderer/utils' import { Slider, Switch, Tooltip } from 'antd' -import { t } from 'i18next' import { Info } from 'lucide-react' import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router' import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' +// Provider logos map +const getProviderLogo = (providerId: WebSearchProviderId): string | undefined => { + switch (providerId) { + case 'zhipu': + return ZhipuLogo + case 'tavily': + return TavilyLogo + case 'searxng': + return SearxngLogo + case 'exa': + case 'exa-mcp': + return ExaLogo + case 'bocha': + return BochaLogo + case 'local-google': + return GoogleLogo + case 'local-bing': + return BingLogo + case 'local-baidu': + return BaiduLogo + default: + return undefined + } +} + const BasicSettings: FC = () => { const { theme } = useTheme() + const { t } = useTranslation() + const { providers } = useWebSearchProviders() + const { provider: defaultProvider, setDefaultProvider } = useDefaultWebSearchProvider() const { searchWithTime, maxResults, compressionConfig } = useWebSearchSettings() + const navigate = useNavigate() const dispatch = useAppDispatch() + const updateSelectedWebSearchProvider = (providerId: string) => { + const provider = providers.find((p) => p.id === providerId) + if (provider) { + // Check if provider needs API key but doesn't have one + const needsApiKey = hasObjectKey(provider, 'apiKey') + const hasApiKey = provider.apiKey && provider.apiKey.trim() !== '' + + if (needsApiKey && !hasApiKey) { + // Don't allow selection, show modal to configure + window.modal.confirm({ + title: t('settings.tool.websearch.api_key_required.title'), + content: t('settings.tool.websearch.api_key_required.content', { provider: provider.name }), + okText: t('settings.tool.websearch.api_key_required.ok'), + cancelText: t('common.cancel'), + centered: true, + onOk: () => { + navigate(`/settings/websearch/provider/${provider.id}`) + } + }) + return + } + + setDefaultProvider(provider as WebSearchProvider) + } + } + + // Sort providers: API providers first, then local providers + const sortedProviders = [...providers].sort((a, b) => { + const aIsLocal = a.id.startsWith('local') + const bIsLocal = b.id.startsWith('local') + if (aIsLocal && !bIsLocal) return 1 + if (!aIsLocal && bIsLocal) return -1 + return 0 + }) + + const renderProviderLabel = (provider: WebSearchProvider) => { + const logo = getProviderLogo(provider.id) + const needsApiKey = hasObjectKey(provider, 'apiKey') + + return ( +
+ {logo ? ( + {provider.name} + ) : ( +
+ )} + + {provider.name} + {needsApiKey && ` (${t('settings.tool.websearch.apikey')})`} + +
+ ) + } + return ( <> + + {t('settings.tool.websearch.search_provider')} + + + {t('settings.tool.websearch.default_provider')} + updateSelectedWebSearchProvider(value)} + placeholder={t('settings.tool.websearch.search_provider_placeholder')} + options={sortedProviders.map((p) => ({ + value: p.id, + label: renderProviderLabel(p) + }))} + /> + + {t('settings.general.title')} @@ -48,4 +164,5 @@ const BasicSettings: FC = () => { ) } + export default BasicSettings diff --git a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchGeneralSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchGeneralSettings.tsx new file mode 100644 index 0000000000..0af3fb4332 --- /dev/null +++ b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchGeneralSettings.tsx @@ -0,0 +1,21 @@ +import { useTheme } from '@renderer/context/ThemeProvider' +import type { FC } from 'react' + +import { SettingContainer } from '..' +import BasicSettings from './BasicSettings' +import BlacklistSettings from './BlacklistSettings' +import CompressionSettings from './CompressionSettings' + +const WebSearchGeneralSettings: FC = () => { + const { theme } = useTheme() + + return ( + + + + + + ) +} + +export default WebSearchGeneralSettings diff --git a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx index a92b8646c1..823f6fac81 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx @@ -1,7 +1,10 @@ import { CheckOutlined, ExportOutlined, LoadingOutlined } from '@ant-design/icons' import { loggerService } from '@logger' +import BaiduLogo from '@renderer/assets/images/search/baidu.svg' +import BingLogo from '@renderer/assets/images/search/bing.svg' import BochaLogo from '@renderer/assets/images/search/bocha.webp' import ExaLogo from '@renderer/assets/images/search/exa.png' +import GoogleLogo from '@renderer/assets/images/search/google.svg' import SearxngLogo from '@renderer/assets/images/search/searxng.svg' import TavilyLogo from '@renderer/assets/images/search/tavily.png' import ZhipuLogo from '@renderer/assets/images/search/zhipu.png' @@ -9,7 +12,7 @@ import { HStack } from '@renderer/components/Layout' import ApiKeyListPopup from '@renderer/components/Popups/ApiKeyListPopup/popup' import { WEB_SEARCH_PROVIDER_CONFIG } from '@renderer/config/webSearchProviders' import { useTimer } from '@renderer/hooks/useTimer' -import { useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders' +import { useDefaultWebSearchProvider, useWebSearchProvider } from '@renderer/hooks/useWebSearchProviders' import WebSearchService from '@renderer/services/WebSearchService' import type { WebSearchProviderId } from '@renderer/types' import { formatApiKeys, hasObjectKey } from '@renderer/utils' @@ -30,6 +33,7 @@ interface Props { const WebSearchProviderSetting: FC = ({ providerId }) => { const { provider, updateProvider } = useWebSearchProvider(providerId) + const { provider: defaultProvider, setDefaultProvider } = useDefaultWebSearchProvider() const { t } = useTranslation() const [apiKey, setApiKey] = useState(provider.apiKey || '') const [apiHost, setApiHost] = useState(provider.apiHost || '') @@ -149,26 +153,79 @@ const WebSearchProviderSetting: FC = ({ providerId }) => { return ExaLogo case 'bocha': return BochaLogo + case 'local-google': + return GoogleLogo + case 'local-bing': + return BingLogo + case 'local-baidu': + return BaiduLogo default: return undefined } } + const isLocalProvider = provider.id.startsWith('local') + + const openLocalProviderSettings = async () => { + if (officialWebsite) { + await window.api.searchService.openSearchWindow(provider.id, true) + await window.api.searchService.openUrlInSearchWindow(provider.id, officialWebsite) + } + } + + const providerLogo = getWebSearchProviderLogo(provider.id) + + // Check if this provider is already the default + const isDefault = defaultProvider?.id === provider.id + + // Check if provider needs API key but doesn't have one configured + const needsApiKey = hasObjectKey(provider, 'apiKey') + const hasApiKey = provider.apiKey && provider.apiKey.trim() !== '' + const canSetAsDefault = !isDefault && (!needsApiKey || hasApiKey) + + const handleSetAsDefault = () => { + if (canSetAsDefault) { + setDefaultProvider(provider) + } + } + return ( <> - - - {provider.name} - {officialWebsite && webSearchProviderConfig?.websites && ( - - - - )} + + + {providerLogo ? ( + {provider.name} + ) : ( +
+ )} + {provider.name} + {officialWebsite && webSearchProviderConfig?.websites && ( + + + + )} + + - {hasObjectKey(provider, 'apiKey') && ( + {isLocalProvider && ( + <> + + {t('settings.tool.websearch.local_provider.settings')} + + + + {t('settings.tool.websearch.local_provider.hint')} + + + )} + {!isLocalProvider && hasObjectKey(provider, 'apiKey') && ( <> = ({ providerId }) => { )} - {hasObjectKey(provider, 'apiHost') && ( + {!isLocalProvider && hasObjectKey(provider, 'apiHost') && ( <> {t('settings.provider.api_host')} @@ -234,10 +291,11 @@ const WebSearchProviderSetting: FC = ({ providerId }) => { )} - {hasObjectKey(provider, 'basicAuthUsername') && ( + {!isLocalProvider && hasObjectKey(provider, 'basicAuthUsername') && ( <> - + {t('settings.provider.basic_auth.label')} @@ -291,10 +349,5 @@ const ProviderName = styled.span` font-size: 14px; font-weight: 500; ` -const ProviderLogo = styled.img` - width: 20px; - height: 20px; - object-fit: contain; -` export default WebSearchProviderSetting diff --git a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSettings.tsx new file mode 100644 index 0000000000..884c43e6b4 --- /dev/null +++ b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSettings.tsx @@ -0,0 +1,26 @@ +import { useTheme } from '@renderer/context/ThemeProvider' +import type { WebSearchProviderId } from '@renderer/types' +import type { FC } from 'react' +import { useParams } from 'react-router' + +import { SettingContainer, SettingGroup } from '..' +import WebSearchProviderSetting from './WebSearchProviderSetting' + +const WebSearchProviderSettings: FC = () => { + const { providerId } = useParams<{ providerId: string }>() + const { theme } = useTheme() + + if (!providerId) { + return null + } + + return ( + + + + + + ) +} + +export default WebSearchProviderSettings diff --git a/src/renderer/src/pages/settings/WebSearchSettings/index.tsx b/src/renderer/src/pages/settings/WebSearchSettings/index.tsx index 7867cb57e0..a21de63764 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/index.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/index.tsx @@ -1,66 +1,195 @@ -import Selector from '@renderer/components/Selector' -import { useTheme } from '@renderer/context/ThemeProvider' +import BaiduLogo from '@renderer/assets/images/search/baidu.svg' +import BingLogo from '@renderer/assets/images/search/bing.svg' +import BochaLogo from '@renderer/assets/images/search/bocha.webp' +import ExaLogo from '@renderer/assets/images/search/exa.png' +import GoogleLogo from '@renderer/assets/images/search/google.svg' +import SearxngLogo from '@renderer/assets/images/search/searxng.svg' +import TavilyLogo from '@renderer/assets/images/search/tavily.png' +import ZhipuLogo from '@renderer/assets/images/search/zhipu.png' +import DividerWithText from '@renderer/components/DividerWithText' +import ListItem from '@renderer/components/ListItem' +import Scrollbar from '@renderer/components/Scrollbar' import { useDefaultWebSearchProvider, useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders' -import type { WebSearchProvider } from '@renderer/types' +import type { WebSearchProviderId } from '@renderer/types' import { hasObjectKey } from '@renderer/utils' +import { Flex, Tag } from 'antd' +import { Search } from 'lucide-react' import type { FC } from 'react' -import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router' +import styled from 'styled-components' -import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' -import BasicSettings from './BasicSettings' -import BlacklistSettings from './BlacklistSettings' -import CompressionSettings from './CompressionSettings' -import WebSearchProviderSetting from './WebSearchProviderSetting' +import WebSearchGeneralSettings from './WebSearchGeneralSettings' +import WebSearchProviderSettings from './WebSearchProviderSettings' const WebSearchSettings: FC = () => { - const { providers } = useWebSearchProviders() - const { provider: defaultProvider, setDefaultProvider } = useDefaultWebSearchProvider() const { t } = useTranslation() - const [selectedProvider, setSelectedProvider] = useState(defaultProvider) - const { theme: themeMode } = useTheme() + const { providers } = useWebSearchProviders() + const { provider: defaultProvider } = useDefaultWebSearchProvider() + const navigate = useNavigate() + const location = useLocation() - const isLocalProvider = selectedProvider?.id.startsWith('local') + // Get the currently active view + const getActiveView = () => { + const path = location.pathname - function updateSelectedWebSearchProvider(providerId: string) { - const provider = providers.find((p) => p.id === providerId) - if (!provider) { - return + if (path === '/settings/websearch/general' || path === '/settings/websearch') { + return 'general' + } + + // Check if it's a provider page + for (const provider of providers) { + if (path === `/settings/websearch/provider/${provider.id}`) { + return provider.id + } + } + + return 'general' + } + + const activeView = getActiveView() + + // Filter providers that have API settings (apiKey or apiHost) + const apiProviders = providers.filter((p) => hasObjectKey(p, 'apiKey') || hasObjectKey(p, 'apiHost')) + const localProviders = providers.filter((p) => p.id.startsWith('local')) + + // Provider logos map + const getProviderLogo = (providerId: WebSearchProviderId): string | undefined => { + switch (providerId) { + case 'zhipu': + return ZhipuLogo + case 'tavily': + return TavilyLogo + case 'searxng': + return SearxngLogo + case 'exa': + case 'exa-mcp': + return ExaLogo + case 'bocha': + return BochaLogo + case 'local-google': + return GoogleLogo + case 'local-bing': + return BingLogo + case 'local-baidu': + return BaiduLogo + default: + return undefined } - setSelectedProvider(provider) - setDefaultProvider(provider) } return ( - - - {t('settings.tool.websearch.title')} - - - {t('settings.tool.websearch.search_provider')} -
- updateSelectedWebSearchProvider(value)} - placeholder={t('settings.tool.websearch.search_provider_placeholder')} - options={providers.map((p) => ({ - value: p.id, - label: `${p.name} (${hasObjectKey(p, 'apiKey') ? t('settings.tool.websearch.apikey') : t('settings.tool.websearch.free')})` - }))} - /> -
-
-
- {!isLocalProvider && ( - - {selectedProvider && } - - )} - - - -
+ + + + navigate('/settings/websearch/general')} + icon={} + titleStyle={{ fontWeight: 500 }} + /> + + {apiProviders.map((provider) => { + const logo = getProviderLogo(provider.id) + const isDefault = defaultProvider?.id === provider.id + return ( + navigate(`/settings/websearch/provider/${provider.id}`)} + icon={ + logo ? ( + {provider.name} + ) : ( +
+ ) + } + titleStyle={{ fontWeight: 500 }} + rightContent={ + isDefault ? ( + + {t('common.default')} + + ) : undefined + } + /> + ) + })} + {localProviders.length > 0 && ( + <> + + {localProviders.map((provider) => { + const logo = getProviderLogo(provider.id) + const isDefault = defaultProvider?.id === provider.id + return ( + navigate(`/settings/websearch/provider/${provider.id}`)} + icon={ + logo ? ( + {provider.name} + ) : ( +
+ ) + } + titleStyle={{ fontWeight: 500 }} + rightContent={ + isDefault ? ( + + {t('common.default')} + + ) : undefined + } + /> + ) + })} + + )} + + + + } /> + } /> + } /> + + + + ) } + +const Container = styled(Flex)` + flex: 1; +` + +const MainContainer = styled.div` + display: flex; + flex: 1; + flex-direction: row; + width: 100%; + height: calc(100vh - var(--navbar-height) - 6px); + overflow: hidden; +` + +const MenuList = styled(Scrollbar)` + display: flex; + flex-direction: column; + gap: 5px; + width: var(--settings-width); + padding: 12px; + padding-bottom: 48px; + border-right: 0.5px solid var(--color-border); + height: calc(100vh - var(--navbar-height)); +` + +const RightContainer = styled.div` + flex: 1; + position: relative; + display: flex; +` + export default WebSearchSettings From 265934be5a0e1a6a7f43a96b858c5224996f1905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Tue, 23 Dec 2025 14:57:03 +0800 Subject: [PATCH 06/24] refactor(notes): move notes settings to popup in NotesPage (#12075) * refactor(notes): move notes settings to popup in NotesPage - Move NotesSettings.tsx from settings directory to notes directory - Add "More Settings" menu item to notes dropdown menu - Show settings in GeneralPopup when clicking "More Settings" - Remove notes settings entry from SettingsPage sidebar and routes * fix(notes): adjust margin in NotesSidebar component for improved layout - Update margin-bottom from 20px to 12px in the NotesSidebar component to enhance visual spacing. * refactor(notes): simplify styles object in HeaderNavbar component - Consolidate styles object for body padding in HeaderNavbar to improve readability and maintainability. --------- Co-authored-by: Claude --- src/renderer/src/pages/notes/HeaderNavbar.tsx | 16 +++++++++++++++- src/renderer/src/pages/notes/MenuConfig.tsx | 14 +++++++++++++- .../{settings => notes}/NotesSettings.tsx | 19 +++++++++---------- src/renderer/src/pages/notes/NotesSidebar.tsx | 2 +- .../src/pages/settings/SettingsPage.tsx | 3 --- 5 files changed, 38 insertions(+), 16 deletions(-) rename src/renderer/src/pages/{settings => notes}/NotesSettings.tsx (98%) diff --git a/src/renderer/src/pages/notes/HeaderNavbar.tsx b/src/renderer/src/pages/notes/HeaderNavbar.tsx index a2d6e66039..92c66ba98d 100644 --- a/src/renderer/src/pages/notes/HeaderNavbar.tsx +++ b/src/renderer/src/pages/notes/HeaderNavbar.tsx @@ -1,6 +1,7 @@ import { loggerService } from '@logger' import { NavbarCenter, NavbarHeader, NavbarRight } from '@renderer/components/app/Navbar' import { HStack } from '@renderer/components/Layout' +import GeneralPopup from '@renderer/components/Popups/GeneralPopup' import { useActiveNode } from '@renderer/hooks/useNotesQuery' import { useNotesSettings } from '@renderer/hooks/useNotesSettings' import { useShowWorkspace } from '@renderer/hooks/useShowWorkspace' @@ -12,6 +13,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import styled from 'styled-components' import { menuItems } from './MenuConfig' +import NotesSettings from './NotesSettings' const logger = loggerService.withContext('HeaderNavbar') @@ -51,6 +53,16 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpand } }, [getCurrentNoteContent]) + const handleShowSettings = useCallback(() => { + GeneralPopup.show({ + title: t('notes.settings.title'), + content: , + footer: null, + width: 600, + styles: { body: { padding: 0 } } + }) + }, []) + const handleBreadcrumbClick = useCallback( (item: { treePath: string; isFolder: boolean }) => { if (item.isFolder && onExpandPath) { @@ -130,6 +142,8 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpand onClick: () => { if (item.copyAction) { handleCopyContent() + } else if (item.showSettingsPopup) { + handleShowSettings() } else if (item.action) { item.action(settings, updateSettings) } @@ -308,7 +322,7 @@ export const StarButton = styled.div` transition: all 0.2s ease-in-out; cursor: pointer; svg { - color: inherit; + color: var(--color-icon); } &:hover { diff --git a/src/renderer/src/pages/notes/MenuConfig.tsx b/src/renderer/src/pages/notes/MenuConfig.tsx index 0f8f2b0128..c157daa417 100644 --- a/src/renderer/src/pages/notes/MenuConfig.tsx +++ b/src/renderer/src/pages/notes/MenuConfig.tsx @@ -1,5 +1,5 @@ import type { NotesSettings } from '@renderer/store/note' -import { Copy, MonitorSpeaker, Type } from 'lucide-react' +import { Copy, MonitorSpeaker, Settings, Type } from 'lucide-react' import type { ReactNode } from 'react' export interface MenuItem { @@ -12,6 +12,7 @@ export interface MenuItem { isActive?: (settings: NotesSettings) => boolean component?: (settings: NotesSettings, updateSettings: (newSettings: Partial) => void) => ReactNode copyAction?: boolean + showSettingsPopup?: boolean } export const menuItems: MenuItem[] = [ @@ -86,5 +87,16 @@ export const menuItems: MenuItem[] = [ isActive: (settings) => settings.fontSize === 20 } ] + }, + { + key: 'divider-settings', + type: 'divider', + labelKey: '' + }, + { + key: 'more-settings', + labelKey: 'settings.moresetting.label', + icon: Settings, + showSettingsPopup: true } ] diff --git a/src/renderer/src/pages/settings/NotesSettings.tsx b/src/renderer/src/pages/notes/NotesSettings.tsx similarity index 98% rename from src/renderer/src/pages/settings/NotesSettings.tsx rename to src/renderer/src/pages/notes/NotesSettings.tsx index be36c0fe6e..114fd844ba 100644 --- a/src/renderer/src/pages/settings/NotesSettings.tsx +++ b/src/renderer/src/pages/notes/NotesSettings.tsx @@ -2,14 +2,6 @@ import { loggerService } from '@logger' import Selector from '@renderer/components/Selector' import { useTheme } from '@renderer/context/ThemeProvider' import { useNotesSettings } from '@renderer/hooks/useNotesSettings' -import type { EditorView } from '@renderer/types' -import { Button, Input, message, Slider, Switch } from 'antd' -import { FolderOpen } from 'lucide-react' -import type { FC } from 'react' -import { useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' - import { SettingContainer, SettingDivider, @@ -18,7 +10,14 @@ import { SettingRow, SettingRowTitle, SettingTitle -} from '.' +} from '@renderer/pages/settings' +import type { EditorView } from '@renderer/types' +import { Button, Input, message, Slider, Switch } from 'antd' +import { FolderOpen } from 'lucide-react' +import type { FC } from 'react' +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' const logger = loggerService.withContext('NotesSettings') @@ -92,7 +91,7 @@ const NotesSettings: FC = () => { const isPathChanged = tempPath !== notesPath return ( - + {t('notes.settings.data.title')} diff --git a/src/renderer/src/pages/notes/NotesSidebar.tsx b/src/renderer/src/pages/notes/NotesSidebar.tsx index 6ed144dd7e..097ffd0f46 100644 --- a/src/renderer/src/pages/notes/NotesSidebar.tsx +++ b/src/renderer/src/pages/notes/NotesSidebar.tsx @@ -412,7 +412,7 @@ const NotesSidebar: FC = ({ {!isShowStarred && !isShowSearch && ( -
+
{ } /> } /> } /> - } /> } /> From e093a18debcb4f124f01a1b7f4253eb21e190baf Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Tue, 23 Dec 2025 15:01:58 +0800 Subject: [PATCH 07/24] refactor(settings): update MCP logo opacity and remove unused notes settings - Adjust MCP logo opacity in MCPSettings and McpTool components for improved visual consistency. - Remove notes settings entry from SettingsPage to streamline the settings interface. --- src/renderer/src/components/Tab/TabContainer.tsx | 3 --- src/renderer/src/pages/settings/MCPSettings/McpTool.tsx | 2 +- src/renderer/src/pages/settings/MCPSettings/index.tsx | 2 +- src/renderer/src/pages/settings/SettingsPage.tsx | 6 ------ 4 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/renderer/src/components/Tab/TabContainer.tsx b/src/renderer/src/components/Tab/TabContainer.tsx index 88091c6831..5ac1353be8 100644 --- a/src/renderer/src/components/Tab/TabContainer.tsx +++ b/src/renderer/src/components/Tab/TabContainer.tsx @@ -39,7 +39,6 @@ import { useTranslation } from 'react-i18next' import { useLocation, useNavigate } from 'react-router-dom' import styled from 'styled-components' -import { McpLogo } from '../Icons' import MinAppIcon from '../Icons/MinAppIcon' import MinAppTabsPool from '../MinApp/MinAppTabsPool' import WindowControls from '../WindowControls' @@ -99,8 +98,6 @@ const getTabIcon = ( return case 'knowledge': return - case 'mcp': - return case 'files': return case 'settings': diff --git a/src/renderer/src/pages/settings/MCPSettings/McpTool.tsx b/src/renderer/src/pages/settings/MCPSettings/McpTool.tsx index e4f01b7475..8aa4faf6d8 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpTool.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpTool.tsx @@ -137,7 +137,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M { title: ( - + {t('settings.mcp.tools.enable')} ), diff --git a/src/renderer/src/pages/settings/MCPSettings/index.tsx b/src/renderer/src/pages/settings/MCPSettings/index.tsx index 4b958dde1c..5c11b7b261 100644 --- a/src/renderer/src/pages/settings/MCPSettings/index.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/index.tsx @@ -86,7 +86,7 @@ const MCPSettings: FC = () => { title={t('settings.mcp.servers', 'MCP Servers')} active={activeView === 'servers'} onClick={() => navigate('/settings/mcp/servers')} - icon={} + icon={} titleStyle={{ fontWeight: 500 }} /> diff --git a/src/renderer/src/pages/settings/SettingsPage.tsx b/src/renderer/src/pages/settings/SettingsPage.tsx index e4aba4b21f..f1b1186c43 100644 --- a/src/renderer/src/pages/settings/SettingsPage.tsx +++ b/src/renderer/src/pages/settings/SettingsPage.tsx @@ -114,12 +114,6 @@ const SettingsPage: FC = () => { {t('settings.tool.preprocess.title')} - - - - {t('notes.settings.title')} - - From 09e58d37560a6ffd61c819dacd6ae7ac2429a00f Mon Sep 17 00:00:00 2001 From: SuYao Date: Tue, 23 Dec 2025 20:08:53 +0800 Subject: [PATCH 08/24] fix: interleaved thinking support (#12084) * fix: update @ai-sdk/openai-compatible to version 1.0.28 and adjust related patches * fix: add sendReasoning option to OpenAICompatibleProviderOptions and update message conversion logic * fix: add interval thinking model support and related tests * fix: add sendReasoning option to OpenAICompatibleProviderOptions and update related logic * fix: remove MiniMax reasoning model support and update interval thinking model regex * chore: add comment * fix: rename interval thinking model references to interleaved thinking model --- ...nai-compatible-npm-1.0.27-06f74278cf.patch | 140 --------- ...nai-compatible-npm-1.0.28-5705188855.patch | 266 ++++++++++++++++++ package.json | 4 +- packages/ai-sdk-provider/package.json | 2 +- packages/aiCore/package.json | 2 +- src/renderer/src/aiCore/utils/options.ts | 11 +- .../config/models/__tests__/reasoning.test.ts | 103 +++++++ src/renderer/src/config/models/reasoning.ts | 17 ++ yarn.lock | 28 +- 9 files changed, 409 insertions(+), 164 deletions(-) delete mode 100644 .yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch create mode 100644 .yarn/patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch diff --git a/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch b/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch deleted file mode 100644 index 2a13c33a78..0000000000 --- a/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch +++ /dev/null @@ -1,140 +0,0 @@ -diff --git a/dist/index.js b/dist/index.js -index 73045a7d38faafdc7f7d2cd79d7ff0e2b031056b..8d948c9ac4ea4b474db9ef3c5491961e7fcf9a07 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -421,6 +421,17 @@ var OpenAICompatibleChatLanguageModel = class { - text: reasoning - }); - } -+ if (choice.message.images) { -+ for (const image of choice.message.images) { -+ const match1 = image.image_url.url.match(/^data:([^;]+)/) -+ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/); -+ content.push({ -+ type: 'file', -+ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg', -+ data: match2 ? match2[1] : image.image_url.url, -+ }); -+ } -+ } - if (choice.message.tool_calls != null) { - for (const toolCall of choice.message.tool_calls) { - content.push({ -@@ -598,6 +609,17 @@ var OpenAICompatibleChatLanguageModel = class { - delta: delta.content - }); - } -+ if (delta.images) { -+ for (const image of delta.images) { -+ const match1 = image.image_url.url.match(/^data:([^;]+)/) -+ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/); -+ controller.enqueue({ -+ type: 'file', -+ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg', -+ data: match2 ? match2[1] : image.image_url.url, -+ }); -+ } -+ } - if (delta.tool_calls != null) { - for (const toolCallDelta of delta.tool_calls) { - const index = toolCallDelta.index; -@@ -765,6 +787,14 @@ var OpenAICompatibleChatResponseSchema = import_v43.z.object({ - arguments: import_v43.z.string() - }) - }) -+ ).nullish(), -+ images: import_v43.z.array( -+ import_v43.z.object({ -+ type: import_v43.z.literal('image_url'), -+ image_url: import_v43.z.object({ -+ url: import_v43.z.string(), -+ }) -+ }) - ).nullish() - }), - finish_reason: import_v43.z.string().nullish() -@@ -795,6 +825,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => import_v43.z.union( - arguments: import_v43.z.string().nullish() - }) - }) -+ ).nullish(), -+ images: import_v43.z.array( -+ import_v43.z.object({ -+ type: import_v43.z.literal('image_url'), -+ image_url: import_v43.z.object({ -+ url: import_v43.z.string(), -+ }) -+ }) - ).nullish() - }).nullish(), - finish_reason: import_v43.z.string().nullish() -diff --git a/dist/index.mjs b/dist/index.mjs -index 1c2b9560bbfbfe10cb01af080aeeed4ff59db29c..2c8ddc4fc9bfc5e7e06cfca105d197a08864c427 100644 ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -405,6 +405,17 @@ var OpenAICompatibleChatLanguageModel = class { - text: reasoning - }); - } -+ if (choice.message.images) { -+ for (const image of choice.message.images) { -+ const match1 = image.image_url.url.match(/^data:([^;]+)/) -+ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/); -+ content.push({ -+ type: 'file', -+ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg', -+ data: match2 ? match2[1] : image.image_url.url, -+ }); -+ } -+ } - if (choice.message.tool_calls != null) { - for (const toolCall of choice.message.tool_calls) { - content.push({ -@@ -582,6 +593,17 @@ var OpenAICompatibleChatLanguageModel = class { - delta: delta.content - }); - } -+ if (delta.images) { -+ for (const image of delta.images) { -+ const match1 = image.image_url.url.match(/^data:([^;]+)/) -+ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/); -+ controller.enqueue({ -+ type: 'file', -+ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg', -+ data: match2 ? match2[1] : image.image_url.url, -+ }); -+ } -+ } - if (delta.tool_calls != null) { - for (const toolCallDelta of delta.tool_calls) { - const index = toolCallDelta.index; -@@ -749,6 +771,14 @@ var OpenAICompatibleChatResponseSchema = z3.object({ - arguments: z3.string() - }) - }) -+ ).nullish(), -+ images: z3.array( -+ z3.object({ -+ type: z3.literal('image_url'), -+ image_url: z3.object({ -+ url: z3.string(), -+ }) -+ }) - ).nullish() - }), - finish_reason: z3.string().nullish() -@@ -779,6 +809,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => z3.union([ - arguments: z3.string().nullish() - }) - }) -+ ).nullish(), -+ images: z3.array( -+ z3.object({ -+ type: z3.literal('image_url'), -+ image_url: z3.object({ -+ url: z3.string(), -+ }) -+ }) - ).nullish() - }).nullish(), - finish_reason: z3.string().nullish() diff --git a/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch b/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch new file mode 100644 index 0000000000..c17729ef93 --- /dev/null +++ b/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch @@ -0,0 +1,266 @@ +diff --git a/dist/index.d.ts b/dist/index.d.ts +index 48e2f6263c6ee4c75d7e5c28733e64f6ebe92200..00d0729c4a3cbf9a48e8e1e962c7e2b256b75eba 100644 +--- a/dist/index.d.ts ++++ b/dist/index.d.ts +@@ -7,6 +7,7 @@ declare const openaiCompatibleProviderOptions: z.ZodObject<{ + user: z.ZodOptional; + reasoningEffort: z.ZodOptional; + textVerbosity: z.ZodOptional; ++ sendReasoning: z.ZodOptional; + }, z.core.$strip>; + type OpenAICompatibleProviderOptions = z.infer; + +diff --git a/dist/index.js b/dist/index.js +index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e5bfe0f9a 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -41,7 +41,7 @@ function getOpenAIMetadata(message) { + var _a, _b; + return (_b = (_a = message == null ? void 0 : message.providerOptions) == null ? void 0 : _a.openaiCompatible) != null ? _b : {}; + } +-function convertToOpenAICompatibleChatMessages(prompt) { ++function convertToOpenAICompatibleChatMessages({prompt, options}) { + const messages = []; + for (const { role, content, ...message } of prompt) { + const metadata = getOpenAIMetadata({ ...message }); +@@ -91,6 +91,7 @@ function convertToOpenAICompatibleChatMessages(prompt) { + } + case "assistant": { + let text = ""; ++ let reasoning_text = ""; + const toolCalls = []; + for (const part of content) { + const partMetadata = getOpenAIMetadata(part); +@@ -99,6 +100,12 @@ function convertToOpenAICompatibleChatMessages(prompt) { + text += part.text; + break; + } ++ case "reasoning": { ++ if (options.sendReasoning) { ++ reasoning_text += part.text; ++ } ++ break; ++ } + case "tool-call": { + toolCalls.push({ + id: part.toolCallId, +@@ -116,6 +123,7 @@ function convertToOpenAICompatibleChatMessages(prompt) { + messages.push({ + role: "assistant", + content: text, ++ reasoning_content: reasoning_text ?? undefined, + tool_calls: toolCalls.length > 0 ? toolCalls : void 0, + ...metadata + }); +@@ -200,7 +208,8 @@ var openaiCompatibleProviderOptions = import_v4.z.object({ + /** + * Controls the verbosity of the generated text. Defaults to `medium`. + */ +- textVerbosity: import_v4.z.string().optional() ++ textVerbosity: import_v4.z.string().optional(), ++ sendReasoning: import_v4.z.boolean().optional() + }); + + // src/openai-compatible-error.ts +@@ -378,7 +387,7 @@ var OpenAICompatibleChatLanguageModel = class { + reasoning_effort: compatibleOptions.reasoningEffort, + verbosity: compatibleOptions.textVerbosity, + // messages: +- messages: convertToOpenAICompatibleChatMessages(prompt), ++ messages: convertToOpenAICompatibleChatMessages({prompt, options: compatibleOptions}), + // tools: + tools: openaiTools, + tool_choice: openaiToolChoice +@@ -421,6 +430,17 @@ var OpenAICompatibleChatLanguageModel = class { + text: reasoning + }); + } ++ if (choice.message.images) { ++ for (const image of choice.message.images) { ++ const match1 = image.image_url.url.match(/^data:([^;]+)/) ++ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/); ++ content.push({ ++ type: 'file', ++ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg', ++ data: match2 ? match2[1] : image.image_url.url, ++ }); ++ } ++ } + if (choice.message.tool_calls != null) { + for (const toolCall of choice.message.tool_calls) { + content.push({ +@@ -598,6 +618,17 @@ var OpenAICompatibleChatLanguageModel = class { + delta: delta.content + }); + } ++ if (delta.images) { ++ for (const image of delta.images) { ++ const match1 = image.image_url.url.match(/^data:([^;]+)/) ++ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/); ++ controller.enqueue({ ++ type: 'file', ++ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg', ++ data: match2 ? match2[1] : image.image_url.url, ++ }); ++ } ++ } + if (delta.tool_calls != null) { + for (const toolCallDelta of delta.tool_calls) { + const index = toolCallDelta.index; +@@ -765,6 +796,14 @@ var OpenAICompatibleChatResponseSchema = import_v43.z.object({ + arguments: import_v43.z.string() + }) + }) ++ ).nullish(), ++ images: import_v43.z.array( ++ import_v43.z.object({ ++ type: import_v43.z.literal('image_url'), ++ image_url: import_v43.z.object({ ++ url: import_v43.z.string(), ++ }) ++ }) + ).nullish() + }), + finish_reason: import_v43.z.string().nullish() +@@ -795,6 +834,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => import_v43.z.union( + arguments: import_v43.z.string().nullish() + }) + }) ++ ).nullish(), ++ images: import_v43.z.array( ++ import_v43.z.object({ ++ type: import_v43.z.literal('image_url'), ++ image_url: import_v43.z.object({ ++ url: import_v43.z.string(), ++ }) ++ }) + ).nullish() + }).nullish(), + finish_reason: import_v43.z.string().nullish() +diff --git a/dist/index.mjs b/dist/index.mjs +index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..565b605eeacd9830b2b0e817e58ad0c5700264de 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -23,7 +23,7 @@ function getOpenAIMetadata(message) { + var _a, _b; + return (_b = (_a = message == null ? void 0 : message.providerOptions) == null ? void 0 : _a.openaiCompatible) != null ? _b : {}; + } +-function convertToOpenAICompatibleChatMessages(prompt) { ++function convertToOpenAICompatibleChatMessages({prompt, options}) { + const messages = []; + for (const { role, content, ...message } of prompt) { + const metadata = getOpenAIMetadata({ ...message }); +@@ -73,6 +73,7 @@ function convertToOpenAICompatibleChatMessages(prompt) { + } + case "assistant": { + let text = ""; ++ let reasoning_text = ""; + const toolCalls = []; + for (const part of content) { + const partMetadata = getOpenAIMetadata(part); +@@ -81,6 +82,12 @@ function convertToOpenAICompatibleChatMessages(prompt) { + text += part.text; + break; + } ++ case "reasoning": { ++ if (options.sendReasoning) { ++ reasoning_text += part.text; ++ } ++ break; ++ } + case "tool-call": { + toolCalls.push({ + id: part.toolCallId, +@@ -98,6 +105,7 @@ function convertToOpenAICompatibleChatMessages(prompt) { + messages.push({ + role: "assistant", + content: text, ++ reasoning_content: reasoning_text ?? undefined, + tool_calls: toolCalls.length > 0 ? toolCalls : void 0, + ...metadata + }); +@@ -182,7 +190,8 @@ var openaiCompatibleProviderOptions = z.object({ + /** + * Controls the verbosity of the generated text. Defaults to `medium`. + */ +- textVerbosity: z.string().optional() ++ textVerbosity: z.string().optional(), ++ sendReasoning: z.boolean().optional() + }); + + // src/openai-compatible-error.ts +@@ -362,7 +371,7 @@ var OpenAICompatibleChatLanguageModel = class { + reasoning_effort: compatibleOptions.reasoningEffort, + verbosity: compatibleOptions.textVerbosity, + // messages: +- messages: convertToOpenAICompatibleChatMessages(prompt), ++ messages: convertToOpenAICompatibleChatMessages({prompt, options: compatibleOptions}), + // tools: + tools: openaiTools, + tool_choice: openaiToolChoice +@@ -405,6 +414,17 @@ var OpenAICompatibleChatLanguageModel = class { + text: reasoning + }); + } ++ if (choice.message.images) { ++ for (const image of choice.message.images) { ++ const match1 = image.image_url.url.match(/^data:([^;]+)/) ++ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/); ++ content.push({ ++ type: 'file', ++ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg', ++ data: match2 ? match2[1] : image.image_url.url, ++ }); ++ } ++ } + if (choice.message.tool_calls != null) { + for (const toolCall of choice.message.tool_calls) { + content.push({ +@@ -582,6 +602,17 @@ var OpenAICompatibleChatLanguageModel = class { + delta: delta.content + }); + } ++ if (delta.images) { ++ for (const image of delta.images) { ++ const match1 = image.image_url.url.match(/^data:([^;]+)/) ++ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/); ++ controller.enqueue({ ++ type: 'file', ++ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg', ++ data: match2 ? match2[1] : image.image_url.url, ++ }); ++ } ++ } + if (delta.tool_calls != null) { + for (const toolCallDelta of delta.tool_calls) { + const index = toolCallDelta.index; +@@ -749,6 +780,14 @@ var OpenAICompatibleChatResponseSchema = z3.object({ + arguments: z3.string() + }) + }) ++ ).nullish(), ++ images: z3.array( ++ z3.object({ ++ type: z3.literal('image_url'), ++ image_url: z3.object({ ++ url: z3.string(), ++ }) ++ }) + ).nullish() + }), + finish_reason: z3.string().nullish() +@@ -779,6 +818,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => z3.union([ + arguments: z3.string().nullish() + }) + }) ++ ).nullish(), ++ images: z3.array( ++ z3.object({ ++ type: z3.literal('image_url'), ++ image_url: z3.object({ ++ url: z3.string(), ++ }) ++ }) + ).nullish() + }).nullish(), + finish_reason: z3.string().nullish() diff --git a/package.json b/package.json index 9560089333..3cf60b9f18 100644 --- a/package.json +++ b/package.json @@ -416,7 +416,9 @@ "@ai-sdk/openai@npm:^2.0.42": "patch:@ai-sdk/openai@npm%3A2.0.85#~/.yarn/patches/@ai-sdk-openai-npm-2.0.85-27483d1d6a.patch", "@ai-sdk/google@npm:^2.0.40": "patch:@ai-sdk/google@npm%3A2.0.40#~/.yarn/patches/@ai-sdk-google-npm-2.0.40-47e0eeee83.patch", "@ai-sdk/openai-compatible@npm:^1.0.27": "patch:@ai-sdk/openai-compatible@npm%3A1.0.27#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch", - "@ai-sdk/google@npm:2.0.49": "patch:@ai-sdk/google@npm%3A2.0.49#~/.yarn/patches/@ai-sdk-google-npm-2.0.49-84720f41bd.patch" + "@ai-sdk/google@npm:2.0.49": "patch:@ai-sdk/google@npm%3A2.0.49#~/.yarn/patches/@ai-sdk-google-npm-2.0.49-84720f41bd.patch", + "@ai-sdk/openai-compatible@npm:1.0.27": "patch:@ai-sdk/openai-compatible@npm%3A1.0.28#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch", + "@ai-sdk/openai-compatible@npm:^1.0.19": "patch:@ai-sdk/openai-compatible@npm%3A1.0.28#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch" }, "packageManager": "yarn@4.9.1", "lint-staged": { diff --git a/packages/ai-sdk-provider/package.json b/packages/ai-sdk-provider/package.json index 25864f3b1f..e635f93aeb 100644 --- a/packages/ai-sdk-provider/package.json +++ b/packages/ai-sdk-provider/package.json @@ -41,7 +41,7 @@ "ai": "^5.0.26" }, "dependencies": { - "@ai-sdk/openai-compatible": "^1.0.28", + "@ai-sdk/openai-compatible": "patch:@ai-sdk/openai-compatible@npm%3A1.0.28#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch", "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.17" }, diff --git a/packages/aiCore/package.json b/packages/aiCore/package.json index 6fc0f53344..e73a843b1d 100644 --- a/packages/aiCore/package.json +++ b/packages/aiCore/package.json @@ -42,7 +42,7 @@ "@ai-sdk/anthropic": "^2.0.49", "@ai-sdk/azure": "^2.0.87", "@ai-sdk/deepseek": "^1.0.31", - "@ai-sdk/openai-compatible": "patch:@ai-sdk/openai-compatible@npm%3A1.0.27#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch", + "@ai-sdk/openai-compatible": "patch:@ai-sdk/openai-compatible@npm%3A1.0.28#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch", "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.17", "@ai-sdk/xai": "^2.0.36", diff --git a/src/renderer/src/aiCore/utils/options.ts b/src/renderer/src/aiCore/utils/options.ts index 4d8d4070e9..36778b7570 100644 --- a/src/renderer/src/aiCore/utils/options.ts +++ b/src/renderer/src/aiCore/utils/options.ts @@ -10,6 +10,7 @@ import { isAnthropicModel, isGeminiModel, isGrokModel, + isInterleavedThinkingModel, isOpenAIModel, isOpenAIOpenWeightModel, isQwenMTModel, @@ -603,7 +604,7 @@ function buildGenericProviderOptions( enableGenerateImage: boolean } ): Record { - const { enableWebSearch } = capabilities + const { enableWebSearch, enableReasoning } = capabilities let providerOptions: Record = {} const reasoningParams = getReasoningEffort(assistant, model) @@ -611,6 +612,14 @@ function buildGenericProviderOptions( ...providerOptions, ...reasoningParams } + if (enableReasoning) { + if (isInterleavedThinkingModel(model)) { + providerOptions = { + ...providerOptions, + sendReasoning: true + } + } + } if (enableWebSearch) { const webSearchParams = getWebSearchParams(model) diff --git a/src/renderer/src/config/models/__tests__/reasoning.test.ts b/src/renderer/src/config/models/__tests__/reasoning.test.ts index 783cb39993..6b00a8912b 100644 --- a/src/renderer/src/config/models/__tests__/reasoning.test.ts +++ b/src/renderer/src/config/models/__tests__/reasoning.test.ts @@ -17,6 +17,7 @@ import { isGeminiReasoningModel, isGrok4FastReasoningModel, isHunyuanReasoningModel, + isInterleavedThinkingModel, isLingReasoningModel, isMiniMaxReasoningModel, isPerplexityReasoningModel, @@ -2157,3 +2158,105 @@ describe('getModelSupportedReasoningEffortOptions', () => { }) }) }) + +describe('isInterleavedThinkingModel', () => { + describe('MiniMax models', () => { + it('should return true for minimax-m2', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'minimax-m2' }))).toBe(true) + }) + + it('should return true for minimax-m2.1', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'minimax-m2.1' }))).toBe(true) + }) + + it('should return true for minimax-m2 with suffixes', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'minimax-m2-pro' }))).toBe(true) + expect(isInterleavedThinkingModel(createModel({ id: 'minimax-m2-preview' }))).toBe(true) + expect(isInterleavedThinkingModel(createModel({ id: 'minimax-m2-lite' }))).toBe(true) + expect(isInterleavedThinkingModel(createModel({ id: 'minimax-m2-ultra-lite' }))).toBe(true) + }) + + it('should return true for minimax-m2.x with suffixes', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'minimax-m2.1-pro' }))).toBe(true) + expect(isInterleavedThinkingModel(createModel({ id: 'minimax-m2.2-preview' }))).toBe(true) + expect(isInterleavedThinkingModel(createModel({ id: 'minimax-m2.5-lite' }))).toBe(true) + }) + + it('should return false for non-m2 minimax models', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'minimax-m1' }))).toBe(false) + expect(isInterleavedThinkingModel(createModel({ id: 'minimax-m3' }))).toBe(false) + expect(isInterleavedThinkingModel(createModel({ id: 'minimax-pro' }))).toBe(false) + }) + + it('should handle case insensitivity', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'MiniMax-M2' }))).toBe(true) + expect(isInterleavedThinkingModel(createModel({ id: 'MINIMAX-M2.1' }))).toBe(true) + }) + }) + + describe('MiMo models', () => { + it('should return true for mimo-v2-flash', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'mimo-v2-flash' }))).toBe(true) + }) + + it('should return false for other mimo models', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'mimo-v1-flash' }))).toBe(false) + expect(isInterleavedThinkingModel(createModel({ id: 'mimo-v2' }))).toBe(false) + expect(isInterleavedThinkingModel(createModel({ id: 'mimo-v2-pro' }))).toBe(false) + expect(isInterleavedThinkingModel(createModel({ id: 'mimo-flash' }))).toBe(false) + }) + + it('should handle case insensitivity', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'MiMo-V2-Flash' }))).toBe(true) + expect(isInterleavedThinkingModel(createModel({ id: 'MIMO-V2-FLASH' }))).toBe(true) + }) + }) + + describe('Zhipu GLM models', () => { + it('should return true for glm-4.5', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'glm-4.5' }))).toBe(true) + }) + + it('should return true for glm-4.6', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'glm-4.6' }))).toBe(true) + }) + + it('should return true for glm-4.7 and higher versions', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'glm-4.7' }))).toBe(true) + expect(isInterleavedThinkingModel(createModel({ id: 'glm-4.8' }))).toBe(true) + expect(isInterleavedThinkingModel(createModel({ id: 'glm-4.9' }))).toBe(true) + }) + + it('should return true for glm-4.x with suffixes', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'glm-4.5-pro' }))).toBe(true) + expect(isInterleavedThinkingModel(createModel({ id: 'glm-4.6-preview' }))).toBe(true) + expect(isInterleavedThinkingModel(createModel({ id: 'glm-4.7-lite' }))).toBe(true) + expect(isInterleavedThinkingModel(createModel({ id: 'glm-4.8-ultra' }))).toBe(true) + }) + + it('should return false for glm-4 without decimal version', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'glm-4' }))).toBe(false) + expect(isInterleavedThinkingModel(createModel({ id: 'glm-4-pro' }))).toBe(false) + }) + + it('should return false for other glm models', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'glm-3.5' }))).toBe(false) + expect(isInterleavedThinkingModel(createModel({ id: 'glm-5.0' }))).toBe(false) + expect(isInterleavedThinkingModel(createModel({ id: 'glm-zero-preview' }))).toBe(false) + }) + + it('should handle case insensitivity', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'GLM-4.5' }))).toBe(true) + expect(isInterleavedThinkingModel(createModel({ id: 'Glm-4.6-Pro' }))).toBe(true) + }) + }) + + describe('Non-matching models', () => { + it('should return false for unrelated models', () => { + expect(isInterleavedThinkingModel(createModel({ id: 'gpt-4' }))).toBe(false) + expect(isInterleavedThinkingModel(createModel({ id: 'claude-3-opus' }))).toBe(false) + expect(isInterleavedThinkingModel(createModel({ id: 'gemini-pro' }))).toBe(false) + expect(isInterleavedThinkingModel(createModel({ id: 'deepseek-v3' }))).toBe(false) + }) + }) +}) diff --git a/src/renderer/src/config/models/reasoning.ts b/src/renderer/src/config/models/reasoning.ts index 27a793bf7d..5d48e9a122 100644 --- a/src/renderer/src/config/models/reasoning.ts +++ b/src/renderer/src/config/models/reasoning.ts @@ -738,3 +738,20 @@ export const findTokenLimit = (modelId: string): { min: number; max: number } | */ export const isFixedReasoningModel = (model: Model) => isReasoningModel(model) && !isSupportedThinkingTokenModel(model) && !isSupportedReasoningEffortModel(model) + +// https://platform.minimaxi.com/docs/guides/text-m2-function-call#openai-sdk +// https://docs.z.ai/guides/capabilities/thinking-mode +// https://platform.moonshot.cn/docs/guide/use-kimi-k2-thinking-model#%E5%A4%9A%E6%AD%A5%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8 +const INTERLEAVED_THINKING_MODEL_REGEX = + /minimax-m2(.(\d+))?(?:-[\w-]+)?|mimo-v2-flash|glm-4.(\d+)(?:-[\w-]+)?|kimi-k2-thinking?$/i + +/** + * Determines whether the given model supports interleaved thinking. + * + * @param model - The model object to check. + * @returns `true` if the model's ID matches the interleaved thinking model pattern; otherwise, `false`. + */ +export const isInterleavedThinkingModel = (model: Model) => { + const modelId = getLowerBaseModelName(model.id) + return INTERLEAVED_THINKING_MODEL_REGEX.test(modelId) +} diff --git a/yarn.lock b/yarn.lock index 8c856e8cec..1dffdbeb42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -242,19 +242,7 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/openai-compatible@npm:1.0.27, @ai-sdk/openai-compatible@npm:^1.0.19": - version: 1.0.27 - resolution: "@ai-sdk/openai-compatible@npm:1.0.27" - dependencies: - "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.17" - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/9f656e4f2ea4d714dc05be588baafd962b2e0360e9195fef373e745efeb20172698ea87e1033c0c5e1f1aa6e0db76a32629427bc8433eb42bd1a0ee00e04af0c - languageName: node - linkType: hard - -"@ai-sdk/openai-compatible@npm:^1.0.28": +"@ai-sdk/openai-compatible@npm:1.0.28": version: 1.0.28 resolution: "@ai-sdk/openai-compatible@npm:1.0.28" dependencies: @@ -266,15 +254,15 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/openai-compatible@patch:@ai-sdk/openai-compatible@npm%3A1.0.27#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch": - version: 1.0.27 - resolution: "@ai-sdk/openai-compatible@patch:@ai-sdk/openai-compatible@npm%3A1.0.27#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch::version=1.0.27&hash=c44b76" +"@ai-sdk/openai-compatible@patch:@ai-sdk/openai-compatible@npm%3A1.0.28#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch": + version: 1.0.28 + resolution: "@ai-sdk/openai-compatible@patch:@ai-sdk/openai-compatible@npm%3A1.0.28#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch::version=1.0.28&hash=f2cb20" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.17" + "@ai-sdk/provider-utils": "npm:3.0.18" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/80c8331bc5fc62dc23d99d861bdc76e4eaf8b4b071d0b2bfa42fbd87f50b1bcdfa5ce4a4deaf7026a603a1ba6eaf5c884d87e3c58b4d6515c220121d3f421de5 + checksum: 10c0/0b1d99fe8ce506e5c0a3703ae0511ac2017781584074d41faa2df82923c64eb1229ffe9f036de150d0248923613c761a463fe89d5923493983e0463a1101e792 languageName: node linkType: hard @@ -1880,7 +1868,7 @@ __metadata: "@ai-sdk/anthropic": "npm:^2.0.49" "@ai-sdk/azure": "npm:^2.0.87" "@ai-sdk/deepseek": "npm:^1.0.31" - "@ai-sdk/openai-compatible": "patch:@ai-sdk/openai-compatible@npm%3A1.0.27#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch" + "@ai-sdk/openai-compatible": "patch:@ai-sdk/openai-compatible@npm%3A1.0.28#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch" "@ai-sdk/provider": "npm:^2.0.0" "@ai-sdk/provider-utils": "npm:^3.0.17" "@ai-sdk/xai": "npm:^2.0.36" @@ -1900,7 +1888,7 @@ __metadata: version: 0.0.0-use.local resolution: "@cherrystudio/ai-sdk-provider@workspace:packages/ai-sdk-provider" dependencies: - "@ai-sdk/openai-compatible": "npm:^1.0.28" + "@ai-sdk/openai-compatible": "patch:@ai-sdk/openai-compatible@npm%3A1.0.28#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch" "@ai-sdk/provider": "npm:^2.0.0" "@ai-sdk/provider-utils": "npm:^3.0.17" tsdown: "npm:^0.13.3" From 89a6d817f1454b2f1e9bc637949eecaaac616447 Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Wed, 24 Dec 2025 13:25:37 +0800 Subject: [PATCH 09/24] fix(display): improve font selector for long font names (#12100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(display): improve font selector for long font names - Increase Select width from 200px to 280px - Increase SelectRow container width from 300px to 380px - Add Tooltip to show full font name on hover - Add text-overflow ellipsis for long font names 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * refactor(DisplaySettings): replace span with div and use CSS class for truncation --------- Co-authored-by: Claude Opus 4.5 Co-authored-by: icarus --- .../DisplaySettings/DisplaySettings.tsx | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx index 49b3b386a9..444fba569f 100644 --- a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx +++ b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx @@ -18,7 +18,7 @@ import { setSidebarIcons } from '@renderer/store/settings' import { ThemeMode } from '@renderer/types' -import { Button, ColorPicker, Segmented, Select, Switch } from 'antd' +import { Button, ColorPicker, Segmented, Select, Switch, Tooltip } from 'antd' import { Minus, Monitor, Moon, Plus, Sun } from 'lucide-react' import type { FC } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -196,6 +196,21 @@ const DisplaySettings: FC = () => { [t] ) + const renderFontOption = useCallback( + (font: string) => ( + +
+ {font} +
+
+ ), + [] + ) + return ( @@ -292,7 +307,7 @@ const DisplaySettings: FC = () => { {t('settings.display.font.global')} { ), value: '' }, - ...fontList.map((font) => ({ label: {font}, value: font })) + ...fontList.map((font) => ({ label: renderFontOption(font), value: font })) ]} value={userTheme.userCodeFontFamily || ''} onChange={(font) => handleUserCodeFontChange(font)} @@ -480,7 +495,7 @@ const SelectRow = styled.div` display: flex; align-items: center; justify-content: flex-end; - width: 300px; + width: 380px; ` export default DisplaySettings From d9171e0596aa6956f9cae77b99ff1a4402bac7b1 Mon Sep 17 00:00:00 2001 From: Phantom Date: Wed, 24 Dec 2025 14:18:41 +0800 Subject: [PATCH 10/24] fix(openrouter): support GPT-5.1/5.2 reasoning effort 'none' for OpenRouter and improve error handling (#12088) --- src/renderer/src/aiCore/utils/reasoning.ts | 12 +- .../config/models/__tests__/openai.test.ts | 139 ++++++++++++++++++ src/renderer/src/config/models/openai.ts | 28 ++++ .../src/pages/translate/TranslatePage.tsx | 21 ++- src/renderer/src/services/TranslateService.ts | 8 +- src/renderer/src/utils/error.ts | 18 ++- 6 files changed, 198 insertions(+), 28 deletions(-) create mode 100644 src/renderer/src/config/models/__tests__/openai.test.ts diff --git a/src/renderer/src/aiCore/utils/reasoning.ts b/src/renderer/src/aiCore/utils/reasoning.ts index a7d6028857..ab8a0b7983 100644 --- a/src/renderer/src/aiCore/utils/reasoning.ts +++ b/src/renderer/src/aiCore/utils/reasoning.ts @@ -14,7 +14,6 @@ import { isDoubaoSeedAfter251015, isDoubaoThinkingAutoModel, isGemini3ThinkingTokenModel, - isGPT51SeriesModel, isGrok4FastReasoningModel, isOpenAIDeepResearchModel, isOpenAIModel, @@ -32,7 +31,8 @@ import { isSupportedThinkingTokenMiMoModel, isSupportedThinkingTokenModel, isSupportedThinkingTokenQwenModel, - isSupportedThinkingTokenZhipuModel + isSupportedThinkingTokenZhipuModel, + isSupportNoneReasoningEffortModel } from '@renderer/config/models' import { getStoreSetting } from '@renderer/hooks/useSettings' import { getAssistantSettings, getProviderByModel } from '@renderer/services/AssistantService' @@ -74,9 +74,7 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin if (reasoningEffort === 'none') { // openrouter: use reasoning if (model.provider === SystemProviderIds.openrouter) { - // 'none' is not an available value for effort for now. - // I think they should resolve this issue soon, so I'll just go ahead and use this value. - if (isGPT51SeriesModel(model) && reasoningEffort === 'none') { + if (isSupportNoneReasoningEffortModel(model) && reasoningEffort === 'none') { return { reasoning: { effort: 'none' } } } return { reasoning: { enabled: false, exclude: true } } @@ -120,8 +118,8 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin return { thinking: { type: 'disabled' } } } - // Specially for GPT-5.1. Suppose this is a OpenAI Compatible provider - if (isGPT51SeriesModel(model)) { + // GPT 5.1, GPT 5.2, or newer + if (isSupportNoneReasoningEffortModel(model)) { return { reasoningEffort: 'none' } diff --git a/src/renderer/src/config/models/__tests__/openai.test.ts b/src/renderer/src/config/models/__tests__/openai.test.ts new file mode 100644 index 0000000000..8c8e8b6671 --- /dev/null +++ b/src/renderer/src/config/models/__tests__/openai.test.ts @@ -0,0 +1,139 @@ +import type { Model } from '@renderer/types' +import { describe, expect, it, vi } from 'vitest' + +import { isSupportNoneReasoningEffortModel } from '../openai' + +// Mock store and settings to avoid initialization issues +vi.mock('@renderer/store', () => ({ + __esModule: true, + default: { + getState: () => ({ + llm: { providers: [] }, + settings: {} + }) + } +})) + +vi.mock('@renderer/hooks/useStore', () => ({ + getStoreProviders: vi.fn(() => []) +})) + +const createModel = (overrides: Partial = {}): Model => ({ + id: 'gpt-4o', + name: 'gpt-4o', + provider: 'openai', + group: 'OpenAI', + ...overrides +}) + +describe('OpenAI Model Detection', () => { + describe('isSupportNoneReasoningEffortModel', () => { + describe('should return true for GPT-5.1 and GPT-5.2 reasoning models', () => { + it('returns true for GPT-5.1 base model', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.1' }))).toBe(true) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'GPT-5.1' }))).toBe(true) + }) + + it('returns true for GPT-5.1 mini model', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.1-mini' }))).toBe(true) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.1-mini-preview' }))).toBe(true) + }) + + it('returns true for GPT-5.1 preview model', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.1-preview' }))).toBe(true) + }) + + it('returns true for GPT-5.2 base model', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.2' }))).toBe(true) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'GPT-5.2' }))).toBe(true) + }) + + it('returns true for GPT-5.2 mini model', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.2-mini' }))).toBe(true) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.2-mini-preview' }))).toBe(true) + }) + + it('returns true for GPT-5.2 preview model', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.2-preview' }))).toBe(true) + }) + }) + + describe('should return false for pro variants', () => { + it('returns false for GPT-5.1-pro models', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.1-pro' }))).toBe(false) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'GPT-5.1-Pro' }))).toBe(false) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.1-pro-preview' }))).toBe(false) + }) + + it('returns false for GPT-5.2-pro models', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.2-pro' }))).toBe(false) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'GPT-5.2-Pro' }))).toBe(false) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.2-pro-preview' }))).toBe(false) + }) + }) + + describe('should return false for chat variants', () => { + it('returns false for GPT-5.1-chat models', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.1-chat' }))).toBe(false) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'GPT-5.1-Chat' }))).toBe(false) + }) + + it('returns false for GPT-5.2-chat models', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.2-chat' }))).toBe(false) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'GPT-5.2-Chat' }))).toBe(false) + }) + }) + + describe('should return false for GPT-5 series (non-5.1/5.2)', () => { + it('returns false for GPT-5 base model', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5' }))).toBe(false) + }) + + it('returns false for GPT-5 pro model', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5-pro' }))).toBe(false) + }) + + it('returns false for GPT-5 preview model', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5-preview' }))).toBe(false) + }) + }) + + describe('should return false for other OpenAI models', () => { + it('returns false for GPT-4 models', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-4o' }))).toBe(false) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-4-turbo' }))).toBe(false) + }) + + it('returns false for o1 models', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'o1' }))).toBe(false) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'o1-mini' }))).toBe(false) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'o1-preview' }))).toBe(false) + }) + + it('returns false for o3 models', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'o3' }))).toBe(false) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'o3-mini' }))).toBe(false) + }) + }) + + describe('edge cases', () => { + it('handles models with version suffixes', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.1-2025-01-01' }))).toBe(true) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.2-latest' }))).toBe(true) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'gpt-5.1-pro-2025-01-01' }))).toBe(false) + }) + + it('handles models with OpenRouter prefixes', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'openai/gpt-5.1' }))).toBe(true) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'openai/gpt-5.2-mini' }))).toBe(true) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'openai/gpt-5.1-pro' }))).toBe(false) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'openai/gpt-5.1-chat' }))).toBe(false) + }) + + it('handles mixed case with chat and pro', () => { + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'GPT-5.1-CHAT' }))).toBe(false) + expect(isSupportNoneReasoningEffortModel(createModel({ id: 'GPT-5.2-PRO' }))).toBe(false) + }) + }) + }) +}) diff --git a/src/renderer/src/config/models/openai.ts b/src/renderer/src/config/models/openai.ts index 86601659e2..ebad589d53 100644 --- a/src/renderer/src/config/models/openai.ts +++ b/src/renderer/src/config/models/openai.ts @@ -77,6 +77,34 @@ export function isSupportVerbosityModel(model: Model): boolean { ) } +/** + * Determines if a model supports the "none" reasoning effort parameter. + * + * This applies to GPT-5.1 and GPT-5.2 series reasoning models (non-chat, non-pro variants). + * These models allow setting reasoning_effort to "none" to skip reasoning steps. + * + * @param model - The model to check + * @returns true if the model supports "none" reasoning effort, false otherwise + * + * @example + * ```ts + * // Returns true + * isSupportNoneReasoningEffortModel({ id: 'gpt-5.1', provider: 'openai' }) + * isSupportNoneReasoningEffortModel({ id: 'gpt-5.2-mini', provider: 'openai' }) + * + * // Returns false + * isSupportNoneReasoningEffortModel({ id: 'gpt-5.1-pro', provider: 'openai' }) + * isSupportNoneReasoningEffortModel({ id: 'gpt-5.1-chat', provider: 'openai' }) + * isSupportNoneReasoningEffortModel({ id: 'gpt-5-pro', provider: 'openai' }) + * ``` + */ +export function isSupportNoneReasoningEffortModel(model: Model): boolean { + const modelId = getLowerBaseModelName(model.id) + return ( + (isGPT51SeriesModel(model) || isGPT52SeriesModel(model)) && !modelId.includes('chat') && !modelId.includes('pro') + ) +} + export function isOpenAIChatCompletionOnlyModel(model: Model): boolean { if (!model) { return false diff --git a/src/renderer/src/pages/translate/TranslatePage.tsx b/src/renderer/src/pages/translate/TranslatePage.tsx index dd47d41c9b..ce4a4625c1 100644 --- a/src/renderer/src/pages/translate/TranslatePage.tsx +++ b/src/renderer/src/pages/translate/TranslatePage.tsx @@ -30,8 +30,7 @@ import { } from '@renderer/types' import { getFileExtension, isTextFile, runAsyncFunction, uuid } from '@renderer/utils' import { abortCompletion } from '@renderer/utils/abortController' -import { isAbortError } from '@renderer/utils/error' -import { formatErrorMessage } from '@renderer/utils/error' +import { formatErrorMessageWithPrefix, isAbortError } from '@renderer/utils/error' import { getFilesFromDropEvent, getTextFromDropEvent } from '@renderer/utils/input' import { createInputScrollHandler, @@ -181,7 +180,7 @@ const TranslatePage: FC = () => { window.toast.info(t('translate.info.aborted')) } else { logger.error('Failed to translate text', e as Error) - window.toast.error(t('translate.error.failed') + ': ' + formatErrorMessage(e)) + window.toast.error(formatErrorMessageWithPrefix(e, t('translate.error.failed'))) } setTranslating(false) return @@ -202,11 +201,11 @@ const TranslatePage: FC = () => { await saveTranslateHistory(text, translated, actualSourceLanguage.langCode, actualTargetLanguage.langCode) } catch (e) { logger.error('Failed to save translate history', e as Error) - window.toast.error(t('translate.history.error.save') + ': ' + formatErrorMessage(e)) + window.toast.error(formatErrorMessageWithPrefix(e, t('translate.history.error.save'))) } } catch (e) { logger.error('Failed to translate', e as Error) - window.toast.error(t('translate.error.unknown') + ': ' + formatErrorMessage(e)) + window.toast.error(formatErrorMessageWithPrefix(e, t('translate.error.unknown'))) } }, [autoCopy, copy, dispatch, setTimeoutTimer, setTranslatedContent, setTranslating, t, translating] @@ -266,7 +265,7 @@ const TranslatePage: FC = () => { await translate(text, actualSourceLanguage, actualTargetLanguage) } catch (error) { logger.error('Translation error:', error as Error) - window.toast.error(t('translate.error.failed') + ': ' + formatErrorMessage(error)) + window.toast.error(formatErrorMessageWithPrefix(error, t('translate.error.failed'))) return } finally { setTranslating(false) @@ -427,7 +426,7 @@ const TranslatePage: FC = () => { setAutoDetectionMethod(method) } catch (e) { logger.error('Failed to update auto detection method setting.', e as Error) - window.toast.error(t('translate.error.detect.update_setting') + formatErrorMessage(e)) + window.toast.error(formatErrorMessageWithPrefix(e, t('translate.error.detect.update_setting'))) } } @@ -498,7 +497,7 @@ const TranslatePage: FC = () => { isText = await isTextFile(file.path) } catch (e) { logger.error('Failed to check file type.', e as Error) - window.toast.error(t('translate.files.error.check_type') + ': ' + formatErrorMessage(e)) + window.toast.error(formatErrorMessageWithPrefix(e, t('translate.files.error.check_type'))) return } } else { @@ -530,11 +529,11 @@ const TranslatePage: FC = () => { setText(text + result) } catch (e) { logger.error('Failed to read file.', e as Error) - window.toast.error(t('translate.files.error.unknown') + ': ' + formatErrorMessage(e)) + window.toast.error(formatErrorMessageWithPrefix(e, t('translate.files.error.unknown'))) } } catch (e) { logger.error('Failed to read file.', e as Error) - window.toast.error(t('translate.files.error.unknown') + ': ' + formatErrorMessage(e)) + window.toast.error(formatErrorMessageWithPrefix(e, t('translate.files.error.unknown'))) } } const promise = _readFile() @@ -578,7 +577,7 @@ const TranslatePage: FC = () => { await processFile(file) } catch (e) { logger.error('Unknown error when selecting file.', e as Error) - window.toast.error(t('translate.files.error.unknown') + ': ' + formatErrorMessage(e)) + window.toast.error(formatErrorMessageWithPrefix(e, t('translate.files.error.unknown'))) } finally { clearFiles() setIsProcessing(false) diff --git a/src/renderer/src/services/TranslateService.ts b/src/renderer/src/services/TranslateService.ts index 328f1a8edf..67e4f66bc3 100644 --- a/src/renderer/src/services/TranslateService.ts +++ b/src/renderer/src/services/TranslateService.ts @@ -42,7 +42,7 @@ export const translateText = async ( abortKey?: string, options?: TranslateOptions ) => { - let abortError + let error const assistantSettings: Partial | undefined = options ? { reasoning_effort: options?.reasoningEffort } : undefined @@ -58,8 +58,8 @@ export const translateText = async ( } else if (chunk.type === ChunkType.TEXT_COMPLETE) { completed = true } else if (chunk.type === ChunkType.ERROR) { + error = chunk.error if (isAbortError(chunk.error)) { - abortError = chunk.error completed = true } } @@ -84,8 +84,8 @@ export const translateText = async ( } } - if (abortError) { - throw abortError + if (error !== undefined && !isAbortError(error)) { + throw error } const trimmedText = translatedText.trim() diff --git a/src/renderer/src/utils/error.ts b/src/renderer/src/utils/error.ts index d4ea2979e2..ec2e15f6d8 100644 --- a/src/renderer/src/utils/error.ts +++ b/src/renderer/src/utils/error.ts @@ -1,3 +1,4 @@ +import { loggerService } from '@logger' import type { McpError } from '@modelcontextprotocol/sdk/types.js' import type { AgentServerError } from '@renderer/types' import { AgentServerErrorSchema } from '@renderer/types' @@ -20,7 +21,7 @@ import { ZodError } from 'zod' import { parseJSON } from './json' import { safeSerialize } from './serialize' -// const logger = loggerService.withContext('Utils:error') +const logger = loggerService.withContext('Utils:error') export function getErrorDetails(err: any, seen = new WeakSet()): any { // Handle circular references @@ -65,11 +66,16 @@ export function formatErrorMessage(error: unknown): string { delete detailedError?.stack delete detailedError?.request_id - const formattedJson = JSON.stringify(detailedError, null, 2) - .split('\n') - .map((line) => ` ${line}`) - .join('\n') - return detailedError.message ? detailedError.message : `Error Details:\n${formattedJson}` + if (detailedError) { + const formattedJson = JSON.stringify(detailedError, null, 2) + .split('\n') + .map((line) => ` ${line}`) + .join('\n') + return detailedError.message ? detailedError.message : `Error Details:\n${formattedJson}` + } else { + logger.warn('Get detailed error failed.') + return '' + } } export function getErrorMessage(error: unknown): string { From f7312697e7c4700dd035fb4b78b7d07dedc93dd0 Mon Sep 17 00:00:00 2001 From: Kejiang Ma Date: Wed, 24 Dec 2025 15:26:19 +0800 Subject: [PATCH 11/24] feat: close ovms process when app quit (#12101) * feat:close ovms process while app quit * add await for execAsync * update 'will-quit' event --- src/main/index.ts | 4 ++++ src/main/ipc.ts | 3 +-- src/main/services/OvmsManager.ts | 33 ++++++-------------------------- 3 files changed, 11 insertions(+), 29 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 657c31dfc4..ec16475d3f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -37,6 +37,7 @@ import { versionService } from './services/VersionService' import { windowService } from './services/WindowService' import { initWebviewHotkeys } from './services/WebviewService' import { runAsyncFunction } from './utils' +import { ovmsManager } from './services/OvmsManager' const logger = loggerService.withContext('MainEntry') @@ -247,12 +248,15 @@ if (!app.requestSingleInstanceLock()) { app.on('will-quit', async () => { // 简单的资源清理,不阻塞退出流程 + await ovmsManager.stopOvms() + try { await mcpService.cleanup() await apiServerService.stop() } catch (error) { logger.warn('Error cleaning up MCP service:', error as Error) } + // finish the logger logger.finish() }) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 08bfbac6f8..8f86a93075 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -59,7 +59,7 @@ import NotificationService from './services/NotificationService' import * as NutstoreService from './services/NutstoreService' import ObsidianVaultService from './services/ObsidianVaultService' import { ocrService } from './services/ocr/OcrService' -import OvmsManager from './services/OvmsManager' +import { ovmsManager } from './services/OvmsManager' import powerMonitorService from './services/PowerMonitorService' import { proxyManager } from './services/ProxyManager' import { pythonService } from './services/PythonService' @@ -107,7 +107,6 @@ const obsidianVaultService = new ObsidianVaultService() const vertexAIService = VertexAIService.getInstance() const memoryService = MemoryService.getInstance() const dxtService = new DxtService() -const ovmsManager = new OvmsManager() const pluginService = PluginService.getInstance() function normalizeError(error: unknown): Error { diff --git a/src/main/services/OvmsManager.ts b/src/main/services/OvmsManager.ts index 3a32d74ecf..54e0a1bb8b 100644 --- a/src/main/services/OvmsManager.ts +++ b/src/main/services/OvmsManager.ts @@ -102,32 +102,10 @@ class OvmsManager { */ public async stopOvms(): Promise<{ success: boolean; message?: string }> { try { - // Check if OVMS process is running - const psCommand = `Get-Process -Name "ovms" -ErrorAction SilentlyContinue | Select-Object Id, Path | ConvertTo-Json` - const { stdout } = await execAsync(`powershell -Command "${psCommand}"`) - - if (!stdout.trim()) { - logger.info('OVMS process is not running') - return { success: true, message: 'OVMS process is not running' } - } - - const processes = JSON.parse(stdout) - const processList = Array.isArray(processes) ? processes : [processes] - - if (processList.length === 0) { - logger.info('OVMS process is not running') - return { success: true, message: 'OVMS process is not running' } - } - - // Terminate all OVMS processes using terminalProcess - for (const process of processList) { - const result = await this.terminalProcess(process.Id) - if (!result.success) { - logger.error(`Failed to terminate OVMS process with PID: ${process.Id}, ${result.message}`) - return { success: false, message: `Failed to terminate OVMS process: ${result.message}` } - } - logger.info(`Terminated OVMS process with PID: ${process.Id}`) - } + // close the OVMS process + await execAsync( + `powershell -Command "Get-WmiObject Win32_Process | Where-Object { $_.CommandLine -like 'ovms.exe*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"` + ) // Reset the ovms instance this.ovms = null @@ -584,4 +562,5 @@ class OvmsManager { } } -export default OvmsManager +// Export singleton instance +export const ovmsManager = new OvmsManager() From 4ba0f2d25c7746fd1215892412a4ebc6b597311e Mon Sep 17 00:00:00 2001 From: fullex <106392080+0xfullex@users.noreply.github.com> Date: Thu, 25 Dec 2025 13:26:32 +0800 Subject: [PATCH 12/24] fix: correct aihubmix anthropic API path (#12115) Remove incorrect /anthropic suffix from aihubmix provider's anthropicApiHost configuration. The correct API endpoint should be https://aihubmix.com/v1/messages, not https://aihubmix.com/anthropic/v1/messages. Fixes issue where Claude API requests to aihubmix provider were failing due to incorrect URL path. --- src/renderer/src/config/providers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 1adeb58ad0..ed618f909c 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -107,7 +107,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = type: 'openai', apiKey: '', apiHost: 'https://aihubmix.com', - anthropicApiHost: 'https://aihubmix.com/anthropic', + anthropicApiHost: 'https://aihubmix.com', models: SYSTEM_MODELS.aihubmix, isSystem: true, enabled: false From 0669253abbada00fe81742b166bea8cc43bf0042 Mon Sep 17 00:00:00 2001 From: Caelan <79105826+jin-wang-c@users.noreply.github.com> Date: Thu, 25 Dec 2025 13:46:33 +0800 Subject: [PATCH 13/24] feat:dmx-painting-add-extend_params (#12098) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * dmx-painting-add-extend_params * format-code * 更新类型 --- .../src/pages/paintings/DmxapiPage.tsx | 31 +++++++++++++------ .../pages/paintings/config/DmxapiConfig.ts | 2 +- src/renderer/src/types/index.ts | 1 + 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/pages/paintings/DmxapiPage.tsx b/src/renderer/src/pages/paintings/DmxapiPage.tsx index 560e3857ba..e4f8323655 100644 --- a/src/renderer/src/pages/paintings/DmxapiPage.tsx +++ b/src/renderer/src/pages/paintings/DmxapiPage.tsx @@ -140,11 +140,14 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { let model = '' let priceModel = '' let image_size = '' + let extend_params = {} + for (const provider of Object.keys(modelGroups)) { if (modelGroups[provider] && modelGroups[provider].length > 0) { model = modelGroups[provider][0].id priceModel = modelGroups[provider][0].price image_size = modelGroups[provider][0].image_sizes[0].value + extend_params = modelGroups[provider][0].extend_params break } } @@ -153,7 +156,8 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { model, priceModel, image_size, - modelGroups + modelGroups, + extend_params } } @@ -162,7 +166,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { const generationMode = params?.generationMode || painting?.generationMode || MODEOPTIONS[0].value - const { model, priceModel, image_size, modelGroups } = getFirstModelInfo(generationMode) + const { model, priceModel, image_size, modelGroups, extend_params } = getFirstModelInfo(generationMode) return { ...DEFAULT_PAINTING, @@ -173,6 +177,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { modelGroups, priceModel, image_size, + extend_params, ...params } } @@ -190,7 +195,12 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { const onSelectModel = (modelId: string) => { const model = allModels.find((m) => m.id === modelId) if (model) { - updatePaintingState({ model: modelId, priceModel: model.price, image_size: model.image_sizes[0].value }) + updatePaintingState({ + model: modelId, + priceModel: model.price, + image_size: model.image_sizes[0].value, + extend_params: model.extend_params + }) } } @@ -293,7 +303,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { clearImages() - const { model, priceModel, image_size, modelGroups } = getFirstModelInfo(v) + const { model, priceModel, image_size, modelGroups, extend_params } = getFirstModelInfo(v) setModelOptions(modelGroups) @@ -309,9 +319,10 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { // 否则更新当前painting updatePaintingState({ generationMode: v, - model: model, - image_size: image_size, - priceModel: priceModel + model, + image_size, + priceModel, + extend_params }) } } @@ -355,7 +366,8 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { const params = { prompt, model: painting.model, - n: painting.n + n: painting.n, + ...painting?.extend_params } const headerExpand = { @@ -397,7 +409,8 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { const params = { prompt, n: painting.n, - model: painting.model + model: painting.model, + ...painting?.extend_params } if (painting.image_size) { diff --git a/src/renderer/src/pages/paintings/config/DmxapiConfig.ts b/src/renderer/src/pages/paintings/config/DmxapiConfig.ts index 7880f6305c..52af9490c8 100644 --- a/src/renderer/src/pages/paintings/config/DmxapiConfig.ts +++ b/src/renderer/src/pages/paintings/config/DmxapiConfig.ts @@ -84,7 +84,7 @@ export const MODEOPTIONS = [ // 获取模型分组数据 export const GetModelGroup = async (): Promise => { try { - const response = await fetch('https://dmxapi.cn/cherry_painting_models_v2.json') + const response = await fetch('https://dmxapi.cn/cherry_painting_models_v3.json') if (response.ok) { const data = await response.json() diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 126c97686e..a75fc1ed3e 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -395,6 +395,7 @@ export interface DmxapiPainting extends PaintingParams { autoCreate?: boolean generationMode?: generationModeType priceModel?: string + extend_params?: Record } export interface TokenFluxPainting extends PaintingParams { From 05dfb459a6c8faf52868e57bf5c2bc40f928157a Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 25 Dec 2025 14:41:52 +0800 Subject: [PATCH 14/24] chore: release v1.7.7 --- electron-builder.yml | 74 +++++++++++++++++++++++++++++++------------- package.json | 2 +- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/electron-builder.yml b/electron-builder.yml index f362542b9a..11dce735c5 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -134,38 +134,68 @@ artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | - Cherry Studio 1.7.6 - New Models & MCP Enhancements + Cherry Studio 1.7.7 - New Models & UI Improvements - This release adds support for new AI models and includes a new MCP server for memory management. + This release adds new AI model support, OpenRouter integration, and UI redesigns. ✨ New Features - - [Models] Add support for Xiaomi MiMo model - - [Models] Add support for Gemini 3 Flash and Pro model detection - - [Models] Add support for Volcengine Doubao-Seed-1.8 model - - [MCP] Add Nowledge Mem builtin MCP server for memory management - - [Settings] Add default reasoning effort option to resolve confusion between undefined and none + - [Models] Add GLM-4.7 and MiniMax-M2.1 model support + - [Provider] Add OpenRouter provider support + - [OVMS] Upgrade to 2025.4 with Qwen3-4B-int4-ov preset model + - [OVMS] Close OVMS process when app quits + - [Search] Show keyword-adjacent snippets in history search + - [Painting] Add extend_params support for DMX painting + - [UI] Add MCP logo and replace Hammer icon + + 🎨 UI Improvements + - [Notes] Move notes settings to popup in NotesPage for quick access + - [WebSearch] Redesign settings with two-column layout and "Set as Default" button + - [Display] Improve font selector for long font names + - [Transfer] Rename LanDrop to LanTransfer 🐛 Bug Fixes - - [Azure] Restore deployment-based URLs for non-v1 apiVersion - - [Translation] Disable reasoning mode for translation to improve efficiency - - [Image] Update API path for image generation requests in OpenAIBaseClient - - [Windows] Auto-discover and persist Git Bash path on Windows for scoop users + - [API] Correct aihubmix Anthropic API path + - [OpenRouter] Support GPT-5.1/5.2 reasoning effort 'none' and improve error handling + - [Thinking] Fix interleaved thinking support + - [Memory] Fix retrieval issues and enable database backup + - [Settings] Update default assistant settings to disable temperature + - [OpenAI] Add persistent server configuration support + - [Azure] Normalize Azure endpoint + - [MCP] Check system npx/uvx before falling back to bundled binaries + - [Prompt] Improve language instruction clarity + - [Models] Include GPT5.2 series in verbosity check + - [URL] Enhance urlContext validation for supported providers and models - Cherry Studio 1.7.6 - 新模型与 MCP 增强 + Cherry Studio 1.7.7 - 新模型与界面改进 - 本次更新添加了多个新 AI 模型支持,并新增记忆管理 MCP 服务器。 + 本次更新添加了新 AI 模型支持、OpenRouter 集成以及界面重新设计。 ✨ 新功能 - - [模型] 添加小米 MiMo 模型支持 - - [模型] 添加 Gemini 3 Flash 和 Pro 模型检测支持 - - [模型] 添加火山引擎 Doubao-Seed-1.8 模型支持 - - [MCP] 新增 Nowledge Mem 内置 MCP 服务器,用于记忆管理 - - [设置] 添加默认推理强度选项,解决 undefined 和 none 之间的混淆 + - [模型] 添加 GLM-4.7 和 MiniMax-M2.1 模型支持 + - [服务商] 添加 OpenRouter 服务商支持 + - [OVMS] 升级至 2025.4,新增 Qwen3-4B-int4-ov 预设模型 + - [OVMS] 应用退出时关闭 OVMS 进程 + - [搜索] 历史搜索显示关键词上下文片段 + - [绘图] DMX 绘图添加扩展参数支持 + - [界面] 添加 MCP 图标并替换锤子图标 + + 🎨 界面改进 + - [笔记] 将笔记设置移至笔记页弹窗,快速访问无需离开当前页面 + - [网页搜索] 采用两栏布局重新设计设置界面,添加"设为默认"按钮 + - [显示] 改进长字体名称的字体选择器 + - [传输] LanDrop 重命名为 LanTransfer 🐛 问题修复 - - [Azure] 修复非 v1 apiVersion 的部署 URL 问题 - - [翻译] 禁用翻译时的推理模式以提高效率 - - [图像] 更新 OpenAIBaseClient 中图像生成请求的 API 路径 - - [Windows] 自动发现并保存 Windows scoop 用户的 Git Bash 路径 + - [API] 修复 aihubmix Anthropic API 路径 + - [OpenRouter] 支持 GPT-5.1/5.2 reasoning effort 'none' 并改进错误处理 + - [思考] 修复交错思考支持 + - [记忆] 修复检索问题并启用数据库备份 + - [设置] 更新默认助手设置禁用温度 + - [OpenAI] 添加持久化服务器配置支持 + - [Azure] 规范化 Azure 端点 + - [MCP] 优先检查系统 npx/uvx 再回退到内置二进制文件 + - [提示词] 改进语言指令清晰度 + - [模型] GPT5.2 系列添加到 verbosity 检查 + - [URL] 增强 urlContext 对支持的服务商和模型的验证 diff --git a/package.json b/package.json index 3cf60b9f18..7ad4140af7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.7.6", + "version": "1.7.7", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", From 4ae9bf8ff49fd910402bd38b6e91d3ee27ff122b Mon Sep 17 00:00:00 2001 From: jardel Date: Thu, 25 Dec 2025 16:59:13 +0800 Subject: [PATCH 15/24] fix: allow more file extensions (#12099) Co-authored-by: icarus --- src/main/services/FileStorage.ts | 38 +++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts index 78bffa6692..f0b7ce32b0 100644 --- a/src/main/services/FileStorage.ts +++ b/src/main/services/FileStorage.ts @@ -2,7 +2,7 @@ import { loggerService } from '@logger' import { checkName, getFilesDir, - getFileType, + getFileType as getFileTypeByExt, getName, getNotesDir, getTempDir, @@ -11,13 +11,13 @@ import { } from '@main/utils/file' import { documentExts, imageExts, KB, MB } from '@shared/config/constant' import type { FileMetadata, NotesTreeNode } from '@types' +import { FileTypes } from '@types' import chardet from 'chardet' import type { FSWatcher } from 'chokidar' import chokidar from 'chokidar' import * as crypto from 'crypto' import type { OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron' -import { app } from 'electron' -import { dialog, net, shell } from 'electron' +import { app, dialog, net, shell } from 'electron' import * as fs from 'fs' import { writeFileSync } from 'fs' import { readFile } from 'fs/promises' @@ -185,7 +185,7 @@ class FileStorage { }) } - findDuplicateFile = async (filePath: string): Promise => { + private findDuplicateFile = async (filePath: string): Promise => { const stats = fs.statSync(filePath) logger.debug(`stats: ${stats}, filePath: ${filePath}`) const fileSize = stats.size @@ -204,6 +204,8 @@ class FileStorage { if (originalHash === storedHash) { const ext = path.extname(file) const id = path.basename(file, ext) + const type = await this.getFileType(filePath) + return { id, origin_name: file, @@ -212,7 +214,7 @@ class FileStorage { created_at: storedStats.birthtime.toISOString(), size: storedStats.size, ext, - type: getFileType(ext), + type, count: 2 } } @@ -222,6 +224,13 @@ class FileStorage { return null } + public getFileType = async (filePath: string): Promise => { + const ext = path.extname(filePath) + const fileType = getFileTypeByExt(ext) + + return fileType === FileTypes.OTHER && (await this._isTextFile(filePath)) ? FileTypes.TEXT : fileType + } + public selectFile = async ( _: Electron.IpcMainInvokeEvent, options?: OpenDialogOptions @@ -241,7 +250,7 @@ class FileStorage { const fileMetadataPromises = result.filePaths.map(async (filePath) => { const stats = fs.statSync(filePath) const ext = path.extname(filePath) - const fileType = getFileType(ext) + const fileType = await this.getFileType(filePath) return { id: uuidv4(), @@ -307,7 +316,7 @@ class FileStorage { } const stats = await fs.promises.stat(destPath) - const fileType = getFileType(ext) + const fileType = await this.getFileType(destPath) const fileMetadata: FileMetadata = { id: uuid, @@ -332,8 +341,7 @@ class FileStorage { } const stats = fs.statSync(filePath) - const ext = path.extname(filePath) - const fileType = getFileType(ext) + const fileType = await this.getFileType(filePath) return { id: uuidv4(), @@ -342,7 +350,7 @@ class FileStorage { path: filePath, created_at: stats.birthtime.toISOString(), size: stats.size, - ext: ext, + ext: path.extname(filePath), type: fileType, count: 1 } @@ -690,7 +698,7 @@ class FileStorage { created_at: new Date().toISOString(), size: buffer.length, ext: ext.slice(1), - type: getFileType(ext), + type: getFileTypeByExt(ext), count: 1 } } catch (error) { @@ -740,7 +748,7 @@ class FileStorage { created_at: new Date().toISOString(), size: stats.size, ext: ext.slice(1), - type: getFileType(ext), + type: getFileTypeByExt(ext), count: 1 } } catch (error) { @@ -1317,7 +1325,7 @@ class FileStorage { await fs.promises.writeFile(destPath, buffer) const stats = await fs.promises.stat(destPath) - const fileType = getFileType(ext) + const fileType = await this.getFileType(destPath) return { id: uuid, @@ -1604,6 +1612,10 @@ class FileStorage { } public isTextFile = async (_: Electron.IpcMainInvokeEvent, filePath: string): Promise => { + return this._isTextFile(filePath) + } + + private _isTextFile = async (filePath: string): Promise => { try { const isBinary = await isBinaryFile(filePath) if (isBinary) { From 0f0e18231d1e384a6c0962390f5625a3cf4bb11d Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 26 Dec 2025 11:44:30 +0800 Subject: [PATCH 16/24] fix: update ollama provider type and increment store version to 190 - Changed ollama provider type from 'openai' to 'ollama' in SYSTEM_PROVIDERS_CONFIG. - Incremented persisted reducer version from 189 to 190. - Added migration logic for version 190 to update existing provider types in state. --- src/renderer/src/config/providers.ts | 2 +- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/migrate.ts | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index ed618f909c..bae473a7d7 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -289,7 +289,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = ollama: { id: 'ollama', name: 'Ollama', - type: 'openai', + type: 'ollama', apiKey: '', apiHost: 'http://localhost:11434', models: SYSTEM_MODELS.ollama, diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 15f45648dc..8d8c793c21 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -67,7 +67,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 189, + version: 190, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 4b9d41b9e4..e0d3524f68 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -3098,6 +3098,21 @@ const migrateConfig = { logger.error('migrate 189 error', error as Error) return state } + }, + // 1.7.8 + '190': (state: RootState) => { + try { + state.llm.providers.forEach((provider) => { + if (provider.id === SystemProviderIds.ollama) { + provider.type = 'ollama' + } + }) + logger.info('migrate 190 success') + return state + } catch (error) { + logger.error('migrate 190 error', error as Error) + return state + } } } From ab3bce33b8154c61e2c06404e4f111a89882cd15 Mon Sep 17 00:00:00 2001 From: Shemol Date: Fri, 26 Dec 2025 17:05:45 +0800 Subject: [PATCH 17/24] docs: fix copy -> cp in development guide (#12142) Signed-off-by: SherlockShemol --- docs/en/guides/development.md | 2 +- docs/zh/guides/development.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/guides/development.md b/docs/en/guides/development.md index fe67742768..032a515f61 100644 --- a/docs/en/guides/development.md +++ b/docs/en/guides/development.md @@ -36,7 +36,7 @@ yarn install ### ENV ```bash -copy .env.example .env +cp .env.example .env ``` ### Start diff --git a/docs/zh/guides/development.md b/docs/zh/guides/development.md index fe67742768..032a515f61 100644 --- a/docs/zh/guides/development.md +++ b/docs/zh/guides/development.md @@ -36,7 +36,7 @@ yarn install ### ENV ```bash -copy .env.example .env +cp .env.example .env ``` ### Start From 99b431ec9299add599e6f74067f058a954f83d02 Mon Sep 17 00:00:00 2001 From: defi-failure <159208748+defi-failure@users.noreply.github.com> Date: Fri, 26 Dec 2025 17:37:58 +0800 Subject: [PATCH 18/24] fix: remove trailing api version in ANTHROPIC_BASE_URL (#12145) --- packages/shared/utils.ts | 53 +++++++++++++++++++ .../agents/services/claudecode/index.ts | 10 +++- .../legacy/clients/gemini/GeminiAPIClient.ts | 2 +- .../aiCore/legacy/clients/ovms/OVMSClient.ts | 3 +- src/renderer/src/utils/__tests__/api.test.ts | 3 +- src/renderer/src/utils/api.ts | 53 ------------------- 6 files changed, 66 insertions(+), 58 deletions(-) diff --git a/packages/shared/utils.ts b/packages/shared/utils.ts index a14f78958d..7e90624aba 100644 --- a/packages/shared/utils.ts +++ b/packages/shared/utils.ts @@ -35,3 +35,56 @@ export const defaultAppHeaders = () => { // return value // } // } + +/** + * Extracts the trailing API version segment from a URL path. + * + * This function extracts API version patterns (e.g., `v1`, `v2beta`) from the end of a URL. + * Only versions at the end of the path are extracted, not versions in the middle. + * The returned version string does not include leading or trailing slashes. + * + * @param {string} url - The URL string to parse. + * @returns {string | undefined} The trailing API version found (e.g., 'v1', 'v2beta'), or undefined if none found. + * + * @example + * getTrailingApiVersion('https://api.example.com/v1') // 'v1' + * getTrailingApiVersion('https://api.example.com/v2beta/') // 'v2beta' + * getTrailingApiVersion('https://api.example.com/v1/chat') // undefined (version not at end) + * getTrailingApiVersion('https://gateway.ai.cloudflare.com/v1/xxx/v1beta') // 'v1beta' + * getTrailingApiVersion('https://api.example.com') // undefined + */ +export function getTrailingApiVersion(url: string): string | undefined { + const match = url.match(TRAILING_VERSION_REGEX) + + if (match) { + // Extract version without leading slash and trailing slash + return match[0].replace(/^\//, '').replace(/\/$/, '') + } + + return undefined +} + +/** + * Matches an API version at the end of a URL (with optional trailing slash). + * Used to detect and extract versions only from the trailing position. + */ +const TRAILING_VERSION_REGEX = /\/v\d+(?:alpha|beta)?\/?$/i + +/** + * Removes the trailing API version segment from a URL path. + * + * This function removes API version patterns (e.g., `/v1`, `/v2beta`) from the end of a URL. + * Only versions at the end of the path are removed, not versions in the middle. + * + * @param {string} url - The URL string to process. + * @returns {string} The URL with the trailing API version removed, or the original URL if no trailing version found. + * + * @example + * withoutTrailingApiVersion('https://api.example.com/v1') // 'https://api.example.com' + * withoutTrailingApiVersion('https://api.example.com/v2beta/') // 'https://api.example.com' + * withoutTrailingApiVersion('https://api.example.com/v1/chat') // 'https://api.example.com/v1/chat' (no change) + * withoutTrailingApiVersion('https://api.example.com') // 'https://api.example.com' + */ +export function withoutTrailingApiVersion(url: string): string { + return url.replace(TRAILING_VERSION_REGEX, '') +} diff --git a/src/main/services/agents/services/claudecode/index.ts b/src/main/services/agents/services/claudecode/index.ts index 45cecb049f..69266f5a61 100644 --- a/src/main/services/agents/services/claudecode/index.ts +++ b/src/main/services/agents/services/claudecode/index.ts @@ -18,6 +18,7 @@ import { validateModelId } from '@main/apiServer/utils' import { isWin } from '@main/constant' import { autoDiscoverGitBash } from '@main/utils/process' import getLoginShellEnvironment from '@main/utils/shell-env' +import { withoutTrailingApiVersion } from '@shared/utils' import { app } from 'electron' import type { GetAgentSessionResponse } from '../..' @@ -112,6 +113,13 @@ class ClaudeCodeService implements AgentServiceInterface { // Auto-discover Git Bash path on Windows (already logs internally) const customGitBashPath = isWin ? autoDiscoverGitBash() : null + // Claude Agent SDK builds the final endpoint as `${ANTHROPIC_BASE_URL}/v1/messages`. + // To avoid malformed URLs like `/v1/v1/messages`, we normalize the provider host + // by stripping any trailing API version (e.g. `/v1`). + const anthropicBaseUrl = withoutTrailingApiVersion( + modelInfo.provider.anthropicApiHost?.trim() || modelInfo.provider.apiHost + ) + const env = { ...loginShellEnvWithoutProxies, // TODO: fix the proxy api server @@ -120,7 +128,7 @@ class ClaudeCodeService implements AgentServiceInterface { // ANTHROPIC_BASE_URL: `http://${apiConfig.host}:${apiConfig.port}/${modelInfo.provider.id}`, ANTHROPIC_API_KEY: modelInfo.provider.apiKey, ANTHROPIC_AUTH_TOKEN: modelInfo.provider.apiKey, - ANTHROPIC_BASE_URL: modelInfo.provider.anthropicApiHost?.trim() || modelInfo.provider.apiHost, + ANTHROPIC_BASE_URL: anthropicBaseUrl, ANTHROPIC_MODEL: modelInfo.modelId, ANTHROPIC_DEFAULT_OPUS_MODEL: modelInfo.modelId, ANTHROPIC_DEFAULT_SONNET_MODEL: modelInfo.modelId, diff --git a/src/renderer/src/aiCore/legacy/clients/gemini/GeminiAPIClient.ts b/src/renderer/src/aiCore/legacy/clients/gemini/GeminiAPIClient.ts index ac10106f37..d7f14326f6 100644 --- a/src/renderer/src/aiCore/legacy/clients/gemini/GeminiAPIClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/gemini/GeminiAPIClient.ts @@ -46,7 +46,6 @@ import type { GeminiSdkRawOutput, GeminiSdkToolCall } from '@renderer/types/sdk' -import { getTrailingApiVersion, withoutTrailingApiVersion } from '@renderer/utils' import { isToolUseModeFunction } from '@renderer/utils/assistant' import { geminiFunctionCallToMcpTool, @@ -56,6 +55,7 @@ import { } from '@renderer/utils/mcp-tools' import { findFileBlocks, findImageBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find' import { defaultTimeout, MB } from '@shared/config/constant' +import { getTrailingApiVersion, withoutTrailingApiVersion } from '@shared/utils' import { t } from 'i18next' import type { GenericChunk } from '../../middleware/schemas' diff --git a/src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts b/src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts index 02ac6de091..4936b693ee 100644 --- a/src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts @@ -3,7 +3,8 @@ import { loggerService } from '@logger' import { isSupportedModel } from '@renderer/config/models' import type { Provider } from '@renderer/types' import { objectKeys } from '@renderer/types' -import { formatApiHost, withoutTrailingApiVersion } from '@renderer/utils' +import { formatApiHost } from '@renderer/utils' +import { withoutTrailingApiVersion } from '@shared/utils' import { OpenAIAPIClient } from '../openai/OpenAIApiClient' diff --git a/src/renderer/src/utils/__tests__/api.test.ts b/src/renderer/src/utils/__tests__/api.test.ts index f5251b8393..ad64dc0d73 100644 --- a/src/renderer/src/utils/__tests__/api.test.ts +++ b/src/renderer/src/utils/__tests__/api.test.ts @@ -1,5 +1,6 @@ import store from '@renderer/store' import type { VertexProvider } from '@renderer/types' +import { getTrailingApiVersion, withoutTrailingApiVersion } from '@shared/utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import { @@ -8,14 +9,12 @@ import { formatAzureOpenAIApiHost, formatOllamaApiHost, formatVertexApiHost, - getTrailingApiVersion, hasAPIVersion, isWithTrailingSharp, maskApiKey, routeToEndpoint, splitApiKeyString, validateApiHost, - withoutTrailingApiVersion, withoutTrailingSharp } from '../api' diff --git a/src/renderer/src/utils/api.ts b/src/renderer/src/utils/api.ts index 25a73dcb16..fd470d5406 100644 --- a/src/renderer/src/utils/api.ts +++ b/src/renderer/src/utils/api.ts @@ -19,12 +19,6 @@ export function formatApiKeys(value: string): string { */ const VERSION_REGEX_PATTERN = '\\/v\\d+(?:alpha|beta)?(?=\\/|$)' -/** - * Matches an API version at the end of a URL (with optional trailing slash). - * Used to detect and extract versions only from the trailing position. - */ -const TRAILING_VERSION_REGEX = /\/v\d+(?:alpha|beta)?\/?$/i - /** * 判断 host 的 path 中是否包含形如版本的字符串(例如 /v1、/v2beta 等), * @@ -272,50 +266,3 @@ export function splitApiKeyString(keyStr: string): string[] { .map((k) => k.replace(/\\,/g, ',')) .filter((k) => k) } - -/** - * Extracts the trailing API version segment from a URL path. - * - * This function extracts API version patterns (e.g., `v1`, `v2beta`) from the end of a URL. - * Only versions at the end of the path are extracted, not versions in the middle. - * The returned version string does not include leading or trailing slashes. - * - * @param {string} url - The URL string to parse. - * @returns {string | undefined} The trailing API version found (e.g., 'v1', 'v2beta'), or undefined if none found. - * - * @example - * getTrailingApiVersion('https://api.example.com/v1') // 'v1' - * getTrailingApiVersion('https://api.example.com/v2beta/') // 'v2beta' - * getTrailingApiVersion('https://api.example.com/v1/chat') // undefined (version not at end) - * getTrailingApiVersion('https://gateway.ai.cloudflare.com/v1/xxx/v1beta') // 'v1beta' - * getTrailingApiVersion('https://api.example.com') // undefined - */ -export function getTrailingApiVersion(url: string): string | undefined { - const match = url.match(TRAILING_VERSION_REGEX) - - if (match) { - // Extract version without leading slash and trailing slash - return match[0].replace(/^\//, '').replace(/\/$/, '') - } - - return undefined -} - -/** - * Removes the trailing API version segment from a URL path. - * - * This function removes API version patterns (e.g., `/v1`, `/v2beta`) from the end of a URL. - * Only versions at the end of the path are removed, not versions in the middle. - * - * @param {string} url - The URL string to process. - * @returns {string} The URL with the trailing API version removed, or the original URL if no trailing version found. - * - * @example - * withoutTrailingApiVersion('https://api.example.com/v1') // 'https://api.example.com' - * withoutTrailingApiVersion('https://api.example.com/v2beta/') // 'https://api.example.com' - * withoutTrailingApiVersion('https://api.example.com/v1/chat') // 'https://api.example.com/v1/chat' (no change) - * withoutTrailingApiVersion('https://api.example.com') // 'https://api.example.com' - */ -export function withoutTrailingApiVersion(url: string): string { - return url.replace(TRAILING_VERSION_REGEX, '') -} From 401d66f3ddbb3c1bdecbb23f6fcf69dafae83252 Mon Sep 17 00:00:00 2001 From: fullex <106392080+0xfullex@users.noreply.github.com> Date: Fri, 26 Dec 2025 23:43:38 +0800 Subject: [PATCH 19/24] fix(windows): remember size not working for SelectionAction window (#12132) Co-authored-by: Claude Opus 4.5 --- src/main/services/SelectionService.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/services/SelectionService.ts b/src/main/services/SelectionService.ts index 695026003b..629e67401c 100644 --- a/src/main/services/SelectionService.ts +++ b/src/main/services/SelectionService.ts @@ -1435,6 +1435,12 @@ export class SelectionService { } actionWindow.setBounds({ x, y, width, height }) + + // [Windows only] Update remembered window size for custom resize + // setBounds() may not trigger the 'resized' event, so we need to update manually + if (this.isRemeberWinSize) { + this.lastActionWindowSize = { width, height } + } } /** From 9586f381577f3bb0e8e1768c56151fb1d2dc6c0c Mon Sep 17 00:00:00 2001 From: Phantom Date: Sat, 27 Dec 2025 12:27:11 +0800 Subject: [PATCH 20/24] build: upgrade electron-vite to 5.0.0 with HMR support (#12120) --- electron.vite.config.ts | 7 +- package.json | 7 +- yarn.lock | 486 +++++++++++++++++++++++++++++----------- 3 files changed, 356 insertions(+), 144 deletions(-) diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 172d48ca9a..89c0cf2f9b 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -1,6 +1,6 @@ import react from '@vitejs/plugin-react-swc' import { CodeInspectorPlugin } from 'code-inspector-plugin' -import { defineConfig, externalizeDepsPlugin } from 'electron-vite' +import { defineConfig } from 'electron-vite' import { resolve } from 'path' import { visualizer } from 'rollup-plugin-visualizer' @@ -17,7 +17,7 @@ const isProd = process.env.NODE_ENV === 'production' export default defineConfig({ main: { - plugins: [externalizeDepsPlugin(), ...visualizerPlugin('main')], + plugins: [...visualizerPlugin('main')], resolve: { alias: { '@main': resolve('src/main'), @@ -51,8 +51,7 @@ export default defineConfig({ plugins: [ react({ tsDecorators: true - }), - externalizeDepsPlugin() + }) ], resolve: { alias: { diff --git a/package.json b/package.json index 7ad4140af7..2c3c05daf2 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "scripts": { "start": "electron-vite preview", "dev": "dotenv electron-vite dev", + "dev:watch": "dotenv electron-vite dev -- -w", "debug": "electron-vite -- --inspect --sourcemap --remote-debugging-port=9222", "build": "npm run typecheck && electron-vite build", "build:check": "yarn lint && yarn test", @@ -273,7 +274,7 @@ "electron-reload": "^2.0.0-alpha.1", "electron-store": "^8.2.0", "electron-updater": "patch:electron-updater@npm%3A6.7.0#~/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch", - "electron-vite": "4.0.1", + "electron-vite": "5.0.0", "electron-window-state": "^5.0.3", "emittery": "^1.0.3", "emoji-picker-element": "^1.22.1", @@ -370,7 +371,7 @@ "undici": "6.21.2", "unified": "^11.0.5", "uuid": "^13.0.0", - "vite": "npm:rolldown-vite@7.1.5", + "vite": "npm:rolldown-vite@7.3.0", "vitest": "^3.2.4", "webdav": "^5.8.0", "winston": "^3.17.0", @@ -400,7 +401,7 @@ "pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch", "tar-fs": "^2.1.4", "undici": "6.21.2", - "vite": "npm:rolldown-vite@7.1.5", + "vite": "npm:rolldown-vite@7.3.0", "tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch", "@ai-sdk/openai@npm:^2.0.52": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch", "@img/sharp-darwin-arm64": "0.34.3", diff --git a/yarn.lock b/yarn.lock index 1dffdbeb42..b6b87c568a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -381,7 +381,7 @@ __metadata: languageName: node linkType: hard -"@ampproject/remapping@npm:^2.2.0, @ampproject/remapping@npm:^2.3.0": +"@ampproject/remapping@npm:^2.3.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" dependencies: @@ -1524,26 +1524,26 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.27.7": - version: 7.28.0 - resolution: "@babel/core@npm:7.28.0" +"@babel/core@npm:^7.28.4": + version: 7.28.5 + resolution: "@babel/core@npm:7.28.5" dependencies: - "@ampproject/remapping": "npm:^2.2.0" "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.28.0" + "@babel/generator": "npm:^7.28.5" "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-module-transforms": "npm:^7.27.3" - "@babel/helpers": "npm:^7.27.6" - "@babel/parser": "npm:^7.28.0" + "@babel/helper-module-transforms": "npm:^7.28.3" + "@babel/helpers": "npm:^7.28.4" + "@babel/parser": "npm:^7.28.5" "@babel/template": "npm:^7.27.2" - "@babel/traverse": "npm:^7.28.0" - "@babel/types": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.5" + "@babel/types": "npm:^7.28.5" + "@jridgewell/remapping": "npm:^2.3.5" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10c0/423302e7c721e73b1c096217880272e02020dfb697a55ccca60ad01bba90037015f84d0c20c6ce297cf33a19bb704bc5c2b3d3095f5284dfa592bd1de0b9e8c3 + checksum: 10c0/535f82238027621da6bdffbdbe896ebad3558b311d6f8abc680637a9859b96edbf929ab010757055381570b29cf66c4a295b5618318d27a4273c0e2033925e72 languageName: node linkType: hard @@ -1560,6 +1560,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/generator@npm:7.28.5" + dependencies: + "@babel/parser": "npm:^7.28.5" + "@babel/types": "npm:^7.28.5" + "@jridgewell/gen-mapping": "npm:^0.3.12" + "@jridgewell/trace-mapping": "npm:^0.3.28" + jsesc: "npm:^3.0.2" + checksum: 10c0/9f219fe1d5431b6919f1a5c60db8d5d34fe546c0d8f5a8511b32f847569234ffc8032beb9e7404649a143f54e15224ecb53a3d11b6bb85c3203e573d91fca752 + languageName: node + linkType: hard + "@babel/helper-compilation-targets@npm:^7.27.2": version: 7.27.2 resolution: "@babel/helper-compilation-targets@npm:7.27.2" @@ -1590,16 +1603,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.27.3": - version: 7.27.3 - resolution: "@babel/helper-module-transforms@npm:7.27.3" +"@babel/helper-module-transforms@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/helper-module-transforms@npm:7.28.3" dependencies: "@babel/helper-module-imports": "npm:^7.27.1" "@babel/helper-validator-identifier": "npm:^7.27.1" - "@babel/traverse": "npm:^7.27.3" + "@babel/traverse": "npm:^7.28.3" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/fccb4f512a13b4c069af51e1b56b20f54024bcf1591e31e978a30f3502567f34f90a80da6a19a6148c249216292a8074a0121f9e52602510ef0f32dbce95ca01 + checksum: 10c0/549be62515a6d50cd4cfefcab1b005c47f89bd9135a22d602ee6a5e3a01f27571868ada10b75b033569f24dc4a2bb8d04bfa05ee75c16da7ade2d0db1437fcdb languageName: node linkType: hard @@ -1624,6 +1637,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-validator-identifier@npm:7.28.5" + checksum: 10c0/42aaebed91f739a41f3d80b72752d1f95fd7c72394e8e4bd7cdd88817e0774d80a432451bcba17c2c642c257c483bf1d409dd4548883429ea9493a3bc4ab0847 + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-validator-option@npm:7.27.1" @@ -1631,13 +1651,13 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.27.6": - version: 7.27.6 - resolution: "@babel/helpers@npm:7.27.6" +"@babel/helpers@npm:^7.28.4": + version: 7.28.4 + resolution: "@babel/helpers@npm:7.28.4" dependencies: "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.27.6" - checksum: 10c0/448bac96ef8b0f21f2294a826df9de6bf4026fd023f8a6bb6c782fe3e61946801ca24381490b8e58d861fee75cd695a1882921afbf1f53b0275ee68c938bd6d3 + "@babel/types": "npm:^7.28.4" + checksum: 10c0/aaa5fb8098926dfed5f223adf2c5e4c7fbba4b911b73dfec2d7d3083f8ba694d201a206db673da2d9b3ae8c01793e795767654558c450c8c14b4c2175b4fcb44 languageName: node linkType: hard @@ -1652,6 +1672,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/parser@npm:7.28.5" + dependencies: + "@babel/types": "npm:^7.28.5" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/5bbe48bf2c79594ac02b490a41ffde7ef5aa22a9a88ad6bcc78432a6ba8a9d638d531d868bd1f104633f1f6bba9905746e15185b8276a3756c42b765d131b1ef + languageName: node + linkType: hard + "@babel/plugin-transform-arrow-functions@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-arrow-functions@npm:7.27.1" @@ -1695,7 +1726,7 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.28.0": +"@babel/traverse@npm:^7.27.1": version: 7.28.0 resolution: "@babel/traverse@npm:7.28.0" dependencies: @@ -1710,7 +1741,22 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.6, @babel/types@npm:^7.28.0": +"@babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/traverse@npm:7.28.5" + dependencies: + "@babel/code-frame": "npm:^7.27.1" + "@babel/generator": "npm:^7.28.5" + "@babel/helper-globals": "npm:^7.28.0" + "@babel/parser": "npm:^7.28.5" + "@babel/template": "npm:^7.27.2" + "@babel/types": "npm:^7.28.5" + debug: "npm:^4.3.1" + checksum: 10c0/f6c4a595993ae2b73f2d4cd9c062f2e232174d293edd4abe1d715bd6281da8d99e47c65857e8d0917d9384c65972f4acdebc6749a7c40a8fcc38b3c7fb3e706f + languageName: node + linkType: hard + +"@babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.28.0": version: 7.28.1 resolution: "@babel/types@npm:7.28.1" dependencies: @@ -1730,6 +1776,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/types@npm:7.28.5" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.28.5" + checksum: 10c0/a5a483d2100befbf125793640dec26b90b95fd233a94c19573325898a5ce1e52cdfa96e495c7dcc31b5eca5b66ce3e6d4a0f5a4a62daec271455959f208ab08a + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^1.0.2": version: 1.0.2 resolution: "@bcoe/v8-coverage@npm:1.0.2" @@ -2887,6 +2943,16 @@ __metadata: languageName: node linkType: hard +"@emnapi/core@npm:^1.7.1": + version: 1.7.1 + resolution: "@emnapi/core@npm:1.7.1" + dependencies: + "@emnapi/wasi-threads": "npm:1.1.0" + tslib: "npm:^2.4.0" + checksum: 10c0/f3740be23440b439333e3ae3832163f60c96c4e35337f3220ceba88f36ee89a57a871d27c94eb7a9ff98a09911ed9a2089e477ab549f4d30029f8b907f84a351 + languageName: node + linkType: hard + "@emnapi/runtime@npm:^1.4.3, @emnapi/runtime@npm:^1.4.4, @emnapi/runtime@npm:^1.4.5": version: 1.4.5 resolution: "@emnapi/runtime@npm:1.4.5" @@ -2896,6 +2962,15 @@ __metadata: languageName: node linkType: hard +"@emnapi/runtime@npm:^1.7.1": + version: 1.7.1 + resolution: "@emnapi/runtime@npm:1.7.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/26b851cd3e93877d8732a985a2ebf5152325bbacc6204ef5336a47359dedcc23faeb08cdfcb8bb389b5401b3e894b882bc1a1e55b4b7c1ed1e67c991a760ddd5 + languageName: node + linkType: hard + "@emnapi/wasi-threads@npm:1.0.4": version: 1.0.4 resolution: "@emnapi/wasi-threads@npm:1.0.4" @@ -2905,7 +2980,7 @@ __metadata: languageName: node linkType: hard -"@emnapi/wasi-threads@npm:^1.0.4": +"@emnapi/wasi-threads@npm:1.1.0, @emnapi/wasi-threads@npm:^1.0.4": version: 1.1.0 resolution: "@emnapi/wasi-threads@npm:1.1.0" dependencies: @@ -3883,7 +3958,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/remapping@npm:^2.3.4": +"@jridgewell/remapping@npm:^2.3.4, @jridgewell/remapping@npm:^2.3.5": version: 2.3.5 resolution: "@jridgewell/remapping@npm:2.3.5" dependencies: @@ -4987,14 +5062,14 @@ __metadata: languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^1.0.3": - version: 1.0.3 - resolution: "@napi-rs/wasm-runtime@npm:1.0.3" +"@napi-rs/wasm-runtime@npm:^1.1.0": + version: 1.1.0 + resolution: "@napi-rs/wasm-runtime@npm:1.1.0" dependencies: - "@emnapi/core": "npm:^1.4.5" - "@emnapi/runtime": "npm:^1.4.5" - "@tybys/wasm-util": "npm:^0.10.0" - checksum: 10c0/7918d82477e75931b6e35bb003464382eb93e526362f81a98bf8610407a67b10f4d041931015ad48072c89db547deb7e471dfb91f4ab11ac63a24d8580297f75 + "@emnapi/core": "npm:^1.7.1" + "@emnapi/runtime": "npm:^1.7.1" + "@tybys/wasm-util": "npm:^0.10.1" + checksum: 10c0/ee351052123bfc635c4cef03ac273a686522394ccd513b1e5b7b3823cecd6abb4a31f23a3a962933192b87eb7b7c3eb3def7748bd410edc66f932d90cf44e9ab languageName: node linkType: hard @@ -5329,6 +5404,13 @@ __metadata: languageName: node linkType: hard +"@oxc-project/runtime@npm:0.101.0": + version: 0.101.0 + resolution: "@oxc-project/runtime@npm:0.101.0" + checksum: 10c0/86fd7bb37e94986e7a09bde07a16fa63cebeaada6bcb8963bc07087d54c107d1a128e1c4a5d27b9b593354c092b8976d7653b6700fbb0da0a2b925fb3de4b34c + languageName: node + linkType: hard + "@oxc-project/runtime@npm:0.71.0": version: 0.71.0 resolution: "@oxc-project/runtime@npm:0.71.0" @@ -5336,13 +5418,6 @@ __metadata: languageName: node linkType: hard -"@oxc-project/runtime@npm:=0.82.3": - version: 0.82.3 - resolution: "@oxc-project/runtime@npm:0.82.3" - checksum: 10c0/48fd0577a9bd146da7eefea8e61a7c855f8947ef6233fe7db2921e5c1f07d73459d8fb4d2d9e45f4d522d5bb31af8157c96020860154fdf7223a9cb0957e36c0 - languageName: node - linkType: hard - "@oxc-project/types@npm:0.71.0": version: 0.71.0 resolution: "@oxc-project/types@npm:0.71.0" @@ -5350,10 +5425,10 @@ __metadata: languageName: node linkType: hard -"@oxc-project/types@npm:=0.82.3": - version: 0.82.3 - resolution: "@oxc-project/types@npm:0.82.3" - checksum: 10c0/17dffc91dc3b726be67b7333d251e811bf4badce8ae77269d1626a107cd7cb673674a3fd6e0f127e40951d630281b9a164fee787a1a0cad12e7372a14b89d7cf +"@oxc-project/types@npm:=0.101.0": + version: 0.101.0 + resolution: "@oxc-project/types@npm:0.101.0" + checksum: 10c0/e4e98da6e34ef0163a652e842e795bda77b703d8282fed4984292ff7b289c4e03d848ed8762e549445e33a142d3883e1013cd9ed43156f6eba34c151b8f599c1 languageName: node linkType: hard @@ -6265,16 +6340,16 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-android-arm64@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/binding-android-arm64@npm:1.0.0-beta.34" +"@rolldown/binding-android-arm64@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/binding-android-arm64@npm:1.0.0-beta.53" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-darwin-arm64@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-beta.34" +"@rolldown/binding-darwin-arm64@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-beta.53" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -6286,9 +6361,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-darwin-x64@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-beta.34" +"@rolldown/binding-darwin-x64@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-beta.53" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -6300,9 +6375,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-freebsd-x64@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-beta.34" +"@rolldown/binding-freebsd-x64@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-beta.53" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -6314,9 +6389,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-beta.34" +"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-beta.53" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -6328,9 +6403,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-beta.34" +"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-beta.53" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard @@ -6342,9 +6417,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-arm64-musl@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-beta.34" +"@rolldown/binding-linux-arm64-musl@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-beta.53" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard @@ -6356,9 +6431,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-x64-gnu@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-beta.34" +"@rolldown/binding-linux-x64-gnu@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-beta.53" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard @@ -6370,9 +6445,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-linux-x64-musl@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-beta.34" +"@rolldown/binding-linux-x64-musl@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-beta.53" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard @@ -6384,18 +6459,18 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-openharmony-arm64@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-beta.34" +"@rolldown/binding-openharmony-arm64@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-beta.53" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-wasm32-wasi@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-beta.34" +"@rolldown/binding-wasm32-wasi@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-beta.53" dependencies: - "@napi-rs/wasm-runtime": "npm:^1.0.3" + "@napi-rs/wasm-runtime": "npm:^1.1.0" conditions: cpu=wasm32 languageName: node linkType: hard @@ -6409,9 +6484,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-beta.34" +"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-beta.53" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -6423,13 +6498,6 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-win32-ia32-msvc@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/binding-win32-ia32-msvc@npm:1.0.0-beta.34" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@rolldown/binding-win32-ia32-msvc@npm:1.0.0-beta.9-commit.d91dfb5": version: 1.0.0-beta.9-commit.d91dfb5 resolution: "@rolldown/binding-win32-ia32-msvc@npm:1.0.0-beta.9-commit.d91dfb5" @@ -6437,9 +6505,9 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-win32-x64-msvc@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-beta.34" +"@rolldown/binding-win32-x64-msvc@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-beta.53" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -6458,10 +6526,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/pluginutils@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "@rolldown/pluginutils@npm:1.0.0-beta.34" - checksum: 10c0/96565287991825ecd90b60607dae908ebfdde233661fc589c98547a75c1fd0282b2e2a7849c3eb0c9941e2fba34667a8d5cdb8d597370815c19c2f29b4c157b4 +"@rolldown/pluginutils@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/pluginutils@npm:1.0.0-beta.53" + checksum: 10c0/e8b0a7eb76be22f6f103171f28072de821525a4e400454850516da91a7381957932ff0ce495f227bcb168e86815788b0c1d249ca9e34dca366a82c8825b714ce languageName: node linkType: hard @@ -8204,6 +8272,15 @@ __metadata: languageName: node linkType: hard +"@tybys/wasm-util@npm:^0.10.1": + version: 0.10.1 + resolution: "@tybys/wasm-util@npm:0.10.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/b255094f293794c6d2289300c5fbcafbb5532a3aed3a5ffd2f8dc1828e639b88d75f6a376dd8f94347a44813fd7a7149d8463477a9a49525c8b2dcaa38c2d1e8 + languageName: node + linkType: hard + "@types/aria-query@npm:^5.0.1": version: 5.0.4 resolution: "@types/aria-query@npm:5.0.4" @@ -10209,7 +10286,7 @@ __metadata: electron-reload: "npm:^2.0.0-alpha.1" electron-store: "npm:^8.2.0" electron-updater: "patch:electron-updater@npm%3A6.7.0#~/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch" - electron-vite: "npm:4.0.1" + electron-vite: "npm:5.0.0" electron-window-state: "npm:^5.0.3" emittery: "npm:^1.0.3" emoji-picker-element: "npm:^1.22.1" @@ -10322,7 +10399,7 @@ __metadata: undici: "npm:6.21.2" unified: "npm:^11.0.5" uuid: "npm:^13.0.0" - vite: "npm:rolldown-vite@7.1.5" + vite: "npm:rolldown-vite@7.3.0" vitest: "npm:^3.2.4" webdav: "npm:^5.8.0" winston: "npm:^3.17.0" @@ -13773,15 +13850,15 @@ __metadata: languageName: node linkType: hard -"electron-vite@npm:4.0.1": - version: 4.0.1 - resolution: "electron-vite@npm:4.0.1" +"electron-vite@npm:5.0.0": + version: 5.0.0 + resolution: "electron-vite@npm:5.0.0" dependencies: - "@babel/core": "npm:^7.27.7" + "@babel/core": "npm:^7.28.4" "@babel/plugin-transform-arrow-functions": "npm:^7.27.1" cac: "npm:^6.7.14" - esbuild: "npm:^0.25.5" - magic-string: "npm:^0.30.17" + esbuild: "npm:^0.25.11" + magic-string: "npm:^0.30.19" picocolors: "npm:^1.1.1" peerDependencies: "@swc/core": ^1.0.0 @@ -13791,7 +13868,7 @@ __metadata: optional: true bin: electron-vite: bin/electron-vite.js - checksum: 10c0/4e81ac4e4ede6060ffec56ba9b1d5ff95bb263496e62527345e8c79542924c54c54def39de9b466a81ed250b68774792c2106b93274c790b4cd8e7be448f6af8 + checksum: 10c0/e7797910b23f23f39c12ded92d07d7164c5c6adab294aa13278c1b49ada3b12868b13ace8546d2656db4dbab89978cf8368a659d1ce6a2fb9f1aeddb1c8de557 languageName: node linkType: hard @@ -17573,6 +17650,13 @@ __metadata: languageName: node linkType: hard +"lightningcss-android-arm64@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-android-arm64@npm:1.30.2" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "lightningcss-darwin-arm64@npm:1.30.1": version: 1.30.1 resolution: "lightningcss-darwin-arm64@npm:1.30.1" @@ -17580,6 +17664,13 @@ __metadata: languageName: node linkType: hard +"lightningcss-darwin-arm64@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-darwin-arm64@npm:1.30.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "lightningcss-darwin-x64@npm:1.30.1": version: 1.30.1 resolution: "lightningcss-darwin-x64@npm:1.30.1" @@ -17587,6 +17678,13 @@ __metadata: languageName: node linkType: hard +"lightningcss-darwin-x64@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-darwin-x64@npm:1.30.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "lightningcss-freebsd-x64@npm:1.30.1": version: 1.30.1 resolution: "lightningcss-freebsd-x64@npm:1.30.1" @@ -17594,6 +17692,13 @@ __metadata: languageName: node linkType: hard +"lightningcss-freebsd-x64@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-freebsd-x64@npm:1.30.2" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "lightningcss-linux-arm-gnueabihf@npm:1.30.1": version: 1.30.1 resolution: "lightningcss-linux-arm-gnueabihf@npm:1.30.1" @@ -17601,6 +17706,13 @@ __metadata: languageName: node linkType: hard +"lightningcss-linux-arm-gnueabihf@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-arm-gnueabihf@npm:1.30.2" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "lightningcss-linux-arm64-gnu@npm:1.30.1": version: 1.30.1 resolution: "lightningcss-linux-arm64-gnu@npm:1.30.1" @@ -17608,6 +17720,13 @@ __metadata: languageName: node linkType: hard +"lightningcss-linux-arm64-gnu@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-arm64-gnu@npm:1.30.2" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "lightningcss-linux-arm64-musl@npm:1.30.1": version: 1.30.1 resolution: "lightningcss-linux-arm64-musl@npm:1.30.1" @@ -17615,6 +17734,13 @@ __metadata: languageName: node linkType: hard +"lightningcss-linux-arm64-musl@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-arm64-musl@npm:1.30.2" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "lightningcss-linux-x64-gnu@npm:1.30.1": version: 1.30.1 resolution: "lightningcss-linux-x64-gnu@npm:1.30.1" @@ -17622,6 +17748,13 @@ __metadata: languageName: node linkType: hard +"lightningcss-linux-x64-gnu@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-x64-gnu@npm:1.30.2" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "lightningcss-linux-x64-musl@npm:1.30.1": version: 1.30.1 resolution: "lightningcss-linux-x64-musl@npm:1.30.1" @@ -17629,6 +17762,13 @@ __metadata: languageName: node linkType: hard +"lightningcss-linux-x64-musl@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-x64-musl@npm:1.30.2" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "lightningcss-win32-arm64-msvc@npm:1.30.1": version: 1.30.1 resolution: "lightningcss-win32-arm64-msvc@npm:1.30.1" @@ -17636,6 +17776,13 @@ __metadata: languageName: node linkType: hard +"lightningcss-win32-arm64-msvc@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-win32-arm64-msvc@npm:1.30.2" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "lightningcss-win32-x64-msvc@npm:1.30.1": version: 1.30.1 resolution: "lightningcss-win32-x64-msvc@npm:1.30.1" @@ -17643,7 +17790,14 @@ __metadata: languageName: node linkType: hard -"lightningcss@npm:1.30.1, lightningcss@npm:^1.30.1": +"lightningcss-win32-x64-msvc@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-win32-x64-msvc@npm:1.30.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"lightningcss@npm:1.30.1": version: 1.30.1 resolution: "lightningcss@npm:1.30.1" dependencies: @@ -17683,6 +17837,49 @@ __metadata: languageName: node linkType: hard +"lightningcss@npm:^1.30.2": + version: 1.30.2 + resolution: "lightningcss@npm:1.30.2" + dependencies: + detect-libc: "npm:^2.0.3" + lightningcss-android-arm64: "npm:1.30.2" + lightningcss-darwin-arm64: "npm:1.30.2" + lightningcss-darwin-x64: "npm:1.30.2" + lightningcss-freebsd-x64: "npm:1.30.2" + lightningcss-linux-arm-gnueabihf: "npm:1.30.2" + lightningcss-linux-arm64-gnu: "npm:1.30.2" + lightningcss-linux-arm64-musl: "npm:1.30.2" + lightningcss-linux-x64-gnu: "npm:1.30.2" + lightningcss-linux-x64-musl: "npm:1.30.2" + lightningcss-win32-arm64-msvc: "npm:1.30.2" + lightningcss-win32-x64-msvc: "npm:1.30.2" + dependenciesMeta: + lightningcss-android-arm64: + optional: true + lightningcss-darwin-arm64: + optional: true + lightningcss-darwin-x64: + optional: true + lightningcss-freebsd-x64: + optional: true + lightningcss-linux-arm-gnueabihf: + optional: true + lightningcss-linux-arm64-gnu: + optional: true + lightningcss-linux-arm64-musl: + optional: true + lightningcss-linux-x64-gnu: + optional: true + lightningcss-linux-x64-musl: + optional: true + lightningcss-win32-arm64-msvc: + optional: true + lightningcss-win32-x64-msvc: + optional: true + checksum: 10c0/5c0c73a33946dab65908d5cd1325df4efa290efb77f940b60f40448b5ab9a87d3ea665ef9bcf00df4209705050ecf2f7ecc649f44d6dfa5905bb50f15717e78d + languageName: node + linkType: hard + "lilconfig@npm:^3.1.3": version: 3.1.3 resolution: "lilconfig@npm:3.1.3" @@ -18046,6 +18243,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.19": + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10c0/299378e38f9a270069fc62358522ddfb44e94244baa0d6a8980ab2a9b2490a1d03b236b447eee309e17eb3bddfa482c61259d47960eb018a904f0ded52780c4a + languageName: node + linkType: hard + "magicast@npm:^0.3.5": version: 0.3.5 resolution: "magicast@npm:0.3.5" @@ -22873,28 +23079,25 @@ __metadata: languageName: node linkType: hard -"rolldown@npm:1.0.0-beta.34": - version: 1.0.0-beta.34 - resolution: "rolldown@npm:1.0.0-beta.34" +"rolldown@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "rolldown@npm:1.0.0-beta.53" dependencies: - "@oxc-project/runtime": "npm:=0.82.3" - "@oxc-project/types": "npm:=0.82.3" - "@rolldown/binding-android-arm64": "npm:1.0.0-beta.34" - "@rolldown/binding-darwin-arm64": "npm:1.0.0-beta.34" - "@rolldown/binding-darwin-x64": "npm:1.0.0-beta.34" - "@rolldown/binding-freebsd-x64": "npm:1.0.0-beta.34" - "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-beta.34" - "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-beta.34" - "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-beta.34" - "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-beta.34" - "@rolldown/binding-linux-x64-musl": "npm:1.0.0-beta.34" - "@rolldown/binding-openharmony-arm64": "npm:1.0.0-beta.34" - "@rolldown/binding-wasm32-wasi": "npm:1.0.0-beta.34" - "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-beta.34" - "@rolldown/binding-win32-ia32-msvc": "npm:1.0.0-beta.34" - "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-beta.34" - "@rolldown/pluginutils": "npm:1.0.0-beta.34" - ansis: "npm:^4.0.0" + "@oxc-project/types": "npm:=0.101.0" + "@rolldown/binding-android-arm64": "npm:1.0.0-beta.53" + "@rolldown/binding-darwin-arm64": "npm:1.0.0-beta.53" + "@rolldown/binding-darwin-x64": "npm:1.0.0-beta.53" + "@rolldown/binding-freebsd-x64": "npm:1.0.0-beta.53" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-beta.53" + "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-beta.53" + "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-beta.53" + "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-beta.53" + "@rolldown/binding-linux-x64-musl": "npm:1.0.0-beta.53" + "@rolldown/binding-openharmony-arm64": "npm:1.0.0-beta.53" + "@rolldown/binding-wasm32-wasi": "npm:1.0.0-beta.53" + "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-beta.53" + "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-beta.53" + "@rolldown/pluginutils": "npm:1.0.0-beta.53" dependenciesMeta: "@rolldown/binding-android-arm64": optional: true @@ -22920,13 +23123,11 @@ __metadata: optional: true "@rolldown/binding-win32-arm64-msvc": optional: true - "@rolldown/binding-win32-ia32-msvc": - optional: true "@rolldown/binding-win32-x64-msvc": optional: true bin: rolldown: bin/cli.mjs - checksum: 10c0/3fdaa36b3bfcdd6913973ef8d785a7e7eeb8c181626ac0d0b8a75aecca2ba3d536ff29a3f5c003f692d7c422e022d0357d7d564ab4aa67cf128230ca137473e8 + checksum: 10c0/363109aa38b31254e682e69aa9f199074d98b823b437faac6d05fd1b4a2b73168b9434043a060fecfc25d3e1d441e2d3b757e92621bc1e843a3e916e2b0d3f58 languageName: node linkType: hard @@ -24478,6 +24679,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.15": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.3" + checksum: 10c0/869c31490d0d88eedb8305d178d4c75e7463e820df5a9b9d388291daf93e8b1eb5de1dad1c1e139767e4269fe75f3b10d5009b2cc14db96ff98986920a186844 + languageName: node + linkType: hard + "tinypool@npm:^1.1.1": version: 1.1.1 resolution: "tinypool@npm:1.1.1" @@ -25590,20 +25801,21 @@ __metadata: languageName: node linkType: hard -"vite@npm:rolldown-vite@7.1.5": - version: 7.1.5 - resolution: "rolldown-vite@npm:7.1.5" +"vite@npm:rolldown-vite@7.3.0": + version: 7.3.0 + resolution: "rolldown-vite@npm:7.3.0" dependencies: + "@oxc-project/runtime": "npm:0.101.0" fdir: "npm:^6.5.0" fsevents: "npm:~2.3.3" - lightningcss: "npm:^1.30.1" + lightningcss: "npm:^1.30.2" picomatch: "npm:^4.0.3" postcss: "npm:^8.5.6" - rolldown: "npm:1.0.0-beta.34" - tinyglobby: "npm:^0.2.14" + rolldown: "npm:1.0.0-beta.53" + tinyglobby: "npm:^0.2.15" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 - esbuild: ^0.25.0 + esbuild: ^0.27.0 jiti: ">=1.21.0" less: ^4.0.0 sass: ^1.70.0 @@ -25641,7 +25853,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/55f6648a8700345700382adac4877208eedcfff5757debba74851227dbc50eae3cc7ccea86bcfda689a9855fbbd2c7e7dd020ffc0c01bfb815dbc6bf65991cbd + checksum: 10c0/7098ba9be029e6530baf6a08e786859910e502e14f18a6fdda856b149fe676ff81d5cb069b8b42f3e88e791fff17f77f9f067c26159fb85a7aab4e4b8692bbb2 languageName: node linkType: hard From 723fa116477f5e36129d6c6e36dc587dadc5d44d Mon Sep 17 00:00:00 2001 From: Shemol Date: Sat, 27 Dec 2025 13:57:33 +0800 Subject: [PATCH 21/24] perf(ModelList): use Map for O(1) model status lookup (#12161) - Replace Array.find() with Map.get() for modelStatus lookup - Add useMemo to create modelStatusMap from modelStatuses array - Stabilize onEditModel callback with useCallback to prevent memo invalidation Fixes #12035 Signed-off-by: SherlockShemol --- .../ProviderSettings/ModelList/ModelList.tsx | 12 ++++++++++-- .../ProviderSettings/ModelList/ModelListGroup.tsx | 7 ++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx index b2455a8ad5..a8eb888813 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx @@ -49,6 +49,9 @@ const ModelList: React.FC = ({ providerId }) => { const { t } = useTranslation() const { provider, models, removeModel } = useProvider(providerId) + // 稳定的编辑模型回调,避免内联函数导致子组件 memo 失效 + const handleEditModel = useCallback((model: Model) => EditModelPopup.show({ provider, model }), [provider]) + const providerConfig = PROVIDER_URLS[provider.id] const docsWebsite = providerConfig?.websites?.docs const modelsWebsite = providerConfig?.websites?.models @@ -63,6 +66,11 @@ const ModelList: React.FC = ({ providerId }) => { const { isChecking: isHealthChecking, modelStatuses, runHealthCheck } = useHealthCheck(provider, models) + // 将 modelStatuses 数组转换为 Map,实现 O(1) 查找 + const modelStatusMap = useMemo(() => { + return new Map(modelStatuses.map((status) => [status.model.id, status])) + }, [modelStatuses]) + const setSearchText = useCallback((text: string) => { startTransition(() => { _setSearchText(text) @@ -138,9 +146,9 @@ const ModelList: React.FC = ({ providerId }) => { key={group} groupName={group} models={displayedModelGroups[group]} - modelStatuses={modelStatuses} + modelStatusMap={modelStatusMap} defaultOpen={i <= 5} - onEditModel={(model) => EditModelPopup.show({ provider, model })} + onEditModel={handleEditModel} onRemoveModel={removeModel} onRemoveGroup={() => displayedModelGroups[group].forEach((model) => removeModel(model))} /> diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelListGroup.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelListGroup.tsx index 0185ef597d..190a49cebd 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelListGroup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelListGroup.tsx @@ -15,7 +15,8 @@ const MAX_SCROLLER_HEIGHT = 390 interface ModelListGroupProps { groupName: string models: Model[] - modelStatuses: ModelWithStatus[] + /** 使用 Map 实现 O(1) 查找,替代原来的数组线性搜索 */ + modelStatusMap: Map defaultOpen: boolean disabled?: boolean onEditModel: (model: Model) => void @@ -26,7 +27,7 @@ interface ModelListGroupProps { const ModelListGroup: React.FC = ({ groupName, models, - modelStatuses, + modelStatusMap, defaultOpen, disabled, onEditModel, @@ -89,7 +90,7 @@ const ModelListGroup: React.FC = ({ {(model) => ( status.model.id === model.id)} + modelStatus={modelStatusMap.get(model.id)} onEdit={onEditModel} onRemove={onRemoveModel} disabled={disabled} From 2008d70707bd66ded6de1d86574dd47aacd35733 Mon Sep 17 00:00:00 2001 From: Shemol Date: Sat, 27 Dec 2025 18:00:20 +0800 Subject: [PATCH 22/24] fix(memory): fix global memory settings submit failure (#12147) --- .../MemorySettings/MemorySettingsModal.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/pages/settings/MemorySettings/MemorySettingsModal.tsx b/src/renderer/src/pages/settings/MemorySettings/MemorySettingsModal.tsx index 509e54e54a..680bfbec8e 100644 --- a/src/renderer/src/pages/settings/MemorySettings/MemorySettingsModal.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/MemorySettingsModal.tsx @@ -3,7 +3,7 @@ import InputEmbeddingDimension from '@renderer/components/InputEmbeddingDimensio import ModelSelector from '@renderer/components/ModelSelector' import { InfoTooltip } from '@renderer/components/TooltipIcons' import { isEmbeddingModel, isRerankModel } from '@renderer/config/models' -import { getModel, useModel } from '@renderer/hooks/useModel' +import { useModel } from '@renderer/hooks/useModel' import { useProviders } from '@renderer/hooks/useProvider' import { getModelUniqId } from '@renderer/services/ModelService' import { selectMemoryConfig, updateMemoryConfig } from '@renderer/store/memory' @@ -55,8 +55,12 @@ const MemorySettingsModal: FC = ({ visible, onSubmit, const handleFormSubmit = async (values: formValue) => { try { // Convert model IDs back to Model objects - const llmModel = getModel(values.llmModel) - const embeddingModel = getModel(values.embeddingModel) + // values.llmModel and values.embeddingModel are JSON strings from getModelUniqId() + // e.g., '{"id":"gpt-4","provider":"openai"}' + // We need to find models by comparing with getModelUniqId() result + const allModels = providers.flatMap((p) => p.models) + const llmModel = allModels.find((m) => getModelUniqId(m) === values.llmModel) + const embeddingModel = allModels.find((m) => getModelUniqId(m) === values.embeddingModel) if (embeddingModel) { setLoading(true) @@ -141,7 +145,9 @@ const MemorySettingsModal: FC = ({ visible, onSubmit, shouldUpdate={(prevValues, currentValues) => prevValues.embeddingModel !== currentValues.embeddingModel}> {({ getFieldValue }) => { const embeddingModelId = getFieldValue('embeddingModel') - const embeddingModel = getModel(embeddingModelId) + // embeddingModelId is a JSON string from getModelUniqId(), find model by comparing + const allModels = providers.flatMap((p) => p.models) + const embeddingModel = allModels.find((m) => getModelUniqId(m) === embeddingModelId) return ( Date: Sat, 27 Dec 2025 20:04:51 +0800 Subject: [PATCH 23/24] fix: shortcut icons sorting disorder (#12151) --- src/renderer/src/pages/home/Inputbar/InputbarTools.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx b/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx index 2dc5ee88a9..1b56b37eb4 100644 --- a/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx +++ b/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx @@ -261,9 +261,12 @@ const InputbarTools = ({ scope, assistantId, session }: InputbarToolsNewProps) = const sourceId = source.droppableId const destinationId = destination.droppableId + const visibleKeys = visibleTools.map((t) => t.key) + const hiddenKeys = hiddenTools.map((t) => t.key) + const newToolOrder: ToolOrderConfig = { - visible: [...toolOrder.visible], - hidden: [...toolOrder.hidden] + visible: [...visibleKeys], + hidden: [...hiddenKeys] } const sourceArray = sourceId === 'inputbar-tools-visible' ? 'visible' : 'hidden' From b78df05f288bf85ad6bd15cf448c2645a7bc9c57 Mon Sep 17 00:00:00 2001 From: Phantom Date: Sun, 28 Dec 2025 15:30:01 +0800 Subject: [PATCH 24/24] fix(AssistantsTab): prevent deleting last assistant and add error message (#12162) feat(AssistantsTab): prevent deleting last assistant and add error message Add validation to prevent deleting the last assistant and show an error message when attempted. Also simplify the active assistant assignment logic when deleting an assistant. --- src/renderer/src/i18n/locales/en-us.json | 3 +++ src/renderer/src/i18n/locales/zh-cn.json | 3 +++ src/renderer/src/i18n/locales/zh-tw.json | 7 +++++-- src/renderer/src/i18n/translate/de-de.json | 7 +++++-- src/renderer/src/i18n/translate/el-gr.json | 7 +++++-- src/renderer/src/i18n/translate/es-es.json | 7 +++++-- src/renderer/src/i18n/translate/fr-fr.json | 7 +++++-- src/renderer/src/i18n/translate/ja-jp.json | 7 +++++-- src/renderer/src/i18n/translate/pt-pt.json | 7 +++++-- src/renderer/src/i18n/translate/ru-ru.json | 7 +++++-- src/renderer/src/pages/home/Tabs/AssistantsTab.tsx | 11 +++++++++-- 11 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 9528b4cd6b..9e60f31f00 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -420,6 +420,9 @@ }, "delete": { "content": "Deleting an assistant will delete all topics and files under the assistant. Are you sure you want to delete it?", + "error": { + "remain_one": "Not allowed to delete the last one assistant" + }, "title": "Delete Assistant" }, "edit": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 524f32c338..b9b07a596c 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -420,6 +420,9 @@ }, "delete": { "content": "删除助手会删除所有该助手下的话题和文件,确定要继续吗?", + "error": { + "remain_one": "不允许删除最后一个助手" + }, "title": "删除助手" }, "edit": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index fe30018ac5..3d613f00f4 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -420,6 +420,9 @@ }, "delete": { "content": "刪除助手會刪除所有該助手下的話題和檔案,確定要繼續嗎?", + "error": { + "remain_one": "不允許刪除最後一個助手" + }, "title": "刪除助手" }, "edit": { @@ -4805,7 +4808,7 @@ "content_limit_tooltip": "限制搜尋結果的內容長度;超過限制的內容將被截斷。", "default_provider": "預設搜尋引擎", "free": "免費", - "is_default": "[to be translated]:Default", + "is_default": "預設", "local_provider": { "hint": "登入網站以獲得更佳搜尋結果並個人化您的搜尋設定。", "open_settings": "開啟 {{provider}} 設定", @@ -4822,7 +4825,7 @@ "search_provider": "搜尋供應商", "search_provider_placeholder": "選擇一個搜尋供應商", "search_with_time": "搜尋包含日期", - "set_as_default": "[to be translated]:Set as Default", + "set_as_default": "設為預設", "subscribe": "黑名單訂閱", "subscribe_add": "新增訂閱", "subscribe_add_failed": "訂閱來源新增失敗", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index e77b9dede1..402437f1e8 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -420,6 +420,9 @@ }, "delete": { "content": "Das Löschen des Assistenten löscht alle Themen und Dateien unter diesem Assistenten. Möchten Sie fortfahren?", + "error": { + "remain_one": "Man darf den letzten Assistenten nicht löschen." + }, "title": "Assistent löschen" }, "edit": { @@ -4805,7 +4808,7 @@ "content_limit_tooltip": "Begrenzen Sie die Länge der Suchergebnisse, überschreitende Inhalte werden abgeschnitten", "default_provider": "Standardanbieter", "free": "Kostenlos", - "is_default": "[to be translated]:Default", + "is_default": "Standard", "local_provider": { "hint": "Melden Sie sich auf der Website an, um bessere Suchergebnisse zu erhalten und Ihre Sucheinstellungen zu personalisieren.", "open_settings": "{{provider}}-Einstellungen öffnen", @@ -4822,7 +4825,7 @@ "search_provider": "Suchanbieter", "search_provider_placeholder": "Einen Suchanbieter auswählen", "search_with_time": "Suche mit Datum", - "set_as_default": "[to be translated]:Set as Default", + "set_as_default": "Als Standard festlegen", "subscribe": "Schwarze Liste-Abonnement", "subscribe_add": "Abonnement hinzufügen", "subscribe_add_failed": "Abonnement-Quelle hinzufügen fehlgeschlagen", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 1593099707..1fb0b08abb 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -420,6 +420,9 @@ }, "delete": { "content": "Η διαγραφή του βοηθού θα διαγράψει όλα τα θέματα και τα αρχεία που είναι συνδεδεμένα με αυτόν. Είστε σίγουροι πως θέλετε να συνεχίσετε;", + "error": { + "remain_one": "Δεν επιτρέπεται η διαγραφή του τελευταίου βοηθού" + }, "title": "Διαγραφή βοηθού" }, "edit": { @@ -4805,7 +4808,7 @@ "content_limit_tooltip": "Περιορίζει το μήκος του περιεχομένου των αποτελεσμάτων αναζήτησης, το περιεχόμενο πέραν του ορίου θα περικοπεί", "default_provider": "Προεπιλεγμένος Πάροχος", "free": "Δωρεάν", - "is_default": "[to be translated]:Default", + "is_default": "Προεπιλογή", "local_provider": { "hint": "Συνδεθείτε στην ιστοσελίδα για να λάβετε καλύτερα αποτελέσματα αναζήτησης και να εξατομικεύσετε τις ρυθμίσεις αναζήτησής σας.", "open_settings": "Άνοιγμα Ρυθμίσεων {{provider}}", @@ -4822,7 +4825,7 @@ "search_provider": "Πάροχος αναζήτησης", "search_provider_placeholder": "Επιλέξτε έναν πάροχο αναζήτησης", "search_with_time": "Αναζήτηση με ημερομηνία", - "set_as_default": "[to be translated]:Set as Default", + "set_as_default": "Ορισμός ως προεπιλογή", "subscribe": "Εγγραφή σε μαύρη λίστα", "subscribe_add": "Προσθήκη εγγραφής", "subscribe_add_failed": "Η προσθήκη της ροής συνδρομής απέτυχε", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 56f06b1b53..1aa78e82dd 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -420,6 +420,9 @@ }, "delete": { "content": "Eliminar el asistente borrará todos los temas y archivos asociados. ¿Está seguro de que desea continuar?", + "error": { + "remain_one": "No se puede eliminar el último asistente" + }, "title": "Eliminar Asistente" }, "edit": { @@ -4805,7 +4808,7 @@ "content_limit_tooltip": "Limita la longitud del contenido en los resultados de búsqueda; el contenido que exceda el límite será truncado", "default_provider": "Proveedor Predeterminado", "free": "Gratis", - "is_default": "[to be translated]:Default", + "is_default": "Por defecto", "local_provider": { "hint": "Inicia sesión en el sitio web para obtener mejores resultados de búsqueda y personalizar tu configuración de búsqueda.", "open_settings": "Abrir configuración de {{provider}}", @@ -4822,7 +4825,7 @@ "search_provider": "Proveedor de búsqueda", "search_provider_placeholder": "Seleccione un proveedor de búsqueda", "search_with_time": "Buscar con fecha", - "set_as_default": "[to be translated]:Set as Default", + "set_as_default": "Establecer como predeterminado", "subscribe": "Suscripción a lista negra", "subscribe_add": "Añadir suscripción", "subscribe_add_failed": "Error al agregar la fuente de suscripción", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 4e8f2ac8e6..4906109228 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -420,6 +420,9 @@ }, "delete": { "content": "La suppression de l'aide supprimera tous les sujets et fichiers sous l'aide. Êtes-vous sûr de vouloir la supprimer ?", + "error": { + "remain_one": "Interdiction de supprimer le dernier assistant" + }, "title": "Supprimer l'Aide" }, "edit": { @@ -4805,7 +4808,7 @@ "content_limit_tooltip": "Limiter la longueur du contenu des résultats de recherche ; le contenu dépassant cette limite sera tronqué", "default_provider": "Fournisseur par défaut", "free": "Gratuit", - "is_default": "[to be translated]:Default", + "is_default": "Défaut", "local_provider": { "hint": "Connectez-vous au site Web pour obtenir de meilleurs résultats de recherche et personnaliser vos paramètres de recherche.", "open_settings": "Ouvrir les paramètres de {{provider}}", @@ -4822,7 +4825,7 @@ "search_provider": "Fournisseur de recherche", "search_provider_placeholder": "Sélectionnez un fournisseur de recherche", "search_with_time": "Rechercher avec date", - "set_as_default": "[to be translated]:Set as Default", + "set_as_default": "Définir par défaut", "subscribe": "Abonnement à la liste noire", "subscribe_add": "Ajouter un abonnement", "subscribe_add_failed": "Échec de l'ajout de la source d'abonnement", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 58ee184061..950fef7130 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -420,6 +420,9 @@ }, "delete": { "content": "アシスタントを削除すると、そのアシスタントのすべてのトピックとファイルが削除されます。削除しますか?", + "error": { + "remain_one": "最後の1人のアシスタントは削除できません" + }, "title": "アシスタントを削除" }, "edit": { @@ -4805,7 +4808,7 @@ "content_limit_tooltip": "検索結果のコンテンツの長さを制限します。制限を超えるコンテンツは切り捨てられます。", "default_provider": "デフォルトプロバイダー", "free": "無料", - "is_default": "[to be translated]:Default", + "is_default": "デフォルト", "local_provider": { "hint": "ウェブサイトにログインして、より良い検索結果を得て、検索設定をパーソナライズしてください。", "open_settings": "{{provider}}設定を開く", @@ -4822,7 +4825,7 @@ "search_provider": "検索サービスプロバイダー", "search_provider_placeholder": "検索サービスプロバイダーを選択する", "search_with_time": "日付を含む検索", - "set_as_default": "[to be translated]:Set as Default", + "set_as_default": "既定として設定", "subscribe": "ブラックリスト購読", "subscribe_add": "購読を追加", "subscribe_add_failed": "購読ソースの追加に失敗しました", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 553795f6b3..73c8e28e4d 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -420,6 +420,9 @@ }, "delete": { "content": "Excluir o assistente removerá todos os tópicos e arquivos sob esse assistente. Tem certeza de que deseja continuar?", + "error": { + "remain_one": "Não é permitido apagar o último assistente." + }, "title": "Excluir Assistente" }, "edit": { @@ -4805,7 +4808,7 @@ "content_limit_tooltip": "Limita o comprimento do conteúdo dos resultados de pesquisa; o conteúdo excedente será truncado", "default_provider": "Provedor Padrão", "free": "Grátis", - "is_default": "[to be translated]:Default", + "is_default": "Padrão", "local_provider": { "hint": "Faça login no site para obter melhores resultados de pesquisa e personalizar suas configurações de busca.", "open_settings": "Abrir Configurações do {{provider}}", @@ -4822,7 +4825,7 @@ "search_provider": "Provedor de pesquisa", "search_provider_placeholder": "Selecione um provedor de pesquisa", "search_with_time": "Pesquisar com data", - "set_as_default": "[to be translated]:Set as Default", + "set_as_default": "Definir como Padrão", "subscribe": "Assinatura de lista negra", "subscribe_add": "Adicionar assinatura", "subscribe_add_failed": "Falha ao adicionar a fonte de subscrição", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 489e8b4695..200b03e6c1 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -420,6 +420,9 @@ }, "delete": { "content": "Удаление ассистента удалит все топики и файлы под ассистентом. Вы уверены, что хотите удалить его?", + "error": { + "remain_one": "Нельзя удалить последнего помощника" + }, "title": "Удалить ассистента" }, "edit": { @@ -4805,7 +4808,7 @@ "content_limit_tooltip": "Ограничить длину контента в результатах поиска; контент, превышающий лимит, будет усечен.", "default_provider": "Поставщик по умолчанию", "free": "Бесплатно", - "is_default": "[to be translated]:Default", + "is_default": "По умолчанию", "local_provider": { "hint": "Войдите на сайт, чтобы получать более точные результаты поиска и настроить параметры поиска под себя.", "open_settings": "Открыть настройки {{provider}}", @@ -4822,7 +4825,7 @@ "search_provider": "поиск сервисного провайдера", "search_provider_placeholder": "Выберите поставщика поисковых услуг", "search_with_time": "Поиск, содержащий дату", - "set_as_default": "[to be translated]:Set as Default", + "set_as_default": "Установить по умолчанию", "subscribe": "Подписка на черный список", "subscribe_add": "Добавить подписку", "subscribe_add_failed": "Не удалось добавить источник подписки", diff --git a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx index 3c0c9cf802..79d7f64d7a 100644 --- a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx @@ -9,6 +9,7 @@ import { useTags } from '@renderer/hooks/useTags' import type { Assistant, AssistantsSortType, Topic } from '@renderer/types' import type { FC } from 'react' import { useCallback, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' import styled from 'styled-components' import UnifiedAddButton from './components/UnifiedAddButton' @@ -32,6 +33,7 @@ const AssistantsTab: FC = (props) => { const { apiServerConfig } = useApiServer() const apiServerEnabled = apiServerConfig.enabled const { chat } = useRuntime() + const { t } = useTranslation() // Agent related hooks const { agents, deleteAgent, isLoading: agentsLoading, error: agentsError } = useAgents() @@ -75,13 +77,18 @@ const AssistantsTab: FC = (props) => { const onDeleteAssistant = useCallback( (assistant: Assistant) => { const remaining = assistants.filter((a) => a.id !== assistant.id) + if (remaining.length === 0) { + window.toast.error(t('assistants.delete.error.remain_one')) + return + } + if (assistant.id === activeAssistant?.id) { const newActive = remaining[remaining.length - 1] - newActive ? setActiveAssistant(newActive) : onCreateDefaultAssistant() + setActiveAssistant(newActive) } removeAssistant(assistant.id) }, - [activeAssistant, assistants, removeAssistant, setActiveAssistant, onCreateDefaultAssistant] + [assistants, activeAssistant?.id, removeAssistant, t, setActiveAssistant] ) const handleSortByChange = useCallback(