Compare commits

...

388 Commits

Author SHA1 Message Date
kangfenmao
31ee7a2e9a chore(version): 1.5.0 2025-07-16 17:55:45 +08:00
one
f84509c824 chore: update check-i18n scripts and remove duplicate keys (#8203) 2025-07-16 17:44:07 +08:00
⌞L⌝
f0d86cbaec feat: add support for 302AI provider in MCP settings (#7755)
* feat: add support for 302AI provider in MCP settings

- Introduced new provider for 302AI, including token management and server synchronization functionality.
- Updated SyncServersPopup to integrate 302AI provider.
- Added new file for 302AI provider utilities, including token storage and server fetching logic.

* fix: re-merge main
2025-07-16 17:40:30 +08:00
fullex
3132150fb8 Revert "feat: optimize minapp cache with LRU (#8160)" (#8205)
This reverts commit f0043b4be5.
2025-07-16 17:34:50 +08:00
自由的世界人
24f7bac3ea refactor: custom mini app loading logic (#8181)
* refactor: custom mini app loading logic

Replaces try-catch with an explicit file existence check before reading 'custom-minapps.json'. Ensures the file is created with an empty array if it does not exist, improving clarity and error handling.

* refactor: custom mini app loading logic

Simplifies the loading of custom mini apps by removing the explicit file existence check and handling the read failure case directly. If reading the file fails, an empty array is written and returned.

* fix: improve error handling in file reading for custom mini apps
2025-07-16 15:36:20 +08:00
SuYao
0930201e5d Fix/mcp bug (#8189)
* feat(models): enhance function calling model detection and update migration logic

- Added support for 'gemini-1' in FUNCTION_CALLING_EXCLUDED_MODELS.
- Updated isFunctionCallingModel to handle optional model input.
- Modified migration logic to change tool use mode for assistants using function calling models.

* feat(models): add new models to vision and function calling lists

- Added 'kimi-thinking-preview' to visionAllowedModels.
- Added 'kimi-k2' to FUNCTION_CALLING_MODELS.
- Updated migration logic to ensure compatibility with new model settings.

* refactor(TextChunkMiddleware): streamline text accumulation logic and improve response handling

- Simplified the logic for accumulating text content and updating the internal state.
- Ensured that the final text is consistently used in response callbacks.
- Removed redundant code for handling text completion in the ToolUseExtractionMiddleware.
- Added mock state for MCP tools in tests to enhance coverage for tool use extraction.

* refactor(BaseApiClient): remove unused content extraction utility

- Replaced the usage of getContentWithTools with getMainTextContent in the getMessageContent method.
- Cleaned up imports by removing the unused getContentWithTools function.
2025-07-16 15:04:19 +08:00
one
df218ee6c8 hotfix: error on deleting assistant (#8190)
fix: error on deleting assistant
2025-07-16 14:16:08 +08:00
happyZYM
27c39415c2 fix: add compatibility for webdav servers that do not support streaming (#7992)
* fix: add compatibility for webdav servers that do not support streaming

* fix: fix grammar error

* fix: fix linter error

* fix: remove unnecessary changes

* revert: restore tolerance for failing to remove temp file after webdav backup failed

* fix: add migration support
2025-07-16 09:53:51 +08:00
luoxu1314
f155b98a92 feat(MCPService):Add notification handlers and clear cache for MCPService (#8179)
* feat(MCPService):Add notification handlers MCPService

添加了MCPService 接收通知更新对应list和其他追踪

* Update MCPService.ts:合并清理缓存
2025-07-16 09:39:10 +08:00
Konv Suu
f0043b4be5 feat: optimize minapp cache with LRU (#8160) 2025-07-15 22:56:34 +08:00
自由的世界人
a6db53873a fix: add channel property to notifications for backup and assistant messages (#8120)
* fix: add channel property to notifications for backup and assistant messages

* Add notification tip and improve assistant notification logic

Added a tooltip in the notification settings UI to clarify that only messages exceeding 30 seconds will trigger a reminder. Updated i18n files for all supported languages with the new tip. Modified notification logic to only send notifications for assistant responses or errors if the message duration exceeds 30 seconds and the user is not on the home page or the window is not focused.

* Remove duplicate InfoCircleOutlined import

Consolidated the import of InfoCircleOutlined from '@ant-design/icons' to avoid redundancy in GeneralSettings.tsx.

* fix: add isFocused mock and simplify createMockStore

Added a mock for isFocused in the window utility and refactored createMockStore to return the configured store directly. This improves test setup clarity and ensures all necessary window utilities are mocked.
2025-07-15 19:25:55 +08:00
fullex
397965f6e9 fix(WindowService): miniWindow should show in current screen (#8132)
feat(WindowService): enhance mini window behavior and resizing logic

- Introduced dynamic resizing for the mini window based on user adjustments.
- Implemented positioning logic to ensure the mini window appears centered on the cursor's screen.
- Added opacity handling to improve user experience during window state changes.
- Refactored mini window creation to utilize predefined size constants for better maintainability.
2025-07-15 18:10:30 +08:00
SuYao
76de357cbf test: add integration test for message thunk and fix some bugs (#8148)
* test: add integration test for message thunk and fix some bugs

* fix: ci
2025-07-15 15:39:40 +08:00
SuYao
40724ad877 fix(AihubmixAPIClient): enhance ID validation logic to exclude 'embed… (#8157)
fix(AihubmixAPIClient): enhance ID validation logic to exclude 'embedding' (#8148)
2025-07-15 14:08:38 +08:00
kangfenmao
be6ecbe0b1 refactor(SettingsPage): Remove redundant menu items and reorganize memory settings link 2025-07-15 12:53:01 +08:00
LiuVaayne
72ae105166 [1.5.0-rc] Feat/memory (#7689)
* Merge memory into main

* Improvement/memory UI (#7655)

* feat: add auto-dimension detection to memory settings

- Add automatic embedding dimension detection for memory configuration
- Add toggle switch to enable/disable auto-detection (enabled by default)
- Detect dimensions by making test API call to embedding provider
- Show dimension input field only when auto-detection is disabled
- Add loading state and error handling during dimension detection
- Maintain consistency with knowledge base dimension handling

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

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

*  feat: implement unified embedding dimensions for memory service

- Add jaison dependency for robust JSON parsing
- Normalize all embeddings to 1536 dimensions for consistency
- Improve embedding dimension logging
- Update memory processor to use jaison for better error handling
- Handle various JSON response formats in fact extraction

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

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

* feat: refactor MemoriesPage layout with new styled components and improved user management features

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Improvement/memory UI (#7656)

Co-authored-by: Claude <noreply@anthropic.com>

*  feat: add memory icon to sidebar for existing users

- Add migration version 118 to enable memory feature visibility
- Adds 'memory' icon to sidebar visible icons if not already present
- Updates store version to trigger migration for existing users

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

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

* fix(memory): include last user message ID in processor config

*  feat(memory): enhance memory settings UI and add new translations

* Enhance memory management UI: Added settings, statistics, search, actions, and user management sections to the memory page. Updated translations for multiple languages to include new UI elements. Refactored component structure for improved layout and readability.

* feat: add i18n

* ui: Enhance memory modals and UI

* refactor(memory): replace direct message calls with window.message for error and success notifications

* fix: eslint error

* feat(memory): enhance memory restoration logic and queries

- Updated MemoryService to restore deleted memories instead of inserting new ones if a memory with the same hash exists.
- Added new SQL queries to check for deleted memories and restore them.
- Improved logging for memory restoration and embedding generation.
- Refactored related API service methods to handle updated memory processing logic.

* refactor: update memory configuration to use ApiClient structure

- Refactored memory-related services and components to utilize the new ApiClient structure for embedding and reranking models.
- Updated constructors and method signatures across multiple files to accept embedApiClient and rerankApiClient parameters.
- Enhanced memory settings UI to reflect changes in memory configuration management.
- Improved type definitions for KnowledgeBaseParams and MemoryConfig to align with the new structure.

* ui: improve user interface for adding new users in memory page

- Enhanced the button for adding new users by incorporating an icon and adjusting padding for better alignment.
- Updated the user selection options to ensure consistent alignment of avatars and user names.
- Refactored layout to improve overall user experience and visual consistency.

* refactor(memory): streamline MemoryProcessor usage in ApiService

- Removed the singleton instance of MemoryProcessor and instantiated it directly within the ApiService methods.
- Updated relevant methods to utilize the new instance for searching and processing memories, improving clarity and encapsulation of memory handling logic.

* chore: move knowledge dir

* fix: correct import paths in KnowledgeService.ts

* fix(Memory): memory deduplicate

* fix(Memory): memory llm provider

* fix: ci error

* fix(Memory): update fact extraction prompt to focus on personal information

* feat: Refactor memory fom sidebar to settings page

- Removed MemoryStick icon from Sidebar component.
- Updated navigation to point to the new memory settings page.
- Introduced MemoriesSettingsModal for managing memory configurations.
- Created MemorySettings component for comprehensive memory management.
- Added user management features including adding, editing, and deleting users.
- Implemented pagination and search functionality for memory items.
- Updated sidebar settings to remove memory icon and ensure proper migration.
- Adjusted Redux store settings to reflect changes in sidebar icons.

* feat: redesign memory settings page with improved UI and layout

* fix i18n

* fix: update citation titles to include memory hash and increment version number

* fix: remove unnecessary prop from KnowledgeCitation component

* feat: enhance fact extraction prompt with clearer guidelines and examples

* 🔧 feat: disable global memory by default and improve UI

- Set globalMemoryEnabled default to false for better user experience
- Remove manual localStorage handling to rely on redux-persist
- Add Beta badge to memory settings section
- Improve layout and styling of memory settings UI components

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

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

* Simplify external tool completion handling

* Fix whitespace in migrate config

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: suyao <sy20010504@gmail.com>
Co-authored-by: eeee0717 <chentao020717Work@outlook.com>
Co-authored-by: kangfenmao <kangfenmao@qq.com>
Co-authored-by: 自由的世界人 <3196812536@qq.com>
2025-07-15 10:24:41 +08:00
SuYao
06baaa1522 fix: openai api client (#8154) 2025-07-15 10:10:55 +08:00
SuYao
fa17c70d85 chore: update .gitignore to include .claude-code-router directory (#8156) 2025-07-15 09:13:49 +08:00
fullex
c606972f0a fix: global shortcut keys (#8084)
* refactor: shortcut keys

* fix:  backward compatibility with old data
2025-07-15 02:23:39 +08:00
luoxu1314
d4dde58e13 fix(OpenAIResponseAPIClient):ensure openai-response providers always use Response API (#8145)
Update OpenAIResponseAPIClient.ts
2025-07-14 23:57:16 +08:00
ous50 | ousfifty | 欧式fifty
71917eb0ec Feat: url context for Gemini models (#7931)
* feat: Add URL Context ability for Gemini Models

* feat: Adding URL Context Button to tool bar and make it visible only when gemini models selected.
It is not working (adding urlContext tools) for now.

* fix: trying to force enable UrlContext function

* fix: enableUrlContext indication reverted

* feat: migration script for refreshing tool order to add URL Context button.

* fix: optimize migrate.ts

* fix: upgrade version

---------

Co-authored-by: suyao <sy20010504@gmail.com>
2025-07-14 23:52:19 +08:00
luoxu1314
1b129636ed chore(OpenAIApiClient): fallback to message when delta.content is empty (#8101)
* chore(OpenAIApiClient): fallback to message when delta.content is empty, fix missing content issue

Signed-off-by: luoxu1314 <xiaoluoxu@163.com>

* Update OpenAIApiClient.ts

* Update OpenAIApiClient.ts

---------

Signed-off-by: luoxu1314 <xiaoluoxu@163.com>
Co-authored-by: one <wangan.cs@gmail.com>
2025-07-14 23:28:27 +08:00
Phantom
c2d438fba3 fix(openai): add compatibility mode for handling tool call responses (#7983)
fix(openai): 添加兼容模式处理工具调用响应

在兼容模式下处理工具调用响应时,添加对数组内容的特殊处理逻辑。当isCompatibleMode为true时,将响应内容转换为特定格式的字符串输出,包括对文本、图片和音频等不同类型内容的处理。
2025-07-14 22:44:51 +08:00
LiuVaayne
ee4553130b [1.5.0-rc] feat(MCP): Add DXT format support for MCP server installation (#7618)
* feat(MCP): Add DXT format support for MCP server installation

- Add comprehensive DXT package upload and extraction functionality
- Support for DXT manifest validation and MCP server configuration
- Hierarchical UI structure: Quick Add | JSON Import | DXT Import
- Variable substitution for DXT args (${__dirname} replacement)
- Automatic cleanup of DXT server directories on removal
- Enhanced error handling and connectivity checks
- Full internationalization support (EN/CN)
- Uses existing node-stream-zip for efficient extraction
- Proper working directory setup for DXT-based servers

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

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

* 🐛 fix(MCP): Fix DXT server installation and deletion issues

- Replace fs.renameSync with cross-filesystem compatible moveDirectory method to handle temp->mcp directory moves across different mount points
- Add recursive copy fallback when rename fails (ENOENT error fix)
- Sanitize server names with slashes to prevent subdirectory creation during installation
- Improve cleanupDxtServer to handle sanitized names and provide fallback lookup
- Add proper error logging and directory existence warnings

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

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

* feat(MCP): Implement comprehensive DXT MCP configuration support

- Add platform_overrides support to DXT manifest interface
- Implement complete variable substitution system (${__dirname}, ${HOME}, ${DESKTOP}, ${DOCUMENTS}, ${pathSeparator}, ${user_config.KEY})
- Add platform detection utilities (getPlatformIdentifier)
- Create resolved MCP configuration system with applyPlatformOverrides
- Export ResolvedMcpConfig interface and utility functions
- Integrate DXT configuration resolution into MCPService runtime
- Support platform-specific command, args, and environment overrides
- Add comprehensive logging for configuration resolution

Addresses DXT MANIFEST.md mcp_configuration requirements:
- Platform-specific configuration variations
- Cross-platform variable substitution
- Flexible command and environment management

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

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

* feat: add downloads directory variable substitution and simplify platform detection

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-14 22:37:56 +08:00
Jason Young
bf6ccea1e2 test: add unit tests for getPotentialIndex and input utils (#7947)
* test: add unit tests for getPotentialIndex and input utils

- Add tests for getPotentialIndex function covering streaming text tag detection scenarios
- Add tests for input utils including file drop and keyboard shortcut detection

* test: refactor test structure to comply with TEST_UTILS.md guidelines

- Add file-level describe blocks for both test files
- Fix mock cleanup in input.test.ts:
  - Add vi.clearAllMocks() in beforeEach
  - Replace vi.clearAllMocks() with vi.restoreAllMocks() in afterEach
- Maintain two-layer describe structure as per project standards
2025-07-14 21:52:52 +08:00
karl
e0eac6ab7e Fix/7973 (#8059)
* fix: 7973 查看原始数据的按钮没有了

* refactor(MessageTools): replace PreviewBlock with CollapsedContent for improved preview rendering

---------

Co-authored-by: suyao <sy20010504@gmail.com>
2025-07-14 21:50:55 +08:00
Teo
094eb5c17e refactor(ThinkingEffect): Enhance thinking effect (#8147)
refactor(ThinkingEffect): simplify opacity calculation and enhance background styling
2025-07-14 20:47:34 +08:00
SuYao
3d3182095d fix: enhance OpenAIResponseAPIClient for Azure API version (#8108)
* feat: enhance OpenAIResponseAPIClient and update localization for Azure API version

- Added a new method `formatApiHost` in OpenAIResponseAPIClient to ensure correct API host formatting.
- Updated localization files for English, Japanese, Russian, and Chinese to include tips for Azure OpenAI API version usage.
- Modified ProviderSetting component to display the new Azure API version tip in the settings interface.

* chore: clean log
2025-07-14 20:15:14 +08:00
EastlingWoo
706f8e1482 Fix: message tool button cannot click on grid mode (#8123)
* Enable pointer events for grid popover message

* Enable pointer events for grid popover message
2025-07-14 15:15:02 +08:00
fullex
ea7e07034a fix: serif font in markdown title styles (#8129)
refactor: update font-family usage in markdown styles
2025-07-14 14:51:01 +08:00
Phantom
1fd92d6a5d feat(constant): add .fxml file extension (#8125)
feat(常量配置): 添加对JavaFX XML文件扩展名.fxml的支持
2025-07-14 14:49:40 +08:00
cnJasonZ
810ebad9ba Fix/ppio links (#8131)
* fix: fix ppio oauth links

* fix: fix ppio links

---------

Co-authored-by: one <wangan.cs@gmail.com>
2025-07-14 13:45:15 +08:00
one
c6554c8f80 perf: prevent unnecessary topic rerendering (#8116) 2025-07-14 13:43:01 +08:00
SuYao
19a8d9e9b3 fix(OpenAIApiClient): refine grok4 check for OpenRouter (#8074)
fix(OpenAIApiClient): refine model ID check for reasoning effort
2025-07-14 13:11:02 +08:00
kangfenmao
a490287b4a refactor: streamline Vite configuration and enhance CitationsList component
- Updated Vite configuration to remove conditional output settings, ensuring consistent build behavior.
- Refactored CitationsList component to use a Scrollbar for improved UI, encapsulating popover content within a styled container.
2025-07-14 12:57:32 +08:00
Konv Suu
90b0c91b2f fix: set source language when checking history item (#8130)
feat: set source language when checking history item
2025-07-14 12:19:35 +08:00
Phantom
1493132974 fix: cannot paste images when mentioned visual models (#7817)
* refactor(paste): 优化粘贴功能逻辑,移除模型类型依赖

重构粘贴服务处理逻辑,将文件类型支持判断移至组件层
简化handlePaste接口,移除isVisionModel和isGenerateImageModel参数

* fix(MessageEditor): 支持生成图片模型的视觉消息检查

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

* refactor(PasteService): 移除调试用的console.log语句
2025-07-14 11:41:54 +08:00
kangfenmao
6a4468193b refactor(vite): set legalComments none in prod mode 2025-07-14 10:51:36 +08:00
SuYao
4dd99b5240 feat: enhance Anthropic and OpenAI API clients with incremental output support (#8104)
- Added support for incremental output in AnthropicAPIClient by introducing TEXT_START and THINKING_START chunk types.
- Updated OpenAIAPIClient to conditionally enable incremental output for specific models.
- Modified messageThunk to handle updated smartBlockUpdate calls with an isComplete parameter for better state management.
- Introduced incremental_output parameter in ReasoningEffortOptionalParams type for enhanced configuration options.
2025-07-14 10:30:51 +08:00
Teo
7961ba87ed feat: thinking effect (#8081)
* feat(i18n): add smooth stream output translations for multiple languages

* feat(ThinkingBlock): integrate MarqueeComponent for enhanced message display

* refactor(i18n): remove smooth stream output references from translations and components

* refactor(typingOutput): enhance typing output logic and add debugging information

* refactor(Markdown): consolidate markdown utility imports for cleaner code

* feat(styles): add new styles for dropdown menus, popovers, and modals

* test(ThinkingBlock): enhance tests for streaming status and content collapse behavior

* refactor(typingOutput): remove debugging console log from outputNextChar function

* refactor(MarqueeComponent): comment out blur effect for last marquee item and adjust ThinkingBlock margin

* style(ThinkingBlock): update snapshot to include margin-top for improved layout

* refactor(typingOutput): 修改流式输出逻辑以支持队列长度检查

* refactor(Markdown): simplify useTypingOutput by removing isStreaming parameter

* test(Markdown): comment out re-render tests for content changes

* test(Markdown): remove commented-out re-render tests for content changes

* feat(ThinkingEffect): implement ThinkingEffect component for dynamic message display

- Introduced ThinkingEffect component to enhance the visual representation of thinking states.
- Integrated the new component into ThinkingBlock, replacing MarqueeComponent for improved functionality.
- Added animations and dynamic height adjustments based on message content and expansion state.

* test(ThinkingBlock): update mocks for ThinkingEffect and motion components in tests

* fix: Delete unnecessary comments
2025-07-14 10:08:09 +08:00
one
6952bea6e1 fix: table resizing in mcp tool setting (#8057)
- remove sticky headers
- add missing i18n keys
2025-07-13 23:02:13 +08:00
Caelan
53600175b9 Feature/dmxapi painting add model (#7851)
* 新增图片模型

* 新增图片生成模型

* 新增模型和调整提示语
2025-07-13 21:59:35 +08:00
Konv Suu
e5956d4039 feat: improve translate history style (#8060) 2025-07-13 21:11:15 +08:00
one
1f9850c04d chore(gitignore): exclude more AI editor settings (#8102) 2025-07-13 21:07:45 +08:00
one
df43cb7a90 fix(Knowledge): pass searchResultCount to embed-js (#8118) 2025-07-13 20:22:02 +08:00
kangfenmao
bea664af0f refactor: simplify HtmlArtifactsPopup component and improve preview functionality
- Removed unnecessary extracted components and integrated their logic directly into HtmlArtifactsPopup.
- Enhanced preview functionality with a debounced update mechanism for HTML content.
- Updated styling for better layout and responsiveness, including fullscreen handling.
- Adjusted view mode management for clearer code structure and improved user experience.
2025-07-13 10:18:39 +08:00
kangfenmao
b265c640ca refactor: improve environment variable handling in electron.vite.config.ts
- Introduced `isDev` and `isProd` constants for clearer environment checks.
- Simplified sourcemap and noDiscovery settings based on environment.
- Enhanced esbuild configuration for production to drop console and debugger statements.
2025-07-13 10:18:39 +08:00
beyondkmp
a3d6f32202 fix: replace Select component with custom Selector in LocalBackupSetting (#8055)
* fix: replace Select component with custom Selector in LocalBackupSettings

- Updated LocalBackupSettings to use a custom Selector component for better styling and functionality.
- Enhanced the options for auto-sync interval and max backups with improved structure and internationalization support.
- Added success notification handling in restoreFromLocalBackup function in BackupService for better user feedback.

* refactor: streamline backup service and settings management

- Removed local backup auto-sync functionality and integrated its logic into the general auto-sync mechanism.
- Updated backup service methods to handle specific backup types (webdav, s3, local) more efficiently.
- Renamed backup functions for consistency and clarity.
- Enhanced local backup management in settings to utilize the new auto-sync structure.
- Improved error handling and logging for backup operations.

* refactor: replace Select component with custom Selector in S3Settings

- Updated S3Settings to utilize a custom Selector component for improved styling and functionality.
- Enhanced the options for auto-sync interval and max backups with better structure and internationalization support.
- Removed deprecated Select component to streamline the settings interface.
2025-07-13 07:09:23 +08:00
fullex
16e65d39be fix: [Linux] support Linux Wayland global shortcuts (#8080)
feat: support Linux Wayland global shortcuts
2025-07-13 00:30:01 +08:00
luoxu1314
186bdb486f fix: 修复从未打开过的话题导出markdown为空的问题 (#8103)
* feat: 优化导出功能使用 TopicManager 确保消息正确加载

- 移除对 db 的直接依赖,改用 TopicManager.getTopicMessages
- 修复从未打开过的话题导出为空的问题

Signed-off-by: luoxu1314 <xiaoluoxu@163.com>

* Update export.test.ts

修复相关测试mock使用TopicManager而非db.topics.get,确保测试与新的消息加载方式兼容

* Update export.ts

* Update export.test.ts

完善测试

* style(test): 移除多余空行

---------

Signed-off-by: luoxu1314 <xiaoluoxu@163.com>
Co-authored-by: GeorgeDong32 <georgedong32@qq.com>
2025-07-12 22:45:01 +08:00
fullex
ea40cc7692 fix(install): update return codes for bun and uv installation scripts (#8039) 2025-07-12 22:22:07 +08:00
LiuVaayne
16ca373c55 feat: add MCP server version display with badges (#8097) 2025-07-12 15:35:59 +08:00
cnJasonZ
38cf3869bc fix: ppio oauth links (#8073) 2025-07-11 18:00:35 +08:00
kangfenmao
60e3431b36 chore(version): 1.4.11 2025-07-11 12:18:38 +08:00
kangfenmao
84a6c2da59 feat: add HTML code detection utility and integrate into CodeBlockView
- Introduced `isHtmlCode` function to identify HTML content based on DOCTYPE and tag presence.
- Updated `CodeBlockView` to utilize `isHtmlCode` for conditional rendering of HTML artifacts.
- Added comprehensive tests for `isHtmlCode` to ensure accurate detection of HTML structures.
2025-07-11 12:16:44 +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
kangfenmao
637019b0a8 chore(version): 1.4.8 2025-07-03 23:57:14 +08:00
kangfenmao
e3775b13a6 style: update modal close margin and adjust settings layout
- Added margin to the modal close button for improved spacing.
- Removed unnecessary divider in OCR settings for a cleaner layout.
- Set a minimum width for the search max result title to enhance alignment and readability.
2025-07-03 23:47:53 +08:00
kangfenmao
7fae55863e refactor(llm, migrate): reorganize PH8 provider configuration and migration logic
- Moved the PH8 provider configuration within the INITIAL_PROVIDERS array for better structure.
- Updated migration logic to ensure the PH8 provider is added and positioned correctly in the state during configuration migration.
- Removed redundant code related to provider initialization in the migration process, streamlining the overall logic.
2025-07-03 23:28:18 +08:00
kangfenmao
52d6c372ed fix(i18n): add provider key confirmation messages in multiple languages
- Added new localization strings for provider API key management, including confirmation and error messages for existing keys.
- Updated English, Japanese, Russian, Simplified Chinese, and Traditional Chinese localization files to reflect these changes, enhancing user experience and clarity in API key operations.
2025-07-03 23:16:40 +08:00
kangfenmao
3bced85fc3 refactor(AddKnowledgePopup): streamline settings panel and enhance advanced options
- Removed the left menu and integrated settings directly into the main panel for a more cohesive user experience.
- Introduced a toggle for advanced settings, allowing users to expand or collapse additional configuration options.
- Updated layout and styling for improved usability, including adjustments to padding and margins.
- Enhanced scroll behavior for the advanced settings section to ensure visibility when expanded.
- Minor adjustments to component imports and state management for better performance and clarity.
2025-07-03 23:16:40 +08:00
littleRiceZhou
f163ace86c feat: add PH8 provider support (#7756)
- Introduced PH8 provider with configuration and logo.
- Updated SYSTEM_MODELS to include PH8 models.
- Added PH8 to internationalization files for multiple languages.
- Implemented migration logic to integrate PH8 into the existing provider structure.

Co-authored-by: jack.li <jack.li@enflame-tech.com>
2025-07-03 23:16:25 +08:00
Chen Tao
25d6a1f02f HotFix: QuotaTag 循环调用 (#7788) 2025-07-03 18:22:29 +08:00
SuYao
9847db5c83 HotFix/dexie error (#7778)
* fix(dexieError): initialize database connection before fetching phrases

- Added an `init` method to the `QuickPhraseService` to ensure the Dexie database is opened before retrieving all quick phrases.
- Updated the `getAll` method to call the `init` method, improving reliability in data retrieval.

* fix(QuickPhraseService): ensure database initialization before updating phrases

- Added calls to the `init` method in the `update` and `updateOrder` methods to guarantee the database connection is established before performing updates, enhancing data integrity and reliability.

* fix(QuickPhraseService): prevent multiple database initializations

- Added a static flag to ensure the database initialization occurs only once, preventing redundant calls to the `init` method and improving performance.
2025-07-03 17:48:25 +08:00
fullex
4c353f4eee fix(SelectionAssistant): [macOS] enable AXAPI in Chrome and Electron Apps (#7782)
* feat(SelectionAssistant): add macOS support and process trust handling

- Updated the selection assistant to support macOS, including new IPC channels for process trust verification.
- Enhanced the SelectionService to check for accessibility permissions on macOS before starting the service.
- Added user interface elements to guide macOS users in granting necessary permissions.
- Updated localization files to reflect macOS support and provide relevant user instructions.
- Refactored selection-related configurations to accommodate both Windows and macOS environments.

* feat(SelectionService): update toolbar window settings for macOS and Windows

- Set the toolbar window to be hidden in Mission Control and accept the first mouse click on macOS.
- Adjusted visibility settings for the toolbar window to ensure it appears correctly on all workspaces, including full-screen mode.
- Refactored the MacProcessTrustHintModal component to improve layout and styling of buttons in the modal footer.

* feat(SelectionToolbar): enhance styling and layout of selection toolbar components

* feat(SelectionService): enhance toolbar window settings and refactor position calculation

* feat(SelectionToolbar): update button padding and add last button padding for improved layout

* chore(dependencies): update selection-hook to version 1.0.2 and refine build file exclusions in electron-builder.yml

* feat(SelectionService): center action window on screen when not following toolbar

* fix(SelectionService): implement workaround to prevent other windows from bringing the app to front on macOS when action window is closed

* fix(SelectionService): refine macOS workaround to prevent other windows from bringing the app to front when action window is closed; update selection-toolbar logo padding in styles

* fix(SelectionService): implement macOS toolbar reload to clear hover status; optimize display retrieval logic

* fix(SelectionService): update macOS toolbar hover status handling by sending mouseMove event instead of reloading the window

* chore: update selection-hook dependency to version 1.0.3 in package.json and yarn.lock

* fix(SelectionService): improve toolbar visibility handling on macOS and ensure focusability of other windows when hiding the toolbar

* chore: update selection-hook dependency to version 1.0.4 in package.json and yarn.lock

---------

Co-authored-by: Teo <cheesen.xu@gmail.com>
2025-07-03 17:06:02 +08:00
Tristan Zhang
870f794796 fix(messageThunk): handle missing user message in response creation (#7375)
* fix(messageThunk): handle missing user message in response creation

* fix(i18n): add missing user message translations

* fix(messageThunk): show error popup for missing user message instead of creating error block

* fix(messageThunk): validate askId and show error popup for missing user message

---------

Co-authored-by: suyao <sy20010504@gmail.com>
2025-07-03 17:03:45 +08:00
Chen Tao
e35b4d9cd1 feat(knowledge): support doc2x, mistral, MacOS, MinerU... OCR (#3734)
Co-authored-by: suyao <sy20010504@gmail.com>
Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-07-03 16:23:02 +08:00
SuYao
1afbb30bfc fix(migrate): enable stream output for existing assistants in migrati… (#7772)
fix(migrate): enable stream output for existing assistants in migration process

- Updated the migration logic to set the default streamOutput setting to true for assistants that do not have this property defined, enhancing the user experience by ensuring consistent behavior across all assistants.
2025-07-03 15:26:09 +08:00
fullex
2f016efc50 feat: SelectionAssistant macOS version / 划词助手macOS版 (#7561)
* feat(SelectionAssistant): add macOS support and process trust handling

- Updated the selection assistant to support macOS, including new IPC channels for process trust verification.
- Enhanced the SelectionService to check for accessibility permissions on macOS before starting the service.
- Added user interface elements to guide macOS users in granting necessary permissions.
- Updated localization files to reflect macOS support and provide relevant user instructions.
- Refactored selection-related configurations to accommodate both Windows and macOS environments.

* feat(SelectionService): update toolbar window settings for macOS and Windows

- Set the toolbar window to be hidden in Mission Control and accept the first mouse click on macOS.
- Adjusted visibility settings for the toolbar window to ensure it appears correctly on all workspaces, including full-screen mode.
- Refactored the MacProcessTrustHintModal component to improve layout and styling of buttons in the modal footer.

* feat(SelectionToolbar): enhance styling and layout of selection toolbar components

* feat(SelectionService): enhance toolbar window settings and refactor position calculation

* feat(SelectionToolbar): update button padding and add last button padding for improved layout

* chore(dependencies): update selection-hook to version 1.0.2 and refine build file exclusions in electron-builder.yml

* feat(SelectionService): center action window on screen when not following toolbar

* fix(SelectionService): implement workaround to prevent other windows from bringing the app to front on macOS when action window is closed

* fix(SelectionService): refine macOS workaround to prevent other windows from bringing the app to front when action window is closed; update selection-toolbar logo padding in styles

* fix(SelectionService): implement macOS toolbar reload to clear hover status; optimize display retrieval logic

* fix(SelectionService): update macOS toolbar hover status handling by sending mouseMove event instead of reloading the window

* chore: update selection-hook dependency to version 1.0.3 in package.json and yarn.lock

* fix(SelectionService): improve toolbar visibility handling on macOS and ensure focusability of other windows when hiding the toolbar

---------

Co-authored-by: Teo <cheesen.xu@gmail.com>
2025-07-03 14:31:31 +08:00
one
cd1ef46577 chore: remove dependency updates (#7743) 2025-07-03 14:05:35 +08:00
beyondkmp
c79ea7d5ad fix: cannot move data dir in linux (#7643)
* fix: cannot move data dir in linux

* delete verion info in path

---------

Co-authored-by: beyondkmp <beyondkmp@debian12.beyondkmp.com>
2025-07-03 13:07:13 +08:00
beyondkmp
01fc98b221 fix(AboutSettings): don't throw a notification when switch to the about page (#7688)
refactor(AboutSettings): streamline test channel change handling

- Moved the test channel change logic into a dedicated function to improve clarity and maintainability.
- Removed the useEffect hook that was previously monitoring changes, simplifying the component's structure.
2025-07-03 11:42:02 +08:00
GuanMu
6c0b614208 feat: Add code linting plugin support to the Electron configuration (#7740)
* feat: 添加代码检查插件支持到 Electron 配置中,并更新依赖项

* test: Update snapshots to reflect the latest changes in component rendering

- Updated snapshots for DragableList, Scrollbar, CitationTooltip, Markdown, Table, and ThinkingBlock components by adding new data attributes to support debugging and testing.
- Ensured snapshots are consistent with the latest component rendering, improving test accuracy and reliability.

* test: 更新快照以反映组件渲染的最新变化

- 更新了 DragableList、Scrollbar、CitationTooltip、Markdown、Table 和 ThinkingBlock 组件的快照,移除了多余的数据属性以简化调试和测试。
- 确保快照与最新的组件渲染一致,提高了测试的准确性和可靠性。
2025-07-03 10:36:54 +08:00
beyondkmp
0218bf6c89 refactor(ProviderSettings): add provider key by urlScheme (#7529)
* refactor(ProviderSettings): streamline API key management and enhance user experience

- Refactored the handleProvidersProtocolUrl function to simplify API key handling and improve navigation logic.
- Updated the useProviders hook to maintain consistency in provider management.
- Enhanced the ApiKeyList component with improved state handling and user feedback for API key validation.
- Updated localization files to reflect changes in API key management and user interactions.
- Improved styling and layout for better visual consistency across provider settings.

* fix(ProviderSettings): enhance confirmation modal title with provider name

- Updated the confirmation modal title in the ProvidersList component to include the provider's display name, improving clarity for users during API key management.

* update info

* udpate line

* update line

* feat(Protocol): add custom protocol handling for Cherry Studio

- Introduced a new protocol handler for 'cherrystudio' in the Electron app, allowing the application to respond to custom URL schemes.
- Updated the electron-builder configuration to register the 'cherrystudio' protocol.
- Enhanced the main application logic to handle incoming protocol URLs effectively, improving user experience when launching the app via custom links.

* feat(ProviderSettings): enhance provider data handling with optional fields

- Updated the handleProviderAddKey function to accept optional 'name' and 'type' fields for providers, improving flexibility in provider management.
- Adjusted the API key handling logic to utilize these new fields, ensuring a more comprehensive provider configuration.
- Enhanced the URL schema documentation to reflect the changes in provider data structure.

* delete apikeylist

* restore apiService

* support utf8

* feat(Protocol): improve URL handling for macOS and Windows

- Added caching for the URL received when the app is already running on macOS, ensuring it is processed correctly.
- Updated the URL processing logic in handleProvidersProtocolUrl to replace characters for proper decoding.
- Simplified base64 decoding in ProviderSettings to enhance readability and maintainability.

* fix start in macOS

* format code

* fix(ProviderSettings): validate provider data before adding

- Added validation to ensure 'id', 'newApiKey', and 'baseUrl' are present before proceeding with provider addition.
- Implemented error handling to notify users of invalid data and redirect them to the provider settings page.

* feat(Protocol): enhance URL processing for versioning

- Updated the URL handling logic in handleProvidersProtocolUrl to support versioning by extracting the 'v' parameter.
- Added logging for version 1 to facilitate future enhancements in handling different protocol versions.
- Improved the processing of the 'data' parameter for better compatibility with the updated URL schema.

* feat(i18n): add provider API key management translations for Japanese, Russian, and Traditional Chinese

- Introduced new translations for API key management features, including confirmation prompts and error messages related to provider API keys.
- Enhanced user experience by providing localized strings for adding, updating, and validating API keys across multiple languages.

---------

Co-authored-by: rcadmin <rcadmin@rcadmins-MacBook-Pro-4.local>
2025-07-03 05:10:18 +08:00
one
8355ed2fa5 chore: update i18n script (#7729) 2025-07-02 22:59:18 +08:00
one
c290906bd9 chore: update markdown-related packages (#7745) 2025-07-02 22:33:02 +08:00
自由的世界人
cf9175c408 fix: i18n missing & model select options (#7760) 2025-07-02 21:16:24 +08:00
亢奋猫
575d6fa91b fix: clear cached web search and knowledge references in BaseApiClient (#7759) 2025-07-02 20:51:47 +08:00
Phantom
fb624cc368 chore: Disable auto-organize imports on save (#7744)
chore: 禁用保存时自动整理导入功能
2025-07-02 19:29:08 +08:00
亢奋猫
7ed6e58f8e refactor: new knowledge base ui layout (#7748) 2025-07-02 17:34:19 +08:00
one
38497597b9 fix: migrate version (#7757) 2025-07-02 17:30:36 +08:00
Jason Young
d0ebdf460f test: add tests for DividerWithText and EmojiIcon components (#7747)
* test: add tests for DividerWithText and EmojiIcon components

- Add DividerWithText test covering basic rendering, styling and edge cases
- Add EmojiIcon test for emoji/icon rendering, tooltips and size customization

* test: add snapshot tests for DividerWithText and EmojiIcon components

- 为 DividerWithText 和 EmojiIcon 组件添加快照测试
- 优化测试用例,移除过度测试的 DOM 结构验证
- 增加对 size 和 fontSize props 的样式验证
- 遵循项目测试规范,使用标准的 toMatchSnapshot()

* test: remove duplicate background test in EmojiIcon

移除重复的背景元素测试
2025-07-02 16:29:29 +08:00
亢奋猫
df47b174ca feat(AppUpdater): integrate User-Agent generation for autoUpdater req… (#7751)
* feat(AppUpdater): integrate User-Agent generation for autoUpdater requests; add systemInfo utility module

* feat(systemInfo): enhance macOS version handling using macos-release package for improved accuracy; update package.json and yarn.lock to include macos-release and opendal dependencies
2025-07-02 16:18:44 +08:00
cnJasonZ
561c563bd7 PPIO OAuth Login (#7717)
* feat: integrate PPIO OAuth login support

Add OAuth authentication support for PPIO provider with complete integration:
- Add PPIO OAuth configuration and client ID
- Implement oauthWithPPIO authentication flow
- Add PPIO to OAuth and charge-supported providers list
- Include PPIO logo and UI components for OAuth settings
- Support charge and billing URL redirects for PPIO

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

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

* fix: fix url

* fix: fix redirect url

* feat: add PPIO OAuth login

* fix: migrate

* fix: migrate

* fix: ppio migrate

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-02 15:49:37 +08:00
自由的世界人
d5e8ffc00f fix: add custom prompt setting for translate model (#7623)
* fix: add custom prompt setting for translate model

Introduces a UI section in TranslateSettings to allow users to view and edit the custom prompt for the translation model. The prompt is now saved to the database and can be toggled for display in the settings modal.

* fix: add reset button for translate prompt and improve prompt editing

Introduced a reset button to restore the translate prompt to its default value. Updated the prompt editing area to use local state, improved UI with a rounded Textarea, and ensured prompt changes are dispatched to the store.

* refactor: bidirectional settings layout in TranslatePage

Removed unnecessary margin and conditional wrapper for the bidirectional settings. The Space component is now only rendered when bidirectional mode is enabled, improving layout clarity and reducing extra DOM nesting.

* Update TranslatePage.tsx
2025-07-02 15:23:58 +08:00
亢奋猫
9f29194180 refactor: Restructure the knowledge base directory (#7754)
重构知识库目录结构,代码逻辑完全不变

├── embeddings
│   ├── Embeddings.ts
│   ├── EmbeddingsFactory.ts
│   └── VoyageEmbeddings.ts
├── loader
│   ├── draftsExportLoader.ts
│   ├── epubLoader.ts
│   ├── index.ts
│   ├── noteLoader.ts
│   └── odLoader.ts
└── reranker
    ├── BaseReranker.ts
    ├── GeneralReranker.ts
    └── Reranker.ts

4 directories, 11 files
2025-07-02 15:23:02 +08:00
one
a7abebc8f4 fix: remove opendal (#7753) 2025-07-02 15:03:31 +08:00
kangfenmao
19212e576f Revert "feat: Add S3 Backup (#6802)"
This reverts commit 3f5901766d.

# Conflicts:
#	src/renderer/src/i18n/locales/zh-cn.json
#	src/renderer/src/i18n/locales/zh-tw.json
2025-07-02 13:22:33 +08:00
Phantom
990ec5cd5c fix(MessageMenubar): Add check for empty relatedUserMessageBlocks to prevent errors (#7733)
* fix(MessageMenubar): 修复未找到相关用户消息块时的处理逻辑

添加对relatedUserMessageBlocks为空的检查,避免后续逻辑报错

* fix(MessageMenubar): 修复检查消息块类型时的空引用问题
2025-07-02 11:34:53 +08:00
beyondkmp
4b92a5ef1e chore: update electron dependency to version 35.6.0 in package.json and yarn.lock (#7730) 2025-07-02 10:57:30 +08:00
one
8d9ac7299a chore(ci): update dependabot (#7725) 2025-07-02 10:22:17 +08:00
kangfenmao
6a2e04aaeb Revert "fix(WindowService): remove backgroundThrottling option for cleaner window configuration (#7704)"
This reverts commit 3eb6d08b34.
2025-07-02 10:04:14 +08:00
SuYao
83f36f5e77 refactor(WebSearchMiddleware, linkConverter): enhance link processing and buffering logic (#7724)
- Updated WebSearchMiddleware to utilize the new smartLinkConverter structure, allowing for better handling of buffered content and fallback logic.
- Introduced flushLinkConverterBuffer function to clear remaining buffered content at stream end.
- Modified convertLinks and smartLinkConverter functions to return structured results indicating whether content was buffered.
- Enhanced unit tests to cover new functionality and edge cases for link conversion and buffering behavior.
2025-07-02 03:03:03 +08:00
Jason Young
f58378daa0 test: add comprehensive tests for CopyButton component (#7719)
* test: add comprehensive tests for CopyButton component

- Add tests for basic rendering and functionality
- Add clipboard API mocking and error handling
- Add tests for custom props (size, tooltip, label)
- Add edge case testing (empty text, special characters)
- Improve component test coverage

Signed-off-by: Jason Young <farion1231@gmail.com>

* fix: resolve linting issues in CopyButton tests

- Sort imports alphabetically
- Remove trailing whitespace
- Add final newline

Signed-off-by: Jason Young <farion1231@gmail.com>

* refactor: consolidate similar test cases in CopyButton tests

- Merge 'should render copy icon' and 'should render with basic structure'
- Merge 'should apply custom size to icon' and 'should apply custom size to label'
- Reduce test duplication while maintaining full coverage
- Address maintainer feedback for better test organization

Signed-off-by: Jason Young <farion1231@gmail.com>

---------

Signed-off-by: Jason Young <farion1231@gmail.com>
2025-07-01 23:37:44 +08:00
kangfenmao
ba21a2c5fa refactor(EmojiIcon): enhance EmojiIcon component to accept size and fontSize props for better customization; update styles accordingly.
fix(AddAssistantPopup): adjust body padding for improved layout consistency.

style(Messages): modify padding in ScrollContainer for better spacing; add missing line for groupedMessages.

style(Prompt): update padding and margin for improved layout aesthetics.
2025-07-01 20:10:04 +08:00
beyondkmp
3eb6d08b34 fix(WindowService): remove backgroundThrottling option for cleaner window configuration (#7704) 2025-07-01 16:50:48 +08:00
SuYao
b5f2abc930 fix: update default timeout configuration across API clients (#7686)
- Increased the default timeout value from 5 minutes to 10 minutes in constant.ts.
- Updated GeminiAPIClient and ImageGenerationMiddleware to utilize the new defaultTimeout constant for API call timeouts, ensuring consistent timeout handling across the application.
2025-07-01 15:09:12 +08:00
Teo
0c3720123d feat(TopicsHistory): add sorting functionality for topics and update UI components (#7673)
* feat(TopicsHistory): add sorting functionality for topics and update UI components

* refactor(assistants): remove console log from updateTopicUpdatedAt function

* refactor(TopicsHistory): update topic date display to use dynamic sorting type
2025-07-01 14:52:52 +08:00
亢奋猫
4aa77d5a82 doc: Developer Co-creation Program 2025-07-01 14:31:53 +08:00
Wang Jiyuan
f500cc6c9a refactor(inputbar): enforce image upload and model mentioning restrictions (#7314)
* feat(inputbar): feat: enforce image upload restrictions
- allow image uploads when mentioning vision models
- disallow image uploads when non-vision models are mentioned

* refactor(Inputbar): improve handleDrop

* fix(Inputbar): Quick panel does not refresh when file changes

* fix(AttachmentButton): Fix the conditional judgment logic when mentionedModels is optional

* stash

* fix(Inputbar): Fix the issue where quickPanel does not close when files are updated

Use useRef to track changes in files, ensuring that quickPanel is properly closed when files are updated

* refactor(Inputbar): 重构附件按钮和工具条逻辑,简化文件类型支持判断

将文件类型支持判断逻辑从组件中提取到父组件,通过props传递couldAddImageFile和extensions
移除不必要的依赖和计算,优化组件性能

* fix(Inputbar): 修正文件上传逻辑并重命名快速面板方法

修复couldAddTextFile条件判断错误
将openQuickPanel重命名为openAttachmentQuickPanel以明确功能

* feat(MessageEditor): 添加基于话题ID的文件类型限制功能

根据关联消息的模型类型动态限制可添加的文件类型

* fix(MessageEditor): 仅在用户消息时显示附件按钮

根据消息角色决定是否显示附件按钮,避免非用户消息出现不必要的附件功能

* feat(MessageMenu): 添加模型筛选功能以支持视觉模型选择

根据关联消息内容动态筛选可提及的模型
当用户消息包含图片时仅显示视觉模型

* fix: 修复模型过滤器默认值处理

修复SelectModelPopup组件中modelFilter未传入时的默认值处理,使用默认值会导致卡死

* feat(输入栏): 添加模型集合功能并优化文件类型支持

添加 isVisionModels 和 isGenerateImageModels 工具函数用于判断模型集合
优化输入栏对文件类型的支持逻辑,重命名 supportExts 为 supportedExts
移除调试日志并简化模型支持判断逻辑

* refactor(Inputbar): 移除未使用的model属性并优化代码结构

清理AttachmentButton和InputbarTools组件中未使用的model属性
优化MessageEditor中的状态管理,使用useAppSelector替代store.getState
修复拼写错误(failback -> fallback)
2025-07-01 12:35:02 +08:00
Wang Jiyuan
68d0b13a64 fix: Ensure tool call results are included in the conversation context (#7463)
* refactor(aiCore): 统一消息内容处理逻辑,优化工具调用结果显示

重构各AI客户端的消息内容处理逻辑,使用新的getContentWithTools函数统一处理
将blocks参数重命名为block以符合语义
使用MessageBlockType枚举替代硬编码字符串

* fix(aiCore): 修复工具调用结果消息的格式问题

调整工具调用结果消息的换行格式,使其显示更清晰

* refactor(aiCore): 将getContentWithTools工具函数移至messageUtils模块

重构代码,将getContentWithTools函数从aiCore/clients/utils.ts移动到messageUtils/find.ts模块中
统一消息处理工具函数的存放位置,提高代码组织性
删除不再使用的utils.ts文件

* refactor(aiCore): 统一使用getMessageContent获取消息内容

将各API客户端中直接调用getContentWithTools改为通过基类的getMessageContent方法获取消息内容,保持行为一致性

* fix(find): 移除冗余的条件判断
2025-07-01 12:34:11 +08:00
SuYao
c37176fe98 refactor(APIClients): apply custom parameters conditionally for chat scenarios to avoid affecting other functionalities (#7702) 2025-07-01 12:26:11 +08:00
beyondkmp
421b4071d6 fix(WindowService): remove backgroundThrottling option for improved window configuration (#7699) 2025-07-01 11:02:59 +08:00
Teo
1e20780c36 refactor(Messages): enhance ImageBlockGroup to dynamically adjust grid columns based on block count (#7678)
* refactor(Messages): enhance ImageBlockGroup to dynamically adjust grid columns based on block count

* fix(ImageBlock): update maxHeight style to use responsive value for better layout
2025-07-01 10:30:51 +08:00
Xin Rui
acbe8c7605 feat(TranslatePage): replace ReactMarkdown with MarkdownIt. (#7545)
* feat(TranslatePage): replace ReactMarkdown with MarkdownIt.

* fix: line wrapping in plain text and shiki code block

---------

Co-authored-by: one <wangan.cs@gmail.com>
2025-07-01 01:42:25 +08:00
Teo
ad0b10c517 style(antd): Optimize antd components through patch method (#7683)
* fix(dependencies): update antd to patch version 5.24.7 and apply custom patch

* refactor(AddAgentPopup): remove unused ChevronDown import

* feat(AntdProvider): add paddingXS to Dropdown component for improved layout
2025-06-30 20:40:32 +08:00
beyondkmp
8c657b57f7 feat: add country flag emoji support and enhance UI components (#7646)
* feat: add country flag emoji support and enhance UI components

* Added country-flag-emoji-polyfill to package.json and yarn.lock
* Integrated polyfill in AddAgentPopup, GeneralSettings, and AssistantPromptSettings components
* Updated emoji rendering styles for better visual consistency

* fix: update country flag emoji polyfill to use 'Twemoji Country Flags'

* feat: enhance emoji components with country flag support

* Integrated country-flag-emoji-polyfill in EmojiIcon, EmojiPicker, and AssistantItem components.
* Updated font-family styles across various components for consistent emoji rendering.
* Removed redundant polyfill calls from AddAgentPopup and AssistantPromptSettings.

* refactor: streamline country flag emoji integration

* Removed redundant polyfill calls from EmojiIcon, AssistantItem, and GeneralSettings components.
* Updated EmojiPicker to use a local font file for country flag emojis.
* Added country flag font import in index.scss for improved styling consistency.

* format code

* refactor: standardize country flag font usage across components

* Introduced a new CSS class for country flag font to streamline styling.
* Updated various components (GeneralSettings, EmojiIcon, EmojiAvatar, AssistantPromptSettings, TranslatePage) to utilize the new class for consistent font application.
* Removed inline font-family styles to enhance maintainability.

* refactor: update font styles for improved consistency and maintainability

* Added Windows-specific font configuration in font.scss for better emoji rendering.
* Removed inline font-family styles from various components (EmojiAvatar, GeneralSettings, AssistantPromptSettings, TranslatePage) to enhance code clarity and maintainability.

* refactor: remove inline font-family styles from EmojiIcon for improved maintainability
2025-06-30 20:23:22 +08:00
beyondkmp
ac03aab29f chore(package): add opendal dependency to package.json (#7685) 2025-06-30 17:04:48 +08:00
Teo
db4ce9fb7f fix(Inputbar): fix enter key confict (#7679)
fix(Inputbar): prevent default behavior for Enter key when quick panel is visible
2025-06-30 16:13:25 +08:00
SuYao
21ba35b6bf fix(ImageGenerationMiddleware): read image binary data (#7681)
- Replaced direct API call for reading binary images with FileManager's readBinaryImage method to streamline image handling in the ImageGenerationMiddleware.
2025-06-30 15:17:05 +08:00
SuYao
a9a9d884ce Fix/gemini (#7659)
* refactor: update Gemini and OpenAI API clients for improved reasoning model handling

- Replaced isGeminiReasoningModel with isSupportedThinkingTokenGeminiModel in GeminiAPIClient for better model validation.
- Enhanced OpenAIAPIClient to support additional configurations for reasoning efforts and thinking budgets based on model type.
- Introduced new thinking tags for Gemini models in ThinkingTagExtractionMiddleware.
- Updated model checks in models.ts to streamline reasoning model identification.
- Adjusted ThinkingButton component to differentiate between Gemini and Gemini Pro models based on regex checks.

* refactor(GeminiAPIClient): streamline reasoning configuration handling

- Simplified the logic for returning thinking configuration when reasoningEffort is undefined in GeminiAPIClient.
- Updated ApiService to include enableReasoning flag for API calls, enhancing control over reasoning capabilities.

* fix(OpenAIAPIClient): add support for non-flash Gemini models in reasoning configuration

- Introduced a check for non-flash models in the OpenAIAPIClient to enhance reasoning configuration handling for supported Gemini models.
- This change ensures that reasoning is correctly configured based on the model type, improving overall model validation.
2025-06-30 13:51:23 +08:00
Wang Jiyuan
1034b94628 fix(translate): improve language options with clearer values (#7640)
* fix(翻译配置): 修正简体中文语言选项的值和标签显示

将'chinese'改为更明确的'chinese-simplified'

* style(translate): 统一语言选项的显示格式为规范名称
2025-06-30 10:43:19 +08:00
cnJasonZ
4c988ede52 Feat/ppio rerank (#7567)
* feat: add PPIO rerank and embedding models

* fix: fix migrate.ts

* fix: set ppio provider type to openai

* fix: remove 'ppio' from ProviderType definition

---------

Co-authored-by: suyao <sy20010504@gmail.com>
2025-06-30 10:16:22 +08:00
David Zhang
7b7819217f chore(OpenAIApiClient): handle empty delta objects in non-streaming esponses (#7658)
chore(OpenAIApiClient): handle empty delta objects in non-streaming responses
2025-06-30 03:14:58 +08:00
SuYao
b0053b94a9 fix(models): enhance Doubao model checks to include model.id conditions (#7657)
- Updated model checks in isFunctionCallingModel, isEmbeddingModel, isVisionModel, and isReasoningModel functions to consider model.id for 'doubao' provider.
- Improved isOpenAIWebSearchModel to include additional conditions for model.id.
2025-06-30 00:15:36 +08:00
Yiyang Suen
218dcc2229 fix: textarea not resizing back after clearing long input (#7609) (#7632)
* fix: textarea not resizing back after clearing long input (#7609)

* fix: text area auto size only when not dragged
2025-06-30 00:01:28 +08:00
beyondkmp
8f64c5ab6a feat: support linux deb (#7652) 2025-06-29 23:58:24 +08:00
Kingsword
9a4c69579d fix: restore message content className logic to resolve search issue (#7651) 2025-06-29 21:32:05 +08:00
Xin Rui
486c5c42f7 chore: format zh-cn and zh-tw i18n strings with pangu. (#7644) 2025-06-29 20:47:17 +08:00
George·Dong
3f5901766d feat: Add S3 Backup (#6802)
* 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

---------

Co-authored-by: suyao <sy20010504@gmail.com>
2025-06-28 22:19:37 +08:00
kangfenmao
27d22e90d4 chore(version): 1.4.7 2025-06-28 20:38:53 +08:00
Kingsword
101d73fc10 ♻️ refactor(ContentSearch): ContentSearch to use CSS highlights API (#7493) 2025-06-28 20:04:03 +08:00
one
8de6ae1772 fix(Menubar): icon for multi select (#7635) 2025-06-28 19:00:26 +08:00
beyondkmp
ece59cfacf fix(migrate): handle state return in migration process and add upgradechannel setting (#7634)
* fix(migrate): handle state return in migration process and add upgrade channel setting

* fix(migrate): move upgrade channel setting to the correct migration step
2025-06-28 17:52:36 +08:00
beyondkmp
780373d5f7 fix: 测试版本 (#7590)
* feat(AppUpdater): add support for pre-release versions and enhance feed URL logic

- Introduced a new FeedUrl for the lowest pre-release version.
- Updated AppUpdater to handle early access and upgrade channel settings more effectively.
- Enhanced IPC logging for early access and upgrade channel changes.
- Refactored feed URL setting logic to streamline update processes.

* fix(AppUpdater, ipc): enhance early access and upgrade channel handling

- Added checks to prevent unnecessary cancellation of downloads when early access and upgrade channel settings remain unchanged.
- Updated IPC handlers to ensure early access is enabled when switching upgrade channels if it was previously disabled.
- Improved logging for better traceability of changes in early access and upgrade channel settings.

* delete code

* delete logs

* refactor(AboutSettings): enhance upgrade channel management

- Introduced logic to determine the current upgrade channel based on version.
- Refactored available test channels to use a more structured approach with tooltips and labels.
- Updated the method for retrieving available test channels to improve clarity and maintainability.

* feat(IpcChannel, ConfigManager, AppUpdater): implement test plan and channel management

- Replaced early access features with test plan and test channel options in IpcChannel and ConfigManager.
- Updated IPC handlers to manage test plan and test channel settings, including logging enhancements.
- Refactored AppUpdater to support fetching pre-release versions based on the selected test channel.
- Modified settings and localization files to reflect the new test plan functionality.
- Adjusted AboutSettings and related components to integrate test plan management and improve user experience.

* format code

* refactor(AppUpdater, AboutSettings): improve test channel logic and localization updates

- Refactored the logic in AppUpdater to enhance the handling of test channels, ensuring correct channel retrieval based on the current version.
- Updated the AboutSettings component to include useEffect for managing test channel changes and displaying appropriate warnings.
- Modified localization files for multiple languages to clarify the behavior of test version switching, aligning with the new logic.
2025-06-28 17:17:47 +08:00
SuYao
dfcebe9767 fix(models): update regex patterns for Doubao models and enhance function checks (#7624)
- Adjusted regex for visionAllowedModels and DOUBAO_THINKING_MODEL_REGEX to allow for optional suffixes.
- Enhanced isFunctionCallingModel and isDoubaoThinkingAutoModel functions to check both model.id and model.name for better matching.
2025-06-28 16:58:17 +08:00
自由的世界人
daaf9c2b06 fix: move ContentSearch below Messages in Chat layout (#7628)
Reordered the ContentSearch component to render after the Messages component within the Chat page. This change likely improves the UI flow by displaying the search functionality below the chat messages.
2025-06-28 16:51:49 +08:00
happyZYM
83b95f9830 fix: restore strict no-think for Openrouter provider with latest api (#7620) 2025-06-28 16:45:54 +08:00
beyondkmp
cf87a840f7 fix(FileStorage): remove redundant WordExtractor import (#7625) 2025-06-28 16:45:02 +08:00
Wang Jiyuan
49653435c2 fix(models): Add inference model detection for qwen-plus and qwen-turbo (#7622)
feat(models): 添加对qwen-plus和qwen-turbo模型的推理模型判断
2025-06-28 14:10:55 +08:00
beyondkmp
14e31018f7 fix: support spell check for mini app (#7602)
* feat(IpcChannel): add Webview_SetSpellCheckEnabled channel and implement spell check handling for webviews

- Introduced a new IPC channel for enabling/disabling spell check in webviews.
- Updated the registerIpc function to handle spell check settings for all webviews.
- Enhanced WebviewContainer to set spell check state on DOM ready event.
- Refactored context menu setup to accommodate webview context menus.

* refactor(ContextMenu): update methods to use Electron.WebContents instead of BrowserWindow

- Changed method signatures to accept Electron.WebContents for better context handling.
- Updated internal calls to utilize the new WebContents reference for toggling dev tools and managing spell check functionality.

* refactor(WebviewContainer): clean up import order and remove unused code

- Adjusted the import order in WebviewContainer.tsx for better readability.
- Removed redundant import of useSettings to streamline the component.
2025-06-28 08:36:32 +08:00
Wang Jiyuan
2d3f5baf72 feat: Increase the upper limit of web search results (#7439)
* fix(WebSearchSettings): 将最大搜索结果限制从20增加到50

* fix(WebSearchSettings): 调整搜索结果滑块宽度并添加50的标记
2025-06-27 22:33:27 +08:00
one
c7c1cf2552 refactor: increase css editor height, fix EditMcpJsonPopup (#7535)
* refactor: increase css editor height

* fix: lint warnings

* refactor: use vh for height

* fix: editmcpjsonpopup editor unavailable after deleting all the code
2025-06-27 21:53:43 +08:00
Chen Tao
98b12fb800 fix: tei reranker (#7606)
fix(tei)
2025-06-27 18:07:17 +08:00
one
d463d6ea2e feat(WebSearch): support RAG for external websearch, improve feedback (#7446)
* feat(WebSearch, RAG): support RAG for external websearch

* refactor(WebSearch): handle content limit in service

* refactor: update migrate

* refactor: UI, constants, types

* refactor: migrate contentLimit to cutoffLimit

* refactor: update default rag document count

* refactor: add a helper function for merging references

* refactor: reference filtering

* feat: feedback for websearch phases

* feat: support cutoff by token

* refactor: add a warning and fix the bound of cutoff limit

* fix: not pass `dimensions` if it is not set by the user

* refactor: update i18n and error message

* refactor: improve UI

* fix: cutoff unit style
2025-06-27 18:04:42 +08:00
Wei Lin
1fe439bb51 docs: add 20 language links of README (#7611)
PR adds 20 languages link to the README and user can easily to access translated READEME, supports google/bing multiple languages SEO search.

Page demo https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=ja

> OpenAiTx is free and open-source : https://github.com/OpenAiTx/OpenAiTx

![Image](https://github.com/user-attachments/assets/41c79fad-5f63-4ed3-8d48-99f3b89879ba)
2025-06-27 18:02:37 +08:00
one
3726ceaf48 refactor: use useLayoutEffect for shiki renderer 2025-06-27 17:56:19 +08:00
one
639ddd5628 refactor: add ShikiTokensRendererProps 2025-06-27 17:56:19 +08:00
one
16772c1d37 refactor(CodePreview): line numbers as elements 2025-06-27 17:56:19 +08:00
one
766897e733 refactor: show error on missing mcp tool (#7587) 2025-06-27 16:09:06 +08:00
one
e8e9a2d86f fix(Markdown, LaTeX): do not touch escaped brackets (#7582)
- Keep `\\[` as is
- Use a custom match algorithm rather than balanced match
2025-06-27 13:46:09 +08:00
Wang Jiyuan
a6b53457b0 fix(models): Resolve case sensitivity issue with model names (#7595)
* fix(models): 修复模型名称大小写敏感问题

确保在检查支持的禁用生成模型时,将模型名称统一转换为小写进行比较

* feat(utils): 添加获取小写基础模型名称的函数

新增 getLowerBaseModelName 函数,用于从模型ID中提取基础名称并转换为小写
替换多处直接调用 getBaseModelName().toLowerCase() 的代码,提高代码复用性
2025-06-27 13:45:15 +08:00
Teo
093d04c386 fix(Selector): Fix the issue with the Selector component being selected. (#7600)
* fix(Selector): update value comparison logic to use 'some' for selected values

* feat(ModelSettings): add ChevronDown icon as suffix for Select components
2025-06-27 12:15:39 +08:00
kangfenmao
46de46965f chore(version): 1.4.6 2025-06-26 18:19:27 +08:00
Teo
f5165e12f1 fix(Messages): Fix single model response style issue (#7560)
* fix(Messages): update multiModelMessageStyle condition to check message count

* style(Messages): update styles for MultiSelectionPopup and MessageGroup components
2025-06-26 17:05:48 +08:00
亢奋猫
0160655dba feat(FileStorage): enhance open dialog to handle large files by retur… (#7568)
feat(FileStorage): enhance open dialog to handle large files by returning size without reading content

- Updated the open method to return file size for files larger than 2GB without reading their content.
- Modified return type to include an optional content field and size property for better file handling.

修复恢复备份的时候选择超过 2GB 文件报错的问题
2025-06-26 16:48:56 +08:00
one
8723bbeaf8 fix(Markdown): falsely early return for display \[\n...\n\] (#7565) 2025-06-26 15:52:58 +08:00
beyondkmp
4c66b205bb feat: implement early access feature toggle and update related configurations (#7304)
* feat: implement early access feature toggle and update related configurations

- Replace FeedUrl with EnableEarlyAccess in IpcChannel and ConfigManager
- Update AppUpdater to handle early access updates from GitHub
- Modify settings and localization files to reflect early access functionality
- Ensure proper integration in the renderer and preload layers

* fix: enhance error handling in AppUpdater for GitHub release fetching

- Wrap the fetch call in a try-catch block to handle potential errors when retrieving the latest non-draft version from GitHub.
- Log an error message if the fetch fails and return a default feed URL.

* refactor: remove early access feature handling from AppUpdater

- Eliminate the early access feature toggle logic from the AppUpdater class.
- Adjust the feed URL setting to ensure it retrieves the latest non-draft version from GitHub when applicable.
- Clean up unnecessary user-agent header in the fetch request.

* feat(AppUpdater): enhance update feed URL logic and disable differential downloads

- Introduced a new private method to streamline feed URL setting based on early access and IP country.
- Disabled differential downloads for compatibility with GitHub and GitCode.
- Cleaned up the checkForUpdates method for better readability and maintainability.

* refactor(AppUpdater): simplify early access feed URL logic

- Consolidated the feed URL setting logic in setEnableEarlyAccess to a single line for improved readability.
- Removed redundant conditional checks while maintaining functionality for early access updates.

* refactor(AppUpdater): update feed URL structure and remove early access setting

- Modified the return structure of the latest release URL to include the channel type.
- Removed the early access setting from the IPC handler, streamlining the update process.
- Ensured the autoUpdater channel is set based on the latest release information.

* feat(UpgradeChannel): add upgrade channel management and IPC integration

- Introduced a new UpgradeChannel enum to manage different upgrade paths (latest, rc, beta).
- Updated IpcChannel to include App_SetUpgradeChannel for setting the upgrade channel.
- Enhanced ConfigManager to store and retrieve the selected upgrade channel.
- Modified AppUpdater to fetch pre-release versions based on the selected upgrade channel.
- Updated settings UI to allow users to select their preferred upgrade channel with tooltips for guidance.
- Localized new strings for upgrade channel options in multiple languages.

* refactor(AboutSettings): update version type detection and localize upgrade channel tooltips

- Changed version type detection to use the UpgradeChannel enum for better clarity.
- Localized success messages for switching upgrade channels to enhance user experience.

* chore: update version to 1.4.4-beta.1 and refactor upgrade channel handling in AboutSettings

- Updated package version to 1.4.4-beta.1.
- Renamed version type detection function to getVersionChannel for clarity.
- Refactored available version options to getAvailableTestChannels for better organization.
- Added logic to clear update info when switching upgrade channels and when toggling early access settings.

* chore: update version to 1.4.4 in package.json

* fix lint error

* feat(AppUpdater): enhance upgrade channel management and localization

- Added cancellation functionality for ongoing downloads in AppUpdater.
- Introduced a new upgrade channel option for the latest stable version.
- Updated IPC handlers to cancel downloads when changing early access settings or upgrade channels.
- Localized new strings for the latest version option in multiple languages.
- Refactored AboutSettings to include the latest version in the upgrade channel selection.

* refactor(AboutSettings): remove version channel detection logic

- Eliminated the getVersionChannel function to simplify version handling.
- Updated AboutSettings to streamline upgrade channel management.

* feat(AboutSettings): set default upgrade channel to latest

- Updated the AboutSettings component to set the default value of the upgrade channel to the latest option, enhancing user experience in channel selection.

* refactor(AboutSettings): simplify upgrade channel change handling

- Removed individual success messages for different upgrade channels in the handleUpgradeChannelChange function, streamlining the code and improving maintainability.

* refactor: file actions into FileAction service (#7413)

* refactor: file actions into FileAction service

Moved file sorting, deletion, and renaming logic from FilesPage to a new FileAction service for better modularity and reuse. Updated FileList and FilesPage to use the new service functions, and improved the delete button UI in FileList.

* fix: add tag collapse state management for assistants (#7436)

Add tag collapse state management for assistants

Introduces a collapsedTags state to manage the collapsed/expanded state of tag groups in the assistants list. Updates useTags and AssistantsTab to use this state, and adds actions to toggle and initialize tag collapse in the Redux store.

* fix(model): doubao thinking param (#7499)

* feat: Implement occupied directories handling during data copy (#7485)

* feat: Implement occupied directories handling during data copy

- Added `occupiedDirs` constant to manage directories that should not be copied.
- Enhanced the `copyOccupiedDirsInMainProcess` function to copy occupied directories to a new app data path in the main process.
- Updated IPC and preload APIs to support passing occupied directories during the copy operation.
- Modified the DataSettings component to utilize the new copy functionality with occupied directories.

* fix: Improve occupied directories handling during data copy

- Updated the filter logic in the `registerIpc` function to resolve directory paths correctly.
- Modified the `DataSettings` component to pass the correct occupied directories format during the copy operation.

* feat: add appcode (#7507)

Co-authored-by: zhaochenxue <zhaochenxue@bixin.cn>

* fix: non streamoutput sometimes (#7512)

* feat(migrate): add default settings for assistants during migration

- Introduced a new migration step to assign default settings for assistants that lack configuration.
- Default settings include temperature, context count, and other parameters to ensure consistent behavior across the application.

* chore(store): increment version number to 115 for persisted reducer

* Revert "feat: Update API Key Management Interface (#3444)"

This reverts commit 31b3ce1049.

* feat:  一些UI上的优化和重构 (#7479)

- 调整AntdProvider中主题配置,包括颜色、尺寸
- 重构聊天气泡模式的样式
- 重构多选模式的样式
- 添加Selector组件取代ant Select组件
- 重构消息搜索弹窗界面
- 重构知识库搜索弹窗界面
- 优化其他弹框UI

* fix: bailian reranker (#7518)

* feat: implement Python MCP server using existing Pyodide infrastructure (#7506)

* refactor: rename isWindows to isWin for consistency across main/renderer (#7530)

refactor: rename isWindows to isWin for consistency across components

* refactor: data migration modal logic in DataSettings (#7503)

* refactor: data migration modal logic in DataSettings

Moved showProgressModal and startMigration functions inside the useEffect hook and added t as a dependency. This improves encapsulation and ensures translation updates are handled correctly.

* remove trailing whitespace in DataSettings.tsx

Cleaned up a line by removing unnecessary trailing whitespace in the DataSettings component.

* fix: clear search cache on resending (#7510)

* fix: Resolve vllm bad request caused by always sending dimensions in embedding requests (#7525)

fix(知识库): 将dimensions字段改为可选并修复相关逻辑

* feat: Support custom registry address when configuring mcp for npm & fix lint error (#7531)

* feat: Support custom registry address when configuring mcp for npm

* fix: lint

* refactor(GeminiAPIClient): separate model and user message handling to adapt vertex (#7511)

- Introduced a new modelParts array to manage model-related messages separately from user messages.
- Updated the logic to push model messages to currentReqMessages only if they exist, improving clarity and structure.
- Adjusted the return order of messages in buildSdkMessages to ensure history is appended correctly.
- Enhanced McpToolChunkMiddleware to reset tool processing state output when output is present.

* feat: enhance WindowFooter with show/hide functionality for UI elements

- Added state management to control visibility of UI elements in the WindowFooter.
- Implemented a timer to automatically hide elements after a period of inactivity.
- Updated hotkey handlers to reset the visibility timer on user interaction.
- Modified styled component to reflect the new visibility logic.

* fix(SelectionAssistant): opacity slider too slow when sliding in settings page (#7537)

feat: enhance opacity control in Selection Assistant Settings

- Added state management for opacity value in SelectionAssistantSettings component.
- Updated Slider component to use the new opacity state instead of the previous actionWindowOpacity variable.
- Ensured onChangeComplete updates the actionWindowOpacity accordingly.

* feat(AihubmixAPIClient): add getBaseURL method to handle client base URL retrieval

* fix(migrate): restore upgradeChannel setting in migration logic

- Reintroduced the upgradeChannel setting to the state during the migration process, ensuring it defaults to LATEST when applicable.
- Adjusted the migration logic to maintain consistency in settings management.

---------

Co-authored-by: 自由的世界人 <3196812536@qq.com>
Co-authored-by: one <wangan.cs@gmail.com>
Co-authored-by: chenxue <DDU1222@users.noreply.github.com>
Co-authored-by: zhaochenxue <zhaochenxue@bixin.cn>
Co-authored-by: SuYao <sy20010504@gmail.com>
Co-authored-by: kangfenmao <kangfenmao@qq.com>
Co-authored-by: Teo <cheesen.xu@gmail.com>
Co-authored-by: Chen Tao <70054568+eeee0717@users.noreply.github.com>
Co-authored-by: LiuVaayne <10231735+vaayne@users.noreply.github.com>
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
Co-authored-by: Wang Jiyuan <59059173+EurFelux@users.noreply.github.com>
Co-authored-by: 陈天寒 <silenceboychen@gmail.com>
Co-authored-by: fullex <0xfullex@gmail.com>
2025-06-26 15:43:45 +08:00
one
6342998c9f feat(MentionedModels): improve feedback for MessageGroupModelList (#7539)
* feat(MentionedModels): improve feedback for MessageGroupModelList

* refactor: reuse pulse animation, fix tooltip triggering area

* refactor: use lightbulbSoftVariants
2025-06-26 15:01:36 +08:00
suyao
f555e604a3 fix(models): update isReasoningModel function to exclude embedding models
- Added a check to the isReasoningModel function to return false for embedding models, ensuring correct model classification.
2025-06-26 13:32:32 +08:00
one
5811adfb7f refactor(CodePreview): handle chunking in ShikiStreamService, make the algorithm more robust (#7409)
* refactor(ShikiStreamService, CodePreview): handle chunking in ShikiStreamService, make the algorithm more robust

- Add highlightStreamingCode with improved robustness
- Improve viewport detection

* perf: improve checks for appending

* chore: update comments
2025-06-26 13:30:49 +08:00
亢奋猫
1db93e8b56 Fix anthropic request cannot handle webSearch and knowbase references (#7559)
修复 Anthropic 模型请求忽略了知识库和网络搜索引用内容的问题
2025-06-26 13:19:36 +08:00
亢奋猫
3048d0850c fix: Gemini reasoning model check and improve citation popover structure (#7554)
- Added a new condition to the Gemini reasoning model check to include models with IDs starting with 'gemini' and containing 'thinking'.
- Refactored the CitationsList component to improve the structure of popover content for web search and knowledge citations.
- Updated styled components for better layout and responsiveness in the citation popover.
- Adjusted margin styles in ErrorBlock for consistent spacing.
2025-06-26 12:00:31 +08:00
Teo
08a526e511 style: 优化消息滚动条 (#7549)
* feat(Messages): integrate Scrollbar component into Message and MessageGroup styled containers

* style(Messages): add margin-top to MessageFooter for improved layout

* fix(SelectionToolbar): update regex to remove background styles more accurately
2025-06-26 11:42:12 +08:00
one
5e0cae06db fix(CodeEditor): save to db (#7504) 2025-06-26 11:19:11 +08:00
fullex
1f09c8a022 refactor(SelectionAssistant): make all Toolbar CSS variables customizable (#7532)
refactor: update selection toolbar styles and structure

- Enhanced the selection toolbar's HTML structure for better readability.
- Updated CSS variables for improved theming and consistency across the toolbar.
- Refactored the styled components in SelectionToolbar.tsx to utilize new CSS variables for layout and styling.
- Added support for hover states and improved button styling for better user experience.
2025-06-26 10:17:09 +08:00
suyao
751879d42e feat(AihubmixAPIClient): add getBaseURL method to handle client base URL retrieval 2025-06-26 01:30:55 +08:00
fullex
5f2d0d4bfc fix(SelectionAssistant): opacity slider too slow when sliding in settings page (#7537)
feat: enhance opacity control in Selection Assistant Settings

- Added state management for opacity value in SelectionAssistantSettings component.
- Updated Slider component to use the new opacity state instead of the previous actionWindowOpacity variable.
- Ensured onChangeComplete updates the actionWindowOpacity accordingly.
2025-06-26 01:16:17 +08:00
fullex
3d535d0e68 feat: enhance WindowFooter with show/hide functionality for UI elements
- Added state management to control visibility of UI elements in the WindowFooter.
- Implemented a timer to automatically hide elements after a period of inactivity.
- Updated hotkey handlers to reset the visibility timer on user interaction.
- Modified styled component to reflect the new visibility logic.
2025-06-25 22:56:48 +08:00
SuYao
9362304db0 refactor(GeminiAPIClient): separate model and user message handling to adapt vertex (#7511)
- Introduced a new modelParts array to manage model-related messages separately from user messages.
- Updated the logic to push model messages to currentReqMessages only if they exist, improving clarity and structure.
- Adjusted the return order of messages in buildSdkMessages to ensure history is appended correctly.
- Enhanced McpToolChunkMiddleware to reset tool processing state output when output is present.
2025-06-25 22:16:27 +08:00
陈天寒
17a8f0a724 feat: Support custom registry address when configuring mcp for npm & fix lint error (#7531)
* feat: Support custom registry address when configuring mcp for npm

* fix: lint
2025-06-25 21:37:10 +08:00
Wang Jiyuan
066aad7fed fix: Resolve vllm bad request caused by always sending dimensions in embedding requests (#7525)
fix(知识库): 将dimensions字段改为可选并修复相关逻辑
2025-06-25 21:15:05 +08:00
one
5138f5b314 fix: clear search cache on resending (#7510) 2025-06-25 21:10:15 +08:00
自由的世界人
839c44eb7a refactor: data migration modal logic in DataSettings (#7503)
* refactor: data migration modal logic in DataSettings

Moved showProgressModal and startMigration functions inside the useEffect hook and added t as a dependency. This improves encapsulation and ensures translation updates are handled correctly.

* remove trailing whitespace in DataSettings.tsx

Cleaned up a line by removing unnecessary trailing whitespace in the DataSettings component.
2025-06-25 21:07:40 +08:00
fullex
0001bc60a9 refactor: rename isWindows to isWin for consistency across main/renderer (#7530)
refactor: rename isWindows to isWin for consistency across components
2025-06-25 19:59:47 +08:00
LiuVaayne
04e6f2c1ad feat: implement Python MCP server using existing Pyodide infrastructure (#7506) 2025-06-25 18:21:10 +08:00
Chen Tao
a94847faeb fix: bailian reranker (#7518) 2025-06-25 15:48:04 +08:00
Teo
64b01cce47 feat: 一些UI上的优化和重构 (#7479)
- 调整AntdProvider中主题配置,包括颜色、尺寸
- 重构聊天气泡模式的样式
- 重构多选模式的样式
- 添加Selector组件取代ant Select组件
- 重构消息搜索弹窗界面
- 重构知识库搜索弹窗界面
- 优化其他弹框UI
2025-06-25 14:34:18 +08:00
kangfenmao
3df5aeb3c3 Revert "feat: Update API Key Management Interface (#3444)"
This reverts commit 31b3ce1049.
2025-06-25 13:10:46 +08:00
SuYao
9fe5fb9a91 fix: non streamoutput sometimes (#7512)
* feat(migrate): add default settings for assistants during migration

- Introduced a new migration step to assign default settings for assistants that lack configuration.
- Default settings include temperature, context count, and other parameters to ensure consistent behavior across the application.

* chore(store): increment version number to 115 for persisted reducer
2025-06-25 12:49:00 +08:00
chenxue
17951ad157 feat: add appcode (#7507)
Co-authored-by: zhaochenxue <zhaochenxue@bixin.cn>
2025-06-25 09:17:27 +08:00
beyondkmp
3640d846b9 feat: Implement occupied directories handling during data copy (#7485)
* feat: Implement occupied directories handling during data copy

- Added `occupiedDirs` constant to manage directories that should not be copied.
- Enhanced the `copyOccupiedDirsInMainProcess` function to copy occupied directories to a new app data path in the main process.
- Updated IPC and preload APIs to support passing occupied directories during the copy operation.
- Modified the DataSettings component to utilize the new copy functionality with occupied directories.

* fix: Improve occupied directories handling during data copy

- Updated the filter logic in the `registerIpc` function to resolve directory paths correctly.
- Modified the `DataSettings` component to pass the correct occupied directories format during the copy operation.
2025-06-25 00:39:28 +08:00
one
becb6543e0 fix(model): doubao thinking param (#7499) 2025-06-24 23:42:55 +08:00
自由的世界人
1055903456 fix: add tag collapse state management for assistants (#7436)
Add tag collapse state management for assistants

Introduces a collapsedTags state to manage the collapsed/expanded state of tag groups in the assistants list. Updates useTags and AssistantsTab to use this state, and adds actions to toggle and initialize tag collapse in the Redux store.
2025-06-24 21:12:49 +08:00
自由的世界人
e2b8133729 refactor: file actions into FileAction service (#7413)
* refactor: file actions into FileAction service

Moved file sorting, deletion, and renaming logic from FilesPage to a new FileAction service for better modularity and reuse. Updated FileList and FilesPage to use the new service functions, and improved the delete button UI in FileList.
2025-06-24 18:51:58 +08:00
one
f2c9bf433e refactor(CodePreview): auto resize gutters (#7481)
* refactor(CodePreview): auto resize gutters

* refactor: remove unnecessary usememo
2025-06-24 04:01:05 +08:00
Xunjin ZHENG
31b3ce1049 feat: Update API Key Management Interface (#3444)
* feat: enhance API key management in ApiCheckPopup: allow users to add new API key

- Enhanced ApiCheckPopup component to allow users to add new API key, including validation for duplicate entries and improved user feedback.

* feat: update localization strings and refactor API key management components

- Added "Invalid API key" message to localization files for English, Japanese, Russian, Simplified Chinese, and Traditional Chinese.
- Refactored API key management by replacing the ApiCheckPopup with a new ApiKeyList component, enhancing user experience and modularity in handling API keys across provider settings.

* refactor: update OAuthButton and ApiKeyList components for improved UI and localization

- Commented out the translation key in OAuthButton for future use.
- Removed unnecessary localization strings related to API key tips across multiple languages.
- Enhanced ApiKeyList component with styled components for better layout and user interaction.
- Updated ProviderSetting and WebSearchProviderSetting to streamline API key management UI.

* refactor: streamline ApiKeyList component and update localization strings

- Removed the "Check Multiple API Keys" translation key from English, Japanese, Russian, Simplified Chinese, and Traditional Chinese localization files.
- Updated ApiKeyList component to eliminate the model prop, enhancing its simplicity and usability.
- Improved error handling in API key validation by integrating model selection directly within the check process.

* feat: add latency tooltip to API key validation in ApiKeyList component and update localization strings

- Introduced a latency tooltip in the ApiKeyList component to display the time taken for API key validation.
- Updated localization files for English, Japanese, Russian, Simplified Chinese, and Traditional Chinese to include the new latency tooltip string.

* refactor: remove unused imports in WebSearchProviderSetting component

* refactor: improve error handling and latency tracking in ApiKeyList component

- Enhanced error handling during model selection to prevent failures when the user cancels the popup.
- Introduced latency tracking for API key validation, ensuring accurate measurement of response times.
- Streamlined the code for better readability and maintainability.

* refactor: improve styling in ApiKeyList component for better UI consistency

- Updated padding styles for error messages and list items in the ApiKeyList component to enhance visual clarity and user experience.
- Adjusted Card component properties to ensure consistent styling across the interface.

* refactor: extract key formatting logic into a separate function in ApiKeyList component

- Created a new function `formatAndConvertKeysToArray` to handle the formatting and conversion of API keys into an array of unique key objects.
- Updated the state initialization and effect hook in the ApiKeyList component to utilize the new function, improving code readability and maintainability.

* refactor: conditionally render API key section for non-copilot providers

- Updated the ProviderSetting component to conditionally display the API key section only for providers other than 'copilot', improving the user interface and experience.
- Maintained existing functionality for API key management while enhancing code readability.

* refactor: enhance ApiKeyList component for copilot provider handling

- Introduced a new condition to manage the rendering and functionality of buttons in the ApiKeyList component based on the provider type, specifically for 'copilot'.
- Updated the ProviderSetting component to ensure the API key section is consistently displayed for all providers, improving overall user experience and code clarity.

* fix model type error

* feat(ApiKeyList): exclude rerank models from being checked for API key validation after #3969 is merged

* refactor(ApiKeyList): conditionally render check and remove buttons based on key statuses

* refactor(ApiKeyList): using Promise.all for improved performance after #4066 is merged

* refactor(ProviderSettings): update API key display and tooltip integration for improved layout and accessibility

* fix(ApiKeyList): prevent notifications from showing when checking multiple API keys

* feat(ApiKeyList): enhance API key handling with improved key formatting and auto-focus logic for add button

* refactor: clean up WebSearchProviderSetting component

* refactor(ApiKeyList): replace icon buttons with styled components for save and cancel actions

* refactor: API key list UI and remove unused components

Simplified the API key list UI by removing custom styled components for status and actions, replacing them with Ant Design icons and buttons. Improved the key checking logic and removed the tooltip for key check results. Also removed an unused help text in ProviderSetting.

* refactor: add edit functionality to API key list

Introduces the ability to edit existing API keys in the ApiKeyList component. Removes custom save/cancel icon buttons in favor of standard input blur/enter and icon actions. Also adjusts styling for help text in ProviderSetting.

* refactor(ApiKeyList): enhance key status display with tooltips and color coding

* feat(i18n): add "checking" status message in multiple languages

* feat(ApiKeyList): enhance API key management with confirmation for deletion and improved state handling

- Added confirmation for deleting API keys, allowing users to confirm before removal.
- Introduced a cancel state for adding new keys to improve user experience.
- Enhanced key status updates to prevent unnecessary re-renders.
- Improved UI interactions with better handling of edit and cancel actions.
- Added escape key functionality for canceling edits and new key entries.

* fix(ApiKeyList): adjust layout of API key list for improved spacing and alignment

- Updated the Flex component to justify content between elements, enhancing the visual layout of the API key list.
- Minor style adjustment to maintain consistency in the user interface.

* fix(ApiKeyList): refine padding for API key list items to enhance visual consistency

- Adjusted padding for API key list text and items to improve overall layout and alignment.
- Ensured consistent spacing across different states of the API key list.

---------

Co-authored-by: Pleasurecruise <3196812536@qq.com>
Co-authored-by: suyao <sy20010504@gmail.com>
2025-06-24 01:54:12 +08:00
Ying-xi
f69ea8648c fix: display updated timestamp when available in knowledge base (#7453)
* fix: display updated timestamp when available in knowledge base

- Add updated_at field when creating knowledge items
- Show updated_at timestamp if it's newer than created_at
- Fallback to created_at if updated_at is not available or older

Fixes #4587

Signed-off-by: Ying-xi <62348590+Ying-xi@users.noreply.github.com>

* refactor(knowledge): extract display time logic into a reusable function

Signed-off-by: Ying-xi <62348590+Ying-xi@users.noreply.github.com>

---------

Signed-off-by: Ying-xi <62348590+Ying-xi@users.noreply.github.com>
2025-06-24 00:06:52 +08:00
beyondkmp
bbe380cc9e feat(ContextMenu): add spell check and dictionary suggestions to context menu (#7067)
* feat(ContextMenu): add spell check and dictionary suggestions to context menu

- Implemented spell check functionality in the context menu with options to learn spelling and view dictionary suggestions.
- Updated WindowService to enable spellcheck in the webview.
- Enabled spell check in Inputbar and MessageEditor components.

* feat(SpellCheck): implement spell check language settings and initialization

- Added support for configuring spell check languages based on user-selected language.
- Introduced IPC channel for setting spell check languages.
- Updated settings to manage spell check enablement and languages.
- Enhanced UI to allow users to toggle spell check functionality and select languages.
- Default spell check languages are set based on the current UI language if none are specified.

* refactor(SpellCheck): enhance spell check language mapping and UI settings

- Updated spell check language mapping to default to English for unsupported languages.
- Improved UI logic to only update spell check languages when enabled and no manual selections are made.
- Added a new selection component for users to choose from commonly supported spell check languages.

* feat(SpellCheck): integrate spell check functionality into Inputbar and MessageEditor

- Added enableSpellCheck setting to control spell check functionality in both Inputbar and MessageEditor components.
- Updated spellCheck prop to utilize the new setting, enhancing user experience by allowing customization of spell check behavior.

* refactor(SpellCheck): move spell check initialization to WindowService

- Removed spell check language initialization from index.ts and integrated it into WindowService.
- Added setupSpellCheck method to configure spell check languages based on user settings.
- Enhanced error handling for spell check language setup.

* feat(SpellCheck): add enable spell check functionality and IPC channel

- Introduced a new IPC channel for enabling/disabling spell check functionality.
- Updated the preload API to include a method for setting spell check enablement.
- Modified the main IPC handler to manage spell check settings based on user input.
- Simplified spell check language handling in the settings component by directly invoking the new API method.

* refactor(SpellCheck): remove spellcheck option from WindowService configuration

- Removed the spellcheck property from the WindowService configuration object.
- This change streamlines the configuration setup as spell check functionality is now managed through IPC channels.

* feat(i18n): add spell check translations for Japanese, Russian, and Traditional Chinese

- Added new translations for spell check functionality in ja-jp, ru-ru, and zh-tw locale files.
- Included descriptions and language selection options for spell check settings to enhance user experience.

* feat(migrate): add spell check configuration migration

- Implemented migration for spell check settings, disabling spell check and clearing selected languages in the new configuration.
- Enhanced error handling to ensure state consistency during migration process.

* fix(migrate): ensure spell check settings are updated safely

- Added a check to ensure state.settings exists before modifying spell check settings during migration.
- Removed redundant error handling that returned the state unmodified in case of an error.

* fix(WindowService): set default values for spell check configuration and update related UI texts

* refactor(Inputbar, MessageEditor): remove contextMenu attribute and add context menu handling in MessageEditor

---------

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>
2025-06-23 21:19:21 +08:00
beyondkmp
be15206234 fix: Data config improvement (#7471)
* fix: update localization files for data migration warnings and path validation messages

* fix: update app data path validation and localization messages for installation path consistency

* fix: enhance app data flushing process by adding connection closure and delay in DataSettings component
2025-06-23 17:18:46 +08:00
Wang Jiyuan
aee8fe6196 feat(mcpServers): Add a thought field to sequential thinking mcp server (#7465)
feat(mcpServers): 在sequentialthinking中添加thought字段
2025-06-23 15:27:20 +08:00
one
4f2c8bd905 fix(Markdown): improve latex brackets handling (#7358) 2025-06-23 15:19:21 +08:00
Murphy
a2e2eb3b73 fix: re-add newline separator between reasoning_summary parts after openai middleware refactor (#7390)
re-add newline separator between reasoning_summary parts after openai client refactor

Signed-off-by: MurphyLo <murphylo@mail.bnu.edu.cn>
2025-06-23 12:51:08 +08:00
Wang Jiyuan
32d6c2e1d8 feat(TopicsTab): Allow deletion of inactive topics (#7415)
* fix(主题列表): 修复主题列表项悬停样式和菜单显示条件

调整主题列表项悬停时的背景色过渡效果,并修正菜单显示逻辑,仅在非挂起状态显示

* fix(TopicsTab): 移除话题待处理状态检查

* fix(TopicsTab): 修复删除话题时未检查当前活跃话题的问题
2025-06-23 09:59:50 +08:00
purefkh
b4c8e42d87 fix(rename): disable thinking for topic rename (#7461) 2025-06-23 09:48:29 +08:00
Tristan Zhang
a8e23966fa feat(FileStorage): add support for .doc files using word-extractor (#7374)
* feat(FileStorage): add support for .doc files and integrate word-extractor

* chore(package): add word-extractor to devdependencies
2025-06-23 08:55:03 +08:00
Wang Jiyuan
2350919f36 fix: use shouldThrow param in checkApi instead of adding error property to CompletionsResult (#7457)
* Revert "refactor(middleware): Add error property to CompletionResult and handle errors when checking API (#7407)"

This reverts commit 50d6f1f831.

* fix: use shouldThrow param in checkApi
2025-06-22 21:33:17 +08:00
kangfenmao
355d2aebb4 chore(version): 1.4.5 2025-06-22 17:31:43 +08:00
Wang Jiyuan
50d6f1f831 refactor(middleware): Add error property to CompletionResult and handle errors when checking API (#7407)
* refactor(aiCore): 添加错误处理

* remove console.log
2025-06-22 17:03:43 +08:00
自由的世界人
d9b8e68c30 fix: update source language handling and persist user selection in TranslatePage component (#7243) 2025-06-22 12:28:31 +08:00
beyondkmp
c660aaba3d fix: 修复数据目录迁移的bug (#7386)
* fix: move initAppDataDir function inline and remove export from utils/file.ts

* fix some bugs

* fix shouldcopy error

* fix: handle appDataPath initialization and update logic in file.ts; update defaultChecked in DataSettings component

* fix: improve appDataPath handling and migration logic in file.ts

* fix: add error message for selecting the same app data path in DataSettings component and update localization files

* fix: ensure migration confirmation modal is shown correctly in DataSettings component

* feat: add new IPC channel for retrieving data path from arguments and update related components for migration handling

* fix: update app data path validation to check for prefix match in DataSettings component

* refactor: simplify data migration logic in DataSettings component by removing unnecessary flag

* fix: update initAppDataDir invocation to check for app packaging status in bootstrap.ts
2025-06-22 10:32:23 +08:00
Wang Jiyuan
60b37876b1 fix: remove duplicated deepseek-v3 in volcengine (#7406)
fix: 移除重复的DeepSeek-V3模型配置
2025-06-21 21:20:40 +08:00
beyondkmp
37aaaee086 fix: add node-stream-zip for zip file extraction in install-bun script (#7403)
* chore(package): add node-stream-zip for zip file extraction in install-bun script

* refactor(install-uv): replace AdmZip with node-stream-zip for improved zip file extraction

* fix(install-uv): ensure correct extraction of uv binary for Unix/Linux/macOS

* refactor(install-uv): remove redundant file handling and cleanup for Unix/Linux/macOS installation

* fix(install-uv): update tar extraction command to strip leading components for Unix/Linux/macOS

* fix(install-uv): clarify comment for zip file extraction on Windows

* fix(install-bun): correct extraction directory for bun binary

* fix(install-bun, install-uv): update default versions and improve zip extraction process

* fix(install-bun): remove redundant cleanup of source directory after bun installation
2025-06-21 19:47:15 +08:00
Wang Jiyuan
b91ac0de1d fix(models): Unexpected inability to disable image generation feature (#7401)
* fix(models): 修复禁用图片生成模型检查逻辑

* fix(models): use getBaseName()
2025-06-20 22:30:14 +08:00
Wang Jiyuan
8d247add98 fix(ApiService): correct enableWebSearch conditional logic error (#7396)
* fix(ApiService): 修复enableWebSearch条件判断逻辑错误

* fix(web搜索): 修正web搜索模型判断逻辑
2025-06-20 18:06:44 +08:00
Wang Jiyuan
a813df993c fix: Chat does not work properly when configuring multiple API keys (#7385)
* refactor(openai): 使用getApiKey方法替代直接访问apiKey属性

* refactor(openai): 使用getApiKey方法替代直接访问provider.apiKey

* refactor(api客户端): 直接使用apiKey属性替代getApiKey方法
2025-06-20 17:46:45 +08:00
SuYao
1915ba5bfb fix(GeminiAPIClient): update abortSignal option and ensure userLastMessage is pushed to messages (#7387) 2025-06-20 14:46:22 +08:00
George·Dong
3e142f67ad fix(i18n): fix model name export help text (#7372) 2025-06-19 23:32:32 +08:00
Tristan Zhang
b4b456ae06 fix(AssistantService): add default settings configuration to assistant initialization (#7371) 2025-06-19 22:56:46 +08:00
one
ed0bb7fd16 feat(Markdown): disable indented code blocks (#7288)
* feat(Markdown): disable indented code blocks

* chore: update remark/rehype packages
2025-06-19 19:39:33 +08:00
kangfenmao
c9f94a3b15 chore(version): 1.4.4 2025-06-19 19:09:28 +08:00
亢奋猫
ec36f78ffb fix: update WindowService transparency and improve Inputbar resizing … (#7362) 2025-06-19 18:37:53 +08:00
one
439ec286b5 refactor: hard-coded language map (#7360) 2025-06-19 17:13:29 +08:00
one
28b58d8e49 refactor(CodeBlock): support more file extensions for code downloading (#7192) 2025-06-19 15:09:01 +08:00
SuYao
26cb37c9be refactor: remove deprecated MCP server handling and knowledge base ID logic from Inputbar and related services (#7339)
- Removed unused MCP server handling from Inputbar and MessagesService.
- Updated ApiService to fetch active MCP servers directly from the store.
- Deprecated knowledgeBaseIds and enabledMCPs in Message types and related functions.
- Cleaned up related utility functions to enhance code clarity and maintainability.
2025-06-19 13:34:36 +08:00
one
115470fce6 chore(WebDav): remove useless webdav restore (#7347)
- remove webdav restore modal
- fix i18n keys
2025-06-19 12:33:59 +08:00
SuYao
23e9184323 fix: openai response tool use (#7332)
* fix: openai response tool use

- Added OpenAIResponseStreamListener interface for handling OpenAI response streams.
- Implemented attachRawStreamListener method in OpenAIResponseAPIClient to manage raw output.
- Updated RawStreamListenerMiddleware to integrate OpenAI response handling.
- Refactored BaseApiClient to remove unused attachRawStreamListener method.
- Improved buildSdkMessages to handle OpenAI response formats.

* fix: remove logging from StreamAdapterMiddleware

- Removed Logger.info call from StreamAdapterMiddleware to streamline output and reduce unnecessary logging.

* fix: update attachRawStreamListener to return a Promise

- Changed attachRawStreamListener method in OpenAIResponseAPIClient to be asynchronous, returning a Promise for better handling of raw output.
- Updated RawStreamListenerMiddleware to await the result of attachRawStreamListener, ensuring proper flow of data handling.

* refactor: enhance attachRawStreamListener to return a ReadableStream

- Updated the attachRawStreamListener method in OpenAIResponseAPIClient to return a ReadableStream, allowing for more efficient handling of streamed responses.
- Modified RawStreamListenerMiddleware to accommodate the new return type, ensuring proper integration of the transformed stream into the middleware flow.

* refactor: update getResponseChunkTransformer to accept CompletionsContext

- Modified the getResponseChunkTransformer method in BaseApiClient and its implementations to accept a CompletionsContext parameter, enhancing the flexibility of response handling.
- Adjusted related middleware and client classes to ensure compatibility with the new method signature, improving the overall integration of response transformations.

* refactor: update getResponseChunkTransformer to accept CompletionsContext

- Modified the getResponseChunkTransformer method in AihubmixAPIClient to accept a CompletionsContext parameter, enhancing the flexibility of response handling.
- Ensured compatibility with the updated method signature across related client classes.
2025-06-19 12:24:27 +08:00
SuYao
deac7de5aa fix(ApiService): improve error handling when fetching tools from MCP servers (#7340)
- Added error handling for tool fetching to log errors and return an empty array if a server fails to respond.
- Changed from Promise.all to Promise.allSettled to ensure all tool fetching attempts are accounted for, filtering out any rejected promises.
2025-06-19 12:02:03 +08:00
Teo
6996cdfbf9 fix: the issue where anchor clicks in multi-model responses fail to redirect (#7342)
* fix: 修复多模型回答的锚点点击无法跳转问题

* chore(Messages): remove debug logging from MessageAnchorLine component
2025-06-19 11:23:42 +08:00
Wang Jiyuan
8c9822cc71 Fix: Handle embedding dimension retrieval failure when creating knowledge base (#7324)
* fix(知识库): 处理获取嵌入维度为0时的错误情况

* fix(aiCore): 修复获取嵌入维度时错误处理不当的问题

修改各AI客户端获取嵌入维度的方法,在出错时抛出异常而不是返回0
同时在调用处移除对返回值为0的特殊处理,直接捕获异常

* refactor(aiCore): 移除获取嵌入维度的冗余try-catch块

简化代码结构,移除不必要的错误处理,因为错误会由上层调用者处理
2025-06-19 02:03:31 +08:00
SuYao
d05ff5ce48 fix(AnthropicAPIClient): non stream tooluse (#7338)
- Added debug logging in buildSdkMessages for better traceability.
- Improved handling of tool calls in the transform method to correctly index multiple tool uses.
- Enqueued additional response types to enhance the output structure for better integration with the streaming API.
- Refactored event listener attachment for clarity and maintainability.
2025-06-19 01:11:15 +08:00
purefkh
ccff6dc2b8 feat: update gemini-2.5 model capabilities and thinking budget (#7323)
Co-authored-by: suyao <sy20010504@gmail.com>
2025-06-19 00:32:49 +08:00
fullex
5ce4f91829 refactor(QuickAssistant): fix loop rendering & support context/pause/thinking block (#7336)
* fix: series bugs of quick assistant

* fix: update quick assistant ID handling and improve error management in HomeWindow

* refactor(HomeWindow, Messages): streamline clipboard handling and improve component structure

- Removed unused imports and hotkey functionality from Messages component.
- Refactored clipboard management in HomeWindow to use refs for better performance.
- Enhanced user input handling and state management in HomeWindow.
- Updated InputBar to accept assistant prop instead of model for better clarity.
- Improved Footer component to handle copy functionality and pin state more effectively.

* Enhance Footer component: add rotation animation to pin icon and adjust margin

- Updated the Pin icon in the Footer component to include a rotation animation based on the pin state.
- Adjusted the margin of the PinButtonArea for improved layout consistency.

* refactor(HomeWindow): improve clipboard handling and input placeholder logic

- Updated clipboard reading logic to check for document focus in addition to startup settings.
- Consolidated key event handling to streamline input processing.
- Enhanced placeholder logic in InputBar to reflect the current assistant's name or model more accurately.
2025-06-19 00:14:32 +08:00
one
757eed1617 fix(OpenAI): respect successful stream without finish reason (#7326)
* fix(OpenAI): respect successful stream without finish reason

* fix: lint errors
2025-06-18 23:19:25 +08:00
SuYao
333cc7b5a8 fix: lint (#7333) 2025-06-18 23:14:54 +08:00
SuYao
91a936c151 fix: initialize messageContents and improve message handling in GeminiAPIClient; add new Gemini model to configuration (#7307)
* fix: initialize messageContents and improve message handling in GeminiAPIClient; add new Gemini model to configuration

* refactor: streamline message handling in GeminiAPIClient; enhance message extraction from SDK payload
2025-06-18 17:40:46 +08:00
beyondkmp
d409ac1b73 feat: Add app data path selection and relaunch functionality (#6096)
* feat: Add app data path selection and relaunch functionality

* Introduced new IPC channels for selecting and setting the app data path.
* Implemented logic to initialize the app data path on startup.
* Added confirmation modal for changing the app data directory in the settings.
* Updated translations for new app data path features in multiple languages.

* feat: Implement user data copying and app data path management

* Added IPC channels for copying user data to a new location and setting the app data path.
* Enhanced the user interface to support data copying with progress notifications.
* Updated translations to reflect new features related to app data management.
* Refactored file utility functions to streamline data path handling.

* refactor: update IPC channel names and streamline app data path handling

- Renamed IPC channels for selecting app data path and copying user data for clarity.
- Simplified the logic for selecting and setting app data paths, removing unnecessary success/error handling.
- Updated related functions and components to reflect the new IPC channel names and improved data handling.
- Removed unused copyUserDataToNewLocation function to clean up the codebase.

* fix: update app data directory selection text in multiple locales

- Changed the text for selecting the app data directory from "Select Directory" to "Modify Directory" in English, Japanese, Russian, Simplified Chinese, and Traditional Chinese locales to better reflect the action being performed.

* refactor: remove redundant success messages in DataSettings component

- Eliminated unnecessary success messages related to app data copying and app relaunching to streamline user feedback and improve code clarity.

* refactor: streamline file utility functions and update app data initialization

- Moved `getDataPath` function to the `utils/index.ts` for better organization and accessibility.
- Renamed `initUserDataDir` to `initAppDataDir` for clarity in its purpose.
- Removed commented-out code in `ConfigManager` to enhance code cleanliness.

* refactor: update import paths and localization strings for app data

- Refactored import statements for `getDataPath` to streamline utility access.
- Updated localization strings for app data in English, Japanese, Russian, Simplified Chinese, and Traditional Chinese to enhance clarity and consistency.

* update i18n

* add fc

* fix: handle errors in app data path retrieval

- Added error handling to the `getAppDataPathFromConfig` function to return null if the configuration file cannot be read or parsed, improving robustness.

* refactor: simplify app data path handling in IPC

- Removed error handling for setting the app data path in the IPC channel, streamlining the process by directly updating the configuration and user data path without try-catch blocks.

* fix: update userData path handling for portable applications

- Modified the initAppDataDir function to set the userData path based on the PORTABLE_EXECUTABLE_DIR environment variable, ensuring compatibility with portable application setups.

* feat: enhance app data path migration with progress indication

- Implemented a loading modal with progress tracking during the app data path migration process.
- Added visual feedback using a progress bar to inform users of the copying status.
- Improved error handling and user notifications for successful and failed migrations.
- Refactored the modal confirmation logic to streamline user interactions during the path selection and migration process.

* feat: add migration paths and update UI for data migration process

- Introduced new translation keys for migration paths in Japanese, Russian, Simplified Chinese, and Traditional Chinese.
- Enhanced the DataSettings component with a structured layout for displaying original and new paths during data migration.
- Updated modal titles and content to improve user experience during the migration process.

* feat: enhance data migration process with improved UI and progress tracking

- Refactored the DataSettings component to streamline the data migration workflow.
- Added a new function to display progress during the migration process, enhancing user feedback.
- Updated modal logic to improve clarity and user experience when selecting new app data paths.
- Implemented error handling and notifications for successful and failed migrations.

* feat: add stop quit app functionality during data migration

- Introduced a new IPC channel to manage the application's quit behavior during data transfer.
- Updated the DataSettings component to prevent the app from quitting while migration is in progress, enhancing user experience.
- Improved modal configurations for better responsiveness and visual appeal.

* feat: enhance app data path handling and localization updates

- Updated IPC handler to use 'filePath' for clarity in app data path management.
- Improved validation to ensure the new app data path is not the root path, enhancing user feedback during path selection.
- Added new translation keys for error messages related to app data path selection in English, Japanese, Russian, Simplified Chinese, and Traditional Chinese, improving localization support.

* feat: add write permission check and enhance quit prevention during data migration

- Introduced a new IPC channel to check write permissions for the app data path.
- Updated the DataSettings component to validate write permissions before proceeding with data migration.
- Enhanced the quit prevention logic to include a reason for blocking the app from quitting during data transfer.
- Added new localization keys for error messages related to write permissions in multiple languages, improving user feedback.

* feat: enhance confirmation modal in DataSettings component

- Updated the confirmation modal to include danger styling for the OK button, improving visual feedback.
- Added localized text for the OK and Cancel buttons, enhancing user experience through better accessibility.

* feat: add localization keys and improve quit prevention during data migration

- Added new localization keys for data migration, including titles and original path labels, enhancing user experience.
- Updated the DataSettings component to ensure the app does not quit during data migration, improving reliability and user feedback.

* feat(DataSettings): add data copying option and update related messages

- Introduced a switch to allow users to choose whether to copy data from the original directory when changing the app data path.
- Updated user notifications and progress messages to reflect the new functionality, including warnings about data copying.
- Enhanced localization files for multiple languages to include new strings related to data copying options and notifications.

* fix(DataSettings): set default for data copying option to true

- Updated the DataSettings component to set the default state of the data copying option to true.
- Added a new CopyDataContent component to enhance the user interface by displaying the data copying option alongside the existing path settings.
- Improved layout by ensuring proper spacing and alignment for better user experience.

---------

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>
2025-06-18 17:39:26 +08:00
GuanMu
9e8f14c9d3 fix: update dify icon (#7301)
* fix: update dify icon

* fix: 更新dify图标尺寸和视图框
2025-06-18 15:20:38 +08:00
fullex
e05eba2450 feat: toggle Selection Assistant on tray menu (#7286)
feat: toggle SelectionService on tray
2025-06-18 00:02:28 +08:00
SuYao
df2bcec768 fix: update buildSdkMessages to handle undefined output in API clients (#7293)
* fix: update buildSdkMessages to handle undefined output in API clients

* fix: update vision model check to include model name in regex validation
2025-06-17 23:11:12 +08:00
shiquda
0bf98cce9e feat: Add pricing configuration and display for models (#3125)
* feat: Add pricing configuration and display for models

- Introduce model pricing fields in ModelEditContent
- Add price calculation and display in MessageTokens
- Update localization files with price-related translations
- Extend Model type with optional pricing information

* fix: Correct currency symbol placement in message token pricing display

* feat: Add custom currency support in model pricing configuration

- Introduce custom currency option in ModelEditContent
- Update localization files with custom currency translations
- Enhance currency symbol selection with custom input
- Improve input styling for pricing configuration

* fix(OpenAIProvider): ensure messages.content of the request is string

* Update ModelEditContent.tsx

* fix(model-price): remove duplicate button

* fix: build error

---------

Co-authored-by: 自由的世界人 <3196812536@qq.com>
2025-06-17 22:53:47 +08:00
MyPrototypeWhat
45ec069dce fix: refactor provider middleware (#7164) 2025-06-17 21:20:52 +08:00
Chen Tao
006f134647 fix: use rewrite to search knowledge (#7289) 2025-06-17 21:02:09 +08:00
Ivan Hanloth
804f9235cd fix: classify agents as Chinese and English (#7287)
* feat: Create i18n for agents in Chinese

* fix: enhance agent loading by supporting language-specific agent files

---------

Co-authored-by: Pleasurecruise <3196812536@qq.com>
2025-06-17 20:05:44 +08:00
自由的世界人
5d9fc292b7 fix: add Markdown preview option in translation settings (#7250) 2025-06-17 14:42:27 +08:00
fullex
37dac7f6ea fix: unified the behavior of SendMessage shortcut (#7276) 2025-06-17 14:38:05 +08:00
koinin
68c1a3e1cc Update models.ts, fix doubao-seed-1-6 (#7274)
* Update models.ts, fix doubao-seed-1-6

* fix2 doubao-seed-1-6
2025-06-17 12:59:18 +08:00
George·Dong
8459e53e39 fix(MessageMenubar): add "copy plain text" control (#7261)
* fix(MessageMenubar): add "copy plain text" control

* fix(migrate): add default plain_text export option in v114
2025-06-17 12:43:36 +08:00
fullex
26597816e5 fix(Inputbar): handle Enter key press correctly during composition (#7269) 2025-06-17 10:28:34 +08:00
Kingsword
b8b1083921 fix(PromptPopup): Textarea overflow causes modal's close button unclickable (#7266)
fix(PromptPopup): Textarea overflow causes modal's close button  unclickable.
2025-06-17 08:46:24 +08:00
Chen Tao
f19ba44574 fix: support tei (#7239)
fix: support mis-tei
2025-06-16 23:52:29 +08:00
chenxue
050bfe1380 [功能]: aihubmix 更新默认模型 (#7242)
Update models.ts

Co-authored-by: zhaochenxue <zhaochenxue@bixin.cn>
2025-06-16 23:44:59 +08:00
自由的世界人
1b5cba94d2 fix: modify siliconflow text-to-image available models (#7165)
* fix: remove painting provider

* Update PaintingsRoutePage.tsx

* fix: text to image models
2025-06-16 23:44:11 +08:00
SuYao
dbd75912aa Feat/vertex ai support (#6416)
* WIP

* feat: integrate Vertex AI support and enhance service account configuration

- Added Vertex AI service integration with authentication via service accounts.
- Implemented IPC channels for Vertex AI authentication and cache management.
- Updated UI components to support service account configuration, including private key and client email fields.
- Enhanced localization for Vertex AI settings in multiple languages.
- Refactored AiProvider to support dynamic provider creation for Vertex AI.
- Updated Redux store to manage Vertex AI settings and service account information.

* chore: remove debug script from package.json and clean up console log in main process

* fix: ensure async handling in useKnowledge hook for base parameters

- Updated the useKnowledge hook to await the result of getKnowledgeBaseParams when removing items, ensuring proper asynchronous behavior.

* fix: ensure async handling in KnowledgeQueue for base parameters

* fix(i18n): add English prompt placeholder to Russian localization

* chore(yarn): update yarn.lock and patch for @google/genai

* fix(AihubmixPage): update AI provider instantiation to use async create method

* refactor: update VertexAPIClient import and class definition

- Changed import statement for VertexAPIClient to use named import.
- Updated VertexProvider class to VertexAPIClient for consistency with naming conventions.

* refactor: update AiProvider instantiation across components

- Replaced the use of AiProvider.create() with the new AiProvider() constructor in AddKnowledgePopup, AihubmixPage, SiliconPage, and KnowledgeService for consistency and improved clarity.

* refactor: simplify getKnowledgeBaseParams and update API key checks

- Changed getKnowledgeBaseParams to a synchronous function for improved performance.
- Updated API key validation logic to remove unnecessary checks for 'vertexai' provider type across multiple functions.

* feat: add Cephalon provider configuration with API and website links

- Introduced a new provider configuration for Cephalon, including API URL and various website links for official resources, API key, documentation, and models.

* refactor: streamline API call in AddKnowledgePopup component

- Removed unnecessary await from the create API call in the AddKnowledgePopup component, improving code clarity and performance.

* refactor: remove unnecessary await from getKnowledgeBaseParams call

- Simplified the searchKnowledgeBase function by removing the await from getKnowledgeBaseParams, enhancing performance and code clarity.

* refactor: remove externalLiveBindings option from Rollup output configuration in electron.vite.config.ts
2025-06-16 21:46:27 +08:00
beyondkmp
9b321af3da fix: enhance AppUpdater with IP country detection (#7235)
* fix: downgrade version in package.json and enhance AppUpdater with IP country detection

- Downgraded the application version from 1.4.2 to 1.4.1 in package.json.
- Added a new private method `_getIpCountry` in AppUpdater to fetch the user's IP country with a timeout mechanism.
- Updated the `setAutoUpdate` method to adjust the feed URL based on the detected country, improving update handling for users outside of China.

* fix: adjust timeout duration and enhance IP country logging in AppUpdater

* fix: extend timeout duration in AppUpdater for improved fetch reliability

---------

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>
2025-06-16 19:25:14 +08:00
jwcrystal
d061cdb3ef feat: add quick assistant settings panel and management functionality (#6201)
* feat: add quick assistant settings panel and management functionality

- Create QuickAssistantSettings component for UI
- Extend useAssistant hook with quick assistant controls
- Add settings button in ModelSettings page
- Implement temperature, context count, max tokens, and other parameters
- Connect settings to store via updateQuickAssistant action

Separate quick assistant preferences from default assistant settings for better customization.

* refactor(QuickAssistantSettings): remove maxTokens and refine UI layout

- Removed maxTokens related state, logic, and UI elements
- Simplified settings page by eliminating unused configuration
- Adjusted layout for Slider and InputNumber for better usability
- Removed fixed width from Modal to enable responsive behavior

* refactor(HomeWindow): optimize message building logic

- Removed redundant quickAssistant fetching logic
- Use `useQuickAssistant` hook directly for cleaner code
- Simplified message content concatenation method

* style(QuickAssistantSettings): Adjust spacing in settings page layout

Change the column width of sliders and input fields from 20/4 to 21/3 for a more reasonable layout
Also set the popup width to 800px to improve user experience

* feat(Quick Assistant): Add option to select assistant or model, and optimize Quick Assistant logic

- Added functionality to choose between using models or referencing other assistants
- Optimized model selection logic to automatically select based on settings
- Added relevant internationalization texts

* fix(HomeWindow): Dynamically display input box placeholder text based on quick assistant states

* refactor(QuickAssistant): remove the implement of the quick assistant feature and restructure related logic

- Remove code related to the quick assistant feature, including the useQuickAssistant hook, QuickAssistantSettings component, and associated store logic.
- Restructure the HomeWindow component to use default or specified assistants instead of the quick assistant functionality, simplifying the code structure.

* refactor(QuickAssistant): Remove custom default model for quick assistant and switch to default assistant

- Refactor quick assistant functionality, remove independent model settings, change to select via assistant ID
- Update multilingual translation text to match new features

* refactor(QuickAssistant): Remove quick assistant-related states and simplify logic

- Remove unused quick assistant states and toggle functionality, simplifying related logic
- Update multilingual files to match the new default model and assistant labels

* refactor(i18n): Unify translation keys for input field placeholders

Unify the placeholder translation keys from `model_empty` and `assistant_empty` into empty across different scenarios, streamlining code logic

* refactor(settings): simplify quick helper selection logic by directly using the preset helper

- Removed redundant helper filtering logic, directly using the preset helper as the quick helper
2025-06-16 18:13:35 +08:00
Wang Jiyuan
97fb24e060 fix: reranker i18n (#7251) 2025-06-16 17:44:10 +08:00
LANYUN
7a035c5734 feat: Add new provider Lanyun Cloud MaaS (#7033)
* Add files via upload

添加蓝耘logo图片

* 添加lanyun api及站点信息

* fix:修改引号

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-06-16 17:29:16 +08:00
one
eb89ca5415 fix: gemini generateImage model detection (#7241)
* fix: gemini generateImage model detection

* refactor: use base name for websearch model detection
2025-06-16 13:06:52 +08:00
SuYao
eb650aa586 fix: enable stream output in assistant settings for chat completion (#7240) 2025-06-16 12:51:09 +08:00
自由的世界人
ce32fd32b6 fix: include image files in block retrieval for improved file handling (#7231) 2025-06-16 12:04:45 +08:00
Murphy
00e395f252 feat: Add PDF file support for OpenAI vision models (#7217)
* feat: add base64 PDF support for OpenAI vision models

Signed-off-by: MurphyLo <1335758958@qq.com>

* sort imports in OpenAIResponseAPIClient.ts

* sort imports in OpenAIResponseAPIClient.ts

* remove pdf-parse

* modify pdfPageCount implementation to use officeparser built-in pdf.js

* chore: update yarn.lock to remove pdf-parse dependency

---------

Signed-off-by: MurphyLo <1335758958@qq.com>
Co-authored-by: suyao <sy20010504@gmail.com>
2025-06-16 11:09:51 +08:00
fullex
b6b1b43094 fix(SelectionService): Win10 showing problem & AlwaysOnTop level (#7215)
refactor(SelectionService): enhance logging and adjust window behavior for Windows compatibility

- Updated logInfo method to include a forceShow parameter for improved logging control.
- Ensured toolbar window is set to always on top when shown.
- Commented out setOpacity calls to prevent transparency issues on Windows 10.
2025-06-16 09:54:20 +08:00
自由的世界人
68ae88dc1b fix: prevent update button from rendering when auto-check for updates… (#7212)
fix: prevent update button from rendering when auto-check for updates is disabled
2025-06-16 00:22:38 +08:00
George·Dong
acf78e8383 refactor: optimize notion export (#7228)
* fix(export): Initial fix for the multi-level list export issue in Notion

* fix(getMessageTitle): optimize loading message

* refactor(notion export): optimize notion export

- import notion-helper
- strengthen the robustness of the Notion Export function

* fix(i18n): optimize notion export infos
2025-06-15 23:18:36 +08:00
Wang Jiyuan
bd87b8a002 feat: use variables in topic naming and improve default prompt (#7083)
* feat: use variables in topic naming

* feat: use structured conversation string

* feat: add i18n

* feat: add i18n

* feat: implement summaries for other providers

* fix: adjust new version

* feat: Structure the conversation as a JSON string when naming the topic

* fix: improve logic

* fix: improve prompts

* update fetchMessageSummary
2025-06-15 22:40:37 +08:00
kangfenmao
7cf7368ae3 lint(SyncServersPopup): fix SyncServersPopup lint error 2025-06-15 14:11:29 +08:00
Aichaellee
9001a96fff feat:add lanyun mcp server 2025-06-15 11:17:02 +08:00
Wang Jiyuan
9ea4d1f99f fix: send message shortcut doesn't work when editing existing message (#6934)
* fix: send message shortcut doesn't work when editing existing message

* fix: resend shortcut only apply on user msg
2025-06-14 23:11:52 +08:00
Chen Tao
fc62a5bdc2 fix: 7127 (#7196) 2025-06-14 23:01:45 +08:00
one
06b543039f chore(ci): remove --fix from lint (#7159)
* chore(ci): remove --fix from lint

* fix: lint errors
2025-06-14 22:58:49 +08:00
Doekin
1c354ffa0a fix(ImageGenerationMiddleware): correctly process image URLs (#7198) 2025-06-14 22:39:32 +08:00
one
163e28d9ba fix(model): qwen3 model detection (#7201) 2025-06-14 21:24:34 +08:00
beyondkmp
fd9ff4a432 fix: update app-builder-lib patch and adjust minimumSystemVersion handling (#7197)
- Updated the resolution and checksum for the app-builder-lib patch in yarn.lock.
- Modified macPackager.js and updateInfoBuilder.js to correctly reference LSMinimumSystemVersion.
- Enhanced ArchiveTarget.js and NsisTarget.js to include minimumSystemVersion in updateInfo if specified.
2025-06-14 19:39:28 +08:00
beyondkmp
cab975f88b fix: update app-builder-lib patch and add excludeReBuildModules option (#7193) 2025-06-14 15:57:39 +08:00
Wang Jiyuan
c644e4afa8 feat: add prompt variables docs on topic naming modal popup (#7175) 2025-06-14 14:59:29 +08:00
Wang Jiyuan
0a498460d6 fix: remove margin-bottom for loading animation (#7191)
* fix: remove margin-bottom for loading animation

* fix: just need to remove the margin-bottom of the last block
2025-06-14 14:57:31 +08:00
Wang Jiyuan
bd4333ab9a fix: transparent background on translate dropdown (#7189) 2025-06-14 14:18:25 +08:00
Wang Jiyuan
9138aecdf0 fix: missing topic prompt on resend/regenerate and duplicate prevention (#7173)
* fix: completion doesn't include topic prompt

* fix: Multiple additions of topic prompts

* fix: improve logic

* fix: improve logic
2025-06-14 13:37:48 +08:00
Wang Jiyuan
e4e4dcbd1e fix: model_name prompt var always use default model (#7178)
* fix: model_name prompt var always use default mode

* fix: incorrect model name
2025-06-14 13:35:32 +08:00
kangfenmao
2a0484ede2 chore(release): update fetch depth in GitHub Actions workflow
- Changed the fetch depth to 0 in the release workflow to ensure all history is available for tagging. This adjustment improves the accuracy of the release process.
2025-06-14 13:18:59 +08:00
Wang Jiyuan
c9f12c2e49 feat: add prompt variable "username" (#7174) 2025-06-14 13:08:32 +08:00
fullex
27354d82e2 fix(SelectionAssistant): make add custom action button bigger (#7185)
fix: make add custom action button bigger
2025-06-14 11:43:13 +08:00
beyondkmp
f5e1885ffa chore(electron.vite.config): update Rollup configuration for single file packaging (#7183)
- Modified the Rollup options to disable code splitting and enable inline dynamic imports, ensuring a single file output for the build process. This change optimizes the packaging of the Electron application.
2025-06-14 10:01:47 +08:00
beyondkmp
afc4731b9d feat: clean up Windows license files (#7133)
* feat: enable minification in build configurations and clean up Windows license files

- Added minification option to the build configurations in electron.vite.config.ts to optimize output size.
- Updated after-pack.js to remove unnecessary license files on Windows, improving the packaging process.

* refactor: remove minification from build configurations in electron.vite.config.ts

- Eliminated the minification option from the build settings in electron.vite.config.ts to streamline the build process.
- This change may improve build times and simplify configuration management.

---------

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>
2025-06-14 08:01:36 +08:00
MyPrototypeWhat
9411866727 refactor(ImageBlock): enhance loading state presentation and improve … (#7160)
* refactor(ImageBlock): enhance loading state presentation and improve layout responsiveness

- Wrapped the loading spinner in a new SpinnerWrapper for better alignment and presentation during streaming and processing states.
- Updated the ImageBlockGroup to use `repeat(auto-fit, minmax(...))` for more flexible grid layout, improving responsiveness across different screen sizes.

These changes enhance the user experience by providing a clearer loading indication and a more adaptable layout for image blocks.

* style(ImageBlockGroup): comment out child styling for future adjustments

- Commented out the child styling rules in ImageBlockGroup to allow for potential layout modifications without removing the code entirely.
- This change prepares the component for further enhancements while maintaining existing functionality.

* refactor(ImageBlock): replace loading spinner with Ant Design Skeleton component

- Updated the loading state presentation in ImageBlock by replacing the custom spinner with Ant Design's Skeleton component for a more consistent UI experience.
- Removed the SpinnerWrapper and simplified the return statement for better readability.
- This change enhances the visual feedback during image loading while maintaining the component's functionality.

---------

Co-authored-by: lizhixuan <zhixuan.li@banosuperapp.com>
2025-06-13 17:55:40 +08:00
one
c7fd1ac373 fix(TopicRenaming): captured activeTopic.id is outdated and causes accidental topic changing after renaming (#7157)
* fix(TopicRenaming): captured activeTopic.id is outdated and causes accidental topic changing after renaming

* fix: prevent topic changing on auto renaming

* fix: filter out main text on summarizing
2025-06-13 17:24:24 +08:00
one
faf14ff10b fix(MermaidPreview): re-render mermaid on display change (#7058)
* fix(MermaidPreview): re-render mermaid on display change

* test: add tests for MermaidPreview
2025-06-13 13:52:50 +08:00
one
3b3b3c961e refactor(CodeEditor): remove the right border of gutters (#7137)
refactor: remove the right border of gutters
2025-06-13 11:02:22 +08:00
beyondkmp
06d495c7e1 feat: Enhance AppUpdater for Windows installation directory support (#7135)
- Added support for setting the installation directory for the autoUpdater on Windows using NsisUpdater.
- Imported the 'path' module to dynamically determine the installation path based on the executable location.
- This change improves the updater's functionality and ensures a smoother installation experience for Windows users.

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>
2025-06-13 10:52:25 +08:00
beyondkmp
922e142079 feat: Reduce app size (#7113)
* chore: update jsdom dependency to patch version 26.1.0

- Changed jsdom version from ^26.0.0 to a patched version 26.1.0 in package.json and yarn.lock.
- Applied a specific patch to address issues with the jsdom package.

* chore: update package.json dependencies

- Removed outdated dependencies and added new ones to improve project functionality.
- Updated versions for several packages, including @strongtz/win32-arm64-msvc, os-proxy-config, and selection-hook.
- Reorganized dependencies and devDependencies for better clarity and maintenance.

* chore: update package dependencies and remove jsdom patch

- Replaced @cherrystudio/embedjs-libsql with @libsql/client and added @libsql/win32-x64-msvc and jsdom as new dependencies.
- Updated turndown version and removed the jsdom patch from the project.
- Ensured consistency in dependency versions across package.json and yarn.lock.
2025-06-13 00:56:34 +08:00
Wang Jiyuan
cdc9347011 fix: token usage always display when assistant msg generation aborted (#7121)
* fix: token usage always display when assistant msg generation aborted

* remove console.log
2025-06-13 00:48:21 +08:00
Xin Rui
e264b5b052 feat: Support reasoning control for Doubao/Mistral models. (#7116)
* feat: Support reasoning control for Doubao models.

* feat: Enhance model handling and support for Doubao and Gemini in API clients

- Added support for Doubao thinking modes in OpenAIAPIClient and GeminiAPIClient.
- Introduced GEMINI_FLASH_MODEL_REGEX for model identification.
- Updated models.ts to include new Doubao and Gemini model regex patterns.
- Added new image asset for ChatGPT in models.
- Enhanced reasoning control and token budget handling for Doubao models.
- Improved the Inputbar's ThinkingButton component to accommodate new thinking options.

---------

Co-authored-by: suyao <sy20010504@gmail.com>
2025-06-13 00:03:58 +08:00
one
28696c0dad fix: start animation only if the topic should be renamed (#7125) 2025-06-12 22:43:44 +08:00
one
8689c07888 feat: animate topic renaming (#6794)
* feat: animate topic renaming

* fix: load messages before renaming a topic

* refactor: better error handling

* refactor: make function names more reasonable

* refactor: update shimmer colors

* refactor: use typing effect
2025-06-12 18:41:15 +08:00
one
aa0b7ed1a8 feat(Markdown): customize table to support source copying (#7019)
* feat(Markdown): customize table to support source copying

- add a customized table component
- update ChatNavigation excluded selectors

* refactor: remove redundant feedback

* test: add tests for Table
2025-06-12 16:28:28 +08:00
MyPrototypeWhat
5f4d73b00d feat: add middleware support for provider (#6176)
* feat: add middleware support for OpenAIProvider with logging capabilities

- Introduced middleware functionality in OpenAIProvider to enhance completions processing.
- Created AiProviderMiddlewareTypes for defining middleware interfaces and contexts.
- Implemented sampleLoggingMiddleware for logging message content and processing times.
- Updated OpenAIProvider constructor to accept middleware as an optional parameter.
- Refactored completions method to utilize middleware for improved extensibility and logging.

* refactor: streamline OpenAIProvider initialization and middleware application

- Removed optional middleware parameter from OpenAIProvider constructor for simplicity.
- Refactored ProviderFactory to create instances of providers and apply logging middleware consistently.
- Enhanced completions method visibility by changing it from private to public.
- Cleaned up unused code related to middleware handling in OpenAIProvider.

* feat: enhance AiProvider with new middleware capabilities and completion context

- Added public getter for provider info in BaseProvider.
- Introduced finalizeSdkRequestParams hook for middleware to modify SDK-specific request parameters.
- Refactored completions method in OpenAIProvider to accept a context object, improving middleware integration.
- Updated middleware types to include new context structure and callback functions for better extensibility.
- Enhanced logging middleware to utilize new context structure for improved logging capabilities.

* refactor: enhance middleware structure and context handling in AiProvider

- Updated BaseProvider and AiProvider to utilize AiProviderMiddlewareCompletionsContext for completions method.
- Introduced new utility functions for middleware context creation and execution.
- Refactored middleware application logic to improve extensibility and maintainability.
- Replaced sampleLoggingMiddleware with a more robust LoggingMiddleware implementation.
- Added new context management features for better middleware integration.

* refactor: update AiProvider and middleware structure for improved completions handling

- Refactored BaseProvider and AiProvider to change completions method signature from context to params.
- Removed unused AiProviderMiddlewareCompletionsContext and related code for cleaner implementation.
- Enhanced middleware configuration by introducing a dedicated middleware registration file.
- Implemented logging middleware for completions to improve observability during processing.
- Streamlined middleware application logic in ProviderFactory for better maintainability.

* docs: 添加中间件编写指南文档

- 新增《如何为 AI Provider 编写中间件》文档,详细介绍中间件架构、类型及编写示例。
- 说明了中间件的执行顺序、注册方法及最佳实践,旨在帮助开发者有效创建和维护中间件。

* refactor: update completions method signatures and introduce CompletionsResult type

- Changed the completions method signature in BaseProvider and AiProvider to return CompletionsResult instead of void.
- Added CompletionsResult type definition to encapsulate streaming and usage metrics.
- Updated middleware and related components to handle the new CompletionsResult structure, ensuring compatibility with existing functionality.
- Introduced new middleware for stream adaptation to enhance chunk processing during completions.

* refactor: enhance AiProvider middleware and streaming handling

- Updated CompletionsResult type to support both OpenAI SDK stream and ReadableStream.
- Modified CompletionsMiddleware to return CompletionsResult, improving type safety.
- Introduced StreamAdapterMiddleware to adapt OpenAI SDK streams to application-specific chunk streams.
- Enhanced logging in CompletionsLoggingMiddleware to capture and return results from next middleware calls.

* refactor: update AiProvider and middleware for OpenAI completions handling

- Renamed CompletionsResult to CompletionsOpenAIResult for clarity and updated its structure to support both OpenAI SDK and application-specific streams.
- Modified completions method signatures in AiProvider and OpenAIProvider to return CompletionsOpenAIResult.
- Enhanced middleware to process and adapt OpenAI SDK streams into standard chunk formats, improving overall streaming handling.
- Introduced new middleware components: FinalChunkConsumerAndNotifierMiddleware and OpenAISDKChunkToStandardChunkMiddleware for better chunk processing and logging.

* 删除 ExtractReasoningCompletionsMiddleware.ts 文件,清理未使用的中间件代码以提高代码整洁性和可维护性。

* refactor: consolidate middleware types and improve imports

- Replaced references to AiProviderMiddlewareTypes with the new middlewareTypes file across various middleware components for better organization.
- Introduced TextChunkMiddleware to enhance chunk processing from OpenAI SDK streams.
- Cleaned up imports in multiple files to reflect the new structure, improving code clarity and maintainability.

* feat: enhance abort handling with AbortController in middleware chain

- Update CompletionsOpenAIResult interface to use AbortController instead of AbortSignal
- Modify OpenAIProvider to pass abortController in completions method return
- Update AbortHandlerMiddleware to use controller from upstream result
- Improve abort handling flexibility by exposing full controller capabilities
- Enable middleware to actively control abort operations beyond passive monitoring

This change provides better control over request cancellation and enables
more sophisticated abort handling patterns in the middleware pipeline.

* refactor: enhance AiProvider and middleware for improved completions handling

- Updated BaseProvider to expose additional methods and properties, including getMessageParam and createAbortController.
- Modified OpenAIProvider to streamline completions processing and integrate new middleware for tool handling.
- Introduced TransformParamsBeforeCompletions middleware to standardize parameter transformation before completions.
- Added McpToolChunkMiddleware for managing tool calls within the completions stream.
- Enhanced middleware types to support new functionalities and improve overall structure.

These changes improve the flexibility and maintainability of the AiProvider and its middleware, facilitating better handling of OpenAI completions and tool interactions.

* refactor: enhance middleware for recursive handling and internal state management

- Introduced internal state management in middleware to support recursive calls, including enhanced dispatch functionality.
- Updated middleware types to include new internal fields for managing recursion depth and call status.
- Improved logging for better traceability of recursive calls and state transitions.
- Adjusted various middleware components to utilize the new internal state, ensuring consistent behavior during recursive processing.

These changes enhance the middleware's ability to handle complex scenarios involving recursive calls, improving overall robustness and maintainability.

* fix(OpenAIProvider): return empty object for missing sdkParams in completions handling

- Updated OpenAIProvider to return an empty object instead of undefined when sdkParams are not found, ensuring consistent return types.
- Enhanced TransformParamsBeforeCompletions middleware to include a flag for built-in web search functionality based on assistant settings.

* refactor(OpenAIProvider): enhance completions handling and middleware integration

- Updated the completions method in OpenAIProvider to include an onChunk callback for improved streaming support.
- Enabled the ThinkChunkMiddleware in the middleware registration for better handling of reasoning content.
- Increased the maximum recursion depth in McpToolChunkMiddleware to prevent infinite loops.
- Refined TextChunkMiddleware to directly enqueue chunks without unnecessary type checks.
- Improved the ThinkChunkMiddleware to better manage reasoning tags and streamline chunk processing.

These changes enhance the overall functionality and robustness of the AI provider and middleware components.

* feat(WebSearchMiddleware): add web search handling and integration

- Introduced WebSearchMiddleware to process various web search results, including annotations and citations, and generate LLM_WEB_SEARCH_COMPLETE chunks.
- Enhanced TextChunkMiddleware to support link conversion based on the model and assistant settings, improving the handling of TEXT_DELTA chunks.
- Updated middleware registration to include WebSearchMiddleware for comprehensive search result processing.

These changes enhance the AI provider's capabilities in handling web search functionalities and improve the overall middleware architecture.

* fix(middleware): improve optional chaining for chunk processing

- Updated McpToolChunkMiddleware and ThinkChunkMiddleware to use optional chaining for accessing choices, enhancing robustness against undefined values.
- Removed commented-out code in ThinkChunkMiddleware to streamline the chunk handling process.

These changes improve the reliability of middleware when processing OpenAI API responses.

* feat(middleware): enhance AbortHandlerMiddleware with recursion handling

- Added logic to detect and handle recursive calls, preventing unnecessary creation of AbortControllers.
- Improved logging for better visibility into middleware operations, including recursion depth and cleanup processes.
- Streamlined cleanup process for non-stream responses to ensure resources are released promptly.

These changes enhance the robustness and efficiency of the AbortHandlerMiddleware in managing API requests.

* docs(middleware): 迁移步骤

* feat(middleware): implement FinalChunkConsumerMiddleware for usage and metrics accumulation

- Introduced FinalChunkConsumerMiddleware to replace the deprecated FinalChunkConsumerAndNotifierMiddleware.
- This new middleware accumulates usage and metrics data from OpenAI API responses, enhancing tracking capabilities.
- Updated middleware registration to utilize the new FinalChunkConsumerMiddleware, ensuring proper integration.
- Added support for handling recursive calls and improved logging for better debugging and monitoring.

These changes enhance the middleware's ability to manage and report usage metrics effectively during API interactions.

* refactor(migrate): update API request and response structures to TypeScript types

- Changed the definitions of `CoreCompletionsRequest` and `Chunk` to use TypeScript types instead of Zod Schemas for better type safety and clarity.
- Updated middleware and service classes to handle the new `Chunk` type, ensuring compatibility with the revised API client structure.
- Enhanced the response processing logic to standardize the handling of raw SDK chunks into application-level `Chunk` objects.
- Adjusted middleware to consume the new `Chunk` type, streamlining the overall architecture and improving maintainability.

These changes facilitate a more robust and type-safe integration with AI provider APIs.

* feat(AiProvider): implement API client architecture

- Introduced ApiClientFactory for creating instances of API clients based on provider configuration.
- Added BaseApiClient as an abstract class to provide common functionality for specific client implementations.
- Implemented OpenAIApiClient for OpenAI and Azure OpenAI, including request and response handling.
- Defined types and interfaces for API client operations, enhancing type safety and clarity.
- Established middleware schemas for standardized request processing across AI providers.

These changes lay the groundwork for a modular and extensible API client architecture, improving the integration of various AI providers.

* refactor(StreamAdapterMiddleware): simplify stream adaptation logic

- Updated StreamAdapterMiddleware to directly use AsyncIterable instead of wrapping it with rawSdkChunkAdapter, streamlining the adaptation process.
- Modified asyncGeneratorToReadableStream to accept AsyncIterable, enhancing its flexibility and usability.

These changes improve the efficiency of stream handling in the middleware.

* refactor(AiProvider): simplify ResponseChunkTransformer interface and streamline OpenAIApiClient response handling

- Changed ResponseChunkTransformer from an interface to a type for improved clarity and simplicity.
- Refactored OpenAIApiClient to streamline the response transformation logic, reducing unnecessary complexity in handling tool calls and reasoning content.
- Enhanced type safety by ensuring consistent handling of optional properties in response processing.

These changes improve the maintainability and readability of the codebase while ensuring robust response handling in the API client.

* doc(technicalArchitecture): add comprehensive documentation for AI Provider architecture

* feat(architecture): introduce AI Core Design documentation and middleware specification

- Added a comprehensive technical architecture document for the new AI Provider (`aiCore`), outlining core design principles, component details, and execution flow.
- Established a middleware specification document to define the design, implementation, and usage of middleware within the `aiCore` module, promoting a flexible and maintainable system.
- These additions provide clarity and guidance for future development and integration of AI functionalities within Cherry Studio.

* refactor(middleware): consolidate and enhance middleware architecture

- Removed deprecated extractReasoningMiddleware and integrated its functionality into existing middleware.
- Streamlined middleware registration and improved type definitions for better clarity and maintainability.
- Introduced new middleware components for handling chunk processing, web search, and reasoning tags, enhancing overall functionality.
- Updated various middleware to utilize the new structures and improve logging for better debugging.

These changes enhance the middleware's efficiency and maintainability, providing a more robust framework for API interactions.

* refactor(AiProvider): enhance API client and middleware integration

- Updated ApiClientFactory to include new SDK types for improved type safety and clarity.
- Refactored BaseApiClient to support additional parameters in the completions method, enhancing flexibility for processing states.
- Streamlined OpenAIApiClient to better handle tool calls and responses, including the introduction of new chunk types for tool management.
- Improved middleware architecture by integrating processing states and refining message handling, ensuring a more robust interaction with the API.

These changes enhance the overall maintainability and functionality of the API client and middleware, providing a more efficient framework for AI interactions.

* fix(McpToolChunkMiddleware): remove redundant logging in recursion state update

* refactor(McpToolChunkMiddleware): update tool call handling and type definitions

- Replaced ChatCompletionMessageToolCall with SdkToolCall for improved type consistency.
- Updated return types of executeToolCalls and executeToolUses functions to SdkMessage[], enhancing clarity in message handling.
- Removed unused import to streamline the code.

These changes enhance the maintainability and type safety of the middleware, ensuring better integration with the SDK.

* refactor(middleware): enhance middleware structure and type handling

- Updated middleware components to utilize new SDK types, improving type safety and clarity across the board.
- Refactored various middleware to streamline processing logic, including enhanced handling of SDK messages and tool calls.
- Improved logging and error handling for better debugging and maintainability.
- Consolidated middleware functions to reduce redundancy and improve overall architecture.

These changes enhance the robustness and maintainability of the middleware framework, ensuring a more efficient interaction with the API.

* refactor(middleware): unify type imports and enhance middleware structure

- Updated middleware components to import types from a unified 'types' file, improving consistency and clarity across the codebase.
- Removed the deprecated 'type.ts' file to streamline the middleware structure.
- Enhanced middleware registration and export mechanisms for better accessibility and maintainability.

These changes contribute to a more organized and efficient middleware framework, facilitating easier future development and integration.

* refactor(AiProvider): enhance API client and middleware integration

- Updated AiProvider components to support new SDK types, improving type safety and clarity.
- Refactored middleware to streamline processing logic, including enhanced handling of tool calls and responses.
- Introduced new middleware for tool use extraction and raw stream listening, improving overall functionality.
- Improved logging and error handling for better debugging and maintainability.

These changes enhance the robustness and maintainability of the API client and middleware, ensuring a more efficient interaction with the API.

* feat(middleware): add new middleware components for raw stream listening and tool use extraction

- Introduced RawStreamListenerMiddleware and ToolUseExtractionMiddleware to enhance middleware capabilities.
- Updated MiddlewareRegistry to include new middleware entries, improving overall functionality and extensibility.

These changes expand the middleware framework, facilitating better handling of streaming and tool usage scenarios.

* refactor(AiProvider): integrate new API client and middleware architecture

- Replaced BaseProvider with ApiClientFactory to enhance API client instantiation.
- Updated completions method to utilize new middleware architecture for improved processing.
- Added TODOs for refactoring remaining methods to align with the new API client structure.
- Removed deprecated middleware wrapping logic from ApiClientFactory for cleaner implementation.

These changes improve the overall structure and maintainability of the AiProvider, facilitating better integration with the new middleware system.

* refactor(middleware): update middleware architecture and documentation

- Revised middleware naming conventions and introduced a centralized MiddlewareRegistry for better management and accessibility.
- Enhanced MiddlewareBuilder to support named middleware and streamline the construction of middleware chains.
- Updated documentation to reflect changes in middleware usage and structure, improving clarity for future development.

These changes improve the organization and usability of the middleware framework, facilitating easier integration and maintenance.

* refactor(AiProvider): enhance completions middleware logic and API client handling

- Updated the completions method to conditionally remove middleware based on parameters, improving flexibility in processing.
- Refactored the response chunk transformer in OpenAIApiClient and AnthropicAPIClient to utilize a more streamlined approach with TransformStream.
- Simplified middleware context handling by removing unnecessary custom state management.
- Improved logging and error handling across middleware components for better debugging and maintainability.

These changes enhance the efficiency and clarity of the AiProvider's middleware integration, ensuring a more adaptable and robust processing framework.

* refactor(AiProvider, middleware): clean up logging and improve method naming

- Removed unnecessary logging of parameters in AiProvider to streamline the code.
- Updated method name assignment in middleware to enhance clarity and consistency.

These changes contribute to a cleaner codebase and improve the readability of the middleware and provider components.

* feat(middleware): enhance middleware types and add RawStreamListenerMiddleware

- Introduced RawStreamListenerMiddleware to the MiddlewareName enum for improved middleware capabilities.
- Updated type definitions across middleware components to enhance type safety and clarity, including the addition of new SDK types.
- Refactored context and middleware API interfaces to support more specific type parameters, improving overall maintainability.

These changes expand the middleware framework, facilitating better handling of streaming scenarios and enhancing type safety across the codebase.

* refactor(messageThunk): convert callback functions to async and handle errors during database updates

This commit updates several callback functions in the messageThunk to be asynchronous, ensuring that block transitions are awaited properly. Additionally, error handling is added for the database update function to log any failures when saving blocks. This improves the reliability and responsiveness of the message processing flow.

* refactor: enhance message block handling in messageThunk

This commit refactors the message processing logic in messageThunk to improve the management of message blocks. Key changes include the introduction of dedicated IDs for different block types (main text, thinking, tool, and image) to streamline updates and transitions. The handling of placeholder blocks has been improved, ensuring that they are correctly converted to their respective types during processing. Additionally, error handling has been enhanced for better reliability in database updates.

* feat(AiProvider): add default timeout configuration and enhance API client aborthandler

- Introduced a default timeout constant to the configuration for improved API client timeout management.
- Updated BaseApiClient and its derived classes to utilize the new timeout setting, ensuring consistent timeout behavior across different API clients.
- Enhanced middleware to pass the timeout value during API calls, improving error handling and responsiveness.

These changes improve the overall robustness and configurability of the API client interactions, facilitating better control over request timeouts.

* feat(GeminiProvider): implement Gemini API client and enhance file handling

- Introduced GeminiAPIClient to facilitate interactions with the Gemini API, replacing the previous GoogleGenAI integration.
- Refactored GeminiProvider to utilize the new API client, improving code organization and maintainability.
- Enhanced file handling capabilities, including support for PDF uploads and retrieval of file metadata.
- Updated message processing to accommodate new SDK types and improve content generation logic.

These changes significantly enhance the functionality and robustness of the GeminiProvider, enabling better integration with the Gemini API and improving overall user experience.

* refactor(AiProvider, middleware): streamline API client and middleware integration

- Removed deprecated methods and types from various API clients, enhancing code clarity and maintainability.
- Updated the CompletionsParams interface to support messages as a string or array, improving flexibility in message handling.
- Refactored middleware components to eliminate unnecessary state management and improve type safety.
- Enhanced the handling of streaming responses and added utility functions for better stream management.

These changes contribute to a more robust and efficient architecture for the AiProvider and its associated middleware, facilitating improved API interactions and user experience.

* refactor(middleware): translation 适配

- Deleted SdkCallMiddleware to streamline middleware architecture and improve maintainability.
- Commented out references to SdkCallModule in examples and registration files to prevent usage.
- Enhanced logging in AbortHandlerMiddleware for better debugging and tracking of middleware execution.
- Updated parameters in ResponseTransformMiddleware to improve flexibility in handling response settings.

These changes contribute to a cleaner and more efficient middleware framework, facilitating better integration and performance.

* refactor(ApiCheck): streamline API validation and error handling

- Updated the API check logic to simplify validation processes and improve error handling across various components.
- Refactored the `checkApi` function to throw errors directly instead of returning validation objects, enhancing clarity in error management.
- Improved the handling of API key checks in `checkModelWithMultipleKeys` to provide more informative error messages.
- Added a new method `getEmbeddingDimensions` in the `AiProvider` class to facilitate embedding dimension retrieval, enhancing model compatibility checks.

These changes contribute to a more robust and maintainable API validation framework, improving overall user experience and error reporting.

* refactor(HealthCheckService, ModelService): improve error handling and performance metrics

- Updated error handling in `checkModelWithMultipleKeys` to truncate error messages for better readability.
- Refactored `performModelCheck` to remove unnecessary error handling, focusing on performance metrics by returning only latency.
- Enhanced the `checkModel` function to ensure consistent return types, improving clarity in API interactions.

These changes contribute to a more efficient and user-friendly error reporting and performance tracking system.

* refactor(AiProvider, models): enhance model handling and API client integration

- Updated the `listModels` method in various API clients to improve model retrieval and ensure consistent return types.
- Refactored the `EditModelsPopup` component to handle model properties more robustly, including fallback options for `id`, `name`, and other attributes.
- Enhanced type definitions for models in the SDK to support new integrations and improve type safety.

These changes contribute to a more reliable and maintainable model management system within the AiProvider, enhancing overall user experience and API interactions.

* refactor(AiProvider, clients): implement image generation functionality

- Refactored the `generateImage` method in the `AiProvider` class to utilize the `apiClient` for image generation, replacing the previous placeholder implementation.
- Updated the `BaseApiClient` to include an abstract `generateImage` method, ensuring all derived clients implement this functionality.
- Implemented the `generateImage` method in `GeminiAPIClient` and `OpenAIAPIClient`, providing specific logic for image generation based on the respective SDKs.
- Added type definitions for `GenerateImageParams` across relevant files to enhance type safety and clarity in image generation parameters.

These changes enhance the image generation capabilities of the AiProvider, improving integration with various API clients and overall user experience.

* refactor(AiProvider, clients): restructure API client architecture and remove deprecated components

- Refactored the `ProviderFactory` and removed the `AihubmixProvider` to streamline the API client architecture.
- Updated the import paths for `isOpenAIProvider` to reflect the new structure.
- Introduced `AihubmixAPIClient` and `OpenAIResponseAPIClient` to enhance client handling based on model types.
- Improved the `AiProvider` class to utilize the new clients for better model-specific API interactions.
- Enhanced type definitions and error handling across various components to improve maintainability and clarity.

These changes contribute to a more efficient and organized API client structure, enhancing overall integration and user experience.

* fix: update system prompt handling in API clients to use await for asynchronous operations

- Modified the `AnthropicAPIClient`, `GeminiAPIClient`, `OpenAIAPIClient`, and `OpenAIResponseAPIClient` to ensure `buildSystemPrompt` is awaited, improving the handling of system prompts.
- Adjusted the `fetchMessagesSummary` function to utilize the last five user messages for better context in API calls and added a utility function to clean up topic names.

These changes enhance the reliability of prompt generation and improve the overall API interaction experience.

* refactor(middleware): remove examples.ts to streamline middleware documentation

- Deleted the `examples.ts` file containing various middleware usage examples to simplify the middleware structure and documentation.
- This change contributes to a cleaner codebase and focuses on essential middleware components, enhancing maintainability.

* refactor(AiProvider, middleware): enhance middleware handling and error management

- Updated the `CompletionsParams` interface to include a new `callType` property for better middleware decision-making based on the context of the API call.
- Introduced `ErrorHandlerMiddleware` to standardize error handling across middleware, allowing errors to be captured and processed as `ErrorChunk` objects.
- Modified the `AbortHandlerMiddleware` to conditionally remove itself based on the `callType`, improving middleware efficiency.
- Cleaned up logging in `AbortHandlerMiddleware` to reduce console output and enhance performance.
- Updated middleware registration to include the new `ErrorHandlerMiddleware`, ensuring comprehensive error management in the middleware pipeline.

These changes contribute to a more robust and maintainable middleware architecture, improving error handling and overall API interaction efficiency.

* feat: implement token estimation for message handling

- Added an abstract method `estimateMessageTokens` to the `BaseApiClient` class for estimating token usage based on message content.
- Implemented the `estimateMessageTokens` method in `AnthropicAPIClient`, `GeminiAPIClient`, `OpenAIAPIClient`, and `OpenAIResponseAPIClient` to calculate token consumption for various message types.
- Enhanced middleware to accumulate token usage for new messages, improving tracking of API call costs.

These changes improve the efficiency of message processing and provide better insights into token usage across different API clients.

* feat: add support for image generation and model handling

- Introduced `SUPPORTED_DISABLE_GENERATION_MODELS` to manage models that disable image generation.
- Updated `isSupportedDisableGenerationModel` function to check model compatibility.
- Enhanced `Inputbar` logic to conditionally enable image generation based on model support.
- Modified API clients to handle image generation calls and responses, including new chunk types for image data.
- Updated middleware and service layers to incorporate image generation parameters and improve overall processing.

These changes enhance the application's capabilities for image generation and improve the handling of various model types.

* feat: enhance GeminiAPIClient for image generation support

- Added `getGenerateImageParameter` method to configure image generation parameters.
- Updated request handling in `GeminiAPIClient` to include image generation options.
- Enhanced response processing to handle image data and enqueue it correctly.

These changes improve the GeminiAPIClient's capabilities for generating and processing images, aligning with recent enhancements in image generation support.

* feat: enhance image generation handling in OpenAIResponseAPIClient and middleware

- Updated OpenAIResponseAPIClient to improve user message processing for image generation.
- Added handling for image creation events in TransformCoreToSdkParamsMiddleware.
- Adjusted ApiService to streamline image generation event handling.
- Modified messageThunk to reflect changes in image block status during processing.

These enhancements improve the integration and responsiveness of image generation features across the application.

* refactor: remove unused AI provider classes

- Deleted `AihubmixProvider`, `AnthropicProvider`, `BaseProvider`, `GeminiProvider`, and `OpenAIProvider` as they are no longer utilized in the codebase.
- This cleanup reduces code complexity and improves maintainability by removing obsolete components related to AI provider functionality.

* chore: remove obsolete test files for middleware

- Deleted test files for `AbortHandlerMiddleware`, `LoggingMiddleware`, `TextChunkMiddleware`, `ThinkChunkMiddleware`, and `WebSearchMiddleware` as they are no longer needed.
- This cleanup helps streamline the codebase and reduces maintenance overhead by removing outdated tests.

* chore: remove Suggestions component and related functionality

- Deleted the `Suggestions` component from the home page as it is no longer needed.
- Removed associated imports and functions related to suggestion fetching, streamlining the codebase.
- This cleanup helps improve maintainability by eliminating unused components.

* feat: enhance OpenAIAPIClient and StreamProcessingService for tool call handling

- Updated OpenAIAPIClient to conditionally include tool calls in the assistant message, improving message processing logic.
- Enhanced tool call handling in the response transformer to correctly manage and enqueue tool call data.
- Added a new callback for LLM response completion in StreamProcessingService, allowing better integration of response handling.

These changes improve the functionality and responsiveness of the OpenAI API client and stream processing capabilities.

* fix: copilot error

* fix: improve chunk handling in TextChunkMiddleware and ThinkChunkMiddleware

- Updated TextChunkMiddleware to enqueue LLM_RESPONSE_COMPLETE chunks based on accumulated text content.
- Refactored ThinkChunkMiddleware to generate THINKING_COMPLETE chunks when receiving non-THINKING_DELTA chunks, ensuring proper handling of accumulated thinking content.
- These changes enhance the middleware's responsiveness and accuracy in processing text and thinking chunks.

* chore: update dependencies and improve styling

- Updated `selection-hook` dependency to version 0.9.23 in `package.json` and `yarn.lock`.
- Removed unused styles from `container.scss` and adjusted padding in `index.scss`.
- Enhanced message rendering and layout in various components, including `Message`, `MessageHeader`, and `MessageMenubar`.
- Added tooltip support for message divider settings in `SettingsTab`.
- Improved handling of citation display in `CitationsList` and `CitationBlock`.

These changes streamline the codebase and enhance the user interface for better usability.

* feat: implement image generation middleware and enhance model handling

- Added `ImageGenerationMiddleware` to handle dedicated image generation models, integrating image processing and OpenAI's image generation API.
- Updated `AiProvider` to utilize the new middleware for dedicated image models, ensuring proper middleware chaining.
- Introduced constants for dedicated image models in `models.ts` to streamline model identification.
- Refactored error handling in `ErrorHandlerMiddleware` to use a utility function for better error management.
- Cleaned up imports and removed unused code in various files for improved maintainability.

* fix: update dedicated image models identification logic

- Modified the `DEDICATED_IMAGE_MODELS` array to include 'grok-2-image' for improved model handling.
- Enhanced the `isDedicatedImageGenerationModel` function to use a more robust check for model identification, ensuring better accuracy in middleware processing.

* refactor: remove OpenAIResponseProvider class

- Deleted the `OpenAIResponseProvider` class from the `AiProvider` module, streamlining the codebase by eliminating unused code.
- This change enhances maintainability and reduces complexity in the provider architecture.

* fix: usermessage

* refactor: simplify AbortHandlerMiddleware for improved abort handling

- Removed direct dependency on ApiClient for creating AbortController, enhancing modularity.
- Introduced utility functions to manage abort controllers, streamlining the middleware's responsibilities.
- Delegated abort signal handling to downstream middlewares, allowing for cleaner separation of concerns.

* refactor(aiCore): Consolidate AI provider and middleware architecture

This commit refactors the AI-related modules by unifying the `clients` and `middleware` directories under a single `aiCore` directory. This change simplifies the project structure, improves modularity, and makes the architecture more cohesive.

Key changes:
- Relocated provider-specific clients and middleware into the `aiCore` directory, removing the previous `providers/AiProvider` structure.
- Updated the architectural documentation (`AI_CORE_DESIGN.md`) to accurately reflect the new, streamlined directory layout and execution flow.
- The main `AiProvider` class is now the primary export of `aiCore/index.ts`, serving as the central access point for AI functionalities.

* refactor: update imports and enhance middleware functionality

- Adjusted import statements in `AnthropicAPIClient` and `GeminiAPIClient` for better organization.
- Improved `AbortHandlerMiddleware` to handle abort signals more effectively, including the conversion of streams to handle abort scenarios.
- Enhanced `ErrorHandlerMiddleware` to differentiate between abort errors and other types, ensuring proper error handling.
- Cleaned up commented-out code in `FinalChunkConsumerMiddleware` for better readability and maintainability.

* refactor: streamline middleware logging and improve error handling

- Removed excessive debug logging from various middleware components, including `AbortHandlerMiddleware`, `FinalChunkConsumerMiddleware`, and `McpToolChunkMiddleware`, to enhance readability and performance.
- Updated logging levels to use warnings for potential issues in `ResponseTransformMiddleware`, `TextChunkMiddleware`, and `ThinkChunkMiddleware`, ensuring better visibility of important messages.
- Cleaned up commented-out code and unnecessary debug statements across multiple middleware files for improved maintainability.

---------

Co-authored-by: suyao <sy20010504@gmail.com>
Co-authored-by: eeee0717 <chentao020717Work@outlook.com>
Co-authored-by: lizhixuan <zhixuan.li@banosuperapp.com>
2025-06-12 16:01:19 +08:00
kangfenmao
6ad9044cd1 refactor: replace 302ai PNG with WEBP format and update provider configurations
- Deleted the old PNG logo for 302ai and added a new WEBP version.
- Updated the provider configuration to use the new WEBP logo.
- Added translations for the new Cephalon provider in Japanese and Russian.
- Disabled the 302ai and Cephalon providers in the initial state of the store.
- Adjusted migration logic to accommodate the new provider setup.
2025-06-12 12:16:43 +08:00
JI4JUN
9e9a1ec024 feat: support 302ai provider (#7044)
* feat(porvider): add provider 302ai

* style(provider): change provider name 302AI to 302.AI

* style(provider): system models replacement of 302.AI provider

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-06-12 12:04:21 +08:00
HzTTT
a214dca6fa feat:add cephalon provider (#7050)
* feat: add Cephalon provider and related assets

* add Cephalon logo image
* update models to include Cephalon's DeepSeek-R1
* add Cephalon provider configuration and API details
* include Cephalon translations in multiple languages
* update store to initialize Cephalon as a provider
* increment version for migration

* feat: update Cephalon provider configuration and assets

* add Cephalon logo image
* enable Cephalon provider in the store
* remove previous disabled configuration for Cephalon

* fix: update Cephalon provider model URL

* fix: update official website URL for Cephalon provider
2025-06-12 12:02:03 +08:00
one
b142e5647e fix(Markdown): inline math overflow (#7095) 2025-06-12 11:05:52 +08:00
614 changed files with 91184 additions and 22305 deletions

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/** linguist-vendored
/.yarn/releases/* binary /.yarn/releases/* binary

View File

@@ -1,86 +1,17 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: "npm" - package-ecosystem: 'github-actions'
directory: "/" directory: '/'
schedule: schedule:
interval: "monthly" interval: 'monthly'
open-pull-requests-limit: 7
target-branch: "main"
commit-message:
prefix: "chore"
include: "scope"
groups:
# 核心框架
core-framework:
patterns:
- "react"
- "react-dom"
- "electron"
- "typescript"
- "@types/react*"
- "@types/node"
update-types:
- "minor"
- "patch"
# Electron 生态和构建工具
electron-build:
patterns:
- "electron-*"
- "@electron*"
- "vite"
- "@vitejs/*"
- "dotenv-cli"
- "rollup-plugin-*"
- "@swc/*"
update-types:
- "minor"
- "patch"
# 测试工具
testing-tools:
patterns:
- "vitest"
- "@vitest/*"
- "playwright"
- "@playwright/*"
- "eslint*"
- "@eslint*"
- "prettier"
- "husky"
- "lint-staged"
update-types:
- "minor"
- "patch"
# CherryStudio 自定义包
cherrystudio-packages:
patterns:
- "@cherrystudio/*"
update-types:
- "minor"
- "patch"
# 兜底其他 dependencies
other-dependencies:
dependency-type: "production"
# 兜底其他 devDependencies
other-dev-dependencies:
dependency-type: "development"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 3 open-pull-requests-limit: 3
commit-message: commit-message:
prefix: "ci" prefix: 'ci'
include: "scope" include: 'scope'
groups: groups:
github-actions: github-actions:
patterns: patterns:
- "*" - '*'
update-types: update-types:
- "minor" - 'minor'
- "patch" - 'patch'

View File

@@ -9,115 +9,115 @@ labels:
# skips and removes # skips and removes
- name: skip all - name: skip all
content: content:
regexes: "[Ss]kip (?:[Aa]ll |)[Ll]abels?" regexes: '[Ss]kip (?:[Aa]ll |)[Ll]abels?'
- name: remove all - name: remove all
content: content:
regexes: "[Rr]emove (?:[Aa]ll |)[Ll]abels?" regexes: '[Rr]emove (?:[Aa]ll |)[Ll]abels?'
- name: skip kind/bug - name: skip kind/bug
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)'
- name: remove kind/bug - name: remove kind/bug
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)'
- name: skip kind/enhancement - name: skip kind/enhancement
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)'
- name: remove kind/enhancement - name: remove kind/enhancement
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)'
- name: skip kind/question - name: skip kind/question
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/question(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)kind/question(?:`|)'
- name: remove kind/question - name: remove kind/question
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/question(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)kind/question(?:`|)'
- name: skip area/Connectivity - name: skip area/Connectivity
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)'
- name: remove area/Connectivity - name: remove area/Connectivity
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)'
- name: skip area/UI/UX - name: skip area/UI/UX
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)'
- name: remove area/UI/UX - name: remove area/UI/UX
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)'
- name: skip kind/documentation - name: skip kind/documentation
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)'
- name: remove kind/documentation - name: remove kind/documentation
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)'
- name: skip client:linux - name: skip client:linux
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:linux(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)client:linux(?:`|)'
- name: remove client:linux - name: remove client:linux
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:linux(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)client:linux(?:`|)'
- name: skip client:mac - name: skip client:mac
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:mac(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)client:mac(?:`|)'
- name: remove client:mac - name: remove client:mac
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:mac(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)client:mac(?:`|)'
- name: skip client:win - name: skip client:win
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:win(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)client:win(?:`|)'
- name: remove client:win - name: remove client:win
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:win(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)client:win(?:`|)'
- name: skip sig/Assistant - name: skip sig/Assistant
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)'
- name: remove sig/Assistant - name: remove sig/Assistant
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)'
- name: skip sig/Data - name: skip sig/Data
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)'
- name: remove sig/Data - name: remove sig/Data
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)'
- name: skip sig/MCP - name: skip sig/MCP
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)'
- name: remove sig/MCP - name: remove sig/MCP
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)'
- name: skip sig/RAG - name: skip sig/RAG
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)'
- name: remove sig/RAG - name: remove sig/RAG
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)'
- name: skip lgtm - name: skip lgtm
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)lgtm(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)lgtm(?:`|)'
- name: remove lgtm - name: remove lgtm
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)lgtm(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)lgtm(?:`|)'
- name: skip License - name: skip License
content: content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)License(?:`|)" regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)License(?:`|)'
- name: remove License - name: remove License
content: content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)License(?:`|)" regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)License(?:`|)'
# `Dev Team` # `Dev Team`
- name: Dev Team - name: Dev Team
@@ -129,7 +129,7 @@ labels:
# Area labels # Area labels
- name: area/Connectivity - name: area/Connectivity
content: area/Connectivity content: area/Connectivity
regexes: "代理|[Pp]roxy" regexes: '代理|[Pp]roxy'
skip-if: skip-if:
- skip all - skip all
- skip area/Connectivity - skip area/Connectivity
@@ -139,7 +139,7 @@ labels:
- name: area/UI/UX - name: area/UI/UX
content: area/UI/UX content: area/UI/UX
regexes: "界面|[Uu][Ii]|重叠|按钮|图标|组件|渲染|菜单|栏目|头像|主题|样式|[Cc][Ss][Ss]" regexes: '界面|[Uu][Ii]|重叠|按钮|图标|组件|渲染|菜单|栏目|头像|主题|样式|[Cc][Ss][Ss]'
skip-if: skip-if:
- skip all - skip all
- skip area/UI/UX - skip area/UI/UX
@@ -150,7 +150,7 @@ labels:
# Kind labels # Kind labels
- name: kind/documentation - name: kind/documentation
content: kind/documentation content: kind/documentation
regexes: "文档|教程|[Dd]oc(s|umentation)|[Rr]eadme" regexes: '文档|教程|[Dd]oc(s|umentation)|[Rr]eadme'
skip-if: skip-if:
- skip all - skip all
- skip kind/documentation - skip kind/documentation
@@ -161,7 +161,7 @@ labels:
# Client labels # Client labels
- name: client:linux - name: client:linux
content: client:linux content: client:linux
regexes: "(?:[Ll]inux|[Uu]buntu|[Dd]ebian)" regexes: '(?:[Ll]inux|[Uu]buntu|[Dd]ebian)'
skip-if: skip-if:
- skip all - skip all
- skip client:linux - skip client:linux
@@ -171,7 +171,7 @@ labels:
- name: client:mac - name: client:mac
content: client:mac content: client:mac
regexes: "(?:[Mm]ac|[Mm]acOS|[Oo]SX)" regexes: '(?:[Mm]ac|[Mm]acOS|[Oo]SX)'
skip-if: skip-if:
- skip all - skip all
- skip client:mac - skip client:mac
@@ -181,7 +181,7 @@ labels:
- name: client:win - name: client:win
content: client:win content: client:win
regexes: "(?:[Ww]in|[Ww]indows)" regexes: '(?:[Ww]in|[Ww]indows)'
skip-if: skip-if:
- skip all - skip all
- skip client:win - skip client:win
@@ -192,7 +192,7 @@ labels:
# SIG labels # SIG labels
- name: sig/Assistant - name: sig/Assistant
content: sig/Assistant content: sig/Assistant
regexes: "快捷助手|[Aa]ssistant" regexes: '快捷助手|[Aa]ssistant'
skip-if: skip-if:
- skip all - skip all
- skip sig/Assistant - skip sig/Assistant
@@ -202,7 +202,7 @@ labels:
- name: sig/Data - name: sig/Data
content: sig/Data content: sig/Data
regexes: "[Ww]ebdav|坚果云|备份|同步|数据|Obsidian|Notion|Joplin|思源" regexes: '[Ww]ebdav|坚果云|备份|同步|数据|Obsidian|Notion|Joplin|思源'
skip-if: skip-if:
- skip all - skip all
- skip sig/Data - skip sig/Data
@@ -212,7 +212,7 @@ labels:
- name: sig/MCP - name: sig/MCP
content: sig/MCP content: sig/MCP
regexes: "[Mm][Cc][Pp]" regexes: '[Mm][Cc][Pp]'
skip-if: skip-if:
- skip all - skip all
- skip sig/MCP - skip sig/MCP
@@ -222,7 +222,7 @@ labels:
- name: sig/RAG - name: sig/RAG
content: sig/RAG content: sig/RAG
regexes: "知识库|[Rr][Aa][Gg]" regexes: '知识库|[Rr][Aa][Gg]'
skip-if: skip-if:
- skip all - skip all
- skip sig/RAG - skip sig/RAG
@@ -233,7 +233,7 @@ labels:
# Other labels # Other labels
- name: lgtm - name: lgtm
content: 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-if:
- skip all - skip all
- skip lgtm - skip lgtm
@@ -243,7 +243,7 @@ labels:
- name: License - name: License
content: 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-if:
- skip all - skip all
- skip License - 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: on:
issues: issues:
@@ -19,7 +19,7 @@ jobs:
steps: steps:
- uses: MaaAssistantArknights/issue-checker@v1.14 - uses: MaaAssistantArknights/issue-checker@v1.14
with: with:
repo-token: "${{ secrets.GITHUB_TOKEN }}" repo-token: '${{ secrets.GITHUB_TOKEN }}'
configuration-path: .github/issue-checker.yml configuration-path: .github/issue-checker.yml
not-before: 2022-08-05T00:00:00Z 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: on:
schedule: schedule:
- cron: "0 0 * * *" - cron: '0 0 * * *'
workflow_dispatch: workflow_dispatch:
env: env:
@@ -24,18 +24,18 @@ jobs:
uses: actions/stale@v9 uses: actions/stale@v9
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "needs-more-info" only-labels: 'needs-more-info'
days-before-stale: ${{ env.daysBeforeStale }} days-before-stale: ${{ env.daysBeforeStale }}
days-before-close: 0 # Close immediately after stale days-before-close: 0 # Close immediately after stale
stale-issue-label: "inactive" stale-issue-label: 'inactive'
close-issue-label: "closed:no-response" close-issue-label: 'closed:no-response'
stale-issue-message: | stale-issue-message: |
This issue has been labeled as needing more information and has been inactive for ${{ env.daysBeforeStale }} days. 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. It will be closed now due to lack of additional information.
该问题被标记为"需要更多信息"且已经 ${{ env.daysBeforeStale }} 天没有任何活动,将立即关闭。 该问题被标记为"需要更多信息"且已经 ${{ env.daysBeforeStale }} 天没有任何活动,将立即关闭。
operations-per-run: 50 operations-per-run: 50
exempt-issue-labels: "pending, Dev Team" exempt-issue-labels: 'pending, Dev Team'
days-before-pr-stale: -1 days-before-pr-stale: -1
days-before-pr-close: -1 days-before-pr-close: -1
@@ -45,11 +45,11 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: ${{ env.daysBeforeStale }} days-before-stale: ${{ env.daysBeforeStale }}
days-before-close: ${{ env.daysBeforeClose }} days-before-close: ${{ env.daysBeforeClose }}
stale-issue-label: "inactive" stale-issue-label: 'inactive'
stale-issue-message: | stale-issue-message: |
This issue has been inactive for a prolonged period and will be closed automatically in ${{ env.daysBeforeClose }} days. This issue has been inactive for a prolonged period and will be closed automatically in ${{ env.daysBeforeClose }} days.
该问题已长时间处于闲置状态,${{ env.daysBeforeClose }} 天后将自动关闭。 该问题已长时间处于闲置状态,${{ 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-stale: -1 # Completely disable stalling for PRs
days-before-pr-close: -1 # Completely disable closing for PRs days-before-pr-close: -1 # Completely disable closing for PRs

View File

@@ -44,4 +44,4 @@ jobs:
run: yarn build:check run: yarn build:check
- name: Lint Check - name: Lint Check
run: yarn lint run: yarn test:lint

View File

@@ -27,7 +27,7 @@ jobs:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: main fetch-depth: 0
- name: Get release tag - name: Get release tag
id: get-tag id: get-tag
@@ -77,8 +77,10 @@ jobs:
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
NODE_OPTIONS: --max-old-space-size=8192 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 - name: Build Mac
if: matrix.os == 'macos-latest' if: matrix.os == 'macos-latest'
@@ -92,9 +94,11 @@ jobs:
APPLE_ID: ${{ vars.APPLE_ID }} APPLE_ID: ${{ vars.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ vars.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_APP_SPECIFIC_PASSWORD: ${{ vars.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: --max-old-space-size=8192 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 - name: Build Windows
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
@@ -103,8 +107,10 @@ jobs:
yarn build:win yarn build:win
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
NODE_OPTIONS: --max-old-space-size=8192 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 - name: Release
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
@@ -115,38 +121,3 @@ jobs:
tag: ${{ steps.get-tag.outputs.tag }} 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' 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 }} 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 }}"}'

4
.gitignore vendored
View File

@@ -46,6 +46,10 @@ local
.aider* .aider*
.cursorrules .cursorrules
.cursor/* .cursor/*
.claude/*
.gemini/*
.trae/*
.claude-code-router/*
# vitest # vitest
coverage coverage

View File

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

1
.vscode/launch.json vendored
View File

@@ -7,7 +7,6 @@
"request": "launch", "request": "launch",
"cwd": "${workspaceRoot}", "cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"runtimeVersion": "20",
"windows": { "windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
}, },

View File

@@ -1,8 +1,10 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit" "source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
}, },
"files.eol": "\n",
"search.exclude": { "search.exclude": {
"**/dist/**": true, "**/dist/**": true,
".yarn/releases/**": true ".yarn/releases/**": true

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,69 @@
diff --git a/es/dropdown/dropdown.js b/es/dropdown/dropdown.js
index 986877a762b9ad0aca596a8552732cd12d2eaabb..1f18aa2ea745e68950e4cee16d4d655f5c835fd5 100644
--- a/es/dropdown/dropdown.js
+++ b/es/dropdown/dropdown.js
@@ -2,7 +2,7 @@
import * as React from 'react';
import LeftOutlined from "@ant-design/icons/es/icons/LeftOutlined";
-import RightOutlined from "@ant-design/icons/es/icons/RightOutlined";
+import { ChevronRight } from 'lucide-react';
import classNames from 'classnames';
import RcDropdown from 'rc-dropdown';
import useEvent from "rc-util/es/hooks/useEvent";
@@ -158,8 +158,10 @@ const Dropdown = props => {
className: `${prefixCls}-menu-submenu-arrow`
}, direction === 'rtl' ? (/*#__PURE__*/React.createElement(LeftOutlined, {
className: `${prefixCls}-menu-submenu-arrow-icon`
- })) : (/*#__PURE__*/React.createElement(RightOutlined, {
- className: `${prefixCls}-menu-submenu-arrow-icon`
+ })) : (/*#__PURE__*/React.createElement(ChevronRight, {
+ size: 16,
+ strokeWidth: 1.8,
+ className: `${prefixCls}-menu-submenu-arrow-icon lucide-custom`
}))),
mode: "vertical",
selectable: false,
diff --git a/es/dropdown/style/index.js b/es/dropdown/style/index.js
index 768c01783002c6901c85a73061ff6b3e776a60ce..39b1b95a56cdc9fb586a193c3adad5141f5cf213 100644
--- a/es/dropdown/style/index.js
+++ b/es/dropdown/style/index.js
@@ -240,7 +240,8 @@ const genBaseStyle = token => {
marginInlineEnd: '0 !important',
color: token.colorTextDescription,
fontSize: fontSizeIcon,
- fontStyle: 'normal'
+ fontStyle: 'normal',
+ marginTop: 3,
}
}
}),
diff --git a/es/select/useIcons.js b/es/select/useIcons.js
index 959115be936ef8901548af2658c5dcfdc5852723..c812edd52123eb0faf4638b1154fcfa1b05b513b 100644
--- a/es/select/useIcons.js
+++ b/es/select/useIcons.js
@@ -4,10 +4,10 @@ import * as React from 'react';
import CheckOutlined from "@ant-design/icons/es/icons/CheckOutlined";
import CloseCircleFilled from "@ant-design/icons/es/icons/CloseCircleFilled";
import CloseOutlined from "@ant-design/icons/es/icons/CloseOutlined";
-import DownOutlined from "@ant-design/icons/es/icons/DownOutlined";
import LoadingOutlined from "@ant-design/icons/es/icons/LoadingOutlined";
import SearchOutlined from "@ant-design/icons/es/icons/SearchOutlined";
import { devUseWarning } from '../_util/warning';
+import { ChevronDown } from 'lucide-react';
export default function useIcons(_ref) {
let {
suffixIcon,
@@ -56,8 +56,10 @@ export default function useIcons(_ref) {
className: iconCls
}));
}
- return getSuffixIconNode(/*#__PURE__*/React.createElement(DownOutlined, {
- className: iconCls
+ return getSuffixIconNode(/*#__PURE__*/React.createElement(ChevronDown, {
+ size: 16,
+ strokeWidth: 1.8,
+ className: `${iconCls} lucide-custom`
}));
};
}

View File

@@ -65,11 +65,44 @@ index e8bd7bb46c8a54b3f55cf3a853ef924195271e01..f956e9f3fe9eb903c78aef3502553b01
await packager.info.emitArtifactBuildCompleted({ await packager.info.emitArtifactBuildCompleted({
file: installerPath, file: installerPath,
updateInfo, updateInfo,
diff --git a/out/util/yarn.js b/out/util/yarn.js
index 1ee20f8b252a8f28d0c7b103789cf0a9a427aec1..c2878ec54d57da50bf14225e0c70c9c88664eb8a 100644
--- a/out/util/yarn.js
+++ b/out/util/yarn.js
@@ -140,6 +140,7 @@ async function rebuild(config, { appDir, projectDir }, options) {
arch,
platform,
buildFromSource,
+ ignoreModules: config.excludeReBuildModules || undefined,
projectRootPath: projectDir,
mode: config.nativeRebuilder || "sequential",
disablePreGypCopy: true,
diff --git a/scheme.json b/scheme.json diff --git a/scheme.json b/scheme.json
index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43ebd0fa8b61 100644 index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..0167441bf928a92f59b5dbe70b2317a74dda74c9 100644
--- a/scheme.json --- a/scheme.json
+++ b/scheme.json +++ b/scheme.json
@@ -1975,6 +1975,13 @@ @@ -1825,6 +1825,20 @@
"string"
]
},
+ "excludeReBuildModules": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The modules to exclude from the rebuild."
+ },
"executableArgs": {
"anyOf": [
{
@@ -1975,6 +1989,13 @@
], ],
"description": "The mime types in addition to specified in the file associations. Use it if you don't want to register a new mime type, but reuse existing." "description": "The mime types in addition to specified in the file associations. Use it if you don't want to register a new mime type, but reuse existing."
}, },
@@ -83,7 +116,7 @@ index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43eb
"packageCategory": { "packageCategory": {
"description": "backward compatibility + to allow specify fpm-only category for all possible fpm targets in one place", "description": "backward compatibility + to allow specify fpm-only category for all possible fpm targets in one place",
"type": [ "type": [
@@ -2327,6 +2334,13 @@ @@ -2327,6 +2348,13 @@
"MacConfiguration": { "MacConfiguration": {
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
@@ -97,7 +130,28 @@ index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43eb
"additionalArguments": { "additionalArguments": {
"anyOf": [ "anyOf": [
{ {
@@ -2737,7 +2751,7 @@ @@ -2527,6 +2555,20 @@
"string"
]
},
+ "excludeReBuildModules": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The modules to exclude from the rebuild."
+ },
"executableName": {
"description": "The executable name. Defaults to `productName`.",
"type": [
@@ -2737,7 +2779,7 @@
"type": "boolean" "type": "boolean"
}, },
"minimumSystemVersion": { "minimumSystemVersion": {
@@ -106,7 +160,7 @@ index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43eb
"type": [ "type": [
"null", "null",
"string" "string"
@@ -2959,6 +2973,13 @@ @@ -2959,6 +3001,13 @@
"MasConfiguration": { "MasConfiguration": {
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
@@ -120,7 +174,28 @@ index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43eb
"additionalArguments": { "additionalArguments": {
"anyOf": [ "anyOf": [
{ {
@@ -3369,7 +3390,7 @@ @@ -3159,6 +3208,20 @@
"string"
]
},
+ "excludeReBuildModules": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The modules to exclude from the rebuild."
+ },
"executableName": {
"description": "The executable name. Defaults to `productName`.",
"type": [
@@ -3369,7 +3432,7 @@
"type": "boolean" "type": "boolean"
}, },
"minimumSystemVersion": { "minimumSystemVersion": {
@@ -129,7 +204,28 @@ index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43eb
"type": [ "type": [
"null", "null",
"string" "string"
@@ -6507,6 +6528,13 @@ @@ -6381,6 +6444,20 @@
"string"
]
},
+ "excludeReBuildModules": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The modules to exclude from the rebuild."
+ },
"executableName": {
"description": "The executable name. Defaults to `productName`.",
"type": [
@@ -6507,6 +6584,13 @@
"string" "string"
] ]
}, },
@@ -143,7 +239,28 @@ index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43eb
"protocols": { "protocols": {
"anyOf": [ "anyOf": [
{ {
@@ -7376,6 +7404,13 @@ @@ -7153,6 +7237,20 @@
"string"
]
},
+ "excludeReBuildModules": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The modules to exclude from the rebuild."
+ },
"executableName": {
"description": "The executable name. Defaults to `productName`.",
"type": [
@@ -7376,6 +7474,13 @@
], ],
"description": "MAS (Mac Application Store) development options (`mas-dev` target)." "description": "MAS (Mac Application Store) development options (`mas-dev` target)."
}, },

View File

@@ -1,4 +1,4 @@
[中文](./docs/CONTRIBUTING.zh.md) | [English](./CONTRIBUTING.md) [中文](docs/CONTRIBUTING.zh.md) | [English](CONTRIBUTING.md)
# Cherry Studio Contributor Guide # 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). 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 ### Other Suggestions
- **Contact Developers**: Before submitting a PR, you can contact the developers first to discuss or get help. - **Contact Developers**: Before submitting a PR, you can contact the developers first to discuss or get help.

163
README.md
View File

@@ -1,11 +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"> <h1 align="center">
<a href="https://github.com/CherryHQ/cherry-studio/releases"> <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> <img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
</a> </a>
</h1> </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"> <div align="center">
@@ -15,20 +44,11 @@
[![][telegram-shield]][telegram-link] [![][telegram-shield]][telegram-link]
</div> </div>
<!-- 项目统计徽章 -->
<div align="center"> <div align="center">
[![][github-stars-shield]][github-stars-link]
[![][github-forks-shield]][github-forks-link]
[![][github-release-shield]][github-release-link] [![][github-release-shield]][github-release-link]
[![][github-nightly-shield]][github-nightly-link]
[![][github-contributors-shield]][github-contributors-link] [![][github-contributors-shield]][github-contributors-link]
</div>
<div align="center">
[![][license-shield]][license-link] [![][license-shield]][license-link]
[![][commercial-shield]][commercial-link] [![][commercial-shield]][commercial-link]
[![][sponsor-shield]][sponsor-link] [![][sponsor-shield]][sponsor-link]
@@ -36,9 +56,9 @@
</div> </div>
<div align="center"> <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://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"><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://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" style="width: 200px; height: 43px;" width="200" height="43" /></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> </div>
# 🍒 Cherry Studio # 🍒 Cherry Studio
@@ -163,10 +183,82 @@ Refer to the [Branching Strategy](docs/branching-strategy-en.md) for contributio
3. **Submit Changes**: Commit and push your changes. 3. **Submit Changes**: Commit and push your changes.
4. **Open a Pull Request**: Describe your changes and reasons. 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! Thank you for your support and contributions!
# 🔧 Developer Co-creation Program
We are launching the Cherry Studio Developer Co-creation Program to foster a healthy and positive-feedback loop within the open-source ecosystem. We believe that great software is built collaboratively, and every merged pull request breathes new life into the project.
We sincerely invite you to join our ranks of contributors and shape the future of Cherry Studio with us.
## Contributor Rewards Program
To give back to our core contributors and create a virtuous cycle, we have established the following long-term incentive plan.
**The inaugural tracking period for this program will be Q3 2025 (July, August, September). Rewards for this cycle will be distributed on October 1st.**
Within any tracking period (e.g., July 1st to September 30th for the first cycle), any developer who contributes more than **30 meaningful commits** to any of Cherry Studio's open-source projects on GitHub is eligible for the following benefits:
- **Cursor Subscription Sponsorship**: Receive a **$70 USD** credit or reimbursement for your [Cursor](https://cursor.sh/) subscription, making AI your most efficient coding partner.
- **Unlimited Model Access**: Get **unlimited** API calls for the **DeepSeek** and **Qwen** models.
- **Cutting-Edge Tech Access**: Enjoy occasional perks, including API access to models like **Claude**, **Gemini**, and **OpenAI**, keeping you at the forefront of technology.
## Growing Together & Future Plans
A vibrant community is the driving force behind any sustainable open-source project. As Cherry Studio grows, so will our rewards program. We are committed to continuously aligning our benefits with the best-in-class tools and resources in the industry. This ensures our core contributors receive meaningful support, creating a positive cycle where developers, the community, and the project grow together.
**Moving forward, the project will also embrace an increasingly open stance to give back to the entire open-source community.**
## How to Get Started?
We look forward to your first Pull Request!
You can start by exploring our repositories, picking up a `good first issue`, or proposing your own enhancements. Every commit is a testament to the spirit of open source.
Thank you for your interest and contributions.
Let's build together.
# 🏢 Enterprise Edition
Building on the Community Edition, we are proud to introduce **Cherry Studio Enterprise Edition**—a privately deployable AI productivity and management platform designed for modern teams and enterprises.
The Enterprise Edition addresses core challenges in team collaboration by centralizing the management of AI resources, knowledge, and data. It empowers organizations to enhance efficiency, foster innovation, and ensure compliance, all while maintaining 100% control over their data in a secure environment.
## Core Advantages
- **Unified Model Management**: Centrally integrate and manage various cloud-based LLMs (e.g., OpenAI, Anthropic, Google Gemini) and locally deployed private models. Employees can use them out-of-the-box without individual configuration.
- **Enterprise-Grade Knowledge Base**: Build, manage, and share team-wide knowledge bases. Ensure knowledge is retained and consistent, enabling team members to interact with AI based on unified and accurate information.
- **Fine-Grained Access Control**: Easily manage employee accounts and assign role-based permissions for different models, knowledge bases, and features through a unified admin backend.
- **Fully Private Deployment**: Deploy the entire backend service on your on-premises servers or private cloud, ensuring your data remains 100% private and under your control to meet the strictest security and compliance standards.
- **Reliable Backend Services**: Provides stable API services, enterprise-grade data backup and recovery mechanisms to ensure business continuity.
## ✨ Online Demo
> 🚧 **Public Beta Notice**
>
> The Enterprise Edition is currently in its early public beta stage, and we are actively iterating and optimizing its features. We are aware that it may not be perfectly stable yet. If you encounter any issues or have valuable suggestions during your trial, we would be very grateful if you could contact us via email to provide feedback.
**🔗 [Cherry Studio Enterprise](https://www.cherry-ai.com/enterprise)**
## Version Comparison
| Feature | Community Edition | Enterprise Edition |
| :---------------- | :----------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
| **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 |
## Get the Enterprise Edition
We believe the Enterprise Edition will become your team's AI productivity engine. If you are interested in Cherry Studio Enterprise Edition and would like to learn more, request a quote, or schedule a demo, please contact us.
- **For Business Inquiries & Purchasing**:
**📧 [bd@cherry-ai.com](mailto:bd@cherry-ai.com)**
# 🔗 Related Projects # 🔗 Related Projects
- [one-api](https://github.com/songquanpeng/one-api):LLM API management and distribution system, supporting mainstream models like OpenAI, Azure, and Anthropic. Features unified API interface, suitable for key management and secondary distribution. - [one-api](https://github.com/songquanpeng/one-api):LLM API management and distribution system, supporting mainstream models like OpenAI, Azure, and Anthropic. Features unified API interface, suitable for key management and secondary distribution.
@@ -180,34 +272,45 @@ Thank you for your support and contributions!
</a> </a>
<br /><br /> <br /><br />
# 📊 GitHub Stats
![Stats](https://repobeats.axiom.co/api/embed/a693f2e5f773eed620f70031e974552156c7f397.svg 'Repobeats analytics image')
# ⭐️ Star History # ⭐️ 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 --> <!-- 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 [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 [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 [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 [telegram-link]: https://t.me/CherryStudioAI
<!-- Links & Images --> <!-- 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-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio?logo=github
[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-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 [github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
<!-- Links & Images --> <!-- 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 [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 [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 [sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md

View File

@@ -1,6 +1,6 @@
# Cherry Studio 贡献者指南 # Cherry Studio 贡献者指南
[**English**](../CONTRIBUTING.md) | [**中文**](./CONTRIBUTING.zh.md) [**English**](../CONTRIBUTING.md) | [**中文**](CONTRIBUTING.zh.md)
欢迎来到 Cherry Studio 的贡献者社区!我们致力于将 Cherry Studio 打造成一个长期提供价值的项目,并希望邀请更多的开发者加入我们的行列。无论您是经验丰富的开发者还是刚刚起步的初学者,您的贡献都将帮助我们更好地服务用户,提升软件质量。 欢迎来到 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"> <h1 align="center">
<a href="https://github.com/CherryHQ/cherry-studio/releases"> <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> <img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
</a> </a>
</h1> </h1>
<p align="center"> <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> </p>
<!-- 题头徽章组合 --> <!-- 题头徽章组合 -->
@@ -18,19 +48,10 @@
</div> </div>
<!-- 项目统计徽章 -->
<div align="center"> <div align="center">
[![][github-stars-shield]][github-stars-link]
[![][github-forks-shield]][github-forks-link]
[![][github-release-shield]][github-release-link] [![][github-release-shield]][github-release-link]
[![][github-contributors-shield]][github-contributors-link] [![][github-contributors-shield]][github-contributors-link]
</div>
<div align="center">
[![][license-shield]][license-link] [![][license-shield]][license-link]
[![][commercial-shield]][commercial-link] [![][commercial-shield]][commercial-link]
[![][sponsor-shield]][sponsor-link] [![][sponsor-shield]][sponsor-link]
@@ -38,9 +59,9 @@
</div> </div>
<div align="center"> <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://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"><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://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" style="width: 200px; height: 43px;" width="200" height="43" /></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> </div>
# 🍒 Cherry Studio # 🍒 Cherry Studio
@@ -51,14 +72,6 @@ Cherry Studio 是一款支持多个大语言模型LLM服务商的桌面客
❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️ ❤️ 喜欢 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 https://docs.cherry-ai.com
@@ -177,10 +190,82 @@ https://docs.cherry-ai.com
3. **提交更改**:提交并推送您的更改 3. **提交更改**:提交并推送您的更改
4. **打开 Pull Request**:描述您的更改和原因 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 接口,可用于密钥管理与二次分发。 - [one-api](https://github.com/songquanpeng/one-api)LLM API 管理及分发系统,支持 OpenAI、Azure、Anthropic 等主流模型,统一 API 接口,可用于密钥管理与二次分发。
@@ -194,34 +279,43 @@ https://docs.cherry-ai.com
</a> </a>
<br /><br /> <br /><br />
# 📊 GitHub 统计
![Stats](https://repobeats.axiom.co/api/embed/a693f2e5f773eed620f70031e974552156c7f397.svg 'Repobeats analytics image')
# ⭐️ Star 记录 # ⭐️ 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 --> <!-- 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 [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 [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 [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 [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-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases [github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio [github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors [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 [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=商业授权咨询 [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 [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 - Only accepts documentation updates and bug fixes
- Thoroughly tested before production deployment - 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 ## Contributing Branches
When contributing to Cherry Studio, please follow these guidelines: When contributing to Cherry Studio, please follow these guidelines:

View File

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

View File

@@ -0,0 +1,222 @@
# Cherry Studio 记忆功能指南
## 功能介绍
Cherry Studio 的记忆功能是一个强大的工具,能够帮助 AI 助手记住对话中的重要信息、用户偏好和上下文。通过记忆功能,您的 AI 助手可以:
- 📝 **记住重要信息**:自动从对话中提取并存储关键事实和信息
- 🧠 **个性化响应**:基于存储的记忆提供更加个性化和相关的回答
- 🔍 **智能检索**:在需要时自动搜索相关记忆,增强对话的连贯性
- 👥 **多用户支持**:为不同用户维护独立的记忆上下文
记忆功能特别适用于需要长期保持上下文的场景,例如个人助手、客户服务、教育辅导等。
## 如何启用记忆功能
### 1. 全局配置(首次设置)
在使用记忆功能之前,您需要先进行全局配置:
1. 点击侧边栏的 **记忆** 图标(记忆棒图标)进入记忆管理页面
2. 点击右上角的 **更多** 按钮(三个点),选择 **设置**
3. 在设置弹窗中配置以下必要项:
- **LLM 模型**:选择用于处理记忆的语言模型(推荐使用 GPT-4 或 Claude 等高级模型)
- **嵌入模型**:选择用于生成向量嵌入的模型(如 text-embedding-3-small
- **嵌入维度**:输入嵌入模型的维度(通常为 1536
4. 点击 **确定** 保存配置
> ⚠️ **注意**:嵌入模型和维度一旦设置后无法更改,请谨慎选择。
### 2. 为助手启用记忆
完成全局配置后,您可以为特定助手启用记忆功能:
1. 进入 **助手** 页面
2. 选择要启用记忆的助手,点击 **编辑**
3. 在助手设置中找到 **记忆** 部分
4. 打开记忆功能开关
5. 保存助手设置
启用后,该助手将在对话过程中自动提取和使用记忆。
## 使用方法
### 查看记忆
1. 点击侧边栏的 **记忆** 图标进入记忆管理页面
2. 您可以看到所有存储的记忆卡片,包括:
- 记忆内容
- 创建时间
- 所属用户
### 添加记忆
手动添加记忆有两种方式:
**方式一:在记忆管理页面添加**
1. 点击右上角的 **添加记忆** 按钮
2. 在弹窗中输入记忆内容
3. 点击 **添加** 保存
**方式二:在对话中自动提取**
- 当助手启用记忆功能后,系统会自动从对话中提取重要信息并存储为记忆
### 编辑记忆
1. 在记忆卡片上点击 **更多** 按钮(三个点)
2. 选择 **编辑**
3. 修改记忆内容
4. 点击 **保存**
### 删除记忆
1. 在记忆卡片上点击 **更多** 按钮
2. 选择 **删除**
3. 确认删除操作
## 记忆搜索
记忆管理页面提供了强大的搜索功能:
1. 在页面顶部的搜索框中输入关键词
2. 系统会实时过滤显示匹配的记忆
3. 搜索支持模糊匹配,可以搜索记忆内容的任何部分
## 用户管理
记忆功能支持多用户,您可以为不同的用户维护独立的记忆库:
### 切换用户
1. 在记忆管理页面,点击右上角的用户选择器
2. 选择要切换到的用户
3. 页面会自动加载该用户的记忆
### 添加新用户
1. 点击用户选择器
2. 选择 **添加新用户**
3. 输入用户 ID支持字母、数字、下划线和连字符
4. 点击 **添加**
### 删除用户
1. 切换到要删除的用户
2. 点击右上角的 **更多** 按钮
3. 选择 **删除用户**
4. 确认删除(注意:这将删除该用户的所有记忆)
> 💡 **提示**默认用户default-user无法删除。
## 设置说明
### LLM 模型
- 用于处理记忆提取和更新的语言模型
- 建议选择能力较强的模型以获得更好的记忆提取效果
- 可随时更改
### 嵌入模型
- 用于将文本转换为向量,支持语义搜索
- 一旦设置后无法更改(为了保证现有记忆的兼容性)
- 推荐使用 OpenAI 的 text-embedding 系列模型
### 嵌入维度
- 嵌入向量的维度,需要与选择的嵌入模型匹配
- 常见维度:
- text-embedding-3-small: 1536
- text-embedding-3-large: 3072
- text-embedding-ada-002: 1536
### 自定义提示词(可选)
- **事实提取提示词**:自定义如何从对话中提取信息
- **记忆更新提示词**:自定义如何更新现有记忆
## 最佳实践
### 1. 合理组织记忆
- 保持记忆简洁明了,每条记忆专注于一个具体信息
- 使用清晰的语言描述事实,避免模糊表达
- 定期审查和清理过时或不准确的记忆
### 2. 多用户场景
- 为不同的使用场景创建独立用户(如工作、个人、学习等)
- 使用有意义的用户 ID便于识别和管理
- 定期备份重要用户的记忆数据
### 3. 模型选择建议
- **LLM 模型**GPT-4、Claude 3 等高级模型能更准确地提取和理解信息
- **嵌入模型**:选择与您的主要使用语言匹配的模型
### 4. 性能优化
- 避免存储过多冗余记忆,这可能影响搜索性能
- 定期整理和合并相似的记忆
- 对于大量记忆的场景,考虑按主题或时间进行分类管理
## 常见问题
### Q: 为什么我无法启用记忆功能?
A: 请确保您已经完成全局配置,包括选择 LLM 模型和嵌入模型。
### Q: 记忆会自动同步到所有助手吗?
A: 不会。每个助手的记忆功能需要单独启用,且记忆是按用户隔离的。
### Q: 如何导出我的记忆数据?
A: 目前系统暂不支持直接导出功能,但所有记忆都存储在本地数据库中。
### Q: 删除的记忆可以恢复吗?
A: 删除操作是永久的,无法恢复。建议在删除前仔细确认。
### Q: 记忆功能会影响对话速度吗?
A: 记忆功能在后台异步处理,不会明显影响对话响应速度。但过多的记忆可能会略微增加搜索时间。
### Q: 如何清空所有记忆?
A: 您可以删除当前用户并重新创建,或者手动删除所有记忆条目。
## 注意事项
### 隐私保护
- 所有记忆数据都存储在您的本地设备上,不会上传到云端
- 请勿在记忆中存储敏感信息(如密码、私钥等)
- 定期审查记忆内容,确保没有意外存储的隐私信息
### 数据安全
- 记忆数据存储在本地数据库中
- 建议定期备份重要数据
- 更换设备时请注意迁移记忆数据
### 使用限制
- 单条记忆的长度建议不超过 500 字
- 每个用户的记忆数量建议控制在 1000 条以内
- 过多的记忆可能影响系统性能
## 技术细节
记忆功能使用了先进的 RAG检索增强生成技术
1. **信息提取**:使用 LLM 从对话中智能提取关键信息
2. **向量化存储**:通过嵌入模型将文本转换为向量,支持语义搜索
3. **智能检索**:在对话时自动搜索相关记忆,提供给 AI 作为上下文
4. **持续学习**:随着对话进行,不断更新和完善记忆库
---
💡 **提示**:记忆功能是 Cherry Studio 的高级特性,合理使用可以大大提升 AI 助手的智能程度和用户体验。如有更多问题,欢迎查阅文档或联系支持团队。

View File

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

View File

@@ -0,0 +1,214 @@
# 如何为 AI Provider 编写中间件
本文档旨在指导开发者如何为我们的 AI Provider 框架创建和集成自定义中间件。中间件提供了一种强大而灵活的方式来增强、修改或观察 Provider 方法的调用过程,例如日志记录、缓存、请求/响应转换、错误处理等。
## 架构概览
我们的中间件架构借鉴了 Redux 的三段式设计,并结合了 JavaScript Proxy 来动态地将中间件应用于 Provider 的方法。
- **Proxy**: 拦截对 Provider 方法的调用,并将调用引导至中间件链。
- **中间件链**: 一系列按顺序执行的中间件函数。每个中间件都可以处理请求/响应,然后将控制权传递给链中的下一个中间件,或者在某些情况下提前终止链。
- **上下文 (Context)**: 一个在中间件之间传递的对象携带了关于当前调用的信息如方法名、原始参数、Provider 实例、以及中间件自定义的数据)。
## 中间件的类型
目前主要支持两种类型的中间件,它们共享相似的结构但针对不同的场景:
1. **`CompletionsMiddleware`**: 专门为 `completions` 方法设计。这是最常用的中间件类型,因为它允许对 AI 模型的核心聊天/文本生成功能进行精细控制。
2. **`ProviderMethodMiddleware`**: 通用中间件,可以应用于 Provider 上的任何其他方法(例如,`translate`, `summarize` 等,如果这些方法也通过中间件系统包装)。
## 编写一个 `CompletionsMiddleware`
`CompletionsMiddleware` 的基本签名TypeScript 类型)如下:
```typescript
import { AiProviderMiddlewareCompletionsContext, CompletionsParams, MiddlewareAPI } from './AiProviderMiddlewareTypes' // 假设类型定义文件路径
export type CompletionsMiddleware = (
api: MiddlewareAPI<AiProviderMiddlewareCompletionsContext, [CompletionsParams]>
) => (
next: (context: AiProviderMiddlewareCompletionsContext, params: CompletionsParams) => Promise<any> // next 返回 Promise<any> 代表原始SDK响应或下游中间件的结果
) => (context: AiProviderMiddlewareCompletionsContext, params: CompletionsParams) => Promise<void> // 最内层函数通常返回 Promise<void>,因为结果通过 onChunk 或 context 副作用传递
```
让我们分解这个三段式结构:
1. **第一层函数 `(api) => { ... }`**:
- 接收一个 `api` 对象。
- `api` 对象提供了以下方法:
- `api.getContext()`: 获取当前调用的上下文对象 (`AiProviderMiddlewareCompletionsContext`)。
- `api.getOriginalArgs()`: 获取传递给 `completions` 方法的原始参数数组 (即 `[CompletionsParams]`)。
- `api.getProviderId()`: 获取当前 Provider 的 ID。
- `api.getProviderInstance()`: 获取原始的 Provider 实例。
- 此函数通常用于进行一次性的设置或获取所需的服务/配置。它返回第二层函数。
2. **第二层函数 `(next) => { ... }`**:
- 接收一个 `next` 函数。
- `next` 函数代表了中间件链中的下一个环节。调用 `next(context, params)` 会将控制权传递给下一个中间件,或者如果当前中间件是链中的最后一个,则会调用核心的 Provider 方法逻辑 (例如,实际的 SDK 调用)。
- `next` 函数接收当前的 `context``params` (这些可能已被上游中间件修改)。
- **重要的是**`next` 的返回类型通常是 `Promise<any>`。对于 `completions` 方法,如果 `next` 调用了实际的 SDK它将返回原始的 SDK 响应例如OpenAI 的流对象或 JSON 对象)。你需要处理这个响应。
- 此函数返回第三层(也是最核心的)函数。
3. **第三层函数 `(context, params) => { ... }`**:
- 这是执行中间件主要逻辑的地方。
- 它接收当前的 `context` (`AiProviderMiddlewareCompletionsContext`) 和 `params` (`CompletionsParams`)。
- 在此函数中,你可以:
- **在调用 `next` 之前**:
- 读取或修改 `params`。例如,添加默认参数、转换消息格式。
- 读取或修改 `context`。例如,设置一个时间戳用于后续计算延迟。
- 执行某些检查,如果不满足条件,可以不调用 `next` 而直接返回或抛出错误(例如,参数校验失败)。
- **调用 `await next(context, params)`**:
- 这是将控制权传递给下游的关键步骤。
- `next` 的返回值是原始的 SDK 响应或下游中间件的结果,你需要根据情况处理它(例如,如果是流,则开始消费流)。
- **在调用 `next` 之后**:
- 处理 `next` 的返回结果。例如,如果 `next` 返回了一个流,你可以在这里开始迭代处理这个流,并通过 `context.onChunk` 发送数据块。
- 基于 `context` 的变化或 `next` 的结果执行进一步操作。例如,计算总耗时、记录日志。
- 修改最终结果(尽管对于 `completions`,结果通常通过 `onChunk` 副作用发出)。
### 示例:一个简单的日志中间件
```typescript
import {
AiProviderMiddlewareCompletionsContext,
CompletionsParams,
MiddlewareAPI,
OnChunkFunction // 假设 OnChunkFunction 类型被导出
} from './AiProviderMiddlewareTypes' // 调整路径
import { ChunkType } from '@renderer/types' // 调整路径
export const createSimpleLoggingMiddleware = (): CompletionsMiddleware => {
return (api: MiddlewareAPI<AiProviderMiddlewareCompletionsContext, [CompletionsParams]>) => {
// console.log(`[LoggingMiddleware] Initialized for provider: ${api.getProviderId()}`);
return (next: (context: AiProviderMiddlewareCompletionsContext, params: CompletionsParams) => Promise<any>) => {
return async (context: AiProviderMiddlewareCompletionsContext, params: CompletionsParams): Promise<void> => {
const startTime = Date.now()
// 从 context 中获取 onChunk (它最初来自 params.onChunk)
const onChunk = context.onChunk
console.log(
`[LoggingMiddleware] Request for ${context.methodName} with params:`,
params.messages?.[params.messages.length - 1]?.content
)
try {
// 调用下一个中间件或核心逻辑
// `rawSdkResponse` 是来自下游的原始响应 (例如 OpenAIStream 或 ChatCompletion 对象)
const rawSdkResponse = await next(context, params)
// 此处简单示例不处理 rawSdkResponse假设下游中间件 (如 StreamingResponseHandler)
// 会处理它并通过 onChunk 发送数据。
// 如果这个日志中间件在 StreamingResponseHandler 之后,那么流已经被处理。
// 如果在之前,那么它需要自己处理 rawSdkResponse 或确保下游会处理。
const duration = Date.now() - startTime
console.log(`[LoggingMiddleware] Request for ${context.methodName} completed in ${duration}ms.`)
// 假设下游已经通过 onChunk 发送了所有数据。
// 如果这个中间件是链的末端,并且需要确保 BLOCK_COMPLETE 被发送,
// 它可能需要更复杂的逻辑来跟踪何时所有数据都已发送。
} catch (error) {
const duration = Date.now() - startTime
console.error(`[LoggingMiddleware] Request for ${context.methodName} failed after ${duration}ms:`, error)
// 如果 onChunk 可用,可以尝试发送一个错误块
if (onChunk) {
onChunk({
type: ChunkType.ERROR,
error: { message: (error as Error).message, name: (error as Error).name, stack: (error as Error).stack }
})
// 考虑是否还需要发送 BLOCK_COMPLETE 来结束流
onChunk({ type: ChunkType.BLOCK_COMPLETE, response: {} })
}
throw error // 重新抛出错误,以便上层或全局错误处理器可以捕获
}
}
}
}
}
```
### `AiProviderMiddlewareCompletionsContext` 的重要性
`AiProviderMiddlewareCompletionsContext` 是在中间件之间传递状态和数据的核心。它通常包含:
- `methodName`: 当前调用的方法名 (总是 `'completions'`)。
- `originalArgs`: 传递给 `completions` 的原始参数数组。
- `providerId`: Provider 的 ID。
- `_providerInstance`: Provider 实例。
- `onChunk`: 从原始 `CompletionsParams` 传入的回调函数,用于流式发送数据块。**所有中间件都应该通过 `context.onChunk` 来发送数据。**
- `messages`, `model`, `assistant`, `mcpTools`: 从原始 `CompletionsParams` 中提取的常用字段,方便访问。
- **自定义字段**: 中间件可以向上下文中添加自定义字段,以供后续中间件使用。例如,一个缓存中间件可能会添加 `context.cacheHit = true`
**关键**: 当你在中间件中修改 `params``context` 时,这些修改会向下游中间件传播(如果它们在 `next` 调用之前修改)。
### 中间件的顺序
中间件的执行顺序非常重要。它们在 `AiProviderMiddlewareConfig` 的数组中定义的顺序就是它们的执行顺序。
- 请求首先通过第一个中间件,然后是第二个,依此类推。
- 响应(或 `next` 的调用结果)则以相反的顺序"冒泡"回来。
例如,如果链是 `[AuthMiddleware, CacheMiddleware, LoggingMiddleware]`
1. `AuthMiddleware` 先执行其 "调用 `next` 之前" 的逻辑。
2. 然后 `CacheMiddleware` 执行其 "调用 `next` 之前" 的逻辑。
3. 然后 `LoggingMiddleware` 执行其 "调用 `next` 之前" 的逻辑。
4. 核心SDK调用或链的末端
5. `LoggingMiddleware` 先接收到结果,执行其 "调用 `next` 之后" 的逻辑。
6. 然后 `CacheMiddleware` 接收到结果(可能已被 LoggingMiddleware 修改的上下文),执行其 "调用 `next` 之后" 的逻辑(例如,存储结果)。
7. 最后 `AuthMiddleware` 接收到结果,执行其 "调用 `next` 之后" 的逻辑。
### 注册中间件
中间件在 `src/renderer/src/providers/middleware/register.ts` (或其他类似的配置文件) 中进行注册。
```typescript
// register.ts
import { AiProviderMiddlewareConfig } from './AiProviderMiddlewareTypes'
import { createSimpleLoggingMiddleware } from './common/SimpleLoggingMiddleware' // 假设你创建了这个文件
import { createCompletionsLoggingMiddleware } from './common/CompletionsLoggingMiddleware' // 已有的
const middlewareConfig: AiProviderMiddlewareConfig = {
completions: [
createSimpleLoggingMiddleware(), // 你新加的中间件
createCompletionsLoggingMiddleware() // 已有的日志中间件
// ... 其他 completions 中间件
],
methods: {
// translate: [createGenericLoggingMiddleware()],
// ... 其他方法的中间件
}
}
export default middlewareConfig
```
### 最佳实践
1. **单一职责**: 每个中间件应专注于一个特定的功能(例如,日志、缓存、转换特定数据)。
2. **无副作用 (尽可能)**: 除了通过 `context``onChunk` 明确的副作用外,尽量避免修改全局状态或产生其他隐蔽的副作用。
3. **错误处理**:
- 在中间件内部使用 `try...catch` 来处理可能发生的错误。
- 决定是自行处理错误(例如,通过 `onChunk` 发送错误块)还是将错误重新抛出给上游。
- 如果重新抛出,确保错误对象包含足够的信息。
4. **性能考虑**: 中间件会增加请求处理的开销。避免在中间件中执行非常耗时的同步操作。对于IO密集型操作确保它们是异步的。
5. **可配置性**: 使中间件的行为可通过参数或配置进行调整。例如,日志中间件可以接受一个日志级别参数。
6. **上下文管理**:
- 谨慎地向 `context` 添加数据。避免污染 `context` 或添加过大的对象。
- 明确你添加到 `context` 的字段的用途和生命周期。
7. **`next` 的调用**:
- 除非你有充分的理由提前终止请求(例如,缓存命中、授权失败),否则**总是确保调用 `await next(context, params)`**。否则,下游的中间件和核心逻辑将不会执行。
- 理解 `next` 的返回值并正确处理它,特别是当它是一个流时。你需要负责消费这个流或将其传递给另一个能够消费它的组件/中间件。
8. **命名清晰**: 给你的中间件和它们创建的函数起描述性的名字。
9. **文档和注释**: 对复杂的中间件逻辑添加注释,解释其工作原理和目的。
### 调试技巧
- 在中间件的关键点使用 `console.log` 或调试器来检查 `params``context` 的状态以及 `next` 的返回值。
- 暂时简化中间件链,只保留你正在调试的中间件和最简单的核心逻辑,以隔离问题。
- 编写单元测试来独立验证每个中间件的行为。
通过遵循这些指南,你应该能够有效地为我们的系统创建强大且可维护的中间件。如果你有任何疑问或需要进一步的帮助,请咨询团队。

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

@@ -11,6 +11,11 @@ electronLanguages:
- en # for macOS - en # for macOS
directories: directories:
buildResources: build buildResources: build
protocols:
- name: Cherry Studio
schemes:
- cherrystudio
files: files:
- '**/*' - '**/*'
- '!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}' - '!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}'
@@ -48,7 +53,11 @@ files:
- '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}' - '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}'
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}' - '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
- '!node_modules/selection-hook/prebuilds/**/*' # we rebuild .node, don't use prebuilds - '!node_modules/selection-hook/prebuilds/**/*' # we rebuild .node, don't use prebuilds
- '!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters}' # filter .node build files - '!node_modules/pdfjs-dist/web/**/*'
- '!node_modules/pdfjs-dist/legacy/web/*'
- '!node_modules/selection-hook/node_modules' # we don't need what in the node_modules dir
- '!node_modules/selection-hook/src' # we don't need source files
- '!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters,Makefile,*.Makefile}' # filter .node build files
asarUnpack: asarUnpack:
- resources/** - resources/**
- '**/*.{metal,exp,lib}' - '**/*.{metal,exp,lib}'
@@ -90,6 +99,7 @@ linux:
artifactName: ${productName}-${version}-${arch}.${ext} artifactName: ${productName}-${version}-${arch}.${ext}
target: target:
- target: AppImage - target: AppImage
- target: deb
maintainer: electronjs.org maintainer: electronjs.org
category: Utility category: Utility
desktop: desktop:
@@ -107,11 +117,8 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
划词助手:支持文本选择快捷键、开关快捷键、思考块支持和引用功能 新增全局记忆功能
复制功能新增纯文本复制去除Markdown格式符号 MCP 支持 DXT 格式导入
知识库支持设置向量维度修复Ollama分数错误和维度编辑问题 全局快捷键支持 Linux 系统
多语言:增加模型名称多语言提示和翻译源语言手动选择 模型思考过程增加动画效果
文件管理:修复主题/消息删除时文件未清理问题,优化文件选择流程 错误修复和性能优化
模型修复Gemini模型推理预算、Voyage AI嵌入问题和DeepSeek翻译模型更新
图像功能统一图片查看器支持Base64图片渲染修复图片预览相关问题
UI实现标签折叠/拖拽排序,修复气泡溢出,增加引文索引显示

View File

@@ -1,4 +1,5 @@
import react from '@vitejs/plugin-react-swc' import react from '@vitejs/plugin-react-swc'
import { CodeInspectorPlugin } from 'code-inspector-plugin'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite' import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { resolve } from 'path' import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
@@ -7,6 +8,9 @@ const visualizerPlugin = (type: 'renderer' | 'main') => {
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : [] return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
} }
const isDev = process.env.NODE_ENV === 'development'
const isProd = process.env.NODE_ENV === 'production'
export default defineConfig({ export default defineConfig({
main: { main: {
plugins: [externalizeDepsPlugin(), ...visualizerPlugin('main')], plugins: [externalizeDepsPlugin(), ...visualizerPlugin('main')],
@@ -19,12 +23,17 @@ export default defineConfig({
}, },
build: { build: {
rollupOptions: { rollupOptions: {
external: ['@libsql/client', 'bufferutil', 'utf-8-validate'] external: ['@libsql/client', 'bufferutil', 'utf-8-validate', '@cherrystudio/mac-system-ocr'],
output: {
manualChunks: undefined, // 彻底禁用代码分割 - 返回 null 强制单文件打包
inlineDynamicImports: true // 内联所有动态导入,这是关键配置
}
}, },
sourcemap: process.env.NODE_ENV === 'development' sourcemap: isDev
}, },
esbuild: isProd ? { legalComments: 'none' } : {},
optimizeDeps: { optimizeDeps: {
noDiscovery: process.env.NODE_ENV === 'development' noDiscovery: isDev
} }
}, },
preload: { preload: {
@@ -35,7 +44,7 @@ export default defineConfig({
} }
}, },
build: { build: {
sourcemap: process.env.NODE_ENV === 'development' sourcemap: isDev
} }
}, },
renderer: { renderer: {
@@ -53,6 +62,7 @@ export default defineConfig({
] ]
] ]
}), }),
...(isDev ? [CodeInspectorPlugin({ bundler: 'vite' })] : []), // 只在开发环境下启用 CodeInspectorPlugin
...visualizerPlugin('renderer') ...visualizerPlugin('renderer')
], ],
resolve: { resolve: {
@@ -62,12 +72,16 @@ export default defineConfig({
} }
}, },
optimizeDeps: { optimizeDeps: {
exclude: ['pyodide'] exclude: ['pyodide'],
esbuildOptions: {
target: 'esnext' // for dev
}
}, },
worker: { worker: {
format: 'es' format: 'es'
}, },
build: { build: {
target: 'esnext', // for build
rollupOptions: { rollupOptions: {
input: { input: {
index: resolve(__dirname, 'src/renderer/index.html'), index: resolve(__dirname, 'src/renderer/index.html'),
@@ -76,6 +90,7 @@ export default defineConfig({
selectionAction: resolve(__dirname, 'src/renderer/selectionAction.html') selectionAction: resolve(__dirname, 'src/renderer/selectionAction.html')
} }
} }
} },
esbuild: isProd ? { legalComments: 'none' } : {}
} }
}) })

View File

@@ -26,7 +26,7 @@ export default defineConfig([
'simple-import-sort/exports': 'error', 'simple-import-sort/exports': 'error',
'unused-imports/no-unused-imports': 'error', 'unused-imports/no-unused-imports': 'error',
'@eslint-react/no-prop-types': '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 // Configuration for ensuring compatibility with the original ESLint(8.x) rules

View File

@@ -1,6 +1,6 @@
{ {
"name": "CherryStudio", "name": "CherryStudio",
"version": "1.4.2", "version": "1.5.0",
"private": true, "private": true,
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",
@@ -27,12 +27,12 @@
"build:win": "dotenv npm run build && electron-builder --win --x64 --arm64", "build:win": "dotenv npm run build && electron-builder --win --x64 --arm64",
"build:win:x64": "dotenv npm run build && electron-builder --win --x64", "build:win:x64": "dotenv npm run build && electron-builder --win --x64",
"build:win:arm64": "dotenv npm run build && electron-builder --win --arm64", "build:win:arm64": "dotenv npm run build && electron-builder --win --arm64",
"build:mac": "dotenv electron-vite build && electron-builder --mac --arm64 --x64", "build:mac": "dotenv npm run build && electron-builder --mac --arm64 --x64",
"build:mac:arm64": "dotenv electron-vite build && electron-builder --mac --arm64", "build:mac:arm64": "dotenv npm run build && electron-builder --mac --arm64",
"build:mac:x64": "dotenv electron-vite build && electron-builder --mac --x64", "build:mac:x64": "dotenv npm run build && electron-builder --mac --x64",
"build:linux": "dotenv electron-vite build && electron-builder --linux --x64 --arm64", "build:linux": "dotenv npm run build && electron-builder --linux --x64 --arm64",
"build:linux:arm64": "dotenv electron-vite build && electron-builder --linux --arm64", "build:linux:arm64": "dotenv npm run build && electron-builder --linux --arm64",
"build:linux:x64": "dotenv electron-vite build && electron-builder --linux --x64", "build:linux:x64": "dotenv npm run build && electron-builder --linux --x64",
"build:npm": "node scripts/build-npm.js", "build:npm": "node scripts/build-npm.js",
"release": "node scripts/version.js", "release": "node scripts/version.js",
"publish": "yarn build:check && yarn release patch push", "publish": "yarn build:check && yarn release patch push",
@@ -55,9 +55,32 @@
"test:lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts", "test:lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
"format": "prettier --write .", "format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", "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": { "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",
"jaison": "^2.0.2",
"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.6",
"turndown": "7.2.0"
},
"devDependencies": {
"@agentic/exa": "^7.3.3",
"@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@anthropic-ai/sdk": "^0.41.0",
"@cherrystudio/embedjs": "^0.1.31", "@cherrystudio/embedjs": "^0.1.31",
"@cherrystudio/embedjs-libsql": "^0.1.31", "@cherrystudio/embedjs-libsql": "^0.1.31",
"@cherrystudio/embedjs-loader-csv": "^0.1.31", "@cherrystudio/embedjs-loader-csv": "^0.1.31",
@@ -70,61 +93,31 @@
"@cherrystudio/embedjs-loader-xml": "^0.1.31", "@cherrystudio/embedjs-loader-xml": "^0.1.31",
"@cherrystudio/embedjs-ollama": "^0.1.31", "@cherrystudio/embedjs-ollama": "^0.1.31",
"@cherrystudio/embedjs-openai": "^0.1.31", "@cherrystudio/embedjs-openai": "^0.1.31",
"@electron-toolkit/utils": "^3.0.0", "@codemirror/view": "^6.0.0",
"@langchain/community": "^0.3.36",
"@langchain/ollama": "^0.2.1",
"@strongtz/win32-arm64-msvc": "^0.4.7",
"@tanstack/react-query": "^5.27.0",
"@types/react-infinite-scroll-component": "^5.0.0",
"archiver": "^7.0.1",
"async-mutex": "^0.5.0",
"diff": "^7.0.0",
"docx": "^9.0.2",
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "6.6.4",
"electron-window-state": "^5.0.3",
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
"fast-xml-parser": "^5.2.0",
"franc-min": "^6.2.0",
"fs-extra": "^11.2.0",
"jsdom": "^26.0.0",
"markdown-it": "^14.1.0",
"node-stream-zip": "^1.15.0",
"officeparser": "^4.1.1",
"os-proxy-config": "^1.1.2",
"proxy-agent": "^6.5.0",
"remove-markdown": "^0.6.2",
"selection-hook": "^0.9.23",
"tar": "^7.4.3",
"turndown": "^7.2.0",
"webdav": "^5.8.0",
"zipread": "^1.3.3"
},
"devDependencies": {
"@agentic/exa": "^7.3.3",
"@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@anthropic-ai/sdk": "^0.41.0",
"@electron-toolkit/eslint-config-prettier": "^3.0.0", "@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0", "@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/preload": "^3.0.0", "@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@electron/notarize": "^2.5.0", "@electron/notarize": "^2.5.0",
"@emotion/is-prop-valid": "^1.3.1", "@emotion/is-prop-valid": "^1.3.1",
"@eslint-react/eslint-plugin": "^1.36.1", "@eslint-react/eslint-plugin": "^1.36.1",
"@eslint/js": "^9.22.0", "@eslint/js": "^9.22.0",
"@google/genai": "^1.0.1", "@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
"@hello-pangea/dnd": "^16.6.0", "@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0", "@kangfenmao/keyv-storage": "^0.1.0",
"@modelcontextprotocol/sdk": "^1.11.4", "@langchain/community": "^0.3.36",
"@langchain/ollama": "^0.2.1",
"@mistralai/mistralai": "^1.6.0",
"@modelcontextprotocol/sdk": "^1.12.3",
"@mozilla/readability": "^0.6.0", "@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15", "@notionhq/client": "^2.2.15",
"@playwright/test": "^1.52.0", "@playwright/test": "^1.52.0",
"@reduxjs/toolkit": "^2.2.5", "@reduxjs/toolkit": "^2.2.5",
"@shikijs/markdown-it": "^3.4.2", "@shikijs/markdown-it": "^3.7.0",
"@swc/plugin-styled-components": "^7.1.5", "@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/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
@@ -141,34 +134,52 @@
"@types/react-infinite-scroll-component": "^5.0.0", "@types/react-infinite-scroll-component": "^5.0.0",
"@types/react-window": "^1", "@types/react-window": "^1",
"@types/tinycolor2": "^1", "@types/tinycolor2": "^1",
"@uiw/codemirror-extensions-langs": "^4.23.12", "@types/word-extractor": "^1",
"@uiw/codemirror-themes-all": "^4.23.12", "@uiw/codemirror-extensions-langs": "^4.23.14",
"@uiw/react-codemirror": "^4.23.12", "@uiw/codemirror-themes-all": "^4.23.14",
"@uiw/react-codemirror": "^4.23.14",
"@vitejs/plugin-react-swc": "^3.9.0", "@vitejs/plugin-react-swc": "^3.9.0",
"@vitest/browser": "^3.1.4", "@vitest/browser": "^3.1.4",
"@vitest/coverage-v8": "^3.1.4", "@vitest/coverage-v8": "^3.1.4",
"@vitest/ui": "^3.1.4", "@vitest/ui": "^3.1.4",
"@vitest/web-worker": "^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", "@xyflow/react": "^12.4.4",
"antd": "^5.22.5", "antd": "patch:antd@npm%3A5.24.7#~/.yarn/patches/antd-npm-5.24.7-356a553ae5.patch",
"archiver": "^7.0.1",
"async-mutex": "^0.5.0",
"axios": "^1.7.3", "axios": "^1.7.3",
"browser-image-compression": "^2.0.2", "browser-image-compression": "^2.0.2",
"code-inspector-plugin": "^0.20.14",
"color": "^5.0.0", "color": "^5.0.0",
"country-flag-emoji-polyfill": "0.1.8",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"dexie": "^4.0.8", "dexie": "^4.0.8",
"dexie-react-hooks": "^1.1.7", "dexie-react-hooks": "^1.1.7",
"diff": "^7.0.0",
"docx": "^9.0.2",
"dotenv-cli": "^7.4.2", "dotenv-cli": "^7.4.2",
"electron": "35.4.0", "electron": "35.6.0",
"electron-builder": "26.0.15", "electron-builder": "26.0.15",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "6.6.4",
"electron-vite": "^3.1.0", "electron-vite": "^3.1.0",
"electron-window-state": "^5.0.3",
"emittery": "^1.0.3", "emittery": "^1.0.3",
"emoji-picker-element": "^1.22.1", "emoji-picker-element": "^1.22.1",
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
"eslint": "^9.22.0", "eslint": "^9.22.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",
"fast-diff": "^1.3.0", "fast-diff": "^1.3.0",
"fast-xml-parser": "^5.2.0",
"franc-min": "^6.2.0",
"fs-extra": "^11.2.0",
"google-auth-library": "^9.15.1",
"html-to-image": "^1.11.13", "html-to-image": "^1.11.13",
"husky": "^9.1.7", "husky": "^9.1.7",
"i18next": "^23.11.5", "i18next": "^23.11.5",
@@ -177,21 +188,24 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lru-cache": "^11.1.0", "lru-cache": "^11.1.0",
"lucide-react": "^0.487.0", "lucide-react": "^0.487.0",
"mermaid": "^11.6.0", "markdown-it": "^14.1.0",
"mermaid": "^11.7.0",
"mime": "^4.0.4", "mime": "^4.0.4",
"motion": "^12.10.5", "motion": "^12.10.5",
"npx-scope-finder": "^1.2.0", "npx-scope-finder": "^1.2.0",
"officeparser": "^4.1.1",
"openai": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch", "openai": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
"p-queue": "^8.1.0", "p-queue": "^8.1.0",
"playwright": "^1.52.0", "playwright": "^1.52.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"proxy-agent": "^6.5.0",
"rc-virtual-list": "^3.18.6", "rc-virtual-list": "^3.18.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hotkeys-hook": "^4.6.1", "react-hotkeys-hook": "^4.6.1",
"react-i18next": "^14.1.2", "react-i18next": "^14.1.2",
"react-infinite-scroll-component": "^6.1.0", "react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^9.0.1", "react-markdown": "^10.1.0",
"react-redux": "^9.1.2", "react-redux": "^9.1.2",
"react-router": "6", "react-router": "6",
"react-router-dom": "6", "react-router-dom": "6",
@@ -200,22 +214,31 @@
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"rehype-mathjax": "^7.0.0", "rehype-mathjax": "^7.1.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-cjk-friendly": "^1.1.0", "remark-cjk-friendly": "^1.2.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.1",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remove-markdown": "^0.6.2",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.88.0", "sass": "^1.88.0",
"shiki": "^3.4.2", "shiki": "^3.7.0",
"string-width": "^7.2.0", "string-width": "^7.2.0",
"styled-components": "^6.1.11", "styled-components": "^6.1.11",
"tar": "^7.4.3",
"tiny-pinyin": "^1.3.2", "tiny-pinyin": "^1.3.2",
"tokenx": "^0.4.1", "tokenx": "^1.1.0",
"typescript": "^5.6.2", "typescript": "^5.6.2",
"unified": "^11.0.5",
"uuid": "^10.0.0", "uuid": "^10.0.0",
"vite": "6.2.6", "vite": "6.2.6",
"vitest": "^3.1.4" "vitest": "^3.1.4",
"webdav": "^5.8.0",
"word-extractor": "^1.0.4",
"zipread": "^1.3.3"
},
"optionalDependencies": {
"@cherrystudio/mac-system-ocr": "^0.2.2"
}, },
"resolutions": { "resolutions": {
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch", "pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",

View File

@@ -3,6 +3,8 @@ export enum IpcChannel {
App_ClearCache = 'app:clear-cache', App_ClearCache = 'app:clear-cache',
App_SetLaunchOnBoot = 'app:set-launch-on-boot', App_SetLaunchOnBoot = 'app:set-launch-on-boot',
App_SetLanguage = 'app:set-language', App_SetLanguage = 'app:set-language',
App_SetEnableSpellCheck = 'app:set-enable-spell-check',
App_SetSpellCheckLanguages = 'app:set-spell-check-languages',
App_ShowUpdateDialog = 'app:show-update-dialog', App_ShowUpdateDialog = 'app:show-update-dialog',
App_CheckForUpdate = 'app:check-for-update', App_CheckForUpdate = 'app:check-for-update',
App_Reload = 'app:reload', App_Reload = 'app:reload',
@@ -13,20 +15,34 @@ export enum IpcChannel {
App_SetTrayOnClose = 'app:set-tray-on-close', App_SetTrayOnClose = 'app:set-tray-on-close',
App_SetTheme = 'app:set-theme', App_SetTheme = 'app:set-theme',
App_SetAutoUpdate = 'app:set-auto-update', App_SetAutoUpdate = 'app:set-auto-update',
App_SetFeedUrl = 'app:set-feed-url', App_SetTestPlan = 'app:set-test-plan',
App_SetTestChannel = 'app:set-test-channel',
App_HandleZoomFactor = 'app:handle-zoom-factor', App_HandleZoomFactor = 'app:handle-zoom-factor',
App_Select = 'app:select',
App_HasWritePermission = 'app:has-write-permission',
App_Copy = 'app:copy',
App_SetStopQuitApp = 'app:set-stop-quit-app',
App_SetAppDataPath = 'app:set-app-data-path',
App_GetDataPathFromArgs = 'app:get-data-path-from-args',
App_FlushAppData = 'app:flush-app-data',
App_IsNotEmptyDir = 'app:is-not-empty-dir',
App_RelaunchApp = 'app:relaunch-app',
App_IsBinaryExist = 'app:is-binary-exist', App_IsBinaryExist = 'app:is-binary-exist',
App_GetBinaryPath = 'app:get-binary-path', App_GetBinaryPath = 'app:get-binary-path',
App_InstallUvBinary = 'app:install-uv-binary', App_InstallUvBinary = 'app:install-uv-binary',
App_InstallBunBinary = 'app:install-bun-binary', App_InstallBunBinary = 'app:install-bun-binary',
App_MacIsProcessTrusted = 'app:mac-is-process-trusted',
App_MacRequestProcessTrust = 'app:mac-request-process-trust',
App_QuoteToMain = 'app:quote-to-main', App_QuoteToMain = 'app:quote-to-main',
App_SetDisableHardwareAcceleration = 'app:set-disable-hardware-acceleration',
Notification_Send = 'notification:send', Notification_Send = 'notification:send',
Notification_OnClick = 'notification:on-click', Notification_OnClick = 'notification:on-click',
Webview_SetOpenLinkExternal = 'webview:set-open-link-external', Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
Webview_SetSpellCheckEnabled = 'webview:set-spell-check-enabled',
// Open // Open
Open_Path = 'open:path', Open_Path = 'open:path',
@@ -58,6 +74,13 @@ export enum IpcChannel {
Mcp_ServersChanged = 'mcp:servers-changed', Mcp_ServersChanged = 'mcp:servers-changed',
Mcp_ServersUpdated = 'mcp:servers-updated', Mcp_ServersUpdated = 'mcp:servers-updated',
Mcp_CheckConnectivity = 'mcp:check-connectivity', Mcp_CheckConnectivity = 'mcp:check-connectivity',
Mcp_UploadDxt = 'mcp:upload-dxt',
Mcp_SetProgress = 'mcp:set-progress',
Mcp_AbortTool = 'mcp:abort-tool',
Mcp_GetServerVersion = 'mcp:get-server-version',
// Python
Python_Execute = 'python:execute',
//copilot //copilot
Copilot_GetAuthMessage = 'copilot:get-auth-message', Copilot_GetAuthMessage = 'copilot:get-auth-message',
@@ -86,6 +109,10 @@ export enum IpcChannel {
Gemini_ListFiles = 'gemini:list-files', Gemini_ListFiles = 'gemini:list-files',
Gemini_DeleteFile = 'gemini:delete-file', Gemini_DeleteFile = 'gemini:delete-file',
// VertexAI
VertexAI_GetAuthHeaders = 'vertexai:get-auth-headers',
VertexAI_ClearAuthCache = 'vertexai:clear-auth-cache',
Windows_ResetMinimumSize = 'window:reset-minimum-size', Windows_ResetMinimumSize = 'window:reset-minimum-size',
Windows_SetMinimumSize = 'window:set-minimum-size', Windows_SetMinimumSize = 'window:set-minimum-size',
@@ -96,6 +123,7 @@ export enum IpcChannel {
KnowledgeBase_Remove = 'knowledge-base:remove', KnowledgeBase_Remove = 'knowledge-base:remove',
KnowledgeBase_Search = 'knowledge-base:search', KnowledgeBase_Search = 'knowledge-base:search',
KnowledgeBase_Rerank = 'knowledge-base:rerank', KnowledgeBase_Rerank = 'knowledge-base:rerank',
KnowledgeBase_Check_Quota = 'knowledge-base:check-quota',
//file //file
File_Open = 'file:open', File_Open = 'file:open',
@@ -106,9 +134,10 @@ export enum IpcChannel {
File_Clear = 'file:clear', File_Clear = 'file:clear',
File_Read = 'file:read', File_Read = 'file:read',
File_Delete = 'file:delete', File_Delete = 'file:delete',
File_DeleteDir = 'file:deleteDir',
File_Get = 'file:get', File_Get = 'file:get',
File_SelectFolder = 'file:selectFolder', File_SelectFolder = 'file:selectFolder',
File_Create = 'file:create', File_CreateTempFile = 'file:createTempFile',
File_Write = 'file:write', File_Write = 'file:write',
File_WriteWithId = 'file:writeWithId', File_WriteWithId = 'file:writeWithId',
File_SaveImage = 'file:saveImage', File_SaveImage = 'file:saveImage',
@@ -118,7 +147,15 @@ export enum IpcChannel {
File_Copy = 'file:copy', File_Copy = 'file:copy',
File_BinaryImage = 'file:binaryImage', File_BinaryImage = 'file:binaryImage',
File_Base64File = 'file:base64File', File_Base64File = 'file:base64File',
File_GetPdfInfo = 'file:getPdfInfo',
Fs_Read = 'fs:read', Fs_Read = 'fs:read',
File_OpenWithRelativePath = 'file:openWithRelativePath',
// file service
FileService_Upload = 'file-service:upload',
FileService_List = 'file-service:list',
FileService_Delete = 'file-service:delete',
FileService_Retrieve = 'file-service:retrieve',
Export_Word = 'export:word', Export_Word = 'export:word',
@@ -133,6 +170,16 @@ export enum IpcChannel {
Backup_CheckConnection = 'backup:checkConnection', Backup_CheckConnection = 'backup:checkConnection',
Backup_CreateDirectory = 'backup:createDirectory', Backup_CreateDirectory = 'backup:createDirectory',
Backup_DeleteWebdavFile = 'backup:deleteWebdavFile', 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
Zip_Compress = 'zip:compress', Zip_Compress = 'zip:compress',
@@ -197,5 +244,17 @@ export enum IpcChannel {
Selection_ActionWindowMinimize = 'selection:action-window-minimize', Selection_ActionWindowMinimize = 'selection:action-window-minimize',
Selection_ActionWindowPin = 'selection:action-window-pin', Selection_ActionWindowPin = 'selection:action-window-pin',
Selection_ProcessAction = 'selection:process-action', Selection_ProcessAction = 'selection:process-action',
Selection_UpdateActionData = 'selection:update-action-data' Selection_UpdateActionData = 'selection:update-action-data',
// Memory
Memory_Add = 'memory:add',
Memory_Search = 'memory:search',
Memory_List = 'memory:list',
Memory_Delete = 'memory:delete',
Memory_Update = 'memory:update',
Memory_Get = 'memory:get',
Memory_SetConfig = 'memory:set-config',
Memory_DeleteUser = 'memory:delete-user',
Memory_DeleteAllMemoriesForUser = 'memory:delete-all-memories-for-user',
Memory_GetUsersList = 'memory:get-users-list'
} }

View File

@@ -1,7 +1,7 @@
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'] export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv'] export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac'] export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods'] export const documentExts = ['.pdf', '.doc', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const thirdPartyApplicationExts = ['.draftsExport'] export const thirdPartyApplicationExts = ['.draftsExport']
export const bookExts = ['.epub'] export const bookExts = ['.epub']
const textExtsByCategory = new Map([ const textExtsByCategory = new Map([
@@ -193,6 +193,7 @@ const textExtsByCategory = new Map([
'.htm', '.htm',
'.xhtml', // HTML '.xhtml', // HTML
'.xml', // XML '.xml', // XML
'.fxml', // JavaFX XML
'.org', // Org-mode '.org', // Org-mode
'.wiki', // Wiki '.wiki', // Wiki
'.tex', '.tex',
@@ -406,5 +407,16 @@ export const defaultLanguage = 'en-US'
export enum FeedUrl { export enum FeedUrl {
PRODUCTION = 'https://releases.cherry-ai.com', PRODUCTION = 'https://releases.cherry-ai.com',
EARLY_ACCESS = 'https://github.com/CherryHQ/cherry-studio/releases/latest/download' GITHUB_LATEST = 'https://github.com/CherryHQ/cherry-studio/releases/latest/download',
PRERELEASE_LOWEST = 'https://github.com/CherryHQ/cherry-studio/releases/download/v1.4.0'
} }
export enum UpgradeChannel {
LATEST = 'latest', // 最新稳定版本
RC = 'rc', // 公测版本
BETA = 'beta' // 预览版本
}
export const defaultTimeout = 10 * 1000 * 60
export const occupiedDirs = ['logs', 'Network', 'Partitions/webview/Network']

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,11 @@
import { ProcessingStatus } from '@types'
export type LoaderReturn = { export type LoaderReturn = {
entriesAdded: number entriesAdded: number
uniqueId: string uniqueId: string
uniqueIds: string[] uniqueIds: string[]
loaderType: string loaderType: string
status?: ProcessingStatus
message?: string
messageSource?: 'preprocess' | 'embedding'
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,12 +2,12 @@ const fs = require('fs')
const path = require('path') const path = require('path')
const os = require('os') const os = require('os')
const { execSync } = require('child_process') const { execSync } = require('child_process')
const AdmZip = require('adm-zip') const StreamZip = require('node-stream-zip')
const { downloadWithRedirects } = require('./download') const { downloadWithRedirects } = require('./download')
// Base URL for downloading bun binaries // Base URL for downloading bun binaries
const BUN_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/bun/releases/download' const BUN_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/bun/releases/download'
const DEFAULT_BUN_VERSION = '1.2.9' // Default fallback version const DEFAULT_BUN_VERSION = '1.2.17' // Default fallback version
// Mapping of platform+arch to binary package name // Mapping of platform+arch to binary package name
const BUN_PACKAGES = { const BUN_PACKAGES = {
@@ -43,7 +43,7 @@ async function downloadBunBinary(platform, arch, version = DEFAULT_BUN_VERSION,
if (!packageName) { if (!packageName) {
console.error(`No binary available for ${platformKey}`) console.error(`No binary available for ${platformKey}`)
return false return 101
} }
// Create output directory structure // Create output directory structure
@@ -66,38 +66,41 @@ async function downloadBunBinary(platform, arch, version = DEFAULT_BUN_VERSION,
// Extract the zip file using adm-zip // Extract the zip file using adm-zip
console.log(`Extracting ${packageName} to ${binDir}...`) console.log(`Extracting ${packageName} to ${binDir}...`)
const zip = new AdmZip(tempFilename) const zip = new StreamZip.async({ file: tempFilename })
zip.extractAllTo(tempdir, true)
// Move files using Node.js fs // Get all entries in the zip file
const sourceDir = path.join(tempdir, packageName.split('.')[0]) const entries = await zip.entries()
const files = fs.readdirSync(sourceDir)
for (const file of files) { // Extract files directly to binDir, flattening the directory structure
const sourcePath = path.join(sourceDir, file) for (const entry of Object.values(entries)) {
const destPath = path.join(binDir, file) if (!entry.isDirectory) {
// Get just the filename without path
const filename = path.basename(entry.name)
const outputPath = path.join(binDir, filename)
fs.copyFileSync(sourcePath, destPath) console.log(`Extracting ${entry.name} -> ${filename}`)
fs.unlinkSync(sourcePath) await zip.extract(entry.name, outputPath)
// Make executable files executable on Unix-like systems
// Set executable permissions for non-Windows platforms if (platform !== 'win32') {
if (platform !== 'win32') { try {
try { fs.chmodSync(outputPath, 0o755)
// 755 permission: rwxr-xr-x } catch (chmodError) {
fs.chmodSync(destPath, '755') console.error(`Warning: Failed to set executable permissions on ${filename}`)
} catch (error) { return 102
console.warn(`Warning: Failed to set executable permissions: ${error.message}`) }
} }
console.log(`Extracted ${entry.name} -> ${outputPath}`)
} }
} }
await zip.close()
// Clean up // Clean up
fs.unlinkSync(tempFilename) fs.unlinkSync(tempFilename)
fs.rmSync(sourceDir, { recursive: true })
console.log(`Successfully installed bun ${version} for ${platformKey}`) console.log(`Successfully installed bun ${version} for ${platformKey}`)
return true return 0
} catch (error) { } catch (error) {
let retCode = 103
console.error(`Error installing bun for ${platformKey}: ${error.message}`) console.error(`Error installing bun for ${platformKey}: ${error.message}`)
// Clean up temporary file if it exists // Clean up temporary file if it exists
if (fs.existsSync(tempFilename)) { if (fs.existsSync(tempFilename)) {
@@ -113,9 +116,10 @@ async function downloadBunBinary(platform, arch, version = DEFAULT_BUN_VERSION,
} }
} catch (cleanupError) { } catch (cleanupError) {
console.warn(`Warning: Failed to clean up directory: ${cleanupError.message}`) console.warn(`Warning: Failed to clean up directory: ${cleanupError.message}`)
retCode = 104
} }
return false return retCode
} }
} }
@@ -158,16 +162,21 @@ async function installBun() {
`Installing bun ${version} for ${platform}-${arch}${isMusl ? ' (MUSL)' : ''}${isBaseline ? ' (baseline)' : ''}...` `Installing bun ${version} for ${platform}-${arch}${isMusl ? ' (MUSL)' : ''}${isBaseline ? ' (baseline)' : ''}...`
) )
await downloadBunBinary(platform, arch, version, isMusl, isBaseline) return await downloadBunBinary(platform, arch, version, isMusl, isBaseline)
} }
// Run the installation // Run the installation
installBun() installBun()
.then(() => { .then((retCode) => {
console.log('Installation successful') if (retCode === 0) {
process.exit(0) console.log('Installation successful')
process.exit(0)
} else {
console.error('Installation failed')
process.exit(retCode)
}
}) })
.catch((error) => { .catch((error) => {
console.error('Installation failed:', error) console.error('Installation failed:', error)
process.exit(1) process.exit(100)
}) })

View File

@@ -2,34 +2,33 @@ const fs = require('fs')
const path = require('path') const path = require('path')
const os = require('os') const os = require('os')
const { execSync } = require('child_process') const { execSync } = require('child_process')
const tar = require('tar') const StreamZip = require('node-stream-zip')
const AdmZip = require('adm-zip')
const { downloadWithRedirects } = require('./download') const { downloadWithRedirects } = require('./download')
// Base URL for downloading uv binaries // Base URL for downloading uv binaries
const UV_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/uv/releases/download' const UV_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/uv/releases/download'
const DEFAULT_UV_VERSION = '0.6.14' const DEFAULT_UV_VERSION = '0.7.13'
// Mapping of platform+arch to binary package name // Mapping of platform+arch to binary package name
const UV_PACKAGES = { const UV_PACKAGES = {
'darwin-arm64': 'uv-aarch64-apple-darwin.tar.gz', 'darwin-arm64': 'uv-aarch64-apple-darwin.zip',
'darwin-x64': 'uv-x86_64-apple-darwin.tar.gz', 'darwin-x64': 'uv-x86_64-apple-darwin.zip',
'win32-arm64': 'uv-aarch64-pc-windows-msvc.zip', 'win32-arm64': 'uv-aarch64-pc-windows-msvc.zip',
'win32-ia32': 'uv-i686-pc-windows-msvc.zip', 'win32-ia32': 'uv-i686-pc-windows-msvc.zip',
'win32-x64': 'uv-x86_64-pc-windows-msvc.zip', 'win32-x64': 'uv-x86_64-pc-windows-msvc.zip',
'linux-arm64': 'uv-aarch64-unknown-linux-gnu.tar.gz', 'linux-arm64': 'uv-aarch64-unknown-linux-gnu.zip',
'linux-ia32': 'uv-i686-unknown-linux-gnu.tar.gz', 'linux-ia32': 'uv-i686-unknown-linux-gnu.zip',
'linux-ppc64': 'uv-powerpc64-unknown-linux-gnu.tar.gz', 'linux-ppc64': 'uv-powerpc64-unknown-linux-gnu.zip',
'linux-ppc64le': 'uv-powerpc64le-unknown-linux-gnu.tar.gz', 'linux-ppc64le': 'uv-powerpc64le-unknown-linux-gnu.zip',
'linux-s390x': 'uv-s390x-unknown-linux-gnu.tar.gz', 'linux-s390x': 'uv-s390x-unknown-linux-gnu.zip',
'linux-x64': 'uv-x86_64-unknown-linux-gnu.tar.gz', 'linux-x64': 'uv-x86_64-unknown-linux-gnu.zip',
'linux-armv7l': 'uv-armv7-unknown-linux-gnueabihf.tar.gz', 'linux-armv7l': 'uv-armv7-unknown-linux-gnueabihf.zip',
// MUSL variants // MUSL variants
'linux-musl-arm64': 'uv-aarch64-unknown-linux-musl.tar.gz', 'linux-musl-arm64': 'uv-aarch64-unknown-linux-musl.zip',
'linux-musl-ia32': 'uv-i686-unknown-linux-musl.tar.gz', 'linux-musl-ia32': 'uv-i686-unknown-linux-musl.zip',
'linux-musl-x64': 'uv-x86_64-unknown-linux-musl.tar.gz', 'linux-musl-x64': 'uv-x86_64-unknown-linux-musl.zip',
'linux-musl-armv6l': 'uv-arm-unknown-linux-musleabihf.tar.gz', 'linux-musl-armv6l': 'uv-arm-unknown-linux-musleabihf.zip',
'linux-musl-armv7l': 'uv-armv7-unknown-linux-musleabihf.tar.gz' 'linux-musl-armv7l': 'uv-armv7-unknown-linux-musleabihf.zip'
} }
/** /**
@@ -45,7 +44,7 @@ async function downloadUvBinary(platform, arch, version = DEFAULT_UV_VERSION, is
if (!packageName) { if (!packageName) {
console.error(`No binary available for ${platformKey}`) console.error(`No binary available for ${platformKey}`)
return false return 101
} }
// Create output directory structure // Create output directory structure
@@ -66,49 +65,40 @@ async function downloadUvBinary(platform, arch, version = DEFAULT_UV_VERSION, is
console.log(`Extracting ${packageName} to ${binDir}...`) console.log(`Extracting ${packageName} to ${binDir}...`)
// 根据文件扩展名选择解压方法 const zip = new StreamZip.async({ file: tempFilename })
if (packageName.endsWith('.zip')) {
// 使用 adm-zip 处理 zip 文件
const zip = new AdmZip(tempFilename)
zip.extractAllTo(binDir, true)
fs.unlinkSync(tempFilename)
console.log(`Successfully installed uv ${version} for ${platform}-${arch}`)
return true
} else {
// tar.gz 文件的处理保持不变
await tar.x({
file: tempFilename,
cwd: tempdir,
z: true
})
// Move files using Node.js fs // Get all entries in the zip file
const sourceDir = path.join(tempdir, packageName.split('.')[0]) const entries = await zip.entries()
const files = fs.readdirSync(sourceDir)
for (const file of files) {
const sourcePath = path.join(sourceDir, file)
const destPath = path.join(binDir, file)
fs.copyFileSync(sourcePath, destPath)
fs.unlinkSync(sourcePath)
// Set executable permissions for non-Windows platforms // Extract files directly to binDir, flattening the directory structure
for (const entry of Object.values(entries)) {
if (!entry.isDirectory) {
// Get just the filename without path
const filename = path.basename(entry.name)
const outputPath = path.join(binDir, filename)
console.log(`Extracting ${entry.name} -> ${filename}`)
await zip.extract(entry.name, outputPath)
// Make executable files executable on Unix-like systems
if (platform !== 'win32') { if (platform !== 'win32') {
try { try {
fs.chmodSync(destPath, '755') fs.chmodSync(outputPath, 0o755)
} catch (error) { } catch (chmodError) {
console.warn(`Warning: Failed to set executable permissions: ${error.message}`) console.error(`Warning: Failed to set executable permissions on ${filename}`)
return 102
} }
} }
console.log(`Extracted ${entry.name} -> ${outputPath}`)
} }
// Clean up
fs.unlinkSync(tempFilename)
fs.rmSync(sourceDir, { recursive: true })
} }
await zip.close()
fs.unlinkSync(tempFilename)
console.log(`Successfully installed uv ${version} for ${platform}-${arch}`) console.log(`Successfully installed uv ${version} for ${platform}-${arch}`)
return true return 0
} catch (error) { } catch (error) {
let retCode = 103
console.error(`Error installing uv for ${platformKey}: ${error.message}`) console.error(`Error installing uv for ${platformKey}: ${error.message}`)
if (fs.existsSync(tempFilename)) { if (fs.existsSync(tempFilename)) {
@@ -124,9 +114,10 @@ async function downloadUvBinary(platform, arch, version = DEFAULT_UV_VERSION, is
} }
} catch (cleanupError) { } catch (cleanupError) {
console.warn(`Warning: Failed to clean up directory: ${cleanupError.message}`) console.warn(`Warning: Failed to clean up directory: ${cleanupError.message}`)
retCode = 104
} }
return false return retCode
} }
} }
@@ -166,16 +157,21 @@ async function installUv() {
console.log(`Installing uv ${version} for ${platform}-${arch}${isMusl ? ' (MUSL)' : ''}...`) console.log(`Installing uv ${version} for ${platform}-${arch}${isMusl ? ' (MUSL)' : ''}...`)
await downloadUvBinary(platform, arch, version, isMusl) return await downloadUvBinary(platform, arch, version, isMusl)
} }
// Run the installation // Run the installation
installUv() installUv()
.then(() => { .then((retCode) => {
console.log('Installation successful') if (retCode === 0) {
process.exit(0) console.log('Installation successful')
process.exit(0)
} else {
console.error('Installation failed')
process.exit(retCode)
}
}) })
.catch((error) => { .catch((error) => {
console.error('Installation failed:', error) console.error('Installation failed:', error)
process.exit(1) process.exit(100)
}) })

View File

@@ -23,6 +23,9 @@ exports.default = async function (context) {
const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules') const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules')
const _arch = arch === Arch.arm64 ? ['linux-arm64-gnu', 'linux-arm64-musl'] : ['linux-x64-gnu', 'linux-x64-musl'] const _arch = arch === Arch.arm64 ? ['linux-arm64-gnu', 'linux-arm64-musl'] : ['linux-x64-gnu', 'linux-x64-musl']
keepPackageNodeFiles(node_modules_path, '@libsql', _arch) keepPackageNodeFiles(node_modules_path, '@libsql', _arch)
// 删除 macOS 专用的 OCR 包
removeMacOnlyPackages(node_modules_path)
} }
if (platform === 'windows') { if (platform === 'windows') {
@@ -35,7 +38,30 @@ exports.default = async function (context) {
keepPackageNodeFiles(node_modules_path, '@strongtz', ['win32-x64-msvc']) keepPackageNodeFiles(node_modules_path, '@strongtz', ['win32-x64-msvc'])
keepPackageNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc']) keepPackageNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
} }
removeMacOnlyPackages(node_modules_path)
} }
if (platform === 'windows') {
fs.rmSync(path.join(context.appOutDir, 'LICENSE.electron.txt'), { force: true })
fs.rmSync(path.join(context.appOutDir, 'LICENSES.chromium.html'), { force: true })
}
}
/**
* 删除 macOS 专用的包
* @param {string} nodeModulesPath
*/
function removeMacOnlyPackages(nodeModulesPath) {
const macOnlyPackages = ['@cherrystudio/mac-system-ocr']
macOnlyPackages.forEach((packageName) => {
const packagePath = path.join(nodeModulesPath, packageName)
if (fs.existsSync(packagePath)) {
fs.rmSync(packagePath, { recursive: true, force: true })
console.log(`[After Pack] Removed macOS-only package: ${packageName}`)
}
})
} }
/** /**

View File

@@ -1,9 +1,60 @@
'use strict' 'use strict'
var __createBinding =
(this && this.__createBinding) ||
(Object.create
? function (o, m, k, k2) {
if (k2 === undefined) k2 = k
var desc = Object.getOwnPropertyDescriptor(m, k)
if (!desc || ('get' in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = {
enumerable: true,
get: function () {
return m[k]
}
}
}
Object.defineProperty(o, k2, desc)
}
: function (o, m, k, k2) {
if (k2 === undefined) k2 = k
o[k2] = m[k]
})
var __setModuleDefault =
(this && this.__setModuleDefault) ||
(Object.create
? function (o, v) {
Object.defineProperty(o, 'default', { enumerable: true, value: v })
}
: function (o, v) {
o['default'] = v
})
var __importStar =
(this && this.__importStar) ||
(function () {
var ownKeys = function (o) {
ownKeys =
Object.getOwnPropertyNames ||
function (o) {
var ar = []
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k
return ar
}
return ownKeys(o)
}
return function (mod) {
if (mod && mod.__esModule) return mod
var result = {}
if (mod != null)
for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== 'default') __createBinding(result, mod, k[i])
__setModuleDefault(result, mod)
return result
}
})()
Object.defineProperty(exports, '__esModule', { value: true }) Object.defineProperty(exports, '__esModule', { value: true })
var fs = require('fs') var fs = __importStar(require('fs'))
var path = require('path') var path = __importStar(require('path'))
var translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales') var translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
var baseLocale = 'en-us' var baseLocale = 'zh-cn'
var baseFileName = ''.concat(baseLocale, '.json') var baseFileName = ''.concat(baseLocale, '.json')
var baseFilePath = path.join(translationsDir, baseFileName) var baseFilePath = path.join(translationsDir, baseFileName)
/** /**
@@ -48,12 +99,43 @@ function syncRecursively(target, template) {
} }
return isUpdated return isUpdated
} }
/**
* 检查 JSON 对象中是否存在重复键,并收集所有重复键
* @param obj 要检查的对象
* @returns 返回重复键的数组(若无重复则返回空数组)
*/
function checkDuplicateKeys(obj) {
var keys = new Set()
var duplicateKeys = []
var checkObject = function (obj, path) {
if (path === void 0) {
path = ''
}
for (var key in obj) {
var fullPath = path ? ''.concat(path, '.').concat(key) : key
if (keys.has(fullPath)) {
// 发现重复键时,添加到数组中(避免重复添加)
if (!duplicateKeys.includes(fullPath)) {
duplicateKeys.push(fullPath)
}
} else {
keys.add(fullPath)
}
// 递归检查子对象
if (typeof obj[key] === 'object' && obj[key] !== null) {
checkObject(obj[key], fullPath)
}
}
}
checkObject(obj)
return duplicateKeys
}
function syncTranslations() { function syncTranslations() {
if (!fs.existsSync(baseFilePath)) { if (!fs.existsSync(baseFilePath)) {
console.error( console.error(
'\u4E3B\u6A21\u677F\u6587\u4EF6 '.concat( '\u4E3B\u6A21\u677F\u6587\u4EF6 '.concat(
baseFileName, baseFileName,
' \u4E0D\u5B58\u5728\uFF0C\u8BF7\u68C0\u67E5\u8DEF\u5F84\u6216\u6587\u4EF6\u540D\u3002' ' \u4E0D\u5B58\u5728\uFF0C\u8BF7\u68C0\u67E5\u8DEF\u5F84\u6216\u6587\u4EF6\u540D'
) )
) )
return return
@@ -63,9 +145,18 @@ function syncTranslations() {
try { try {
baseJson = JSON.parse(baseContent) baseJson = JSON.parse(baseContent)
} catch (error) { } catch (error) {
console.error('\u89E3\u6790 '.concat(baseFileName, ' \u51FA\u9519:'), error) console.error('\u89E3\u6790 '.concat(baseFileName, ' \u51FA\u9519\u3002').concat(error))
return return
} }
// 检查主模板是否存在重复键
var duplicateKeys = checkDuplicateKeys(baseJson)
if (duplicateKeys.length > 0) {
throw new Error(
'\u4E3B\u6A21\u677F\u6587\u4EF6 '
.concat(baseFileName, ' \u5B58\u5728\u4EE5\u4E0B\u91CD\u590D\u952E\uFF1A\n')
.concat(duplicateKeys.join('\n'))
)
}
var files = fs.readdirSync(translationsDir).filter(function (file) { var files = fs.readdirSync(translationsDir).filter(function (file) {
return file.endsWith('.json') && file !== baseFileName return file.endsWith('.json') && file !== baseFileName
}) })
@@ -77,27 +168,19 @@ function syncTranslations() {
var fileContent = fs.readFileSync(filePath, 'utf-8') var fileContent = fs.readFileSync(filePath, 'utf-8')
targetJson = JSON.parse(fileContent) targetJson = JSON.parse(fileContent)
} catch (error) { } catch (error) {
console.error( console.error('\u89E3\u6790 '.concat(file, ' \u51FA\u9519\uFF0C\u8DF3\u8FC7\u6B64\u6587\u4EF6\u3002'), error)
'\u89E3\u6790 '.concat(
file,
' \u51FA\u9519\uFF0C\u8DF3\u8FC7\u6B64\u6587\u4EF6\u3002\u9519\u8BEF\u4FE1\u606F:'
),
error
)
continue continue
} }
var isUpdated = syncRecursively(targetJson, baseJson) var isUpdated = syncRecursively(targetJson, baseJson)
if (isUpdated) { if (isUpdated) {
try { try {
fs.writeFileSync(filePath, JSON.stringify(targetJson, null, 2), 'utf-8') fs.writeFileSync(filePath, JSON.stringify(targetJson, null, 2) + '\n', 'utf-8')
console.log( console.log('\u6587\u4EF6 '.concat(file, ' \u5DF2\u66F4\u65B0\u540C\u6B65\u4E3B\u6A21\u677F\u7684\u5185\u5BB9'))
'\u6587\u4EF6 '.concat(file, ' \u5DF2\u66F4\u65B0\u540C\u6B65\u4E3B\u6A21\u677F\u7684\u5185\u5BB9\u3002')
)
} catch (error) { } catch (error) {
console.error('\u5199\u5165 '.concat(file, ' \u51FA\u9519:'), error) console.error('\u5199\u5165 '.concat(file, ' \u51FA\u9519\u3002').concat(error))
} }
} else { } else {
console.log('\u6587\u4EF6 '.concat(file, ' \u65E0\u9700\u66F4\u65B0\u3002')) console.log('\u6587\u4EF6 '.concat(file, ' \u65E0\u9700\u66F4\u65B0'))
} }
} }
} }

View File

@@ -2,7 +2,7 @@ import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
const translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales') const translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
const baseLocale = 'zh-CN' const baseLocale = 'zh-cn'
const baseFileName = `${baseLocale}.json` const baseFileName = `${baseLocale}.json`
const baseFilePath = path.join(translationsDir, baseFileName) const baseFilePath = path.join(translationsDir, baseFileName)
@@ -52,6 +52,39 @@ function syncRecursively(target: any, template: any): boolean {
return isUpdated return isUpdated
} }
/**
* 检查 JSON 对象中是否存在重复键,并收集所有重复键
* @param obj 要检查的对象
* @returns 返回重复键的数组(若无重复则返回空数组)
*/
function checkDuplicateKeys(obj: Record<string, any>): string[] {
const keys = new Set<string>()
const duplicateKeys: string[] = []
const checkObject = (obj: Record<string, any>, path: string = '') => {
for (const key in obj) {
const fullPath = path ? `${path}.${key}` : key
if (keys.has(fullPath)) {
// 发现重复键时,添加到数组中(避免重复添加)
if (!duplicateKeys.includes(fullPath)) {
duplicateKeys.push(fullPath)
}
} else {
keys.add(fullPath)
}
// 递归检查子对象
if (typeof obj[key] === 'object' && obj[key] !== null) {
checkObject(obj[key], fullPath)
}
}
}
checkObject(obj)
return duplicateKeys
}
function syncTranslations() { function syncTranslations() {
if (!fs.existsSync(baseFilePath)) { if (!fs.existsSync(baseFilePath)) {
console.error(`主模板文件 ${baseFileName} 不存在,请检查路径或文件名`) console.error(`主模板文件 ${baseFileName} 不存在,请检查路径或文件名`)
@@ -63,10 +96,16 @@ function syncTranslations() {
try { try {
baseJson = JSON.parse(baseContent) baseJson = JSON.parse(baseContent)
} catch (error) { } catch (error) {
console.error(`解析 ${baseFileName} 出错:`, error) console.error(`解析 ${baseFileName} 出错${error}`)
return return
} }
// 检查主模板是否存在重复键
const duplicateKeys = checkDuplicateKeys(baseJson)
if (duplicateKeys.length > 0) {
throw new Error(`主模板文件 ${baseFileName} 存在以下重复键:\n${duplicateKeys.join('\n')}`)
}
const files = fs.readdirSync(translationsDir).filter((file) => file.endsWith('.json') && file !== baseFileName) const files = fs.readdirSync(translationsDir).filter((file) => file.endsWith('.json') && file !== baseFileName)
for (const file of files) { for (const file of files) {
@@ -76,7 +115,7 @@ function syncTranslations() {
const fileContent = fs.readFileSync(filePath, 'utf-8') const fileContent = fs.readFileSync(filePath, 'utf-8')
targetJson = JSON.parse(fileContent) targetJson = JSON.parse(fileContent)
} catch (error) { } catch (error) {
console.error(`解析 ${file} 出错,跳过此文件。错误信息:`, error) console.error(`解析 ${file} 出错,跳过此文件。`, error)
continue continue
} }
@@ -87,7 +126,7 @@ function syncTranslations() {
fs.writeFileSync(filePath, JSON.stringify(targetJson, null, 2) + '\n', 'utf-8') fs.writeFileSync(filePath, JSON.stringify(targetJson, null, 2) + '\n', 'utf-8')
console.log(`文件 ${file} 已更新同步主模板的内容`) console.log(`文件 ${file} 已更新同步主模板的内容`)
} catch (error) { } catch (error) {
console.error(`写入 ${file} 出错:`, error) console.error(`写入 ${file} 出错${error}`)
} }
} else { } else {
console.log(`文件 ${file} 无需更新`) console.log(`文件 ${file} 无需更新`)

View File

@@ -1,16 +1,19 @@
/** /**
* Paratera_API_KEY=sk-abcxxxxxxxxxxxxxxxxxxxxxxx123 ts-node scripts/update-i18n.ts * 使用 OpenAI 兼容的模型生成 i18n 文本,并更新到 translate 目录
*
* API_KEY=sk-xxxx BASE_URL=xxxx MODEL=xxxx ts-node scripts/update-i18n.ts
*/ */
// OCOOL API KEY const API_KEY = process.env.API_KEY
const Paratera_API_KEY = process.env.Paratera_API_KEY const BASE_URL = process.env.BASE_URL || 'https://llmapi.paratera.com/v1'
const MODEL = process.env.MODEL || 'Qwen3-235B-A22B'
const INDEX = [ const INDEX = [
// 语言的名称 代码 用来翻译的模型 // 语言的名称代码用来翻译的模型
{ name: 'France', code: 'fr-fr', model: 'Qwen3-235B-A22B' }, { name: 'France', code: 'fr-fr', model: MODEL },
{ name: 'Spanish', code: 'es-es', model: 'Qwen3-235B-A22B' }, { name: 'Spanish', code: 'es-es', model: MODEL },
{ name: 'Portuguese', code: 'pt-pt', model: 'Qwen3-235B-A22B' }, { name: 'Portuguese', code: 'pt-pt', model: MODEL },
{ name: 'Greek', code: 'el-gr', model: 'Qwen3-235B-A22B' } { name: 'Greek', code: 'el-gr', model: MODEL }
] ]
const fs = require('fs') const fs = require('fs')
@@ -19,8 +22,8 @@ import OpenAI from 'openai'
const zh = JSON.parse(fs.readFileSync('src/renderer/src/i18n/locales/zh-cn.json', 'utf8')) as object const zh = JSON.parse(fs.readFileSync('src/renderer/src/i18n/locales/zh-cn.json', 'utf8')) as object
const openai = new OpenAI({ const openai = new OpenAI({
apiKey: Paratera_API_KEY, apiKey: API_KEY,
baseURL: 'https://llmapi.paratera.com/v1' baseURL: BASE_URL
}) })
// 递归遍历翻译 // 递归遍历翻译

33
src/main/bootstrap.ts Normal file
View File

@@ -0,0 +1,33 @@
import { occupiedDirs } from '@shared/config/constant'
import { app } from 'electron'
import fs from 'fs'
import path from 'path'
import { initAppDataDir } from './utils/file'
app.isPackaged && initAppDataDir()
// 在主进程中复制 appData 中某些一直被占用的文件
// 在renderer进程还没有启动时主进程可以复制这些文件到新的appData中
function copyOccupiedDirsInMainProcess() {
const newAppDataPath = process.argv
.slice(1)
.find((arg) => arg.startsWith('--new-data-path='))
?.split('--new-data-path=')[1]
if (!newAppDataPath) {
return
}
if (process.platform === 'win32') {
const appDataPath = app.getPath('userData')
occupiedDirs.forEach((dir) => {
const dirPath = path.join(appDataPath, dir)
const newDirPath = path.join(newAppDataPath, dir)
if (fs.existsSync(dirPath)) {
fs.cpSync(dirPath, newDirPath, { recursive: true })
}
})
}
}
copyOccupiedDirsInMainProcess()

View File

@@ -1,7 +1,6 @@
import { app } from 'electron' import { app } from 'electron'
import { getDataPath } from './utils' import { getDataPath } from './utils'
const isDev = process.env.NODE_ENV === 'development' const isDev = process.env.NODE_ENV === 'development'
if (isDev) { if (isDev) {

View File

@@ -1,6 +1,6 @@
interface IFilterList { interface IFilterList {
WINDOWS: string[] WINDOWS: string[]
MAC?: string[] MAC: string[]
} }
interface IFinetunedList { interface IFinetunedList {
@@ -45,14 +45,17 @@ export const SELECTION_PREDEFINED_BLACKLIST: IFilterList = {
'sldworks.exe', 'sldworks.exe',
// Remote Desktop // Remote Desktop
'mstsc.exe' 'mstsc.exe'
] ],
MAC: []
} }
export const SELECTION_FINETUNED_LIST: IFinetunedList = { export const SELECTION_FINETUNED_LIST: IFinetunedList = {
EXCLUDE_CLIPBOARD_CURSOR_DETECT: { EXCLUDE_CLIPBOARD_CURSOR_DETECT: {
WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe'] WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe'],
MAC: []
}, },
INCLUDE_CLIPBOARD_DELAY_READ: { INCLUDE_CLIPBOARD_DELAY_READ: {
WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe', 'foxitphantom.exe'] WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe', 'foxitphantom.exe'],
MAC: []
} }
} }

View File

@@ -1,3 +1,8 @@
// don't reorder this file, it's used to initialize the app data dir and
// other which should be run before the main process is ready
// eslint-disable-next-line
import './bootstrap'
import '@main/config' import '@main/config'
import { electronApp, optimizer } from '@electron-toolkit/utils' import { electronApp, optimizer } from '@electron-toolkit/utils'
@@ -6,7 +11,7 @@ import { app } from 'electron'
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer' import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
import Logger from 'electron-log' import Logger from 'electron-log'
import { isDev, isWin } from './constant' import { isDev, isWin, isLinux } from './constant'
import { registerIpc } from './ipc' import { registerIpc } from './ipc'
import { configManager } from './services/ConfigManager' import { configManager } from './services/ConfigManager'
import mcpService from './services/MCPService' import mcpService from './services/MCPService'
@@ -20,10 +25,17 @@ import selectionService, { initSelectionService } from './services/SelectionServ
import { registerShortcuts } from './services/ShortcutService' import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService' import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService' import { windowService } from './services/WindowService'
import { setUserDataDir } from './utils/file'
Logger.initialize() Logger.initialize()
/**
* Disable hardware acceleration if setting is enabled
*/
const disableHardwareAcceleration = configManager.getDisableHardwareAcceleration()
if (disableHardwareAcceleration) {
app.disableHardwareAcceleration()
}
/** /**
* Disable chromium's window animations * Disable chromium's window animations
* main purpose for this is to avoid the transparent window flashing when it is shown * main purpose for this is to avoid the transparent window flashing when it is shown
@@ -34,6 +46,14 @@ if (isWin) {
app.commandLine.appendSwitch('wm-window-animations-disabled') app.commandLine.appendSwitch('wm-window-animations-disabled')
} }
/**
* Enable GlobalShortcutsPortal for Linux Wayland Protocol
* see: https://www.electronjs.org/docs/latest/api/global-shortcut
*/
if (isLinux && process.env.XDG_SESSION_TYPE === 'wayland') {
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal')
}
// Enable features for unresponsive renderer js call stacks // Enable features for unresponsive renderer js call stacks
app.commandLine.appendSwitch('enable-features', 'DocumentPolicyIncludeJSCallStacksInCrashReports') app.commandLine.appendSwitch('enable-features', 'DocumentPolicyIncludeJSCallStacksInCrashReports')
app.on('web-contents-created', (_, webContents) => { app.on('web-contents-created', (_, webContents) => {
@@ -72,9 +92,6 @@ if (!app.requestSingleInstanceLock()) {
app.quit() app.quit()
process.exit(0) process.exit(0)
} else { } else {
// Portable dir must be setup before app ready
setUserDataDir()
// This method will be called when Electron has finished // This method will be called when Electron has finished
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.
@@ -123,19 +140,27 @@ if (!app.requestSingleInstanceLock()) {
registerProtocolClient(app) registerProtocolClient(app)
// macOS specific: handle protocol when app is already running // macOS specific: handle protocol when app is already running
app.on('open-url', (event, url) => { app.on('open-url', (event, url) => {
event.preventDefault() event.preventDefault()
handleProtocolUrl(url) handleProtocolUrl(url)
}) })
const handleOpenUrl = (args: string[]) => {
const url = args.find((arg) => arg.startsWith(CHERRY_STUDIO_PROTOCOL + '://'))
if (url) handleProtocolUrl(url)
}
// for windows to start with url
handleOpenUrl(process.argv)
// Listen for second instance // Listen for second instance
app.on('second-instance', (_event, argv) => { app.on('second-instance', (_event, argv) => {
windowService.showMainWindow() windowService.showMainWindow()
// Protocol handler for Windows/Linux // Protocol handler for Windows/Linux
// The commandLine is an array of strings where the last item might be the URL // The commandLine is an array of strings where the last item might be the URL
const url = argv.find((arg) => arg.startsWith(CHERRY_STUDIO_PROTOCOL + '://')) handleOpenUrl(argv)
if (url) handleProtocolUrl(url)
}) })
app.on('browser-window-created', (_, window) => { app.on('browser-window-created', (_, window) => {

View File

@@ -1,50 +1,63 @@
import fs from 'node:fs' import fs from 'node:fs'
import { arch } from 'node:os' import { arch } from 'node:os'
import path from 'node:path'
import { isMac, isWin } from '@main/constant' import { isLinux, isMac, isWin } from '@main/constant'
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process' import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
import { handleZoomFactor } from '@main/utils/zoom' import { handleZoomFactor } from '@main/utils/zoom'
import { UpgradeChannel } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { Shortcut, ThemeMode } from '@types' import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types'
import { BrowserWindow, ipcMain, session, shell } from 'electron' import { BrowserWindow, dialog, ipcMain, session, shell, systemPreferences, webContents } from 'electron'
import log from 'electron-log' import log from 'electron-log'
import { Notification } from 'src/renderer/src/types/notification' import { Notification } from 'src/renderer/src/types/notification'
import appService from './services/AppService'
import AppUpdater from './services/AppUpdater' import AppUpdater from './services/AppUpdater'
import BackupManager from './services/BackupManager' import BackupManager from './services/BackupManager'
import { configManager } from './services/ConfigManager' import { configManager } from './services/ConfigManager'
import CopilotService from './services/CopilotService' import CopilotService from './services/CopilotService'
import DxtService from './services/DxtService'
import { ExportService } from './services/ExportService' import { ExportService } from './services/ExportService'
import FileService from './services/FileService'
import FileStorage from './services/FileStorage' import FileStorage from './services/FileStorage'
import FileService from './services/FileSystemService'
import KnowledgeService from './services/KnowledgeService' import KnowledgeService from './services/KnowledgeService'
import mcpService from './services/MCPService' import mcpService from './services/MCPService'
import MemoryService from './services/memory/MemoryService'
import NotificationService from './services/NotificationService' import NotificationService from './services/NotificationService'
import * as NutstoreService from './services/NutstoreService' import * as NutstoreService from './services/NutstoreService'
import ObsidianVaultService from './services/ObsidianVaultService' import ObsidianVaultService from './services/ObsidianVaultService'
import { ProxyConfig, proxyManager } from './services/ProxyManager' import { ProxyConfig, proxyManager } from './services/ProxyManager'
import { pythonService } from './services/PythonService'
import { FileServiceManager } from './services/remotefile/FileServiceManager'
import { searchService } from './services/SearchService' import { searchService } from './services/SearchService'
import { SelectionService } from './services/SelectionService' import { SelectionService } from './services/SelectionService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService' import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import storeSyncService from './services/StoreSyncService' import storeSyncService from './services/StoreSyncService'
import { themeService } from './services/ThemeService' import { themeService } from './services/ThemeService'
import VertexAIService from './services/VertexAIService'
import { setOpenLinkExternal } from './services/WebviewService' import { setOpenLinkExternal } from './services/WebviewService'
import { windowService } from './services/WindowService' import { windowService } from './services/WindowService'
import { calculateDirectorySize, getResourcePath } from './utils' import { calculateDirectorySize, getResourcePath } from './utils'
import { decrypt, encrypt } from './utils/aes' import { decrypt, encrypt } from './utils/aes'
import { getCacheDir, getConfigDir, getFilesDir } from './utils/file' import { getCacheDir, getConfigDir, getFilesDir, hasWritePermission, updateAppDataConfig } from './utils/file'
import { compress, decompress } from './utils/zip' import { compress, decompress } from './utils/zip'
import { FeedUrl } from '@shared/config/constant'
const fileManager = new FileStorage() const fileManager = new FileStorage()
const backupManager = new BackupManager() const backupManager = new BackupManager()
const exportService = new ExportService(fileManager) const exportService = new ExportService(fileManager)
const obsidianVaultService = new ObsidianVaultService() const obsidianVaultService = new ObsidianVaultService()
const vertexAIService = VertexAIService.getInstance()
const memoryService = MemoryService.getInstance()
const dxtService = new DxtService()
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const appUpdater = new AppUpdater(mainWindow) const appUpdater = new AppUpdater(mainWindow)
const notificationService = new NotificationService(mainWindow) const notificationService = new NotificationService(mainWindow)
// Initialize Python service with main window
pythonService.setMainWindow(mainWindow)
ipcMain.handle(IpcChannel.App_Info, () => ({ ipcMain.handle(IpcChannel.App_Info, () => ({
version: app.getVersion(), version: app.getVersion(),
isPackaged: app.isPackaged, isPackaged: app.isPackaged,
@@ -55,7 +68,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
resourcesPath: getResourcePath(), resourcesPath: getResourcePath(),
logsPath: log.transports.file.getFile().path, logsPath: log.transports.file.getFile().path,
arch: arch(), arch: arch(),
isPortable: isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env isPortable: isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env,
installPath: path.dirname(app.getPath('exe'))
})) }))
ipcMain.handle(IpcChannel.App_Proxy, async (_, proxy: string) => { ipcMain.handle(IpcChannel.App_Proxy, async (_, proxy: string) => {
@@ -83,13 +97,30 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
configManager.setLanguage(language) configManager.setLanguage(language)
}) })
// launch on boot // spell check
ipcMain.handle(IpcChannel.App_SetLaunchOnBoot, (_, openAtLogin: boolean) => { ipcMain.handle(IpcChannel.App_SetEnableSpellCheck, (_, isEnable: boolean) => {
// Set login item settings for windows and mac // disable spell check for all webviews
// linux is not supported because it requires more file operations const webviews = webContents.getAllWebContents()
if (isWin || isMac) { webviews.forEach((webview) => {
app.setLoginItemSettings({ openAtLogin }) webview.session.setSpellCheckerEnabled(isEnable)
})
})
// spell check languages
ipcMain.handle(IpcChannel.App_SetSpellCheckLanguages, (_, languages: string[]) => {
if (languages.length === 0) {
return
} }
const windows = BrowserWindow.getAllWindows()
windows.forEach((window) => {
window.webContents.session.setSpellCheckerLanguages(languages)
})
configManager.set('spellCheckLanguages', languages)
})
// launch on boot
ipcMain.handle(IpcChannel.App_SetLaunchOnBoot, (_, isLaunchOnBoot: boolean) => {
appService.setAppLaunchOnBoot(isLaunchOnBoot)
}) })
// launch to tray // launch to tray
@@ -113,10 +144,34 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
configManager.setAutoUpdate(isActive) configManager.setAutoUpdate(isActive)
}) })
ipcMain.handle(IpcChannel.App_SetFeedUrl, (_, feedUrl: FeedUrl) => { ipcMain.handle(IpcChannel.App_SetTestPlan, async (_, isActive: boolean) => {
appUpdater.setFeedUrl(feedUrl) log.info('set test plan', isActive)
if (isActive !== configManager.getTestPlan()) {
appUpdater.cancelDownload()
configManager.setTestPlan(isActive)
}
}) })
ipcMain.handle(IpcChannel.App_SetTestChannel, async (_, channel: UpgradeChannel) => {
log.info('set test channel', channel)
if (channel !== configManager.getTestChannel()) {
appUpdater.cancelDownload()
configManager.setTestChannel(channel)
}
})
//only for mac
if (isMac) {
ipcMain.handle(IpcChannel.App_MacIsProcessTrusted, (): boolean => {
return systemPreferences.isTrustedAccessibilityClient(false)
})
//return is only the current state, not the new state
ipcMain.handle(IpcChannel.App_MacRequestProcessTrust, (): boolean => {
return systemPreferences.isTrustedAccessibilityClient(true)
})
}
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => { ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => {
configManager.set(key, value, isNotify) configManager.set(key, value, isNotify)
}) })
@@ -173,6 +228,113 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
} }
}) })
let preventQuitListener: ((event: Electron.Event) => void) | null = null
ipcMain.handle(IpcChannel.App_SetStopQuitApp, (_, stop: boolean = false, reason: string = '') => {
if (stop) {
// Only add listener if not already added
if (!preventQuitListener) {
preventQuitListener = (event: Electron.Event) => {
event.preventDefault()
notificationService.sendNotification({
title: reason,
message: reason
} as Notification)
}
app.on('before-quit', preventQuitListener)
}
} else {
// Remove listener if it exists
if (preventQuitListener) {
app.removeListener('before-quit', preventQuitListener)
preventQuitListener = null
}
}
})
// Select app data path
ipcMain.handle(IpcChannel.App_Select, async (_, options: Electron.OpenDialogOptions) => {
try {
const { canceled, filePaths } = await dialog.showOpenDialog(options)
if (canceled || filePaths.length === 0) {
return null
}
return filePaths[0]
} catch (error: any) {
log.error('Failed to select app data path:', error)
return null
}
})
ipcMain.handle(IpcChannel.App_HasWritePermission, async (_, filePath: string) => {
return hasWritePermission(filePath)
})
// Set app data path
ipcMain.handle(IpcChannel.App_SetAppDataPath, async (_, filePath: string) => {
updateAppDataConfig(filePath)
app.setPath('userData', filePath)
})
ipcMain.handle(IpcChannel.App_GetDataPathFromArgs, () => {
return process.argv
.slice(1)
.find((arg) => arg.startsWith('--new-data-path='))
?.split('--new-data-path=')[1]
})
ipcMain.handle(IpcChannel.App_FlushAppData, () => {
BrowserWindow.getAllWindows().forEach((w) => {
w.webContents.session.flushStorageData()
w.webContents.session.cookies.flushStore()
w.webContents.session.closeAllConnections()
})
session.defaultSession.flushStorageData()
session.defaultSession.cookies.flushStore()
session.defaultSession.closeAllConnections()
})
ipcMain.handle(IpcChannel.App_IsNotEmptyDir, async (_, path: string) => {
return fs.readdirSync(path).length > 0
})
// Copy user data to new location
ipcMain.handle(IpcChannel.App_Copy, async (_, oldPath: string, newPath: string, occupiedDirs: string[] = []) => {
try {
await fs.promises.cp(oldPath, newPath, {
recursive: true,
filter: (src) => {
if (occupiedDirs.some((dir) => src.startsWith(path.resolve(dir)))) {
return false
}
return true
}
})
return { success: true }
} catch (error: any) {
log.error('Failed to copy user data:', error)
return { success: false, error: error.message }
}
})
// Relaunch app
ipcMain.handle(IpcChannel.App_RelaunchApp, (_, options?: Electron.RelaunchOptions) => {
// Fix for .AppImage
if (isLinux && process.env.APPIMAGE) {
log.info('Relaunching app with options:', process.env.APPIMAGE, options)
// On Linux, we need to use the APPIMAGE environment variable to relaunch
// https://github.com/electron-userland/electron-builder/issues/1727#issuecomment-769896927
options = options || {}
options.execPath = process.env.APPIMAGE
options.args = options.args || []
options.args.unshift('--appimage-extract-and-run')
}
app.relaunch(options)
app.exit(0)
})
// check for update // check for update
ipcMain.handle(IpcChannel.App_CheckForUpdate, async () => { ipcMain.handle(IpcChannel.App_CheckForUpdate, async () => {
return await appUpdater.checkForUpdates() return await appUpdater.checkForUpdates()
@@ -207,6 +369,16 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.Backup_CheckConnection, backupManager.checkConnection) ipcMain.handle(IpcChannel.Backup_CheckConnection, backupManager.checkConnection)
ipcMain.handle(IpcChannel.Backup_CreateDirectory, backupManager.createDirectory) ipcMain.handle(IpcChannel.Backup_CreateDirectory, backupManager.createDirectory)
ipcMain.handle(IpcChannel.Backup_DeleteWebdavFile, backupManager.deleteWebdavFile) 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 // file
ipcMain.handle(IpcChannel.File_Open, fileManager.open) ipcMain.handle(IpcChannel.File_Open, fileManager.open)
@@ -217,18 +389,42 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.File_Clear, fileManager.clear) ipcMain.handle(IpcChannel.File_Clear, fileManager.clear)
ipcMain.handle(IpcChannel.File_Read, fileManager.readFile) ipcMain.handle(IpcChannel.File_Read, fileManager.readFile)
ipcMain.handle(IpcChannel.File_Delete, fileManager.deleteFile) ipcMain.handle(IpcChannel.File_Delete, fileManager.deleteFile)
ipcMain.handle('file:deleteDir', fileManager.deleteDir)
ipcMain.handle(IpcChannel.File_Get, fileManager.getFile) ipcMain.handle(IpcChannel.File_Get, fileManager.getFile)
ipcMain.handle(IpcChannel.File_SelectFolder, fileManager.selectFolder) ipcMain.handle(IpcChannel.File_SelectFolder, fileManager.selectFolder)
ipcMain.handle(IpcChannel.File_Create, fileManager.createTempFile) ipcMain.handle(IpcChannel.File_CreateTempFile, fileManager.createTempFile)
ipcMain.handle(IpcChannel.File_Write, fileManager.writeFile) ipcMain.handle(IpcChannel.File_Write, fileManager.writeFile)
ipcMain.handle(IpcChannel.File_WriteWithId, fileManager.writeFileWithId) ipcMain.handle(IpcChannel.File_WriteWithId, fileManager.writeFileWithId)
ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage) ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage)
ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image) ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image)
ipcMain.handle(IpcChannel.File_SaveBase64Image, fileManager.saveBase64Image) ipcMain.handle(IpcChannel.File_SaveBase64Image, fileManager.saveBase64Image)
ipcMain.handle(IpcChannel.File_Base64File, fileManager.base64File) ipcMain.handle(IpcChannel.File_Base64File, fileManager.base64File)
ipcMain.handle(IpcChannel.File_GetPdfInfo, fileManager.pdfPageCount)
ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile) ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile)
ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile) ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile)
ipcMain.handle(IpcChannel.File_BinaryImage, fileManager.binaryImage) 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) => {
const service = FileServiceManager.getInstance().getService(provider)
return await service.uploadFile(file)
})
ipcMain.handle(IpcChannel.FileService_List, async (_, provider: Provider) => {
const service = FileServiceManager.getInstance().getService(provider)
return await service.listFiles()
})
ipcMain.handle(IpcChannel.FileService_Delete, async (_, provider: Provider, fileId: string) => {
const service = FileServiceManager.getInstance().getService(provider)
return await service.deleteFile(fileId)
})
ipcMain.handle(IpcChannel.FileService_Retrieve, async (_, provider: Provider, fileId: string) => {
const service = FileServiceManager.getInstance().getService(provider)
return await service.retrieveFile(fileId)
})
// fs // fs
ipcMain.handle(IpcChannel.Fs_Read, FileService.readFile) ipcMain.handle(IpcChannel.Fs_Read, FileService.readFile)
@@ -259,6 +455,39 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.KnowledgeBase_Remove, KnowledgeService.remove) ipcMain.handle(IpcChannel.KnowledgeBase_Remove, KnowledgeService.remove)
ipcMain.handle(IpcChannel.KnowledgeBase_Search, KnowledgeService.search) ipcMain.handle(IpcChannel.KnowledgeBase_Search, KnowledgeService.search)
ipcMain.handle(IpcChannel.KnowledgeBase_Rerank, KnowledgeService.rerank) ipcMain.handle(IpcChannel.KnowledgeBase_Rerank, KnowledgeService.rerank)
ipcMain.handle(IpcChannel.KnowledgeBase_Check_Quota, KnowledgeService.checkQuota)
// memory
ipcMain.handle(IpcChannel.Memory_Add, async (_, messages, config) => {
return await memoryService.add(messages, config)
})
ipcMain.handle(IpcChannel.Memory_Search, async (_, query, config) => {
return await memoryService.search(query, config)
})
ipcMain.handle(IpcChannel.Memory_List, async (_, config) => {
return await memoryService.list(config)
})
ipcMain.handle(IpcChannel.Memory_Delete, async (_, id) => {
return await memoryService.delete(id)
})
ipcMain.handle(IpcChannel.Memory_Update, async (_, id, memory, metadata) => {
return await memoryService.update(id, memory, metadata)
})
ipcMain.handle(IpcChannel.Memory_Get, async (_, memoryId) => {
return await memoryService.get(memoryId)
})
ipcMain.handle(IpcChannel.Memory_SetConfig, async (_, config) => {
memoryService.setConfig(config)
})
ipcMain.handle(IpcChannel.Memory_DeleteUser, async (_, userId) => {
return await memoryService.deleteUser(userId)
})
ipcMain.handle(IpcChannel.Memory_DeleteAllMemoriesForUser, async (_, userId) => {
return await memoryService.deleteAllMemoriesForUser(userId)
})
ipcMain.handle(IpcChannel.Memory_GetUsersList, async () => {
return await memoryService.getUsersList()
})
// window // window
ipcMain.handle(IpcChannel.Windows_SetMinimumSize, (_, width: number, height: number) => { ipcMain.handle(IpcChannel.Windows_SetMinimumSize, (_, width: number, height: number) => {
@@ -273,6 +502,15 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
} }
}) })
// VertexAI
ipcMain.handle(IpcChannel.VertexAI_GetAuthHeaders, async (_, params) => {
return vertexAIService.getAuthHeaders(params)
})
ipcMain.handle(IpcChannel.VertexAI_ClearAuthCache, async (_, projectId: string, clientEmail?: string) => {
vertexAIService.clearAuthCache(projectId, clientEmail)
})
// mini window // mini window
ipcMain.handle(IpcChannel.MiniWindow_Show, () => windowService.showMiniWindow()) ipcMain.handle(IpcChannel.MiniWindow_Show, () => windowService.showMiniWindow())
ipcMain.handle(IpcChannel.MiniWindow_Hide, () => windowService.hideMiniWindow()) ipcMain.handle(IpcChannel.MiniWindow_Hide, () => windowService.hideMiniWindow())
@@ -300,6 +538,37 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.Mcp_GetResource, mcpService.getResource) ipcMain.handle(IpcChannel.Mcp_GetResource, mcpService.getResource)
ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo) ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo)
ipcMain.handle(IpcChannel.Mcp_CheckConnectivity, mcpService.checkMcpConnectivity) ipcMain.handle(IpcChannel.Mcp_CheckConnectivity, mcpService.checkMcpConnectivity)
ipcMain.handle(IpcChannel.Mcp_AbortTool, mcpService.abortTool)
ipcMain.handle(IpcChannel.Mcp_GetServerVersion, mcpService.getServerVersion)
ipcMain.handle(IpcChannel.Mcp_SetProgress, (_, progress: number) => {
mainWindow.webContents.send('mcp-progress', progress)
})
// DXT upload handler
ipcMain.handle(IpcChannel.Mcp_UploadDxt, async (event, fileBuffer: ArrayBuffer, fileName: string) => {
try {
// Create a temporary file with the uploaded content
const tempPath = await fileManager.createTempFile(event, fileName)
await fileManager.writeFile(event, tempPath, Buffer.from(fileBuffer))
// Process DXT file using the temporary path
return await dxtService.uploadDxt(event, tempPath)
} catch (error) {
log.error('[IPC] DXT upload error:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to upload DXT file'
}
}
})
// Register Python execution handler
ipcMain.handle(
IpcChannel.Python_Execute,
async (_, script: string, context?: Record<string, any>, timeout?: number) => {
return await pythonService.executeScript(script, context, timeout)
}
)
ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name)) ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name)) ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name))
@@ -346,6 +615,12 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
setOpenLinkExternal(webviewId, isExternal) setOpenLinkExternal(webviewId, isExternal)
) )
ipcMain.handle(IpcChannel.Webview_SetSpellCheckEnabled, (_, webviewId: number, isEnable: boolean) => {
const webview = webContents.fromId(webviewId)
if (!webview) return
webview.session.setSpellCheckerEnabled(isEnable)
})
// store sync // store sync
storeSyncService.registerIpcHandler() storeSyncService.registerIpcHandler()
@@ -353,4 +628,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
SelectionService.registerIpcHandler() SelectionService.registerIpcHandler()
ipcMain.handle(IpcChannel.App_QuoteToMain, (_, text: string) => windowService.quoteToMainWindow(text)) ipcMain.handle(IpcChannel.App_QuoteToMain, (_, text: string) => windowService.quoteToMainWindow(text))
ipcMain.handle(IpcChannel.App_SetDisableHardwareAcceleration, (_, isDisable: boolean) => {
configManager.setDisableHardwareAcceleration(isDisable)
})
} }

View File

@@ -0,0 +1,122 @@
import fs from 'node:fs'
import path from 'node:path'
import { windowService } from '@main/services/WindowService'
import { getFileExt } from '@main/utils/file'
import { FileMetadata, OcrProvider } from '@types'
import { app } from 'electron'
import { TypedArray } from 'pdfjs-dist/types/src/display/api'
export default abstract class BaseOcrProvider {
protected provider: OcrProvider
public storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
constructor(provider: OcrProvider) {
if (!provider) {
throw new Error('OCR provider is not set')
}
this.provider = provider
}
abstract parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata; quota?: number }>
/**
* 检查文件是否已经被预处理过
* 统一检测方法:如果 Data/Files/{file.id} 是目录,说明已被预处理
* @param file 文件信息
* @returns 如果已处理返回处理后的文件信息否则返回null
*/
public async checkIfAlreadyProcessed(file: FileMetadata): Promise<FileMetadata | null> {
try {
// 检查 Data/Files/{file.id} 是否是目录
const preprocessDirPath = path.join(this.storageDir, file.id)
if (fs.existsSync(preprocessDirPath)) {
const stats = await fs.promises.stat(preprocessDirPath)
// 如果是目录,说明已经被预处理过
if (stats.isDirectory()) {
// 查找目录中的处理结果文件
const files = await fs.promises.readdir(preprocessDirPath)
// 查找主要的处理结果文件(.md 或 .txt
const processedFile = files.find((fileName) => fileName.endsWith('.md') || fileName.endsWith('.txt'))
if (processedFile) {
const processedFilePath = path.join(preprocessDirPath, processedFile)
const processedStats = await fs.promises.stat(processedFilePath)
const ext = getFileExt(processedFile)
return {
...file,
name: file.name.replace(file.ext, ext),
path: processedFilePath,
ext: ext,
size: processedStats.size,
created_at: processedStats.birthtime.toISOString()
}
}
}
}
return null
} catch (error) {
// 如果检查过程中出现错误返回null表示未处理
return null
}
}
/**
* 辅助方法:延迟执行
*/
public delay = (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms))
}
public async readPdf(
source: string | URL | TypedArray,
passwordCallback?: (fn: (password: string) => void, reason: string) => string
) {
const { getDocument } = await import('pdfjs-dist/legacy/build/pdf.mjs')
const documentLoadingTask = getDocument(source)
if (passwordCallback) {
documentLoadingTask.onPassword = passwordCallback
}
const document = await documentLoadingTask.promise
return document
}
public async sendOcrProgress(sourceId: string, progress: number): Promise<void> {
const mainWindow = windowService.getMainWindow()
mainWindow?.webContents.send('file-ocr-progress', {
itemId: sourceId,
progress: progress
})
}
/**
* 将文件移动到附件目录
* @param fileId 文件id
* @param filePaths 需要移动的文件路径数组
* @returns 移动后的文件路径数组
*/
public moveToAttachmentsDir(fileId: string, filePaths: string[]): string[] {
const attachmentsPath = path.join(this.storageDir, fileId)
if (!fs.existsSync(attachmentsPath)) {
fs.mkdirSync(attachmentsPath, { recursive: true })
}
const movedPaths: string[] = []
for (const filePath of filePaths) {
if (fs.existsSync(filePath)) {
const fileName = path.basename(filePath)
const destPath = path.join(attachmentsPath, fileName)
fs.copyFileSync(filePath, destPath)
fs.unlinkSync(filePath) // 删除原文件,实现"移动"
movedPaths.push(destPath)
}
}
return movedPaths
}
}

View File

@@ -0,0 +1,12 @@
import { FileMetadata, OcrProvider } from '@types'
import BaseOcrProvider from './BaseOcrProvider'
export default class DefaultOcrProvider extends BaseOcrProvider {
constructor(provider: OcrProvider) {
super(provider)
}
public parseFile(): Promise<{ processedFile: FileMetadata }> {
throw new Error('Method not implemented.')
}
}

View File

@@ -0,0 +1,128 @@
import { isMac } from '@main/constant'
import { FileMetadata, OcrProvider } from '@types'
import Logger from 'electron-log'
import * as fs from 'fs'
import * as path from 'path'
import { TextItem } from 'pdfjs-dist/types/src/display/api'
import BaseOcrProvider from './BaseOcrProvider'
export default class MacSysOcrProvider extends BaseOcrProvider {
private readonly MIN_TEXT_LENGTH = 1000
private MacOCR: any
private async initMacOCR() {
if (!isMac) {
throw new Error('MacSysOcrProvider is only available on macOS')
}
if (!this.MacOCR) {
try {
// @ts-ignore This module is optional and only installed/available on macOS. Runtime checks prevent execution on other platforms.
const module = await import('@cherrystudio/mac-system-ocr')
this.MacOCR = module.default
} catch (error) {
Logger.error('[OCR] Failed to load mac-system-ocr:', error)
throw error
}
}
return this.MacOCR
}
private getRecognitionLevel(level?: number) {
return level === 0 ? this.MacOCR.RECOGNITION_LEVEL_FAST : this.MacOCR.RECOGNITION_LEVEL_ACCURATE
}
constructor(provider: OcrProvider) {
super(provider)
}
private async processPages(
results: any,
totalPages: number,
sourceId: string,
writeStream: fs.WriteStream
): Promise<void> {
await this.initMacOCR()
// TODO: 下个版本后面使用批处理以及p-queue来优化
for (let i = 0; i < totalPages; i++) {
// Convert pages to buffers
const pageNum = i + 1
const pageBuffer = await results.getPage(pageNum)
// Process batch
const ocrResult = await this.MacOCR.recognizeFromBuffer(pageBuffer, {
ocrOptions: {
recognitionLevel: this.getRecognitionLevel(this.provider.options?.recognitionLevel),
minConfidence: this.provider.options?.minConfidence || 0.5
}
})
// Write results in order
writeStream.write(ocrResult.text + '\n')
// Update progress
await this.sendOcrProgress(sourceId, (pageNum / totalPages) * 100)
}
}
public async isScanPdf(buffer: Buffer): Promise<boolean> {
const doc = await this.readPdf(new Uint8Array(buffer))
const pageLength = doc.numPages
let counts = 0
const pagesToCheck = Math.min(pageLength, 10)
for (let i = 0; i < pagesToCheck; i++) {
const page = await doc.getPage(i + 1)
const pageData = await page.getTextContent()
const pageText = pageData.items.map((item) => (item as TextItem).str).join('')
counts += pageText.length
if (counts >= this.MIN_TEXT_LENGTH) {
return false
}
}
return true
}
public async parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata }> {
Logger.info(`[OCR] Starting OCR process for file: ${file.name}`)
if (file.ext === '.pdf') {
try {
const { pdf } = await import('@cherrystudio/pdf-to-img-napi')
const pdfBuffer = await fs.promises.readFile(file.path)
const results = await pdf(pdfBuffer, {
scale: 2
})
const totalPages = results.length
const baseDir = path.dirname(file.path)
const baseName = path.basename(file.path, path.extname(file.path))
const txtFileName = `${baseName}.txt`
const txtFilePath = path.join(baseDir, txtFileName)
const writeStream = fs.createWriteStream(txtFilePath)
await this.processPages(results, totalPages, sourceId, writeStream)
await new Promise<void>((resolve, reject) => {
writeStream.end(() => {
Logger.info(`[OCR] OCR process completed successfully for ${file.origin_name}`)
resolve()
})
writeStream.on('error', reject)
})
const movedPaths = this.moveToAttachmentsDir(file.id, [txtFilePath])
return {
processedFile: {
...file,
name: txtFileName,
path: movedPaths[0],
ext: '.txt',
size: fs.statSync(movedPaths[0]).size
}
}
} catch (error) {
Logger.error('[OCR] Error during OCR process:', error)
throw error
}
}
return { processedFile: file }
}
}

View File

@@ -0,0 +1,26 @@
import { FileMetadata, OcrProvider as Provider } from '@types'
import BaseOcrProvider from './BaseOcrProvider'
import OcrProviderFactory from './OcrProviderFactory'
export default class OcrProvider {
private sdk: BaseOcrProvider
constructor(provider: Provider) {
this.sdk = OcrProviderFactory.create(provider)
}
public async parseFile(
sourceId: string,
file: FileMetadata
): Promise<{ processedFile: FileMetadata; quota?: number }> {
return this.sdk.parseFile(sourceId, file)
}
/**
* 检查文件是否已经被预处理过
* @param file 文件信息
* @returns 如果已处理返回处理后的文件信息否则返回null
*/
public async checkIfAlreadyProcessed(file: FileMetadata): Promise<FileMetadata | null> {
return this.sdk.checkIfAlreadyProcessed(file)
}
}

View File

@@ -0,0 +1,20 @@
import { isMac } from '@main/constant'
import { OcrProvider } from '@types'
import Logger from 'electron-log'
import BaseOcrProvider from './BaseOcrProvider'
import DefaultOcrProvider from './DefaultOcrProvider'
import MacSysOcrProvider from './MacSysOcrProvider'
export default class OcrProviderFactory {
static create(provider: OcrProvider): BaseOcrProvider {
switch (provider.id) {
case 'system':
if (!isMac) {
Logger.warn('[OCR] System OCR provider is only available on macOS')
}
return new MacSysOcrProvider(provider)
default:
return new DefaultOcrProvider(provider)
}
}
}

View File

@@ -0,0 +1,126 @@
import fs from 'node:fs'
import path from 'node:path'
import { windowService } from '@main/services/WindowService'
import { getFileExt } from '@main/utils/file'
import { FileMetadata, PreprocessProvider } from '@types'
import { app } from 'electron'
import { TypedArray } from 'pdfjs-dist/types/src/display/api'
export default abstract class BasePreprocessProvider {
protected provider: PreprocessProvider
protected userId?: string
public storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
constructor(provider: PreprocessProvider, userId?: string) {
if (!provider) {
throw new Error('Preprocess provider is not set')
}
this.provider = provider
this.userId = userId
}
abstract parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata; quota?: number }>
abstract checkQuota(): Promise<number>
/**
* 检查文件是否已经被预处理过
* 统一检测方法:如果 Data/Files/{file.id} 是目录,说明已被预处理
* @param file 文件信息
* @returns 如果已处理返回处理后的文件信息否则返回null
*/
public async checkIfAlreadyProcessed(file: FileMetadata): Promise<FileMetadata | null> {
try {
// 检查 Data/Files/{file.id} 是否是目录
const preprocessDirPath = path.join(this.storageDir, file.id)
if (fs.existsSync(preprocessDirPath)) {
const stats = await fs.promises.stat(preprocessDirPath)
// 如果是目录,说明已经被预处理过
if (stats.isDirectory()) {
// 查找目录中的处理结果文件
const files = await fs.promises.readdir(preprocessDirPath)
// 查找主要的处理结果文件(.md 或 .txt
const processedFile = files.find((fileName) => fileName.endsWith('.md') || fileName.endsWith('.txt'))
if (processedFile) {
const processedFilePath = path.join(preprocessDirPath, processedFile)
const processedStats = await fs.promises.stat(processedFilePath)
const ext = getFileExt(processedFile)
return {
...file,
name: file.name.replace(file.ext, ext),
path: processedFilePath,
ext: ext,
size: processedStats.size,
created_at: processedStats.birthtime.toISOString()
}
}
}
}
return null
} catch (error) {
// 如果检查过程中出现错误返回null表示未处理
return null
}
}
/**
* 辅助方法:延迟执行
*/
public delay = (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms))
}
public async readPdf(
source: string | URL | TypedArray,
passwordCallback?: (fn: (password: string) => void, reason: string) => string
) {
const { getDocument } = await import('pdfjs-dist/legacy/build/pdf.mjs')
const documentLoadingTask = getDocument(source)
if (passwordCallback) {
documentLoadingTask.onPassword = passwordCallback
}
const document = await documentLoadingTask.promise
return document
}
public async sendPreprocessProgress(sourceId: string, progress: number): Promise<void> {
const mainWindow = windowService.getMainWindow()
mainWindow?.webContents.send('file-preprocess-progress', {
itemId: sourceId,
progress: progress
})
}
/**
* 将文件移动到附件目录
* @param fileId 文件id
* @param filePaths 需要移动的文件路径数组
* @returns 移动后的文件路径数组
*/
public moveToAttachmentsDir(fileId: string, filePaths: string[]): string[] {
const attachmentsPath = path.join(this.storageDir, fileId)
if (!fs.existsSync(attachmentsPath)) {
fs.mkdirSync(attachmentsPath, { recursive: true })
}
const movedPaths: string[] = []
for (const filePath of filePaths) {
if (fs.existsSync(filePath)) {
const fileName = path.basename(filePath)
const destPath = path.join(attachmentsPath, fileName)
fs.copyFileSync(filePath, destPath)
fs.unlinkSync(filePath) // 删除原文件,实现"移动"
movedPaths.push(destPath)
}
}
return movedPaths
}
}

View File

@@ -0,0 +1,16 @@
import { FileMetadata, PreprocessProvider } from '@types'
import BasePreprocessProvider from './BasePreprocessProvider'
export default class DefaultPreprocessProvider extends BasePreprocessProvider {
constructor(provider: PreprocessProvider) {
super(provider)
}
public parseFile(): Promise<{ processedFile: FileMetadata }> {
throw new Error('Method not implemented.')
}
public checkQuota(): Promise<number> {
throw new Error('Method not implemented.')
}
}

View File

@@ -0,0 +1,329 @@
import fs from 'node:fs'
import path from 'node:path'
import { FileMetadata, PreprocessProvider } from '@types'
import AdmZip from 'adm-zip'
import axios, { AxiosRequestConfig } from 'axios'
import Logger from 'electron-log'
import BasePreprocessProvider from './BasePreprocessProvider'
type ApiResponse<T> = {
code: string
data: T
message?: string
}
type PreuploadResponse = {
uid: string
url: string
}
type StatusResponse = {
status: string
progress: number
}
type ParsedFileResponse = {
status: string
url: string
}
export default class Doc2xPreprocessProvider extends BasePreprocessProvider {
constructor(provider: PreprocessProvider) {
super(provider)
}
private async validateFile(filePath: string): Promise<void> {
const pdfBuffer = await fs.promises.readFile(filePath)
const doc = await this.readPdf(new Uint8Array(pdfBuffer))
// 文件页数小于1000页
if (doc.numPages >= 1000) {
throw new Error(`PDF page count (${doc.numPages}) exceeds the limit of 1000 pages`)
}
// 文件大小小于300MB
if (pdfBuffer.length >= 300 * 1024 * 1024) {
const fileSizeMB = Math.round(pdfBuffer.length / (1024 * 1024))
throw new Error(`PDF file size (${fileSizeMB}MB) exceeds the limit of 300MB`)
}
}
public async parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata }> {
try {
Logger.info(`Preprocess processing started: ${file.path}`)
// 步骤1: 准备上传
const { uid, url } = await this.preupload()
Logger.info(`Preprocess preupload completed: uid=${uid}`)
await this.validateFile(file.path)
// 步骤2: 上传文件
await this.putFile(file.path, url)
// 步骤3: 等待处理完成
await this.waitForProcessing(sourceId, uid)
Logger.info(`Preprocess parsing completed successfully for: ${file.path}`)
// 步骤4: 导出文件
const { path: outputPath } = await this.exportFile(file, uid)
// 步骤5: 创建处理后的文件信息
return {
processedFile: this.createProcessedFileInfo(file, outputPath)
}
} catch (error) {
Logger.error(
`Preprocess processing failed for ${file.path}: ${error instanceof Error ? error.message : String(error)}`
)
throw error
}
}
private createProcessedFileInfo(file: FileMetadata, outputPath: string): FileMetadata {
const outputFilePath = `${outputPath}/${file.name.split('.').slice(0, -1).join('.')}.md`
return {
...file,
name: file.name.replace('.pdf', '.md'),
path: outputFilePath,
ext: '.md',
size: fs.statSync(outputFilePath).size
}
}
/**
* 导出文件
* @param file 文件信息
* @param uid 预上传响应的uid
* @returns 导出文件的路径
*/
public async exportFile(file: FileMetadata, uid: string): Promise<{ path: string }> {
Logger.info(`Exporting file: ${file.path}`)
// 步骤1: 转换文件
await this.convertFile(uid, file.path)
Logger.info(`File conversion completed for: ${file.path}`)
// 步骤2: 等待导出并获取URL
const exportUrl = await this.waitForExport(uid)
// 步骤3: 下载并解压文件
return this.downloadFile(exportUrl, file)
}
/**
* 等待处理完成
* @param sourceId 源文件ID
* @param uid 预上传响应的uid
*/
private async waitForProcessing(sourceId: string, uid: string): Promise<void> {
while (true) {
await this.delay(1000)
const { status, progress } = await this.getStatus(uid)
await this.sendPreprocessProgress(sourceId, progress)
Logger.info(`Preprocess processing status: ${status}, progress: ${progress}%`)
if (status === 'success') {
return
} else if (status === 'failed') {
throw new Error('Preprocess processing failed')
}
}
}
/**
* 等待导出完成
* @param uid 预上传响应的uid
* @returns 导出文件的url
*/
private async waitForExport(uid: string): Promise<string> {
while (true) {
await this.delay(1000)
const { status, url } = await this.getParsedFile(uid)
Logger.info(`Export status: ${status}`)
if (status === 'success' && url) {
return url
} else if (status === 'failed') {
throw new Error('Export failed')
}
}
}
/**
* 预上传文件
* @returns 预上传响应的url和uid
*/
private async preupload(): Promise<PreuploadResponse> {
const config = this.createAuthConfig()
const endpoint = `${this.provider.apiHost}/api/v2/parse/preupload`
try {
const { data } = await axios.post<ApiResponse<PreuploadResponse>>(endpoint, null, config)
if (data.code === 'success' && data.data) {
return data.data
} else {
throw new Error(`API returned error: ${data.message || JSON.stringify(data)}`)
}
} catch (error) {
Logger.error(`Failed to get preupload URL: ${error instanceof Error ? error.message : String(error)}`)
throw new Error('Failed to get preupload URL')
}
}
/**
* 上传文件
* @param filePath 文件路径
* @param url 预上传响应的url
*/
private async putFile(filePath: string, url: string): Promise<void> {
try {
const fileStream = fs.createReadStream(filePath)
const response = await axios.put(url, fileStream)
if (response.status !== 200) {
throw new Error(`HTTP status ${response.status}: ${response.statusText}`)
}
} catch (error) {
Logger.error(`Failed to upload file ${filePath}: ${error instanceof Error ? error.message : String(error)}`)
throw new Error('Failed to upload file')
}
}
private async getStatus(uid: string): Promise<StatusResponse> {
const config = this.createAuthConfig()
const endpoint = `${this.provider.apiHost}/api/v2/parse/status?uid=${uid}`
try {
const response = await axios.get<ApiResponse<StatusResponse>>(endpoint, config)
if (response.data.code === 'success' && response.data.data) {
return response.data.data
} else {
throw new Error(`API returned error: ${response.data.message || JSON.stringify(response.data)}`)
}
} catch (error) {
Logger.error(`Failed to get status for uid ${uid}: ${error instanceof Error ? error.message : String(error)}`)
throw new Error('Failed to get processing status')
}
}
/**
* Preprocess文件
* @param uid 预上传响应的uid
* @param filePath 文件路径
*/
private async convertFile(uid: string, filePath: string): Promise<void> {
const fileName = path.parse(filePath).name
const config = {
...this.createAuthConfig(),
headers: {
...this.createAuthConfig().headers,
'Content-Type': 'application/json'
}
}
const payload = {
uid,
to: 'md',
formula_mode: 'normal',
filename: fileName
}
const endpoint = `${this.provider.apiHost}/api/v2/convert/parse`
try {
const response = await axios.post<ApiResponse<any>>(endpoint, payload, config)
if (response.data.code !== 'success') {
throw new Error(`API returned error: ${response.data.message || JSON.stringify(response.data)}`)
}
} catch (error) {
Logger.error(`Failed to convert file ${filePath}: ${error instanceof Error ? error.message : String(error)}`)
throw new Error('Failed to convert file')
}
}
/**
* 获取解析后的文件信息
* @param uid 预上传响应的uid
* @returns 解析后的文件信息
*/
private async getParsedFile(uid: string): Promise<ParsedFileResponse> {
const config = this.createAuthConfig()
const endpoint = `${this.provider.apiHost}/api/v2/convert/parse/result?uid=${uid}`
try {
const response = await axios.get<ApiResponse<ParsedFileResponse>>(endpoint, config)
if (response.status === 200 && response.data.data) {
return response.data.data
} else {
throw new Error(`HTTP status ${response.status}: ${response.statusText}`)
}
} catch (error) {
Logger.error(
`Failed to get parsed file for uid ${uid}: ${error instanceof Error ? error.message : String(error)}`
)
throw new Error('Failed to get parsed file information')
}
}
/**
* 下载文件
* @param url 导出文件的url
* @param file 文件信息
* @returns 下载文件的路径
*/
private async downloadFile(url: string, file: FileMetadata): Promise<{ path: string }> {
const dirPath = this.storageDir
// 使用统一的存储路径Data/Files/{file.id}/
const extractPath = path.join(dirPath, file.id)
const zipPath = path.join(dirPath, `${file.id}.zip`)
// 确保目录存在
fs.mkdirSync(dirPath, { recursive: true })
fs.mkdirSync(extractPath, { recursive: true })
Logger.info(`Downloading to export path: ${zipPath}`)
try {
// 下载文件
const response = await axios.get(url, { responseType: 'arraybuffer' })
fs.writeFileSync(zipPath, response.data)
// 确保提取目录存在
if (!fs.existsSync(extractPath)) {
fs.mkdirSync(extractPath, { recursive: true })
}
// 解压文件
const zip = new AdmZip(zipPath)
zip.extractAllTo(extractPath, true)
Logger.info(`Extracted files to: ${extractPath}`)
// 删除临时ZIP文件
fs.unlinkSync(zipPath)
return { path: extractPath }
} catch (error) {
Logger.error(`Failed to download and extract file: ${error instanceof Error ? error.message : String(error)}`)
throw new Error('Failed to download and extract file')
}
}
private createAuthConfig(): AxiosRequestConfig {
return {
headers: {
Authorization: `Bearer ${this.provider.apiKey}`
}
}
}
public checkQuota(): Promise<number> {
throw new Error('Method not implemented.')
}
}

View File

@@ -0,0 +1,394 @@
import fs from 'node:fs'
import path from 'node:path'
import { FileMetadata, PreprocessProvider } from '@types'
import AdmZip from 'adm-zip'
import axios from 'axios'
import Logger from 'electron-log'
import BasePreprocessProvider from './BasePreprocessProvider'
type ApiResponse<T> = {
code: number
data: T
msg?: string
trace_id?: string
}
type BatchUploadResponse = {
batch_id: string
file_urls: string[]
}
type ExtractProgress = {
extracted_pages: number
total_pages: number
start_time: string
}
type ExtractFileResult = {
file_name: string
state: 'done' | 'waiting-file' | 'pending' | 'running' | 'converting' | 'failed'
err_msg: string
full_zip_url?: string
extract_progress?: ExtractProgress
}
type ExtractResultResponse = {
batch_id: string
extract_result: ExtractFileResult[]
}
type QuotaResponse = {
code: number
data: {
user_left_quota: number
total_left_quota: number
}
msg?: string
trace_id?: string
}
export default class MineruPreprocessProvider extends BasePreprocessProvider {
constructor(provider: PreprocessProvider, userId?: string) {
super(provider, userId)
// todo免费期结束后删除
this.provider.apiKey = this.provider.apiKey || import.meta.env.MAIN_VITE_MINERU_API_KEY
}
public async parseFile(
sourceId: string,
file: FileMetadata
): Promise<{ processedFile: FileMetadata; quota: number }> {
try {
Logger.info(`MinerU preprocess processing started: ${file.path}`)
await this.validateFile(file.path)
// 1. 获取上传URL并上传文件
const batchId = await this.uploadFile(file)
Logger.info(`MinerU file upload completed: batch_id=${batchId}`)
// 2. 等待处理完成并获取结果
const extractResult = await this.waitForCompletion(sourceId, batchId, file.origin_name)
Logger.info(`MinerU processing completed for batch: ${batchId}`)
// 3. 下载并解压文件
const { path: outputPath } = await this.downloadAndExtractFile(extractResult.full_zip_url!, file)
// 4. check quota
const quota = await this.checkQuota()
// 5. 创建处理后的文件信息
return {
processedFile: this.createProcessedFileInfo(file, outputPath),
quota
}
} catch (error: any) {
Logger.error(`MinerU preprocess processing failed for ${file.path}: ${error.message}`)
throw new Error(error.message)
}
}
public async checkQuota() {
try {
const quota = await fetch(`${this.provider.apiHost}/api/v4/quota`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.provider.apiKey}`,
token: this.userId ?? ''
}
})
if (!quota.ok) {
throw new Error(`HTTP ${quota.status}: ${quota.statusText}`)
}
const response: QuotaResponse = await quota.json()
return response.data.user_left_quota
} catch (error) {
console.error('Error checking quota:', error)
throw error
}
}
private async validateFile(filePath: string): Promise<void> {
const pdfBuffer = await fs.promises.readFile(filePath)
const doc = await this.readPdf(new Uint8Array(pdfBuffer))
// 文件页数小于600页
if (doc.numPages >= 600) {
throw new Error(`PDF page count (${doc.numPages}) exceeds the limit of 600 pages`)
}
// 文件大小小于200MB
if (pdfBuffer.length >= 200 * 1024 * 1024) {
const fileSizeMB = Math.round(pdfBuffer.length / (1024 * 1024))
throw new Error(`PDF file size (${fileSizeMB}MB) exceeds the limit of 200MB`)
}
}
private createProcessedFileInfo(file: FileMetadata, outputPath: string): FileMetadata {
// 查找解压后的主要文件
let finalPath = ''
let finalName = file.origin_name.replace('.pdf', '.md')
try {
const files = fs.readdirSync(outputPath)
const mdFile = files.find((f) => f.endsWith('.md'))
if (mdFile) {
const originalMdPath = path.join(outputPath, mdFile)
const newMdPath = path.join(outputPath, finalName)
// 重命名文件为原始文件名
try {
fs.renameSync(originalMdPath, newMdPath)
finalPath = newMdPath
Logger.info(`Renamed markdown file from ${mdFile} to ${finalName}`)
} catch (renameError) {
Logger.warn(`Failed to rename file ${mdFile} to ${finalName}: ${renameError}`)
// 如果重命名失败,使用原文件
finalPath = originalMdPath
finalName = mdFile
}
}
} catch (error) {
Logger.warn(`Failed to read output directory ${outputPath}: ${error}`)
finalPath = path.join(outputPath, `${file.id}.md`)
}
return {
...file,
name: finalName,
path: finalPath,
ext: '.md',
size: fs.existsSync(finalPath) ? fs.statSync(finalPath).size : 0
}
}
private async downloadAndExtractFile(zipUrl: string, file: FileMetadata): Promise<{ path: string }> {
const dirPath = this.storageDir
const zipPath = path.join(dirPath, `${file.id}.zip`)
const extractPath = path.join(dirPath, `${file.id}`)
Logger.info(`Downloading MinerU result to: ${zipPath}`)
try {
// 下载ZIP文件
const response = await axios.get(zipUrl, { responseType: 'arraybuffer' })
fs.writeFileSync(zipPath, response.data)
Logger.info(`Downloaded ZIP file: ${zipPath}`)
// 确保提取目录存在
if (!fs.existsSync(extractPath)) {
fs.mkdirSync(extractPath, { recursive: true })
}
// 解压文件
const zip = new AdmZip(zipPath)
zip.extractAllTo(extractPath, true)
Logger.info(`Extracted files to: ${extractPath}`)
// 删除临时ZIP文件
fs.unlinkSync(zipPath)
return { path: extractPath }
} catch (error: any) {
Logger.error(`Failed to download and extract file: ${error.message}`)
throw new Error(error.message)
}
}
private async uploadFile(file: FileMetadata): Promise<string> {
try {
// 步骤1: 获取上传URL
const { batchId, fileUrls } = await this.getBatchUploadUrls(file)
Logger.info(`Got upload URLs for batch: ${batchId}`)
console.log('batchId:', batchId, 'fileurls:', fileUrls)
// 步骤2: 上传文件到获取的URL
await this.putFileToUrl(file.path, fileUrls[0])
Logger.info(`File uploaded successfully: ${file.path}`)
return batchId
} catch (error: any) {
Logger.error(`Failed to upload file ${file.path}: ${error.message}`)
throw new Error(error.message)
}
}
private async getBatchUploadUrls(file: FileMetadata): Promise<{ batchId: string; fileUrls: string[] }> {
const endpoint = `${this.provider.apiHost}/api/v4/file-urls/batch`
const payload = {
language: 'auto',
enable_formula: true,
enable_table: true,
files: [
{
name: file.origin_name,
is_ocr: true,
data_id: file.id
}
]
}
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.provider.apiKey}`,
token: this.userId ?? '',
Accept: '*/*'
},
body: JSON.stringify(payload)
})
if (response.ok) {
const data: ApiResponse<BatchUploadResponse> = await response.json()
if (data.code === 0 && data.data) {
const { batch_id, file_urls } = data.data
return {
batchId: batch_id,
fileUrls: file_urls
}
} else {
throw new Error(`API returned error: ${data.msg || JSON.stringify(data)}`)
}
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
} catch (error: any) {
Logger.error(`Failed to get batch upload URLs: ${error.message}`)
throw new Error(error.message)
}
}
private async putFileToUrl(filePath: string, uploadUrl: string): Promise<void> {
try {
const fileBuffer = await fs.promises.readFile(filePath)
const response = await fetch(uploadUrl, {
method: 'PUT',
body: fileBuffer,
headers: {
'Content-Type': 'application/pdf'
}
// headers: {
// 'Content-Length': fileBuffer.length.toString()
// }
})
if (!response.ok) {
// 克隆 response 以避免消费 body stream
const responseClone = response.clone()
try {
const responseBody = await responseClone.text()
const errorInfo = {
status: response.status,
statusText: response.statusText,
url: response.url,
type: response.type,
redirected: response.redirected,
headers: Object.fromEntries(response.headers.entries()),
body: responseBody
}
console.error('Response details:', errorInfo)
throw new Error(`Upload failed with status ${response.status}: ${responseBody}`)
} catch (parseError) {
throw new Error(`Upload failed with status ${response.status}. Could not parse response body.`)
}
}
Logger.info(`File uploaded successfully to: ${uploadUrl}`)
} catch (error: any) {
Logger.error(`Failed to upload file to URL ${uploadUrl}: ${error}`)
throw new Error(error.message)
}
}
private async getExtractResults(batchId: string): Promise<ExtractResultResponse> {
const endpoint = `${this.provider.apiHost}/api/v4/extract-results/batch/${batchId}`
try {
const response = await fetch(endpoint, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.provider.apiKey}`,
token: this.userId ?? ''
}
})
if (response.ok) {
const data: ApiResponse<ExtractResultResponse> = await response.json()
if (data.code === 0 && data.data) {
return data.data
} else {
throw new Error(`API returned error: ${data.msg || JSON.stringify(data)}`)
}
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
} catch (error: any) {
Logger.error(`Failed to get extract results for batch ${batchId}: ${error.message}`)
throw new Error(error.message)
}
}
private async waitForCompletion(
sourceId: string,
batchId: string,
fileName: string,
maxRetries: number = 60,
intervalMs: number = 5000
): Promise<ExtractFileResult> {
let retries = 0
while (retries < maxRetries) {
try {
const result = await this.getExtractResults(batchId)
// 查找对应文件的处理结果
const fileResult = result.extract_result.find((item) => item.file_name === fileName)
if (!fileResult) {
throw new Error(`File ${fileName} not found in batch results`)
}
// 检查处理状态
if (fileResult.state === 'done' && fileResult.full_zip_url) {
Logger.info(`Processing completed for file: ${fileName}`)
return fileResult
} else if (fileResult.state === 'failed') {
throw new Error(`Processing failed for file: ${fileName}, error: ${fileResult.err_msg}`)
} else if (fileResult.state === 'running') {
// 发送进度更新
if (fileResult.extract_progress) {
const progress = Math.round(
(fileResult.extract_progress.extracted_pages / fileResult.extract_progress.total_pages) * 100
)
await this.sendPreprocessProgress(sourceId, progress)
Logger.info(`File ${fileName} processing progress: ${progress}%`)
} else {
// 如果没有具体进度信息,发送一个通用进度
await this.sendPreprocessProgress(sourceId, 50)
Logger.info(`File ${fileName} is still processing...`)
}
}
} catch (error) {
Logger.warn(`Failed to check status for batch ${batchId}, retry ${retries + 1}/${maxRetries}`)
if (retries === maxRetries - 1) {
throw error
}
}
retries++
await new Promise((resolve) => setTimeout(resolve, intervalMs))
}
throw new Error(`Processing timeout for batch: ${batchId}`)
}
}

View File

@@ -0,0 +1,187 @@
import fs from 'node:fs'
import { MistralClientManager } from '@main/services/MistralClientManager'
import { MistralService } from '@main/services/remotefile/MistralService'
import { Mistral } from '@mistralai/mistralai'
import { DocumentURLChunk } from '@mistralai/mistralai/models/components/documenturlchunk'
import { ImageURLChunk } from '@mistralai/mistralai/models/components/imageurlchunk'
import { OCRResponse } from '@mistralai/mistralai/models/components/ocrresponse'
import { FileMetadata, FileTypes, PreprocessProvider, Provider } from '@types'
import Logger from 'electron-log'
import path from 'path'
import BasePreprocessProvider from './BasePreprocessProvider'
type PreuploadResponse = DocumentURLChunk | ImageURLChunk
export default class MistralPreprocessProvider extends BasePreprocessProvider {
private sdk: Mistral
private fileService: MistralService
constructor(provider: PreprocessProvider) {
super(provider)
const clientManager = MistralClientManager.getInstance()
const aiProvider: Provider = {
id: provider.id,
type: 'mistral',
name: provider.name,
apiKey: provider.apiKey!,
apiHost: provider.apiHost!,
models: []
}
clientManager.initializeClient(aiProvider)
this.sdk = clientManager.getClient()
this.fileService = new MistralService(aiProvider)
}
private async preupload(file: FileMetadata): Promise<PreuploadResponse> {
let document: PreuploadResponse
Logger.info(`preprocess preupload started for local file: ${file.path}`)
if (file.ext.toLowerCase() === '.pdf') {
const uploadResponse = await this.fileService.uploadFile(file)
if (uploadResponse.status === 'failed') {
Logger.error('File upload failed:', uploadResponse)
throw new Error('Failed to upload file: ' + uploadResponse.displayName)
}
await this.sendPreprocessProgress(file.id, 15)
const fileUrl = await this.sdk.files.getSignedUrl({
fileId: uploadResponse.fileId
})
Logger.info('Got signed URL:', fileUrl)
await this.sendPreprocessProgress(file.id, 20)
document = {
type: 'document_url',
documentUrl: fileUrl.url
}
} else {
const base64Image = Buffer.from(fs.readFileSync(file.path)).toString('base64')
document = {
type: 'image_url',
imageUrl: `data:image/png;base64,${base64Image}`
}
}
if (!document) {
throw new Error('Unsupported file type')
}
return document
}
public async parseFile(sourceId: string, file: FileMetadata): Promise<{ processedFile: FileMetadata }> {
try {
const document = await this.preupload(file)
const result = await this.sdk.ocr.process({
model: this.provider.model!,
document: document,
includeImageBase64: true
})
if (result) {
await this.sendPreprocessProgress(sourceId, 100)
const processedFile = this.convertFile(result, file)
return {
processedFile
}
} else {
throw new Error('preprocess processing failed: OCR response is empty')
}
} catch (error) {
throw new Error('preprocess processing failed: ' + error)
}
}
private convertFile(result: OCRResponse, file: FileMetadata): FileMetadata {
// 使用统一的存储路径Data/Files/{file.id}/
const conversionId = file.id
const outputPath = path.join(this.storageDir, file.id)
// const outputPath = this.storageDir
const outputFileName = path.basename(file.path, path.extname(file.path))
fs.mkdirSync(outputPath, { recursive: true })
const markdownParts: string[] = []
let counter = 0
// Process each page
result.pages.forEach((page) => {
let pageMarkdown = page.markdown
// Process images from this page
page.images.forEach((image) => {
if (image.imageBase64) {
let imageFormat = 'jpeg' // default format
let imageBase64Data = image.imageBase64
// Check for data URL prefix more efficiently
const prefixEnd = image.imageBase64.indexOf(';base64,')
if (prefixEnd > 0) {
const prefix = image.imageBase64.substring(0, prefixEnd)
const formatIndex = prefix.indexOf('image/')
if (formatIndex >= 0) {
imageFormat = prefix.substring(formatIndex + 6)
}
imageBase64Data = image.imageBase64.substring(prefixEnd + 8)
}
const imageFileName = `img-${counter}.${imageFormat}`
const imagePath = path.join(outputPath, imageFileName)
// Save image file
try {
fs.writeFileSync(imagePath, Buffer.from(imageBase64Data, 'base64'))
// Update image reference in markdown
// Use relative path for better portability
const relativeImagePath = `./${imageFileName}`
// Find the start and end of the image markdown
const imgStart = pageMarkdown.indexOf(image.imageBase64)
if (imgStart >= 0) {
// Find the markdown image syntax around this base64
const mdStart = pageMarkdown.lastIndexOf('![', imgStart)
const mdEnd = pageMarkdown.indexOf(')', imgStart)
if (mdStart >= 0 && mdEnd >= 0) {
// Replace just this specific image reference
pageMarkdown =
pageMarkdown.substring(0, mdStart) +
`![Image ${counter}](${relativeImagePath})` +
pageMarkdown.substring(mdEnd + 1)
}
}
counter++
} catch (error) {
Logger.error(`Failed to save image ${imageFileName}:`, error)
}
}
})
markdownParts.push(pageMarkdown)
})
// Combine all markdown content with double newlines for readability
const combinedMarkdown = markdownParts.join('\n\n')
// Write the markdown content to a file
const mdFileName = `${outputFileName}.md`
const mdFilePath = path.join(outputPath, mdFileName)
fs.writeFileSync(mdFilePath, combinedMarkdown)
return {
id: conversionId,
name: file.name.replace(/\.[^/.]+$/, '.md'),
origin_name: file.origin_name,
path: mdFilePath,
created_at: new Date().toISOString(),
type: FileTypes.DOCUMENT,
ext: '.md',
size: fs.statSync(mdFilePath).size,
count: 1
} as FileMetadata
}
public checkQuota(): Promise<number> {
throw new Error('Method not implemented.')
}
}

View File

@@ -0,0 +1,30 @@
import { FileMetadata, PreprocessProvider as Provider } from '@types'
import BasePreprocessProvider from './BasePreprocessProvider'
import PreprocessProviderFactory from './PreprocessProviderFactory'
export default class PreprocessProvider {
private sdk: BasePreprocessProvider
constructor(provider: Provider, userId?: string) {
this.sdk = PreprocessProviderFactory.create(provider, userId)
}
public async parseFile(
sourceId: string,
file: FileMetadata
): Promise<{ processedFile: FileMetadata; quota?: number }> {
return this.sdk.parseFile(sourceId, file)
}
public async checkQuota(): Promise<number> {
return this.sdk.checkQuota()
}
/**
* 检查文件是否已经被预处理过
* @param file 文件信息
* @returns 如果已处理返回处理后的文件信息否则返回null
*/
public async checkIfAlreadyProcessed(file: FileMetadata): Promise<FileMetadata | null> {
return this.sdk.checkIfAlreadyProcessed(file)
}
}

View File

@@ -0,0 +1,21 @@
import { PreprocessProvider } from '@types'
import BasePreprocessProvider from './BasePreprocessProvider'
import DefaultPreprocessProvider from './DefaultPreprocessProvider'
import Doc2xPreprocessProvider from './Doc2xPreprocessProvider'
import MineruPreprocessProvider from './MineruPreprocessProvider'
import MistralPreprocessProvider from './MistralPreprocessProvider'
export default class PreprocessProviderFactory {
static create(provider: PreprocessProvider, userId?: string): BasePreprocessProvider {
switch (provider.id) {
case 'doc2x':
return new Doc2xPreprocessProvider(provider)
case 'mistral':
return new MistralPreprocessProvider(provider)
case 'mineru':
return new MineruPreprocessProvider(provider, userId)
default:
return new DefaultPreprocessProvider(provider)
}
}
}

View File

@@ -1,19 +1,15 @@
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces' import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { KnowledgeBaseParams } from '@types' import { ApiClient } from '@types'
import EmbeddingsFactory from './EmbeddingsFactory' import EmbeddingsFactory from './EmbeddingsFactory'
export default class Embeddings { export default class Embeddings {
private sdk: BaseEmbeddings private sdk: BaseEmbeddings
constructor({ model, provider, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams) { constructor({ embedApiClient, dimensions }: { embedApiClient: ApiClient; dimensions?: number }) {
this.sdk = EmbeddingsFactory.create({ this.sdk = EmbeddingsFactory.create({
model, embedApiClient,
provider,
apiKey,
apiVersion,
baseURL,
dimensions dimensions
} as KnowledgeBaseParams) })
} }
public async init(): Promise<void> { public async init(): Promise<void> {
return this.sdk.init() return this.sdk.init()

View File

@@ -3,28 +3,22 @@ import { OllamaEmbeddings } from '@cherrystudio/embedjs-ollama'
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai' import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings' import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings'
import { getInstanceName } from '@main/utils' import { getInstanceName } from '@main/utils'
import { KnowledgeBaseParams } from '@types' import { ApiClient } 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 { export default class EmbeddingsFactory {
static create({ model, provider, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams): BaseEmbeddings { static create({ embedApiClient, dimensions }: { embedApiClient: ApiClient; dimensions?: number }): BaseEmbeddings {
const batchSize = 10 const batchSize = 10
const { model, provider, apiKey, apiVersion, baseURL } = embedApiClient
if (provider === 'voyageai') { if (provider === 'voyageai') {
if (VOYAGE_SUPPORTED_DIM_MODELS.includes(model)) { return new VoyageEmbeddings({
return new VoyageEmbeddings({ modelName: model,
modelName: model, apiKey,
apiKey, outputDimension: VOYAGE_SUPPORTED_DIM_MODELS.includes(model) ? dimensions : undefined,
outputDimension: dimensions, batchSize: 8
batchSize: 8 })
})
} else {
return new VoyageEmbeddings({
modelName: model,
apiKey,
batchSize: 8
})
}
} }
if (provider === 'ollama') { if (provider === 'ollama') {
if (baseURL.includes('v1/')) { if (baseURL.includes('v1/')) {

View File

@@ -1,27 +1,29 @@
import { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces' import { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { VoyageEmbeddings as _VoyageEmbeddings } from '@langchain/community/embeddings/voyage' 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 { export class VoyageEmbeddings extends BaseEmbeddings {
private model: _VoyageEmbeddings private model: _VoyageEmbeddings
constructor(private readonly configuration?: ConstructorParameters<typeof _VoyageEmbeddings>[0]) { constructor(private readonly configuration?: ConstructorParameters<typeof _VoyageEmbeddings>[0]) {
super() super()
if (!this.configuration) this.configuration = {} if (!this.configuration) {
if (!this.configuration.modelName) this.configuration.modelName = 'voyage-3' throw new Error('Pass in a configuration.')
if (!SUPPORTED_DIM_MODELS.includes(this.configuration.modelName) && this.configuration.outputDimension) {
throw new Error(`VoyageEmbeddings only supports ${SUPPORTED_DIM_MODELS.join(', ')}`)
} }
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> { override async getDimensions(): Promise<number> {
if (!this.configuration?.outputDimension) { return this.configuration?.outputDimension ?? (this.configuration?.modelName === 'voyage-code-2' ? 1536 : 1024)
throw new Error('You need to pass in the optional dimensions parameter for this model')
}
return this.configuration?.outputDimension
} }
override async embedDocuments(texts: string[]): Promise<number[][]> { 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,10 +1,9 @@
import * as fs from 'node:fs'
import { JsonLoader, LocalPathLoader, RAGApplication, TextLoader } from '@cherrystudio/embedjs' import { JsonLoader, LocalPathLoader, RAGApplication, TextLoader } from '@cherrystudio/embedjs'
import type { AddLoaderReturn } from '@cherrystudio/embedjs-interfaces' import type { AddLoaderReturn } from '@cherrystudio/embedjs-interfaces'
import { WebLoader } from '@cherrystudio/embedjs-loader-web' import { WebLoader } from '@cherrystudio/embedjs-loader-web'
import { readTextFileWithAutoEncoding } from '@main/utils/file'
import { LoaderReturn } from '@shared/config/types' import { LoaderReturn } from '@shared/config/types'
import { FileType, KnowledgeBaseParams } from '@types' import { FileMetadata, KnowledgeBaseParams } from '@types'
import Logger from 'electron-log' import Logger from 'electron-log'
import { DraftsExportLoader } from './draftsExportLoader' import { DraftsExportLoader } from './draftsExportLoader'
@@ -16,6 +15,7 @@ const FILE_LOADER_MAP: Record<string, string> = {
// 内置类型 // 内置类型
'.pdf': 'common', '.pdf': 'common',
'.csv': 'common', '.csv': 'common',
'.doc': 'common',
'.docx': 'common', '.docx': 'common',
'.pptx': 'common', '.pptx': 'common',
'.xlsx': 'common', '.xlsx': 'common',
@@ -38,7 +38,7 @@ const FILE_LOADER_MAP: Record<string, string> = {
export async function addOdLoader( export async function addOdLoader(
ragApplication: RAGApplication, ragApplication: RAGApplication,
file: FileType, file: FileMetadata,
base: KnowledgeBaseParams, base: KnowledgeBaseParams,
forceReload: boolean forceReload: boolean
): Promise<AddLoaderReturn> { ): Promise<AddLoaderReturn> {
@@ -64,7 +64,7 @@ export async function addOdLoader(
export async function addFileLoader( export async function addFileLoader(
ragApplication: RAGApplication, ragApplication: RAGApplication,
file: FileType, file: FileMetadata,
base: KnowledgeBaseParams, base: KnowledgeBaseParams,
forceReload: boolean forceReload: boolean
): Promise<LoaderReturn> { ): Promise<LoaderReturn> {
@@ -114,7 +114,7 @@ export async function addFileLoader(
// HTML类型处理 // HTML类型处理
loaderReturn = await ragApplication.addLoader( loaderReturn = await ragApplication.addLoader(
new WebLoader({ new WebLoader({
urlOrContent: fs.readFileSync(file.path, 'utf-8'), urlOrContent: await readTextFileWithAutoEncoding(file.path),
chunkSize: base.chunkSize, chunkSize: base.chunkSize,
chunkOverlap: base.chunkOverlap chunkOverlap: base.chunkOverlap
}) as any, }) as any,
@@ -124,7 +124,7 @@ export async function addFileLoader(
case 'json': case 'json':
try { try {
jsonObject = JSON.parse(fs.readFileSync(file.path, 'utf-8')) jsonObject = JSON.parse(await readTextFileWithAutoEncoding(file.path))
} catch (error) { } catch (error) {
jsonParsed = false jsonParsed = false
Logger.warn('[KnowledgeBase] failed parsing json file, falling back to text processing:', file.path, error) Logger.warn('[KnowledgeBase] failed parsing json file, falling back to text processing:', file.path, error)
@@ -140,7 +140,7 @@ export async function addFileLoader(
// 如果是其他文本类型且尚未读取文件,则读取文件 // 如果是其他文本类型且尚未读取文件,则读取文件
loaderReturn = await ragApplication.addLoader( loaderReturn = await ragApplication.addLoader(
new TextLoader({ new TextLoader({
text: fs.readFileSync(file.path, 'utf-8'), text: await readTextFileWithAutoEncoding(file.path),
chunkSize: base.chunkSize, chunkSize: base.chunkSize,
chunkOverlap: base.chunkOverlap chunkOverlap: base.chunkOverlap
}) as any, }) as any,

View File

@@ -0,0 +1,44 @@
import { BaseLoader } from '@cherrystudio/embedjs-interfaces'
import { cleanString } from '@cherrystudio/embedjs-utils'
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
import md5 from 'md5'
export class NoteLoader extends BaseLoader<{ type: 'NoteLoader' }> {
private readonly text: string
private readonly sourceUrl?: string
constructor({
text,
sourceUrl,
chunkSize,
chunkOverlap
}: {
text: string
sourceUrl?: string
chunkSize?: number
chunkOverlap?: number
}) {
super(`NoteLoader_${md5(text + (sourceUrl || ''))}`, { text, sourceUrl }, chunkSize ?? 2000, chunkOverlap ?? 0)
this.text = text
this.sourceUrl = sourceUrl
}
override async *getUnfilteredChunks() {
const chunker = new RecursiveCharacterTextSplitter({
chunkSize: this.chunkSize,
chunkOverlap: this.chunkOverlap
})
const chunks = await chunker.splitText(cleanString(this.text))
for (const chunk of chunks) {
yield {
pageContent: chunk,
metadata: {
type: 'NoteLoader' as const,
source: this.sourceUrl || 'note'
}
}
}
}
}

View File

@@ -5,7 +5,7 @@ export default abstract class BaseReranker {
protected base: KnowledgeBaseParams protected base: KnowledgeBaseParams
constructor(base: KnowledgeBaseParams) { constructor(base: KnowledgeBaseParams) {
if (!base.rerankModel) { if (!base.rerankApiClient) {
throw new Error('Rerank model is required') throw new Error('Rerank model is required')
} }
this.base = base this.base = base
@@ -17,14 +17,17 @@ export default abstract class BaseReranker {
* Get Rerank Request Url * Get Rerank Request Url
*/ */
protected getRerankUrl() { protected getRerankUrl() {
if (this.base.rerankModelProvider === 'dashscope') { if (this.base.rerankApiClient?.provider === 'bailian') {
return 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank' return 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank'
} }
let baseURL = this.base?.rerankBaseURL?.endsWith('/') let baseURL = this.base.rerankApiClient?.baseURL
? this.base.rerankBaseURL.slice(0, -1)
: this.base.rerankBaseURL if (baseURL && baseURL.endsWith('/')) {
// 必须携带/v1否则会404 // `/` 结尾强制使用rerankBaseURL
return `${baseURL}rerank`
}
if (baseURL && !baseURL.endsWith('/v1')) { if (baseURL && !baseURL.endsWith('/v1')) {
baseURL = `${baseURL}/v1` baseURL = `${baseURL}/v1`
} }
@@ -36,20 +39,20 @@ export default abstract class BaseReranker {
* Get Rerank Request Body * Get Rerank Request Body
*/ */
protected getRerankRequestBody(query: string, searchResults: ExtractChunkData[]) { protected getRerankRequestBody(query: string, searchResults: ExtractChunkData[]) {
const provider = this.base.rerankModelProvider const provider = this.base.rerankApiClient?.provider
const documents = searchResults.map((doc) => doc.pageContent) const documents = searchResults.map((doc) => doc.pageContent)
const topN = this.base.documentCount const topN = this.base.documentCount
if (provider === 'voyageai') { if (provider === 'voyageai') {
return { return {
model: this.base.rerankModel, model: this.base.rerankApiClient?.model,
query, query,
documents, documents,
top_k: topN top_k: topN
} }
} else if (provider === 'dashscope') { } else if (provider === 'bailian') {
return { return {
model: this.base.rerankModel, model: this.base.rerankApiClient?.model,
input: { input: {
query, query,
documents documents
@@ -58,9 +61,15 @@ export default abstract class BaseReranker {
top_n: topN top_n: topN
} }
} }
} else if (provider?.includes('tei')) {
return {
query,
texts: documents,
return_text: true
}
} else { } else {
return { return {
model: this.base.rerankModel, model: this.base.rerankApiClient?.model,
query, query,
documents, documents,
top_n: topN top_n: topN
@@ -72,11 +81,18 @@ export default abstract class BaseReranker {
* Extract Rerank Result * Extract Rerank Result
*/ */
protected extractRerankResult(data: any) { protected extractRerankResult(data: any) {
const provider = this.base.rerankModelProvider const provider = this.base.rerankApiClient?.provider
if (provider === 'dashscope') { if (provider === 'bailian') {
return data.output.results return data.output.results
} else if (provider === 'voyageai') { } else if (provider === 'voyageai') {
return data.data return data.data
} else if (provider?.includes('tei')) {
return data.map((item: any) => {
return {
index: item.index,
relevance_score: item.score
}
})
} else { } else {
return data.results return data.results
} }
@@ -113,7 +129,7 @@ export default abstract class BaseReranker {
public defaultHeaders() { public defaultHeaders() {
return { return {
Authorization: `Bearer ${this.base.rerankApiKey}`, Authorization: `Bearer ${this.base.rerankApiClient?.apiKey}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
} }

View File

@@ -6,6 +6,7 @@ import DifyKnowledgeServer from './dify-knowledge'
import FetchServer from './fetch' import FetchServer from './fetch'
import FileSystemServer from './filesystem' import FileSystemServer from './filesystem'
import MemoryServer from './memory' import MemoryServer from './memory'
import PythonServer from './python'
import ThinkingServer from './sequentialthinking' import ThinkingServer from './sequentialthinking'
export function createInMemoryMCPServer(name: string, args: string[] = [], envs: Record<string, string> = {}): Server { export function createInMemoryMCPServer(name: string, args: string[] = [], envs: Record<string, string> = {}): Server {
@@ -31,6 +32,9 @@ export function createInMemoryMCPServer(name: string, args: string[] = [], envs:
const difyKey = envs.DIFY_KEY const difyKey = envs.DIFY_KEY
return new DifyKnowledgeServer(difyKey, args).server return new DifyKnowledgeServer(difyKey, args).server
} }
case '@cherry/python': {
return new PythonServer().server
}
default: default:
throw new Error(`Unknown in-memory MCP server: ${name}`) throw new Error(`Unknown in-memory MCP server: ${name}`)
} }

View File

@@ -0,0 +1,113 @@
import { pythonService } from '@main/services/PythonService'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'
import Logger from 'electron-log'
/**
* Python MCP Server for executing Python code using Pyodide
*/
class PythonServer {
public server: Server
constructor() {
this.server = new Server(
{
name: 'python-server',
version: '1.0.0'
},
{
capabilities: {
tools: {}
}
}
)
this.setupRequestHandlers()
}
private setupRequestHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'python_execute',
description: `Execute Python code using Pyodide in a sandboxed environment. Supports most Python standard library and scientific packages.
The code will be executed with Python 3.12.
Dependencies may be defined via PEP 723 script metadata, e.g. to install "pydantic", the script should start
with a comment of the form:
# /// script
# dependencies = ['pydantic']
# ///
print('python code here')`,
inputSchema: {
type: 'object',
properties: {
code: {
type: 'string',
description: 'The Python code to execute'
},
context: {
type: 'object',
description: 'Optional context variables to pass to the Python execution environment',
additionalProperties: true
},
timeout: {
type: 'number',
description: 'Timeout in milliseconds (default: 60000)',
default: 60000
}
},
required: ['code']
}
}
]
}
})
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params
if (name !== 'python_execute') {
throw new McpError(ErrorCode.MethodNotFound, `Tool ${name} not found`)
}
try {
const {
code,
context = {},
timeout = 60000
} = args as {
code: string
context?: Record<string, any>
timeout?: number
}
if (!code || typeof code !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Code parameter is required and must be a string')
}
Logger.info('Executing Python code via Pyodide')
const result = await pythonService.executeScript(code, context, timeout)
return {
content: [
{
type: 'text',
text: result
}
]
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
Logger.error('Python execution error:', errorMessage)
throw new McpError(ErrorCode.InternalError, `Python execution failed: ${errorMessage}`)
}
})
}
}
export default PythonServer

View File

@@ -106,6 +106,7 @@ class SequentialThinkingServer {
type: 'text', type: 'text',
text: JSON.stringify( text: JSON.stringify(
{ {
thought: validatedInput.thought,
thoughtNumber: validatedInput.thoughtNumber, thoughtNumber: validatedInput.thoughtNumber,
totalThoughts: validatedInput.totalThoughts, totalThoughts: validatedInput.totalThoughts,
nextThoughtNeeded: validatedInput.nextThoughtNeeded, nextThoughtNeeded: validatedInput.nextThoughtNeeded,

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,11 +1,13 @@
import { isWin } from '@main/constant' import { isWin } from '@main/constant'
import { locales } from '@main/utils/locales' import { locales } from '@main/utils/locales'
import { generateUserAgent } from '@main/utils/systemInfo'
import { FeedUrl, UpgradeChannel } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { FeedUrl } from '@shared/config/constant' import { CancellationToken, UpdateInfo } from 'builder-util-runtime'
import { UpdateInfo } from 'builder-util-runtime'
import { app, BrowserWindow, dialog } from 'electron' import { app, BrowserWindow, dialog } from 'electron'
import logger from 'electron-log' import logger from 'electron-log'
import { AppUpdater as _AppUpdater, autoUpdater } from 'electron-updater' import { AppUpdater as _AppUpdater, autoUpdater, NsisUpdater, UpdateCheckResult } from 'electron-updater'
import path from 'path'
import icon from '../../../build/icon.png?asset' import icon from '../../../build/icon.png?asset'
import { configManager } from './ConfigManager' import { configManager } from './ConfigManager'
@@ -13,6 +15,8 @@ import { configManager } from './ConfigManager'
export default class AppUpdater { export default class AppUpdater {
autoUpdater: _AppUpdater = autoUpdater autoUpdater: _AppUpdater = autoUpdater
private releaseInfo: UpdateInfo | undefined private releaseInfo: UpdateInfo | undefined
private cancellationToken: CancellationToken = new CancellationToken()
private updateCheckResult: UpdateCheckResult | null = null
constructor(mainWindow: BrowserWindow) { constructor(mainWindow: BrowserWindow) {
logger.transports.file.level = 'info' logger.transports.file.level = 'info'
@@ -21,9 +25,11 @@ export default class AppUpdater {
autoUpdater.forceDevUpdateConfig = !app.isPackaged autoUpdater.forceDevUpdateConfig = !app.isPackaged
autoUpdater.autoDownload = configManager.getAutoUpdate() autoUpdater.autoDownload = configManager.getAutoUpdate()
autoUpdater.autoInstallOnAppQuit = configManager.getAutoUpdate() autoUpdater.autoInstallOnAppQuit = configManager.getAutoUpdate()
autoUpdater.setFeedURL(configManager.getFeedUrl()) autoUpdater.requestHeaders = {
...autoUpdater.requestHeaders,
'User-Agent': generateUserAgent()
}
// 检测下载错误
autoUpdater.on('error', (error) => { autoUpdater.on('error', (error) => {
// 简单记录错误信息和时间戳 // 简单记录错误信息和时间戳
logger.error('更新异常', { logger.error('更新异常', {
@@ -56,17 +62,137 @@ export default class AppUpdater {
logger.info('下载完成', releaseInfo) logger.info('下载完成', releaseInfo)
}) })
if (isWin) {
;(autoUpdater as NsisUpdater).installDirectory = path.dirname(app.getPath('exe'))
}
this.autoUpdater = autoUpdater this.autoUpdater = autoUpdater
} }
private async _getPreReleaseVersionFromGithub(channel: UpgradeChannel) {
try {
logger.info('get pre release version from github', channel)
const responses = await fetch('https://api.github.com/repos/CherryHQ/cherry-studio/releases?per_page=8', {
headers: {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'Accept-Language': 'en-US,en;q=0.9'
}
})
const data = (await responses.json()) as GithubReleaseInfo[]
const release: GithubReleaseInfo | undefined = data.find((item: GithubReleaseInfo) => {
return item.prerelease && item.tag_name.includes(`-${channel}.`)
})
logger.info('release info', release)
if (!release) {
return null
}
logger.info('release info', release.tag_name)
return `https://github.com/CherryHQ/cherry-studio/releases/download/${release.tag_name}`
} catch (error) {
logger.error('Failed to get latest not draft version from github:', error)
return null
}
}
private async _getIpCountry() {
try {
// add timeout using AbortController
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000)
const ipinfo = await fetch('https://ipinfo.io/json', {
signal: controller.signal,
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Accept-Language': 'en-US,en;q=0.9'
}
})
clearTimeout(timeoutId)
const data = await ipinfo.json()
return data.country || 'CN'
} catch (error) {
logger.error('Failed to get ipinfo:', error)
return 'CN'
}
}
public setAutoUpdate(isActive: boolean) { public setAutoUpdate(isActive: boolean) {
autoUpdater.autoDownload = isActive autoUpdater.autoDownload = isActive
autoUpdater.autoInstallOnAppQuit = isActive autoUpdater.autoInstallOnAppQuit = isActive
} }
public setFeedUrl(feedUrl: FeedUrl) { private _getChannelByVersion(version: string) {
autoUpdater.setFeedURL(feedUrl) if (version.includes(`-${UpgradeChannel.BETA}.`)) {
configManager.setFeedUrl(feedUrl) return UpgradeChannel.BETA
}
if (version.includes(`-${UpgradeChannel.RC}.`)) {
return UpgradeChannel.RC
}
return UpgradeChannel.LATEST
}
private _getTestChannel() {
const currentChannel = this._getChannelByVersion(app.getVersion())
const savedChannel = configManager.getTestChannel()
if (currentChannel === UpgradeChannel.LATEST) {
return savedChannel || UpgradeChannel.RC
}
if (savedChannel === currentChannel) {
return savedChannel
}
// if the upgrade channel is not equal to the current channel, use the latest channel
return UpgradeChannel.LATEST
}
private async _setFeedUrl() {
const testPlan = configManager.getTestPlan()
if (testPlan) {
const channel = this._getTestChannel()
if (channel === UpgradeChannel.LATEST) {
this.autoUpdater.channel = UpgradeChannel.LATEST
this.autoUpdater.setFeedURL(FeedUrl.GITHUB_LATEST)
return
}
const preReleaseUrl = await this._getPreReleaseVersionFromGithub(channel)
if (preReleaseUrl) {
this.autoUpdater.setFeedURL(preReleaseUrl)
this.autoUpdater.channel = channel
return
}
// if no prerelease url, use lowest prerelease version to avoid error
this.autoUpdater.setFeedURL(FeedUrl.PRERELEASE_LOWEST)
this.autoUpdater.channel = UpgradeChannel.LATEST
return
}
this.autoUpdater.channel = UpgradeChannel.LATEST
this.autoUpdater.setFeedURL(FeedUrl.PRODUCTION)
const ipCountry = await this._getIpCountry()
logger.info('ipCountry', ipCountry)
if (ipCountry.toLowerCase() !== 'cn') {
this.autoUpdater.setFeedURL(FeedUrl.GITHUB_LATEST)
}
}
public cancelDownload() {
this.cancellationToken.cancel()
this.cancellationToken = new CancellationToken()
if (this.autoUpdater.autoDownload) {
this.updateCheckResult?.cancellationToken?.cancel()
}
} }
public async checkForUpdates() { public async checkForUpdates() {
@@ -77,17 +203,26 @@ export default class AppUpdater {
} }
} }
await this._setFeedUrl()
// disable downgrade after change the channel
this.autoUpdater.allowDowngrade = false
// github and gitcode don't support multiple range download
this.autoUpdater.disableDifferentialDownload = true
try { try {
const update = await this.autoUpdater.checkForUpdates() this.updateCheckResult = await this.autoUpdater.checkForUpdates()
if (update?.isUpdateAvailable && !this.autoUpdater.autoDownload) { if (this.updateCheckResult?.isUpdateAvailable && !this.autoUpdater.autoDownload) {
// 如果 autoDownload 为 false则需要再调用下面的函数触发下 // 如果 autoDownload 为 false则需要再调用下面的函数触发下
// do not use await, because it will block the return of this function // do not use await, because it will block the return of this function
this.autoUpdater.downloadUpdate() logger.info('downloadUpdate manual by check for updates', this.cancellationToken)
this.autoUpdater.downloadUpdate(this.cancellationToken)
} }
return { return {
currentVersion: this.autoUpdater.currentVersion, currentVersion: this.autoUpdater.currentVersion,
updateInfo: update?.updateInfo updateInfo: this.updateCheckResult?.updateInfo
} }
} catch (error) { } catch (error) {
logger.error('Failed to check for update:', error) logger.error('Failed to check for update:', error)
@@ -143,7 +278,11 @@ export default class AppUpdater {
return releaseNotes.map((note) => note.note).join('\n') return releaseNotes.map((note) => note.note).join('\n')
} }
} }
interface GithubReleaseInfo {
draft: boolean
prerelease: boolean
tag_name: string
}
interface ReleaseNoteInfo { interface ReleaseNoteInfo {
readonly version: string readonly version: string
readonly note: string | null readonly note: string | null

View File

@@ -1,5 +1,6 @@
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { WebDavConfig } from '@types' import { WebDavConfig } from '@types'
import { S3Config } from '@types'
import archiver from 'archiver' import archiver from 'archiver'
import { exec } from 'child_process' import { exec } from 'child_process'
import { app } from 'electron' import { app } from 'electron'
@@ -9,6 +10,8 @@ import StreamZip from 'node-stream-zip'
import * as path from 'path' import * as path from 'path'
import { CreateDirectoryOptions, FileStat } from 'webdav' import { CreateDirectoryOptions, FileStat } from 'webdav'
import { getDataPath } from '../utils'
import S3Storage from './S3Storage'
import WebDav from './WebDav' import WebDav from './WebDav'
import { windowService } from './WindowService' import { windowService } from './WindowService'
@@ -24,6 +27,16 @@ class BackupManager {
this.restoreFromWebdav = this.restoreFromWebdav.bind(this) this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
this.listWebdavFiles = this.listWebdavFiles.bind(this) this.listWebdavFiles = this.listWebdavFiles.bind(this)
this.deleteWebdavFile = this.deleteWebdavFile.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> { private async setWritableRecursive(dirPath: string): Promise<void> {
@@ -84,7 +97,11 @@ class BackupManager {
const onProgress = (processData: { stage: string; progress: number; total: number }) => { const onProgress = (processData: { stage: string; progress: number; total: number }) => {
mainWindow?.webContents.send(IpcChannel.BackupProgress, processData) 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 { try {
@@ -146,18 +163,23 @@ class BackupManager {
let totalBytes = 0 let totalBytes = 0
let processedBytes = 0 let processedBytes = 0
// 首先计算总文件数和总大小 // 首先计算总文件数和总大小,但不记录详细日志
const calculateTotals = async (dirPath: string) => { const calculateTotals = async (dirPath: string) => {
const items = await fs.readdir(dirPath, { withFileTypes: true }) try {
for (const item of items) { const items = await fs.readdir(dirPath, { withFileTypes: true })
const fullPath = path.join(dirPath, item.name) for (const item of items) {
if (item.isDirectory()) { const fullPath = path.join(dirPath, item.name)
await calculateTotals(fullPath) if (item.isDirectory()) {
} else { await calculateTotals(fullPath)
totalEntries++ } else {
const stats = await fs.stat(fullPath) totalEntries++
totalBytes += stats.size const stats = await fs.stat(fullPath)
totalBytes += stats.size
}
} }
} catch (error) {
// 仅在出错时记录日志
Logger.error('[BackupManager] Error calculating totals:', error)
} }
} }
@@ -229,7 +251,11 @@ class BackupManager {
const onProgress = (processData: { stage: string; progress: number; total: number }) => { const onProgress = (processData: { stage: string; progress: number; total: number }) => {
mainWindow?.webContents.send(IpcChannel.RestoreProgress, processData) 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 { try {
@@ -253,7 +279,7 @@ class BackupManager {
Logger.log('[backup] step 3: restore Data directory') Logger.log('[backup] step 3: restore Data directory')
// 恢复 Data 目录 // 恢复 Data 目录
const sourcePath = path.join(this.tempDir, 'Data') const sourcePath = path.join(this.tempDir, 'Data')
const destPath = path.join(app.getPath('userData'), 'Data') const destPath = getDataPath()
const dataExists = await fs.pathExists(sourcePath) const dataExists = await fs.pathExists(sourcePath)
const dataFiles = dataExists ? await fs.readdir(sourcePath) : [] const dataFiles = dataExists ? await fs.readdir(sourcePath) : []
@@ -295,14 +321,22 @@ class BackupManager {
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) { async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip' const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
const backupedFilePath = await this.backup(_, filename, data, undefined, webdavConfig.skipBackupFile) const backupedFilePath = await this.backup(_, filename, data, undefined, webdavConfig.skipBackupFile)
const contentLength = (await fs.stat(backupedFilePath)).size
const webdavClient = new WebDav(webdavConfig) const webdavClient = new WebDav(webdavConfig)
try { try {
const result = await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), { let result
overwrite: true, if (webdavConfig.disableStream) {
contentLength const fileContent = await fs.readFile(backupedFilePath)
}) result = await webdavClient.putFileContents(filename, fileContent, {
// 上传成功后删除本地备份文件 overwrite: true
})
} else {
const contentLength = (await fs.stat(backupedFilePath)).size
result = await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
overwrite: true,
contentLength
})
}
await fs.remove(backupedFilePath) await fs.remove(backupedFilePath)
return result return result
} catch (error) { } catch (error) {
@@ -381,21 +415,54 @@ class BackupManager {
destination: string, destination: string,
onProgress: (size: number) => void onProgress: (size: number) => void
): Promise<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 countFiles = async (dir: string): Promise<number> => {
const destPath = path.join(destination, item.name) 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()) { totalFiles = await countFiles(source)
await fs.ensureDir(destPath)
await this.copyDirWithProgress(sourcePath, destPath, onProgress) // 复制文件并更新进度
} else { const copyDir = async (src: string, dest: string): Promise<void> => {
const stats = await fs.stat(sourcePath) const items = await fs.readdir(src, { withFileTypes: true })
await fs.copy(sourcePath, destPath)
onProgress(stats.size) 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) { async checkConnection(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
@@ -422,6 +489,191 @@ class BackupManager {
throw new Error(error.message || 'Failed to delete backup file') 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 export default BackupManager

View File

@@ -1,4 +1,4 @@
import { defaultLanguage, FeedUrl, ZOOM_SHORTCUTS } from '@shared/config/constant' import { defaultLanguage, UpgradeChannel, ZOOM_SHORTCUTS } from '@shared/config/constant'
import { LanguageVarious, Shortcut, ThemeMode } from '@types' import { LanguageVarious, Shortcut, ThemeMode } from '@types'
import { app } from 'electron' import { app } from 'electron'
import Store from 'electron-store' import Store from 'electron-store'
@@ -16,14 +16,16 @@ export enum ConfigKeys {
ClickTrayToShowQuickAssistant = 'clickTrayToShowQuickAssistant', ClickTrayToShowQuickAssistant = 'clickTrayToShowQuickAssistant',
EnableQuickAssistant = 'enableQuickAssistant', EnableQuickAssistant = 'enableQuickAssistant',
AutoUpdate = 'autoUpdate', AutoUpdate = 'autoUpdate',
FeedUrl = 'feedUrl', TestPlan = 'testPlan',
TestChannel = 'testChannel',
EnableDataCollection = 'enableDataCollection', EnableDataCollection = 'enableDataCollection',
SelectionAssistantEnabled = 'selectionAssistantEnabled', SelectionAssistantEnabled = 'selectionAssistantEnabled',
SelectionAssistantTriggerMode = 'selectionAssistantTriggerMode', SelectionAssistantTriggerMode = 'selectionAssistantTriggerMode',
SelectionAssistantFollowToolbar = 'selectionAssistantFollowToolbar', SelectionAssistantFollowToolbar = 'selectionAssistantFollowToolbar',
SelectionAssistantRemeberWinSize = 'selectionAssistantRemeberWinSize', SelectionAssistantRemeberWinSize = 'selectionAssistantRemeberWinSize',
SelectionAssistantFilterMode = 'selectionAssistantFilterMode', SelectionAssistantFilterMode = 'selectionAssistantFilterMode',
SelectionAssistantFilterList = 'selectionAssistantFilterList' SelectionAssistantFilterList = 'selectionAssistantFilterList',
DisableHardwareAcceleration = 'disableHardwareAcceleration'
} }
export class ConfigManager { export class ConfigManager {
@@ -142,12 +144,20 @@ export class ConfigManager {
this.set(ConfigKeys.AutoUpdate, value) this.set(ConfigKeys.AutoUpdate, value)
} }
getFeedUrl(): string { getTestPlan(): boolean {
return this.get<string>(ConfigKeys.FeedUrl, FeedUrl.PRODUCTION) return this.get<boolean>(ConfigKeys.TestPlan, false)
} }
setFeedUrl(value: FeedUrl) { setTestPlan(value: boolean) {
this.set(ConfigKeys.FeedUrl, value) this.set(ConfigKeys.TestPlan, value)
}
getTestChannel(): UpgradeChannel {
return this.get<UpgradeChannel>(ConfigKeys.TestChannel)
}
setTestChannel(value: UpgradeChannel) {
this.set(ConfigKeys.TestChannel, value)
} }
getEnableDataCollection(): boolean { getEnableDataCollection(): boolean {
@@ -209,6 +219,14 @@ export class ConfigManager {
this.setAndNotify(ConfigKeys.SelectionAssistantFilterList, value) 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) { setAndNotify(key: string, value: unknown) {
this.set(key, value, true) this.set(key, value, true)
} }

View File

@@ -4,18 +4,29 @@ import { locales } from '../utils/locales'
import { configManager } from './ConfigManager' import { configManager } from './ConfigManager'
class ContextMenu { class ContextMenu {
public contextMenu(w: Electron.BrowserWindow) { public contextMenu(w: Electron.WebContents) {
w.webContents.on('context-menu', (_event, properties) => { w.on('context-menu', (_event, properties) => {
const template: MenuItemConstructorOptions[] = this.createEditMenuItems(properties) const template: MenuItemConstructorOptions[] = this.createEditMenuItems(properties)
const filtered = template.filter((item) => item.visible !== false) const filtered = template.filter((item) => item.visible !== false)
if (filtered.length > 0) { if (filtered.length > 0) {
const menu = Menu.buildFromTemplate([...filtered, ...this.createInspectMenuItems(w)]) let template = [...filtered, ...this.createInspectMenuItems(w)]
const dictionarySuggestions = this.createDictionarySuggestions(properties, w)
if (dictionarySuggestions.length > 0) {
template = [
...dictionarySuggestions,
{ type: 'separator' },
this.createSpellCheckMenuItem(properties, w),
{ type: 'separator' },
...template
]
}
const menu = Menu.buildFromTemplate(template)
menu.popup() menu.popup()
} }
}) })
} }
private createInspectMenuItems(w: Electron.BrowserWindow): MenuItemConstructorOptions[] { private createInspectMenuItems(w: Electron.WebContents): MenuItemConstructorOptions[] {
const locale = locales[configManager.getLanguage()] const locale = locales[configManager.getLanguage()]
const { common } = locale.translation const { common } = locale.translation
const template: MenuItemConstructorOptions[] = [ const template: MenuItemConstructorOptions[] = [
@@ -23,7 +34,7 @@ class ContextMenu {
id: 'inspect', id: 'inspect',
label: common.inspect, label: common.inspect,
click: () => { click: () => {
w.webContents.toggleDevTools() w.toggleDevTools()
}, },
enabled: true enabled: true
} }
@@ -72,6 +83,53 @@ class ContextMenu {
return template return template
} }
private createSpellCheckMenuItem(
properties: Electron.ContextMenuParams,
w: Electron.WebContents
): MenuItemConstructorOptions {
const hasText = properties.selectionText.length > 0
return {
id: 'learnSpelling',
label: '&Learn Spelling',
visible: Boolean(properties.isEditable && hasText && properties.misspelledWord),
click: () => {
w.session.addWordToSpellCheckerDictionary(properties.misspelledWord)
}
}
}
private createDictionarySuggestions(
properties: Electron.ContextMenuParams,
w: Electron.WebContents
): MenuItemConstructorOptions[] {
const hasText = properties.selectionText.length > 0
if (!hasText || !properties.misspelledWord) {
return []
}
if (properties.dictionarySuggestions.length === 0) {
return [
{
id: 'dictionarySuggestions',
label: 'No Guesses Found',
visible: true,
enabled: false
}
]
}
return properties.dictionarySuggestions.map((suggestion) => ({
id: 'dictionarySuggestions',
label: suggestion,
visible: Boolean(properties.isEditable && hasText && properties.misspelledWord),
click: (menuItem: Electron.MenuItem) => {
w.replaceMisspelling(menuItem.label)
}
}))
}
} }
export const contextMenu = new ContextMenu() export const contextMenu = new ContextMenu()

View File

@@ -0,0 +1,396 @@
import { getMcpDir, getTempDir } from '@main/utils/file'
import logger from 'electron-log'
import * as fs from 'fs'
import StreamZip from 'node-stream-zip'
import * as os from 'os'
import * as path from 'path'
import { v4 as uuidv4 } from 'uuid'
// Type definitions
export interface DxtManifest {
dxt_version: string
name: string
display_name?: string
version: string
description?: string
long_description?: string
author?: {
name?: string
email?: string
url?: string
}
repository?: {
type?: string
url?: string
}
homepage?: string
documentation?: string
support?: string
icon?: string
server: {
type: string
entry_point: string
mcp_config: {
command: string
args: string[]
env?: Record<string, string>
platform_overrides?: {
[platform: string]: {
command?: string
args?: string[]
env?: Record<string, string>
}
}
}
}
tools?: Array<{
name: string
description: string
}>
keywords?: string[]
license?: string
user_config?: Record<string, any>
compatibility?: {
claude_desktop?: string
platforms?: string[]
runtimes?: Record<string, string>
}
}
export interface DxtUploadResult {
success: boolean
data?: {
manifest: DxtManifest
extractDir: string
}
error?: string
}
export function performVariableSubstitution(
value: string,
extractDir: string,
userConfig?: Record<string, any>
): string {
let result = value
// Replace ${__dirname} with the extraction directory
result = result.replace(/\$\{__dirname\}/g, extractDir)
// Replace ${HOME} with user's home directory
result = result.replace(/\$\{HOME\}/g, os.homedir())
// Replace ${DESKTOP} with user's desktop directory
const desktopDir = path.join(os.homedir(), 'Desktop')
result = result.replace(/\$\{DESKTOP\}/g, desktopDir)
// Replace ${DOCUMENTS} with user's documents directory
const documentsDir = path.join(os.homedir(), 'Documents')
result = result.replace(/\$\{DOCUMENTS\}/g, documentsDir)
// Replace ${DOWNLOADS} with user's downloads directory
const downloadsDir = path.join(os.homedir(), 'Downloads')
result = result.replace(/\$\{DOWNLOADS\}/g, downloadsDir)
// Replace ${pathSeparator} or ${/} with the platform-specific path separator
result = result.replace(/\$\{pathSeparator\}/g, path.sep)
result = result.replace(/\$\{\/\}/g, path.sep)
// Replace ${user_config.KEY} with user-configured values
if (userConfig) {
result = result.replace(/\$\{user_config\.([^}]+)\}/g, (match, key) => {
return userConfig[key] || match // Keep original if not found
})
}
return result
}
export function applyPlatformOverrides(mcpConfig: any, extractDir: string, userConfig?: Record<string, any>): any {
const platform = process.platform
const resolvedConfig = { ...mcpConfig }
// Apply platform-specific overrides
if (mcpConfig.platform_overrides && mcpConfig.platform_overrides[platform]) {
const override = mcpConfig.platform_overrides[platform]
// Override command if specified
if (override.command) {
resolvedConfig.command = override.command
}
// Override args if specified
if (override.args) {
resolvedConfig.args = override.args
}
// Merge environment variables
if (override.env) {
resolvedConfig.env = { ...resolvedConfig.env, ...override.env }
}
}
// Apply variable substitution to all string values
if (resolvedConfig.command) {
resolvedConfig.command = performVariableSubstitution(resolvedConfig.command, extractDir, userConfig)
}
if (resolvedConfig.args) {
resolvedConfig.args = resolvedConfig.args.map((arg: string) =>
performVariableSubstitution(arg, extractDir, userConfig)
)
}
if (resolvedConfig.env) {
for (const [key, value] of Object.entries(resolvedConfig.env)) {
resolvedConfig.env[key] = performVariableSubstitution(value as string, extractDir, userConfig)
}
}
return resolvedConfig
}
export interface ResolvedMcpConfig {
command: string
args: string[]
env?: Record<string, string>
}
class DxtService {
private tempDir = path.join(getTempDir(), 'dxt_uploads')
private mcpDir = getMcpDir()
constructor() {
this.ensureDirectories()
}
private ensureDirectories() {
try {
// Create temp directory
if (!fs.existsSync(this.tempDir)) {
fs.mkdirSync(this.tempDir, { recursive: true })
}
// Create MCP directory
if (!fs.existsSync(this.mcpDir)) {
fs.mkdirSync(this.mcpDir, { recursive: true })
}
} catch (error) {
logger.error('[DxtService] Failed to create directories:', error)
}
}
private async moveDirectory(source: string, destination: string): Promise<void> {
try {
// Try rename first (works if on same filesystem)
fs.renameSync(source, destination)
} catch (error) {
// If rename fails (cross-filesystem), use copy + remove
logger.info('[DxtService] Cross-filesystem move detected, using copy + remove')
// Ensure parent directory exists
const parentDir = path.dirname(destination)
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true })
}
// Recursively copy directory
await this.copyDirectory(source, destination)
// Remove source directory
fs.rmSync(source, { recursive: true, force: true })
}
}
private async copyDirectory(source: string, destination: string): Promise<void> {
// Create destination directory
fs.mkdirSync(destination, { recursive: true })
// Read source directory
const entries = fs.readdirSync(source, { withFileTypes: true })
// Copy each entry
for (const entry of entries) {
const sourcePath = path.join(source, entry.name)
const destPath = path.join(destination, entry.name)
if (entry.isDirectory()) {
await this.copyDirectory(sourcePath, destPath)
} else {
fs.copyFileSync(sourcePath, destPath)
}
}
}
public async uploadDxt(_: Electron.IpcMainInvokeEvent, filePath: string): Promise<DxtUploadResult> {
const tempExtractDir = path.join(this.tempDir, `dxt_${uuidv4()}`)
try {
// Validate file exists
if (!fs.existsSync(filePath)) {
throw new Error('DXT file not found')
}
// Extract the DXT file (which is a ZIP archive) to a temporary directory
logger.info('[DxtService] Extracting DXT file:', filePath)
const zip = new StreamZip.async({ file: filePath })
await zip.extract(null, tempExtractDir)
await zip.close()
// Read and validate the manifest.json
const manifestPath = path.join(tempExtractDir, 'manifest.json')
if (!fs.existsSync(manifestPath)) {
throw new Error('manifest.json not found in DXT file')
}
const manifestContent = fs.readFileSync(manifestPath, 'utf-8')
const manifest: DxtManifest = JSON.parse(manifestContent)
// Validate required fields in manifest
if (!manifest.dxt_version) {
throw new Error('Invalid manifest: missing dxt_version')
}
if (!manifest.name) {
throw new Error('Invalid manifest: missing name')
}
if (!manifest.version) {
throw new Error('Invalid manifest: missing version')
}
if (!manifest.server) {
throw new Error('Invalid manifest: missing server configuration')
}
if (!manifest.server.mcp_config) {
throw new Error('Invalid manifest: missing server.mcp_config')
}
if (!manifest.server.mcp_config.command) {
throw new Error('Invalid manifest: missing server.mcp_config.command')
}
if (!Array.isArray(manifest.server.mcp_config.args)) {
throw new Error('Invalid manifest: server.mcp_config.args must be an array')
}
// Use server name as the final extract directory for automatic version management
// Sanitize the name to prevent creating subdirectories
const sanitizedName = manifest.name.replace(/\//g, '-')
const serverDirName = `server-${sanitizedName}`
const finalExtractDir = path.join(this.mcpDir, serverDirName)
// Clean up any existing version of this server
if (fs.existsSync(finalExtractDir)) {
logger.info('[DxtService] Removing existing server directory:', finalExtractDir)
fs.rmSync(finalExtractDir, { recursive: true, force: true })
}
// Move the temporary directory to the final location
// Use recursive copy + remove instead of rename to handle cross-filesystem moves
await this.moveDirectory(tempExtractDir, finalExtractDir)
logger.info('[DxtService] DXT server extracted to:', finalExtractDir)
// Clean up the uploaded DXT file if it's in temp directory
if (filePath.startsWith(this.tempDir)) {
fs.unlinkSync(filePath)
}
// Return success with manifest and extraction path
return {
success: true,
data: {
manifest,
extractDir: finalExtractDir
}
}
} catch (error) {
// Clean up on error
if (fs.existsSync(tempExtractDir)) {
fs.rmSync(tempExtractDir, { recursive: true, force: true })
}
const errorMessage = error instanceof Error ? error.message : 'Failed to process DXT file'
logger.error('[DxtService] DXT upload error:', error)
return {
success: false,
error: errorMessage
}
}
}
/**
* Get resolved MCP configuration for a DXT server with platform overrides and variable substitution
*/
public getResolvedMcpConfig(dxtPath: string, userConfig?: Record<string, any>): ResolvedMcpConfig | null {
try {
// Read the manifest from the DXT server directory
const manifestPath = path.join(dxtPath, 'manifest.json')
if (!fs.existsSync(manifestPath)) {
logger.error('[DxtService] Manifest not found:', manifestPath)
return null
}
const manifestContent = fs.readFileSync(manifestPath, 'utf-8')
const manifest: DxtManifest = JSON.parse(manifestContent)
if (!manifest.server?.mcp_config) {
logger.error('[DxtService] No mcp_config found in manifest')
return null
}
// Apply platform overrides and variable substitution
const resolvedConfig = applyPlatformOverrides(manifest.server.mcp_config, dxtPath, userConfig)
logger.info('[DxtService] Resolved MCP config:', {
command: resolvedConfig.command,
args: resolvedConfig.args,
env: resolvedConfig.env ? Object.keys(resolvedConfig.env) : undefined
})
return resolvedConfig
} catch (error) {
logger.error('[DxtService] Failed to resolve MCP config:', error)
return null
}
}
public cleanupDxtServer(serverName: string): boolean {
try {
// Handle server names that might contain slashes (e.g., "anthropic/sequential-thinking")
// by replacing slashes with the same separator used during installation
const sanitizedName = serverName.replace(/\//g, '-')
const serverDirName = `server-${sanitizedName}`
const serverDir = path.join(this.mcpDir, serverDirName)
// First try the sanitized path
if (fs.existsSync(serverDir)) {
logger.info('[DxtService] Removing DXT server directory:', serverDir)
fs.rmSync(serverDir, { recursive: true, force: true })
return true
}
// Fallback: try with original name in case it was stored differently
const originalServerDir = path.join(this.mcpDir, `server-${serverName}`)
if (fs.existsSync(originalServerDir)) {
logger.info('[DxtService] Removing DXT server directory:', originalServerDir)
fs.rmSync(originalServerDir, { recursive: true, force: true })
return true
}
logger.warn('[DxtService] Server directory not found:', serverDir)
return false
} catch (error) {
logger.error('[DxtService] Failed to cleanup DXT server:', error)
return false
}
}
public cleanup() {
try {
// Clean up temp directory
if (fs.existsSync(this.tempDir)) {
fs.rmSync(this.tempDir, { recursive: true, force: true })
}
} catch (error) {
logger.error('[DxtService] Cleanup error:', error)
}
}
}
export default DxtService

View File

@@ -1,6 +1,6 @@
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 { documentExts, imageExts, MB } from '@shared/config/constant'
import { FileType } from '@types' import { FileMetadata } from '@types'
import * as crypto from 'crypto' import * as crypto from 'crypto'
import { import {
dialog, dialog,
@@ -15,9 +15,11 @@ import * as fs from 'fs'
import { writeFileSync } from 'fs' import { writeFileSync } from 'fs'
import { readFile } from 'fs/promises' import { readFile } from 'fs/promises'
import officeParser from 'officeparser' import officeParser from 'officeparser'
import { getDocument } from 'officeparser/pdfjs-dist-build/pdf.js'
import * as path from 'path' import * as path from 'path'
import { chdir } from 'process' import { chdir } from 'process'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import WordExtractor from 'word-extractor'
class FileStorage { class FileStorage {
private storageDir = getFilesDir() private storageDir = getFilesDir()
@@ -51,8 +53,9 @@ class FileStorage {
}) })
} }
findDuplicateFile = async (filePath: string): Promise<FileType | null> => { findDuplicateFile = async (filePath: string): Promise<FileMetadata | null> => {
const stats = fs.statSync(filePath) const stats = fs.statSync(filePath)
console.log('stats', stats, filePath)
const fileSize = stats.size const fileSize = stats.size
const files = await fs.promises.readdir(this.storageDir) const files = await fs.promises.readdir(this.storageDir)
@@ -90,7 +93,7 @@ class FileStorage {
public selectFile = async ( public selectFile = async (
_: Electron.IpcMainInvokeEvent, _: Electron.IpcMainInvokeEvent,
options?: OpenDialogOptions options?: OpenDialogOptions
): Promise<FileType[] | null> => { ): Promise<FileMetadata[] | null> => {
const defaultOptions: OpenDialogOptions = { const defaultOptions: OpenDialogOptions = {
properties: ['openFile'] properties: ['openFile']
} }
@@ -149,7 +152,7 @@ class FileStorage {
} }
} }
public uploadFile = async (_: Electron.IpcMainInvokeEvent, file: FileType): Promise<FileType> => { public uploadFile = async (_: Electron.IpcMainInvokeEvent, file: FileMetadata): Promise<FileMetadata> => {
const duplicateFile = await this.findDuplicateFile(file.path) const duplicateFile = await this.findDuplicateFile(file.path)
if (duplicateFile) { if (duplicateFile) {
@@ -173,7 +176,7 @@ class FileStorage {
const stats = await fs.promises.stat(destPath) const stats = await fs.promises.stat(destPath)
const fileType = getFileType(ext) const fileType = getFileType(ext)
const fileMetadata: FileType = { const fileMetadata: FileMetadata = {
id: uuid, id: uuid,
origin_name, origin_name,
name: uuid + ext, name: uuid + ext,
@@ -185,10 +188,12 @@ class FileStorage {
count: 1 count: 1
} }
logger.info('[FileStorage] File uploaded:', fileMetadata)
return fileMetadata return fileMetadata
} }
public getFile = async (_: Electron.IpcMainInvokeEvent, filePath: string): Promise<FileType | null> => { public getFile = async (_: Electron.IpcMainInvokeEvent, filePath: string): Promise<FileMetadata | null> => {
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
return null return null
} }
@@ -197,7 +202,7 @@ class FileStorage {
const ext = path.extname(filePath) const ext = path.extname(filePath)
const fileType = getFileType(ext) const fileType = getFileType(ext)
const fileInfo: FileType = { const fileInfo: FileMetadata = {
id: uuidv4(), id: uuidv4(),
origin_name: path.basename(filePath), origin_name: path.basename(filePath),
name: path.basename(filePath), name: path.basename(filePath),
@@ -213,16 +218,40 @@ class FileStorage {
} }
public deleteFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => { public deleteFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => {
if (!fs.existsSync(path.join(this.storageDir, id))) {
return
}
await fs.promises.unlink(path.join(this.storageDir, id)) await fs.promises.unlink(path.join(this.storageDir, id))
} }
public readFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<string> => { public deleteDir = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => {
if (!fs.existsSync(path.join(this.storageDir, id))) {
return
}
await fs.promises.rm(path.join(this.storageDir, id), { recursive: true })
}
public readFile = async (
_: Electron.IpcMainInvokeEvent,
id: string,
detectEncoding: boolean = false
): Promise<string> => {
const filePath = path.join(this.storageDir, id) const filePath = path.join(this.storageDir, id)
if (documentExts.includes(path.extname(filePath))) { const fileExtension = path.extname(filePath)
if (documentExts.includes(fileExtension)) {
const originalCwd = process.cwd() const originalCwd = process.cwd()
try { try {
chdir(this.tempDir) chdir(this.tempDir)
if (fileExtension === '.doc') {
const extractor = new WordExtractor()
const extracted = await extractor.extract(filePath)
chdir(originalCwd)
return extracted.getBody()
}
const data = await officeParser.parseOfficeAsync(filePath) const data = await officeParser.parseOfficeAsync(filePath)
chdir(originalCwd) chdir(originalCwd)
return data return data
@@ -233,15 +262,24 @@ 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)
throw new Error(`Failed to read file: ${filePath}.`)
}
} }
public createTempFile = async (_: Electron.IpcMainInvokeEvent, fileName: string): Promise<string> => { public createTempFile = async (_: Electron.IpcMainInvokeEvent, fileName: string): Promise<string> => {
if (!fs.existsSync(this.tempDir)) { if (!fs.existsSync(this.tempDir)) {
fs.mkdirSync(this.tempDir, { recursive: true }) fs.mkdirSync(this.tempDir, { recursive: true })
} }
const tempFilePath = path.join(this.tempDir, `temp_file_${uuidv4()}_${fileName}`)
return tempFilePath return path.join(this.tempDir, `temp_file_${uuidv4()}_${fileName}`)
} }
public writeFile = async ( public writeFile = async (
@@ -268,7 +306,7 @@ class FileStorage {
} }
} }
public saveBase64Image = async (_: Electron.IpcMainInvokeEvent, base64Data: string): Promise<FileType> => { public saveBase64Image = async (_: Electron.IpcMainInvokeEvent, base64Data: string): Promise<FileMetadata> => {
try { try {
if (!base64Data) { if (!base64Data) {
throw new Error('Base64 data is required') throw new Error('Base64 data is required')
@@ -294,7 +332,7 @@ class FileStorage {
await fs.promises.writeFile(destPath, buffer) await fs.promises.writeFile(destPath, buffer)
const fileMetadata: FileType = { const fileMetadata: FileMetadata = {
id: uuid, id: uuid,
origin_name: uuid + ext, origin_name: uuid + ext,
name: uuid + ext, name: uuid + ext,
@@ -321,6 +359,16 @@ class FileStorage {
return { data: base64, mime } return { data: base64, mime }
} }
public pdfPageCount = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<number> => {
const filePath = path.join(this.storageDir, id)
const buffer = await fs.promises.readFile(filePath)
const doc = await getDocument({ data: buffer }).promise
const pages = doc.numPages
await doc.destroy()
return pages
}
public binaryImage = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: Buffer; mime: string }> => { public binaryImage = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: Buffer; mime: string }> => {
const filePath = path.join(this.storageDir, id) const filePath = path.join(this.storageDir, id)
const data = await fs.promises.readFile(filePath) const data = await fs.promises.readFile(filePath)
@@ -341,7 +389,7 @@ class FileStorage {
public open = async ( public open = async (
_: Electron.IpcMainInvokeEvent, _: Electron.IpcMainInvokeEvent,
options: OpenDialogOptions options: OpenDialogOptions
): Promise<{ fileName: string; filePath: string; content: Buffer } | null> => { ): Promise<{ fileName: string; filePath: string; content?: Buffer; size: number } | null> => {
try { try {
const result: OpenDialogReturnValue = await dialog.showOpenDialog({ const result: OpenDialogReturnValue = await dialog.showOpenDialog({
title: '打开文件', title: '打开文件',
@@ -353,8 +401,16 @@ class FileStorage {
if (!result.canceled && result.filePaths.length > 0) { if (!result.canceled && result.filePaths.length > 0) {
const filePath = result.filePaths[0] const filePath = result.filePaths[0]
const fileName = filePath.split('/').pop() || '' const fileName = filePath.split('/').pop() || ''
const content = await readFile(filePath) const stats = await fs.promises.stat(filePath)
return { fileName, filePath, content }
// If the file is less than 2GB, read the content
if (stats.size < 2 * 1024 * 1024 * 1024) {
const content = await readFile(filePath)
return { fileName, filePath, content, size: stats.size }
}
// For large files, only return file information, do not read content
return { fileName, filePath, size: stats.size }
} }
return null return null
@@ -368,6 +424,19 @@ class FileStorage {
shell.openPath(path).catch((err) => logger.error('[IPC - Error] Failed to open file:', err)) 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 ( public save = async (
_: Electron.IpcMainInvokeEvent, _: Electron.IpcMainInvokeEvent,
fileName: string, fileName: string,
@@ -435,7 +504,7 @@ class FileStorage {
_: Electron.IpcMainInvokeEvent, _: Electron.IpcMainInvokeEvent,
url: string, url: string,
isUseContentType?: boolean isUseContentType?: boolean
): Promise<FileType> => { ): Promise<FileMetadata> => {
try { try {
const response = await fetch(url) const response = await fetch(url)
if (!response.ok) { if (!response.ok) {
@@ -477,7 +546,7 @@ class FileStorage {
const stats = await fs.promises.stat(destPath) const stats = await fs.promises.stat(destPath)
const fileType = getFileType(ext) const fileType = getFileType(ext)
const fileMetadata: FileType = { const fileMetadata: FileMetadata = {
id: uuid, id: uuid,
origin_name: filename, origin_name: filename,
name: uuid + ext, name: uuid + ext,

View File

@@ -16,21 +16,24 @@
import * as fs from 'node:fs' import * as fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { RAGApplication, RAGApplicationBuilder, TextLoader } from '@cherrystudio/embedjs' import { RAGApplication, RAGApplicationBuilder } from '@cherrystudio/embedjs'
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces' import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { LibSqlDb } from '@cherrystudio/embedjs-libsql' import { LibSqlDb } from '@cherrystudio/embedjs-libsql'
import { SitemapLoader } from '@cherrystudio/embedjs-loader-sitemap' import { SitemapLoader } from '@cherrystudio/embedjs-loader-sitemap'
import { WebLoader } from '@cherrystudio/embedjs-loader-web' import { WebLoader } from '@cherrystudio/embedjs-loader-web'
import Embeddings from '@main/embeddings/Embeddings' import OcrProvider from '@main/knowledage/ocr/OcrProvider'
import { addFileLoader } from '@main/loader' import PreprocessProvider from '@main/knowledage/preprocess/PreprocessProvider'
import Reranker from '@main/reranker/Reranker' import Embeddings from '@main/knowledge/embeddings/Embeddings'
import { addFileLoader } from '@main/knowledge/loader'
import { NoteLoader } from '@main/knowledge/loader/noteLoader'
import Reranker from '@main/knowledge/reranker/Reranker'
import { windowService } from '@main/services/WindowService' import { windowService } from '@main/services/WindowService'
import { getDataPath } from '@main/utils'
import { getAllFiles } from '@main/utils/file' import { getAllFiles } from '@main/utils/file'
import { MB } from '@shared/config/constant' import { MB } from '@shared/config/constant'
import type { LoaderReturn } from '@shared/config/types' import type { LoaderReturn } from '@shared/config/types'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types' import { FileMetadata, KnowledgeBaseParams, KnowledgeItem } from '@types'
import { app } from 'electron'
import Logger from 'electron-log' import Logger from 'electron-log'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
@@ -38,12 +41,14 @@ export interface KnowledgeBaseAddItemOptions {
base: KnowledgeBaseParams base: KnowledgeBaseParams
item: KnowledgeItem item: KnowledgeItem
forceReload?: boolean forceReload?: boolean
userId?: string
} }
interface KnowledgeBaseAddItemOptionsNonNullableAttribute { interface KnowledgeBaseAddItemOptionsNonNullableAttribute {
base: KnowledgeBaseParams base: KnowledgeBaseParams
item: KnowledgeItem item: KnowledgeItem
forceReload: boolean forceReload: boolean
userId: string
} }
interface EvaluateTaskWorkload { interface EvaluateTaskWorkload {
@@ -88,14 +93,20 @@ const loaderTaskIntoOfSet = (loaderTask: LoaderTask): LoaderTaskOfSet => {
} }
class KnowledgeService { class KnowledgeService {
private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase') private storageDir = path.join(getDataPath(), 'KnowledgeBase')
// Byte based // Byte based
private workload = 0 private workload = 0
private processingItemCount = 0 private processingItemCount = 0
private knowledgeItemProcessingQueueMappingPromise: Map<LoaderTaskOfSet, () => void> = new Map() private knowledgeItemProcessingQueueMappingPromise: Map<LoaderTaskOfSet, () => void> = new Map()
private static MAXIMUM_WORKLOAD = 80 * MB private static MAXIMUM_WORKLOAD = 80 * MB
private static MAXIMUM_PROCESSING_ITEM_COUNT = 30 private static MAXIMUM_PROCESSING_ITEM_COUNT = 30
private static ERROR_LOADER_RETURN: LoaderReturn = { entriesAdded: 0, uniqueId: '', uniqueIds: [''], loaderType: '' } private static ERROR_LOADER_RETURN: LoaderReturn = {
entriesAdded: 0,
uniqueId: '',
uniqueIds: [''],
loaderType: '',
status: 'failed'
}
constructor() { constructor() {
this.initStorageDir() this.initStorageDir()
@@ -109,27 +120,21 @@ class KnowledgeService {
private getRagApplication = async ({ private getRagApplication = async ({
id, id,
model, embedApiClient,
provider, dimensions,
apiKey, documentCount
apiVersion,
baseURL,
dimensions
}: KnowledgeBaseParams): Promise<RAGApplication> => { }: KnowledgeBaseParams): Promise<RAGApplication> => {
let ragApplication: RAGApplication let ragApplication: RAGApplication
const embeddings = new Embeddings({ const embeddings = new Embeddings({
model, embedApiClient,
provider,
apiKey,
apiVersion,
baseURL,
dimensions dimensions
} as KnowledgeBaseParams) })
try { try {
ragApplication = await new RAGApplicationBuilder() ragApplication = await new RAGApplicationBuilder()
.setModel('NO_MODEL') .setModel('NO_MODEL')
.setEmbeddingModel(embeddings) .setEmbeddingModel(embeddings)
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) })) .setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
.setSearchResultCount(documentCount || 30)
.build() .build()
} catch (e) { } catch (e) {
Logger.error(e) Logger.error(e)
@@ -143,12 +148,13 @@ class KnowledgeService {
this.getRagApplication(base) this.getRagApplication(base)
} }
public reset = async (_: Electron.IpcMainInvokeEvent, { base }: { base: KnowledgeBaseParams }): Promise<void> => { public reset = async (_: Electron.IpcMainInvokeEvent, base: KnowledgeBaseParams): Promise<void> => {
const ragApplication = await this.getRagApplication(base) const ragApplication = await this.getRagApplication(base)
await ragApplication.reset() await ragApplication.reset()
} }
public delete = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => { public delete = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => {
console.log('id', id)
const dbPath = path.join(this.storageDir, id) const dbPath = path.join(this.storageDir, id)
if (fs.existsSync(dbPath)) { if (fs.existsSync(dbPath)) {
fs.rmSync(dbPath, { recursive: true }) fs.rmSync(dbPath, { recursive: true })
@@ -161,28 +167,49 @@ class KnowledgeService {
this.workload >= KnowledgeService.MAXIMUM_WORKLOAD this.workload >= KnowledgeService.MAXIMUM_WORKLOAD
) )
} }
private fileTask( private fileTask(
ragApplication: RAGApplication, ragApplication: RAGApplication,
options: KnowledgeBaseAddItemOptionsNonNullableAttribute options: KnowledgeBaseAddItemOptionsNonNullableAttribute
): LoaderTask { ): LoaderTask {
const { base, item, forceReload } = options const { base, item, forceReload, userId } = options
const file = item.content as FileType const file = item.content as FileMetadata
const loaderTask: LoaderTask = { const loaderTask: LoaderTask = {
loaderTasks: [ loaderTasks: [
{ {
state: LoaderTaskItemState.PENDING, state: LoaderTaskItemState.PENDING,
task: () => task: async () => {
addFileLoader(ragApplication, file, base, forceReload) try {
.then((result) => { // 添加预处理逻辑
loaderTask.loaderDoneReturn = result const fileToProcess: FileMetadata = await this.preprocessing(file, base, item, userId)
return result
}) // 使用处理后的文件进行加载
.catch((err) => { return addFileLoader(ragApplication, fileToProcess, base, forceReload)
Logger.error(err) .then((result) => {
return KnowledgeService.ERROR_LOADER_RETURN loaderTask.loaderDoneReturn = result
}), return result
})
.catch((e) => {
Logger.error(`Error in addFileLoader for ${file.name}: ${e}`)
const errorResult: LoaderReturn = {
...KnowledgeService.ERROR_LOADER_RETURN,
message: e.message,
messageSource: 'embedding'
}
loaderTask.loaderDoneReturn = errorResult
return errorResult
})
} catch (e: any) {
Logger.error(`Preprocessing failed for ${file.name}: ${e}`)
const errorResult: LoaderReturn = {
...KnowledgeService.ERROR_LOADER_RETURN,
message: e.message,
messageSource: 'preprocess'
}
loaderTask.loaderDoneReturn = errorResult
return errorResult
}
},
evaluateTaskWorkload: { workload: file.size } evaluateTaskWorkload: { workload: file.size }
} }
], ],
@@ -191,7 +218,6 @@ class KnowledgeService {
return loaderTask return loaderTask
} }
private directoryTask( private directoryTask(
ragApplication: RAGApplication, ragApplication: RAGApplication,
options: KnowledgeBaseAddItemOptionsNonNullableAttribute options: KnowledgeBaseAddItemOptionsNonNullableAttribute
@@ -231,7 +257,11 @@ class KnowledgeService {
}) })
.catch((err) => { .catch((err) => {
Logger.error(err) Logger.error(err)
return KnowledgeService.ERROR_LOADER_RETURN return {
...KnowledgeService.ERROR_LOADER_RETURN,
message: `Failed to add dir loader: ${err.message}`,
messageSource: 'embedding'
}
}), }),
evaluateTaskWorkload: { workload: file.size } evaluateTaskWorkload: { workload: file.size }
}) })
@@ -277,7 +307,11 @@ class KnowledgeService {
}) })
.catch((err) => { .catch((err) => {
Logger.error(err) Logger.error(err)
return KnowledgeService.ERROR_LOADER_RETURN return {
...KnowledgeService.ERROR_LOADER_RETURN,
message: `Failed to add url loader: ${err.message}`,
messageSource: 'embedding'
}
}) })
}, },
evaluateTaskWorkload: { workload: 2 * MB } evaluateTaskWorkload: { workload: 2 * MB }
@@ -317,7 +351,11 @@ class KnowledgeService {
}) })
.catch((err) => { .catch((err) => {
Logger.error(err) Logger.error(err)
return KnowledgeService.ERROR_LOADER_RETURN return {
...KnowledgeService.ERROR_LOADER_RETURN,
message: `Failed to add sitemap loader: ${err.message}`,
messageSource: 'embedding'
}
}), }),
evaluateTaskWorkload: { workload: 20 * MB } evaluateTaskWorkload: { workload: 20 * MB }
} }
@@ -333,6 +371,7 @@ class KnowledgeService {
): LoaderTask { ): LoaderTask {
const { base, item, forceReload } = options const { base, item, forceReload } = options
const content = item.content as string const content = item.content as string
const sourceUrl = (item as any).sourceUrl
const encoder = new TextEncoder() const encoder = new TextEncoder()
const contentBytes = encoder.encode(content) const contentBytes = encoder.encode(content)
@@ -342,7 +381,12 @@ class KnowledgeService {
state: LoaderTaskItemState.PENDING, state: LoaderTaskItemState.PENDING,
task: () => { task: () => {
const loaderReturn = ragApplication.addLoader( const loaderReturn = ragApplication.addLoader(
new TextLoader({ text: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }), new NoteLoader({
text: content,
sourceUrl,
chunkSize: base.chunkSize,
chunkOverlap: base.chunkOverlap
}),
forceReload forceReload
) as Promise<LoaderReturn> ) as Promise<LoaderReturn>
@@ -357,7 +401,11 @@ class KnowledgeService {
}) })
.catch((err) => { .catch((err) => {
Logger.error(err) Logger.error(err)
return KnowledgeService.ERROR_LOADER_RETURN return {
...KnowledgeService.ERROR_LOADER_RETURN,
message: `Failed to add note loader: ${err.message}`,
messageSource: 'embedding'
}
}) })
}, },
evaluateTaskWorkload: { workload: contentBytes.length } evaluateTaskWorkload: { workload: contentBytes.length }
@@ -423,10 +471,10 @@ class KnowledgeService {
}) })
} }
public add = (_: Electron.IpcMainInvokeEvent, options: KnowledgeBaseAddItemOptions): Promise<LoaderReturn> => { public add = async (_: Electron.IpcMainInvokeEvent, options: KnowledgeBaseAddItemOptions): Promise<LoaderReturn> => {
return new Promise((resolve) => { return new Promise((resolve) => {
const { base, item, forceReload = false } = options const { base, item, forceReload = false, userId = '' } = options
const optionsNonNullableAttribute = { base, item, forceReload } const optionsNonNullableAttribute = { base, item, forceReload, userId }
this.getRagApplication(base) this.getRagApplication(base)
.then((ragApplication) => { .then((ragApplication) => {
const task = (() => { const task = (() => {
@@ -452,12 +500,20 @@ class KnowledgeService {
}) })
this.processingQueueHandle() this.processingQueueHandle()
} else { } else {
resolve(KnowledgeService.ERROR_LOADER_RETURN) resolve({
...KnowledgeService.ERROR_LOADER_RETURN,
message: 'Unsupported item type',
messageSource: 'embedding'
})
} }
}) })
.catch((err) => { .catch((err) => {
Logger.error(err) Logger.error(err)
resolve(KnowledgeService.ERROR_LOADER_RETURN) resolve({
...KnowledgeService.ERROR_LOADER_RETURN,
message: `Failed to add item: ${err.message}`,
messageSource: 'embedding'
})
}) })
}) })
} }
@@ -490,6 +546,69 @@ class KnowledgeService {
} }
return await new Reranker(base).rerank(search, results) return await new Reranker(base).rerank(search, results)
} }
public getStorageDir = (): string => {
return this.storageDir
}
private preprocessing = async (
file: FileMetadata,
base: KnowledgeBaseParams,
item: KnowledgeItem,
userId: string
): Promise<FileMetadata> => {
let fileToProcess: FileMetadata = file
if (base.preprocessOrOcrProvider && file.ext.toLowerCase() === '.pdf') {
try {
let provider: PreprocessProvider | OcrProvider
if (base.preprocessOrOcrProvider.type === 'preprocess') {
provider = new PreprocessProvider(base.preprocessOrOcrProvider.provider, userId)
} else {
provider = new OcrProvider(base.preprocessOrOcrProvider.provider)
}
// 首先检查文件是否已经被预处理过
const alreadyProcessed = await provider.checkIfAlreadyProcessed(file)
if (alreadyProcessed) {
Logger.info(`File already preprocess processed, using cached result: ${file.path}`)
return alreadyProcessed
}
// 执行预处理
Logger.info(`Starting preprocess processing for scanned PDF: ${file.path}`)
const { processedFile, quota } = await provider.parseFile(item.id, file)
fileToProcess = processedFile
const mainWindow = windowService.getMainWindow()
mainWindow?.webContents.send('file-preprocess-finished', {
itemId: item.id,
quota: quota
})
} catch (err) {
Logger.error(`Preprocess processing failed: ${err}`)
// 如果预处理失败,使用原始文件
// fileToProcess = file
throw new Error(`Preprocess processing failed: ${err}`)
}
}
return fileToProcess
}
public checkQuota = async (
_: Electron.IpcMainInvokeEvent,
base: KnowledgeBaseParams,
userId: string
): Promise<number> => {
try {
if (base.preprocessOrOcrProvider && base.preprocessOrOcrProvider.type === 'preprocess') {
const provider = new PreprocessProvider(base.preprocessOrOcrProvider.provider, userId)
return await provider.checkQuota()
}
throw new Error('No preprocess provider configured')
} catch (err) {
Logger.error(`Failed to check quota: ${err}`)
throw new Error(`Failed to check quota: ${err}`)
}
}
} }
export default new KnowledgeService() export default new KnowledgeService()

View File

@@ -14,6 +14,16 @@ import {
type StreamableHTTPClientTransportOptions type StreamableHTTPClientTransportOptions
} from '@modelcontextprotocol/sdk/client/streamableHttp' } from '@modelcontextprotocol/sdk/client/streamableHttp'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory' import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'
// Import notification schemas from MCP SDK
import {
CancelledNotificationSchema,
LoggingMessageNotificationSchema,
ProgressNotificationSchema,
PromptListChangedNotificationSchema,
ResourceListChangedNotificationSchema,
ResourceUpdatedNotificationSchema,
ToolListChangedNotificationSchema
} from '@modelcontextprotocol/sdk/types.js'
import { nanoid } from '@reduxjs/toolkit' import { nanoid } from '@reduxjs/toolkit'
import { import {
GetMCPPromptResponse, GetMCPPromptResponse,
@@ -28,8 +38,10 @@ import { app } from 'electron'
import Logger from 'electron-log' import Logger from 'electron-log'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { memoize } from 'lodash' import { memoize } from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { CacheService } from './CacheService' import { CacheService } from './CacheService'
import DxtService from './DxtService'
import { CallBackServer } from './mcp/oauth/callback' import { CallBackServer } from './mcp/oauth/callback'
import { McpOAuthClientProvider } from './mcp/oauth/provider' import { McpOAuthClientProvider } from './mcp/oauth/provider'
import getLoginShellEnvironment from './mcp/shell-env' import getLoginShellEnvironment from './mcp/shell-env'
@@ -71,6 +83,8 @@ function withCache<T extends unknown[], R>(
class McpService { class McpService {
private clients: Map<string, Client> = new Map() private clients: Map<string, Client> = new Map()
private pendingClients: Map<string, Promise<Client>> = new Map() private pendingClients: Map<string, Promise<Client>> = new Map()
private dxtService = new DxtService()
private activeToolCalls: Map<string, AbortController> = new Map()
constructor() { constructor() {
this.initClient = this.initClient.bind(this) this.initClient = this.initClient.bind(this)
@@ -84,7 +98,10 @@ class McpService {
this.removeServer = this.removeServer.bind(this) this.removeServer = this.removeServer.bind(this)
this.restartServer = this.restartServer.bind(this) this.restartServer = this.restartServer.bind(this)
this.stopServer = this.stopServer.bind(this) this.stopServer = this.stopServer.bind(this)
this.abortTool = this.abortTool.bind(this)
this.cleanup = this.cleanup.bind(this) this.cleanup = this.cleanup.bind(this)
this.checkMcpConnectivity = this.checkMcpConnectivity.bind(this)
this.getServerVersion = this.getServerVersion.bind(this)
} }
private getServerKey(server: MCPServer): string { private getServerKey(server: MCPServer): string {
@@ -133,7 +150,7 @@ class McpService {
// Create new client instance for each connection // Create new client instance for each connection
const client = new Client({ name: 'Cherry Studio', version: app.getVersion() }, { capabilities: {} }) const client = new Client({ name: 'Cherry Studio', version: app.getVersion() }, { capabilities: {} })
const args = [...(server.args || [])] let args = [...(server.args || [])]
// let transport: StdioClientTransport | SSEClientTransport | InMemoryTransport | StreamableHTTPClientTransport // let transport: StdioClientTransport | SSEClientTransport | InMemoryTransport | StreamableHTTPClientTransport
const authProvider = new McpOAuthClientProvider({ const authProvider = new McpOAuthClientProvider({
@@ -203,6 +220,23 @@ class McpService {
} else if (server.command) { } else if (server.command) {
let cmd = server.command let cmd = server.command
// For DXT servers, use resolved configuration with platform overrides and variable substitution
if (server.dxtPath) {
const resolvedConfig = this.dxtService.getResolvedMcpConfig(server.dxtPath)
if (resolvedConfig) {
cmd = resolvedConfig.command
args = resolvedConfig.args
// Merge resolved environment variables with existing ones
server.env = {
...server.env,
...resolvedConfig.env
}
Logger.info(`[MCP] Using resolved DXT config - command: ${cmd}, args: ${args?.join(' ')}`)
} else {
Logger.warn(`[MCP] Failed to resolve DXT config for ${server.name}, falling back to manifest values`)
}
}
if (server.command === 'npx') { if (server.command === 'npx') {
cmd = await getBinaryPath('bun') cmd = await getBinaryPath('bun')
Logger.info(`[MCP] Using command: ${cmd}`) Logger.info(`[MCP] Using command: ${cmd}`)
@@ -249,7 +283,7 @@ class McpService {
this.removeProxyEnv(loginShellEnv) this.removeProxyEnv(loginShellEnv)
} }
const stdioTransport = new StdioClientTransport({ const transportOptions: any = {
command: cmd, command: cmd,
args, args,
env: { env: {
@@ -257,7 +291,15 @@ class McpService {
...server.env ...server.env
}, },
stderr: 'pipe' stderr: 'pipe'
}) }
// For DXT servers, set the working directory to the extracted path
if (server.dxtPath) {
transportOptions.cwd = server.dxtPath
Logger.info(`[MCP] Setting working directory for DXT server: ${server.dxtPath}`)
}
const stdioTransport = new StdioClientTransport(transportOptions)
stdioTransport.stderr?.on('data', (data) => stdioTransport.stderr?.on('data', (data) =>
Logger.info(`[MCP] Stdio stderr for server: ${server.name} `, data.toString()) Logger.info(`[MCP] Stdio stderr for server: ${server.name} `, data.toString())
) )
@@ -331,6 +373,12 @@ class McpService {
// Store the new client in the cache // Store the new client in the cache
this.clients.set(serverKey, client) this.clients.set(serverKey, client)
// Set up notification handlers
this.setupNotificationHandlers(client, server)
// Clear existing cache to ensure fresh data
this.clearServerCache(serverKey)
Logger.info(`[MCP] Activated server: ${server.name}`) Logger.info(`[MCP] Activated server: ${server.name}`)
return client return client
} catch (error: any) { } catch (error: any) {
@@ -349,6 +397,79 @@ class McpService {
return initPromise return initPromise
} }
/**
* Set up notification handlers for MCP client
*/
private setupNotificationHandlers(client: Client, server: MCPServer) {
const serverKey = this.getServerKey(server)
try {
// Set up tools list changed notification handler
client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
Logger.info(`[MCP] Tools list changed for server: ${server.name}`)
// Clear tools cache
CacheService.remove(`mcp:list_tool:${serverKey}`)
})
// Set up resources list changed notification handler
client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => {
Logger.info(`[MCP] Resources list changed for server: ${server.name}`)
// Clear resources cache
CacheService.remove(`mcp:list_resources:${serverKey}`)
})
// Set up prompts list changed notification handler
client.setNotificationHandler(PromptListChangedNotificationSchema, async () => {
Logger.info(`[MCP] Prompts list changed for server: ${server.name}`)
// Clear prompts cache
CacheService.remove(`mcp:list_prompts:${serverKey}`)
})
// Set up resource updated notification handler
client.setNotificationHandler(ResourceUpdatedNotificationSchema, async () => {
Logger.info(`[MCP] Resource updated for server: ${server.name}`)
// Clear resource-specific caches
this.clearResourceCaches(serverKey)
})
// Set up progress notification handler
client.setNotificationHandler(ProgressNotificationSchema, async (notification) => {
Logger.info(`[MCP] Progress notification received for server: ${server.name}`, notification.params)
})
// Set up cancelled notification handler
client.setNotificationHandler(CancelledNotificationSchema, async (notification) => {
Logger.info(`[MCP] Operation cancelled for server: ${server.name}`, notification.params)
})
// Set up logging message notification handler
client.setNotificationHandler(LoggingMessageNotificationSchema, async (notification) => {
Logger.info(`[MCP] Message from server ${server.name}:`, notification.params)
})
Logger.info(`[MCP] Set up notification handlers for server: ${server.name}`)
} catch (error) {
Logger.error(`[MCP] Failed to set up notification handlers for server ${server.name}:`, error)
}
}
/**
* Clear resource-specific caches for a server
*/
private clearResourceCaches(serverKey: string) {
CacheService.remove(`mcp:list_resources:${serverKey}`)
}
/**
* Clear all caches for a specific server
*/
private clearServerCache(serverKey: string) {
CacheService.remove(`mcp:list_tool:${serverKey}`)
CacheService.remove(`mcp:list_prompts:${serverKey}`)
CacheService.remove(`mcp:list_resources:${serverKey}`)
Logger.info(`[MCP] Cleared all caches for server: ${serverKey}`)
}
async closeClient(serverKey: string) { async closeClient(serverKey: string) {
const client = this.clients.get(serverKey) const client = this.clients.get(serverKey)
if (client) { if (client) {
@@ -356,8 +477,8 @@ class McpService {
await client.close() await client.close()
Logger.info(`[MCP] Closed server: ${serverKey}`) Logger.info(`[MCP] Closed server: ${serverKey}`)
this.clients.delete(serverKey) this.clients.delete(serverKey)
CacheService.remove(`mcp:list_tool:${serverKey}`) // Clear all caches for this server
Logger.info(`[MCP] Cleared cache for server: ${serverKey}`) this.clearServerCache(serverKey)
} else { } else {
Logger.warn(`[MCP] No client found for server: ${serverKey}`) Logger.warn(`[MCP] No client found for server: ${serverKey}`)
} }
@@ -375,12 +496,26 @@ class McpService {
if (existingClient) { if (existingClient) {
await this.closeClient(serverKey) await this.closeClient(serverKey)
} }
// If this is a DXT server, cleanup its directory
if (server.dxtPath) {
try {
const cleaned = this.dxtService.cleanupDxtServer(server.name)
if (cleaned) {
Logger.info(`[MCP] Cleaned up DXT server directory for: ${server.name}`)
}
} catch (error) {
Logger.error(`[MCP] Failed to cleanup DXT server: ${server.name}`, error)
}
}
} }
async restartServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) { async restartServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
Logger.info(`[MCP] Restarting server: ${server.name}`) Logger.info(`[MCP] Restarting server: ${server.name}`)
const serverKey = this.getServerKey(server) const serverKey = this.getServerKey(server)
await this.closeClient(serverKey) await this.closeClient(serverKey)
// Clear cache before restarting to ensure fresh data
this.clearServerCache(serverKey)
await this.initClient(server) await this.initClient(server)
} }
@@ -400,6 +535,12 @@ class McpService {
public async checkMcpConnectivity(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise<boolean> { public async checkMcpConnectivity(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise<boolean> {
Logger.info(`[MCP] Checking connectivity for server: ${server.name}`) Logger.info(`[MCP] Checking connectivity for server: ${server.name}`)
try { try {
Logger.info(`[MCP] About to call initClient for server: ${server.name}`, { hasInitClient: !!this.initClient })
if (!this.initClient) {
throw new Error('initClient method is not available')
}
const client = await this.initClient(server) const client = await this.initClient(server)
// Attempt to list tools as a way to check connectivity // Attempt to list tools as a way to check connectivity
await client.listTools() await client.listTools()
@@ -455,10 +596,14 @@ class McpService {
*/ */
public async callTool( public async callTool(
_: Electron.IpcMainInvokeEvent, _: 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> { ): Promise<MCPCallToolResponse> {
const toolCallId = callId || uuidv4()
const abortController = new AbortController()
this.activeToolCalls.set(toolCallId, abortController)
try { try {
Logger.info('[MCP] Calling:', server.name, name, args) Logger.info('[MCP] Calling:', server.name, name, args, 'callId:', toolCallId)
if (typeof args === 'string') { if (typeof args === 'string') {
try { try {
args = JSON.parse(args) args = JSON.parse(args)
@@ -468,12 +613,19 @@ class McpService {
} }
const client = await this.initClient(server) const client = await this.initClient(server)
const result = await client.callTool({ name, arguments: args }, undefined, { 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 return result as MCPCallToolResponse
} catch (error) { } catch (error) {
Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error) Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error)
throw error throw error
} finally {
this.activeToolCalls.delete(toolCallId)
} }
} }
@@ -664,6 +816,45 @@ class McpService {
delete env.http_proxy delete env.http_proxy
delete env.https_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
}
}
/**
* Get the server version information
*/
public async getServerVersion(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise<string | null> {
try {
Logger.info(`[MCP] Getting server version for: ${server.name}`)
const client = await this.initClient(server)
// Try to get server information which may include version
const serverInfo = client.getServerVersion()
Logger.info(`[MCP] Server info for ${server.name}:`, serverInfo)
if (serverInfo && serverInfo.version) {
Logger.info(`[MCP] Server version for ${server.name}: ${serverInfo.version}`)
return serverInfo.version
}
Logger.warn(`[MCP] No version information available for server: ${server.name}`)
return null
} catch (error: any) {
Logger.error(`[MCP] Failed to get server version for ${server.name}:`, error?.message)
return null
}
}
} }
export default new McpService() export default new McpService()

View File

@@ -0,0 +1,33 @@
import { Mistral } from '@mistralai/mistralai'
import { Provider } from '@types'
export class MistralClientManager {
private static instance: MistralClientManager
private client: Mistral | null = null
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
public static getInstance(): MistralClientManager {
if (!MistralClientManager.instance) {
MistralClientManager.instance = new MistralClientManager()
}
return MistralClientManager.instance
}
public initializeClient(provider: Provider): void {
if (!this.client) {
this.client = new Mistral({
apiKey: provider.apiKey,
serverURL: provider.apiHost
})
}
}
public getClient(): Mistral {
if (!this.client) {
throw new Error('Mistral client not initialized. Call initializeClient first.')
}
return this.client
}
}

View File

@@ -1,8 +1,6 @@
import { BrowserWindow, Notification as ElectronNotification } from 'electron' import { BrowserWindow, Notification as ElectronNotification } from 'electron'
import { Notification } from 'src/renderer/src/types/notification' import { Notification } from 'src/renderer/src/types/notification'
import icon from '../../../build/icon.png?asset'
class NotificationService { class NotificationService {
private window: BrowserWindow private window: BrowserWindow
@@ -15,8 +13,7 @@ class NotificationService {
// 使用 Electron Notification API // 使用 Electron Notification API
const electronNotification = new ElectronNotification({ const electronNotification = new ElectronNotification({
title: notification.title, title: notification.title,
body: notification.message, body: notification.message
icon: icon
}) })
electronNotification.on('click', () => { electronNotification.on('click', () => {

View File

@@ -19,7 +19,7 @@ export function registerProtocolClient(app: Electron.App) {
} }
} }
app.setAsDefaultProtocolClient('cherrystudio') app.setAsDefaultProtocolClient(CHERRY_STUDIO_PROTOCOL)
} }
export function handleProtocolUrl(url: string) { export function handleProtocolUrl(url: string) {

View File

@@ -0,0 +1,102 @@
import { randomUUID } from 'node:crypto'
import { BrowserWindow, ipcMain } from 'electron'
interface PythonExecutionRequest {
id: string
script: string
context: Record<string, any>
timeout: number
}
interface PythonExecutionResponse {
id: string
result?: string
error?: string
}
/**
* Service for executing Python code by communicating with the PyodideService in the renderer process
*/
export class PythonService {
private static instance: PythonService | null = null
private mainWindow: BrowserWindow | null = null
private pendingRequests = new Map<string, { resolve: (value: string) => void; reject: (error: Error) => void }>()
private constructor() {
// Private constructor for singleton pattern
this.setupIpcHandlers()
}
public static getInstance(): PythonService {
if (!PythonService.instance) {
PythonService.instance = new PythonService()
}
return PythonService.instance
}
private setupIpcHandlers() {
// Handle responses from renderer
ipcMain.on('python-execution-response', (_, response: PythonExecutionResponse) => {
const request = this.pendingRequests.get(response.id)
if (request) {
this.pendingRequests.delete(response.id)
if (response.error) {
request.reject(new Error(response.error))
} else {
request.resolve(response.result || '')
}
}
})
}
public setMainWindow(mainWindow: BrowserWindow) {
this.mainWindow = mainWindow
}
/**
* Execute Python code by sending request to renderer PyodideService
*/
public async executeScript(
script: string,
context: Record<string, any> = {},
timeout: number = 60000
): Promise<string> {
if (!this.mainWindow) {
throw new Error('Main window not set in PythonService')
}
return new Promise((resolve, reject) => {
const requestId = randomUUID()
// Store the request
this.pendingRequests.set(requestId, { resolve, reject })
// Set up timeout
const timeoutId = setTimeout(() => {
this.pendingRequests.delete(requestId)
reject(new Error('Python execution timed out'))
}, timeout + 5000) // Add 5s buffer for IPC communication
// Update resolve/reject to clear timeout
const originalResolve = resolve
const originalReject = reject
this.pendingRequests.set(requestId, {
resolve: (value: string) => {
clearTimeout(timeoutId)
originalResolve(value)
},
reject: (error: Error) => {
clearTimeout(timeoutId)
originalReject(error)
}
})
// Send request to renderer
const request: PythonExecutionRequest = { id: requestId, script, context, timeout }
this.mainWindow?.webContents.send('python-execution-request', request)
})
}
}
export const pythonService = PythonService.getInstance()

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 { SELECTION_FINETUNED_LIST, SELECTION_PREDEFINED_BLACKLIST } from '@main/configs/SelectionConfig'
import { isDev, isWin } from '@main/constant' import { isDev, isMac, isWin } from '@main/constant'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { BrowserWindow, ipcMain, screen } from 'electron' import { app, BrowserWindow, ipcMain, screen, systemPreferences } from 'electron'
import Logger from 'electron-log' import Logger from 'electron-log'
import { join } from 'path' import { join } from 'path'
import type { import type {
@@ -16,9 +16,12 @@ import type { ActionItem } from '../../renderer/src/types/selectionTypes'
import { ConfigKeys, configManager } from './ConfigManager' import { ConfigKeys, configManager } from './ConfigManager'
import storeSyncService from './StoreSyncService' import storeSyncService from './StoreSyncService'
const isSupportedOS = isWin || isMac
let SelectionHook: SelectionHookConstructor | null = null let SelectionHook: SelectionHookConstructor | null = null
try { try {
if (isWin) { //since selection-hook v1.0.0, it supports macOS
if (isSupportedOS) {
SelectionHook = require('selection-hook') SelectionHook = require('selection-hook')
} }
} catch (error) { } catch (error) {
@@ -118,7 +121,7 @@ export class SelectionService {
} }
public static getInstance(): SelectionService | null { public static getInstance(): SelectionService | null {
if (!isWin) return null if (!isSupportedOS) return null
if (!SelectionService.instance) { if (!SelectionService.instance) {
SelectionService.instance = new SelectionService() SelectionService.instance = new SelectionService()
@@ -138,7 +141,7 @@ export class SelectionService {
* Initialize zoom factor from config and subscribe to changes * Initialize zoom factor from config and subscribe to changes
* Ensures UI elements scale properly with system DPI settings * Ensures UI elements scale properly with system DPI settings
*/ */
private initZoomFactor() { private initZoomFactor(): void {
const zoomFactor = configManager.getZoomFactor() const zoomFactor = configManager.getZoomFactor()
if (zoomFactor) { if (zoomFactor) {
this.setZoomFactor(zoomFactor) this.setZoomFactor(zoomFactor)
@@ -151,7 +154,7 @@ export class SelectionService {
this.zoomFactor = zoomFactor this.zoomFactor = zoomFactor
} }
private initConfig() { private initConfig(): void {
this.triggerMode = configManager.getSelectionAssistantTriggerMode() as TriggerMode this.triggerMode = configManager.getSelectionAssistantTriggerMode() as TriggerMode
this.isFollowToolbar = configManager.getSelectionAssistantFollowToolbar() this.isFollowToolbar = configManager.getSelectionAssistantFollowToolbar()
this.isRemeberWinSize = configManager.getSelectionAssistantRemeberWinSize() this.isRemeberWinSize = configManager.getSelectionAssistantRemeberWinSize()
@@ -204,7 +207,7 @@ export class SelectionService {
* @param mode - The mode to set, either 'default', 'whitelist', or 'blacklist' * @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 * @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 if (!this.selectionHook) return
const modeMap = { const modeMap = {
@@ -213,6 +216,8 @@ export class SelectionService {
blacklist: SelectionHook!.FilterMode.EXCLUDE_LIST blacklist: SelectionHook!.FilterMode.EXCLUDE_LIST
} }
const predefinedBlacklist = isWin ? SELECTION_PREDEFINED_BLACKLIST.WINDOWS : SELECTION_PREDEFINED_BLACKLIST.MAC
let combinedList: string[] = list let combinedList: string[] = list
let combinedMode = mode let combinedMode = mode
@@ -221,7 +226,7 @@ export class SelectionService {
switch (mode) { switch (mode) {
case 'blacklist': case 'blacklist':
//combine the predefined blacklist with the user-defined blacklist //combine the predefined blacklist with the user-defined blacklist
combinedList = [...new Set([...list, ...SELECTION_PREDEFINED_BLACKLIST.WINDOWS])] combinedList = [...new Set([...list, ...predefinedBlacklist])]
break break
case 'whitelist': case 'whitelist':
combinedList = [...list] combinedList = [...list]
@@ -229,7 +234,7 @@ export class SelectionService {
case 'default': case 'default':
default: default:
//use the predefined blacklist as the default filter list //use the predefined blacklist as the default filter list
combinedList = [...SELECTION_PREDEFINED_BLACKLIST.WINDOWS] combinedList = [...predefinedBlacklist]
combinedMode = 'blacklist' combinedMode = 'blacklist'
break break
} }
@@ -240,17 +245,24 @@ export class SelectionService {
} }
} }
private setHookFineTunedList() { private setHookFineTunedList(): void {
if (!this.selectionHook) return if (!this.selectionHook) return
const excludeClipboardCursorDetectList = isWin
? SELECTION_FINETUNED_LIST.EXCLUDE_CLIPBOARD_CURSOR_DETECT.WINDOWS
: SELECTION_FINETUNED_LIST.EXCLUDE_CLIPBOARD_CURSOR_DETECT.MAC
const includeClipboardDelayReadList = isWin
? SELECTION_FINETUNED_LIST.INCLUDE_CLIPBOARD_DELAY_READ.WINDOWS
: SELECTION_FINETUNED_LIST.INCLUDE_CLIPBOARD_DELAY_READ.MAC
this.selectionHook.setFineTunedList( this.selectionHook.setFineTunedList(
SelectionHook!.FineTunedListType.EXCLUDE_CLIPBOARD_CURSOR_DETECT, SelectionHook!.FineTunedListType.EXCLUDE_CLIPBOARD_CURSOR_DETECT,
SELECTION_FINETUNED_LIST.EXCLUDE_CLIPBOARD_CURSOR_DETECT.WINDOWS excludeClipboardCursorDetectList
) )
this.selectionHook.setFineTunedList( this.selectionHook.setFineTunedList(
SelectionHook!.FineTunedListType.INCLUDE_CLIPBOARD_DELAY_READ, SelectionHook!.FineTunedListType.INCLUDE_CLIPBOARD_DELAY_READ,
SELECTION_FINETUNED_LIST.INCLUDE_CLIPBOARD_DELAY_READ.WINDOWS includeClipboardDelayReadList
) )
} }
@@ -259,11 +271,33 @@ export class SelectionService {
* @returns {boolean} Success status of service start * @returns {boolean} Success status of service start
*/ */
public start(): boolean { public start(): boolean {
if (!this.selectionHook || this.started) { if (!isSupportedOS) {
this.logError(new Error('SelectionService start(): instance is null or already started')) this.logError(new Error('SelectionService start(): not supported on this OS'))
return false return false
} }
if (!this.selectionHook) {
this.logError(new Error('SelectionService start(): instance is null'))
return false
}
if (this.started) {
this.logError(new Error('SelectionService start(): already started'))
return false
}
//On macOS, we need to check if the process is trusted
if (isMac) {
if (!systemPreferences.isTrustedAccessibilityClient(false)) {
this.logError(
new Error(
'SelectionSerice not started: process is not trusted on macOS, please turn on the Accessibility permission'
)
)
return false
}
}
try { try {
//make sure the toolbar window is ready //make sure the toolbar window is ready
this.createToolbarWindow() this.createToolbarWindow()
@@ -285,7 +319,7 @@ export class SelectionService {
this.processTriggerMode() this.processTriggerMode()
this.started = true this.started = true
this.logInfo('SelectionService Started') this.logInfo('SelectionService Started', true)
return true return true
} }
@@ -306,6 +340,7 @@ export class SelectionService {
if (!this.selectionHook) return false if (!this.selectionHook) return false
this.selectionHook.stop() this.selectionHook.stop()
this.selectionHook.cleanup() //already remove all listeners this.selectionHook.cleanup() //already remove all listeners
//reset the listener states //reset the listener states
@@ -316,10 +351,11 @@ export class SelectionService {
this.toolbarWindow.close() this.toolbarWindow.close()
this.toolbarWindow = null this.toolbarWindow = null
} }
this.closePreloadedActionWindows() this.closePreloadedActionWindows()
this.started = false this.started = false
this.logInfo('SelectionService Stopped') this.logInfo('SelectionService Stopped', true)
return true return true
} }
@@ -335,14 +371,14 @@ export class SelectionService {
this.selectionHook = null this.selectionHook = null
this.initStatus = false this.initStatus = false
SelectionService.instance = null SelectionService.instance = null
this.logInfo('SelectionService Quitted') this.logInfo('SelectionService Quitted', true)
} }
/** /**
* Toggle the enabled state of the selection service * Toggle the enabled state of the selection service
* Will sync the new enabled store to all renderer windows * 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 if (!this.selectionHook) return
const newEnabled = enabled === undefined ? !configManager.getSelectionAssistantEnabled() : enabled const newEnabled = enabled === undefined ? !configManager.getSelectionAssistantEnabled() : enabled
@@ -358,7 +394,7 @@ export class SelectionService {
* Sets up window properties, event handlers, and loads the toolbar UI * Sets up window properties, event handlers, and loads the toolbar UI
* @param readyCallback Optional callback when window is ready to show * @param readyCallback Optional callback when window is ready to show
*/ */
private createToolbarWindow(readyCallback?: () => void) { private createToolbarWindow(readyCallback?: () => void): void {
if (this.isToolbarAlive()) return if (this.isToolbarAlive()) return
const { toolbarWidth, toolbarHeight } = this.getToolbarRealSize() const { toolbarWidth, toolbarHeight } = this.getToolbarRealSize()
@@ -366,21 +402,31 @@ export class SelectionService {
this.toolbarWindow = new BrowserWindow({ this.toolbarWindow = new BrowserWindow({
width: toolbarWidth, width: toolbarWidth,
height: toolbarHeight, height: toolbarHeight,
show: false,
frame: false, frame: false,
transparent: true, transparent: true,
alwaysOnTop: true, alwaysOnTop: true,
skipTaskbar: true, skipTaskbar: true,
autoHideMenuBar: true,
resizable: false, resizable: false,
minimizable: false, minimizable: false,
maximizable: false, maximizable: false,
fullscreenable: false, // [macOS] must be false
movable: true, movable: true,
focusable: false,
hasShadow: false, hasShadow: false,
thickFrame: false, thickFrame: false,
roundedCorners: true, roundedCorners: true,
backgroundMaterial: 'none', backgroundMaterial: 'none',
type: 'toolbar',
show: false, // Platform specific settings
// [macOS] DO NOT set focusable to false, it will make other windows bring to front together
// [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]
webPreferences: { webPreferences: {
preload: join(__dirname, '../preload/index.js'), preload: join(__dirname, '../preload/index.js'),
contextIsolation: true, contextIsolation: true,
@@ -392,7 +438,9 @@ export class SelectionService {
// Hide when losing focus // Hide when losing focus
this.toolbarWindow.on('blur', () => { this.toolbarWindow.on('blur', () => {
this.hideToolbar() if (this.toolbarWindow!.isVisible()) {
this.hideToolbar()
}
}) })
// Clean up when closed // Clean up when closed
@@ -437,10 +485,10 @@ export class SelectionService {
* @param point Reference point for positioning, logical coordinates * @param point Reference point for positioning, logical coordinates
* @param orientation Preferred position relative to reference point * @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()) { if (!this.isToolbarAlive()) {
this.createToolbarWindow(() => { this.createToolbarWindow(() => {
this.showToolbarAtPosition(point, orientation) this.showToolbarAtPosition(point, orientation, programName)
}) })
return return
} }
@@ -456,9 +504,60 @@ export class SelectionService {
x: posX, x: posX,
y: posY y: posY
}) })
this.toolbarWindow!.show()
this.toolbarWindow!.setOpacity(1) //set the window to always on top (highest level)
//should set every time the window is shown
this.toolbarWindow!.setAlwaysOnTop(true, 'screen-saver')
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()
// [macOS] restore the focusable status
this.toolbarWindow!.setFocusable(true)
this.startHideByMouseKeyListener() this.startHideByMouseKeyListener()
return
} }
/** /**
@@ -467,18 +566,60 @@ export class SelectionService {
public hideToolbar(): void { public hideToolbar(): void {
if (!this.isToolbarAlive()) return if (!this.isToolbarAlive()) return
this.toolbarWindow!.setOpacity(0) this.stopHideByMouseKeyListener()
// [Windows] just hide the toolbar window is enough
if (!isMac) {
this.toolbarWindow!.hide()
return
}
/************************************************
* [macOS] the following code is only for macOS
*************************************************/
// [macOS] a HACKY way
// make sure other windows do not bring to front when toolbar is hidden
// get all focusable windows and set them to not focusable
const focusableWindows: BrowserWindow[] = []
for (const window of BrowserWindow.getAllWindows()) {
if (!window.isDestroyed() && window.isVisible()) {
if (window.isFocusable()) {
focusableWindows.push(window)
window.setFocusable(false)
}
}
}
this.toolbarWindow!.hide() this.toolbarWindow!.hide()
this.stopHideByMouseKeyListener() // set them back to focusable after 50ms
setTimeout(() => {
for (const window of focusableWindows) {
if (!window.isDestroyed()) {
window.setFocusable(true)
}
}
}, 50)
// [macOS] hacky way
// Because toolbar is not a FOCUSED window, so the hover status will remain when next time show
// so we just send mouseMove event to the toolbar window to make the hover status disappear
this.toolbarWindow!.webContents.sendInputEvent({
type: 'mouseMove',
x: -1,
y: -1
})
return
} }
/** /**
* Check if toolbar window exists and is not destroyed * Check if toolbar window exists and is not destroyed
* @returns {boolean} Toolbar window status * @returns {boolean} Toolbar window status
*/ */
private isToolbarAlive() { private isToolbarAlive(): boolean {
return this.toolbarWindow && !this.toolbarWindow.isDestroyed() return !!(this.toolbarWindow && !this.toolbarWindow.isDestroyed())
} }
/** /**
@@ -487,7 +628,7 @@ export class SelectionService {
* @param width New toolbar width * @param width New toolbar width
* @param height New toolbar height * @param height New toolbar height
*/ */
public determineToolbarSize(width: number, height: number) { public determineToolbarSize(width: number, height: number): void {
const toolbarWidth = Math.ceil(width) const toolbarWidth = Math.ceil(width)
// only update toolbar width if it's changed // only update toolbar width if it's changed
@@ -500,7 +641,7 @@ export class SelectionService {
* Get actual toolbar dimensions accounting for zoom factor * Get actual toolbar dimensions accounting for zoom factor
* @returns Object containing toolbar width and height * @returns Object containing toolbar width and height
*/ */
private getToolbarRealSize() { private getToolbarRealSize(): { toolbarWidth: number; toolbarHeight: number } {
return { return {
toolbarWidth: this.TOOLBAR_WIDTH * this.zoomFactor, toolbarWidth: this.TOOLBAR_WIDTH * this.zoomFactor,
toolbarHeight: this.TOOLBAR_HEIGHT * this.zoomFactor toolbarHeight: this.TOOLBAR_HEIGHT * this.zoomFactor
@@ -510,71 +651,71 @@ export class SelectionService {
/** /**
* Calculate optimal toolbar position based on selection context * Calculate optimal toolbar position based on selection context
* Ensures toolbar stays within screen boundaries and follows selection direction * Ensures toolbar stays within screen boundaries and follows selection direction
* @param point Reference point for positioning, must be INTEGER * @param refPoint Reference point for positioning, must be INTEGER
* @param orientation Preferred position relative to reference point * @param orientation Preferred position relative to reference point
* @returns Calculated screen coordinates for toolbar, INTEGER * @returns Calculated screen coordinates for toolbar, INTEGER
*/ */
private calculateToolbarPosition(point: Point, orientation: RelativeOrientation): Point { private calculateToolbarPosition(refPoint: Point, orientation: RelativeOrientation): Point {
// Calculate initial position based on the specified anchor // Calculate initial position based on the specified anchor
let posX: number, posY: number const posPoint: Point = { x: 0, y: 0 }
const { toolbarWidth, toolbarHeight } = this.getToolbarRealSize() const { toolbarWidth, toolbarHeight } = this.getToolbarRealSize()
switch (orientation) { switch (orientation) {
case 'topLeft': case 'topLeft':
posX = point.x - toolbarWidth posPoint.x = refPoint.x - toolbarWidth
posY = point.y - toolbarHeight posPoint.y = refPoint.y - toolbarHeight
break break
case 'topRight': case 'topRight':
posX = point.x posPoint.x = refPoint.x
posY = point.y - toolbarHeight posPoint.y = refPoint.y - toolbarHeight
break break
case 'topMiddle': case 'topMiddle':
posX = point.x - toolbarWidth / 2 posPoint.x = refPoint.x - toolbarWidth / 2
posY = point.y - toolbarHeight posPoint.y = refPoint.y - toolbarHeight
break break
case 'bottomLeft': case 'bottomLeft':
posX = point.x - toolbarWidth posPoint.x = refPoint.x - toolbarWidth
posY = point.y posPoint.y = refPoint.y
break break
case 'bottomRight': case 'bottomRight':
posX = point.x posPoint.x = refPoint.x
posY = point.y posPoint.y = refPoint.y
break break
case 'bottomMiddle': case 'bottomMiddle':
posX = point.x - toolbarWidth / 2 posPoint.x = refPoint.x - toolbarWidth / 2
posY = point.y posPoint.y = refPoint.y
break break
case 'middleLeft': case 'middleLeft':
posX = point.x - toolbarWidth posPoint.x = refPoint.x - toolbarWidth
posY = point.y - toolbarHeight / 2 posPoint.y = refPoint.y - toolbarHeight / 2
break break
case 'middleRight': case 'middleRight':
posX = point.x posPoint.x = refPoint.x
posY = point.y - toolbarHeight / 2 posPoint.y = refPoint.y - toolbarHeight / 2
break break
case 'center': case 'center':
posX = point.x - toolbarWidth / 2 posPoint.x = refPoint.x - toolbarWidth / 2
posY = point.y - toolbarHeight / 2 posPoint.y = refPoint.y - toolbarHeight / 2
break break
default: default:
// Default to 'topMiddle' if invalid position // Default to 'topMiddle' if invalid position
posX = point.x - toolbarWidth / 2 posPoint.x = refPoint.x - toolbarWidth / 2
posY = point.y - toolbarHeight / 2 posPoint.y = refPoint.y - toolbarHeight / 2
} }
//use original point to get the display //use original point to get the display
const display = screen.getDisplayNearestPoint({ x: point.x, y: point.y }) const display = screen.getDisplayNearestPoint(refPoint)
// Ensure toolbar stays within screen boundaries // Ensure toolbar stays within screen boundaries
posX = Math.round( posPoint.x = Math.round(
Math.max(display.workArea.x, Math.min(posX, display.workArea.x + display.workArea.width - toolbarWidth)) Math.max(display.workArea.x, Math.min(posPoint.x, display.workArea.x + display.workArea.width - toolbarWidth))
) )
posY = Math.round( posPoint.y = Math.round(
Math.max(display.workArea.y, Math.min(posY, display.workArea.y + display.workArea.height - toolbarHeight)) Math.max(display.workArea.y, Math.min(posPoint.y, display.workArea.y + display.workArea.height - toolbarHeight))
) )
return { x: posX, y: posY } return posPoint
} }
private isSamePoint(point1: Point, point2: Point): boolean { private isSamePoint(point1: Point, point2: Point): boolean {
@@ -763,13 +904,17 @@ export class SelectionService {
} }
if (!isLogical) { if (!isLogical) {
// [macOS] don't need to convert by screenToDipPoint
if (!isMac) {
refPoint = screen.screenToDipPoint(refPoint)
}
//screenToDipPoint can be float, so we need to round it //screenToDipPoint can be float, so we need to round it
refPoint = screen.screenToDipPoint(refPoint)
refPoint = { x: Math.round(refPoint.x), y: Math.round(refPoint.y) } refPoint = { x: Math.round(refPoint.x), y: Math.round(refPoint.y) }
} }
this.showToolbarAtPosition(refPoint, refOrientation) // [macOS] isFullscreen is only available on macOS
this.toolbarWindow?.webContents.send(IpcChannel.Selection_TextSelected, selectionData) this.showToolbarAtPosition(refPoint, refOrientation, selectionData.programName)
this.toolbarWindow!.webContents.send(IpcChannel.Selection_TextSelected, selectionData)
} }
/** /**
@@ -777,7 +922,7 @@ export class SelectionService {
*/ */
// Start monitoring global mouse clicks // Start monitoring global mouse clicks
private startHideByMouseKeyListener() { private startHideByMouseKeyListener(): void {
try { try {
// Register event handlers // Register event handlers
this.selectionHook!.on('mouse-down', this.handleMouseDownHide) this.selectionHook!.on('mouse-down', this.handleMouseDownHide)
@@ -790,7 +935,7 @@ export class SelectionService {
} }
// Stop monitoring global mouse clicks // Stop monitoring global mouse clicks
private stopHideByMouseKeyListener() { private stopHideByMouseKeyListener(): void {
if (!this.isHideByMouseKeyListenerActive) return if (!this.isHideByMouseKeyListenerActive) return
try { try {
@@ -822,8 +967,8 @@ export class SelectionService {
return return
} }
//data point is physical coordinates, convert to logical coordinates //data point is physical coordinates, convert to logical coordinates(only for windows/linux)
const mousePoint = screen.screenToDipPoint({ x: data.x, y: data.y }) const mousePoint = isMac ? { x: data.x, y: data.y } : screen.screenToDipPoint({ x: data.x, y: data.y })
const bounds = this.toolbarWindow!.getBounds() const bounds = this.toolbarWindow!.getBounds()
@@ -956,7 +1101,8 @@ export class SelectionService {
frame: false, frame: false,
transparent: true, transparent: true,
autoHideMenuBar: true, autoHideMenuBar: true,
titleBarStyle: 'hidden', titleBarStyle: 'hidden', // [macOS]
trafficLightPosition: { x: 12, y: 9 }, // [macOS]
hasShadow: false, hasShadow: false,
thickFrame: false, thickFrame: false,
show: false, show: false,
@@ -983,7 +1129,7 @@ export class SelectionService {
* Initialize preloaded action windows * Initialize preloaded action windows
* Creates a pool of windows at startup for faster response * Creates a pool of windows at startup for faster response
*/ */
private async initPreloadedActionWindows() { private async initPreloadedActionWindows(): Promise<void> {
try { try {
// Create initial pool of preloaded windows // Create initial pool of preloaded windows
for (let i = 0; i < this.PRELOAD_ACTION_WINDOW_COUNT; i++) { for (let i = 0; i < this.PRELOAD_ACTION_WINDOW_COUNT; i++) {
@@ -997,7 +1143,7 @@ export class SelectionService {
/** /**
* Close all preloaded action windows * Close all preloaded action windows
*/ */
private closePreloadedActionWindows() { private closePreloadedActionWindows(): void {
for (const actionWindow of this.preloadedActionWindows) { for (const actionWindow of this.preloadedActionWindows) {
if (!actionWindow.isDestroyed()) { if (!actionWindow.isDestroyed()) {
actionWindow.destroy() actionWindow.destroy()
@@ -1009,7 +1155,7 @@ export class SelectionService {
* Preload a new action window asynchronously * Preload a new action window asynchronously
* This method is called after popping a window to ensure we always have windows ready * This method is called after popping a window to ensure we always have windows ready
*/ */
private async pushNewActionWindow() { private async pushNewActionWindow(): Promise<void> {
try { try {
const actionWindow = this.createPreloadedActionWindow() const actionWindow = this.createPreloadedActionWindow()
this.preloadedActionWindows.push(actionWindow) this.preloadedActionWindows.push(actionWindow)
@@ -1023,7 +1169,7 @@ export class SelectionService {
* Immediately returns a window and asynchronously creates a new one * Immediately returns a window and asynchronously creates a new one
* @returns {BrowserWindow} The action window * @returns {BrowserWindow} The action window
*/ */
private popActionWindow() { private popActionWindow(): BrowserWindow {
// Get a window from the preloaded queue or create a new one if empty // Get a window from the preloaded queue or create a new one if empty
const actionWindow = this.preloadedActionWindows.pop() || this.createPreloadedActionWindow() const actionWindow = this.preloadedActionWindows.pop() || this.createPreloadedActionWindow()
@@ -1033,6 +1179,27 @@ export class SelectionService {
if (!actionWindow.isDestroyed()) { if (!actionWindow.isDestroyed()) {
actionWindow.destroy() actionWindow.destroy()
} }
// [macOS] a HACKY way
// make sure other windows do not bring to front when action window is closed
if (isMac) {
const focusableWindows: BrowserWindow[] = []
for (const window of BrowserWindow.getAllWindows()) {
if (!window.isDestroyed() && window.isVisible()) {
if (window.isFocusable()) {
focusableWindows.push(window)
window.setFocusable(false)
}
}
}
setTimeout(() => {
for (const window of focusableWindows) {
if (!window.isDestroyed()) {
window.setFocusable(true)
}
}
}, 50)
}
}) })
//remember the action window size //remember the action window size
@@ -1053,20 +1220,26 @@ export class SelectionService {
return actionWindow 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() const actionWindow = this.popActionWindow()
actionWindow.webContents.send(IpcChannel.Selection_UpdateActionData, actionItem) actionWindow.webContents.send(IpcChannel.Selection_UpdateActionData, actionItem)
this.showActionWindow(actionWindow) this.showActionWindow(actionWindow, isFullScreen)
} }
/** /**
* Show action window with proper positioning relative to toolbar * Show action window with proper positioning relative to toolbar
* Ensures window stays within screen boundaries * Ensures window stays within screen boundaries
* @param actionWindow Window to position and show * @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 actionWindowWidth = this.ACTION_WINDOW_WIDTH
let actionWindowHeight = this.ACTION_WINDOW_HEIGHT let actionWindowHeight = this.ACTION_WINDOW_HEIGHT
@@ -1076,63 +1249,124 @@ export class SelectionService {
actionWindowHeight = this.lastActionWindowSize.height actionWindowHeight = this.lastActionWindowSize.height
} }
//center way /********************************************
* 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) { if (!this.isFollowToolbar || !this.toolbarWindow) {
if (this.isRemeberWinSize) { const centerX = workArea.x + (workArea.width - actionWindowWidth) / 2
actionWindow.setBounds({ const centerY = workArea.y + (workArea.height - actionWindowHeight) / 2
width: actionWindowWidth,
height: actionWindowHeight actionWindow.setBounds({
}) width: actionWindowWidth,
height: actionWindowHeight,
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() actionWindow.show()
this.hideToolbar()
return return
} }
//follow toolbar /************************************************
* [macOS] the following code is only for macOS
*
* WARNING:
* DO NOT MODIFY THESE CODES, UNLESS YOU REALLY KNOW WHAT YOU ARE DOING!!!!
*************************************************/
const toolbarBounds = this.toolbarWindow!.getBounds() // act normally when the app is not in fullscreen mode
const display = screen.getDisplayNearestPoint({ x: toolbarBounds.x, y: toolbarBounds.y }) if (!isFullScreen) {
const workArea = display.workArea actionWindow.show()
const GAP = 6 // 6px gap from screen edges return
//make sure action window is inside screen
if (actionWindowWidth > workArea.width - 2 * GAP) {
actionWindowWidth = workArea.width - 2 * GAP
} }
if (actionWindowHeight > workArea.height - 2 * GAP) { // [macOS] an UGLY HACKY way for fullscreen override settings
actionWindowHeight = workArea.height - 2 * GAP
}
// Calculate initial position to center action window horizontally below toolbar // FIXME sometimes the dock will be shown when the action window is shown
let posX = Math.round(toolbarBounds.x + (toolbarBounds.width - actionWindowWidth) / 2) // FIXME if actionWindow show on the fullscreen app, switch to other space will cause the mainWindow to be shown
let posY = Math.round(toolbarBounds.y) // 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 // setFocusable(false) to prevent the action window hide when blur (if auto hide on blur is enabled)
if (posX + actionWindowWidth > workArea.x + workArea.width) { actionWindow.setFocusable(false)
posX = workArea.x + workArea.width - actionWindowWidth - GAP actionWindow.setAlwaysOnTop(true, 'floating')
} 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) // `setVisibleOnAllWorkspaces(true)` will cause the dock icon disappeared
//KEY to make window not resize // just store the dock icon status, and show it again
actionWindow.setBounds({ const isDockShown = app.dock?.isVisible()
width: actionWindowWidth,
height: actionWindowHeight, // DO NOT set `skipTransformProcessType: true`,
x: posX, // it will cause the action window to be shown on other space
y: posY 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 { public closeActionWindow(actionWindow: BrowserWindow): void {
@@ -1152,38 +1386,40 @@ export class SelectionService {
* Switches between selection-based and alt-key based triggering * Switches between selection-based and alt-key based triggering
* Manages appropriate event listeners for each mode * Manages appropriate event listeners for each mode
*/ */
private processTriggerMode() { private processTriggerMode(): void {
if (!this.selectionHook) return
switch (this.triggerMode) { switch (this.triggerMode) {
case TriggerMode.Selected: case TriggerMode.Selected:
if (this.isCtrlkeyListenerActive) { if (this.isCtrlkeyListenerActive) {
this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode) this.selectionHook.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode) this.selectionHook.off('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = false this.isCtrlkeyListenerActive = false
} }
this.selectionHook!.setSelectionPassiveMode(false) this.selectionHook.setSelectionPassiveMode(false)
break break
case TriggerMode.Ctrlkey: case TriggerMode.Ctrlkey:
if (!this.isCtrlkeyListenerActive) { if (!this.isCtrlkeyListenerActive) {
this.selectionHook!.on('key-down', this.handleKeyDownCtrlkeyMode) this.selectionHook.on('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.on('key-up', this.handleKeyUpCtrlkeyMode) this.selectionHook.on('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = true this.isCtrlkeyListenerActive = true
} }
this.selectionHook!.setSelectionPassiveMode(true) this.selectionHook.setSelectionPassiveMode(true)
break break
case TriggerMode.Shortcut: case TriggerMode.Shortcut:
//remove the ctrlkey listener, don't need any key listener for shortcut mode //remove the ctrlkey listener, don't need any key listener for shortcut mode
if (this.isCtrlkeyListenerActive) { if (this.isCtrlkeyListenerActive) {
this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode) this.selectionHook.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode) this.selectionHook.off('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = false this.isCtrlkeyListenerActive = false
} }
this.selectionHook!.setSelectionPassiveMode(true) this.selectionHook.setSelectionPassiveMode(true)
break break
} }
} }
@@ -1204,7 +1440,7 @@ export class SelectionService {
selectionService?.hideToolbar() selectionService?.hideToolbar()
}) })
ipcMain.handle(IpcChannel.Selection_WriteToClipboard, (_, text: string) => { ipcMain.handle(IpcChannel.Selection_WriteToClipboard, (_, text: string): boolean => {
return selectionService?.writeToClipboard(text) ?? false return selectionService?.writeToClipboard(text) ?? false
}) })
@@ -1236,8 +1472,9 @@ export class SelectionService {
configManager.setSelectionAssistantFilterList(filterList) configManager.setSelectionAssistantFilterList(filterList)
}) })
ipcMain.handle(IpcChannel.Selection_ProcessAction, (_, actionItem: ActionItem) => { // [macOS] only macOS has the available isFullscreen mode
selectionService?.processAction(actionItem) ipcMain.handle(IpcChannel.Selection_ProcessAction, (_, actionItem: ActionItem, isFullScreen: boolean = false) => {
selectionService?.processAction(actionItem, isFullScreen)
}) })
ipcMain.handle(IpcChannel.Selection_ActionWindowClose, (event) => { ipcMain.handle(IpcChannel.Selection_ActionWindowClose, (event) => {
@@ -1264,11 +1501,13 @@ export class SelectionService {
this.isIpcHandlerRegistered = true this.isIpcHandlerRegistered = true
} }
private logInfo(message: string) { private logInfo(message: string, forceShow: boolean = false): void {
isDev && Logger.info('[SelectionService] Info: ', message) if (isDev || forceShow) {
Logger.info('[SelectionService] Info: ', message)
}
} }
private logError(...args: [...string[], Error]) { private logError(...args: [...string[], Error]): void {
Logger.error('[SelectionService] Error: ', ...args) Logger.error('[SelectionService] Error: ', ...args)
} }
} }
@@ -1279,9 +1518,9 @@ export class SelectionService {
* @returns {boolean} Success status of initialization * @returns {boolean} Success status of initialization
*/ */
export function initSelectionService(): boolean { export function initSelectionService(): boolean {
if (!isWin) return false if (!isSupportedOS) return false
configManager.subscribe(ConfigKeys.SelectionAssistantEnabled, (enabled: boolean) => { configManager.subscribe(ConfigKeys.SelectionAssistantEnabled, (enabled: boolean): void => {
//avoid closure //avoid closure
const ss = SelectionService.getInstance() const ss = SelectionService.getInstance()
if (!ss) { if (!ss) {

View File

@@ -55,7 +55,8 @@ function formatShortcutKey(shortcut: string[]): string {
return shortcut.join('+') return shortcut.join('+')
} }
// convert the shortcut recorded by keyboard event key value to electron global shortcut format // convert the shortcut recorded by JS keyboard event key value to electron global shortcut format
// see: https://www.electronjs.org/zh/docs/latest/api/accelerator
const convertShortcutFormat = (shortcut: string | string[]): string => { const convertShortcutFormat = (shortcut: string | string[]): string => {
const accelerator = (() => { const accelerator = (() => {
if (Array.isArray(shortcut)) { if (Array.isArray(shortcut)) {
@@ -68,12 +69,34 @@ const convertShortcutFormat = (shortcut: string | string[]): string => {
return accelerator return accelerator
.map((key) => { .map((key) => {
switch (key) { switch (key) {
// OLD WAY FOR MODIFIER KEYS, KEEP THEM HERE FOR REFERENCE
// case 'Command':
// return 'CommandOrControl'
// case 'Control':
// return 'Control'
// case 'Ctrl':
// return 'Control'
// NEW WAY FOR MODIFIER KEYS
// you can see all the modifier keys in the same
case 'CommandOrControl':
return 'CommandOrControl'
case 'Ctrl':
return 'Ctrl'
case 'Alt':
return 'Alt' // Use `Alt` instead of `Option`. The `Option` key only exists on macOS, whereas the `Alt` key is available on all platforms.
case 'Meta':
return 'Meta' // `Meta` key is mapped to the Windows key on Windows and Linux, `Cmd` on macOS.
case 'Shift':
return 'Shift'
// For backward compatibility with old data
case 'Command': case 'Command':
case 'Cmd':
return 'CommandOrControl' return 'CommandOrControl'
case 'Control': case 'Control':
return 'Control' return 'Ctrl'
case 'Ctrl':
return 'Control'
case 'ArrowUp': case 'ArrowUp':
return 'Up' return 'Up'
case 'ArrowDown': case 'ArrowDown':
@@ -83,7 +106,7 @@ const convertShortcutFormat = (shortcut: string | string[]): string => {
case 'ArrowRight': case 'ArrowRight':
return 'Right' return 'Right'
case 'AltGraph': case 'AltGraph':
return 'Alt' return 'AltGr'
case 'Slash': case 'Slash':
return '/' return '/'
case 'Semicolon': case 'Semicolon':

View File

@@ -1,4 +1,4 @@
import { isMac } from '@main/constant' import { isLinux, isMac, isWin } from '@main/constant'
import { locales } from '@main/utils/locales' import { locales } from '@main/utils/locales'
import { app, Menu, MenuItemConstructorOptions, nativeImage, nativeTheme, Tray } from 'electron' import { app, Menu, MenuItemConstructorOptions, nativeImage, nativeTheme, Tray } from 'electron'
@@ -6,6 +6,7 @@ import icon from '../../../build/tray_icon.png?asset'
import iconDark from '../../../build/tray_icon_dark.png?asset' import iconDark from '../../../build/tray_icon_dark.png?asset'
import iconLight from '../../../build/tray_icon_light.png?asset' import iconLight from '../../../build/tray_icon_light.png?asset'
import { ConfigKeys, configManager } from './ConfigManager' import { ConfigKeys, configManager } from './ConfigManager'
import selectionService from './SelectionService'
import { windowService } from './WindowService' import { windowService } from './WindowService'
export class TrayService { export class TrayService {
@@ -29,14 +30,14 @@ export class TrayService {
const iconPath = isMac ? (nativeTheme.shouldUseDarkColors ? iconLight : iconDark) : icon const iconPath = isMac ? (nativeTheme.shouldUseDarkColors ? iconLight : iconDark) : icon
const tray = new Tray(iconPath) const tray = new Tray(iconPath)
if (process.platform === 'win32') { if (isWin) {
tray.setImage(iconPath) tray.setImage(iconPath)
} else if (process.platform === 'darwin') { } else if (isMac) {
const image = nativeImage.createFromPath(iconPath) const image = nativeImage.createFromPath(iconPath)
const resizedImage = image.resize({ width: 16, height: 16 }) const resizedImage = image.resize({ width: 16, height: 16 })
resizedImage.setTemplateImage(true) resizedImage.setTemplateImage(true)
tray.setImage(resizedImage) tray.setImage(resizedImage)
} else if (process.platform === 'linux') { } else if (isLinux) {
const image = nativeImage.createFromPath(iconPath) const image = nativeImage.createFromPath(iconPath)
const resizedImage = image.resize({ width: 16, height: 16 }) const resizedImage = image.resize({ width: 16, height: 16 })
tray.setImage(resizedImage) tray.setImage(resizedImage)
@@ -46,7 +47,7 @@ export class TrayService {
this.updateContextMenu() this.updateContextMenu()
if (process.platform === 'linux') { if (isLinux) {
this.tray.setContextMenu(this.contextMenu) this.tray.setContextMenu(this.contextMenu)
} }
@@ -69,19 +70,29 @@ export class TrayService {
private updateContextMenu() { private updateContextMenu() {
const locale = locales[configManager.getLanguage()] const locale = locales[configManager.getLanguage()]
const { tray: trayLocale } = locale.translation const { tray: trayLocale, selection: selectionLocale } = locale.translation
const enableQuickAssistant = configManager.getEnableQuickAssistant() const quickAssistantEnabled = configManager.getEnableQuickAssistant()
const selectionAssistantEnabled = configManager.getSelectionAssistantEnabled()
const template = [ const template = [
{ {
label: trayLocale.show_window, label: trayLocale.show_window,
click: () => windowService.showMainWindow() click: () => windowService.showMainWindow()
}, },
enableQuickAssistant && { quickAssistantEnabled && {
label: trayLocale.show_mini_window, label: trayLocale.show_mini_window,
click: () => windowService.showMiniWindow() click: () => windowService.showMiniWindow()
}, },
(isWin || isMac) && {
label: selectionLocale.name + (selectionAssistantEnabled ? ' - On' : ' - Off'),
click: () => {
if (selectionService) {
selectionService.toggleEnabled()
this.updateContextMenu()
}
}
},
{ type: 'separator' }, { type: 'separator' },
{ {
label: trayLocale.quit, label: trayLocale.quit,
@@ -118,6 +129,10 @@ export class TrayService {
configManager.subscribe(ConfigKeys.EnableQuickAssistant, () => { configManager.subscribe(ConfigKeys.EnableQuickAssistant, () => {
this.updateContextMenu() this.updateContextMenu()
}) })
configManager.subscribe(ConfigKeys.SelectionAssistantEnabled, () => {
this.updateContextMenu()
})
} }
private quit() { private quit() {

View File

@@ -0,0 +1,142 @@
import { GoogleAuth } from 'google-auth-library'
interface ServiceAccountCredentials {
privateKey: string
clientEmail: string
}
interface VertexAIAuthParams {
projectId: string
serviceAccount?: ServiceAccountCredentials
}
const REQUIRED_VERTEX_AI_SCOPE = 'https://www.googleapis.com/auth/cloud-platform'
class VertexAIService {
private static instance: VertexAIService
private authClients: Map<string, GoogleAuth> = new Map()
static getInstance(): VertexAIService {
if (!VertexAIService.instance) {
VertexAIService.instance = new VertexAIService()
}
return VertexAIService.instance
}
/**
* 格式化私钥确保它包含正确的PEM头部和尾部
*/
private formatPrivateKey(privateKey: string): string {
if (!privateKey || typeof privateKey !== 'string') {
throw new Error('Private key must be a non-empty string')
}
// 处理JSON字符串中的转义换行符
let key = privateKey.replace(/\\n/g, '\n')
// 如果已经是正确格式的PEM直接返回
if (key.includes('-----BEGIN PRIVATE KEY-----') && key.includes('-----END PRIVATE KEY-----')) {
return key
}
// 移除所有换行符和空白字符(为了重新格式化)
key = key.replace(/\s+/g, '')
// 移除可能存在的头部和尾部
key = key.replace(/-----BEGIN[^-]*-----/g, '')
key = key.replace(/-----END[^-]*-----/g, '')
// 确保私钥不为空
if (!key) {
throw new Error('Private key is empty after formatting')
}
// 添加正确的PEM头部和尾部并格式化为64字符一行
const formattedKey = key.match(/.{1,64}/g)?.join('\n') || key
return `-----BEGIN PRIVATE KEY-----\n${formattedKey}\n-----END PRIVATE KEY-----`
}
/**
* 获取认证头用于 Vertex AI 请求
*/
async getAuthHeaders(params: VertexAIAuthParams): Promise<Record<string, string>> {
const { projectId, serviceAccount } = params
if (!serviceAccount?.privateKey || !serviceAccount?.clientEmail) {
throw new Error('Service account credentials are required')
}
// 创建缓存键
const cacheKey = `${projectId}-${serviceAccount.clientEmail}`
// 检查是否已有客户端实例
let auth = this.authClients.get(cacheKey)
if (!auth) {
try {
// 格式化私钥
const formattedPrivateKey = this.formatPrivateKey(serviceAccount.privateKey)
// 创建新的认证客户端
auth = new GoogleAuth({
credentials: {
private_key: formattedPrivateKey,
client_email: serviceAccount.clientEmail
},
projectId,
scopes: [REQUIRED_VERTEX_AI_SCOPE]
})
this.authClients.set(cacheKey, auth)
} catch (formatError: any) {
throw new Error(`Invalid private key format: ${formatError.message}`)
}
}
try {
// 获取认证头
const authHeaders = await auth.getRequestHeaders()
// 转换为普通对象
const headers: Record<string, string> = {}
for (const [key, value] of Object.entries(authHeaders)) {
if (typeof value === 'string') {
headers[key] = value
}
}
return headers
} catch (error: any) {
// 如果认证失败,清除缓存的客户端
this.authClients.delete(cacheKey)
throw new Error(`Failed to authenticate with service account: ${error.message}`)
}
}
/**
* 清理指定项目的认证缓存
*/
clearAuthCache(projectId: string, clientEmail?: string): void {
if (clientEmail) {
const cacheKey = `${projectId}-${clientEmail}`
this.authClients.delete(cacheKey)
} else {
// 清理该项目的所有缓存
for (const [key] of this.authClients) {
if (key.startsWith(`${projectId}-`)) {
this.authClients.delete(key)
}
}
}
}
/**
* 清理所有认证缓存
*/
clearAllAuthCache(): void {
this.authClients.clear()
}
}
export default VertexAIService

View File

@@ -5,7 +5,7 @@ import { is } from '@electron-toolkit/utils'
import { isDev, isLinux, isMac, isWin } from '@main/constant' import { isDev, isLinux, isMac, isWin } from '@main/constant'
import { getFilesDir } from '@main/utils/file' import { getFilesDir } from '@main/utils/file'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { app, BrowserWindow, nativeTheme, shell } from 'electron' import { app, BrowserWindow, nativeTheme, screen, shell } from 'electron'
import Logger from 'electron-log' import Logger from 'electron-log'
import windowStateKeeper from 'electron-window-state' import windowStateKeeper from 'electron-window-state'
import { join } from 'path' import { join } from 'path'
@@ -16,6 +16,9 @@ import { configManager } from './ConfigManager'
import { contextMenu } from './ContextMenu' import { contextMenu } from './ContextMenu'
import { initSessionUserAgent } from './WebviewService' import { initSessionUserAgent } from './WebviewService'
const DEFAULT_MINIWINDOW_WIDTH = 550
const DEFAULT_MINIWINDOW_HEIGHT = 400
export class WindowService { export class WindowService {
private static instance: WindowService | null = null private static instance: WindowService | null = null
private mainWindow: BrowserWindow | null = null private mainWindow: BrowserWindow | null = null
@@ -26,6 +29,11 @@ export class WindowService {
private wasMainWindowFocused: boolean = false private wasMainWindowFocused: boolean = false
private lastRendererProcessCrashTime: number = 0 private lastRendererProcessCrashTime: number = 0
private miniWindowSize: { width: number; height: number } = {
width: DEFAULT_MINIWINDOW_WIDTH,
height: DEFAULT_MINIWINDOW_HEIGHT
}
public static getInstance(): WindowService { public static getInstance(): WindowService {
if (!WindowService.instance) { if (!WindowService.instance) {
WindowService.instance = new WindowService() WindowService.instance = new WindowService()
@@ -41,8 +49,8 @@ export class WindowService {
} }
const mainWindowState = windowStateKeeper({ const mainWindowState = windowStateKeeper({
defaultWidth: 1080, defaultWidth: 960,
defaultHeight: 670, defaultHeight: 600,
fullScreen: false, fullScreen: false,
maximize: false maximize: false
}) })
@@ -52,11 +60,11 @@ export class WindowService {
y: mainWindowState.y, y: mainWindowState.y,
width: mainWindowState.width, width: mainWindowState.width,
height: mainWindowState.height, height: mainWindowState.height,
minWidth: 1080, minWidth: 960,
minHeight: 600, minHeight: 600,
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
transparent: isMac, transparent: false,
vibrancy: 'sidebar', vibrancy: 'sidebar',
visualEffectState: 'active', visualEffectState: 'active',
titleBarStyle: 'hidden', titleBarStyle: 'hidden',
@@ -95,6 +103,7 @@ export class WindowService {
this.setupMaximize(mainWindow, mainWindowState.isMaximized) this.setupMaximize(mainWindow, mainWindowState.isMaximized)
this.setupContextMenu(mainWindow) this.setupContextMenu(mainWindow)
this.setupSpellCheck(mainWindow)
this.setupWindowEvents(mainWindow) this.setupWindowEvents(mainWindow)
this.setupWebContentsHandlers(mainWindow) this.setupWebContentsHandlers(mainWindow)
this.setupWindowLifecycleEvents(mainWindow) this.setupWindowLifecycleEvents(mainWindow)
@@ -102,6 +111,18 @@ export class WindowService {
this.loadMainWindowContent(mainWindow) this.loadMainWindowContent(mainWindow)
} }
private setupSpellCheck(mainWindow: BrowserWindow) {
const enableSpellCheck = configManager.get('enableSpellCheck', false)
if (enableSpellCheck) {
try {
const spellCheckLanguages = configManager.get('spellCheckLanguages', []) as string[]
spellCheckLanguages.length > 0 && mainWindow.webContents.session.setSpellCheckerLanguages(spellCheckLanguages)
} catch (error) {
Logger.error('Failed to set spell check languages:', error as Error)
}
}
}
private setupMainWindowMonitor(mainWindow: BrowserWindow) { private setupMainWindowMonitor(mainWindow: BrowserWindow) {
mainWindow.webContents.on('render-process-gone', (_, details) => { mainWindow.webContents.on('render-process-gone', (_, details) => {
Logger.error(`Renderer process crashed with: ${JSON.stringify(details)}`) Logger.error(`Renderer process crashed with: ${JSON.stringify(details)}`)
@@ -130,9 +151,10 @@ export class WindowService {
} }
private setupContextMenu(mainWindow: BrowserWindow) { private setupContextMenu(mainWindow: BrowserWindow) {
contextMenu.contextMenu(mainWindow) contextMenu.contextMenu(mainWindow.webContents)
app.on('browser-window-created', (_, win) => { // setup context menu for all webviews like miniapp
contextMenu.contextMenu(win) app.on('web-contents-created', (_, webContents) => {
contextMenu.contextMenu(webContents)
}) })
// Dangerous API // Dangerous API
@@ -412,8 +434,8 @@ export class WindowService {
public createMiniWindow(isPreload: boolean = false): BrowserWindow { public createMiniWindow(isPreload: boolean = false): BrowserWindow {
this.miniWindow = new BrowserWindow({ this.miniWindow = new BrowserWindow({
width: 550, width: this.miniWindowSize.width,
height: 400, height: this.miniWindowSize.height,
minWidth: 350, minWidth: 350,
minHeight: 380, minHeight: 380,
maxWidth: 1024, maxWidth: 1024,
@@ -423,13 +445,12 @@ export class WindowService {
transparent: isMac, transparent: isMac,
vibrancy: 'under-window', vibrancy: 'under-window',
visualEffectState: 'followWindow', visualEffectState: 'followWindow',
center: true,
frame: false, frame: false,
alwaysOnTop: true, alwaysOnTop: true,
resizable: true,
useContentSize: true, useContentSize: true,
...(isMac ? { type: 'panel' } : {}), ...(isMac ? { type: 'panel' } : {}),
skipTaskbar: true, skipTaskbar: true,
resizable: true,
minimizable: false, minimizable: false,
maximizable: false, maximizable: false,
fullscreenable: false, fullscreenable: false,
@@ -437,8 +458,7 @@ export class WindowService {
preload: join(__dirname, '../preload/index.js'), preload: join(__dirname, '../preload/index.js'),
sandbox: false, sandbox: false,
webSecurity: false, webSecurity: false,
webviewTag: true, webviewTag: true
backgroundThrottling: false
} }
}) })
@@ -472,6 +492,13 @@ export class WindowService {
this.miniWindow?.webContents.send(IpcChannel.HideMiniWindow) this.miniWindow?.webContents.send(IpcChannel.HideMiniWindow)
}) })
this.miniWindow.on('resized', () => {
this.miniWindowSize = this.miniWindow?.getBounds() || {
width: DEFAULT_MINIWINDOW_WIDTH,
height: DEFAULT_MINIWINDOW_HEIGHT
}
})
this.miniWindow.on('show', () => { this.miniWindow.on('show', () => {
this.miniWindow?.webContents.send(IpcChannel.ShowMiniWindow) this.miniWindow?.webContents.send(IpcChannel.ShowMiniWindow)
}) })
@@ -495,10 +522,48 @@ export class WindowService {
if (this.miniWindow && !this.miniWindow.isDestroyed()) { if (this.miniWindow && !this.miniWindow.isDestroyed()) {
this.wasMainWindowFocused = this.mainWindow?.isFocused() || false this.wasMainWindowFocused = this.mainWindow?.isFocused() || false
if (this.miniWindow.isMinimized()) { // [Windows] hacky fix
this.miniWindow.restore() // the window is minimized only when in Windows platform
// because it's a workround for Windows, see `hideMiniWindow()`
if (this.miniWindow?.isMinimized()) {
// don't let the window being seen before we finish adusting the position across screens
this.miniWindow?.setOpacity(0)
// DO NOT use `restore()` here, Electron has the bug with screens of different scale factor
// We have to use `show()` here, then set the position and bounds
this.miniWindow?.show()
} }
this.miniWindow.show()
const miniWindowBounds = this.miniWindow.getBounds()
// Check if miniWindow is on the same screen as mouse cursor
const cursorDisplay = screen.getDisplayNearestPoint(screen.getCursorScreenPoint())
const miniWindowDisplay = screen.getDisplayNearestPoint(miniWindowBounds)
// Show the miniWindow on the cursor's screen center
// If miniWindow is not on the same screen as cursor, move it to cursor's screen center
if (cursorDisplay.id !== miniWindowDisplay.id) {
const workArea = cursorDisplay.bounds
// use remembered size to avoid the bug of Electron with screens of different scale factor
const miniWindowWidth = this.miniWindowSize.width
const miniWindowHeight = this.miniWindowSize.height
// move to the center of the cursor's screen
const miniWindowX = Math.round(workArea.x + (workArea.width - miniWindowWidth) / 2)
const miniWindowY = Math.round(workArea.y + (workArea.height - miniWindowHeight) / 2)
this.miniWindow.setPosition(miniWindowX, miniWindowY, false)
this.miniWindow.setBounds({
x: miniWindowX,
y: miniWindowY,
width: miniWindowWidth,
height: miniWindowHeight
})
}
this.miniWindow?.setOpacity(1)
this.miniWindow?.show()
return return
} }
@@ -506,20 +571,26 @@ export class WindowService {
} }
public hideMiniWindow() { public hideMiniWindow() {
//hacky-fix:[mac/win] previous window(not self-app) should be focused again after miniWindow hide if (!this.miniWindow || this.miniWindow.isDestroyed()) {
return
}
//[macOs/Windows] hacky fix
// previous window(not self-app) should be focused again after miniWindow hide
// this workaround is to make previous window focused again after miniWindow hide
if (isWin) { if (isWin) {
this.miniWindow?.minimize() this.miniWindow.setOpacity(0) // don't show the minimizing animation
this.miniWindow?.hide() this.miniWindow.minimize()
return return
} else if (isMac) { } else if (isMac) {
this.miniWindow?.hide() this.miniWindow.hide()
if (!this.wasMainWindowFocused) { if (!this.wasMainWindowFocused) {
app.hide() app.hide()
} }
return return
} }
this.miniWindow?.hide() this.miniWindow.hide()
} }
public closeMiniWindow() { public closeMiniWindow() {

View File

@@ -0,0 +1,829 @@
import { Client, createClient } from '@libsql/client'
import Embeddings from '@main/knowledge/embeddings/Embeddings'
import type {
AddMemoryOptions,
AssistantMessage,
MemoryConfig,
MemoryHistoryItem,
MemoryItem,
MemoryListOptions,
MemorySearchOptions
} from '@types'
import crypto from 'crypto'
import { app } from 'electron'
import Logger from 'electron-log'
import path from 'path'
import { MemoryQueries } from './queries'
export interface EmbeddingOptions {
model: string
provider: string
apiKey: string
apiVersion?: string
baseURL: string
dimensions?: number
batchSize?: number
}
export interface VectorSearchOptions {
limit?: number
threshold?: number
userId?: string
agentId?: string
filters?: Record<string, any>
}
export interface SearchResult {
memories: MemoryItem[]
count: number
error?: string
}
export class MemoryService {
private static instance: MemoryService | null = null
private db: Client | null = null
private isInitialized = false
private embeddings: Embeddings | null = null
private config: MemoryConfig | null = null
private static readonly UNIFIED_DIMENSION = 1536
private static readonly SIMILARITY_THRESHOLD = 0.85
private constructor() {
// Private constructor to enforce singleton pattern
}
public static getInstance(): MemoryService {
if (!MemoryService.instance) {
MemoryService.instance = new MemoryService()
}
return MemoryService.instance
}
public static reload(): MemoryService {
if (MemoryService.instance) {
MemoryService.instance.close()
}
MemoryService.instance = new MemoryService()
return MemoryService.instance
}
/**
* Initialize the database connection and create tables
*/
private async init(): Promise<void> {
if (this.isInitialized && this.db) {
return
}
try {
const userDataPath = app.getPath('userData')
const dbPath = path.join(userDataPath, 'memories.db')
this.db = createClient({
url: `file:${dbPath}`,
intMode: 'number'
})
// Create tables
await this.createTables()
this.isInitialized = true
Logger.info('Memory database initialized successfully')
} catch (error) {
Logger.error('Failed to initialize memory database:', error)
throw new Error(
`Memory database initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
}
private async createTables(): Promise<void> {
if (!this.db) throw new Error('Database not initialized')
// Create memories table with native vector support
await this.db.execute(MemoryQueries.createTables.memories)
// Create memory history table
await this.db.execute(MemoryQueries.createTables.memoryHistory)
// Create indexes
await this.db.execute(MemoryQueries.createIndexes.userId)
await this.db.execute(MemoryQueries.createIndexes.agentId)
await this.db.execute(MemoryQueries.createIndexes.createdAt)
await this.db.execute(MemoryQueries.createIndexes.hash)
await this.db.execute(MemoryQueries.createIndexes.memoryHistory)
// Create vector index for similarity search
try {
await this.db.execute(MemoryQueries.createIndexes.vector)
} catch (error) {
// Vector index might not be supported in all versions
Logger.warn('Failed to create vector index, falling back to non-indexed search:', error)
}
}
/**
* Add new memories from messages
*/
public async add(messages: string | AssistantMessage[], options: AddMemoryOptions): Promise<SearchResult> {
await this.init()
if (!this.db) throw new Error('Database not initialized')
const { userId, agentId, runId, metadata } = options
try {
// Convert messages to memory strings
const memoryStrings = Array.isArray(messages)
? messages.map((m) => (typeof m === 'string' ? m : m.content))
: [messages]
const addedMemories: MemoryItem[] = []
for (const memory of memoryStrings) {
const trimmedMemory = memory.trim()
if (!trimmedMemory) continue
// Generate hash for deduplication
const hash = crypto.createHash('sha256').update(trimmedMemory).digest('hex')
// Check if memory already exists
const existing = await this.db.execute({
sql: MemoryQueries.memory.checkExistsIncludeDeleted,
args: [hash]
})
if (existing.rows.length > 0) {
const existingRecord = existing.rows[0] as any
const isDeleted = existingRecord.is_deleted === 1
if (!isDeleted) {
// Active record exists, skip insertion
Logger.info(`Memory already exists with hash: ${hash}`)
continue
} else {
// Deleted record exists, restore it instead of inserting new one
Logger.info(`Restoring deleted memory with hash: ${hash}`)
// Generate embedding if model is configured
let embedding: number[] | null = null
const embedderApiClient = this.config?.embedderApiClient
if (embedderApiClient) {
try {
embedding = await this.generateEmbedding(trimmedMemory)
Logger.info(
`Generated embedding for restored memory with dimension: ${embedding.length} (target: ${this.config?.embedderDimensions || MemoryService.UNIFIED_DIMENSION})`
)
} catch (error) {
Logger.error('Failed to generate embedding for restored memory:', error)
}
}
const now = new Date().toISOString()
// Restore the deleted record
await this.db.execute({
sql: MemoryQueries.memory.restoreDeleted,
args: [
trimmedMemory,
embedding ? this.embeddingToVector(embedding) : null,
metadata ? JSON.stringify(metadata) : null,
now,
existingRecord.id
]
})
// Add to history
await this.addHistory(existingRecord.id, null, trimmedMemory, 'ADD')
addedMemories.push({
id: existingRecord.id,
memory: trimmedMemory,
hash,
createdAt: now,
updatedAt: now,
metadata
})
continue
}
}
// Generate embedding if model is configured
let embedding: number[] | null = null
if (this.config?.embedderApiClient) {
try {
embedding = await this.generateEmbedding(trimmedMemory)
Logger.info(
`Generated embedding with dimension: ${embedding.length} (target: ${this.config?.embedderDimensions || MemoryService.UNIFIED_DIMENSION})`
)
// Check for similar memories using vector similarity
const similarMemories = await this.hybridSearch(trimmedMemory, embedding, {
limit: 5,
threshold: 0.1, // Lower threshold to get more candidates
userId,
agentId
})
// Check if any similar memory exceeds the similarity threshold
if (similarMemories.memories.length > 0) {
const highestSimilarity = Math.max(...similarMemories.memories.map((m) => m.score || 0))
if (highestSimilarity >= MemoryService.SIMILARITY_THRESHOLD) {
Logger.info(
`Skipping memory addition due to high similarity: ${highestSimilarity.toFixed(3)} >= ${MemoryService.SIMILARITY_THRESHOLD}`
)
Logger.info(`Similar memory found: "${similarMemories.memories[0].memory}"`)
continue
}
}
} catch (error) {
Logger.error('Failed to generate embedding:', error)
}
}
// Insert new memory
const id = crypto.randomUUID()
const now = new Date().toISOString()
await this.db.execute({
sql: MemoryQueries.memory.insert,
args: [
id,
trimmedMemory,
hash,
embedding ? this.embeddingToVector(embedding) : null,
metadata ? JSON.stringify(metadata) : null,
userId || null,
agentId || null,
runId || null,
now,
now
]
})
// Add to history
await this.addHistory(id, null, trimmedMemory, 'ADD')
addedMemories.push({
id,
memory: trimmedMemory,
hash,
createdAt: now,
updatedAt: now,
metadata
})
}
return {
memories: addedMemories,
count: addedMemories.length
}
} catch (error) {
Logger.error('Failed to add memories:', error)
return {
memories: [],
count: 0,
error: error instanceof Error ? error.message : 'Unknown error'
}
}
}
/**
* Search memories using text or vector similarity
*/
public async search(query: string, options: MemorySearchOptions = {}): Promise<SearchResult> {
await this.init()
if (!this.db) throw new Error('Database not initialized')
const { limit = 10, userId, agentId, filters = {} } = options
try {
// If we have an embedder model configured, use vector search
if (this.config?.embedderApiClient) {
try {
const queryEmbedding = await this.generateEmbedding(query)
return await this.hybridSearch(query, queryEmbedding, { limit, userId, agentId, filters })
} catch (error) {
Logger.error('Vector search failed, falling back to text search:', error)
}
}
// Fallback to text search
const conditions: string[] = ['m.is_deleted = 0']
const params: any[] = []
// Add search conditions
conditions.push('(m.memory LIKE ? OR m.memory LIKE ?)')
params.push(`%${query}%`, `%${query.split(' ').join('%')}%`)
if (userId) {
conditions.push('m.user_id = ?')
params.push(userId)
}
if (agentId) {
conditions.push('m.agent_id = ?')
params.push(agentId)
}
// Add custom filters
for (const [key, value] of Object.entries(filters)) {
if (value !== undefined && value !== null) {
conditions.push(`json_extract(m.metadata, '$.${key}') = ?`)
params.push(value)
}
}
const whereClause = conditions.join(' AND ')
params.push(limit)
const result = await this.db.execute({
sql: `${MemoryQueries.memory.list} ${whereClause}
ORDER BY m.created_at DESC
LIMIT ?
`,
args: params
})
const memories: MemoryItem[] = result.rows.map((row: any) => ({
id: row.id as string,
memory: row.memory as string,
hash: (row.hash as string) || undefined,
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined,
createdAt: row.created_at as string,
updatedAt: row.updated_at as string
}))
return {
memories,
count: memories.length
}
} catch (error) {
Logger.error('Search failed:', error)
return {
memories: [],
count: 0,
error: error instanceof Error ? error.message : 'Unknown error'
}
}
}
/**
* List all memories with optional filters
*/
public async list(options: MemoryListOptions = {}): Promise<SearchResult> {
await this.init()
if (!this.db) throw new Error('Database not initialized')
const { userId, agentId, limit = 100, offset = 0 } = options
try {
const conditions: string[] = ['m.is_deleted = 0']
const params: any[] = []
if (userId) {
conditions.push('m.user_id = ?')
params.push(userId)
}
if (agentId) {
conditions.push('m.agent_id = ?')
params.push(agentId)
}
const whereClause = conditions.join(' AND ')
// Get total count
const countResult = await this.db.execute({
sql: `${MemoryQueries.memory.count} ${whereClause}`,
args: params
})
const totalCount = (countResult.rows[0] as any).total as number
// Get paginated results
params.push(limit, offset)
const result = await this.db.execute({
sql: `${MemoryQueries.memory.list} ${whereClause}
ORDER BY m.created_at DESC
LIMIT ? OFFSET ?
`,
args: params
})
const memories: MemoryItem[] = result.rows.map((row: any) => ({
id: row.id as string,
memory: row.memory as string,
hash: (row.hash as string) || undefined,
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined,
createdAt: row.created_at as string,
updatedAt: row.updated_at as string
}))
return {
memories,
count: totalCount
}
} catch (error) {
Logger.error('List failed:', error)
return {
memories: [],
count: 0,
error: error instanceof Error ? error.message : 'Unknown error'
}
}
}
/**
* Delete a memory (soft delete)
*/
public async delete(id: string): Promise<void> {
await this.init()
if (!this.db) throw new Error('Database not initialized')
try {
// Get current memory value for history
const current = await this.db.execute({
sql: MemoryQueries.memory.getForDelete,
args: [id]
})
if (current.rows.length === 0) {
throw new Error('Memory not found')
}
const currentMemory = (current.rows[0] as any).memory as string
// Soft delete
await this.db.execute({
sql: MemoryQueries.memory.softDelete,
args: [new Date().toISOString(), id]
})
// Add to history
await this.addHistory(id, currentMemory, null, 'DELETE')
Logger.info(`Memory deleted: ${id}`)
} catch (error) {
Logger.error('Delete failed:', error)
throw new Error(`Failed to delete memory: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Update a memory
*/
public async update(id: string, memory: string, metadata?: Record<string, any>): Promise<void> {
await this.init()
if (!this.db) throw new Error('Database not initialized')
try {
// Get current memory
const current = await this.db.execute({
sql: MemoryQueries.memory.getForUpdate,
args: [id]
})
if (current.rows.length === 0) {
throw new Error('Memory not found')
}
const row = current.rows[0] as any
const previousMemory = row.memory as string
const previousMetadata = row.metadata ? JSON.parse(row.metadata as string) : {}
// Generate new hash
const hash = crypto.createHash('sha256').update(memory.trim()).digest('hex')
// Generate new embedding if model is configured
let embedding: number[] | null = null
if (this.config?.embedderApiClient) {
try {
embedding = await this.generateEmbedding(memory)
Logger.info(
`Updated embedding with dimension: ${embedding.length} (target: ${this.config?.embedderDimensions || MemoryService.UNIFIED_DIMENSION})`
)
} catch (error) {
Logger.error('Failed to generate embedding for update:', error)
}
}
// Merge metadata
const mergedMetadata = { ...previousMetadata, ...metadata }
// Update memory
await this.db.execute({
sql: MemoryQueries.memory.update,
args: [
memory.trim(),
hash,
embedding ? this.embeddingToVector(embedding) : null,
JSON.stringify(mergedMetadata),
new Date().toISOString(),
id
]
})
// Add to history
await this.addHistory(id, previousMemory, memory, 'UPDATE')
Logger.info(`Memory updated: ${id}`)
} catch (error) {
Logger.error('Update failed:', error)
throw new Error(`Failed to update memory: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Get memory history
*/
public async get(memoryId: string): Promise<MemoryHistoryItem[]> {
await this.init()
if (!this.db) throw new Error('Database not initialized')
try {
const result = await this.db.execute({
sql: MemoryQueries.history.getByMemoryId,
args: [memoryId]
})
return result.rows.map((row: any) => ({
id: row.id as number,
memoryId: row.memory_id as string,
previousValue: row.previous_value as string | undefined,
newValue: row.new_value as string,
action: row.action as 'ADD' | 'UPDATE' | 'DELETE',
createdAt: row.created_at as string,
updatedAt: row.updated_at as string,
isDeleted: row.is_deleted === 1
}))
} catch (error) {
Logger.error('Get history failed:', error)
throw new Error(`Failed to get memory history: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Delete all memories for a user without deleting the user (hard delete)
*/
public async deleteAllMemoriesForUser(userId: string): Promise<void> {
await this.init()
if (!this.db) throw new Error('Database not initialized')
if (!userId) {
throw new Error('User ID is required')
}
try {
// Get count of memories to be deleted
const countResult = await this.db.execute({
sql: MemoryQueries.users.countMemoriesForUser,
args: [userId]
})
const totalCount = (countResult.rows[0] as any).total as number
// Delete history entries for this user's memories
await this.db.execute({
sql: MemoryQueries.users.deleteHistoryForUser,
args: [userId]
})
// Hard delete all memories for this user
await this.db.execute({
sql: MemoryQueries.users.deleteAllMemoriesForUser,
args: [userId]
})
Logger.info(`Reset all memories for user ${userId} (${totalCount} memories deleted)`)
} catch (error) {
Logger.error('Reset user memories failed:', error)
throw new Error(`Failed to reset user memories: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Delete a user and all their memories (hard delete)
*/
public async deleteUser(userId: string): Promise<void> {
await this.init()
if (!this.db) throw new Error('Database not initialized')
if (!userId) {
throw new Error('User ID is required')
}
if (userId === 'default-user') {
throw new Error('Cannot delete the default user')
}
try {
// Get count of memories to be deleted
const countResult = await this.db.execute({
sql: `SELECT COUNT(*) as total FROM memories WHERE user_id = ?`,
args: [userId]
})
const totalCount = (countResult.rows[0] as any).total as number
// Delete history entries for this user's memories
await this.db.execute({
sql: `DELETE FROM memory_history WHERE memory_id IN (SELECT id FROM memories WHERE user_id = ?)`,
args: [userId]
})
// Delete all memories for this user (hard delete)
await this.db.execute({
sql: `DELETE FROM memories WHERE user_id = ?`,
args: [userId]
})
Logger.info(`Deleted user ${userId} and ${totalCount} memories`)
} catch (error) {
Logger.error('Delete user failed:', error)
throw new Error(`Failed to delete user: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Get list of unique user IDs with their memory counts
*/
public async getUsersList(): Promise<{ userId: string; memoryCount: number; lastMemoryDate: string }[]> {
await this.init()
if (!this.db) throw new Error('Database not initialized')
try {
const result = await this.db.execute({
sql: MemoryQueries.users.getUniqueUsers,
args: []
})
return result.rows.map((row: any) => ({
userId: row.user_id as string,
memoryCount: row.memory_count as number,
lastMemoryDate: row.last_memory_date as string
}))
} catch (error) {
Logger.error('Get users list failed:', error)
throw new Error(`Failed to get users list: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Update configuration
*/
public setConfig(config: MemoryConfig): void {
this.config = config
// Reset embeddings instance when config changes
this.embeddings = null
}
/**
* Close database connection
*/
public async close(): Promise<void> {
if (this.db) {
await this.db.close()
this.db = null
this.isInitialized = false
}
}
// ========== EMBEDDING OPERATIONS (Previously EmbeddingService) ==========
/**
* Normalize embedding dimensions to unified size
*/
private normalizeEmbedding(embedding: number[]): number[] {
if (embedding.length === MemoryService.UNIFIED_DIMENSION) {
return embedding
}
if (embedding.length < MemoryService.UNIFIED_DIMENSION) {
// Pad with zeros
return [...embedding, ...new Array(MemoryService.UNIFIED_DIMENSION - embedding.length).fill(0)]
} else {
// Truncate
return embedding.slice(0, MemoryService.UNIFIED_DIMENSION)
}
}
/**
* Generate embedding for text
*/
private async generateEmbedding(text: string): Promise<number[]> {
if (!this.config?.embedderApiClient) {
throw new Error('Embedder model not configured')
}
try {
// Initialize embeddings instance if needed
if (!this.embeddings) {
if (!this.config.embedderApiClient) {
throw new Error('Embedder provider not configured')
}
this.embeddings = new Embeddings({
embedApiClient: this.config.embedderApiClient,
dimensions: this.config.embedderDimensions
})
await this.embeddings.init()
}
const embedding = await this.embeddings.embedQuery(text)
// Normalize to unified dimension
return this.normalizeEmbedding(embedding)
} catch (error) {
Logger.error('Embedding generation failed:', error)
throw new Error(`Failed to generate embedding: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
// ========== VECTOR SEARCH OPERATIONS (Previously VectorSearch) ==========
/**
* Convert embedding array to libsql vector format
*/
private embeddingToVector(embedding: number[]): string {
return `[${embedding.join(',')}]`
}
/**
* Hybrid search combining text and vector similarity (currently vector-only)
*/
private async hybridSearch(
_: string,
queryEmbedding: number[],
options: VectorSearchOptions = {}
): Promise<SearchResult> {
if (!this.db) throw new Error('Database not initialized')
const { limit = 10, threshold = 0.5, userId } = options
try {
const queryVector = this.embeddingToVector(queryEmbedding)
const conditions: string[] = ['m.is_deleted = 0']
const params: any[] = []
// Vector search only - three vector parameters for distance, vector_similarity, and combined_score
params.push(queryVector, queryVector, queryVector)
if (userId) {
conditions.push('m.user_id = ?')
params.push(userId)
}
const whereClause = conditions.join(' AND ')
const hybridQuery = `${MemoryQueries.search.hybridSearch} ${whereClause}
) AS results
WHERE vector_similarity >= ?
ORDER BY vector_similarity DESC
LIMIT ?`
params.push(threshold, limit)
const result = await this.db.execute({
sql: hybridQuery,
args: params
})
const memories: MemoryItem[] = result.rows.map((row: any) => ({
id: row.id as string,
memory: row.memory as string,
hash: (row.hash as string) || undefined,
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined,
createdAt: row.created_at as string,
updatedAt: row.updated_at as string,
score: row.vector_similarity as number
}))
return {
memories,
count: memories.length
}
} catch (error) {
Logger.error('Hybrid search failed:', error)
throw new Error(`Hybrid search failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
// ========== HELPER METHODS ==========
/**
* Add entry to memory history
*/
private async addHistory(
memoryId: string,
previousValue: string | null,
newValue: string | null,
action: 'ADD' | 'UPDATE' | 'DELETE'
): Promise<void> {
if (!this.db) throw new Error('Database not initialized')
const now = new Date().toISOString()
await this.db.execute({
sql: MemoryQueries.history.insert,
args: [memoryId, previousValue, newValue, action, now, now]
})
}
}
export default MemoryService

View File

@@ -0,0 +1,164 @@
/**
* SQL queries for MemoryService
* All SQL queries are centralized here for better maintainability
*/
export const MemoryQueries = {
// Table creation queries
createTables: {
memories: `
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
memory TEXT NOT NULL,
hash TEXT UNIQUE,
embedding F32_BLOB(1536), -- Native vector column (1536 dimensions for OpenAI embeddings)
metadata TEXT, -- JSON string
user_id TEXT,
agent_id TEXT,
run_id TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_deleted INTEGER DEFAULT 0
)
`,
memoryHistory: `
CREATE TABLE IF NOT EXISTS memory_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memory_id TEXT NOT NULL,
previous_value TEXT,
new_value TEXT,
action TEXT NOT NULL, -- ADD, UPDATE, DELETE
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_deleted INTEGER DEFAULT 0,
FOREIGN KEY (memory_id) REFERENCES memories (id)
)
`
},
// Index creation queries
createIndexes: {
userId: 'CREATE INDEX IF NOT EXISTS idx_memories_user_id ON memories(user_id)',
agentId: 'CREATE INDEX IF NOT EXISTS idx_memories_agent_id ON memories(agent_id)',
createdAt: 'CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at)',
hash: 'CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash)',
memoryHistory: 'CREATE INDEX IF NOT EXISTS idx_memory_history_memory_id ON memory_history(memory_id)',
vector: 'CREATE INDEX IF NOT EXISTS idx_memories_vector ON memories (libsql_vector_idx(embedding))'
},
// Memory operations
memory: {
checkExists: 'SELECT id FROM memories WHERE hash = ? AND is_deleted = 0',
checkExistsIncludeDeleted: 'SELECT id, is_deleted FROM memories WHERE hash = ?',
restoreDeleted: `
UPDATE memories
SET is_deleted = 0, memory = ?, embedding = ?, metadata = ?, updated_at = ?
WHERE id = ?
`,
insert: `
INSERT INTO memories (id, memory, hash, embedding, metadata, user_id, agent_id, run_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
getForDelete: 'SELECT memory FROM memories WHERE id = ? AND is_deleted = 0',
softDelete: 'UPDATE memories SET is_deleted = 1, updated_at = ? WHERE id = ?',
getForUpdate: 'SELECT memory, metadata FROM memories WHERE id = ? AND is_deleted = 0',
update: `
UPDATE memories
SET memory = ?, hash = ?, embedding = ?, metadata = ?, updated_at = ?
WHERE id = ?
`,
count: 'SELECT COUNT(*) as total FROM memories m WHERE',
list: `
SELECT
m.id,
m.memory,
m.hash,
m.metadata,
m.user_id,
m.agent_id,
m.run_id,
m.created_at,
m.updated_at
FROM memories m
WHERE
`
},
// History operations
history: {
insert: `
INSERT INTO memory_history (memory_id, previous_value, new_value, action, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
`,
getByMemoryId: `
SELECT * FROM memory_history
WHERE memory_id = ? AND is_deleted = 0
ORDER BY created_at DESC
`
},
// Search operations
search: {
hybridSearch: `
SELECT * FROM (
SELECT
m.id,
m.memory,
m.hash,
m.metadata,
m.user_id,
m.agent_id,
m.run_id,
m.created_at,
m.updated_at,
CASE
WHEN m.embedding IS NULL THEN 2.0
ELSE vector_distance_cos(m.embedding, vector32(?))
END as distance,
CASE
WHEN m.embedding IS NULL THEN 0.0
ELSE (1 - vector_distance_cos(m.embedding, vector32(?)))
END as vector_similarity,
0.0 as text_similarity,
(
CASE
WHEN m.embedding IS NULL THEN 0.0
ELSE (1 - vector_distance_cos(m.embedding, vector32(?)))
END
) as combined_score
FROM memories m
WHERE
`
},
// User operations
users: {
getUniqueUsers: `
SELECT DISTINCT
user_id,
COUNT(*) as memory_count,
MAX(created_at) as last_memory_date
FROM memories
WHERE user_id IS NOT NULL AND is_deleted = 0
GROUP BY user_id
ORDER BY last_memory_date DESC
`,
countMemoriesForUser: 'SELECT COUNT(*) as total FROM memories WHERE user_id = ?',
deleteAllMemoriesForUser: 'DELETE FROM memories WHERE user_id = ?',
deleteHistoryForUser: 'DELETE FROM memory_history WHERE memory_id IN (SELECT id FROM memories WHERE user_id = ?)'
}
} as const

View File

@@ -0,0 +1,13 @@
import { FileListResponse, FileMetadata, FileUploadResponse, Provider } from '@types'
export abstract class BaseFileService {
protected readonly provider: Provider
protected constructor(provider: Provider) {
this.provider = provider
}
abstract uploadFile(file: FileMetadata): Promise<FileUploadResponse>
abstract deleteFile(fileId: string): Promise<void>
abstract listFiles(): Promise<FileListResponse>
abstract retrieveFile(fileId: string): Promise<FileUploadResponse>
}

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