From d41e239b89aa407ebddba5df8ce795afdf993592 Mon Sep 17 00:00:00 2001 From: Zhaokun Date: Fri, 26 Sep 2025 05:07:10 +0800 Subject: [PATCH 1/5] Fix slash newline (#10305) * Fix slash menu Shift+Enter newline * fix: enable Shift+Enter newline in rich editor with slash commands Fixed an issue where users couldn't create new lines using Shift+Enter when slash command menu (/foo) was active. The problem was caused by globa keyboard event handlers intercepting all Enter key variants. Changes: - Allow Shift+Enter to pass through QuickPanel event handling - Add Shift+Enter detection in CommandListPopover to return false - Implement fallback Shift+Enter handling in command suggestion render - Remove unused import in AppUpdater.ts - Convert Chinese comments to English in QuickPanel - Add test coverage for command suggestion functionality --------- Co-authored-by: Zhaokun Zhang --- .../src/components/QuickPanel/view.tsx | 8 ++++++- .../RichEditor/CommandListPopover.tsx | 3 +++ .../__tests__/commandSuggestion.test.ts | 17 ++++++++++++++ .../src/components/RichEditor/command.ts | 23 ++++++++++++++++++- 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/components/RichEditor/__tests__/commandSuggestion.test.ts diff --git a/src/renderer/src/components/QuickPanel/view.tsx b/src/renderer/src/components/QuickPanel/view.tsx index 52c33607c7..6ad34b4557 100644 --- a/src/renderer/src/components/QuickPanel/view.tsx +++ b/src/renderer/src/components/QuickPanel/view.tsx @@ -457,7 +457,13 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { // 面板可见且未折叠时:拦截所有 Enter 变体; // 纯 Enter 选择项,带修饰键仅拦截不处理 - if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) { + if (e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) { + // Don't prevent default or stop propagation - let it create a newline + setIsMouseOver(false) + break + } + + if (e.ctrlKey || e.metaKey || e.altKey) { e.preventDefault() e.stopPropagation() setIsMouseOver(false) diff --git a/src/renderer/src/components/RichEditor/CommandListPopover.tsx b/src/renderer/src/components/RichEditor/CommandListPopover.tsx index 4f8df4d20c..1f2250a437 100644 --- a/src/renderer/src/components/RichEditor/CommandListPopover.tsx +++ b/src/renderer/src/components/RichEditor/CommandListPopover.tsx @@ -87,6 +87,9 @@ const CommandListPopover = ({ return true case 'Enter': + if (event.shiftKey) { + return false + } event.preventDefault() if (items[internalSelectedIndex]) { selectItem(internalSelectedIndex) diff --git a/src/renderer/src/components/RichEditor/__tests__/commandSuggestion.test.ts b/src/renderer/src/components/RichEditor/__tests__/commandSuggestion.test.ts new file mode 100644 index 0000000000..e352e957d0 --- /dev/null +++ b/src/renderer/src/components/RichEditor/__tests__/commandSuggestion.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest' + +import { commandSuggestion } from '../command' + +describe('commandSuggestion render', () => { + it('has render function', () => { + expect(commandSuggestion.render).toBeDefined() + expect(typeof commandSuggestion.render).toBe('function') + }) + + it('render function returns object with onKeyDown', () => { + const renderResult = commandSuggestion.render?.() + expect(renderResult).toBeDefined() + expect(renderResult?.onKeyDown).toBeDefined() + expect(typeof renderResult?.onKeyDown).toBe('function') + }) +}) diff --git a/src/renderer/src/components/RichEditor/command.ts b/src/renderer/src/components/RichEditor/command.ts index a460e210d4..1371b3ebb6 100644 --- a/src/renderer/src/components/RichEditor/command.ts +++ b/src/renderer/src/components/RichEditor/command.ts @@ -628,13 +628,34 @@ export const commandSuggestion: Omit { + // Let CommandListPopover handle events first + const popoverHandled = component.ref?.onKeyDown?.(props.event) + if (popoverHandled) { + return true + } + + // Handle Shift+Enter for newline when popover doesn't handle it + if (props.event.key === 'Enter' && props.event.shiftKey) { + props.event.preventDefault() + // Close the suggestion menu + if (cleanup) cleanup() + component.destroy() + // Use the view from SuggestionKeyDownProps to insert newline + const { view } = props + const { state, dispatch } = view + const { tr } = state + tr.insertText('\n') + dispatch(tr) + return true + } + if (props.event.key === 'Escape') { if (cleanup) cleanup() component.destroy() return true } - return component.ref?.onKeyDown(props.event) + return false }, onExit: () => { From 3b7ab2aec8fabe2f3ee3a51a88d930b209b01eab Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 26 Sep 2025 10:36:17 +0800 Subject: [PATCH 2/5] chore: remove cherryin provider references and update versioning - Commented out all references to the 'cherryin' provider in configuration files. - Updated the version in the persisted reducer from 157 to 158. - Added migration logic to remove 'cherryin' from the state during version 158 migration. --- src/renderer/src/config/models/default.ts | 2 +- src/renderer/src/config/providers.ts | 42 +++++++++++------------ src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/migrate.ts | 9 +++++ src/renderer/src/types/index.ts | 2 +- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/renderer/src/config/models/default.ts b/src/renderer/src/config/models/default.ts index 02bf37af9e..9fdced6a6a 100644 --- a/src/renderer/src/config/models/default.ts +++ b/src/renderer/src/config/models/default.ts @@ -25,7 +25,7 @@ export const SYSTEM_MODELS: Record = // Default quick assistant model glm45FlashModel ], - cherryin: [], + // cherryin: [], vertexai: [], '302ai': [ { diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 3b8821905a..543422d212 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -78,16 +78,16 @@ export const CHERRYAI_PROVIDER: SystemProvider = { } export const SYSTEM_PROVIDERS_CONFIG: Record = { - cherryin: { - id: 'cherryin', - name: 'CherryIN', - type: 'openai', - apiKey: '', - apiHost: 'https://open.cherryin.ai', - models: [], - isSystem: true, - enabled: true - }, + // cherryin: { + // id: 'cherryin', + // name: 'CherryIN', + // type: 'openai', + // apiKey: '', + // apiHost: 'https://open.cherryin.ai', + // models: [], + // isSystem: true, + // enabled: true + // }, silicon: { id: 'silicon', name: 'Silicon', @@ -708,17 +708,17 @@ type ProviderUrls = { } export const PROVIDER_URLS: Record = { - cherryin: { - api: { - url: 'https://open.cherryin.ai' - }, - websites: { - official: 'https://open.cherryin.ai', - apiKey: 'https://open.cherryin.ai/console/token', - docs: 'https://open.cherryin.ai', - models: 'https://open.cherryin.ai/pricing' - } - }, + // cherryin: { + // api: { + // url: 'https://open.cherryin.ai' + // }, + // websites: { + // official: 'https://open.cherryin.ai', + // apiKey: 'https://open.cherryin.ai/console/token', + // docs: 'https://open.cherryin.ai', + // models: 'https://open.cherryin.ai/pricing' + // } + // }, ph8: { api: { url: 'https://ph8.co' diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index ba532ecc65..4b74ba91a2 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: 157, + version: 158, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index e26a382fc9..f10fc623da 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2539,6 +2539,15 @@ const migrateConfig = { logger.error('migrate 157 error', error as Error) return state } + }, + '158': (state: RootState) => { + try { + state.llm.providers = state.llm.providers.filter((provider) => provider.id !== 'cherryin') + return state + } catch (error) { + logger.error('migrate 158 error', error as Error) + return state + } } } diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 2d580c8e37..33abec0853 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -269,7 +269,7 @@ export type Provider = { } export const SystemProviderIds = { - cherryin: 'cherryin', + // cherryin: 'cherryin', silicon: 'silicon', aihubmix: 'aihubmix', ocoolai: 'ocoolai', From 52a980f75125382cc76f74828943880476d287e3 Mon Sep 17 00:00:00 2001 From: MyPrototypeWhat Date: Fri, 26 Sep 2025 12:10:28 +0800 Subject: [PATCH 3/5] fix(websearch): handle blocked domains conditionally in web search (#10374) fix(websearch): handle blocked domains conditionally in web search configurations - Updated the handling of blocked domains in both Google Vertex and Anthropic web search configurations to only include them if they are present, improving robustness and preventing unnecessary parameters from being passed. --- src/renderer/src/aiCore/prepareParams/parameterBuilder.ts | 3 ++- src/renderer/src/aiCore/utils/websearch.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/aiCore/prepareParams/parameterBuilder.ts b/src/renderer/src/aiCore/prepareParams/parameterBuilder.ts index 0a89e73c62..1ad04230b5 100644 --- a/src/renderer/src/aiCore/prepareParams/parameterBuilder.ts +++ b/src/renderer/src/aiCore/prepareParams/parameterBuilder.ts @@ -134,9 +134,10 @@ export async function buildStreamTextParams( if (aiSdkProviderId === 'google-vertex') { tools.google_search = vertex.tools.googleSearch({}) as ProviderDefinedTool } else if (aiSdkProviderId === 'google-vertex-anthropic') { + const blockedDomains = mapRegexToPatterns(webSearchConfig.excludeDomains) tools.web_search = vertexAnthropic.tools.webSearch_20250305({ maxUses: webSearchConfig.maxResults, - blockedDomains: mapRegexToPatterns(webSearchConfig.excludeDomains) + blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined }) as ProviderDefinedTool } } diff --git a/src/renderer/src/aiCore/utils/websearch.ts b/src/renderer/src/aiCore/utils/websearch.ts index 2fda7c1b19..9e29454b79 100644 --- a/src/renderer/src/aiCore/utils/websearch.ts +++ b/src/renderer/src/aiCore/utils/websearch.ts @@ -61,9 +61,10 @@ export function buildProviderBuiltinWebSearchConfig( } } case 'anthropic': { + const blockedDomains = mapRegexToPatterns(webSearchConfig.excludeDomains) const anthropicSearchOptions: AnthropicSearchConfig = { maxUses: webSearchConfig.maxResults, - blockedDomains: mapRegexToPatterns(webSearchConfig.excludeDomains) + blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined } return { anthropic: anthropicSearchOptions From 4aa9c9f22542bfe2c345c01fe5f3524bd89a33df Mon Sep 17 00:00:00 2001 From: Zhaokun Date: Fri, 26 Sep 2025 17:49:24 +0800 Subject: [PATCH 4/5] feat: improve content protection during file operations (#10378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: improve content protection during file operations - Add validation for knowledge base configuration before saving - Enhance error handling for note content reading - Implement content backup and restoration during file rename - Add content verification after rename operations - Improve user feedback with specific error messages * fix: format check --------- Co-authored-by: 自由的世界人 <3196812536@qq.com> --- .../Popups/SaveToKnowledgePopup.tsx | 53 ++++++++++++++++--- src/renderer/src/pages/notes/NotesPage.tsx | 36 ++++++++++++- 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/Popups/SaveToKnowledgePopup.tsx b/src/renderer/src/components/Popups/SaveToKnowledgePopup.tsx index b7c02cd4ec..cea3aca7cb 100644 --- a/src/renderer/src/components/Popups/SaveToKnowledgePopup.tsx +++ b/src/renderer/src/components/Popups/SaveToKnowledgePopup.tsx @@ -253,12 +253,39 @@ const PopupContainer: React.FC = ({ source, title, resolve }) => { let savedCount = 0 try { + // Validate knowledge base configuration before proceeding + if (!selectedBaseId) { + throw new Error('No knowledge base selected') + } + + const selectedBase = bases.find((base) => base.id === selectedBaseId) + if (!selectedBase) { + throw new Error('Selected knowledge base not found') + } + + if (!selectedBase.version) { + throw new Error('Knowledge base is not properly configured. Please check the knowledge base settings.') + } + if (isNoteMode) { const note = source.data as NotesTreeNode - const content = note.externalPath - ? await window.api.file.readExternal(note.externalPath) - : await window.api.file.read(note.id + '.md') - logger.debug('Note content:', content) + if (!note.externalPath) { + throw new Error('Note external path is required for export') + } + + let content = '' + try { + content = await window.api.file.readExternal(note.externalPath) + } catch (error) { + logger.error('Failed to read note file:', error as Error) + throw new Error('Failed to read note content. Please ensure the file exists and is accessible.') + } + + if (!content || content.trim() === '') { + throw new Error('Note content is empty. Cannot export empty notes to knowledge base.') + } + + logger.debug('Note content loaded', { contentLength: content.length }) await addNote(content) savedCount = 1 } else { @@ -283,9 +310,23 @@ const PopupContainer: React.FC = ({ source, title, resolve }) => { resolve({ success: true, savedCount }) } catch (error) { logger.error('save failed:', error as Error) - window.toast.error( - t(isTopicMode ? 'chat.save.topic.knowledge.error.save_failed' : 'chat.save.knowledge.error.save_failed') + + // Provide more specific error messages + let errorMessage = t( + isTopicMode ? 'chat.save.topic.knowledge.error.save_failed' : 'chat.save.knowledge.error.save_failed' ) + + if (error instanceof Error) { + if (error.message.includes('not properly configured')) { + errorMessage = error.message + } else if (error.message.includes('empty')) { + errorMessage = error.message + } else if (error.message.includes('read note content')) { + errorMessage = error.message + } + } + + window.toast.error(errorMessage) setLoading(false) } } diff --git a/src/renderer/src/pages/notes/NotesPage.tsx b/src/renderer/src/pages/notes/NotesPage.tsx index c85793e781..bc039e5ef2 100644 --- a/src/renderer/src/pages/notes/NotesPage.tsx +++ b/src/renderer/src/pages/notes/NotesPage.tsx @@ -492,10 +492,42 @@ const NotesPage: FC = () => { if (node && node.name !== newName) { const oldExternalPath = node.externalPath + let currentContent = '' + + // Save current content before rename to prevent content loss + if (node.type === 'file' && activeFilePath === oldExternalPath) { + // Get content from editor or current cache + currentContent = editorRef.current?.getMarkdown() || lastContentRef.current || currentContent + + // Save current content to the file before renaming + if (currentContent.trim()) { + try { + await saveCurrentNote(currentContent, oldExternalPath) + } catch (error) { + logger.warn('Failed to save content before rename:', error as Error) + } + } + } + const renamedNode = await renameNode(nodeId, newName) if (renamedNode.type === 'file' && activeFilePath === oldExternalPath) { + // Restore content to the new file path if content was lost during rename + if (currentContent.trim()) { + try { + const newFileContent = await window.api.file.readExternal(renamedNode.externalPath) + if (!newFileContent || newFileContent.trim() === '') { + await window.api.file.write(renamedNode.externalPath, currentContent) + logger.info('Restored content to renamed file') + } + } catch (error) { + logger.error('Failed to restore content after rename:', error as Error) + } + } + dispatch(setActiveFilePath(renamedNode.externalPath)) + // Invalidate cache for the new path to ensure content is loaded correctly + invalidateFileContent(renamedNode.externalPath) } else if ( renamedNode.type === 'folder' && activeFilePath && @@ -504,6 +536,8 @@ const NotesPage: FC = () => { const relativePath = activeFilePath.substring(oldExternalPath.length) const newFilePath = renamedNode.externalPath + relativePath dispatch(setActiveFilePath(newFilePath)) + // Invalidate cache for the new file path after folder rename + invalidateFileContent(newFilePath) } await sortAllLevels(sortType) if (renamedNode.name !== newName) { @@ -518,7 +552,7 @@ const NotesPage: FC = () => { }, 500) } }, - [activeFilePath, dispatch, findNodeById, sortType, t] + [activeFilePath, dispatch, findNodeById, sortType, t, invalidateFileContent, saveCurrentNote] ) // 处理文件上传 From dabfb8dc0ed9fb374abb32bc837c2e206b61dccb Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Fri, 26 Sep 2025 17:50:00 +0800 Subject: [PATCH 5/5] style(settings): remove unnecessary padding from ContentContainer (#10379) --- src/renderer/src/pages/settings/SettingsPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/renderer/src/pages/settings/SettingsPage.tsx b/src/renderer/src/pages/settings/SettingsPage.tsx index ca83e149f0..00032484b7 100644 --- a/src/renderer/src/pages/settings/SettingsPage.tsx +++ b/src/renderer/src/pages/settings/SettingsPage.tsx @@ -191,7 +191,6 @@ const ContentContainer = styled.div` flex: 1; flex-direction: row; height: calc(100vh - var(--navbar-height)); - padding: 1px 0; ` const SettingMenus = styled(Scrollbar)`