Merge remote-tracking branch 'origin/main' into migrate/v6

This commit is contained in:
suyao 2025-12-28 17:06:29 +08:00
commit 9b1165e073
No known key found for this signature in database
84 changed files with 2153 additions and 806 deletions

View File

@ -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()

View File

@ -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<z.ZodString>;
reasoningEffort: z.ZodOptional<z.ZodString>;
textVerbosity: z.ZodOptional<z.ZodString>;
+ sendReasoning: z.ZodOptional<z.ZodBoolean>;
}, z.core.$strip>;
type OpenAICompatibleProviderOptions = z.infer<typeof openaiCompatibleProviderOptions>;
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()

View File

@ -36,7 +36,7 @@ yarn install
### ENV
```bash
copy .env.example .env
cp .env.example .env
```
### Start

View File

@ -36,7 +36,7 @@ yarn install
### ENV
```bash
copy .env.example .env
cp .env.example .env
```
### Start

View File

@ -134,38 +134,68 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
<!--LANG:en-->
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
<!--LANG:zh-CN-->
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 对支持的服务商和模型的验证
<!--LANG:END-->

View File

@ -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: {

View File

@ -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",
@ -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",
@ -416,7 +417,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": {

View File

@ -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',

View File

@ -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, '')
}

View File

@ -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()
})

View File

@ -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 {
@ -686,36 +685,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) => {
@ -875,8 +857,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)

View File

@ -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<FileMetadata | null> => {
private findDuplicateFile = async (filePath: string): Promise<FileMetadata | null> => {
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<FileTypes> => {
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<boolean> => {
return this._isTextFile(filePath)
}
private _isTextFile = async (filePath: string): Promise<boolean> => {
try {
const isBinary = await isBinaryFile(filePath)
if (isBinary) {

View File

@ -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()

View File

@ -14,38 +14,36 @@ export class SearchService {
return SearchService.instance
}
constructor() {
// Initialize the service
}
private async createNewSearchWindow(uid: string): Promise<BrowserWindow> {
private async createNewSearchWindow(uid: string, show: boolean = false): Promise<BrowserWindow> {
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<void> {
await this.createNewSearchWindow(uid)
public async openSearchWindow(uid: string, show: boolean = false): Promise<void> {
const existingWindow = this.searchWindows[uid]
if (existingWindow) {
show && existingWindow.show()
return
}
await this.createNewSearchWindow(uid, show)
}
public async closeSearchWindow(uid: string): Promise<void> {

View File

@ -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 }
}
}
/**

View File

@ -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,

View File

@ -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<number[]> {
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()
}

View File

@ -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) =>
@ -441,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)
},

View File

@ -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'

View File

@ -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'

View File

@ -24,7 +24,8 @@ export const memorySearchTool = () => {
}
const memoryConfig = selectMemoryConfig(store.getState())
if (!memoryConfig.llmApiClient || !memoryConfig.embedderApiClient) {
if (!memoryConfig.llmModel || !memoryConfig.embeddingModel) {
return []
}

View File

@ -464,7 +464,8 @@ describe('options utils', () => {
custom_param: 'custom_value',
another_param: 123,
serviceTier: undefined,
textVerbosity: undefined
textVerbosity: undefined,
store: false
}
})
})

View File

@ -10,6 +10,7 @@ import {
isAnthropicModel,
isGeminiModel,
isGrokModel,
isInterleavedThinkingModel,
isOpenAIModel,
isOpenAIOpenWeightModel,
isQwenMTModel,
@ -396,10 +397,12 @@ function buildOpenAIProviderOptions(
}
}
// TODO: 支持配置是否在服务端持久化
providerOptions = {
...providerOptions,
serviceTier,
textVerbosity
textVerbosity,
store: false
}
return {
@ -601,7 +604,7 @@ function buildGenericProviderOptions(
enableGenerateImage: boolean
}
): Record<string, any> {
const { enableWebSearch } = capabilities
const { enableWebSearch, enableReasoning } = capabilities
let providerOptions: Record<string, any> = {}
const reasoningParams = getReasoningEffort(assistant, model)
@ -609,6 +612,14 @@ function buildGenericProviderOptions(
...providerOptions,
...reasoningParams
}
if (enableReasoning) {
if (isInterleavedThinkingModel(model)) {
providerOptions = {
...providerOptions,
sendReasoning: true
}
}
}
if (enableWebSearch) {
const webSearchParams = getWebSearchParams(model)

View File

@ -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'
}

View File

@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Baidu</title><path d="M8.859 11.735c1.017-1.71 4.059-3.083 6.202.286 1.579 2.284 4.284 4.397 4.284 4.397s2.027 1.601.73 4.684c-1.24 2.956-5.64 1.607-6.005 1.49l-.024-.009s-1.746-.568-3.776-.112c-2.026.458-3.773.286-3.773.286l-.045-.001c-.328-.01-2.38-.187-3.001-2.968-.675-3.028 2.365-4.687 2.592-4.968.226-.288 1.802-1.37 2.816-3.085zm.986 1.738v2.032h-1.64s-1.64.138-2.213 2.014c-.2 1.252.177 1.99.242 2.148.067.157.596 1.073 1.927 1.342h3.078v-7.514l-1.394-.022zm3.588 2.191l-1.44.024v3.956s.064.985 1.44 1.344h3.541v-5.3h-1.528v3.979h-1.46s-.466-.068-.553-.447v-3.556zM9.82 16.715v3.06H8.58s-.863-.045-1.126-1.049c-.136-.445.02-.959.088-1.16.063-.203.353-.671.951-.85H9.82zm9.525-9.036c2.086 0 2.646 2.06 2.646 2.742 0 .688.284 3.597-2.309 3.655-2.595.057-2.704-1.77-2.704-3.08 0-1.374.277-3.317 2.367-3.317zM4.24 6.08c1.523-.135 2.645 1.55 2.762 2.513.07.625.393 3.486-1.975 4-2.364.515-3.244-2.249-2.984-3.544 0 0 .28-2.797 2.197-2.969zm8.847-1.483c.14-1.31 1.69-3.316 2.931-3.028 1.236.285 2.367 1.944 2.137 3.37-.224 1.428-1.345 3.313-3.095 3.082-1.748-.226-2.143-1.823-1.973-3.424zM9.425 1c1.307 0 2.364 1.519 2.364 3.398 0 1.879-1.057 3.4-2.364 3.4s-2.367-1.521-2.367-3.4C7.058 2.518 8.118 1 9.425 1z" fill="#2932E1" fill-rule="nonzero"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Bing</title><path d="M11.97 7.569a.92.92 0 00-.805.863c-.013.195-.01.209.43 1.347 1 2.59 1.242 3.214 1.283 3.302.099.213.237.413.41.592.134.138.222.212.37.311.26.176.39.224 1.405.527.989.295 1.529.49 1.994.723.603.302 1.024.644 1.29 1.051.191.292.36.815.434 1.342.029.206.029.661 0 .847a2.491 2.491 0 01-.376 1.026c-.1.151-.065.126.081-.058.415-.52.838-1.408 1.054-2.213a6.728 6.728 0 00.102-3.012 6.626 6.626 0 00-3.291-4.53 104.157 104.157 0 00-1.322-.698l-.254-.133a737.941 737.941 0 01-1.575-.827c-.548-.29-.78-.406-.846-.426a1.376 1.376 0 00-.29-.045l-.093.01z" fill="url(#lobe-icons-bing-fill-0)"></path><path d="M13.164 17.24a4.385 4.385 0 00-.202.125 511.45 511.45 0 00-1.795 1.115 163.087 163.087 0 01-.989.614l-.463.288a99.198 99.198 0 01-1.502.941c-.326.2-.704.334-1.09.387-.18.024-.52.024-.7 0a2.807 2.807 0 01-1.318-.538 3.665 3.665 0 01-.543-.545 2.837 2.837 0 01-.506-1.141 2.161 2.161 0 00-.041-.182c-.008-.008.006.138.032.33.027.199.085.487.147.733.482 1.907 1.85 3.457 3.705 4.195a6.31 6.31 0 001.658.412c.22.025.844.035 1.074.017 1.054-.08 1.972-.393 2.913-.992a325.28 325.28 0 01.937-.596l.384-.244.684-.435.234-.149.009-.005.025-.017.013-.007.172-.11.597-.38c.76-.481.987-.65 1.34-.998.148-.146.37-.394.381-.425.002-.007.042-.068.088-.136a2.49 2.49 0 00.373-1.023 4.181 4.181 0 000-.847 4.336 4.336 0 00-.318-1.137c-.224-.472-.7-.9-1.383-1.245a2.972 2.972 0 00-.406-.181c-.01 0-.646.392-1.413.87a7089.171 7089.171 0 00-1.658 1.031l-.439.274z" fill="url(#lobe-icons-bing-fill-1)" fill-rule="nonzero"></path><path d="M4.003 14.946l.004 3.33.042.193c.134.604.366 1.04.77 1.445a2.701 2.701 0 001.955.814c.536 0 1-.135 1.479-.43l.703-.435.556-.346V8.003c0-2.306-.004-3.675-.012-3.782a2.734 2.734 0 00-.797-1.765c-.145-.144-.268-.24-.637-.496A1780.102 1780.102 0 015.762.362C5.406.115 5.38.098 5.271.059a.943.943 0 00-1.254.696C4.003.818 4 1.659 4 6.223v5.394H4l.003 3.329z" fill="url(#lobe-icons-bing-fill-2)" fill-rule="nonzero"></path><defs><radialGradient cx="93.717%" cy="77.818%" fx="93.717%" fy="77.818%" gradientTransform="scale(-1 -.7146) rotate(49.288 2.035 -2.198)" id="lobe-icons-bing-fill-0" r="143.691%"><stop offset="0%" stop-color="#00CACC"></stop><stop offset="100%" stop-color="#048FCE"></stop></radialGradient><radialGradient cx="13.893%" cy="71.448%" fx="13.893%" fy="71.448%" gradientTransform="scale(.6042 1) rotate(-23.34 .184 .494)" id="lobe-icons-bing-fill-1" r="149.21%"><stop offset="0%" stop-color="#00BBEC"></stop><stop offset="100%" stop-color="#2756A9"></stop></radialGradient><linearGradient id="lobe-icons-bing-fill-2" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#00BBEC"></stop><stop offset="100%" stop-color="#2756A9"></stop></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Google</title><path d="M23 12.245c0-.905-.075-1.565-.236-2.25h-10.54v4.083h6.186c-.124 1.014-.797 2.542-2.294 3.569l-.021.136 3.332 2.53.23.022C21.779 18.417 23 15.593 23 12.245z" fill="#4285F4"></path><path d="M12.225 23c3.03 0 5.574-.978 7.433-2.665l-3.542-2.688c-.948.648-2.22 1.1-3.891 1.1a6.745 6.745 0 01-6.386-4.572l-.132.011-3.465 2.628-.045.124C4.043 20.531 7.835 23 12.225 23z" fill="#34A853"></path><path d="M5.84 14.175A6.65 6.65 0 015.463 12c0-.758.138-1.491.361-2.175l-.006-.147-3.508-2.67-.115.054A10.831 10.831 0 001 12c0 1.772.436 3.447 1.197 4.938l3.642-2.763z" fill="#FBBC05"></path><path d="M12.225 5.253c2.108 0 3.529.892 4.34 1.638l3.167-3.031C17.787 2.088 15.255 1 12.225 1 7.834 1 4.043 3.469 2.197 7.062l3.63 2.763a6.77 6.77 0 016.398-4.572z" fill="#EB4335"></path></svg>

After

Width:  |  Height:  |  Size: 920 B

View File

@ -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 <NotepadText size={14} />
case 'knowledge':
return <FileSearch size={14} />
case 'mcp':
return <McpLogo width={14} height={14} />
case 'files':
return <Folder size={14} />
case 'settings':

View File

@ -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> = {}): 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)
})
})
})
})

View File

@ -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)
})
})
})

View File

@ -617,6 +617,12 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
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<SystemProviderId | 'defaultModel', Model[]> =
provider: 'minimax',
name: 'MiniMax M2 Stable',
group: 'minimax-m2'
},
{
id: 'MiniMax-M2.1',
provider: 'minimax',
name: 'MiniMax M2.1',
group: 'minimax-m2'
}
],
hyperbolic: [

View File

@ -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

View File

@ -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 {
@ -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)
}

View File

@ -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

View File

@ -107,7 +107,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
type: 'openai',
apiKey: '',
apiHost: 'https://aihubmix.com',
anthropicApiHost: 'https://aihubmix.com/anthropic',
anthropicApiHost: 'https://aihubmix.com',
models: SYSTEM_MODELS.aihubmix,
isSystem: true,
enabled: false
@ -289,7 +289,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
ollama: {
id: 'ollama',
name: 'Ollama',
type: 'openai',
type: 'ollama',
apiKey: '',
apiHost: 'http://localhost:11434',
models: SYSTEM_MODELS.ollama,

View File

@ -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(() => {

View File

@ -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": {
@ -4756,6 +4759,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 +4806,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 +4825,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",

View File

@ -420,6 +420,9 @@
},
"delete": {
"content": "删除助手会删除所有该助手下的话题和文件,确定要继续吗?",
"error": {
"remain_one": "不允许删除最后一个助手"
},
"title": "删除助手"
},
"edit": {
@ -4756,6 +4759,12 @@
},
"title": "其他设置",
"websearch": {
"api_key_required": {
"content": "{{provider}} 需要 API 密钥才能使用。是否现在去配置?",
"ok": "去配置",
"title": "需要 API 密钥"
},
"api_providers": "API 服务商",
"apikey": "API 密钥",
"blacklist": "黑名单",
"blacklist_description": "在搜索结果中不会出现以下网站的结果",
@ -4797,7 +4806,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 +4825,7 @@
"search_provider": "搜索服务商",
"search_provider_placeholder": "选择一个搜索服务商",
"search_with_time": "搜索包含日期",
"set_as_default": "设为默认",
"subscribe": "黑名单订阅",
"subscribe_add": "添加订阅",
"subscribe_add_failed": "订阅源添加失败",

View File

@ -420,6 +420,9 @@
},
"delete": {
"content": "刪除助手會刪除所有該助手下的話題和檔案,確定要繼續嗎?",
"error": {
"remain_one": "不允許刪除最後一個助手"
},
"title": "刪除助手"
},
"edit": {
@ -4756,6 +4759,12 @@
},
"title": "其他設定",
"websearch": {
"api_key_required": {
"content": "{{provider}} 需要 API 金鑰才能運作。您現在要設定嗎?",
"ok": "設定",
"title": "需要 API 金鑰"
},
"api_providers": "API 服務商",
"apikey": "API 金鑰",
"blacklist": "黑名單",
"blacklist_description": "以下網站不會出現在搜尋結果中",
@ -4797,7 +4806,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": "強制使用搜尋服務而不是 LLM",
@ -4808,6 +4825,7 @@
"search_provider": "搜尋供應商",
"search_provider_placeholder": "選擇一個搜尋供應商",
"search_with_time": "搜尋包含日期",
"set_as_default": "設為預設",
"subscribe": "黑名單訂閱",
"subscribe_add": "新增訂閱",
"subscribe_add_failed": "訂閱來源新增失敗",

View File

@ -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": {
@ -4756,6 +4759,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 +4806,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": "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",
"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 +4825,7 @@
"search_provider": "Suchanbieter",
"search_provider_placeholder": "Einen Suchanbieter auswählen",
"search_with_time": "Suche mit Datum",
"set_as_default": "Als Standard festlegen",
"subscribe": "Schwarze Liste-Abonnement",
"subscribe_add": "Abonnement hinzufügen",
"subscribe_add_failed": "Abonnement-Quelle hinzufügen fehlgeschlagen",

View File

@ -420,6 +420,9 @@
},
"delete": {
"content": "Η διαγραφή του βοηθού θα διαγράψει όλα τα θέματα και τα αρχεία που είναι συνδεδεμένα με αυτόν. Είστε σίγουροι πως θέλετε να συνεχίσετε;",
"error": {
"remain_one": "Δεν επιτρέπεται η διαγραφή του τελευταίου βοηθού"
},
"title": "Διαγραφή βοηθού"
},
"edit": {
@ -4756,6 +4759,12 @@
},
"title": "Ρυθμίσεις Εργαλείων",
"websearch": {
"api_key_required": {
"content": "Ο {{provider}} απαιτεί κλειδί API για να λειτουργήσει. Θα θέλατε να το διαμορφώσετε τώρα;",
"ok": "Ρυθμίστε",
"title": "Απαιτείται κλειδί API"
},
"api_providers": "Πάροχοι API",
"apikey": "Κλειδί API",
"blacklist": "Μαύρη Λίστα",
"blacklist_description": "Τα αποτελέσματα από τους παρακάτω ιστότοπους δεν θα εμφανίζονται στα αποτελέσματα αναζήτησης",
@ -4797,7 +4806,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 +4825,7 @@
"search_provider": "Πάροχος αναζήτησης",
"search_provider_placeholder": "Επιλέξτε έναν πάροχο αναζήτησης",
"search_with_time": "Αναζήτηση με ημερομηνία",
"set_as_default": "Ορισμός ως προεπιλογή",
"subscribe": "Εγγραφή σε μαύρη λίστα",
"subscribe_add": "Προσθήκη εγγραφής",
"subscribe_add_failed": "Η προσθήκη της ροής συνδρομής απέτυχε",

View File

@ -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": {
@ -4756,6 +4759,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 +4806,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": "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}}",
"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 +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": "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",

View File

@ -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": {
@ -4756,6 +4759,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 +4806,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": "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}}",
"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 +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": "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",

View File

@ -420,6 +420,9 @@
},
"delete": {
"content": "アシスタントを削除すると、そのアシスタントのすべてのトピックとファイルが削除されます。削除しますか?",
"error": {
"remain_one": "最後の1人のアシスタントは削除できません"
},
"title": "アシスタントを削除"
},
"edit": {
@ -4756,6 +4759,12 @@
},
"title": "その他の設定",
"websearch": {
"api_key_required": {
"content": "{{provider}}はAPIキーが必要です。今すぐ設定しますか",
"ok": "設定",
"title": "APIキーが必要"
},
"api_providers": "APIプロバイダー",
"apikey": "APIキー",
"blacklist": "ブラックリスト",
"blacklist_description": "以下のウェブサイトの結果は検索結果に表示されません",
@ -4797,7 +4806,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": "LLMの代わりに検索サービスを強制的に使用する",
@ -4808,6 +4825,7 @@
"search_provider": "検索サービスプロバイダー",
"search_provider_placeholder": "検索サービスプロバイダーを選択する",
"search_with_time": "日付を含む検索",
"set_as_default": "既定として設定",
"subscribe": "ブラックリスト購読",
"subscribe_add": "購読を追加",
"subscribe_add_failed": "購読ソースの追加に失敗しました",

View File

@ -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."
},
@ -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": {
@ -2198,7 +2201,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 +4759,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 +4806,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": "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}}",
"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 +4825,7 @@
"search_provider": "Provedor de pesquisa",
"search_provider_placeholder": "Selecione um provedor de pesquisa",
"search_with_time": "Pesquisar com data",
"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",

View File

@ -420,6 +420,9 @@
},
"delete": {
"content": "Удаление ассистента удалит все топики и файлы под ассистентом. Вы уверены, что хотите удалить его?",
"error": {
"remain_one": "Нельзя удалить последнего помощника"
},
"title": "Удалить ассистента"
},
"edit": {
@ -4756,6 +4759,12 @@
},
"title": "Другие настройки",
"websearch": {
"api_key_required": {
"content": "{{provider}} требует API-ключ для работы. Хотите настроить его сейчас?",
"ok": "Настроить",
"title": "Требуется ключ API"
},
"api_providers": "Поставщики API",
"apikey": "API ключ",
"blacklist": "Черный список",
"blacklist_description": "Результаты из следующих веб-сайтов не будут отображаться в результатах поиска",
@ -4797,7 +4806,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": "Принудительно использовать поисковый сервис вместо LLM",
@ -4808,6 +4825,7 @@
"search_provider": "поиск сервисного провайдера",
"search_provider_placeholder": "Выберите поставщика поисковых услуг",
"search_with_time": "Поиск, содержащий дату",
"set_as_default": "Установить по умолчанию",
"subscribe": "Подписка на черный список",
"subscribe_add": "Добавить подписку",
"subscribe_add_failed": "Не удалось добавить источник подписки",

View File

@ -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'

View File

@ -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<AssistantsTabProps> = (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<AssistantsTabProps> = (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(

View File

@ -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: <NotesSettings />,
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 {

View File

@ -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<NotesSettings>) => 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
}
]

View File

@ -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 (
<SettingContainer theme={theme}>
<SettingContainer theme={theme} style={{ background: 'transparent' }}>
<SettingGroup theme={theme}>
<SettingTitle>{t('notes.settings.data.title')}</SettingTitle>
<SettingDivider />

View File

@ -412,7 +412,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
</DynamicVirtualList>
</Dropdown>
{!isShowStarred && !isShowSearch && (
<div style={{ padding: '0 8px', marginTop: '6px', marginBottom: '20px' }}>
<div style={{ padding: '0 8px', marginTop: '6px', marginBottom: '12px' }}>
<TreeNode
node={{
id: 'hint-node',

View File

@ -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) {

View File

@ -84,7 +84,7 @@ export const MODEOPTIONS = [
// 获取模型分组数据
export const GetModelGroup = async (): Promise<DMXApiModelGroups> => {
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()

View File

@ -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<Props> = ({ 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<Props> = ({ assistant, updateAssistant,
<Text strong>{t('memory.stored_memories')}: </Text>
<Text>{memoryStats.loading ? t('common.loading') : memoryStats.count}</Text>
</div>
{memoryConfig.embedderApiClient && (
{memoryConfig.embeddingModel && (
<div>
<Text strong>{t('memory.embedding_model')}: </Text>
<Text code>{memoryConfig.embedderApiClient.model}</Text>
<Text code>{memoryConfig.embeddingModel.id}</Text>
</div>
)}
{memoryConfig.llmApiClient && (
{memoryConfig.llmModel && (
<div>
<Text strong>{t('memory.llm_model')}: </Text>
<Text code>{memoryConfig.llmApiClient.model}</Text>
<Text code>{memoryConfig.llmModel.id}</Text>
</div>
)}
</Space>

View File

@ -41,7 +41,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
const [customParameters, setCustomParameters] = useState<AssistantSettingCustomParameters[]>(
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<Props> = ({ 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)

View File

@ -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) => (
<Tooltip title={font} placement="left" mouseEnterDelay={0.5}>
<div
className="truncate"
style={{
fontFamily: font
}}>
{font}
</div>
</Tooltip>
),
[]
)
return (
<SettingContainer theme={theme}>
<SettingGroup theme={theme}>
@ -292,7 +307,7 @@ const DisplaySettings: FC = () => {
<SettingRowTitle>{t('settings.display.font.global')}</SettingRowTitle>
<SelectRow>
<Select
style={{ width: 200 }}
style={{ width: 280 }}
placeholder={t('settings.display.font.select')}
options={[
{
@ -303,7 +318,7 @@ const DisplaySettings: FC = () => {
),
value: ''
},
...fontList.map((font) => ({ label: <span style={{ fontFamily: font }}>{font}</span>, value: font }))
...fontList.map((font) => ({ label: renderFontOption(font), value: font }))
]}
value={userTheme.userFontFamily || ''}
onChange={(font) => handleUserFontChange(font)}
@ -324,7 +339,7 @@ const DisplaySettings: FC = () => {
<SettingRowTitle>{t('settings.display.font.code')}</SettingRowTitle>
<SelectRow>
<Select
style={{ width: 200 }}
style={{ width: 280 }}
placeholder={t('settings.display.font.select')}
options={[
{
@ -335,7 +350,7 @@ const DisplaySettings: FC = () => {
),
value: ''
},
...fontList.map((font) => ({ label: <span style={{ fontFamily: font }}>{font}</span>, 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

View File

@ -137,7 +137,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool, onToggleAutoApprove }: M
{
title: (
<Flex align="center" justify="center" gap={4}>
<McpLogo width={14} height={14} />
<McpLogo width={14} height={14} style={{ opacity: 0.8 }} />
<Typography.Text strong>{t('settings.mcp.tools.enable')}</Typography.Text>
</Flex>
),

View File

@ -86,7 +86,7 @@ const MCPSettings: FC = () => {
title={t('settings.mcp.servers', 'MCP Servers')}
active={activeView === 'servers'}
onClick={() => navigate('/settings/mcp/servers')}
icon={<McpLogo width={18} height={18} />}
icon={<McpLogo width={18} height={18} style={{ opacity: 0.8 }} />}
titleStyle={{ fontWeight: 500 }}
/>
<DividerWithText text={t('settings.mcp.discover', 'Discover')} style={{ margin: '10px 0 8px 0' }} />

View File

@ -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<EditMemoryModalProps> = ({ 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={[
<Button key="cancel" size="large" onClick={onCancel}>
{t('common.cancel')}
</Button>,
<Button key="submit" type="primary" size="large" loading={loading} onClick={() => form.submit()}>
{t('common.save')}
</Button>
]}>
}}>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item
label={t('memory.memory_content')}
@ -548,10 +542,10 @@ const MemorySettings = () => {
}
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]}
/>
<MemoriesSettingsModal
<MemorySettingsModal
visible={settingsModalVisible}
onSubmit={async () => await handleSettingsSubmit()}
onCancel={handleSettingsCancel}

View File

@ -1,5 +1,4 @@
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'
@ -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,61 @@ interface MemoriesSettingsModalProps {
type formValue = {
llmModel: string
embedderModel: string
embedderDimensions: number
embeddingModel: string
embeddingDimensions: number
}
const MemoriesSettingsModal: FC<MemoriesSettingsModalProps> = ({ visible, onSubmit, onCancel, form }) => {
const MemorySettingsModal: FC<MemorySettingsModalProps> = ({ 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) {
// 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)
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 +132,7 @@ const MemoriesSettingsModal: FC<MemoriesSettingsModalProps> = ({ visible, onSubm
</Form.Item>
<Form.Item
label={t('memory.embedding_model')}
name="embedderModel"
name="embeddingModel"
rules={[{ required: true, message: t('memory.please_select_embedding_model') }]}>
<ModelSelector
providers={providers}
@ -160,10 +142,12 @@ const MemoriesSettingsModal: FC<MemoriesSettingsModalProps> = ({ visible, onSubm
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => prevValues.embedderModel !== currentValues.embedderModel}>
shouldUpdate={(prevValues, currentValues) => prevValues.embeddingModel !== currentValues.embeddingModel}>
{({ getFieldValue }) => {
const embedderModelId = getFieldValue('embedderModel')
const embedderModel = findModelById(embedderModelId)
const embeddingModelId = getFieldValue('embeddingModel')
// 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 (
<Form.Item
label={
@ -172,7 +156,7 @@ const MemoriesSettingsModal: FC<MemoriesSettingsModalProps> = ({ visible, onSubm
<InfoTooltip title={t('knowledge.dimensions_size_tooltip')} />
</Flex>
}
name="embedderDimensions"
name="embeddingDimensions"
rules={[
{
validator(_, value) {
@ -183,7 +167,7 @@ const MemoriesSettingsModal: FC<MemoriesSettingsModalProps> = ({ visible, onSubm
}
}
]}>
<InputEmbeddingDimension model={embedderModel} disabled={!embedderModel} />
<InputEmbeddingDimension model={embeddingModel} disabled={!embeddingModel} />
</Form.Item>
)
}}
@ -199,4 +183,4 @@ const MemoriesSettingsModal: FC<MemoriesSettingsModalProps> = ({ visible, onSubm
)
}
export default MemoriesSettingsModal
export default MemorySettingsModal

View File

@ -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<UserSelectorProps> = ({ 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 (
<HStack alignItems="center" gap={10}>
<Avatar size={20} style={{ background: 'var(--color-primary)' }}>
{getUserAvatar(userId)}
</Avatar>
<span>{userName}</span>
</HStack>
)
},
[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 (
<Space.Compact>

View File

@ -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 }
})
}

View File

@ -49,6 +49,9 @@ const ModelList: React.FC<ModelListProps> = ({ 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<ModelListProps> = ({ 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<ModelListProps> = ({ 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))}
/>

View File

@ -15,7 +15,8 @@ const MAX_SCROLLER_HEIGHT = 390
interface ModelListGroupProps {
groupName: string
models: Model[]
modelStatuses: ModelWithStatus[]
/** 使用 Map 实现 O(1) 查找,替代原来的数组线性搜索 */
modelStatusMap: Map<string, ModelWithStatus>
defaultOpen: boolean
disabled?: boolean
onEditModel: (model: Model) => void
@ -26,7 +27,7 @@ interface ModelListGroupProps {
const ModelListGroup: React.FC<ModelListGroupProps> = ({
groupName,
models,
modelStatuses,
modelStatusMap,
defaultOpen,
disabled,
onEditModel,
@ -89,7 +90,7 @@ const ModelListGroup: React.FC<ModelListGroupProps> = ({
{(model) => (
<ModelListItem
model={model}
modelStatus={modelStatuses.find((status) => status.model.id === model.id)}
modelStatus={modelStatusMap.get(model.id)}
onEdit={onEditModel}
onRemove={onRemoveModel}
disabled={disabled}

View File

@ -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'
@ -12,9 +11,9 @@ import {
HardDrive,
Info,
MonitorCog,
NotebookPen,
Package,
PictureInPicture2,
Search,
Server,
Settings2,
TextCursorInput,
@ -32,7 +31,6 @@ import DocProcessSettings from './DocProcessSettings'
import GeneralSettings from './GeneralSettings'
import MCPSettings from './MCPSettings'
import MemorySettings from './MemorySettings'
import NotesSettings from './NotesSettings'
import { ProviderList } from './ProviderSettings'
import QuickAssistantSettings from './QuickAssistantSettings'
import QuickPhraseSettings from './QuickPhraseSettings'
@ -88,19 +86,13 @@ const SettingsPage: FC = () => {
<Divider />
<MenuItemLink to="/settings/mcp">
<MenuItem className={isRoute('/settings/mcp')}>
<McpLogo width={18} height={18} />
<McpLogo width={18} height={18} style={{ opacity: 0.8 }} />
{t('settings.mcp.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/notes">
<MenuItem className={isRoute('/settings/notes')}>
<NotebookPen size={18} />
{t('notes.settings.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/websearch">
<MenuItem className={isRoute('/settings/websearch')}>
<GlobalOutlined style={{ fontSize: 18 }} />
<Search size={18} />
{t('settings.tool.websearch.title')}
</MenuItem>
</MenuItemLink>
@ -159,7 +151,7 @@ const SettingsPage: FC = () => {
<Routes>
<Route path="provider" element={<ProviderList />} />
<Route path="model" element={<ModelSettings />} />
<Route path="websearch" element={<WebSearchSettings />} />
<Route path="websearch/*" element={<WebSearchSettings />} />
<Route path="api-server" element={<ApiServerSettings />} />
<Route path="docprocess" element={<DocProcessSettings />} />
<Route path="quickphrase" element={<QuickPhraseSettings />} />
@ -171,7 +163,6 @@ const SettingsPage: FC = () => {
<Route path="quickAssistant" element={<QuickAssistantSettings />} />
<Route path="selectionAssistant" element={<SelectionAssistantSettings />} />
<Route path="data" element={<DataSettings />} />
<Route path="notes" element={<NotesSettings />} />
<Route path="about" element={<AboutSettings />} />
</Routes>
</SettingContent>

View File

@ -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 (
<div className="flex items-center gap-2">
{logo ? (
<img src={logo} alt={provider.name} className="h-4 w-4 rounded-sm object-contain" />
) : (
<div className="h-4 w-4 rounded-sm bg-[var(--color-background-soft)]" />
)}
<span>
{provider.name}
{needsApiKey && ` (${t('settings.tool.websearch.apikey')})`}
</span>
</div>
)
}
return (
<>
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.tool.websearch.search_provider')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.tool.websearch.default_provider')}</SettingRowTitle>
<Selector
size={14}
value={defaultProvider?.id}
onChange={(value: string) => updateSelectedWebSearchProvider(value)}
placeholder={t('settings.tool.websearch.search_provider_placeholder')}
options={sortedProviders.map((p) => ({
value: p.id,
label: renderProviderLabel(p)
}))}
/>
</SettingRow>
</SettingGroup>
<SettingGroup theme={theme} style={{ paddingBottom: 8 }}>
<SettingTitle>{t('settings.general.title')}</SettingTitle>
<SettingDivider />
@ -48,4 +164,5 @@ const BasicSettings: FC = () => {
</>
)
}
export default BasicSettings

View File

@ -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 (
<SettingContainer theme={theme}>
<BasicSettings />
<CompressionSettings />
<BlacklistSettings />
</SettingContainer>
)
}
export default WebSearchGeneralSettings

View File

@ -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<Props> = ({ 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<Props> = ({ 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 (
<>
<SettingTitle>
<Flex align="center" gap={8}>
<ProviderLogo src={getWebSearchProviderLogo(provider.id)} />
<ProviderName> {provider.name}</ProviderName>
{officialWebsite && webSearchProviderConfig?.websites && (
<Link target="_blank" href={webSearchProviderConfig.websites.official}>
<ExportOutlined style={{ color: 'var(--color-text)', fontSize: '12px' }} />
</Link>
)}
<Flex align="center" justify="space-between" style={{ width: '100%' }}>
<Flex align="center" gap={8}>
{providerLogo ? (
<img src={providerLogo} alt={provider.name} className="h-5 w-5 object-contain" />
) : (
<div className="h-5 w-5 rounded bg-[var(--color-background-soft)]" />
)}
<ProviderName> {provider.name}</ProviderName>
{officialWebsite && webSearchProviderConfig?.websites && (
<Link target="_blank" href={webSearchProviderConfig.websites.official}>
<ExportOutlined style={{ color: 'var(--color-text)', fontSize: '12px' }} />
</Link>
)}
</Flex>
<Button type="default" disabled={!canSetAsDefault} onClick={handleSetAsDefault}>
{isDefault ? t('settings.tool.websearch.is_default') : t('settings.tool.websearch.set_as_default')}
</Button>
</Flex>
</SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} />
{hasObjectKey(provider, 'apiKey') && (
{isLocalProvider && (
<>
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>
{t('settings.tool.websearch.local_provider.settings')}
</SettingSubtitle>
<Button type="primary" onClick={openLocalProviderSettings} icon={<ExportOutlined />}>
{t('settings.tool.websearch.local_provider.open_settings', { provider: provider.name })}
</Button>
<SettingHelpTextRow style={{ marginTop: 10 }}>
<SettingHelpText>{t('settings.tool.websearch.local_provider.hint')}</SettingHelpText>
</SettingHelpTextRow>
</>
)}
{!isLocalProvider && hasObjectKey(provider, 'apiKey') && (
<>
<SettingSubtitle
style={{
@ -219,7 +276,7 @@ const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
</SettingHelpTextRow>
</>
)}
{hasObjectKey(provider, 'apiHost') && (
{!isLocalProvider && hasObjectKey(provider, 'apiHost') && (
<>
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>
{t('settings.provider.api_host')}
@ -234,10 +291,11 @@ const WebSearchProviderSetting: FC<Props> = ({ providerId }) => {
</Flex>
</>
)}
{hasObjectKey(provider, 'basicAuthUsername') && (
{!isLocalProvider && hasObjectKey(provider, 'basicAuthUsername') && (
<>
<SettingDivider style={{ marginTop: 12, marginBottom: 12 }} />
<SettingSubtitle style={{ marginTop: 5, marginBottom: 10 }}>
<SettingSubtitle
style={{ marginTop: 5, marginBottom: 10, display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
{t('settings.provider.basic_auth.label')}
<Tooltip title={t('settings.provider.basic_auth.tip')} placement="right">
<Info size={16} color="var(--color-icon)" style={{ marginLeft: 5, cursor: 'pointer' }} />
@ -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

View File

@ -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 (
<SettingContainer theme={theme}>
<SettingGroup theme={theme}>
<WebSearchProviderSetting providerId={providerId as WebSearchProviderId} />
</SettingGroup>
</SettingContainer>
)
}
export default WebSearchProviderSettings

View File

@ -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<WebSearchProvider | undefined>(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 (
<SettingContainer theme={themeMode}>
<SettingGroup theme={themeMode}>
<SettingTitle>{t('settings.tool.websearch.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.tool.websearch.search_provider')}</SettingRowTitle>
<div style={{ display: 'flex', gap: '8px' }}>
<Selector
size={14}
value={selectedProvider?.id}
onChange={(value: string) => 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')})`
}))}
/>
</div>
</SettingRow>
</SettingGroup>
{!isLocalProvider && (
<SettingGroup theme={themeMode}>
{selectedProvider && <WebSearchProviderSetting providerId={selectedProvider.id} />}
</SettingGroup>
)}
<BasicSettings />
<CompressionSettings />
<BlacklistSettings />
</SettingContainer>
<Container>
<MainContainer>
<MenuList>
<ListItem
title={t('settings.tool.websearch.title')}
active={activeView === 'general'}
onClick={() => navigate('/settings/websearch/general')}
icon={<Search size={18} />}
titleStyle={{ fontWeight: 500 }}
/>
<DividerWithText text={t('settings.tool.websearch.api_providers')} style={{ margin: '10px 0 8px 0' }} />
{apiProviders.map((provider) => {
const logo = getProviderLogo(provider.id)
const isDefault = defaultProvider?.id === provider.id
return (
<ListItem
key={provider.id}
title={provider.name}
active={activeView === provider.id}
onClick={() => navigate(`/settings/websearch/provider/${provider.id}`)}
icon={
logo ? (
<img src={logo} alt={provider.name} className="h-5 w-5 rounded object-contain" />
) : (
<div className="h-5 w-5 rounded bg-[var(--color-background-soft)]" />
)
}
titleStyle={{ fontWeight: 500 }}
rightContent={
isDefault ? (
<Tag color="green" style={{ marginLeft: 'auto', marginRight: 0, borderRadius: 16 }}>
{t('common.default')}
</Tag>
) : undefined
}
/>
)
})}
{localProviders.length > 0 && (
<>
<DividerWithText text={t('settings.tool.websearch.local_providers')} style={{ margin: '10px 0 8px 0' }} />
{localProviders.map((provider) => {
const logo = getProviderLogo(provider.id)
const isDefault = defaultProvider?.id === provider.id
return (
<ListItem
key={provider.id}
title={provider.name}
active={activeView === provider.id}
onClick={() => navigate(`/settings/websearch/provider/${provider.id}`)}
icon={
logo ? (
<img src={logo} alt={provider.name} className="h-5 w-5 rounded object-contain" />
) : (
<div className="h-5 w-5 rounded bg-[var(--color-background-soft)]" />
)
}
titleStyle={{ fontWeight: 500 }}
rightContent={
isDefault ? (
<Tag color="green" style={{ marginLeft: 'auto', marginRight: 0, borderRadius: 16 }}>
{t('common.default')}
</Tag>
) : undefined
}
/>
)
})}
</>
)}
</MenuList>
<RightContainer>
<Routes>
<Route index element={<Navigate to="general" replace />} />
<Route path="general" element={<WebSearchGeneralSettings />} />
<Route path="provider/:providerId" element={<WebSearchProviderSettings />} />
</Routes>
</RightContainer>
</MainContainer>
</Container>
)
}
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

View File

@ -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)

View File

@ -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
}
}

View File

@ -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 []

View File

@ -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

View File

@ -42,7 +42,7 @@ export const translateText = async (
abortKey?: string,
options?: TranslateOptions
) => {
let abortError
let error
const assistantSettings: Partial<AssistantSettings> | 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()

View File

@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 188,
version: 190,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
migrate
},

View File

@ -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

View File

@ -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,50 @@ 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
}
},
// 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
}
}
}

View File

@ -395,6 +395,7 @@ export interface DmxapiPainting extends PaintingParams {
autoCreate?: boolean
generationMode?: generationModeType
priceModel?: string
extend_params?: Record<string, unknown>
}
export interface TokenFluxPainting extends PaintingParams {
@ -915,17 +916,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 */

View File

@ -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'

View File

@ -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, '')
}

View File

@ -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 {

486
yarn.lock
View File

@ -365,7 +365,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:
@ -1508,26 +1508,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
@ -1544,6 +1544,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"
@ -1574,16 +1587,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
@ -1608,6 +1621,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"
@ -1615,13 +1635,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
@ -1636,6 +1656,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"
@ -1679,7 +1710,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:
@ -1694,7 +1725,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:
@ -1714,6 +1760,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"
@ -2871,6 +2927,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"
@ -2880,6 +2946,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"
@ -2889,7 +2964,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:
@ -3867,7 +3942,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:
@ -4971,14 +5046,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
@ -5313,6 +5388,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"
@ -5320,13 +5402,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"
@ -5334,10 +5409,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
@ -6249,16 +6324,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
@ -6270,9 +6345,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
@ -6284,9 +6359,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
@ -6298,9 +6373,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
@ -6312,9 +6387,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
@ -6326,9 +6401,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
@ -6340,9 +6415,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
@ -6354,9 +6429,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
@ -6368,18 +6443,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
@ -6393,9 +6468,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
@ -6407,13 +6482,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"
@ -6421,9 +6489,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
@ -6442,10 +6510,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
@ -8195,6 +8263,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"
@ -10200,7 +10277,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"
@ -10313,7 +10390,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"
@ -13764,15 +13841,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
@ -13782,7 +13859,7 @@ __metadata:
optional: true
bin:
electron-vite: bin/electron-vite.js
checksum: 10c0/4e81ac4e4ede6060ffec56ba9b1d5ff95bb263496e62527345e8c79542924c54c54def39de9b466a81ed250b68774792c2106b93274c790b4cd8e7be448f6af8
checksum: 10c0/e7797910b23f23f39c12ded92d07d7164c5c6adab294aa13278c1b49ada3b12868b13ace8546d2656db4dbab89978cf8368a659d1ce6a2fb9f1aeddb1c8de557
languageName: node
linkType: hard
@ -17564,6 +17641,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"
@ -17571,6 +17655,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"
@ -17578,6 +17669,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"
@ -17585,6 +17683,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"
@ -17592,6 +17697,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"
@ -17599,6 +17711,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"
@ -17606,6 +17725,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"
@ -17613,6 +17739,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"
@ -17620,6 +17753,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"
@ -17627,6 +17767,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"
@ -17634,7 +17781,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:
@ -17674,6 +17828,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"
@ -18037,6 +18234,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"
@ -22864,28 +23070,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
@ -22911,13 +23114,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
@ -24476,6 +24677,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