From 8e33ff8d901660f4bc3dc1d435a453353d9be55c Mon Sep 17 00:00:00 2001 From: fullex <0xfullex@gmail.com> Date: Fri, 7 Nov 2025 12:02:28 +0800 Subject: [PATCH 001/173] docs: update test plan documentation to clarify upgrade behavior for RC and Beta channels --- docs/testplan-en.md | 2 ++ docs/testplan-zh.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/testplan-en.md b/docs/testplan-en.md index 0f7cd41473..fad894f22b 100644 --- a/docs/testplan-en.md +++ b/docs/testplan-en.md @@ -11,6 +11,8 @@ The Test Plan is divided into the RC channel and the Beta channel, with the foll Users can enable the "Test Plan" and select the version channel in the software's `Settings` > `About`. Please note that the versions in the "Test Plan" cannot guarantee data consistency, so be sure to back up your data before using them. +After enabling the RC channel or Beta channel, if a stable version is released, users will still be upgraded to the stable version. + Users are welcome to submit issues or provide feedback through other channels for any bugs encountered during testing. Your feedback is very important to us. ## Developer Guide diff --git a/docs/testplan-zh.md b/docs/testplan-zh.md index ed4913d4a4..77d25981de 100644 --- a/docs/testplan-zh.md +++ b/docs/testplan-zh.md @@ -11,6 +11,8 @@ 用户可以在软件的`设置`-`关于`中,开启“测试计划”并选择版本通道。请注意“测试计划”的版本无法保证数据的一致性,请使用前一定要备份数据。 +用户选择RC版通道或Beta版通道后,若发布了正式版,仍旧会升级到正式版。 + 用户在测试过程中发现的BUG,欢迎提交issue或通过其他渠道反馈。用户的反馈对我们非常重要。 ## 开发者指南 From bfef0c558039f47c7ce3ab8857bb87f022ef4d81 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 7 Nov 2025 18:01:37 +0800 Subject: [PATCH 002/173] feat(MCPSettings): enhance MCP server management and localization support - Updated auto-translation script to allow configurable max concurrent translations and delay via environment variables. - Added new translations for "discover", "fetch", "marketplaces", "providers", and "servers" across multiple locales (en-us, zh-cn, zh-tw, de-de, el-gr, es-es, fr-fr, ja-jp, pt-pt, ru-ru). - Improved MCPSettings UI by adjusting layout and adding a new provider settings component for better server management. - Refactored MCP server list and market list components for improved usability and styling consistency. --- scripts/auto-translate-i18n.ts | 6 +- src/renderer/src/i18n/locales/en-us.json | 8 + src/renderer/src/i18n/locales/zh-cn.json | 8 + src/renderer/src/i18n/locales/zh-tw.json | 8 + src/renderer/src/i18n/translate/de-de.json | 43 +++ src/renderer/src/i18n/translate/el-gr.json | 43 +++ src/renderer/src/i18n/translate/es-es.json | 43 +++ src/renderer/src/i18n/translate/fr-fr.json | 43 +++ src/renderer/src/i18n/translate/ja-jp.json | 43 +++ src/renderer/src/i18n/translate/pt-pt.json | 43 +++ src/renderer/src/i18n/translate/ru-ru.json | 43 +++ .../MCPSettings/BuiltinMCPServerList.tsx | 2 +- .../settings/MCPSettings/McpMarketList.tsx | 4 +- .../MCPSettings/McpProviderSettings.tsx | 260 ++++++++++++++++++ .../settings/MCPSettings/McpServersList.tsx | 21 +- .../MCPSettings/McpSettingsNavbar.tsx | 32 --- .../src/pages/settings/MCPSettings/index.tsx | 200 ++++++++++++-- .../settings/MCPSettings/providers/config.ts | 77 ++++++ 18 files changed, 847 insertions(+), 80 deletions(-) create mode 100644 src/renderer/src/pages/settings/MCPSettings/McpProviderSettings.tsx delete mode 100644 src/renderer/src/pages/settings/MCPSettings/McpSettingsNavbar.tsx create mode 100644 src/renderer/src/pages/settings/MCPSettings/providers/config.ts diff --git a/scripts/auto-translate-i18n.ts b/scripts/auto-translate-i18n.ts index 71650f6618..7a1bea6f35 100644 --- a/scripts/auto-translate-i18n.ts +++ b/scripts/auto-translate-i18n.ts @@ -18,8 +18,10 @@ import { sortedObjectByKeys } from './sort' // ========== SCRIPT CONFIGURATION AREA - MODIFY SETTINGS HERE ========== const SCRIPT_CONFIG = { // 🔧 Concurrency Control Configuration - MAX_CONCURRENT_TRANSLATIONS: 5, // Max concurrent requests (Make sure the concurrency level does not exceed your provider's limits.) - TRANSLATION_DELAY_MS: 100, // Delay between requests to avoid rate limiting (Recommended: 100-500ms, Range: 0-5000ms) + MAX_CONCURRENT_TRANSLATIONS: process.env.TRANSLATION_MAX_CONCURRENT_REQUESTS + ? parseInt(process.env.TRANSLATION_MAX_CONCURRENT_REQUESTS) + : 5, // Max concurrent requests (Make sure the concurrency level does not exceed your provider's limits.) + TRANSLATION_DELAY_MS: process.env.TRANSLATION_DELAY_MS ? parseInt(process.env.TRANSLATION_DELAY_MS) : 500, // Delay between requests to avoid rate limiting (Recommended: 100-500ms, Range: 0-5000ms) // 🔑 API Configuration API_KEY: process.env.TRANSLATION_API_KEY || '', // API key from environment variable diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index e851242559..fbffb92777 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -3801,6 +3801,7 @@ "description": "Do not enable MCP server functionality", "label": "Disable MCP Server" }, + "discover": "Discover", "duplicateName": "A server with this name already exists", "editJson": "Edit JSON", "editMcpJson": "Edit MCP Configuration", @@ -3811,6 +3812,10 @@ "32000": "MCP server failed to start, please check the parameters according to the tutorial", "toolNotFound": "Tool {{name}} not found" }, + "fetch": { + "button": "Fetch Servers", + "success": "Successfully fetched MCP servers" + }, "findMore": "Find More MCP", "headers": "Headers", "headersTooltip": "Custom headers for HTTP requests", @@ -3826,6 +3831,7 @@ "logoUrl": "Logo URL", "longRunning": "Long Running Mode", "longRunningTooltip": "When enabled, the server supports long-running tasks. When receiving progress notifications, the timeout will be reset and the maximum execution time will be extended to 10 minutes.", + "marketplaces": "Marketplaces", "missingDependencies": "is Missing, please install it to continue.", "more": { "awesome": "Curated MCP Server List", @@ -3874,6 +3880,7 @@ "provider": "Provider", "providerPlaceholder": "Provider name", "providerUrl": "Provider URL", + "providers": "Providers", "registry": "Package Registry", "registryDefault": "Default", "registryTooltip": "Choose the registry for package installation to resolve network issues with the default registry.", @@ -3896,6 +3903,7 @@ "searchNpx": "Search MCP", "serverPlural": "servers", "serverSingular": "server", + "servers": "MCP Servers", "sse": "Server-Sent Events (sse)", "startError": "Start failed", "stdio": "Standard Input/Output (stdio)", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 4a42f6c92a..26fb5dbe75 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -3801,6 +3801,7 @@ "description": "不启用 MCP 服务功能", "label": "不使用 MCP 服务器" }, + "discover": "发现", "duplicateName": "已存在同名服务器", "editJson": "编辑 JSON", "editMcpJson": "编辑 MCP 配置", @@ -3811,6 +3812,10 @@ "32000": "MCP 服务器启动失败,请根据教程检查参数是否填写完整", "toolNotFound": "未找到工具 {{name}}" }, + "fetch": { + "button": "获取服务器", + "success": "服务器获取成功" + }, "findMore": "更多 MCP", "headers": "请求头", "headersTooltip": "HTTP 请求的自定义请求头", @@ -3826,6 +3831,7 @@ "logoUrl": "标志网址", "longRunning": "长时间运行模式", "longRunningTooltip": "启用后,服务器支持长时间任务,接收到进度通知时会重置超时计时器,并延长最大超时时间至10分钟", + "marketplaces": "市场", "missingDependencies": "缺失,请安装它以继续", "more": { "awesome": "精选的 MCP 服务器列表", @@ -3874,6 +3880,7 @@ "provider": "提供者", "providerPlaceholder": "提供者名称", "providerUrl": "提供者网址", + "providers": "提供商", "registry": "包管理源", "registryDefault": "默认", "registryTooltip": "选择用于安装包的源,以解决默认源的网络问题", @@ -3896,6 +3903,7 @@ "searchNpx": "搜索 MCP", "serverPlural": "服务器", "serverSingular": "服务器", + "servers": "MCP 服务器", "sse": "服务器发送事件 (sse)", "startError": "启动失败", "stdio": "标准输入 / 输出 (stdio)", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index c65815ad5b..8b16b3e94e 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -3801,6 +3801,7 @@ "description": "不啟用 MCP 服務功能", "label": "不使用 MCP 伺服器" }, + "discover": "發現", "duplicateName": "已存在相同名稱的伺服器", "editJson": "編輯 JSON", "editMcpJson": "編輯 MCP 配置", @@ -3811,6 +3812,10 @@ "32000": "MCP 伺服器啟動失敗,請根據教程檢查參數是否填寫完整", "toolNotFound": "未找到工具 {{name}}" }, + "fetch": { + "button": "獲取伺服器", + "success": "伺服器獲取成功" + }, "findMore": "更多 MCP", "headers": "請求標頭", "headersTooltip": "HTTP 請求的自定義標頭", @@ -3826,6 +3831,7 @@ "logoUrl": "標誌網址", "longRunning": "長時間運行模式", "longRunningTooltip": "啟用後,伺服器支援長時間任務,接收到進度通知時會重置超時計時器,並延長最大超時時間至10分鐘", + "marketplaces": "市場", "missingDependencies": "缺失,請安裝它以繼續", "more": { "awesome": "精選的 MCP 伺服器清單", @@ -3874,6 +3880,7 @@ "provider": "提供者", "providerPlaceholder": "提供者名稱", "providerUrl": "提供者網址", + "providers": "提供商", "registry": "套件管理源", "registryDefault": "預設", "registryTooltip": "選擇用於安裝套件的源,以解決預設源的網路問題", @@ -3896,6 +3903,7 @@ "searchNpx": "搜索 MCP", "serverPlural": "伺服器", "serverSingular": "伺服器", + "servers": "MCP 伺服器", "sse": "伺服器傳送事件 (sse)", "startError": "啟動失敗", "stdio": "標準輸入 / 輸出 (stdio)", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index 359acf8c33..d94e74422b 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -339,6 +339,41 @@ }, "title": "API-Server" }, + "appMenu": { + "about": "Über", + "close": "Fenster schließen", + "copy": "Kopieren", + "cut": "Schneiden", + "delete": "Löschen", + "documentation": "Dokumentation", + "edit": "Bearbeiten", + "feedback": "Rückmeldung", + "file": "Datei", + "forceReload": "Neu laden erzwingen", + "front": "Alle in den Vordergrund bringen", + "help": "Hilfe", + "hide": "Verstecken", + "hideOthers": "Andere ausblenden", + "minimize": "Minimieren", + "paste": "Einfügen", + "quit": "Aufhören", + "redo": "Wiederholen", + "releases": "Veröffentlichungen", + "reload": "Neu laden", + "resetZoom": "Tatsächliche Größe", + "selectAll": "Alle auswählen", + "services": "Dienstleistungen", + "toggleDevTools": "Entwicklertools ein-/ausblenden", + "toggleFullscreen": "Vollbild umschalten", + "undo": "Rückgängig machen", + "unhide": "Alle anzeigen", + "view": "Ansicht", + "website": "Website", + "window": "Fenster", + "zoom": "Zoom", + "zoomIn": "Heranzoomen", + "zoomOut": "Herauszoomen" + }, "assistants": { "abbr": "Assistent", "clear": { @@ -3766,6 +3801,7 @@ "description": "MCP-Service-Funktion nicht aktivieren", "label": "MCP-Server nicht verwenden" }, + "discover": "Entdecken", "duplicateName": "Server mit gleichem Namen existiert bereits", "editJson": "JSON bearbeiten", "editMcpJson": "MCP-Konfiguration bearbeiten", @@ -3776,6 +3812,10 @@ "32000": "MCP-Server starten fehlgeschlagen, bitte überprüfen Sie, ob alle Parameter vollständig ausgefüllt sind", "toolNotFound": "Tool {{name}} nicht gefunden" }, + "fetch": { + "button": "Server abrufen", + "success": "MCP-Server erfolgreich abgerufen" + }, "findMore": "Mehr MCP", "headers": "Request-Header", "headersTooltip": "Benutzerdefinierte Request-Header für HTTP-Anfragen", @@ -3791,6 +3831,7 @@ "logoUrl": "Logo-URL", "longRunning": "Lang laufender Modus", "longRunningTooltip": "Nach Aktivierung unterstützt der Server lange Aufgaben. Wenn ein Fortschrittsbenachrichtigung empfangen wird, wird der Timeout-Timer zurückgesetzt und die maximale Timeout-Zeit auf 10 Minuten verlängert", + "marketplaces": "Marktplätze", "missingDependencies": "Abhängigkeiten fehlen, bitte installieren Sie sie, um fortzufahren", "more": { "awesome": "Kuratierte MCP-Serverliste", @@ -3839,6 +3880,7 @@ "provider": "Anbieter", "providerPlaceholder": "Anbietername", "providerUrl": "Anbieter-Website", + "providers": "Anbieter", "registry": "Paketverwaltungsquelle", "registryDefault": "Standard", "registryTooltip": "Quelle für Paketinstallation auswählen um Netzwerkprobleme der Standardquelle zu lösen", @@ -3861,6 +3903,7 @@ "searchNpx": "MCP durchsuchen", "serverPlural": "Server", "serverSingular": "Server", + "servers": "MCP-Server", "sse": "Server-Sende-Ereignisse (sse)", "startError": "Start fehlgeschlagen", "stdio": "Standard-Eingabe / -Ausgabe (stdio)", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 4b9ed72159..069cd8da8b 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -339,6 +339,41 @@ }, "title": "Διακομιστής API" }, + "appMenu": { + "about": "Σχετικά", + "close": "Κλείσιμο Παραθύρου", + "copy": "Αντιγραφή", + "cut": "Κόψε", + "delete": "Διαγραφή", + "documentation": "Τεκμηρίωση", + "edit": "Επεξεργασία", + "feedback": "Σχόλια", + "file": "Αρχείο", + "forceReload": "Εξαναγκασμένη επαναφόρτωση", + "front": "Μεταφορά Όλων Μπροστά", + "help": "Βοήθεια", + "hide": "Κρύψε", + "hideOthers": "Απόκρυψη Άλλων", + "minimize": "Ελαχιστοποίηση", + "paste": "Επικόλληση", + "quit": "Παραιτήσου", + "redo": "Ξανακάνε", + "releases": "Κυκλοφορίες", + "reload": "Επαναφόρτωση", + "resetZoom": "Πραγματικό Μέγεθος", + "selectAll": "Επιλογή Όλων", + "services": "Υπηρεσίες", + "toggleDevTools": "Εναλλαγή Εργαλείων Προγραμματιστή", + "toggleFullscreen": "Εναλλαγή πλήρους οθόνης", + "undo": "Αναίρεση", + "unhide": "Εμφάνιση Όλων", + "view": "Προβολή", + "website": "Ιστοσελίδα", + "window": "Παράθυρο", + "zoom": "Ζουμ", + "zoomIn": "Μεγέθυνση", + "zoomOut": "Σμίκρυνση" + }, "assistants": { "abbr": "Βοηθός", "clear": { @@ -3766,6 +3801,7 @@ "description": "Να μην ενεργοποιείται η λειτουργία υπηρεσίας MCP", "label": "Να μην χρησιμοποιείται διακομιστής MCP" }, + "discover": "Ανακαλύψτε", "duplicateName": "Υπάρχει ήδη ένας διακομιστής με αυτό το όνομα", "editJson": "Επεξεργασία JSON", "editMcpJson": "Επεξεργασία ρύθμισης MCP", @@ -3776,6 +3812,10 @@ "32000": "Η εκκίνηση του MCP απέτυχε. Παρακαλώ ελέγξτε αν όλες οι παράμετροι έχουν συμπληρωθεί σύμφωνα με τον οδηγό.", "toolNotFound": "Δεν βρέθηκε το εργαλείο {{name}}" }, + "fetch": { + "button": "Λήψη Διακομιστών", + "success": "Επιτυχής ανάκτηση διακομιστών MCP" + }, "findMore": "Περισσότεροι διακομιστές MCP", "headers": "Κεφαλίδες", "headersTooltip": "Προσαρμοσμένες κεφαλίδες HTTP αιτήσεων", @@ -3791,6 +3831,7 @@ "logoUrl": "URL Λογότυπου", "longRunning": "Μακροχρόνια λειτουργία", "longRunningTooltip": "Όταν ενεργοποιηθεί, ο διακομιστής υποστηρίζει μακροχρόνιες εργασίες, επαναφέρει το χρονικό όριο μετά από λήψη ειδοποίησης προόδου και επεκτείνει το μέγιστο χρονικό όριο σε 10 λεπτά.", + "marketplaces": "Αγορές", "missingDependencies": "Λείπει, παρακαλώ εγκαταστήστε το για να συνεχίσετε", "more": { "awesome": "Επιλεγμένος κατάλογος διακομιστών MCP", @@ -3839,6 +3880,7 @@ "provider": "Πάροχος", "providerPlaceholder": "Όνομα παρόχου", "providerUrl": "URL Παρόχου", + "providers": "Πάροχοι", "registry": "Πηγή Διαχείρισης πακέτων", "registryDefault": "Προεπιλεγμένη", "registryTooltip": "Επιλέξτε την πηγή για την εγκατάσταση πακέτων, για να αντιμετωπιστούν προβλήματα δικτύου από την προεπιλεγμένη πηγή.", @@ -3861,6 +3903,7 @@ "searchNpx": "Αναζήτηση MCP", "serverPlural": "Διακομιστές", "serverSingular": "Διακομιστής", + "servers": "Διακομιστές MCP", "sse": "Συμβάντα Αποστολής από τον Διακομιστή (sse)", "startError": "Εκκίνηση Απέτυχε", "stdio": "Πρότυπη Είσοδος/Έξοδος (stdio)", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index cf1b029db3..f3c7342b21 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -339,6 +339,41 @@ }, "title": "Servidor API" }, + "appMenu": { + "about": "Acerca de", + "close": "Cerrar ventana", + "copy": "Copiar", + "cut": "Cortar", + "delete": "Eliminar", + "documentation": "Documentación", + "edit": "Editar", + "feedback": "Retroalimentación", + "file": "Archivo", + "forceReload": "Forzar recarga", + "front": "Traer todo al frente", + "help": "Ayuda", + "hide": "Ocultar", + "hideOthers": "Ocultar Otros", + "minimize": "Minimizar", + "paste": "Pegar", + "quit": "Abandonar", + "redo": "Rehacer", + "releases": "Lanzamientos", + "reload": "Recargar", + "resetZoom": "Tamaño Real", + "selectAll": "Seleccionar todo", + "services": "Servicios", + "toggleDevTools": "Alternar herramientas de desarrollo", + "toggleFullscreen": "Activar pantalla completa", + "undo": "Deshacer", + "unhide": "Mostrar todo", + "view": "Vista", + "website": "Sitio web", + "window": "Ventana", + "zoom": "Zoom", + "zoomIn": "Acercar", + "zoomOut": "Alejar" + }, "assistants": { "abbr": "Asistente", "clear": { @@ -3766,6 +3801,7 @@ "description": "No habilitar funciones del servicio MCP", "label": "No utilizar servidor MCP" }, + "discover": "Descubrir", "duplicateName": "Ya existe un servidor con el mismo nombre", "editJson": "Editar JSON", "editMcpJson": "Editar configuración MCP", @@ -3776,6 +3812,10 @@ "32000": "El servidor MCP no se pudo iniciar, verifique si los parámetros están completos según la guía", "toolNotFound": "Herramienta no encontrada {{name}}" }, + "fetch": { + "button": "Obtener Servidores", + "success": "Servidores MCP obtenidos con éxito" + }, "findMore": "Más servidores MCP", "headers": "Encabezados", "headersTooltip": "Encabezados personalizados para solicitudes HTTP", @@ -3791,6 +3831,7 @@ "logoUrl": "URL del logotipo", "longRunning": "Modo de ejecución prolongada", "longRunningTooltip": "Una vez habilitado, el servidor admite tareas de larga duración, reinicia el temporizador de tiempo de espera al recibir notificaciones de progreso y amplía el tiempo máximo de espera hasta 10 minutos.", + "marketplaces": "Mercados", "missingDependencies": "Faltan, instalelas para continuar", "more": { "awesome": "Lista seleccionada de servidores MCP", @@ -3839,6 +3880,7 @@ "provider": "Proveedor", "providerPlaceholder": "Nombre del proveedor", "providerUrl": "URL del proveedor", + "providers": "Proveedores", "registry": "Repositorio de paquetes", "registryDefault": "Predeterminado", "registryTooltip": "Seleccione un repositorio para instalar paquetes, útil para resolver problemas de red con el repositorio predeterminado.", @@ -3861,6 +3903,7 @@ "searchNpx": "Buscar MCP", "serverPlural": "Servidores", "serverSingular": "Servidor", + "servers": "Servidores MCP", "sse": "Eventos enviados por el servidor (sse)", "startError": "Inicio fallido", "stdio": "Entrada/Salida estándar (stdio)", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index c1c699afaf..79dd7c4141 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -339,6 +339,41 @@ }, "title": "Serveur API" }, + "appMenu": { + "about": "À propos", + "close": "Fermer la fenêtre", + "copy": "Copier", + "cut": "Couper", + "delete": "Supprimer", + "documentation": "Documentation", + "edit": "Modifier", + "feedback": "Retour d'information", + "file": "Fichier", + "forceReload": "Rechargement forcé", + "front": "Tout ramener au premier plan", + "help": "Aide", + "hide": "Cacher", + "hideOthers": "Masquer les autres", + "minimize": "Minimiser", + "paste": "Coller", + "quit": "Quitter", + "redo": "Refaire", + "releases": "Sorties", + "reload": "Recharger", + "resetZoom": "Taille réelle", + "selectAll": "Tout sélectionner", + "services": "Services", + "toggleDevTools": "Basculer les outils de développement", + "toggleFullscreen": "Basculer en plein écran", + "undo": "Annuler", + "unhide": "Tout afficher", + "view": "Vue", + "website": "Site web", + "window": "Fenêtre", + "zoom": "Zoom", + "zoomIn": "Zoom Avant", + "zoomOut": "Dézoomer" + }, "assistants": { "abbr": "Aide", "clear": { @@ -3766,6 +3801,7 @@ "description": "Désactiver les fonctionnalités du service MCP", "label": "Ne pas utiliser le serveur MCP" }, + "discover": "Découvrir", "duplicateName": "Un serveur portant le même nom existe déjà", "editJson": "Modifier le JSON", "editMcpJson": "Редактировать конфигурацию MCP", @@ -3776,6 +3812,10 @@ "32000": "Échec du démarrage du serveur MCP, veuillez vérifier si tous les paramètres sont correctement remplis conformément au tutoriel", "toolNotFound": "Outil non trouvé {{name}}" }, + "fetch": { + "button": "Récupérer les serveurs", + "success": "Serveurs MCP récupérés avec succès" + }, "findMore": "Plus de serveurs MCP", "headers": "Заголовки запроса", "headersTooltip": "Пользовательские заголовки HTTP-запроса", @@ -3791,6 +3831,7 @@ "logoUrl": "Адрес логотипа", "longRunning": "Mode d'exécution prolongée", "longRunningTooltip": "Une fois activé, le serveur prend en charge les tâches de longue durée, réinitialise le minuteur de temporisation à la réception des notifications de progression, et prolonge le délai d'expiration maximal à 10 minutes.", + "marketplaces": "Places de marché", "missingDependencies": "Manquantes, veuillez les installer pour continuer", "more": { "awesome": "Liste sélectionnée de serveurs MCP", @@ -3839,6 +3880,7 @@ "provider": "Поставщик", "providerPlaceholder": "Название поставщика", "providerUrl": "Адрес поставщика", + "providers": "Fournisseurs", "registry": "Источник управления пакетами", "registryDefault": "По умолчанию", "registryTooltip": "Выберите источник для установки пакетов, чтобы решить проблемы с сетью по умолчанию.", @@ -3861,6 +3903,7 @@ "searchNpx": "Поиск MCP", "serverPlural": "Serveurs", "serverSingular": "Serveur", + "servers": "Serveurs MCP", "sse": "Серверные отправляемые события (sse)", "startError": "Ошибка запуска", "stdio": "Стандартный ввод/вывод (stdio)", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 95b5a7e694..9655d8fb7b 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -339,6 +339,41 @@ }, "title": "API サーバー" }, + "appMenu": { + "about": "について", + "close": "ウィンドウを閉じる", + "copy": "コピー", + "cut": "切る", + "delete": "削除", + "documentation": "ドキュメント", + "edit": "編集", + "feedback": "フィードバック", + "file": "ファイル", + "forceReload": "強制再読み込み", + "front": "すべてを前面に移動", + "help": "助け", + "hide": "隠す", + "hideOthers": "他を隠す", + "minimize": "最小化", + "paste": "ペースト", + "quit": "やめる", + "redo": "やり直し", + "releases": "リリース", + "reload": "リロード", + "resetZoom": "実寸", + "selectAll": "すべて選択", + "services": "サービス", + "toggleDevTools": "開発者ツールを切り替え", + "toggleFullscreen": "全画面表示を切り替え", + "undo": "元に戻す", + "unhide": "すべて表示", + "view": "表示", + "website": "ウェブサイト", + "window": "窓", + "zoom": "ズーム", + "zoomIn": "ズームイン", + "zoomOut": "ズームアウト" + }, "assistants": { "abbr": "アシスタント", "clear": { @@ -3766,6 +3801,7 @@ "description": "MCP機能を有効にしない", "label": "MCPサーバーを無効にする" }, + "discover": "発見", "duplicateName": "同じ名前のサーバーが既に存在します", "editJson": "JSONを編集", "editMcpJson": "MCP 設定を編集", @@ -3776,6 +3812,10 @@ "32000": "MCP サーバーが起動しませんでした。パラメーターを確認してください", "toolNotFound": "ツール {{name}} が見つかりません" }, + "fetch": { + "button": "サーバーを取得", + "success": "MCPサーバーの取得に成功しました" + }, "findMore": "MCP を見つける", "headers": "ヘッダー", "headersTooltip": "HTTP リクエストのカスタムヘッダー", @@ -3791,6 +3831,7 @@ "logoUrl": "ロゴURL", "longRunning": "長時間運行モード", "longRunningTooltip": "このオプションを有効にすると、サーバーは長時間のタスクをサポートします。進行状況通知を受信すると、タイムアウトがリセットされ、最大実行時間が10分に延長されます。", + "marketplaces": "マーケットプレイス", "missingDependencies": "が不足しています。続行するにはインストールしてください。", "more": { "awesome": "厳選された MCP サーバーリスト", @@ -3839,6 +3880,7 @@ "provider": "プロバイダー", "providerPlaceholder": "プロバイダー名", "providerUrl": "プロバイダーURL", + "providers": "プロバイダー", "registry": "パッケージ管理レジストリ", "registryDefault": "デフォルト", "registryTooltip": "デフォルトのレジストリでネットワークの問題が発生した場合、パッケージインストールに使用するレジストリを選択してください。", @@ -3861,6 +3903,7 @@ "searchNpx": "MCP を検索", "serverPlural": "サーバー", "serverSingular": "サーバー", + "servers": "MCPサーバー", "sse": "サーバー送信イベント (sse)", "startError": "起動に失敗しました", "stdio": "標準入力/出力 (stdio)", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 7ad1184b15..4be4a3dd97 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -339,6 +339,41 @@ }, "title": "Servidor API" }, + "appMenu": { + "about": "Sobre", + "close": "Fechar Janela", + "copy": "Copiar", + "cut": "Corte", + "delete": "Excluir", + "documentation": "Documentação", + "edit": "Editar", + "feedback": "Feedback", + "file": "Arquivo", + "forceReload": "Forçar Recarregamento", + "front": "Trazer Tudo para a Frente", + "help": "Ajuda", + "hide": "Esconder", + "hideOthers": "Ocultar Outros", + "minimize": "Minimizar", + "paste": "Colar", + "quit": "Sair", + "redo": "Refazer", + "releases": "Lançamentos", + "reload": "Recarregar", + "resetZoom": "Tamanho Real", + "selectAll": "Selecionar Todos", + "services": "Serviços", + "toggleDevTools": "Alternar Ferramentas de Desenvolvedor", + "toggleFullscreen": "Alternar Tela Cheia", + "undo": "Desfazer", + "unhide": "Mostrar Todos", + "view": "Visualizar", + "website": "Site", + "window": "Janela", + "zoom": "Zoom", + "zoomIn": "Ampliar", + "zoomOut": "Reduzir Zoom" + }, "assistants": { "abbr": "Assistente", "clear": { @@ -3766,6 +3801,7 @@ "description": "Não ativar a funcionalidade do serviço MCP", "label": "Não usar servidor MCP" }, + "discover": "Descobrir", "duplicateName": "Já existe um servidor com o mesmo nome", "editJson": "Editar JSON", "editMcpJson": "Editar Configuração MCP", @@ -3776,6 +3812,10 @@ "32000": "Falha ao iniciar o servidor MCP, verifique se todos os parâmetros foram preenchidos corretamente conforme o tutorial", "toolNotFound": "Ferramenta não encontrada {{name}}" }, + "fetch": { + "button": "Buscar Servidores", + "success": "Servidores MCP obtidos com sucesso" + }, "findMore": "Mais servidores MCP", "headers": "Cabeçalhos da Requisição", "headersTooltip": "Cabeçalhos HTTP personalizados para as requisições", @@ -3791,6 +3831,7 @@ "logoUrl": "URL do Logotipo", "longRunning": "Modo de execução prolongada", "longRunningTooltip": "Quando ativado, o servidor suporta tarefas de longa duração, redefinindo o temporizador de tempo limite ao receber notificações de progresso e estendendo o tempo máximo de tempo limite para 10 minutos.", + "marketplaces": "Mercados", "missingDependencies": "Ausente, instale para continuar", "more": { "awesome": "Lista selecionada de servidores MCP", @@ -3839,6 +3880,7 @@ "provider": "Fornecedor", "providerPlaceholder": "Nome do Fornecedor", "providerUrl": "URL do Fornecedor", + "providers": "Fornecedores", "registry": "Fonte de Gerenciamento de Pacotes", "registryDefault": "Padrão", "registryTooltip": "Selecione uma fonte alternativa para instalar pacotes, caso tenha problemas de rede com a fonte padrão.", @@ -3861,6 +3903,7 @@ "searchNpx": "Buscar MCP", "serverPlural": "Servidores", "serverSingular": "Servidor", + "servers": "Servidores MCP", "sse": "Eventos do Servidor (sse)", "startError": "Falha ao Iniciar", "stdio": "Entrada/Saída Padrão (stdio)", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index ecdc0ecef0..241fde8fd3 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -339,6 +339,41 @@ }, "title": "API Сервер" }, + "appMenu": { + "about": "О", + "close": "Закрыть окно", + "copy": "Копировать", + "cut": "Резать", + "delete": "Удалить", + "documentation": "Документация", + "edit": "Редактировать", + "feedback": "Обратная связь", + "file": "Файл", + "forceReload": "Принудительная перезагрузка", + "front": "Показать все поверх других", + "help": "Помощь", + "hide": "Скрыть", + "hideOthers": "Скрыть остальные", + "minimize": "Минимизировать", + "paste": "Вставить", + "quit": "Выйти", + "redo": "Переделать", + "releases": "Релизы", + "reload": "Перезагрузка", + "resetZoom": "Настоящий размер", + "selectAll": "Выбрать все", + "services": "Услуги", + "toggleDevTools": "Переключить инструменты разработчика", + "toggleFullscreen": "Переключить полноэкранный режим", + "undo": "Отменить", + "unhide": "Показать все", + "view": "Вид", + "website": "Веб-сайт", + "window": "Окно", + "zoom": "Zoom", + "zoomIn": "Увеличить", + "zoomOut": "Отдалить" + }, "assistants": { "abbr": "Ассистент", "clear": { @@ -3766,6 +3801,7 @@ "description": "Не включать функциональность сервера MCP", "label": "Отключить сервер MCP" }, + "discover": "Откройте", "duplicateName": "Сервер с таким именем уже существует", "editJson": "Редактировать JSON", "editMcpJson": "Редактировать MCP", @@ -3776,6 +3812,10 @@ "32000": "MCP сервер не запущен, пожалуйста, проверьте параметры", "toolNotFound": "Инструмент {{name}} не найден" }, + "fetch": { + "button": "Получить серверы", + "success": "Успешно получены MCP-серверы" + }, "findMore": "Найти больше MCP", "headers": "Заголовки", "headersTooltip": "Пользовательские заголовки для HTTP-запросов", @@ -3791,6 +3831,7 @@ "logoUrl": "URL логотипа", "longRunning": "Длительный режим работы", "longRunningTooltip": "Включив эту опцию, сервер будет поддерживать длительные задачи. При получении уведомлений о ходе выполнения будет сброшен тайм-аут и максимальное время выполнения будет увеличено до 10 минут.", + "marketplaces": "Торговые площадки", "missingDependencies": "отсутствует, пожалуйста, установите для продолжения.", "more": { "awesome": "Кураторский список серверов MCP", @@ -3839,6 +3880,7 @@ "provider": "Провайдер", "providerPlaceholder": "Имя провайдера", "providerUrl": "URL провайдера", + "providers": "Поставщики", "registry": "Реестр пакетов", "registryDefault": "По умолчанию", "registryTooltip": "Выберите реестр для установки пакетов, если возникают проблемы с сетью при использовании реестра по умолчанию.", @@ -3861,6 +3903,7 @@ "searchNpx": "Найти MCP", "serverPlural": "серверы", "serverSingular": "сервер", + "servers": "Серверы MCP", "sse": "События, отправляемые сервером (sse)", "startError": "Запуск не удалось", "stdio": "Стандартный ввод/вывод (stdio)", diff --git a/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServerList.tsx b/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServerList.tsx index 617688fb22..6ee9f3efca 100644 --- a/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServerList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServerList.tsx @@ -15,7 +15,7 @@ const BuiltinMCPServerList: FC = () => { return ( <> - {t('settings.mcp.builtinServers')} + {t('settings.mcp.builtinServers')} {builtinMCPServers.map((server) => { const isInstalled = mcpServers.some((existingServer) => existingServer.name === server.name) diff --git a/src/renderer/src/pages/settings/MCPSettings/McpMarketList.tsx b/src/renderer/src/pages/settings/MCPSettings/McpMarketList.tsx index 274fa93686..9255a82697 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpMarketList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpMarketList.tsx @@ -34,7 +34,7 @@ const mcpMarkets = [ { name: 'smithery.ai', url: 'https://smithery.ai/', - logo: 'https://smithery.ai/logo.svg', + logo: 'https://smithery.ai/icon.svg', descriptionKey: 'settings.mcp.more.smithery' }, { @@ -74,7 +74,7 @@ const McpMarketList: FC = () => { return ( <> - {t('settings.mcp.findMore')} + {t('settings.mcp.findMore')} {mcpMarkets.map((resource) => ( window.open(resource.url, '_blank', 'noopener,noreferrer')}> diff --git a/src/renderer/src/pages/settings/MCPSettings/McpProviderSettings.tsx b/src/renderer/src/pages/settings/MCPSettings/McpProviderSettings.tsx new file mode 100644 index 0000000000..d97fc86fea --- /dev/null +++ b/src/renderer/src/pages/settings/MCPSettings/McpProviderSettings.tsx @@ -0,0 +1,260 @@ +import { loggerService } from '@logger' +import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar' +import db from '@renderer/databases' +import { useMCPServers } from '@renderer/hooks/useMCPServers' +import type { MCPServer } from '@renderer/types' +import { Button, Divider, Flex, Input, Space } from 'antd' +import Link from 'antd/es/typography/Link' +import { Check, Plus, SquareArrowOutUpRight } from 'lucide-react' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { SettingHelpLink, SettingHelpTextRow, SettingSubtitle } from '..' +import type { ProviderConfig } from './providers/config' + +const logger = loggerService.withContext('McpProviderSettings') + +interface Props { + provider: ProviderConfig + existingServers: MCPServer[] +} + +const McpProviderSettings: React.FC = ({ provider, existingServers }) => { + const { addMCPServer } = useMCPServers() + const [isFetching, setIsFetching] = useState(false) + const [token, setToken] = useState('') + const [availableServers, setAvailableServers] = useState([]) + const [searchText, setSearchText] = useState('') + const { t } = useTranslation() + + useEffect(() => { + setToken(provider.getToken() || '') + }, [provider]) + + // Load available servers from database when provider changes + useEffect(() => { + const loadServersFromDb = async () => { + try { + const dbKey = `mcp:provider:${provider.key}:servers` + const setting = await db.settings.get(dbKey) + const savedServers = setting?.value || [] + setAvailableServers(savedServers) + } catch (error) { + logger.error('Failed to load servers from database', error as Error) + setAvailableServers([]) + } + } + + loadServersFromDb() + }, [provider.key]) + + // Sort servers: servers with logo first, then by name + const sortedServers = useMemo(() => { + return [...availableServers].sort((a, b) => { + // Servers with logo come first + if (a.logoUrl && !b.logoUrl) return -1 + if (!a.logoUrl && b.logoUrl) return 1 + // If both have or both don't have logo, sort by name + return a.name.localeCompare(b.name) + }) + }, [availableServers]) + + // Filter servers based on search text + const filteredServers = useMemo(() => { + if (!searchText.trim()) { + return sortedServers + } + const lowerSearchText = searchText.toLowerCase() + return sortedServers.filter( + (server) => + server.name.toLowerCase().includes(lowerSearchText) || + server.description?.toLowerCase().includes(lowerSearchText) + ) + }, [sortedServers, searchText]) + + const handleTokenChange = useCallback( + (value: string) => { + setToken(value) + // Auto-save token when user types + if (value.trim()) { + provider.saveToken(value) + } + }, + [provider] + ) + + const handleFetch = useCallback(async () => { + if (!token.trim()) { + window.toast.error(t('settings.mcp.sync.tokenRequired', 'API Token is required')) + return + } + + setIsFetching(true) + + try { + provider.saveToken(token) + const result = await provider.syncServers(token, existingServers) + + if (result.success) { + const servers = result.addedServers || [] + setAvailableServers(servers) + + // Save to database + const dbKey = `mcp:provider:${provider.key}:servers` + await db.settings.put({ id: dbKey, value: servers }) + + window.toast.success(t('settings.mcp.fetch.success', 'Successfully fetched MCP servers')) + } else { + window.toast.error(result.message) + } + } catch (error: any) { + logger.error('Failed to fetch MCP servers', error) + window.toast.error(`${t('settings.mcp.sync.error')}: ${error.message}`) + } finally { + setIsFetching(false) + } + }, [existingServers, provider, t, token]) + + const isFetchDisabled = !token + + return ( + + + + {provider.name} + {provider.discoverUrl && ( + + + + )} + + + + + {t('settings.provider.api_key.label')} + + handleTokenChange(e.target.value)} + spellCheck={false} + /> + + + + {provider.apiKeyUrl && ( + + {t('settings.provider.get_api_key')} + + )} + + + + {sortedServers.length > 0 && ( + <> + + + {t('settings.mcp.servers', 'Available MCP Servers')} + + + + + {filteredServers.map((server) => ( + +
+ {server.logoUrl && ( +
+ {server.name} +
+ )} +
+ {server.name} + {server.description} +
+
+ {(() => { + const isAlreadyAdded = existingServers.some((existing) => existing.id === server.id) + return ( + - + - { /> )} - - - setIsAddModalVisible(false)} diff --git a/src/renderer/src/pages/settings/MCPSettings/McpSettingsNavbar.tsx b/src/renderer/src/pages/settings/MCPSettings/McpSettingsNavbar.tsx deleted file mode 100644 index 465c6b3f65..0000000000 --- a/src/renderer/src/pages/settings/MCPSettings/McpSettingsNavbar.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { NavbarRight } from '@renderer/components/app/Navbar' -import { HStack } from '@renderer/components/Layout' -import { isLinux, isWin } from '@renderer/config/constant' -import { useFullscreen } from '@renderer/hooks/useFullscreen' -import { Button } from 'antd' -import { Search } from 'lucide-react' -import { useTranslation } from 'react-i18next' -import { useNavigate } from 'react-router' - -import InstallNpxUv from './InstallNpxUv' - -export const McpSettingsNavbar = () => { - const { t } = useTranslation() - const navigate = useNavigate() - - return ( - - - - - - - ) -} diff --git a/src/renderer/src/pages/settings/MCPSettings/index.tsx b/src/renderer/src/pages/settings/MCPSettings/index.tsx index 9fe0a1cc62..6e4ac3d2af 100644 --- a/src/renderer/src/pages/settings/MCPSettings/index.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/index.tsx @@ -1,39 +1,131 @@ import { ArrowLeftOutlined } from '@ant-design/icons' -import { ErrorBoundary } from '@renderer/components/ErrorBoundary' +import Ai302ProviderLogo from '@renderer/assets/images/providers/302ai.webp' +import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png' +import LanyunProviderLogo from '@renderer/assets/images/providers/lanyun.png' +import ModelScopeProviderLogo from '@renderer/assets/images/providers/modelscope.png' +import TokenFluxProviderLogo from '@renderer/assets/images/providers/tokenflux.png' +import DividerWithText from '@renderer/components/DividerWithText' +import ListItem from '@renderer/components/ListItem' +import Scrollbar from '@renderer/components/Scrollbar' import { useTheme } from '@renderer/context/ThemeProvider' -import { Button } from 'antd' +import { useMCPServers } from '@renderer/hooks/useMCPServers' +import { Button, Flex } from 'antd' +import { FolderCog, Package, ShoppingBag } from 'lucide-react' import type { FC } from 'react' -import { Route, Routes, useLocation } from 'react-router' +import { useTranslation } from 'react-i18next' +import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router' import { Link } from 'react-router-dom' import styled from 'styled-components' import { SettingContainer } from '..' +import BuiltinMCPServerList from './BuiltinMCPServerList' import InstallNpxUv from './InstallNpxUv' +import McpMarketList from './McpMarketList' +import ProviderDetail from './McpProviderSettings' import McpServersList from './McpServersList' import McpSettings from './McpSettings' import NpxSearch from './NpxSearch' +import { providers } from './providers/config' const MCPSettings: FC = () => { const { theme } = useTheme() - + const { t } = useTranslation() + const { mcpServers } = useMCPServers() + const navigate = useNavigate() const location = useLocation() - const pathname = location.pathname - const isHome = pathname === '/settings/mcp' + // 获取当前激活的页面 + const getActiveView = () => { + const path = location.pathname + + // 精确匹配路径 + if (path === '/settings/mcp/builtin') return 'builtin' + if (path === '/settings/mcp/marketplaces') return 'marketplaces' + + // 检查是否是服务商页面 - 精确匹配 + for (const provider of providers) { + if (path === `/settings/mcp/${provider.key}`) { + return provider.key + } + } + + // 其他所有情况(包括 servers、settings/:serverId、npx-search、mcp-install)都属于 servers + return 'servers' + } + + const activeView = getActiveView() + + // 判断是否为主页面(是否显示返回按钮) + const isHomePage = () => { + const path = location.pathname + // 主页面不显示返回按钮 + if (path === '/settings/mcp' || path === '/settings/mcp/servers') return true + if (path === '/settings/mcp/builtin' || path === '/settings/mcp/marketplaces') return true + + // 服务商页面也是主页面 + return providers.some((p) => path === `/settings/mcp/${p.key}`) + } + + // Provider icons map + const providerIcons: Record = { + modelscope: , + tokenflux: , + lanyun: , + '302ai': , + bailian: + } return ( - - {!isHome && ( - - - + + + )} - } /> + } /> + } /> } /> { } /> + + + + } + /> + + + + } + /> + {providers.map((provider) => ( + } + /> + ))} - + - + ) } +const Container = styled(Flex)` + flex: 1; +` + +const MainContainer = styled.div` + display: flex; + flex: 1; + flex-direction: row; + width: 100%; + height: calc(100vh - var(--navbar-height) - 6px); + overflow: hidden; +` + +const MenuList = styled(Scrollbar)` + display: flex; + flex-direction: column; + gap: 5px; + width: var(--settings-width); + padding: 12px; + padding-bottom: 48px; + border-right: 0.5px solid var(--color-border); + height: calc(100vh - var(--navbar-height)); +` + +const RightContainer = styled(Scrollbar)` + flex: 1; + position: relative; +` + +const ProviderIcon = styled.img` + width: 24px; + height: 24px; + object-fit: cover; + border-radius: 50%; + background-color: var(--color-background-soft); +` + +const ContentWrapper = styled.div` + padding: 20px; + overflow-y: auto; + height: 100%; +` + const BackButtonContainer = styled.div` display: flex; align-items: center; @@ -70,10 +228,4 @@ const BackButtonContainer = styled.div` z-index: 1000; ` -const MainContainer = styled.div` - display: flex; - flex: 1; - width: 100%; -` - export default MCPSettings diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/config.ts b/src/renderer/src/pages/settings/MCPSettings/providers/config.ts new file mode 100644 index 0000000000..efe46be5fb --- /dev/null +++ b/src/renderer/src/pages/settings/MCPSettings/providers/config.ts @@ -0,0 +1,77 @@ +import type { MCPServer } from '@renderer/types' + +import { getAI302Token, saveAI302Token, syncAi302Servers } from './302ai' +import { getBailianToken, saveBailianToken, syncBailianServers } from './bailian' +import { getTokenLanYunToken, LANYUN_KEY_HOST, saveTokenLanYunToken, syncTokenLanYunServers } from './lanyun' +import { getModelScopeToken, MODELSCOPE_HOST, saveModelScopeToken, syncModelScopeServers } from './modelscope' +import { getTokenFluxToken, saveTokenFluxToken, syncTokenFluxServers, TOKENFLUX_HOST } from './tokenflux' + +export interface ProviderConfig { + key: string + name: string + description: string + discoverUrl: string + apiKeyUrl: string + tokenFieldName: string + getToken: () => string | null + saveToken: (token: string) => void + syncServers: (token: string, existingServers: MCPServer[]) => Promise +} + +export const providers: ProviderConfig[] = [ + { + key: 'modelscope', + name: 'ModelScope', + description: 'ModelScope 平台 MCP 服务', + discoverUrl: `${MODELSCOPE_HOST}/mcp?hosted=1&page=1`, + apiKeyUrl: `${MODELSCOPE_HOST}/my/myaccesstoken`, + tokenFieldName: 'modelScopeToken', + getToken: getModelScopeToken, + saveToken: saveModelScopeToken, + syncServers: syncModelScopeServers + }, + { + key: 'tokenflux', + name: 'TokenFlux', + description: 'TokenFlux 平台 MCP 服务', + discoverUrl: `${TOKENFLUX_HOST}/mcps`, + apiKeyUrl: `${TOKENFLUX_HOST}/dashboard/api-keys`, + tokenFieldName: 'tokenfluxToken', + getToken: getTokenFluxToken, + saveToken: saveTokenFluxToken, + syncServers: syncTokenFluxServers + }, + { + key: 'lanyun', + name: '蓝耘科技', + description: '蓝耘科技云平台 MCP 服务', + discoverUrl: 'https://mcp.lanyun.net', + apiKeyUrl: LANYUN_KEY_HOST, + tokenFieldName: 'tokenLanyunToken', + getToken: getTokenLanYunToken, + saveToken: saveTokenLanYunToken, + syncServers: syncTokenLanYunServers + }, + { + key: '302ai', + name: '302.AI', + description: '302.AI 平台 MCP 服务', + discoverUrl: 'https://302.ai', + apiKeyUrl: 'https://dash.302.ai/apis/list', + tokenFieldName: 'token302aiToken', + getToken: getAI302Token, + saveToken: saveAI302Token, + syncServers: syncAi302Servers + }, + { + key: 'bailian', + name: '阿里云百炼', + description: '百炼平台服务', + discoverUrl: `https://bailian.console.aliyun.com/?tab=mcp#/mcp-market`, + apiKeyUrl: `https://bailian.console.aliyun.com/?tab=app#/api-key`, + tokenFieldName: 'bailianToken', + getToken: getBailianToken, + saveToken: saveBailianToken, + syncServers: syncBailianServers + } +] From 44b2b859dadc3c07d56dbdc83e7848bb184d0e7c Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 7 Nov 2025 18:41:15 +0800 Subject: [PATCH 003/173] feat(MCPRouter): add MCPRouter provider support and integration - Introduced MCPRouter provider with token management and server synchronization functionalities. - Added MCPRouter logo to settings page for visual representation. - Updated provider configuration to include MCPRouter details and API interactions. - Implemented functions for saving, retrieving, and clearing MCPRouter tokens, along with server synchronization logic. --- .../assets/images/providers/mcprouter.webp | Bin 0 -> 1628 bytes .../src/pages/settings/MCPSettings/index.tsx | 4 +- .../settings/MCPSettings/providers/config.ts | 12 ++ .../MCPSettings/providers/mcprouter.ts | 157 ++++++++++++++++++ 4 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/assets/images/providers/mcprouter.webp create mode 100644 src/renderer/src/pages/settings/MCPSettings/providers/mcprouter.ts diff --git a/src/renderer/src/assets/images/providers/mcprouter.webp b/src/renderer/src/assets/images/providers/mcprouter.webp new file mode 100644 index 0000000000000000000000000000000000000000..7d6557fafc58a4b4919e6be065b9006bfcd339e9 GIT binary patch literal 1628 zcmV-i2BY~>Nk&Fg1^@t8MM6+kP&gn+1^@uiDgd1UD#!rH06vjOpGu{qqamZTI#{q0 z32AQNEzmNMy!+Xo`F*&i0rUshzVP4Y-{yDRKnHsS&eeP}|JhGH-%a}g{vh0$yvX{f;Qax zo5FxN&XVwEuFP*|!r0DWvNX$!fzkAoA0u_7w~Sr!$wEi$WAY1}dP3BdY?|=7j=FBF z>Sx(CF8fI5uKKUEbxg+l6l!p_gyK$KV5*j|N6=V);hZ&ki~Hj zjzK0cC}vS6>wNFHYybfM`R;;2ou~i+0O?)wyN!L5=(Il`cTAcz@MdAvprag7!lHBx z*N@01UU@fzv;tz@C_Uu&>d9<68C=|UQp+q*rqDfewp3dq_u=WzPi6x5{mfqUF8+&Z zm=Z*+!YMJb#>#AG_3olNU9{rv@jK&jEPg#dBDKmu`-)PQcLy!rJ@JP$q!DyV3&Fr* zfD&{u*av;MtK<=Oy37mznJ)6GWmEz{e#jAsVH7TV;{1oGTE7awBTH|2*(3`7jwR#4Df#X-bEC{4-B{(Td94o@KV$M?dr{e>&p-pxKUC4^8cZ-jIq$HG@~PkAQ%(3DX>>#VD9_2xVvF zTALuZ4gO%h12M_`m+3Emya4*e{JlyELt)W)#F9>0rU&Ft>_IK3B(toag>Y#kwo(C^qkc4zA(V&!28E`D1^)J zM-8 zRr7=aISJ;E#GV;M$*Spy$Aju#0@fgYrn7*U&zz0edOEJNnnVOsY^A@GmtS>i*O!tG zn1y+tx1qa=zqj29WJFHz?`$DGK+hChn&|;(AZLyOpYFZ+&Xi0)xyL_0!@T$^rnv$5 z8Z!s>`M)ME-veZ|j?O@keA+r@G8}{vz;8~7zZC8IH}DSWF6^3urZs*X-AO>HG)F4; zVk8&2%d}ZhA-j*J^?ev<%u% z3Lk}>=lcu)x!$%wvX223*NQmsyOEWT3M&!zMzzwv$@PR<1ET!|+dncRid?3)OIZK= zzREfB&2ijR3B7AxF*T5DmokJHI>xF=!&hitJrLZP!=m{#Jk8EP047VY6LZ(306w%` zW-cPdSOnOxa@{$H(815H74fw-H?-}DHzaL`e?-szhN{diQ<)v!VfB*opaX2FY literal 0 HcmV?d00001 diff --git a/src/renderer/src/pages/settings/MCPSettings/index.tsx b/src/renderer/src/pages/settings/MCPSettings/index.tsx index 6e4ac3d2af..6adb64ca23 100644 --- a/src/renderer/src/pages/settings/MCPSettings/index.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/index.tsx @@ -2,6 +2,7 @@ import { ArrowLeftOutlined } from '@ant-design/icons' import Ai302ProviderLogo from '@renderer/assets/images/providers/302ai.webp' import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png' import LanyunProviderLogo from '@renderer/assets/images/providers/lanyun.png' +import MCPRouterProviderLogo from '@renderer/assets/images/providers/mcprouter.webp' import ModelScopeProviderLogo from '@renderer/assets/images/providers/modelscope.png' import TokenFluxProviderLogo from '@renderer/assets/images/providers/tokenflux.png' import DividerWithText from '@renderer/components/DividerWithText' @@ -72,7 +73,8 @@ const MCPSettings: FC = () => { tokenflux: , lanyun: , '302ai': , - bailian: + bailian: , + mcprouter: } return ( diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/config.ts b/src/renderer/src/pages/settings/MCPSettings/providers/config.ts index efe46be5fb..6b094536e1 100644 --- a/src/renderer/src/pages/settings/MCPSettings/providers/config.ts +++ b/src/renderer/src/pages/settings/MCPSettings/providers/config.ts @@ -3,6 +3,7 @@ import type { MCPServer } from '@renderer/types' import { getAI302Token, saveAI302Token, syncAi302Servers } from './302ai' import { getBailianToken, saveBailianToken, syncBailianServers } from './bailian' import { getTokenLanYunToken, LANYUN_KEY_HOST, saveTokenLanYunToken, syncTokenLanYunServers } from './lanyun' +import { getMCPRouterToken, saveMCPRouterToken, syncMCPRouterServers } from './mcprouter' import { getModelScopeToken, MODELSCOPE_HOST, saveModelScopeToken, syncModelScopeServers } from './modelscope' import { getTokenFluxToken, saveTokenFluxToken, syncTokenFluxServers, TOKENFLUX_HOST } from './tokenflux' @@ -73,5 +74,16 @@ export const providers: ProviderConfig[] = [ getToken: getBailianToken, saveToken: saveBailianToken, syncServers: syncBailianServers + }, + { + key: 'mcprouter', + name: 'MCP Router', + description: 'MCP Router 平台 MCP 服务', + discoverUrl: 'https://mcprouter.co', + apiKeyUrl: 'https://mcprouter.co/settings/keys', + tokenFieldName: 'mcprouterToken', + getToken: getMCPRouterToken, + saveToken: saveMCPRouterToken, + syncServers: syncMCPRouterServers } ] diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/mcprouter.ts b/src/renderer/src/pages/settings/MCPSettings/providers/mcprouter.ts new file mode 100644 index 0000000000..a993ce7383 --- /dev/null +++ b/src/renderer/src/pages/settings/MCPSettings/providers/mcprouter.ts @@ -0,0 +1,157 @@ +import { loggerService } from '@logger' +import { nanoid } from '@reduxjs/toolkit' +import type { MCPServer } from '@renderer/types' +import i18next from 'i18next' + +const logger = loggerService.withContext('MCPRouterSyncUtils') + +// Token storage constants and utilities +const TOKEN_STORAGE_KEY = 'mcprouter_token' +export const MCPROUTER_HOST = 'https://mcprouter.co' + +export const saveMCPRouterToken = (token: string): void => { + localStorage.setItem(TOKEN_STORAGE_KEY, token) +} + +export const getMCPRouterToken = (): string | null => { + return localStorage.getItem(TOKEN_STORAGE_KEY) +} + +export const clearMCPRouterToken = (): void => { + localStorage.removeItem(TOKEN_STORAGE_KEY) +} + +export const hasMCPRouterToken = (): boolean => { + return !!getMCPRouterToken() +} + +interface MCPRouterServer { + created_at: string + updated_at: string + name: string + author_name?: string + title?: string + description?: string + content?: string + server_key: string + config_name: string + server_url: string +} + +interface MCPRouterSyncResult { + success: boolean + message: string + addedServers: MCPServer[] + updatedServers: MCPServer[] + errorDetails?: string +} + +// Function to fetch and process MCPRouter servers +export const syncMCPRouterServers = async ( + token: string, + existingServers: MCPServer[] +): Promise => { + const t = i18next.t + + try { + const response = await fetch('https://api.mcprouter.to/v1/list-servers', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + 'HTTP-Referer': 'https://cherry-ai.com', + 'X-Title': 'Cherry Studio' + }, + body: JSON.stringify({}) + }) + + // Handle authentication errors + if (response.status === 401 || response.status === 403) { + clearMCPRouterToken() + return { + success: false, + message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'), + addedServers: [], + updatedServers: [] + } + } + + // Handle server errors + if (response.status === 500 || !response.ok) { + return { + success: false, + message: t('settings.mcp.sync.error'), + addedServers: [], + updatedServers: [], + errorDetails: `Status: ${response.status}` + } + } + + // Process successful response + const data = await response.json() + const servers: MCPRouterServer[] = data.data?.servers || [] + + if (servers.length === 0) { + return { + success: true, + message: t('settings.mcp.sync.noServersAvailable', 'No MCP servers available'), + addedServers: [], + updatedServers: [] + } + } + + // Transform MCPRouter servers to MCP servers format + const addedServers: MCPServer[] = [] + const updatedServers: MCPServer[] = [] + + for (const server of servers) { + try { + // Check if server already exists using server_key + const existingServer = existingServers.find((s) => s.id === `@mcprouter/${server.server_key}`) + + const mcpServer: MCPServer = { + id: `@mcprouter/${server.server_key}`, + name: server.title || server.name || `MCPRouter Server ${nanoid()}`, + description: server.description || '', + type: 'streamableHttp', + baseUrl: server.server_url, + isActive: true, + provider: 'MCPRouter', + providerUrl: `https://mcprouter.co/${server.server_key}`, + logoUrl: '', + tags: [], + headers: { + Authorization: `Bearer ${token}` + } + } + + if (existingServer) { + // Update existing server with corrected URL and latest info + updatedServers.push(mcpServer) + } else { + // Add new server + addedServers.push(mcpServer) + } + } catch (err) { + logger.error('Error processing MCPRouter server:', err as Error) + } + } + + const totalServers = addedServers.length + updatedServers.length + return { + success: true, + message: t('settings.mcp.sync.success', { count: totalServers }), + addedServers, + updatedServers + } + } catch (error) { + logger.error('MCPRouter sync error:', error as Error) + return { + success: false, + message: t('settings.mcp.sync.error'), + addedServers: [], + updatedServers: [], + errorDetails: String(error) + } + } +} From 10e78ac60e85f8875993a55059334b4e5696b483 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 7 Nov 2025 19:22:58 +0800 Subject: [PATCH 004/173] refactor(MCPSettings): update styled components and enhance server synchronization - Changed RightContainer from Scrollbar to a standard div for layout adjustments. - Updated DetailContainer to use Scrollbar for improved scrolling behavior. - Modified server synchronization logic across multiple providers to include allServers in the results, enhancing server management capabilities. - Refactored provider configurations to ensure consistency and support for new server data structure. --- .../MCPSettings/McpProviderSettings.tsx | 6 +++-- .../src/pages/settings/MCPSettings/index.tsx | 2 +- .../settings/MCPSettings/providers/302ai.ts | 13 +++++++--- .../settings/MCPSettings/providers/bailian.ts | 11 ++++++-- .../settings/MCPSettings/providers/config.ts | 25 ++++++++++--------- .../settings/MCPSettings/providers/lanyun.ts | 17 ++++++++++--- .../MCPSettings/providers/mcprouter.ts | 15 ++++++++--- .../MCPSettings/providers/modelscope.ts | 16 +++++++++--- .../MCPSettings/providers/tokenflux.ts | 16 +++++++++--- 9 files changed, 86 insertions(+), 35 deletions(-) diff --git a/src/renderer/src/pages/settings/MCPSettings/McpProviderSettings.tsx b/src/renderer/src/pages/settings/MCPSettings/McpProviderSettings.tsx index d97fc86fea..ab0c1979cf 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpProviderSettings.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpProviderSettings.tsx @@ -1,5 +1,6 @@ import { loggerService } from '@logger' import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar' +import Scrollbar from '@renderer/components/Scrollbar' import db from '@renderer/databases' import { useMCPServers } from '@renderer/hooks/useMCPServers' import type { MCPServer } from '@renderer/types' @@ -97,7 +98,7 @@ const McpProviderSettings: React.FC = ({ provider, existingServers }) => const result = await provider.syncServers(token, existingServers) if (result.success) { - const servers = result.addedServers || [] + const servers = result.allServers || [] setAvailableServers(servers) // Save to database @@ -208,10 +209,11 @@ const McpProviderSettings: React.FC = ({ provider, existingServers }) => ) } -const DetailContainer = styled.div` +const DetailContainer = styled(Scrollbar)` padding: 20px; display: flex; flex-direction: column; + height: calc(100vh - var(--navbar-height)); ` const ProviderHeader = styled.div` diff --git a/src/renderer/src/pages/settings/MCPSettings/index.tsx b/src/renderer/src/pages/settings/MCPSettings/index.tsx index 6adb64ca23..987b6cd0d6 100644 --- a/src/renderer/src/pages/settings/MCPSettings/index.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/index.tsx @@ -199,7 +199,7 @@ const MenuList = styled(Scrollbar)` height: calc(100vh - var(--navbar-height)); ` -const RightContainer = styled(Scrollbar)` +const RightContainer = styled.div` flex: 1; position: relative; ` diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/302ai.ts b/src/renderer/src/pages/settings/MCPSettings/providers/302ai.ts index 646b929d47..2c7f7dfecb 100644 --- a/src/renderer/src/pages/settings/MCPSettings/providers/302ai.ts +++ b/src/renderer/src/pages/settings/MCPSettings/providers/302ai.ts @@ -30,6 +30,7 @@ interface Ai302SyncResult { message: string addedServers: MCPServer[] updatedServers: MCPServer[] + allServers: MCPServer[] errorDetails?: string } @@ -53,7 +54,8 @@ export const syncAi302Servers = async (token: string, existingServers: MCPServer success: false, message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'), addedServers: [], - updatedServers: [] + updatedServers: [], + allServers: [] } } @@ -64,6 +66,7 @@ export const syncAi302Servers = async (token: string, existingServers: MCPServer message: t('settings.mcp.sync.error'), addedServers: [], updatedServers: [], + allServers: [], errorDetails: `Status: ${response.status}` } } @@ -78,13 +81,15 @@ export const syncAi302Servers = async (token: string, existingServers: MCPServer success: true, message: t('settings.mcp.sync.noServersAvailable', 'No MCP servers available'), addedServers: [], - updatedServers: [] + updatedServers: [], + allServers: [] } } // Transform 302ai servers to MCP servers format const addedServers: MCPServer[] = [] const updatedServers: MCPServer[] = [] + const allServers: MCPServer[] = [] for (const server of servers) { try { @@ -121,7 +126,8 @@ export const syncAi302Servers = async (token: string, existingServers: MCPServer success: true, message: t('settings.mcp.sync.success', { count: totalServers }), addedServers, - updatedServers + updatedServers, + allServers } } catch (error) { logger.error('302ai sync error:', error as Error) @@ -130,6 +136,7 @@ export const syncAi302Servers = async (token: string, existingServers: MCPServer message: t('settings.mcp.sync.error'), addedServers: [], updatedServers: [], + allServers: [], errorDetails: String(error) } } diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/bailian.ts b/src/renderer/src/pages/settings/MCPSettings/providers/bailian.ts index 8eecb2bac2..1e2fb3910c 100644 --- a/src/renderer/src/pages/settings/MCPSettings/providers/bailian.ts +++ b/src/renderer/src/pages/settings/MCPSettings/providers/bailian.ts @@ -55,6 +55,7 @@ export interface BailianSyncResult { message: string addedServers: MCPServer[] updatedServers: MCPServer[] + allServers: MCPServer[] errorDetails?: string } @@ -117,6 +118,7 @@ export const syncBailianServers = async (token: string, existingServers: MCPServ const addedServers: MCPServer[] = [] const updatedServers: MCPServer[] = [] + const allServers: MCPServer[] = [] for (const server of servers) { try { @@ -151,6 +153,7 @@ export const syncBailianServers = async (token: string, existingServers: MCPServ } else { addedServers.push(mcpServer) } + allServers.push(mcpServer) } catch (err) { logger.error(`Error processing Bailian server ${server.id}:`, err as Error) } @@ -162,7 +165,8 @@ export const syncBailianServers = async (token: string, existingServers: MCPServ success: true, message: t('settings.mcp.sync.success', { count: totalServers }), addedServers, - updatedServers + updatedServers, + allServers } } catch (error) { let message = '' @@ -176,7 +180,8 @@ export const syncBailianServers = async (token: string, existingServers: MCPServ success: false, message, addedServers: [], - updatedServers: [] + updatedServers: [], + allServers: [] } } @@ -189,6 +194,7 @@ export const syncBailianServers = async (token: string, existingServers: MCPServ message, addedServers: [], updatedServers: [], + allServers: [], errorDetails } } @@ -202,6 +208,7 @@ export const syncBailianServers = async (token: string, existingServers: MCPServ message, addedServers: [], updatedServers: [], + allServers: [], errorDetails } } diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/config.ts b/src/renderer/src/pages/settings/MCPSettings/providers/config.ts index 6b094536e1..7c3f2974b9 100644 --- a/src/renderer/src/pages/settings/MCPSettings/providers/config.ts +++ b/src/renderer/src/pages/settings/MCPSettings/providers/config.ts @@ -1,3 +1,4 @@ +import { getProviderLabel } from '@renderer/i18n/label' import type { MCPServer } from '@renderer/types' import { getAI302Token, saveAI302Token, syncAi302Servers } from './302ai' @@ -20,6 +21,17 @@ export interface ProviderConfig { } export const providers: ProviderConfig[] = [ + { + key: 'bailian', + name: getProviderLabel('dashscope'), + description: '百炼平台服务', + discoverUrl: `https://bailian.console.aliyun.com/?tab=mcp#/mcp-market`, + apiKeyUrl: `https://bailian.console.aliyun.com/?tab=app#/api-key`, + tokenFieldName: 'bailianToken', + getToken: getBailianToken, + saveToken: saveBailianToken, + syncServers: syncBailianServers + }, { key: 'modelscope', name: 'ModelScope', @@ -44,7 +56,7 @@ export const providers: ProviderConfig[] = [ }, { key: 'lanyun', - name: '蓝耘科技', + name: getProviderLabel('lanyun'), description: '蓝耘科技云平台 MCP 服务', discoverUrl: 'https://mcp.lanyun.net', apiKeyUrl: LANYUN_KEY_HOST, @@ -64,17 +76,6 @@ export const providers: ProviderConfig[] = [ saveToken: saveAI302Token, syncServers: syncAi302Servers }, - { - key: 'bailian', - name: '阿里云百炼', - description: '百炼平台服务', - discoverUrl: `https://bailian.console.aliyun.com/?tab=mcp#/mcp-market`, - apiKeyUrl: `https://bailian.console.aliyun.com/?tab=app#/api-key`, - tokenFieldName: 'bailianToken', - getToken: getBailianToken, - saveToken: saveBailianToken, - syncServers: syncBailianServers - }, { key: 'mcprouter', name: 'MCP Router', diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/lanyun.ts b/src/renderer/src/pages/settings/MCPSettings/providers/lanyun.ts index f227eb5670..2cc1d1ee74 100644 --- a/src/renderer/src/pages/settings/MCPSettings/providers/lanyun.ts +++ b/src/renderer/src/pages/settings/MCPSettings/providers/lanyun.ts @@ -56,6 +56,7 @@ interface TokenLanYunSyncResult { message: string addedServers: MCPServer[] updatedServers: MCPServer[] + allServers: MCPServer[] errorDetails?: string } @@ -82,7 +83,8 @@ export const syncTokenLanYunServers = async ( success: false, message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'), addedServers: [], - updatedServers: [] + updatedServers: [], + allServers: [] } } @@ -93,6 +95,7 @@ export const syncTokenLanYunServers = async ( message: t('settings.mcp.sync.error'), addedServers: [], updatedServers: [], + allServers: [], errorDetails: `Status: ${response.status}` } } @@ -105,6 +108,7 @@ export const syncTokenLanYunServers = async ( message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'), addedServers: [], updatedServers: [], + allServers: [], errorDetails: `Status: ${response.status}` } } @@ -114,6 +118,7 @@ export const syncTokenLanYunServers = async ( message: t('settings.mcp.sync.error'), addedServers: [], updatedServers: [], + allServers: [], errorDetails: `Status: ${response.status}` } } @@ -125,14 +130,17 @@ export const syncTokenLanYunServers = async ( success: true, message: t('settings.mcp.sync.noServersAvailable', 'No MCP servers available'), addedServers: [], - updatedServers: [] + updatedServers: [], + allServers: [] } } // Transform Token servers to MCP servers format const addedServers: MCPServer[] = [] const updatedServers: MCPServer[] = [] + const allServers: MCPServer[] = [] logger.debug('TokenLanYun servers:', servers) + for (const server of servers) { try { if (!server.operationalUrls?.[0]?.url) continue @@ -164,6 +172,7 @@ export const syncTokenLanYunServers = async ( // Add new server addedServers.push(mcpServer) } + allServers.push(mcpServer) } catch (err) { logger.error('Error processing LanYun server:', err as Error) } @@ -174,7 +183,8 @@ export const syncTokenLanYunServers = async ( success: true, message: t('settings.mcp.sync.success', { count: totalServers }), addedServers, - updatedServers + updatedServers, + allServers } } catch (error) { logger.error('TokenLanyun sync error:', error as Error) @@ -183,6 +193,7 @@ export const syncTokenLanYunServers = async ( message: t('settings.mcp.sync.error'), addedServers: [], updatedServers: [], + allServers: [], errorDetails: String(error) } } diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/mcprouter.ts b/src/renderer/src/pages/settings/MCPSettings/providers/mcprouter.ts index a993ce7383..152f68e718 100644 --- a/src/renderer/src/pages/settings/MCPSettings/providers/mcprouter.ts +++ b/src/renderer/src/pages/settings/MCPSettings/providers/mcprouter.ts @@ -43,6 +43,7 @@ interface MCPRouterSyncResult { message: string addedServers: MCPServer[] updatedServers: MCPServer[] + allServers: MCPServer[] errorDetails?: string } @@ -72,7 +73,8 @@ export const syncMCPRouterServers = async ( success: false, message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'), addedServers: [], - updatedServers: [] + updatedServers: [], + allServers: [] } } @@ -83,6 +85,7 @@ export const syncMCPRouterServers = async ( message: t('settings.mcp.sync.error'), addedServers: [], updatedServers: [], + allServers: [], errorDetails: `Status: ${response.status}` } } @@ -96,14 +99,15 @@ export const syncMCPRouterServers = async ( success: true, message: t('settings.mcp.sync.noServersAvailable', 'No MCP servers available'), addedServers: [], - updatedServers: [] + updatedServers: [], + allServers: [] } } // Transform MCPRouter servers to MCP servers format const addedServers: MCPServer[] = [] const updatedServers: MCPServer[] = [] - + const allServers: MCPServer[] = [] for (const server of servers) { try { // Check if server already exists using server_key @@ -132,6 +136,7 @@ export const syncMCPRouterServers = async ( // Add new server addedServers.push(mcpServer) } + allServers.push(mcpServer) } catch (err) { logger.error('Error processing MCPRouter server:', err as Error) } @@ -142,7 +147,8 @@ export const syncMCPRouterServers = async ( success: true, message: t('settings.mcp.sync.success', { count: totalServers }), addedServers, - updatedServers + updatedServers, + allServers } } catch (error) { logger.error('MCPRouter sync error:', error as Error) @@ -151,6 +157,7 @@ export const syncMCPRouterServers = async ( message: t('settings.mcp.sync.error'), addedServers: [], updatedServers: [], + allServers: [], errorDetails: String(error) } } diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/modelscope.ts b/src/renderer/src/pages/settings/MCPSettings/providers/modelscope.ts index 36fd4b0c04..d86c54e54e 100644 --- a/src/renderer/src/pages/settings/MCPSettings/providers/modelscope.ts +++ b/src/renderer/src/pages/settings/MCPSettings/providers/modelscope.ts @@ -40,6 +40,7 @@ interface ModelScopeSyncResult { message: string addedServers: MCPServer[] updatedServers: MCPServer[] + allServers: MCPServer[] errorDetails?: string } @@ -66,7 +67,8 @@ export const syncModelScopeServers = async ( success: false, message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'), addedServers: [], - updatedServers: [] + updatedServers: [], + allServers: [] } } @@ -77,6 +79,7 @@ export const syncModelScopeServers = async ( message: t('settings.mcp.sync.error'), addedServers: [], updatedServers: [], + allServers: [], errorDetails: `Status: ${response.status}` } } @@ -90,14 +93,16 @@ export const syncModelScopeServers = async ( success: true, message: t('settings.mcp.sync.noServersAvailable', 'No MCP servers available'), addedServers: [], - updatedServers: [] + updatedServers: [], + allServers: [] } } // Transform ModelScope servers to MCP servers format const addedServers: MCPServer[] = [] const updatedServers: MCPServer[] = [] - + const allServers: MCPServer[] = [] + logger.debug('ModelScope servers:', servers) for (const server of servers) { try { if (!server.operational_urls?.[0]?.url) continue @@ -128,6 +133,7 @@ export const syncModelScopeServers = async ( // Add new server addedServers.push(mcpServer) } + allServers.push(mcpServer) } catch (err) { logger.error('Error processing ModelScope server:', err as Error) } @@ -138,7 +144,8 @@ export const syncModelScopeServers = async ( success: true, message: t('settings.mcp.sync.success', { count: totalServers }), addedServers, - updatedServers + updatedServers, + allServers } } catch (error) { logger.error('ModelScope sync error:', error as Error) @@ -147,6 +154,7 @@ export const syncModelScopeServers = async ( message: t('settings.mcp.sync.error'), addedServers: [], updatedServers: [], + allServers: [], errorDetails: String(error) } } diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/tokenflux.ts b/src/renderer/src/pages/settings/MCPSettings/providers/tokenflux.ts index e3a10f8ddd..cf101cba36 100644 --- a/src/renderer/src/pages/settings/MCPSettings/providers/tokenflux.ts +++ b/src/renderer/src/pages/settings/MCPSettings/providers/tokenflux.ts @@ -46,6 +46,7 @@ interface TokenFluxSyncResult { message: string addedServers: MCPServer[] updatedServers: MCPServer[] + allServers: MCPServer[] errorDetails?: string } @@ -72,7 +73,8 @@ export const syncTokenFluxServers = async ( success: false, message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'), addedServers: [], - updatedServers: [] + updatedServers: [], + allServers: [] } } @@ -83,6 +85,7 @@ export const syncTokenFluxServers = async ( message: t('settings.mcp.sync.error'), addedServers: [], updatedServers: [], + allServers: [], errorDetails: `Status: ${response.status}` } } @@ -96,14 +99,16 @@ export const syncTokenFluxServers = async ( success: true, message: t('settings.mcp.sync.noServersAvailable', 'No MCP servers available'), addedServers: [], - updatedServers: [] + updatedServers: [], + allServers: [] } } // Transform TokenFlux servers to MCP servers format const addedServers: MCPServer[] = [] const updatedServers: MCPServer[] = [] - + const allServers: MCPServer[] = [] + logger.debug('TokenFlux servers:', servers) for (const server of servers) { try { // Check if server already exists @@ -138,6 +143,7 @@ export const syncTokenFluxServers = async ( // Add new server addedServers.push(mcpServer) } + allServers.push(mcpServer) } catch (err) { logger.error('Error processing TokenFlux server:', err as Error) } @@ -148,7 +154,8 @@ export const syncTokenFluxServers = async ( success: true, message: t('settings.mcp.sync.success', { count: totalServers }), addedServers, - updatedServers + updatedServers, + allServers } } catch (error) { logger.error('TokenFlux sync error:', error as Error) @@ -157,6 +164,7 @@ export const syncTokenFluxServers = async ( message: t('settings.mcp.sync.error'), addedServers: [], updatedServers: [], + allServers: [], errorDetails: String(error) } } From e268e69597177cdb165f55b6c005086f776aec47 Mon Sep 17 00:00:00 2001 From: Phantom Date: Fri, 7 Nov 2025 22:24:05 +0800 Subject: [PATCH 005/173] refactor(config): centralize home directory constant to shared config (#11158) Replace hardcoded '.cherrystudio' directory references with HOME_CHERRY_DIR constant --- packages/shared/config/constant.ts | 3 +++ src/main/services/CodeToolsService.ts | 13 +++++++------ src/main/services/MCPService.ts | 3 ++- src/main/services/OvmsManager.ts | 17 +++++++++-------- src/main/services/SpanCacheService.ts | 3 ++- src/main/services/ocr/builtin/OvOcrService.ts | 5 +++-- src/main/utils/file.ts | 6 +++--- src/main/utils/init.ts | 3 ++- src/main/utils/process.ts | 5 +++-- 9 files changed, 34 insertions(+), 24 deletions(-) diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index 3b38592005..9d9240223a 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -470,3 +470,6 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [ }) } ] + +// resources/scripts should be maintained manually +export const HOME_CHERRY_DIR = '.cherrystudio' diff --git a/src/main/services/CodeToolsService.ts b/src/main/services/CodeToolsService.ts index 3a93a40d79..82c9c64f87 100644 --- a/src/main/services/CodeToolsService.ts +++ b/src/main/services/CodeToolsService.ts @@ -10,6 +10,7 @@ import { getBinaryName } from '@main/utils/process' import type { TerminalConfig, TerminalConfigWithCommand } from '@shared/config/constant' import { codeTools, + HOME_CHERRY_DIR, MACOS_TERMINALS, MACOS_TERMINALS_WITH_COMMANDS, terminalApps, @@ -66,7 +67,7 @@ class CodeToolsService { } public async getBunPath() { - const dir = path.join(os.homedir(), '.cherrystudio', 'bin') + const dir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const bunName = await getBinaryName('bun') const bunPath = path.join(dir, bunName) return bunPath @@ -362,7 +363,7 @@ class CodeToolsService { private async isPackageInstalled(cliTool: string): Promise { const executableName = await this.getCliExecutableName(cliTool) - const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') + const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) // Ensure bin directory exists @@ -389,7 +390,7 @@ class CodeToolsService { logger.info(`${cliTool} is installed, getting current version`) try { const executableName = await this.getCliExecutableName(cliTool) - const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') + const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) const { stdout } = await execAsync(`"${executablePath}" --version`, { @@ -500,7 +501,7 @@ class CodeToolsService { try { const packageName = await this.getPackageName(cliTool) const bunPath = await this.getBunPath() - const bunInstallPath = path.join(os.homedir(), '.cherrystudio') + const bunInstallPath = path.join(os.homedir(), HOME_CHERRY_DIR) const registryUrl = await this.getNpmRegistryUrl() const installEnvPrefix = isWin @@ -550,7 +551,7 @@ class CodeToolsService { const packageName = await this.getPackageName(cliTool) const bunPath = await this.getBunPath() const executableName = await this.getCliExecutableName(cliTool) - const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') + const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) logger.debug(`Package name: ${packageName}`) @@ -652,7 +653,7 @@ class CodeToolsService { baseCommand = `${baseCommand} ${configParams}` } - const bunInstallPath = path.join(os.homedir(), '.cherrystudio') + const bunInstallPath = path.join(os.homedir(), HOME_CHERRY_DIR) if (isInstalled) { // If already installed, run executable directly (with optional update message) diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index ba3340780b..3831d0af1e 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -30,6 +30,7 @@ import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js' import { nanoid } from '@reduxjs/toolkit' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import type { MCPProgressEvent } from '@shared/config/types' import { IpcChannel } from '@shared/IpcChannel' import { defaultAppHeaders } from '@shared/utils' @@ -715,7 +716,7 @@ class McpService { } public async getInstallInfo() { - const dir = path.join(os.homedir(), '.cherrystudio', 'bin') + const dir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const uvName = await getBinaryName('uv') const bunName = await getBinaryName('bun') const uvPath = path.join(dir, uvName) diff --git a/src/main/services/OvmsManager.ts b/src/main/services/OvmsManager.ts index f319200ac3..3a32d74ecf 100644 --- a/src/main/services/OvmsManager.ts +++ b/src/main/services/OvmsManager.ts @@ -3,6 +3,7 @@ import { homedir } from 'node:os' import { promisify } from 'node:util' import { loggerService } from '@logger' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import * as fs from 'fs-extra' import * as path from 'path' @@ -145,7 +146,7 @@ class OvmsManager { */ public async runOvms(): Promise<{ success: boolean; message?: string }> { const homeDir = homedir() - const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const configPath = path.join(ovmsDir, 'models', 'config.json') const runBatPath = path.join(ovmsDir, 'run.bat') @@ -195,7 +196,7 @@ class OvmsManager { */ public async getOvmsStatus(): Promise<'not-installed' | 'not-running' | 'running'> { const homeDir = homedir() - const ovmsPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'ovms.exe') + const ovmsPath = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms', 'ovms.exe') try { // Check if OVMS executable exists @@ -273,7 +274,7 @@ class OvmsManager { } const homeDir = homedir() - const configPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'models', 'config.json') + const configPath = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms', 'models', 'config.json') try { if (!(await fs.pathExists(configPath))) { logger.warn(`Config file does not exist: ${configPath}`) @@ -304,7 +305,7 @@ class OvmsManager { private async applyModelPath(modelDirPath: string): Promise { const homeDir = homedir() - const patchDir = path.join(homeDir, '.cherrystudio', 'ovms', 'patch') + const patchDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'patch') if (!(await fs.pathExists(patchDir))) { return true } @@ -355,7 +356,7 @@ class OvmsManager { logger.info(`Adding model: ${modelName} with ID: ${modelId}, Source: ${modelSource}, Task: ${task}`) const homeDir = homedir() - const ovdndDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovdndDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const pathModel = path.join(ovdndDir, 'models', modelId) try { @@ -468,7 +469,7 @@ class OvmsManager { */ public async checkModelExists(modelId: string): Promise { const homeDir = homedir() - const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const configPath = path.join(ovmsDir, 'models', 'config.json') try { @@ -495,7 +496,7 @@ class OvmsManager { */ public async updateModelConfig(modelName: string, modelId: string): Promise { const homeDir = homedir() - const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const configPath = path.join(ovmsDir, 'models', 'config.json') try { @@ -548,7 +549,7 @@ class OvmsManager { */ public async getModels(): Promise { const homeDir = homedir() - const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const configPath = path.join(ovmsDir, 'models', 'config.json') try { diff --git a/src/main/services/SpanCacheService.ts b/src/main/services/SpanCacheService.ts index 62707388a4..47a89d4327 100644 --- a/src/main/services/SpanCacheService.ts +++ b/src/main/services/SpanCacheService.ts @@ -3,6 +3,7 @@ import type { Attributes, SpanEntity, TokenUsage, TraceCache } from '@mcp-trace/ import { convertSpanToSpanEntity } from '@mcp-trace/trace-core' import { SpanStatusCode } from '@opentelemetry/api' import type { ReadableSpan } from '@opentelemetry/sdk-trace-base' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import fs from 'fs/promises' import * as os from 'os' import * as path from 'path' @@ -18,7 +19,7 @@ class SpanCacheService implements TraceCache { pri constructor() { - this.fileDir = path.join(os.homedir(), '.cherrystudio', 'trace') + this.fileDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'trace') } createSpan: (span: ReadableSpan) => void = (span: ReadableSpan) => { diff --git a/src/main/services/ocr/builtin/OvOcrService.ts b/src/main/services/ocr/builtin/OvOcrService.ts index 6e0eee1c37..052682be64 100644 --- a/src/main/services/ocr/builtin/OvOcrService.ts +++ b/src/main/services/ocr/builtin/OvOcrService.ts @@ -1,5 +1,6 @@ import { loggerService } from '@logger' import { isWin } from '@main/constant' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import type { OcrOvConfig, OcrResult, SupportedOcrFile } from '@types' import { isImageFileMetadata } from '@types' import { exec } from 'child_process' @@ -13,7 +14,7 @@ import { OcrBaseService } from './OcrBaseService' const logger = loggerService.withContext('OvOcrService') const execAsync = promisify(exec) -const PATH_BAT_FILE = path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr', 'run.npu.bat') +const PATH_BAT_FILE = path.join(os.homedir(), HOME_CHERRY_DIR, 'ovms', 'ovocr', 'run.npu.bat') export class OvOcrService extends OcrBaseService { constructor() { @@ -30,7 +31,7 @@ export class OvOcrService extends OcrBaseService { } private getOvOcrPath(): string { - return path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'ovms', 'ovocr') } private getImgDir(): string { diff --git a/src/main/utils/file.ts b/src/main/utils/file.ts index 17155f423b..1432dccc8a 100644 --- a/src/main/utils/file.ts +++ b/src/main/utils/file.ts @@ -5,7 +5,7 @@ import os from 'node:os' import path from 'node:path' import { loggerService } from '@logger' -import { audioExts, documentExts, imageExts, MB, textExts, videoExts } from '@shared/config/constant' +import { audioExts, documentExts, HOME_CHERRY_DIR, imageExts, MB, textExts, videoExts } from '@shared/config/constant' import type { FileMetadata, NotesTreeNode } from '@types' import { FileTypes } from '@types' import chardet from 'chardet' @@ -160,7 +160,7 @@ export function getNotesDir() { } export function getConfigDir() { - return path.join(os.homedir(), '.cherrystudio', 'config') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'config') } export function getCacheDir() { @@ -172,7 +172,7 @@ export function getAppConfigDir(name: string) { } export function getMcpDir() { - return path.join(os.homedir(), '.cherrystudio', 'mcp') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'mcp') } /** diff --git a/src/main/utils/init.ts b/src/main/utils/init.ts index 63cf69e89b..20884b1eeb 100644 --- a/src/main/utils/init.ts +++ b/src/main/utils/init.ts @@ -3,6 +3,7 @@ import os from 'node:os' import path from 'node:path' import { isLinux, isPortable, isWin } from '@main/constant' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import { app } from 'electron' // Please don't import any other modules which is not node/electron built-in modules @@ -17,7 +18,7 @@ function hasWritePermission(path: string) { } function getConfigDir() { - return path.join(os.homedir(), '.cherrystudio', 'config') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'config') } export function initAppDataDir() { diff --git a/src/main/utils/process.ts b/src/main/utils/process.ts index f028f2d3c7..f36e86861d 100644 --- a/src/main/utils/process.ts +++ b/src/main/utils/process.ts @@ -1,4 +1,5 @@ import { loggerService } from '@logger' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import { spawn } from 'child_process' import fs from 'fs' import os from 'os' @@ -46,11 +47,11 @@ export async function getBinaryName(name: string): Promise { export async function getBinaryPath(name?: string): Promise { if (!name) { - return path.join(os.homedir(), '.cherrystudio', 'bin') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') } const binaryName = await getBinaryName(name) - const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin') + const binariesDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const binariesDirExists = fs.existsSync(binariesDir) return binariesDirExists ? path.join(binariesDir, binaryName) : binaryName } From 9a10516b52496b20b52e7d319ac07a060cfe07c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sun, 9 Nov 2025 00:31:00 +0800 Subject: [PATCH 006/173] chore: update bun and uv versions (#11193) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update bun and uv versions - Update bun from 1.2.17 to 1.3.1 - Update uv from 0.7.13 to 0.9.5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * refactor: update UV installer to support tar.gz format - Update UV package mappings from .zip to .tar.gz for macOS and Linux - Add RISCV64 Linux platform support - Implement dual extraction logic: - tar.gz extraction for macOS/Linux using tar command - zip extraction for Windows using StreamZip - Flatten directory structure during extraction - Maintain executable permissions on Unix-like systems 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * 🐛 fix: correct error handling in UV installer Remove ineffective error code 102 return from nested function. Chmod errors now properly propagate to outer try-catch block. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- resources/scripts/install-bun.js | 2 +- resources/scripts/install-uv.js | 96 ++++++++++++++++++++------------ 2 files changed, 62 insertions(+), 36 deletions(-) diff --git a/resources/scripts/install-bun.js b/resources/scripts/install-bun.js index 1467a4cde4..33ee18d732 100644 --- a/resources/scripts/install-bun.js +++ b/resources/scripts/install-bun.js @@ -7,7 +7,7 @@ const { downloadWithRedirects } = require('./download') // Base URL for downloading bun binaries const BUN_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/bun/releases/download' -const DEFAULT_BUN_VERSION = '1.2.17' // Default fallback version +const DEFAULT_BUN_VERSION = '1.3.1' // Default fallback version // Mapping of platform+arch to binary package name const BUN_PACKAGES = { diff --git a/resources/scripts/install-uv.js b/resources/scripts/install-uv.js index 3dc8b3e477..c3d34efc33 100644 --- a/resources/scripts/install-uv.js +++ b/resources/scripts/install-uv.js @@ -7,28 +7,29 @@ const { downloadWithRedirects } = require('./download') // Base URL for downloading uv binaries const UV_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/uv/releases/download' -const DEFAULT_UV_VERSION = '0.7.13' +const DEFAULT_UV_VERSION = '0.9.5' // Mapping of platform+arch to binary package name const UV_PACKAGES = { - 'darwin-arm64': 'uv-aarch64-apple-darwin.zip', - 'darwin-x64': 'uv-x86_64-apple-darwin.zip', + 'darwin-arm64': 'uv-aarch64-apple-darwin.tar.gz', + 'darwin-x64': 'uv-x86_64-apple-darwin.tar.gz', 'win32-arm64': 'uv-aarch64-pc-windows-msvc.zip', 'win32-ia32': 'uv-i686-pc-windows-msvc.zip', 'win32-x64': 'uv-x86_64-pc-windows-msvc.zip', - 'linux-arm64': 'uv-aarch64-unknown-linux-gnu.zip', - 'linux-ia32': 'uv-i686-unknown-linux-gnu.zip', - 'linux-ppc64': 'uv-powerpc64-unknown-linux-gnu.zip', - 'linux-ppc64le': 'uv-powerpc64le-unknown-linux-gnu.zip', - 'linux-s390x': 'uv-s390x-unknown-linux-gnu.zip', - 'linux-x64': 'uv-x86_64-unknown-linux-gnu.zip', - 'linux-armv7l': 'uv-armv7-unknown-linux-gnueabihf.zip', + 'linux-arm64': 'uv-aarch64-unknown-linux-gnu.tar.gz', + 'linux-ia32': 'uv-i686-unknown-linux-gnu.tar.gz', + 'linux-ppc64': 'uv-powerpc64-unknown-linux-gnu.tar.gz', + 'linux-ppc64le': 'uv-powerpc64le-unknown-linux-gnu.tar.gz', + 'linux-riscv64': 'uv-riscv64gc-unknown-linux-gnu.tar.gz', + 'linux-s390x': 'uv-s390x-unknown-linux-gnu.tar.gz', + 'linux-x64': 'uv-x86_64-unknown-linux-gnu.tar.gz', + 'linux-armv7l': 'uv-armv7-unknown-linux-gnueabihf.tar.gz', // MUSL variants - 'linux-musl-arm64': 'uv-aarch64-unknown-linux-musl.zip', - 'linux-musl-ia32': 'uv-i686-unknown-linux-musl.zip', - 'linux-musl-x64': 'uv-x86_64-unknown-linux-musl.zip', - 'linux-musl-armv6l': 'uv-arm-unknown-linux-musleabihf.zip', - 'linux-musl-armv7l': 'uv-armv7-unknown-linux-musleabihf.zip' + 'linux-musl-arm64': 'uv-aarch64-unknown-linux-musl.tar.gz', + 'linux-musl-ia32': 'uv-i686-unknown-linux-musl.tar.gz', + 'linux-musl-x64': 'uv-x86_64-unknown-linux-musl.tar.gz', + 'linux-musl-armv6l': 'uv-arm-unknown-linux-musleabihf.tar.gz', + 'linux-musl-armv7l': 'uv-armv7-unknown-linux-musleabihf.tar.gz' } /** @@ -56,6 +57,7 @@ async function downloadUvBinary(platform, arch, version = DEFAULT_UV_VERSION, is const downloadUrl = `${UV_RELEASE_BASE_URL}/${version}/${packageName}` const tempdir = os.tmpdir() const tempFilename = path.join(tempdir, packageName) + const isTarGz = packageName.endsWith('.tar.gz') try { console.log(`Downloading uv ${version} for ${platformKey}...`) @@ -65,34 +67,58 @@ async function downloadUvBinary(platform, arch, version = DEFAULT_UV_VERSION, is console.log(`Extracting ${packageName} to ${binDir}...`) - const zip = new StreamZip.async({ file: tempFilename }) + if (isTarGz) { + // Use tar command to extract tar.gz files (macOS and Linux) + const tempExtractDir = path.join(tempdir, `uv-extract-${Date.now()}`) + fs.mkdirSync(tempExtractDir, { recursive: true }) - // Get all entries in the zip file - const entries = await zip.entries() + execSync(`tar -xzf "${tempFilename}" -C "${tempExtractDir}"`, { stdio: 'inherit' }) - // Extract files directly to binDir, flattening the directory structure - for (const entry of Object.values(entries)) { - if (!entry.isDirectory) { - // Get just the filename without path - const filename = path.basename(entry.name) - const outputPath = path.join(binDir, filename) - - console.log(`Extracting ${entry.name} -> ${filename}`) - await zip.extract(entry.name, outputPath) - // Make executable files executable on Unix-like systems - if (platform !== 'win32') { - try { + // Find all files in the extracted directory and move them to binDir + const findAndMoveFiles = (dir) => { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + findAndMoveFiles(fullPath) + } else { + const filename = path.basename(entry.name) + const outputPath = path.join(binDir, filename) + fs.copyFileSync(fullPath, outputPath) + console.log(`Extracted ${entry.name} -> ${outputPath}`) + // Make executable on Unix-like systems fs.chmodSync(outputPath, 0o755) - } catch (chmodError) { - console.error(`Warning: Failed to set executable permissions on ${filename}`) - return 102 } } - console.log(`Extracted ${entry.name} -> ${outputPath}`) } + + findAndMoveFiles(tempExtractDir) + + // Clean up temporary extraction directory + fs.rmSync(tempExtractDir, { recursive: true }) + } else { + // Use StreamZip for zip files (Windows) + const zip = new StreamZip.async({ file: tempFilename }) + + // Get all entries in the zip file + const entries = await zip.entries() + + // Extract files directly to binDir, flattening the directory structure + for (const entry of Object.values(entries)) { + if (!entry.isDirectory) { + // Get just the filename without path + const filename = path.basename(entry.name) + const outputPath = path.join(binDir, filename) + + console.log(`Extracting ${entry.name} -> ${filename}`) + await zip.extract(entry.name, outputPath) + console.log(`Extracted ${entry.name} -> ${outputPath}`) + } + } + + await zip.close() } - await zip.close() fs.unlinkSync(tempFilename) console.log(`Successfully installed uv ${version} for ${platform}-${arch}`) return 0 From 58afbe8a797b72b18d7c2160487145a9d8941c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sun, 9 Nov 2025 00:31:20 +0800 Subject: [PATCH 007/173] refactor(config): optimize oxlint configuration by removing redundant default rules (#11192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove ~60 redundant rule declarations that match oxlint's default behavior. This reduces the config file by 28% (211 -> 152 lines) while maintaining identical linting behavior. Changes: - Remove default error-level rules (constructor-super, no-debugger, etc.) - Retain only custom configurations that differ from defaults - Keep all environment overrides and plugin settings unchanged - Preserve all modified severity levels (warn) and disabled rules (off) Benefits: - Improved readability: clearly shows project-specific lint strategy - Reduced maintenance: no need to sync with oxlint default updates - Smaller config: 46% fewer rule declarations (130 -> 70 rules) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- .oxlintrc.json | 83 ++++++-------------------------------------------- 1 file changed, 10 insertions(+), 73 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 5d63538e2c..0b2e1a2ab5 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -22,7 +22,6 @@ "eslint.config.mjs" ], "overrides": [ - // set different env { "env": { "node": true @@ -55,74 +54,16 @@ "files": ["src/preload/**"] } ], - // We don't use the React plugin here because its behavior differs slightly from that of ESLint's React plugin. "plugins": ["unicorn", "typescript", "oxc", "import"], "rules": { - "constructor-super": "error", - "for-direction": "error", - "getter-return": "error", "no-array-constructor": "off", - // "import/no-cycle": "error", // tons of error, bro - "no-async-promise-executor": "error", "no-caller": "warn", - "no-case-declarations": "error", - "no-class-assign": "error", - "no-compare-neg-zero": "error", - "no-cond-assign": "error", - "no-const-assign": "error", - "no-constant-binary-expression": "error", - "no-constant-condition": "error", - "no-control-regex": "error", - "no-debugger": "error", - "no-delete-var": "error", - "no-dupe-args": "error", - "no-dupe-class-members": "error", - "no-dupe-else-if": "error", - "no-dupe-keys": "error", - "no-duplicate-case": "error", - "no-empty": "error", - "no-empty-character-class": "error", - "no-empty-pattern": "error", - "no-empty-static-block": "error", "no-eval": "warn", - "no-ex-assign": "error", - "no-extra-boolean-cast": "error", "no-fallthrough": "warn", - "no-func-assign": "error", - "no-global-assign": "error", - "no-import-assign": "error", - "no-invalid-regexp": "error", - "no-irregular-whitespace": "error", - "no-loss-of-precision": "error", - "no-misleading-character-class": "error", - "no-new-native-nonconstructor": "error", - "no-nonoctal-decimal-escape": "error", - "no-obj-calls": "error", - "no-octal": "error", - "no-prototype-builtins": "error", - "no-redeclare": "error", - "no-regex-spaces": "error", - "no-self-assign": "error", - "no-setter-return": "error", - "no-shadow-restricted-names": "error", - "no-sparse-arrays": "error", - "no-this-before-super": "error", "no-unassigned-vars": "warn", - "no-undef": "error", - "no-unexpected-multiline": "error", - "no-unreachable": "error", - "no-unsafe-finally": "error", - "no-unsafe-negation": "error", - "no-unsafe-optional-chaining": "error", - "no-unused-expressions": "off", // this rule disallow us to use expression to call function, like `condition && fn()` - "no-unused-labels": "error", - "no-unused-private-class-members": "error", + "no-unused-expressions": "off", "no-unused-vars": ["warn", { "caughtErrors": "none" }], - "no-useless-backreference": "error", - "no-useless-catch": "error", - "no-useless-escape": "error", "no-useless-rename": "warn", - "no-with": "error", "oxc/bad-array-method-on-arguments": "warn", "oxc/bad-char-at-comparison": "warn", "oxc/bad-comparison-sequence": "warn", @@ -134,19 +75,17 @@ "oxc/erasing-op": "warn", "oxc/missing-throw": "warn", "oxc/number-arg-out-of-range": "warn", - "oxc/only-used-in-recursion": "off", // manually off bacause of existing warning. may turn it on in the future + "oxc/only-used-in-recursion": "off", "oxc/uninvoked-array-callback": "warn", - "require-yield": "error", "typescript/await-thenable": "warn", - // "typescript/ban-ts-comment": "error", - "typescript/no-array-constructor": "error", "typescript/consistent-type-imports": "error", + "typescript/no-array-constructor": "error", "typescript/no-array-delete": "warn", "typescript/no-base-to-string": "warn", "typescript/no-duplicate-enum-values": "error", "typescript/no-duplicate-type-constituents": "warn", "typescript/no-empty-object-type": "off", - "typescript/no-explicit-any": "off", // not safe but too many errors + "typescript/no-explicit-any": "off", "typescript/no-extra-non-null-assertion": "error", "typescript/no-floating-promises": "warn", "typescript/no-for-in-array": "warn", @@ -155,7 +94,7 @@ "typescript/no-misused-new": "error", "typescript/no-misused-spread": "warn", "typescript/no-namespace": "error", - "typescript/no-non-null-asserted-optional-chain": "off", // it's off now. but may turn it on. + "typescript/no-non-null-asserted-optional-chain": "off", "typescript/no-redundant-type-constituents": "warn", "typescript/no-require-imports": "off", "typescript/no-this-alias": "error", @@ -173,20 +112,18 @@ "typescript/triple-slash-reference": "error", "typescript/unbound-method": "warn", "unicorn/no-await-in-promise-methods": "warn", - "unicorn/no-empty-file": "off", // manually off bacause of existing warning. may turn it on in the future + "unicorn/no-empty-file": "off", "unicorn/no-invalid-fetch-options": "warn", "unicorn/no-invalid-remove-event-listener": "warn", - "unicorn/no-new-array": "off", // manually off bacause of existing warning. may turn it on in the future + "unicorn/no-new-array": "off", "unicorn/no-single-promise-in-promise-methods": "warn", - "unicorn/no-thenable": "off", // manually off bacause of existing warning. may turn it on in the future + "unicorn/no-thenable": "off", "unicorn/no-unnecessary-await": "warn", "unicorn/no-useless-fallback-in-spread": "warn", "unicorn/no-useless-length-check": "warn", - "unicorn/no-useless-spread": "off", // manually off bacause of existing warning. may turn it on in the future + "unicorn/no-useless-spread": "off", "unicorn/prefer-set-size": "warn", - "unicorn/prefer-string-starts-ends-with": "warn", - "use-isnan": "error", - "valid-typeof": "error" + "unicorn/prefer-string-starts-ends-with": "warn" }, "settings": { "jsdoc": { From 57d9a31c0f5c43def814bf187d7b3a6031c9ea71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sun, 9 Nov 2025 00:31:35 +0800 Subject: [PATCH 008/173] refactor(migrate): consolidate migrations into version 172 (#11194) * refactor(migrate): consolidate migrations into version 172 Consolidates migrations 162-166 into a single migration 172 to fix data inconsistencies between release/v1.6.x and v1.7.0-x versions. This ensures a single, consistent migration path and corrects data deviations that occurred during version upgrades. Changes: - Remove separate migrations 162-166 - Add consolidated migration 172 that includes: - Mini app additions (ling, huggingchat) - OCR provider updates (ovocr) - Agent to preset migration - Sidebar icon updates (agents -> store) - LLM provider Anthropic API host configurations - Assistant preset settings initialization Co-Authored-By: Claude * refactor(store): update persist version to 172 Update the redux-persist version number from 171 to 172 to match the consolidated migration version. Co-Authored-By: Claude * fix(migrate): add missing break statement in switch case Add missing break statement after 'grok' case to prevent fall-through to 'cherryin' case. Also add break statement for 'longcat' case. Co-Authored-By: Claude --------- Co-authored-by: Claude --- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/migrate.ts | 218 +++++++++++++----------------- 2 files changed, 93 insertions(+), 127 deletions(-) diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index e68ada058f..14adb6cde0 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -67,7 +67,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 171, + version: 172, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index ba9bf21f45..d15fe05cb0 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2623,132 +2623,6 @@ const migrateConfig = { return state } }, - '162': (state: RootState) => { - try { - // @ts-ignore - if (state?.agents?.agents) { - // @ts-ignore - state.assistants.presets = [...state.agents.agents] - // @ts-ignore - delete state.agents.agents - } - - if (state.settings.sidebarIcons) { - state.settings.sidebarIcons.visible = state.settings.sidebarIcons.visible.map((icon) => { - // @ts-ignore - return icon === 'agents' ? 'store' : icon - }) - state.settings.sidebarIcons.disabled = state.settings.sidebarIcons.disabled.map((icon) => { - // @ts-ignore - return icon === 'agents' ? 'store' : icon - }) - } - - state.llm.providers.forEach((provider) => { - if (provider.anthropicApiHost) { - return - } - - switch (provider.id) { - case 'deepseek': - provider.anthropicApiHost = 'https://api.deepseek.com/anthropic' - break - case 'moonshot': - provider.anthropicApiHost = 'https://api.moonshot.cn/anthropic' - break - case 'zhipu': - provider.anthropicApiHost = 'https://open.bigmodel.cn/api/anthropic' - break - case 'dashscope': - provider.anthropicApiHost = 'https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy' - break - case 'modelscope': - provider.anthropicApiHost = 'https://api-inference.modelscope.cn' - break - case 'aihubmix': - provider.anthropicApiHost = 'https://aihubmix.com' - break - case 'new-api': - provider.anthropicApiHost = 'http://localhost:3000' - break - case 'grok': - provider.anthropicApiHost = 'https://api.x.ai' - } - }) - return state - } catch (error) { - logger.error('migrate 162 error', error as Error) - return state - } - }, - '163': (state: RootState) => { - try { - addOcrProvider(state, BUILTIN_OCR_PROVIDERS_MAP.ovocr) - state.llm.providers.forEach((provider) => { - if (provider.id === 'cherryin') { - provider.anthropicApiHost = 'https://open.cherryin.net' - } - }) - state.paintings.ovms_paintings = [] - return state - } catch (error) { - logger.error('migrate 163 error', error as Error) - return state - } - }, - '164': (state: RootState) => { - try { - addMiniApp(state, 'ling') - return state - } catch (error) { - logger.error('migrate 164 error', error as Error) - return state - } - }, - '165': (state: RootState) => { - try { - addMiniApp(state, 'huggingchat') - return state - } catch (error) { - logger.error('migrate 165 error', error as Error) - return state - } - }, - '166': (state: RootState) => { - try { - if (state.assistants.presets === undefined) { - state.assistants.presets = [] - } - state.assistants.presets.forEach((preset) => { - if (!preset.settings) { - preset.settings = DEFAULT_ASSISTANT_SETTINGS - } else if (!preset.settings.toolUseMode) { - preset.settings.toolUseMode = DEFAULT_ASSISTANT_SETTINGS.toolUseMode - } - }) - // 更新阿里云百炼的 Anthropic API 地址 - const dashscopeProvider = state.llm.providers.find((provider) => provider.id === 'dashscope') - if (dashscopeProvider) { - dashscopeProvider.anthropicApiHost = 'https://dashscope.aliyuncs.com/apps/anthropic' - } - - state.llm.providers.forEach((provider) => { - if (provider.id === SystemProviderIds['new-api'] && provider.type !== 'new-api') { - provider.type = 'new-api' - } - if (provider.id === SystemProviderIds.longcat) { - // https://longcat.chat/platform/docs/zh/#anthropic-api-%E6%A0%BC%E5%BC%8F - if (!provider.anthropicApiHost) { - provider.anthropicApiHost = 'https://api.longcat.chat/anthropic' - } - } - }) - return state - } catch (error) { - logger.error('migrate 166 error', error as Error) - return state - } - }, '167': (state: RootState) => { try { addProvider(state, 'huggingface') @@ -2817,6 +2691,98 @@ const migrateConfig = { logger.error('migrate 171 error', error as Error) return state } + }, + '172': (state: RootState) => { + try { + // Add ling and huggingchat mini apps + addMiniApp(state, 'ling') + addMiniApp(state, 'huggingchat') + + // Add ovocr provider and clear ovms paintings + addOcrProvider(state, BUILTIN_OCR_PROVIDERS_MAP.ovocr) + if (isEmpty(state.paintings.ovms_paintings)) { + state.paintings.ovms_paintings = [] + } + + // Migrate agents to assistants presets + // @ts-ignore + if (state?.agents?.agents) { + // @ts-ignore + state.assistants.presets = [...state.agents.agents] + // @ts-ignore + delete state.agents.agents + } + + // Initialize assistants presets + if (state.assistants.presets === undefined) { + state.assistants.presets = [] + } + + // Migrate assistants presets + state.assistants.presets.forEach((preset) => { + if (!preset.settings) { + preset.settings = DEFAULT_ASSISTANT_SETTINGS + } else if (!preset.settings.toolUseMode) { + preset.settings.toolUseMode = DEFAULT_ASSISTANT_SETTINGS.toolUseMode + } + }) + + // Migrate sidebar icons + if (state.settings.sidebarIcons) { + state.settings.sidebarIcons.visible = state.settings.sidebarIcons.visible.map((icon) => { + // @ts-ignore + return icon === 'agents' ? 'store' : icon + }) + state.settings.sidebarIcons.disabled = state.settings.sidebarIcons.disabled.map((icon) => { + // @ts-ignore + return icon === 'agents' ? 'store' : icon + }) + } + + // Migrate llm providers + state.llm.providers.forEach((provider) => { + if (provider.id === SystemProviderIds['new-api'] && provider.type !== 'new-api') { + provider.type = 'new-api' + } + + switch (provider.id) { + case 'deepseek': + provider.anthropicApiHost = 'https://api.deepseek.com/anthropic' + break + case 'moonshot': + provider.anthropicApiHost = 'https://api.moonshot.cn/anthropic' + break + case 'zhipu': + provider.anthropicApiHost = 'https://open.bigmodel.cn/api/anthropic' + break + case 'dashscope': + provider.anthropicApiHost = 'https://dashscope.aliyuncs.com/apps/anthropic' + break + case 'modelscope': + provider.anthropicApiHost = 'https://api-inference.modelscope.cn' + break + case 'aihubmix': + provider.anthropicApiHost = 'https://aihubmix.com' + break + case 'new-api': + provider.anthropicApiHost = 'http://localhost:3000' + break + case 'grok': + provider.anthropicApiHost = 'https://api.x.ai' + break + case 'cherryin': + provider.anthropicApiHost = 'https://open.cherryin.net' + break + case 'longcat': + provider.anthropicApiHost = 'https://api.longcat.chat/anthropic' + break + } + }) + return state + } catch (error) { + logger.error('migrate 172 error', error as Error) + return state + } } } From ed453750fea208b0b536a1875d67df305f0177a1 Mon Sep 17 00:00:00 2001 From: cheng chao Date: Sun, 9 Nov 2025 01:45:25 +0800 Subject: [PATCH 009/173] fix(mcp): resolve OAuth callback page hanging and add i18n support (#11195) - Fix OAuth callback server not sending HTTP response, causing browser to hang - Add internationalization support for OAuth callback page (10 languages) - Simplify callback page design with clean white background - Improve user experience with localized success messages Changes: - src/main/services/mcp/oauth/callback.ts: Add HTTP response to OAuth callback - src/renderer/src/i18n/: Add callback page translations for all supported languages Signed-off-by: charles --- src/main/services/mcp/oauth/callback.ts | 81 ++++++++++++++++++++++ src/renderer/src/i18n/locales/en-us.json | 6 ++ src/renderer/src/i18n/locales/zh-cn.json | 6 ++ src/renderer/src/i18n/locales/zh-tw.json | 6 ++ src/renderer/src/i18n/translate/de-de.json | 6 ++ src/renderer/src/i18n/translate/el-gr.json | 6 ++ src/renderer/src/i18n/translate/es-es.json | 6 ++ src/renderer/src/i18n/translate/fr-fr.json | 6 ++ src/renderer/src/i18n/translate/ja-jp.json | 6 ++ src/renderer/src/i18n/translate/pt-pt.json | 6 ++ src/renderer/src/i18n/translate/ru-ru.json | 6 ++ 11 files changed, 141 insertions(+) diff --git a/src/main/services/mcp/oauth/callback.ts b/src/main/services/mcp/oauth/callback.ts index 81d435f867..c13ecd5c07 100644 --- a/src/main/services/mcp/oauth/callback.ts +++ b/src/main/services/mcp/oauth/callback.ts @@ -1,4 +1,6 @@ import { loggerService } from '@logger' +import { configManager } from '@main/services/ConfigManager' +import { locales } from '@main/utils/locales' import type EventEmitter from 'events' import http from 'http' import { URL } from 'url' @@ -7,6 +9,36 @@ import type { OAuthCallbackServerOptions } from './types' const logger = loggerService.withContext('MCP:OAuthCallbackServer') +function getTranslation(key: string): string { + const language = configManager.getLanguage() + const localeData = locales[language] + + if (!localeData) { + logger.warn(`No locale data found for language: ${language}`) + return key + } + + const translations = localeData.translation as any + if (!translations) { + logger.warn(`No translations found for language: ${language}`) + return key + } + + const keys = key.split('.') + let value = translations + + for (const k of keys) { + if (value && typeof value === 'object' && k in value) { + value = value[k] + } else { + logger.warn(`Translation key not found: ${key} (failed at: ${k})`) + return key // fallback to key if translation not found + } + } + + return typeof value === 'string' ? value : key +} + export class CallBackServer { private server: Promise private events: EventEmitter @@ -28,6 +60,55 @@ export class CallBackServer { if (code) { // Emit the code event this.events.emit('auth-code-received', code) + // Send success response to browser + const title = getTranslation('settings.mcp.oauth.callback.title') + const message = getTranslation('settings.mcp.oauth.callback.message') + + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }) + res.end(` + + + + + ${title} + + + +
+

${title}

+

${message}

+
+ + + `) + } else { + res.writeHead(400, { 'Content-Type': 'text/plain' }) + res.end('Missing authorization code') } } catch (error) { logger.error('Error processing OAuth callback:', error as Error) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index fbffb92777..0695075051 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -3863,6 +3863,12 @@ "usage": "Usage", "version": "Version" }, + "oauth": { + "callback": { + "message": "You can close this page and return to Cherry Studio", + "title": "Authentication Successful" + } + }, "prompts": { "arguments": "Arguments", "availablePrompts": "Available Prompts", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 26fb5dbe75..37659a7dd7 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -3863,6 +3863,12 @@ "usage": "用法", "version": "版本" }, + "oauth": { + "callback": { + "message": "您可以关闭此页面并返回 Cherry Studio", + "title": "认证成功" + } + }, "prompts": { "arguments": "参数", "availablePrompts": "可用提示", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 8b16b3e94e..5016bcfe1d 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -3863,6 +3863,12 @@ "usage": "用法", "version": "版本" }, + "oauth": { + "callback": { + "message": "您可以關閉此頁面並返回 Cherry Studio", + "title": "認證成功" + } + }, "prompts": { "arguments": "參數", "availablePrompts": "可用提示", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index d94e74422b..59a1f8489e 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -3863,6 +3863,12 @@ "usage": "Verwendung", "version": "Version" }, + "oauth": { + "callback": { + "message": "Sie können diese Seite schließen und zu Cherry Studio zurückkehren", + "title": "Authentifizierung erfolgreich" + } + }, "prompts": { "arguments": "Parameter", "availablePrompts": "Verfügbare Prompts", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 069cd8da8b..c77b757c05 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -3863,6 +3863,12 @@ "usage": "Χρήση", "version": "Έκδοση" }, + "oauth": { + "callback": { + "message": "Μπορείτε να κλείσετε αυτήν τη σελίδα και να επιστρέψετε στο Cherry Studio", + "title": "Επιτυχής Ταυτοποίηση" + } + }, "prompts": { "arguments": "Ορίσματα", "availablePrompts": "Διαθέσιμες Υποδείξεις", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index f3c7342b21..722730c645 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -3863,6 +3863,12 @@ "usage": "Uso", "version": "Versión" }, + "oauth": { + "callback": { + "message": "Puede cerrar esta página y volver a Cherry Studio", + "title": "Autenticación Exitosa" + } + }, "prompts": { "arguments": "Argumentos", "availablePrompts": "Indicaciones disponibles", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 79dd7c4141..47ceca85e5 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -3863,6 +3863,12 @@ "usage": "Utilisation", "version": "Version" }, + "oauth": { + "callback": { + "message": "Vous pouvez fermer cette page et retourner à Cherry Studio", + "title": "Authentification Réussie" + } + }, "prompts": { "arguments": "Arguments", "availablePrompts": "Invites disponibles", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 9655d8fb7b..409a5ba997 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -3863,6 +3863,12 @@ "usage": "使用法", "version": "バージョン" }, + "oauth": { + "callback": { + "message": "このページを閉じてCherry Studioに戻ることができます", + "title": "認証成功" + } + }, "prompts": { "arguments": "引数", "availablePrompts": "利用可能なプロンプト", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 4be4a3dd97..b7cd03ceca 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -3863,6 +3863,12 @@ "usage": "Uso", "version": "Versão" }, + "oauth": { + "callback": { + "message": "Você pode fechar esta página e retornar ao Cherry Studio", + "title": "Autenticação Bem-Sucedida" + } + }, "prompts": { "arguments": "Argumentos", "availablePrompts": "Dicas disponíveis", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 241fde8fd3..87921d9c5e 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -3863,6 +3863,12 @@ "usage": "Использование", "version": "Версия" }, + "oauth": { + "callback": { + "message": "Вы можете закрыть эту страницу и вернуться в Cherry Studio", + "title": "Аутентификация Успешна" + } + }, "prompts": { "arguments": "Аргументы", "availablePrompts": "Доступные подсказки", From 85a628f8dd211682b3fd235c5ba676fc94e22b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sun, 9 Nov 2025 12:06:50 +0800 Subject: [PATCH 010/173] style(ui): center plugin browser tabs (#11205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 💄 style(ui): center plugin browser tabs Center the tab items in the plugin browser component for better visual alignment and improved user experience. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- .../pages/settings/AgentSettings/components/PluginBrowser.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/src/pages/settings/AgentSettings/components/PluginBrowser.tsx b/src/renderer/src/pages/settings/AgentSettings/components/PluginBrowser.tsx index 9251a47ae6..6d3c21e4dc 100644 --- a/src/renderer/src/pages/settings/AgentSettings/components/PluginBrowser.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/components/PluginBrowser.tsx @@ -263,6 +263,7 @@ export const PluginBrowser: FC = ({ items={pluginTypeTabItems} className="w-full" size="small" + centered /> From d5826c2dc7bd626eadc00a9743a38bf2ab363fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sun, 9 Nov 2025 12:27:15 +0800 Subject: [PATCH 011/173] fix(ui): truncate long Bash command in tag with popover (#11200) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 fix(ui): truncate long Bash command in tag with popover Add automatic truncation for Bash commands exceeding 200 characters in the tag display. When truncated, users can hover over the tag to view the full command in a popover. - Add MAX_TAG_LENGTH constant (200 chars) - Implement command truncation logic - Add Popover component for full command display on hover - Prevent UI overflow issues with long commands 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * ♻️ refactor(ui): reduce MAX_TAG_LENGTH to 100 for smaller screens Reduce the command truncation threshold from 200 to 100 characters to better support smaller screen sizes and improve readability. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * docs: remove emoji requirement from conventional commits Update commit message guidelines to use standard Conventional Commit format without emoji prefixes for better compatibility and consistency. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- CLAUDE.md | 3 +-- .../Tools/MessageAgentTools/BashTool.tsx | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0728605824..372bff256c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,8 +10,7 @@ This file provides guidance to AI coding assistants when working with code in th - **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`. - **Research via subagent**: Lean on `subagent` for external docs, APIs, news, and references. - **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications. -- **Write conventional commits with emoji**: Commit small, focused changes using emoji-prefixed Conventional Commit messages (e.g., `✨ feat:`, `🐛 fix:`, `♻️ refactor:`, ` -📝 docs:`). +- **Write conventional commits**: Commit small, focused changes using Conventional Commit messages (e.g., `feat:`, `fix:`, `refactor:`, `docs:`). ## Development Commands diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx index d92b6461a4..9b9d98054d 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx @@ -1,10 +1,12 @@ import type { CollapseProps } from 'antd' -import { Tag } from 'antd' +import { Popover, Tag } from 'antd' import { Terminal } from 'lucide-react' import { ToolTitle } from './GenericTools' import type { BashToolInput as BashToolInputType, BashToolOutput as BashToolOutputType } from './types' +const MAX_TAG_LENGTH = 100 + export function BashTool({ input, output @@ -15,6 +17,13 @@ export function BashTool({ // 如果有输出,计算输出行数 const outputLines = output ? output.split('\n').length : 0 + // 处理命令字符串的截断 + const command = input.command + const needsTruncate = command.length > MAX_TAG_LENGTH + const displayCommand = needsTruncate ? `${command.slice(0, MAX_TAG_LENGTH)}...` : command + + const tagContent = {displayCommand} + return { key: 'tool', label: ( @@ -26,7 +35,15 @@ export function BashTool({ stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined} />
- {input.command} + {needsTruncate ? ( + {command}
} + trigger="hover"> + {tagContent} + + ) : ( + tagContent + )} ), From 66f66fe08ea8ebaa3d8561746b14b29b787e4c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sun, 9 Nov 2025 17:50:41 +0800 Subject: [PATCH 012/173] fix: prevent MCP card description text from overflowing dialog width (#11203) * fix: prevent MCP card description text from overflowing dialog width Add whitespace-pre-wrap and break-all classes to the MCP server description text in Agent Settings to ensure long descriptions wrap properly within the dialog bounds instead of causing layout overflow issues. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * feat: display MCP server logo in Agent Settings tooling section Add logo display support for MCP servers in the Agent Settings tooling section. When a server has a logoUrl defined, it will now be shown next to the server name as a 20x20px rounded image, matching the design pattern used in MCPSettings. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- .../AgentSettings/ToolingSettings.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx b/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx index e353799c5b..40dc2249a6 100644 --- a/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx @@ -459,11 +459,22 @@ export const ToolingSettings: FC = ({ agentBase, upda key={server.id} className="border border-default-200" title={ -
-
- {server.name} +
+
+
+ {server.logoUrl && ( + {`${server.name} + )} + {server.name} +
{server.description ? ( - {server.description} + + {server.description} + ) : null}
Date: Sun, 9 Nov 2025 17:53:05 +0800 Subject: [PATCH 013/173] fix(ErrorBlock): reorder field (#11057) feat(ErrorBlock): add responseBody display above requestBodyValues Move responseBody display to appear before requestBodyValues for better error flow readability --- .../pages/home/Messages/Blocks/ErrorBlock.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx index adf38d9aed..f3ed182aee 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx @@ -303,7 +303,7 @@ const BuiltinError = ({ error }: { error: SerializedError }) => { ) } -// 作为 base,渲染公共字段,应当在 ErrorDetailList 中渲染 +// Base component to render common fields, should be rendered inside ErrorDetailList const AiSdkErrorBase = ({ error }: { error: SerializedAiSdkError }) => { const { t } = useTranslation() const { highlightCode } = useCodeStyle() @@ -368,6 +368,13 @@ const AiSdkError = ({ error }: { error: SerializedAiSdkErrorUnion }) => { {isSerializedAiSdkAPICallError(error) && ( <> + {error.responseBody && ( + + {t('error.responseBody')}: + + + )} + {error.requestBodyValues && ( {t('error.requestBodyValues')}: @@ -392,13 +399,6 @@ const AiSdkError = ({ error }: { error: SerializedAiSdkErrorUnion }) => { )} - {error.responseBody && ( - - {t('error.responseBody')}: - - - )} - {error.data && ( {t('error.data')}: From 9013fcba14ccdfe3fd8605c5001a3e5f532555e8 Mon Sep 17 00:00:00 2001 From: Phantom Date: Sun, 9 Nov 2025 18:17:34 +0800 Subject: [PATCH 014/173] fix(useMessageOperations): skip timestamp update for UI-only changes (#10927) Prevent unnecessary message updates when only UI-related states change by checking the update keys and skipping timestamp updates in those cases --- src/renderer/src/hooks/useMessageOperations.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/hooks/useMessageOperations.ts b/src/renderer/src/hooks/useMessageOperations.ts index b1836f3fa7..b3b920085a 100644 --- a/src/renderer/src/hooks/useMessageOperations.ts +++ b/src/renderer/src/hooks/useMessageOperations.ts @@ -20,11 +20,11 @@ import { updateMessageAndBlocksThunk, updateTranslationBlockThunk } from '@renderer/store/thunk/messageThunk' -import type { Assistant, Model, Topic, TranslateLanguageCode } from '@renderer/types' +import { type Assistant, type Model, objectKeys, type Topic, type TranslateLanguageCode } from '@renderer/types' import type { Message, MessageBlock } from '@renderer/types/newMessage' import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage' import { abortCompletion } from '@renderer/utils/abortController' -import { throttle } from 'lodash' +import { difference, throttle } from 'lodash' import { useCallback } from 'react' const logger = loggerService.withContext('UseMessageOperations') @@ -82,10 +82,12 @@ export function useMessageOperations(topic: Topic) { logger.error('[editMessage] Topic prop is not valid.') return } - + const uiStates = ['multiModelMessageStyle', 'foldSelected'] as const satisfies (keyof Message)[] + const extraUpdate = difference(objectKeys(updates), uiStates) + const isUiUpdateOnly = extraUpdate.length === 0 const messageUpdates: Partial & Pick = { id: messageId, - updatedAt: new Date().toISOString(), + updatedAt: isUiUpdateOnly ? undefined : new Date().toISOString(), ...updates } From 120ac122ebbf09f3bca9299ebb8a062a744ed8a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Sun, 9 Nov 2025 23:24:35 +0800 Subject: [PATCH 015/173] fix(ui): resolve sidebar tooltip overlap with window controls on macOS (#11216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #11125 Add placement="right" to sidebar toggle tooltips in ChatNavbar, Navbar, and Notes HeaderNavbar to prevent tooltips from overlapping with macOS window control buttons (minimize, maximize, close) in the top-left corner. This ensures tooltips appear to the right of the toggle buttons rather than above them, avoiding overlap with native window controls. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- src/renderer/src/pages/home/ChatNavbar.tsx | 2 +- src/renderer/src/pages/home/Navbar.tsx | 2 +- src/renderer/src/pages/notes/HeaderNavbar.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/pages/home/ChatNavbar.tsx b/src/renderer/src/pages/home/ChatNavbar.tsx index bdc000f223..17b45ad189 100644 --- a/src/renderer/src/pages/home/ChatNavbar.tsx +++ b/src/renderer/src/pages/home/ChatNavbar.tsx @@ -84,7 +84,7 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo )} {isTopNavbar && !showAssistants && ( - + toggleShowAssistants()} style={{ marginRight: 8 }}> diff --git a/src/renderer/src/pages/home/Navbar.tsx b/src/renderer/src/pages/home/Navbar.tsx index dd3a215922..0d93a4bd01 100644 --- a/src/renderer/src/pages/home/Navbar.tsx +++ b/src/renderer/src/pages/home/Navbar.tsx @@ -98,7 +98,7 @@ const HeaderNavbar: FC = ({ paddingRight: 0, minWidth: 'auto' }}> - + toggleShowAssistants()}> diff --git a/src/renderer/src/pages/notes/HeaderNavbar.tsx b/src/renderer/src/pages/notes/HeaderNavbar.tsx index 044e87bbe6..a2d6e66039 100644 --- a/src/renderer/src/pages/notes/HeaderNavbar.tsx +++ b/src/renderer/src/pages/notes/HeaderNavbar.tsx @@ -181,7 +181,7 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpand )} {!showWorkspace && ( - + From e43562423e7b639e1d0e4cf55dfa1c552ffc4610 Mon Sep 17 00:00:00 2001 From: fullex <106392080+0xfullex@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:14:32 +0800 Subject: [PATCH 016/173] refactor: remove unused files and configurations (#11176) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ refactor: remove unused resources/js directory and references Remove legacy resources/js directory (bridge.js and utils.js) that was left over after minapp.html removal in commit 461458e5e. Also update .oxlintrc.json to remove the unused resources/js/** file pattern. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * ♻️ refactor: remove additional unused files - Remove duplicate ipService.js (superseded by TypeScript version in src/main/utils/) - Remove unused components.json (shadcn config with non-existent target directory) - Remove unused context-menu.tsx component (no imports found) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- .oxlintrc.json | 3 +- components.json | 21 --- resources/js/bridge.js | 36 ----- resources/js/utils.js | 5 - resources/scripts/ipService.js | 88 ------------ src/renderer/src/ui/context-menu.tsx | 207 --------------------------- 6 files changed, 1 insertion(+), 359 deletions(-) delete mode 100644 components.json delete mode 100644 resources/js/bridge.js delete mode 100644 resources/js/utils.js delete mode 100644 resources/scripts/ipService.js delete mode 100644 src/renderer/src/ui/context-menu.tsx diff --git a/.oxlintrc.json b/.oxlintrc.json index 0b2e1a2ab5..329d08c043 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -35,8 +35,7 @@ "files": [ "src/renderer/**/*.{ts,tsx}", "packages/aiCore/**", - "packages/extension-table-plus/**", - "resources/js/**" + "packages/extension-table-plus/**" ] }, { diff --git a/components.json b/components.json deleted file mode 100644 index c5aceeb3ce..0000000000 --- a/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "aliases": { - "components": "@renderer/ui/third-party", - "hooks": "@renderer/hooks", - "lib": "@renderer/lib", - "ui": "@renderer/ui", - "utils": "@renderer/utils" - }, - "iconLibrary": "lucide", - "rsc": false, - "style": "new-york", - "tailwind": { - "baseColor": "zinc", - "config": "", - "css": "src/renderer/src/assets/styles/tailwind.css", - "cssVariables": true, - "prefix": "" - }, - "tsx": true -} diff --git a/resources/js/bridge.js b/resources/js/bridge.js deleted file mode 100644 index f6c0021a63..0000000000 --- a/resources/js/bridge.js +++ /dev/null @@ -1,36 +0,0 @@ -;(() => { - let messageId = 0 - const pendingCalls = new Map() - - function api(method, ...args) { - const id = messageId++ - return new Promise((resolve, reject) => { - pendingCalls.set(id, { resolve, reject }) - window.parent.postMessage({ id, type: 'api-call', method, args }, '*') - }) - } - - window.addEventListener('message', (event) => { - if (event.data.type === 'api-response') { - const { id, result, error } = event.data - const pendingCall = pendingCalls.get(id) - if (pendingCall) { - if (error) { - pendingCall.reject(new Error(error)) - } else { - pendingCall.resolve(result) - } - pendingCalls.delete(id) - } - } - }) - - window.api = new Proxy( - {}, - { - get: (target, prop) => { - return (...args) => api(prop, ...args) - } - } - ) -})() diff --git a/resources/js/utils.js b/resources/js/utils.js deleted file mode 100644 index 36981ac44f..0000000000 --- a/resources/js/utils.js +++ /dev/null @@ -1,5 +0,0 @@ -export function getQueryParam(paramName) { - const url = new URL(window.location.href) - const params = new URLSearchParams(url.search) - return params.get(paramName) -} diff --git a/resources/scripts/ipService.js b/resources/scripts/ipService.js deleted file mode 100644 index 8e997659a7..0000000000 --- a/resources/scripts/ipService.js +++ /dev/null @@ -1,88 +0,0 @@ -const https = require('https') -const { loggerService } = require('@logger') - -const logger = loggerService.withContext('IpService') - -/** - * 获取用户的IP地址所在国家 - * @returns {Promise} 返回国家代码,默认为'CN' - */ -async function getIpCountry() { - return new Promise((resolve) => { - // 添加超时控制 - const timeout = setTimeout(() => { - logger.info('IP Address Check Timeout, default to China Mirror') - resolve('CN') - }, 5000) - - const options = { - hostname: 'ipinfo.io', - path: '/json', - method: 'GET', - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', - 'Accept-Language': 'en-US,en;q=0.9' - } - } - - const req = https.request(options, (res) => { - clearTimeout(timeout) - let data = '' - - res.on('data', (chunk) => { - data += chunk - }) - - res.on('end', () => { - try { - const parsed = JSON.parse(data) - const country = parsed.country || 'CN' - logger.info(`Detected user IP address country: ${country}`) - resolve(country) - } catch (error) { - logger.error('Failed to parse IP address information:', error.message) - resolve('CN') - } - }) - }) - - req.on('error', (error) => { - clearTimeout(timeout) - logger.error('Failed to get IP address information:', error.message) - resolve('CN') - }) - - req.end() - }) -} - -/** - * 检查用户是否在中国 - * @returns {Promise} 如果用户在中国返回true,否则返回false - */ -async function isUserInChina() { - const country = await getIpCountry() - return country.toLowerCase() === 'cn' -} - -/** - * 根据用户位置获取适合的npm镜像URL - * @returns {Promise} 返回npm镜像URL - */ -async function getNpmRegistryUrl() { - const inChina = await isUserInChina() - if (inChina) { - logger.info('User in China, using Taobao npm mirror') - return 'https://registry.npmmirror.com' - } else { - logger.info('User not in China, using default npm mirror') - return 'https://registry.npmjs.org' - } -} - -module.exports = { - getIpCountry, - isUserInChina, - getNpmRegistryUrl -} diff --git a/src/renderer/src/ui/context-menu.tsx b/src/renderer/src/ui/context-menu.tsx deleted file mode 100644 index 7fdd27c38f..0000000000 --- a/src/renderer/src/ui/context-menu.tsx +++ /dev/null @@ -1,207 +0,0 @@ -'use client' - -import * as React from 'react' -import * as ContextMenuPrimitive from '@radix-ui/react-context-menu' -import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react' -import { cn } from '@renderer/utils' - -function ContextMenu({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuTrigger({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuGroup({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuPortal({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuSub({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuRadioGroup({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuSubTrigger({ - className, - inset, - children, - ...props -}: React.ComponentProps & { - inset?: boolean -}) { - return ( - - {children} - - - ) -} - -function ContextMenuSubContent({ className, ...props }: React.ComponentProps) { - return ( - - ) -} - -function ContextMenuContent({ className, ...props }: React.ComponentProps) { - return ( - - - - ) -} - -function ContextMenuItem({ - className, - inset, - variant = 'default', - ...props -}: React.ComponentProps & { - inset?: boolean - variant?: 'default' | 'destructive' -}) { - return ( - - ) -} - -function ContextMenuCheckboxItem({ - className, - children, - checked, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ) -} - -function ContextMenuRadioItem({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ) -} - -function ContextMenuLabel({ - className, - inset, - ...props -}: React.ComponentProps & { - inset?: boolean -}) { - return ( - - ) -} - -function ContextMenuSeparator({ className, ...props }: React.ComponentProps) { - return ( - - ) -} - -function ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) { - return ( - - ) -} - -export { - ContextMenu, - ContextMenuTrigger, - ContextMenuContent, - ContextMenuItem, - ContextMenuCheckboxItem, - ContextMenuRadioItem, - ContextMenuLabel, - ContextMenuSeparator, - ContextMenuShortcut, - ContextMenuGroup, - ContextMenuPortal, - ContextMenuSub, - ContextMenuSubContent, - ContextMenuSubTrigger, - ContextMenuRadioGroup -} From bc8b0a8d5322e94aad50d23e12fe59253f14c117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Mon, 10 Nov 2025 11:26:36 +0800 Subject: [PATCH 017/173] feat(agent): add permission mode display component for empty session state (#11204) Replace empty state text with a visual permission mode display card that shows: - Permission mode icon with unique colors for each mode (default, plan, acceptEdits, bypassPermissions) - Permission mode title and description - Clickable to navigate directly to tooling settings tab Replace loading text with Ant Design Spin component for better UX. --- .../home/Messages/AgentSessionMessages.tsx | 16 ++-- .../home/Messages/PermissionModeDisplay.tsx | 82 +++++++++++++++++++ 2 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 src/renderer/src/pages/home/Messages/PermissionModeDisplay.tsx diff --git a/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx b/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx index 90b284d6c4..611216919a 100644 --- a/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx +++ b/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx @@ -5,11 +5,13 @@ import { useTopicMessages } from '@renderer/hooks/useMessageOperations' import { getGroupedMessages } from '@renderer/services/MessagesService' import { type Topic, TopicType } from '@renderer/types' import { buildAgentSessionTopicId } from '@renderer/utils/agentSession' +import { Spin } from 'antd' import { memo, useMemo } from 'react' import styled from 'styled-components' import MessageGroup from './MessageGroup' import NarrowLayout from './NarrowLayout' +import PermissionModeDisplay from './PermissionModeDisplay' import { MessagesContainer, ScrollContainer } from './shared' const logger = loggerService.withContext('AgentSessionMessages') @@ -67,8 +69,12 @@ const AgentSessionMessages: React.FC = ({ agentId, sessionId }) => { groupedMessages.map(([key, groupMessages]) => ( )) + ) : session ? ( + ) : ( - {session ? 'No messages yet.' : 'Loading session...'} + + + )} @@ -77,10 +83,10 @@ const AgentSessionMessages: React.FC = ({ agentId, sessionId }) => { ) } -const EmptyState = styled.div` - color: var(--color-text-3); - font-size: 12px; - text-align: center; +const LoadingState = styled.div` + display: flex; + justify-content: center; + align-items: center; padding: 20px 0; ` diff --git a/src/renderer/src/pages/home/Messages/PermissionModeDisplay.tsx b/src/renderer/src/pages/home/Messages/PermissionModeDisplay.tsx new file mode 100644 index 0000000000..c8ad773484 --- /dev/null +++ b/src/renderer/src/pages/home/Messages/PermissionModeDisplay.tsx @@ -0,0 +1,82 @@ +import { permissionModeCards } from '@renderer/config/agent' +import SessionSettingsPopup from '@renderer/pages/settings/AgentSettings/SessionSettingsPopup' +import type { GetAgentSessionResponse, PermissionMode } from '@renderer/types' +import { FileEdit, Lightbulb, Shield, ShieldOff } from 'lucide-react' +import type { FC } from 'react' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +interface Props { + session: GetAgentSessionResponse + agentId: string +} + +const getPermissionModeConfig = (mode: PermissionMode) => { + switch (mode) { + case 'default': + return { + icon: + } + case 'plan': + return { + icon: + } + case 'acceptEdits': + return { + icon: + } + case 'bypassPermissions': + return { + icon: + } + default: + return { + icon: + } + } +} + +const PermissionModeDisplay: FC = ({ session, agentId }) => { + const { t } = useTranslation() + + const permissionMode = session?.configuration?.permission_mode ?? 'default' + + const modeCard = useMemo(() => { + return permissionModeCards.find((card) => card.mode === permissionMode) + }, [permissionMode]) + + const modeConfig = useMemo(() => getPermissionModeConfig(permissionMode), [permissionMode]) + + const handleClick = () => { + SessionSettingsPopup.show({ + agentId, + sessionId: session.id, + tab: 'tooling' + }) + } + + if (!modeCard) { + return null + } + + return ( +
+
+
{modeConfig.icon}
+
+
+ {t(modeCard.titleKey, modeCard.titleFallback)} +
+
+ {t(modeCard.descriptionKey, modeCard.descriptionFallback)}{' '} + {t(modeCard.behaviorKey, modeCard.behaviorFallback)} +
+
+
+
+ ) +} + +export default PermissionModeDisplay From 5e0a66fa1f9ef768ba55345c086646a3fb39f091 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Mon, 10 Nov 2025 15:41:17 +0800 Subject: [PATCH 018/173] docs(README): update AI Web Service Integration section and remove public beta notice - Added a hyperlink to Poe in the AI Web Service Integration list for better accessibility. - Removed the public beta notice for the Enterprise Edition to streamline the documentation. - Updated the cost section to include a link to the AGPL-3.0 License for clarity. --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c3d3f915a1..1223f73ed0 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Cherry Studio is a desktop client that supports multiple LLM providers, availabl 1. **Diverse LLM Provider Support**: - ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more -- 🔗 AI Web Service Integration: Claude, Perplexity, Poe, and others +- 🔗 AI Web Service Integration: Claude, Perplexity, [Poe](https://poe.com/), and others - 💻 Local Model Support with Ollama, LM Studio 2. **AI Assistants & Conversations**: @@ -238,10 +238,6 @@ The Enterprise Edition addresses core challenges in team collaboration by centra ## ✨ Online Demo -> 🚧 **Public Beta Notice** -> -> The Enterprise Edition is currently in its early public beta stage, and we are actively iterating and optimizing its features. We are aware that it may not be perfectly stable yet. If you encounter any issues or have valuable suggestions during your trial, we would be very grateful if you could contact us via email to provide feedback. - **🔗 [Cherry Studio Enterprise](https://www.cherry-ai.com/enterprise)** ## Version Comparison @@ -249,7 +245,7 @@ The Enterprise Edition addresses core challenges in team collaboration by centra | Feature | Community Edition | Enterprise Edition | | :---------------- | :----------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | | **Open Source** | ✅ Yes | ⭕️ Partially released to customers | -| **Cost** | Free for Personal Use / Commercial License | Buyout / Subscription Fee | +| **Cost** | [AGPL-3.0 License](https://github.com/CherryHQ/cherry-studio?tab=AGPL-3.0-1-ov-file) | Buyout / Subscription Fee | | **Admin Backend** | — | ● Centralized **Model** Access
● **Employee** Management
● Shared **Knowledge Base**
● **Access** Control
● **Data** Backup | | **Server** | — | ✅ Dedicated Private Deployment | @@ -262,8 +258,12 @@ We believe the Enterprise Edition will become your team's AI productivity engine # 🔗 Related Projects +- [new-api](https://github.com/QuantumNous/new-api): The next-generation LLM gateway and AI asset management system supports multiple languages. + - [one-api](https://github.com/songquanpeng/one-api): LLM API management and distribution system supporting mainstream models like OpenAI, Azure, and Anthropic. Features a unified API interface, suitable for key management and secondary distribution. +- [Poe](https://poe.com/): Poe gives you access to the best AI, all in one place. Explore GPT-5, Claude Opus 4.1, DeepSeek-R1, Veo 3, ElevenLabs, and millions of others. + - [ublacklist](https://github.com/iorate/ublacklist): Blocks specific sites from appearing in Google search results # 🚀 Contributors From e2c8edab61239365530d6d415e517f2ed0831c9f Mon Sep 17 00:00:00 2001 From: Konjac-XZ <71483384+Konjac-XZ@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:42:34 +0800 Subject: [PATCH 019/173] =?UTF-8?q?fix:=20incorrect=20spelling=20caused=20?= =?UTF-8?q?Gemini=20endpoint=E2=80=99s=20thinking=20budget=20to=20fail=20(?= =?UTF-8?q?#11217)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/aiCore/utils/reasoning.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/aiCore/utils/reasoning.ts b/src/renderer/src/aiCore/utils/reasoning.ts index 3a36fb658a..1d7123a47b 100644 --- a/src/renderer/src/aiCore/utils/reasoning.ts +++ b/src/renderer/src/aiCore/utils/reasoning.ts @@ -418,6 +418,8 @@ export function getAnthropicReasoningParams(assistant: Assistant, model: Model): /** * 获取 Gemini 推理参数 * 从 GeminiAPIClient 中提取的逻辑 + * 注意:Gemini/GCP 端点所使用的 thinkingBudget 等参数应该按照驼峰命名法传递 + * 而在 Google 官方提供的 OpenAI 兼容端点中则使用蛇形命名法 thinking_budget */ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Record { if (!isReasoningModel(model)) { @@ -431,8 +433,8 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re if (reasoningEffort === undefined) { return { thinkingConfig: { - include_thoughts: false, - ...(GEMINI_FLASH_MODEL_REGEX.test(model.id) ? { thinking_budget: 0 } : {}) + includeThoughts: false, + ...(GEMINI_FLASH_MODEL_REGEX.test(model.id) ? { thinkingBudget: 0 } : {}) } } } @@ -442,7 +444,7 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re if (effortRatio > 1) { return { thinkingConfig: { - include_thoughts: true + includeThoughts: true } } } @@ -452,8 +454,8 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re return { thinkingConfig: { - ...(budget > 0 ? { thinking_budget: budget } : {}), - include_thoughts: true + ...(budget > 0 ? { thinkingBudget: budget } : {}), + includeThoughts: true } } } From 2d8555c326d0ec15d5d947cb8dc51e95f9a80bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Mon, 10 Nov 2025 18:44:33 +0800 Subject: [PATCH 020/173] fix(agents): inherit allowed_tools from Agent when creating Session (#11201) When creating a Session under an Agent, the Session should inherit the Agent's allowed_tools configuration. Previously, the allowed_tools parameter was missing from the Session creation API call, causing inconsistency between Agent and Session configurations. Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- src/main/services/agents/services/SessionService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/services/agents/services/SessionService.ts b/src/main/services/agents/services/SessionService.ts index 62dad3ed51..0bb1515696 100644 --- a/src/main/services/agents/services/SessionService.ts +++ b/src/main/services/agents/services/SessionService.ts @@ -78,6 +78,7 @@ export class SessionService extends BaseService { plan_model: serializedData.plan_model || null, small_model: serializedData.small_model || null, mcps: serializedData.mcps || null, + allowed_tools: serializedData.allowed_tools || null, configuration: serializedData.configuration || null, created_at: now, updated_at: now From 2f66f5b511ef34da53e4669c26874047d82f4113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Mon, 10 Nov 2025 20:10:38 +0800 Subject: [PATCH 021/173] refactor(AssistantPresetsPage): added assistants subscribe settings to AssistantPresetsPage (#11184) refactor(DataSettings, MCPSettings, AssistantPresetsPage): clean up imports and enhance UI components - Removed unused imports and components from DataSettings for better clarity. - Updated MCPSettings layout by introducing a new Container styled with Scrollbar for improved scrolling. - Added AssistantsSubscribeUrlSettings component to manage subscription URLs in AssistantPresetsPage, enhancing user interaction. - Adjusted button styles and layout in AssistantPresetsPage for a more cohesive design. --- .../settings/DataSettings/DataSettings.tsx | 9 +- .../settings/MCPSettings/McpSettings.tsx | 89 ++++++++++--------- .../presets/AssistantPresetsPage.tsx | 25 ++++-- .../AssistantsSubscribeUrlSettings.tsx} | 30 +++++-- 4 files changed, 89 insertions(+), 64 deletions(-) rename src/renderer/src/pages/{settings/DataSettings/AgentsSubscribeUrlSettings.tsx => store/assistants/presets/components/AssistantsSubscribeUrlSettings.tsx} (60%) diff --git a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx index c7fbffeb64..379165192e 100644 --- a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx @@ -23,7 +23,7 @@ import type { AppInfo } from '@renderer/types' import { formatFileSize } from '@renderer/utils' import { occupiedDirs } from '@shared/config/constant' import { Button, Progress, Switch, Typography } from 'antd' -import { FileText, FolderCog, FolderInput, FolderOpen, SaveIcon, Sparkle } from 'lucide-react' +import { FileText, FolderCog, FolderInput, FolderOpen, SaveIcon } from 'lucide-react' import type { FC } from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -38,7 +38,6 @@ import { SettingRowTitle, SettingTitle } from '..' -import AgentsSubscribeUrlSettings from './AgentsSubscribeUrlSettings' import ExportMenuOptions from './ExportMenuSettings' import JoplinSettings from './JoplinSettings' import LocalBackupSettings from './LocalBackupSettings' @@ -129,11 +128,6 @@ const DataSettings: FC = () => { key: 'siyuan', title: t('settings.data.siyuan.title'), icon: - }, - { - key: 'agentssubscribe_url', - title: t('assistants.presets.settings.title'), - icon: } ] @@ -704,7 +698,6 @@ const DataSettings: FC = () => { {menu === 'joplin' && } {menu === 'obsidian' && } {menu === 'siyuan' && } - {menu === 'agentssubscribe_url' && } {menu === 'local_backup' && } diff --git a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx index d8f26610c9..6d4210fd1d 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx @@ -1,6 +1,7 @@ import { loggerService } from '@logger' import type { McpError } from '@modelcontextprotocol/sdk/types.js' import { DeleteIcon } from '@renderer/components/Icons' +import Scrollbar from '@renderer/components/Scrollbar' import { useTheme } from '@renderer/context/ThemeProvider' import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers' import { useMCPServerTrust } from '@renderer/hooks/useMCPServerTrust' @@ -740,51 +741,57 @@ const McpSettings: React.FC = () => { } return ( - - - - - - {server?.name} - {serverVersion && } + + + + + + + {server?.name} + {serverVersion && } + + - - - - setActiveTab(key as TabKey)} - style={{ marginTop: 8, backgroundColor: 'transparent' }} - /> - - + + + + + + + setActiveTab(key as TabKey)} + style={{ marginTop: 8, backgroundColor: 'transparent' }} + /> + + + ) } +const Container = styled(Scrollbar)` + height: calc(100vh - var(--navbar-height)); +` + const ServerName = styled.span` font-size: 14px; font-weight: 500; diff --git a/src/renderer/src/pages/store/assistants/presets/AssistantPresetsPage.tsx b/src/renderer/src/pages/store/assistants/presets/AssistantPresetsPage.tsx index b5fdc1d335..91ea8cce75 100644 --- a/src/renderer/src/pages/store/assistants/presets/AssistantPresetsPage.tsx +++ b/src/renderer/src/pages/store/assistants/presets/AssistantPresetsPage.tsx @@ -1,7 +1,7 @@ -import { ImportOutlined, PlusOutlined } from '@ant-design/icons' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { HStack } from '@renderer/components/Layout' import ListItem from '@renderer/components/ListItem' +import GeneralPopup from '@renderer/components/Popups/GeneralPopup' import Scrollbar from '@renderer/components/Scrollbar' import CustomTag from '@renderer/components/Tags/CustomTag' import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets' @@ -11,7 +11,7 @@ import type { AssistantPreset } from '@renderer/types' import { uuid } from '@renderer/utils' import { Button, Empty, Flex, Input } from 'antd' import { omit } from 'lodash' -import { Search } from 'lucide-react' +import { Import, Plus, Rss, Search } from 'lucide-react' import type { FC } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -23,6 +23,7 @@ import { groupTranslations } from './assistantPresetGroupTranslations' import AddAssistantPresetPopup from './components/AddAssistantPresetPopup' import AssistantPresetCard from './components/AssistantPresetCard' import { AssistantPresetGroupIcon } from './components/AssistantPresetGroupIcon' +import AssistantsSubscribeUrlSettings from './components/AssistantsSubscribeUrlSettings' import ImportAssistantPresetPopup from './components/ImportAssistantPresetPopup' const AssistantPresetsPage: FC = () => { @@ -175,6 +176,15 @@ const AssistantPresetsPage: FC = () => { } } + const handleSubscribeSettings = () => { + GeneralPopup.show({ + title: t('assistants.presets.settings.title'), + content: , + footer: null, + width: 600 + }) + } + return ( @@ -246,12 +256,12 @@ const AssistantPresetsPage: FC = () => { } - + {isSearchExpanded ? ( { ) )} - - + diff --git a/src/renderer/src/pages/settings/DataSettings/AgentsSubscribeUrlSettings.tsx b/src/renderer/src/pages/store/assistants/presets/components/AssistantsSubscribeUrlSettings.tsx similarity index 60% rename from src/renderer/src/pages/settings/DataSettings/AgentsSubscribeUrlSettings.tsx rename to src/renderer/src/pages/store/assistants/presets/components/AssistantsSubscribeUrlSettings.tsx index ecf4fbc756..8ea3b92fde 100755 --- a/src/renderer/src/pages/settings/DataSettings/AgentsSubscribeUrlSettings.tsx +++ b/src/renderer/src/pages/store/assistants/presets/components/AssistantsSubscribeUrlSettings.tsx @@ -1,15 +1,15 @@ import { HStack } from '@renderer/components/Layout' import { useTheme } from '@renderer/context/ThemeProvider' import { useSettings } from '@renderer/hooks/useSettings' +import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '@renderer/pages/settings' import { useAppDispatch } from '@renderer/store' import { setAgentssubscribeUrl } from '@renderer/store/settings' import Input from 'antd/es/input/Input' +import { HelpCircle } from 'lucide-react' import type { FC } from 'react' import { useTranslation } from 'react-i18next' -import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' - -const AgentsSubscribeUrlSettings: FC = () => { +const AssistantsSubscribeUrlSettings: FC = () => { const { t } = useTranslation() const { theme } = useTheme() const dispatch = useAppDispatch() @@ -20,12 +20,24 @@ const AgentsSubscribeUrlSettings: FC = () => { dispatch(setAgentssubscribeUrl(e.target.value)) } + const handleHelpClick = () => { + window.open('https://docs.cherry-ai.com/data-settings/assistants-subscribe', '_blank') + } + return ( - - {t('assistants.presets.tag.agent')} - {t('settings.tool.websearch.subscribe_add')} - + + + {t('assistants.presets.tag.agent')} + {t('settings.tool.websearch.subscribe_add')} + + + {t('settings.tool.websearch.subscribe_url')} @@ -35,7 +47,7 @@ const AgentsSubscribeUrlSettings: FC = () => { value={agentssubscribeUrl || ''} onChange={handleAgentChange} style={{ width: 315 }} - placeholder={t('settings.tool.websearch.subscribe_name.placeholder')} + placeholder={t('settings.tool.websearch.subscribe_url')} /> @@ -43,4 +55,4 @@ const AgentsSubscribeUrlSettings: FC = () => { ) } -export default AgentsSubscribeUrlSettings +export default AssistantsSubscribeUrlSettings From c1fa24522d7e3a38dd2b6877113da32ad863047b Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Mon, 10 Nov 2025 20:19:40 +0800 Subject: [PATCH 022/173] chore(release): update release notes for v1.7.0-beta.5 - Add MCPRouter provider and MCP marketplace features - Improve UI optimization and MCP OAuth callback - Fix various bugs including Agent allowed_tools inheritance --- electron-builder.yml | 73 +++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/electron-builder.yml b/electron-builder.yml index 6b14548b75..9128f4c654 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -135,59 +135,50 @@ artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | - What's New in v1.7.0-beta.4 - - Major Changes: - - UI Framework Upgrade: Improved performance and user experience with new design system - - App Menu i18n: Menu now supports multiple languages and syncs with app language settings + What's New in v1.7.0-beta.5 New Features: - - AWS Bedrock API Key: Support Bedrock API key authentication with Extended Thinking (reasoning) capability - - SophNet Provider: Added support for SophNet LLM provider - - Auto Session Rename: Agent sessions automatically rename based on conversation topics - - TopP Parameter: Added TopP parameter support for more precise model control - - Reasoning Effort Control: Quick access to reasoning effort settings in input bar + - MCPRouter Provider: Added MCPRouter provider integration with token management and server synchronization + - MCP Marketplace: Enhanced MCP server discovery and management with multi-provider marketplace support + - Agent Permission Mode Display: Visual permission mode cards in empty session states + - Assistant Subscription Settings: Added subscription URL management in assistant presets Improvements: - - Topics & Sessions: Enhanced UI with better styling and smoother interactions - - Quick Panel: Improved option visibility and control - - Painting Models: Smarter model initialization with better defaults - - System Shutdown: Better handling of shutdown events to prevent data loss - - Smaller Package Size: Optimized build configuration for faster downloads + - UI Optimization: Sidebar tooltip placement improved on macOS to avoid overlapping window controls + - MCP Server Logos: Display server logos in Agent settings tooling section + - Long Command Handling: Bash command tags now auto-truncate (hover to view full command for commands over 100 chars) + - MCP OAuth Callback: Fixed callback page hanging and added multilingual support (10 languages) + - Error Display: Improved error block display order for better readability + - Plugin Browser: Centered tab alignment for better visual consistency Bug Fixes: - - Fixed Perplexity provider support and API host formatting - - Fixed CherryAI provider support and API host formatting - - Fixed i18n translations for painting image size options - - Fixed agent session message token usage tracking - - Fixed prompt stream handling on completion or error - - Fixed message API initialization issues + - Fixed Agent sessions not inheriting allowed_tools configuration + - Fixed Gemini endpoint thinking budget spelling error + - Fixed MCP card description text overflow + - Fixed unnecessary message timestamp updates on UI-only state changes + - Updated dependencies: Bun to 1.3.1, uv to 0.9.5 - v1.7.0-beta.4 新特性 - - 重大变更: - - UI 框架升级:采用新设计系统,提升性能和用户体验 - - 应用菜单国际化:菜单支持多语言,并自动同步应用语言设置 + v1.7.0-beta.5 新特性 新功能: - - AWS Bedrock API 密钥:支持 Bedrock API 密钥身份验证,并支持扩展思考(推理)能力 - - SophNet 提供商:添加 SophNet LLM 提供商支持 - - 自动会话重命名:Agent 会话根据对话主题自动重命名 - - TopP 参数:添加 TopP 参数支持,更精确控制模型输出 + - MCPRouter 提供商:新增 MCPRouter 提供商集成,支持 token 管理和服务器同步 + - MCP 市场:增强 MCP 服务器发现和管理功能,支持多提供商市场 + - Agent 权限模式展示:空会话状态显示可视化权限模式卡片 + - 助手订阅设置:在助手预设中添加订阅 URL 管理功能 改进: - - 主题和会话:增强 UI,改进样式和交互体验 - - 快速面板:改进选项可见性和控制 - - 绘图模型:更智能的模型初始化和更好的默认值 - - 系统关机:更好地处理关机事件,防止数据丢失 - - 更小的安装包:优化构建配置,下载更快 + - UI 优化:macOS 上侧边栏工具提示位置优化,避免与窗口控制按钮重叠 + - MCP 服务器标志:在 Agent 设置工具部分显示服务器 logo + - 长命令处理:Bash 命令标签自动截断(超过 100 字符时悬停查看完整内容) + - MCP OAuth 回调:修复回调页面挂起问题并添加多语言支持(10 种语言) + - 错误信息展示:改进错误块显示顺序,提高可读性 + - 插件浏览器:标签页居中对齐,视觉效果更统一 问题修复: - - 修复 Perplexity 提供商支持和 API 主机格式化 - - 修复 CherryAI 提供商支持和 API 主机格式化 - - 修复绘图图像大小选项的 i18n 翻译 - - 修复 Agent 会话消息的 token 使用量跟踪 - - 修复完成或错误时的提示流处理 - - 修复消息 API 初始化问题 + - 修复 Agent 会话未继承 allowed_tools 配置 + - 修复 Gemini 端点 thinking budget 拼写错误 + - 修复 MCP 卡片描述文本溢出问题 + - 修复仅 UI 状态变化时消息时间戳不必要的更新 + - 依赖更新:Bun 升级到 1.3.1,uv 升级到 0.9.5 From ce5d46bfc7d1c323ba48c425e10a3387fc10101c Mon Sep 17 00:00:00 2001 From: Pleasure1234 <3196812536@qq.com> Date: Mon, 10 Nov 2025 15:28:38 +0000 Subject: [PATCH 023/173] fix: remove explicit Content-Type header in file upload (#11231) The Content-Type header was removed from the fetch request when uploading files. This change may allow the server to infer the correct content type or handle uploads more flexibly. --- .../knowledge/preprocess/MineruPreprocessProvider.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/knowledge/preprocess/MineruPreprocessProvider.ts b/src/main/knowledge/preprocess/MineruPreprocessProvider.ts index 0e93af674a..7a5362a116 100644 --- a/src/main/knowledge/preprocess/MineruPreprocessProvider.ts +++ b/src/main/knowledge/preprocess/MineruPreprocessProvider.ts @@ -275,15 +275,10 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider { try { const fileBuffer = await fs.promises.readFile(filePath) + // https://mineru.net/apiManage/docs const response = await net.fetch(uploadUrl, { method: 'PUT', - body: fileBuffer, - headers: { - 'Content-Type': 'application/pdf' - } - // headers: { - // 'Content-Length': fileBuffer.length.toString() - // } + body: fileBuffer }) if (!response.ok) { From 2663cb19cea8fb11ce9d2f08e16491b02d14fc6a Mon Sep 17 00:00:00 2001 From: SuYao Date: Tue, 11 Nov 2025 00:09:26 +0800 Subject: [PATCH 024/173] Chore/aisdk (#11232) * chore(dependencies): update AI SDK dependencies to latest versions * chore(patches): update AI SDK patches for Hugging Face, OpenAI, and Google --- ...ai-sdk-google-npm-2.0.30-3b31632362.patch} | 4 +- ...dk-huggingface-npm-0.0.8-d4d0aaac93.patch} | 0 ...ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch} | 18 +- package.json | 17 +- packages/aiCore/package.json | 14 +- yarn.lock | 243 +++++++++--------- 6 files changed, 146 insertions(+), 150 deletions(-) rename .yarn/patches/{@ai-sdk-google-npm-2.0.23-81682e07b0.patch => @ai-sdk-google-npm-2.0.30-3b31632362.patch} (81%) rename .yarn/patches/{@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch => @ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch} (100%) rename .yarn/patches/{@ai-sdk-openai-npm-2.0.52-b36d949c76.patch => @ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch} (85%) diff --git a/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch b/.yarn/patches/@ai-sdk-google-npm-2.0.30-3b31632362.patch similarity index 81% rename from .yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch rename to .yarn/patches/@ai-sdk-google-npm-2.0.30-3b31632362.patch index ba4cd59d4c..2b6fea2d37 100644 --- a/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch +++ b/.yarn/patches/@ai-sdk-google-npm-2.0.30-3b31632362.patch @@ -1,5 +1,5 @@ diff --git a/dist/index.js b/dist/index.js -index 4cc66d83af1cef39f6447dc62e680251e05ddf9f..eb9819cb674c1808845ceb29936196c4bb355172 100644 +index cac044aab0255fa72f68b36ecd2c5b12d424c379..ad6ee8ecfc5cbc3ec43ba59a44eda21e8e4d353f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -471,7 +471,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) { @@ -12,7 +12,7 @@ index 4cc66d83af1cef39f6447dc62e680251e05ddf9f..eb9819cb674c1808845ceb29936196c4 // src/google-generative-ai-options.ts diff --git a/dist/index.mjs b/dist/index.mjs -index a032505ec54e132dc386dde001dc51f710f84c83..5efada51b9a8b56e3f01b35e734908ebe3c37043 100644 +index 0793085005d7968638d355f2f1e127939d965165..1c8bf852baf025d56dc35a0691eb95967de7e5c8 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) { diff --git a/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch b/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch similarity index 100% rename from .yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch rename to .yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch diff --git a/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch b/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch similarity index 85% rename from .yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch rename to .yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch index a7985ddfcd..22b5cf6ea8 100644 --- a/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch +++ b/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch @@ -1,5 +1,5 @@ diff --git a/dist/index.js b/dist/index.js -index cc6652c4e7f32878a64a2614115bf7eeb3b7c890..76e989017549c89b45d633525efb1f318026d9b2 100644 +index 992c85ac6656e51c3471af741583533c5a7bf79f..83c05952a07aebb95fc6c62f9ddb8aa96b52ac0d 100644 --- a/dist/index.js +++ b/dist/index.js @@ -274,6 +274,7 @@ var openaiChatResponseSchema = (0, import_provider_utils3.lazyValidator)( @@ -18,30 +18,29 @@ index cc6652c4e7f32878a64a2614115bf7eeb3b7c890..76e989017549c89b45d633525efb1f31 tool_calls: import_v42.z.array( import_v42.z.object({ index: import_v42.z.number(), -@@ -785,6 +787,14 @@ var OpenAIChatLanguageModel = class { +@@ -785,6 +787,13 @@ var OpenAIChatLanguageModel = class { if (text != null && text.length > 0) { content.push({ type: "text", text }); } -+ const reasoning = -+ choice.message.reasoning_content; ++ const reasoning = choice.message.reasoning_content; + if (reasoning != null && reasoning.length > 0) { + content.push({ + type: 'reasoning', -+ text: reasoning, ++ text: reasoning + }); + } for (const toolCall of (_a = choice.message.tool_calls) != null ? _a : []) { content.push({ type: "tool-call", -@@ -866,6 +876,7 @@ var OpenAIChatLanguageModel = class { +@@ -866,6 +875,7 @@ var OpenAIChatLanguageModel = class { }; - let isFirstChunk = true; + let metadataExtracted = false; let isActiveText = false; + let isActiveReasoning = false; const providerMetadata = { openai: {} }; return { stream: response.pipeThrough( -@@ -920,6 +931,22 @@ var OpenAIChatLanguageModel = class { +@@ -923,6 +933,21 @@ var OpenAIChatLanguageModel = class { return; } const delta = choice.delta; @@ -54,7 +53,6 @@ index cc6652c4e7f32878a64a2614115bf7eeb3b7c890..76e989017549c89b45d633525efb1f31 + }); + isActiveReasoning = true; + } -+ + controller.enqueue({ + type: 'reasoning-delta', + id: 'reasoning-0', @@ -64,7 +62,7 @@ index cc6652c4e7f32878a64a2614115bf7eeb3b7c890..76e989017549c89b45d633525efb1f31 if (delta.content != null) { if (!isActiveText) { controller.enqueue({ type: "text-start", id: "0" }); -@@ -1032,6 +1059,9 @@ var OpenAIChatLanguageModel = class { +@@ -1035,6 +1060,9 @@ var OpenAIChatLanguageModel = class { } }, flush(controller) { diff --git a/package.json b/package.json index 5c41f0df65..e433434791 100644 --- a/package.json +++ b/package.json @@ -106,11 +106,11 @@ "@agentic/exa": "^7.3.3", "@agentic/searxng": "^7.3.3", "@agentic/tavily": "^7.3.3", - "@ai-sdk/amazon-bedrock": "^3.0.42", - "@ai-sdk/google-vertex": "^3.0.48", - "@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch", - "@ai-sdk/mistral": "^2.0.19", - "@ai-sdk/perplexity": "^2.0.13", + "@ai-sdk/amazon-bedrock": "^3.0.53", + "@ai-sdk/google-vertex": "^3.0.61", + "@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch", + "@ai-sdk/mistral": "^2.0.23", + "@ai-sdk/perplexity": "^2.0.17", "@ant-design/v5-patch-for-react-19": "^1.0.3", "@anthropic-ai/sdk": "^0.41.0", "@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch", @@ -231,7 +231,7 @@ "@viz-js/lang-dot": "^1.0.5", "@viz-js/viz": "^3.14.0", "@xyflow/react": "^12.4.4", - "ai": "^5.0.76", + "ai": "^5.0.90", "antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch", "archiver": "^7.0.1", "async-mutex": "^0.5.0", @@ -405,7 +405,10 @@ "openai@npm:5.12.2": "npm:@cherrystudio/openai@6.5.0", "@langchain/openai@npm:>=0.1.0 <0.6.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch", "@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch", - "@langchain/openai@npm:>=0.2.0 <0.7.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch" + "@langchain/openai@npm:>=0.2.0 <0.7.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch", + "@ai-sdk/google@npm:2.0.30": "patch:@ai-sdk/google@npm%3A2.0.30#~/.yarn/patches/@ai-sdk-google-npm-2.0.30-3b31632362.patch", + "@ai-sdk/openai@npm:2.0.64": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch", + "@ai-sdk/openai@npm:^2.0.42": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch" }, "packageManager": "yarn@4.9.1", "lint-staged": { diff --git a/packages/aiCore/package.json b/packages/aiCore/package.json index 8310b4164c..c8b12c4895 100644 --- a/packages/aiCore/package.json +++ b/packages/aiCore/package.json @@ -36,14 +36,14 @@ "ai": "^5.0.26" }, "dependencies": { - "@ai-sdk/anthropic": "^2.0.32", - "@ai-sdk/azure": "^2.0.53", - "@ai-sdk/deepseek": "^1.0.23", - "@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch", - "@ai-sdk/openai-compatible": "^1.0.22", + "@ai-sdk/anthropic": "^2.0.43", + "@ai-sdk/azure": "^2.0.66", + "@ai-sdk/deepseek": "^1.0.27", + "@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch", + "@ai-sdk/openai-compatible": "^1.0.26", "@ai-sdk/provider": "^2.0.0", - "@ai-sdk/provider-utils": "^3.0.12", - "@ai-sdk/xai": "^2.0.26", + "@ai-sdk/provider-utils": "^3.0.16", + "@ai-sdk/xai": "^2.0.31", "zod": "^4.1.5" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 6da1d01ecf..e584286e7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -74,159 +74,159 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/amazon-bedrock@npm:^3.0.42": - version: 3.0.42 - resolution: "@ai-sdk/amazon-bedrock@npm:3.0.42" +"@ai-sdk/amazon-bedrock@npm:^3.0.53": + version: 3.0.53 + resolution: "@ai-sdk/amazon-bedrock@npm:3.0.53" dependencies: - "@ai-sdk/anthropic": "npm:2.0.32" + "@ai-sdk/anthropic": "npm:2.0.43" "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" "@smithy/eventstream-codec": "npm:^4.0.1" "@smithy/util-utf8": "npm:^4.0.0" aws4fetch: "npm:^1.0.20" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/659de3d62f1907489bb14cd7fe049274c0a5f754222eda41b500d66573422ddaad3380cf8fc6eaae8a39ab25445e81aca7664ca2068b4a93c49bcb605889b2ba + checksum: 10c0/4ad693af6796fac6cb6f5aacf512708478a045070435f10781072aeb02f4f97083b86ae4fff135329703af7ceb158349c6b62e6f05b394817dca5d90ff31d528 languageName: node linkType: hard -"@ai-sdk/anthropic@npm:2.0.32, @ai-sdk/anthropic@npm:^2.0.32": - version: 2.0.32 - resolution: "@ai-sdk/anthropic@npm:2.0.32" +"@ai-sdk/anthropic@npm:2.0.43, @ai-sdk/anthropic@npm:^2.0.43": + version: 2.0.43 + resolution: "@ai-sdk/anthropic@npm:2.0.43" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/f83ec81fe150dacd9207b67a173f7e150b44a0b2b57e6361c061e35b663bbb95240ea18066bd2bce73df722b85772ca174c4f1546b29eb6e6d1fcf4f349e756b + checksum: 10c0/a83029edc541a9cecda9e15b8732de111ed739a586b55d6a0e7d2b8ef40660289986d7a144252736bfc9ee067ee19b11d5c5830278513aa32c6fa24666bd0e78 languageName: node linkType: hard -"@ai-sdk/azure@npm:^2.0.53": - version: 2.0.53 - resolution: "@ai-sdk/azure@npm:2.0.53" +"@ai-sdk/azure@npm:^2.0.66": + version: 2.0.66 + resolution: "@ai-sdk/azure@npm:2.0.66" dependencies: - "@ai-sdk/openai": "npm:2.0.52" + "@ai-sdk/openai": "npm:2.0.64" "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/39346f50434c3568b40bb57aa64010261ae767d9aa49b4477999ca78431326275b111879b9c5431ce35ca4ca376c47455618c8bf528c54402b0dad1b03e10487 + checksum: 10c0/261c00a3998611857f0e7c95962849d8e4468262477b07dafd29b0d447ae4088a8b3fc351ca84086e4cf008e2ee9d6efeb379964a091539d6af16a25a8726cd4 languageName: node linkType: hard -"@ai-sdk/deepseek@npm:^1.0.23": - version: 1.0.23 - resolution: "@ai-sdk/deepseek@npm:1.0.23" +"@ai-sdk/deepseek@npm:^1.0.27": + version: 1.0.27 + resolution: "@ai-sdk/deepseek@npm:1.0.27" dependencies: - "@ai-sdk/openai-compatible": "npm:1.0.22" + "@ai-sdk/openai-compatible": "npm:1.0.26" "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/39736e9787420ce86d0f2ce6935ba51f2b721acfb4c0d2b77064a8b939cf22b0767a83b82a5c99efff1311080532a3aaa2f34804d7981133f671a050521ed197 + checksum: 10c0/8d05887ef5e9c08d63a54f0b51c1ff6c9242daab339aaae919d2dc48a11d1065a84b0dc3e5f1e9b48ef20122ff330a5eee826f0632402d1ff87fcec9a2edd516 languageName: node linkType: hard -"@ai-sdk/gateway@npm:2.0.0": - version: 2.0.0 - resolution: "@ai-sdk/gateway@npm:2.0.0" +"@ai-sdk/gateway@npm:2.0.7": + version: 2.0.7 + resolution: "@ai-sdk/gateway@npm:2.0.7" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" "@vercel/oidc": "npm:3.0.3" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/720cfb827bc64f3eb6bb86d17e7e7947c54bdc7d74db7f6e9e162be0973a45368c05829e4b257704182ca9c4886e7f3c74f6b64841e88359930f48f288aa958a + checksum: 10c0/b57db87ccfbda6d28c8ac6e24df5e57a45f18826bff3ca5d1b65b00d863dd779d2b0d80496eee8eea8cbf6db232c31bd00494cd0d25e745cb402aa98b0b4d50d languageName: node linkType: hard -"@ai-sdk/google-vertex@npm:^3.0.48": - version: 3.0.48 - resolution: "@ai-sdk/google-vertex@npm:3.0.48" +"@ai-sdk/google-vertex@npm:^3.0.61": + version: 3.0.61 + resolution: "@ai-sdk/google-vertex@npm:3.0.61" dependencies: - "@ai-sdk/anthropic": "npm:2.0.32" - "@ai-sdk/google": "npm:2.0.23" + "@ai-sdk/anthropic": "npm:2.0.43" + "@ai-sdk/google": "npm:2.0.30" "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" google-auth-library: "npm:^9.15.0" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/79f0ccb78c4930ea57a41e81f31a1935531d8f02b738d0aae13fa865272f4dac6b1c31b2e1c8b8ca65671a96b90cd4f14fabaa9d60ab0252c6c0e6a1828e7f09 + checksum: 10c0/e4c074f7bb6227b84d17e1f8fda03e90da34b1bdca441a744cd4226836f44f7b97df7e908e482d974fe6e768ae13d7b038d9324dbb5e53423f1e9489ea2f7785 languageName: node linkType: hard -"@ai-sdk/google@npm:2.0.23": - version: 2.0.23 - resolution: "@ai-sdk/google@npm:2.0.23" +"@ai-sdk/google@npm:2.0.30": + version: 2.0.30 + resolution: "@ai-sdk/google@npm:2.0.30" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/402b78f392196c3e23c75cc35fc1d701f9521b57aace2fb1bbae6a0d57bbb3894a778b0485305bd6674998403e44c3883dca2416f2d48377722351debead9f11 + checksum: 10c0/6f4fc6b92cc03437fcb46f5e32c32195c0fcc9b523c3818d57c4f1526c65af61173f13c2222f725361656377452b9063d5257edf08326f7fb6ad709f7796ab4e languageName: node linkType: hard -"@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.23#~/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch": - version: 2.0.23 - resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.23#~/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch::version=2.0.23&hash=df67ed" +"@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.30#~/.yarn/patches/@ai-sdk-google-npm-2.0.30-3b31632362.patch": + version: 2.0.30 + resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.30#~/.yarn/patches/@ai-sdk-google-npm-2.0.30-3b31632362.patch::version=2.0.30&hash=be6198" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/e7fda169f04190b3ef37937e61219dcf8dade735cf76a9af8f1a1def83a43846659a361835814f0b68a2c392bc840a457a693cb69fed42af375771dd210ebdbe + checksum: 10c0/60c4eda30a4f8460594f1f825c3a1a9ea564035f99c4da954d641b406394d8cfa411f3fcdd370a4e60006a84e360e084bc7f925f4517c87ffcdd149165d8989e languageName: node linkType: hard -"@ai-sdk/huggingface@npm:0.0.4": - version: 0.0.4 - resolution: "@ai-sdk/huggingface@npm:0.0.4" +"@ai-sdk/huggingface@npm:0.0.8": + version: 0.0.8 + resolution: "@ai-sdk/huggingface@npm:0.0.8" dependencies: - "@ai-sdk/openai-compatible": "npm:1.0.22" + "@ai-sdk/openai-compatible": "npm:1.0.26" "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/756b8f820b89bf9550c9281dfe2a1a813477dec82be5557e236e8b5eaaf0204b65a65925ad486b7576c687f33c709f6d99fd4fc87a46b1add210435b08834986 + checksum: 10c0/12d5064bb3dbb591941c76a33ffa76e75df0c1fb547255c20acbdc9cfd00a434c8210d92df382717c188022aa705ad36c3e31ddcb6b1387f154f956c9ea61e66 languageName: node linkType: hard -"@ai-sdk/huggingface@patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch": - version: 0.0.4 - resolution: "@ai-sdk/huggingface@patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch::version=0.0.4&hash=ceb48e" +"@ai-sdk/huggingface@patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch": + version: 0.0.8 + resolution: "@ai-sdk/huggingface@patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch::version=0.0.8&hash=ceb48e" dependencies: - "@ai-sdk/openai-compatible": "npm:1.0.22" + "@ai-sdk/openai-compatible": "npm:1.0.26" "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" peerDependencies: zod: ^3.25.76 || ^4 - checksum: 10c0/4726a10de7a6fd554b58d62f79cd6514c2cc5166052e035ba1517e224a310ddb355a5d2922ee8507fb8d928d6d5b2b102d3d221af5a44b181e436e6b64382087 + checksum: 10c0/30760547543f7e33fe088a4a5b5be7ce0cd37f446a5ddb13c99c5a2725c6c020fc76d6cf6bc1c5cdd8f765366ecb3022605096dc45cd50acf602ef46a89c1eb7 languageName: node linkType: hard -"@ai-sdk/mistral@npm:^2.0.19": - version: 2.0.19 - resolution: "@ai-sdk/mistral@npm:2.0.19" +"@ai-sdk/mistral@npm:^2.0.23": + version: 2.0.23 + resolution: "@ai-sdk/mistral@npm:2.0.23" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/522d1e4631e9318f82f5993030c8fa2a28341e749bc920d32966d91d5cd5a4d1638980b7e0a62601aaaaf7a25e04fefed18b07ce50034c5c5d903ac5bebb65ec + checksum: 10c0/7b7597740d1e48ee4905f48276c46591fbdd6d7042f001ec1a34256c8b054f480f547c6aa9175987e6fdfc4c068925176d0123fa3b4b5af985d55b7890cfe80a languageName: node linkType: hard -"@ai-sdk/openai-compatible@npm:1.0.22, @ai-sdk/openai-compatible@npm:^1.0.22": - version: 1.0.22 - resolution: "@ai-sdk/openai-compatible@npm:1.0.22" +"@ai-sdk/openai-compatible@npm:1.0.26, @ai-sdk/openai-compatible@npm:^1.0.26": + version: 1.0.26 + resolution: "@ai-sdk/openai-compatible@npm:1.0.26" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/31eb07b63eaf07384391e81d824e16589af540f3af2fde1cb24f2a6d04dd07ddb843c9301dbceca78fa5ae5002cb235fc376c41532ab167d1564491526e6011b + checksum: 10c0/b419641f1e97c2db688f2371cdc4efb4c16652fde74fff92afaa614eea5aabee40d7f2e4e082f00d3805f12390084c7b47986de570e836beb1466c2dd48d31e9 languageName: node linkType: hard @@ -242,51 +242,39 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/openai@npm:2.0.52": - version: 2.0.52 - resolution: "@ai-sdk/openai@npm:2.0.52" +"@ai-sdk/openai@npm:2.0.64": + version: 2.0.64 + resolution: "@ai-sdk/openai@npm:2.0.64" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/253125303235dc677e272eaffbcd5c788373e12f897e42da7cce827bcc952f31e4bb11b72ba06931f37d49a2588f6cba8526127d539025bbd58d78d7bcfc691d + checksum: 10c0/fde91951ca5f2612458d618fd2b8a6e29a8cae61f1bda45816258c697af5ec6f047dbd3acc1fcc921db6e39dfa3158799f0e66f737bcd40f5f0cdd10be74d2a7 languageName: node linkType: hard -"@ai-sdk/openai@npm:^2.0.42": - version: 2.0.53 - resolution: "@ai-sdk/openai@npm:2.0.53" +"@ai-sdk/openai@patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch": + version: 2.0.64 + resolution: "@ai-sdk/openai@patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch::version=2.0.64&hash=e78090" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/acb014c7e4d99be0502fe2190c3b91c76ee86ade25e80dad939ffd113a5f013f29a81f06e13fa0e6a76b49fcb8cc524aab180fc1a622ceb8d3dac58fd655de1c + checksum: 10c0/e4a0967cbdb25309144c6263e6d691fa67898953207e050c23ba99df23ce76ab025fed3a79d541d54b99b4a049a945db2a4a3fbae5ab3a52207f024f5b4e6f4a languageName: node linkType: hard -"@ai-sdk/openai@patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch": - version: 2.0.52 - resolution: "@ai-sdk/openai@patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch::version=2.0.52&hash=c7ceb9" +"@ai-sdk/perplexity@npm:^2.0.17": + version: 2.0.17 + resolution: "@ai-sdk/perplexity@npm:2.0.17" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/a3ac267a645ffd50952c312318d0ea6190e1ca961f910f9e3067211df731ac4ba0eb89face21b5cc195770b643326b295a6fece91f07b60db8aef32f45d4664e - languageName: node - linkType: hard - -"@ai-sdk/perplexity@npm:^2.0.13": - version: 2.0.13 - resolution: "@ai-sdk/perplexity@npm:2.0.13" - dependencies: - "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/80434eebec088d5f373901f1beb77ca4ba564df5f04dec43c69a7996ea0d88344a3d86ca5e5ef2dc4f5c45f45fc478dabf3a0e44a3faea86a0190c087491a661 + checksum: 10c0/7c900a507bc7a60efb120ee4d251cb98314a6ea0f2d876552caf7b8c18e44ff38a8e205e94e6fa823629ac30c4e191c2441b107556c2b50bc4e90f80e6094bb1 languageName: node linkType: hard @@ -303,16 +291,16 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/provider-utils@npm:3.0.12, @ai-sdk/provider-utils@npm:^3.0.12": - version: 3.0.12 - resolution: "@ai-sdk/provider-utils@npm:3.0.12" +"@ai-sdk/provider-utils@npm:3.0.16, @ai-sdk/provider-utils@npm:^3.0.16": + version: 3.0.16 + resolution: "@ai-sdk/provider-utils@npm:3.0.16" dependencies: "@ai-sdk/provider": "npm:2.0.0" "@standard-schema/spec": "npm:^1.0.0" - eventsource-parser: "npm:^3.0.5" + eventsource-parser: "npm:^3.0.6" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/83886bf188cad0cc655b680b710a10413989eaba9ec59dd24a58b985c02a8a1d50ad0f96dd5259385c07592ec3c37a7769fdf4a1ef569a73c9edbdb2cd585915 + checksum: 10c0/0922af1864b31aed4704174683d356c482199bf691c3d1a3e27cdedd574eec2249ea386b1081023d301a87e38dea09ec259ee45c5889316f7eed0de0a6064a49 languageName: node linkType: hard @@ -334,16 +322,16 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/xai@npm:^2.0.26": - version: 2.0.26 - resolution: "@ai-sdk/xai@npm:2.0.26" +"@ai-sdk/xai@npm:^2.0.31": + version: 2.0.31 + resolution: "@ai-sdk/xai@npm:2.0.31" dependencies: - "@ai-sdk/openai-compatible": "npm:1.0.22" + "@ai-sdk/openai-compatible": "npm:1.0.26" "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/72fef55a96d9c3820de02beb9b63e53902649c5db906a892b7818a984b6e8afe161daa225b8d527b74f783e2c4eecd474af6e96efbb95761aca2c508e0c7c2d9 + checksum: 10c0/33a0336f032a12b8406cc1aa1541fdf1a7b9924555456b77844e47a5ddf11b726fdbcec1a240cb66a9c7597a1a05503cf03204866730c39f99e7d4442b781ec0 languageName: node linkType: hard @@ -1819,14 +1807,14 @@ __metadata: version: 0.0.0-use.local resolution: "@cherrystudio/ai-core@workspace:packages/aiCore" dependencies: - "@ai-sdk/anthropic": "npm:^2.0.32" - "@ai-sdk/azure": "npm:^2.0.53" - "@ai-sdk/deepseek": "npm:^1.0.23" - "@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch" - "@ai-sdk/openai-compatible": "npm:^1.0.22" + "@ai-sdk/anthropic": "npm:^2.0.43" + "@ai-sdk/azure": "npm:^2.0.66" + "@ai-sdk/deepseek": "npm:^1.0.27" + "@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch" + "@ai-sdk/openai-compatible": "npm:^1.0.26" "@ai-sdk/provider": "npm:^2.0.0" - "@ai-sdk/provider-utils": "npm:^3.0.12" - "@ai-sdk/xai": "npm:^2.0.26" + "@ai-sdk/provider-utils": "npm:^3.0.16" + "@ai-sdk/xai": "npm:^2.0.31" tsdown: "npm:^0.12.9" typescript: "npm:^5.0.0" vitest: "npm:^3.2.4" @@ -9846,11 +9834,11 @@ __metadata: "@agentic/exa": "npm:^7.3.3" "@agentic/searxng": "npm:^7.3.3" "@agentic/tavily": "npm:^7.3.3" - "@ai-sdk/amazon-bedrock": "npm:^3.0.42" - "@ai-sdk/google-vertex": "npm:^3.0.48" - "@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch" - "@ai-sdk/mistral": "npm:^2.0.19" - "@ai-sdk/perplexity": "npm:^2.0.13" + "@ai-sdk/amazon-bedrock": "npm:^3.0.53" + "@ai-sdk/google-vertex": "npm:^3.0.61" + "@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch" + "@ai-sdk/mistral": "npm:^2.0.23" + "@ai-sdk/perplexity": "npm:^2.0.17" "@ant-design/v5-patch-for-react-19": "npm:^1.0.3" "@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.25#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.25-08bbabb5d3.patch" "@anthropic-ai/sdk": "npm:^0.41.0" @@ -9977,7 +9965,7 @@ __metadata: "@viz-js/lang-dot": "npm:^1.0.5" "@viz-js/viz": "npm:^3.14.0" "@xyflow/react": "npm:^12.4.4" - ai: "npm:^5.0.76" + ai: "npm:^5.0.90" antd: "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch" archiver: "npm:^7.0.1" async-mutex: "npm:^0.5.0" @@ -10250,17 +10238,17 @@ __metadata: languageName: node linkType: hard -"ai@npm:^5.0.76": - version: 5.0.76 - resolution: "ai@npm:5.0.76" +"ai@npm:^5.0.90": + version: 5.0.90 + resolution: "ai@npm:5.0.90" dependencies: - "@ai-sdk/gateway": "npm:2.0.0" + "@ai-sdk/gateway": "npm:2.0.7" "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.12" + "@ai-sdk/provider-utils": "npm:3.0.16" "@opentelemetry/api": "npm:1.9.0" peerDependencies: zod: ^3.25.76 || ^4.1.8 - checksum: 10c0/167a191354b72106b1af6cfc8b53975637ca43919b8f48db81c0cf542ef0172f55958ed9331adcd08d017a608a98cb0b4a253c62732038322c78091e32595771 + checksum: 10c0/feee8908803743cee49216a37bcbc6f33e2183423d623863e8a0c5ce065dcb18d17c5c86b8f587bf391818bb47a882287f14650a77a857accb5cb7a0ecb2653c languageName: node linkType: hard @@ -14506,6 +14494,13 @@ __metadata: languageName: node linkType: hard +"eventsource-parser@npm:^3.0.6": + version: 3.0.6 + resolution: "eventsource-parser@npm:3.0.6" + checksum: 10c0/70b8ccec7dac767ef2eca43f355e0979e70415701691382a042a2df8d6a68da6c2fca35363669821f3da876d29c02abe9b232964637c1b6635c940df05ada78a + languageName: node + linkType: hard + "eventsource@npm:^3.0.2": version: 3.0.6 resolution: "eventsource@npm:3.0.6" From 31f8fff6e2588edc29327a8b9be53952aeccd636 Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Tue, 11 Nov 2025 19:19:30 +0800 Subject: [PATCH 025/173] chore: update claude code plugins (#11237) * chore: update claude code plugins * update version --------- Co-authored-by: Payne Fu --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e433434791..83c86baa7a 100644 --- a/package.json +++ b/package.json @@ -241,7 +241,7 @@ "check-disk-space": "3.4.0", "cheerio": "^1.1.2", "chokidar": "^4.0.3", - "claude-code-plugins": "1.0.1", + "claude-code-plugins": "1.0.3", "cli-progress": "^3.12.0", "clsx": "^2.1.1", "code-inspector-plugin": "^0.20.14", diff --git a/yarn.lock b/yarn.lock index e584286e7d..4dfbee7864 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9975,7 +9975,7 @@ __metadata: check-disk-space: "npm:3.4.0" cheerio: "npm:^1.1.2" chokidar: "npm:^4.0.3" - claude-code-plugins: "npm:1.0.1" + claude-code-plugins: "npm:1.0.3" cli-progress: "npm:^3.12.0" clsx: "npm:^2.1.1" code-inspector-plugin: "npm:^0.20.14" @@ -11616,10 +11616,10 @@ __metadata: languageName: node linkType: hard -"claude-code-plugins@npm:1.0.1": - version: 1.0.1 - resolution: "claude-code-plugins@npm:1.0.1" - checksum: 10c0/13fb614d1b65ea001f774183b8e9ce3deaab5402f2d99fc92f0786de5931db33b30cf975f723186bbfcf694f675c9a9ba182e531e92d25a4350844279e0bd6d5 +"claude-code-plugins@npm:1.0.3": + version: 1.0.3 + resolution: "claude-code-plugins@npm:1.0.3" + checksum: 10c0/e91f47df5c5e5cfb368568f5786fdd180caf1e4f5a9006670e7820ad9abcf57d5e6e692bedc8dc16b6c799630351f1b6485b46e1e68f7d7ff1a50b28da25ce50 languageName: node linkType: hard From 803f4b5a64b74e758e9f232627d2697da084f40e Mon Sep 17 00:00:00 2001 From: Phantom Date: Wed, 12 Nov 2025 10:05:21 +0800 Subject: [PATCH 026/173] fix(migrate): use provider apiHost for new-api (#11244) fix(migrate): use provider apiHost for new-api case instead of hardcoded value --- src/renderer/src/store/migrate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index d15fe05cb0..7233d2c951 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2765,7 +2765,7 @@ const migrateConfig = { provider.anthropicApiHost = 'https://aihubmix.com' break case 'new-api': - provider.anthropicApiHost = 'http://localhost:3000' + provider.anthropicApiHost = provider.apiHost break case 'grok': provider.anthropicApiHost = 'https://api.x.ai' From 2552d97ea78869aa7db6b1e2c6412e61ec53318f Mon Sep 17 00:00:00 2001 From: "Xiang, Haihao" Date: Wed, 12 Nov 2025 13:30:23 +0800 Subject: [PATCH 027/173] fix: ensure the user can select any image in NewApiPage (#11238) NewApiPage always show the first image in filteredPaintings. As a result, the user is unable to select other images. This issue was introduced in commit 0502ff4. --- src/renderer/src/pages/paintings/NewApiPage.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/pages/paintings/NewApiPage.tsx b/src/renderer/src/pages/paintings/NewApiPage.tsx index a038a655f6..c1d8f160f6 100644 --- a/src/renderer/src/pages/paintings/NewApiPage.tsx +++ b/src/renderer/src/pages/paintings/NewApiPage.tsx @@ -472,9 +472,15 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => { addPainting(mode, newPainting) setPainting(newPainting) } else { - setPainting(filteredPaintings[0]) + // 如果当前 painting 存在于 filteredPaintings 中,则优先显示当前 painting + const found = filteredPaintings.find((p) => p.id === painting.id) + if (found) { + setPainting(found) + } else { + setPainting(filteredPaintings[0]) + } } - }, [filteredPaintings, mode, addPainting, getNewPainting]) + }, [filteredPaintings, mode, addPainting, getNewPainting, painting.id]) useEffect(() => { const timer = spaceClickTimer.current From 649f9420a4fa4e60276fd6d64335f5aeac9fb422 Mon Sep 17 00:00:00 2001 From: MyPrototypeWhat Date: Wed, 12 Nov 2025 18:16:27 +0800 Subject: [PATCH 028/173] feat: add @cherrystudio/ai-sdk-provider package and integrate (#10715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add @cherrystudio/ai-sdk-provider package and integrate with CherryIN - Introduced the @cherrystudio/ai-sdk-provider package, providing a CherryIN routing solution for AI SDKs. - Updated configuration files to include the new provider. - Enhanced provider initialization to support CherryIN as a new AI provider. - Added README and documentation for usage instructions. * chore: remove deprecated @ai-sdk/google dependency and clean up package files - Removed the @ai-sdk/google dependency from package.json and yarn.lock as it is no longer needed. - Simplified the createGeminiModel function in index.ts for better readability and maintainability. * feat: update CherryIN provider integration and dependencies - Updated @ai-sdk/anthropic and @ai-sdk/google dependencies to their latest versions in package.json and yarn.lock. - Introduced a new CherryInProvider implementation in cherryin-provider.ts, enhancing support for CherryIN API. - Refactored provider initialization to include CherryIN as a supported provider in schemas.ts and options.ts. - Updated web search plugin to utilize the new CherryIN provider capabilities. - Cleaned up and organized imports across various files for better maintainability. * chore: clean up tsconfig and remove unnecessary nullish coalescing in CherryIn provider - Simplified tsconfig.json by consolidating exclude and include arrays. - Removed nullish coalescing in cherryin-provider.ts for cleaner header handling in model initialization. * fix: remove console.log from webSearchPlugin to clean up code - Eliminated unnecessary console.log statement in the webSearchPlugin to enhance code clarity and maintainability. * fix(i18n): Auto update translations for PR #10715 * chore: update yarn.lock with new package versions and dependencies - Added new versions for @ai-sdk/anthropic, @ai-sdk/google, @ai-sdk/provider-utils, and eventsource-parser. - Updated dependencies and peerDependencies for the newly added packages. * feat: enhance CherryIn provider with chat model support and custom fetch logic - Introduced CherryInOpenAIChatLanguageModel to handle chat-specific configurations. - Updated createChatModel to support CherryIn chat models. - Modified webSearchPlugin to accommodate both 'cherryin' and 'cherryin-chat' provider IDs. - Added 'cherryin-chat' provider ID to schemas and provider configurations. - Adjusted provider factory to correctly set provider ID for chat mode. - Enhanced web search utility to handle CherryIn chat models. * 🐛 fix: resolve cherryin provider lint errors and web search config - Add fetch global variable declaration for ai-sdk-provider in oxlintrc - Fix endpoint_type mapping fallback logic in cherryin provider - Add error handling comment for better code readability * chore(dependencies): update AI SDK packages and patches - Added new versions for @ai-sdk/anthropic, @ai-sdk/google, and @ai-sdk/provider-utils in yarn.lock. - Updated @ai-sdk/openai dependency to use a patch version in package.json. - Included @cherrystudio/ai-sdk-provider as a new dependency in the workspace. * chore(dependencies): update peer dependencies and installation instructions - Removed specific versions of @ai-sdk/anthropic and @ai-sdk/google from package.json and yarn.lock. - Updated peer dependencies in package.json to include @ai-sdk/anthropic, @ai-sdk/google, and @ai-sdk/openai. - Revised installation instructions in README.md to reflect the new dependencies. --------- Co-authored-by: GitHub Action --- .oxlintrc.json | 6 + electron.vite.config.ts | 3 +- packages/ai-sdk-provider/README.md | 39 +++ packages/ai-sdk-provider/package.json | 64 ++++ .../ai-sdk-provider/src/cherryin-provider.ts | 319 ++++++++++++++++++ packages/ai-sdk-provider/src/index.ts | 1 + packages/ai-sdk-provider/tsconfig.json | 19 ++ packages/ai-sdk-provider/tsdown.config.ts | 12 + packages/aiCore/package.json | 1 + .../built-in/webSearchPlugin/helper.ts | 60 +++- .../plugins/built-in/webSearchPlugin/index.ts | 59 +--- packages/aiCore/src/core/providers/schemas.ts | 23 ++ src/renderer/src/aiCore/provider/factory.ts | 2 + .../src/aiCore/provider/providerConfig.ts | 2 +- src/renderer/src/aiCore/utils/options.ts | 31 ++ src/renderer/src/aiCore/utils/websearch.ts | 5 + tsconfig.web.json | 6 +- yarn.lock | 53 ++- 18 files changed, 644 insertions(+), 61 deletions(-) create mode 100644 packages/ai-sdk-provider/README.md create mode 100644 packages/ai-sdk-provider/package.json create mode 100644 packages/ai-sdk-provider/src/cherryin-provider.ts create mode 100644 packages/ai-sdk-provider/src/index.ts create mode 100644 packages/ai-sdk-provider/tsconfig.json create mode 100644 packages/ai-sdk-provider/tsdown.config.ts diff --git a/.oxlintrc.json b/.oxlintrc.json index 329d08c043..7d18f83c7c 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -51,6 +51,12 @@ "node": true }, "files": ["src/preload/**"] + }, + { + "files": ["packages/ai-sdk-provider/**"], + "globals": { + "fetch": "readonly" + } } ], "plugins": ["unicorn", "typescript", "oxc", "import"], diff --git a/electron.vite.config.ts b/electron.vite.config.ts index b4914539c7..172d48ca9a 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -95,7 +95,8 @@ export default defineConfig({ '@cherrystudio/ai-core/provider': resolve('packages/aiCore/src/core/providers'), '@cherrystudio/ai-core/built-in/plugins': resolve('packages/aiCore/src/core/plugins/built-in'), '@cherrystudio/ai-core': resolve('packages/aiCore/src'), - '@cherrystudio/extension-table-plus': resolve('packages/extension-table-plus/src') + '@cherrystudio/extension-table-plus': resolve('packages/extension-table-plus/src'), + '@cherrystudio/ai-sdk-provider': resolve('packages/ai-sdk-provider/src') } }, optimizeDeps: { diff --git a/packages/ai-sdk-provider/README.md b/packages/ai-sdk-provider/README.md new file mode 100644 index 0000000000..ecd9df2923 --- /dev/null +++ b/packages/ai-sdk-provider/README.md @@ -0,0 +1,39 @@ +# @cherrystudio/ai-sdk-provider + +CherryIN provider bundle for the [Vercel AI SDK](https://ai-sdk.dev/). +It exposes the CherryIN OpenAI-compatible entrypoints and dynamically routes Anthropic and Gemini model ids to their CherryIN upstream equivalents. + +## Installation + +```bash +npm install ai @cherrystudio/ai-sdk-provider @ai-sdk/anthropic @ai-sdk/google @ai-sdk/openai +# or +yarn add ai @cherrystudio/ai-sdk-provider @ai-sdk/anthropic @ai-sdk/google @ai-sdk/openai +``` + +> **Note**: This package requires peer dependencies `ai`, `@ai-sdk/anthropic`, `@ai-sdk/google`, and `@ai-sdk/openai` to be installed. + +## Usage + +```ts +import { createCherryIn, cherryIn } from '@cherrystudio/ai-sdk-provider' + +const cherryInProvider = createCherryIn({ + apiKey: process.env.CHERRYIN_API_KEY, + // optional overrides: + // baseURL: 'https://open.cherryin.net/v1', + // anthropicBaseURL: 'https://open.cherryin.net/anthropic', + // geminiBaseURL: 'https://open.cherryin.net/gemini/v1beta', +}) + +// Chat models will auto-route based on the model id prefix: +const openaiModel = cherryInProvider.chat('gpt-4o-mini') +const anthropicModel = cherryInProvider.chat('claude-3-5-sonnet-latest') +const geminiModel = cherryInProvider.chat('gemini-2.0-pro-exp') + +const { text } = await openaiModel.invoke('Hello CherryIN!') +``` + +The provider also exposes `completion`, `responses`, `embedding`, `image`, `transcription`, and `speech` helpers aligned with the upstream APIs. + +See [AI SDK docs](https://ai-sdk.dev/providers/community-providers/custom-providers) for configuring custom providers. diff --git a/packages/ai-sdk-provider/package.json b/packages/ai-sdk-provider/package.json new file mode 100644 index 0000000000..fd0aac2643 --- /dev/null +++ b/packages/ai-sdk-provider/package.json @@ -0,0 +1,64 @@ +{ + "name": "@cherrystudio/ai-sdk-provider", + "version": "0.1.0", + "description": "Cherry Studio AI SDK provider bundle with CherryIN routing.", + "keywords": [ + "ai-sdk", + "provider", + "cherryin", + "vercel-ai-sdk", + "cherry-studio" + ], + "author": "Cherry Studio", + "license": "MIT", + "homepage": "https://github.com/CherryHQ/cherry-studio", + "repository": { + "type": "git", + "url": "git+https://github.com/CherryHQ/cherry-studio.git", + "directory": "packages/ai-sdk-provider" + }, + "bugs": { + "url": "https://github.com/CherryHQ/cherry-studio/issues" + }, + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsdown", + "dev": "tsc -w", + "clean": "rm -rf dist", + "test": "vitest run", + "test:watch": "vitest" + }, + "peerDependencies": { + "@ai-sdk/anthropic": "^2.0.29", + "@ai-sdk/google": "^2.0.23", + "@ai-sdk/openai": "^2.0.64", + "ai": "^5.0.26" + }, + "dependencies": { + "@ai-sdk/provider": "^2.0.0", + "@ai-sdk/provider-utils": "^3.0.12" + }, + "devDependencies": { + "tsdown": "^0.13.3", + "typescript": "^5.8.2", + "vitest": "^3.2.4" + }, + "sideEffects": false, + "engines": { + "node": ">=18.0.0" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "default": "./dist/index.js" + } + } +} diff --git a/packages/ai-sdk-provider/src/cherryin-provider.ts b/packages/ai-sdk-provider/src/cherryin-provider.ts new file mode 100644 index 0000000000..478380a411 --- /dev/null +++ b/packages/ai-sdk-provider/src/cherryin-provider.ts @@ -0,0 +1,319 @@ +import { AnthropicMessagesLanguageModel } from '@ai-sdk/anthropic/internal' +import { GoogleGenerativeAILanguageModel } from '@ai-sdk/google/internal' +import type { OpenAIProviderSettings } from '@ai-sdk/openai' +import { + OpenAIChatLanguageModel, + OpenAICompletionLanguageModel, + OpenAIEmbeddingModel, + OpenAIImageModel, + OpenAIResponsesLanguageModel, + OpenAISpeechModel, + OpenAITranscriptionModel +} from '@ai-sdk/openai/internal' +import { + type EmbeddingModelV2, + type ImageModelV2, + type LanguageModelV2, + type ProviderV2, + type SpeechModelV2, + type TranscriptionModelV2 +} from '@ai-sdk/provider' +import type { FetchFunction } from '@ai-sdk/provider-utils' +import { loadApiKey, withoutTrailingSlash } from '@ai-sdk/provider-utils' + +export const CHERRYIN_PROVIDER_NAME = 'cherryin' as const +export const DEFAULT_CHERRYIN_BASE_URL = 'https://open.cherryin.net/v1' +export const DEFAULT_CHERRYIN_ANTHROPIC_BASE_URL = 'https://open.cherryin.net/v1' +export const DEFAULT_CHERRYIN_GEMINI_BASE_URL = 'https://open.cherryin.net/v1beta/models' + +const ANTHROPIC_PREFIX = /^anthropic\//i +const GEMINI_PREFIX = /^google\//i +// const GEMINI_EXCLUDED_SUFFIXES = ['-nothink', '-search'] + +type HeaderValue = string | undefined + +type HeadersInput = Record | (() => Record) + +export interface CherryInProviderSettings { + /** + * CherryIN API key. + * + * If omitted, the provider will read the `CHERRYIN_API_KEY` environment variable. + */ + apiKey?: string + /** + * Optional custom fetch implementation. + */ + fetch?: FetchFunction + /** + * Base URL for OpenAI-compatible CherryIN endpoints. + * + * Defaults to `https://open.cherryin.net/v1`. + */ + baseURL?: string + /** + * Base URL for Anthropic-compatible endpoints. + * + * Defaults to `https://open.cherryin.net/anthropic`. + */ + anthropicBaseURL?: string + /** + * Base URL for Gemini-compatible endpoints. + * + * Defaults to `https://open.cherryin.net/gemini/v1beta`. + */ + geminiBaseURL?: string + /** + * Optional static headers applied to every request. + */ + headers?: HeadersInput +} + +export interface CherryInProvider extends ProviderV2 { + (modelId: string, settings?: OpenAIProviderSettings): LanguageModelV2 + languageModel(modelId: string, settings?: OpenAIProviderSettings): LanguageModelV2 + chat(modelId: string, settings?: OpenAIProviderSettings): LanguageModelV2 + responses(modelId: string): LanguageModelV2 + completion(modelId: string, settings?: OpenAIProviderSettings): LanguageModelV2 + embedding(modelId: string, settings?: OpenAIProviderSettings): EmbeddingModelV2 + textEmbedding(modelId: string, settings?: OpenAIProviderSettings): EmbeddingModelV2 + textEmbeddingModel(modelId: string, settings?: OpenAIProviderSettings): EmbeddingModelV2 + image(modelId: string, settings?: OpenAIProviderSettings): ImageModelV2 + imageModel(modelId: string, settings?: OpenAIProviderSettings): ImageModelV2 + transcription(modelId: string): TranscriptionModelV2 + transcriptionModel(modelId: string): TranscriptionModelV2 + speech(modelId: string): SpeechModelV2 + speechModel(modelId: string): SpeechModelV2 +} + +const resolveApiKey = (options: CherryInProviderSettings): string => + loadApiKey({ + apiKey: options.apiKey, + environmentVariableName: 'CHERRYIN_API_KEY', + description: 'CherryIN' + }) + +const isAnthropicModel = (modelId: string) => ANTHROPIC_PREFIX.test(modelId) +const isGeminiModel = (modelId: string) => GEMINI_PREFIX.test(modelId) + +const createCustomFetch = (originalFetch?: any) => { + return async (url: string, options: any) => { + if (options?.body) { + try { + const body = JSON.parse(options.body) + if (body.tools && Array.isArray(body.tools) && body.tools.length === 0 && body.tool_choice) { + delete body.tool_choice + options.body = JSON.stringify(body) + } + } catch (error) { + // ignore error + } + } + + return originalFetch ? originalFetch(url, options) : fetch(url, options) + } +} +class CherryInOpenAIChatLanguageModel extends OpenAIChatLanguageModel { + constructor(modelId: string, settings: any) { + super(modelId, { + ...settings, + fetch: createCustomFetch(settings.fetch) + }) + } +} + +const resolveConfiguredHeaders = (headers?: HeadersInput): Record => { + if (typeof headers === 'function') { + return { ...headers() } + } + return headers ? { ...headers } : {} +} + +const toBearerToken = (authorization?: string) => (authorization ? authorization.replace(/^Bearer\s+/i, '') : undefined) + +const createJsonHeadersGetter = (options: CherryInProviderSettings): (() => Record) => { + return () => ({ + Authorization: `Bearer ${resolveApiKey(options)}`, + 'Content-Type': 'application/json', + ...resolveConfiguredHeaders(options.headers) + }) +} + +const createAuthHeadersGetter = (options: CherryInProviderSettings): (() => Record) => { + return () => ({ + Authorization: `Bearer ${resolveApiKey(options)}`, + ...resolveConfiguredHeaders(options.headers) + }) +} + +export const createCherryIn = (options: CherryInProviderSettings = {}): CherryInProvider => { + const { + baseURL = DEFAULT_CHERRYIN_BASE_URL, + anthropicBaseURL = DEFAULT_CHERRYIN_ANTHROPIC_BASE_URL, + geminiBaseURL = DEFAULT_CHERRYIN_GEMINI_BASE_URL, + fetch + } = options + + const getJsonHeaders = createJsonHeadersGetter(options) + const getAuthHeaders = createAuthHeadersGetter(options) + + const url = ({ path }: { path: string; modelId: string }) => `${withoutTrailingSlash(baseURL)}${path}` + + const createAnthropicModel = (modelId: string) => + new AnthropicMessagesLanguageModel(modelId, { + provider: `${CHERRYIN_PROVIDER_NAME}.anthropic`, + baseURL: anthropicBaseURL, + headers: () => { + const headers = getJsonHeaders() + const apiKey = toBearerToken(headers.Authorization) + return { + ...headers, + 'x-api-key': apiKey + } + }, + fetch, + supportedUrls: () => ({ + 'image/*': [/^https?:\/\/.*$/] + }) + }) + + const createGeminiModel = (modelId: string) => + new GoogleGenerativeAILanguageModel(modelId, { + provider: `${CHERRYIN_PROVIDER_NAME}.google`, + baseURL: geminiBaseURL, + headers: () => { + const headers = getJsonHeaders() + const apiKey = toBearerToken(headers.Authorization) + return { + ...headers, + 'x-goog-api-key': apiKey + } + }, + fetch, + generateId: () => `${CHERRYIN_PROVIDER_NAME}-${Date.now()}`, + supportedUrls: () => ({}) + }) + + const createOpenAIChatModel = (modelId: string, settings: OpenAIProviderSettings = {}) => + new CherryInOpenAIChatLanguageModel(modelId, { + provider: `${CHERRYIN_PROVIDER_NAME}.openai-chat`, + url, + headers: () => ({ + ...getJsonHeaders(), + ...settings.headers + }), + fetch + }) + + const createChatModel = (modelId: string, settings: OpenAIProviderSettings = {}) => { + if (isAnthropicModel(modelId)) { + return createAnthropicModel(modelId) + } + if (isGeminiModel(modelId)) { + return createGeminiModel(modelId) + } + return new OpenAIResponsesLanguageModel(modelId, { + provider: `${CHERRYIN_PROVIDER_NAME}.openai`, + url, + headers: () => ({ + ...getJsonHeaders(), + ...settings.headers + }), + fetch + }) + } + + const createCompletionModel = (modelId: string, settings: OpenAIProviderSettings = {}) => + new OpenAICompletionLanguageModel(modelId, { + provider: `${CHERRYIN_PROVIDER_NAME}.completion`, + url, + headers: () => ({ + ...getJsonHeaders(), + ...settings.headers + }), + fetch + }) + + const createEmbeddingModel = (modelId: string, settings: OpenAIProviderSettings = {}) => + new OpenAIEmbeddingModel(modelId, { + provider: `${CHERRYIN_PROVIDER_NAME}.embeddings`, + url, + headers: () => ({ + ...getJsonHeaders(), + ...settings.headers + }), + fetch + }) + + const createResponsesModel = (modelId: string) => + new OpenAIResponsesLanguageModel(modelId, { + provider: `${CHERRYIN_PROVIDER_NAME}.responses`, + url, + headers: () => ({ + ...getJsonHeaders() + }), + fetch + }) + + const createImageModel = (modelId: string, settings: OpenAIProviderSettings = {}) => + new OpenAIImageModel(modelId, { + provider: `${CHERRYIN_PROVIDER_NAME}.image`, + url, + headers: () => ({ + ...getJsonHeaders(), + ...settings.headers + }), + fetch + }) + + const createTranscriptionModel = (modelId: string) => + new OpenAITranscriptionModel(modelId, { + provider: `${CHERRYIN_PROVIDER_NAME}.transcription`, + url, + headers: () => ({ + ...getAuthHeaders() + }), + fetch + }) + + const createSpeechModel = (modelId: string) => + new OpenAISpeechModel(modelId, { + provider: `${CHERRYIN_PROVIDER_NAME}.speech`, + url, + headers: () => ({ + ...getJsonHeaders() + }), + fetch + }) + + const provider: CherryInProvider = function (modelId: string, settings?: OpenAIProviderSettings) { + if (new.target) { + throw new Error('CherryIN provider function cannot be called with the new keyword.') + } + + return createChatModel(modelId, settings) + } + + provider.languageModel = createChatModel + provider.chat = createOpenAIChatModel + + provider.responses = createResponsesModel + provider.completion = createCompletionModel + + provider.embedding = createEmbeddingModel + provider.textEmbedding = createEmbeddingModel + provider.textEmbeddingModel = createEmbeddingModel + + provider.image = createImageModel + provider.imageModel = createImageModel + + provider.transcription = createTranscriptionModel + provider.transcriptionModel = createTranscriptionModel + + provider.speech = createSpeechModel + provider.speechModel = createSpeechModel + + return provider +} + +export const cherryIn = createCherryIn() diff --git a/packages/ai-sdk-provider/src/index.ts b/packages/ai-sdk-provider/src/index.ts new file mode 100644 index 0000000000..d397dd5af5 --- /dev/null +++ b/packages/ai-sdk-provider/src/index.ts @@ -0,0 +1 @@ +export * from './cherryin-provider' diff --git a/packages/ai-sdk-provider/tsconfig.json b/packages/ai-sdk-provider/tsconfig.json new file mode 100644 index 0000000000..26ee731bb7 --- /dev/null +++ b/packages/ai-sdk-provider/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "declaration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "bundler", + "noEmitOnError": false, + "outDir": "./dist", + "resolveJsonModule": true, + "rootDir": "./src", + "skipLibCheck": true, + "strict": true, + "target": "ES2020" + }, + "exclude": ["node_modules", "dist"], + "include": ["src/**/*"] +} diff --git a/packages/ai-sdk-provider/tsdown.config.ts b/packages/ai-sdk-provider/tsdown.config.ts new file mode 100644 index 0000000000..0e07d34cac --- /dev/null +++ b/packages/ai-sdk-provider/tsdown.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsdown' + +export default defineConfig({ + entry: { + index: 'src/index.ts' + }, + outDir: 'dist', + format: ['esm', 'cjs'], + clean: true, + dts: true, + tsconfig: 'tsconfig.json' +}) diff --git a/packages/aiCore/package.json b/packages/aiCore/package.json index c8b12c4895..3973bd9af4 100644 --- a/packages/aiCore/package.json +++ b/packages/aiCore/package.json @@ -44,6 +44,7 @@ "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.16", "@ai-sdk/xai": "^2.0.31", + "@cherrystudio/ai-sdk-provider": "workspace:*", "zod": "^4.1.5" }, "devDependencies": { diff --git a/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/helper.ts b/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/helper.ts index 42bd17e09c..a50356130d 100644 --- a/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/helper.ts +++ b/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/helper.ts @@ -1,9 +1,10 @@ -import type { anthropic } from '@ai-sdk/anthropic' -import type { google } from '@ai-sdk/google' -import type { openai } from '@ai-sdk/openai' +import { anthropic } from '@ai-sdk/anthropic' +import { google } from '@ai-sdk/google' +import { openai } from '@ai-sdk/openai' import type { InferToolInput, InferToolOutput } from 'ai' import { type Tool } from 'ai' +import { createOpenRouterOptions, createXaiOptions, mergeProviderOptions } from '../../../options' import type { ProviderOptionsMap } from '../../../options/types' import type { OpenRouterSearchConfig } from './openrouter' @@ -95,3 +96,56 @@ export type WebSearchToolInputSchema = { google: InferToolInput 'openai-chat': InferToolInput } + +export const switchWebSearchTool = (providerId: string, config: WebSearchPluginConfig, params: any) => { + switch (providerId) { + case 'openai': { + if (config.openai) { + if (!params.tools) params.tools = {} + params.tools.web_search = openai.tools.webSearch(config.openai) + } + break + } + case 'openai-chat': { + if (config['openai-chat']) { + if (!params.tools) params.tools = {} + params.tools.web_search_preview = openai.tools.webSearchPreview(config['openai-chat']) + } + break + } + + case 'anthropic': { + if (config.anthropic) { + if (!params.tools) params.tools = {} + params.tools.web_search = anthropic.tools.webSearch_20250305(config.anthropic) + } + break + } + + case 'google': { + // case 'google-vertex': + if (!params.tools) params.tools = {} + params.tools.web_search = google.tools.googleSearch(config.google || {}) + break + } + + case 'xai': { + if (config.xai) { + const searchOptions = createXaiOptions({ + searchParameters: { ...config.xai, mode: 'on' } + }) + params.providerOptions = mergeProviderOptions(params.providerOptions, searchOptions) + } + break + } + + case 'openrouter': { + if (config.openrouter) { + const searchOptions = createOpenRouterOptions(config.openrouter) + params.providerOptions = mergeProviderOptions(params.providerOptions, searchOptions) + } + break + } + } + return params +} diff --git a/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/index.ts b/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/index.ts index 34eba79637..23ea952323 100644 --- a/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/index.ts +++ b/packages/aiCore/src/core/plugins/built-in/webSearchPlugin/index.ts @@ -2,15 +2,11 @@ * Web Search Plugin * 提供统一的网络搜索能力,支持多个 AI Provider */ -import { anthropic } from '@ai-sdk/anthropic' -import { google } from '@ai-sdk/google' -import { openai } from '@ai-sdk/openai' -import { createOpenRouterOptions, createXaiOptions, mergeProviderOptions } from '../../../options' import { definePlugin } from '../../' import type { AiRequestContext } from '../../types' import type { WebSearchPluginConfig } from './helper' -import { DEFAULT_WEB_SEARCH_CONFIG } from './helper' +import { DEFAULT_WEB_SEARCH_CONFIG, switchWebSearchTool } from './helper' /** * 网络搜索插件 @@ -24,56 +20,13 @@ export const webSearchPlugin = (config: WebSearchPluginConfig = DEFAULT_WEB_SEAR transformParams: async (params: any, context: AiRequestContext) => { const { providerId } = context - switch (providerId) { - case 'openai': { - if (config.openai) { - if (!params.tools) params.tools = {} - params.tools.web_search = openai.tools.webSearch(config.openai) - } - break - } - case 'openai-chat': { - if (config['openai-chat']) { - if (!params.tools) params.tools = {} - params.tools.web_search_preview = openai.tools.webSearchPreview(config['openai-chat']) - } - break - } + switchWebSearchTool(providerId, config, params) - case 'anthropic': { - if (config.anthropic) { - if (!params.tools) params.tools = {} - params.tools.web_search = anthropic.tools.webSearch_20250305(config.anthropic) - } - break - } - - case 'google': { - // case 'google-vertex': - if (!params.tools) params.tools = {} - params.tools.web_search = google.tools.googleSearch(config.google || {}) - break - } - - case 'xai': { - if (config.xai) { - const searchOptions = createXaiOptions({ - searchParameters: { ...config.xai, mode: 'on' } - }) - params.providerOptions = mergeProviderOptions(params.providerOptions, searchOptions) - } - break - } - - case 'openrouter': { - if (config.openrouter) { - const searchOptions = createOpenRouterOptions(config.openrouter) - params.providerOptions = mergeProviderOptions(params.providerOptions, searchOptions) - } - break - } + if (providerId === 'cherryin' || providerId === 'cherryin-chat') { + // cherryin.gemini + const _providerId = params.model.provider.split('.')[1] + switchWebSearchTool(_providerId, config, params) } - return params } }) diff --git a/packages/aiCore/src/core/providers/schemas.ts b/packages/aiCore/src/core/providers/schemas.ts index 7ca4f6b0c8..778b1b705a 100644 --- a/packages/aiCore/src/core/providers/schemas.ts +++ b/packages/aiCore/src/core/providers/schemas.ts @@ -12,6 +12,7 @@ import { createOpenAI, type OpenAIProviderSettings } from '@ai-sdk/openai' import { createOpenAICompatible } from '@ai-sdk/openai-compatible' import type { LanguageModelV2 } from '@ai-sdk/provider' import { createXai } from '@ai-sdk/xai' +import { type CherryInProviderSettings, createCherryIn } from '@cherrystudio/ai-sdk-provider' import { createOpenRouter } from '@openrouter/ai-sdk-provider' import type { Provider } from 'ai' import { customProvider } from 'ai' @@ -31,6 +32,8 @@ export const baseProviderIds = [ 'azure-responses', 'deepseek', 'openrouter', + 'cherryin', + 'cherryin-chat', 'huggingface' ] as const @@ -136,6 +139,26 @@ export const baseProviders = [ creator: createOpenRouter, supportsImageGeneration: true }, + { + id: 'cherryin', + name: 'CherryIN', + creator: createCherryIn, + supportsImageGeneration: true + }, + { + id: 'cherryin-chat', + name: 'CherryIN Chat', + creator: (options: CherryInProviderSettings) => { + const provider = createCherryIn(options) + return customProvider({ + fallbackProvider: { + ...provider, + languageModel: (modelId: string) => provider.chat(modelId) + } + }) + }, + supportsImageGeneration: true + }, { id: 'huggingface', name: 'HuggingFace', diff --git a/src/renderer/src/aiCore/provider/factory.ts b/src/renderer/src/aiCore/provider/factory.ts index 4cdbfb6d40..569b5628cd 100644 --- a/src/renderer/src/aiCore/provider/factory.ts +++ b/src/renderer/src/aiCore/provider/factory.ts @@ -84,6 +84,8 @@ export async function createAiSdkProvider(config) { config.providerId = `${config.providerId}-chat` } else if (config.providerId === 'azure' && config.options?.mode === 'responses') { config.providerId = `${config.providerId}-responses` + } else if (config.providerId === 'cherryin' && config.options?.mode === 'chat') { + config.providerId = 'cherryin-chat' } localProvider = await createProviderCore(config.providerId, config.options) diff --git a/src/renderer/src/aiCore/provider/providerConfig.ts b/src/renderer/src/aiCore/provider/providerConfig.ts index 7f279a3898..4eb1ffeed7 100644 --- a/src/renderer/src/aiCore/provider/providerConfig.ts +++ b/src/renderer/src/aiCore/provider/providerConfig.ts @@ -171,7 +171,7 @@ export function providerToAiSdkConfig( extraOptions.endpoint = endpoint if (actualProvider.type === 'openai-response' && !isOpenAIChatCompletionOnlyModel(model)) { extraOptions.mode = 'responses' - } else if (aiSdkProviderId === 'openai') { + } else if (aiSdkProviderId === 'openai' || (aiSdkProviderId === 'cherryin' && actualProvider.type === 'openai')) { extraOptions.mode = 'chat' } diff --git a/src/renderer/src/aiCore/utils/options.ts b/src/renderer/src/aiCore/utils/options.ts index 60d9b1e098..9e296597c2 100644 --- a/src/renderer/src/aiCore/utils/options.ts +++ b/src/renderer/src/aiCore/utils/options.ts @@ -113,6 +113,9 @@ export function buildProviderOptions( } break } + case 'cherryin': + providerSpecificOptions = buildCherryInProviderOptions(assistant, model, capabilities, actualProvider) + break default: throw new Error(`Unsupported base provider ${baseProviderId}`) } @@ -270,6 +273,34 @@ function buildXAIProviderOptions( return providerOptions } +function buildCherryInProviderOptions( + assistant: Assistant, + model: Model, + capabilities: { + enableReasoning: boolean + enableWebSearch: boolean + enableGenerateImage: boolean + }, + actualProvider: Provider +): Record { + const serviceTierSetting = getServiceTier(model, actualProvider) + + switch (actualProvider.type) { + case 'openai': + return { + ...buildOpenAIProviderOptions(assistant, model, capabilities), + serviceTier: serviceTierSetting + } + + case 'anthropic': + return buildAnthropicProviderOptions(assistant, model, capabilities) + + case 'gemini': + return buildGeminiProviderOptions(assistant, model, capabilities) + } + return {} +} + /** * Build Bedrock providerOptions */ diff --git a/src/renderer/src/aiCore/utils/websearch.ts b/src/renderer/src/aiCore/utils/websearch.ts index fde4ff534d..02619b54cf 100644 --- a/src/renderer/src/aiCore/utils/websearch.ts +++ b/src/renderer/src/aiCore/utils/websearch.ts @@ -107,6 +107,11 @@ export function buildProviderBuiltinWebSearchConfig( } } } + case 'cherryin': { + const _providerId = + { 'openai-response': 'openai', openai: 'openai-chat' }[model?.endpoint_type ?? ''] ?? model?.endpoint_type + return buildProviderBuiltinWebSearchConfig(_providerId, webSearchConfig, model) + } default: { return {} } diff --git a/tsconfig.web.json b/tsconfig.web.json index 1204192253..2d91fe0260 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -9,7 +9,8 @@ "packages/mcp-trace/**/*", "packages/aiCore/src/**/*", "src/main/integration/cherryai/index.js", - "packages/extension-table-plus/**/*" + "packages/extension-table-plus/**/*", + "packages/ai-sdk-provider/**/*" ], "compilerOptions": { "composite": true, @@ -27,7 +28,8 @@ "@cherrystudio/ai-core/built-in/plugins": ["./packages/aiCore/src/core/plugins/built-in/index.ts"], "@cherrystudio/ai-core/*": ["./packages/aiCore/src/*"], "@cherrystudio/ai-core": ["./packages/aiCore/src/index.ts"], - "@cherrystudio/extension-table-plus": ["./packages/extension-table-plus/src/index.ts"] + "@cherrystudio/extension-table-plus": ["./packages/extension-table-plus/src/index.ts"], + "@cherrystudio/ai-sdk-provider": ["./packages/ai-sdk-provider/src/index.ts"] }, "experimentalDecorators": true, "emitDecoratorMetadata": true, diff --git a/yarn.lock b/yarn.lock index 4dfbee7864..ac9f3c8305 100644 --- a/yarn.lock +++ b/yarn.lock @@ -278,7 +278,7 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/provider-utils@npm:3.0.10, @ai-sdk/provider-utils@npm:^3.0.10": +"@ai-sdk/provider-utils@npm:3.0.10": version: 3.0.10 resolution: "@ai-sdk/provider-utils@npm:3.0.10" dependencies: @@ -304,6 +304,19 @@ __metadata: languageName: node linkType: hard +"@ai-sdk/provider-utils@npm:^3.0.10, @ai-sdk/provider-utils@npm:^3.0.12": + version: 3.0.17 + resolution: "@ai-sdk/provider-utils@npm:3.0.17" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@standard-schema/spec": "npm:^1.0.0" + eventsource-parser: "npm:^3.0.6" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/1bae6dc4cacd0305b6aa152f9589bbd61c29f150155482c285a77f83d7ed416d52bc2aa7fdaba2e5764530392d9e8f799baea34a63dce6c72ecd3de364dc62d1 + languageName: node + linkType: hard + "@ai-sdk/provider@npm:2.0.0, @ai-sdk/provider@npm:^2.0.0": version: 2.0.0 resolution: "@ai-sdk/provider@npm:2.0.0" @@ -1815,6 +1828,7 @@ __metadata: "@ai-sdk/provider": "npm:^2.0.0" "@ai-sdk/provider-utils": "npm:^3.0.16" "@ai-sdk/xai": "npm:^2.0.31" + "@cherrystudio/ai-sdk-provider": "workspace:*" tsdown: "npm:^0.12.9" typescript: "npm:^5.0.0" vitest: "npm:^3.2.4" @@ -1824,6 +1838,23 @@ __metadata: languageName: unknown linkType: soft +"@cherrystudio/ai-sdk-provider@workspace:*, @cherrystudio/ai-sdk-provider@workspace:packages/ai-sdk-provider": + version: 0.0.0-use.local + resolution: "@cherrystudio/ai-sdk-provider@workspace:packages/ai-sdk-provider" + dependencies: + "@ai-sdk/provider": "npm:^2.0.0" + "@ai-sdk/provider-utils": "npm:^3.0.12" + tsdown: "npm:^0.13.3" + typescript: "npm:^5.8.2" + vitest: "npm:^3.2.4" + peerDependencies: + "@ai-sdk/anthropic": ^2.0.29 + "@ai-sdk/google": ^2.0.23 + "@ai-sdk/openai": ^2.0.64 + ai: ^5.0.26 + languageName: unknown + linkType: soft + "@cherrystudio/embedjs-interfaces@npm:0.1.30": version: 0.1.30 resolution: "@cherrystudio/embedjs-interfaces@npm:0.1.30" @@ -24851,6 +24882,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^5.8.2": + version: 5.9.3 + resolution: "typescript@npm:5.9.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/6bd7552ce39f97e711db5aa048f6f9995b53f1c52f7d8667c1abdc1700c68a76a308f579cd309ce6b53646deb4e9a1be7c813a93baaf0a28ccd536a30270e1c5 + languageName: node + linkType: hard + "typescript@patch:typescript@npm%3A^5.0.0#optional!builtin": version: 5.9.2 resolution: "typescript@patch:typescript@npm%3A5.9.2#optional!builtin::version=5.9.2&hash=5786d5" @@ -24871,6 +24912,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A^5.8.2#optional!builtin": + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/ad09fdf7a756814dce65bc60c1657b40d44451346858eea230e10f2e95a289d9183b6e32e5c11e95acc0ccc214b4f36289dcad4bf1886b0adb84d711d336a430 + languageName: node + linkType: hard + "ua-parser-js@npm:^1.0.35": version: 1.0.40 resolution: "ua-parser-js@npm:1.0.40" From a6182eaf854a61a8fac21adcfd29fd4ede140774 Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 12 Nov 2025 20:04:58 +0800 Subject: [PATCH 029/173] Refactor/inputbar (#10332) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor inputbar system with configurable scope-based architecture - **Implement scope-based configuration** for chat, agent sessions, and mini-window with feature toggles - **Add tool registry system** with dependency injection for modular inputbar tools - **Create shared state management** via InputbarToolsProvider for consistent state handling - **Migrate existing tools** to registry-based definitions with proper scope filtering The changes introduce a flexible inputbar architecture that supports different use cases through scope-based configuration while maintaining feature parity and improving code organization. * Remove unused import and refactor tool rendering - Delete obsolete '@renderer/pages/home/Inputbar/tools' import from Inputbar.tsx - Extract ToolButton component to render tools outside useMemo dependency cycle - Store tool definitions in config for deferred rendering with current context - Fix potential stale closure issues in tool rendering by rebuilding context on each render * Wrap ToolButton in React.memo and optimize quick panel menu updates - Memoize ToolButton component to prevent unnecessary re-renders when tool key remains unchanged - Replace direct menu state updates with version-based triggering to batch registry changes - Add useEffect to consolidate menu updates and reduce redundant flat operations * chore style * refactor(InputbarToolsProvider): simplify quick panel menu update logic * Improve QuickPanel behavior and input handling - Default select first item when panel symbol changes to enhance user experience - Add Tab key support for selecting template variables in input field - Refactor QuickPanel trigger logic with better symbol tracking and boundary checks - Fix typo in translation key for model selection menu item * Refactor import statements to use type-only imports - Convert inline type imports to explicit type imports in Inputbar.tsx and types.ts - Replace combined type/value imports with separate type imports in InputbarToolsProvider and tools - Remove unnecessary menu version state and effect in InputbarToolsProvider * Refactor InputbarTools context to separate state and dispatch concerns - Split single context into separate state and dispatch contexts to optimize re-renders - Introduce derived state for `couldMentionNotVisionModel` based on file types - Encapsulate Quick Panel API in stable object with memoized functions - Add internal dispatch context for Inputbar-specific state setters * Refactor Inputbar to use split context hooks and optimize QuickPanel - Replace monolithic `useInputbarTools` with separate state, dispatch, and internal dispatch hooks - Move text state from context to local component state in InputbarInner - Optimize QuickPanel trigger registration to use ref pattern, avoiding frequent re-registrations * Refactor QuickPanel API to separate concerns between tools and inputbar - Split QuickPanel API into `toolsRegistry` for tool registration and `triggers` for inputbar triggering - Remove unused QuickPanel state variables and clean up dependencies - Update tool context to use new API structure with proper type safety * Optimize the state management of QuickPanel and Inputbar, add text update functionality, and improve the tool registration logic. * chore * Add reusable React hooks and InputbarCore component for chat input - Create `useInputText`, `useKeyboardHandler`, and `useTextareaResize` hooks for text management, keyboard shortcuts, and auto-resizing - Implement `InputbarCore` component with modular toolbar sections, drag-drop support, and textarea customization - Add `useFileDragDrop` and `usePasteHandler` hooks for file uploads and paste handling with type filtering * Refactor Inputbar to use custom hooks for text and textarea management - Replace manual text state with useInputText hook for text management and empty state - Replace textarea resize logic with useTextareaResize hook for automatic height adjustment - Add comprehensive refactoring documentation with usage examples and guidelines * Refactor inputbar drag-drop and paste handling into custom hooks - Extract paste handling logic into usePasteHandler hook - Extract drag-drop file handling into useFileDragDrop hook - Remove inline drag-drop state and handlers, use hook interfaces - Clean up dependencies and callback optimizations * Refactor Inputbar component to use InputbarCore composition - Extract complex UI logic into InputbarCore component for better separation of concerns - Remove intermediate wrapper component and action ref forwarding pattern - Consolidate focus/blur handlers and simplify component structure * Refactor Inputbar to expose actions via ref for external control - Extract action handlers into ProviderActionHandlers interface and expose via ref - Split component into Inputbar wrapper and InputbarInner implementation - Update useEffect to sync inner component actions with ref for external access * feat: inputbar core * refactor: Update QuickPanel integration across various tools * refactor: migrate to antd * chore: format * fix: clean code * clean code * fix i18n * fix: i18n * relative path * model type * 🤖 Weekly Automated Update: Nov 09, 2025 (#11209) feat(bot): Weekly automated script run Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com> Co-authored-by: SuYao * format * fix * fix: format * use ripgrep * update with input * add common filters * fix build issue * format * fix error * smooth change * adjust * support listing dir * keep list files when focus and blur * support draft save * Optimize the rendering logic of session messages and input bars, and simplify conditional judgments. * Upgrade to agentId * format * 🐛 fix: force quick triggers for agent sessions * revert * fix migrate * fix: filter * fix: trigger * chore packages * feat: 添加过滤和排序功能,支持自定义函数 * fix cursor bug * fix format --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: beyondkmp Co-authored-by: kangfenmao --- ...ai-sdk-google-npm-2.0.31-b0de047210.patch} | 4 +- ...ude-agent-sdk-npm-0.1.30-b50a299674.patch} | 4 +- package.json | 9 +- packages/aiCore/package.json | 1 + packages/shared/IpcChannel.ts | 1 + .../database/drizzle/0002_wealthy_naoko.sql | 1 + .../database/drizzle/meta/0002_snapshot.json | 346 ++++++ resources/database/drizzle/meta/_journal.json | 7 + src/main/ipc.ts | 1 + src/main/services/FileStorage.ts | 366 ++++++ src/main/services/agents/BaseService.ts | 9 +- .../agents/database/schema/sessions.schema.ts | 1 + .../agents/services/SessionService.ts | 61 +- .../claudecode/__tests__/transform.test.ts | 15 +- .../agents/services/claudecode/commands.ts | 25 +- .../agents/services/claudecode/index.ts | 75 +- .../agents/services/claudecode/transform.ts | 25 +- src/preload/index.ts | 12 + .../src/aiCore/chunk/AiSdkToChunkAdapter.ts | 31 +- .../QuickPanel/defaultStrategies.ts | 104 ++ .../src/components/QuickPanel/index.ts | 1 + .../src/components/QuickPanel/provider.tsx | 55 +- .../src/components/QuickPanel/types.ts | 39 +- .../src/components/QuickPanel/view.tsx | 300 +++-- .../hooks/agents/useCreateDefaultSession.ts | 6 + src/renderer/src/hooks/useInputText.ts | 63 + src/renderer/src/hooks/useKeyboardHandler.ts | 94 ++ src/renderer/src/hooks/useTextareaResize.ts | 125 ++ src/renderer/src/i18n/locales/en-us.json | 18 + src/renderer/src/i18n/locales/zh-cn.json | 18 + src/renderer/src/i18n/locales/zh-tw.json | 18 + src/renderer/src/i18n/translate/de-de.json | 18 + src/renderer/src/i18n/translate/el-gr.json | 18 + src/renderer/src/i18n/translate/es-es.json | 18 + src/renderer/src/i18n/translate/fr-fr.json | 18 + src/renderer/src/i18n/translate/ja-jp.json | 18 + src/renderer/src/i18n/translate/pt-pt.json | 18 + src/renderer/src/i18n/translate/ru-ru.json | 18 + src/renderer/src/pages/home/Chat.tsx | 33 +- .../home/Inputbar/AgentSessionInputbar.tsx | 656 +++++----- .../src/pages/home/Inputbar/Inputbar.tsx | 1106 ++++------------- .../src/pages/home/Inputbar/InputbarTools.tsx | 839 +++++-------- .../home/Inputbar/MentionModelsButton.tsx | 318 ----- .../home/Inputbar/components/InputbarCore.tsx | 803 ++++++++++++ .../context/InputbarToolsProvider.tsx | 347 ++++++ .../home/Inputbar/hooks/useFileDragDrop.ts | 96 ++ .../home/Inputbar/hooks/usePasteHandler.ts | 62 + .../src/pages/home/Inputbar/registry.ts | 53 + .../Inputbar/tools/activityDirectoryTool.tsx | 51 + .../home/Inputbar/tools/attachmentTool.tsx | 33 + .../home/Inputbar/tools/clearTopicTool.tsx | 34 + .../components/ActivityDirectoryButton.tsx | 41 + .../ActivityDirectoryQuickPanelManager.tsx | 35 + .../components}/AttachmentButton.tsx | 44 +- .../components}/GenerateImageButton.tsx | 0 .../components}/KnowledgeBaseButton.tsx | 50 +- .../{ => tools/components}/MCPToolsButton.tsx | 80 +- .../tools/components/MentionModelsButton.tsx | 56 + .../MentionModelsQuickPanelManager.tsx | 35 + .../components}/NewContextButton.tsx | 0 .../components}/QuickPhrasesButton.tsx | 157 ++- .../tools/components/SlashCommandsButton.tsx | 47 + .../{ => tools/components}/ThinkingButton.tsx | 44 +- .../components}/UrlContextbutton.tsx | 0 .../tools/components/WebSearchButton.tsx | 41 + .../WebSearchQuickPanelManager.tsx} | 172 +-- .../components/useActivityDirectoryPanel.tsx | 457 +++++++ .../components/useMentionModelsPanel.tsx | 324 +++++ .../home/Inputbar/tools/createSessionTool.tsx | 57 + .../home/Inputbar/tools/generateImageTool.tsx | 28 + .../src/pages/home/Inputbar/tools/index.ts | 23 + .../home/Inputbar/tools/knowledgeBaseTool.tsx | 61 + .../home/Inputbar/tools/mcpToolsTool.tsx | 26 + .../home/Inputbar/tools/mentionModelsTool.tsx | 45 + .../home/Inputbar/tools/newContextTool.tsx | 17 + .../home/Inputbar/tools/newTopicTool.tsx | 38 + .../home/Inputbar/tools/quickPhrasesTool.tsx | 30 + .../home/Inputbar/tools/slashCommandsTool.tsx | 226 ++++ .../home/Inputbar/tools/thinkingTool.tsx | 17 + .../home/Inputbar/tools/toggleExpandTool.tsx | 44 + .../home/Inputbar/tools/urlContextTool.tsx | 22 + .../home/Inputbar/tools/webSearchTool.tsx | 30 + src/renderer/src/pages/home/Inputbar/types.ts | 228 ++++ .../src/pages/home/Markdown/Markdown.tsx | 9 +- .../home/Messages/Blocks/CompactBlock.tsx | 93 ++ .../src/pages/home/Messages/Blocks/index.tsx | 4 + .../src/pages/home/Messages/MessageEditor.tsx | 15 +- .../src/services/StreamProcessingService.ts | 6 + .../src/services/db/DexieMessageDataSource.ts | 28 +- .../callbacks/compactCallbacks.ts | 192 +++ .../messageStreaming/callbacks/index.ts | 20 +- .../callbacks/textCallbacks.ts | 14 +- src/renderer/src/store/assistants.ts | 1 + src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/inputTools.ts | 32 +- src/renderer/src/store/migrate.ts | 21 +- src/renderer/src/store/thunk/messageThunk.ts | 41 +- src/renderer/src/types/agent.ts | 2 +- src/renderer/src/types/chat.ts | 4 + src/renderer/src/types/newMessage.ts | 11 +- src/renderer/src/utils/messageUtils/create.ts | 23 + src/renderer/src/utils/messageUtils/is.ts | 11 + yarn.lock | 69 +- 103 files changed, 7037 insertions(+), 2428 deletions(-) rename .yarn/patches/{@ai-sdk-google-npm-2.0.30-3b31632362.patch => @ai-sdk-google-npm-2.0.31-b0de047210.patch} (81%) rename .yarn/patches/{@anthropic-ai-claude-agent-sdk-npm-0.1.25-08bbabb5d3.patch => @anthropic-ai-claude-agent-sdk-npm-0.1.30-b50a299674.patch} (92%) create mode 100644 resources/database/drizzle/0002_wealthy_naoko.sql create mode 100644 resources/database/drizzle/meta/0002_snapshot.json create mode 100644 src/renderer/src/components/QuickPanel/defaultStrategies.ts create mode 100644 src/renderer/src/hooks/useInputText.ts create mode 100644 src/renderer/src/hooks/useKeyboardHandler.ts create mode 100644 src/renderer/src/hooks/useTextareaResize.ts delete mode 100644 src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/components/InputbarCore.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/context/InputbarToolsProvider.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/hooks/useFileDragDrop.ts create mode 100644 src/renderer/src/pages/home/Inputbar/hooks/usePasteHandler.ts create mode 100644 src/renderer/src/pages/home/Inputbar/registry.ts create mode 100644 src/renderer/src/pages/home/Inputbar/tools/activityDirectoryTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/attachmentTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/clearTopicTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/components/ActivityDirectoryButton.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/components/ActivityDirectoryQuickPanelManager.tsx rename src/renderer/src/pages/home/Inputbar/{ => tools/components}/AttachmentButton.tsx (82%) rename src/renderer/src/pages/home/Inputbar/{ => tools/components}/GenerateImageButton.tsx (100%) rename src/renderer/src/pages/home/Inputbar/{ => tools/components}/KnowledgeBaseButton.tsx (72%) rename src/renderer/src/pages/home/Inputbar/{ => tools/components}/MCPToolsButton.tsx (88%) create mode 100644 src/renderer/src/pages/home/Inputbar/tools/components/MentionModelsButton.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/components/MentionModelsQuickPanelManager.tsx rename src/renderer/src/pages/home/Inputbar/{ => tools/components}/NewContextButton.tsx (100%) rename src/renderer/src/pages/home/Inputbar/{ => tools/components}/QuickPhrasesButton.tsx (58%) create mode 100644 src/renderer/src/pages/home/Inputbar/tools/components/SlashCommandsButton.tsx rename src/renderer/src/pages/home/Inputbar/{ => tools/components}/ThinkingButton.tsx (80%) rename src/renderer/src/pages/home/Inputbar/{ => tools/components}/UrlContextbutton.tsx (100%) create mode 100644 src/renderer/src/pages/home/Inputbar/tools/components/WebSearchButton.tsx rename src/renderer/src/pages/home/Inputbar/{WebSearchButton.tsx => tools/components/WebSearchQuickPanelManager.tsx} (59%) create mode 100644 src/renderer/src/pages/home/Inputbar/tools/components/useActivityDirectoryPanel.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/components/useMentionModelsPanel.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/createSessionTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/generateImageTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/index.ts create mode 100644 src/renderer/src/pages/home/Inputbar/tools/knowledgeBaseTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/mcpToolsTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/mentionModelsTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/newContextTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/newTopicTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/quickPhrasesTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/slashCommandsTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/thinkingTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/toggleExpandTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/urlContextTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/tools/webSearchTool.tsx create mode 100644 src/renderer/src/pages/home/Inputbar/types.ts create mode 100644 src/renderer/src/pages/home/Messages/Blocks/CompactBlock.tsx create mode 100644 src/renderer/src/services/messageStreaming/callbacks/compactCallbacks.ts diff --git a/.yarn/patches/@ai-sdk-google-npm-2.0.30-3b31632362.patch b/.yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch similarity index 81% rename from .yarn/patches/@ai-sdk-google-npm-2.0.30-3b31632362.patch rename to .yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch index 2b6fea2d37..75c418e591 100644 --- a/.yarn/patches/@ai-sdk-google-npm-2.0.30-3b31632362.patch +++ b/.yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch @@ -1,5 +1,5 @@ diff --git a/dist/index.js b/dist/index.js -index cac044aab0255fa72f68b36ecd2c5b12d424c379..ad6ee8ecfc5cbc3ec43ba59a44eda21e8e4d353f 100644 +index ff305b112779b718f21a636a27b1196125a332d9..cf32ff5086d4d9e56f8fe90c98724559083bafc3 100644 --- a/dist/index.js +++ b/dist/index.js @@ -471,7 +471,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) { @@ -12,7 +12,7 @@ index cac044aab0255fa72f68b36ecd2c5b12d424c379..ad6ee8ecfc5cbc3ec43ba59a44eda21e // src/google-generative-ai-options.ts diff --git a/dist/index.mjs b/dist/index.mjs -index 0793085005d7968638d355f2f1e127939d965165..1c8bf852baf025d56dc35a0691eb95967de7e5c8 100644 +index 57659290f1cec74878a385626ad75b2a4d5cd3fc..d04e5927ec3725b6ffdb80868bfa1b5a48849537 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) { diff --git a/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.25-08bbabb5d3.patch b/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.30-b50a299674.patch similarity index 92% rename from .yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.25-08bbabb5d3.patch rename to .yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.30-b50a299674.patch index 057443aa43..896b2d4cbf 100644 --- a/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.25-08bbabb5d3.patch +++ b/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.30-b50a299674.patch @@ -1,5 +1,5 @@ diff --git a/sdk.mjs b/sdk.mjs -index 10162e5b1624f8ce667768943347a6e41089ad2f..32568ae08946590e382270c88d85fba81187568e 100755 +index 8cc6aaf0b25bcdf3c579ec95cde12d419fcb2a71..3b3b8beaea5ad2bbac26a15f792058306d0b059f 100755 --- a/sdk.mjs +++ b/sdk.mjs @@ -6213,7 +6213,7 @@ function createAbortController(maxListeners = DEFAULT_MAX_LISTENERS) { @@ -11,7 +11,7 @@ index 10162e5b1624f8ce667768943347a6e41089ad2f..32568ae08946590e382270c88d85fba8 import { createInterface } from "readline"; // ../src/utils/fsOperations.ts -@@ -6487,14 +6487,11 @@ class ProcessTransport { +@@ -6505,14 +6505,11 @@ class ProcessTransport { const errorMessage = isNativeBinary(pathToClaudeCodeExecutable) ? `Claude Code native binary not found at ${pathToClaudeCodeExecutable}. Please ensure Claude Code is installed via native installer or specify a valid path with options.pathToClaudeCodeExecutable.` : `Claude Code executable not found at ${pathToClaudeCodeExecutable}. Is options.pathToClaudeCodeExecutable set?`; throw new ReferenceError(errorMessage); } diff --git a/package.json b/package.json index 83c86baa7a..a207b9d8aa 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "release:aicore": "yarn workspace @cherrystudio/ai-core version patch --immediate && yarn workspace @cherrystudio/ai-core npm publish --access public" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.25#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.25-08bbabb5d3.patch", + "@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.30#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.30-b50a299674.patch", "@libsql/client": "0.14.0", "@libsql/win32-x64-msvc": "^0.4.7", "@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch", @@ -107,7 +107,7 @@ "@agentic/searxng": "^7.3.3", "@agentic/tavily": "^7.3.3", "@ai-sdk/amazon-bedrock": "^3.0.53", - "@ai-sdk/google-vertex": "^3.0.61", + "@ai-sdk/google-vertex": "^3.0.62", "@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch", "@ai-sdk/mistral": "^2.0.23", "@ai-sdk/perplexity": "^2.0.17", @@ -394,7 +394,6 @@ "undici": "6.21.2", "vite": "npm:rolldown-vite@7.1.5", "tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch", - "@ai-sdk/google@npm:2.0.23": "patch:@ai-sdk/google@npm%3A2.0.23#~/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch", "@ai-sdk/openai@npm:^2.0.52": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch", "@img/sharp-darwin-arm64": "0.34.3", "@img/sharp-darwin-x64": "0.34.3", @@ -406,9 +405,9 @@ "@langchain/openai@npm:>=0.1.0 <0.6.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch", "@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch", "@langchain/openai@npm:>=0.2.0 <0.7.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch", - "@ai-sdk/google@npm:2.0.30": "patch:@ai-sdk/google@npm%3A2.0.30#~/.yarn/patches/@ai-sdk-google-npm-2.0.30-3b31632362.patch", "@ai-sdk/openai@npm:2.0.64": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch", - "@ai-sdk/openai@npm:^2.0.42": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch" + "@ai-sdk/openai@npm:^2.0.42": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch", + "@ai-sdk/google@npm:2.0.31": "patch:@ai-sdk/google@npm%3A2.0.31#~/.yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch" }, "packageManager": "yarn@4.9.1", "lint-staged": { diff --git a/packages/aiCore/package.json b/packages/aiCore/package.json index 3973bd9af4..bb673392a2 100644 --- a/packages/aiCore/package.json +++ b/packages/aiCore/package.json @@ -39,6 +39,7 @@ "@ai-sdk/anthropic": "^2.0.43", "@ai-sdk/azure": "^2.0.66", "@ai-sdk/deepseek": "^1.0.27", + "@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.31#~/.yarn/patches/@ai-sdk-google-npm-2.0.31-b0de047210.patch", "@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch", "@ai-sdk/openai-compatible": "^1.0.26", "@ai-sdk/provider": "^2.0.0", diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 7704bbaa13..81e9f02929 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -189,6 +189,7 @@ export enum IpcChannel { Fs_ReadText = 'fs:readText', File_OpenWithRelativePath = 'file:openWithRelativePath', File_IsTextFile = 'file:isTextFile', + File_ListDirectory = 'file:listDirectory', File_GetDirectoryStructure = 'file:getDirectoryStructure', File_CheckFileName = 'file:checkFileName', File_ValidateNotesDirectory = 'file:validateNotesDirectory', diff --git a/resources/database/drizzle/0002_wealthy_naoko.sql b/resources/database/drizzle/0002_wealthy_naoko.sql new file mode 100644 index 0000000000..c369ccf61f --- /dev/null +++ b/resources/database/drizzle/0002_wealthy_naoko.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD `slash_commands` text; \ No newline at end of file diff --git a/resources/database/drizzle/meta/0002_snapshot.json b/resources/database/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000000..ef5eefcb65 --- /dev/null +++ b/resources/database/drizzle/meta/0002_snapshot.json @@ -0,0 +1,346 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "0cf3d79e-69bf-4dba-8df4-996b9b67d2e8", + "prevId": "dabab6db-a2cd-4e96-b06e-6cb87d445a87", + "tables": { + "agents": { + "name": "agents", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accessible_paths": { + "name": "accessible_paths", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_model": { + "name": "plan_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "small_model": { + "name": "small_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcps": { + "name": "mcps", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session_messages": { + "name": "session_messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_session_id": { + "name": "agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "migrations": { + "name": "migrations", + "columns": { + "version": { + "name": "version", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "executed_at": { + "name": "executed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "agent_type": { + "name": "agent_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accessible_paths": { + "name": "accessible_paths", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plan_model": { + "name": "plan_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "small_model": { + "name": "small_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcps": { + "name": "mcps", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slash_commands": { + "name": "slash_commands", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/resources/database/drizzle/meta/_journal.json b/resources/database/drizzle/meta/_journal.json index 8648e01703..ac026637aa 100644 --- a/resources/database/drizzle/meta/_journal.json +++ b/resources/database/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1758187378775, "tag": "0001_woozy_captain_flint", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1762526423527, + "tag": "0002_wealthy_naoko", + "breakpoints": true } ] } diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 5bf5c73051..fd75d8c7ea 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -551,6 +551,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.File_BinaryImage, fileManager.binaryImage.bind(fileManager)) ipcMain.handle(IpcChannel.File_OpenWithRelativePath, fileManager.openFileWithRelativePath.bind(fileManager)) ipcMain.handle(IpcChannel.File_IsTextFile, fileManager.isTextFile.bind(fileManager)) + ipcMain.handle(IpcChannel.File_ListDirectory, fileManager.listDirectory.bind(fileManager)) ipcMain.handle(IpcChannel.File_GetDirectoryStructure, fileManager.getDirectoryStructure.bind(fileManager)) ipcMain.handle(IpcChannel.File_CheckFileName, fileManager.fileNameGuard.bind(fileManager)) ipcMain.handle(IpcChannel.File_ValidateNotesDirectory, fileManager.validateNotesDirectory.bind(fileManager)) diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts index 00dda778be..3165fcf27e 100644 --- a/src/main/services/FileStorage.ts +++ b/src/main/services/FileStorage.ts @@ -16,6 +16,7 @@ import type { FSWatcher } from 'chokidar' import chokidar from 'chokidar' import * as crypto from 'crypto' import type { OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron' +import { app } from 'electron' import { dialog, net, shell } from 'electron' import * as fs from 'fs' import { writeFileSync } from 'fs' @@ -30,6 +31,73 @@ import WordExtractor from 'word-extractor' const logger = loggerService.withContext('FileStorage') +// Get ripgrep binary path +const getRipgrepBinaryPath = (): string | null => { + try { + const arch = process.arch === 'arm64' ? 'arm64' : 'x64' + const platform = process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'win32' : 'linux' + let ripgrepBinaryPath = path.join( + __dirname, + '../../node_modules/@anthropic-ai/claude-agent-sdk/vendor/ripgrep', + `${arch}-${platform}`, + process.platform === 'win32' ? 'rg.exe' : 'rg' + ) + + if (app.isPackaged) { + ripgrepBinaryPath = ripgrepBinaryPath.replace(/\.asar([\\/])/, '.asar.unpacked$1') + } + + if (fs.existsSync(ripgrepBinaryPath)) { + return ripgrepBinaryPath + } + return null + } catch (error) { + logger.error('Failed to locate ripgrep binary:', error as Error) + return null + } +} + +/** + * Execute ripgrep with captured output + */ +function executeRipgrep(args: string[]): Promise<{ exitCode: number; output: string }> { + return new Promise((resolve, reject) => { + const ripgrepBinaryPath = getRipgrepBinaryPath() + + if (!ripgrepBinaryPath) { + reject(new Error('Ripgrep binary not available')) + return + } + + const { spawn } = require('child_process') + const child = spawn(ripgrepBinaryPath, ['--no-config', '--ignore-case', ...args], { + stdio: ['pipe', 'pipe', 'pipe'] + }) + + let output = '' + let errorOutput = '' + + child.stdout.on('data', (data: Buffer) => { + output += data.toString() + }) + + child.stderr.on('data', (data: Buffer) => { + errorOutput += data.toString() + }) + + child.on('close', (code: number) => { + resolve({ + exitCode: code || 0, + output: output || errorOutput + }) + }) + + child.on('error', (error: Error) => { + reject(error) + }) + }) +} + interface FileWatcherConfig { watchExtensions?: string[] ignoredPatterns?: (string | RegExp)[] @@ -54,6 +122,26 @@ const DEFAULT_WATCHER_CONFIG: Required = { eventChannel: 'file-change' } +interface DirectoryListOptions { + recursive?: boolean + maxDepth?: number + includeHidden?: boolean + includeFiles?: boolean + includeDirectories?: boolean + maxEntries?: number + searchPattern?: string +} + +const DEFAULT_DIRECTORY_LIST_OPTIONS: Required = { + recursive: true, + maxDepth: 3, + includeHidden: false, + includeFiles: true, + includeDirectories: true, + maxEntries: 10, + searchPattern: '.' +} + class FileStorage { private storageDir = getFilesDir() private notesDir = getNotesDir() @@ -748,6 +836,284 @@ class FileStorage { } } + public listDirectory = async ( + _: Electron.IpcMainInvokeEvent, + dirPath: string, + options?: DirectoryListOptions + ): Promise => { + const mergedOptions: Required = { + ...DEFAULT_DIRECTORY_LIST_OPTIONS, + ...options + } + + const resolvedPath = path.resolve(dirPath) + + const stat = await fs.promises.stat(resolvedPath).catch((error) => { + logger.error(`[IPC - Error] Failed to access directory: ${resolvedPath}`, error as Error) + throw error + }) + + if (!stat.isDirectory()) { + throw new Error(`Path is not a directory: ${resolvedPath}`) + } + + // Use ripgrep for file listing with relevance-based sorting + if (!getRipgrepBinaryPath()) { + throw new Error('Ripgrep binary not available') + } + + return await this.listDirectoryWithRipgrep(resolvedPath, mergedOptions) + } + + /** + * Search directories by name pattern + */ + private async searchDirectories( + resolvedPath: string, + options: Required, + currentDepth: number = 0 + ): Promise { + if (!options.includeDirectories) return [] + if (!options.recursive && currentDepth > 0) return [] + if (options.maxDepth > 0 && currentDepth >= options.maxDepth) return [] + + const directories: string[] = [] + const excludedDirs = new Set([ + 'node_modules', + '.git', + '.idea', + '.vscode', + 'dist', + 'build', + '.next', + '.nuxt', + 'coverage', + '.cache' + ]) + + try { + const entries = await fs.promises.readdir(resolvedPath, { withFileTypes: true }) + const searchPatternLower = options.searchPattern.toLowerCase() + + for (const entry of entries) { + if (!entry.isDirectory()) continue + + // Skip hidden directories unless explicitly included + if (!options.includeHidden && entry.name.startsWith('.')) continue + + // Skip excluded directories + if (excludedDirs.has(entry.name)) continue + + const fullPath = path.join(resolvedPath, entry.name).replace(/\\/g, '/') + + // Check if directory name matches search pattern + if (options.searchPattern === '.' || entry.name.toLowerCase().includes(searchPatternLower)) { + directories.push(fullPath) + } + + // Recursively search subdirectories + if (options.recursive && currentDepth < options.maxDepth) { + const subDirs = await this.searchDirectories(fullPath, options, currentDepth + 1) + directories.push(...subDirs) + } + } + } catch (error) { + logger.warn(`Failed to search directories in: ${resolvedPath}`, error as Error) + } + + return directories + } + + /** + * Search files by filename pattern + */ + private async searchByFilename(resolvedPath: string, options: Required): Promise { + const files: string[] = [] + const directories: string[] = [] + + // Search for files using ripgrep + if (options.includeFiles) { + const args: string[] = ['--files'] + + // Handle hidden files + if (!options.includeHidden) { + args.push('--glob', '!.*') + } + + // Use --iglob to let ripgrep filter filenames (case-insensitive) + if (options.searchPattern && options.searchPattern !== '.') { + args.push('--iglob', `*${options.searchPattern}*`) + } + + // Exclude common hidden directories and large directories + args.push('-g', '!**/node_modules/**') + args.push('-g', '!**/.git/**') + args.push('-g', '!**/.idea/**') + args.push('-g', '!**/.vscode/**') + args.push('-g', '!**/.DS_Store') + args.push('-g', '!**/dist/**') + args.push('-g', '!**/build/**') + args.push('-g', '!**/.next/**') + args.push('-g', '!**/.nuxt/**') + args.push('-g', '!**/coverage/**') + args.push('-g', '!**/.cache/**') + + // Handle max depth + if (!options.recursive) { + args.push('--max-depth', '1') + } else if (options.maxDepth > 0) { + args.push('--max-depth', options.maxDepth.toString()) + } + + // Add the directory path + args.push(resolvedPath) + + const { exitCode, output } = await executeRipgrep(args) + + // Exit code 0 means files found, 1 means no files found (still success), 2+ means error + if (exitCode >= 2) { + throw new Error(`Ripgrep failed with exit code ${exitCode}: ${output}`) + } + + // Parse ripgrep output (no need to filter by filename - ripgrep already did it) + files.push( + ...output + .split('\n') + .filter((line) => line.trim()) + .map((line) => line.replace(/\\/g, '/')) + ) + } + + // Search for directories + if (options.includeDirectories) { + directories.push(...(await this.searchDirectories(resolvedPath, options))) + } + + // Combine and sort: directories first (alphabetically), then files (alphabetically) + const sortedDirectories = directories.sort((a, b) => { + const aName = path.basename(a) + const bName = path.basename(b) + return aName.localeCompare(bName) + }) + + const sortedFiles = files.sort((a, b) => { + const aName = path.basename(a) + const bName = path.basename(b) + return aName.localeCompare(bName) + }) + + return [...sortedDirectories, ...sortedFiles].slice(0, options.maxEntries) + } + + /** + * Search files by content pattern + */ + private async searchByContent(resolvedPath: string, options: Required): Promise { + const args: string[] = ['-l'] + + // Handle hidden files + if (!options.includeHidden) { + args.push('--glob', '!.*') + } + + // Exclude common hidden directories and large directories + args.push('-g', '!**/node_modules/**') + args.push('-g', '!**/.git/**') + args.push('-g', '!**/.idea/**') + args.push('-g', '!**/.vscode/**') + args.push('-g', '!**/.DS_Store') + args.push('-g', '!**/dist/**') + args.push('-g', '!**/build/**') + args.push('-g', '!**/.next/**') + args.push('-g', '!**/.nuxt/**') + args.push('-g', '!**/coverage/**') + args.push('-g', '!**/.cache/**') + + // Handle max depth + if (!options.recursive) { + args.push('--max-depth', '1') + } else if (options.maxDepth > 0) { + args.push('--max-depth', options.maxDepth.toString()) + } + + // Handle max count + if (options.maxEntries > 0) { + args.push('--max-count', options.maxEntries.toString()) + } + + // Add search pattern (search in content) + args.push(options.searchPattern) + + // Add the directory path + args.push(resolvedPath) + + const { exitCode, output } = await executeRipgrep(args) + + // Exit code 0 means files found, 1 means no files found (still success), 2+ means error + if (exitCode >= 2) { + throw new Error(`Ripgrep failed with exit code ${exitCode}: ${output}`) + } + + // Parse ripgrep output (already sorted by relevance) + const results = output + .split('\n') + .filter((line) => line.trim()) + .map((line) => line.replace(/\\/g, '/')) + .slice(0, options.maxEntries) + + return results + } + + private async listDirectoryWithRipgrep( + resolvedPath: string, + options: Required + ): Promise { + const maxEntries = options.maxEntries + + // Step 1: Search by filename first + logger.debug('Searching by filename pattern', { pattern: options.searchPattern, path: resolvedPath }) + const filenameResults = await this.searchByFilename(resolvedPath, options) + + logger.debug('Found matches by filename', { count: filenameResults.length }) + + // If we have enough filename matches, return them + if (filenameResults.length >= maxEntries) { + return filenameResults.slice(0, maxEntries) + } + + // Step 2: If filename matches are less than maxEntries, search by content to fill up + logger.debug('Filename matches insufficient, searching by content to fill up', { + filenameCount: filenameResults.length, + needed: maxEntries - filenameResults.length + }) + + // Adjust maxEntries for content search to get enough results + const contentOptions = { + ...options, + maxEntries: maxEntries - filenameResults.length + 20 // Request extra to account for duplicates + } + + const contentResults = await this.searchByContent(resolvedPath, contentOptions) + + logger.debug('Found matches by content', { count: contentResults.length }) + + // Combine results: filename matches first, then content matches (deduplicated) + const combined = [...filenameResults] + const filenameSet = new Set(filenameResults) + + for (const filePath of contentResults) { + if (!filenameSet.has(filePath)) { + combined.push(filePath) + if (combined.length >= maxEntries) { + break + } + } + } + + logger.debug('Combined results', { total: combined.length, filenameCount: filenameResults.length }) + return combined.slice(0, maxEntries) + } + public validateNotesDirectory = async (_: Electron.IpcMainInvokeEvent, dirPath: string): Promise => { try { if (!dirPath || typeof dirPath !== 'string') { diff --git a/src/main/services/agents/BaseService.ts b/src/main/services/agents/BaseService.ts index d96ce1b8e4..1c9b438e4a 100644 --- a/src/main/services/agents/BaseService.ts +++ b/src/main/services/agents/BaseService.ts @@ -36,7 +36,14 @@ export abstract class BaseService { protected static db: LibSQLDatabase | null = null protected static isInitialized = false protected static initializationPromise: Promise | null = null - protected jsonFields: string[] = ['tools', 'mcps', 'configuration', 'accessible_paths', 'allowed_tools'] + protected jsonFields: string[] = [ + 'tools', + 'mcps', + 'configuration', + 'accessible_paths', + 'allowed_tools', + 'slash_commands' + ] /** * Initialize database with retry logic and proper error handling diff --git a/src/main/services/agents/database/schema/sessions.schema.ts b/src/main/services/agents/database/schema/sessions.schema.ts index 21ac2fe2c6..4b16a9ec41 100644 --- a/src/main/services/agents/database/schema/sessions.schema.ts +++ b/src/main/services/agents/database/schema/sessions.schema.ts @@ -22,6 +22,7 @@ export const sessionsTable = sqliteTable('sessions', { mcps: text('mcps'), // JSON array of MCP tool IDs allowed_tools: text('allowed_tools'), // JSON array of allowed tool IDs (whitelist) + slash_commands: text('slash_commands'), // JSON array of slash command objects from SDK init configuration: text('configuration'), // JSON, extensible settings diff --git a/src/main/services/agents/services/SessionService.ts b/src/main/services/agents/services/SessionService.ts index 0bb1515696..c9ecf72c32 100644 --- a/src/main/services/agents/services/SessionService.ts +++ b/src/main/services/agents/services/SessionService.ts @@ -1,4 +1,5 @@ -import type { UpdateSessionResponse } from '@types' +import { loggerService } from '@logger' +import type { SlashCommand, UpdateSessionResponse } from '@types' import { AgentBaseSchema, type AgentEntity, @@ -13,6 +14,10 @@ import { and, count, desc, eq, type SQL } from 'drizzle-orm' import { BaseService } from '../BaseService' import { agentsTable, type InsertSessionRow, type SessionRow, sessionsTable } from '../database/schema' import type { AgentModelField } from '../errors' +import { pluginService } from '../plugins/PluginService' +import { builtinSlashCommands } from './claudecode/commands' + +const logger = loggerService.withContext('SessionService') export class SessionService extends BaseService { private static instance: SessionService | null = null @@ -29,6 +34,52 @@ export class SessionService extends BaseService { await BaseService.initialize() } + /** + * Override BaseService.listSlashCommands to merge builtin and plugin commands + */ + async listSlashCommands(agentType: string, agentId?: string): Promise { + const commands: SlashCommand[] = [] + + // Add builtin slash commands + if (agentType === 'claude-code') { + commands.push(...builtinSlashCommands) + } + + // Add local command plugins from .claude/commands/ + if (agentId) { + try { + const installedPlugins = await pluginService.listInstalled(agentId) + + // Filter for command type plugins + const commandPlugins = installedPlugins.filter((p) => p.type === 'command') + + // Convert plugin metadata to SlashCommand format + for (const plugin of commandPlugins) { + const commandName = plugin.metadata.filename.replace(/\.md$/i, '') + commands.push({ + command: `/${commandName}`, + description: plugin.metadata.description + }) + } + + logger.info('Listed slash commands', { + agentType, + agentId, + builtinCount: builtinSlashCommands.length, + localCount: commandPlugins.length, + totalCount: commands.length + }) + } catch (error) { + logger.warn('Failed to list local command plugins', { + agentId, + error: error instanceof Error ? error.message : String(error) + }) + } + } + + return commands + } + async createSession( agentId: string, req: Partial = {} @@ -111,7 +162,13 @@ export class SessionService extends BaseService { const session = this.deserializeJsonFields(result[0]) as GetAgentSessionResponse session.tools = await this.listMcpTools(session.agent_type, session.mcps) - session.slash_commands = await this.listSlashCommands(session.agent_type) + + // If slash_commands is not in database yet (e.g., first invoke before init message), + // fall back to builtin + local commands. Otherwise, use the merged commands from database. + if (!session.slash_commands || session.slash_commands.length === 0) { + session.slash_commands = await this.listSlashCommands(session.agent_type, agentId) + } + return session } diff --git a/src/main/services/agents/services/claudecode/__tests__/transform.test.ts b/src/main/services/agents/services/claudecode/__tests__/transform.test.ts index 1c5c2ade6b..8f8c1df038 100644 --- a/src/main/services/agents/services/claudecode/__tests__/transform.test.ts +++ b/src/main/services/agents/services/claudecode/__tests__/transform.test.ts @@ -1,7 +1,7 @@ import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk' import { describe, expect, it } from 'vitest' -import { ClaudeStreamState, transformSDKMessageToStreamParts } from '../transform' +import { ClaudeStreamState, stripLocalCommandTags, transformSDKMessageToStreamParts } from '../transform' const baseStreamMetadata = { parent_tool_use_id: null, @@ -10,6 +10,19 @@ const baseStreamMetadata = { const uuid = (n: number) => `00000000-0000-0000-0000-${n.toString().padStart(12, '0')}` +describe('stripLocalCommandTags', () => { + it('removes stdout wrapper while preserving inner text', () => { + const input = 'before echo "hi" after' + expect(stripLocalCommandTags(input)).toBe('before echo "hi" after') + }) + + it('strips multiple stdout/stderr blocks and leaves other content intact', () => { + const input = + 'line1\nkeep\nError' + expect(stripLocalCommandTags(input)).toBe('line1\nkeep\nError') + }) +}) + describe('Claude → AiSDK transform', () => { it('handles tool call streaming lifecycle', () => { const state = new ClaudeStreamState() diff --git a/src/main/services/agents/services/claudecode/commands.ts b/src/main/services/agents/services/claudecode/commands.ts index f30d620572..0ce4f4ccef 100644 --- a/src/main/services/agents/services/claudecode/commands.ts +++ b/src/main/services/agents/services/claudecode/commands.ts @@ -1,25 +1,12 @@ import type { SlashCommand } from '@types' export const builtinSlashCommands: SlashCommand[] = [ - { command: '/add-dir', description: 'Add additional working directories' }, - { command: '/agents', description: 'Manage custom AI subagents for specialized tasks' }, - { command: '/bug', description: 'Report bugs (sends conversation to Anthropic)' }, { command: '/clear', description: 'Clear conversation history' }, { command: '/compact', description: 'Compact conversation with optional focus instructions' }, - { command: '/config', description: 'View/modify configuration' }, - { command: '/cost', description: 'Show token usage statistics' }, - { command: '/doctor', description: 'Checks the health of your Claude Code installation' }, - { command: '/help', description: 'Get usage help' }, - { command: '/init', description: 'Initialize project with CLAUDE.md guide' }, - { command: '/login', description: 'Switch Anthropic accounts' }, - { command: '/logout', description: 'Sign out from your Anthropic account' }, - { command: '/mcp', description: 'Manage MCP server connections and OAuth authentication' }, - { command: '/memory', description: 'Edit CLAUDE.md memory files' }, - { command: '/model', description: 'Select or change the AI model' }, - { command: '/permissions', description: 'View or update permissions' }, - { command: '/pr_comments', description: 'View pull request comments' }, - { command: '/review', description: 'Request code review' }, - { command: '/status', description: 'View account and system statuses' }, - { command: '/terminal-setup', description: 'Install Shift+Enter key binding for newlines (iTerm2 and VSCode only)' }, - { command: '/vim', description: 'Enter vim mode for alternating insert and command modes' } + { command: '/context', description: 'Visualize current context usage as a colored grid' }, + { + command: '/cost', + description: 'Show token usage statistics (see cost tracking guide for subscription-specific details)' + }, + { command: '/todos', description: 'List current todo items' } ] diff --git a/src/main/services/agents/services/claudecode/index.ts b/src/main/services/agents/services/claudecode/index.ts index 4e20520017..a8f3f54fa8 100644 --- a/src/main/services/agents/services/claudecode/index.ts +++ b/src/main/services/agents/services/claudecode/index.ts @@ -12,6 +12,7 @@ import { app } from 'electron' import type { GetAgentSessionResponse } from '../..' import type { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface' +import { sessionService } from '../SessionService' import { promptForToolApproval } from './tool-permissions' import { ClaudeStreamState, transformSDKMessageToStreamParts } from './transform' @@ -19,6 +20,7 @@ const require_ = createRequire(import.meta.url) const logger = loggerService.withContext('ClaudeCodeService') const DEFAULT_AUTO_ALLOW_TOOLS = new Set(['Read', 'Glob', 'Grep']) const shouldAutoApproveTools = process.env.CHERRY_AUTO_ALLOW_TOOLS === '1' +const NO_RESUME_COMMANDS = ['/clear'] type UserInputMessage = { type: 'user' @@ -197,7 +199,7 @@ class ClaudeCodeService implements AgentServiceInterface { options.strictMcpConfig = true } - if (lastAgentSessionId) { + if (lastAgentSessionId && !NO_RESUME_COMMANDS.some((cmd) => prompt.includes(cmd))) { options.resume = lastAgentSessionId // TODO: use fork session when we support branching sessions // options.forkSession = true @@ -220,7 +222,15 @@ class ClaudeCodeService implements AgentServiceInterface { // Start async processing on the next tick so listeners can subscribe first setImmediate(() => { - this.processSDKQuery(userInputStream, closeUserStream, options, aiStream, errorChunks).catch((error) => { + this.processSDKQuery( + userInputStream, + closeUserStream, + options, + aiStream, + errorChunks, + session.agent_id, + session.id + ).catch((error) => { logger.error('Unhandled Claude Code stream error', { error: error instanceof Error ? { name: error.name, message: error.message } : String(error) }) @@ -329,7 +339,9 @@ class ClaudeCodeService implements AgentServiceInterface { closePromptStream: () => void, options: Options, stream: ClaudeCodeStream, - errorChunks: string[] + errorChunks: string[], + agentId: string, + sessionId: string ): Promise { const jsonOutput: SDKMessage[] = [] let hasCompleted = false @@ -342,6 +354,62 @@ class ClaudeCodeService implements AgentServiceInterface { jsonOutput.push(message) + // Handle init message - merge builtin and SDK slash_commands + if (message.type === 'system' && message.subtype === 'init') { + const sdkSlashCommands = message.slash_commands || [] + logger.info('Received init message with slash commands', { + sessionId, + commands: sdkSlashCommands + }) + + try { + // Get builtin + local slash commands from BaseService + const existingCommands = await sessionService.listSlashCommands('claude-code', agentId) + + // Convert SDK slash_commands (string[]) to SlashCommand[] format + // Ensure all commands start with '/' + const sdkCommands = sdkSlashCommands.map((cmd) => { + const normalizedCmd = cmd.startsWith('/') ? cmd : `/${cmd}` + return { + command: normalizedCmd, + description: undefined + } + }) + + // Merge: existing commands (builtin + local) + SDK commands, deduplicate by command name + const commandMap = new Map() + + for (const cmd of existingCommands) { + commandMap.set(cmd.command, cmd) + } + + for (const cmd of sdkCommands) { + if (!commandMap.has(cmd.command)) { + commandMap.set(cmd.command, cmd) + } + } + + const mergedCommands = Array.from(commandMap.values()) + + // Update session in database + await sessionService.updateSession(agentId, sessionId, { + slash_commands: mergedCommands + }) + + logger.info('Updated session with merged slash commands', { + sessionId, + existingCount: existingCommands.length, + sdkCount: sdkCommands.length, + totalCount: mergedCommands.length + }) + } catch (error) { + logger.error('Failed to update session slash_commands', { + sessionId, + error: error instanceof Error ? error.message : String(error) + }) + } + } + if (message.type === 'assistant' || message.type === 'user') { logger.silly('claude response', { message, @@ -378,7 +446,6 @@ class ClaudeCodeService implements AgentServiceInterface { } } - hasCompleted = true const duration = Date.now() - startTime logger.debug('SDK query completed successfully', { diff --git a/src/main/services/agents/services/claudecode/transform.ts b/src/main/services/agents/services/claudecode/transform.ts index 5905ed6434..41285175b4 100644 --- a/src/main/services/agents/services/claudecode/transform.ts +++ b/src/main/services/agents/services/claudecode/transform.ts @@ -73,13 +73,21 @@ const emptyUsage: LanguageModelUsage = { */ const generateMessageId = (): string => `msg_${uuidv4().replace(/-/g, '')}` +/** + * Removes any local command stdout/stderr XML wrappers that should never surface to the UI. + */ +export const stripLocalCommandTags = (text: string): string => { + return text.replace(/(.*?)<\/local-command-\1>/gs, '$2') +} + /** * Filters out command-* tags from text content to prevent internal command * messages from appearing in the user-facing UI. * Removes tags like ... and ... */ const filterCommandTags = (text: string): string => { - return text.replace(/]+>.*?<\/command-[^>]+>/gs, '').trim() + const withoutLocalCommandTags = stripLocalCommandTags(text) + return withoutLocalCommandTags.replace(/]+>.*?<\/command-[^>]+>/gs, '').trim() } /** @@ -102,6 +110,7 @@ const sdkMessageToProviderMetadata = (message: SDKMessage): ProviderMetadata => * blocks across calls so that incremental deltas can be correlated correctly. */ export function transformSDKMessageToStreamParts(sdkMessage: SDKMessage, state: ClaudeStreamState): AgentStreamPart[] { + logger.silly('Transforming SDKMessage', { message: sdkMessage }) switch (sdkMessage.type) { case 'assistant': return handleAssistantMessage(sdkMessage, state) @@ -135,7 +144,8 @@ function handleAssistantMessage( const isStreamingActive = state.hasActiveStep() if (typeof content === 'string') { - if (!content) { + const sanitizedContent = stripLocalCommandTags(content) + if (!sanitizedContent) { return chunks } @@ -157,7 +167,7 @@ function handleAssistantMessage( chunks.push({ type: 'text-delta', id: textId, - text: content, + text: sanitizedContent, providerMetadata }) chunks.push({ @@ -178,7 +188,10 @@ function handleAssistantMessage( switch (block.type) { case 'text': if (!isStreamingActive) { - textBlocks.push(block.text) + const sanitizedText = stripLocalCommandTags(block.text) + if (sanitizedText) { + textBlocks.push(sanitizedText) + } } break case 'tool_use': @@ -537,6 +550,10 @@ function handleContentBlockDelta( logger.warn('Received text_delta for unknown block', { index }) return } + block.text = stripLocalCommandTags(block.text) + if (!block.text) { + break + } chunks.push({ type: 'text-delta', id: block.id, diff --git a/src/preload/index.ts b/src/preload/index.ts index d60e0edfe9..671284d88d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -48,6 +48,16 @@ import type { } from '../renderer/src/types/plugin' import type { ActionItem } from '../renderer/src/types/selectionTypes' +type DirectoryListOptions = { + recursive?: boolean + maxDepth?: number + includeHidden?: boolean + includeFiles?: boolean + includeDirectories?: boolean + maxEntries?: number + searchPattern?: string +} + export function tracedInvoke(channel: string, spanContext: SpanContext | undefined, ...args: any[]) { if (spanContext) { const data = { type: 'trace', context: spanContext } @@ -201,6 +211,8 @@ const api = { openFileWithRelativePath: (file: FileMetadata) => ipcRenderer.invoke(IpcChannel.File_OpenWithRelativePath, file), isTextFile: (filePath: string): Promise => ipcRenderer.invoke(IpcChannel.File_IsTextFile, filePath), getDirectoryStructure: (dirPath: string) => ipcRenderer.invoke(IpcChannel.File_GetDirectoryStructure, dirPath), + listDirectory: (dirPath: string, options?: DirectoryListOptions) => + ipcRenderer.invoke(IpcChannel.File_ListDirectory, dirPath, options), checkFileName: (dirPath: string, fileName: string, isFile: boolean) => ipcRenderer.invoke(IpcChannel.File_CheckFileName, dirPath, fileName, isFile), validateNotesDirectory: (dirPath: string) => ipcRenderer.invoke(IpcChannel.File_ValidateNotesDirectory, dirPath), diff --git a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts index 6e4288d241..544ec443aa 100644 --- a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts +++ b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts @@ -30,18 +30,22 @@ export class AiSdkToChunkAdapter { private onSessionUpdate?: (sessionId: string) => void private responseStartTimestamp: number | null = null private firstTokenTimestamp: number | null = null + private hasTextContent = false + private getSessionWasCleared?: () => boolean constructor( private onChunk: (chunk: Chunk) => void, mcpTools: MCPTool[] = [], accumulate?: boolean, enableWebSearch?: boolean, - onSessionUpdate?: (sessionId: string) => void + onSessionUpdate?: (sessionId: string) => void, + getSessionWasCleared?: () => boolean ) { this.toolCallHandler = new ToolCallChunkHandler(onChunk, mcpTools) this.accumulate = accumulate this.enableWebSearch = enableWebSearch || false this.onSessionUpdate = onSessionUpdate + this.getSessionWasCleared = getSessionWasCleared } private markFirstTokenIfNeeded() { @@ -84,8 +88,9 @@ export class AiSdkToChunkAdapter { } this.resetTimingState() this.responseStartTimestamp = Date.now() - // Reset link converter state at the start of stream + // Reset state at the start of stream this.isFirstChunk = true + this.hasTextContent = false try { while (true) { @@ -129,6 +134,8 @@ export class AiSdkToChunkAdapter { const agentRawMessage = chunk.rawValue as ClaudeCodeRawValue if (agentRawMessage.type === 'init' && agentRawMessage.session_id) { this.onSessionUpdate?.(agentRawMessage.session_id) + } else if (agentRawMessage.type === 'compact' && agentRawMessage.session_id) { + this.onSessionUpdate?.(agentRawMessage.session_id) } this.onChunk({ type: ChunkType.RAW, @@ -143,6 +150,7 @@ export class AiSdkToChunkAdapter { }) break case 'text-delta': { + this.hasTextContent = true const processedText = chunk.text || '' let finalText: string @@ -301,6 +309,25 @@ export class AiSdkToChunkAdapter { } case 'finish': { + // Check if session was cleared (e.g., /clear command) and no text was output + const sessionCleared = this.getSessionWasCleared?.() ?? false + if (sessionCleared && !this.hasTextContent) { + // Inject a "context cleared" message for the user + const clearMessage = '✨ Context cleared. Starting fresh conversation.' + this.onChunk({ + type: ChunkType.TEXT_START + }) + this.onChunk({ + type: ChunkType.TEXT_DELTA, + text: clearMessage + }) + this.onChunk({ + type: ChunkType.TEXT_COMPLETE, + text: clearMessage + }) + final.text = clearMessage + } + const usage = { completion_tokens: chunk.totalUsage?.outputTokens || 0, prompt_tokens: chunk.totalUsage?.inputTokens || 0, diff --git a/src/renderer/src/components/QuickPanel/defaultStrategies.ts b/src/renderer/src/components/QuickPanel/defaultStrategies.ts new file mode 100644 index 0000000000..22f46db98d --- /dev/null +++ b/src/renderer/src/components/QuickPanel/defaultStrategies.ts @@ -0,0 +1,104 @@ +import * as tinyPinyin from 'tiny-pinyin' + +import type { QuickPanelFilterFn, QuickPanelListItem, QuickPanelSortFn } from './types' + +/** + * Default filter function + * Implements standard filtering logic with pinyin support + */ +export const defaultFilterFn: QuickPanelFilterFn = (item, searchText, fuzzyRegex, pinyinCache) => { + if (!searchText) return true + + let filterText = item.filterText || '' + if (typeof item.label === 'string') { + filterText += item.label + } + if (typeof item.description === 'string') { + filterText += item.description + } + + const lowerFilterText = filterText.toLowerCase() + const lowerSearchText = searchText.toLowerCase() + + // Direct substring match + if (lowerFilterText.includes(lowerSearchText)) { + return true + } + + // Pinyin fuzzy match for Chinese characters + if (tinyPinyin.isSupported() && /[\u4e00-\u9fa5]/.test(filterText)) { + try { + let pinyinText = pinyinCache.get(item) + if (!pinyinText) { + pinyinText = tinyPinyin.convertToPinyin(filterText, '', true).toLowerCase() + pinyinCache.set(item, pinyinText) + } + return fuzzyRegex.test(pinyinText) + } catch (error) { + return true + } + } else { + return fuzzyRegex.test(filterText.toLowerCase()) + } +} + +/** + * Calculate match score for sorting + * Higher score = better match + */ +const calculateMatchScore = (item: QuickPanelListItem, searchText: string): number => { + let filterText = item.filterText || '' + if (typeof item.label === 'string') { + filterText += item.label + } + if (typeof item.description === 'string') { + filterText += item.description + } + + const lowerFilterText = filterText.toLowerCase() + const lowerSearchText = searchText.toLowerCase() + + // Exact match (highest priority) + if (lowerFilterText === lowerSearchText) { + return 1000 + } + + // Label exact match (very high priority) + if (typeof item.label === 'string' && item.label.toLowerCase() === lowerSearchText) { + return 900 + } + + // Starts with search text (high priority) + if (lowerFilterText.startsWith(lowerSearchText)) { + return 800 + } + + // Label starts with search text + if (typeof item.label === 'string' && item.label.toLowerCase().startsWith(lowerSearchText)) { + return 700 + } + + // Contains search text (medium priority) + if (lowerFilterText.includes(lowerSearchText)) { + // Earlier position = higher score + const position = lowerFilterText.indexOf(lowerSearchText) + return 600 - position + } + + // Pinyin fuzzy match (lower priority) + return 100 +} + +/** + * Default sort function + * Sorts items by match score in descending order + */ +export const defaultSortFn: QuickPanelSortFn = (items, searchText) => { + if (!searchText) return items + + return [...items].sort((a, b) => { + const scoreA = calculateMatchScore(a, searchText) + const scoreB = calculateMatchScore(b, searchText) + return scoreB - scoreA + }) +} diff --git a/src/renderer/src/components/QuickPanel/index.ts b/src/renderer/src/components/QuickPanel/index.ts index ec3bed20ee..4a3a9b3391 100644 --- a/src/renderer/src/components/QuickPanel/index.ts +++ b/src/renderer/src/components/QuickPanel/index.ts @@ -1,3 +1,4 @@ +export * from './defaultStrategies' export * from './hook' export * from './provider' export * from './types' diff --git a/src/renderer/src/components/QuickPanel/provider.tsx b/src/renderer/src/components/QuickPanel/provider.tsx index cda1d0fa9b..08111d0c4f 100644 --- a/src/renderer/src/components/QuickPanel/provider.tsx +++ b/src/renderer/src/components/QuickPanel/provider.tsx @@ -4,11 +4,12 @@ import type { QuickPanelCallBackOptions, QuickPanelCloseAction, QuickPanelContextType, + QuickPanelFilterFn, QuickPanelListItem, QuickPanelOpenOptions, + QuickPanelSortFn, QuickPanelTriggerInfo } from './types' - const QuickPanelContext = createContext(null) export const QuickPanelProvider: React.FC = ({ children }) => { @@ -17,19 +18,39 @@ export const QuickPanelProvider: React.FC = ({ children const [list, setList] = useState([]) const [title, setTitle] = useState() - const [defaultIndex, setDefaultIndex] = useState(0) + const [defaultIndex, setDefaultIndex] = useState(-1) const [pageSize, setPageSize] = useState(7) const [multiple, setMultiple] = useState(false) + const [manageListExternally, setManageListExternally] = useState(false) const [triggerInfo, setTriggerInfo] = useState() + const [filterFn, setFilterFn] = useState() + const [sortFn, setSortFn] = useState() const [onClose, setOnClose] = useState<((Options: Partial) => void) | undefined>() const [beforeAction, setBeforeAction] = useState<((Options: QuickPanelCallBackOptions) => void) | undefined>() const [afterAction, setAfterAction] = useState<((Options: QuickPanelCallBackOptions) => void) | undefined>() + const [onSearchChange, setOnSearchChange] = useState<((searchText: string) => void) | undefined>() + const [lastCloseAction, setLastCloseAction] = useState(undefined) const clearTimer = useRef(null) // 添加更新item选中状态的方法 const updateItemSelection = useCallback((targetItem: QuickPanelListItem, isSelected: boolean) => { - setList((prevList) => prevList.map((item) => (item === targetItem ? { ...item, isSelected } : item))) + setList((prevList) => { + // 先尝试引用匹配(快速路径) + const refIndex = prevList.findIndex((item) => item === targetItem) + if (refIndex !== -1) { + return prevList.map((item, idx) => (idx === refIndex ? { ...item, isSelected } : item)) + } + + // 如果引用匹配失败,使用内容匹配(兜底方案) + // 通过 label 和 filterText 来识别同一个item + return prevList.map((item) => { + const isSameItem = + (item.label === targetItem.label || item.filterText === targetItem.filterText) && + (!targetItem.filterText || item.filterText === targetItem.filterText) + return isSameItem ? { ...item, isSelected } : item + }) + }) }, []) // 添加更新整个列表的方法 @@ -43,17 +64,23 @@ export const QuickPanelProvider: React.FC = ({ children clearTimer.current = null } + setLastCloseAction(undefined) setTitle(options.title) setList(options.list) - setDefaultIndex(options.defaultIndex ?? 0) + const nextDefaultIndex = typeof options.defaultIndex === 'number' ? Math.max(-1, options.defaultIndex) : -1 + setDefaultIndex(nextDefaultIndex) setPageSize(options.pageSize ?? 7) setMultiple(options.multiple ?? false) + setManageListExternally(options.manageListExternally ?? false) setSymbol(options.symbol) setTriggerInfo(options.triggerInfo) setOnClose(() => options.onClose) setBeforeAction(() => options.beforeAction) setAfterAction(() => options.afterAction) + setOnSearchChange(() => options.onSearchChange) + setFilterFn(() => options.filterFn) + setSortFn(() => options.sortFn) setIsVisible(true) }, []) @@ -61,6 +88,8 @@ export const QuickPanelProvider: React.FC = ({ children const close = useCallback( (action?: QuickPanelCloseAction, searchText?: string) => { setIsVisible(false) + setManageListExternally(false) + setLastCloseAction(action) onClose?.({ action, searchText, item: {} as QuickPanelListItem, context: this }) clearTimer.current = setTimeout(() => { @@ -68,9 +97,13 @@ export const QuickPanelProvider: React.FC = ({ children setOnClose(undefined) setBeforeAction(undefined) setAfterAction(undefined) + setOnSearchChange(undefined) + setFilterFn(undefined) + setSortFn(undefined) setTitle(undefined) setSymbol('') setTriggerInfo(undefined) + setManageListExternally(false) }, 200) }, [onClose] @@ -100,10 +133,15 @@ export const QuickPanelProvider: React.FC = ({ children defaultIndex, pageSize, multiple, + manageListExternally, triggerInfo, + lastCloseAction, + filterFn, + sortFn, onClose, beforeAction, - afterAction + afterAction, + onSearchChange }), [ open, @@ -117,10 +155,15 @@ export const QuickPanelProvider: React.FC = ({ children defaultIndex, pageSize, multiple, + manageListExternally, triggerInfo, + lastCloseAction, + filterFn, + sortFn, onClose, beforeAction, - afterAction + afterAction, + onSearchChange ] ) diff --git a/src/renderer/src/components/QuickPanel/types.ts b/src/renderer/src/components/QuickPanel/types.ts index 519180c5b7..da7b37cee3 100644 --- a/src/renderer/src/components/QuickPanel/types.ts +++ b/src/renderer/src/components/QuickPanel/types.ts @@ -10,7 +10,8 @@ export enum QuickPanelReservedSymbol { WebSearch = '?', Mcp = 'mcp', McpPrompt = 'mcp-prompt', - McpResource = 'mcp-resource' + McpResource = 'mcp-resource', + SlashCommands = 'slash-commands' } export type QuickPanelCloseAction = 'enter' | 'click' | 'esc' | 'outsideclick' | 'enter_empty' | string | undefined @@ -27,6 +28,29 @@ export type QuickPanelCallBackOptions = { searchText?: string } +/** + * Filter function type + * @param item - The item to check + * @param searchText - The search text (without leading symbol) + * @param fuzzyRegex - Fuzzy matching regex + * @param pinyinCache - Cache for pinyin conversions + * @returns true if item matches the search + */ +export type QuickPanelFilterFn = ( + item: QuickPanelListItem, + searchText: string, + fuzzyRegex: RegExp, + pinyinCache: WeakMap +) => boolean + +/** + * Sort function type + * @param items - The filtered items to sort + * @param searchText - The search text (without leading symbol) + * @returns sorted items + */ +export type QuickPanelSortFn = (items: QuickPanelListItem[], searchText: string) => QuickPanelListItem[] + export type QuickPanelOpenOptions = { /** 显示在底部左边,类似于Placeholder */ title?: string @@ -48,6 +72,14 @@ export type QuickPanelOpenOptions = { beforeAction?: (options: QuickPanelCallBackOptions) => void afterAction?: (options: QuickPanelCallBackOptions) => void onClose?: (options: QuickPanelCallBackOptions) => void + /** Callback when search text changes (called with debounced search text) */ + onSearchChange?: (searchText: string) => void + /** Tool manages list + collapse behavior externally (skip filtering/auto-close) */ + manageListExternally?: boolean + /** Custom filter function for items (follows open-closed principle) */ + filterFn?: QuickPanelFilterFn + /** Custom sort function for filtered items (follows open-closed principle) */ + sortFn?: QuickPanelSortFn } export type QuickPanelListItem = { @@ -88,10 +120,15 @@ export interface QuickPanelContextType { readonly pageSize: number readonly multiple: boolean readonly triggerInfo?: QuickPanelTriggerInfo + readonly manageListExternally?: boolean + readonly lastCloseAction?: QuickPanelCloseAction + readonly filterFn?: QuickPanelFilterFn + readonly sortFn?: QuickPanelSortFn readonly onClose?: (Options: QuickPanelCallBackOptions) => void readonly beforeAction?: (Options: QuickPanelCallBackOptions) => void readonly afterAction?: (Options: QuickPanelCallBackOptions) => void + readonly onSearchChange?: (searchText: string) => void } export type QuickPanelScrollTrigger = 'initial' | 'keyboard' | 'none' diff --git a/src/renderer/src/components/QuickPanel/view.tsx b/src/renderer/src/components/QuickPanel/view.tsx index 5c6afcbf61..9ed9b966df 100644 --- a/src/renderer/src/components/QuickPanel/view.tsx +++ b/src/renderer/src/components/QuickPanel/view.tsx @@ -10,8 +10,8 @@ import { debounce } from 'lodash' import { Check } from 'lucide-react' import React, { use, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import styled from 'styled-components' -import * as tinyPinyin from 'tiny-pinyin' +import { defaultFilterFn, defaultSortFn } from './defaultStrategies' import { QuickPanelContext } from './provider' import type { QuickPanelCallBackOptions, @@ -62,21 +62,50 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { const [_searchText, setSearchText] = useState('') const searchText = useDeferredValue(_searchText) + const setSearchTextDebounced = useMemo(() => debounce((val: string) => setSearchText(val), 50), []) + const searchTextRef = useRef('') // 缓存:按 item 缓存拼音文本,避免重复转换 const pinyinCacheRef = useRef>(new WeakMap()) - // 轻量防抖:减少高频输入时的过滤调用 - const setSearchTextDebounced = useMemo(() => debounce((val: string) => setSearchText(val), 50), []) - // 跟踪上一次的搜索文本和符号,用于判断是否需要重置index const prevSearchTextRef = useRef('') const prevSymbolRef = useRef('') const { setTimeoutTimer } = useTimer() + + // Use injected filter and sort functions, or fall back to defaults + const filterFn = ctx.filterFn || defaultFilterFn + const sortFn = ctx.sortFn || defaultSortFn // 处理搜索,过滤列表(始终保留 alwaysVisible 项在顶部) const list = useMemo(() => { if (!ctx.isVisible && !ctx.symbol) return [] + + const baseList = (ctx.list || []).filter((item) => !item.hidden) + + if (ctx.manageListExternally) { + const combinedLength = baseList.length + const isSymbolChanged = prevSymbolRef.current !== ctx.symbol + if (isSymbolChanged) { + const maxIndex = combinedLength > 0 ? combinedLength - 1 : -1 + const desiredIndex = + typeof ctx.defaultIndex === 'number' ? Math.min(Math.max(ctx.defaultIndex, -1), maxIndex) : -1 + setIndex(desiredIndex) + } else { + setIndex((prevIndex) => { + if (prevIndex >= combinedLength) { + return combinedLength > 0 ? combinedLength - 1 : -1 + } + return prevIndex + }) + } + + prevSearchTextRef.current = '' + prevSymbolRef.current = ctx.symbol + + return baseList + } + const _searchText = searchText.replace(/^[/@]/, '') const lowerSearchText = _searchText.toLowerCase() const fuzzyPattern = lowerSearchText @@ -86,52 +115,35 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { const fuzzyRegex = new RegExp(fuzzyPattern, 'ig') // 拆分:固定显示项(不参与过滤)与普通项 - const pinnedItems = (ctx.list || []).filter((item) => item.alwaysVisible) - const normalItems = (ctx.list || []).filter((item) => !item.alwaysVisible) + const pinnedItems = baseList.filter((item) => item.alwaysVisible) + const normalItems = baseList.filter((item) => !item.alwaysVisible) + // Filter normal items using injected filter function const filteredNormalItems = normalItems.filter((item) => { - if (!_searchText) return true - - let filterText = item.filterText || '' - if (typeof item.label === 'string') { - filterText += item.label - } - if (typeof item.description === 'string') { - filterText += item.description - } - - const lowerFilterText = filterText.toLowerCase() - - if (lowerFilterText.includes(lowerSearchText)) { - return true - } - - if (tinyPinyin.isSupported() && /[\u4e00-\u9fa5]/.test(filterText)) { - try { - let pinyinText = pinyinCacheRef.current.get(item) - if (!pinyinText) { - pinyinText = tinyPinyin.convertToPinyin(filterText, '', true).toLowerCase() - pinyinCacheRef.current.set(item, pinyinText) - } - return fuzzyRegex.test(pinyinText) - } catch (error) { - return true - } - } else { - return fuzzyRegex.test(filterText.toLowerCase()) - } + return filterFn(item, _searchText, fuzzyRegex, pinyinCacheRef.current) }) + // Sort filtered items using injected sort function + const sortedNormalItems = sortFn(filteredNormalItems, _searchText) + // 只有在搜索文本变化或面板符号变化时才重置index const isSearchChanged = prevSearchTextRef.current !== searchText const isSymbolChanged = prevSymbolRef.current !== ctx.symbol if (isSearchChanged || isSymbolChanged) { - setIndex(-1) // 不默认高亮任何项,让用户主动选择 + const combinedLength = pinnedItems.length + sortedNormalItems.length + if (isSymbolChanged) { + const maxIndex = combinedLength > 0 ? combinedLength - 1 : -1 + const desiredIndex = + typeof ctx.defaultIndex === 'number' ? Math.min(Math.max(ctx.defaultIndex, -1), maxIndex) : -1 + setIndex(desiredIndex) + } else { + setIndex(-1) // 搜索文本变化时不默认高亮 + } } else { // 如果当前index超出范围,调整到有效范围内 setIndex((prevIndex) => { - const combinedLength = pinnedItems.length + filteredNormalItems.length + const combinedLength = pinnedItems.length + sortedNormalItems.length if (prevIndex >= combinedLength) { return combinedLength > 0 ? combinedLength - 1 : -1 } @@ -142,10 +154,9 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { prevSearchTextRef.current = searchText prevSymbolRef.current = ctx.symbol - // 固定项置顶 + 过滤后的普通项 - const pinnedFiltered = [...pinnedItems, ...filteredNormalItems] - return pinnedFiltered.filter((item) => !item.hidden) - }, [ctx.isVisible, ctx.symbol, ctx.list, searchText]) + // 固定项置顶 + 排序后的普通项 + return [...pinnedItems, ...sortedNormalItems] + }, [ctx.isVisible, ctx.symbol, ctx.manageListExternally, ctx.list, ctx.defaultIndex, searchText, filterFn, sortFn]) const canForwardAndBackward = useMemo(() => { return list.some((item) => item.isMenu) || historyPanel.length > 0 @@ -179,19 +190,64 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { if (deleteStart >= deleteEnd) return - // 删除文本 - const newText = textArea.value.slice(0, deleteStart) + textArea.value.slice(deleteEnd) - setInputText(newText) + const activeSearchText = searchTextRef.current ?? '' - // 设置光标位置 - setTimeoutTimer( - 'quickpanel_focus', - () => { - textArea.focus() - textArea.setSelectionRange(deleteStart, deleteStart) - }, - 0 - ) + setInputText((currentText) => { + const safeText = currentText ?? '' + const expectedSegment = includeSymbol ? symbolSegment : symbolSegment.slice(1) + const typedSearch = activeSearchText + const normalizedTyped = includeSymbol + ? typedSearch + : typedSearch.startsWith(symbolSegment[0] ?? '') + ? typedSearch.slice(1) + : typedSearch + + if (normalizedTyped && expectedSegment !== normalizedTyped) { + return safeText + } + + const segmentStart = includeSymbol ? symbolStart : symbolStart + 1 + const segmentEnd = segmentStart + expectedSegment.length + + if (segmentStart < 0 || segmentStart > safeText.length) { + return safeText + } + + if (segmentEnd > safeText.length) { + return safeText + } + + const actualSegment = safeText.slice(segmentStart, segmentEnd) + if (actualSegment !== expectedSegment) { + return safeText + } + + const clampedDeleteStart = Math.max(0, Math.min(deleteStart, safeText.length)) + const clampedDeleteEnd = Math.max(clampedDeleteStart, Math.min(deleteEnd, safeText.length)) + + if (clampedDeleteStart >= clampedDeleteEnd) { + return safeText + } + + const updatedText = safeText.slice(0, clampedDeleteStart) + safeText.slice(clampedDeleteEnd) + + if (updatedText === safeText) { + return safeText + } + + setTimeoutTimer( + 'quickpanel_focus', + () => { + const textareaEl = document.querySelector('.inputbar textarea') as HTMLTextAreaElement | null + if (!textareaEl) return + textareaEl.focus() + textareaEl.setSelectionRange(clampedDeleteStart, clampedDeleteStart) + }, + 0 + ) + + return updatedText + }) setSearchText('') }, @@ -211,11 +267,21 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { if (textArea) { setInputText(textArea.value) } - } else if (action && !['outsideclick', 'esc', 'enter_empty', 'no_result'].includes(action)) { - clearSearchText(true) + } else if ( + action && + !['outsideclick', 'esc', 'enter_empty', 'no_result'].includes(action) && + ctx.triggerInfo?.type === 'input' + ) { + setTimeoutTimer( + 'quickpanel_deferred_clear', + () => { + clearSearchText(true) + }, + 0 + ) } }, - [ctx, clearSearchText, setInputText, searchText] + [ctx, clearSearchText, setInputText, searchText, setTimeoutTimer] ) const handleItemAction = useCallback( @@ -285,12 +351,86 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { searchTextRef.current = searchText }, [searchText]) + // Track onSearchChange callback and search state for debouncing + const prevSearchCallbackTextRef = useRef('') + const isFirstSearchRef = useRef(true) + const searchCallbackTimerRef = useRef(null) + const onSearchChangeRef = useRef(ctx.onSearchChange) + + // Keep onSearchChange ref up to date + useEffect(() => { + onSearchChangeRef.current = ctx.onSearchChange + }, [ctx.onSearchChange]) + + // Reset search history when panel closes + useEffect(() => { + if (!ctx.isVisible) { + prevSearchCallbackTextRef.current = '' + isFirstSearchRef.current = true + if (searchCallbackTimerRef.current) { + clearTimeout(searchCallbackTimerRef.current) + searchCallbackTimerRef.current = null + } + } + }, [ctx.isVisible]) + + // Trigger onSearchChange with debounce (called from handleInput) + const triggerSearchChange = useCallback((searchText: string) => { + if (!onSearchChangeRef.current) return + + // Clean search text: remove leading symbol (/ or @) and trim + const cleanSearchText = searchText.replace(/^[/@]/, '').trim() + + // Don't trigger if search text hasn't changed + if (cleanSearchText === prevSearchCallbackTextRef.current) { + return + } + + // Don't trigger callback for empty search text + if (!cleanSearchText) { + prevSearchCallbackTextRef.current = '' + return + } + + // Clear previous timer + if (searchCallbackTimerRef.current) { + clearTimeout(searchCallbackTimerRef.current) + } + + // First search triggers immediately (0ms), subsequent searches have 300ms debounce + const delay = isFirstSearchRef.current ? 0 : 300 + + searchCallbackTimerRef.current = setTimeout(() => { + prevSearchCallbackTextRef.current = cleanSearchText + isFirstSearchRef.current = false + onSearchChangeRef.current?.(cleanSearchText) + searchCallbackTimerRef.current = null + }, delay) + }, []) + + // Cleanup timer on unmount + useEffect(() => { + return () => { + if (searchCallbackTimerRef.current) { + clearTimeout(searchCallbackTimerRef.current) + searchCallbackTimerRef.current = null + } + } + }, []) + // 获取当前输入的搜索词 const isComposing = useRef(false) + useEffect(() => { + return () => { + setSearchTextDebounced.cancel() + } + }, [setSearchTextDebounced]) + useEffect(() => { if (!ctx.isVisible) return const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement + if (!textArea) return const handleInput = (e: Event) => { if (isComposing.current) return @@ -305,6 +445,8 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { if (lastSymbolIndex !== -1) { const newSearchText = textBeforeCursor.slice(lastSymbolIndex) setSearchTextDebounced(newSearchText) + // Trigger server-side search callback immediately (with its own debounce) + triggerSearchChange(newSearchText) } else { // 使用本地 handleClose,确保在删除触发符时同步受控输入值 handleClose('delete-symbol') @@ -328,16 +470,17 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { textArea.removeEventListener('input', handleInput) textArea.removeEventListener('compositionupdate', handleCompositionUpdate) textArea.removeEventListener('compositionend', handleCompositionEnd) - setSearchTextDebounced.cancel() - setTimeoutTimer( - 'quickpanel_clear_search', - () => { - setSearchText('') - }, - 200 - ) // 等待面板关闭动画结束后,再清空搜索词 } - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ctx.isVisible, ctx.symbol, handleClose, setSearchTextDebounced, triggerSearchChange]) + + useEffect(() => { + if (ctx.isVisible) return + + const timer = setTimeout(() => { + setSearchText('') + }, 200) + + return () => clearTimeout(timer) }, [ctx.isVisible]) useLayoutEffect(() => { @@ -545,19 +688,7 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { const hasSearchText = useMemo(() => searchText.replace(/^[/@]/, '').length > 0, [searchText]) // 折叠仅依据“非固定项”的匹配数;仅剩固定项(如“清除”)时仍视为无匹配,保持折叠 const visibleNonPinnedCount = useMemo(() => list.filter((i) => !i.alwaysVisible).length, [list]) - const collapsed = hasSearchText && visibleNonPinnedCount === 0 - - useEffect(() => { - if (!ctx.isVisible) return - if (!collapsed) return - if (ctx.triggerInfo?.type !== 'input') return - if (ctx.multiple) return - - const trimmedSearch = searchText.replace(/^[/@]/, '').trim() - if (!trimmedSearch) return - - handleClose('no_result') - }, [collapsed, ctx.isVisible, ctx.triggerInfo, ctx.multiple, handleClose, searchText]) + const collapsed = !ctx.manageListExternally && hasSearchText && visibleNonPinnedCount === 0 const estimateSize = useCallback(() => ITEM_HEIGHT, []) @@ -616,7 +747,9 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { return prev ? prev : true }) }> - {!collapsed && ( + {collapsed ? ( + {t('settings.quickPanel.noResult', 'No results')} + ) : ( { } return created + } catch (error) { + logger.error('Error creating default session:', error as Error) + return null } finally { setCreatingSession(false) } diff --git a/src/renderer/src/hooks/useInputText.ts b/src/renderer/src/hooks/useInputText.ts new file mode 100644 index 0000000000..6bcd2f7644 --- /dev/null +++ b/src/renderer/src/hooks/useInputText.ts @@ -0,0 +1,63 @@ +import { useCallback, useRef, useState } from 'react' + +export interface UseInputTextOptions { + initialValue?: string + onChange?: (text: string) => void +} + +export interface UseInputTextReturn { + text: string + setText: (text: string | ((prev: string) => string)) => void + prevText: string + isEmpty: boolean + clear: () => void +} + +/** + * 管理文本输入状态的通用 Hook + * + * 提供文本状态管理、历史追踪和便捷方法 + * + * @param options - 配置选项 + * @param options.initialValue - 初始文本值 + * @param options.onChange - 文本变化回调 + * @returns 文本状态和操作方法 + * + * @example + * ```tsx + * const { text, setText, isEmpty, clear } = useInputText({ + * initialValue: '', + * onChange: (text) => console.log('Text changed:', text) + * }) + * + * setText(e.target.value)} /> + * + * + * ``` + */ +export function useInputText(options: UseInputTextOptions = {}): UseInputTextReturn { + const [text, setText] = useState(options.initialValue ?? '') + const prevTextRef = useRef(text) + + const handleSetText = useCallback( + (value: string | ((prev: string) => string)) => { + const newText = typeof value === 'function' ? value(text) : value + prevTextRef.current = text + setText(newText) + options.onChange?.(newText) + }, + [text, options] + ) + + const clear = useCallback(() => { + handleSetText('') + }, [handleSetText]) + + return { + text, + setText: handleSetText, + prevText: prevTextRef.current, + isEmpty: text.trim().length === 0, + clear + } +} diff --git a/src/renderer/src/hooks/useKeyboardHandler.ts b/src/renderer/src/hooks/useKeyboardHandler.ts new file mode 100644 index 0000000000..c3f8e654a5 --- /dev/null +++ b/src/renderer/src/hooks/useKeyboardHandler.ts @@ -0,0 +1,94 @@ +import { useCallback, useRef } from 'react' + +export interface KeyboardHandlerCallbacks { + onSend?: () => void + onEscape?: () => void + onTab?: () => void + onCustom?: (event: React.KeyboardEvent) => void +} + +export interface KeyboardHandlerOptions { + sendShortcut?: 'Enter' | 'Ctrl+Enter' | 'Cmd+Enter' | 'Shift+Enter' + enableTabNavigation?: boolean + enableEscape?: boolean +} + +/** + * 通用键盘事件处理 Hook + * + * 提供常见的键盘快捷键处理(发送、取消、Tab 导航等) + * + * @param callbacks - 键盘事件回调函数 + * @param callbacks.onSend - 发送消息回调(根据 sendShortcut 触发) + * @param callbacks.onEscape - Escape 键回调 + * @param callbacks.onTab - Tab 键回调 + * @param callbacks.onCustom - 自定义键盘处理回调 + * @param options - 配置选项 + * @param options.sendShortcut - 发送快捷键类型(默认 'Enter') + * @param options.enableTabNavigation - 是否启用 Tab 导航(默认 false) + * @param options.enableEscape - 是否启用 Escape 键处理(默认 false) + * @returns 键盘事件处理函数 + * + * @example + * ```tsx + * const handleKeyDown = useKeyboardHandler( + * { + * onSend: () => sendMessage(), + * onEscape: () => closeModal(), + * onTab: () => navigateToNextField() + * }, + * { + * sendShortcut: 'Ctrl+Enter', + * enableTabNavigation: true, + * enableEscape: true + * } + * ) + * + *