From 4611e2c058f02c4805485924c5c46a1a77aec9a6 Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Sat, 26 Jul 2025 23:38:32 +0800 Subject: [PATCH 001/112] feat(mcp): add shouldConfig flag and i18n support for builtin MCPServer description (#8440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(mcp): 添加shouldConfig字段用于标记需要配置的服务器 在MCPServer接口中添加shouldConfig字段,用于标识需要额外配置的服务器 修改BuiltinMCPServersSection组件,根据shouldConfig字段显示配置提示标签 更新内置服务器列表,为需要配置的服务器添加shouldConfig字段 * feat(i18n): 添加内置服务器描述的多语言支持 * feat(mcp): 为内置MCP服务器添加国际化描述支持 将内置MCP服务器的描述从硬编码改为通过i18n获取,支持多语言显示 * feat(i18n): 添加内置MCP服务器的多语言描述 为内置MCP服务器添加多语言描述文本,包括中文、英文、日文等语言版本 同时优化mcp.ts中的描述文本引用方式,直接使用完整路径而非拼接前缀 * feat(mcp): 为内置服务器添加默认描述并修复无效描述显示 为内置MCP服务器添加默认描述字段,并在UI中使用国际化文本替换硬编码的"Invalid description"。同时为所有语言环境添加"no"键作为默认描述文本。 --- src/renderer/src/i18n/locales/en-us.json | 11 ++++++++ src/renderer/src/i18n/locales/ja-jp.json | 11 ++++++++ src/renderer/src/i18n/locales/ru-ru.json | 11 ++++++++ src/renderer/src/i18n/locales/zh-cn.json | 11 ++++++++ src/renderer/src/i18n/locales/zh-tw.json | 11 ++++++++ src/renderer/src/i18n/translate/el-gr.json | 11 ++++++++ src/renderer/src/i18n/translate/es-es.json | 11 ++++++++ src/renderer/src/i18n/translate/fr-fr.json | 11 ++++++++ src/renderer/src/i18n/translate/pt-pt.json | 11 ++++++++ .../MCPSettings/BuiltinMCPServersSection.tsx | 15 ++++++++--- src/renderer/src/store/mcp.ts | 26 +++++++++++-------- src/renderer/src/types/index.ts | 2 ++ 12 files changed, 128 insertions(+), 14 deletions(-) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index a584a1ed78..d7183599cb 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -2595,6 +2595,17 @@ "argsTooltip": "Each argument on a new line", "baseUrlTooltip": "Remote server base URL", "builtinServers": "Builtin Servers", + "builtinServersDescriptions": { + "brave_search": "An MCP server implementation integrating the Brave Search API, providing both web and local search functionalities. Requires configuring the BRAVE_API_KEY environment variable", + "dify_knowledge": "Dify's MCP server implementation provides a simple API to interact with Dify. Requires configuring the Dify Key", + "fetch": "MCP server for retrieving URL web content", + "filesystem": "A Node.js server implementing the Model Context Protocol (MCP) for file system operations. Requires configuration of directories allowed for access.", + "mcp_auto_install": "Automatically install MCP service (beta) https://docs.cherry-ai.com/advanced-basic/mcp/auto-install", + "memory": "Persistent memory implementation based on a local knowledge graph. This enables the model to remember user-related information across different conversations. Requires configuring the MEMORY_FILE_PATH environment variable. https://github.com/modelcontextprotocol/servers/tree/main/src/memory", + "no": "No description", + "python": "Execute Python code in a secure sandbox environment. Run Python with Pyodide, supporting most standard libraries and scientific computing packages", + "sequentialthinking": "A MCP server implementation that provides tools for dynamic and reflective problem solving through structured thinking processes" + }, "command": "Command", "config_description": "Configure Model Context Protocol servers", "customRegistryPlaceholder": "Enter private registry URL, e.g.: https://npm.company.com", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 667aff7db0..76cd8183d8 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -2595,6 +2595,17 @@ "argsTooltip": "1行に1つの引数を入力してください", "baseUrlTooltip": "リモートURLアドレス", "builtinServers": "組み込みサーバー", + "builtinServersDescriptions": { + "brave_search": "Brave検索APIを統合したMCPサーバーの実装で、ウェブ検索とローカル検索の両機能を提供します。BRAVE_API_KEY環境変数の設定が必要です", + "dify_knowledge": "DifyのMCPサーバー実装は、Difyと対話するためのシンプルなAPIを提供します。Dify Keyの設定が必要です。", + "fetch": "URLのウェブページコンテンツを取得するためのMCPサーバー", + "filesystem": "Node.jsサーバーによるファイルシステム操作を実現するモデルコンテキストプロトコル(MCP)。アクセスを許可するディレクトリの設定が必要です", + "mcp_auto_install": "MCPサービスの自動インストール(ベータ版)https://docs.cherry-ai.com/advanced-basic/mcp/auto-install", + "memory": "ローカルのナレッジグラフに基づく永続的なメモリの基本的な実装です。これにより、モデルは異なる会話間でユーザーの関連情報を記憶できるようになります。MEMORY_FILE_PATH 環境変数の設定が必要です。https://github.com/modelcontextprotocol/servers/tree/main/src/memory", + "no": "説明なし", + "python": "安全なサンドボックス環境でPythonコードを実行します。Pyodideを使用してPythonを実行し、ほとんどの標準ライブラリと科学計算パッケージをサポートしています。", + "sequentialthinking": "構造化された思考プロセスを通じて動的かつ反省的な問題解決を行うためのツールを提供するMCPサーバーの実装" + }, "command": "コマンド", "config_description": "モデルコンテキストプロトコルサーバーの設定", "customRegistryPlaceholder": "プライベート倉庫のアドレスを入力してください(例:https://npm.company.com)", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index dbf8f8a6ff..257a08cddf 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -2595,6 +2595,17 @@ "argsTooltip": "Каждый аргумент с новой строки", "baseUrlTooltip": "Адрес удаленного URL", "builtinServers": "Встроенные серверы", + "builtinServersDescriptions": { + "brave_search": "реализация сервера MCP с интеграцией API поиска Brave, обеспечивающая функции веб-поиска и локального поиска. Требуется настройка переменной среды BRAVE_API_KEY", + "dify_knowledge": "Реализация сервера MCP Dify, предоставляющая простой API для взаимодействия с Dify. Требуется настройка ключа Dify", + "fetch": "MCP-сервер для получения содержимого веб-страниц по URL", + "filesystem": "Node.js-сервер протокола контекста модели (MCP) для реализации операций файловой системы. Требуется настройка каталогов, к которым разрешён доступ", + "mcp_auto_install": "Автоматическая установка службы MCP (бета-версия) https://docs.cherry-ai.com/advanced-basic/mcp/auto-install", + "memory": "реализация постоянной памяти на основе локального графа знаний. Это позволяет модели запоминать информацию о пользователе между различными диалогами. Требуется настроить переменную среды MEMORY_FILE_PATH. https://github.com/modelcontextprotocol/servers/tree/main/src/memory", + "no": "без описания", + "python": "Выполняйте код Python в безопасной песочнице. Запускайте Python с помощью Pyodide, поддерживается большинство стандартных библиотек и пакетов для научных вычислений", + "sequentialthinking": "MCP серверная реализация, предоставляющая инструменты для динамического и рефлексивного решения проблем посредством структурированного мыслительного процесса" + }, "command": "Команда", "config_description": "Настройка серверов протокола контекста модели", "customRegistryPlaceholder": "Введите адрес частного склада, например: https://npm.company.com", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index d088417cb4..8f5fb1e073 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -2595,6 +2595,17 @@ "argsTooltip": "每个参数占一行", "baseUrlTooltip": "远程 URL 地址", "builtinServers": "内置服务器", + "builtinServersDescriptions": { + "brave_search": "一个集成了Brave 搜索 API 的 MCP 服务器实现,提供网页与本地搜索双重功能。需要配置 BRAVE_API_KEY 环境变量", + "dify_knowledge": "Dify 的 MCP 服务器实现,提供了一个简单的 API 来与 Dify 进行交互。需要配置 Dify Key", + "fetch": "用于获取 URL 网页内容的 MCP 服务器", + "filesystem": "实现文件系统操作的模型上下文协议(MCP)的 Node.js 服务器。需要配置允许访问的目录", + "mcp_auto_install": "自动安装 MCP 服务(测试版)https://docs.cherry-ai.com/advanced-basic/mcp/auto-install", + "memory": "基于本地知识图谱的持久性记忆基础实现。这使得模型能够在不同对话间记住用户的相关信息。需要配置 MEMORY_FILE_PATH 环境变量。https://github.com/modelcontextprotocol/servers/tree/main/src/memory", + "no": "无描述", + "python": "在安全的沙盒环境中执行 Python 代码。使用 Pyodide 运行 Python,支持大多数标准库和科学计算包", + "sequentialthinking": "一个 MCP 服务器实现,提供了通过结构化思维过程进行动态和反思性问题解决的工具" + }, "command": "命令", "config_description": "配置模型上下文协议服务器", "customRegistryPlaceholder": "请输入私有仓库地址,如: https://npm.company.com", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 24f07e2387..0caa17591f 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -2595,6 +2595,17 @@ "argsTooltip": "每個參數佔一行", "baseUrlTooltip": "遠端 URL 地址", "builtinServers": "內置伺服器", + "builtinServersDescriptions": { + "brave_search": "一個集成了Brave 搜索 API 的 MCP 伺服器實現,提供網頁與本地搜尋雙重功能。需要配置 BRAVE_API_KEY 環境變數", + "dify_knowledge": "Dify 的 MCP 伺服器實現,提供了一個簡單的 API 來與 Dify 進行互動。需要配置 Dify Key", + "fetch": "用於獲取 URL 網頁內容的 MCP 伺服器", + "filesystem": "實現文件系統操作的模型上下文協議(MCP)的 Node.js 伺服器。需要配置允許訪問的目錄", + "mcp_auto_install": "自動安裝 MCP 服務(測試版)https://docs.cherry-ai.com/advanced-basic/mcp/auto-install", + "memory": "基於本地知識圖譜的持久性記憶基礎實現。這使得模型能夠在不同對話間記住使用者的相關資訊。需要配置 MEMORY_FILE_PATH 環境變數。https://github.com/modelcontextprotocol/servers/tree/main/src/memory", + "no": "無描述", + "python": "在安全的沙盒環境中執行 Python 代碼。使用 Pyodide 運行 Python,支援大多數標準庫和科學計算套件", + "sequentialthinking": "一個 MCP 伺服器實現,提供了透過結構化思維過程進行動態和反思性問題解決的工具" + }, "command": "指令", "config_description": "設定模型上下文協議伺服器", "customRegistryPlaceholder": "請輸入私有倉庫位址,如: https://npm.company.com", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 4d804285fe..8cb83a2cb7 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -2595,6 +2595,17 @@ "argsTooltip": "Κάθε παράμετρος σε μια γραμμή", "baseUrlTooltip": "Σύνδεσμος Απομακρυσμένης διεύθυνσης URL", "builtinServers": "Ενσωματωμένοι Διακομιστές", + "builtinServersDescriptions": { + "brave_search": "μια εφαρμογή διακομιστή MCP που ενσωματώνει το Brave Search API, παρέχοντας δυνατότητες αναζήτησης στον ιστό και τοπικής αναζήτησης. Απαιτείται η ρύθμιση της μεταβλητής περιβάλλοντος BRAVE_API_KEY", + "dify_knowledge": "Η υλοποίηση του Dify για τον διακομιστή MCP, παρέχει μια απλή API για να αλληλεπιδρά με το Dify. Απαιτείται η ρύθμιση του κλειδιού Dify", + "fetch": "Εξυπηρετητής MCP για λήψη περιεχομένου ιστοσελίδας URL", + "filesystem": "Εξυπηρετητής Node.js για το πρωτόκολλο περιβάλλοντος μοντέλου (MCP) που εφαρμόζει λειτουργίες συστήματος αρχείων. Απαιτείται διαμόρφωση για την επιτροπή πρόσβασης σε καταλόγους", + "mcp_auto_install": "Αυτόματη εγκατάσταση υπηρεσίας MCP (προβολή) https://docs.cherry-ai.com/advanced-basic/mcp/auto-install", + "memory": "Βασική υλοποίηση μόνιμης μνήμης με βάση τοπικό γράφημα γνώσης. Αυτό επιτρέπει στο μοντέλο να θυμάται πληροφορίες σχετικές με τον χρήστη ανάμεσα σε διαφορετικές συνομιλίες. Απαιτείται η ρύθμιση της μεταβλητής περιβάλλοντος MEMORY_FILE_PATH. https://github.com/modelcontextprotocol/servers/tree/main/src/memory", + "no": "Χωρίς περιγραφή", + "python": "Εκτελέστε κώδικα Python σε ένα ασφαλές περιβάλλον sandbox. Χρησιμοποιήστε το Pyodide για να εκτελέσετε Python, υποστηρίζοντας την πλειονότητα των βιβλιοθηκών της τυπικής βιβλιοθήκης και των πακέτων επιστημονικού υπολογισμού", + "sequentialthinking": "ένας εξυπηρετητής MCP που υλοποιείται, παρέχοντας εργαλεία για δυναμική και αναστοχαστική επίλυση προβλημάτων μέσω δομημένων διαδικασιών σκέψης" + }, "command": "Εντολή", "config_description": "Ρυθμίζει το πλαίσιο συντονισμού πρωτοκόλλων διακομιστή", "customRegistryPlaceholder": "Παρακαλώ εισάγετε τη διεύθυνση του ιδιωτικού αποθετηρίου, π.χ.: https://npm.company.com", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 79f822ded6..9aaf9b38e5 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -2595,6 +2595,17 @@ "argsTooltip": "Cada argumento en una línea", "baseUrlTooltip": "Dirección URL remota", "builtinServers": "Servidores integrados", + "builtinServersDescriptions": { + "brave_search": "Una implementación de servidor MCP que integra la API de búsqueda de Brave, proporcionando funciones de búsqueda web y búsqueda local. Requiere configurar la variable de entorno BRAVE_API_KEY", + "dify_knowledge": "Implementación del servidor MCP de Dify, que proporciona una API sencilla para interactuar con Dify. Se requiere configurar la clave de Dify.", + "fetch": "Servidor MCP para obtener el contenido de la página web de una URL", + "filesystem": "Servidor Node.js que implementa el protocolo de contexto del modelo (MCP) para operaciones del sistema de archivos. Requiere configuración del directorio permitido para el acceso", + "mcp_auto_install": "Instalación automática del servicio MCP (versión beta) https://docs.cherry-ai.com/advanced-basic/mcp/auto-install", + "memory": "Implementación básica de memoria persistente basada en un grafo de conocimiento local. Esto permite que el modelo recuerde información relevante del usuario entre diferentes conversaciones. Es necesario configurar la variable de entorno MEMORY_FILE_PATH. https://github.com/modelcontextprotocol/servers/tree/main/src/memory", + "no": "sin descripción", + "python": "Ejecuta código Python en un entorno sandbox seguro. Usa Pyodide para ejecutar Python, compatible con la mayoría de las bibliotecas estándar y paquetes de cálculo científico.", + "sequentialthinking": "Una implementación de servidor MCP que proporciona herramientas para la resolución dinámica y reflexiva de problemas mediante un proceso de pensamiento estructurado" + }, "command": "Comando", "config_description": "Configurar modelo de contexto del protocolo del servidor", "customRegistryPlaceholder": "Por favor ingresa la dirección del repositorio privado, por ejemplo: https://npm.company.com", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 120d358152..a143c9a8f3 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -2595,6 +2595,17 @@ "argsTooltip": "Chaque argument sur une ligne", "baseUrlTooltip": "Adresse URL distante", "builtinServers": "Serveurs intégrés", + "builtinServersDescriptions": { + "brave_search": "Une implémentation de serveur MCP intégrant l'API de recherche Brave, offrant des fonctionnalités de recherche web et locale. Nécessite la configuration de la variable d'environnement BRAVE_API_KEY", + "dify_knowledge": "Implémentation du serveur MCP de Dify, fournissant une API simple pour interagir avec Dify. Nécessite la configuration de la clé Dify", + "fetch": "serveur MCP utilisé pour récupérer le contenu des pages web URL", + "filesystem": "Serveur Node.js implémentant le protocole de contexte de modèle (MCP) pour les opérations de système de fichiers. Nécessite une configuration des répertoires autorisés à être accédés.", + "mcp_auto_install": "Installation automatique du service MCP (version bêta) https://docs.cherry-ai.com/advanced-basic/mcp/auto-install", + "memory": "Implémentation de base de mémoire persistante basée sur un graphe de connaissances local. Cela permet au modèle de se souvenir des informations relatives à l'utilisateur entre différentes conversations. Nécessite la configuration de la variable d'environnement MEMORY_FILE_PATH. https://github.com/modelcontextprotocol/servers/tree/main/src/memory", + "no": "sans description", + "python": "Exécutez du code Python dans un environnement bac à sable sécurisé. Utilisez Pyodide pour exécuter Python, prenant en charge la plupart des bibliothèques standard et des packages de calcul scientifique.", + "sequentialthinking": "Un serveur MCP qui fournit des outils permettant une résolution dynamique et réflexive des problèmes à travers un processus de pensée structuré" + }, "command": "Commande", "config_description": "Configurer le modèle du protocole de contexte du serveur", "customRegistryPlaceholder": "Veuillez entrer l'adresse du registre privé, par exemple : https://npm.company.com", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 99c0e6a7fa..bb35817c80 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -2595,6 +2595,17 @@ "argsTooltip": "Cada argumento em uma linha", "baseUrlTooltip": "Endereço de URL remoto", "builtinServers": "Servidores integrados", + "builtinServersDescriptions": { + "brave_search": "uma implementação de servidor MCP integrada com a API de pesquisa Brave, fornecendo funcionalidades de pesquisa web e local. Requer a configuração da variável de ambiente BRAVE_API_KEY", + "dify_knowledge": "Implementação do servidor MCP do Dify, que fornece uma API simples para interagir com o Dify. Requer a configuração da chave Dify", + "fetch": "servidor MCP para obter o conteúdo da página web do URL", + "filesystem": "Servidor Node.js do protocolo de contexto de modelo (MCP) para implementar operações de sistema de ficheiros. Requer configuração do diretório permitido para acesso", + "mcp_auto_install": "Instalação automática do serviço MCP (beta) https://docs.cherry-ai.com/advanced-basic/mcp/auto-install", + "memory": "Implementação base de memória persistente baseada em grafos de conhecimento locais. Isso permite que o modelo lembre informações relevantes do utilizador entre diferentes conversas. É necessário configurar a variável de ambiente MEMORY_FILE_PATH. https://github.com/modelcontextprotocol/servers/tree/main/src/memory", + "no": "sem descrição", + "python": "Executar código Python num ambiente sandbox seguro. Utilizar Pyodide para executar Python, suportando a maioria das bibliotecas padrão e pacotes de computação científica", + "sequentialthinking": "Uma implementação de servidor MCP que fornece ferramentas para resolução dinâmica e reflexiva de problemas através de um processo de pensamento estruturado" + }, "command": "Comando", "config_description": "Configurar modelo de protocolo de contexto do servidor", "customRegistryPlaceholder": "Por favor, insira o endereço do repositório privado, por exemplo: https://npm.company.com", diff --git a/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServersSection.tsx b/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServersSection.tsx index 25500fb16b..d2f82e1f4d 100644 --- a/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServersSection.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServersSection.tsx @@ -44,13 +44,22 @@ const BuiltinMCPServersSection: FC = () => { {server.description}} + content={ + + {server.getBuiltinDescription + ? server.getBuiltinDescription() + : t('settings.mcp.builtinServersDescriptions.no')} + + } title={server.name} trigger="hover" placement="topLeft" overlayStyle={{ maxWidth: 400 }}> - {server.description} + {/* {server.getBuiltinDescription ? server.getBuiltinDescription() : 'Invalid description'} */} + {server.getBuiltinDescription + ? server.getBuiltinDescription() + : t('settings.mcp.builtinServersDescriptions.no')} ... @@ -58,7 +67,7 @@ const BuiltinMCPServersSection: FC = () => { {getMcpTypeLabel(server.type ?? 'stdio')} - {server.env && Object.keys(server.env).length > 0 && ( + {server?.shouldConfig && ( {t('settings.mcp.requiresConfig')} diff --git a/src/renderer/src/store/mcp.ts b/src/renderer/src/store/mcp.ts index 0b1d2bb7da..330534bdc2 100644 --- a/src/renderer/src/store/mcp.ts +++ b/src/renderer/src/store/mcp.ts @@ -1,5 +1,6 @@ import { loggerService } from '@logger' import { createSlice, nanoid, type PayloadAction } from '@reduxjs/toolkit' +import i18n from '@renderer/i18n' import type { MCPConfig, MCPServer } from '@renderer/types' const logger = loggerService.withContext('Store:MCP') @@ -74,8 +75,8 @@ export const builtinMCPServers: MCPServer[] = [ { id: nanoid(), name: '@cherry/mcp-auto-install', - description: '自动安装 MCP 服务(测试版)https://docs.cherry-ai.com/advanced-basic/mcp/auto-install', - type: 'stdio', + getBuiltinDescription: () => i18n.t('settings.mcp.builtinServersDescriptions.mcp_auto_install'), + type: 'inMemory', command: 'npx', args: ['-y', '@mcpmarket/mcp-auto-install', 'connect', '--json'], isActive: false, @@ -84,20 +85,20 @@ export const builtinMCPServers: MCPServer[] = [ { id: nanoid(), name: '@cherry/memory', + getBuiltinDescription: () => i18n.t('settings.mcp.builtinServersDescriptions.memory'), type: 'inMemory', - description: - '基于本地知识图谱的持久性记忆基础实现。这使得模型能够在不同对话间记住用户的相关信息。需要配置 MEMORY_FILE_PATH 环境变量。https://github.com/modelcontextprotocol/servers/tree/main/src/memory', isActive: true, env: { MEMORY_FILE_PATH: 'YOUR_MEMORY_FILE_PATH' }, + shouldConfig: true, provider: 'CherryAI' }, { id: nanoid(), name: '@cherry/sequentialthinking', + getBuiltinDescription: () => i18n.t('settings.mcp.builtinServersDescriptions.sequentialthinking'), type: 'inMemory', - description: '一个 MCP 服务器实现,提供了通过结构化思维过程进行动态和反思性问题解决的工具', isActive: true, provider: 'CherryAI' }, @@ -105,19 +106,19 @@ export const builtinMCPServers: MCPServer[] = [ id: nanoid(), name: '@cherry/brave-search', type: 'inMemory', - description: - '一个集成了Brave 搜索 API 的 MCP 服务器实现,提供网页与本地搜索双重功能。需要配置 BRAVE_API_KEY 环境变量', + getBuiltinDescription: () => i18n.t('settings.mcp.builtinServersDescriptions.brave_search'), isActive: false, env: { BRAVE_API_KEY: 'YOUR_API_KEY' }, + shouldConfig: true, provider: 'CherryAI' }, { id: nanoid(), name: '@cherry/fetch', type: 'inMemory', - description: '用于获取 URL 网页内容的 MCP 服务器', + getBuiltinDescription: () => i18n.t('settings.mcp.builtinServersDescriptions.fetch'), isActive: true, provider: 'CherryAI' }, @@ -125,8 +126,10 @@ export const builtinMCPServers: MCPServer[] = [ id: nanoid(), name: '@cherry/filesystem', type: 'inMemory', - description: '实现文件系统操作的模型上下文协议(MCP)的 Node.js 服务器', + description: i18n.t('settings.mcp.builtinServersDescriptions.filesystem'), + getBuiltinDescription: () => i18n.t('settings.mcp.builtinServersDescriptions.filesystem'), args: ['/Users/username/Desktop', '/path/to/other/allowed/dir'], + shouldConfig: true, isActive: false, provider: 'CherryAI' }, @@ -134,18 +137,19 @@ export const builtinMCPServers: MCPServer[] = [ id: nanoid(), name: '@cherry/dify-knowledge', type: 'inMemory', - description: 'Dify 的 MCP 服务器实现,提供了一个简单的 API 来与 Dify 进行交互', + getBuiltinDescription: () => i18n.t('settings.mcp.builtinServersDescriptions.dify_knowledge'), isActive: false, env: { DIFY_KEY: 'YOUR_DIFY_KEY' }, + shouldConfig: true, provider: 'CherryAI' }, { id: nanoid(), name: '@cherry/python', type: 'inMemory', - description: '在安全的沙盒环境中执行 Python 代码。使用 Pyodide 运行 Python,支持大多数标准库和科学计算包', + getBuiltinDescription: () => i18n.t('settings.mcp.builtinServersDescriptions.python'), isActive: false, provider: 'CherryAI' } diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index c3bd8eb3ba..28241084f0 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -654,6 +654,8 @@ export interface MCPServer { registryUrl?: string args?: string[] env?: Record + shouldConfig?: boolean + getBuiltinDescription?: () => string isActive: boolean disabledTools?: string[] // List of tool names that are disabled for this server disabledAutoApproveTools?: string[] // Whether to auto-approve tools for this server From fd0165316456ba009ebd20bbb21adb7ece70c19e Mon Sep 17 00:00:00 2001 From: SuYao Date: Sat, 26 Jul 2025 23:48:45 +0800 Subject: [PATCH 002/112] refactor(OpenAIApiClient, models, ThinkingButton): streamline reasoning model checks and enhance support for Perplexity models (#8487) - Removed the specific check for Grok models in OpenAIApiClient and consolidated it with the general reasoning effort model check. - Added support for a new Perplexity model, 'sonar-deep-research', in the models configuration. - Updated the reasoning model checks to include Perplexity models in the models.ts file. - Enhanced the ThinkingButton component to recognize and handle Perplexity model options. --- .../aiCore/clients/openai/OpenAIApiClient.ts | 12 ++----- src/renderer/src/config/models.ts | 35 +++++++++++++++++-- .../pages/home/Inputbar/ThinkingButton.tsx | 9 +++-- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts index eeaa1c7bad..199868e2d4 100644 --- a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts +++ b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts @@ -10,7 +10,6 @@ import { isQwenMTModel, isQwenReasoningModel, isReasoningModel, - isSupportedReasoningEffortGrokModel, isSupportedReasoningEffortModel, isSupportedReasoningEffortOpenAIModel, isSupportedThinkingTokenClaudeModel, @@ -199,15 +198,8 @@ export class OpenAIAPIClient extends OpenAIBaseClient< } } - // Grok models - if (isSupportedReasoningEffortGrokModel(model)) { - return { - reasoning_effort: reasoningEffort - } - } - - // OpenAI models - if (isSupportedReasoningEffortOpenAIModel(model)) { + // Grok models/Perplexity models/OpenAI models + if (isSupportedReasoningEffortModel(model)) { return { reasoning_effort: reasoningEffort } diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 02b89cb938..aa124002ad 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -1975,6 +1975,12 @@ export const SYSTEM_MODELS: Record = { provider: 'perplexity', name: 'sonar', group: 'Sonar' + }, + { + id: 'sonar-deep-research', + provider: 'perplexity', + name: 'sonar-deep-research', + group: 'Sonar' } ], infini: [ @@ -2406,7 +2412,13 @@ export const GEMINI_SEARCH_REGEX = new RegExp('gemini-2\\..*', 'i') export const OPENAI_NO_SUPPORT_DEV_ROLE_MODELS = ['o1-preview', 'o1-mini'] -export const PERPLEXITY_SEARCH_MODELS = ['sonar-pro', 'sonar', 'sonar-reasoning', 'sonar-reasoning-pro'] +export const PERPLEXITY_SEARCH_MODELS = [ + 'sonar-pro', + 'sonar', + 'sonar-reasoning', + 'sonar-reasoning-pro', + 'sonar-deep-research' +] export function isTextToImageModel(model: Model): boolean { return TEXT_TO_IMAGE_REGEX.test(model.id) @@ -2547,7 +2559,11 @@ export function isSupportedReasoningEffortModel(model?: Model): boolean { return false } - return isSupportedReasoningEffortOpenAIModel(model) || isSupportedReasoningEffortGrokModel(model) + return ( + isSupportedReasoningEffortOpenAIModel(model) || + isSupportedReasoningEffortGrokModel(model) || + isSupportedReasoningEffortPerplexityModel(model) + ) } export function isGrokModel(model?: Model): boolean { @@ -2683,6 +2699,20 @@ export const isHunyuanReasoningModel = (model?: Model): boolean => { return isSupportedThinkingTokenHunyuanModel(model) || model.id.toLowerCase().includes('hunyuan-t1') } +export const isPerplexityReasoningModel = (model?: Model): boolean => { + if (!model) { + return false + } + + const baseName = getLowerBaseModelName(model.id, '/') + return isSupportedReasoningEffortPerplexityModel(model) || baseName.includes('reasoning') +} + +export const isSupportedReasoningEffortPerplexityModel = (model: Model): boolean => { + const baseName = getLowerBaseModelName(model.id, '/') + return baseName.includes('sonar-deep-research') +} + export function isReasoningModel(model?: Model): boolean { if (!model || isEmbeddingModel(model) || isRerankModel(model) || isTextToImageModel(model)) { return false @@ -2708,6 +2738,7 @@ export function isReasoningModel(model?: Model): boolean { isQwenReasoningModel(model) || isGrokReasoningModel(model) || isHunyuanReasoningModel(model) || + isPerplexityReasoningModel(model) || model.id.toLowerCase().includes('glm-z1') || model.id.toLowerCase().includes('magistral') || model.id.toLowerCase().includes('minimax-m1') diff --git a/src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx b/src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx index a23a34d27a..2dd4ea960d 100644 --- a/src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx @@ -10,6 +10,7 @@ import { GEMINI_FLASH_MODEL_REGEX, isDoubaoThinkingAutoModel, isSupportedReasoningEffortGrokModel, + isSupportedReasoningEffortPerplexityModel, isSupportedThinkingTokenDoubaoModel, isSupportedThinkingTokenGeminiModel, isSupportedThinkingTokenHunyuanModel, @@ -44,7 +45,8 @@ const MODEL_SUPPORTED_OPTIONS: Record = { qwen: ['off', 'low', 'medium', 'high'], qwen_3235ba22b_thinking: ['low', 'medium', 'high'], doubao: ['off', 'auto', 'high'], - hunyuan: ['off', 'auto'] + hunyuan: ['off', 'auto'], + perplexity: ['low', 'medium', 'high'] } // 选项转换映射表:当选项不支持时使用的替代选项 @@ -68,6 +70,7 @@ const ThinkingButton: FC = ({ ref, model, assistant, ToolbarButton }): Re const isQwen3235BA22BThinkingModel = model.id.includes('qwen3-235b-a22b-thinking') const isDoubaoModel = isSupportedThinkingTokenDoubaoModel(model) const isHunyuanModel = isSupportedThinkingTokenHunyuanModel(model) + const isPerplexityModel = isSupportedReasoningEffortPerplexityModel(model) const currentReasoningEffort = useMemo(() => { return assistant.settings?.reasoning_effort || 'off' @@ -91,14 +94,16 @@ const ThinkingButton: FC = ({ ref, model, assistant, ToolbarButton }): Re } if (isDoubaoModel) return 'doubao' if (isHunyuanModel) return 'hunyuan' + if (isPerplexityModel) return 'perplexity' return 'default' }, [ isGeminiModel, isGrokModel, isQwenModel, isDoubaoModel, - isHunyuanModel, isGeminiFlashModel, + isHunyuanModel, + isPerplexityModel, isQwen3235BA22BThinkingModel ]) From dfceed875121019c59b9a51d06c9801fc8a5500b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=C2=B7Dong?= <98630204+GeorgeDong32@users.noreply.github.com> Date: Sun, 27 Jul 2025 01:18:11 +0800 Subject: [PATCH 003/112] feat(models): add Grok 4 model and update reasoning regex (#8545) * feat(models): add Grok 4 model and update reasoning regex * feat(models): add grok-4 to vision model --- src/renderer/src/config/models.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index aa124002ad..398d9f1b55 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -172,6 +172,7 @@ const visionAllowedModels = [ 'qvq', 'internvl2', 'grok-vision-beta', + 'grok-4(?:-[\\w-]+)?', 'pixtral', 'gpt-4(?:-[\\w-]+)', 'gpt-4.1(?:-[\\w-]+)?', @@ -217,7 +218,7 @@ export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview| // Reasoning models export const REASONING_REGEX = - /^(o\d+(?:-[\w-]+)?|.*\b(?:reasoning|reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*|.*\bhunyuan-t1(?:-[\w-]+)?\b.*|.*\bglm-zero-preview\b.*|.*\bgrok-3-mini(?:-[\w-]+)?\b.*)$/i + /^(o\d+(?:-[\w-]+)?|.*\b(?:reasoning|reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*|.*\bhunyuan-t1(?:-[\w-]+)?\b.*|.*\bglm-zero-preview\b.*|.*\bgrok-(?:3-mini|4)(?:-[\w-]+)?\b.*)$/i // Embedding models export const EMBEDDING_REGEX = @@ -1552,6 +1553,12 @@ export const SYSTEM_MODELS: Record = { } ], grok: [ + { + id: 'grok-4', + provider: 'grok', + name: 'Grok 4', + group: 'Grok' + }, { id: 'grok-3', provider: 'grok', From 46d98c2b224ce73415c2e759c1baa1f4a319def7 Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Sun, 27 Jul 2025 11:17:45 +0800 Subject: [PATCH 004/112] feat(assistants): place copied assistant after the original one (#8557) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(assistants): 复制助手功能插入到原助手后 添加 insertAssistant 方法用于在指定位置插入助手 实现 copyAssistant 功能用于复制助手并插入到原助手后面 更新相关组件以使用新的复制功能 * fix(useAssistant): 修复助手索引检查逻辑错误 原条件判断错误导致未找到索引时仍执行更新操作,现修正为仅在索引存在时更新 * fix(useAssistant): 添加错误处理及国际化支持 捕获插入助手时的异常并显示错误提示 添加react-i18next的翻译功能用于错误消息 --- src/renderer/src/hooks/useAssistant.ts | 28 +++++++++++++++++++ .../src/pages/home/Tabs/AssistantsTab.tsx | 6 ++-- .../home/Tabs/components/AssistantItem.tsx | 19 +++++++------ src/renderer/src/store/assistants.ts | 10 +++++++ 4 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/renderer/src/hooks/useAssistant.ts b/src/renderer/src/hooks/useAssistant.ts index ee375ec28d..375a41e37b 100644 --- a/src/renderer/src/hooks/useAssistant.ts +++ b/src/renderer/src/hooks/useAssistant.ts @@ -1,9 +1,11 @@ import { db } from '@renderer/databases' import { getDefaultTopic } from '@renderer/services/AssistantService' +import { loggerService } from '@renderer/services/LoggerService' import { useAppDispatch, useAppSelector } from '@renderer/store' import { addAssistant, addTopic, + insertAssistant, removeAllTopics, removeAssistant, removeTopic, @@ -17,18 +19,44 @@ import { } from '@renderer/store/assistants' import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm' import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types' +import { uuid } from '@renderer/utils' import { useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' import { TopicManager } from './useTopic' export function useAssistants() { + const { t } = useTranslation() const { assistants } = useAppSelector((state) => state.assistants) const dispatch = useAppDispatch() + const logger = loggerService.withContext('useAssistants') return { assistants, updateAssistants: (assistants: Assistant[]) => dispatch(updateAssistants(assistants)), addAssistant: (assistant: Assistant) => dispatch(addAssistant(assistant)), + insertAssistant: (index: number, assistant: Assistant) => dispatch(insertAssistant({ index, assistant })), + copyAssistant: (assistant: Assistant): Assistant | undefined => { + if (!assistant) { + logger.error("assistant doesn't exists.") + return + } + const index = assistants.findIndex((_assistant) => _assistant.id === assistant.id) + const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic(assistant.id)] } + if (index === -1) { + logger.warn("Origin assistant's id not found. Fallback to addAssistant.") + dispatch(addAssistant(_assistant)) + } else { + // 插入到后面 + try { + dispatch(insertAssistant({ index: index + 1, assistant: _assistant })) + } catch (e) { + logger.error('Failed to insert assistant', e as Error) + window.message.error(t('message.error.copy')) + } + } + return _assistant + }, removeAssistant: (id: string) => { dispatch(removeAssistant({ id })) const assistant = assistants.find((a) => a.id === id) diff --git a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx index b42c6fbafe..e2c7589243 100644 --- a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx @@ -25,7 +25,7 @@ const Assistants: FC = ({ onCreateAssistant, onCreateDefaultAssistant }) => { - const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants() + const { assistants, removeAssistant, copyAssistant, updateAssistants } = useAssistants() const [dragging, setDragging] = useState(false) const { addAgent } = useAgents() const { t } = useTranslation() @@ -106,7 +106,7 @@ const Assistants: FC = ({ onSwitch={setActiveAssistant} onDelete={onDelete} addAgent={addAgent} - addAssistant={addAssistant} + copyAssistant={copyAssistant} onCreateDefaultAssistant={onCreateDefaultAssistant} handleSortByChange={handleSortByChange} /> @@ -143,7 +143,7 @@ const Assistants: FC = ({ onSwitch={setActiveAssistant} onDelete={onDelete} addAgent={addAgent} - addAssistant={addAssistant} + copyAssistant={copyAssistant} onCreateDefaultAssistant={onCreateDefaultAssistant} handleSortByChange={handleSortByChange} /> diff --git a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx index 0c169a7c24..d33c26c613 100644 --- a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx +++ b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx @@ -17,7 +17,7 @@ import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant' import { useSettings } from '@renderer/hooks/useSettings' import { useTags } from '@renderer/hooks/useTags' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' -import { getDefaultModel, getDefaultTopic } from '@renderer/services/AssistantService' +import { getDefaultModel } from '@renderer/services/AssistantService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { Assistant, AssistantsSortType } from '@renderer/types' import { getLeadingEmoji, uuid } from '@renderer/utils' @@ -40,7 +40,7 @@ interface AssistantItemProps { onDelete: (assistant: Assistant) => void onCreateDefaultAssistant: () => void addAgent: (agent: any) => void - addAssistant: (assistant: Assistant) => void + copyAssistant: (assistant: Assistant) => void onTagClick?: (tag: string) => void handleSortByChange?: (sortType: AssistantsSortType) => void } @@ -52,7 +52,7 @@ const AssistantItem: FC = ({ onSwitch, onDelete, addAgent, - addAssistant, + copyAssistant, handleSortByChange }) => { const { t } = useTranslation() @@ -91,7 +91,7 @@ const AssistantItem: FC = ({ assistants, updateAssistants, addAgent, - addAssistant, + copyAssistant, onSwitch, onDelete, removeAllTopics, @@ -108,7 +108,7 @@ const AssistantItem: FC = ({ assistants, updateAssistants, addAgent, - addAssistant, + copyAssistant, onSwitch, onDelete, removeAllTopics, @@ -246,7 +246,7 @@ function getMenuItems({ assistants, updateAssistants, addAgent, - addAssistant, + copyAssistant, onSwitch, onDelete, removeAllTopics, @@ -268,9 +268,10 @@ function getMenuItems({ key: 'duplicate', icon: , onClick: async () => { - const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic(assistant.id)] } - addAssistant(_assistant) - onSwitch(_assistant) + const _assistant = copyAssistant(assistant) + if (_assistant) { + onSwitch(_assistant) + } } }, { diff --git a/src/renderer/src/store/assistants.ts b/src/renderer/src/store/assistants.ts index 70dbfa9841..238a5b44fd 100644 --- a/src/renderer/src/store/assistants.ts +++ b/src/renderer/src/store/assistants.ts @@ -32,6 +32,15 @@ const assistantsSlice = createSlice({ addAssistant: (state, action: PayloadAction) => { state.assistants.push(action.payload) }, + insertAssistant: (state, action: PayloadAction<{ index: number; assistant: Assistant }>) => { + const { index, assistant } = action.payload + + if (index < 0 || index > state.assistants.length) { + throw new Error(`InsertAssistant: index ${index} is out of bounds [0, ${state.assistants.length}]`) + } + + state.assistants.splice(index, 0, assistant) + }, removeAssistant: (state, action: PayloadAction<{ id: string }>) => { state.assistants = state.assistants.filter((c) => c.id !== action.payload.id) }, @@ -170,6 +179,7 @@ export const { updateDefaultAssistant, updateAssistants, addAssistant, + insertAssistant, removeAssistant, updateAssistant, addTopic, From 8ffdb4d1c272a7e8dbf49e8ef1e5e6d4db257ad4 Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Sun, 27 Jul 2025 17:30:52 +0800 Subject: [PATCH 005/112] perf(i18n): improve performance when getting i18n text (#8548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(i18n): 重构国际化标签映射为独立的键值映射对象 对象定义移出函数以优化性能 * docs(i18n): 更新国际化文档中的动态翻译推荐做法 修改中英文文档,添加通过维护键映射表来避免动态翻译键缺失的最佳实践 * chore(ProviderService): 添加注释 * chore: 移动注释位置 * refactor(ProviderService): 将获取提供者名称的逻辑移到utils中 移除冗余代码并使用统一的工具函数getFancyProviderName来获取提供者名称 --- docs/technical/how-to-i18n-en.md | 18 +- docs/technical/how-to-i18n-zh.md | 18 +- src/renderer/src/i18n/label.ts | 415 ++++++++++--------- src/renderer/src/services/ProviderService.ts | 8 +- 4 files changed, 248 insertions(+), 211 deletions(-) diff --git a/docs/technical/how-to-i18n-en.md b/docs/technical/how-to-i18n-en.md index 861810dc6b..1bbf7edca8 100644 --- a/docs/technical/how-to-i18n-en.md +++ b/docs/technical/how-to-i18n-en.md @@ -84,15 +84,21 @@ Since the plugin cannot track such usages, developers must manually verify the e ### Recommended Approach +To avoid missing keys, all dynamically translated texts should first maintain a `FooKeyMap`, then retrieve the translation text through a function. + +For example: + ```ts -const fruitLabels = { - apple: t('fruits.apple'), - banana: t('fruits.banana') +// src/renderer/src/i18n/label.ts +const themeModeKeyMap = { + dark: 'settings.theme.dark', + light: 'settings.theme.light', + system: 'settings.theme.system' } as const -const fruit = getFruit() - -const label = fruitLabels[fruit] +export const getThemeModeLabel = (key: string): string => { + return themeModeKeyMap[key] ? t(themeModeKeyMap[key]) : key +} ``` By avoiding template strings, you gain better developer experience, more reliable translation checks, and a more maintainable codebase. diff --git a/docs/technical/how-to-i18n-zh.md b/docs/technical/how-to-i18n-zh.md index e4fd9c637d..5d0a93c369 100644 --- a/docs/technical/how-to-i18n-zh.md +++ b/docs/technical/how-to-i18n-zh.md @@ -78,15 +78,21 @@ i18n ally是一个强大的VSCode插件,它能在开发阶段提供实时反 ### 推荐做法 +为了避免键的缺失,所有需要动态翻译的文本都应当先维护一个`FooKeyMap`,再通过函数获取翻译文本。 + +例如: + ```ts -const fruitLabels = { - apple: t('fruits.apple'), - banana: t('fruits.banana') +// src/renderer/src/i18n/label.ts +const themeModeKeyMap = { + dark: 'settings.theme.dark', + light: 'settings.theme.light', + system: 'settings.theme.system' } as const -const fruit = getFruit() - -const label = fruitLabels[fruit] +export const getThemeModeLabel = (key: string): string => { + return themeModeKeyMap[key] ? t(themeModeKeyMap[key]) : key +} ``` 通过避免模板字符串,可以获得更好的开发体验、更可靠的翻译检查以及更易维护的代码库。 diff --git a/src/renderer/src/i18n/label.ts b/src/renderer/src/i18n/label.ts index 5e84c9bf4a..bb1cadb766 100644 --- a/src/renderer/src/i18n/label.ts +++ b/src/renderer/src/i18n/label.ts @@ -1,245 +1,274 @@ +/** + * 对于需要动态获取的翻译文本: + * 1. 储存 key -> i18n-key 的 keyMap + * 2. 通过函数翻译文本 + */ + import i18n from './index' const t = i18n.t -/** 使用函数形式是为了动态获取,如果使用静态对象的话,导出的对象将不会随语言切换而改变 */ +const providerKeyMap = { + '302ai': 'provider.302ai', + aihubmix: 'provider.aihubmix', + alayanew: 'provider.alayanew', + anthropic: 'provider.anthropic', + 'azure-openai': 'provider.azure-openai', + baichuan: 'provider.baichuan', + 'baidu-cloud': 'provider.baidu-cloud', + burncloud: 'provider.burncloud', + cephalon: 'provider.cephalon', + copilot: 'provider.copilot', + dashscope: 'provider.dashscope', + deepseek: 'provider.deepseek', + dmxapi: 'provider.dmxapi', + doubao: 'provider.doubao', + fireworks: 'provider.fireworks', + gemini: 'provider.gemini', + 'gitee-ai': 'provider.gitee-ai', + github: 'provider.github', + gpustack: 'provider.gpustack', + grok: 'provider.grok', + groq: 'provider.groq', + hunyuan: 'provider.hunyuan', + hyperbolic: 'provider.hyperbolic', + infini: 'provider.infini', + jina: 'provider.jina', + lanyun: 'provider.lanyun', + lmstudio: 'provider.lmstudio', + minimax: 'provider.minimax', + mistral: 'provider.mistral', + modelscope: 'provider.modelscope', + moonshot: 'provider.moonshot', + 'new-api': 'provider.new-api', + nvidia: 'provider.nvidia', + o3: 'provider.o3', + ocoolai: 'provider.ocoolai', + ollama: 'provider.ollama', + openai: 'provider.openai', + openrouter: 'provider.openrouter', + perplexity: 'provider.perplexity', + ph8: 'provider.ph8', + ppio: 'provider.ppio', + qiniu: 'provider.qiniu', + qwenlm: 'provider.qwenlm', + silicon: 'provider.silicon', + stepfun: 'provider.stepfun', + 'tencent-cloud-ti': 'provider.tencent-cloud-ti', + together: 'provider.together', + tokenflux: 'provider.tokenflux', + vertexai: 'provider.vertexai', + voyageai: 'provider.voyageai', + xirang: 'provider.xirang', + yi: 'provider.yi', + zhinao: 'provider.zhinao', + zhipu: 'provider.zhipu' +} as const +/** + * 获取内置供应商的本地化标签 + * @param key - 供应商的key + * @returns 本地化后的供应商名称 + * @remarks + * 该函数仅用于获取内置供应商的 i18n label + * + * 对于可能处理自定义供应商的情况,使用 getProviderName 或 getFancyProviderName 更安全 + */ export const getProviderLabel = (key: string): string => { - const labelMap = { - '302ai': t('provider.302ai'), - aihubmix: t('provider.aihubmix'), - alayanew: t('provider.alayanew'), - anthropic: t('provider.anthropic'), - 'azure-openai': t('provider.azure-openai'), - baichuan: t('provider.baichuan'), - 'baidu-cloud': t('provider.baidu-cloud'), - burncloud: t('provider.burncloud'), - cephalon: t('provider.cephalon'), - copilot: t('provider.copilot'), - dashscope: t('provider.dashscope'), - deepseek: t('provider.deepseek'), - dmxapi: t('provider.dmxapi'), - doubao: t('provider.doubao'), - fireworks: t('provider.fireworks'), - gemini: t('provider.gemini'), - 'gitee-ai': t('provider.gitee-ai'), - github: t('provider.github'), - gpustack: t('provider.gpustack'), - grok: t('provider.grok'), - groq: t('provider.groq'), - hunyuan: t('provider.hunyuan'), - hyperbolic: t('provider.hyperbolic'), - infini: t('provider.infini'), - jina: t('provider.jina'), - lanyun: t('provider.lanyun'), - lmstudio: t('provider.lmstudio'), - minimax: t('provider.minimax'), - mistral: t('provider.mistral'), - modelscope: t('provider.modelscope'), - moonshot: t('provider.moonshot'), - 'new-api': t('provider.new-api'), - nvidia: t('provider.nvidia'), - o3: t('provider.o3'), - ocoolai: t('provider.ocoolai'), - ollama: t('provider.ollama'), - openai: t('provider.openai'), - openrouter: t('provider.openrouter'), - perplexity: t('provider.perplexity'), - ph8: t('provider.ph8'), - ppio: t('provider.ppio'), - qiniu: t('provider.qiniu'), - qwenlm: t('provider.qwenlm'), - silicon: t('provider.silicon'), - stepfun: t('provider.stepfun'), - 'tencent-cloud-ti': t('provider.tencent-cloud-ti'), - together: t('provider.together'), - tokenflux: t('provider.tokenflux'), - vertexai: t('provider.vertexai'), - voyageai: t('provider.voyageai'), - xirang: t('provider.xirang'), - yi: t('provider.yi'), - zhinao: t('provider.zhinao'), - zhipu: t('provider.zhipu') - } as const - return labelMap[key] ?? key + return providerKeyMap[key] ? t(providerKeyMap[key]) : key } +const progressKeyMap = { + completed: 'backup.progress.completed', + compressing: 'backup.progress.compressing', + copying_files: 'backup.progress.copying_files', + preparing: 'backup.progress.preparing', + title: 'backup.progress.title', + writing_data: 'backup.progress.writing_data' +} as const + export const getProgressLabel = (key: string): string => { - const labelMap = { - completed: t('backup.progress.completed'), - compressing: t('backup.progress.compressing'), - copying_files: t('backup.progress.copying_files'), - preparing: t('backup.progress.preparing'), - title: t('backup.progress.title'), - writing_data: t('backup.progress.writing_data') - } as const - return labelMap[key] ?? key + return progressKeyMap[key] ? t(progressKeyMap[key]) : key } +const titleKeyMap = { + agents: 'title.agents', + apps: 'title.apps', + files: 'title.files', + home: 'title.home', + knowledge: 'title.knowledge', + launchpad: 'title.launchpad', + 'mcp-servers': 'title.mcp-servers', + memories: 'title.memories', + paintings: 'title.paintings', + settings: 'title.settings', + translate: 'title.translate' +} as const + export const getTitleLabel = (key: string): string => { - const labelMap = { - agents: t('title.agents'), - apps: t('title.apps'), - files: t('title.files'), - home: t('title.home'), - knowledge: t('title.knowledge'), - launchpad: t('title.launchpad'), - 'mcp-servers': t('title.mcp-servers'), - memories: t('title.memories'), - paintings: t('title.paintings'), - settings: t('title.settings'), - translate: t('title.translate') - } as const - return labelMap[key] ?? key + return titleKeyMap[key] ? t(titleKeyMap[key]) : key } +const themeModeKeyMap = { + dark: 'settings.theme.dark', + light: 'settings.theme.light', + system: 'settings.theme.system' +} as const + export const getThemeModeLabel = (key: string): string => { - const labelMap = { - dark: t('settings.theme.dark'), - light: t('settings.theme.light'), - system: t('settings.theme.system') - } as const - return labelMap[key] ?? key + return themeModeKeyMap[key] ? t(themeModeKeyMap[key]) : key } +const sidebarIconKeyMap = { + assistants: 'assistants.title', + agents: 'agents.title', + paintings: 'paintings.title', + translate: 'translate.title', + minapp: 'minapp.title', + knowledge: 'knowledge.title', + files: 'files.title' +} as const + export const getSidebarIconLabel = (key: string): string => { - const labelMap = { - assistants: t('assistants.title'), - agents: t('agents.title'), - paintings: t('paintings.title'), - translate: t('translate.title'), - minapp: t('minapp.title'), - knowledge: t('knowledge.title'), - files: t('files.title') - } as const - return labelMap[key] ?? key + return sidebarIconKeyMap[key] ? t(sidebarIconKeyMap[key]) : key } +const shortcutKeyMap = { + action: 'settings.shortcuts.action', + actions: 'settings.shortcuts.actions', + clear_shortcut: 'settings.shortcuts.clear_shortcut', + clear_topic: 'settings.shortcuts.clear_topic', + copy_last_message: 'settings.shortcuts.copy_last_message', + enabled: 'settings.shortcuts.enabled', + exit_fullscreen: 'settings.shortcuts.exit_fullscreen', + label: 'settings.shortcuts.label', + mini_window: 'settings.shortcuts.mini_window', + new_topic: 'settings.shortcuts.new_topic', + press_shortcut: 'settings.shortcuts.press_shortcut', + reset_defaults: 'settings.shortcuts.reset_defaults', + reset_defaults_confirm: 'settings.shortcuts.reset_defaults_confirm', + reset_to_default: 'settings.shortcuts.reset_to_default', + search_message: 'settings.shortcuts.search_message', + search_message_in_chat: 'settings.shortcuts.search_message_in_chat', + selection_assistant_select_text: 'settings.shortcuts.selection_assistant_select_text', + selection_assistant_toggle: 'settings.shortcuts.selection_assistant_toggle', + show_app: 'settings.shortcuts.show_app', + show_settings: 'settings.shortcuts.show_settings', + title: 'settings.shortcuts.title', + toggle_new_context: 'settings.shortcuts.toggle_new_context', + toggle_show_assistants: 'settings.shortcuts.toggle_show_assistants', + toggle_show_topics: 'settings.shortcuts.toggle_show_topics', + zoom_in: 'settings.shortcuts.zoom_in', + zoom_out: 'settings.shortcuts.zoom_out', + zoom_reset: 'settings.shortcuts.zoom_reset' +} as const + export const getShortcutLabel = (key: string): string => { - const labelMap = { - action: t('settings.shortcuts.action'), - actions: t('settings.shortcuts.actions'), - clear_shortcut: t('settings.shortcuts.clear_shortcut'), - clear_topic: t('settings.shortcuts.clear_topic'), - copy_last_message: t('settings.shortcuts.copy_last_message'), - enabled: t('settings.shortcuts.enabled'), - exit_fullscreen: t('settings.shortcuts.exit_fullscreen'), - label: t('settings.shortcuts.label'), - mini_window: t('settings.shortcuts.mini_window'), - new_topic: t('settings.shortcuts.new_topic'), - press_shortcut: t('settings.shortcuts.press_shortcut'), - reset_defaults: t('settings.shortcuts.reset_defaults'), - reset_defaults_confirm: t('settings.shortcuts.reset_defaults_confirm'), - reset_to_default: t('settings.shortcuts.reset_to_default'), - search_message: t('settings.shortcuts.search_message'), - search_message_in_chat: t('settings.shortcuts.search_message_in_chat'), - selection_assistant_select_text: t('settings.shortcuts.selection_assistant_select_text'), - selection_assistant_toggle: t('settings.shortcuts.selection_assistant_toggle'), - show_app: t('settings.shortcuts.show_app'), - show_settings: t('settings.shortcuts.show_settings'), - title: t('settings.shortcuts.title'), - toggle_new_context: t('settings.shortcuts.toggle_new_context'), - toggle_show_assistants: t('settings.shortcuts.toggle_show_assistants'), - toggle_show_topics: t('settings.shortcuts.toggle_show_topics'), - zoom_in: t('settings.shortcuts.zoom_in'), - zoom_out: t('settings.shortcuts.zoom_out'), - zoom_reset: t('settings.shortcuts.zoom_reset') - } as const - return labelMap[key] ?? key + return shortcutKeyMap[key] ? t(shortcutKeyMap[key]) : key } +const selectionDescriptionKeyMap = { + mac: 'selection.settings.toolbar.trigger_mode.description_note.mac', + windows: 'selection.settings.toolbar.trigger_mode.description_note.windows' +} as const + export const getSelectionDescriptionLabel = (key: string): string => { - const labelMap = { - mac: t('selection.settings.toolbar.trigger_mode.description_note.mac'), - windows: t('selection.settings.toolbar.trigger_mode.description_note.windows') - } as const - return labelMap[key] ?? key + return selectionDescriptionKeyMap[key] ? t(selectionDescriptionKeyMap[key]) : key } +const paintingsImageSizeOptionsKeyMap = { + auto: 'paintings.image_size_options.auto' +} as const + export const getPaintingsImageSizeOptionsLabel = (key: string): string => { - const labelMap = { - auto: t('paintings.image_size_options.auto') - } as const - return labelMap[key] ?? key + return paintingsImageSizeOptionsKeyMap[key] ? t(paintingsImageSizeOptionsKeyMap[key]) : key } +const paintingsQualityOptionsKeyMap = { + auto: 'paintings.quality_options.auto', + high: 'paintings.quality_options.high', + low: 'paintings.quality_options.low', + medium: 'paintings.quality_options.medium' +} as const + export const getPaintingsQualityOptionsLabel = (key: string): string => { - const labelMap = { - auto: t('paintings.quality_options.auto'), - high: t('paintings.quality_options.high'), - low: t('paintings.quality_options.low'), - medium: t('paintings.quality_options.medium') - } as const - return labelMap[key] ?? key + return paintingsQualityOptionsKeyMap[key] ? t(paintingsQualityOptionsKeyMap[key]) : key } +const paintingsModerationOptionsKeyMap = { + auto: 'paintings.moderation_options.auto', + low: 'paintings.moderation_options.low' +} as const + export const getPaintingsModerationOptionsLabel = (key: string): string => { - const labelMap = { - auto: t('paintings.moderation_options.auto'), - low: t('paintings.moderation_options.low') - } as const - return labelMap[key] ?? key + return paintingsModerationOptionsKeyMap[key] ? t(paintingsModerationOptionsKeyMap[key]) : key } +const paintingsBackgroundOptionsKeyMap = { + auto: 'paintings.background_options.auto', + opaque: 'paintings.background_options.opaque', + transparent: 'paintings.background_options.transparent' +} as const + export const getPaintingsBackgroundOptionsLabel = (key: string): string => { - const labelMap = { - auto: t('paintings.background_options.auto'), - opaque: t('paintings.background_options.opaque'), - transparent: t('paintings.background_options.transparent') - } as const - return labelMap[key] ?? key + return paintingsBackgroundOptionsKeyMap[key] ? t(paintingsBackgroundOptionsKeyMap[key]) : key } +const mcpTypeKeyMap = { + inMemory: 'settings.mcp.types.inMemory', + sse: 'settings.mcp.types.sse', + stdio: 'settings.mcp.types.stdio', + streamableHttp: 'settings.mcp.types.streamableHttp' +} as const + export const getMcpTypeLabel = (key: string): string => { - const labelMap = { - inMemory: t('settings.mcp.types.inMemory'), - sse: t('settings.mcp.types.sse'), - stdio: t('settings.mcp.types.stdio'), - streamableHttp: t('settings.mcp.types.streamableHttp') - } as const - return labelMap[key] ?? key + return mcpTypeKeyMap[key] ? t(mcpTypeKeyMap[key]) : key } +const miniappsStatusKeyMap = { + visible: 'settings.miniapps.visible', + disabled: 'settings.miniapps.disabled' +} as const + export const getMiniappsStatusLabel = (key: string): string => { - const labelMap = { - visible: t('settings.miniapps.visible'), - disabled: t('settings.miniapps.disabled') - } as const - return labelMap[key] ?? key + return miniappsStatusKeyMap[key] ? t(miniappsStatusKeyMap[key]) : key } +const httpMessageKeyMap = { + '400': 'error.http.400', + '401': 'error.http.401', + '403': 'error.http.403', + '404': 'error.http.404', + '429': 'error.http.429', + '500': 'error.http.500', + '502': 'error.http.502', + '503': 'error.http.503', + '504': 'error.http.504' +} as const + export const getHttpMessageLabel = (key: string): string => { - const labelMap = { - '400': t('error.http.400'), - '401': t('error.http.401'), - '403': t('error.http.403'), - '404': t('error.http.404'), - '429': t('error.http.429'), - '500': t('error.http.500'), - '502': t('error.http.502'), - '503': t('error.http.503'), - '504': t('error.http.504') - } as const - return labelMap[key] ?? key + return httpMessageKeyMap[key] ? t(httpMessageKeyMap[key]) : key } +const reasoningEffortOptionsKeyMap = { + auto: 'assistants.settings.reasoning_effort.default', + high: 'assistants.settings.reasoning_effort.high', + label: 'assistants.settings.reasoning_effort.label', + low: 'assistants.settings.reasoning_effort.low', + medium: 'assistants.settings.reasoning_effort.medium', + off: 'assistants.settings.reasoning_effort.off' +} as const + export const getReasoningEffortOptionsLabel = (key: string): string => { - const labelMap = { - auto: t('assistants.settings.reasoning_effort.default'), - high: t('assistants.settings.reasoning_effort.high'), - label: t('assistants.settings.reasoning_effort.label'), - low: t('assistants.settings.reasoning_effort.low'), - medium: t('assistants.settings.reasoning_effort.medium'), - off: t('assistants.settings.reasoning_effort.off') - } as const - return labelMap[key] ?? key + return reasoningEffortOptionsKeyMap[key] ? t(reasoningEffortOptionsKeyMap[key]) : key } +const fileFieldKeyMap = { + created_at: 'files.created_at', + size: 'files.size', + name: 'files.name' +} as const + export const getFileFieldLabel = (key: string): string => { - const labelMap = { - created_at: t('files.created_at'), - size: t('files.size'), - name: t('files.name') - } as const - return labelMap[key] ?? key + return fileFieldKeyMap[key] ? t(fileFieldKeyMap[key]) : key } diff --git a/src/renderer/src/services/ProviderService.ts b/src/renderer/src/services/ProviderService.ts index 48a20b0533..51d62c251d 100644 --- a/src/renderer/src/services/ProviderService.ts +++ b/src/renderer/src/services/ProviderService.ts @@ -1,6 +1,6 @@ -import { getProviderLabel } from '@renderer/i18n/label' import store from '@renderer/store' import { Provider } from '@renderer/types' +import { getFancyProviderName } from '@renderer/utils' export function getProviderName(id: string) { const provider = store.getState().llm.providers.find((p) => p.id === id) @@ -8,11 +8,7 @@ export function getProviderName(id: string) { return '' } - if (provider.isSystem) { - return getProviderLabel(provider.id) ?? provider.name - } - - return provider?.name + return getFancyProviderName(provider) } export function isProviderSupportAuth(provider: Provider) { From 2e87c76b6e7b845cb6ec3a7e26ddc07510053e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=87=AA=E7=94=B1=E7=9A=84=E4=B8=96=E7=95=8C=E4=BA=BA?= <3196812536@qq.com> Date: Sun, 27 Jul 2025 19:30:46 +0800 Subject: [PATCH 006/112] fix: pangu-pro-moe not reasoning (#8572) --- .../src/assets/images/models/pangu.svg | 99 +++++++++++++++++++ src/renderer/src/config/models.ts | 7 +- 2 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/assets/images/models/pangu.svg diff --git a/src/renderer/src/assets/images/models/pangu.svg b/src/renderer/src/assets/images/models/pangu.svg new file mode 100644 index 0000000000..05894dc5b3 --- /dev/null +++ b/src/renderer/src/assets/images/models/pangu.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 398d9f1b55..ca673a0230 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -108,6 +108,7 @@ import NvidiaModelLogo from '@renderer/assets/images/models/nvidia.png' import NvidiaModelLogoDark from '@renderer/assets/images/models/nvidia_dark.png' import PalmModelLogo from '@renderer/assets/images/models/palm.png' import PalmModelLogoDark from '@renderer/assets/images/models/palm_dark.png' +import PanguModelLogo from '@renderer/assets/images/models/pangu.svg' import { default as PerplexityModelLogo, default as PerplexityModelLogoDark @@ -413,7 +414,8 @@ export function getModelLogo(modelId: string) { 'bge-': BgeModelLogo, 'voyage-': VoyageModelLogo, tokenflux: isLight ? TokenFluxModelLogo : TokenFluxModelLogoDark, - 'nomic-': NomicLogo + 'nomic-': NomicLogo, + 'pangu-': PanguModelLogo } for (const key in logoMap) { @@ -2748,7 +2750,8 @@ export function isReasoningModel(model?: Model): boolean { isPerplexityReasoningModel(model) || model.id.toLowerCase().includes('glm-z1') || model.id.toLowerCase().includes('magistral') || - model.id.toLowerCase().includes('minimax-m1') + model.id.toLowerCase().includes('minimax-m1') || + model.id.toLowerCase().includes('pangu-pro-moe') ) { return true } From 392f1e0a244bb2aa2656b88d6ada5add7b89d75f Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Sun, 27 Jul 2025 21:56:30 +0800 Subject: [PATCH 007/112] fix(ModelEditContent): preserve model price settings (#8560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(ModelEditContent): 为价格输入字段添加默认值 --- src/renderer/src/components/ModelList/ModelEditContent.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/renderer/src/components/ModelList/ModelEditContent.tsx b/src/renderer/src/components/ModelList/ModelEditContent.tsx index f4d8811309..b1085908c7 100644 --- a/src/renderer/src/components/ModelList/ModelEditContent.tsx +++ b/src/renderer/src/components/ModelList/ModelEditContent.tsx @@ -373,6 +373,7 @@ const ModelEditContent: FC = ({ provider, model, onUpdate setCurrencySymbol(e.target.value)} /> @@ -382,6 +383,7 @@ const ModelEditContent: FC = ({ provider, model, onUpdate = ({ provider, model, onUpdate Date: Sun, 27 Jul 2025 22:34:05 +0800 Subject: [PATCH 008/112] feat(contentsearch): optimize searchbar UI (#8556) --- src/renderer/src/components/ContentSearch.tsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/components/ContentSearch.tsx b/src/renderer/src/components/ContentSearch.tsx index 6842312137..499323d39a 100644 --- a/src/renderer/src/components/ContentSearch.tsx +++ b/src/renderer/src/components/ContentSearch.tsx @@ -1,3 +1,5 @@ +import { useSettings } from '@renderer/hooks/useSettings' +import { useShowTopics } from '@renderer/hooks/useStore' import { ToolbarButton } from '@renderer/pages/home/Inputbar/Inputbar' import NarrowLayout from '@renderer/pages/home/Messages/NarrowLayout' import { Tooltip } from 'antd' @@ -126,6 +128,8 @@ const findRangesInTarget = ( // eslint-disable-next-line @eslint-react/no-forward-ref export const ContentSearch = React.forwardRef( ({ searchTarget, filter, includeUser = false, onIncludeUserChange }, ref) => { + const { topicPosition } = useSettings() + const { showTopics } = useShowTopics() const target: HTMLElement | null = (() => { if (searchTarget instanceof HTMLElement) { return searchTarget @@ -333,10 +337,12 @@ export const ContentSearch = React.forwardRef( searchInputFocus() } + const isRightTopicsVisible = topicPosition === 'right' && showTopics + return ( - + ` border: 1px solid var(--color-primary); border-radius: 10px; transition: all 0.2s ease; position: fixed; - top: 15px; - left: 20px; - right: 20px; + top: 30px; + left: 50%; + transform: translateX(-50%); + width: ${(props) => + props.$isRightTopicsVisible + ? 'min(60%, calc(100vw - 400px))' + : 'min(60%, calc(100vw - 140px))'}; /* 容器2/3宽度,并考虑右侧避让 */ margin-bottom: 5px; padding: 5px 15px; display: flex; @@ -448,7 +458,7 @@ const ToolBar = styled.div` display: flex; flex-direction: row; align-items: center; - gap: tpx; + gap: 2px; ` const Separator = styled.div` @@ -463,8 +473,8 @@ const Separator = styled.div` const SearchResults = styled.div` display: flex; justify-content: center; - width: 80px; - margin: 0 2px; + width: 60px; + margin: 0 1px; flex: 0 0 auto; color: var(--color-text-1); font-size: 14px; From 5bafb3f1b7f3dde79e7f5785a8e4c27926567ef2 Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Mon, 28 Jul 2025 01:10:57 +0800 Subject: [PATCH 009/112] feat(inputbar): drop text into inputbar (#8579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(输入栏): 添加从拖拽事件获取文本的功能 新增getTextFromDropEvent工具函数,用于从拖拽事件中提取文本数据 在Inputbar组件中集成该功能,支持拖拽文本到输入框 --- src/renderer/src/pages/home/Inputbar/Inputbar.tsx | 13 +++++++++++-- src/renderer/src/utils/input.ts | 4 ++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 17739c171c..ec67d2cd3e 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -37,7 +37,12 @@ import { Assistant, FileType, FileTypes, KnowledgeBase, KnowledgeItem, Model, To import type { MessageInputBaseParams } from '@renderer/types/newMessage' import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils' import { formatQuotedText } from '@renderer/utils/formats' -import { getFilesFromDropEvent, getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input' +import { + getFilesFromDropEvent, + getSendMessageShortcutLabel, + getTextFromDropEvent, + isSendMessageKeyPressed +} from '@renderer/utils/input' import { getLanguageByLangcode } from '@renderer/utils/translate' import { documentExts, imageExts, textExts } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' @@ -567,6 +572,10 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = e.stopPropagation() setIsFileDragging(false) + const data = await getTextFromDropEvent(e) + + setText(text + data) + const files = await getFilesFromDropEvent(e).catch((err) => { logger.error('handleDrop:', err) return null @@ -591,7 +600,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = } } }, - [supportedExts, t] + [supportedExts, t, text] ) const onTranslated = (translatedText: string) => { diff --git a/src/renderer/src/utils/input.ts b/src/renderer/src/utils/input.ts index 53af52252a..85eb804255 100644 --- a/src/renderer/src/utils/input.ts +++ b/src/renderer/src/utils/input.ts @@ -5,6 +5,10 @@ import { FileMetadata } from '@renderer/types' const logger = loggerService.withContext('Utils:Input') +export const getTextFromDropEvent = async (e: React.DragEvent): Promise => { + return e.dataTransfer.getData('text') +} + export const getFilesFromDropEvent = async (e: React.DragEvent): Promise => { if (e.dataTransfer.files.length > 0) { // 使用新的API获取文件路径 From c4182a950f47d6a242ccd2d572221070f39a415b Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Mon, 28 Jul 2025 15:45:52 +0800 Subject: [PATCH 010/112] fix: add isPathInside functionality to check path relationships (#8590) * feat(ipc): add isPathInside functionality to check path relationships - Introduced a new IPC channel for checking if a path is inside another path, enhancing path validation capabilities. - Implemented the isPathInside function in the file utility, which accurately determines parent-child path relationships, handling edge cases. - Updated relevant components to utilize the new isPathInside function for validating app data and backup paths, ensuring better user experience and error handling. - Added comprehensive tests for isPathInside to cover various scenarios, including edge cases and error handling. * format code --- packages/shared/IpcChannel.ts | 1 + src/main/ipc.ts | 7 +- src/main/utils/__tests__/file.test.ts | 161 +++++++++++++++++- src/main/utils/file.ts | 36 ++++ src/preload/index.ts | 2 + .../settings/DataSettings/DataSettings.tsx | 6 +- .../DataSettings/LocalBackupSettings.tsx | 4 +- 7 files changed, 211 insertions(+), 6 deletions(-) diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index f3894dc97b..cfc00913aa 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -21,6 +21,7 @@ export enum IpcChannel { App_Select = 'app:select', App_HasWritePermission = 'app:has-write-permission', App_ResolvePath = 'app:resolve-path', + App_IsPathInside = 'app:is-path-inside', App_Copy = 'app:copy', App_SetStopQuitApp = 'app:set-stop-quit-app', App_SetAppDataPath = 'app:set-app-data-path', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 899a280187..a3e3ef4432 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -55,7 +55,7 @@ import { setOpenLinkExternal } from './services/WebviewService' import { windowService } from './services/WindowService' import { calculateDirectorySize, getResourcePath } from './utils' import { decrypt, encrypt } from './utils/aes' -import { getCacheDir, getConfigDir, getFilesDir, hasWritePermission, untildify } from './utils/file' +import { getCacheDir, getConfigDir, getFilesDir, hasWritePermission, isPathInside, untildify } from './utils/file' import { updateAppDataConfig } from './utils/init' import { compress, decompress } from './utils/zip' @@ -294,6 +294,11 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { return path.resolve(untildify(filePath)) }) + // Check if a path is inside another path (proper parent-child relationship) + ipcMain.handle(IpcChannel.App_IsPathInside, async (_, childPath: string, parentPath: string) => { + return isPathInside(childPath, parentPath) + }) + // Set app data path ipcMain.handle(IpcChannel.App_SetAppDataPath, async (_, filePath: string) => { updateAppDataConfig(filePath) diff --git a/src/main/utils/__tests__/file.test.ts b/src/main/utils/__tests__/file.test.ts index 8c651f237c..a95665f815 100644 --- a/src/main/utils/__tests__/file.test.ts +++ b/src/main/utils/__tests__/file.test.ts @@ -9,7 +9,16 @@ import { detectAll as detectEncodingAll } from 'jschardet' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { readTextFileWithAutoEncoding } from '../file' -import { getAllFiles, getAppConfigDir, getConfigDir, getFilesDir, getFileType, getTempDir, untildify } from '../file' +import { + getAllFiles, + getAppConfigDir, + getConfigDir, + getFilesDir, + getFileType, + getTempDir, + isPathInside, + untildify +} from '../file' // Mock dependencies vi.mock('node:fs') @@ -343,4 +352,154 @@ describe('file', () => { expect(untildify('~/folder_with_underscores')).toBe('/mock/home/folder_with_underscores') }) }) + + describe('isPathInside', () => { + beforeEach(() => { + // Mock path.resolve to simulate path resolution + vi.mocked(path.resolve).mockImplementation((...args) => { + const joined = args.join('/') + return joined.startsWith('/') ? joined : `/${joined}` + }) + + // Mock path.normalize to simulate path normalization + vi.mocked(path.normalize).mockImplementation((p) => p.replace(/\/+/g, '/')) + + // Mock path.relative to calculate relative paths + vi.mocked(path.relative).mockImplementation((from, to) => { + // Simple mock implementation for testing + const fromParts = from.split('/').filter((p) => p) + const toParts = to.split('/').filter((p) => p) + + // Find common prefix + let i = 0 + while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) { + i++ + } + + // Calculate relative path + const upLevels = fromParts.length - i + const downPath = toParts.slice(i) + + if (upLevels === 0 && downPath.length === 0) { + return '' + } + + const result = ['..'.repeat(upLevels), ...downPath].filter((p) => p).join('/') + return result || '.' + }) + + // Mock path.isAbsolute + vi.mocked(path.isAbsolute).mockImplementation((p) => p.startsWith('/')) + }) + + describe('basic parent-child relationships', () => { + it('should return true when child is inside parent', () => { + expect(isPathInside('/root/test/child', '/root/test')).toBe(true) + expect(isPathInside('/root/test/deep/child', '/root/test')).toBe(true) + expect(isPathInside('child/deep', 'child')).toBe(true) + }) + + it('should return false when child is not inside parent', () => { + expect(isPathInside('/root/test', '/root/test/child')).toBe(false) + expect(isPathInside('/root/other', '/root/test')).toBe(false) + expect(isPathInside('/different/path', '/root/test')).toBe(false) + expect(isPathInside('child', 'child/deep')).toBe(false) + }) + + it('should return true when paths are the same', () => { + expect(isPathInside('/root/test', '/root/test')).toBe(true) + expect(isPathInside('child', 'child')).toBe(true) + }) + }) + + describe('edge cases that startsWith cannot handle', () => { + it('should correctly distinguish similar path names', () => { + // The problematic case mentioned by user + expect(isPathInside('/root/test aaa', '/root/test')).toBe(false) + expect(isPathInside('/root/test', '/root/test aaa')).toBe(false) + + // More similar cases + expect(isPathInside('/home/user-data', '/home/user')).toBe(false) + expect(isPathInside('/home/user', '/home/user-data')).toBe(false) + expect(isPathInside('/var/log-backup', '/var/log')).toBe(false) + }) + + it('should handle paths with spaces correctly', () => { + expect(isPathInside('/path with spaces/child', '/path with spaces')).toBe(true) + expect(isPathInside('/path with spaces', '/path with spaces/child')).toBe(false) + }) + + it('should handle Windows-style paths', () => { + // Mock for Windows paths + vi.mocked(path.resolve).mockImplementation((...args) => { + const joined = args.join('\\').replace(/\//g, '\\') + return joined.match(/^[A-Z]:/) ? joined : `C:${joined}` + }) + + vi.mocked(path.normalize).mockImplementation((p) => p.replace(/\\+/g, '\\')) + + // Mock path.relative for Windows paths + vi.mocked(path.relative).mockImplementation((from, to) => { + const fromParts = from.split('\\').filter((p) => p && p !== 'C:') + const toParts = to.split('\\').filter((p) => p && p !== 'C:') + + // Find common prefix + let i = 0 + while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) { + i++ + } + + // Calculate relative path + const upLevels = fromParts.length - i + const downPath = toParts.slice(i) + + if (upLevels === 0 && downPath.length === 0) { + return '' + } + + const upPath = Array(upLevels).fill('..').join('\\') + const result = [upPath, ...downPath].filter((p) => p).join('\\') + return result || '.' + }) + + expect(isPathInside('C:\\Users\\test\\child', 'C:\\Users\\test')).toBe(true) + expect(isPathInside('C:\\Users\\test aaa', 'C:\\Users\\test')).toBe(false) + }) + }) + + describe('error handling', () => { + it('should return false when path operations throw errors', () => { + vi.mocked(path.resolve).mockImplementation(() => { + throw new Error('Path resolution failed') + }) + + expect(isPathInside('/any/path', '/any/parent')).toBe(false) + }) + }) + + describe('comparison with startsWith behavior', () => { + const testCases: [string, string, boolean, boolean][] = [ + ['/root/test aaa', '/root/test', false, true], // isPathInside vs startsWith + ['/root/test', '/root/test aaa', false, false], + ['/root/test/child', '/root/test', true, true], + ['/home/user-data', '/home/user', false, true] + ] + + it.each(testCases)( + 'should correctly handle %s vs %s', + (child: string, parent: string, expectedIsPathInside: boolean, expectedStartsWith: boolean) => { + const isPathInsideResult = isPathInside(child, parent) + const startsWithResult = child.startsWith(parent) + + expect(isPathInsideResult).toBe(expectedIsPathInside) + expect(startsWithResult).toBe(expectedStartsWith) + + // Verify that isPathInside gives different (correct) result in problematic cases + if (expectedIsPathInside !== expectedStartsWith) { + expect(isPathInsideResult).not.toBe(startsWithResult) + } + } + ) + }) + }) }) diff --git a/src/main/utils/file.ts b/src/main/utils/file.ts index 28a04f30fe..e73e546022 100644 --- a/src/main/utils/file.ts +++ b/src/main/utils/file.ts @@ -46,6 +46,42 @@ export async function hasWritePermission(dir: string) { } } +/** + * Check if a path is inside another path (proper parent-child relationship) + * This function correctly handles edge cases that string.startsWith() cannot handle, + * such as distinguishing between '/root/test' and '/root/test aaa' + * + * @param childPath - The path that might be inside the parent path + * @param parentPath - The path that might contain the child path + * @returns true if childPath is inside parentPath, false otherwise + */ +export function isPathInside(childPath: string, parentPath: string): boolean { + try { + const resolvedChild = path.resolve(childPath) + const resolvedParent = path.resolve(parentPath) + + // Normalize paths to handle different separators + const normalizedChild = path.normalize(resolvedChild) + const normalizedParent = path.normalize(resolvedParent) + + // Check if they are the same path + if (normalizedChild === normalizedParent) { + return true + } + + // Get relative path from parent to child + const relativePath = path.relative(normalizedParent, normalizedChild) + + // If relative path is empty, they are the same + // If relative path starts with '..', child is not inside parent + // If relative path is absolute, child is not inside parent + return relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath) + } catch (error) { + logger.error('Failed to check path relationship:', error as Error) + return false + } +} + export function getFileType(ext: string): FileTypes { ext = ext.toLowerCase() return fileTypeMap.get(ext) || FileTypes.OTHER diff --git a/src/preload/index.ts b/src/preload/index.ts index abf3d39593..5b492846d1 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -60,6 +60,8 @@ const api = { select: (options: Electron.OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.App_Select, options), hasWritePermission: (path: string) => ipcRenderer.invoke(IpcChannel.App_HasWritePermission, path), resolvePath: (path: string) => ipcRenderer.invoke(IpcChannel.App_ResolvePath, path), + isPathInside: (childPath: string, parentPath: string) => + ipcRenderer.invoke(IpcChannel.App_IsPathInside, childPath, parentPath), setAppDataPath: (path: string) => ipcRenderer.invoke(IpcChannel.App_SetAppDataPath, path), getDataPathFromArgs: () => ipcRenderer.invoke(IpcChannel.App_GetDataPathFromArgs), copy: (oldPath: string, newPath: string, occupiedDirs: string[] = []) => diff --git a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx index 47c16859ea..87f1881fcc 100644 --- a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx @@ -210,13 +210,15 @@ const DataSettings: FC = () => { } // check new app data path is not in old app data path - if (newAppDataPath.startsWith(appInfo.appDataPath)) { + const isInOldPath = await window.api.isPathInside(newAppDataPath, appInfo.appDataPath) + if (isInOldPath) { window.message.error(t('settings.data.app_data.select_error_same_path')) return } // check new app data path is not in app install path - if (newAppDataPath.startsWith(appInfo.installPath)) { + const isInInstallPath = await window.api.isPathInside(newAppDataPath, appInfo.installPath) + if (isInInstallPath) { window.message.error(t('settings.data.app_data.select_error_in_app_path')) return } diff --git a/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx b/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx index 53bd9973fc..388a6b94ca 100644 --- a/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx @@ -75,14 +75,14 @@ const LocalBackupSettings: React.FC = () => { // check new local backup dir is not in app data path // if is in app data path, show error - if (resolvedDir.startsWith(appInfo!.appDataPath)) { + if (await window.api.isPathInside(resolvedDir, appInfo!.appDataPath)) { window.message.error(t('settings.data.local.directory.select_error_app_data_path')) return false } // check new local backup dir is not in app install path // if is in app install path, show error - if (resolvedDir.startsWith(appInfo!.installPath)) { + if (await window.api.isPathInside(resolvedDir, appInfo!.installPath)) { window.message.error(t('settings.data.local.directory.select_error_in_app_install_path')) return false } From 536aa68389ea10d363e35ce8dc5d965a19566d01 Mon Sep 17 00:00:00 2001 From: lihqi Date: Mon, 28 Jul 2025 21:27:31 +0800 Subject: [PATCH 011/112] feat: add Top-P parameter toggle with default enabled state and improved UI styling (#8137) * feat: add Top-P parameter toggle with default enabled state and improved UI styling * fix: resolve undefined enableTopP issue in ppio models by using getAssistantSettings * refactor(api): Unify getTopP method to BaseApiClient * feat(settings): adjust layout of Top-P setting in assistant model settings * feat: add temperature parameter toggle control with UI and multi-language support * fix: Fix lint error * fix: Sort these imports * style(settings): refactor model settings layout and styles * chore: yarn sync:i18n --- .../src/aiCore/clients/BaseApiClient.ts | 13 +- .../clients/anthropic/AnthropicAPIClient.ts | 4 +- .../aiCore/clients/openai/OpenAIBaseClient.ts | 15 +- src/renderer/src/i18n/locales/zh-cn.json | 2 +- .../src/pages/home/Tabs/SettingsTab.tsx | 39 ++-- .../AssistantModelSettings.tsx | 167 ++++++++++-------- .../DefaultAssistantSettings.tsx | 156 +++++++++------- src/renderer/src/services/AssistantService.ts | 6 + src/renderer/src/types/index.ts | 8 +- 9 files changed, 246 insertions(+), 164 deletions(-) diff --git a/src/renderer/src/aiCore/clients/BaseApiClient.ts b/src/renderer/src/aiCore/clients/BaseApiClient.ts index 576381c875..9bb0f92789 100644 --- a/src/renderer/src/aiCore/clients/BaseApiClient.ts +++ b/src/renderer/src/aiCore/clients/BaseApiClient.ts @@ -8,6 +8,7 @@ import { import { REFERENCE_PROMPT } from '@renderer/config/prompts' import { getLMStudioKeepAliveTime } from '@renderer/hooks/useLMStudio' import { getStoreSetting } from '@renderer/hooks/useSettings' +import { getAssistantSettings } from '@renderer/services/AssistantService' import { SettingsState } from '@renderer/store/settings' import { Assistant, @@ -185,11 +186,19 @@ export abstract class BaseApiClient< } public getTemperature(assistant: Assistant, model: Model): number | undefined { - return isNotSupportTemperatureAndTopP(model) ? undefined : assistant.settings?.temperature + if (isNotSupportTemperatureAndTopP(model)) { + return undefined + } + const assistantSettings = getAssistantSettings(assistant) + return assistantSettings?.enableTemperature ? assistantSettings?.temperature : undefined } public getTopP(assistant: Assistant, model: Model): number | undefined { - return isNotSupportTemperatureAndTopP(model) ? undefined : assistant.settings?.topP + if (isNotSupportTemperatureAndTopP(model)) { + return undefined + } + const assistantSettings = getAssistantSettings(assistant) + return assistantSettings?.enableTopP ? assistantSettings?.topP : undefined } protected getServiceTier(model: Model) { diff --git a/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts b/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts index ae34a94d5c..6a73bf47ce 100644 --- a/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts +++ b/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts @@ -138,14 +138,14 @@ export class AnthropicAPIClient extends BaseApiClient< if (assistant.settings?.reasoning_effort && isClaudeReasoningModel(model)) { return undefined } - return assistant.settings?.temperature + return super.getTemperature(assistant, model) } override getTopP(assistant: Assistant, model: Model): number | undefined { if (assistant.settings?.reasoning_effort && isClaudeReasoningModel(model)) { return undefined } - return assistant.settings?.topP + return super.getTopP(assistant, model) } /** diff --git a/src/renderer/src/aiCore/clients/openai/OpenAIBaseClient.ts b/src/renderer/src/aiCore/clients/openai/OpenAIBaseClient.ts index 144dea130c..9e4042fa3c 100644 --- a/src/renderer/src/aiCore/clients/openai/OpenAIBaseClient.ts +++ b/src/renderer/src/aiCore/clients/openai/OpenAIBaseClient.ts @@ -1,7 +1,6 @@ import { loggerService } from '@logger' import { isClaudeReasoningModel, - isNotSupportTemperatureAndTopP, isOpenAIReasoningModel, isSupportedModel, isSupportedReasoningEffortOpenAIModel @@ -172,23 +171,17 @@ export abstract class OpenAIBaseClient< } override getTemperature(assistant: Assistant, model: Model): number | undefined { - if ( - isNotSupportTemperatureAndTopP(model) || - (assistant.settings?.reasoning_effort && isClaudeReasoningModel(model)) - ) { + if (assistant.settings?.reasoning_effort && isClaudeReasoningModel(model)) { return undefined } - return assistant.settings?.temperature + return super.getTemperature(assistant, model) } override getTopP(assistant: Assistant, model: Model): number | undefined { - if ( - isNotSupportTemperatureAndTopP(model) || - (assistant.settings?.reasoning_effort && isClaudeReasoningModel(model)) - ) { + if (assistant.settings?.reasoning_effort && isClaudeReasoningModel(model)) { return undefined } - return assistant.settings?.topP + return super.getTopP(assistant, model) } /** diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 8f5fb1e073..30bc78ebb0 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -465,7 +465,7 @@ }, "top_p": { "label": "Top-P", - "tip": "默认值为 1,值越小,AI 生成的内容越单调,也越容易理解;值越大,AI 回复的词汇围越大,越多样化" + "tip": "默认值为 1,值越小,AI 生成的内容越单调,也越容易理解;值越大,AI 回复的词汇范围越大,越多样化" } }, "suggestions": { diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index 317e3f7edf..7f0d91602c 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -68,6 +68,7 @@ const SettingsTab: FC = (props) => { const { themeNames } = useCodeStyle() const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE) + const [enableTemperature, setEnableTemperature] = useState(assistant?.settings?.enableTemperature ?? true) const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT) const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false) const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0) @@ -154,6 +155,7 @@ const SettingsTab: FC = (props) => { useEffect(() => { setTemperature(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE) + setEnableTemperature(assistant?.settings?.enableTemperature ?? true) setContextCount(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT) setEnableMaxTokens(assistant?.settings?.enableMaxTokens ?? false) setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS) @@ -193,19 +195,32 @@ const SettingsTab: FC = (props) => { + { + setEnableTemperature(enabled) + onUpdateAssistantSettings({ enableTemperature: enabled }) + }} + /> - - - - - + {enableTemperature ? ( + + + + + + ) : ( + + )} {t('chat.settings.context_count.label')} diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx index 0e16d855df..574fa86fc8 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx @@ -29,9 +29,11 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA const [toolUseMode, setToolUseMode] = useState(assistant?.settings?.toolUseMode ?? 'prompt') const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel) const [topP, setTopP] = useState(assistant?.settings?.topP ?? 1) + const [enableTopP, setEnableTopP] = useState(assistant?.settings?.enableTopP ?? true) const [customParameters, setCustomParameters] = useState( assistant?.settings?.customParameters ?? [] ) + const [enableTemperature, setEnableTemperature] = useState(assistant?.settings?.enableTemperature ?? true) const customParametersRef = useRef(customParameters) @@ -151,20 +153,24 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA const onReset = () => { setTemperature(DEFAULT_TEMPERATURE) + setEnableTemperature(true) setContextCount(DEFAULT_CONTEXTCOUNT) setEnableMaxTokens(false) setMaxTokens(0) setStreamOutput(true) setTopP(1) + setEnableTopP(true) setCustomParameters([]) setToolUseMode('prompt') updateAssistantSettings({ temperature: DEFAULT_TEMPERATURE, + enableTemperature: true, contextCount: DEFAULT_CONTEXTCOUNT, enableMaxTokens: false, maxTokens: 0, streamOutput: true, topP: 1, + enableTopP: true, customParameters: [], toolUseMode: 'prompt' }) @@ -226,86 +232,103 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA - - + + + - - - { - if (value !== null) { - setTemperature(value) - setTimeout(() => updateAssistantSettings({ temperature: value }), 500) - } - }} - style={{ width: '100%' }} - /> - - - - - - - + + { + setEnableTemperature(enabled) + updateAssistantSettings({ enableTemperature: enabled }) + }} + /> + + {enableTemperature && ( + + + + + + { + if (!isNull(value)) { + setTemperature(value) + setTimeout(() => updateAssistantSettings({ temperature: value }), 500) + } + }} + style={{ width: '100%' }} + /> + + + )} - - - - - - { - if (!isNull(value)) { - setTopP(value) - setTimeout(() => updateAssistantSettings({ topP: value }), 500) - } - }} - style={{ width: '100%' }} - /> - - - - - - - + + + + + + + + { + setEnableTopP(enabled) + updateAssistantSettings({ enableTopP: enabled }) + }} + /> + + {enableTopP && ( + + + + + + { + if (!isNull(value)) { + setTopP(value) + setTimeout(() => updateAssistantSettings({ topP: value }), 500) + } + }} + style={{ width: '100%' }} + /> + + + )} diff --git a/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx b/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx index 6f97b9762a..edf96505bd 100644 --- a/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx +++ b/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx @@ -13,15 +13,17 @@ import { Dispatch, FC, SetStateAction, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { SettingContainer, SettingSubtitle } from '..' +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 [contextCount, setContextCount] = useState(defaultAssistant.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT) const [enableMaxTokens, setEnableMaxTokens] = useState(defaultAssistant?.settings?.enableMaxTokens ?? false) const [maxTokens, setMaxTokens] = useState(defaultAssistant?.settings?.maxTokens ?? 0) const [topP, setTopP] = useState(defaultAssistant.settings?.topP ?? 1) + const [enableTopP, setEnableTopP] = useState(defaultAssistant.settings?.enableTopP ?? true) const [emoji, setEmoji] = useState(defaultAssistant.emoji || getLeadingEmoji(defaultAssistant.name) || '') const [name, setName] = useState( defaultAssistant.name.replace(getLeadingEmoji(defaultAssistant.name) || '', '').trim() @@ -36,11 +38,13 @@ const AssistantSettings: FC = () => { settings: { ...defaultAssistant.settings, temperature: settings.temperature ?? temperature, + enableTemperature: settings.enableTemperature ?? enableTemperature, contextCount: settings.contextCount ?? contextCount, enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens, maxTokens: settings.maxTokens ?? maxTokens, streamOutput: settings.streamOutput ?? true, - topP: settings.topP ?? topP + topP: settings.topP ?? topP, + enableTopP: settings.enableTopP ?? enableTopP } }) } @@ -61,20 +65,24 @@ const AssistantSettings: FC = () => { const onReset = () => { setTemperature(DEFAULT_TEMPERATURE) + setEnableTemperature(true) setContextCount(DEFAULT_CONTEXTCOUNT) setEnableMaxTokens(false) setMaxTokens(0) setTopP(1) + setEnableTopP(true) updateDefaultAssistant({ ...defaultAssistant, settings: { ...defaultAssistant.settings, temperature: DEFAULT_TEMPERATURE, + enableTemperature: true, contextCount: DEFAULT_CONTEXTCOUNT, enableMaxTokens: false, maxTokens: DEFAULT_MAX_TOKENS, streamOutput: true, - topP: 1 + topP: 1, + enableTopP: true } }) } @@ -96,9 +104,11 @@ const AssistantSettings: FC = () => { } return ( - + {t('common.name')} - + } arrow> @@ -129,84 +139,108 @@ const AssistantSettings: FC = () => { style={{ flex: 1 }} /> - {t('common.prompt')} + {t('common.prompt')}