Compare commits

...

193 Commits

Author SHA1 Message Date
suyao
e4434eb7c8 feat: add settings window functionality and shortcuts
- Introduced a new IPC channel for showing the settings window.
- Registered IPC handler for the settings window in the main process.
- Updated shortcut service to include a shortcut for opening the settings window.
- Modified the settings popup to use the new IPC method for displaying the settings.
- Adjusted the main sidebar to navigate to the settings window using the new IPC call.
- Enhanced the navbar to accommodate the new settings window functionality.
2025-07-11 16:05:57 +08:00
suyao
762732af9d Merge branch 'main' into feat/sidebar-ui 2025-07-11 13:55:51 +08:00
kangfenmao
5b9ff3053b refactor: update styles and layout in markdown and message components
- Removed unnecessary letter and word spacing in markdown styles.
- Adjusted padding in Inputbar for improved layout.
- Modified margin properties in CitationsList and Message components for consistency.
- Enhanced MessageHeader logic to conditionally hide based on message type.
- Updated icon sizes in MessageMenubar for better alignment.
- Added margin adjustments in ThinkingBlock for improved spacing.
2025-07-11 11:33:20 +08:00
SuYao
8340922263 fix: smartblock update not persist to db (#8046)
* chore(version): 1.4.10

* feat: enhance ThinkingTagExtractionMiddleware and update smartBlockUpdate function

- Added support for THINKING_START and TEXT_START chunk types in ThinkingTagExtractionMiddleware.
- Updated smartBlockUpdate function to include an isComplete parameter for better block state management.
- Ensured proper handling of block updates based on completion status across various message types.

* fix: refine block update logic in messageThunk

- Adjusted conditions for canceling throttled block updates based on block type changes and completion status.
- Improved handling of block updates to ensure accurate state management during message processing.

* chore: add comment

* fix: update message block status handling

- Changed the status of image blocks from STREAMING to PENDING to better reflect the processing state.
- Refined logic in OpenAIResponseAPIClient to ensure user messages are correctly handled based on assistant message content.
- Improved rendering conditions in ImageBlock component for better user experience during image loading.

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>
2025-07-11 11:33:05 +08:00
one
a93cab6b43 fix(CodePreview): revert to absolute positioning (#7980)
* fix(CodePreview): revert to absolute positioning

* fix: add min width to codeblockview
2025-07-11 11:06:21 +08:00
one
9a81c400ab fix: sticky code toolbar (#8012)
fix: sticky code toolbar for single-model message
2025-07-11 11:05:08 +08:00
西街工坊
808a22d5c6 fix(Doc2xPreprocessProvider): replace filePath split with path.parse… (#8042) 2025-07-11 11:03:05 +08:00
kangfenmao
10e512f32e chore(version): 1.4.10 2025-07-10 23:31:03 +08:00
one
4d75515bd6 refactor: raise the max count of document chunks from 30 to 50 (#7863)
* refactor: raise the max count of document chunks from 30 to 70

- Raise the max count of document chunks count
- Update i18n for websearch rag for consistency

* refactor: lower the count to 50
2025-07-10 22:52:18 +08:00
kangfenmao
3d6c84de6d refactor: improve styling and layout in MessageTools and Prompt components
- Adjusted spacing and border styles in MessageTools for better alignment.
- Updated margin and border properties in Prompt for consistent UI.
- Enhanced background color handling in ToolContentWrapper based on status.
2025-07-10 22:36:48 +08:00
SuYao
3dd393b840 fix: azure-openai (#7978) 2025-07-10 22:17:20 +08:00
LiuVaayne
8f86c53941 feat: implement MCP tool auto-approve functionality (#8007)
*  feat: implement MCP tool auto-approve functionality

- Add auto-approve toggle for MCP tools in settings
- Add improved UI for tool approval with Run/Cancel/Auto-approve buttons
- Add internationalization support for tool approval interface
- Update tool confirmation logic to support auto-approved tools
- Enhance tool status indicators and button styling
- Add disabledAutoApproveTools configuration for MCP servers

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: use table for mcp tools setting

* refactor: improve styles, add missing i18n

* refactor: extract renderStatusIndicator, reuse colors

* refactor: simplify the table

* feat: auto approve same tool in a turn

* feat(i18n): add confirmation tooltip for auto-approve tool in multiple languages

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: one <wangan.cs@gmail.com>
Co-authored-by: suyao <sy20010504@gmail.com>
Co-authored-by: Teo <cheesen.xu@gmail.com>
2025-07-10 22:14:52 +08:00
Phantom
a7b78c547a fix(encoding): encoding detection and decoding logic (#8024) 2025-07-10 22:13:40 +08:00
Chen Tao
bcc1046cdf feat: add upload file (#8035) 2025-07-10 22:06:09 +08:00
Phantom
c05c06b7a1 fix: VoyageEmbeddings (#8034)
* fix(embeddings): 修复VoyageAI嵌入格式和模型验证错误

修复OpenAIBaseClient中VoyageAI提供商的embedding格式设置问题
完善VoyageEmbeddings模型验证的错误提示信息

* refactor(embeddings): 移除VoyageEmbeddings的模型维度限制检查

简化VoyageEmbeddings的创建逻辑,不再对支持的模型维度进行校验

* fix(embeddings): 修复VoyageEmbeddings模型维度设置问题

修复VoyageEmbeddings中未正确校验模型是否支持设置outputDimension的问题
当provider为voyageai且模型不支持设置dimensions时,自动忽略传入的dimensions参数

* refactor(embeddings): 集中管理支持设置维度的模型列表

将各嵌入模型支持设置维度的模型列表集中到utils模块
不再让VoyageEmbeddings中getDimensions抛出错误,而是自动修复
2025-07-10 21:53:37 +08:00
Phantom
446ebae175 feat(ui): better infinite context (#8021)
* feat(上下文): 添加最大上下文数量限制及显示组件

- 在常量配置中添加 MAX_CONTEXT_COUNT
- 创建 MaxContextCount 组件用于显示无限上下文标识
- 在相关组件中替换硬编码的上下文最大值
- 优化 TokenCount 组件的上下文计数显示样式

* refactor(常量): 添加UNLIMITED_CONTEXT_COUNT常量并替换硬编码值

使用UNLIMITED_CONTEXT_COUNT常量替代多处硬编码的100000值,提高代码可维护性

* refactor(Inputbar): 使用 SlashSeparatorSpan 组件替换内联样式

将 TokenCount.tsx 中的斜杠分隔符内联样式替换为 SlashSeparatorSpan 组件,提高代码可维护性

* fix: 为 InfinityIcon 添加 aria-label 并统一样式
2025-07-10 21:51:31 +08:00
one
ba742b7b1f feat: save to knowledge (#7528)
* feat: save to knowledge

* refactor: simplify checkbox

* feat(i18n): add 'Save to Local File' translation key for multiple languages

---------

Co-authored-by: suyao <sy20010504@gmail.com>
2025-07-10 21:34:01 +08:00
fullex
7c6db809bb fix(SelectionAssistant): [macOS] show actionWindow on fullscreen app (#8004)
* feat(SelectionService): enhance action window handling for macOS fullscreen mode

- Updated processAction and showActionWindow methods to support fullscreen mode on macOS.
- Added isFullScreen parameter to manage action window visibility and positioning.
- Improved action window positioning logic to ensure it remains within screen boundaries.
- Adjusted IPC channel to pass fullscreen state from the renderer to the service.
- Updated SelectionToolbar to track fullscreen state and pass it to the action processing function.

* chore(deps): update selection-hook to version 1.0.6 in package.json and yarn.lock

* fix(SelectionService): improve macOS fullscreen handling and action window focus

- Added app import to manage dock visibility on macOS.
- Enhanced fullscreen handling logic to ensure the dock icon is restored correctly.
- Updated action window focus behavior to prevent unintended hiding when blurred.
- Refactored SelectionActionApp to streamline auto pinning logic and remove redundant useEffect.
- Cleaned up SelectionToolbar by removing unnecessary window size updates when demo is false.

* refactor(SelectionService): remove commented-out code for clarity

* refactor(SelectionService): streamline macOS handling and improve code clarity
2025-07-10 20:41:01 +08:00
Konv Suu
855499681f feat: add confirm for unsaved content in creating agent (#7965) 2025-07-10 19:37:18 +08:00
one
92be3c0f56 chore: update vscode settings (#7974)
* chore: update vscode settings

* refactor: add editorconfig to extensions
2025-07-10 19:34:57 +08:00
one
2a72f391b7 feat: codeblock dot language (#6783)
* feat(CodeBlock): support dot language in code block

- render DOT using @viz-js/viz
- highlight DOT using @viz-js/lang-dot (CodeEditor only)
- extract a special view map, update file structure
- extract and reuse the PreviewError component across special views
- update dependencies, fix peer dependencies

* chore: prepare for merge
2025-07-10 19:32:51 +08:00
SuYao
db642f0837 feat(models): support Grok4 (#8032)
refactor(models): rename and enhance reasoning model functions for clarity and functionality
2025-07-10 19:27:53 +08:00
kangfenmao
fca93b6c51 style: update various component styles for improved layout and readability
- Adjusted color for list items in color.scss for better contrast.
- Modified line-height and margins in markdown.scss for improved text readability.
- Changed height property in FloatingSidebar.tsx for consistent layout.
- Increased padding in AgentsPage.tsx for better spacing.
- Updated padding and border-radius in Inputbar.tsx for enhanced aesthetics.
- Reduced margin in MessageHeader.tsx for tighter layout.
- Refactored GroupTitle styles in AssistantsTab.tsx for better alignment and spacing.
2025-07-10 18:59:00 +08:00
one
7e672d86e7 refactor: do not jump on enabling content search (#7922)
* fix: content search count on enable

* refactor(ContentSearch): do not jump on enabling content search

* refactor: simplify result count
2025-07-10 17:29:43 +08:00
SuYao
e9112cad0f fix(McpToolChunkMiddleware): add logging for tool calls and enhance l… (#8028)
fix(McpToolChunkMiddleware): add logging for tool calls and enhance lookup logic
2025-07-10 17:26:57 +08:00
one
ffbd6445df refactor(Inputbar): make button tooltips disappear faster (#8011) 2025-07-10 17:26:38 +08:00
Alaina Hardie
dff44f2721 Fix: Require typechecking for Mac and Linux target builds (#7219)
fix: Mac builds do not auto-run typecheck, but Windows builds do. This requires an extra manual step when building for Mac.

Update build scripts in package.json to use `npm run build` directly for Mac and Linux targets..
2025-07-10 17:01:31 +08:00
SuYao
3afa81eb5d fix(Anthropic): content truncation (#7942)
* fix(Anthropic): content truncation

* feat: add start event and fix content truncation

* fix (gemini): some event

* revert: index.tsx

* revert(messageThunk): error block

* fix: ci

* chore: unuse log
2025-07-10 16:58:35 +08:00
SuYao
3350c3e2e5 fix(GeminiAPIClient, mcp-tools): enhance tool call handling and lookup logic (#8009)
* fix(GeminiAPIClient, mcp-tools): enhance tool call handling and lookup logic

* fix: unuse log
2025-07-10 15:16:23 +08:00
SuYao
f85f46c248 fix(middleware): ollama qwen think (#8026)
refactor(AiProvider): comment out unnecessary middleware removal for performance optimization

- Commented out the removal of ThinkingTagExtractionMiddlewareName to prevent potential performance degradation while maintaining existing functionality.
- Retained the removal of ThinkChunkMiddlewareName as part of the existing logic for non-reasoning scenarios.
2025-07-10 15:15:38 +08:00
SuYao
05f3b88f30 fix(Inputbar): update resizeTextArea call to improve functionality (#8010) 2025-07-10 15:15:13 +08:00
自由的世界人
f8c6b5c05f Fix translation key for unlimited backups label (#7987)
Updated the translation key for the 'unlimited' backups option in WebDavSettings to use the correct namespace.
2025-07-10 15:09:59 +08:00
Jason Young
97dbfe492e test: enhance download and fetch utility test coverage with bug fix (#7891)
* test: enhance download and fetch utility test coverage

- Add MIME type handling tests for data URLs in download.test.ts
- Add timestamp generation tests for blob and network downloads
- Add Content-Type header handling test for extensionless files
- Add format parameter tests (markdown/html/text) for fetchWebContent
- Add timeout signal handling tests for fetch operations
- Add combined signal (user + timeout) test for AbortSignal.any

These tests improve coverage of edge cases and ensure critical functionality
is properly tested.

* fix: add missing error handling for fetch in download utility

- Add .catch() handler for network request failures in download()
- Use window.message.error() for user-friendly error notifications
- Update tests to verify error handling behavior
- Ensure proper error messages are shown to users

This fixes a missing error handler that was discovered during test development.

* refactor: improve test structure and add i18n support for download utility

- Unified test structure with two-layer describe blocks (filename -> function name)
- Added afterEach with restoreAllMocks for consistent mock cleanup
- Removed individual mockRestore calls in favor of centralized cleanup
- Added i18n support to download.ts for error messages
- Updated error handling logic to avoid duplicate messages
- Updated test expectations to match new i18n error messages

* test: fix react-i18next mock for Markdown test

Add missing initReactI18next to mock to resolve test failures caused by i18n initialization when download utility imports i18n module.
2025-07-10 14:35:40 +08:00
kangfenmao
186f0ed06f feat(MCPSettings): enhance MCP server management and localization
- Added BuiltinMCPServersSection and McpResourcesSection components to display available MCP servers and resources.
- Updated navigation logic to redirect users to the MCP settings upon adding a server.
- Enhanced localization by adding new keys for built-in servers in multiple languages.
- Improved the SettingsPage layout by reordering menu items for better accessibility.
2025-07-10 12:39:01 +08:00
kangfenmao
daf134f331 refactor(HtmlArtifacts): enhance HTML validation and rendering logic
- Added checks for complete HTML documents based on presence of critical tags.
- Updated unmatched tag detection to include a comprehensive list of HTML5 void elements.
- Improved HTML content rendering with a fixed interval update mechanism.
- Adjusted modal header styles for better layout consistency.
- Enabled editing capabilities in the CodeEditor component for HTML content.
2025-07-10 12:28:25 +08:00
kangfenmao
3f7f78da15 fix(release.yml): add missing environment variables for build jobs 2025-07-10 10:49:10 +08:00
kangfenmao
1d289621fc style(markdown): enhance typography and spacing for improved readability
- Increased line height and adjusted margins for headers and paragraphs to enhance text clarity.
- Added letter and word spacing for better text presentation.
- Updated blockquote and table styles for a more visually appealing layout.
- Improved hover effect for table rows to enhance user interaction.
2025-07-10 10:49:10 +08:00
kangfenmao
d7002cda11 refactor: quick panel remove multi-select mode 2025-07-10 02:45:41 +08:00
kangfenmao
559fcecf77 refactor(CodeBlockView): replace HtmlArtifacts component with HtmlArtifactsCard
- Removed the obsolete HtmlArtifacts component and its associated logic.
- Introduced the new HtmlArtifactsCard component to enhance the rendering of HTML artifacts.
- Updated the CodeBlockView to utilize HtmlArtifactsCard, improving maintainability and user experience.
- Added a new HtmlArtifactsPopup component for better HTML content preview and editing capabilities.
- Enhanced localization by adding translation keys for HTML artifacts in multiple languages.
2025-07-10 02:45:32 +08:00
kangfenmao
1d854c232e refactor(Messages): update message styling and structure for improved clarity
- Simplified the message header and footer components by removing unnecessary props and logic.
- Adjusted the message container styles for better alignment and spacing.
- Enhanced the message tokens display logic and corrected the component name for consistency.
- Removed unused translation keys related to token usage from multiple language files to streamline localization.
2025-07-10 02:45:32 +08:00
kangfenmao
8c6684cbdf refactor(WebSearchButton): simplify web search button logic and improve tooltip behavior
- Removed unused imports and streamlined the logic for enabling web search.
- Updated the tooltip title to reflect the current state of web search functionality.
- Enhanced the handling of quick panel opening based on the assistant's web search settings.
2025-07-10 02:45:32 +08:00
kangfenmao
c7ab71f01f refactor(OpenAISettingsGroup): simplify component structure and remove styled components
- Removed unused imports and the StyledSelect component, replacing it with a standard Selector for improved clarity.
- Streamlined the layout by eliminating unnecessary styles, enhancing maintainability and readability of the code.
2025-07-10 00:42:36 +08:00
SuYao
9b57351d1e fix(McpToolChunkMiddleware): enhance tool call confirmation logic (#8005)
* fix(McpToolChunkMiddleware): enhance tool call confirmation logic

- Added additional condition to confirm tool calls by checking the toolCallId in the confirmed object.
- Included a console log for confirmed tool calls to aid in debugging and tracking tool call execution.

* chore: unuse log
2025-07-09 23:39:58 +08:00
kangfenmao
f9e88fb6ee refactor(Navbar): remove MinAppsPopover component
- Deleted the MinAppsPopover component to streamline the Navbar.
- Updated Navbar to remove references to MinAppsPopover, enhancing code maintainability.
2025-07-09 19:32:36 +08:00
kangfenmao
074ba0ae05 feat(i18n): add "Open Logs" button translations for multiple languages
- Introduced new translation keys for the "Open Logs" button in various languages (en-us, ja-jp, ru-ru, zh-cn, zh-tw, el-gr, es-es, fr-fr, pt-pt).
- Updated the DataSettings component to include a button for opening application logs, enhancing user accessibility to log files.
2025-07-09 19:22:57 +08:00
kangfenmao
4a8a5e8428 feat(i18n): enhance localization for GitHub Copilot settings
- Added new translation keys for error messages and steps in the GitHub Copilot authentication process across multiple languages (en-us, ja-jp, ru-ru, zh-cn, zh-tw).
- Updated the GitHubCopilotSettings component to reflect the new steps for user guidance during the authentication process.
- Improved user experience by providing detailed descriptions and success/error messages related to the authorization flow.
2025-07-09 19:07:32 +08:00
kangfenmao
f7fa665f3a feat(CustomHeaderPopup): add custom headers management for providers
- Introduced a new CustomHeaderPopup component for managing extra headers for providers.
- Integrated the popup into the ProviderSetting component, allowing users to edit headers via a modal.
- Refactored ApiKeyListPopup to use a styled container for improved layout.
2025-07-09 18:15:42 +08:00
beyondkmp
e273ddcfb0 fix(LocalBackupSettings): update input and select styles for better responsiveness (#7977)
refactor(LocalBackupSettings): update input and select styles for better responsiveness

- Adjusted the input field to have a flexible width between 200 and 400 pixels.
- Modified select components to use a minimum width of 120 pixels for improved layout consistency.
- Enhanced onChange handlers for select components to ensure proper value handling.
2025-07-09 18:05:32 +08:00
kangfenmao
41d3a1fd55 refactor(SettingsPage): reorder menu items for improved organization 2025-07-09 17:50:57 +08:00
kangfenmao
7237ba34db docs: short i18n keys 2025-07-09 17:26:42 +08:00
Phantom
fbf89b3f0a fix(translate): prevent translation from being triggered unexpectedly during IME composition (#7968)
fix(translate): 修复在输入法组合文字时意外触发翻译的问题
2025-07-09 13:46:39 +08:00
kangfenmao
8f38422e7f chore(version): 1.4.9 2025-07-09 10:17:40 +08:00
自由的世界人
79a64f0118 fix: Improve model filtering and group handling (#7950)
Expanded the text-to-image model regex to include more identifiers. Removed the getModelGroup function and now use the model's group property directly. Updated model selection in ModelSettings and TranslatePage to also filter out rerank and text-to-image models, ensuring only appropriate models are shown in dropdowns.
2025-07-09 00:15:47 +08:00
Chen Tao
55648350ed fix: knowledge file cannot open (#7957) 2025-07-08 20:47:45 +08:00
one
2a33a9af64 revert: timing for adding citation references (#7953) 2025-07-08 20:02:51 +08:00
SuYao
14c5357fa3 fix(ApiClientFactory): adjust provider type handling for OpenAI clients (#7675) 2025-07-08 19:21:49 +08:00
Calcium-Ion
a343377a43 feat: add painting support for NewAPI provider (#7905)
* feat: add NewAPI painting support

* fix(NewApiPage): update help link to point to the correct documentation

* feat(NewApiPage): support image generation in API client

* fix: resolve the issue of messy drawing data from aihubmix provider

* feat: group model options in dropdown by category

* fix: update translation to use LanguagesEnum
2025-07-08 17:27:53 +08:00
one
de75992e7b refactor(CodePreview): smoothing code highlighting on streaming (#7842) 2025-07-08 17:25:14 +08:00
SuYao
fba6c1642d feat: implement tool call progress handling and status updates (#7303)
* feat: implement tool call progress handling and status updates

- Update MCP tool response handling to include 'pending' and 'cancelled' statuses.
- Introduce new IPC channel for progress updates.
- Enhance UI components to reflect tool call statuses, including pending and cancelled states.
- Add localization for new status messages in multiple languages.
- Refactor message handling logic to accommodate new tool response types.

* fix: adjust alignment of action tool container in MessageTools component

- Change justify-content from flex-end to flex-start to improve layout consistency.

* feat: enhance tool confirmation handling and update related components

- Introduced a new tool confirmation mechanism in userConfirmation.ts, allowing for individual tool confirmations.
- Updated GeminiAPIClient and OpenAIResponseAPIClient to include tool configuration options.
- Refactored MessageTools component to utilize new confirmation functions and improved styling.
- Enhanced mcp-tools.ts to manage tool invocation and confirmation processes more effectively, ensuring real-time status updates.

* refactor(McpToolChunkMiddleware): enhance tool execution handling and confirmation tracking

- Updated createToolHandlingTransform to manage confirmed tool calls and results more effectively.
- Refactored executeToolCalls and executeToolUseResponses to return both tool results and confirmed tool calls.
- Adjusted buildParamsWithToolResults to utilize confirmed tool calls for building new request messages.
- Improved error handling in messageThunk for tool call status updates, ensuring accurate block ID mapping.

* feat(McpToolChunkMiddleware, ToolUseExtractionMiddleware, mcp-tools, userConfirmation): enhance tool execution and confirmation handling

- Updated McpToolChunkMiddleware to execute tool calls and responses asynchronously, improving performance and response handling.
- Enhanced ToolUseExtractionMiddleware to generate unique tool IDs for better tracking.
- Modified parseToolUse function to accept a starting index for tool extraction.
- Improved user confirmation handling with abort signal support to manage tool action confirmations more effectively.
- Updated SYSTEM_PROMPT to clarify the use of multiple tools per message.

* fix(tagExtraction): update test expectations for tag extraction results

- Adjusted expected length of results from 7 to 9 to reflect changes in tag extraction logic.
- Modified content assertions for specific tag contents to ensure accurate validation of extracted tags.

* refactor(GeminiAPIClient, OpenAIResponseAPIClient): remove unused function calling configurations

- Removed the unused FunctionCallingConfigMode from GeminiAPIClient to streamline the code.
- Eliminated the parallel_tool_calls property from OpenAIResponseAPIClient, simplifying the tool call configuration.

* feat(McpToolChunkMiddleware): enhance LLM response handling and tool call confirmation

- Added notification to UI for new LLM response processing before recursive calls in createToolHandlingTransform.
- Improved tool call confirmation logic in executeToolCalls to match tool IDs more accurately, enhancing response validation.

* refactor(McpToolChunkMiddleware, ToolUseExtractionMiddleware, messageThunk): remove unnecessary console logs

- Eliminated redundant console log statements in McpToolChunkMiddleware, ToolUseExtractionMiddleware, and messageThunk to clean up the code and improve performance.
- Focused on enhancing readability and maintainability by reducing clutter in the logging output.

* refactor(McpToolChunkMiddleware): remove redundant logging statements

- Eliminated unnecessary logging in createToolHandlingTransform to streamline the code and enhance readability.
- Focused on reducing clutter in the logging output while maintaining error handling functionality.

* feat: enhance action button functionality with cancel and confirm options

* refactor(AbortHandlerMiddleware, McpToolChunkMiddleware, ToolUseExtractionMiddleware, messageThunk): improve error handling and code clarity

- Updated AbortHandlerMiddleware to skip abort status checks if an error chunk is received, enhancing error handling logic.
- Replaced console.error with Logger.error in McpToolChunkMiddleware for consistent logging practices.
- Refined ToolUseExtractionMiddleware to improve tool use extraction logic and ensure proper handling of tool_use tags.
- Enhanced messageThunk to include initialPlaceholderBlockId in block ID checks, improving error state management.

* refactor(ToolUseExtractionMiddleware): enhance tool use parsing logic with counter

- Introduced a toolCounter to track the number of tool use responses processed.
- Updated parseToolUse function calls to include the toolCounter, improving the extraction logic and ensuring accurate response handling.

* feat(McpService, IpcChannel, MessageTools): implement tool abort functionality

- Added Mcp_AbortTool channel to handle tool abortion requests.
- Implemented abortTool method in McpService to manage active tool calls and provide logging.
- Updated MessageTools component to include an abort button for ongoing tool calls, enhancing user control.
- Modified API calls to support optional callId for better tracking of tool executions.
- Added localization strings for tool abort messages in multiple languages.

---------

Co-authored-by: Vaayne <liu.vaayne@gmail.com>
2025-07-08 17:17:58 +08:00
LiuVaayne
4f7ca3ede8 fix: include headers when importing MCP server configurations (#7944)
- Add missing headers field to newServer object creation in AddMcpServerModal.tsx
- Update streamableHttp JSON example to show headers format
- Fixes issue where Content-Type and Authorization headers were not imported

Fixes #7932

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-08 17:16:59 +08:00
SuYao
8fd59e89de feat: provider custom header (#7874)
* feat: provider custom header

* fix: state update dependency

* refactor: migrate to code editor onBlur

* fix: lint

* fix: migrate
2025-07-08 17:11:43 +08:00
one
da5badc189 perf: draggable virtual list (#7904)
* perf(TopicsTab): use DraggableVirtualList for the topic list

- Add a DraggableVirtualList implemented using react-virtual
- Rename DragableList to DraggableList
- Add tests

* refactor: improve props, fix drag area
2025-07-08 17:05:40 +08:00
beyondkmp
33da5d31cf fix: cannot show window in mini and hide status (#7943)
* feat(ProtocolClient): show main window on protocol URL handling

* refactor(ProtocolClient): remove main window display logic; update handleProviders to show window on macOS

* fix lint

---------

Co-authored-by: rcadmin <rcadmin@rcadmins-MacBook-Pro-4.local>
2025-07-08 16:49:28 +08:00
SuYao
f506a9d7ac fix(provider): fix azure type (#7926)
* fix(provider): fix azure type

* fix: lint

---------

Co-authored-by: George·Dong <98630204+GeorgeDong32@users.noreply.github.com>
2025-07-08 16:31:10 +08:00
one
915291d780 fix: content search count on enable (#7920) 2025-07-08 14:44:21 +08:00
one
08b9e0788f chore: git blame ignore (#7925) 2025-07-08 14:23:55 +08:00
beyondkmp
115d2078b9 feat: implement local cloud directory backup functionality (#6353)
* feat: implement local backup functionality

- Added new IPC channels for local backup operations including backup, restore, list, delete, and set directory.
- Enhanced BackupManager with methods for handling local backups and integrated auto-sync capabilities.
- Updated settings to include local backup configurations and integrated UI components for managing local backups.
- Localized new features in English, Japanese, Russian, and Chinese.

* refactor: enhance BackupManager and LocalBackupModals for improved file handling

- Updated BackupManager to specify the type of result array for better type safety.
- Refactored showBackupModal in LocalBackupModals to use useCallback and generate a more descriptive default file name based on device type and timestamp.

* refactor: update localBackupDir path in BackupManager for consistency

- Changed localBackupDir to use the temp directory instead of userData for better alignment with backup storage practices.

* refactor: enforce localBackupDir parameter in BackupManager methods

- Updated BackupManager methods to require localBackupDir as a parameter, removing fallback to a default value for improved clarity and consistency in backup operations.
- Removed the localBackupDir property from the class, streamlining the backup management process.

* fix: update localization strings for improved clarity and consistency

- Revised English, Russian, Chinese, and Traditional Chinese localization strings for better user understanding.
- Adjusted phrases related to backup and restore processes to enhance clarity.
- Standardized terminology across different languages for consistency.

* fix: update Chinese localization strings for consistency and clarity

- Revised export menu strings in zh-cn.json to improve formatting and consistency.
- Removed spaces in phrases for a more streamlined appearance.

* feat(settings): add option to disable hardware acceleration

- Introduced a new setting to allow users to disable hardware acceleration.
- Added corresponding IPC channel and configuration management methods.
- Updated UI components to reflect the new setting and prompt for app restart.
- Localized confirmation messages for hardware acceleration changes in multiple languages.

* udpate migrate

* format code

* feat(i18n): add localized error messages for backup directory selection

- Introduced new error messages for selecting a backup directory in multiple languages, including English, Japanese, Russian, Simplified Chinese, and Traditional Chinese.
- Added checks in the LocalBackupSettings component to ensure the selected directory is not the same as the application data or installation paths, and that it has write permissions.

* format

* update migrate

* refactor(LocalBackup): streamline local backup directory validation and enhance settings UI

- Removed translation dependency in LocalBackupModals for error handling.
- Added comprehensive validation for local backup directory in LocalBackupSettings, including checks for app data path, install path, and write permissions.
- Introduced a clear directory button in the settings UI to reset the local backup directory.
- Updated the auto-sync logic to account for local backup settings.

* refactor(LocalBackupManager): remove redundant error messages for invalid local backup directory

- Eliminated repeated error message calls for invalid local backup directory in the LocalBackupManager component.
- Streamlined the validation logic to enhance code clarity and maintainability.
2025-07-08 14:04:29 +08:00
fullex
00151f2c67 fix: linux launch on boot (#7907)
* fix: linux launch on boot

* fix

* fix(AppService): change fs ops from sync to async
2025-07-08 14:01:58 +08:00
Konv Suu
0e670329c8 feat(miniapp): add swap fo betterr filtering (#7784)
* feat(miniapp): add swap fo betterr filtering

* update
2025-07-08 13:42:24 +08:00
one
4ac8a38834 style: set eol to lf, code formatting (#7923)
* chore(gitattributes): set eol to lf

* chore: git renormalize

* style: reformatting

* chore: keep eslint prettier plugin consistent on eol
2025-07-08 09:50:33 +08:00
Phantom
4111ee4c58 fix(databases): fix upgrade undefined error (#7929)
fix(databases): 修复升级到V8时语言对映射的逻辑错误

修复在数据库升级到V8版本时,语言对映射逻辑中未正确处理originPair为空的情况
2025-07-08 09:44:54 +08:00
Phantom
05b8afd681 feat: more encoding (#7898)
* feat(文件处理): 添加文件编码支持以正确处理不同编码的文本文件

添加文件编码检测和指定编码读取功能
- 在FileMetadata接口中添加encoding字段
- 添加iconv-lite和jschardet依赖用于编码处理和检测
- 文件上传时自动检测文本文件编码
- 文件读取时支持指定编码参数
- 更新所有API客户端以传递文件编码参数

* feat(文件处理): 添加文件编码检测和UTF-8读取功能

新增文件编码检测工具函数和UTF-8读取功能,统一处理不同编码的文件读取
移除重复的编码检测逻辑,优化代码结构

* refactor(FileStorage): 使用 readFileUTF8 替换 decodeBuffer 读取文件

移除冗余的 decodeBuffer 逻辑,直接使用封装好的 readFileUTF8 方法读取文件内容

* docs(utils): 为文件编码相关函数添加注释说明

添加对 detectEncoding、decodeBuffer 和 readFileUTF8 函数的详细注释,说明其功能和使用方法

* fix(utils): 为detectEncoding函数添加返回类型声明

* refactor(文件处理): 移除冗余的decodeBuffer函数并直接使用iconv.decode

简化文件读取逻辑,直接调用iconv.decode而不是通过中间函数decodeBuffer

* test(file): 添加文件编码检测的测试用例

* test(文件编码检测): 移除ISO-8859-1编码的测试匹配

* refactor(file): 移除文件编码相关逻辑,统一使用UTF-8读取文本文件

移除FileMetadata接口中的encoding字段及相关检测逻辑
将所有文件读取操作统一改为使用readTextFileUTF8方法

* fix(文件读取): 改进文本文件解码逻辑以处理编码识别错误

当自动识别的编码包含错误字符时,尝试其他可能的编码

* refactor(utils): 将 console 日志替换为 electron-log 记录器

* refactor(文件存储): 移除文件读取时的可选编码参数

简化文件读取逻辑,始终使用UTF-8编码读取文本文件

* fix(utils): 修复文件编码检测中的文件描述符泄漏

在detectEncoding函数中,文件描述符在使用后未关闭,可能导致资源泄漏

* refactor(文件处理): 将readTextFileUTF8重命名为readTextFileWithAutoEncoding并改进编码检测

修复文件编码检测中未正确关闭文件描述符的问题
改进文本文件读取功能以支持自动编码检测

* test(file): 重构编码检测测试用例并改进测试结构

- 将 describe 块重命名为更明确的 detectEncoding
- 提取公共的 mock 逻辑到 beforeEach
- 更新测试描述为英文并保持一致性
- 简化测试实现,移除重复代码

* test(file): 添加对readTextFileWithAutoEncoding的测试用例
2025-07-08 00:57:31 +08:00
Phantom
2b4ca03376 fix(store): fix store migrate and version (#7924)
* fix(store): 更新持久化存储版本号至121

更新版本号以匹配最新的迁移配置变更

* fix(store): 合并migrate版本121到120
2025-07-08 00:57:08 +08:00
Phantom
a314a43f0f refactor(translate): Language Type (#7727)
* refactor(translate): 重构翻译功能使用语言枚举类型

统一翻译功能中的语言表示方式,使用枚举类型替代字符串
更新相关组件和服务以适配新的语言类型定义
添加数据库迁移脚本处理语言类型变更
添加store迁移处理语言类型变更

* refactor(translate): 移除调试用的console.log语句

* refactor(translate): 移除冗余的类型检查逻辑

* fix(db): 添加对TranslateHistory的db迁移

* fix(databases): 捕获数据库升级时的语言映射错误

添加错误处理以防止语言映射失败时中断升级过程

* fix(翻译组件): 修复语言比较和选择逻辑错误

修复语言比较时直接比较对象而非langCode的问题
更新Select组件使用langCode作为值并正确处理语言切换

* refactor(translate): 将saveTranslateHistory参数类型从Language改为LanguageCode

* refactor(hooks): 更新useMessageOperations中的语言代码类型

将targetLanguage和sourceLanguage参数类型从string更新为LanguageCode,提高类型安全性

* docs(translate): 更新JSDoc注释以使用TypeScript类型语法

* feat(备份服务): 升级数据库版本至v8并添加迁移逻辑

添加从v7到v8的数据库迁移支持
更新翻译历史记录中的语言代码映射
优化迁移过程中的日志记录和错误处理

* fix(store): 修复目标语言迁移时的默认值处理

确保在迁移配置时将旧版语言代码正确映射到新版格式,无法映射时使用默认英语

* refactor(translate): 将语言标签从字符串改为函数以支持动态翻译

* refactor(translate): 优化翻译窗口语言选择逻辑

重构翻译窗口的目标语言选择逻辑,使用语言代码获取完整语言信息
移除冗余的Space组件,简化Select选项渲染方式

* docs(技术文档): 新增数据库设置字段文档

添加数据库设置字段的说明文档,包含翻译相关字段的类型和用途

* refactor(translate): 修改db中biDirectionLangPair存储类型

将语言代码处理统一改为存储langCode而非Language对象
修改相关代码以使用getLanguageByLangcode进行转换
更新数据库升级逻辑以兼容新格式

* docs(translate): 为getLanguageByLangcode函数添加注释说明

* fix(数据库升级): 修复升级到V8时可能出现的空值访问问题

* refactor(databases): 优化语言映射错误处理逻辑

将不必要的try-catch块替换为if条件判断

* docs(technical): 修正数据库设置文档中的类型描述

* refactor: 优化语言代码处理和变量命名

* fix(ActionTranslate): 使用langCode存储双向翻译语言对

* fix(migrate): 修复错误的迁移过程

* refactor(translate): 重构语言选项从硬编码改为动态生成

将translateLanguageOptions从硬编码的数组改为通过LanguagesEnum动态生成,提高可维护性

* fix(store): 更新持久化存储版本并修复语言映射迁移问题

将持久化存储版本从119升级到120,并修复语言代码映射迁移问题。迁移过程中将旧的语言标识转换为新的标准语言代码格式。
2025-07-07 22:08:56 +08:00
George·Dong
278fd931fb feat: object storage backup (#7791)
* chore: import opendal

* feat: 添加S3备份支持及相关设置界面

- 在IpcChannel中新增S3备份相关IPC事件,支持备份、恢复、
  列表、删除文件及连接检测
- 在ipc主进程注册对应的S3备份处理函数,集成backupManager
- 新增S3设置页面,支持配置Endpoint、Region、Bucket、AccessKey等
  参数,并提供同步和备份策略的UI控制
- 删除未使用的RemoteStorage.ts,简化代码库

提升备份功能的灵活性,支持S3作为远程存储目标

* feat(S3 Backup): 完善S3备份功能

- 支持自动备份
- 优化设置前端
- 优化备份恢复代码

* feat(i18n): add S3 storage translations

* feat(settings): 优化数据设置页面和S3设置页面UI

* feat(settings): optimize S3 settings state structure and update usage

* refactor: simplify S3 backup and restore modal logic

* feat(s3 backup): improve S3 settings defaults and modal props

* fix(i18n): optimize S3 access key translations

* feat(backup): optimize logging and progress reporting

* fix(settings): set S3 maxBackups as unlimited by default

* chore(package): restore opendal dependency in package.json

* feat(backup): migrate S3 Backup dependency from opendal to aws-sdk

* refactor(backup): simplify S3 config handling and partial updates

* refactor(backup): update Nutstore sync state to use RemoteSyncState

* feat(store): add migration 120 to initialize missing s3 settings

* feat(settings): add tooltip and help link for S3 storage

* fix(s3settings): disable backup button until all fields are set

---------

Co-authored-by: suyao <sy20010504@gmail.com>
2025-07-07 21:00:51 +08:00
Phantom
1e0f0f47fa feat(mcp): Add default args for built-in file system MCP server (#7865)
feat(mcp): 为内置文件系统MCP服务器添加允许目录参数
2025-07-07 11:05:47 +08:00
Konv Suu
942faf474b fix(MessageMenubar): use classNames function to handle className (#7903) 2025-07-07 10:53:19 +08:00
one
9fd2583fd5 refactor(CodeEditor): add blur extension, move some extensions to hooks (#7882) 2025-07-07 10:51:56 +08:00
Chen Tao
463ca6185b chore: move ocr and preprocess into knowledge folder (#7896)
chore: move ocr and preprocess into knowledge file
2025-07-07 07:29:15 +08:00
one
2c5bb5b699 chore: remove useless classnames (#7795)
* chore: remove useless classnames

* fix: respect filterIncludeUser
2025-07-07 01:46:11 +08:00
fullex
40519b48c5 fix(SelectionAssistant): overall bug fix from v1.4.8 (#7834)
* feat(SelectionService): enable toolbar visibility on all workspaces

* feat: update selection-hook to v1.0.5

* fix: show toolbar over fullscreen apps

* fix(SelectionService): adjust macOS window type handling for fullscreen apps
2025-07-06 23:41:20 +08:00
自由的世界人
7f8ad88c06 feat: add show/hide toggle for API keys in settings (#7883)
* feat: add show/hide toggle for API keys in settings

Introduces an eye icon button to toggle visibility of API keys and tokens in Joplin, Notion, Siyuan, and Yuque settings pages. Refactors input fields to allow users to view or hide sensitive credentials, improving usability and security. Also updates translation keys in AgentsSubscribeUrlSettings for consistency.

* refactor: settings pages to use Input.Password for tokens

Replaced custom password visibility toggles and related styled-components with Ant Design's Input.Password component in Joplin, Notion, Siyuan, and Yuque settings pages. This simplifies the codebase and improves consistency in handling sensitive input fields.

* fix: Improve layout of token input fields in settings

Wrapped token/password input and check button pairs in Joplin, Notion, and Siyuan settings with Ant Design's Space.Compact for better alignment and consistent UI.

* fix: trigger token change handler on blur in settings

Added onBlur event handlers to the Joplin and Siyuan token input fields to ensure token changes are handled when the input loses focus, improving reliability of token updates.
2025-07-06 21:37:17 +08:00
tommyzhang100504
c5d1f2dd7a 使自动更新版本号更健壮 (#7864) 2025-07-06 20:31:08 +08:00
one
8ab4682519 fix: hide scrollbars on capturing (#7867) 2025-07-06 19:51:59 +08:00
one
84b4ae0634 chore: update readme badges (#7888) 2025-07-06 19:50:47 +08:00
SuYao
8de304accf fix: model recognize (#7887)
* fix(image generation): model recognize

* fix(grok): disable off option
2025-07-06 19:50:18 +08:00
Phantom
ed9ecd4667 fix(MCPSettings): ensure save button only restarts MCP server if it is running (#7869)
fix(MCPSettings): 修复服务器状态更新逻辑错误

仅在服务器激活时尝试重启,避免不必要的操作
2025-07-06 17:34:06 +08:00
Phantom
4c81efc5b3 fix(LMStudioSettings): prevent negative values in keepAliveMinutes input (#7868)
fix(LMStudioSettings): 修复keepAliveMinutes输入为负数的问题

确保输入值通过Math.floor处理且最小值为0,避免负数输入
2025-07-06 15:42:22 +08:00
one
a4620f8c68 refactor(ApiKeyList): add a popup for api key list (#7491)
* refactor(ApiKeyList): add a popup for api key list

- ApiKeyList for key management
- ApiKeyListPopup triggerred by a button
- Move formatApiKeys to utils for better reuse
- Simplify apikey related states in ProviderSettings for better
  integration with ApiKeyList
- Modify `updateProvider` to accept partial updates
- Update api key placeholder

* fix: strict type

* refactor: support websearch provider

* refactor: remove ApiCheckPopup

* refactor: simplify interfaces for ProviderSetting and WebSearchProviderSetting

* fix: sync input api key between sub-pages, futher simplification

* fix: bold title

* refactor: extract status icon colors

* refactor: add a status indicator to input box on error, update type definitions

* refactor: further simplification, make data flow clearer

* feat: support api key list for preprocess settings

* refactor: better naming, less confusion
2025-07-06 15:10:44 +08:00
SuYao
bf7e713eec fix: qwen3 empty think block (#7873) 2025-07-06 14:40:55 +08:00
Phantom
c25f1f856a fix(QuickPhrasesButton): resolve QuickPhrases database error (#7872)
fix(QuickPhrasesButton): 修复依赖assistant导致的频繁更新报错问题
2025-07-06 14:22:06 +08:00
Jason Young
60a3cac80d fix: improve abortController robustness with defensive programming (#7856) 2025-07-06 14:18:03 +08:00
Jason Young
a1304054ce test: add comprehensive unit tests for asyncInitializer and copy utilities (#7858)
* test: add unit tests for asyncInitializer and copy utilities

- Add tests for asyncInitializer class functionality
- Add tests for clipboard copy operations

* refactor(test): improve copy.test.ts structure and maintainability

- Remove complex shared testCopyFunction in favor of individual test cases
- Simplify mock cleanup by removing redundant afterEach
- Split test scenarios into focused, independent test cases
- Improve test readability with clear Chinese comments
- Maintain full test coverage while following TEST_UTILS.md guidelines
- Fix minor formatting in asyncInitializer.test.ts

* test: remove unnecessary test cases

- Remove AsyncInitializer type support test
- Remove maintain separate instances test
- These tests verify language features rather than business logic

* refactor(test): reorganize copy and export test structure

Restructure test organization based on PR review feedback:

- Move export functionality tests from copy.test.ts to export.test.ts
- Remove unnecessary "clipboard API not available" test
- Merge duplicate empty content tests for better coverage
- Add boundary tests for special characters and Markdown formatting
- Fix ESLint formatting issues

Test responsibilities are now clearer:
- copy.test.ts: Focus on clipboard operations (8 tests)
- export.test.ts: Focus on content conversion and edge cases

* fix(test): correct markdown formatting test for list items

Fix the regex pattern to properly handle markdown list items.
Replace  with separate patterns to avoid removing
the dash from list items incorrectly.

* fix(test): format prettier style for markdown test
2025-07-06 04:51:41 +08:00
fullex
a567666c79 docs: add testplan md (#7854) 2025-07-05 17:19:25 +08:00
one
1ebf546b70 chore: fix vite warning on dynamic imports (#7852) 2025-07-05 15:08:02 +08:00
Jason Young
19e9ba773f test: add comprehensive tests for CopyIcon and MinAppIcon components (#7833)
* test: add comprehensive tests for CopyIcon and MinAppIcon components

- Add tests for CopyIcon covering default rendering, className merging, and prop passing
- Add tests for MinAppIcon covering default props, custom size, sidebar mode, styles, and edge cases
- Include snapshot tests for both components

* fix: update test snapshots after component styling changes

Update snapshots for CopyIcon and MinAppIcon components to match current
styled-components implementation (replaces inline styles with generated classes).

* refactor: simplify icon component tests based on PR review feedback

- CopyIcon: replace multiple redundant tests with single snapshot test
- MinAppIcon: remove duplicate test that overlaps with snapshot test
- Keep essential business logic tests for MinAppIcon (sidebar behavior, null return)
- Update test snapshots accordingly
2025-07-05 13:28:33 +08:00
SuYao
619aadce41 fix(models): update glm-4 model regex for improved matching (#7793)
- Changed the glm-4 model entry to use a regex pattern for better flexibility in version matching, allowing for optional version numbers and suffixes.
2025-07-05 13:25:19 +08:00
beyondkmp
a924da10c2 fix(WindowService): update default window dimensions to improve user experience (#7789)
- Changed the default width from 1080 to 960 and height from 670 to 600 for the main window.
- Adjusted minimum width and height settings to match the new defaults, enhancing compatibility with various screen sizes.
2025-07-05 00:13:22 +08:00
Konv Suu
ee4c4b16ec fix(message-group): revert grid layout to use min-width (#7830) 2025-07-04 23:56:22 +08:00
one
f8c221f51a fix(CodePreview): line height rounding (#7835) 2025-07-04 23:55:31 +08:00
one
2a48babd50 fix: update websearch i18n, allow more search results (#7797) 2025-07-04 23:50:42 +08:00
Chen Tao
e5d94d9a53 fix(MinerU): remove check quota (#7804)
fix: remove check quota
2025-07-04 17:47:52 +08:00
beyondkmp
8cfe6a5848 feat(settings): add option to disable hardware acceleration (#7811)
* feat(settings): add option to disable hardware acceleration

- Introduced a new setting to allow users to disable hardware acceleration.
- Added corresponding IPC channel and configuration management methods.
- Updated UI components to reflect the new setting and prompt for app restart.
- Localized confirmation messages for hardware acceleration changes in multiple languages.

* fix(settings): add delay before relaunching app after disabling hardware acceleration

- Introduced a 500ms delay before the application relaunches to ensure settings are applied correctly.
- This change improves user experience by allowing time for the setting to take effect before the app restarts.

* fix lint

* fix(settings): handle errors when disabling hardware acceleration

- Wrapped the hardware acceleration disabling function in a try-catch block to manage potential errors.
- Added user feedback through an error message if the operation fails, improving overall robustness.
2025-07-04 17:19:22 +08:00
SuYao
134ea51b0f fix: websearch block and citation formatting (#7776)
* feat: enhance citation handling for Perplexity web search results

- Implemented formatting for Perplexity citations in MainTextBlock, including data-citation attributes.
- Updated citation processing in message store and thunk to support new citation structure.
- Added utility functions for link completion based on web search results.
- Enhanced tests to verify correct handling of Perplexity citations and links.

* refactor: streamline chunk processing in OpenAIApiClient

- Replaced single choice handling with a loop to process all choices in the chunk.
- Improved handling of content sources, ensuring fallback mechanisms are in place for delta and message fields.
- Enhanced tool call processing to accommodate missing function names and arguments.
- Maintained existing functionality for web search data and reasoning content processing.

* fix: improve citation handling and web search integration

- Enhanced citation formatting to support legacy data compatibility in messageBlock.ts.
- Updated messageThunk.ts to manage main text block references and citation updates more effectively.
- Removed unnecessary web search flag and streamlined block processing logic.

* fix: improve citation transforms to skip code blocks
- Add withCitationTags for better code structure
- Add tests
- Remove outdated code
- The Citation type in @renderer/types/index.ts is not referenced anywhere, so removed
- Move the actual Citation type from @renderer/pages/home/Messages/CitationsList.tsx to @renderer/types/index.ts
- Allow text selecting in tooltip

* test: update tests

* refactor(messageThunk): streamline citation handling in response processing

- Removed redundant citation block source retrieval during text chunk processing.
- Updated citation references handling to ensure proper inclusion only when available.
- Simplified the logic for managing citation references in both streaming and final text updates.

* refactor: simplify determineCitationSource for backward compatibility

---------

Co-authored-by: one <wangan.cs@gmail.com>
2025-07-04 17:03:45 +08:00
MyPrototypeWhat
2fad7c0ff6 refactor(messageThunk): streamline loading state management for topics (#7809)
* refactor(messageThunk): streamline loading state management for topics

- Reintroduced the handleChangeLoadingOfTopic function to manage loading states more effectively.
- Updated thunk implementations to ensure loading state is correctly set after message processing.
- Removed commented-out code for clarity and maintainability.

* fix(messageThunk): ensure loading state is managed correctly after message sending

- Added a finally block to guarantee that the loading state is updated after the sendMessage thunk execution.
- Removed commented-out code for improved clarity and maintainability.
2025-07-04 16:07:13 +08:00
Konv Suu
985859f1c3 feat(message-group): improve layout style (#7803) 2025-07-04 12:57:17 +08:00
one
d7f2ebcb6e perf(CodePreview): virtual list for shiki code block (#7621)
* perf(CodePreview: virtual list for shiki code block

- move code highlighting to a hook
- use @tanstack/react-virtual dynamic list for CodePreview
- highlight visible items on demand

* refactor: change absolute position to relative position

* refactor: update shiki styles, set scrollbar color for shiki themes
2025-07-04 03:11:30 +08:00
Calcium-Ion
e3057f90ea feat: add NewAPI provider (#7774)
* feat(provider): add NewAPI provider

* feat(providers): Enhance New API model discovery and configuration

This commit refactors the model fetching mechanism for the "New API" provider to improve user experience and support more detailed model information.

The `NewAPIClient` now fetches models directly from the `/models` endpoint, which provides richer metadata, including a new `supported_endpoint_types` field.

Key changes:
- The "Edit Models" popup now automatically adds a model if its `supported_endpoint_types` are provided by the API, using the first available type.
- The manual "Add Model" popup is now a fallback for models that do not declare their endpoint types.
- A new `NewApiModel` type is introduced to handle the structured API response.
- Added support for the `jina-rerank` endpoint type.

* chore(store): update version to 119 and adjust migration function for state management

* fix: adjust label column flex for New API provider in ModelEditContent and NewApiAddModelPopup

* feat: Implement batch adding for New API provider

* feat: Add useDynamicLabelWidth hook for adaptive label widths in forms and fix localization typos

* fix: update dependencies in various components to include translation function

---------

Co-authored-by: 自由的世界人 <3196812536@qq.com>
2025-07-04 01:22:22 +08:00
kangfenmao
244a42f3be chore(docs): update README files and remove Japanese version
- Updated the English and Chinese README files to improve layout and add new language options.
- Removed the Japanese README file as part of the documentation cleanup.
- Enhanced badge visibility and adjusted image sizes for better presentation.
- Added GitHub statistics section to provide insights into project activity.
2025-07-04 01:15:31 +08:00
Jason Young
8c06a87582 test: add comprehensive tests for IndicatorLight and Spinner components (#7781)
- Add tests for IndicatorLight component covering size, color conversion, shadow, and animation props
- Add tests for Spinner component with proper motion/react mocking
- Include snapshot tests for both components
2025-07-04 00:54:11 +08:00
suyao
5d9b47198b Merge branch 'main' into feat/sidebar-ui 2025-07-03 05:21:20 +08:00
suyao
db1c03f9fa refactor(App): replace TabsContainer with AppLayout for improved layout structure; integrate ChatProvider for chat context management; update routing logic in NavigationHandler to utilize SettingsPopup; adjust main height variable in styles for consistency; enhance FilesPage layout by repositioning SideNav; implement new SettingsPopup component for settings management. 2025-07-02 11:50:41 +08:00
suyao
bedea8aaaa refactor(ContextMenu): simplify context menu implementation by removing unused props and handlers; streamline rendering logic in Message component 2025-07-02 03:48:21 +08:00
suyao
27d959caed refactor(SettingsTab): update import for Windows constant and improve shortcut label logic 2025-07-02 03:07:16 +08:00
suyao
0609b93a14 Merge branch 'main' into feat/sidebar-ui 2025-07-02 03:04:43 +08:00
one
c7a0b05841 refactor(Select): provide consistent search experience for antd Select (#7363)
- Add CustomSelect for enhanced searching
- Replace some Select components with CustomSelect
- Fix translation language searching problem
2025-06-19 19:23:25 +08:00
suyao
5ca0ce682b Merge branch 'main' into feat/sidebar-ui 2025-06-19 14:58:48 +08:00
suyao
7c3752a8e6 feat(TopicsHistory): integrate assistant information into topic display
- Added functionality to map and display assistant details alongside topics in the TopicsHistory component.
- Introduced a new AssistantTag styled component for better visual representation of assistants.
- Updated the layout of TopicItem to accommodate assistant information.
2025-06-19 14:20:20 +08:00
Teo
7ae10be387 refactor(ImportAgentPopup): replace Space with Flex for footer layout 2025-06-18 19:02:26 +08:00
Teo
7767dfaac7 feat(styles): add colorSplit property for Divider component in AntdProvider 2025-06-18 18:52:19 +08:00
Teo
c89ff17b36 feat(styles): add EditableNumber component and update styles for improved UI 2025-06-18 18:41:15 +08:00
Teo
0810a63fd8 feat(styles): enhance dropdown and slider components for improved UI 2025-06-18 14:15:50 +08:00
Teo
ff261fb52b refactor(styles): clean up and optimize dropdown and popover styles
- Removed redundant styles from mention-models-dropdown in ant.scss for better maintainability.
- Simplified Selector component by removing ConfigProvider wrapper around Dropdown.
- Updated AntdProvider to set consistent border radius for Dropdown and Popover components.
- Adjusted padding in SettingsTab for improved layout.
2025-06-17 15:35:29 +08:00
suyao
8f8deb9275 fix: enhance MessageSettingsPopup styles for better usability
- Added max height and overflow styles to the body of the MessageSettingsPopup to improve content visibility and scrolling behavior.
2025-06-17 13:46:18 +08:00
suyao
0a7e591f0e refactor: update icon colors to use primary color scheme
- Changed icon colors from var(--color-link) to var(--color-primary) in multiple components for consistency.
- Updated styles in ContentSearch, ReasoningIcon, WebSearchIcon, GenerateImageButton, ThinkingButton, and WebSearchButton to enhance visual coherence.
2025-06-17 12:55:32 +08:00
kangfenmao
c1e8f1063a Merge branch 'main' into feat/sidebar-ui 2025-06-16 17:53:21 +08:00
kangfenmao
4317f4b672 feat: use tabs
wip

wip

wip

wip
2025-06-16 17:52:18 +08:00
one
202504fd17 refactor(Navbar&Sidebar): rearrange navbar icons, add search functionality for assistants and topics (#7170)
* refactor(Navbar&Sidebar): rearrange navbar icons, add search functionality for assistants and topics

- Updated ChatNavbar and MainNavbar to streamline the display of assistant icons based on their visibility state.
- Introduced search input in MainSidebar for filtering assistants and topics.
- Enhanced AssistantsTab and TopicsTab to support search functionality, allowing users to filter displayed items based on input.
- Added a new utility function to improve search keyword matching logic.
- Improved overall layout and styling for better user experience.

* refactor: update icons

* refactor(Search): allow clear

* refactor: enhance search bar

* refactor: improve search bar style

* feat: new panel left icon
2025-06-16 12:35:24 +08:00
Wang Jiyuan
05727c637f refactor: Encapsulate image display related components (#7211) 2025-06-16 00:05:25 +08:00
Teo
c7843ca288 fix: 修复合并main分之后样式问题 2025-06-15 13:00:08 +08:00
kangfenmao
facf29e02b Merge branch 'main' into feat/sidebar-ui
# Conflicts:
#	src/renderer/src/assets/styles/index.scss
#	src/renderer/src/hooks/useTopic.ts
#	src/renderer/src/pages/home/Messages/Blocks/ImageBlock.tsx
#	src/renderer/src/pages/home/Messages/Blocks/index.tsx
#	src/renderer/src/pages/mcp-servers/providers/lanyun.ts
#	src/renderer/src/pages/settings/ModelSettings/TopicNamingModalPopup.tsx
2025-06-15 11:39:21 +08:00
kangfenmao
66d280136c style: enhance MainSidebar and PageContainer styles for improved UI
- Added 3D transform styles to PageContainer for better visual effects.
- Updated MainSidebar to use optional chaining for safer property access.
- Removed background color from MainSidebar to support transparency.
2025-06-15 11:36:33 +08:00
Teo
91892ea619 feat(FilesPage): 添加批量删除功能 2025-06-14 21:52:25 +08:00
Teo
7d70425c75 revert: remove backgroundMaterial for win 2025-06-14 18:44:35 +08:00
Teo
20b3db0c01 Revert "refactor(FileItem, FileList, FilesPage, i18n): enhance file management UI and localization"
This reverts commit edeb9f84f9.
2025-06-14 15:48:47 +08:00
Teo
f1804bc3a0 Revert "refactor(FileItem, FileList, FilesPage): enhance file selection UI and improve checkbox handling"
This reverts commit 58f3edb352.
2025-06-14 15:46:55 +08:00
Teo
2ec0c29087 style: change hover color 2025-06-14 15:03:58 +08:00
Teo
20a572aa46 feat(styles, useAppInit, DisplaySettings): 更新导航栏背景色和列表项样式,优化透明窗口设置
- 在 color.scss 中添加 Windows 版本的导航栏背景色。
- 修改 useAppInit 钩子以支持 Windows 和 Mac 的背景色设置。
- 更新 MainSidebar 的悬停样式以使用新的背景色。
- 在 DisplaySettings 中简化透明窗口设置的显示逻辑。
2025-06-14 14:49:30 +08:00
kangfenmao
142b624001 feat(ThemeProvider, color.scss, useAppInit): integrate transparent window settings and update styles 2025-06-14 10:36:45 +08:00
kangfenmao
3775562956 feat(DisplaySettings, MainSidebar, useSettings): implement transparent window feature and update styles
- Added functionality to manage transparent window settings in DisplaySettings and MainSidebar components.
- Updated useSettings hook to include transparent window state management.
- Modified MainSidebar and its styles to support transparency based on user settings.
- Removed obsolete opaque window translations from localization files across multiple languages for consistency.
2025-06-14 08:32:40 +08:00
Teo
3544c40d9a feat(TranslatePage, ant.scss): add ChevronDown icon to select components and update dropdown styles
- Introduced ChevronDown icon to various Select components in TranslatePage for improved visual consistency.
- Updated ant.scss to add a border style to the select dropdown for enhanced UI clarity.
2025-06-14 00:41:02 +08:00
Teo
36fa3af9e9 feat(CustomCollapse, ThinkingBlock, KnowledgeSearchPopup): add expand icons and adjust styles for improved UI
- Introduced ChevronRight expand icons in CustomCollapse and ThinkingBlock components for better visual feedback.
- Adjusted height and styling in KnowledgeSearchPopup for enhanced usability and consistency.
- Updated various styled components to improve layout and user interaction.
2025-06-14 00:28:33 +08:00
Teo
eb832cc25a refactor(AddKnowledgePopup, KnowledgeSettingsPopup, ModelSettings): enhance select components with consistent icons
- Updated Select components in AddKnowledgePopup and KnowledgeSettingsPopup to include a ChevronDown icon for improved visual consistency.
- Refactored ModelSettings to add ChevronDown icon to Select components, enhancing the overall UI experience.
- Simplified Slider component usage by removing unnecessary styling for a cleaner layout.
2025-06-13 23:27:46 +08:00
Teo
0378f9ceb1 refactor(styles, AntdProvider, DisplaySettings, DefaultAssistantSettings): enhance UI elements and improve styling consistency
- Updated ant.scss to refine modal styles for better visual distinction.
- Adjusted AntdProvider theme settings for Segmented and Switch components to improve color consistency.
- Modified DisplaySettings to change button sizes and styles for a more cohesive appearance.
- Tweaked DefaultAssistantSettings button dimensions for improved layout and usability.
2025-06-13 23:04:17 +08:00
suyao
99b23e1d5d fix(migrate): correct filter condition for assistants' template status
- Updated the filter condition in migrateConfig to check for undefined isTemplate, ensuring proper assignment of template status for assistants.
2025-06-13 22:24:50 +08:00
kangfenmao
8d23c810fe refactor(App, MainSidebar, OpenedMinapps): streamline component code and enhance layout
- Simplified the handleClick function in App component by removing unnecessary line breaks.
- Adjusted MainMenu component in MainSidebar to remove inline margin styling for cleaner layout.
- Updated OpenedMinapps to apply margin styling directly to TabsContainer for improved spacing.
- Removed the PinnedApps component to declutter the sidebar and enhance performance.
2025-06-13 20:44:08 +08:00
kangfenmao
cdfa2ac13a refactor(ChatProvider, useChat): implement context for chat state management and enhance event handling
- Introduced ChatProvider to manage active assistant and topic state using React context.
- Updated useChat hook to utilize context for accessing chat state and actions.
- Refactored message navigation to emit events for setting active assistant and topic.
- Improved topic selection logic to ensure active topic is valid when topics change.
- Integrated ChatProvider into HomePage and HistoryPage for consistent chat state management.
2025-06-13 19:31:21 +08:00
suyao
58f3edb352 refactor(FileItem, FileList, FilesPage): enhance file selection UI and improve checkbox handling
- Added isSelected prop to FileItem for visual indication of selection.
- Updated FileList to manage selected file IDs and pass selection state to FileItem.
- Enhanced FilesPage checkbox handling with improved visibility on hover and selection state.
- Introduced CheckboxContainer for consistent styling of checkboxes across components.
2025-06-13 19:20:16 +08:00
suyao
edeb9f84f9 refactor(FileItem, FileList, FilesPage, i18n): enhance file management UI and localization
- Updated FileItem component to display file count and additional metadata.
- Refactored FileList to support checkbox selection and improved layout.
- Enhanced FilesPage with batch delete functionality and improved file selection handling.
- Updated localization strings across multiple languages for consistency in delete confirmation messages.
2025-06-13 19:11:10 +08:00
Teo
2757fcf6b9 refactor(Selector, HomePage, MessageGroupSettings, NutstoreSettings, WebDavSettings): replace Select components with Selector for improved consistency
- Updated Selector component to handle value checks more robustly.
- Removed unused ContentContainer styles in HomePage for cleaner code.
- Replaced Select components with Selector in MessageGroupSettings, NutstoreSettings, and WebDavSettings for a unified UI experience.
- Enhanced option handling in Selector for better integration with existing settings.
2025-06-13 14:03:30 +08:00
kangfenmao
5339f4a9a3 refactor(MainSidebar, AssistantItem): enhance sidebar functionality and improve event handling
- Updated event handling in MainSidebar to conditionally show topics based on assistant selection.
- Integrated new settings option for showing topics when clicking on assistants.
- Removed unused code related to tab navigation for a cleaner implementation.
- Improved dependency management in useEffect hooks for better performance.
2025-06-13 13:49:15 +08:00
kangfenmao
e786feb165 refactor(i18n, settings): update localization and UI elements for improved consistency
- Removed redundant settings entries in multiple language files.
- Updated various settings titles for clarity, including "Code Settings" to "Code Block Settings".
- Added new localization keys for assistant and topic settings.
- Enhanced the layout of the ChatNavbar and MainSidebar components for better user experience.
- Adjusted the MessageMenubar to reflect updated settings terminology.
2025-06-13 13:11:06 +08:00
suyao
5794c36d0d Merge branch 'feat/sidebar-ui' of https://github.com/CherryHQ/cherry-studio into feat/sidebar-ui 2025-06-13 12:24:37 +08:00
Teo
7d8b88c56e refactor(styles, HistoryPage, MessageGroup): improve UI consistency and layout
- Removed padding from dropdown menu items for a cleaner look.
- Updated suffix rendering in HistoryPage to conditionally show icon based on search length.
- Added padding to MessageGroup for better spacing in grid layout.
2025-06-13 11:46:20 +08:00
Teo
d0daeddf14 Merge branch 'feat/sidebar-ui' of github.com:CherryHQ/cherry-studio into feat/sidebar-ui 2025-06-13 10:58:35 +08:00
Teo
2fe2ae797b refactor(styles): update CitationTooltip and CitationsList components for improved UI consistency and layout 2025-06-13 10:58:20 +08:00
kangfenmao
9bb66227b4 refactor(DragableList, AssistantsTab, AssistantItem): improve layout and styling consistency
- Adjusted margin and padding in DragableList for better spacing.
- Simplified structure in AssistantsTab by removing unnecessary divs and enhancing item layout.
- Added style prop to AssistantItem for flexible styling options.
- Updated GroupTitle and TagsContainer for improved visual consistency.
2025-06-13 10:42:09 +08:00
kangfenmao
5f4736e8c1 refactor(AssistantsTab): streamline drag-and-drop structure and improve layout consistency
- Removed unnecessary div wrappers to simplify the component structure.
- Enhanced the layout of the assistant items for better spacing and alignment.
- Added padding to the GroupTitle for improved visual consistency.
2025-06-13 10:24:52 +08:00
kangfenmao
7a44910847 Merge branch 'main' into feat/sidebar-ui
# Conflicts:
#	package.json
#	src/renderer/src/hooks/useTopic.ts
#	src/renderer/src/pages/home/Messages/Blocks/ImageBlock.tsx
#	src/renderer/src/pages/home/Messages/MessageTokens.tsx
#	src/renderer/src/store/index.ts
#	src/renderer/src/store/migrate.ts
#	src/renderer/src/store/runtime.ts
2025-06-13 10:05:43 +08:00
suyao
509632030b refactor(assistant): replace useAgents with useAssistants across components, streamline assistant management and enhance template handling 2025-06-13 05:14:53 +08:00
suyao
09fe2aa67b refactor(styles): update dropdown menu item styles to include submenu titles for consistent padding 2025-06-13 00:15:48 +08:00
neko engineer
793c641e1c feat: Optimize assistant drag-and-drop effect (#7115)
* feat: 优化助手拖拽效果
增加组内,组件,跨组拖拽效果

* feat: 提交注释

---------

Co-authored-by: linshuhao <nmnm1996>
2025-06-13 00:08:36 +08:00
Teo
38eb206a8a refactor(WindowService): 更新窗口服务配置,优化背景材质和透明度设置 2025-06-12 22:36:40 +08:00
Teo
3d9236a09a fix: fix ui 2025-06-12 21:47:24 +08:00
suyao
e6fd9b5678 Merge branch 'feat/sidebar-ui' of https://github.com/CherryHQ/cherry-studio into feat/sidebar-ui 2025-06-12 21:03:31 +08:00
suyao
d41f175a05 refactor(assistant): enhance assistant creation by automatically adding default topics; streamline default assistant handling 2025-06-12 21:03:17 +08:00
Teo
6d6a554fd3 refactor(styles): reorganize popover and citations list for improved UI consistency and user experience 2025-06-12 19:05:06 +08:00
suyao
cb1fcf7d2d Merge branch 'feat/sidebar-ui' of https://github.com/CherryHQ/cherry-studio into feat/sidebar-ui 2025-06-12 18:29:07 +08:00
suyao
6ed30fd78a Merge 'refactor/assistant-debounce' into 'feat/sidebar-ui' 2025-06-12 18:29:00 +08:00
Teo
69acb2fccd refactor(styles): adjust modal confirm body padding for improved layout consistency 2025-06-12 18:24:27 +08:00
Teo
d95a4e56f5 refactor(styles): update dropdown and modal styles for improved layout and consistency across components 2025-06-12 18:15:00 +08:00
Teo
b15dac9ef4 refactor(styles): standardize padding and width across various components for improved layout consistency 2025-06-12 17:37:20 +08:00
Teo
73fced37b4 refactor(styles): adjust border-radius and padding in various components for improved UI consistency; update layout in settings pages 2025-06-12 17:04:41 +08:00
Teo
1124090d87 refactor(styles): enhance popover styles with border and content overflow handling; update AntdProvider for background mask color 2025-06-12 15:56:35 +08:00
Teo
a92fa1b1ba refactor(MainSidebar): enhance dropdown menu with theme toggle and documentation link; update styles for dropdown menu 2025-06-12 15:46:10 +08:00
Teo
13fdfc58b6 refactor(ChatNavbar): add new topic button with tooltip and adjust sidebar styles for improved visibility 2025-06-12 15:25:03 +08:00
Teo
048a9135ac refactor(styles): update markdown table styles for improved layout and border handling 2025-06-12 14:59:26 +08:00
Teo
b5636646c9 refactor(Selector): enhance Selector component with option grouping, disabled state handling, and improved label retrieval logic 2025-06-12 12:03:04 +08:00
kangfenmao
d9def89ced refactor(HomePage): add id attribute to HStack and remove redundant id from Container for improved structure 2025-06-12 11:28:13 +08:00
kangfenmao
b16d0069bf refactor(Settings): remove windowStyle from settings and related components; transparent window 2025-06-12 10:39:56 +08:00
kangfenmao
30823691f9 refactor(SettingsPage): implement lazy loading for ProvidersList with Suspense and Spin component; remove unnecessary loading state from ProvidersList 2025-06-12 10:13:42 +08:00
kangfenmao
8be98ccbb3 refactor(Routes): enhance page transition animations and improve Navbar styling; conditionally render user name in MainSidebar 2025-06-12 10:08:46 +08:00
kangfenmao
922e85754a feat(Navigation): add IPC channels for navigation handling and implement cache service for URL management 2025-06-12 09:39:51 +08:00
kangfenmao
f041f9a231 refactor(RouteContainer): remove unused state and effect for route readiness; simplify animation handling 2025-06-12 09:39:51 +08:00
kangfenmao
0d9f1882b9 feat: add route animation
feat: add route animation
2025-06-12 09:39:51 +08:00
Teo
26d823e0a5 refactor(HomePage): update HomePage component to accept style prop and simplify layout structure 2025-06-12 09:39:51 +08:00
Teo
daa89df479 refactor(Inputbar): improve textarea height adjustment and update padding for better layout consistency 2025-06-12 09:39:51 +08:00
Teo
527740bf42 refactor(MessageSettingsPopup): replace StyledSelect with Selector component for improved consistency and maintainability 2025-06-12 09:39:51 +08:00
Teo
881e0b4713 refactor(Selector): update LabelIcon styles for improved hover effects and background consistency 2025-06-12 09:39:51 +08:00
Teo
88e251cee7 refactor(Settings): update input components to use Space.Compact for better layout and consistency across various settings pages 2025-06-12 09:39:51 +08:00
Teo
76387643f7 refactor(DisplaySettings): update button styles for zoom controls and color picker to enhance UI consistency 2025-06-12 09:39:51 +08:00
Teo
b41c89972b refactor(Selector): enhance Selector component with placeholder support and improve type safety; update GeneralSettings and WebSearchSettings to utilize Selector 2025-06-12 09:39:51 +08:00
kangfenmao
11a8154458 feat: new app sidebar
fix: adjust navbar and title bar dimensions, update icon handling

feat: implement ChatNavbar component and enhance MainNavbar with search functionality

fix: invert transparency setting for WindowService based on OS

refactor: clean up MainSidebar and useChat hooks, remove unused state handling and improve topic selection logic

refactor: simplify HomeTabs component by removing unused imports and commented code, update AssistantAddItem hover styles

fix: set WindowService transparency to false for consistent behavior across platforms

feat: add event listener to MainSidebar for topic tab navigation

feat: enhance summarization prompt and add topic sidebar visibility toggle

feat(Inputbar): add SettingButton component for settings access

style(color.scss): update border color to improve UI consistency

style: update chat background colors and margins for improved UI consistency

refactor(MainSidebar, i18n): update MCP title in sidebar and localization files, remove unused MCP entries

feat: remove prompt component

refactor(SettingsTab, OpenAISettingsGroup): restructure settings components for improved readability and maintainability, update layout and styling for better user experience

feat(ChatNavbar): add maximize and minimize icons for narrow mode toggle

style(markdown.scss, CodeBlockView): update border-radius for improved UI consistency and adjust CodeBlock styling

style(SettingsTab, OpenAISettingsGroup): adjust padding and minimum height for improved layout, comment out unused components for cleaner code

fix(i18n, MessageEditor, Settings): update localization keys for code settings and adjust border radius in MessageEditor, add gap to SettingRow for improved layout

feat(ChatNavbar, SVGIcon): add ExpandWidth icon and integrate it into ChatNavbar for narrow mode toggle

refactor(ImageBlock, MessageBlockRenderer): enhance image block styling and layout for better responsiveness

refactor(MessageAttachments): enhance file icon rendering and filename display with truncation for better UX

feat(MainSidebar, AssistantItem): integrate AssistantItem into MainSidebar for topic view, enhance event handling and styling adjustments

refactor(MessageAnchorLine): replace DownOutlined icon with CircleChevronDown for improved styling

feat(NarrowModeIcon, ChatNavbar): add NarrowModeIcon component and integrate it into ChatNavbar for narrow mode toggle

fix(MainSidebar, Inputbar, McpServersList): update event handling, adjust textarea rows, and enhance DragableList styling for improved layout and functionality

feat(MainSidebar): enhance submenu animation with framer-motion for improved user experience

style(CodeEditor, markdown.scss, SettingsTab): update border-radius to inherit and remove unused SettingDivider for improved consistency

style(MainNavbar, Message): adjust padding in MainNavbar and refine alignment logic in MessageItem for improved layout consistency

style(Inputbar, SelectModelButton): adjust margins and padding for improved layout consistency and add icon to SelectModelButton

wip

feat(MainSidebar): restructure sidebar components and add MainNavbar for enhanced navigation and user experience

style(MainSidebar, AssistantsTab): adjust margins and padding for improved layout consistency, integrate Scrollbar component for better scrolling experience

fix(MessageAnchorLine): prevent rendering of clear message type

refactor(SearchMessage, TopicMessages, MessagesService): update locateToMessage function to accept additional parameters for setting active assistant and topic, enhancing navigation logic

style(ColorStyles, Messages): update chat background colors for improved visibility and remove redundant background styles in message bubbles

revert: hide token show

refactor: settings tab

refactor(ChatNavbar): remove unused topic handling and simplify shortcut functions

fix(useChat): prevent setting active topic if it already exists in active assistant's topics

refactor(NavigationHandler, ChatNavbar, HomePage, MainSidebar): streamline navigation logic and remove unused code

chore: update react-router and react-router-dom to version 7.6.2, refactor routing logic in App component to use a RouteContainer for better location management

refactor(i18n): remove unused topic position settings and assistant display options from English, Japanese, Russian, Chinese, and Greek translations

refactor(MainSidebar, PinnedApps): remove unused imports and streamline component structure; update styling for better layout

refactor(TopicsTab): remove unused setTopicPosition function and streamline topic time display; update font size and family for TopicTime component

refactor(MainSidebar): integrate UserPopup for user settings access; streamline theme toggle logic and enhance styling for active menu items

refactor: remove topic position

fix(MainSidebar): update settings navigation path from '/settings/general' to '/settings/provider'

wip

refactor(SettingsTab): replace StyledSelect with Selector component and update styles for better UI consistency

chore(release): update fetch depth in GitHub Actions workflow for full history retrieval
2025-06-12 09:39:50 +08:00
422 changed files with 37735 additions and 20670 deletions

View File

@@ -1,9 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

2
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,2 @@
# ignore #7923 eol change and code formatting
4ac8a388347ff35f34de42c3ef4a2f81f03fb3b1

1
.gitattributes vendored
View File

@@ -1,2 +1,3 @@
* text=auto eol=lf
/.yarn/** linguist-vendored
/.yarn/releases/* binary

View File

@@ -73,4 +73,4 @@ body:
id: additional
attributes:
label: 附加信息
description: 任何能让我们对您的问题有更多了解的信息,包括截图或相关链接
description: 任何能让我们对您的问题有更多了解的信息,包括截图或相关链接

View File

@@ -73,4 +73,4 @@ body:
id: additional
attributes:
label: Additional Information
description: Any other information that could help us better understand your question, including screenshots or relevant links
description: Any other information that could help us better understand your question, including screenshots or relevant links

View File

@@ -9,115 +9,115 @@ labels:
# skips and removes
- name: skip all
content:
regexes: "[Ss]kip (?:[Aa]ll |)[Ll]abels?"
regexes: '[Ss]kip (?:[Aa]ll |)[Ll]abels?'
- name: remove all
content:
regexes: "[Rr]emove (?:[Aa]ll |)[Ll]abels?"
regexes: '[Rr]emove (?:[Aa]ll |)[Ll]abels?'
- name: skip kind/bug
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)'
- name: remove kind/bug
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)'
- name: skip kind/enhancement
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)'
- name: remove kind/enhancement
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)'
- name: skip kind/question
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/question(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)kind/question(?:`|)'
- name: remove kind/question
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/question(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)kind/question(?:`|)'
- name: skip area/Connectivity
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)'
- name: remove area/Connectivity
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)'
- name: skip area/UI/UX
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)'
- name: remove area/UI/UX
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)'
- name: skip kind/documentation
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)'
- name: remove kind/documentation
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)'
- name: skip client:linux
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:linux(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)client:linux(?:`|)'
- name: remove client:linux
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:linux(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)client:linux(?:`|)'
- name: skip client:mac
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:mac(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)client:mac(?:`|)'
- name: remove client:mac
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:mac(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)client:mac(?:`|)'
- name: skip client:win
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:win(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)client:win(?:`|)'
- name: remove client:win
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:win(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)client:win(?:`|)'
- name: skip sig/Assistant
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)'
- name: remove sig/Assistant
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)'
- name: skip sig/Data
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)'
- name: remove sig/Data
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)'
- name: skip sig/MCP
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)'
- name: remove sig/MCP
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)'
- name: skip sig/RAG
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)'
- name: remove sig/RAG
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)'
- name: skip lgtm
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)lgtm(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)lgtm(?:`|)'
- name: remove lgtm
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)lgtm(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)lgtm(?:`|)'
- name: skip License
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)License(?:`|)"
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)License(?:`|)'
- name: remove License
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)License(?:`|)"
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)License(?:`|)'
# `Dev Team`
- name: Dev Team
@@ -129,7 +129,7 @@ labels:
# Area labels
- name: area/Connectivity
content: area/Connectivity
regexes: "代理|[Pp]roxy"
regexes: '代理|[Pp]roxy'
skip-if:
- skip all
- skip area/Connectivity
@@ -139,7 +139,7 @@ labels:
- name: area/UI/UX
content: area/UI/UX
regexes: "界面|[Uu][Ii]|重叠|按钮|图标|组件|渲染|菜单|栏目|头像|主题|样式|[Cc][Ss][Ss]"
regexes: '界面|[Uu][Ii]|重叠|按钮|图标|组件|渲染|菜单|栏目|头像|主题|样式|[Cc][Ss][Ss]'
skip-if:
- skip all
- skip area/UI/UX
@@ -150,7 +150,7 @@ labels:
# Kind labels
- name: kind/documentation
content: kind/documentation
regexes: "文档|教程|[Dd]oc(s|umentation)|[Rr]eadme"
regexes: '文档|教程|[Dd]oc(s|umentation)|[Rr]eadme'
skip-if:
- skip all
- skip kind/documentation
@@ -161,7 +161,7 @@ labels:
# Client labels
- name: client:linux
content: client:linux
regexes: "(?:[Ll]inux|[Uu]buntu|[Dd]ebian)"
regexes: '(?:[Ll]inux|[Uu]buntu|[Dd]ebian)'
skip-if:
- skip all
- skip client:linux
@@ -171,7 +171,7 @@ labels:
- name: client:mac
content: client:mac
regexes: "(?:[Mm]ac|[Mm]acOS|[Oo]SX)"
regexes: '(?:[Mm]ac|[Mm]acOS|[Oo]SX)'
skip-if:
- skip all
- skip client:mac
@@ -181,7 +181,7 @@ labels:
- name: client:win
content: client:win
regexes: "(?:[Ww]in|[Ww]indows)"
regexes: '(?:[Ww]in|[Ww]indows)'
skip-if:
- skip all
- skip client:win
@@ -192,7 +192,7 @@ labels:
# SIG labels
- name: sig/Assistant
content: sig/Assistant
regexes: "快捷助手|[Aa]ssistant"
regexes: '快捷助手|[Aa]ssistant'
skip-if:
- skip all
- skip sig/Assistant
@@ -202,7 +202,7 @@ labels:
- name: sig/Data
content: sig/Data
regexes: "[Ww]ebdav|坚果云|备份|同步|数据|Obsidian|Notion|Joplin|思源"
regexes: '[Ww]ebdav|坚果云|备份|同步|数据|Obsidian|Notion|Joplin|思源'
skip-if:
- skip all
- skip sig/Data
@@ -212,7 +212,7 @@ labels:
- name: sig/MCP
content: sig/MCP
regexes: "[Mm][Cc][Pp]"
regexes: '[Mm][Cc][Pp]'
skip-if:
- skip all
- skip sig/MCP
@@ -222,7 +222,7 @@ labels:
- name: sig/RAG
content: sig/RAG
regexes: "知识库|[Rr][Aa][Gg]"
regexes: '知识库|[Rr][Aa][Gg]'
skip-if:
- skip all
- skip sig/RAG
@@ -233,7 +233,7 @@ labels:
# Other labels
- name: lgtm
content: lgtm
regexes: "(?:[Ll][Gg][Tt][Mm]|[Ll]ooks [Gg]ood [Tt]o [Mm]e)"
regexes: '(?:[Ll][Gg][Tt][Mm]|[Ll]ooks [Gg]ood [Tt]o [Mm]e)'
skip-if:
- skip all
- skip lgtm
@@ -243,7 +243,7 @@ labels:
- name: License
content: License
regexes: "(?:[Ll]icense|[Cc]opyright|[Mm][Ii][Tt]|[Aa]pache)"
regexes: '(?:[Ll]icense|[Cc]opyright|[Mm][Ii][Tt]|[Aa]pache)'
skip-if:
- skip all
- skip License

View File

@@ -0,0 +1,27 @@
name: Dispatch Docs Update on Release
on:
release:
types: [released]
permissions:
contents: write
jobs:
dispatch-docs-update:
runs-on: ubuntu-latest
steps:
- name: Get Release Tag from Event
id: get-event-tag
shell: bash
run: |
# 从当前 Release 事件中获取 tag_name
echo "tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
- name: Dispatch update-download-version workflow to cherry-studio-docs
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: CherryHQ/cherry-studio-docs
event-type: update-download-version
client-payload: '{"version": "${{ steps.get-event-tag.outputs.tag }}"}'

View File

@@ -1,4 +1,4 @@
name: "Issue Checker"
name: 'Issue Checker'
on:
issues:
@@ -19,7 +19,7 @@ jobs:
steps:
- uses: MaaAssistantArknights/issue-checker@v1.14
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
repo-token: '${{ secrets.GITHUB_TOKEN }}'
configuration-path: .github/issue-checker.yml
not-before: 2022-08-05T00:00:00Z
include-title: 1
include-title: 1

View File

@@ -1,8 +1,8 @@
name: "Stale Issue Management"
name: 'Stale Issue Management'
on:
schedule:
- cron: "0 0 * * *"
- cron: '0 0 * * *'
workflow_dispatch:
env:
@@ -24,18 +24,18 @@ jobs:
uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "needs-more-info"
only-labels: 'needs-more-info'
days-before-stale: ${{ env.daysBeforeStale }}
days-before-close: 0 # Close immediately after stale
stale-issue-label: "inactive"
close-issue-label: "closed:no-response"
days-before-close: 0 # Close immediately after stale
stale-issue-label: 'inactive'
close-issue-label: 'closed:no-response'
stale-issue-message: |
This issue has been labeled as needing more information and has been inactive for ${{ env.daysBeforeStale }} days.
It will be closed now due to lack of additional information.
该问题被标记为"需要更多信息"且已经 ${{ env.daysBeforeStale }} 天没有任何活动,将立即关闭。
operations-per-run: 50
exempt-issue-labels: "pending, Dev Team"
exempt-issue-labels: 'pending, Dev Team'
days-before-pr-stale: -1
days-before-pr-close: -1
@@ -45,11 +45,11 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: ${{ env.daysBeforeStale }}
days-before-close: ${{ env.daysBeforeClose }}
stale-issue-label: "inactive"
stale-issue-label: 'inactive'
stale-issue-message: |
This issue has been inactive for a prolonged period and will be closed automatically in ${{ env.daysBeforeClose }} days.
该问题已长时间处于闲置状态,${{ env.daysBeforeClose }} 天后将自动关闭。
exempt-issue-labels: "pending, Dev Team, kind/enhancement"
exempt-issue-labels: 'pending, Dev Team, kind/enhancement'
days-before-pr-stale: -1 # Completely disable stalling for PRs
days-before-pr-close: -1 # Completely disable closing for PRs

View File

@@ -77,9 +77,10 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
NODE_OPTIONS: --max-old-space-size=8192
MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }}
- name: Build Mac
if: matrix.os == 'macos-latest'
@@ -93,10 +94,11 @@ jobs:
APPLE_ID: ${{ vars.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ vars.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: --max-old-space-size=8192
MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }}
- name: Build Windows
if: matrix.os == 'windows-latest'
@@ -105,9 +107,10 @@ jobs:
yarn build:win
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
NODE_OPTIONS: --max-old-space-size=8192
MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }}
- name: Release
uses: ncipollo/release-action@v1
@@ -118,38 +121,3 @@ jobs:
tag: ${{ steps.get-tag.outputs.tag }}
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/*.blockmap'
token: ${{ secrets.GITHUB_TOKEN }}
dispatch-docs-update:
needs: release
if: success() && github.repository == 'CherryHQ/cherry-studio' # 确保所有构建成功且在主仓库中运行
runs-on: ubuntu-latest
steps:
- name: Get release tag
id: get-tag
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
else
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi
- name: Check if tag is pre-release
id: check-tag
shell: bash
run: |
TAG="${{ steps.get-tag.outputs.tag }}"
if [[ "$TAG" == *"rc"* || "$TAG" == *"pre-release"* ]]; then
echo "is_pre_release=true" >> $GITHUB_OUTPUT
else
echo "is_pre_release=false" >> $GITHUB_OUTPUT
fi
- name: Dispatch update-download-version workflow to cherry-studio-docs
if: steps.check-tag.outputs.is_pre_release == 'false'
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: CherryHQ/cherry-studio-docs
event-type: update-download-version
client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}'

View File

@@ -1,3 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "editorconfig.editorconfig"]
}

View File

@@ -4,6 +4,7 @@
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"files.eol": "\n",
"search.exclude": {
"**/dist/**": true,
".yarn/releases/**": true

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
[中文](./docs/CONTRIBUTING.zh.md) | [English](./CONTRIBUTING.md)
[中文](docs/CONTRIBUTING.zh.md) | [English](CONTRIBUTING.md)
# Cherry Studio Contributor Guide
@@ -58,6 +58,10 @@ git commit --signoff -m "Your commit message"
Maintainers are here to help you implement your use case within a reasonable timeframe. They will do their best to review your code and provide constructive feedback promptly. However, if you get stuck during the review process or feel your Pull Request is not receiving the attention it deserves, please contact us via comments in the Issue or through the [Community](README.md#-community).
### Participating in the Test Plan
The Test Plan aims to provide users with a more stable application experience and faster iteration speed. For details, please refer to the [Test Plan](docs/testplan-en.md).
### Other Suggestions
- **Contact Developers**: Before submitting a PR, you can contact the developers first to discuss or get help.

View File

@@ -33,32 +33,22 @@
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
</a>
</h1>
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="./docs/README.ja.md">日本語</a> | <a href="https://cherry-ai.com">Official Site</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us">Documents</a> | <a href="./docs/dev.md">Development</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">Feedback</a><br></p>
<!-- 题头徽章组合 -->
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="https://cherry-ai.com">Official Site</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us">Documents</a> | <a href="./docs/dev.md">Development</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">Feedback</a><br></p>
<div align="center">
[![][deepwiki-shield]][deepwiki-link]
[![][twitter-shield]][twitter-link]
[![][discord-shield]][discord-link]
[![][telegram-shield]][telegram-link]
</div>
<!-- 项目统计徽章 -->
<div align="center">
[![][github-stars-shield]][github-stars-link]
[![][github-forks-shield]][github-forks-link]
[![][github-release-shield]][github-release-link]
[![][github-nightly-shield]][github-nightly-link]
[![][github-contributors-shield]][github-contributors-link]
</div>
<div align="center">
[![][license-shield]][license-link]
[![][commercial-shield]][commercial-link]
[![][sponsor-shield]][sponsor-link]
@@ -66,9 +56,9 @@
</div>
<div align="center">
<a href="https://hellogithub.com/repository/1605492e1e2a4df3be07abfa4578dd37" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=1605492e1e2a4df3be07abfa4578dd37" alt="FeaturedHelloGitHub" style="width: 200px; height: 43px;" width="200" height="43" /></a>
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 200px; height: 43px;" width="200" height="43" /></a>
<a href="https://hellogithub.com/repository/1605492e1e2a4df3be07abfa4578dd37" target="_blank" style="text-decoration: none"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=1605492e1e2a4df3be07abfa4578dd37" alt="FeaturedHelloGitHub" width="220" height="55" /></a>
<a href="https://trendshift.io/repositories/11772" target="_blank" style="text-decoration: none"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" width="220" height="55" /></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" width="220" height="55" /></a>
</div>
# 🍒 Cherry Studio
@@ -193,7 +183,7 @@ Refer to the [Branching Strategy](docs/branching-strategy-en.md) for contributio
3. **Submit Changes**: Commit and push your changes.
4. **Open a Pull Request**: Describe your changes and reasons.
For more detailed guidelines, please refer to our [Contributing Guide](./CONTRIBUTING.md).
For more detailed guidelines, please refer to our [Contributing Guide](CONTRIBUTING.md).
Thank you for your support and contributions!
@@ -257,7 +247,7 @@ The Enterprise Edition addresses core challenges in team collaboration by centra
| Feature | Community Edition | Enterprise Edition |
| :---------------- | :----------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
| **Open Source** | ✅ Yes | ⭕️ part. released to cust. |
| **Open Source** | ✅ Yes | ⭕️ part. released to cust. |
| **Cost** | Free for Personal Use / Commercial License | Buyout / Subscription Fee |
| **Admin Backend** | — | ● Centralized **Model** Access<br>● **Employee** Management<br>● Shared **Knowledge Base**<br>● **Access** Control<br>● **Data** Backup |
| **Server** | — | ✅ Dedicated Private Deployment |
@@ -282,37 +272,45 @@ We believe the Enterprise Edition will become your team's AI productivity engine
</a>
<br /><br />
# 📊 GitHub Stats
![Stats](https://repobeats.axiom.co/api/embed/a693f2e5f773eed620f70031e974552156c7f397.svg 'Repobeats analytics image')
# ⭐️ Star History
[![Star History Chart](https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Timeline)](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
<a href="https://www.star-history.com/#CherryHQ/cherry-studio&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Date" />
</picture>
</a>
<!-- Links & Images -->
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNy45MyAzMiI+PHBhdGggZD0iTTE5LjMzIDE0LjEyYy42Ny0uMzkgMS41LS4zOSAyLjE4IDBsMS43NCAxYy4wNi4wMy4xMS4wNi4xOC4wN2guMDRjLjA2LjAzLjEyLjAzLjE4LjAzaC4wMmMuMDYgMCAuMTEgMCAuMTctLjAyaC4wM2MuMDYtLjAyLjEyLS4wNS4xNy0uMDhoLjAybDMuNDgtMi4wMWMuMjUtLjE0LjQtLjQxLjQtLjdWOC40YS44MS44MSAwIDAgMC0uNC0uN2wtMy40OC0yLjAxYS44My44MyAwIDAgMC0uODEgMEwxOS43NyA3LjdoLS4wMWwtLjE1LjEyLS4wMi4wMnMtLjA3LjA5LS4xLjE0VjhhLjQuNCAwIDAgMC0uMDguMTd2LjA0Yy0uMDMuMDYtLjAzLjEyLS4wMy4xOXYyLjAxYzAgLjc4LS40MSAxLjQ5LTEuMDkgMS44OC0uNjcuMzktMS41LjM5LTIuMTggMGwtMS43NC0xYS42LjYgMCAwIDAtLjIxLS4wOGMtLjA2LS4wMS0uMTItLjAyLS4xOC0uMDJoLS4wM2MtLjA2IDAtLjExLjAxLS4xNy4wMmgtLjAzYy0uMDYuMDItLjEyLjA0LS4xNy4wN2gtLjAybC0zLjQ3IDIuMDFjLS4yNS4xNC0uNC40MS0uNC43VjE4YzAgLjI5LjE1LjU1LjQuN2wzLjQ4IDIuMDFoLjAyYy4wNi4wNC4xMS4wNi4xNy4wOGguMDNjLjA1LjAyLjExLjAzLjE3LjAzaC4wMmMuMDYgMCAuMTIgMCAuMTgtLjAyaC4wNGMuMDYtLjAzLjEyLS4wNS4xOC0uMDhsMS43NC0xYy42Ny0uMzkgMS41LS4zOSAyLjE3IDBzMS4wOSAxLjExIDEuMDkgMS44OHYyLjAxYzAgLjA3IDAgLjEzLjAyLjE5di4wNGMuMDMuMDYuMDUuMTIuMDguMTd2LjAycy4wOC4wOS4xMi4xM2wuMDIuMDJzLjA5LjA4LjE1LjExYzAgMCAuMDEgMCAuMDEuMDFsMy40OCAyLjAxYy4yNS4xNC41Ni4xNC44MSAwbDMuNDgtMi4wMWMuMjUtLjE0LjQtLjQxLjQtLjd2LTQuMDFhLjgxLjgxIDAgMCAwLS40LS43bC0zLjQ4LTIuMDFoLS4wMmMtLjA1LS4wNC0uMTEtLjA2LS4xNy0uMDhoLS4wM2EuNS41IDAgMCAwLS4xNy0uMDNoLS4wM2MtLjA2IDAtLjEyIDAtLjE4LjAyLS4wNy4wMi0uMTUuMDUtLjIxLjA4bC0xLjc0IDFjLS42Ny4zOS0xLjUuMzktMi4xNyAwYTIuMTkgMi4xOSAwIDAgMS0xLjA5LTEuODhjMC0uNzguNDItMS40OSAxLjA5LTEuODhaIiBzdHlsZT0iZmlsbDojNWRiZjlkIi8+PHBhdGggZD0ibS40IDEzLjExIDMuNDcgMi4wMWMuMjUuMTQuNTYuMTQuOCAwbDMuNDctMi4wMWguMDFsLjE1LS4xMi4wMi0uMDJzLjA3LS4wOS4xLS4xNGwuMDItLjAyYy4wMy0uMDUuMDUtLjExLjA3LS4xN3YtLjA0Yy4wMy0uMDYuMDMtLjEyLjAzLS4xOVYxMC40YzAtLjc4LjQyLTEuNDkgMS4wOS0xLjg4czEuNS0uMzkgMi4xOCAwbDEuNzQgMWMuMDcuMDQuMTQuMDcuMjEuMDguMDYuMDEuMTIuMDIuMTguMDJoLjAzYy4wNiAwIC4xMS0uMDEuMTctLjAyaC4wM2MuMDYtLjAyLjEyLS4wNC4xNy0uMDdoLjAybDMuNDctMi4wMmMuMjUtLjE0LjQtLjQxLjQtLjd2LTRhLjgxLjgxIDAgMCAwLS40LS43bC0zLjQ2LTJhLjgzLjgzIDAgMCAwLS44MSAwbC0zLjQ4IDIuMDFoLS4wMWwtLjE1LjEyLS4wMi4wMi0uMS4xMy0uMDIuMDJjLS4wMy4wNS0uMDUuMTEtLjA3LjE3di4wNGMtLjAzLjA2LS4wMy4xMi0uMDMuMTl2Mi4wMWMwIC43OC0uNDIgMS40OS0xLjA5IDEuODhzLTEuNS4zOS0yLjE4IDBsLTEuNzQtMWEuNi42IDAgMCAwLS4yMS0uMDhjLS4wNi0uMDEtLjEyLS4wMi0uMTgtLjAyaC0uMDNjLS4wNiAwLS4xMS4wMS0uMTcuMDJoLS4wM2MtLjA2LjAyLS4xMi4wNS0uMTcuMDhoLS4wMkwuNCA3LjcxYy0uMjUuMTQtLjQuNDEtLjQuNjl2NC4wMWMwIC4yOS4xNS41Ni40LjciIHN0eWxlPSJmaWxsOiM0NDY4YzQiLz48cGF0aCBkPSJtMTcuODQgMjQuNDgtMy40OC0yLjAxaC0uMDJjLS4wNS0uMDQtLjExLS4wNi0uMTctLjA4aC0uMDNhLjUuNSAwIDAgMC0uMTctLjAzaC0uMDNjLS4wNiAwLS4xMiAwLS4xOC4wMmgtLjA0Yy0uMDYuMDMtLjEyLjA1LS4xOC4wOGwtMS43NCAxYy0uNjcuMzktMS41LjM5LTIuMTggMGEyLjE5IDIuMTkgMCAwIDEtMS4wOS0xLjg4di0yLjAxYzAtLjA2IDAtLjEzLS4wMi0uMTl2LS4wNGMtLjAzLS4wNi0uMDUtLjExLS4wOC0uMTdsLS4wMi0uMDJzLS4wNi0uMDktLjEtLjEzTDguMjkgMTlzLS4wOS0uMDgtLjE1LS4xMWgtLjAxbC0zLjQ3LTIuMDJhLjgzLjgzIDAgMCAwLS44MSAwTC4zNyAxOC44OGEuODcuODcgMCAwIDAtLjM3LjcxdjQuMDFjMCAuMjkuMTUuNTUuNC43bDMuNDcgMi4wMWguMDJjLjA1LjA0LjExLjA2LjE3LjA4aC4wM2MuMDUuMDIuMTEuMDMuMTYuMDNoLjAzYy4wNiAwIC4xMiAwIC4xOC0uMDJoLjA0Yy4wNi0uMDMuMTItLjA1LjE4LS4wOGwxLjc0LTFjLjY3LS4zOSAxLjUtLjM5IDIuMTcgMHMxLjA5IDEuMTEgMS4wOSAxLjg4djIuMDFjMCAuMDcgMCAuMTMuMDIuMTl2LjA0Yy4wMy4wNi4wNS4xMS4wOC4xN2wuMDIuMDJzLjA2LjA5LjEuMTRsLjAyLjAycy4wOS4wOC4xNS4xMWguMDFsMy40OCAyLjAyYy4yNS4xNC41Ni4xNC44MSAwbDMuNDgtMi4wMWMuMjUtLjE0LjQtLjQxLjQtLjdWMjUuMmEuODEuODEgMCAwIDAtLjQtLjdaIiBzdHlsZT0iZmlsbDojNDI5M2Q5Ii8+PC9zdmc+
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?logo=x
[twitter-link]: https://twitter.com/CherryStudioHQ
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?logo=telegram
[telegram-link]: https://t.me/CherryStudioAI
<!-- Links & Images -->
[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio?logo=github
[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
[github-nightly-shield]: https://img.shields.io/github/actions/workflow/status/CherryHQ/cherry-studio/nightly-build.yml?label=nightly%20build&logo=github
[github-nightly-link]: https://github.com/CherryHQ/cherry-studio/actions/workflows/nightly-build.yml
[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio?logo=github
[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
<!-- Links & Images -->
[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?logo=gnu
[license-link]: https://www.gnu.org/licenses/agpl-3.0
[commercial-shield]: https://img.shields.io/badge/License-Contact-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
[commercial-shield]: https://img.shields.io/badge/License-Contact-white.svg?logoColor=white&logo=telegram&color=blue
[commercial-link]: mailto:license@cherry-ai.com?subject=Commercial%20License%20Inquiry
[sponsor-shield]: https://img.shields.io/badge/Sponsor-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
[sponsor-shield]: https://img.shields.io/badge/Sponsor-FF6699.svg?logo=githubsponsors&logoColor=white
[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md

View File

@@ -1,6 +1,6 @@
# Cherry Studio 贡献者指南
[**English**](../CONTRIBUTING.md) | [**中文**](./CONTRIBUTING.zh.md)
[**English**](../CONTRIBUTING.md) | [**中文**](CONTRIBUTING.zh.md)
欢迎来到 Cherry Studio 的贡献者社区!我们致力于将 Cherry Studio 打造成一个长期提供价值的项目,并希望邀请更多的开发者加入我们的行列。无论您是经验丰富的开发者还是刚刚起步的初学者,您的贡献都将帮助我们更好地服务用户,提升软件质量。
@@ -24,7 +24,7 @@
## 开始之前
请确保阅读了[行为准则](CODE_OF_CONDUCT.md)和[LICENSE](LICENSE)。
请确保阅读了[行为准则](../CODE_OF_CONDUCT.md)和[LICENSE](../LICENSE)。
## 开始贡献
@@ -32,7 +32,7 @@
### 测试
未经测试的功能等同于不存在。为确保代码真正有效,应通过单元测试和功能测试覆盖相关流程。因此,在考虑贡献时,也请考虑可测试性。所有测试均可本地运行,无需依赖 CI。请参阅[开发者指南](docs/dev.md#test)中的“Test”部分。
未经测试的功能等同于不存在。为确保代码真正有效,应通过单元测试和功能测试覆盖相关流程。因此,在考虑贡献时,也请考虑可测试性。所有测试均可本地运行,无需依赖 CI。请参阅[开发者指南](dev.md#test)中的“Test”部分。
### 拉取请求的自动化测试
@@ -60,7 +60,11 @@ git commit --signoff -m "Your commit message"
### 获取代码审查/合并
维护者在此帮助您在合理时间内实现您的用例。他们会尽力在合理时间内审查您的代码并提供建设性反馈。但如果您在审查过程中受阻,或认为您的 Pull Request 未得到应有的关注,请通过 Issue 中的评论或者[社群](README.md#-community)联系我们
维护者在此帮助您在合理时间内实现您的用例。他们会尽力在合理时间内审查您的代码并提供建设性反馈。但如果您在审查过程中受阻,或认为您的 Pull Request 未得到应有的关注,请通过 Issue 中的评论或者[社群](README.zh.md#-community)联系我们
### 参与测试计划
测试计划旨在为用户提供更稳定的应用体验和更快的迭代速度,详细情况请参阅[测试计划](testplan-zh.md)。
### 其他建议

View File

@@ -1,215 +0,0 @@
<h1 align="center">
<a href="https://github.com/CherryHQ/cherry-studio/releases">
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
</a>
</h1>
<p align="center">
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | <a href="./README.zh.md">中文</a> | 日本語 | <a href="https://cherry-ai.com">公式サイト</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/ja">ドキュメント</a> | <a href="./dev.md">開発</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">フィードバック</a><br>
</p>
<!-- バッジコレクション -->
<div align="center">
[![][deepwiki-shield]][deepwiki-link]
[![][twitter-shield]][twitter-link]
[![][discord-shield]][discord-link]
[![][telegram-shield]][telegram-link]
</div>
<!-- プロジェクト統計 -->
<div align="center">
[![][github-stars-shield]][github-stars-link]
[![][github-forks-shield]][github-forks-link]
[![][github-release-shield]][github-release-link]
[![][github-contributors-shield]][github-contributors-link]
</div>
<div align="center">
[![][license-shield]][license-link]
[![][commercial-shield]][commercial-link]
[![][sponsor-shield]][sponsor-link]
</div>
<div align="center">
<a href="https://hellogithub.com/repository/1605492e1e2a4df3be07abfa4578dd37" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=1605492e1e2a4df3be07abfa4578dd37" alt="FeaturedHelloGitHub" style="width: 200px; height: 43px;" width="200" height="43" /></a>
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 200px; height: 43px;" width="200" height="43" /></a>
</div>
# 🍒 Cherry Studio
Cherry Studio は、複数の LLM プロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linux で利用可能です。
👏 [Telegram](https://t.me/CherryStudioAI)[Discord](https://discord.gg/wez8HtpxqQ) | [QQグループ(575014769)](https://qm.qq.com/q/lo0D4qVZKi)
❤️ Cherry Studio をお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!
# 🌠 スクリーンショット
![](https://github.com/user-attachments/assets/36dddb2c-e0fb-4a5f-9411-91447bab6e18)
![](https://github.com/user-attachments/assets/f549e8a0-2385-40b4-b52b-2039e39f2930)
![](https://github.com/user-attachments/assets/58e0237c-4d36-40de-b428-53051d982026)
# 🌟 主な機能
1. **多様な LLM サービス対応**
- ☁️ 主要な LLM クラウドサービス対応OpenAI、Gemini、Anthropic など
- 🔗 AI Web サービス統合Claude、Peplexity、Poe など
- 💻 Ollama、LM Studio によるローカルモデル実行対応
2. **AI アシスタントと対話**
- 📚 300+ の事前設定済み AI アシスタント
- 🤖 カスタム AI アシスタントの作成
- 💬 複数モデルでの同時対話機能
3. **文書とデータ処理**
- 📄 テキスト、画像、Office、PDF など多様な形式対応
- ☁️ WebDAV によるファイル管理とバックアップ
- 📊 Mermaid による図表作成
- 💻 コードハイライト機能
4. **実用的なツール統合**
- 🔍 グローバル検索機能
- 📝 トピック管理システム
- 🔤 AI による翻訳機能
- 🎯 ドラッグ&ドロップによる整理
- 🔌 ミニプログラム対応
- ⚙️ MCPモデルコンテキストプロトコルサービス
5. **優れたユーザー体験**
- 🖥️ Windows、Mac、Linux のクロスプラットフォーム対応
- 📦 環境構築不要ですぐに使用可能
- 🎨 ライト/ダークテーマと透明ウィンドウ対応
- 📝 完全な Markdown レンダリング
- 🤲 簡単な共有機能
# 📝 開発計画
以下の機能と改善に積極的に取り組んでいます:
1. 🎯 **コア機能**
- 選択アシスタント - スマートな内容選択の強化
- ディープリサーチ - 高度な研究能力
- メモリーシステム - グローバルコンテキスト認識
- ドキュメント前処理 - 文書処理の改善
- MCP マーケットプレイス - モデルコンテキストプロトコルエコシステム
2. 🗂 **ナレッジ管理**
- ノートとコレクション
- ダイナミックキャンバス可視化
- OCR 機能
- TTSテキスト読み上げサポート
3. 📱 **プラットフォーム対応**
- HarmonyOS エディション
- Android アプリフェーズ1
- iOS アプリフェーズ1
- マルチウィンドウ対応
- ウィンドウピン留め機能
4. 🔌 **高度な機能**
- プラグインシステム
- ASR音声認識
- アシスタントとトピックの対話機能リファクタリング
[プロジェクトボード](https://github.com/orgs/CherryHQ/projects/7)で進捗を確認し、貢献することができます。
開発計画に影響を与えたいですか?[GitHub ディスカッション](https://github.com/CherryHQ/cherry-studio/discussions)に参加して、アイデアやフィードバックを共有してください!
# 🌈 テーマ
- テーマギャラリーhttps://cherrycss.com
- Aero テーマhttps://github.com/hakadao/CherryStudio-Aero
- PaperMaterial テーマhttps://github.com/rainoffallingstar/CherryStudio-PaperMaterial
- Claude テーマhttps://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
- メープルネオンテーマhttps://github.com/BoningtonChen/CherryStudio_themes
より多くのテーマの PR を歓迎します
# 🤝 貢献
Cherry Studio への貢献を歓迎します!以下の方法で貢献できます:
1. **コードの貢献**:新機能を開発するか、既存のコードを最適化します
2. **バグの修正**:見つけたバグを修正します
3. **問題の管理**GitHub の問題を管理するのを手伝います
4. **製品デザイン**:デザインの議論に参加します
5. **ドキュメントの作成**:ユーザーマニュアルやガイドを改善します
6. **コミュニティの参加**:ディスカッションに参加し、ユーザーを支援します
7. **使用の促進**Cherry Studio を広めます
[ブランチ戦略](branching-strategy-en.md)を参照して貢献ガイドラインを確認してください
## 始め方
1. **リポジトリをフォーク**:フォークしてローカルマシンにクローンします
2. **ブランチを作成**:変更のためのブランチを作成します
3. **変更を提出**:変更をコミットしてプッシュします
4. **プルリクエストを開く**:変更内容と理由を説明します
詳細なガイドラインについては、[貢献ガイド](../CONTRIBUTING.md)をご覧ください。
ご支援と貢献に感謝します!
# 🔗 関連プロジェクト
- [one-api](https://github.com/songquanpeng/one-api)LLM API の管理・配信システム。OpenAI、Azure、Anthropic などの主要モデルに対応し、統一 API インターフェースを提供。API キー管理と再配布に利用可能。
- [ublacklist](https://github.com/iorate/ublacklist)Google 検索結果から特定のサイトを非表示にします
# 🚀 コントリビューター
<a href="https://github.com/CherryHQ/cherry-studio/graphs/contributors">
<img src="https://contrib.rocks/image?repo=CherryHQ/cherry-studio" />
</a>
<br /><br />
# ⭐️ スター履歴
[![Star History Chart](https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Timeline)](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
<!-- リンクと画像 -->
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-link]: https://twitter.com/CherryStudioHQ
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
[telegram-link]: https://t.me/CherryStudioAI
<!-- プロジェクト統計 -->
[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
<!-- ライセンスとスポンサー -->
[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
[license-link]: https://www.gnu.org/licenses/agpl-3.0
[commercial-shield]: https://img.shields.io/badge/商用ライセンス-お問い合わせ-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
[commercial-link]: mailto:license@cherry-ai.com?subject=商業ライセンスについて
[sponsor-shield]: https://img.shields.io/badge/スポンサー-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md

View File

@@ -1,10 +1,40 @@
<div align="right" >
<details>
<summary >🌐 Language</summary>
<div>
<div align="right">
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=en">English</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=zh-CN">简体中文</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=zh-TW">繁體中文</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=ja">日本語</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=ko">한국어</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=hi">हिन्दी</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=th">ไทย</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=fr">Français</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=de">Deutsch</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=es">Español</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=it">Itapano</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=ru">Русский</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=pt">Português</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=nl">Nederlands</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=pl">Polski</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=ar">العربية</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=fa">فارسی</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=tr">Türkçe</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=vi">Tiếng Việt</a></p>
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=id">Bahasa Indonesia</a></p>
</div>
</div>
</details>
</div>
<h1 align="center">
<a href="https://github.com/CherryHQ/cherry-studio/releases">
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
</a>
</h1>
<p align="center">
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="./README.ja.md">日本語</a> | <a href="https://cherry-ai.com">官方网站</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/zh-cn">文档</a> | <a href="./dev.md">开发</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">反馈</a><br>
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="https://cherry-ai.com">官方网站</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/zh-cn">文档</a> | <a href="./dev.md">开发</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">反馈</a><br>
</p>
<!-- 题头徽章组合 -->
@@ -18,19 +48,10 @@
</div>
<!-- 项目统计徽章 -->
<div align="center">
[![][github-stars-shield]][github-stars-link]
[![][github-forks-shield]][github-forks-link]
[![][github-release-shield]][github-release-link]
[![][github-contributors-shield]][github-contributors-link]
</div>
<div align="center">
[![][license-shield]][license-link]
[![][commercial-shield]][commercial-link]
[![][sponsor-shield]][sponsor-link]
@@ -38,9 +59,9 @@
</div>
<div align="center">
<a href="https://hellogithub.com/repository/1605492e1e2a4df3be07abfa4578dd37" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=1605492e1e2a4df3be07abfa4578dd37" alt="FeaturedHelloGitHub" style="width: 200px; height: 43px;" width="200" height="43" /></a>
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 200px; height: 43px;" width="200" height="43" /></a>
<a href="https://hellogithub.com/repository/1605492e1e2a4df3be07abfa4578dd37" target="_blank" style="text-decoration: none"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=1605492e1e2a4df3be07abfa4578dd37" alt="FeaturedHelloGitHub" width="220" height="55" /></a>
<a href="https://trendshift.io/repositories/11772" target="_blank" style="text-decoration: none"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" width="220" height="55" /></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" width="220" height="55" /></a>
</div>
# 🍒 Cherry Studio
@@ -51,14 +72,6 @@ Cherry Studio 是一款支持多个大语言模型LLM服务商的桌面客
❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️
# GitCode✖Cherry Studio【新源力】贡献挑战赛
<p align="center">
<a href="https://gitcode.com/CherryHQ/cherry-studio/discussion/2">
<img src="https://raw.gitcode.com/user-images/assets/5007375/8d8d7559-1141-4691-b90f-d154558c6896/cherry-studio-gitcode.jpg" width="100%" alt="banner" />
</a>
</p>
# 📖 使用教程
https://docs.cherry-ai.com
@@ -177,10 +190,82 @@ https://docs.cherry-ai.com
3. **提交更改**:提交并推送您的更改
4. **打开 Pull Request**:描述您的更改和原因
有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.zh.md)
有关更详细的指南,请参阅我们的 [贡献指南](CONTRIBUTING.zh.md)
感谢您的支持和贡献!
# 🔧 开发者共创计划
我们正在启动 Cherry Studio 开发者共创计划,旨在为开源生态系统构建一个健康、正向反馈的循环。我们相信,优秀的软件是通过协作构建的,每一个合并的拉取请求都为项目注入新的生命力。
我们诚挚地邀请您加入我们的贡献者队伍,与我们一起塑造 Cherry Studio 的未来。
## 贡献者奖励计划
为了回馈我们的核心贡献者并创造良性循环,我们建立了以下长期激励计划。
**该计划的首个跟踪周期将是 2025 年第三季度7月、8月、9月。此周期的奖励将在 10月1日 发放。**
在任何跟踪周期内(例如,首个周期的 7月1日 至 9月30日任何为 Cherry Studio 在 GitHub 上的开源项目贡献超过 **30 个有意义提交** 的开发者都有资格获得以下福利:
- **Cursor 订阅赞助**:获得 **70 美元** 的 [Cursor](https://cursor.sh/) 订阅积分或报销,让 AI 成为您最高效的编码伙伴。
- **无限模型访问**:获得 **DeepSeek****Qwen** 模型的 **无限次** API 调用。
- **前沿技术访问**:享受偶尔的特殊福利,包括 **Claude**、**Gemini** 和 **OpenAI** 等模型的 API 访问权限,让您始终站在技术前沿。
## 共同成长与未来规划
活跃的社区是任何可持续开源项目背后的推动力。随着 Cherry Studio 的发展,我们的奖励计划也将随之发展。我们致力于持续将我们的福利与行业内最优秀的工具和资源保持一致。这确保我们的核心贡献者获得有意义的支持,创造一个开发者、社区和项目共同成长的正向循环。
**展望未来,该项目还将采取越来越开放的态度来回馈整个开源社区。**
## 如何开始?
我们期待您的第一个拉取请求!
您可以从探索我们的仓库开始,选择一个 `good first issue`,或者提出您自己的改进建议。每一个提交都是开源精神的体现。
感谢您的关注和贡献。
让我们一起建设。
# 🏢 企业版
在社区版的基础上,我们自豪地推出 **Cherry Studio 企业版**——一个为现代团队和企业设计的私有部署 AI 生产力与管理平台。
企业版通过集中管理 AI 资源、知识和数据,解决了团队协作中的核心挑战。它赋能组织提升效率、促进创新并确保合规,同时在安全环境中保持对数据的 100% 控制。
## 核心优势
- **统一模型管理**:集中整合和管理各种基于云的大语言模型(如 OpenAI、Anthropic、Google Gemini和本地部署的私有模型。员工可以开箱即用无需单独配置。
- **企业级知识库**:构建、管理和分享全团队的知识库。确保知识得到保留且一致,使团队成员能够基于统一准确的信息与 AI 交互。
- **细粒度访问控制**:通过统一的管理后台轻松管理员工账户,并为不同模型、知识库和功能分配基于角色的权限。
- **完全私有部署**:在您的本地服务器或私有云上部署整个后端服务,确保您的数据 100% 私有且在您的控制之下,满足最严格的安全和合规标准。
- **可靠的后端服务**:提供稳定的 API 服务、企业级数据备份和恢复机制,确保业务连续性。
## ✨ 在线演示
> 🚧 **公开测试版通知**
>
> 企业版目前处于早期公开测试阶段,我们正在积极迭代和优化其功能。我们知道它可能还不够完全稳定。如果您在试用过程中遇到任何问题或有宝贵建议,我们非常感谢您能通过邮件联系我们提供反馈。
**🔗 [Cherry Studio 企业版](https://www.cherry-ai.com/enterprise)**
## 版本对比
| 功能 | 社区版 | 企业版 |
| :----------- | :---------------------- | :--------------------------------------------------------------------------------------------- |
| **开源** | ✅ 是 | ⭕️ 部分开源,对客户开放 |
| **成本** | 个人使用免费 / 商业授权 | 买断 / 订阅费用 |
| **管理后台** | — | ● 集中化**模型**访问<br>● **员工**管理<br>● 共享**知识库**<br>● **访问**控制<br>● **数据**备份 |
| **服务器** | — | ✅ 专用私有部署 |
## 获取企业版
我们相信企业版将成为您团队的 AI 生产力引擎。如果您对 Cherry Studio 企业版感兴趣,希望了解更多信息、请求报价或安排演示,请联系我们。
- **商业咨询与购买**
**📧 [bd@cherry-ai.com](mailto:bd@cherry-ai.com)**
# 🔗 相关项目
- [one-api](https://github.com/songquanpeng/one-api)LLM API 管理及分发系统,支持 OpenAI、Azure、Anthropic 等主流模型,统一 API 接口,可用于密钥管理与二次分发。
@@ -194,34 +279,43 @@ https://docs.cherry-ai.com
</a>
<br /><br />
# 📊 GitHub 统计
![Stats](https://repobeats.axiom.co/api/embed/a693f2e5f773eed620f70031e974552156c7f397.svg 'Repobeats analytics image')
# ⭐️ Star 记录
[![Star History Chart](https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Timeline)](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
<a href="https://www.star-history.com/#CherryHQ/cherry-studio&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Date" />
</picture>
</a>
<!-- Links & Images -->
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?logo=x
[twitter-link]: https://twitter.com/CherryStudioHQ
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?logo=telegram
[telegram-link]: https://t.me/CherryStudioAI
<!-- 项目统计徽章 -->
[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
<!-- 许可和赞助徽章 -->
[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?logo=gnu
[license-link]: https://www.gnu.org/licenses/agpl-3.0
[commercial-shield]: https://img.shields.io/badge/商用授权-联系-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
[commercial-shield]: https://img.shields.io/badge/商用授权-联系-white.svg?logoColor=white&logo=telegram&color=blue
[commercial-link]: mailto:license@cherry-ai.com?subject=商业授权咨询
[sponsor-shield]: https://img.shields.io/badge/赞助支持-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
[sponsor-shield]: https://img.shields.io/badge/赞助支持-FF6699.svg?logo=githubsponsors&logoColor=white
[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md

View File

@@ -16,6 +16,8 @@ Cherry Studio implements a structured branching strategy to maintain code qualit
- Only accepts documentation updates and bug fixes
- Thoroughly tested before production deployment
For details about the `testplan` branch used in the Test Plan, please refer to the [Test Plan](testplan-en.md).
## Contributing Branches
When contributing to Cherry Studio, please follow these guidelines:

View File

@@ -16,6 +16,8 @@ Cherry Studio 采用结构化的分支策略来维护代码质量并简化开发
- 只接受文档更新和 bug 修复
- 经过完整测试后可以发布到生产环境
关于测试计划所使用的`testplan`分支,请查阅[测试计划](testplan-zh.md)。
## 贡献分支
在为 Cherry Studio 贡献代码时,请遵循以下准则:

View File

@@ -0,0 +1,11 @@
# 数据库设置字段
此文档包含部分字段的数据类型说明。
## 字段
| 字段名 | 类型 | 说明 |
| ------------------------------ | ------------------------------ | ------------ |
| `translate:target:language` | `LanguageCode` | 翻译目标语言 |
| `translate:source:language` | `LanguageCode` | 翻译源语言 |
| `translate:bidirectional:pair` | `[LanguageCode, LanguageCode]` | 双向翻译对 |

99
docs/testplan-en.md Normal file
View File

@@ -0,0 +1,99 @@
# Test Plan
To provide users with a more stable application experience and faster iteration speed, Cherry Studio has launched the "Test Plan".
## User Guide
The Test Plan is divided into the RC channel and the Beta channel, with the following differences:
- **RC (Release Candidate)**: The features are stable, with fewer bugs, and it is close to the official release.
- **Beta**: Features may change at any time, and there may be more bugs, but users can experience future features earlier.
Users can enable the "Test Plan" and select the version channel in the software's `Settings` > `About`. Please note that the versions in the "Test Plan" cannot guarantee data consistency, so be sure to back up your data before using them.
Users are welcome to submit issues or provide feedback through other channels for any bugs encountered during testing. Your feedback is very important to us.
## Developer Guide
### Participating in the Test Plan
Developers should submit `PRs` according to the [Contributor Guide](../CONTRIBUTING.md) (and ensure the target branch is `main`). The repository maintainers will evaluate whether the `PR` should be included in the Test Plan based on factors such as the impact of the feature on the application, its importance, and whether broader testing is needed.
If the `PR` is added to the Test Plan, the repository maintainers will:
- Notify the `PR` submitter.
- Set the PR to `draft` status (to avoid accidental merging into `main` before testing is complete).
- Set the `milestone` to the specific Test Plan version.
- Modify the `PR` title.
During participation in the Test Plan, `PR` submitters should:
- Keep the `PR` branch synchronized with the latest `main` (i.e., the `PR` branch should always be based on the latest `main` code).
- Ensure the `PR` branch is conflict-free.
- Actively respond to comments & reviews and fix bugs.
- Enable maintainers to modify the `PR` branch to allow for bug fixes at any time.
Inclusion in the Test Plan does not guarantee the final merging of the `PR`. It may be shelved due to immature features or poor testing feedback.
### Test Plan Lead
A maintainer will be assigned as the lead for a specific version (e.g., `1.5.0-rc`). The responsibilities of the Test Plan lead include:
- Determining whether a `PR` meets the Test Plan requirements and deciding whether it should be included in the current Test Plan.
- Modifying the status of `PRs` added to the Test Plan and communicating relevant matters with the `PR` submitter.
- Before the Test Plan release, merging the branches of `PRs` added to the Test Plan (using squash merge) into the corresponding version branch of `testplan` and resolving conflicts.
- Ensuring the `testplan` branch is synchronized with the latest `main`.
- Overseeing the Test Plan release.
## In-Depth Understanding
### About `PRs`
A `PR` is a collection of a specific branch (and commits), comments, reviews, and other information, and it is the **smallest management unit** of the Test Plan.
Compared to submitting all features to a single branch, the Test Plan manages features through `PRs`, which offers greater flexibility and efficiency:
- Features can be added or removed between different versions of the Test Plan without cumbersome `revert` operations.
- Clear feature boundaries and responsibilities are established. Bug fixes are completed within their respective `PRs`, isolating cross-impact and better tracking progress.
- The `PR` submitter is responsible for resolving conflicts with the latest `main`. The Test Plan lead is responsible for resolving conflicts between `PR` branches. However, since features added to the Test Plan are relatively independent (in other words, if a feature has broad implications, it should be independently included in the Test Plan), conflicts are generally few or simple.
### The `testplan` Branch
The `testplan` branch is a **temporary** branch used for Test Plan releases.
Note:
- **Do not develop based on this branch**. It may change or even be deleted at any time, and there is no guarantee of commit completeness or order.
- **Do not submit `commits` or `PRs` to this branch**, as they will not be retained.
- The `testplan` branch is always based on the latest `main` branch (not on a released version), with features added on top.
#### RC Branch
Branch name: `testplan/rc/x.y.z`
Used for RC releases, where `x.y.z` is the target version number. Note that whether it is rc.1 or rc.5, as long as the major version number is `x.y.z`, it is completed in this branch.
Generally, the version number for releases from this branch is named `x.y.z-rc.n`.
#### Beta Branch
Branch name: `testplan/beta/x.y.z`
Used for Beta releases, where `x.y.z` is the target version number. Note that whether it is beta.1 or beta.5, as long as the major version number is `x.y.z`, it is completed in this branch.
Generally, the version number for releases from this branch is named `x.y.z-beta.n`.
### Version Rules
The application version number for the Test Plan is: `x.y.z-CHA.n`, where:
- `x.y.z` is the conventional version number, referred to here as the **target version number**.
- `CHA` is the channel code (Channel), currently divided into `rc` and `beta`.
- `n` is the release number, starting from `1`.
Examples of complete version numbers: `1.5.0-rc.3`, `1.5.1-beta.1`, `1.6.0-beta.6`.
The **target version number** of the Test Plan points to the official version number where these features are expected to be added. For example:
- `1.5.0-rc.3` means this is a preview of the `1.5.0` official release (the current latest official release is `1.4.9`, and `1.5.0` has not yet been officially released).
- `1.5.1-beta.1` means this is a beta version of the `1.5.1` official release (the current latest official release is `1.5.0`, and `1.5.1` has not yet been officially released).

99
docs/testplan-zh.md Normal file
View File

@@ -0,0 +1,99 @@
# 测试计划
为了给用户提供更稳定的应用体验并提供更快的迭代速度Cherry Studio推出“测试计划”。
## 用户指南
测试计划分为RC版通道和Beta版通道吗区别在于
- **RC版预览版**RC即Release Candidate功能已经稳定BUG较少接近正式版
- **Beta版测试版**功能可能随时变化BUG较多可以较早体验未来功能
用户可以在软件的`设置`-`关于`中,开启“测试计划”并选择版本通道。请注意“测试计划”的版本无法保证数据的一致性,请使用前一定要备份数据。
用户在测试过程中发现的BUG欢迎提交issue或通过其他渠道反馈。用户的反馈对我们非常重要。
## 开发者指南
### 参与测试计划
开发者按照[贡献者指南](CONTRIBUTING.zh.md)要求正常提交`PR`并注意提交target为`main`)。仓库维护者会综合考虑(例如该功能对应用的影响程度,功能的重要性,是否需要更广泛的测试等),决定该`PR`是否应加入测试计划。
若该`PR`加入测试计划,仓库维护者会做如下操作:
- 通知`PR`提交人
- 设置PR为`draft`状态(避免在测试完成前意外并入`main`
- `milestone`设置为具体测试计划版本
- 修改`PR`标题
`PR`提交人在参与测试计划过程中,应做到:
- 保持`PR`分支与最新`main`同步(即`PR`分支总是应基于最新`main`代码)
- 保持`PR`分支为无冲突状态
- 积极响应 comments & reviews修复bug
- 开启维护者可以修改`PR`分支的权限以便维护者能随时修改BUG
加入测试计划并不保证`PR`的最终合并,也有可能由于功能不成熟或测试反馈不佳而搁置
### 测试计划负责人
某个维护者会被指定为某个版本期间(例如`1.5.0-rc`)的测试计划负责人。测试计划负责人的工作为:
- 判断某个`PR`是否符合测试计划要求,并决定是否应合入当期测试计划
- 修改加入测试计划的`PR`状态,并与`PR`提交人沟通相关事宜
- 在测试计划发版前,将加入测试计划的`PR`分支逐一合并采用squash merge`testplan`对应版本分支,并解决冲突
- 保证`testplan`分支与最新`main`同步
- 负责测试计划发版
## 深入理解
### 关于`PR`
`PR`是特定分支及commits、comments、reviews等各种信息的集合也是测试计划的**最小管理单元**。
相比将所有功能都提交到某个分支,测试计划通过`PR`来管理功能,这可以带来极大的灵活度和效率:
- 测试计划的各个版本间,可以随意增减功能,而无需繁琐的`revert`操作
- 明确了功能边界和负责人bug修复在各自`PR`中完成,隔离了交叉影响,也能更好观察进度
- `PR`提交人负责与最新`main`之间的冲突;测试计划负责人负责各`PR`分支之间的冲突,但因加入测试计划的各功能相对比较独立(话句话说,如果功能牵涉较广,则应独立上测试计划),冲突一般比较少或简单。
### `testplan`分支
`testplan`分支是用于测试计划发版所用的**临时**分支。
注意:
- **请勿基于该分支开发**。该分支随时会变化甚至删除且并不保证commit的完整和顺序。
- **请勿向该分支提交`commit``PR`**,将不会得到保留
- `testplan`分支总是基于最新`main`分支(而不是基于已发布版本),在其之上添加功能
#### RC版分支
分支名称:`testplan/rc/x.y.z`
用于RC版的发版x.y.z为目标版本号注意无论是rc.1还是rc.5只要主版本号为x.y.z都在该分支完成。
一般而言,该分支发版的版本号命名为`x.y.z-rc.n`
#### Beta版分支
分支名称:`testplan/beta/x.y.z`
用于Beta版的发版x.y.z为目标版本号注意无论是beta.1还是beta.5只要主版本号为x.y.z都在该分支完成。
一般而言,该分支发版的版本号命名为`x.y.z-beta.n`
### 版本规则
测试计划的应用版本号为:`x.y.z-CHA.n`,其中:
- `x.y.z`为一般意义上的版本号,在这里称为**目标版本号**
- `CHA`为通道号Channel现在分为`rc``beta`
- `n`为发版编号,从`1`计数
完整的版本号举例:`1.5.0-rc.3``1.5.1-beta.1``1.6.0-beta.6`
测试计划的**目标版本号**指向希望添加这些功能的正式版版本号。例如:
- `1.5.0-rc.3`是指,这是`1.5.0`正式版的预览版(当前最新正式版是`1.4.9`,而`1.5.0`正式版还未发布)
- `1.5.1-beta.1`是指,这是`1.5.1`正式版的测试版(当前最新正式版是`1.5.0`,而`1.5.1`正式版还未发布)

View File

@@ -117,9 +117,8 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
划词助手:支持 macOS 系统
文档处理:增加 MinerU、Doc2xMistral 等服务商支持
知识库:新的知识库界面,增加扫描版 PDF 支持
OCRmacOS 增加系统 OCR 支持
服务商:支持一键添加服务商,新增 PH8 大模型开放平台, 支持 PPIO OAuth 登录
修复Linux下数据目录移动问题
• [新增] MCP 工具调用自动审批流程
• [优化] 输入框快捷弹窗多选交互支持
• [新增] 网页内容生成实时预览功能
• [支持] Grok-4 大语言模型接入
• [修复] Anthropic 模型输出截断缺陷

View File

@@ -26,7 +26,7 @@ export default defineConfig([
'simple-import-sort/exports': 'error',
'unused-imports/no-unused-imports': 'error',
'@eslint-react/no-prop-types': 'error',
'prettier/prettier': ['error', { endOfLine: 'auto' }]
'prettier/prettier': ['error']
}
},
// Configuration for ensuring compatibility with the original ESLint(8.x) rules

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.4.8",
"version": "1.4.10",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -27,12 +27,12 @@
"build:win": "dotenv npm run build && electron-builder --win --x64 --arm64",
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
"build:win:arm64": "dotenv npm run build && electron-builder --win --arm64",
"build:mac": "dotenv electron-vite build && electron-builder --mac --arm64 --x64",
"build:mac:arm64": "dotenv electron-vite build && electron-builder --mac --arm64",
"build:mac:x64": "dotenv electron-vite build && electron-builder --mac --x64",
"build:linux": "dotenv electron-vite build && electron-builder --linux --x64 --arm64",
"build:linux:arm64": "dotenv electron-vite build && electron-builder --linux --arm64",
"build:linux:x64": "dotenv electron-vite build && electron-builder --linux --x64",
"build:mac": "dotenv npm run build && electron-builder --mac --arm64 --x64",
"build:mac:arm64": "dotenv npm run build && electron-builder --mac --arm64",
"build:mac:x64": "dotenv npm run build && electron-builder --mac --x64",
"build:linux": "dotenv npm run build && electron-builder --linux --x64 --arm64",
"build:linux:arm64": "dotenv npm run build && electron-builder --linux --arm64",
"build:linux:x64": "dotenv npm run build && electron-builder --linux --x64",
"build:npm": "node scripts/build-npm.js",
"release": "node scripts/version.js",
"publish": "yarn build:check && yarn release patch push",
@@ -55,20 +55,23 @@
"test:lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"prepare": "husky"
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.840.0",
"@cherrystudio/pdf-to-img-napi": "^0.0.1",
"@libsql/client": "0.14.0",
"@libsql/win32-x64-msvc": "^0.4.7",
"@strongtz/win32-arm64-msvc": "^0.4.7",
"iconv-lite": "^0.6.3",
"jschardet": "^3.1.4",
"jsdom": "26.1.0",
"macos-release": "^3.4.0",
"node-stream-zip": "^1.15.0",
"notion-helper": "^1.3.22",
"os-proxy-config": "^1.1.2",
"pdfjs-dist": "4.10.38",
"selection-hook": "^1.0.4",
"selection-hook": "^1.0.6",
"turndown": "7.2.0"
},
"devDependencies": {
@@ -89,6 +92,7 @@
"@cherrystudio/embedjs-loader-xml": "^0.1.31",
"@cherrystudio/embedjs-ollama": "^0.1.31",
"@cherrystudio/embedjs-openai": "^0.1.31",
"@codemirror/view": "^6.0.0",
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/preload": "^3.0.0",
@@ -104,7 +108,7 @@
"@langchain/community": "^0.3.36",
"@langchain/ollama": "^0.2.1",
"@mistralai/mistralai": "^1.6.0",
"@modelcontextprotocol/sdk": "^1.11.4",
"@modelcontextprotocol/sdk": "^1.12.3",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@playwright/test": "^1.52.0",
@@ -112,6 +116,7 @@
"@shikijs/markdown-it": "^3.7.0",
"@swc/plugin-styled-components": "^7.1.5",
"@tanstack/react-query": "^5.27.0",
"@tanstack/react-virtual": "^3.13.12",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -137,6 +142,8 @@
"@vitest/coverage-v8": "^3.1.4",
"@vitest/ui": "^3.1.4",
"@vitest/web-worker": "^3.1.4",
"@viz-js/lang-dot": "^1.0.5",
"@viz-js/viz": "^3.14.0",
"@xyflow/react": "^12.4.4",
"antd": "patch:antd@npm%3A5.24.7#~/.yarn/patches/antd-npm-5.24.7-356a553ae5.patch",
"archiver": "^7.0.1",
@@ -169,6 +176,7 @@
"eslint-plugin-unused-imports": "^4.1.4",
"fast-diff": "^1.3.0",
"fast-xml-parser": "^5.2.0",
"framer-motion": "^12.17.3",
"franc-min": "^6.2.0",
"fs-extra": "^11.2.0",
"google-auth-library": "^9.15.1",
@@ -199,8 +207,8 @@
"react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.1.2",
"react-router": "6",
"react-router-dom": "6",
"react-router": "^7.6.2",
"react-router-dom": "^7.6.2",
"react-spinners": "^0.14.1",
"react-window": "^1.8.11",
"redux": "^5.0.1",
@@ -221,6 +229,7 @@
"tiny-pinyin": "^1.3.2",
"tokenx": "^1.1.0",
"typescript": "^5.6.2",
"unified": "^11.0.5",
"uuid": "^10.0.0",
"vite": "6.2.6",
"vitest": "^3.1.4",

View File

@@ -36,6 +36,7 @@ export enum IpcChannel {
App_MacRequestProcessTrust = 'app:mac-request-process-trust',
App_QuoteToMain = 'app:quote-to-main',
App_SetDisableHardwareAcceleration = 'app:set-disable-hardware-acceleration',
Notification_Send = 'notification:send',
Notification_OnClick = 'notification:on-click',
@@ -73,6 +74,8 @@ export enum IpcChannel {
Mcp_ServersChanged = 'mcp:servers-changed',
Mcp_ServersUpdated = 'mcp:servers-updated',
Mcp_CheckConnectivity = 'mcp:check-connectivity',
Mcp_SetProgress = 'mcp:set-progress',
Mcp_AbortTool = 'mcp:abort-tool',
// Python
Python_Execute = 'python:execute',
@@ -144,6 +147,7 @@ export enum IpcChannel {
File_Base64File = 'file:base64File',
File_GetPdfInfo = 'file:getPdfInfo',
Fs_Read = 'fs:read',
File_OpenWithRelativePath = 'file:openWithRelativePath',
// file service
FileService_Upload = 'file-service:upload',
@@ -164,6 +168,16 @@ export enum IpcChannel {
Backup_CheckConnection = 'backup:checkConnection',
Backup_CreateDirectory = 'backup:createDirectory',
Backup_DeleteWebdavFile = 'backup:deleteWebdavFile',
Backup_BackupToLocalDir = 'backup:backupToLocalDir',
Backup_RestoreFromLocalBackup = 'backup:restoreFromLocalBackup',
Backup_ListLocalBackupFiles = 'backup:listLocalBackupFiles',
Backup_DeleteLocalBackupFile = 'backup:deleteLocalBackupFile',
Backup_SetLocalBackupDir = 'backup:setLocalBackupDir',
Backup_BackupToS3 = 'backup:backupToS3',
Backup_RestoreFromS3 = 'backup:restoreFromS3',
Backup_ListS3Files = 'backup:listS3Files',
Backup_DeleteS3File = 'backup:deleteS3File',
Backup_CheckS3Connection = 'backup:checkS3Connection',
// zip
Zip_Compress = 'zip:compress',
@@ -228,5 +242,12 @@ export enum IpcChannel {
Selection_ActionWindowMinimize = 'selection:action-window-minimize',
Selection_ActionWindowPin = 'selection:action-window-pin',
Selection_ProcessAction = 'selection:process-action',
Selection_UpdateActionData = 'selection:update-action-data'
Selection_UpdateActionData = 'selection:update-action-data',
// Navigation
Navigation_Url = 'navigation:url',
Navigation_Close = 'navigation:close',
// Settings Window
SettingsWindow_Show = 'settings-window:show'
}

View File

@@ -10,13 +10,13 @@ if (isDev) {
export const DATA_PATH = getDataPath()
export const titleBarOverlayDark = {
height: 40,
height: 42,
color: 'rgba(255,255,255,0)',
symbolColor: '#fff'
}
export const titleBarOverlayLight = {
height: 40,
height: 42,
color: 'rgba(255,255,255,0)',
symbolColor: '#000'
}

View File

@@ -28,6 +28,14 @@ import { windowService } from './services/WindowService'
Logger.initialize()
/**
* Disable hardware acceleration if setting is enabled
*/
const disableHardwareAcceleration = configManager.getDisableHardwareAcceleration()
if (disableHardwareAcceleration) {
app.disableHardwareAcceleration()
}
/**
* Disable chromium's window animations
* main purpose for this is to avoid the transparent window flashing when it is shown

View File

@@ -12,8 +12,10 @@ import { BrowserWindow, dialog, ipcMain, session, shell, systemPreferences, webC
import log from 'electron-log'
import { Notification } from 'src/renderer/src/types/notification'
import appService from './services/AppService'
import AppUpdater from './services/AppUpdater'
import BackupManager from './services/BackupManager'
import { CacheService } from './services/CacheService'
import { configManager } from './services/ConfigManager'
import CopilotService from './services/CopilotService'
import { ExportService } from './services/ExportService'
@@ -29,6 +31,7 @@ import { pythonService } from './services/PythonService'
import { FileServiceManager } from './services/remotefile/FileServiceManager'
import { searchService } from './services/SearchService'
import { SelectionService } from './services/SelectionService'
import { SettingsWindowService } from './services/SettingsWindowService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import storeSyncService from './services/StoreSyncService'
import { themeService } from './services/ThemeService'
@@ -114,12 +117,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
})
// launch on boot
ipcMain.handle(IpcChannel.App_SetLaunchOnBoot, (_, openAtLogin: boolean) => {
// Set login item settings for windows and mac
// linux is not supported because it requires more file operations
if (isWin || isMac) {
app.setLoginItemSettings({ openAtLogin })
}
ipcMain.handle(IpcChannel.App_SetLaunchOnBoot, (_, isLaunchOnBoot: boolean) => {
appService.setAppLaunchOnBoot(isLaunchOnBoot)
})
// launch to tray
@@ -368,6 +367,16 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.Backup_CheckConnection, backupManager.checkConnection)
ipcMain.handle(IpcChannel.Backup_CreateDirectory, backupManager.createDirectory)
ipcMain.handle(IpcChannel.Backup_DeleteWebdavFile, backupManager.deleteWebdavFile)
ipcMain.handle(IpcChannel.Backup_BackupToLocalDir, backupManager.backupToLocalDir)
ipcMain.handle(IpcChannel.Backup_RestoreFromLocalBackup, backupManager.restoreFromLocalBackup)
ipcMain.handle(IpcChannel.Backup_ListLocalBackupFiles, backupManager.listLocalBackupFiles)
ipcMain.handle(IpcChannel.Backup_DeleteLocalBackupFile, backupManager.deleteLocalBackupFile)
ipcMain.handle(IpcChannel.Backup_SetLocalBackupDir, backupManager.setLocalBackupDir)
ipcMain.handle(IpcChannel.Backup_BackupToS3, backupManager.backupToS3)
ipcMain.handle(IpcChannel.Backup_RestoreFromS3, backupManager.restoreFromS3)
ipcMain.handle(IpcChannel.Backup_ListS3Files, backupManager.listS3Files)
ipcMain.handle(IpcChannel.Backup_DeleteS3File, backupManager.deleteS3File)
ipcMain.handle(IpcChannel.Backup_CheckS3Connection, backupManager.checkS3Connection)
// file
ipcMain.handle(IpcChannel.File_Open, fileManager.open)
@@ -392,6 +401,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile)
ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile)
ipcMain.handle(IpcChannel.File_BinaryImage, fileManager.binaryImage)
ipcMain.handle(IpcChannel.File_OpenWithRelativePath, fileManager.openFileWithRelativePath)
// file service
ipcMain.handle(IpcChannel.FileService_Upload, async (_, provider: Provider, file: FileMetadata) => {
@@ -494,6 +504,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.Mcp_GetResource, mcpService.getResource)
ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo)
ipcMain.handle(IpcChannel.Mcp_CheckConnectivity, mcpService.checkMcpConnectivity)
ipcMain.handle(IpcChannel.Mcp_AbortTool, mcpService.abortTool)
ipcMain.handle(IpcChannel.Mcp_SetProgress, (_, progress: number) => {
mainWindow.webContents.send('mcp-progress', progress)
})
// Register Python execution handler
ipcMain.handle(
@@ -561,4 +575,16 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
SelectionService.registerIpcHandler()
ipcMain.handle(IpcChannel.App_QuoteToMain, (_, text: string) => windowService.quoteToMainWindow(text))
ipcMain.handle(IpcChannel.App_SetDisableHardwareAcceleration, (_, isDisable: boolean) => {
configManager.setDisableHardwareAcceleration(isDisable)
})
// Navigation
ipcMain.handle(IpcChannel.Navigation_Url, (_, url: string) => {
CacheService.set('navigation-url', url)
})
// Settings Window
SettingsWindowService.registerIpcHandler()
}

View File

@@ -5,26 +5,19 @@ import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-op
import { getInstanceName } from '@main/utils'
import { KnowledgeBaseParams } from '@types'
import { SUPPORTED_DIM_MODELS as VOYAGE_SUPPORTED_DIM_MODELS, VoyageEmbeddings } from './VoyageEmbeddings'
import { VOYAGE_SUPPORTED_DIM_MODELS } from './utils'
import { VoyageEmbeddings } from './VoyageEmbeddings'
export default class EmbeddingsFactory {
static create({ model, provider, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams): BaseEmbeddings {
const batchSize = 10
if (provider === 'voyageai') {
if (VOYAGE_SUPPORTED_DIM_MODELS.includes(model)) {
return new VoyageEmbeddings({
modelName: model,
apiKey,
outputDimension: dimensions,
batchSize: 8
})
} else {
return new VoyageEmbeddings({
modelName: model,
apiKey,
batchSize: 8
})
}
return new VoyageEmbeddings({
modelName: model,
apiKey,
outputDimension: VOYAGE_SUPPORTED_DIM_MODELS.includes(model) ? dimensions : undefined,
batchSize: 8
})
}
if (provider === 'ollama') {
if (baseURL.includes('v1/')) {

View File

@@ -1,27 +1,29 @@
import { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { VoyageEmbeddings as _VoyageEmbeddings } from '@langchain/community/embeddings/voyage'
import { VOYAGE_SUPPORTED_DIM_MODELS } from './utils'
/**
* 支持设置嵌入维度的模型
*/
export const SUPPORTED_DIM_MODELS = ['voyage-3-large', 'voyage-3.5', 'voyage-3.5-lite', 'voyage-code-3']
export class VoyageEmbeddings extends BaseEmbeddings {
private model: _VoyageEmbeddings
constructor(private readonly configuration?: ConstructorParameters<typeof _VoyageEmbeddings>[0]) {
super()
if (!this.configuration) this.configuration = {}
if (!this.configuration.modelName) this.configuration.modelName = 'voyage-3'
if (!SUPPORTED_DIM_MODELS.includes(this.configuration.modelName) && this.configuration.outputDimension) {
throw new Error(`VoyageEmbeddings only supports ${SUPPORTED_DIM_MODELS.join(', ')}`)
if (!this.configuration) {
throw new Error('Pass in a configuration.')
}
if (!this.configuration.modelName) this.configuration.modelName = 'voyage-3'
this.model = new _VoyageEmbeddings(this.configuration)
if (!VOYAGE_SUPPORTED_DIM_MODELS.includes(this.configuration.modelName) && this.configuration.outputDimension) {
console.error(`VoyageEmbeddings only supports ${VOYAGE_SUPPORTED_DIM_MODELS.join(', ')} to set outputDimension.`)
this.model = new _VoyageEmbeddings({ ...this.configuration, outputDimension: undefined })
} else {
this.model = new _VoyageEmbeddings(this.configuration)
}
}
override async getDimensions(): Promise<number> {
if (!this.configuration?.outputDimension) {
throw new Error('You need to pass in the optional dimensions parameter for this model')
}
return this.configuration?.outputDimension
return this.configuration?.outputDimension ?? (this.configuration?.modelName === 'voyage-code-2' ? 1536 : 1024)
}
override async embedDocuments(texts: string[]): Promise<number[][]> {

View File

@@ -0,0 +1,45 @@
export const VOYAGE_SUPPORTED_DIM_MODELS = ['voyage-3-large', 'voyage-3.5', 'voyage-3.5-lite', 'voyage-code-3']
// NOTE: 下面的暂时没用上,但先留着吧
export const OPENAI_SUPPORTED_DIM_MODELS = ['text-embedding-3-small', 'text-embedding-3-large']
export const DASHSCOPE_SUPPORTED_DIM_MODELS = ['text-embedding-v3', 'text-embedding-v4']
export const OPENSOURCE_SUPPORTED_DIM_MODELS = ['qwen3-embedding-0.6B', 'qwen3-embedding-4B', 'qwen3-embedding-8B']
export const GOOGLE_SUPPORTED_DIM_MODELS = ['gemini-embedding-exp-03-07', 'gemini-embedding-exp']
export const SUPPORTED_DIM_MODELS = [
...VOYAGE_SUPPORTED_DIM_MODELS,
...OPENAI_SUPPORTED_DIM_MODELS,
...DASHSCOPE_SUPPORTED_DIM_MODELS,
...OPENSOURCE_SUPPORTED_DIM_MODELS,
...GOOGLE_SUPPORTED_DIM_MODELS
]
/**
* 从模型 ID 中提取基础名称。
* 例如:
* - 'deepseek/deepseek-r1' => 'deepseek-r1'
* - 'deepseek-ai/deepseek/deepseek-r1' => 'deepseek-r1'
* @param {string} id 模型 ID
* @param {string} [delimiter='/'] 分隔符,默认为 '/'
* @returns {string} 基础名称
*/
export const getBaseModelName = (id: string, delimiter: string = '/'): string => {
const parts = id.split(delimiter)
return parts[parts.length - 1]
}
/**
* 从模型 ID 中提取基础名称并转换为小写。
* 例如:
* - 'deepseek/DeepSeek-R1' => 'deepseek-r1'
* - 'deepseek-ai/deepseek/DeepSeek-R1' => 'deepseek-r1'
* @param {string} id 模型 ID
* @param {string} [delimiter='/'] 分隔符,默认为 '/'
* @returns {string} 小写的基础名称
*/
export const getLowerBaseModelName = (id: string, delimiter: string = '/'): string => {
return getBaseModelName(id, delimiter).toLowerCase()
}

View File

@@ -1,8 +1,7 @@
import * as fs from 'node:fs'
import { JsonLoader, LocalPathLoader, RAGApplication, TextLoader } from '@cherrystudio/embedjs'
import type { AddLoaderReturn } from '@cherrystudio/embedjs-interfaces'
import { WebLoader } from '@cherrystudio/embedjs-loader-web'
import { readTextFileWithAutoEncoding } from '@main/utils/file'
import { LoaderReturn } from '@shared/config/types'
import { FileMetadata, KnowledgeBaseParams } from '@types'
import Logger from 'electron-log'
@@ -115,7 +114,7 @@ export async function addFileLoader(
// HTML类型处理
loaderReturn = await ragApplication.addLoader(
new WebLoader({
urlOrContent: fs.readFileSync(file.path, 'utf-8'),
urlOrContent: await readTextFileWithAutoEncoding(file.path),
chunkSize: base.chunkSize,
chunkOverlap: base.chunkOverlap
}) as any,
@@ -125,7 +124,7 @@ export async function addFileLoader(
case 'json':
try {
jsonObject = JSON.parse(fs.readFileSync(file.path, 'utf-8'))
jsonObject = JSON.parse(await readTextFileWithAutoEncoding(file.path))
} catch (error) {
jsonParsed = false
Logger.warn('[KnowledgeBase] failed parsing json file, falling back to text processing:', file.path, error)
@@ -141,7 +140,7 @@ export async function addFileLoader(
// 如果是其他文本类型且尚未读取文件,则读取文件
loaderReturn = await ragApplication.addLoader(
new TextLoader({
text: fs.readFileSync(file.path, 'utf-8'),
text: await readTextFileWithAutoEncoding(file.path),
chunkSize: base.chunkSize,
chunkOverlap: base.chunkOverlap
}) as any,

View File

@@ -217,7 +217,7 @@ export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
* @param filePath
*/
private async convertFile(uid: string, filePath: string): Promise<void> {
const fileName = path.basename(filePath).split('.')[0]
const fileName = path.parse(filePath).name
const config = {
...this.createAuthConfig(),
headers: {

View File

@@ -111,7 +111,6 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
}
private async validateFile(filePath: string): Promise<void> {
const quota = await this.checkQuota()
const pdfBuffer = await fs.promises.readFile(filePath)
const doc = await this.readPdf(new Uint8Array(pdfBuffer))
@@ -125,10 +124,6 @@ export default class MineruPreprocessProvider extends BasePreprocessProvider {
const fileSizeMB = Math.round(pdfBuffer.length / (1024 * 1024))
throw new Error(`PDF file size (${fileSizeMB}MB) exceeds the limit of 200MB`)
}
// 检查配额
if (quota <= 0 || quota - doc.numPages <= 0) {
throw new Error('MinerU解析配额不足请申请企业账户或自行部署剩余额度' + quota)
}
}
private createProcessedFileInfo(file: FileMetadata, outputPath: string): FileMetadata {

View File

@@ -0,0 +1,81 @@
import { isDev, isLinux, isMac, isWin } from '@main/constant'
import { app } from 'electron'
import log from 'electron-log'
import fs from 'fs'
import os from 'os'
import path from 'path'
export class AppService {
private static instance: AppService
private constructor() {
// Private constructor to prevent direct instantiation
}
public static getInstance(): AppService {
if (!AppService.instance) {
AppService.instance = new AppService()
}
return AppService.instance
}
public async setAppLaunchOnBoot(isLaunchOnBoot: boolean): Promise<void> {
// Set login item settings for windows and mac
// linux is not supported because it requires more file operations
if (isWin || isMac) {
app.setLoginItemSettings({ openAtLogin: isLaunchOnBoot })
} else if (isLinux) {
try {
const autostartDir = path.join(os.homedir(), '.config', 'autostart')
const desktopFile = path.join(autostartDir, isDev ? 'cherry-studio-dev.desktop' : 'cherry-studio.desktop')
if (isLaunchOnBoot) {
// Ensure autostart directory exists
try {
await fs.promises.access(autostartDir)
} catch {
await fs.promises.mkdir(autostartDir, { recursive: true })
}
// Get executable path
let executablePath = app.getPath('exe')
if (process.env.APPIMAGE) {
// For AppImage packaged apps, use APPIMAGE environment variable
executablePath = process.env.APPIMAGE
}
// Create desktop file content
const desktopContent = `[Desktop Entry]
Type=Application
Name=Cherry Studio
Comment=A powerful AI assistant for producer.
Exec=${executablePath}
Icon=cherrystudio
Terminal=false
StartupNotify=false
Categories=Development;Utility;
X-GNOME-Autostart-enabled=true
Hidden=false`
// Write desktop file
await fs.promises.writeFile(desktopFile, desktopContent)
log.info('Created autostart desktop file for Linux')
} else {
// Remove desktop file
try {
await fs.promises.access(desktopFile)
await fs.promises.unlink(desktopFile)
log.info('Removed autostart desktop file for Linux')
} catch {
// File doesn't exist, no need to remove
}
}
} catch (error) {
log.error('Failed to set launch on boot for Linux:', error)
}
}
}
}
// Default export as singleton instance
export default AppService.getInstance()

View File

@@ -1,5 +1,6 @@
import { IpcChannel } from '@shared/IpcChannel'
import { WebDavConfig } from '@types'
import { S3Config } from '@types'
import archiver from 'archiver'
import { exec } from 'child_process'
import { app } from 'electron'
@@ -10,6 +11,7 @@ import * as path from 'path'
import { CreateDirectoryOptions, FileStat } from 'webdav'
import { getDataPath } from '../utils'
import S3Storage from './S3Storage'
import WebDav from './WebDav'
import { windowService } from './WindowService'
@@ -25,6 +27,16 @@ class BackupManager {
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
this.listWebdavFiles = this.listWebdavFiles.bind(this)
this.deleteWebdavFile = this.deleteWebdavFile.bind(this)
this.listLocalBackupFiles = this.listLocalBackupFiles.bind(this)
this.deleteLocalBackupFile = this.deleteLocalBackupFile.bind(this)
this.backupToLocalDir = this.backupToLocalDir.bind(this)
this.restoreFromLocalBackup = this.restoreFromLocalBackup.bind(this)
this.setLocalBackupDir = this.setLocalBackupDir.bind(this)
this.backupToS3 = this.backupToS3.bind(this)
this.restoreFromS3 = this.restoreFromS3.bind(this)
this.listS3Files = this.listS3Files.bind(this)
this.deleteS3File = this.deleteS3File.bind(this)
this.checkS3Connection = this.checkS3Connection.bind(this)
}
private async setWritableRecursive(dirPath: string): Promise<void> {
@@ -85,7 +97,11 @@ class BackupManager {
const onProgress = (processData: { stage: string; progress: number; total: number }) => {
mainWindow?.webContents.send(IpcChannel.BackupProgress, processData)
Logger.log('[BackupManager] backup progress', processData)
// 只在关键阶段记录日志:开始、结束和主要阶段转换点
const logStages = ['preparing', 'writing_data', 'preparing_compression', 'completed']
if (logStages.includes(processData.stage) || processData.progress === 100) {
Logger.log('[BackupManager] backup progress', processData)
}
}
try {
@@ -147,18 +163,23 @@ class BackupManager {
let totalBytes = 0
let processedBytes = 0
// 首先计算总文件数和总大小
// 首先计算总文件数和总大小,但不记录详细日志
const calculateTotals = async (dirPath: string) => {
const items = await fs.readdir(dirPath, { withFileTypes: true })
for (const item of items) {
const fullPath = path.join(dirPath, item.name)
if (item.isDirectory()) {
await calculateTotals(fullPath)
} else {
totalEntries++
const stats = await fs.stat(fullPath)
totalBytes += stats.size
try {
const items = await fs.readdir(dirPath, { withFileTypes: true })
for (const item of items) {
const fullPath = path.join(dirPath, item.name)
if (item.isDirectory()) {
await calculateTotals(fullPath)
} else {
totalEntries++
const stats = await fs.stat(fullPath)
totalBytes += stats.size
}
}
} catch (error) {
// 仅在出错时记录日志
Logger.error('[BackupManager] Error calculating totals:', error)
}
}
@@ -230,7 +251,11 @@ class BackupManager {
const onProgress = (processData: { stage: string; progress: number; total: number }) => {
mainWindow?.webContents.send(IpcChannel.RestoreProgress, processData)
Logger.log('[BackupManager] restore progress', processData)
// 只在关键阶段记录日志
const logStages = ['preparing', 'extracting', 'extracted', 'reading_data', 'completed']
if (logStages.includes(processData.stage) || processData.progress === 100) {
Logger.log('[BackupManager] restore progress', processData)
}
}
try {
@@ -382,21 +407,54 @@ class BackupManager {
destination: string,
onProgress: (size: number) => void
): Promise<void> {
const items = await fs.readdir(source, { withFileTypes: true })
// 先统计总文件数
let totalFiles = 0
let processedFiles = 0
let lastProgressReported = 0
for (const item of items) {
const sourcePath = path.join(source, item.name)
const destPath = path.join(destination, item.name)
// 计算总文件数
const countFiles = async (dir: string): Promise<number> => {
let count = 0
const items = await fs.readdir(dir, { withFileTypes: true })
for (const item of items) {
if (item.isDirectory()) {
count += await countFiles(path.join(dir, item.name))
} else {
count++
}
}
return count
}
if (item.isDirectory()) {
await fs.ensureDir(destPath)
await this.copyDirWithProgress(sourcePath, destPath, onProgress)
} else {
const stats = await fs.stat(sourcePath)
await fs.copy(sourcePath, destPath)
onProgress(stats.size)
totalFiles = await countFiles(source)
// 复制文件并更新进度
const copyDir = async (src: string, dest: string): Promise<void> => {
const items = await fs.readdir(src, { withFileTypes: true })
for (const item of items) {
const sourcePath = path.join(src, item.name)
const destPath = path.join(dest, item.name)
if (item.isDirectory()) {
await fs.ensureDir(destPath)
await copyDir(sourcePath, destPath)
} else {
const stats = await fs.stat(sourcePath)
await fs.copy(sourcePath, destPath)
processedFiles++
// 只在进度变化超过5%时报告进度
const currentProgress = Math.floor((processedFiles / totalFiles) * 100)
if (currentProgress - lastProgressReported >= 5 || processedFiles === totalFiles) {
lastProgressReported = currentProgress
onProgress(stats.size)
}
}
}
}
await copyDir(source, destination)
}
async checkConnection(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
@@ -423,6 +481,191 @@ class BackupManager {
throw new Error(error.message || 'Failed to delete backup file')
}
}
async backupToLocalDir(
_: Electron.IpcMainInvokeEvent,
data: string,
fileName: string,
localConfig: {
localBackupDir: string
skipBackupFile: boolean
}
) {
try {
const backupDir = localConfig.localBackupDir
// Create backup directory if it doesn't exist
await fs.ensureDir(backupDir)
const backupedFilePath = await this.backup(_, fileName, data, backupDir, localConfig.skipBackupFile)
return backupedFilePath
} catch (error) {
Logger.error('[BackupManager] Local backup failed:', error)
throw error
}
}
async backupToS3(_: Electron.IpcMainInvokeEvent, data: string, s3Config: S3Config) {
const os = require('os')
const deviceName = os.hostname ? os.hostname() : 'device'
const timestamp = new Date()
.toISOString()
.replace(/[-:T.Z]/g, '')
.slice(0, 14)
const filename = s3Config.fileName || `cherry-studio.backup.${deviceName}.${timestamp}.zip`
Logger.log(`[BackupManager] Starting S3 backup to ${filename}`)
const backupedFilePath = await this.backup(_, filename, data, undefined, s3Config.skipBackupFile)
const s3Client = new S3Storage(s3Config)
try {
const fileBuffer = await fs.promises.readFile(backupedFilePath)
const result = await s3Client.putFileContents(filename, fileBuffer)
await fs.remove(backupedFilePath)
Logger.log(`[BackupManager] S3 backup completed successfully: ${filename}`)
return result
} catch (error) {
Logger.error(`[BackupManager] S3 backup failed:`, error)
await fs.remove(backupedFilePath)
throw error
}
}
async restoreFromLocalBackup(_: Electron.IpcMainInvokeEvent, fileName: string, localBackupDir: string) {
try {
const backupDir = localBackupDir
const backupPath = path.join(backupDir, fileName)
if (!fs.existsSync(backupPath)) {
throw new Error(`Backup file not found: ${backupPath}`)
}
return await this.restore(_, backupPath)
} catch (error) {
Logger.error('[BackupManager] Local restore failed:', error)
throw error
}
}
async listLocalBackupFiles(_: Electron.IpcMainInvokeEvent, localBackupDir: string) {
try {
const files = await fs.readdir(localBackupDir)
const result: Array<{ fileName: string; modifiedTime: string; size: number }> = []
for (const file of files) {
const filePath = path.join(localBackupDir, file)
const stat = await fs.stat(filePath)
if (stat.isFile() && file.endsWith('.zip')) {
result.push({
fileName: file,
modifiedTime: stat.mtime.toISOString(),
size: stat.size
})
}
}
// Sort by modified time, newest first
return result.sort((a, b) => new Date(b.modifiedTime).getTime() - new Date(a.modifiedTime).getTime())
} catch (error) {
Logger.error('[BackupManager] List local backup files failed:', error)
throw error
}
}
async deleteLocalBackupFile(_: Electron.IpcMainInvokeEvent, fileName: string, localBackupDir: string) {
try {
const filePath = path.join(localBackupDir, fileName)
if (!fs.existsSync(filePath)) {
throw new Error(`Backup file not found: ${filePath}`)
}
await fs.remove(filePath)
return true
} catch (error) {
Logger.error('[BackupManager] Delete local backup file failed:', error)
throw error
}
}
async setLocalBackupDir(_: Electron.IpcMainInvokeEvent, dirPath: string) {
try {
// Check if directory exists
await fs.ensureDir(dirPath)
return true
} catch (error) {
Logger.error('[BackupManager] Set local backup directory failed:', error)
throw error
}
}
async restoreFromS3(_: Electron.IpcMainInvokeEvent, s3Config: S3Config) {
const filename = s3Config.fileName || 'cherry-studio.backup.zip'
Logger.log(`[BackupManager] Starting restore from S3: ${filename}`)
const s3Client = new S3Storage(s3Config)
try {
const retrievedFile = await s3Client.getFileContents(filename)
const backupedFilePath = path.join(this.backupDir, filename)
if (!fs.existsSync(this.backupDir)) {
fs.mkdirSync(this.backupDir, { recursive: true })
}
await new Promise<void>((resolve, reject) => {
const writeStream = fs.createWriteStream(backupedFilePath)
writeStream.write(retrievedFile as Buffer)
writeStream.end()
writeStream.on('finish', () => resolve())
writeStream.on('error', (error) => reject(error))
})
Logger.log(`[BackupManager] S3 restore file downloaded successfully: ${filename}`)
return await this.restore(_, backupedFilePath)
} catch (error: any) {
Logger.error('[BackupManager] Failed to restore from S3:', error)
throw new Error(error.message || 'Failed to restore backup file')
}
}
listS3Files = async (_: Electron.IpcMainInvokeEvent, s3Config: S3Config) => {
try {
const s3Client = new S3Storage(s3Config)
const objects = await s3Client.listFiles()
const files = objects
.filter((obj) => obj.key.endsWith('.zip'))
.map((obj) => {
const segments = obj.key.split('/')
const fileName = segments[segments.length - 1]
return {
fileName,
modifiedTime: obj.lastModified || '',
size: obj.size
}
})
return files.sort((a, b) => new Date(b.modifiedTime).getTime() - new Date(a.modifiedTime).getTime())
} catch (error: any) {
Logger.error('Failed to list S3 files:', error)
throw new Error(error.message || 'Failed to list backup files')
}
}
async deleteS3File(_: Electron.IpcMainInvokeEvent, fileName: string, s3Config: S3Config) {
try {
const s3Client = new S3Storage(s3Config)
return await s3Client.deleteFile(fileName)
} catch (error: any) {
Logger.error('Failed to delete S3 file:', error)
throw new Error(error.message || 'Failed to delete backup file')
}
}
async checkS3Connection(_: Electron.IpcMainInvokeEvent, s3Config: S3Config) {
const s3Client = new S3Storage(s3Config)
return await s3Client.checkConnection()
}
}
export default BackupManager

View File

@@ -1,7 +1,7 @@
interface CacheItem<T> {
data: T
timestamp: number
duration: number
duration?: number
}
export class CacheService {
@@ -11,9 +11,9 @@ export class CacheService {
* Set cache
* @param key Cache key
* @param data Cache data
* @param duration Cache duration (in milliseconds)
* @param duration Cache duration (in milliseconds), if not set the cache will never expire
*/
static set<T>(key: string, data: T, duration: number): void {
static set<T>(key: string, data: T, duration?: number): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
@@ -30,6 +30,11 @@ export class CacheService {
const item = this.cache.get(key)
if (!item) return null
// If duration is undefined, cache never expires
if (item.duration === undefined) {
return item.data
}
const now = Date.now()
if (now - item.timestamp > item.duration) {
this.remove(key)
@@ -63,6 +68,11 @@ export class CacheService {
const item = this.cache.get(key)
if (!item) return false
// If duration is undefined, cache never expires
if (item.duration === undefined) {
return true
}
const now = Date.now()
if (now - item.timestamp > item.duration) {
this.remove(key)

View File

@@ -24,7 +24,8 @@ export enum ConfigKeys {
SelectionAssistantFollowToolbar = 'selectionAssistantFollowToolbar',
SelectionAssistantRemeberWinSize = 'selectionAssistantRemeberWinSize',
SelectionAssistantFilterMode = 'selectionAssistantFilterMode',
SelectionAssistantFilterList = 'selectionAssistantFilterList'
SelectionAssistantFilterList = 'selectionAssistantFilterList',
DisableHardwareAcceleration = 'disableHardwareAcceleration'
}
export class ConfigManager {
@@ -218,6 +219,14 @@ export class ConfigManager {
this.setAndNotify(ConfigKeys.SelectionAssistantFilterList, value)
}
getDisableHardwareAcceleration(): boolean {
return this.get<boolean>(ConfigKeys.DisableHardwareAcceleration, false)
}
setDisableHardwareAcceleration(value: boolean) {
this.set(ConfigKeys.DisableHardwareAcceleration, value)
}
setAndNotify(key: string, value: unknown) {
this.set(key, value, true)
}

View File

@@ -1,4 +1,4 @@
import { getFilesDir, getFileType, getTempDir } from '@main/utils/file'
import { getFilesDir, getFileType, getTempDir, readTextFileWithAutoEncoding } from '@main/utils/file'
import { documentExts, imageExts, MB } from '@shared/config/constant'
import { FileMetadata } from '@types'
import * as crypto from 'crypto'
@@ -188,6 +188,8 @@ class FileStorage {
count: 1
}
logger.info('[FileStorage] File uploaded:', fileMetadata)
return fileMetadata
}
@@ -229,7 +231,11 @@ class FileStorage {
await fs.promises.rm(path.join(this.storageDir, id), { recursive: true })
}
public readFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<string> => {
public readFile = async (
_: Electron.IpcMainInvokeEvent,
id: string,
detectEncoding: boolean = false
): Promise<string> => {
const filePath = path.join(this.storageDir, id)
const fileExtension = path.extname(filePath)
@@ -256,7 +262,16 @@ class FileStorage {
}
}
return fs.readFileSync(filePath, 'utf8')
try {
if (detectEncoding) {
return readTextFileWithAutoEncoding(filePath)
} else {
return fs.readFileSync(filePath, 'utf-8')
}
} catch (error) {
logger.error(error)
return 'failed to read file'
}
}
public createTempFile = async (_: Electron.IpcMainInvokeEvent, fileName: string): Promise<string> => {
@@ -409,6 +424,19 @@ class FileStorage {
shell.openPath(path).catch((err) => logger.error('[IPC - Error] Failed to open file:', err))
}
/**
* 通过相对路径打开文件,跨设备时使用
* @param file
*/
public openFileWithRelativePath = async (_: Electron.IpcMainInvokeEvent, file: FileMetadata): Promise<void> => {
const filePath = path.join(this.storageDir, file.name)
if (fs.existsSync(filePath)) {
shell.openPath(filePath).catch((err) => logger.error('[IPC - Error] Failed to open file:', err))
} else {
logger.warn('[IPC - Warning] File does not exist:', filePath)
}
}
public save = async (
_: Electron.IpcMainInvokeEvent,
fileName: string,

View File

@@ -24,9 +24,9 @@ import { WebLoader } from '@cherrystudio/embedjs-loader-web'
import Embeddings from '@main/knowledage/embeddings/Embeddings'
import { addFileLoader } from '@main/knowledage/loader'
import { NoteLoader } from '@main/knowledage/loader/noteLoader'
import OcrProvider from '@main/knowledage/ocr/OcrProvider'
import PreprocessProvider from '@main/knowledage/preprocess/PreprocessProvider'
import Reranker from '@main/knowledage/reranker/Reranker'
import OcrProvider from '@main/ocr/OcrProvider'
import PreprocessProvider from '@main/preprocess/PreprocessProvider'
import { windowService } from '@main/services/WindowService'
import { getDataPath } from '@main/utils'
import { getAllFiles } from '@main/utils/file'

View File

@@ -28,6 +28,7 @@ import { app } from 'electron'
import Logger from 'electron-log'
import { EventEmitter } from 'events'
import { memoize } from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { CacheService } from './CacheService'
import { CallBackServer } from './mcp/oauth/callback'
@@ -71,6 +72,7 @@ function withCache<T extends unknown[], R>(
class McpService {
private clients: Map<string, Client> = new Map()
private pendingClients: Map<string, Promise<Client>> = new Map()
private activeToolCalls: Map<string, AbortController> = new Map()
constructor() {
this.initClient = this.initClient.bind(this)
@@ -84,6 +86,7 @@ class McpService {
this.removeServer = this.removeServer.bind(this)
this.restartServer = this.restartServer.bind(this)
this.stopServer = this.stopServer.bind(this)
this.abortTool = this.abortTool.bind(this)
this.cleanup = this.cleanup.bind(this)
}
@@ -455,10 +458,14 @@ class McpService {
*/
public async callTool(
_: Electron.IpcMainInvokeEvent,
{ server, name, args }: { server: MCPServer; name: string; args: any }
{ server, name, args, callId }: { server: MCPServer; name: string; args: any; callId?: string }
): Promise<MCPCallToolResponse> {
const toolCallId = callId || uuidv4()
const abortController = new AbortController()
this.activeToolCalls.set(toolCallId, abortController)
try {
Logger.info('[MCP] Calling:', server.name, name, args)
Logger.info('[MCP] Calling:', server.name, name, args, 'callId:', toolCallId)
if (typeof args === 'string') {
try {
args = JSON.parse(args)
@@ -468,12 +475,19 @@ class McpService {
}
const client = await this.initClient(server)
const result = await client.callTool({ name, arguments: args }, undefined, {
timeout: server.timeout ? server.timeout * 1000 : 60000 // Default timeout of 1 minute
onprogress: (process) => {
console.log('[MCP] Progress:', process.progress / (process.total || 1))
window.api.mcp.setProgress(process.progress / (process.total || 1))
},
timeout: server.timeout ? server.timeout * 1000 : 60000, // Default timeout of 1 minute
signal: this.activeToolCalls.get(toolCallId)?.signal
})
return result as MCPCallToolResponse
} catch (error) {
Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error)
throw error
} finally {
this.activeToolCalls.delete(toolCallId)
}
}
@@ -664,6 +678,20 @@ class McpService {
delete env.http_proxy
delete env.https_proxy
}
// 实现 abortTool 方法
public async abortTool(_: Electron.IpcMainInvokeEvent, callId: string) {
const activeToolCall = this.activeToolCalls.get(callId)
if (activeToolCall) {
activeToolCall.abort()
this.activeToolCalls.delete(callId)
Logger.info(`[MCP] Aborted tool call: ${callId}`)
return true
} else {
Logger.warn(`[MCP] No active tool call found for callId: ${callId}`)
return false
}
}
}
export default new McpService()

View File

@@ -1,57 +0,0 @@
// import Logger from 'electron-log'
// import { Operator } from 'opendal'
// export default class RemoteStorage {
// public instance: Operator | undefined
// /**
// *
// * @param scheme is the scheme for opendal services. Available value includes "azblob", "azdls", "cos", "gcs", "obs", "oss", "s3", "webdav", "webhdfs", "aliyun-drive", "alluxio", "azfile", "dropbox", "gdrive", "onedrive", "postgresql", "mysql", "redis", "swift", "mongodb", "alluxio", "b2", "seafile", "upyun", "koofr", "yandex-disk"
// * @param options is the options for given opendal services. Valid options depend on the scheme. Checkout https://docs.rs/opendal/latest/opendal/services/index.html for all valid options.
// *
// * For example, use minio as remote storage:
// *
// * ```typescript
// * const storage = new RemoteStorage('s3', {
// * endpoint: 'http://localhost:9000',
// * region: 'us-east-1',
// * bucket: 'testbucket',
// * access_key_id: 'user',
// * secret_access_key: 'password',
// * root: '/path/to/basepath',
// * })
// * ```
// */
// constructor(scheme: string, options?: Record<string, string> | undefined | null) {
// this.instance = new Operator(scheme, options)
// this.putFileContents = this.putFileContents.bind(this)
// this.getFileContents = this.getFileContents.bind(this)
// }
// public putFileContents = async (filename: string, data: string | Buffer) => {
// if (!this.instance) {
// return new Error('RemoteStorage client not initialized')
// }
// try {
// return await this.instance.write(filename, data)
// } catch (error) {
// Logger.error('[RemoteStorage] Error putting file contents:', error)
// throw error
// }
// }
// public getFileContents = async (filename: string) => {
// if (!this.instance) {
// throw new Error('RemoteStorage client not initialized')
// }
// try {
// return await this.instance.read(filename)
// } catch (error) {
// Logger.error('[RemoteStorage] Error getting file contents:', error)
// throw error
// }
// }
// }

View File

@@ -0,0 +1,183 @@
import {
DeleteObjectCommand,
GetObjectCommand,
HeadBucketCommand,
ListObjectsV2Command,
PutObjectCommand,
S3Client
} from '@aws-sdk/client-s3'
import type { S3Config } from '@types'
import Logger from 'electron-log'
import * as net from 'net'
import { Readable } from 'stream'
/**
* 将可读流转换为 Buffer
*/
function streamToBuffer(stream: Readable): Promise<Buffer> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = []
stream.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)))
stream.on('error', reject)
stream.on('end', () => resolve(Buffer.concat(chunks)))
})
}
// 需要使用 Virtual Host-Style 的服务商域名后缀白名单
const VIRTUAL_HOST_SUFFIXES = ['aliyuncs.com', 'myqcloud.com']
/**
* 使用 AWS SDK v3 的简单 S3 封装,兼容之前 RemoteStorage 的最常用接口。
*/
export default class S3Storage {
private client: S3Client
private bucket: string
private root: string
constructor(config: S3Config) {
const { endpoint, region, accessKeyId, secretAccessKey, bucket, root } = config
const usePathStyle = (() => {
if (!endpoint) return false
try {
const { hostname } = new URL(endpoint)
if (hostname === 'localhost' || net.isIP(hostname) !== 0) {
return true
}
const isInWhiteList = VIRTUAL_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix))
return !isInWhiteList
} catch (e) {
Logger.warn('[S3Storage] Failed to parse endpoint, fallback to Path-Style:', endpoint, e)
return true
}
})()
this.client = new S3Client({
region,
endpoint: endpoint || undefined,
credentials: {
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey
},
forcePathStyle: usePathStyle
})
this.bucket = bucket
this.root = root?.replace(/^\/+/g, '').replace(/\/+$/g, '') || ''
this.putFileContents = this.putFileContents.bind(this)
this.getFileContents = this.getFileContents.bind(this)
this.deleteFile = this.deleteFile.bind(this)
this.listFiles = this.listFiles.bind(this)
this.checkConnection = this.checkConnection.bind(this)
}
/**
* 内部辅助方法,用来拼接带 root 的对象 key
*/
private buildKey(key: string): string {
if (!this.root) return key
return key.startsWith(`${this.root}/`) ? key : `${this.root}/${key}`
}
async putFileContents(key: string, data: Buffer | string) {
try {
const contentType = key.endsWith('.zip') ? 'application/zip' : 'application/octet-stream'
return await this.client.send(
new PutObjectCommand({
Bucket: this.bucket,
Key: this.buildKey(key),
Body: data,
ContentType: contentType
})
)
} catch (error) {
Logger.error('[S3Storage] Error putting object:', error)
throw error
}
}
async getFileContents(key: string): Promise<Buffer> {
try {
const res = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: this.buildKey(key) }))
if (!res.Body || !(res.Body instanceof Readable)) {
throw new Error('Empty body received from S3')
}
return await streamToBuffer(res.Body as Readable)
} catch (error) {
Logger.error('[S3Storage] Error getting object:', error)
throw error
}
}
async deleteFile(key: string) {
try {
const keyWithRoot = this.buildKey(key)
const variations = new Set([keyWithRoot, key.replace(/^\//, '')])
for (const k of variations) {
try {
await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: k }))
} catch {
// 忽略删除失败
}
}
} catch (error) {
Logger.error('[S3Storage] Error deleting object:', error)
throw error
}
}
/**
* 列举指定前缀下的对象,默认列举全部。
*/
async listFiles(prefix = ''): Promise<Array<{ key: string; lastModified?: string; size: number }>> {
const files: Array<{ key: string; lastModified?: string; size: number }> = []
let continuationToken: string | undefined
const fullPrefix = this.buildKey(prefix)
try {
do {
const res = await this.client.send(
new ListObjectsV2Command({
Bucket: this.bucket,
Prefix: fullPrefix === '' ? undefined : fullPrefix,
ContinuationToken: continuationToken
})
)
res.Contents?.forEach((obj) => {
if (!obj.Key) return
files.push({
key: obj.Key,
lastModified: obj.LastModified?.toISOString(),
size: obj.Size ?? 0
})
})
continuationToken = res.IsTruncated ? res.NextContinuationToken : undefined
} while (continuationToken)
return files
} catch (error) {
Logger.error('[S3Storage] Error listing objects:', error)
throw error
}
}
/**
* 尝试调用 HeadBucket 判断凭证/网络是否可用
*/
async checkConnection() {
try {
await this.client.send(new HeadBucketCommand({ Bucket: this.bucket }))
return true
} catch (error) {
Logger.error('[S3Storage] Error checking connection:', error)
throw error
}
}
}

View File

@@ -1,7 +1,7 @@
import { SELECTION_FINETUNED_LIST, SELECTION_PREDEFINED_BLACKLIST } from '@main/configs/SelectionConfig'
import { isDev, isMac, isWin } from '@main/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { BrowserWindow, ipcMain, screen, systemPreferences } from 'electron'
import { app, BrowserWindow, ipcMain, screen, systemPreferences } from 'electron'
import Logger from 'electron-log'
import { join } from 'path'
import type {
@@ -141,7 +141,7 @@ export class SelectionService {
* Initialize zoom factor from config and subscribe to changes
* Ensures UI elements scale properly with system DPI settings
*/
private initZoomFactor() {
private initZoomFactor(): void {
const zoomFactor = configManager.getZoomFactor()
if (zoomFactor) {
this.setZoomFactor(zoomFactor)
@@ -154,7 +154,7 @@ export class SelectionService {
this.zoomFactor = zoomFactor
}
private initConfig() {
private initConfig(): void {
this.triggerMode = configManager.getSelectionAssistantTriggerMode() as TriggerMode
this.isFollowToolbar = configManager.getSelectionAssistantFollowToolbar()
this.isRemeberWinSize = configManager.getSelectionAssistantRemeberWinSize()
@@ -207,7 +207,7 @@ export class SelectionService {
* @param mode - The mode to set, either 'default', 'whitelist', or 'blacklist'
* @param list - An array of strings representing the list of items to include or exclude
*/
private setHookGlobalFilterMode(mode: string, list: string[]) {
private setHookGlobalFilterMode(mode: string, list: string[]): void {
if (!this.selectionHook) return
const modeMap = {
@@ -245,7 +245,7 @@ export class SelectionService {
}
}
private setHookFineTunedList() {
private setHookFineTunedList(): void {
if (!this.selectionHook) return
const excludeClipboardCursorDetectList = isWin
@@ -271,6 +271,11 @@ export class SelectionService {
* @returns {boolean} Success status of service start
*/
public start(): boolean {
if (!isSupportedOS) {
this.logError(new Error('SelectionService start(): not supported on this OS'))
return false
}
if (!this.selectionHook) {
this.logError(new Error('SelectionService start(): instance is null'))
return false
@@ -373,7 +378,7 @@ export class SelectionService {
* Toggle the enabled state of the selection service
* Will sync the new enabled store to all renderer windows
*/
public toggleEnabled(enabled: boolean | undefined = undefined) {
public toggleEnabled(enabled: boolean | undefined = undefined): void {
if (!this.selectionHook) return
const newEnabled = enabled === undefined ? !configManager.getSelectionAssistantEnabled() : enabled
@@ -389,7 +394,7 @@ export class SelectionService {
* Sets up window properties, event handlers, and loads the toolbar UI
* @param readyCallback Optional callback when window is ready to show
*/
private createToolbarWindow(readyCallback?: () => void) {
private createToolbarWindow(readyCallback?: () => void): void {
if (this.isToolbarAlive()) return
const { toolbarWidth, toolbarHeight } = this.getToolbarRealSize()
@@ -414,9 +419,11 @@ export class SelectionService {
backgroundMaterial: 'none',
// Platform specific settings
// [macOS] DO NOT set type to 'panel', it will not work because it conflicts with other settings
// [macOS] DO NOT set focusable to false, it will make other windows bring to front together
...(isWin ? { type: 'toolbar', focusable: false } : {}),
// [macOS] `panel` conflicts with other settings ,
// and log will show `NSWindow does not support nonactivating panel styleMask 0x80`
// but it seems still work on fullscreen apps, so we set this anyway
...(isWin ? { type: 'toolbar', focusable: false } : { type: 'panel' }),
hiddenInMissionControl: true, // [macOS only]
acceptFirstMouse: true, // [macOS only]
@@ -447,13 +454,6 @@ export class SelectionService {
// Add show/hide event listeners
this.toolbarWindow.on('show', () => {
this.toolbarWindow?.webContents.send(IpcChannel.Selection_ToolbarVisibilityChange, true)
// [macOS] force the toolbar window to be visible on current desktop
// but it will make docker icon flash. And we found that it's not necessary now.
// will remove after testing
// if (isMac) {
// this.toolbarWindow!.setVisibleOnAllWorkspaces(false)
// }
})
this.toolbarWindow.on('hide', () => {
@@ -485,10 +485,10 @@ export class SelectionService {
* @param point Reference point for positioning, logical coordinates
* @param orientation Preferred position relative to reference point
*/
private showToolbarAtPosition(point: Point, orientation: RelativeOrientation) {
private showToolbarAtPosition(point: Point, orientation: RelativeOrientation, programName: string): void {
if (!this.isToolbarAlive()) {
this.createToolbarWindow(() => {
this.showToolbarAtPosition(point, orientation)
this.showToolbarAtPosition(point, orientation, programName)
})
return
}
@@ -509,25 +509,55 @@ export class SelectionService {
//should set every time the window is shown
this.toolbarWindow!.setAlwaysOnTop(true, 'screen-saver')
// [macOS] force the toolbar window to be visible on current desktop
// but it will make docker icon flash. And we found that it's not necessary now.
// will remove after testing
// if (isMac) {
// this.toolbarWindow!.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })
// }
if (!isMac) {
this.toolbarWindow!.show()
/**
* [Windows]
* In Windows 10, setOpacity(1) will make the window completely transparent
* It's a strange behavior, so we don't use it for compatibility
*/
// this.toolbarWindow!.setOpacity(1)
this.startHideByMouseKeyListener()
return
}
/************************************************
* [macOS] the following code is only for macOS
*
* WARNING:
* DO NOT MODIFY THESE CODES, UNLESS YOU REALLY KNOW WHAT YOU ARE DOING!!!!
*************************************************/
// [macOS] a hacky way
// when set `skipTransformProcessType: true`, if the selection is in self app, it will make the selection canceled after toolbar showing
// so we just don't set `skipTransformProcessType: true` when in self app
const isSelf = ['com.github.Electron', 'com.kangfenmao.CherryStudio'].includes(programName)
if (!isSelf) {
// [macOS] an ugly hacky way
// `focusable: true` will make mainWindow disappeared when `setVisibleOnAllWorkspaces`
// so we set `focusable: true` before showing, and then set false after showing
this.toolbarWindow!.setFocusable(false)
// [macOS]
// force `setVisibleOnAllWorkspaces: true` to let toolbar show in all workspaces. And we MUST not set it to false again
// set `skipTransformProcessType: true` to avoid dock icon spinning when `setVisibleOnAllWorkspaces`
this.toolbarWindow!.setVisibleOnAllWorkspaces(true, {
visibleOnFullScreen: true,
skipTransformProcessType: true
})
}
// [macOS] MUST use `showInactive()` to prevent other windows bring to front together
// [Windows] is OK for both `show()` and `showInactive()` because of `focusable: false`
this.toolbarWindow!.showInactive()
/**
* [Windows]
* In Windows 10, setOpacity(1) will make the window completely transparent
* It's a strange behavior, so we don't use it for compatibility
*/
// this.toolbarWindow!.setOpacity(1)
// [macOS] restore the focusable status
this.toolbarWindow!.setFocusable(true)
this.startHideByMouseKeyListener()
return
}
/**
@@ -588,8 +618,8 @@ export class SelectionService {
* Check if toolbar window exists and is not destroyed
* @returns {boolean} Toolbar window status
*/
private isToolbarAlive() {
return this.toolbarWindow && !this.toolbarWindow.isDestroyed()
private isToolbarAlive(): boolean {
return !!(this.toolbarWindow && !this.toolbarWindow.isDestroyed())
}
/**
@@ -598,7 +628,7 @@ export class SelectionService {
* @param width New toolbar width
* @param height New toolbar height
*/
public determineToolbarSize(width: number, height: number) {
public determineToolbarSize(width: number, height: number): void {
const toolbarWidth = Math.ceil(width)
// only update toolbar width if it's changed
@@ -611,7 +641,7 @@ export class SelectionService {
* Get actual toolbar dimensions accounting for zoom factor
* @returns Object containing toolbar width and height
*/
private getToolbarRealSize() {
private getToolbarRealSize(): { toolbarWidth: number; toolbarHeight: number } {
return {
toolbarWidth: this.TOOLBAR_WIDTH * this.zoomFactor,
toolbarHeight: this.TOOLBAR_HEIGHT * this.zoomFactor
@@ -882,8 +912,9 @@ export class SelectionService {
refPoint = { x: Math.round(refPoint.x), y: Math.round(refPoint.y) }
}
this.showToolbarAtPosition(refPoint, refOrientation)
this.toolbarWindow?.webContents.send(IpcChannel.Selection_TextSelected, selectionData)
// [macOS] isFullscreen is only available on macOS
this.showToolbarAtPosition(refPoint, refOrientation, selectionData.programName)
this.toolbarWindow!.webContents.send(IpcChannel.Selection_TextSelected, selectionData)
}
/**
@@ -891,7 +922,7 @@ export class SelectionService {
*/
// Start monitoring global mouse clicks
private startHideByMouseKeyListener() {
private startHideByMouseKeyListener(): void {
try {
// Register event handlers
this.selectionHook!.on('mouse-down', this.handleMouseDownHide)
@@ -904,7 +935,7 @@ export class SelectionService {
}
// Stop monitoring global mouse clicks
private stopHideByMouseKeyListener() {
private stopHideByMouseKeyListener(): void {
if (!this.isHideByMouseKeyListenerActive) return
try {
@@ -1098,7 +1129,7 @@ export class SelectionService {
* Initialize preloaded action windows
* Creates a pool of windows at startup for faster response
*/
private async initPreloadedActionWindows() {
private async initPreloadedActionWindows(): Promise<void> {
try {
// Create initial pool of preloaded windows
for (let i = 0; i < this.PRELOAD_ACTION_WINDOW_COUNT; i++) {
@@ -1112,7 +1143,7 @@ export class SelectionService {
/**
* Close all preloaded action windows
*/
private closePreloadedActionWindows() {
private closePreloadedActionWindows(): void {
for (const actionWindow of this.preloadedActionWindows) {
if (!actionWindow.isDestroyed()) {
actionWindow.destroy()
@@ -1124,7 +1155,7 @@ export class SelectionService {
* Preload a new action window asynchronously
* This method is called after popping a window to ensure we always have windows ready
*/
private async pushNewActionWindow() {
private async pushNewActionWindow(): Promise<void> {
try {
const actionWindow = this.createPreloadedActionWindow()
this.preloadedActionWindows.push(actionWindow)
@@ -1138,7 +1169,7 @@ export class SelectionService {
* Immediately returns a window and asynchronously creates a new one
* @returns {BrowserWindow} The action window
*/
private popActionWindow() {
private popActionWindow(): BrowserWindow {
// Get a window from the preloaded queue or create a new one if empty
const actionWindow = this.preloadedActionWindows.pop() || this.createPreloadedActionWindow()
@@ -1189,20 +1220,26 @@ export class SelectionService {
return actionWindow
}
public processAction(actionItem: ActionItem): void {
/**
* Process action item
* @param actionItem Action item to process
* @param isFullScreen [macOS] only macOS has the available isFullscreen mode
*/
public processAction(actionItem: ActionItem, isFullScreen: boolean = false): void {
const actionWindow = this.popActionWindow()
actionWindow.webContents.send(IpcChannel.Selection_UpdateActionData, actionItem)
this.showActionWindow(actionWindow)
this.showActionWindow(actionWindow, isFullScreen)
}
/**
* Show action window with proper positioning relative to toolbar
* Ensures window stays within screen boundaries
* @param actionWindow Window to position and show
* @param isFullScreen [macOS] only macOS has the available isFullscreen mode
*/
private showActionWindow(actionWindow: BrowserWindow) {
private showActionWindow(actionWindow: BrowserWindow, isFullScreen: boolean = false): void {
let actionWindowWidth = this.ACTION_WINDOW_WIDTH
let actionWindowHeight = this.ACTION_WINDOW_HEIGHT
@@ -1212,11 +1249,14 @@ export class SelectionService {
actionWindowHeight = this.lastActionWindowSize.height
}
//center way
if (!this.isFollowToolbar || !this.toolbarWindow) {
const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint())
const workArea = display.workArea
/********************************************
* Setting the position of the action window
********************************************/
const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint())
const workArea = display.workArea
// Center of the screen
if (!this.isFollowToolbar || !this.toolbarWindow) {
const centerX = workArea.x + (workArea.width - actionWindowWidth) / 2
const centerY = workArea.y + (workArea.height - actionWindowHeight) / 2
@@ -1226,53 +1266,107 @@ export class SelectionService {
x: Math.round(centerX),
y: Math.round(centerY)
})
} else {
// Follow toolbar position
const toolbarBounds = this.toolbarWindow!.getBounds()
const GAP = 6 // 6px gap from screen edges
//make sure action window is inside screen
if (actionWindowWidth > workArea.width - 2 * GAP) {
actionWindowWidth = workArea.width - 2 * GAP
}
if (actionWindowHeight > workArea.height - 2 * GAP) {
actionWindowHeight = workArea.height - 2 * GAP
}
// Calculate initial position to center action window horizontally below toolbar
let posX = Math.round(toolbarBounds.x + (toolbarBounds.width - actionWindowWidth) / 2)
let posY = Math.round(toolbarBounds.y)
// Ensure action window stays within screen boundaries with a small gap
if (posX + actionWindowWidth > workArea.x + workArea.width) {
posX = workArea.x + workArea.width - actionWindowWidth - GAP
} else if (posX < workArea.x) {
posX = workArea.x + GAP
}
if (posY + actionWindowHeight > workArea.y + workArea.height) {
// If window would go below screen, try to position it above toolbar
posY = workArea.y + workArea.height - actionWindowHeight - GAP
} else if (posY < workArea.y) {
posY = workArea.y + GAP
}
actionWindow.setPosition(posX, posY, false)
//KEY to make window not resize
actionWindow.setBounds({
width: actionWindowWidth,
height: actionWindowHeight,
x: posX,
y: posY
})
}
if (!isMac) {
actionWindow.show()
return
}
//follow toolbar
const toolbarBounds = this.toolbarWindow!.getBounds()
const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint())
const workArea = display.workArea
const GAP = 6 // 6px gap from screen edges
/************************************************
* [macOS] the following code is only for macOS
*
* WARNING:
* DO NOT MODIFY THESE CODES, UNLESS YOU REALLY KNOW WHAT YOU ARE DOING!!!!
*************************************************/
//make sure action window is inside screen
if (actionWindowWidth > workArea.width - 2 * GAP) {
actionWindowWidth = workArea.width - 2 * GAP
// act normally when the app is not in fullscreen mode
if (!isFullScreen) {
actionWindow.show()
return
}
if (actionWindowHeight > workArea.height - 2 * GAP) {
actionWindowHeight = workArea.height - 2 * GAP
}
// [macOS] an UGLY HACKY way for fullscreen override settings
// Calculate initial position to center action window horizontally below toolbar
let posX = Math.round(toolbarBounds.x + (toolbarBounds.width - actionWindowWidth) / 2)
let posY = Math.round(toolbarBounds.y)
// FIXME sometimes the dock will be shown when the action window is shown
// FIXME if actionWindow show on the fullscreen app, switch to other space will cause the mainWindow to be shown
// FIXME When setVisibleOnAllWorkspaces is true, docker icon disappeared when the first action window is shown on the fullscreen app
// use app.dock.show() to show the dock again will cause the action window to be closed when auto hide on blur is enabled
// Ensure action window stays within screen boundaries with a small gap
if (posX + actionWindowWidth > workArea.x + workArea.width) {
posX = workArea.x + workArea.width - actionWindowWidth - GAP
} else if (posX < workArea.x) {
posX = workArea.x + GAP
}
if (posY + actionWindowHeight > workArea.y + workArea.height) {
// If window would go below screen, try to position it above toolbar
posY = workArea.y + workArea.height - actionWindowHeight - GAP
} else if (posY < workArea.y) {
posY = workArea.y + GAP
}
// setFocusable(false) to prevent the action window hide when blur (if auto hide on blur is enabled)
actionWindow.setFocusable(false)
actionWindow.setAlwaysOnTop(true, 'floating')
actionWindow.setPosition(posX, posY, false)
//KEY to make window not resize
actionWindow.setBounds({
width: actionWindowWidth,
height: actionWindowHeight,
x: posX,
y: posY
// `setVisibleOnAllWorkspaces(true)` will cause the dock icon disappeared
// just store the dock icon status, and show it again
const isDockShown = app.dock?.isVisible()
// DO NOT set `skipTransformProcessType: true`,
// it will cause the action window to be shown on other space
actionWindow.setVisibleOnAllWorkspaces(true, {
visibleOnFullScreen: true
})
actionWindow.show()
actionWindow.showInactive()
// show the dock again if last time it was shown
// do not put it after `actionWindow.focus()`, will cause the action window to be closed when auto hide on blur is enabled
if (!app.dock?.isVisible() && isDockShown) {
app.dock?.show()
}
// unset everything
setTimeout(() => {
actionWindow.setVisibleOnAllWorkspaces(false, {
visibleOnFullScreen: true,
skipTransformProcessType: true
})
actionWindow.setAlwaysOnTop(false)
actionWindow.setFocusable(true)
// regain the focus when all the works done
actionWindow.focus()
}, 50)
}
public closeActionWindow(actionWindow: BrowserWindow): void {
@@ -1292,38 +1386,40 @@ export class SelectionService {
* Switches between selection-based and alt-key based triggering
* Manages appropriate event listeners for each mode
*/
private processTriggerMode() {
private processTriggerMode(): void {
if (!this.selectionHook) return
switch (this.triggerMode) {
case TriggerMode.Selected:
if (this.isCtrlkeyListenerActive) {
this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
this.selectionHook.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook.off('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = false
}
this.selectionHook!.setSelectionPassiveMode(false)
this.selectionHook.setSelectionPassiveMode(false)
break
case TriggerMode.Ctrlkey:
if (!this.isCtrlkeyListenerActive) {
this.selectionHook!.on('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.on('key-up', this.handleKeyUpCtrlkeyMode)
this.selectionHook.on('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook.on('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = true
}
this.selectionHook!.setSelectionPassiveMode(true)
this.selectionHook.setSelectionPassiveMode(true)
break
case TriggerMode.Shortcut:
//remove the ctrlkey listener, don't need any key listener for shortcut mode
if (this.isCtrlkeyListenerActive) {
this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
this.selectionHook.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook.off('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = false
}
this.selectionHook!.setSelectionPassiveMode(true)
this.selectionHook.setSelectionPassiveMode(true)
break
}
}
@@ -1376,8 +1472,9 @@ export class SelectionService {
configManager.setSelectionAssistantFilterList(filterList)
})
ipcMain.handle(IpcChannel.Selection_ProcessAction, (_, actionItem: ActionItem) => {
selectionService?.processAction(actionItem)
// [macOS] only macOS has the available isFullscreen mode
ipcMain.handle(IpcChannel.Selection_ProcessAction, (_, actionItem: ActionItem, isFullScreen: boolean = false) => {
selectionService?.processAction(actionItem, isFullScreen)
})
ipcMain.handle(IpcChannel.Selection_ActionWindowClose, (event) => {
@@ -1404,13 +1501,13 @@ export class SelectionService {
this.isIpcHandlerRegistered = true
}
private logInfo(message: string, forceShow: boolean = false) {
private logInfo(message: string, forceShow: boolean = false): void {
if (isDev || forceShow) {
Logger.info('[SelectionService] Info: ', message)
}
}
private logError(...args: [...string[], Error]) {
private logError(...args: [...string[], Error]): void {
Logger.error('[SelectionService] Error: ', ...args)
}
}
@@ -1423,7 +1520,7 @@ export class SelectionService {
export function initSelectionService(): boolean {
if (!isSupportedOS) return false
configManager.subscribe(ConfigKeys.SelectionAssistantEnabled, (enabled: boolean) => {
configManager.subscribe(ConfigKeys.SelectionAssistantEnabled, (enabled: boolean): void => {
//avoid closure
const ss = SelectionService.getInstance()
if (!ss) {

View File

@@ -0,0 +1,149 @@
import { is } from '@electron-toolkit/utils'
import { isLinux, isMac } from '@main/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { BrowserWindow, nativeTheme } from 'electron'
import { join } from 'path'
import icon from '../../../build/icon.png?asset'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
export class SettingsWindowService {
private static instance: SettingsWindowService | null = null
private settingsWindow: BrowserWindow | null = null
public static getInstance(): SettingsWindowService {
if (!SettingsWindowService.instance) {
SettingsWindowService.instance = new SettingsWindowService()
}
return SettingsWindowService.instance
}
public createSettingsWindow(defaultTab?: string): BrowserWindow {
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
this.settingsWindow.show()
this.settingsWindow.focus()
return this.settingsWindow
}
this.settingsWindow = new BrowserWindow({
width: 1000,
height: 700,
minWidth: 900,
minHeight: 600,
show: false,
autoHideMenuBar: true,
transparent: false,
vibrancy: 'sidebar',
visualEffectState: 'active',
titleBarStyle: 'hidden',
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
darkTheme: nativeTheme.shouldUseDarkColors,
trafficLightPosition: { x: 12, y: 12 },
...(isLinux ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: false,
webviewTag: true,
allowRunningInsecureContent: true,
backgroundThrottling: false
}
})
this.setupSettingsWindow()
this.loadSettingsWindowContent(defaultTab)
return this.settingsWindow
}
private setupSettingsWindow() {
if (!this.settingsWindow) return
this.settingsWindow.on('ready-to-show', () => {
this.settingsWindow?.show()
})
this.settingsWindow.on('closed', () => {
this.settingsWindow = null
})
this.settingsWindow.on('close', () => {
// Clean up when window is closed
})
// Handle theme changes
nativeTheme.on('updated', () => {
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
this.settingsWindow.setTitleBarOverlay(
nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight
)
}
})
}
private loadSettingsWindowContent(defaultTab?: string) {
if (!this.settingsWindow) return
const queryParam = defaultTab ? `?tab=${defaultTab}` : ''
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
this.settingsWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/settingsWindow.html' + queryParam)
} else {
this.settingsWindow.loadFile(join(__dirname, '../renderer/settingsWindow.html'))
if (defaultTab) {
this.settingsWindow.webContents.once('did-finish-load', () => {
this.settingsWindow?.webContents.send(IpcChannel.SettingsWindow_Show, { defaultTab })
})
}
}
}
public showSettingsWindow(defaultTab?: string) {
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
if (this.settingsWindow.isMinimized()) {
this.settingsWindow.restore()
}
if (!isLinux) {
this.settingsWindow.setVisibleOnAllWorkspaces(true)
}
this.settingsWindow.show()
this.settingsWindow.focus()
if (!isLinux) {
this.settingsWindow.setVisibleOnAllWorkspaces(false)
}
} else {
this.createSettingsWindow(defaultTab)
}
}
public hideSettingsWindow() {
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
this.settingsWindow.hide()
}
}
public closeSettingsWindow() {
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
this.settingsWindow.close()
}
}
public getSettingsWindow(): BrowserWindow | null {
return this.settingsWindow
}
public static registerIpcHandler() {
const { ipcMain } = require('electron')
const service = SettingsWindowService.getInstance()
ipcMain.handle(IpcChannel.SettingsWindow_Show, (_, options?: { defaultTab?: string }) => {
service.showSettingsWindow(options?.defaultTab)
})
}
}
export const settingsWindowService = SettingsWindowService.getInstance()

View File

@@ -5,10 +5,12 @@ import Logger from 'electron-log'
import { configManager } from './ConfigManager'
import selectionService from './SelectionService'
import { settingsWindowService } from './SettingsWindowService'
import { windowService } from './WindowService'
let showAppAccelerator: string | null = null
let showMiniWindowAccelerator: string | null = null
let showSettingsAccelerator: string | null = null
let selectionAssistantToggleAccelerator: string | null = null
let selectionAssistantSelectTextAccelerator: string | null = null
@@ -26,6 +28,10 @@ function getShortcutHandler(shortcut: Shortcut) {
return (window: BrowserWindow) => handleZoomFactor([window], -0.1)
case 'zoom_reset':
return (window: BrowserWindow) => handleZoomFactor([window], 0, true)
case 'show_settings':
return () => {
settingsWindowService.showSettingsWindow()
}
case 'show_app':
return () => {
windowService.toggleMainWindow()
@@ -146,9 +152,13 @@ export function registerShortcuts(window: BrowserWindow) {
// only register universal shortcuts when needed
if (
onlyUniversalShortcuts &&
!['show_app', 'mini_window', 'selection_assistant_toggle', 'selection_assistant_select_text'].includes(
shortcut.key
)
![
'show_app',
'mini_window',
'show_settings',
'selection_assistant_toggle',
'selection_assistant_select_text'
].includes(shortcut.key)
) {
return
}
@@ -171,6 +181,10 @@ export function registerShortcuts(window: BrowserWindow) {
showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut)
break
case 'show_settings':
showSettingsAccelerator = formatShortcutKey(shortcut.shortcut)
break
case 'selection_assistant_toggle':
selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut)
break
@@ -222,6 +236,12 @@ export function registerShortcuts(window: BrowserWindow) {
handler && globalShortcut.register(accelerator, () => handler(window))
}
if (showSettingsAccelerator) {
const handler = getShortcutHandler({ key: 'show_settings' } as Shortcut)
const accelerator = convertShortcutFormat(showSettingsAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
}
if (selectionAssistantToggleAccelerator) {
const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut)
const accelerator = convertShortcutFormat(selectionAssistantToggleAccelerator)
@@ -258,6 +278,7 @@ export function unregisterAllShortcuts() {
try {
showAppAccelerator = null
showMiniWindowAccelerator = null
showSettingsAccelerator = null
selectionAssistantToggleAccelerator = null
selectionAssistantSelectTextAccelerator = null
windowOnHandlers.forEach((handlers, window) => {

View File

@@ -1,48 +1,48 @@
import { IpcChannel } from '@shared/IpcChannel'
import { ThemeMode } from '@types'
import { BrowserWindow, nativeTheme } from 'electron'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
import { configManager } from './ConfigManager'
class ThemeService {
private theme: ThemeMode = ThemeMode.system
constructor() {
this.theme = configManager.getTheme()
if (this.theme === ThemeMode.dark || this.theme === ThemeMode.light || this.theme === ThemeMode.system) {
nativeTheme.themeSource = this.theme
} else {
// 兼容旧版本
configManager.setTheme(ThemeMode.system)
nativeTheme.themeSource = ThemeMode.system
}
nativeTheme.on('updated', this.themeUpdatadHandler.bind(this))
}
themeUpdatadHandler() {
BrowserWindow.getAllWindows().forEach((win) => {
if (win && !win.isDestroyed() && win.setTitleBarOverlay) {
try {
win.setTitleBarOverlay(nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight)
} catch (error) {
// don't throw error if setTitleBarOverlay failed
// Because it may be called with some windows have some title bar
}
}
win.webContents.send(IpcChannel.ThemeUpdated, nativeTheme.shouldUseDarkColors ? ThemeMode.dark : ThemeMode.light)
})
}
setTheme(theme: ThemeMode) {
if (theme === this.theme) {
return
}
this.theme = theme
nativeTheme.themeSource = theme
configManager.setTheme(theme)
}
}
export const themeService = new ThemeService()
import { IpcChannel } from '@shared/IpcChannel'
import { ThemeMode } from '@types'
import { BrowserWindow, nativeTheme } from 'electron'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
import { configManager } from './ConfigManager'
class ThemeService {
private theme: ThemeMode = ThemeMode.system
constructor() {
this.theme = configManager.getTheme()
if (this.theme === ThemeMode.dark || this.theme === ThemeMode.light || this.theme === ThemeMode.system) {
nativeTheme.themeSource = this.theme
} else {
// 兼容旧版本
configManager.setTheme(ThemeMode.system)
nativeTheme.themeSource = ThemeMode.system
}
nativeTheme.on('updated', this.themeUpdatadHandler.bind(this))
}
themeUpdatadHandler() {
BrowserWindow.getAllWindows().forEach((win) => {
if (win && !win.isDestroyed() && win.setTitleBarOverlay) {
try {
win.setTitleBarOverlay(nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight)
} catch (error) {
// don't throw error if setTitleBarOverlay failed
// Because it may be called with some windows have some title bar
}
}
win.webContents.send(IpcChannel.ThemeUpdated, nativeTheme.shouldUseDarkColors ? ThemeMode.dark : ThemeMode.light)
})
}
setTheme(theme: ThemeMode) {
if (theme === this.theme) {
return
}
this.theme = theme
nativeTheme.themeSource = theme
configManager.setTheme(theme)
}
}
export const themeService = new ThemeService()

View File

@@ -12,6 +12,7 @@ import { join } from 'path'
import icon from '../../../build/icon.png?asset'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
import { CacheService } from './CacheService'
import { configManager } from './ConfigManager'
import { contextMenu } from './ContextMenu'
import { initSessionUserAgent } from './WebviewService'
@@ -41,8 +42,8 @@ export class WindowService {
}
const mainWindowState = windowStateKeeper({
defaultWidth: 1080,
defaultHeight: 670,
defaultWidth: 960,
defaultHeight: 600,
fullScreen: false,
maximize: false
})
@@ -52,7 +53,7 @@ export class WindowService {
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
minWidth: 1080,
minWidth: 960,
minHeight: 600,
show: false,
autoHideMenuBar: true,
@@ -63,7 +64,7 @@ export class WindowService {
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
darkTheme: nativeTheme.shouldUseDarkColors,
trafficLightPosition: { x: 8, y: 12 },
trafficLightPosition: { x: 12, y: 12 },
...(isLinux ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
@@ -313,6 +314,11 @@ export class WindowService {
return app.quit()
}
if (CacheService.get('navigation-url') !== '/') {
event.preventDefault()
return mainWindow.webContents.send(IpcChannel.Navigation_Close)
}
// 托盘及关闭行为设置
const isShowTray = configManager.getTray()
const isTrayOnClose = configManager.getTrayOnClose()
@@ -435,8 +441,9 @@ export class WindowService {
show: false,
autoHideMenuBar: true,
transparent: isMac,
vibrancy: 'under-window',
visualEffectState: 'followWindow',
vibrancy: isMac ? 'under-window' : undefined,
visualEffectState: isMac ? 'followWindow' : undefined,
backgroundMaterial: isWin ? 'acrylic' : undefined,
center: true,
frame: false,
alwaysOnTop: true,

View File

@@ -1,3 +1,4 @@
import { isMac } from '@main/constant'
import Logger from 'electron-log'
import { windowService } from '../WindowService'
@@ -33,8 +34,13 @@ export async function handleProvidersProtocolUrl(url: URL) {
(await mainWindow.webContents.executeJavaScript(`typeof window.navigate === 'function'`))
) {
mainWindow.webContents.executeJavaScript(`window.navigate('/settings/provider?addProviderData=${data}')`)
if (isMac) {
windowService.showMainWindow()
}
} else {
setTimeout(() => {
Logger.info('handleProvidersProtocolUrl timeout', { data, version })
handleProvidersProtocolUrl(url)
}, 1000)
}

View File

@@ -44,7 +44,9 @@ export function handleMcpProtocolUrl(url: URL) {
// }
// }
// cherrystudio://mcp/install?servers={base64Encode(JSON.stringify(jsonConfig))}
const data = params.get('servers')
if (data) {
const stringify = Buffer.from(data, 'base64').toString('utf8')
Logger.info('install MCP servers from urlschema: ', stringify)
@@ -63,10 +65,8 @@ export function handleMcpProtocolUrl(url: URL) {
}
}
const mainWindow = windowService.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.executeJavaScript("window.navigate('/settings/mcp')")
}
windowService.getMainWindow()?.show()
break
}
default:

View File

@@ -1,14 +1,19 @@
import * as fs from 'node:fs'
import * as fsPromises from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { FileTypes } from '@types'
import iconv from 'iconv-lite'
import { detectAll as detectEncodingAll } from 'jschardet'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { readTextFileWithAutoEncoding } from '../file'
import { getAllFiles, getAppConfigDir, getConfigDir, getFilesDir, getFileType, getTempDir } from '../file'
// Mock dependencies
vi.mock('node:fs')
vi.mock('node:fs/promises')
vi.mock('node:os')
vi.mock('node:path')
vi.mock('uuid', () => ({
@@ -241,4 +246,54 @@ describe('file', () => {
expect(appConfigDir).toBe('/mock/home/.cherrystudio/config/')
})
})
describe('readTextFileWithAutoEncoding', () => {
const mockFilePath = '/path/to/mock/file.txt'
it('should read file with auto encoding', async () => {
const content = '这是一段GB2312编码的测试内容'
const buffer = iconv.encode(content, 'GB2312')
// 创建模拟的 FileHandle 对象
const mockFileHandle = {
read: vi.fn().mockResolvedValue({
bytesRead: buffer.byteLength,
buffer: buffer
}),
close: vi.fn().mockResolvedValue(undefined)
}
// 模拟 open 方法
vi.spyOn(fsPromises, 'open').mockResolvedValue(mockFileHandle as any)
vi.spyOn(fsPromises, 'readFile').mockResolvedValue(buffer)
const result = await readTextFileWithAutoEncoding(mockFilePath)
expect(result).toBe(content)
})
it('should try to fix bad detected encoding', async () => {
const content = '这是一段GB2312编码的测试内容'
const buffer = iconv.encode(content, 'GB2312')
// 创建模拟的 FileHandle 对象
const mockFileHandle = {
read: vi.fn().mockResolvedValue({
bytesRead: buffer.byteLength,
buffer: buffer
}),
close: vi.fn().mockResolvedValue(undefined)
}
// 模拟 fs.open 方法
vi.spyOn(fsPromises, 'open').mockResolvedValue(mockFileHandle as any)
vi.spyOn(fsPromises, 'readFile').mockResolvedValue(buffer)
vi.mocked(vi.fn(detectEncodingAll)).mockReturnValue([
{ encoding: 'UTF-8', confidence: 0.9 },
{ encoding: 'GB2312', confidence: 0.8 }
])
const result = await readTextFileWithAutoEncoding(mockFilePath)
expect(result).toBe(content)
})
})
})

View File

@@ -1,11 +1,15 @@
import * as fs from 'node:fs'
import { open, readFile } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { isLinux, isPortable } from '@main/constant'
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant'
import { audioExts, documentExts, imageExts, MB, textExts, videoExts } from '@shared/config/constant'
import { FileMetadata, FileTypes } from '@types'
import { app } from 'electron'
import Logger from 'electron-log'
import iconv from 'iconv-lite'
import * as jschardet from 'jschardet'
import { v4 as uuidv4 } from 'uuid'
export function initAppDataDir() {
@@ -202,3 +206,49 @@ export function getCacheDir() {
export function getAppConfigDir(name: string) {
return path.join(getConfigDir(), name)
}
/**
* 读取文件内容并自动检测编码格式进行解码
* @param filePath - 文件路径
* @returns 解码后的文件内容
*/
export async function readTextFileWithAutoEncoding(filePath: string): Promise<string> {
// 读取前1MB以检测编码
const buffer = Buffer.alloc(1 * MB)
const fh = await open(filePath, 'r')
const { buffer: bufferRead } = await fh.read(buffer, 0, 1 * MB, 0)
await fh.close()
// 获取文件编码格式,最多取前两个可能的编码
const encodings = jschardet
.detectAll(bufferRead)
.map((item) => ({
...item,
encoding: item.encoding === 'ascii' ? 'UTF-8' : item.encoding
}))
.filter((item, index, array) => array.findIndex((prevItem) => prevItem.encoding === item.encoding) === index)
.slice(0, 2)
if (encodings.length === 0) {
Logger.error('Failed to detect encoding. Use utf-8 to decode.')
const data = await readFile(filePath)
return iconv.decode(data, 'UTF-8')
}
const data = await readFile(filePath)
for (const item of encodings) {
const encoding = item.encoding
const content = iconv.decode(data, encoding)
if (content.includes('\uFFFD')) {
Logger.error(
`File ${filePath} was auto-detected as ${encoding} encoding, but contains invalid characters. Trying other encodings`
)
} else {
return content
}
}
Logger.error(`File ${filePath} failed to decode with all possible encodings, trying UTF-8 encoding`)
return iconv.decode(data, 'UTF-8')
}

View File

@@ -1,26 +1,26 @@
import { BrowserWindow } from 'electron'
import { configManager } from '../services/ConfigManager'
export function handleZoomFactor(wins: BrowserWindow[], delta: number, reset: boolean = false) {
if (reset) {
wins.forEach((win) => {
win.webContents.setZoomFactor(1)
})
configManager.setZoomFactor(1)
return
}
if (delta === 0) {
return
}
const currentZoom = configManager.getZoomFactor()
const newZoom = Number((currentZoom + delta).toFixed(1))
if (newZoom >= 0.5 && newZoom <= 2.0) {
wins.forEach((win) => {
win.webContents.setZoomFactor(newZoom)
})
configManager.setZoomFactor(newZoom)
}
}
import { BrowserWindow } from 'electron'
import { configManager } from '../services/ConfigManager'
export function handleZoomFactor(wins: BrowserWindow[], delta: number, reset: boolean = false) {
if (reset) {
wins.forEach((win) => {
win.webContents.setZoomFactor(1)
})
configManager.setZoomFactor(1)
return
}
if (delta === 0) {
return
}
const currentZoom = configManager.getZoomFactor()
const newZoom = Number((currentZoom + delta).toFixed(1))
if (newZoom >= 0.5 && newZoom <= 2.0) {
wins.forEach((win) => {
win.webContents.setZoomFactor(newZoom)
})
configManager.setZoomFactor(newZoom)
}
}

View File

@@ -10,6 +10,7 @@ import {
KnowledgeItem,
MCPServer,
Provider,
S3Config,
Shortcut,
ThemeMode,
WebDavConfig
@@ -72,9 +73,9 @@ const api = {
decompress: (text: Buffer) => ipcRenderer.invoke(IpcChannel.Zip_Decompress, text)
},
backup: {
backup: (fileName: string, data: string, destinationPath?: string, skipBackupFile?: boolean) =>
ipcRenderer.invoke(IpcChannel.Backup_Backup, fileName, data, destinationPath, skipBackupFile),
restore: (backupPath: string) => ipcRenderer.invoke(IpcChannel.Backup_Restore, backupPath),
backup: (filename: string, content: string, path: string, skipBackupFile: boolean) =>
ipcRenderer.invoke(IpcChannel.Backup_Backup, filename, content, path, skipBackupFile),
restore: (path: string) => ipcRenderer.invoke(IpcChannel.Backup_Restore, path),
backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
ipcRenderer.invoke(IpcChannel.Backup_BackupToWebdav, data, webdavConfig),
restoreFromWebdav: (webdavConfig: WebDavConfig) =>
@@ -86,14 +87,36 @@ const api = {
createDirectory: (webdavConfig: WebDavConfig, path: string, options?: CreateDirectoryOptions) =>
ipcRenderer.invoke(IpcChannel.Backup_CreateDirectory, webdavConfig, path, options),
deleteWebdavFile: (fileName: string, webdavConfig: WebDavConfig) =>
ipcRenderer.invoke(IpcChannel.Backup_DeleteWebdavFile, fileName, webdavConfig)
ipcRenderer.invoke(IpcChannel.Backup_DeleteWebdavFile, fileName, webdavConfig),
backupToLocalDir: (
data: string,
fileName: string,
localConfig: { localBackupDir?: string; skipBackupFile?: boolean }
) => ipcRenderer.invoke(IpcChannel.Backup_BackupToLocalDir, data, fileName, localConfig),
restoreFromLocalBackup: (fileName: string, localBackupDir?: string) =>
ipcRenderer.invoke(IpcChannel.Backup_RestoreFromLocalBackup, fileName, localBackupDir),
listLocalBackupFiles: (localBackupDir?: string) =>
ipcRenderer.invoke(IpcChannel.Backup_ListLocalBackupFiles, localBackupDir),
deleteLocalBackupFile: (fileName: string, localBackupDir?: string) =>
ipcRenderer.invoke(IpcChannel.Backup_DeleteLocalBackupFile, fileName, localBackupDir),
setLocalBackupDir: (dirPath: string) => ipcRenderer.invoke(IpcChannel.Backup_SetLocalBackupDir, dirPath),
checkWebdavConnection: (webdavConfig: WebDavConfig) =>
ipcRenderer.invoke(IpcChannel.Backup_CheckConnection, webdavConfig),
backupToS3: (data: string, s3Config: S3Config) => ipcRenderer.invoke(IpcChannel.Backup_BackupToS3, data, s3Config),
restoreFromS3: (s3Config: S3Config) => ipcRenderer.invoke(IpcChannel.Backup_RestoreFromS3, s3Config),
listS3Files: (s3Config: S3Config) => ipcRenderer.invoke(IpcChannel.Backup_ListS3Files, s3Config),
deleteS3File: (fileName: string, s3Config: S3Config) =>
ipcRenderer.invoke(IpcChannel.Backup_DeleteS3File, fileName, s3Config),
checkS3Connection: (s3Config: S3Config) => ipcRenderer.invoke(IpcChannel.Backup_CheckS3Connection, s3Config)
},
file: {
select: (options?: OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.File_Select, options),
upload: (file: FileMetadata) => ipcRenderer.invoke(IpcChannel.File_Upload, file),
delete: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Delete, fileId),
deleteDir: (dirPath: string) => ipcRenderer.invoke(IpcChannel.File_DeleteDir, dirPath),
read: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Read, fileId),
read: (fileId: string, detectEncoding?: boolean) =>
ipcRenderer.invoke(IpcChannel.File_Read, fileId, detectEncoding),
clear: () => ipcRenderer.invoke(IpcChannel.File_Clear),
get: (filePath: string) => ipcRenderer.invoke(IpcChannel.File_Get, filePath),
/**
@@ -124,7 +147,8 @@ const api = {
copy: (fileId: string, destPath: string) => ipcRenderer.invoke(IpcChannel.File_Copy, fileId, destPath),
base64File: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64File, fileId),
pdfInfo: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_GetPdfInfo, fileId),
getPathForFile: (file: File) => webUtils.getPathForFile(file)
getPathForFile: (file: File) => webUtils.getPathForFile(file),
openFileWithRelativePath: (file: FileMetadata) => ipcRenderer.invoke(IpcChannel.File_OpenWithRelativePath, file)
},
fs: {
read: (pathOrUrl: string, encoding?: BufferEncoding) => ipcRenderer.invoke(IpcChannel.Fs_Read, pathOrUrl, encoding)
@@ -206,8 +230,8 @@ const api = {
restartServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RestartServer, server),
stopServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_StopServer, server),
listTools: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListTools, server),
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) =>
ipcRenderer.invoke(IpcChannel.Mcp_CallTool, { server, name, args }),
callTool: ({ server, name, args, callId }: { server: MCPServer; name: string; args: any; callId?: string }) =>
ipcRenderer.invoke(IpcChannel.Mcp_CallTool, { server, name, args, callId }),
listPrompts: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListPrompts, server),
getPrompt: ({ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }) =>
ipcRenderer.invoke(IpcChannel.Mcp_GetPrompt, { server, name, args }),
@@ -215,7 +239,9 @@ const api = {
getResource: ({ server, uri }: { server: MCPServer; uri: string }) =>
ipcRenderer.invoke(IpcChannel.Mcp_GetResource, { server, uri }),
getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo),
checkMcpConnectivity: (server: any) => ipcRenderer.invoke(IpcChannel.Mcp_CheckConnectivity, server)
checkMcpConnectivity: (server: any) => ipcRenderer.invoke(IpcChannel.Mcp_CheckConnectivity, server),
abortTool: (callId: string) => ipcRenderer.invoke(IpcChannel.Mcp_AbortTool, callId),
setProgress: (progress: number) => ipcRenderer.invoke(IpcChannel.Mcp_SetProgress, progress)
},
python: {
execute: (script: string, context?: Record<string, any>, timeout?: number) =>
@@ -285,12 +311,32 @@ const api = {
ipcRenderer.invoke(IpcChannel.Selection_SetRemeberWinSize, isRemeberWinSize),
setFilterMode: (filterMode: string) => ipcRenderer.invoke(IpcChannel.Selection_SetFilterMode, filterMode),
setFilterList: (filterList: string[]) => ipcRenderer.invoke(IpcChannel.Selection_SetFilterList, filterList),
processAction: (actionItem: ActionItem) => ipcRenderer.invoke(IpcChannel.Selection_ProcessAction, actionItem),
processAction: (actionItem: ActionItem, isFullScreen: boolean = false) =>
ipcRenderer.invoke(IpcChannel.Selection_ProcessAction, actionItem, isFullScreen),
closeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowClose),
minimizeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowMinimize),
pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned)
},
quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text)
quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text),
navigation: {
url: (url: string) => ipcRenderer.invoke(IpcChannel.Navigation_Url, url)
},
setDisableHardwareAcceleration: (isDisable: boolean) =>
ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable),
// Settings Window
showSettingsWindow: (options?: { defaultTab?: string }) =>
ipcRenderer.invoke(IpcChannel.SettingsWindow_Show, options),
on: (channel: string, func: any) => {
const listener = (_event: Electron.IpcRendererEvent, ...args: any[]) => {
func(...args)
}
ipcRenderer.on(channel, listener)
return () => {
ipcRenderer.off(channel, listener)
}
}
}
// Use `contextBridge` APIs to expose Electron APIs to

View File

@@ -1,46 +1,45 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' 'unsafe-inline' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<title>Cherry Studio</title>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' 'unsafe-inline' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<title>Cherry Studio</title>
<style>
html,
body {
margin: 0;
}
<style>
html,
body {
margin: 0;
}
#spinner {
position: fixed;
width: 100vw;
height: 100vh;
flex-direction: row;
justify-content: center;
align-items: center;
display: flex;
}
#spinner {
position: fixed;
width: 100vw;
height: 100vh;
flex-direction: row;
justify-content: center;
align-items: center;
display: flex;
}
#spinner img {
width: 100px;
border-radius: 50px;
}
</style>
</head>
#spinner img {
width: 100px;
border-radius: 50px;
}
</style>
</head>
<body>
<div id="root"></div>
<div id="spinner">
<img src="/src/assets/images/logo.png" />
</div>
<script>
console.time('init')
</script>
<script type="module" src="/src/init.ts"></script>
<script type="module" src="/src/entryPoint.tsx"></script>
</body>
</html>
<body>
<div id="root"></div>
<div id="spinner">
<img src="/src/assets/images/logo.png" />
</div>
<script>
console.time('init')
</script>
<script type="module" src="/src/init.ts"></script>
<script type="module" src="/src/entryPoint.tsx"></script>
</body>
</html>

View File

@@ -1,24 +1,23 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<title>Cherry Studio</title>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<title>Cherry Studio</title>
<style>
html,
body {
margin: 0;
}
</style>
</head>
<style>
html,
body {
margin: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/windows/mini/entryPoint.tsx"></script>
</body>
</html>
<body>
<div id="root"></div>
<script type="module" src="/src/windows/mini/entryPoint.tsx"></script>
</body>
</html>

View File

@@ -1,41 +1,39 @@
<!doctype html>
<html lang="zh-CN">
<head>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<title>Cherry Studio Selection Assistant</title>
</head>
</head>
<body>
<body>
<div id="root"></div>
<script type="module" src="/src/windows/selection/action/entryPoint.tsx"></script>
<style>
html {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
box-sizing: border-box;
}
#root {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
}
#root {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
}
</style>
</body>
</html>
</body>
</html>

View File

@@ -1,46 +1,43 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<title>Cherry Studio Selection Toolbar</title>
</head>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<title>Cherry Studio Selection Toolbar</title>
<body>
<div id="root"></div>
<script type="module" src="/src/windows/selection/toolbar/entryPoint.tsx"></script>
<style>
html {
margin: 0 !important;
background-color: transparent !important;
background-image: none !important;
}
</head>
body {
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
width: 100vw !important;
height: 100vh !important;
<body>
<div id="root"></div>
<script type="module" src="/src/windows/selection/toolbar/entryPoint.tsx"></script>
<style>
html {
margin: 0 !important;
background-color: transparent !important;
background-image: none !important;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
}
body {
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
width: 100vw !important;
height: 100vh !important;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#root {
margin: 0 !important;
padding: 0 !important;
width: max-content !important;
height: fit-content !important;
}
</style>
</body>
</html>
#root {
margin: 0 !important;
padding: 0 !important;
width: max-content !important;
height: fit-content !important;
}
</style>
</body>
</html>

View File

@@ -0,0 +1,23 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<title>Cherry Studio</title>
<style>
html,
body {
margin: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/windows/settings/entryPoint.tsx"></script>
</body>
</html>

View File

@@ -2,10 +2,10 @@ import '@renderer/databases'
import store, { persistor } from '@renderer/store'
import { Provider } from 'react-redux'
import { HashRouter, Route, Routes } from 'react-router-dom'
import { HashRouter } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar'
import AppLayout from './components/Layout/AppLayout'
import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider'
import { CodeStyleProvider } from './context/CodeStyleProvider'
@@ -13,14 +13,8 @@ import { NotificationProvider } from './context/NotificationProvider'
import StyleSheetManager from './context/StyleSheetManager'
import { ThemeProvider } from './context/ThemeProvider'
import NavigationHandler from './handler/NavigationHandler'
import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage'
import FilesPage from './pages/files/FilesPage'
import HomePage from './pages/home/HomePage'
import KnowledgePage from './pages/knowledge/KnowledgePage'
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
import { ChatProvider } from './hooks/useChat'
import Routes from './Routes'
function App(): React.ReactElement {
return (
@@ -31,22 +25,16 @@ function App(): React.ReactElement {
<NotificationProvider>
<CodeStyleProvider>
<PersistGate loading={null} persistor={persistor}>
<TopViewContainer>
<HashRouter>
<HashRouter>
<TopViewContainer>
<NavigationHandler />
<Sidebar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</HashRouter>
</TopViewContainer>
<ChatProvider>
<AppLayout>
<Routes />
</AppLayout>
</ChatProvider>
</TopViewContainer>
</HashRouter>
</PersistGate>
</CodeStyleProvider>
</NotificationProvider>

View File

@@ -0,0 +1,29 @@
import { Route, Routes } from 'react-router-dom'
import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage'
import FilesPage from './pages/files/FilesPage'
import HomePage from './pages/home/HomePage'
import KnowledgePage from './pages/knowledge/KnowledgePage'
import McpServersPage from './pages/mcp-servers'
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
import TranslatePage from './pages/translate/TranslatePage'
const RouteContainer = () => {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/mcp-servers/*" element={<McpServersPage />} />
{/* <Route path="/settings/*" element={<SettingsPage />} />
<Route path="/launchpad" element={<LaunchpadPage />} /> */}
</Routes>
)
}
export default RouteContainer

View File

@@ -5,6 +5,7 @@ import { AnthropicAPIClient } from './anthropic/AnthropicAPIClient'
import { BaseApiClient } from './BaseApiClient'
import { GeminiAPIClient } from './gemini/GeminiAPIClient'
import { VertexAPIClient } from './gemini/VertexAPIClient'
import { NewAPIClient } from './NewAPIClient'
import { OpenAIAPIClient } from './openai/OpenAIApiClient'
import { OpenAIResponseAPIClient } from './openai/OpenAIResponseAPIClient'
import { PPIOAPIClient } from './ppio/PPIOAPIClient'
@@ -32,6 +33,11 @@ export class ApiClientFactory {
instance = new AihubmixAPIClient(provider) as BaseApiClient
return instance
}
if (provider.id === 'new-api') {
console.log(`[ApiClientFactory] Creating NewAPIClient for provider: ${provider.id}`)
instance = new NewAPIClient(provider) as BaseApiClient
return instance
}
if (provider.id === 'ppio') {
console.log(`[ApiClientFactory] Creating PPIOAPIClient for provider: ${provider.id}`)
instance = new PPIOAPIClient(provider) as BaseApiClient
@@ -41,10 +47,9 @@ export class ApiClientFactory {
// 然后检查标准的provider type
switch (provider.type) {
case 'openai':
case 'azure-openai':
console.log(`[ApiClientFactory] Creating OpenAIApiClient for provider: ${provider.id}`)
instance = new OpenAIAPIClient(provider) as BaseApiClient
break
case 'azure-openai':
case 'openai-response':
instance = new OpenAIResponseAPIClient(provider) as BaseApiClient
break

View File

@@ -254,7 +254,7 @@ export abstract class BaseApiClient<
for (const fileBlock of textFileBlocks) {
const file = fileBlock.file
const fileContent = (await window.api.file.read(file.id + file.ext)).trim()
const fileContent = (await window.api.file.read(file.id + file.ext, true)).trim()
const fileNameRow = 'file: ' + file.origin_name + '\n\n'
text = text + fileNameRow + fileContent + divider
}

View File

@@ -0,0 +1,233 @@
import { isSupportedModel } from '@renderer/config/models'
import {
GenerateImageParams,
MCPCallToolResponse,
MCPTool,
MCPToolResponse,
Model,
Provider,
ToolCallResponse
} from '@renderer/types'
import {
NewApiModel,
RequestOptions,
SdkInstance,
SdkMessageParam,
SdkParams,
SdkRawChunk,
SdkRawOutput,
SdkTool,
SdkToolCall
} from '@renderer/types/sdk'
import { CompletionsContext } from '../middleware/types'
import { AnthropicAPIClient } from './anthropic/AnthropicAPIClient'
import { BaseApiClient } from './BaseApiClient'
import { GeminiAPIClient } from './gemini/GeminiAPIClient'
import { OpenAIAPIClient } from './openai/OpenAIApiClient'
import { OpenAIResponseAPIClient } from './openai/OpenAIResponseAPIClient'
import { RequestTransformer, ResponseChunkTransformer } from './types'
export class NewAPIClient extends BaseApiClient {
// 使用联合类型而不是any保持类型安全
private clients: Map<string, AnthropicAPIClient | GeminiAPIClient | OpenAIResponseAPIClient | OpenAIAPIClient> =
new Map()
private defaultClient: OpenAIAPIClient
private currentClient: BaseApiClient
constructor(provider: Provider) {
super(provider)
const claudeClient = new AnthropicAPIClient(provider)
const geminiClient = new GeminiAPIClient(provider)
const openaiClient = new OpenAIAPIClient(provider)
const openaiResponseClient = new OpenAIResponseAPIClient(provider)
this.clients.set('claude', claudeClient)
this.clients.set('gemini', geminiClient)
this.clients.set('openai', openaiClient)
this.clients.set('openai-response', openaiResponseClient)
// 设置默认client
this.defaultClient = openaiClient
this.currentClient = this.defaultClient as BaseApiClient
}
override getBaseURL(): string {
if (!this.currentClient) {
return this.provider.apiHost
}
return this.currentClient.getBaseURL()
}
/**
* 类型守卫确保client是BaseApiClient的实例
*/
private isValidClient(client: unknown): client is BaseApiClient {
return (
client !== null &&
client !== undefined &&
typeof client === 'object' &&
'createCompletions' in client &&
'getRequestTransformer' in client &&
'getResponseChunkTransformer' in client
)
}
/**
* 根据模型获取合适的client
*/
private getClient(model: Model): BaseApiClient {
if (!model.endpoint_type) {
throw new Error('Model endpoint type is not defined')
}
if (model.endpoint_type === 'anthropic') {
const client = this.clients.get('claude')
if (!client || !this.isValidClient(client)) {
throw new Error('Failed to get claude client')
}
return client
}
if (model.endpoint_type === 'openai-response') {
const client = this.clients.get('openai-response')
if (!client || !this.isValidClient(client)) {
throw new Error('Failed to get openai-response client')
}
return client
}
if (model.endpoint_type === 'gemini') {
const client = this.clients.get('gemini')
if (!client || !this.isValidClient(client)) {
throw new Error('Failed to get gemini client')
}
return client
}
if (model.endpoint_type === 'openai' || model.endpoint_type === 'image-generation') {
const client = this.clients.get('openai')
if (!client || !this.isValidClient(client)) {
throw new Error('Failed to get openai client')
}
return client
}
throw new Error('Invalid model endpoint type: ' + model.endpoint_type)
}
/**
* 根据模型选择合适的client并委托调用
*/
public getClientForModel(model: Model): BaseApiClient {
this.currentClient = this.getClient(model)
return this.currentClient
}
// ============ BaseApiClient 抽象方法实现 ============
async createCompletions(payload: SdkParams, options?: RequestOptions): Promise<SdkRawOutput> {
// 尝试从payload中提取模型信息来选择client
const modelId = this.extractModelFromPayload(payload)
if (modelId) {
const modelObj = { id: modelId } as Model
const targetClient = this.getClient(modelObj)
return targetClient.createCompletions(payload, options)
}
// 如果无法从payload中提取模型使用当前设置的client
return this.currentClient.createCompletions(payload, options)
}
/**
* 从SDK payload中提取模型ID
*/
private extractModelFromPayload(payload: SdkParams): string | null {
// 不同的SDK可能有不同的字段名
if ('model' in payload && typeof payload.model === 'string') {
return payload.model
}
return null
}
async generateImage(params: GenerateImageParams): Promise<string[]> {
return this.currentClient.generateImage(params)
}
async getEmbeddingDimensions(model?: Model): Promise<number> {
const client = model ? this.getClient(model) : this.currentClient
return client.getEmbeddingDimensions(model)
}
override async listModels(): Promise<NewApiModel[]> {
try {
const sdk = await this.defaultClient.getSdkInstance()
// Explicitly type the expected response shape so that `data` is recognised.
const response = await sdk.request<{ data: NewApiModel[] }>({
method: 'get',
path: '/models'
})
const models: NewApiModel[] = response.data ?? []
models.forEach((model) => {
model.id = model.id.trim()
})
return models.filter(isSupportedModel)
} catch (error) {
console.error('Error listing models:', error)
return []
}
}
async getSdkInstance(): Promise<SdkInstance> {
return this.currentClient.getSdkInstance()
}
getRequestTransformer(): RequestTransformer<SdkParams, SdkMessageParam> {
return this.currentClient.getRequestTransformer()
}
getResponseChunkTransformer(ctx: CompletionsContext): ResponseChunkTransformer<SdkRawChunk> {
return this.currentClient.getResponseChunkTransformer(ctx)
}
convertMcpToolsToSdkTools(mcpTools: MCPTool[]): SdkTool[] {
return this.currentClient.convertMcpToolsToSdkTools(mcpTools)
}
convertSdkToolCallToMcp(toolCall: SdkToolCall, mcpTools: MCPTool[]): MCPTool | undefined {
return this.currentClient.convertSdkToolCallToMcp(toolCall, mcpTools)
}
convertSdkToolCallToMcpToolResponse(toolCall: SdkToolCall, mcpTool: MCPTool): ToolCallResponse {
return this.currentClient.convertSdkToolCallToMcpToolResponse(toolCall, mcpTool)
}
buildSdkMessages(
currentReqMessages: SdkMessageParam[],
output: SdkRawOutput | string,
toolResults: SdkMessageParam[],
toolCalls?: SdkToolCall[]
): SdkMessageParam[] {
return this.currentClient.buildSdkMessages(currentReqMessages, output, toolResults, toolCalls)
}
convertMcpToolResponseToSdkMessageParam(
mcpToolResponse: MCPToolResponse,
resp: MCPCallToolResponse,
model: Model
): SdkMessageParam | undefined {
const client = this.getClient(model)
return client.convertMcpToolResponseToSdkMessageParam(mcpToolResponse, resp, model)
}
extractMessagesFromSdkPayload(sdkPayload: SdkParams): SdkMessageParam[] {
return this.currentClient.extractMessagesFromSdkPayload(sdkPayload)
}
estimateMessageTokens(message: SdkMessageParam): number {
return this.currentClient.estimateMessageTokens(message)
}
}

View File

@@ -50,7 +50,9 @@ import {
LLMWebSearchInProgressChunk,
MCPToolCreatedChunk,
TextDeltaChunk,
ThinkingDeltaChunk
TextStartChunk,
ThinkingDeltaChunk,
ThinkingStartChunk
} from '@renderer/types/chunk'
import { type Message } from '@renderer/types/newMessage'
import {
@@ -229,7 +231,7 @@ export class AnthropicAPIClient extends BaseApiClient<
}
})
} else {
const fileContent = await (await window.api.file.read(file.id + file.ext)).trim()
const fileContent = await (await window.api.file.read(file.id + file.ext, true)).trim()
parts.push({
type: 'text',
text: file.origin_name + '\n' + fileContent
@@ -517,7 +519,6 @@ export class AnthropicAPIClient extends BaseApiClient<
return () => {
let accumulatedJson = ''
const toolCalls: Record<number, ToolUseBlock> = {}
return {
async transform(rawChunk: AnthropicSdkRawChunk, controller: TransformStreamDefaultController<GenericChunk>) {
switch (rawChunk.type) {
@@ -612,6 +613,19 @@ export class AnthropicAPIClient extends BaseApiClient<
toolCalls[rawChunk.index] = contentBlock
break
}
case 'text': {
controller.enqueue({
type: ChunkType.TEXT_START
} as TextStartChunk)
break
}
case 'thinking':
case 'redacted_thinking': {
controller.enqueue({
type: ChunkType.THINKING_START
} as ThinkingStartChunk)
break
}
}
break
}

View File

@@ -41,7 +41,7 @@ import {
ToolCallResponse,
WebSearchSource
} from '@renderer/types'
import { ChunkType, LLMWebSearchCompleteChunk } from '@renderer/types/chunk'
import { ChunkType, LLMWebSearchCompleteChunk, TextStartChunk, ThinkingStartChunk } from '@renderer/types/chunk'
import { Message } from '@renderer/types/newMessage'
import {
GeminiOptions,
@@ -288,7 +288,7 @@ export class GeminiAPIClient extends BaseApiClient<
continue
}
if ([FileTypes.TEXT, FileTypes.DOCUMENT].includes(file.type)) {
const fileContent = await (await window.api.file.read(file.id + file.ext)).trim()
const fileContent = await (await window.api.file.read(file.id + file.ext, true)).trim()
parts.push({
text: file.origin_name + '\n' + fileContent
})
@@ -547,20 +547,34 @@ export class GeminiAPIClient extends BaseApiClient<
}
getResponseChunkTransformer(): ResponseChunkTransformer<GeminiSdkRawChunk> {
const toolCalls: FunctionCall[] = []
let isFirstTextChunk = true
let isFirstThinkingChunk = true
return () => ({
async transform(chunk: GeminiSdkRawChunk, controller: TransformStreamDefaultController<GenericChunk>) {
const toolCalls: FunctionCall[] = []
if (chunk.candidates && chunk.candidates.length > 0) {
for (const candidate of chunk.candidates) {
if (candidate.content) {
candidate.content.parts?.forEach((part) => {
const text = part.text || ''
if (part.thought) {
if (isFirstThinkingChunk) {
controller.enqueue({
type: ChunkType.THINKING_START
} as ThinkingStartChunk)
isFirstThinkingChunk = false
}
controller.enqueue({
type: ChunkType.THINKING_DELTA,
text: text
})
} else if (part.text) {
if (isFirstTextChunk) {
controller.enqueue({
type: ChunkType.TEXT_START
} as TextStartChunk)
isFirstTextChunk = false
}
controller.enqueue({
type: ChunkType.TEXT_DELTA,
text: text
@@ -593,6 +607,13 @@ export class GeminiAPIClient extends BaseApiClient<
}
} as LLMWebSearchCompleteChunk)
}
if (toolCalls.length > 0) {
controller.enqueue({
type: ChunkType.MCP_TOOL_CREATED,
tool_calls: [...toolCalls]
})
toolCalls.length = 0
}
controller.enqueue({
type: ChunkType.LLM_RESPONSE_COMPLETE,
response: {

View File

@@ -31,7 +31,7 @@ import {
ToolCallResponse,
WebSearchSource
} from '@renderer/types'
import { ChunkType } from '@renderer/types/chunk'
import { ChunkType, TextStartChunk, ThinkingStartChunk } from '@renderer/types/chunk'
import { Message } from '@renderer/types/newMessage'
import {
OpenAISdkMessageParam,
@@ -307,7 +307,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
}
if ([FileTypes.TEXT, FileTypes.DOCUMENT].includes(file.type)) {
const fileContent = await (await window.api.file.read(file.id + file.ext)).trim()
const fileContent = await (await window.api.file.read(file.id + file.ext, true)).trim()
parts.push({
type: 'text',
text: file.origin_name + '\n' + fileContent
@@ -564,11 +564,11 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
// Perplexity citations
// @ts-ignore - citations may not be in standard type definitions
if (context.provider?.id === 'perplexity' && chunk.citations && chunk.citations.length > 0) {
if (context.provider?.id === 'perplexity' && chunk.search_results && chunk.search_results.length > 0) {
hasBeenCollectedWebSearch = true
return {
// @ts-ignore - citations may not be in standard type definitions
results: chunk.citations,
results: chunk.search_results,
source: WebSearchSource.PERPLEXITY
}
}
@@ -659,6 +659,8 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
isFinished = true
}
let isFirstThinkingChunk = true
let isFirstTextChunk = true
return (context: ResponseChunkTransformerContext) => ({
async transform(chunk: OpenAISdkRawChunk, controller: TransformStreamDefaultController<GenericChunk>) {
// 持续更新usage信息
@@ -672,74 +674,21 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
// 处理chunk
if ('choices' in chunk && chunk.choices && chunk.choices.length > 0) {
const choice = chunk.choices[0]
for (const choice of chunk.choices) {
if (!choice) continue
if (!choice) return
// 对于流式响应,使用 delta对于非流式响应使用 message
// 然而某些 OpenAI 兼容平台在非流式请求时会错误地返回一个空对象的 delta 字段。
// 如果 delta 为空对象,应当忽略它并回退到 message避免造成内容缺失。
let contentSource: OpenAISdkRawContentSource | null = null
if ('delta' in choice && choice.delta && Object.keys(choice.delta).length > 0) {
contentSource = choice.delta
} else if ('message' in choice) {
contentSource = choice.message
}
if (!contentSource) return
const webSearchData = collectWebSearchData(chunk, contentSource, context)
if (webSearchData) {
controller.enqueue({
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
llm_web_search: webSearchData
})
}
// 处理推理内容 (e.g. from OpenRouter DeepSeek-R1)
// @ts-ignore - reasoning_content is not in standard OpenAI types but some providers use it
const reasoningText = contentSource.reasoning_content || contentSource.reasoning
if (reasoningText) {
controller.enqueue({
type: ChunkType.THINKING_DELTA,
text: reasoningText
})
}
// 处理文本内容
if (contentSource.content) {
controller.enqueue({
type: ChunkType.TEXT_DELTA,
text: contentSource.content
})
}
// 处理工具调用
if (contentSource.tool_calls) {
for (const toolCall of contentSource.tool_calls) {
if ('index' in toolCall) {
const { id, index, function: fun } = toolCall
if (fun?.name) {
toolCalls[index] = {
id: id || '',
function: {
name: fun.name,
arguments: fun.arguments || ''
},
type: 'function'
}
} else if (fun?.arguments) {
toolCalls[index].function.arguments += fun.arguments
}
} else {
toolCalls.push(toolCall)
}
// 对于流式响应,使用 delta对于非流式响应使用 message。
// 然而某些 OpenAI 兼容平台在非流式请求时会错误地返回一个空对象的 delta 字段。
// 如果 delta 为空对象,应当忽略它并回退到 message避免造成内容缺失
let contentSource: OpenAISdkRawContentSource | null = null
if ('delta' in choice && choice.delta && Object.keys(choice.delta).length > 0) {
contentSource = choice.delta
} else if ('message' in choice) {
contentSource = choice.message
}
}
// 处理finish_reason发送流结束信号
if ('finish_reason' in choice && choice.finish_reason) {
Logger.debug(`[OpenAIApiClient] Stream finished with reason: ${choice.finish_reason}`)
if (!contentSource) continue
const webSearchData = collectWebSearchData(chunk, contentSource, context)
if (webSearchData) {
controller.enqueue({
@@ -747,7 +696,72 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
llm_web_search: webSearchData
})
}
emitCompletionSignals(controller)
// 处理推理内容 (e.g. from OpenRouter DeepSeek-R1)
// @ts-ignore - reasoning_content is not in standard OpenAI types but some providers use it
const reasoningText = contentSource.reasoning_content || contentSource.reasoning
if (reasoningText) {
if (isFirstThinkingChunk) {
controller.enqueue({
type: ChunkType.THINKING_START
} as ThinkingStartChunk)
isFirstThinkingChunk = false
}
controller.enqueue({
type: ChunkType.THINKING_DELTA,
text: reasoningText
})
}
// 处理文本内容
if (contentSource.content) {
if (isFirstTextChunk) {
controller.enqueue({
type: ChunkType.TEXT_START
} as TextStartChunk)
isFirstTextChunk = false
}
controller.enqueue({
type: ChunkType.TEXT_DELTA,
text: contentSource.content
})
}
// 处理工具调用
if (contentSource.tool_calls) {
for (const toolCall of contentSource.tool_calls) {
if ('index' in toolCall) {
const { id, index, function: fun } = toolCall
if (fun?.name) {
toolCalls[index] = {
id: id || '',
function: {
name: fun.name,
arguments: fun.arguments || ''
},
type: 'function'
}
} else if (fun?.arguments) {
toolCalls[index].function.arguments += fun.arguments
}
} else {
toolCalls.push(toolCall)
}
}
}
// 处理finish_reason发送流结束信号
if ('finish_reason' in choice && choice.finish_reason) {
Logger.debug(`[OpenAIApiClient] Stream finished with reason: ${choice.finish_reason}`)
const webSearchData = collectWebSearchData(chunk, contentSource, context)
if (webSearchData) {
controller.enqueue({
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
llm_web_search: webSearchData
})
}
emitCompletionSignals(controller)
}
}
}
},

View File

@@ -89,7 +89,7 @@ export abstract class OpenAIBaseClient<
const data = await sdk.embeddings.create({
model: model.id,
input: model?.provider === 'baidu-cloud' ? ['hi'] : 'hi',
encoding_format: 'float'
encoding_format: this.provider.id === 'voyageai' ? undefined : 'float'
})
return data.data[0].embedding.length
}

View File

@@ -2,6 +2,7 @@ import { GenericChunk } from '@renderer/aiCore/middleware/schemas'
import { CompletionsContext } from '@renderer/aiCore/middleware/types'
import {
isOpenAIChatCompletionOnlyModel,
isOpenAILLMModel,
isSupportedReasoningEffortOpenAIModel,
isVisionModel
} from '@renderer/config/models'
@@ -38,7 +39,7 @@ import { findFileBlocks, findImageBlocks } from '@renderer/utils/messageUtils/fi
import { buildSystemPrompt } from '@renderer/utils/prompt'
import { MB } from '@shared/config/constant'
import { isEmpty } from 'lodash'
import OpenAI from 'openai'
import OpenAI, { AzureOpenAI } from 'openai'
import { ResponseInput } from 'openai/resources/responses/responses'
import { RequestTransformer, ResponseChunkTransformer } from '../types'
@@ -64,10 +65,13 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
* 根据模型特征选择合适的客户端
*/
public getClient(model: Model) {
if (isOpenAIChatCompletionOnlyModel(model)) {
return this.client
} else {
if (isOpenAILLMModel(model) && !isOpenAIChatCompletionOnlyModel(model)) {
if (this.provider.id === 'azure-openai' || this.provider.type === 'azure-openai') {
this.provider = { ...this.provider, apiVersion: 'preview' }
}
return this
} else {
return this.client
}
}
@@ -76,15 +80,25 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
return this.sdkInstance
}
return new OpenAI({
dangerouslyAllowBrowser: true,
apiKey: this.apiKey,
baseURL: this.getBaseURL(),
defaultHeaders: {
...this.defaultHeaders(),
...this.provider.extra_headers
}
})
if (this.provider.id === 'azure-openai' || this.provider.type === 'azure-openai') {
this.provider = { ...this.provider, apiHost: `${this.provider.apiHost}/openai/v1` }
return new AzureOpenAI({
dangerouslyAllowBrowser: true,
apiKey: this.apiKey,
apiVersion: this.provider.apiVersion,
baseURL: this.provider.apiHost
})
} else {
return new OpenAI({
dangerouslyAllowBrowser: true,
apiKey: this.apiKey,
baseURL: this.getBaseURL(),
defaultHeaders: {
...this.defaultHeaders(),
...this.provider.extra_headers
}
})
}
}
override async createCompletions(
@@ -172,7 +186,7 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
}
if ([FileTypes.TEXT, FileTypes.DOCUMENT].includes(file.type)) {
const fileContent = (await window.api.file.read(file.id + file.ext)).trim()
const fileContent = (await window.api.file.read(file.id + file.ext, true)).trim()
parts.push({
type: 'input_text',
text: file.origin_name + '\n' + fileContent
@@ -353,16 +367,15 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
(m) => (m as OpenAI.Responses.EasyInputMessage).role === 'assistant'
) as OpenAI.Responses.EasyInputMessage
const finalUserMessage = userMessage.pop() as OpenAI.Responses.EasyInputMessage
if (
finalAssistantMessage &&
Array.isArray(finalAssistantMessage.content) &&
finalUserMessage &&
Array.isArray(finalUserMessage.content)
) {
finalAssistantMessage.content = [...finalAssistantMessage.content, ...finalUserMessage.content]
if (finalUserMessage && Array.isArray(finalUserMessage.content)) {
if (finalAssistantMessage && Array.isArray(finalAssistantMessage.content)) {
finalAssistantMessage.content = [...finalAssistantMessage.content, ...finalUserMessage.content]
// 这里是故意将上条助手消息的内容(包含图片和文件)作为用户消息发送
userMessage = [{ ...finalAssistantMessage, role: 'user' } as OpenAI.Responses.EasyInputMessage]
} else {
userMessage.push(finalUserMessage)
}
}
// 这里是故意将上条助手消息的内容(包含图片和文件)作为用户消息发送
userMessage = [{ ...finalAssistantMessage, role: 'user' } as OpenAI.Responses.EasyInputMessage]
}
// 4. 最终请求消息
@@ -423,6 +436,8 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
const outputItems: OpenAI.Responses.ResponseOutputItem[] = []
let hasBeenCollectedToolCalls = false
let hasReasoningSummary = false
let isFirstThinkingChunk = true
let isFirstTextChunk = true
return () => ({
async transform(chunk: OpenAIResponseSdkRawChunk, controller: TransformStreamDefaultController<GenericChunk>) {
// 处理chunk
@@ -434,6 +449,12 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
switch (output.type) {
case 'message':
if (output.content[0].type === 'output_text') {
if (isFirstTextChunk) {
controller.enqueue({
type: ChunkType.TEXT_START
})
isFirstTextChunk = false
}
controller.enqueue({
type: ChunkType.TEXT_DELTA,
text: output.content[0].text
@@ -450,6 +471,12 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
}
break
case 'reasoning':
if (isFirstThinkingChunk) {
controller.enqueue({
type: ChunkType.THINKING_START
})
isFirstThinkingChunk = false
}
controller.enqueue({
type: ChunkType.THINKING_DELTA,
text: output.summary.map((s) => s.text).join('\n')
@@ -492,6 +519,10 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
case 'response.output_item.added':
if (chunk.item.type === 'function_call') {
outputItems.push(chunk.item)
} else if (chunk.item.type === 'web_search_call') {
controller.enqueue({
type: ChunkType.LLM_WEB_SEARCH_IN_PROGRESS
})
}
break
case 'response.reasoning_summary_part.added':
@@ -505,6 +536,12 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
hasReasoningSummary = true
break
case 'response.reasoning_summary_text.delta':
if (isFirstThinkingChunk) {
controller.enqueue({
type: ChunkType.THINKING_START
})
isFirstThinkingChunk = false
}
controller.enqueue({
type: ChunkType.THINKING_DELTA,
text: chunk.delta
@@ -530,6 +567,12 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
})
break
case 'response.output_text.delta': {
if (isFirstTextChunk) {
controller.enqueue({
type: ChunkType.TEXT_START
})
isFirstTextChunk = false
}
controller.enqueue({
type: ChunkType.TEXT_DELTA,
text: chunk.delta

View File

@@ -8,6 +8,7 @@ import { isEnabledToolUse } from '@renderer/utils/mcp-tools'
import { OpenAIAPIClient } from './clients'
import { AihubmixAPIClient } from './clients/AihubmixAPIClient'
import { AnthropicAPIClient } from './clients/anthropic/AnthropicAPIClient'
import { NewAPIClient } from './clients/NewAPIClient'
import { OpenAIResponseAPIClient } from './clients/openai/OpenAIResponseAPIClient'
import { CompletionsMiddlewareBuilder } from './middleware/builder'
import { MIDDLEWARE_NAME as AbortHandlerMiddlewareName } from './middleware/common/AbortHandlerMiddleware'
@@ -48,6 +49,11 @@ export default class AiProvider {
if (client instanceof OpenAIResponseAPIClient) {
client = client.getClient(model) as BaseApiClient
}
} else if (this.apiClient instanceof NewAPIClient) {
client = this.apiClient.getClientForModel(model)
if (client instanceof OpenAIResponseAPIClient) {
client = client.getClient(model) as BaseApiClient
}
} else if (this.apiClient instanceof OpenAIResponseAPIClient) {
// OpenAIResponseAPIClient: 根据模型特征选择API类型
client = this.apiClient.getClient(model) as BaseApiClient
@@ -69,11 +75,12 @@ export default class AiProvider {
} else {
// Existing logic for other models
if (!params.enableReasoning) {
builder.remove(ThinkingTagExtractionMiddlewareName)
// 这里注释掉不会影响正常的关闭思考,可忽略不计的性能下降
// builder.remove(ThinkingTagExtractionMiddlewareName)
builder.remove(ThinkChunkMiddlewareName)
}
// 注意用client判断会导致typescript类型收窄
if (!(this.apiClient instanceof OpenAIAPIClient)) {
if (!(this.apiClient instanceof OpenAIAPIClient) && !(this.apiClient instanceof OpenAIResponseAPIClient)) {
builder.remove(ThinkingTagExtractionMiddlewareName)
}
if (!(this.apiClient instanceof AnthropicAPIClient) && !(this.apiClient instanceof OpenAIResponseAPIClient)) {

View File

@@ -67,7 +67,12 @@ export const AbortHandlerMiddleware: CompletionsMiddleware =
const streamWithAbortHandler = (result.stream as ReadableStream<Chunk>).pipeThrough(
new TransformStream<Chunk, Chunk | ErrorChunk>({
transform(chunk, controller) {
// 检查 abort 状态
// 如果已经收到错误块,不再检查 abort 状态
if (chunk.type === ChunkType.ERROR) {
controller.enqueue(chunk)
return
}
if (abortSignal?.aborted) {
// 转换为 ErrorChunk
const errorChunk: ErrorChunk = {

View File

@@ -136,7 +136,6 @@ function extractAndAccumulateUsageMetrics(ctx: CompletionsContext, chunk: Generi
Logger.debug(`[${MIDDLEWARE_NAME}] First token timestamp: ${ctx._internal.customState.firstTokenTimestamp}`)
}
if (chunk.type === ChunkType.LLM_RESPONSE_COMPLETE) {
Logger.debug(`[${MIDDLEWARE_NAME}] LLM_RESPONSE_COMPLETE chunk received:`, ctx._internal)
// 从LLM_RESPONSE_COMPLETE chunk中提取usage数据
if (chunk.response?.usage) {
accumulateUsage(ctx._internal.observer.usage, chunk.response.usage)

View File

@@ -89,6 +89,11 @@ function createToolHandlingTransform(
let hasToolUseResponses = false
let streamEnded = false
// 存储已执行的工具结果
const executedToolResults: SdkMessageParam[] = []
const executedToolCalls: SdkToolCall[] = []
const executionPromises: Promise<void>[] = []
return new TransformStream({
async transform(chunk: GenericChunk, controller) {
try {
@@ -98,22 +103,64 @@ function createToolHandlingTransform(
// 1. 处理Function Call方式的工具调用
if (createdChunk.tool_calls && createdChunk.tool_calls.length > 0) {
toolCalls.push(...createdChunk.tool_calls)
hasToolCalls = true
for (const toolCall of createdChunk.tool_calls) {
toolCalls.push(toolCall)
const executionPromise = (async () => {
try {
const result = await executeToolCalls(
ctx,
[toolCall],
mcpTools,
allToolResponses,
currentParams.onChunk,
currentParams.assistant.model!
)
// 缓存执行结果
executedToolResults.push(...result.toolResults)
executedToolCalls.push(...result.confirmedToolCalls)
} catch (error) {
console.error(`🔧 [${MIDDLEWARE_NAME}] Error executing tool call asynchronously:`, error)
}
})()
executionPromises.push(executionPromise)
}
}
// 2. 处理Tool Use方式的工具调用
if (createdChunk.tool_use_responses && createdChunk.tool_use_responses.length > 0) {
toolUseResponses.push(...createdChunk.tool_use_responses)
hasToolUseResponses = true
for (const toolUseResponse of createdChunk.tool_use_responses) {
toolUseResponses.push(toolUseResponse)
const executionPromise = (async () => {
try {
const result = await executeToolUseResponses(
ctx,
[toolUseResponse], // 单个执行
mcpTools,
allToolResponses,
currentParams.onChunk,
currentParams.assistant.model!
)
// 缓存执行结果
executedToolResults.push(...result.toolResults)
} catch (error) {
console.error(`🔧 [${MIDDLEWARE_NAME}] Error executing tool use response asynchronously:`, error)
// 错误时不影响其他工具的执行
}
})()
executionPromises.push(executionPromise)
}
}
// 不转发MCP工具进展chunks避免重复处理
return
} else {
controller.enqueue(chunk)
}
// 转发其他所有chunk
controller.enqueue(chunk)
} catch (error) {
console.error(`🔧 [${MIDDLEWARE_NAME}] Error processing chunk:`, error)
controller.error(error)
@@ -121,43 +168,33 @@ function createToolHandlingTransform(
},
async flush(controller) {
const shouldExecuteToolCalls = hasToolCalls && toolCalls.length > 0
const shouldExecuteToolUseResponses = hasToolUseResponses && toolUseResponses.length > 0
if (!streamEnded && (shouldExecuteToolCalls || shouldExecuteToolUseResponses)) {
// 在流结束时等待所有异步工具执行完成,然后进行递归调用
if (!streamEnded && (hasToolCalls || hasToolUseResponses)) {
streamEnded = true
try {
let toolResult: SdkMessageParam[] = []
if (shouldExecuteToolCalls) {
toolResult = await executeToolCalls(
ctx,
toolCalls,
mcpTools,
allToolResponses,
currentParams.onChunk,
currentParams.assistant.model!
)
} else if (shouldExecuteToolUseResponses) {
toolResult = await executeToolUseResponses(
ctx,
toolUseResponses,
mcpTools,
allToolResponses,
currentParams.onChunk,
currentParams.assistant.model!
)
}
if (toolResult.length > 0) {
await Promise.all(executionPromises)
if (executedToolResults.length > 0) {
const output = ctx._internal.toolProcessingState?.output
const newParams = buildParamsWithToolResults(
ctx,
currentParams,
output,
executedToolResults,
executedToolCalls
)
// 在递归调用前通知UI开始新的LLM响应处理
if (currentParams.onChunk) {
currentParams.onChunk({
type: ChunkType.LLM_RESPONSE_CREATED
})
}
const newParams = buildParamsWithToolResults(ctx, currentParams, output, toolResult, toolCalls)
await executeWithToolHandling(newParams, depth + 1)
}
} catch (error) {
console.error(`🔧 [${MIDDLEWARE_NAME}] Error in tool processing:`, error)
Logger.error(`🔧 [${MIDDLEWARE_NAME}] Error in tool processing:`, error)
controller.error(error)
} finally {
hasToolCalls = false
@@ -178,8 +215,7 @@ async function executeToolCalls(
allToolResponses: MCPToolResponse[],
onChunk: CompletionsParams['onChunk'],
model: Model
): Promise<SdkMessageParam[]> {
// 转换为MCPToolResponse格式
): Promise<{ toolResults: SdkMessageParam[]; confirmedToolCalls: SdkToolCall[] }> {
const mcpToolResponses: ToolCallResponse[] = toolCalls
.map((toolCall) => {
const mcpTool = ctx.apiClientInstance.convertSdkToolCallToMcp(toolCall, mcpTools)
@@ -192,11 +228,11 @@ async function executeToolCalls(
if (mcpToolResponses.length === 0) {
console.warn(`🔧 [${MIDDLEWARE_NAME}] No valid MCP tool responses to execute`)
return []
return { toolResults: [], confirmedToolCalls: [] }
}
// 使用现有的parseAndCallTools函数执行工具
const toolResults = await parseAndCallTools(
const { toolResults, confirmedToolResponses } = await parseAndCallTools(
mcpToolResponses,
allToolResponses,
onChunk,
@@ -204,10 +240,26 @@ async function executeToolCalls(
return ctx.apiClientInstance.convertMcpToolResponseToSdkMessageParam(mcpToolResponse, resp, model)
},
model,
mcpTools
mcpTools,
ctx._internal?.flowControl?.abortSignal
)
return toolResults
// 找出已确认工具对应的原始toolCalls
const confirmedToolCalls = toolCalls.filter((toolCall) => {
return confirmedToolResponses.find((confirmed) => {
// 根据不同的ID字段匹配原始toolCall
return (
('name' in toolCall &&
(toolCall.name?.includes(confirmed.tool.name) || toolCall.name?.includes(confirmed.tool.id))) ||
confirmed.tool.name === toolCall.id ||
confirmed.tool.id === toolCall.id ||
('toolCallId' in confirmed && confirmed.toolCallId === toolCall.id) ||
('function' in toolCall && toolCall.function.name.toLowerCase().includes(confirmed.tool.name.toLowerCase()))
)
})
})
return { toolResults, confirmedToolCalls }
}
/**
@@ -221,9 +273,9 @@ async function executeToolUseResponses(
allToolResponses: MCPToolResponse[],
onChunk: CompletionsParams['onChunk'],
model: Model
): Promise<SdkMessageParam[]> {
): Promise<{ toolResults: SdkMessageParam[] }> {
// 直接使用parseAndCallTools函数处理已经解析好的ToolUseResponse
const toolResults = await parseAndCallTools(
const { toolResults } = await parseAndCallTools(
toolUseResponses,
allToolResponses,
onChunk,
@@ -231,10 +283,11 @@ async function executeToolUseResponses(
return ctx.apiClientInstance.convertMcpToolResponseToSdkMessageParam(mcpToolResponse, resp, model)
},
model,
mcpTools
mcpTools,
ctx._internal?.flowControl?.abortSignal
)
return toolResults
return { toolResults }
}
/**
@@ -245,7 +298,7 @@ function buildParamsWithToolResults(
currentParams: CompletionsParams,
output: SdkRawOutput | string | undefined,
toolResults: SdkMessageParam[],
toolCalls: SdkToolCall[]
confirmedToolCalls: SdkToolCall[]
): CompletionsParams {
// 获取当前已经转换好的reqMessages如果没有则使用原始messages
const currentReqMessages = getCurrentReqMessages(ctx)
@@ -253,7 +306,7 @@ function buildParamsWithToolResults(
const apiClient = ctx.apiClientInstance
// 从回复中构建助手消息
const newReqMessages = apiClient.buildSdkMessages(currentReqMessages, output, toolResults, toolCalls)
const newReqMessages = apiClient.buildSdkMessages(currentReqMessages, output, toolResults, confirmedToolCalls)
if (output && ctx._internal.toolProcessingState) {
ctx._internal.toolProcessingState.output = undefined

View File

@@ -38,7 +38,6 @@ export const TextChunkMiddleware: CompletionsMiddleware =
// 用于跨chunk的状态管理
let accumulatedTextContent = ''
let hasEnqueue = false
const enhancedTextStream = resultFromUpstream.pipeThrough(
new TransformStream<GenericChunk, GenericChunk>({
transform(chunk: GenericChunk, controller) {
@@ -53,30 +52,32 @@ export const TextChunkMiddleware: CompletionsMiddleware =
// 创建新的chunk包含处理后的文本
controller.enqueue(chunk)
} else if (accumulatedTextContent) {
if (chunk.type !== ChunkType.LLM_RESPONSE_COMPLETE) {
} else if (accumulatedTextContent && chunk.type !== ChunkType.TEXT_START) {
if (chunk.type === ChunkType.LLM_RESPONSE_COMPLETE) {
const finalText = accumulatedTextContent
ctx._internal.customState!.accumulatedText = finalText
if (ctx._internal.toolProcessingState && !ctx._internal.toolProcessingState?.output) {
ctx._internal.toolProcessingState.output = finalText
}
// 处理 onResponse 回调 - 发送最终完整文本
if (params.onResponse) {
params.onResponse(finalText, true)
}
controller.enqueue({
type: ChunkType.TEXT_COMPLETE,
text: finalText
})
controller.enqueue(chunk)
} else {
controller.enqueue({
type: ChunkType.TEXT_COMPLETE,
text: accumulatedTextContent
})
controller.enqueue(chunk)
hasEnqueue = true
}
const finalText = accumulatedTextContent
ctx._internal.customState!.accumulatedText = finalText
if (ctx._internal.toolProcessingState && !ctx._internal.toolProcessingState?.output) {
ctx._internal.toolProcessingState.output = finalText
}
// 处理 onResponse 回调 - 发送最终完整文本
if (params.onResponse) {
params.onResponse(finalText, true)
}
controller.enqueue({
type: ChunkType.TEXT_COMPLETE,
text: finalText
})
accumulatedTextContent = ''
if (!hasEnqueue) {
controller.enqueue(chunk)
}
} else {
// 其他类型的chunk直接传递
controller.enqueue(chunk)

View File

@@ -65,7 +65,7 @@ export const ThinkChunkMiddleware: CompletionsMiddleware =
thinking_millsec: thinkingStartTime > 0 ? Date.now() - thinkingStartTime : 0
}
controller.enqueue(enhancedChunk)
} else if (hasThinkingContent && thinkingStartTime > 0) {
} else if (hasThinkingContent && thinkingStartTime > 0 && chunk.type !== ChunkType.THINKING_START) {
// 收到任何非THINKING_DELTA的chunk时如果有累积的思考内容生成THINKING_COMPLETE
const thinkingCompleteChunk: ThinkingCompleteChunk = {
type: ChunkType.THINKING_COMPLETE,

View File

@@ -42,7 +42,12 @@ export const WebSearchMiddleware: CompletionsMiddleware =
const providerType = model.provider || 'openai'
// 使用当前可用的Web搜索结果进行链接转换
const text = chunk.text
const result = smartLinkConverter(text, providerType, isFirstChunk)
const result = smartLinkConverter(
text,
providerType,
isFirstChunk,
ctx._internal.webSearchState!.results
)
if (isFirstChunk) {
isFirstChunk = false
}

View File

@@ -1,5 +1,11 @@
import { Model } from '@renderer/types'
import { ChunkType, TextDeltaChunk, ThinkingCompleteChunk, ThinkingDeltaChunk } from '@renderer/types/chunk'
import {
ChunkType,
TextDeltaChunk,
ThinkingCompleteChunk,
ThinkingDeltaChunk,
ThinkingStartChunk
} from '@renderer/types/chunk'
import { TagConfig, TagExtractor } from '@renderer/utils/tagExtraction'
import Logger from 'electron-log/renderer'
@@ -59,6 +65,8 @@ export const ThinkingTagExtractionMiddleware: CompletionsMiddleware =
let hasThinkingContent = false
let thinkingStartTime = 0
let isFirstTextChunk = true
const processedStream = resultFromUpstream.pipeThrough(
new TransformStream<GenericChunk, GenericChunk>({
transform(chunk: GenericChunk, controller) {
@@ -69,7 +77,7 @@ export const ThinkingTagExtractionMiddleware: CompletionsMiddleware =
const extractionResults = tagExtractor.processText(textChunk.text)
for (const extractionResult of extractionResults) {
if (extractionResult.complete && extractionResult.tagContentExtracted) {
if (extractionResult.complete && extractionResult.tagContentExtracted?.trim()) {
// 生成 THINKING_COMPLETE 事件
const thinkingCompleteChunk: ThinkingCompleteChunk = {
type: ChunkType.THINKING_COMPLETE,
@@ -87,15 +95,26 @@ export const ThinkingTagExtractionMiddleware: CompletionsMiddleware =
if (!hasThinkingContent) {
hasThinkingContent = true
thinkingStartTime = Date.now()
controller.enqueue({
type: ChunkType.THINKING_START
} as ThinkingStartChunk)
}
const thinkingDeltaChunk: ThinkingDeltaChunk = {
type: ChunkType.THINKING_DELTA,
text: extractionResult.content,
thinking_millsec: thinkingStartTime > 0 ? Date.now() - thinkingStartTime : 0
if (extractionResult.content?.trim()) {
const thinkingDeltaChunk: ThinkingDeltaChunk = {
type: ChunkType.THINKING_DELTA,
text: extractionResult.content,
thinking_millsec: thinkingStartTime > 0 ? Date.now() - thinkingStartTime : 0
}
controller.enqueue(thinkingDeltaChunk)
}
controller.enqueue(thinkingDeltaChunk)
} else {
if (isFirstTextChunk) {
controller.enqueue({
type: ChunkType.TEXT_START
})
isFirstTextChunk = false
}
// 发送清理后的文本内容
const cleanTextChunk: TextDeltaChunk = {
...textChunk,
@@ -105,7 +124,7 @@ export const ThinkingTagExtractionMiddleware: CompletionsMiddleware =
}
}
}
} else {
} else if (chunk.type !== ChunkType.TEXT_START) {
// 其他类型的chunk直接传递包括 THINKING_DELTA, THINKING_COMPLETE 等)
controller.enqueue(chunk)
}

View File

@@ -22,7 +22,8 @@ const TOOL_USE_TAG_CONFIG: TagConfig = {
* 1. 从文本流中检测并提取 <tool_use></tool_use> 标签
* 2. 解析工具调用信息并转换为 ToolUseResponse 格式
* 3. 生成 MCP_TOOL_CREATED chunk 供 McpToolChunkMiddleware 处理
* 4. 清理文本流,移除工具使用标签但保留正常文本
* 4. 丢弃 tool_use 之后的所有内容(助手幻觉)
* 5. 清理文本流,移除工具使用标签但保留正常文本
*
* 注意:此中间件只负责提取和转换,实际工具调用由 McpToolChunkMiddleware 处理
*/
@@ -32,13 +33,10 @@ export const ToolUseExtractionMiddleware: CompletionsMiddleware =
async (ctx: CompletionsContext, params: CompletionsParams): Promise<CompletionsResult> => {
const mcpTools = params.mcpTools || []
// 如果没有工具,直接调用下一个中间件
if (!mcpTools || mcpTools.length === 0) return next(ctx, params)
// 调用下游中间件
const result = await next(ctx, params)
// 响应后处理:处理工具使用标签提取
if (result.stream) {
const resultFromUpstream = result.stream as ReadableStream<GenericChunk>
@@ -60,7 +58,9 @@ function createToolUseExtractionTransform(
_ctx: CompletionsContext,
mcpTools: MCPTool[]
): TransformStream<GenericChunk, GenericChunk> {
const tagExtractor = new TagExtractor(TOOL_USE_TAG_CONFIG)
const toolUseExtractor = new TagExtractor(TOOL_USE_TAG_CONFIG)
let hasAnyToolUse = false
let toolCounter = 0
return new TransformStream({
async transform(chunk: GenericChunk, controller) {
@@ -68,30 +68,38 @@ function createToolUseExtractionTransform(
// 处理文本内容,检测工具使用标签
if (chunk.type === ChunkType.TEXT_DELTA) {
const textChunk = chunk as TextDeltaChunk
const extractionResults = tagExtractor.processText(textChunk.text)
for (const result of extractionResults) {
// 处理 tool_use 标签
const toolUseResults = toolUseExtractor.processText(textChunk.text)
for (const result of toolUseResults) {
if (result.complete && result.tagContentExtracted) {
// 提取到完整的工具使用内容,解析并转换为 SDK ToolCall 格式
const toolUseResponses = parseToolUse(result.tagContentExtracted, mcpTools)
const toolUseResponses = parseToolUse(result.tagContentExtracted, mcpTools, toolCounter)
toolCounter += toolUseResponses.length
if (toolUseResponses.length > 0) {
// 生成 MCP_TOOL_CREATED chunk复用现有的处理流程
controller.enqueue({ type: ChunkType.TEXT_COMPLETE, text: '' })
// 生成 MCP_TOOL_CREATED chunk
const mcpToolCreatedChunk: MCPToolCreatedChunk = {
type: ChunkType.MCP_TOOL_CREATED,
tool_use_responses: toolUseResponses
}
controller.enqueue(mcpToolCreatedChunk)
// 标记已有工具调用
hasAnyToolUse = true
}
} else if (!result.isTagContent && result.content) {
// 发送标签外的正常文本内容
const cleanTextChunk: TextDeltaChunk = {
...textChunk,
text: result.content
if (!hasAnyToolUse) {
const cleanTextChunk: TextDeltaChunk = {
...textChunk,
text: result.content
}
controller.enqueue(cleanTextChunk)
}
controller.enqueue(cleanTextChunk)
}
// 注意标签内的内容不会作为TEXT_DELTA转发,避免重复显示
// tool_use 标签内的内容不转发,避免重复显示
}
return
}
@@ -105,16 +113,17 @@ function createToolUseExtractionTransform(
},
async flush(controller) {
// 检查是否有未完成的标签内容
const finalResult = tagExtractor.finalize()
if (finalResult && finalResult.tagContentExtracted) {
const toolUseResponses = parseToolUse(finalResult.tagContentExtracted, mcpTools)
// 检查是否有未完成的 tool_use 标签内容
const finalToolUseResult = toolUseExtractor.finalize()
if (finalToolUseResult && finalToolUseResult.tagContentExtracted) {
const toolUseResponses = parseToolUse(finalToolUseResult.tagContentExtracted, mcpTools, toolCounter)
if (toolUseResponses.length > 0) {
const mcpToolCreatedChunk: MCPToolCreatedChunk = {
type: ChunkType.MCP_TOOL_CREATED,
tool_use_responses: toolUseResponses
}
controller.enqueue(mcpToolCreatedChunk)
hasAnyToolUse = true
}
}
}

View File

@@ -1,13 +1,13 @@
@font-face {
font-family: 'Twemoji Country Flags';
unicode-range:
U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F;
/*https://github.com/beyondkmp/country-flag-emoji-polyfill/blob/master/font/TwemojiCountryFlags.woff2 */
src: url('TwemojiCountryFlags.woff2') format('woff2');
font-display: swap;
}
/* 国旗字体样式类 */
.country-flag-font {
font-family: 'Twemoji Country Flags', 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
}
@font-face {
font-family: 'Twemoji Country Flags';
unicode-range:
U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F;
/*https://github.com/beyondkmp/country-flag-emoji-polyfill/blob/master/font/TwemojiCountryFlags.woff2 */
src: url('TwemojiCountryFlags.woff2') format('woff2');
font-display: swap;
}
/* 国旗字体样式类 */
.country-flag-font {
font-family: 'Twemoji Country Flags', 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -25,7 +25,6 @@
}
.minapp-drawer {
max-width: calc(100vw - var(--sidebar-width));
.ant-drawer-content-wrapper {
box-shadow: none;
}
@@ -33,7 +32,7 @@
position: absolute;
-webkit-app-region: drag;
min-height: calc(var(--navbar-height) + 0.5px);
width: calc(100vw - var(--sidebar-width));
width: 100%;
margin-top: -0.5px;
border-bottom: none;
}

View File

@@ -29,7 +29,7 @@
--color-text-secondary: rgba(235, 235, 245, 0.7);
--color-icon: #ffffff99;
--color-icon-white: #ffffff;
--color-border: #ffffff19;
--color-border: #383838;
--color-border-soft: #ffffff10;
--color-border-mute: #ffffff05;
--color-error: #f44336;
@@ -44,7 +44,7 @@
--color-reference-text: #ffffff;
--color-reference-background: #0b0e12;
--color-list-item: #222;
--color-list-item: #252525;
--color-list-item-hover: #1e1e1e;
--modal-background: #111111;
@@ -54,24 +54,17 @@
--color-background-highlight-accent: rgba(255, 150, 50, 0.9);
--navbar-background-mac: rgba(20, 20, 20, 0.55);
--navbar-background-win: rgba(20, 20, 20, 0.75);
--navbar-background: #1f1f1f;
--navbar-height: 40px;
--sidebar-width: 50px;
--status-bar-height: 40px;
--input-bar-height: 100px;
--assistants-width: 275px;
--topic-list-width: 275px;
--settings-width: 250px;
--scrollbar-width: 5px;
--chat-background: transparent;
--chat-background-user: rgba(255, 255, 255, 0.08);
--chat-background-assistant: transparent;
--chat-text-user: var(--color-black);
--list-item-border-radius: 20px;
--color-status-success: #52c41a;
--color-status-error: #ff4d4f;
--color-status-warning: #faad14;
}
[theme-mode='light'] {
@@ -120,8 +113,8 @@
--color-reference-text: #000000;
--color-reference-background: #f1f7ff;
--color-list-item: #eee;
--color-list-item-hover: #f5f5f5;
--color-list-item: rgba(0, 0, 0, 0.05);
--color-list-item-hover: rgba(0, 0, 0, 0.03);
--modal-background: var(--color-white);
@@ -130,6 +123,7 @@
--color-background-highlight-accent: rgba(255, 150, 50, 0.5);
--navbar-background-mac: rgba(255, 255, 255, 0.55);
--navbar-background-win: rgba(255, 255, 255, 0.75);
--navbar-background: rgba(244, 244, 244);
--chat-background: transparent;

View File

@@ -1,6 +1,14 @@
#content-container {
background-color: var(--color-background);
border-top: 0.5px solid var(--color-border);
border-top-left-radius: 10px;
border-left: 0.5px solid var(--color-border);
}
.group-container {
.context-menu-container {
width: 100%;
}
}
.context-menu-container {
max-width: 100%;
}

View File

@@ -1,20 +1,20 @@
:root {
--font-family:
Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen, Cantarell, 'Open Sans',
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--font-family-serif:
serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Ubuntu, Roboto, Oxygen, Cantarell, 'Open Sans',
'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--code-font-family: 'Cascadia Code', 'Fira Code', 'Consolas', Menlo, Courier, monospace;
}
// Windows系统专用字体配置
body[os='windows'] {
--font-family:
'Twemoji Country Flags', Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen,
Cantarell, 'Open Sans', 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}
:root {
--font-family:
Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen, Cantarell, 'Open Sans',
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--font-family-serif:
serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Ubuntu, Roboto, Oxygen, Cantarell, 'Open Sans',
'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--code-font-family: 'Cascadia Code', 'Fira Code', 'Consolas', Menlo, Courier, monospace;
}
// Windows系统专用字体配置
body[os='windows'] {
--font-family:
'Twemoji Country Flags', Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen,
Cantarell, 'Open Sans', 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}

View File

@@ -1,3 +1,4 @@
@use './variables.scss';
@use './color.scss';
@use './font.scss';
@use './markdown.scss';
@@ -139,7 +140,7 @@ ul {
}
}
.message-content-container {
border-radius: 10px 0 10px 10px;
border-radius: 10px;
padding: 10px 16px 10px 16px;
background-color: var(--chat-background-user);
align-self: self-end;

View File

@@ -19,12 +19,14 @@
h4,
h5,
h6 {
margin: 1em 0 1em 0;
margin: 1.5em 0 1em 0;
line-height: 1.3;
font-weight: bold;
font-family: var(--font-family);
}
h1 {
margin-top: 0;
font-size: 2em;
border-bottom: 0.5px solid var(--color-border);
padding-bottom: 0.3em;
@@ -53,8 +55,9 @@
}
p {
margin: 1em 0;
margin: 1.3em 0;
white-space: pre-wrap;
line-height: 1.6;
&:last-child {
margin-bottom: 5px;
@@ -82,7 +85,7 @@
li {
margin-bottom: 0.5em;
pre {
margin: 1.5em 0;
margin: 1.5em 0 !important;
}
&::marker {
color: var(--color-text-3);
@@ -108,6 +111,7 @@
li code {
background: var(--color-background-mute);
padding: 3px 5px;
margin: 0 2px;
border-radius: 5px;
word-break: keep-all;
white-space: pre;
@@ -122,9 +126,7 @@
overflow-x: auto;
font-family: 'Fira Code', 'Courier New', Courier, monospace;
background-color: var(--color-background-mute);
&:has(.mermaid),
&:has(.plantuml-preview),
&:has(.svg-preview) {
&:has(.special-preview) {
background-color: transparent;
}
&:not(pre pre) {
@@ -148,16 +150,19 @@
}
blockquote {
margin: 1em 0;
padding-left: 1em;
color: var(--color-text-light);
border-left: 4px solid var(--color-border);
font-family: var(--font-family);
margin: 1.5em 0;
padding: 1em 1.5em;
background-color: var(--color-background-soft);
border-left: 4px solid var(--color-primary);
border-radius: 0 8px 8px 0;
font-style: italic;
position: relative;
}
table {
--table-border-radius: 8px;
margin: 1em 0;
margin: 2em 0;
font-size: 0.9em;
width: 100%;
border-radius: var(--table-border-radius);
overflow: hidden;
@@ -182,13 +187,19 @@
th {
background-color: var(--color-background-mute);
font-weight: bold;
font-weight: 600;
font-family: var(--font-family);
text-align: left;
}
tr:hover {
background-color: var(--color-background-soft);
}
img {
max-width: 100%;
height: auto;
margin: 10px 0;
}
a,
@@ -323,6 +334,13 @@ mjx-container {
margin-top: -2px;
}
/* Shiki 相关样式 */
.shiki {
font-family: var(--code-font-family);
// 保持行高为初始值,在 shiki 代码块中处理
line-height: initial;
}
/* CodeMirror 相关样式 */
.cm-editor {
border-radius: inherit;

View File

@@ -1,11 +1,16 @@
:root {
--color-scrollbar-thumb: rgba(255, 255, 255, 0.15);
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.2);
--color-scrollbar-thumb-dark: rgba(255, 255, 255, 0.15);
--color-scrollbar-thumb-dark-hover: rgba(255, 255, 255, 0.2);
--color-scrollbar-thumb-light: rgba(0, 0, 0, 0.15);
--color-scrollbar-thumb-light-hover: rgba(0, 0, 0, 0.2);
--color-scrollbar-thumb: var(--color-scrollbar-thumb-dark);
--color-scrollbar-thumb-hover: var(--color-scrollbar-thumb-dark-hover);
}
body[theme-mode='light'] {
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.2);
--color-scrollbar-thumb: var(--color-scrollbar-thumb-light);
--color-scrollbar-thumb-hover: var(--color-scrollbar-thumb-light-hover);
}
/* 全局初始化滚动条样式 */
@@ -34,3 +39,21 @@ pre:not(.shiki)::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
}
}
.shiki-dark {
--color-scrollbar-thumb: var(--color-scrollbar-thumb-dark);
--color-scrollbar-thumb-hover: var(--color-scrollbar-thumb-dark-hover);
}
.shiki-light {
--color-scrollbar-thumb: var(--color-scrollbar-thumb-light);
--color-scrollbar-thumb-hover: var(--color-scrollbar-thumb-light-hover);
}
/* 用于截图时隐藏滚动条
* FIXME: 临时方案,因为 html-to-image 没有正确处理伪元素。
*/
.hide-scrollbar,
.hide-scrollbar * {
scrollbar-width: none !important;
}

View File

@@ -0,0 +1,18 @@
:root {
--navbar-height: 42px;
--sidebar-width: 50px;
--status-bar-height: 40px;
--input-bar-height: 100px;
--assistants-width: 275px;
--topic-list-width: 275px;
--settings-width: 250px;
--scrollbar-width: 5px;
--list-item-border-radius: 15px;
--border-width: 0.5px;
--main-height: 100vh;
--border-width: 0.5px;
}

Some files were not shown because too many files have changed in this diff Show More