Merge branch 'main' of github.com:CherryHQ/cherry-studio into feat/mcp-runjs

This commit is contained in:
icarus 2025-10-09 00:27:30 +08:00
commit d5ae3e6edc
22 changed files with 204 additions and 49 deletions

View File

@ -125,7 +125,21 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
Optimized note-taking feature, now able to quickly rename by modifying the title What's New in v1.6.3
Fixed issue where CherryAI free model could not be used
Fixed issue where VertexAI proxy address could not be called normally Features:
Fixed issue where built-in tools from service providers could not be called normally - Notes: Add spell-check control, automatic table line wrapping, export functionality, and LLM-based renaming
- UI: Expand topic rename clickable area, add middle-click tab closing, remove redundant scrollbars, fix message menubar overflow
- Editor: Add read-only extension support, make TextFilePreview read-only but copyable
- Models: Update support for DeepSeek v3.2, Claude 4.5, GLM 4.6, Gemini regex, and vision models
- Code Tools: Add GitHub Copilot CLI integration
Bug Fixes:
- Fix migration for missing providers
- Fix forked topic retaining old name after rename
- Restore first token latency reporting in metrics
- Fix UI scrollbar and overflow issues
Technical Updates:
- Upgrade to Electron 37.6.0
- Update dependencies across packages

View File

@ -1,6 +1,6 @@
{ {
"name": "CherryStudio", "name": "CherryStudio",
"version": "1.6.2", "version": "1.6.3",
"private": true, "private": true,
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",

View File

@ -48,7 +48,8 @@ const RichEditor = ({
enableContentSearch = false, enableContentSearch = false,
isFullWidth = false, isFullWidth = false,
fontFamily = 'default', fontFamily = 'default',
fontSize = 16 fontSize = 16,
enableSpellCheck = false
// toolbarItems: _toolbarItems // TODO: Implement custom toolbar items // toolbarItems: _toolbarItems // TODO: Implement custom toolbar items
}: RichEditorProps & { ref?: React.RefObject<RichEditorRef | null> }) => { }: RichEditorProps & { ref?: React.RefObject<RichEditorRef | null> }) => {
// Use the rich editor hook for complete editor management // Use the rich editor hook for complete editor management
@ -71,6 +72,7 @@ const RichEditor = ({
onBlur, onBlur,
placeholder, placeholder,
editable, editable,
enableSpellCheck,
scrollParent: () => scrollContainerRef.current, scrollParent: () => scrollContainerRef.current,
onShowTableActionMenu: ({ position, actions }) => { onShowTableActionMenu: ({ position, actions }) => {
const iconMap: Record<string, React.ReactNode> = { const iconMap: Record<string, React.ReactNode> = {

View File

@ -50,6 +50,8 @@ export interface RichEditorProps {
fontFamily?: 'default' | 'serif' fontFamily?: 'default' | 'serif'
/** Font size in pixels */ /** Font size in pixels */
fontSize?: number fontSize?: number
/** Whether to enable spell check */
enableSpellCheck?: boolean
} }
export interface ToolbarItem { export interface ToolbarItem {

View File

@ -57,6 +57,8 @@ export interface UseRichEditorOptions {
editable?: boolean editable?: boolean
/** Whether to enable table of contents functionality */ /** Whether to enable table of contents functionality */
enableTableOfContents?: boolean enableTableOfContents?: boolean
/** Whether to enable spell check */
enableSpellCheck?: boolean
/** Show table action menu (row/column) with concrete actions and position */ /** Show table action menu (row/column) with concrete actions and position */
onShowTableActionMenu?: (payload: { onShowTableActionMenu?: (payload: {
type: 'row' | 'column' type: 'row' | 'column'
@ -126,6 +128,7 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor
previewLength = 50, previewLength = 50,
placeholder = '', placeholder = '',
editable = true, editable = true,
enableSpellCheck = false,
onShowTableActionMenu, onShowTableActionMenu,
scrollParent scrollParent
} = options } = options
@ -410,7 +413,9 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor
// Allow text selection even when not editable // Allow text selection even when not editable
style: editable style: editable
? '' ? ''
: 'user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;' : 'user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;',
// Set spellcheck attribute on the contenteditable element
spellcheck: enableSpellCheck ? 'true' : 'false'
} }
}, },
onUpdate: ({ editor }) => { onUpdate: ({ editor }) => {

View File

@ -1784,6 +1784,8 @@
"sort_updated_asc": "Update time (oldest first)", "sort_updated_asc": "Update time (oldest first)",
"sort_updated_desc": "Update time (newest first)", "sort_updated_desc": "Update time (newest first)",
"sort_z2a": "File name (Z-A)", "sort_z2a": "File name (Z-A)",
"spell_check": "Spell Check",
"spell_check_tooltip": "Enable/Disable spell check",
"star": "Favorite note", "star": "Favorite note",
"starred_notes": "Collected notes", "starred_notes": "Collected notes",
"title": "Notes", "title": "Notes",

View File

@ -1784,6 +1784,8 @@
"sort_updated_asc": "更新时间(从旧到新)", "sort_updated_asc": "更新时间(从旧到新)",
"sort_updated_desc": "更新时间(从新到旧)", "sort_updated_desc": "更新时间(从新到旧)",
"sort_z2a": "文件名Z-A", "sort_z2a": "文件名Z-A",
"spell_check": "拼写检查",
"spell_check_tooltip": "启用/禁用拼写检查",
"star": "收藏笔记", "star": "收藏笔记",
"starred_notes": "收藏的笔记", "starred_notes": "收藏的笔记",
"title": "笔记", "title": "笔记",

View File

@ -1784,6 +1784,8 @@
"sort_updated_asc": "更新時間(從舊到新)", "sort_updated_asc": "更新時間(從舊到新)",
"sort_updated_desc": "更新時間(從新到舊)", "sort_updated_desc": "更新時間(從新到舊)",
"sort_z2a": "文件名Z-A", "sort_z2a": "文件名Z-A",
"spell_check": "拼寫檢查",
"spell_check_tooltip": "啟用/禁用拼寫檢查",
"star": "收藏筆記", "star": "收藏筆記",
"starred_notes": "收藏的筆記", "starred_notes": "收藏的筆記",
"title": "筆記", "title": "筆記",

View File

@ -334,7 +334,7 @@
"new_topic": "Νέο θέμα {{Command}}", "new_topic": "Νέο θέμα {{Command}}",
"pause": "Παύση", "pause": "Παύση",
"placeholder": "Εισάγετε μήνυμα εδώ...", "placeholder": "Εισάγετε μήνυμα εδώ...",
"placeholder_without_triggers": "[to be translated]:在这里输入消息,按 {{key}} 发送", "placeholder_without_triggers": "Εδώ εισαγάγετε το μήνυμα, πατήστε {{key}} για αποστολή",
"send": "Αποστολή", "send": "Αποστολή",
"settings": "Ρυθμίσεις", "settings": "Ρυθμίσεις",
"thinking": { "thinking": {
@ -1698,10 +1698,10 @@
}, },
"notes": { "notes": {
"auto_rename": { "auto_rename": {
"empty_note": "[to be translated]:笔记为空,无法生成名称", "empty_note": "Το σημείωμα είναι κενό, δεν μπορεί να δημιουργηθεί όνομα",
"failed": "[to be translated]:生成笔记名称失败", "failed": "Αποτυχία δημιουργίας ονόματος σημείωσης",
"label": "[to be translated]:生成笔记名称", "label": "Δημιουργία ονόματος σημείωσης",
"success": "[to be translated]:笔记名称生成成功" "success": "Η δημιουργία του ονόματος σημειώσεων ολοκληρώθηκε με επιτυχία"
}, },
"characters": "χαρακτήρας", "characters": "χαρακτήρας",
"collapse": "σύμπτυξη", "collapse": "σύμπτυξη",
@ -1784,6 +1784,8 @@
"sort_updated_asc": "χρόνος ενημέρωσης (από παλιά στα νέα)", "sort_updated_asc": "χρόνος ενημέρωσης (από παλιά στα νέα)",
"sort_updated_desc": "χρόνος ενημέρωσης (από νεώτερο σε παλαιότερο)", "sort_updated_desc": "χρόνος ενημέρωσης (από νεώτερο σε παλαιότερο)",
"sort_z2a": "όνομα αρχείου (Z-A)", "sort_z2a": "όνομα αρχείου (Z-A)",
"spell_check": "Έλεγχος ορθογραφίας",
"spell_check_tooltip": "Ενεργοποίηση/Απενεργοποίηση ελέγχου ορθογραφίας",
"star": "Αγαπημένες σημειώσεις", "star": "Αγαπημένες σημειώσεις",
"starred_notes": "Σημειώσεις συλλογής", "starred_notes": "Σημειώσεις συλλογής",
"title": "σημειώσεις", "title": "σημειώσεις",
@ -3346,7 +3348,6 @@
"dify_knowledge": "Η υλοποίηση του Dify για τον διακομιστή MCP, παρέχει μια απλή API για να αλληλεπιδρά με το Dify. Απαιτείται η ρύθμιση του κλειδιού Dify", "dify_knowledge": "Η υλοποίηση του Dify για τον διακομιστή MCP, παρέχει μια απλή API για να αλληλεπιδρά με το Dify. Απαιτείται η ρύθμιση του κλειδιού Dify",
"fetch": "Εξυπηρετητής MCP για λήψη περιεχομένου ιστοσελίδας URL", "fetch": "Εξυπηρετητής MCP για λήψη περιεχομένου ιστοσελίδας URL",
"filesystem": "Εξυπηρετητής Node.js για το πρωτόκολλο περιβάλλοντος μοντέλου (MCP) που εφαρμόζει λειτουργίες συστήματος αρχείων. Απαιτείται διαμόρφωση για την επιτροπή πρόσβασης σε καταλόγους", "filesystem": "Εξυπηρετητής Node.js για το πρωτόκολλο περιβάλλοντος μοντέλου (MCP) που εφαρμόζει λειτουργίες συστήματος αρχείων. Απαιτείται διαμόρφωση για την επιτροπή πρόσβασης σε καταλόγους",
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript支持大多数标准库和流行的第三方库",
"mcp_auto_install": "Αυτόματη εγκατάσταση υπηρεσίας MCP (προβολή)", "mcp_auto_install": "Αυτόματη εγκατάσταση υπηρεσίας MCP (προβολή)",
"memory": "Βασική υλοποίηση μόνιμης μνήμης με βάση τοπικό γράφημα γνώσης. Αυτό επιτρέπει στο μοντέλο να θυμάται πληροφορίες σχετικές με τον χρήστη ανάμεσα σε διαφορετικές συνομιλίες. Απαιτείται η ρύθμιση της μεταβλητής περιβάλλοντος MEMORY_FILE_PATH.", "memory": "Βασική υλοποίηση μόνιμης μνήμης με βάση τοπικό γράφημα γνώσης. Αυτό επιτρέπει στο μοντέλο να θυμάται πληροφορίες σχετικές με τον χρήστη ανάμεσα σε διαφορετικές συνομιλίες. Απαιτείται η ρύθμιση της μεταβλητής περιβάλλοντος MEMORY_FILE_PATH.",
"no": "Χωρίς περιγραφή", "no": "Χωρίς περιγραφή",

View File

@ -334,7 +334,7 @@
"new_topic": "Nuevo tema {{Command}}", "new_topic": "Nuevo tema {{Command}}",
"pause": "Pausar", "pause": "Pausar",
"placeholder": "Escribe aquí tu mensaje...", "placeholder": "Escribe aquí tu mensaje...",
"placeholder_without_triggers": "[to be translated]:在这里输入消息,按 {{key}} 发送", "placeholder_without_triggers": "Escriba un mensaje aquí y presione {{key}} para enviar",
"send": "Enviar", "send": "Enviar",
"settings": "Configuración", "settings": "Configuración",
"thinking": { "thinking": {
@ -1698,10 +1698,10 @@
}, },
"notes": { "notes": {
"auto_rename": { "auto_rename": {
"empty_note": "[to be translated]:笔记为空,无法生成名称", "empty_note": "La nota está vacía, no se puede generar un nombre",
"failed": "[to be translated]:生成笔记名称失败", "failed": "Error al generar el nombre de la nota",
"label": "[to be translated]:生成笔记名称", "label": "Generar nombre de nota",
"success": "[to be translated]:笔记名称生成成功" "success": "Se ha generado correctamente el nombre de la nota"
}, },
"characters": "carácter", "characters": "carácter",
"collapse": "ocultar", "collapse": "ocultar",
@ -1784,6 +1784,8 @@
"sort_updated_asc": "Fecha de actualización (de más antigua a más reciente)", "sort_updated_asc": "Fecha de actualización (de más antigua a más reciente)",
"sort_updated_desc": "Fecha de actualización (de más nuevo a más antiguo)", "sort_updated_desc": "Fecha de actualización (de más nuevo a más antiguo)",
"sort_z2a": "Nombre de archivo (Z-A)", "sort_z2a": "Nombre de archivo (Z-A)",
"spell_check": "comprobación ortográfica",
"spell_check_tooltip": "Habilitar/deshabilitar revisión ortográfica",
"star": "Notas guardadas", "star": "Notas guardadas",
"starred_notes": "notas guardadas", "starred_notes": "notas guardadas",
"title": "notas", "title": "notas",
@ -3346,7 +3348,6 @@
"dify_knowledge": "Implementación del servidor MCP de Dify, que proporciona una API sencilla para interactuar con Dify. Se requiere configurar la clave de Dify.", "dify_knowledge": "Implementación del servidor MCP de Dify, que proporciona una API sencilla para interactuar con Dify. Se requiere configurar la clave de Dify.",
"fetch": "Servidor MCP para obtener el contenido de la página web de una URL", "fetch": "Servidor MCP para obtener el contenido de la página web de una URL",
"filesystem": "Servidor Node.js que implementa el protocolo de contexto del modelo (MCP) para operaciones del sistema de archivos. Requiere configuración del directorio permitido para el acceso", "filesystem": "Servidor Node.js que implementa el protocolo de contexto del modelo (MCP) para operaciones del sistema de archivos. Requiere configuración del directorio permitido para el acceso",
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript支持大多数标准库和流行的第三方库",
"mcp_auto_install": "Instalación automática del servicio MCP (versión beta)", "mcp_auto_install": "Instalación automática del servicio MCP (versión beta)",
"memory": "Implementación básica de memoria persistente basada en un grafo de conocimiento local. Esto permite que el modelo recuerde información relevante del usuario entre diferentes conversaciones. Es necesario configurar la variable de entorno MEMORY_FILE_PATH.", "memory": "Implementación básica de memoria persistente basada en un grafo de conocimiento local. Esto permite que el modelo recuerde información relevante del usuario entre diferentes conversaciones. Es necesario configurar la variable de entorno MEMORY_FILE_PATH.",
"no": "sin descripción", "no": "sin descripción",

View File

@ -334,7 +334,7 @@
"new_topic": "Nouveau sujet {{Command}}", "new_topic": "Nouveau sujet {{Command}}",
"pause": "Pause", "pause": "Pause",
"placeholder": "Entrez votre message ici...", "placeholder": "Entrez votre message ici...",
"placeholder_without_triggers": "[to be translated]:在这里输入消息,按 {{key}} 发送", "placeholder_without_triggers": "Entrez votre message ici, appuyez sur {{key}} pour envoyer",
"send": "Envoyer", "send": "Envoyer",
"settings": "Paramètres", "settings": "Paramètres",
"thinking": { "thinking": {
@ -1698,10 +1698,10 @@
}, },
"notes": { "notes": {
"auto_rename": { "auto_rename": {
"empty_note": "[to be translated]:笔记为空,无法生成名称", "empty_note": "La note est vide, impossible de générer un nom",
"failed": "[to be translated]:生成笔记名称失败", "failed": "Échec de la génération du nom de note",
"label": "[to be translated]:生成笔记名称", "label": "Générer un nom de note",
"success": "[to be translated]:笔记名称生成成功" "success": "La génération du nom de note a réussi"
}, },
"characters": "caractère", "characters": "caractère",
"collapse": "réduire", "collapse": "réduire",
@ -1784,6 +1784,8 @@
"sort_updated_asc": "Heure de mise à jour (du plus ancien au plus récent)", "sort_updated_asc": "Heure de mise à jour (du plus ancien au plus récent)",
"sort_updated_desc": "Date de mise à jour (du plus récent au plus ancien)", "sort_updated_desc": "Date de mise à jour (du plus récent au plus ancien)",
"sort_z2a": "Nom de fichier (Z-A)", "sort_z2a": "Nom de fichier (Z-A)",
"spell_check": "Vérification orthographique",
"spell_check_tooltip": "Activer/Désactiver la vérification orthographique",
"star": "Notes enregistrées", "star": "Notes enregistrées",
"starred_notes": "notes de collection", "starred_notes": "notes de collection",
"title": "notes", "title": "notes",
@ -3346,7 +3348,6 @@
"dify_knowledge": "Implémentation du serveur MCP de Dify, fournissant une API simple pour interagir avec Dify. Nécessite la configuration de la clé Dify", "dify_knowledge": "Implémentation du serveur MCP de Dify, fournissant une API simple pour interagir avec Dify. Nécessite la configuration de la clé Dify",
"fetch": "serveur MCP utilisé pour récupérer le contenu des pages web URL", "fetch": "serveur MCP utilisé pour récupérer le contenu des pages web URL",
"filesystem": "Serveur Node.js implémentant le protocole de contexte de modèle (MCP) pour les opérations de système de fichiers. Nécessite une configuration des répertoires autorisés à être accédés.", "filesystem": "Serveur Node.js implémentant le protocole de contexte de modèle (MCP) pour les opérations de système de fichiers. Nécessite une configuration des répertoires autorisés à être accédés.",
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript支持大多数标准库和流行的第三方库",
"mcp_auto_install": "Installation automatique du service MCP (version bêta)", "mcp_auto_install": "Installation automatique du service MCP (version bêta)",
"memory": "Implémentation de base de mémoire persistante basée sur un graphe de connaissances local. Cela permet au modèle de se souvenir des informations relatives à l'utilisateur entre différentes conversations. Nécessite la configuration de la variable d'environnement MEMORY_FILE_PATH.", "memory": "Implémentation de base de mémoire persistante basée sur un graphe de connaissances local. Cela permet au modèle de se souvenir des informations relatives à l'utilisateur entre différentes conversations. Nécessite la configuration de la variable d'environnement MEMORY_FILE_PATH.",
"no": "sans description", "no": "sans description",

View File

@ -334,7 +334,7 @@
"new_topic": "新しいトピック {{Command}}", "new_topic": "新しいトピック {{Command}}",
"pause": "一時停止", "pause": "一時停止",
"placeholder": "ここにメッセージを入力し、{{key}} を押して送信...", "placeholder": "ここにメッセージを入力し、{{key}} を押して送信...",
"placeholder_without_triggers": "[to be translated]:在这里输入消息,按 {{key}} 发送", "placeholder_without_triggers": "ここにメッセージを入力し、{{key}} を押して送信してください",
"send": "送信", "send": "送信",
"settings": "設定", "settings": "設定",
"thinking": { "thinking": {
@ -1698,10 +1698,10 @@
}, },
"notes": { "notes": {
"auto_rename": { "auto_rename": {
"empty_note": "[to be translated]:笔记为空,无法生成名称", "empty_note": "ノートが空です。名前を生成できません。",
"failed": "[to be translated]:生成笔记名称失败", "failed": "ノート名の生成に失敗しました",
"label": "[to be translated]:生成笔记名称", "label": "ノート名の生成",
"success": "[to be translated]:笔记名称生成成功" "success": "ノート名の生成に成功しました"
}, },
"characters": "文字", "characters": "文字",
"collapse": "閉じる", "collapse": "閉じる",
@ -1784,6 +1784,8 @@
"sort_updated_asc": "更新日時(古い順)", "sort_updated_asc": "更新日時(古い順)",
"sort_updated_desc": "更新日時(新しい順)", "sort_updated_desc": "更新日時(新しい順)",
"sort_z2a": "ファイル名Z-A", "sort_z2a": "ファイル名Z-A",
"spell_check": "スペルチェック",
"spell_check_tooltip": "スペルチェックの有効/無効",
"star": "お気に入りのノート", "star": "お気に入りのノート",
"starred_notes": "収集したノート", "starred_notes": "収集したノート",
"title": "ノート", "title": "ノート",
@ -3346,7 +3348,6 @@
"dify_knowledge": "DifyのMCPサーバー実装は、Difyと対話するためのシンプルなAPIを提供します。Dify Keyの設定が必要です。", "dify_knowledge": "DifyのMCPサーバー実装は、Difyと対話するためのシンプルなAPIを提供します。Dify Keyの設定が必要です。",
"fetch": "URLのウェブページコンテンツを取得するためのMCPサーバー", "fetch": "URLのウェブページコンテンツを取得するためのMCPサーバー",
"filesystem": "Node.jsサーバーによるファイルシステム操作を実現するモデルコンテキストプロトコルMCP。アクセスを許可するディレクトリの設定が必要です", "filesystem": "Node.jsサーバーによるファイルシステム操作を実現するモデルコンテキストプロトコルMCP。アクセスを許可するディレクトリの設定が必要です",
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript支持大多数标准库和流行的第三方库",
"mcp_auto_install": "MCPサービスの自動インストールベータ版", "mcp_auto_install": "MCPサービスの自動インストールベータ版",
"memory": "ローカルのナレッジグラフに基づく永続的なメモリの基本的な実装です。これにより、モデルは異なる会話間でユーザーの関連情報を記憶できるようになります。MEMORY_FILE_PATH 環境変数の設定が必要です。", "memory": "ローカルのナレッジグラフに基づく永続的なメモリの基本的な実装です。これにより、モデルは異なる会話間でユーザーの関連情報を記憶できるようになります。MEMORY_FILE_PATH 環境変数の設定が必要です。",
"no": "説明なし", "no": "説明なし",

View File

@ -334,7 +334,7 @@
"new_topic": "Novo tópico {{Command}}", "new_topic": "Novo tópico {{Command}}",
"pause": "Pausar", "pause": "Pausar",
"placeholder": "Digite sua mensagem aqui...", "placeholder": "Digite sua mensagem aqui...",
"placeholder_without_triggers": "[to be translated]:在这里输入消息,按 {{key}} 发送", "placeholder_without_triggers": "Digite a mensagem aqui, pressione {{key}} para enviar",
"send": "Enviar", "send": "Enviar",
"settings": "Configurações", "settings": "Configurações",
"thinking": { "thinking": {
@ -1698,10 +1698,10 @@
}, },
"notes": { "notes": {
"auto_rename": { "auto_rename": {
"empty_note": "[to be translated]:笔记为空,无法生成名称", "empty_note": "A nota está vazia, não é possível gerar um nome",
"failed": "[to be translated]:生成笔记名称失败", "failed": "Falha ao gerar o nome da nota",
"label": "[to be translated]:生成笔记名称", "label": "Gerar nome da nota",
"success": "[to be translated]:笔记名称生成成功" "success": "Nome da nota gerado com sucesso"
}, },
"characters": "caractere", "characters": "caractere",
"collapse": "[minimizar]", "collapse": "[minimizar]",
@ -1784,6 +1784,8 @@
"sort_updated_asc": "Tempo de atualização (do mais antigo para o mais recente)", "sort_updated_asc": "Tempo de atualização (do mais antigo para o mais recente)",
"sort_updated_desc": "atualização de tempo (do mais novo para o mais antigo)", "sort_updated_desc": "atualização de tempo (do mais novo para o mais antigo)",
"sort_z2a": "Nome do arquivo (Z-A)", "sort_z2a": "Nome do arquivo (Z-A)",
"spell_check": "verificação ortográfica",
"spell_check_tooltip": "Ativar/Desativar verificação ortográfica",
"star": "Notas favoritas", "star": "Notas favoritas",
"starred_notes": "notas salvas", "starred_notes": "notas salvas",
"title": "nota", "title": "nota",
@ -3346,7 +3348,6 @@
"dify_knowledge": "Implementação do servidor MCP do Dify, que fornece uma API simples para interagir com o Dify. Requer a configuração da chave Dify", "dify_knowledge": "Implementação do servidor MCP do Dify, que fornece uma API simples para interagir com o Dify. Requer a configuração da chave Dify",
"fetch": "servidor MCP para obter o conteúdo da página web do URL", "fetch": "servidor MCP para obter o conteúdo da página web do URL",
"filesystem": "Servidor Node.js do protocolo de contexto de modelo (MCP) para implementar operações de sistema de ficheiros. Requer configuração do diretório permitido para acesso", "filesystem": "Servidor Node.js do protocolo de contexto de modelo (MCP) para implementar operações de sistema de ficheiros. Requer configuração do diretório permitido para acesso",
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript支持大多数标准库和流行的第三方库",
"mcp_auto_install": "Instalação automática do serviço MCP (beta)", "mcp_auto_install": "Instalação automática do serviço MCP (beta)",
"memory": "Implementação base de memória persistente baseada em grafos de conhecimento locais. Isso permite que o modelo lembre informações relevantes do utilizador entre diferentes conversas. É necessário configurar a variável de ambiente MEMORY_FILE_PATH.", "memory": "Implementação base de memória persistente baseada em grafos de conhecimento locais. Isso permite que o modelo lembre informações relevantes do utilizador entre diferentes conversas. É necessário configurar a variável de ambiente MEMORY_FILE_PATH.",
"no": "sem descrição", "no": "sem descrição",

View File

@ -334,7 +334,7 @@
"new_topic": "Новый топик {{Command}}", "new_topic": "Новый топик {{Command}}",
"pause": "Остановить", "pause": "Остановить",
"placeholder": "Введите ваше сообщение здесь, нажмите {{key}} для отправки...", "placeholder": "Введите ваше сообщение здесь, нажмите {{key}} для отправки...",
"placeholder_without_triggers": "[to be translated]:在这里输入消息,按 {{key}} 发送", "placeholder_without_triggers": "Введите сообщение здесь, нажмите {{key}}, чтобы отправить",
"send": "Отправить", "send": "Отправить",
"settings": "Настройки", "settings": "Настройки",
"thinking": { "thinking": {
@ -1698,10 +1698,10 @@
}, },
"notes": { "notes": {
"auto_rename": { "auto_rename": {
"empty_note": "[to be translated]:笔记为空,无法生成名称", "empty_note": "Заметки пусты, имя невозможно сгенерировать",
"failed": "[to be translated]:生成笔记名称失败", "failed": "Создание названия заметки не удалось",
"label": "[to be translated]:生成笔记名称", "label": "Создать название заметки",
"success": "[to be translated]:笔记名称生成成功" "success": "Имя заметки успешно создано"
}, },
"characters": "Символы", "characters": "Символы",
"collapse": "Свернуть", "collapse": "Свернуть",
@ -1784,6 +1784,8 @@
"sort_updated_asc": "Время обновления (от старого к новому)", "sort_updated_asc": "Время обновления (от старого к новому)",
"sort_updated_desc": "Время обновления (от нового к старому)", "sort_updated_desc": "Время обновления (от нового к старому)",
"sort_z2a": "Имя файла (Я-А)", "sort_z2a": "Имя файла (Я-А)",
"spell_check": "Проверка орфографии",
"spell_check_tooltip": "Включить/отключить проверку орфографии",
"star": "Избранные заметки", "star": "Избранные заметки",
"starred_notes": "Сохраненные заметки", "starred_notes": "Сохраненные заметки",
"title": "заметки", "title": "заметки",
@ -3346,7 +3348,6 @@
"dify_knowledge": "Реализация сервера MCP Dify, предоставляющая простой API для взаимодействия с Dify. Требуется настройка ключа Dify", "dify_knowledge": "Реализация сервера MCP Dify, предоставляющая простой API для взаимодействия с Dify. Требуется настройка ключа Dify",
"fetch": "MCP-сервер для получения содержимого веб-страниц по URL", "fetch": "MCP-сервер для получения содержимого веб-страниц по URL",
"filesystem": "Node.js-сервер протокола контекста модели (MCP) для реализации операций файловой системы. Требуется настройка каталогов, к которым разрешён доступ", "filesystem": "Node.js-сервер протокола контекста модели (MCP) для реализации операций файловой системы. Требуется настройка каталогов, к которым разрешён доступ",
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript支持大多数标准库和流行的第三方库",
"mcp_auto_install": "Автоматическая установка службы MCP (бета-версия)", "mcp_auto_install": "Автоматическая установка службы MCP (бета-версия)",
"memory": "реализация постоянной памяти на основе локального графа знаний. Это позволяет модели запоминать информацию о пользователе между различными диалогами. Требуется настроить переменную среды MEMORY_FILE_PATH.", "memory": "реализация постоянной памяти на основе локального графа знаний. Это позволяет модели запоминать информацию о пользователе между различными диалогами. Требуется настроить переменную среды MEMORY_FILE_PATH.",
"no": "без описания", "no": "без описания",

View File

@ -231,7 +231,7 @@ const MessageItem: FC<Props> = ({
<HorizontalScrollContainer <HorizontalScrollContainer
classNames={{ classNames={{
content: cn( content: cn(
'items-center', 'flex-1 items-center justify-between',
isLastMessage && messageStyle === 'plain' ? 'flex-row-reverse' : 'flex-row' isLastMessage && messageStyle === 'plain' ? 'flex-row-reverse' : 'flex-row'
) )
}}> }}>

View File

@ -386,6 +386,7 @@ const Container = styled.div`
border-radius: var(--list-item-border-radius); border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent; border: 0.5px solid transparent;
width: calc(var(--assistants-width) - 20px); width: calc(var(--assistants-width) - 20px);
cursor: pointer;
&:hover { &:hover {
background-color: var(--color-list-item-hover); background-color: var(--color-list-item-hover);

View File

@ -1,11 +1,16 @@
import ActionIconButton from '@renderer/components/Buttons/ActionIconButton'
import CodeEditor from '@renderer/components/CodeEditor' import CodeEditor from '@renderer/components/CodeEditor'
import { HSpaceBetweenStack } from '@renderer/components/Layout' import { HSpaceBetweenStack } from '@renderer/components/Layout'
import RichEditor from '@renderer/components/RichEditor' import RichEditor from '@renderer/components/RichEditor'
import { RichEditorRef } from '@renderer/components/RichEditor/types' import { RichEditorRef } from '@renderer/components/RichEditor/types'
import Selector from '@renderer/components/Selector' import Selector from '@renderer/components/Selector'
import { useNotesSettings } from '@renderer/hooks/useNotesSettings' import { useNotesSettings } from '@renderer/hooks/useNotesSettings'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { setEnableSpellCheck } from '@renderer/store/settings'
import { EditorView } from '@renderer/types' import { EditorView } from '@renderer/types'
import { Empty } from 'antd' import { Empty, Tooltip } from 'antd'
import { SpellCheck } from 'lucide-react'
import { FC, memo, RefObject, useCallback, useMemo, useState } from 'react' import { FC, memo, RefObject, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -21,7 +26,9 @@ interface NotesEditorProps {
const NotesEditor: FC<NotesEditorProps> = memo( const NotesEditor: FC<NotesEditorProps> = memo(
({ activeNodeId, currentContent, tokenCount, onMarkdownChange, editorRef }) => { ({ activeNodeId, currentContent, tokenCount, onMarkdownChange, editorRef }) => {
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useAppDispatch()
const { settings } = useNotesSettings() const { settings } = useNotesSettings()
const { enableSpellCheck } = useSettings()
const currentViewMode = useMemo(() => { const currentViewMode = useMemo(() => {
if (settings.defaultViewMode === 'edit') { if (settings.defaultViewMode === 'edit') {
return settings.defaultEditMode return settings.defaultEditMode
@ -78,6 +85,7 @@ const NotesEditor: FC<NotesEditorProps> = memo(
isFullWidth={settings.isFullWidth} isFullWidth={settings.isFullWidth}
fontFamily={settings.fontFamily} fontFamily={settings.fontFamily}
fontSize={settings.fontSize} fontSize={settings.fontSize}
enableSpellCheck={enableSpellCheck}
/> />
)} )}
</RichEditorContainer> </RichEditorContainer>
@ -92,8 +100,21 @@ const NotesEditor: FC<NotesEditorProps> = memo(
color: 'var(--color-text-3)', color: 'var(--color-text-3)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 8 gap: 12
}}> }}>
{tmpViewMode === 'preview' && (
<Tooltip placement="top" title={t('notes.spell_check_tooltip')} mouseLeaveDelay={0} arrow>
<ActionIconButton
active={enableSpellCheck}
onClick={() => {
const newValue = !enableSpellCheck
dispatch(setEnableSpellCheck(newValue))
window.api.setEnableSpellCheck(newValue)
}}>
<SpellCheck size={18} />
</ActionIconButton>
</Tooltip>
)}
<Selector <Selector
value={tmpViewMode as EditorView} value={tmpViewMode as EditorView}
onChange={(value: EditorView) => setTmpViewMode(value)} onChange={(value: EditorView) => setTmpViewMode(value)}

View File

@ -359,6 +359,23 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
[bases.length, t] [bases.length, t]
) )
const handleImageAction = useCallback(
async (node: NotesTreeNode, platform: 'copyImage' | 'exportImage') => {
try {
if (activeNode?.id !== node.id) {
onSelectNode(node)
await new Promise((resolve) => setTimeout(resolve, 500))
}
await exportNote({ node, platform })
} catch (error) {
logger.error(`Failed to ${platform === 'copyImage' ? 'copy' : 'export'} as image:`, error as Error)
window.toast.error(t('common.copy_failed'))
}
},
[activeNode, onSelectNode, t]
)
const handleAutoRename = useCallback( const handleAutoRename = useCallback(
async (note: NotesTreeNode) => { async (note: NotesTreeNode) => {
if (note.type !== 'file') return if (note.type !== 'file') return
@ -612,6 +629,16 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
key: 'export', key: 'export',
icon: <UploadIcon size={14} />, icon: <UploadIcon size={14} />,
children: [ children: [
exportMenuOptions.image && {
label: t('chat.topics.copy.image'),
key: 'copy-image',
onClick: () => handleImageAction(node, 'copyImage')
},
exportMenuOptions.image && {
label: t('chat.topics.export.image'),
key: 'export-image',
onClick: () => handleImageAction(node, 'exportImage')
},
exportMenuOptions.markdown && { exportMenuOptions.markdown && {
label: t('chat.topics.export.md.label'), label: t('chat.topics.export.md.label'),
key: 'markdown', key: 'markdown',
@ -671,6 +698,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
handleStartEdit, handleStartEdit,
onToggleStar, onToggleStar,
handleExportKnowledge, handleExportKnowledge,
handleImageAction,
handleDeleteNode, handleDeleteNode,
renamingNodeIds, renamingNodeIds,
handleAutoRename, handleAutoRename,
@ -1129,7 +1157,7 @@ const DragOverIndicator = styled.div`
` `
const DropHintNode = styled.div` const DropHintNode = styled.div`
margin: 8px; margin: 6px 0;
margin-bottom: 20px; margin-bottom: 20px;
${TreeNodeContainer} { ${TreeNodeContainer} {

View File

@ -89,6 +89,7 @@ export async function restore() {
} }
await handleData(data) await handleData(data)
notificationService.send({ notificationService.send({
id: uuid(), id: uuid(),
type: 'success', type: 'success',
@ -850,6 +851,12 @@ export async function handleData(data: Record<string, any>) {
if (data.version >= 2) { if (data.version >= 2) {
localStorage.setItem('persist:cherry-studio', data.localStorage['persist:cherry-studio']) localStorage.setItem('persist:cherry-studio', data.localStorage['persist:cherry-studio'])
// remove notes_tree from indexedDB
if (data.indexedDB['notes_tree']) {
delete data.indexedDB['notes_tree']
}
await restoreDatabase(data.indexedDB) await restoreDatabase(data.indexedDB)
if (data.version === 3) { if (data.version === 3) {

View File

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

View File

@ -85,6 +85,15 @@ function addProvider(state: RootState, id: string) {
} }
} }
// Fix missing provider
function fixMissingProvider(state: RootState) {
SYSTEM_PROVIDERS.forEach((p) => {
if (!state.llm.providers.find((provider) => provider.id === p.id)) {
state.llm.providers.push(p)
}
})
}
// add ocr provider // add ocr provider
function addOcrProvider(state: RootState, provider: BuiltinOcrProvider) { function addOcrProvider(state: RootState, provider: BuiltinOcrProvider) {
if (!state.ocr.providers.find((p) => p.id === provider.id)) { if (!state.ocr.providers.find((p) => p.id === provider.id)) {
@ -2553,6 +2562,7 @@ const migrateConfig = {
'159': (state: RootState) => { '159': (state: RootState) => {
try { try {
addProvider(state, 'ovms') addProvider(state, 'ovms')
fixMissingProvider(state)
return state return state
} catch (error) { } catch (error) {
logger.error('migrate 159 error', error as Error) logger.error('migrate 159 error', error as Error)

View File

@ -9,6 +9,7 @@ import { setExportState } from '@renderer/store/runtime'
import type { Topic } from '@renderer/types' import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage' import type { Message } from '@renderer/types/newMessage'
import { removeSpecialCharactersForFileName } from '@renderer/utils/file' import { removeSpecialCharactersForFileName } from '@renderer/utils/file'
import { captureScrollableAsBlob, captureScrollableAsDataURL } from '@renderer/utils/image'
import { convertMathFormula, markdownToPlainText } from '@renderer/utils/markdown' import { convertMathFormula, markdownToPlainText } from '@renderer/utils/markdown'
import { getCitationContent, getMainTextContent, getThinkingContent } from '@renderer/utils/messageUtils/find' import { getCitationContent, getMainTextContent, getThinkingContent } from '@renderer/utils/messageUtils/find'
import { markdownToBlocks } from '@tryfabric/martian' import { markdownToBlocks } from '@tryfabric/martian'
@ -1092,9 +1093,57 @@ const exportNoteAsMarkdown = async (noteName: string, content: string): Promise<
} }
} }
const getScrollableElement = (): HTMLElement | null => {
const notesPage = document.querySelector('#notes-page')
if (!notesPage) return null
const allDivs = notesPage.querySelectorAll('div')
for (const div of Array.from(allDivs)) {
const style = window.getComputedStyle(div)
if (style.overflowY === 'auto' || style.overflowY === 'scroll') {
if (div.querySelector('.ProseMirror')) {
return div as HTMLElement
}
}
}
return null
}
const getScrollableRef = (): { current: HTMLElement } | null => {
const element = getScrollableElement()
if (!element) {
window.toast.warning(i18n.t('notes.no_content_to_copy'))
return null
}
return { current: element }
}
const exportNoteAsImageToClipboard = async (): Promise<void> => {
const scrollableRef = getScrollableRef()
if (!scrollableRef) return
await captureScrollableAsBlob(scrollableRef, async (blob) => {
if (blob) {
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
window.toast.success(i18n.t('common.copied'))
}
})
}
const exportNoteAsImageFile = async (noteName: string): Promise<void> => {
const scrollableRef = getScrollableRef()
if (!scrollableRef) return
const dataUrl = await captureScrollableAsDataURL(scrollableRef)
if (dataUrl) {
const fileName = removeSpecialCharactersForFileName(noteName)
await window.api.file.saveImage(fileName, dataUrl)
}
}
interface NoteExportOptions { interface NoteExportOptions {
node: { name: string; externalPath: string } node: { name: string; externalPath: string }
platform: 'markdown' | 'docx' | 'notion' | 'yuque' | 'obsidian' | 'joplin' | 'siyuan' platform: 'markdown' | 'docx' | 'notion' | 'yuque' | 'obsidian' | 'joplin' | 'siyuan' | 'copyImage' | 'exportImage'
} }
export const exportNote = async ({ node, platform }: NoteExportOptions): Promise<void> => { export const exportNote = async ({ node, platform }: NoteExportOptions): Promise<void> => {
@ -1102,6 +1151,10 @@ export const exportNote = async ({ node, platform }: NoteExportOptions): Promise
const content = await window.api.file.readExternal(node.externalPath) const content = await window.api.file.readExternal(node.externalPath)
switch (platform) { switch (platform) {
case 'copyImage':
return await exportNoteAsImageToClipboard()
case 'exportImage':
return await exportNoteAsImageFile(node.name)
case 'markdown': case 'markdown':
return await exportNoteAsMarkdown(node.name, content) return await exportNoteAsMarkdown(node.name, content)
case 'docx': case 'docx':