From 1db93e8b569f066b243b1fe8671399ab27bf9c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Thu, 26 Jun 2025 13:19:36 +0800 Subject: [PATCH 1/9] Fix anthropic request cannot handle webSearch and knowbase references (#7559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 Anthropic 模型请求忽略了知识库和网络搜索引用内容的问题 --- .../src/aiCore/clients/anthropic/AnthropicAPIClient.ts | 4 ++-- src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx | 2 +- .../__tests__/__snapshots__/ThinkingBlock.test.tsx.snap | 2 +- src/renderer/src/pages/home/Messages/CitationsList.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts b/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts index 465cdff5a..e07b7508d 100644 --- a/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts +++ b/src/renderer/src/aiCore/clients/anthropic/AnthropicAPIClient.ts @@ -66,7 +66,7 @@ import { mcpToolCallResponseToAnthropicMessage, mcpToolsToAnthropicTools } from '@renderer/utils/mcp-tools' -import { findFileBlocks, findImageBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find' +import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/find' import { buildSystemPrompt } from '@renderer/utils/prompt' import { BaseApiClient } from '../BaseApiClient' @@ -192,7 +192,7 @@ export class AnthropicAPIClient extends BaseApiClient< const parts: MessageParam['content'] = [ { type: 'text', - text: getMainTextContent(message) + text: await this.getMessageContent(message) } ] diff --git a/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx index e1420ba6c..fa422ea52 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/ThinkingBlock.tsx @@ -150,7 +150,7 @@ const ThinkingTimeSeconds = memo( ) const CollapseContainer = styled(Collapse)` - margin-bottom: 15px; + margin: 15px 0; ` const MessageTitleLabel = styled.div` diff --git a/src/renderer/src/pages/home/Messages/Blocks/__tests__/__snapshots__/ThinkingBlock.test.tsx.snap b/src/renderer/src/pages/home/Messages/Blocks/__tests__/__snapshots__/ThinkingBlock.test.tsx.snap index 7f1f866b8..805e7d2a9 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/__tests__/__snapshots__/ThinkingBlock.test.tsx.snap +++ b/src/renderer/src/pages/home/Messages/Blocks/__tests__/__snapshots__/ThinkingBlock.test.tsx.snap @@ -2,7 +2,7 @@ exports[`ThinkingBlock > basic rendering > should match snapshot 1`] = ` .c0 { - margin-bottom: 15px; + margin: 15px 0; } .c1 { diff --git a/src/renderer/src/pages/home/Messages/CitationsList.tsx b/src/renderer/src/pages/home/Messages/CitationsList.tsx index 8929ffd89..f147619ad 100644 --- a/src/renderer/src/pages/home/Messages/CitationsList.tsx +++ b/src/renderer/src/pages/home/Messages/CitationsList.tsx @@ -326,7 +326,7 @@ const PopoverContent = styled.div` ` const KnowledgePopoverContent = styled(PopoverContent)` - max-width: 800px; + max-width: 600px; ` const PopoverContentItem = styled.div` From 5811adfb7f324b5cf039af35fc6962b0f956d1ae Mon Sep 17 00:00:00 2001 From: one Date: Thu, 26 Jun 2025 13:30:49 +0800 Subject: [PATCH 2/9] refactor(CodePreview): handle chunking in ShikiStreamService, make the algorithm more robust (#7409) * refactor(ShikiStreamService, CodePreview): handle chunking in ShikiStreamService, make the algorithm more robust - Add highlightStreamingCode with improved robustness - Improve viewport detection * perf: improve checks for appending * chore: update comments --- .../components/CodeBlockView/CodePreview.tsx | 118 ++++++++---------- .../src/context/CodeStyleProvider.tsx | 13 ++ .../src/services/ShikiStreamService.ts | 83 +++++++++++- 3 files changed, 147 insertions(+), 67 deletions(-) diff --git a/src/renderer/src/components/CodeBlockView/CodePreview.tsx b/src/renderer/src/components/CodeBlockView/CodePreview.tsx index dde163283..b550cd246 100644 --- a/src/renderer/src/components/CodeBlockView/CodePreview.tsx +++ b/src/renderer/src/components/CodeBlockView/CodePreview.tsx @@ -4,7 +4,7 @@ import { useSettings } from '@renderer/hooks/useSettings' import { uuid } from '@renderer/utils' import { getReactStyleFromToken } from '@renderer/utils/shiki' import { ChevronsDownUp, ChevronsUpDown, Text as UnWrapIcon, WrapText as WrapIcon } from 'lucide-react' -import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { memo, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { ThemedToken } from 'shiki/core' import styled from 'styled-components' @@ -18,19 +18,20 @@ interface CodePreviewProps { /** * Shiki 流式代码高亮组件 * - * - 通过 shiki tokenizer 处理流式响应 - * - 为了正确执行语法高亮,必须保证流式响应都依次到达 tokenizer,不能跳过 + * - 通过 shiki tokenizer 处理流式响应,高性能 + * - 进入视口后触发高亮,改善页面内有大量长代码块时的响应 + * - 并发安全 */ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => { const { codeShowLineNumbers, fontSize, codeCollapsible, codeWrappable } = useSettings() - const { activeShikiTheme, highlightCodeChunk, cleanupTokenizers } = useCodeStyle() + const { activeShikiTheme, highlightStreamingCode, cleanupTokenizers } = useCodeStyle() const [isExpanded, setIsExpanded] = useState(!codeCollapsible) const [isUnwrapped, setIsUnwrapped] = useState(!codeWrappable) const [tokenLines, setTokenLines] = useState([]) - const codeContentRef = useRef(null) - const prevCodeLengthRef = useRef(0) - const safeCodeStringRef = useRef(children) - const highlightQueueRef = useRef>(Promise.resolve()) + const [isInViewport, setIsInViewport] = useState(false) + const codeContainerRef = useRef(null) + const processingRef = useRef(false) + const latestRequestedContentRef = useRef(null) const callerId = useRef(`${Date.now()}-${uuid()}`).current const shikiThemeRef = useRef(activeShikiTheme) @@ -45,7 +46,7 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => { icon: isExpanded ? : , tooltip: isExpanded ? t('code_block.collapse') : t('code_block.expand'), visible: () => { - const scrollHeight = codeContentRef.current?.scrollHeight + const scrollHeight = codeContainerRef.current?.scrollHeight return codeCollapsible && (scrollHeight ?? 0) > 350 }, onClick: () => setIsExpanded((prev) => !prev) @@ -77,81 +78,63 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => { setIsUnwrapped(!codeWrappable) }, [codeWrappable]) - // 处理尾部空白字符 - const safeCodeString = useMemo(() => { - return typeof children === 'string' ? children.trimEnd() : '' - }, [children]) - const highlightCode = useCallback(async () => { - if (!safeCodeString) return + const currentContent = typeof children === 'string' ? children.trimEnd() : '' - if (prevCodeLengthRef.current === safeCodeString.length) return + // 记录最新要处理的内容,为了保证最终状态正确 + latestRequestedContentRef.current = currentContent - // 捕获当前状态 - const startPos = prevCodeLengthRef.current - const endPos = safeCodeString.length + // 如果正在处理,先跳出,等到完成后会检查是否有新内容 + if (processingRef.current) return - // 添加到处理队列,确保按顺序处理 - highlightQueueRef.current = highlightQueueRef.current.then(async () => { - // FIXME: 长度有问题,或者破坏了流式内容,需要清理 tokenizer 并使用完整代码重新高亮 - if (prevCodeLengthRef.current > safeCodeString.length || !safeCodeString.startsWith(safeCodeStringRef.current)) { - cleanupTokenizers(callerId) - prevCodeLengthRef.current = 0 - safeCodeStringRef.current = '' + processingRef.current = true - const result = await highlightCodeChunk(safeCodeString, language, callerId) - setTokenLines(result.lines) + try { + // 循环处理,确保会处理最新内容 + while (latestRequestedContentRef.current !== null) { + const contentToProcess = latestRequestedContentRef.current + latestRequestedContentRef.current = null // 标记开始处理 - prevCodeLengthRef.current = safeCodeString.length - safeCodeStringRef.current = safeCodeString + // 传入完整内容,让 ShikiStreamService 检测变化并处理增量高亮 + const result = await highlightStreamingCode(contentToProcess, language, callerId) - return + // 如有结果,更新 tokenLines + if (result.lines.length > 0 || result.recall !== 0) { + setTokenLines((prev) => { + return result.recall === -1 + ? result.lines + : [...prev.slice(0, Math.max(0, prev.length - result.recall)), ...result.lines] + }) + } } - - // 跳过 race condition,延迟到后续任务 - if (prevCodeLengthRef.current !== startPos) { - return - } - - const incrementalCode = safeCodeString.slice(startPos, endPos) - const result = await highlightCodeChunk(incrementalCode, language, callerId) - setTokenLines((lines) => [...lines.slice(0, Math.max(0, lines.length - result.recall)), ...result.lines]) - prevCodeLengthRef.current = endPos - safeCodeStringRef.current = safeCodeString - }) - }, [callerId, cleanupTokenizers, highlightCodeChunk, language, safeCodeString]) + } finally { + processingRef.current = false + } + }, [highlightStreamingCode, language, callerId, children]) // 主题变化时强制重新高亮 useEffect(() => { if (shikiThemeRef.current !== activeShikiTheme) { - prevCodeLengthRef.current++ shikiThemeRef.current = activeShikiTheme + cleanupTokenizers(callerId) + setTokenLines([]) } - }, [activeShikiTheme]) + }, [activeShikiTheme, callerId, cleanupTokenizers]) // 组件卸载时清理资源 useEffect(() => { return () => cleanupTokenizers(callerId) }, [callerId, cleanupTokenizers]) - // 触发代码高亮 - // - 进入视口后触发第一次高亮 - // - 内容变化后触发之后的高亮 + // 视口检测逻辑,进入视口后触发第一次代码高亮 useEffect(() => { - let isMounted = true - - if (prevCodeLengthRef.current > 0) { - setTimeout(highlightCode, 0) - return - } - - const codeElement = codeContentRef.current + const codeElement = codeContainerRef.current if (!codeElement) return const observer = new IntersectionObserver( (entries) => { - if (entries[0].intersectionRatio > 0 && isMounted) { - setTimeout(highlightCode, 0) + if (entries[0].intersectionRatio > 0) { + setIsInViewport(true) observer.disconnect() } }, @@ -161,15 +144,18 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => { ) observer.observe(codeElement) + return () => observer.disconnect() + }, []) // 只执行一次 - return () => { - isMounted = false - observer.disconnect() - } - }, [highlightCode]) + // 触发代码高亮 + useEffect(() => { + if (!isInViewport) return + + setTimeout(highlightCode, 0) + }, [isInViewport, highlightCode]) useEffect(() => { - const container = codeContentRef.current + const container = codeContainerRef.current if (!container || !codeShowLineNumbers) return const digits = Math.max(tokenLines.length.toString().length, 1) @@ -180,7 +166,7 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => { return ( Promise + highlightStreamingCode: (code: string, language: string, callerId: string) => Promise cleanupTokenizers: (callerId: string) => void getShikiPreProperties: (language: string) => Promise highlightCode: (code: string, language: string) => Promise @@ -22,6 +23,7 @@ interface CodeStyleContextType { const defaultCodeStyleContext: CodeStyleContextType = { highlightCodeChunk: async () => ({ lines: [], recall: 0 }), + highlightStreamingCode: async () => ({ lines: [], recall: 0 }), cleanupTokenizers: () => {}, getShikiPreProperties: async () => ({ class: '', style: '', tabindex: 0 }), highlightCode: async () => '', @@ -114,6 +116,15 @@ export const CodeStyleProvider: React.FC = ({ children }) => shikiStreamService.cleanupTokenizers(callerId) }, []) + // 高亮流式输出的代码 + const highlightStreamingCode = useCallback( + async (fullContent: string, language: string, callerId: string) => { + const normalizedLang = languageMap[language as keyof typeof languageMap] || language.toLowerCase() + return shikiStreamService.highlightStreamingCode(fullContent, normalizedLang, activeShikiTheme, callerId) + }, + [activeShikiTheme, languageMap] + ) + // 获取 Shiki pre 标签属性 const getShikiPreProperties = useCallback( async (language: string) => { @@ -148,6 +159,7 @@ export const CodeStyleProvider: React.FC = ({ children }) => const contextValue = useMemo( () => ({ highlightCodeChunk, + highlightStreamingCode, cleanupTokenizers, getShikiPreProperties, highlightCode, @@ -159,6 +171,7 @@ export const CodeStyleProvider: React.FC = ({ children }) => }), [ highlightCodeChunk, + highlightStreamingCode, cleanupTokenizers, getShikiPreProperties, highlightCode, diff --git a/src/renderer/src/services/ShikiStreamService.ts b/src/renderer/src/services/ShikiStreamService.ts index 8ddc2a8be..8d5c6f622 100644 --- a/src/renderer/src/services/ShikiStreamService.ts +++ b/src/renderer/src/services/ShikiStreamService.ts @@ -20,7 +20,7 @@ export type ShikiPreProperties = { * 代码 chunk 高亮结果 * * @param lines 所有高亮行(包括稳定和不稳定) - * @param recall 需要撤回的行数 + * @param recall 需要撤回的行数,-1 表示撤回所有行 */ export interface HighlightChunkResult { lines: ThemedToken[][] @@ -47,6 +47,13 @@ class ShikiStreamService { } }) + // 缓存每个 callerId 对应的已处理内容 + private codeCache = new LRUCache({ + max: 100, // 最大缓存数量 + ttl: 1000 * 60 * 30, // 30分钟过期时间 + updateAgeOnGet: true + }) + // Worker 相关资源 private worker: Worker | null = null private workerInitPromise: Promise | null = null @@ -261,6 +268,72 @@ class ShikiStreamService { return hast.children[0].properties as ShikiPreProperties } + /** + * 高亮流式输出的代码,调用方传入完整代码内容,得到增量高亮结果。 + * + * - 检测当前内容与上次处理内容的差异。 + * - 如果是末尾追加,只传输增量部分(此时性能最好,如遇性能问题,考虑检查这里的逻辑)。 + * - 如果不是追加,重置 tokenizer 并处理完整内容。 + * + * 调用者需要自行处理撤回。 + * @param code 完整代码内容 + * @param language 语言 + * @param theme 主题 + * @param callerId 调用者ID + * @returns 高亮结果,recall 为 -1 表示撤回所有行 + */ + async highlightStreamingCode( + code: string, + language: string, + theme: string, + callerId: string + ): Promise { + const cacheKey = `${callerId}-${language}-${theme}` + const lastContent = this.codeCache.get(cacheKey) || '' + + let isAppend = false + + if (code.length === lastContent.length) { + // 内容没有变化,返回空结果 + if (code === lastContent) { + return { lines: [], recall: 0 } + } + } else if (code.length > lastContent.length) { + // 长度增加,可能是追加 + isAppend = code.startsWith(lastContent) + } + + try { + let result: HighlightChunkResult + + if (isAppend) { + // 流式追加,只传输增量 + const chunk = code.slice(lastContent.length) + result = await this.highlightCodeChunk(chunk, language, theme, callerId) + } else { + // 非追加变化,重置并处理完整内容 + this.cleanupTokenizers(callerId) + this.codeCache.delete(cacheKey) // 清除缓存 + + result = await this.highlightCodeChunk(code, language, theme, callerId) + + // 撤回所有行 + result = { + ...result, + recall: -1 + } + } + + // 成功处理后更新缓存 + this.codeCache.set(cacheKey, code) + return result + } catch (error) { + // 处理失败时不更新缓存,保持之前的状态 + console.error('Failed to highlight streaming code:', error) + throw error + } + } + /** * 高亮代码 chunk,返回本次高亮的所有 ThemedToken 行 * @@ -405,6 +478,13 @@ class ShikiStreamService { }) } + // 清理对应的内容缓存 + for (const key of this.codeCache.keys()) { + if (key.startsWith(`${callerId}-`)) { + this.codeCache.delete(key) + } + } + // 再清理主线程中的 tokenizers,移除所有以 callerId 开头的缓存项 for (const key of this.tokenizerCache.keys()) { if (key.startsWith(`${callerId}-`)) { @@ -429,6 +509,7 @@ class ShikiStreamService { this.workerDegradationCache.clear() this.tokenizerCache.clear() + this.codeCache.clear() this.highlighter = null this.workerInitPromise = null this.workerInitRetryCount = 0 From f555e604a361563548d06e847b71fc9622099e41 Mon Sep 17 00:00:00 2001 From: suyao Date: Thu, 26 Jun 2025 01:31:44 +0800 Subject: [PATCH 3/9] fix(models): update isReasoningModel function to exclude embedding models - Added a check to the isReasoningModel function to return false for embedding models, ensuring correct model classification. --- src/renderer/src/config/models.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 6bd366617..f934fbbb8 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -2545,6 +2545,10 @@ export function isReasoningModel(model?: Model): boolean { return false } + if (isEmbeddingModel(model)) { + return false + } + if (model.provider === 'doubao') { return ( REASONING_REGEX.test(model.name) || From 6342998c9f9d4c63203f041c7981a6282db7eb09 Mon Sep 17 00:00:00 2001 From: one Date: Thu, 26 Jun 2025 15:01:36 +0800 Subject: [PATCH 4/9] feat(MentionedModels): improve feedback for MessageGroupModelList (#7539) * feat(MentionedModels): improve feedback for MessageGroupModelList * refactor: reuse pulse animation, fix tooltip triggering area * refactor: use lightbulbSoftVariants --- .../home/Messages/MessageGroupModelList.tsx | 50 ++++++++++++------- src/renderer/src/utils/motionVariants.ts | 19 +++++++ 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx b/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx index 8fe085aa2..c185d509f 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx @@ -6,8 +6,10 @@ import { useSettings } from '@renderer/hooks/useSettings' import { useAppDispatch } from '@renderer/store' import { setFoldDisplayMode } from '@renderer/store/settings' import type { Model } from '@renderer/types' -import type { Message } from '@renderer/types/newMessage' +import { AssistantMessageStatus, type Message } from '@renderer/types/newMessage' +import { lightbulbSoftVariants } from '@renderer/utils/motionVariants' import { Avatar, Segmented as AntdSegmented, Tooltip } from 'antd' +import { motion } from 'motion/react' import { FC, memo, useCallback } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -26,50 +28,62 @@ const MessageGroupModelList: FC = ({ messages, selec const { foldDisplayMode } = useSettings() const isCompact = foldDisplayMode === 'compact' + const isMessageProcessing = useCallback((message: Message) => { + return [ + AssistantMessageStatus.PENDING, + AssistantMessageStatus.PROCESSING, + AssistantMessageStatus.SEARCHING + ].includes(message.status as AssistantMessageStatus) + }, []) + const renderLabel = useCallback( (message: Message) => { const modelTip = message.model?.name + const isProcessing = isMessageProcessing(message) if (isCompact) { return ( - + { setSelectedMessage(message) }}> - + + + ) } return ( - + {message.model?.name} ) }, - [isCompact, selectMessageId, setSelectedMessage] + [isCompact, isMessageProcessing, selectMessageId, setSelectedMessage] ) return ( - dispatch(setFoldDisplayMode(isCompact ? 'expanded' : 'compact'))}> - + + dispatch(setFoldDisplayMode(isCompact ? 'expanded' : 'compact'))}> {isCompact ? : } - - - + + {isCompact ? ( /* Compact style display */ diff --git a/src/renderer/src/utils/motionVariants.ts b/src/renderer/src/utils/motionVariants.ts index 1f4812369..8d5d25ade 100644 --- a/src/renderer/src/utils/motionVariants.ts +++ b/src/renderer/src/utils/motionVariants.ts @@ -16,3 +16,22 @@ export const lightbulbVariants = { } } } + +export const lightbulbSoftVariants = { + active: { + opacity: [1, 0.5, 1], + transition: { + duration: 2, + ease: 'easeInOut', + times: [0, 0.5, 1], + repeat: Infinity + } + }, + idle: { + opacity: 1, + transition: { + duration: 0.3, + ease: 'easeInOut' + } + } +} From 4c66b205bb7ae525f33dc356d33ba753b18c3260 Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Thu, 26 Jun 2025 15:43:45 +0800 Subject: [PATCH 5/9] feat: implement early access feature toggle and update related configurations (#7304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implement early access feature toggle and update related configurations - Replace FeedUrl with EnableEarlyAccess in IpcChannel and ConfigManager - Update AppUpdater to handle early access updates from GitHub - Modify settings and localization files to reflect early access functionality - Ensure proper integration in the renderer and preload layers * fix: enhance error handling in AppUpdater for GitHub release fetching - Wrap the fetch call in a try-catch block to handle potential errors when retrieving the latest non-draft version from GitHub. - Log an error message if the fetch fails and return a default feed URL. * refactor: remove early access feature handling from AppUpdater - Eliminate the early access feature toggle logic from the AppUpdater class. - Adjust the feed URL setting to ensure it retrieves the latest non-draft version from GitHub when applicable. - Clean up unnecessary user-agent header in the fetch request. * feat(AppUpdater): enhance update feed URL logic and disable differential downloads - Introduced a new private method to streamline feed URL setting based on early access and IP country. - Disabled differential downloads for compatibility with GitHub and GitCode. - Cleaned up the checkForUpdates method for better readability and maintainability. * refactor(AppUpdater): simplify early access feed URL logic - Consolidated the feed URL setting logic in setEnableEarlyAccess to a single line for improved readability. - Removed redundant conditional checks while maintaining functionality for early access updates. * refactor(AppUpdater): update feed URL structure and remove early access setting - Modified the return structure of the latest release URL to include the channel type. - Removed the early access setting from the IPC handler, streamlining the update process. - Ensured the autoUpdater channel is set based on the latest release information. * feat(UpgradeChannel): add upgrade channel management and IPC integration - Introduced a new UpgradeChannel enum to manage different upgrade paths (latest, rc, beta). - Updated IpcChannel to include App_SetUpgradeChannel for setting the upgrade channel. - Enhanced ConfigManager to store and retrieve the selected upgrade channel. - Modified AppUpdater to fetch pre-release versions based on the selected upgrade channel. - Updated settings UI to allow users to select their preferred upgrade channel with tooltips for guidance. - Localized new strings for upgrade channel options in multiple languages. * refactor(AboutSettings): update version type detection and localize upgrade channel tooltips - Changed version type detection to use the UpgradeChannel enum for better clarity. - Localized success messages for switching upgrade channels to enhance user experience. * chore: update version to 1.4.4-beta.1 and refactor upgrade channel handling in AboutSettings - Updated package version to 1.4.4-beta.1. - Renamed version type detection function to getVersionChannel for clarity. - Refactored available version options to getAvailableTestChannels for better organization. - Added logic to clear update info when switching upgrade channels and when toggling early access settings. * chore: update version to 1.4.4 in package.json * fix lint error * feat(AppUpdater): enhance upgrade channel management and localization - Added cancellation functionality for ongoing downloads in AppUpdater. - Introduced a new upgrade channel option for the latest stable version. - Updated IPC handlers to cancel downloads when changing early access settings or upgrade channels. - Localized new strings for the latest version option in multiple languages. - Refactored AboutSettings to include the latest version in the upgrade channel selection. * refactor(AboutSettings): remove version channel detection logic - Eliminated the getVersionChannel function to simplify version handling. - Updated AboutSettings to streamline upgrade channel management. * feat(AboutSettings): set default upgrade channel to latest - Updated the AboutSettings component to set the default value of the upgrade channel to the latest option, enhancing user experience in channel selection. * refactor(AboutSettings): simplify upgrade channel change handling - Removed individual success messages for different upgrade channels in the handleUpgradeChannelChange function, streamlining the code and improving maintainability. * refactor: file actions into FileAction service (#7413) * refactor: file actions into FileAction service Moved file sorting, deletion, and renaming logic from FilesPage to a new FileAction service for better modularity and reuse. Updated FileList and FilesPage to use the new service functions, and improved the delete button UI in FileList. * fix: add tag collapse state management for assistants (#7436) Add tag collapse state management for assistants Introduces a collapsedTags state to manage the collapsed/expanded state of tag groups in the assistants list. Updates useTags and AssistantsTab to use this state, and adds actions to toggle and initialize tag collapse in the Redux store. * fix(model): doubao thinking param (#7499) * feat: Implement occupied directories handling during data copy (#7485) * feat: Implement occupied directories handling during data copy - Added `occupiedDirs` constant to manage directories that should not be copied. - Enhanced the `copyOccupiedDirsInMainProcess` function to copy occupied directories to a new app data path in the main process. - Updated IPC and preload APIs to support passing occupied directories during the copy operation. - Modified the DataSettings component to utilize the new copy functionality with occupied directories. * fix: Improve occupied directories handling during data copy - Updated the filter logic in the `registerIpc` function to resolve directory paths correctly. - Modified the `DataSettings` component to pass the correct occupied directories format during the copy operation. * feat: add appcode (#7507) Co-authored-by: zhaochenxue * fix: non streamoutput sometimes (#7512) * feat(migrate): add default settings for assistants during migration - Introduced a new migration step to assign default settings for assistants that lack configuration. - Default settings include temperature, context count, and other parameters to ensure consistent behavior across the application. * chore(store): increment version number to 115 for persisted reducer * Revert "feat: Update API Key Management Interface (#3444)" This reverts commit 31b3ce1049a607d2448213e5bc1c1fc0f28231d2. * feat: 一些UI上的优化和重构 (#7479) - 调整AntdProvider中主题配置,包括颜色、尺寸 - 重构聊天气泡模式的样式 - 重构多选模式的样式 - 添加Selector组件取代ant Select组件 - 重构消息搜索弹窗界面 - 重构知识库搜索弹窗界面 - 优化其他弹框UI * fix: bailian reranker (#7518) * feat: implement Python MCP server using existing Pyodide infrastructure (#7506) * refactor: rename isWindows to isWin for consistency across main/renderer (#7530) refactor: rename isWindows to isWin for consistency across components * refactor: data migration modal logic in DataSettings (#7503) * refactor: data migration modal logic in DataSettings Moved showProgressModal and startMigration functions inside the useEffect hook and added t as a dependency. This improves encapsulation and ensures translation updates are handled correctly. * remove trailing whitespace in DataSettings.tsx Cleaned up a line by removing unnecessary trailing whitespace in the DataSettings component. * fix: clear search cache on resending (#7510) * fix: Resolve vllm bad request caused by always sending dimensions in embedding requests (#7525) fix(知识库): 将dimensions字段改为可选并修复相关逻辑 * feat: Support custom registry address when configuring mcp for npm & fix lint error (#7531) * feat: Support custom registry address when configuring mcp for npm * fix: lint * refactor(GeminiAPIClient): separate model and user message handling to adapt vertex (#7511) - Introduced a new modelParts array to manage model-related messages separately from user messages. - Updated the logic to push model messages to currentReqMessages only if they exist, improving clarity and structure. - Adjusted the return order of messages in buildSdkMessages to ensure history is appended correctly. - Enhanced McpToolChunkMiddleware to reset tool processing state output when output is present. * feat: enhance WindowFooter with show/hide functionality for UI elements - Added state management to control visibility of UI elements in the WindowFooter. - Implemented a timer to automatically hide elements after a period of inactivity. - Updated hotkey handlers to reset the visibility timer on user interaction. - Modified styled component to reflect the new visibility logic. * fix(SelectionAssistant): opacity slider too slow when sliding in settings page (#7537) feat: enhance opacity control in Selection Assistant Settings - Added state management for opacity value in SelectionAssistantSettings component. - Updated Slider component to use the new opacity state instead of the previous actionWindowOpacity variable. - Ensured onChangeComplete updates the actionWindowOpacity accordingly. * feat(AihubmixAPIClient): add getBaseURL method to handle client base URL retrieval * fix(migrate): restore upgradeChannel setting in migration logic - Reintroduced the upgradeChannel setting to the state during the migration process, ensuring it defaults to LATEST when applicable. - Adjusted the migration logic to maintain consistency in settings management. --------- Co-authored-by: 自由的世界人 <3196812536@qq.com> Co-authored-by: one Co-authored-by: chenxue Co-authored-by: zhaochenxue Co-authored-by: SuYao Co-authored-by: kangfenmao Co-authored-by: Teo Co-authored-by: Chen Tao <70054568+eeee0717@users.noreply.github.com> Co-authored-by: LiuVaayne <10231735+vaayne@users.noreply.github.com> Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com> Co-authored-by: Wang Jiyuan <59059173+EurFelux@users.noreply.github.com> Co-authored-by: 陈天寒 Co-authored-by: fullex <0xfullex@gmail.com> --- packages/shared/IpcChannel.ts | 3 +- packages/shared/config/constant.ts | 9 +- src/main/ipc.ts | 12 ++- src/main/services/AppUpdater.ts | 94 ++++++++++++++++--- src/main/services/ConfigManager.ts | 21 +++-- src/preload/index.ts | 5 +- src/renderer/src/hooks/useSettings.ts | 10 +- src/renderer/src/i18n/locales/en-us.json | 9 +- src/renderer/src/i18n/locales/ja-jp.json | 9 +- src/renderer/src/i18n/locales/ru-ru.json | 9 +- src/renderer/src/i18n/locales/zh-cn.json | 9 +- src/renderer/src/i18n/locales/zh-tw.json | 9 +- .../src/pages/settings/AboutSettings.tsx | 82 +++++++++++++++- src/renderer/src/store/migrate.ts | 4 + src/renderer/src/store/settings.ts | 7 ++ 15 files changed, 254 insertions(+), 38 deletions(-) diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index b77fd5d43..8da9a6742 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -15,7 +15,8 @@ export enum IpcChannel { App_SetTrayOnClose = 'app:set-tray-on-close', App_SetTheme = 'app:set-theme', App_SetAutoUpdate = 'app:set-auto-update', - App_SetFeedUrl = 'app:set-feed-url', + App_SetEnableEarlyAccess = 'app:set-enable-early-access', + App_SetUpgradeChannel = 'app:set-upgrade-channel', App_HandleZoomFactor = 'app:handle-zoom-factor', App_Select = 'app:select', App_HasWritePermission = 'app:has-write-permission', diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index 0b568461e..975767fef 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -406,8 +406,15 @@ export const defaultLanguage = 'en-US' export enum FeedUrl { PRODUCTION = 'https://releases.cherry-ai.com', - EARLY_ACCESS = 'https://github.com/CherryHQ/cherry-studio/releases/latest/download' + GITHUB_LATEST = 'https://github.com/CherryHQ/cherry-studio/releases/latest/download' } + +export enum UpgradeChannel { + LATEST = 'latest', // 最新稳定版本 + RC = 'rc', // 公测版本 + BETA = 'beta' // 预览版本 +} + export const defaultTimeout = 5 * 1000 * 60 export const occupiedDirs = ['logs', 'Network', 'Partitions/webview/Network'] diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 8d8690a54..b2003f8db 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -5,7 +5,7 @@ import path from 'node:path' import { isMac, isWin } from '@main/constant' import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process' import { handleZoomFactor } from '@main/utils/zoom' -import { FeedUrl } from '@shared/config/constant' +import { UpgradeChannel } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import { Shortcut, ThemeMode } from '@types' import { BrowserWindow, dialog, ipcMain, session, shell } from 'electron' @@ -141,8 +141,14 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { configManager.setAutoUpdate(isActive) }) - ipcMain.handle(IpcChannel.App_SetFeedUrl, (_, feedUrl: FeedUrl) => { - appUpdater.setFeedUrl(feedUrl) + ipcMain.handle(IpcChannel.App_SetEnableEarlyAccess, async (_, isActive: boolean) => { + appUpdater.cancelDownload() + configManager.setEnableEarlyAccess(isActive) + }) + + ipcMain.handle(IpcChannel.App_SetUpgradeChannel, async (_, channel: UpgradeChannel) => { + appUpdater.cancelDownload() + configManager.setUpgradeChannel(channel) }) ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => { diff --git a/src/main/services/AppUpdater.ts b/src/main/services/AppUpdater.ts index 4c71a0555..e26a779d5 100644 --- a/src/main/services/AppUpdater.ts +++ b/src/main/services/AppUpdater.ts @@ -1,8 +1,8 @@ import { isWin } from '@main/constant' import { locales } from '@main/utils/locales' -import { FeedUrl } from '@shared/config/constant' +import { FeedUrl, UpgradeChannel } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' -import { UpdateInfo } from 'builder-util-runtime' +import { CancellationToken, UpdateInfo } from 'builder-util-runtime' import { app, BrowserWindow, dialog } from 'electron' import logger from 'electron-log' import { AppUpdater as _AppUpdater, autoUpdater, NsisUpdater } from 'electron-updater' @@ -14,6 +14,7 @@ import { configManager } from './ConfigManager' export default class AppUpdater { autoUpdater: _AppUpdater = autoUpdater private releaseInfo: UpdateInfo | undefined + private cancellationToken: CancellationToken = new CancellationToken() constructor(mainWindow: BrowserWindow) { logger.transports.file.level = 'info' @@ -22,9 +23,7 @@ export default class AppUpdater { autoUpdater.forceDevUpdateConfig = !app.isPackaged autoUpdater.autoDownload = configManager.getAutoUpdate() autoUpdater.autoInstallOnAppQuit = configManager.getAutoUpdate() - autoUpdater.setFeedURL(configManager.getFeedUrl()) - // 检测下载错误 autoUpdater.on('error', (error) => { // 简单记录错误信息和时间戳 logger.error('更新异常', { @@ -64,6 +63,33 @@ export default class AppUpdater { this.autoUpdater = autoUpdater } + private async _getPreReleaseVersionFromGithub(channel: UpgradeChannel) { + try { + const responses = await fetch('https://api.github.com/repos/CherryHQ/cherry-studio/releases?per_page=8', { + headers: { + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + 'Accept-Language': 'en-US,en;q=0.9' + } + }) + const data = (await responses.json()) as GithubReleaseInfo[] + logger.debug('github release data', data) + const release: GithubReleaseInfo | undefined = data.find((item: GithubReleaseInfo) => { + return item.prerelease && item.tag_name.includes(`-${channel}.`) + }) + + if (!release) { + return null + } + + logger.info('release info', release.tag_name) + return `https://github.com/CherryHQ/cherry-studio/releases/download/${release.tag_name}` + } catch (error) { + logger.error('Failed to get latest not draft version from github:', error) + return null + } + } + private async _getIpCountry() { try { // add timeout using AbortController @@ -93,9 +119,44 @@ export default class AppUpdater { autoUpdater.autoInstallOnAppQuit = isActive } - public setFeedUrl(feedUrl: FeedUrl) { - autoUpdater.setFeedURL(feedUrl) - configManager.setFeedUrl(feedUrl) + private async _setFeedUrl() { + // disable downgrade and differential download + // github and gitcode don't support multiple range download + this.autoUpdater.allowDowngrade = false + this.autoUpdater.disableDifferentialDownload = true + + if (configManager.getEnableEarlyAccess()) { + const channel = configManager.getUpgradeChannel() + if (channel === UpgradeChannel.LATEST) { + this.autoUpdater.setFeedURL(FeedUrl.GITHUB_LATEST) + this.autoUpdater.channel = UpgradeChannel.LATEST + return true + } + + const preReleaseUrl = await this._getPreReleaseVersionFromGithub(channel) + if (preReleaseUrl) { + this.autoUpdater.setFeedURL(preReleaseUrl) + this.autoUpdater.channel = channel + return true + } + return false + } + + // no early access, use latest version + this.autoUpdater.channel = 'latest' + this.autoUpdater.setFeedURL(FeedUrl.PRODUCTION) + + const ipCountry = await this._getIpCountry() + logger.info('ipCountry', ipCountry) + if (ipCountry.toLowerCase() !== 'cn') { + this.autoUpdater.setFeedURL(FeedUrl.GITHUB_LATEST) + } + return true + } + + public cancelDownload() { + this.cancellationToken.cancel() + this.cancellationToken = new CancellationToken() } public async checkForUpdates() { @@ -106,10 +167,12 @@ export default class AppUpdater { } } - const ipCountry = await this._getIpCountry() - logger.info('ipCountry', ipCountry) - if (ipCountry !== 'CN') { - this.autoUpdater.setFeedURL(FeedUrl.EARLY_ACCESS) + const isSetFeedUrl = await this._setFeedUrl() + if (!isSetFeedUrl) { + return { + currentVersion: app.getVersion(), + updateInfo: null + } } try { @@ -117,7 +180,8 @@ export default class AppUpdater { if (update?.isUpdateAvailable && !this.autoUpdater.autoDownload) { // 如果 autoDownload 为 false,则需要再调用下面的函数触发下 // do not use await, because it will block the return of this function - this.autoUpdater.downloadUpdate() + logger.info('downloadUpdate manual by check for updates', this.cancellationToken) + this.autoUpdater.downloadUpdate(this.cancellationToken) } return { @@ -178,7 +242,11 @@ export default class AppUpdater { return releaseNotes.map((note) => note.note).join('\n') } } - +interface GithubReleaseInfo { + draft: boolean + prerelease: boolean + tag_name: string +} interface ReleaseNoteInfo { readonly version: string readonly note: string | null diff --git a/src/main/services/ConfigManager.ts b/src/main/services/ConfigManager.ts index 573674bd7..6d33b6e3d 100644 --- a/src/main/services/ConfigManager.ts +++ b/src/main/services/ConfigManager.ts @@ -1,4 +1,4 @@ -import { defaultLanguage, FeedUrl, ZOOM_SHORTCUTS } from '@shared/config/constant' +import { defaultLanguage, UpgradeChannel, ZOOM_SHORTCUTS } from '@shared/config/constant' import { LanguageVarious, Shortcut, ThemeMode } from '@types' import { app } from 'electron' import Store from 'electron-store' @@ -16,7 +16,8 @@ export enum ConfigKeys { ClickTrayToShowQuickAssistant = 'clickTrayToShowQuickAssistant', EnableQuickAssistant = 'enableQuickAssistant', AutoUpdate = 'autoUpdate', - FeedUrl = 'feedUrl', + EnableEarlyAccess = 'enableEarlyAccess', + UpgradeChannel = 'upgradeChannel', EnableDataCollection = 'enableDataCollection', SelectionAssistantEnabled = 'selectionAssistantEnabled', SelectionAssistantTriggerMode = 'selectionAssistantTriggerMode', @@ -142,12 +143,20 @@ export class ConfigManager { this.set(ConfigKeys.AutoUpdate, value) } - getFeedUrl(): string { - return this.get(ConfigKeys.FeedUrl, FeedUrl.PRODUCTION) + getEnableEarlyAccess(): boolean { + return this.get(ConfigKeys.EnableEarlyAccess, false) } - setFeedUrl(value: FeedUrl) { - this.set(ConfigKeys.FeedUrl, value) + setEnableEarlyAccess(value: boolean) { + this.set(ConfigKeys.EnableEarlyAccess, value) + } + + getUpgradeChannel(): UpgradeChannel { + return this.get(ConfigKeys.UpgradeChannel, UpgradeChannel.LATEST) + } + + setUpgradeChannel(value: UpgradeChannel) { + this.set(ConfigKeys.UpgradeChannel, value) } getEnableDataCollection(): boolean { diff --git a/src/preload/index.ts b/src/preload/index.ts index 3e9ae532b..ed2a2042e 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,6 +1,6 @@ import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces' import { electronAPI } from '@electron-toolkit/preload' -import { FeedUrl } from '@shared/config/constant' +import { UpgradeChannel } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import { FileType, KnowledgeBaseParams, KnowledgeItem, MCPServer, Shortcut, ThemeMode, WebDavConfig } from '@types' import { contextBridge, ipcRenderer, OpenDialogOptions, shell, webUtils } from 'electron' @@ -23,7 +23,8 @@ const api = { setLaunchToTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetLaunchToTray, isActive), setTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTray, isActive), setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive), - setFeedUrl: (feedUrl: FeedUrl) => ipcRenderer.invoke(IpcChannel.App_SetFeedUrl, feedUrl), + setEnableEarlyAccess: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetEnableEarlyAccess, isActive), + setUpgradeChannel: (channel: UpgradeChannel) => ipcRenderer.invoke(IpcChannel.App_SetUpgradeChannel, channel), setTheme: (theme: ThemeMode) => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme), handleZoomFactor: (delta: number, reset: boolean = false) => ipcRenderer.invoke(IpcChannel.App_HandleZoomFactor, delta, reset), diff --git a/src/renderer/src/hooks/useSettings.ts b/src/renderer/src/hooks/useSettings.ts index 10f9ee900..72560706e 100644 --- a/src/renderer/src/hooks/useSettings.ts +++ b/src/renderer/src/hooks/useSettings.ts @@ -17,10 +17,11 @@ import { setTopicPosition, setTray as _setTray, setTrayOnClose, + setUpgradeChannel as _setUpgradeChannel, setWindowStyle } from '@renderer/store/settings' import { SidebarIcon, ThemeMode, TranslateLanguageVarious } from '@renderer/types' -import { FeedUrl } from '@shared/config/constant' +import { UpgradeChannel } from '@shared/config/constant' export function useSettings() { const settings = useAppSelector((state) => state.settings) @@ -62,7 +63,12 @@ export function useSettings() { setEarlyAccess(isEarlyAccess: boolean) { dispatch(_setEarlyAccess(isEarlyAccess)) - window.api.setFeedUrl(isEarlyAccess ? FeedUrl.EARLY_ACCESS : FeedUrl.PRODUCTION) + window.api.setEnableEarlyAccess(isEarlyAccess) + }, + + setUpgradeChannel(channel: UpgradeChannel) { + dispatch(_setUpgradeChannel(channel)) + window.api.setUpgradeChannel(channel) }, setTheme(theme: ThemeMode) { diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 303899f2e..b58056ae9 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1385,7 +1385,14 @@ "general.image_upload": "Image Upload", "general.auto_check_update.title": "Auto Update", "general.early_access.title": "Early Access", - "general.early_access.tooltip": "Enable to use the latest version from GitHub, which may be slower. Please backup your data in advance.", + "general.early_access.tooltip": "Updating to test versions cannot be downgraded, there is a risk of data loss, please backup your data in advance", + "general.early_access.beta_version": "Beta Version", + "general.early_access.rc_version": "RC Version", + "general.early_access.latest_version": "Latest Version", + "general.early_access.latest_version_tooltip": "github latest version, latest stable version", + "general.early_access.version_options": "Version Options", + "general.early_access.rc_version_tooltip": "More stable, please backup your data", + "general.early_access.beta_version_tooltip": "Latest features but unstable, use with caution", "general.reset.button": "Reset", "general.reset.title": "Data Reset", "general.restore.button": "Restore", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index bd20c61ea..64f6a9847 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1830,7 +1830,14 @@ }, "general.auto_check_update.title": "自動更新", "general.early_access.title": "早期アクセス", - "general.early_access.tooltip": "有効にすると、GitHub の最新バージョンを使用します。ダウンロード速度が遅く、不安定な場合があります。データを事前にバックアップしてください。", + "general.early_access.tooltip": "更新すると、データが失われる可能性があります。データを事前にバックアップしてください。", + "general.early_access.beta_version": "ベータ版", + "general.early_access.rc_version": "RC版", + "general.early_access.latest_version": "最新版", + "general.early_access.latest_version_tooltip": "github latest バージョン, 最新安定版", + "general.early_access.version_options": "バージョンオプション", + "general.early_access.rc_version_tooltip": "より安定しています。データを事前にバックアップしてください。", + "general.early_access.beta_version_tooltip": "最新の機能ですが、不安定な場合があります。使用には注意してください。", "quickPhrase": { "title": "クイックフレーズ", "add": "フレーズを追加", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index fcd5487b9..e027e7270 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1830,7 +1830,14 @@ }, "general.auto_check_update.title": "Автоматическое обновление", "general.early_access.title": "Ранний доступ", - "general.early_access.tooltip": "Включить для использования последней версии из GitHub, что может быть медленнее и нестабильно. Пожалуйста, сделайте резервную копию данных заранее.", + "general.early_access.tooltip": "Обновление до тестовых версий не может быть откачено, существует риск потери данных, пожалуйста, сделайте резервную копию данных заранее", + "general.early_access.beta_version": "Бета версия", + "general.early_access.rc_version": "RC версия", + "general.early_access.latest_version": "Стабильная версия", + "general.early_access.latest_version_tooltip": "github latest версия, стабильная версия", + "general.early_access.version_options": "Варианты версии", + "general.early_access.rc_version_tooltip": "Более стабильно, пожалуйста, сделайте резервную копию данных заранее", + "general.early_access.beta_version_tooltip": "Самые последние функции, но нестабильно, используйте с осторожностью", "quickPhrase": { "title": "Быстрые фразы", "add": "Добавить фразу", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 9bc4201f7..894d53c2b 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1385,7 +1385,14 @@ "general.image_upload": "图片上传", "general.auto_check_update.title": "自动更新", "general.early_access.title": "抢先体验", - "general.early_access.tooltip": "开启后,将使用 GitHub 的最新版本,下载速度可能较慢,请务必提前备份数据", + "general.early_access.tooltip": "更新到测试版本不能降级,有数据丢失风险,请务必提前备份数据", + "general.early_access.beta_version": "预览版本", + "general.early_access.rc_version": "公测版本", + "general.early_access.latest_version": "稳定版本", + "general.early_access.version_options": "版本选择", + "general.early_access.rc_version_tooltip": "相对稳定,请备份数据", + "general.early_access.beta_version_tooltip": "功能最新但不稳定,谨慎使用", + "general.early_access.latest_version_tooltip": "github latest 版本, 最新稳定版本", "general.reset.button": "重置", "general.reset.title": "重置数据", "general.restore.button": "恢复", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index e99c57e17..65a3a3f58 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1833,7 +1833,14 @@ }, "general.auto_check_update.title": "自動更新", "general.early_access.title": "搶先體驗", - "general.early_access.tooltip": "開啟後,將使用 GitHub 的最新版本,下載速度可能較慢,請務必提前備份數據", + "general.early_access.tooltip": "更新到測試版本不能降級,有數據丟失風險,請務必提前備份數據", + "general.early_access.beta_version": "預覽版本", + "general.early_access.rc_version": "公測版本", + "general.early_access.latest_version": "穩定版本", + "general.early_access.latest_version_tooltip": "github latest 版本, 最新穩定版本", + "general.early_access.version_options": "版本選項", + "general.early_access.rc_version_tooltip": "相對穩定,請務必提前備份數據", + "general.early_access.beta_version_tooltip": "功能最新但不穩定,謹慎使用", "quickPhrase": { "title": "快捷短語", "add": "新增短語", diff --git a/src/renderer/src/pages/settings/AboutSettings.tsx b/src/renderer/src/pages/settings/AboutSettings.tsx index 82f11ec7d..9bf65e448 100644 --- a/src/renderer/src/pages/settings/AboutSettings.tsx +++ b/src/renderer/src/pages/settings/AboutSettings.tsx @@ -10,7 +10,8 @@ import { useAppDispatch } from '@renderer/store' import { setUpdateState } from '@renderer/store/runtime' import { ThemeMode } from '@renderer/types' import { compareVersions, runAsyncFunction } from '@renderer/utils' -import { Avatar, Button, Progress, Row, Switch, Tag, Tooltip } from 'antd' +import { UpgradeChannel } from '@shared/config/constant' +import { Avatar, Button, Progress, Radio, Row, Switch, Tag, Tooltip } from 'antd' import { debounce } from 'lodash' import { Bug, FileCheck, Github, Globe, Mail, Rss } from 'lucide-react' import { FC, useEffect, useState } from 'react' @@ -25,7 +26,8 @@ const AboutSettings: FC = () => { const [version, setVersion] = useState('') const [isPortable, setIsPortable] = useState(false) const { t } = useTranslation() - const { autoCheckUpdate, setAutoCheckUpdate, earlyAccess, setEarlyAccess } = useSettings() + const { autoCheckUpdate, setAutoCheckUpdate, earlyAccess, setEarlyAccess, upgradeChannel, setUpgradeChannel } = + useSettings() const { theme } = useTheme() const dispatch = useAppDispatch() const { update } = useRuntime() @@ -95,15 +97,65 @@ const AboutSettings: FC = () => { const hasNewVersion = update?.info?.version && version ? compareVersions(update.info.version, version) > 0 : false + const handleUpgradeChannelChange = async (value: UpgradeChannel) => { + setUpgradeChannel(value) + // Clear update info when switching upgrade channel + dispatch( + setUpdateState({ + available: false, + info: null, + downloaded: false, + checking: false, + downloading: false, + downloadProgress: 0 + }) + ) + } + + // Get available test version options based on current version + const getAvailableTestChannels = () => { + return [ + { + tooltip: t('settings.general.early_access.latest_version_tooltip'), + label: t('settings.general.early_access.latest_version'), + value: UpgradeChannel.LATEST + }, + { + tooltip: t('settings.general.early_access.rc_version_tooltip'), + label: t('settings.general.early_access.rc_version'), + value: UpgradeChannel.RC + }, + { + tooltip: t('settings.general.early_access.beta_version_tooltip'), + label: t('settings.general.early_access.beta_version'), + value: UpgradeChannel.BETA + } + ] + } + + const handlerSetEarlyAccess = (value: boolean) => { + setEarlyAccess(value) + dispatch( + setUpdateState({ + available: false, + info: null, + downloaded: false, + checking: false, + downloading: false, + downloadProgress: 0 + }) + ) + if (value === false) setUpgradeChannel(UpgradeChannel.LATEST) + } + useEffect(() => { runAsyncFunction(async () => { const appInfo = await window.api.getAppInfo() setVersion(appInfo.version) setIsPortable(appInfo.isPortable) }) - setEarlyAccess(earlyAccess) setAutoCheckUpdate(autoCheckUpdate) - }, [autoCheckUpdate, earlyAccess, setAutoCheckUpdate, setEarlyAccess]) + }, [autoCheckUpdate, setAutoCheckUpdate, setEarlyAccess]) return ( @@ -167,9 +219,29 @@ const AboutSettings: FC = () => { {t('settings.general.early_access.title')} - setEarlyAccess(v)} /> + handlerSetEarlyAccess(v)} /> + {earlyAccess && getAvailableTestChannels().length > 0 && ( + <> + + + {t('settings.general.early_access.version_options')} + handleUpgradeChannelChange(e.target.value)}> + {getAvailableTestChannels().map((option) => ( + + {option.label} + + ))} + + + + )} )} diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index df89be66b..0e9385de0 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -7,6 +7,7 @@ import db from '@renderer/databases' import i18n from '@renderer/i18n' import { Assistant, Provider, WebSearchProvider } from '@renderer/types' import { getDefaultGroupName, getLeadingEmoji, runAsyncFunction, uuid } from '@renderer/utils' +import { UpgradeChannel } from '@shared/config/constant' import { isEmpty } from 'lodash' import { createMigrate } from 'redux-persist' @@ -1627,6 +1628,9 @@ const migrateConfig = { } } }) + if (state.settings) { + state.settings.upgradeChannel = UpgradeChannel.LATEST + } return state } catch (error) { return state diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index d4014adf2..ee991556f 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -11,6 +11,7 @@ import { ThemeMode, TranslateLanguageVarious } from '@renderer/types' +import { UpgradeChannel } from '@shared/config/constant' import { WebDAVSyncState } from './backup' @@ -68,6 +69,7 @@ export interface SettingsState { clickAssistantToShowTopic: boolean autoCheckUpdate: boolean earlyAccess: boolean + upgradeChannel: UpgradeChannel renderInputMessageAsMarkdown: boolean // 代码执行 codeExecution: { @@ -221,6 +223,7 @@ export const initialState: SettingsState = { clickAssistantToShowTopic: true, autoCheckUpdate: true, earlyAccess: false, + upgradeChannel: UpgradeChannel.LATEST, renderInputMessageAsMarkdown: false, codeExecution: { enabled: false, @@ -429,6 +432,9 @@ const settingsSlice = createSlice({ setEarlyAccess: (state, action: PayloadAction) => { state.earlyAccess = action.payload }, + setUpgradeChannel: (state, action: PayloadAction) => { + state.upgradeChannel = action.payload + }, setRenderInputMessageAsMarkdown: (state, action: PayloadAction) => { state.renderInputMessageAsMarkdown = action.payload }, @@ -725,6 +731,7 @@ export const { setPasteLongTextAsFile, setAutoCheckUpdate, setEarlyAccess, + setUpgradeChannel, setRenderInputMessageAsMarkdown, setClickAssistantToShowTopic, setSkipBackupFile, From 8723bbeaf8a21a65688787a5f643a4d181244e55 Mon Sep 17 00:00:00 2001 From: one Date: Thu, 26 Jun 2025 15:52:58 +0800 Subject: [PATCH 6/9] fix(Markdown): falsely early return for display `\[\n...\n\]` (#7565) --- .../src/utils/__tests__/markdown.test.ts | 32 +++++++++++++++++-- src/renderer/src/utils/markdown.ts | 4 +-- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/utils/__tests__/markdown.test.ts b/src/renderer/src/utils/__tests__/markdown.test.ts index 4f48deba0..f16509ced 100644 --- a/src/renderer/src/utils/__tests__/markdown.test.ts +++ b/src/renderer/src/utils/__tests__/markdown.test.ts @@ -465,10 +465,28 @@ describe('markdown', () => { describe('processLatexBrackets', () => { describe('basic LaTeX conversion', () => { - it('should convert display math \\[...\\] to $$...$$', () => { + it('should convert (inline) display math \\[...\\] to $$...$$', () => { expect(processLatexBrackets('The formula is \\[a+b=c\\]')).toBe('The formula is $$a+b=c$$') }) + it('should convert display math \\[...\\] to $$...$$', () => { + const input = ` +The formula is + +\\[ +a+b=c +\\] +` + const expected = ` +The formula is + +$$ +a+b=c +$$ +` + expect(processLatexBrackets(input)).toBe(expected) + }) + it('should convert inline math \\(...\\) to $...$', () => { expect(processLatexBrackets('The formula is \\(a+b=c\\)')).toBe('The formula is $a+b=c$') }) @@ -611,9 +629,13 @@ const func = \\(x\\) => x * 2; Read more in [Section \\[3.2\\]: Advanced Topics](url) and see inline code \`\\[array\\]\`. -Final thoughts on \\(\\nabla \\cdot \\vec{F} = \\rho\\) and display math: +Final thoughts on \\(\\nabla \\cdot \\vec{F} = \\rho\\) in inline math and display math: \\[\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}\\] + +\\[ +\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2} +\\] ` const expectedOutput = ` @@ -647,9 +669,13 @@ const func = \\(x\\) => x * 2; Read more in [Section \\[3.2\\]: Advanced Topics](url) and see inline code \`\\[array\\]\`. -Final thoughts on $\\nabla \\cdot \\vec{F} = \\rho$ and display math: +Final thoughts on $\\nabla \\cdot \\vec{F} = \\rho$ in inline math and display math: $$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$ + +$$ +\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2} +$$ ` expect(processLatexBrackets(complexInput)).toBe(expectedOutput) diff --git a/src/renderer/src/utils/markdown.ts b/src/renderer/src/utils/markdown.ts index 57025ca63..4fc499300 100644 --- a/src/renderer/src/utils/markdown.ts +++ b/src/renderer/src/utils/markdown.ts @@ -1,5 +1,5 @@ import { languages } from '@shared/config/languages' -import balanced from 'balanced-match' +import { default as balanced } from 'balanced-match' import remarkParse from 'remark-parse' import remarkStringify from 'remark-stringify' import removeMarkdown from 'remove-markdown' @@ -31,7 +31,7 @@ export const findCitationInChildren = (children: any): string => { } // 检查是否包含潜在的 LaTeX 模式 -const containsLatexRegex = /\\\(.*?\\\)|\\\[.*?\\\]|\$.*?\$|\\begin\{equation\}.*?\\end\{equation\}/ +const containsLatexRegex = /\\\(.*?\\\)|\\\[.*?\\\]|\$.*?\$|\\begin\{equation\}.*?\\end\{equation\}/s /** * 转换 LaTeX 公式括号 `\[\]` 和 `\(\)` 为 Markdown 格式 `$$...$$` 和 `$...$` From 0160655dbaa7722f1bb2c3f030bd04062a3bf1d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Thu, 26 Jun 2025 16:48:56 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat(FileStorage):=20enhance=20open=20dialo?= =?UTF-8?q?g=20to=20handle=20large=20files=20by=20retur=E2=80=A6=20(#7568)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(FileStorage): enhance open dialog to handle large files by returning size without reading content - Updated the open method to return file size for files larger than 2GB without reading their content. - Modified return type to include an optional content field and size property for better file handling. 修复恢复备份的时候选择超过 2GB 文件报错的问题 --- src/main/services/FileStorage.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts index 437f25f78..2d8810adc 100644 --- a/src/main/services/FileStorage.ts +++ b/src/main/services/FileStorage.ts @@ -363,7 +363,7 @@ class FileStorage { public open = async ( _: Electron.IpcMainInvokeEvent, options: OpenDialogOptions - ): Promise<{ fileName: string; filePath: string; content: Buffer } | null> => { + ): Promise<{ fileName: string; filePath: string; content?: Buffer; size: number } | null> => { try { const result: OpenDialogReturnValue = await dialog.showOpenDialog({ title: '打开文件', @@ -375,8 +375,16 @@ class FileStorage { if (!result.canceled && result.filePaths.length > 0) { const filePath = result.filePaths[0] const fileName = filePath.split('/').pop() || '' - const content = await readFile(filePath) - return { fileName, filePath, content } + const stats = await fs.promises.stat(filePath) + + // If the file is less than 2GB, read the content + if (stats.size < 2 * 1024 * 1024 * 1024) { + const content = await readFile(filePath) + return { fileName, filePath, content, size: stats.size } + } + + // For large files, only return file information, do not read content + return { fileName, filePath, size: stats.size } } return null From f5165e12f1861e849ce9ec278765a83b7b1c9a1d Mon Sep 17 00:00:00 2001 From: Teo Date: Thu, 26 Jun 2025 17:05:48 +0800 Subject: [PATCH 8/9] fix(Messages): Fix single model response style issue (#7560) * fix(Messages): update multiModelMessageStyle condition to check message count * style(Messages): update styles for MultiSelectionPopup and MessageGroup components --- .../components/Popups/MultiSelectionPopup.tsx | 2 +- .../src/pages/home/Messages/MessageGroup.tsx | 25 ++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/components/Popups/MultiSelectionPopup.tsx b/src/renderer/src/components/Popups/MultiSelectionPopup.tsx index f277fbe3a..31c9877ec 100644 --- a/src/renderer/src/components/Popups/MultiSelectionPopup.tsx +++ b/src/renderer/src/components/Popups/MultiSelectionPopup.tsx @@ -90,7 +90,7 @@ const ActionBar = styled.div` background-color: var(--color-background); padding: 4px 4px; border-radius: 99px; - box-shadow: 0 0px 5px 0px rgb(128 128 128 / 30%); + box-shadow: 0px 2px 8px 0px rgb(128 128 128 / 20%); border: 0.5px solid var(--color-border); gap: 16px; ` diff --git a/src/renderer/src/pages/home/Messages/MessageGroup.tsx b/src/renderer/src/pages/home/Messages/MessageGroup.tsx index 995540a6c..9d0fb1c6f 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroup.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroup.tsx @@ -173,7 +173,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { key={message.id} className={classNames([ { - [multiModelMessageStyle]: message.role === 'assistant', + [multiModelMessageStyle]: message.role === 'assistant' && messages.length > 1, selected: message.id === selectedMessageId } ])}> @@ -191,7 +191,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { className={classNames([ 'in-popover', { - [multiModelMessageStyle]: message.role === 'assistant', + [multiModelMessageStyle]: message.role === 'assistant' && messages.length > 1, selected: message.id === selectedMessageId } ])}> @@ -210,7 +210,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => { return messageContent }, - [isGrid, isGrouped, topic, multiModelMessageStyle, selectedMessageId, gridPopoverTrigger] + [isGrid, isGrouped, topic, multiModelMessageStyle, messages.length, selectedMessageId, gridPopoverTrigger] ) return ( @@ -284,6 +284,9 @@ const GridContainer = styled(Scrollbar)<{ $count: number; $gridColumns: number } &.multi-select-mode { grid-template-columns: repeat(1, minmax(0, 1fr)); gap: 10px; + .grid { + height: auto; + } .message { border: 0.5px solid var(--color-border); border-radius: 10px; @@ -307,10 +310,12 @@ const MessageWrapper = styled.div` &.horizontal { overflow-y: auto; .message { + height: 100%; border: 0.5px solid var(--color-border); border-radius: 10px; } .message-content-container { + flex: 1; padding-left: 0; max-height: calc(100vh - 350px); overflow-y: auto !important; @@ -328,6 +333,20 @@ const MessageWrapper = styled.div` border: 0.5px solid var(--color-border); border-radius: 10px; cursor: pointer; + .message { + height: 100%; + } + .message-content-container { + overflow: hidden; + padding-left: 0; + flex: 1; + pointer-events: none; + } + .MessageFooter { + margin-left: 0; + margin-top: 2px; + margin-bottom: 2px; + } } &.in-popover { height: auto; From 46de46965fd85b4c942b0d703efac4c3eb748d8a Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 26 Jun 2025 18:19:27 +0800 Subject: [PATCH 9/9] chore(version): 1.4.6 --- electron-builder.yml | 13 +++++++------ package.json | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/electron-builder.yml b/electron-builder.yml index 0da2d7f5f..d1a70bf89 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -107,9 +107,10 @@ afterSign: scripts/notarize.js artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | - - 新功能:可选数据保存目录 - - 快捷助手:支持单独选择助手,支持暂停、上下文、思考过程、流式 - - 划词助手:系统托盘菜单开关 - - 翻译:新增 Markdown 预览选项 - - 新供应商:新增 Vertex AI 服务商 - - 错误修复和界面优化 + 界面优化:优化多处界面样式,气泡样式改版,自动调整代码预览边栏宽度 + 知识库:修复知识库引用不显示问题,修复部分嵌入模型适配问题 + 备份与恢复:修复超过 2GB 大文件无法恢复问题 + 文件处理:添加 .doc 文件支持 + 划词助手:支持自定义 CSS 样式 + MCP:基于 Pyodide 实现 Python MCP 服务 + 其他错误修复和优化 diff --git a/package.json b/package.json index ee795f17e..6127aa6f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.4.5", + "version": "1.4.6", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js",