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
releaseInfo:
releaseNotes: |
Optimized note-taking feature, now able to quickly rename by modifying the title
Fixed issue where CherryAI free model could not be used
Fixed issue where VertexAI proxy address could not be called normally
Fixed issue where built-in tools from service providers could not be called normally
What's New in v1.6.3
Features:
- 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",
"version": "1.6.2",
"version": "1.6.3",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",

View File

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

View File

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

View File

@ -57,6 +57,8 @@ export interface UseRichEditorOptions {
editable?: boolean
/** Whether to enable table of contents functionality */
enableTableOfContents?: boolean
/** Whether to enable spell check */
enableSpellCheck?: boolean
/** Show table action menu (row/column) with concrete actions and position */
onShowTableActionMenu?: (payload: {
type: 'row' | 'column'
@ -126,6 +128,7 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor
previewLength = 50,
placeholder = '',
editable = true,
enableSpellCheck = false,
onShowTableActionMenu,
scrollParent
} = options
@ -410,7 +413,9 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor
// Allow text selection even when not 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 }) => {

View File

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

View File

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

View File

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

View File

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

View File

@ -334,7 +334,7 @@
"new_topic": "Nuevo tema {{Command}}",
"pause": "Pausar",
"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",
"settings": "Configuración",
"thinking": {
@ -1698,10 +1698,10 @@
},
"notes": {
"auto_rename": {
"empty_note": "[to be translated]:笔记为空,无法生成名称",
"failed": "[to be translated]:生成笔记名称失败",
"label": "[to be translated]:生成笔记名称",
"success": "[to be translated]:笔记名称生成成功"
"empty_note": "La nota está vacía, no se puede generar un nombre",
"failed": "Error al generar el nombre de la nota",
"label": "Generar nombre de nota",
"success": "Se ha generado correctamente el nombre de la nota"
},
"characters": "carácter",
"collapse": "ocultar",
@ -1784,6 +1784,8 @@
"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_z2a": "Nombre de archivo (Z-A)",
"spell_check": "comprobación ortográfica",
"spell_check_tooltip": "Habilitar/deshabilitar revisión ortográfica",
"star": "Notas guardadas",
"starred_notes": "notas guardadas",
"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.",
"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",
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript支持大多数标准库和流行的第三方库",
"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.",
"no": "sin descripción",

View File

@ -334,7 +334,7 @@
"new_topic": "Nouveau sujet {{Command}}",
"pause": "Pause",
"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",
"settings": "Paramètres",
"thinking": {
@ -1698,10 +1698,10 @@
},
"notes": {
"auto_rename": {
"empty_note": "[to be translated]:笔记为空,无法生成名称",
"failed": "[to be translated]:生成笔记名称失败",
"label": "[to be translated]:生成笔记名称",
"success": "[to be translated]:笔记名称生成成功"
"empty_note": "La note est vide, impossible de générer un nom",
"failed": "Échec de la génération du nom de note",
"label": "Générer un nom de note",
"success": "La génération du nom de note a réussi"
},
"characters": "caractère",
"collapse": "réduire",
@ -1784,6 +1784,8 @@
"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_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",
"starred_notes": "notes de collection",
"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",
"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.",
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript支持大多数标准库和流行的第三方库",
"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.",
"no": "sans description",

View File

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

View File

@ -334,7 +334,7 @@
"new_topic": "Novo tópico {{Command}}",
"pause": "Pausar",
"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",
"settings": "Configurações",
"thinking": {
@ -1698,10 +1698,10 @@
},
"notes": {
"auto_rename": {
"empty_note": "[to be translated]:笔记为空,无法生成名称",
"failed": "[to be translated]:生成笔记名称失败",
"label": "[to be translated]:生成笔记名称",
"success": "[to be translated]:笔记名称生成成功"
"empty_note": "A nota está vazia, não é possível gerar um nome",
"failed": "Falha ao gerar o nome da nota",
"label": "Gerar nome da nota",
"success": "Nome da nota gerado com sucesso"
},
"characters": "caractere",
"collapse": "[minimizar]",
@ -1784,6 +1784,8 @@
"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_z2a": "Nome do arquivo (Z-A)",
"spell_check": "verificação ortográfica",
"spell_check_tooltip": "Ativar/Desativar verificação ortográfica",
"star": "Notas favoritas",
"starred_notes": "notas salvas",
"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",
"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",
"js": "[to be translated]:在安全的沙盒环境中执行 JavaScript 代码。使用 quickJs 运行 JavaScript支持大多数标准库和流行的第三方库",
"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.",
"no": "sem descrição",

View File

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

View File

@ -231,7 +231,7 @@ const MessageItem: FC<Props> = ({
<HorizontalScrollContainer
classNames={{
content: cn(
'items-center',
'flex-1 items-center justify-between',
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: 0.5px solid transparent;
width: calc(var(--assistants-width) - 20px);
cursor: pointer;
&: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 { HSpaceBetweenStack } from '@renderer/components/Layout'
import RichEditor from '@renderer/components/RichEditor'
import { RichEditorRef } from '@renderer/components/RichEditor/types'
import Selector from '@renderer/components/Selector'
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 { Empty } from 'antd'
import { Empty, Tooltip } from 'antd'
import { SpellCheck } from 'lucide-react'
import { FC, memo, RefObject, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -21,7 +26,9 @@ interface NotesEditorProps {
const NotesEditor: FC<NotesEditorProps> = memo(
({ activeNodeId, currentContent, tokenCount, onMarkdownChange, editorRef }) => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const { settings } = useNotesSettings()
const { enableSpellCheck } = useSettings()
const currentViewMode = useMemo(() => {
if (settings.defaultViewMode === 'edit') {
return settings.defaultEditMode
@ -78,6 +85,7 @@ const NotesEditor: FC<NotesEditorProps> = memo(
isFullWidth={settings.isFullWidth}
fontFamily={settings.fontFamily}
fontSize={settings.fontSize}
enableSpellCheck={enableSpellCheck}
/>
)}
</RichEditorContainer>
@ -92,8 +100,21 @@ const NotesEditor: FC<NotesEditorProps> = memo(
color: 'var(--color-text-3)',
display: 'flex',
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
value={tmpViewMode as EditorView}
onChange={(value: EditorView) => setTmpViewMode(value)}

View File

@ -359,6 +359,23 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
[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(
async (note: NotesTreeNode) => {
if (note.type !== 'file') return
@ -612,6 +629,16 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
key: 'export',
icon: <UploadIcon size={14} />,
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 && {
label: t('chat.topics.export.md.label'),
key: 'markdown',
@ -671,6 +698,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
handleStartEdit,
onToggleStar,
handleExportKnowledge,
handleImageAction,
handleDeleteNode,
renamingNodeIds,
handleAutoRename,
@ -1129,7 +1157,7 @@ const DragOverIndicator = styled.div`
`
const DropHintNode = styled.div`
margin: 8px;
margin: 6px 0;
margin-bottom: 20px;
${TreeNodeContainer} {

View File

@ -89,6 +89,7 @@ export async function restore() {
}
await handleData(data)
notificationService.send({
id: uuid(),
type: 'success',
@ -850,6 +851,12 @@ export async function handleData(data: Record<string, any>) {
if (data.version >= 2) {
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)
if (data.version === 3) {

View File

@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 158,
version: 159,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
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
function addOcrProvider(state: RootState, provider: BuiltinOcrProvider) {
if (!state.ocr.providers.find((p) => p.id === provider.id)) {
@ -2553,6 +2562,7 @@ const migrateConfig = {
'159': (state: RootState) => {
try {
addProvider(state, 'ovms')
fixMissingProvider(state)
return state
} catch (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 { Message } from '@renderer/types/newMessage'
import { removeSpecialCharactersForFileName } from '@renderer/utils/file'
import { captureScrollableAsBlob, captureScrollableAsDataURL } from '@renderer/utils/image'
import { convertMathFormula, markdownToPlainText } from '@renderer/utils/markdown'
import { getCitationContent, getMainTextContent, getThinkingContent } from '@renderer/utils/messageUtils/find'
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 {
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> => {
@ -1102,6 +1151,10 @@ export const exportNote = async ({ node, platform }: NoteExportOptions): Promise
const content = await window.api.file.readExternal(node.externalPath)
switch (platform) {
case 'copyImage':
return await exportNoteAsImageToClipboard()
case 'exportImage':
return await exportNoteAsImageFile(node.name)
case 'markdown':
return await exportNoteAsMarkdown(node.name, content)
case 'docx':