Compare commits

...

2 Commits

Author SHA1 Message Date
qiuqiu
7d74a2330e Windows7 support V1.4.1 (#7111)
* feat: Add-aihubmix-ideogram-v3 (#5958)

* chore: update Yarn version to 4.9.1 and add rendering speed option for V3 model

* Discard changes to .yarnrc.yml

* Discard changes to .yarn/releases/yarn-4.6.0.cjs

* Update package.json

* Delete .yarn/releases/yarn-4.9.1.cjs

* Discard changes to .yarn/releases/yarn-4.6.0.cjs

* docs: Update README.zh.md add GitCode✖️Cherry Studio【新源力】贡献挑战赛

GitCode✖️Cherry Studio【新源力】贡献挑战赛

* fix: Update the file permissions for yarn-4.6.0.cjs, modify image links and formatting in README.zh.md

* clean

* refactor: improve switch case blocks and update config parameters in AihubmixPage

* feat: add error handling for empty image URLs and update localization messages

* refactor: replace modal error handling with warning messages for empty image URLs

* feat: update localization for rendering speed and translating messages in Japanese, Russian, and Traditional Chinese

* feat: add style types and rendering speed options to localization files

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
Co-authored-by: suyao <sy20010504@gmail.com>

* refactor(Scrollbar): Optimize scroll handling logic to support external scroll events (#6047)

* refactor(Scrollbar): Optimize scroll handling logic to support external scroll events

- Refactor `onScroll` logic to support external scroll events
- Integrate with `useScrollPosition` hook for better scroll state management
- Memorize the scoll state for better user experience
- Fix type definition for `ref` attribute
- Remove unnecessary `ref` type overrides
- Improve component compatibility and maintainability

* perf(useScrollPosition): Optimize scroll position updates using requestAnimationFrame

- Wrap the `window.keyv.set` call in `requestAnimationFrame` to reduce unnecessary performance overhead and improve responsiveness during scrolling.

* fix(Messages): Remove unused FC imports and add onComponentUpdate and onFirstUpdate properties

* hotfix: enhance reasoning summary handling in OpenAI response processing (#6037)

* feat: add zoom factor handling on window restore in WindowService (#6169)

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>

* Hotfix/gemini-para-bug (#6173)

* fix(OpenAIProvider): enhance model support and formatting logic

* fix(OpenAIProvider): Gemini OpenAI Compatible

* hotfix: respect user-defined model group name (#6174)

* feat(settings): add OpenAI alert (#6164)

* hotfix: add OpenAI settings tab and related functionality (#6040)

* feat: add OpenAI settings tab and related functionality

* fix: update related logic to support flexible service layer.

* fix(OpenAIResponseProvider): remove unused isOpenAILLMModel import

* feat: support default Quick Assistant model (#6150)

* feat: support default Quick Assistant model

- support the configuration of the quick assistant model.
- manage the models independently in Quick Assistant and Default Assistant.

* docs(i18n): Add translation for quick assistant model

* fix(llm): fix the default value of quickAssistantModel to silicon[1]

* refactor(i18n): uniformly the terms, changing "快速助手" to "快捷助手"

* refactor: remove constructor from KnowledgeQueue and invoke checkAllBases in useAppInit hook

* feat: add resolveFilePath functionality to resolve restoring from different computer (#5980)

* feat: add resolveFilePath functionality to file management

* Added new IPC channel for resolving file paths.
* Implemented resolveFilePath method in FileStorage service.
* Updated FileManager to utilize the new resolveFilePath method.
* Enhanced preload API to expose resolveFilePath to the renderer.
* Updated KnowledgeService to ensure file paths are correctly resolved in knowledge bases.

* refactor: remove unused path import from preload index

* Removed the unused 'resolve' import to clean up the codebase.
* Improved code readability by eliminating unnecessary dependencies.

---------

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>

* docs: update branching strategy documentation and add English and Chinese versions

- Updated the README to link to the new English branching strategy document.
- Added a new English version of the branching strategy document.
- Removed the outdated branching strategy document.
- Added a Chinese version of the branching strategy document.

* fix: update file path resolution in new Electron

* fix: update default values and improve component structure

- Changed default value for `getTrayOnClose` to true in `ConfigManager`.
- Removed fullscreen toggle logic from `WindowService`.
- Adjusted styles in `OpenAIAlert` for better spacing.
- Reorganized imports in `Navbar` and updated component paths in `AssistantsTab` and `SettingsTab`.
- Added new components `AssistantItem` and `OpenAISettingsGroup` for better modularity.
- Enhanced `SettingGroup` styles for improved UI consistency.
- Updated `QuickPhraseSettings` to utilize theme context.
- Minor fixes and refactoring across various services and components.

* feat: add MCP servers via JSON quickly (#6099)

* feat: add MCP servers via JSON quickly

* refactor(MCPSettings): Extract JSON parsing logic into a helper function

* feat: json linter for EditMcpJsonPopup

* feat(mcp): confirm the MCP server status as connection

* refactor(AddMcpServerModal): 移除冗余注释并修复加载状态

* feat(MCPSettings): Add support for SSE and streamableHttp formats and optimize server configuration parsing

- Add server type validation to ensure the type is stdio, SSE, or streamableHttp
- Optimize JSON parsing logic to ensure server configuration completeness and validity
- Update example text to provide more detailed configuration examples

* fix(MCPSettings): fix AddMcpServerModal default baseUrl login

移除serverToAdd.url作为baseUrl的备选值,确保baseUrl仅使用serverToAdd.baseUrl的值

* feat(MCPSettings): support CodeEditor in AddMcpServerModal

* fix: Remove unnecessary type checks for JSON parsing login

* fix(MCPSettings): fix compatibility issues with the URL field when parsing server data

* refactor: remove unnessary cdoe

* chore: Add a server dropdown button to integrate new features in UI

- Integrate the two buttons for adding a server into a single dropdown menu to enhance user experience and simplify the interface

* chroe: modify the Dropdown items' name of addServer

* refactor(i18n): unify the translation for the MCP server import function

---------

Co-authored-by: one <wangan.cs@gmail.com>

* fix: set default initial values for rendering speed in aihubmixConfig

- Added initialValue 'DEFAULT' for renderingSpeed in multiple mode configurations to ensure consistent default behavior across the application.

* chore(version): 1.3.7

* refactor: centralize paste handling logic with PasteService (#6199)

- Integrated PasteService for handling paste events in Inputbar and MessageEditor components.
- Removed redundant paste handling code and improved maintainability.
- Registered paste handlers and set last focused component for better user experience.
- Ensured consistent behavior for text and file pasting across components.

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>

* chore: update Hailuo logo (#6208)

* feat: update Hailuo model configuration with new logo property and border option

* chore: update hailuo_dark model image

* feat: add new model provider BurnCloud

* fix(mcp): mcp result preview is missing parameters (#6165)

* fix: knowledge icon consistency across components (#6209)

feat: knowledge icon consistency across components

* feat: enhance ContentSearch and MessageTools components

- Added placeholder and adjusted styles for the Input in ContentSearch.
- Updated SearchBarContainer to use fixed positioning and improved padding.
- Refactored MessageTools to render raw content correctly and added a new MarkdownContainer for better styling.
- Minor adjustments to other components for improved layout and user experience.

* fix: add context menu trigger to FloatingSidebar component

* refactor: improve FloatingSidebar and HomeTabs components

- Simplified the onOpenChange handler in FloatingSidebar for better readability.
- Added style prop to HomeTabs for enhanced customization.
- Adjusted Container style in HomeTabs to merge with existing border styles.

* Revert "feat: add resolveFilePath functionality to resolve restoring from different computer (#5980)"

This reverts commit 3abe0e803c.

* feat: implement getFilePath method in FileManager and update KnowledgeContent to use it

- Added getFilePath method in FileManager to construct file paths based on file ID and extension.
- Updated KnowledgeContent to utilize the new getFilePath method for opening file paths, improving code clarity and maintainability.

* chore(version): 1.3.8

* refactor(PasteService): optimize handler registration logic (#6223)

- Updated registerHandler to only log and update the handler if it changes, reducing unnecessary operations.
- Removed logging from unregisterHandler for cleaner code.

* Feat/mcp run python (#6151)

* feat: add MCP Run Python server and integrate Pyodide for executing Python code

* fix: comment out unused polyfill imports in MCP Run Python server

* fix: handle undefined html title (#6229)

* fix: improve CodeEditor controlled mode, prevent unnecessary trimming (#6221)

* fix: improve CodeEditor controlled mode, prevent unnecessary trimming

* fix: useEffect dependency

* fix: sse mcp 404 not found

* feat: add message multiple select (#6085)

* feat: add message multiple select

* fix: build error

* feat: add drag-and-drop multi-selection

* fix: code review

* Revert "fix: code review"

This reverts commit 7e29d5147c.

* fix: hide input bar display

* fix: extract the ChatContext

* fix: eventemitter

* feat: enhance multi-select functionality with message registration

* fix: history page message search

* fix: build error

* fix: remove Event Emitter

* fix: build error

* feat: add hideMenuBar prop to MessageItem and integrate MessageEditingProvider

* fix: improve message selection logic and handle drag events

* fix: update translation keys for multiple select functionality

* fix: refactor message deletion logic and enhance message selection handling

* fix: replace useSelector with useStore for message selection in ChatContext

* fix: refactor MessageGroup to utilize context for multi-select handling and message registration

* Revert "fix: refactor MessageGroup to utilize context for multi-select handling and message registration"

This reverts commit f4d1454525.

* fix: simplify MessageGroup props and utilize context for message selection handling

* fix: streamline multi-select handling by consolidating context usage and simplifying component props

* feat/notification (#6144)

* WIP

* feat: integrate notification system using Electron's Notification API

- Replaced the previous notification implementation with Electron's Notification API for better integration.
- Updated notification types and structures to support new features.
- Added translations for notification messages in multiple languages.
- Cleaned up unused dependencies related to notifications.

* refactor: remove unused node-notifier dependency from Electron config

* clean: remove node-notifier from asarUnpack in Electron config

* fix: update notification type in preload script to align with new structure

* feat: Integrate NotificationService for user notifications across various components

- Implemented NotificationService in useUpdateHandler to notify users of available updates.
- Enhanced KnowledgeQueue to send success and error notifications during item processing.
- Updated BackupService to notify users upon successful restoration and backup completion.
- Added error notifications in message handling to improve user feedback on assistant responses.

This update improves user experience by providing timely notifications for important actions and events.

* feat: Refactor notification handling and integrate new notification settings

- Moved SYSTEM_MODELS to a new file for better organization.
- Enhanced notification handling across various components, including BackupService and KnowledgeQueue, to include a source attribute for better context.
- Introduced notification settings in the GeneralSettings component, allowing users to toggle notifications for assistant messages, backups, and knowledge embedding.
- Updated the Redux store to manage notification settings and added migration logic for the new settings structure.
- Improved user feedback by ensuring notifications are sent based on user preferences.

This update enhances the user experience by providing customizable notification options and clearer context for notifications.

* feat: Add 'update' source to NotificationSource type for enhanced notification context

* feat: Integrate electron-notification-state for improved notification handling

- Added electron-notification-state package to manage Do Not Disturb settings.
- Updated NotificationService to respect user DND preferences when sending notifications.
- Adjusted notification settings in various components (BackupService, KnowledgeQueue, useUpdateHandler) to ensure notifications are sent based on user preferences.
- Enhanced user feedback by allowing notifications to be silenced based on DND status.

* feat: Add notification icon to Electron notifications

* fix: import SYSTEM_MODELS in EditModelsPopup for improved model handling

* feat(i18n): add knowledge base notifications in multiple languages

- Added success and error messages for knowledge base operations in English, Japanese, Russian, Chinese (Simplified and Traditional).
- Updated the KnowledgeQueue to utilize the new notification messages for better user feedback.

* feat(notification): introduce NotificationProvider and integrate into App component

- Added NotificationProvider to manage notifications within the application.
- Updated App component to include NotificationProvider, enhancing user feedback through notifications.
- Refactored NotificationQueue to support multiple listeners for notification handling.

* refactor: update MultiSelectionPopup icons and improve styling

- Replaced Ant Design icons with Lucide icons for a more modern look.
- Adjusted ActionButton styling to have a circular border radius.
- Updated translation keys in Chinese locales for better formatting.
- Enhanced event handling in ChatContext to manage multi-select mode more effectively.
- Cleaned up unused imports and props in MessageGroup and MessageSelect components.

* feat: add Google miniapp

* fix: enhance OpenAI service tier handling and initialize settings during migration

- Updated getServiceTier method to ensure proper handling of undefined OpenAI models.
- Added initialization for OpenAI settings in the migration process to set default values if not present.

* refactor: simplify MCP service integration and cleanup logic

- Replaced singleton pattern with direct instantiation of McpService for cleaner code.
- Updated IPC handlers to use the new mcpService instance directly.
- Removed unnecessary logging in Inputbar and MCPToolsButton components to streamline functionality.
- Cleaned up error handling in McpSettings to improve user experience.

* fix: topic deletion button covering the text

* fix: message content width will exceed the bubble limit in narrow layout mode

* fix: settings -> openAI -> summaryText undefined

* chore: remove electron-notification-state dependency and update notification settings

- Removed the electron-notification-state package from dependencies as it is no longer used.
- Updated NotificationService to eliminate Do Not Disturb handling.
- Changed default notification settings in the Redux store to false for assistant messages, backups, and knowledge embedding.

* fix: topic prompt not working #6226

close #6226

* fix: english serif font rendering issue #6224

close #6224

* chore(version): 1.3.9

* fix: increase the upper limit of issue-management operations-per-run (#6257)

* refactor: reuse shiki highlighter utils (#6235)

* refactor: improve shiki highlighter utils and reuse it in ShikiStreamService

* refactor: reuse shiki highlighter and markdown-it renderer

* refactor: import shiki/markdown-it/core dynamically

* chore: update shiki

* refactor: improve ContextMenu and Message components layout

- Replaced the div in ContextMenu with a styled component for better styling control.
- Enhanced Message component to handle editing state more cleanly, separating the editor from the message display.
- Adjusted styling for the MessageEditor and FileBlocksContainer for improved layout and responsiveness.

* refactor: migrate chat context to a custom hook and enhance multi-selection functionality

- Replaced the ChatContext with a custom hook `useChatContext` for better modularity and reusability.
- Updated components to utilize the new hook, passing the active topic as an argument.
- Enhanced multi-selection logic and state management for messages, improving user experience in the chat interface.
- Removed the old ChatContext file to streamline the codebase.

* refactor: enhance multi-selection handling in Inputbar and MessageEditor

- Integrated `useAppSelector` in Inputbar to manage multi-selection state.
- Updated Inputbar to conditionally render based on multi-selection mode.
- Modified MessageEditor to display the resend button only for assistant messages, improving UI clarity.

* style: update border color in SearchBarContainer for improved UI consistency

* fix: handle user cancellation and improve error reporting in file saving process

- Added a check for user cancellation in the file save dialog, rejecting the promise if canceled.
- Enhanced error handling to reject the promise with a detailed error message instead of returning null.

* refactor: update IPC channel names for notifications

- Renamed IPC channels for notifications to improve clarity and consistency.
- Updated related handlers in the main process and preload scripts to reflect the new naming convention.
- Enhanced notification service to respect user settings before sending notifications.

* refactor: enhance notification handling based on page context

- Updated NotificationProvider to truncate long messages for better display.
- Modified message sending logic in messageThunk to prevent notifications on the home page, improving user experience.

* chore: update release notes in electron-builder.yml

- Added new features including message notification functionality and support for Google Mini Programs.
- Improved MCP capabilities to run Python code and fixed several issues related to message editing and display.
- Updated release notes to reflect these changes and enhancements.

* fix: update service tier check to exclude GitHub and Copilot models

* refactor: improve shiki highlighter utils and reuse it in ShikiStreamService

* refactor: reuse shiki highlighter and markdown-it renderer

* hotfix: exclude GitHub and Copilot models

* fix: update service tier check to exclude GitHub and Copilot models

---------

Co-authored-by: one <wangan.cs@gmail.com>

* fix: show x-scrollbar in codeblock if unwrapped, simplify style definitions (#6266)

* fix: show x-scrollbar in codeblock if unwrapped, simplify style definitions

* chore: clean up useless code

* revert: mcp run python (#6151)

This reverts commit c468c3cfd5.

* refactor: CodePreview fade in on the first highlighting (#6228)

* refactor(CodePreview): fade in on the first highlighting

* refactor: improve code placeholder style

* refactor: update Gemini file upload method to accept baseURL parameter

- Modified the uploadFile method in GeminiService to include baseURL in the parameters.
- Updated the corresponding calls in the preload and renderer layers to pass the baseURL along with the apiKey.

* fix: enhance message loading and search functionality in HistoryPage and SearchResults

- Integrated Redux dispatch to load topic messages when a message is clicked in HistoryPage.
- Updated SearchResults to utilize message blocks for improved search results, including content extraction.
- Refactored state management to accommodate new content structure in search results.

* fix: token 取整 (#6300)

* fix: Use effective theme for code style in SettingsTab (#6305)

* Fix: Use effective theme for code style in SettingsTab

The SettingsTab component was previously using the theme setting directly from useSettings to determine whether to apply the light or dark code style. This caused an issue when the theme was set to 'auto', as it wouldn't correctly reflect the actual system theme (light or dark).

This commit modifies SettingsTab.tsx to use the `theme` from the `useTheme` hook (which provides the effective theme) for the logic that determines the code editor and preview styles. This ensures that the code style accurately reflects your current effective theme, including when 'auto' theme is selected and the OS theme changes.

* Refactor: Remove unnecessary comments in SettingsTab

This commit removes non-essential comments that were added during a previous refactoring of `SettingsTab.tsx`. The core logic for using the effective theme for code style selection remains unchanged. This change is purely for code cleanliness.

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>

* fix: aihubmix provider model proxy rule (#6293)

Update AihubmixProvider.ts

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

* feat: add functionality to insert messages at a specific index in the… (#6299)

* feat: add functionality to insert messages at a specific index in the Redux store

- Introduced a new interface for inserting messages at a specified index.
- Implemented the insertMessageAtIndex reducer to handle message insertion.
- Updated saveMessageAndBlocksToDB to support message insertion logic.
- Modified appendAssistantResponseThunk to utilize the new insertion functionality.

* feat: integrate multi-select mode handling in MessageGroup component

- Added useChatContext hook to access multi-select mode state.
- Updated isGrouped logic to account for multi-select mode, ensuring proper message grouping behavior.
- Enhanced MessageWrapper styles for better layout management in different modes.

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>

* feat: implement message location functionality and refactor multi-select handling

- Added event listeners for LOCATE_MESSAGE events to scroll to specific messages in the MessageGroup component.
- Introduced a new SelectionBox component to handle multi-select functionality, allowing users to select multiple messages with drag actions.
- Refactored Messages component to remove unused multi-select logic and improve overall structure.
- Cleaned up code by removing commented-out sections and unnecessary state management related to dragging.

* chore: upgrade yarn version to v4.9.1

* build: add win-sign script

* fix: enhance backup and restore functionality with skip option (#6294)

* fix: enhance backup and restore functionality with skip option

- Updated `restore` and `restoreFromWebdav` methods in `BackupManager` to include a `skipBackupFile` parameter, allowing users to skip restoring the Data directory if desired.
- Modified corresponding IPC calls in `preload` and `BackupService` to support the new parameter.
- Added success message translations in multiple languages for improved user feedback.

* fix: integrate skipBackupFile option in RestorePopup for enhanced restore functionality

* refactor: remove skipBackupFile parameter from restore methods for simplified usage

- Updated `restore` and `restoreFromWebdav` methods in `BackupManager`, `BackupService`, and `NutstoreService` to remove the `skipBackupFile` parameter, streamlining the restore process.
- Adjusted corresponding IPC calls in `preload` and `RestorePopup` to reflect the changes, ensuring consistent functionality across the application.

* fix: handle empty block content in MessageTranslate component

* fix: non-streaming reasoning_content (#6308)

* fix: non-streaming reasoning_content

* fix: streamline reasoning handling in OpenAIProvider

* fix: #6301 (#6317)

* fix: #6301

* fix: update dependencies in useUpdateHandler and add eslint comment in ContentSearch

* fix: shiki does not load language as a fallback & themes error (#6281)

* fix: shiki does not load language as a fallback & themes error

* feat: shiki automatic loading language

* fix: adjusted max-width in McpDescription.tsx

* fix: handle proxy environment variables for bun command in MCPService

- Added logic to remove proxy environment variables when the command ends with 'bun'.
- Introduced a new private method `removeProxyEnv` to clean up the environment variables before starting the server.

* feat: add support for 'grok' provider in web search functionality

- Enhanced `isWebSearchModel` to recognize 'grok' as a valid web search model.
- Updated `getOpenAIWebSearchParams` to return specific search parameters for 'grok'.
- Modified `OpenAIProvider` to handle citations from 'grok' in web search results.
- Added 'grok' to the `WebSearchSource` enum for citation formatting.

* feat: toggle MCP servers on the card list (#6232)

* feat: Support Claude 4 (#6328)

* feat: gemini thinking summary support

- Removed unnecessary content accumulation and streamlined chunk processing logic.
- Introduced distinct handling for 'thinking' and 'text' parts, ensuring accurate onChunk calls for both types.
- Enhanced timing tracking for reasoning content, improving overall responsiveness in streaming scenarios.

* chore(version): 1.3.10

* fix: editing user messages is not re-sent; it can only be saved#6327

* feat: support pin topic to the top

Signed-off-by: jtsang4 <info@jtsang.me>

* fix: minapp search error

* fix: ensure correct PATH assignment in shell environment

- Updated the environment variable initialization to use a consistent type.
- Added logic to set the PATH variable correctly, ensuring it falls back to existing values if necessary.

* feat: add navigation buttons for webview history in MinApp popup (#6342)

* feat: add navigation buttons for webview history in MinApp popup

- Implemented 'Go Back' and 'Go Forward' functionality in the MinApp popup.
- Added corresponding translations for English, Japanese, Russian, and Chinese locales.
- Included icons for navigation buttons to enhance user experience.

* fix: update Russian and Traditional Chinese translations for UI elements

- Revised translations for "rightclick_copyurl", "close", and "minimize" to improve clarity and consistency in the Russian and Traditional Chinese locales.
- Ensured that the translations align better with user expectations and common usage.

* fix: update Russian translations for MinApp popup UI elements

- Revised translations for "close" and "minimize" to specify their context within the MinApp, enhancing clarity for users.
- Ensured consistency with existing translations and improved user understanding of the interface.

---------

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>

* chore: update electron version to 35.4.0 in package.json and yarn.lock

* feat: support tokenflux provider (#6358)

* feat: add Cherry Cloud provider with associated assets and localization support

* feat: add Cherry Cloud provider support with OAuth integration and IPC handling

* fix: add success message for Cherry Cloud API key update

* feat: enhance provider navigation with dynamic ID in URL and update selected provider state

* feat: implement Cherry Cloud server synchronization with token management and error handling

* feat: add CherryCloud provider configuration and token management

* feat: integrate TokenFlux provider support and update related configurations

fix: update redux-persist version to 104

refactor: remove redundant tokenflux provider model assignment in migration

fix: update migration to add TokenFlux provider instead of CherryCloud

* feat: enhance TokenFlux server synchronization with API key authentication support

* feat: update TokenFlux provider assets and add new models

* feat: update migration logic for version 106 to add TokenFlux provider

* feat: disable TokenFlux provider by default in INITIAL_PROVIDERS

* feat: add TokenFlux billing URLs to providerCharge and providerBills functions

* refactor: remove manual chunking logic from Vite configuration

- Eliminated custom output chunking strategy for worker files and node_modules from the electron Vite configuration.
- Streamlined the configuration for improved maintainability and clarity.

* chore: update release notes and improve README assets

- Updated release notes to include new features such as TokenFlux service support, Claude 4 model integration, and fixes for various issues including Windows user startup problems and search crashes.
- Replaced outdated screenshots in README files with new images across English, Japanese, and Chinese documentation.
- Enhanced the 'Related Projects' section for better visibility.

* chore(version): 1.3.11

* chore: disable code signature verification for Windows updates in electron-builder configuration

* feat: add support for Windows ARM64 architecture in bun installation script

- Included package mappings for 'win32-arm64' and 'win32-arm64-baseline' to the BUN_PACKAGES object in install-bun.js, enhancing compatibility with ARM64 devices on Windows.

* fix: floating-sidebar header sticky (#6371)

* chore: add dependabot (#6369)

* fix: improve multi-select functionality in Messages and SelectionBox components

* feat: add disable MCP server functionality and update translations (#6398)

* feat: add disable MCP server functionality and update translations

* feat: update MCPToolsButton and WebSearchButton to use CircleX icon and change labels to 'close'

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>

* feat: 文字生成图新增提供商DMXAPI (#6352)

dmxapi文字生成图

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>

* fix: update MainTextBlock to use a class for markdown styling

- Changed the paragraph element in MainTextBlock to include a "markdown" class for improved styling consistency.

* fix: handle optional usage properties in AnthropicProvider (#6418)

* fix: ensure args are an array in AddMcpServerModal and MCPService com… (#6413)

fix: ensure args are an array in AddMcpServerModal and MCPService components

* fix: update dimensions handling in KnowledgeBaseParams (#6417)

fix: update dimensions handling in KnowledgeBaseParams and add supported dimension providers

* fix: MessageMenubar copy uses latest content (#6435)

* Fix: MessageMenubar copy uses latest content

The 'Copy' button in MessageMenubar was previously using memoized content
derived from component props. This could lead to copying stale content if
you edited a message (e.g., a code block), saved it, and then
immediately clicked 'Copy', because the asynchronous Redux store update
might not have completed and propagated to the props yet.

This commit modifies the onCopy function in MessageMenubar.tsx to
fetch the latest version of the message directly from the Redux store
(store.getState().messages.entities[message.id]) at the moment the
copy action is performed. This ensures that the most up-to-date content
is always copied, resolving the stale content issue.

* Chore: Remove unnecessary comments from MessageMenubar

Removes a few explanatory comments from the onCopy function in
MessageMenubar.tsx that were deemed unnecessary, to keep the code
cleaner.

The comments originated from an example provided in a previous description. The core logic of the function remains unchanged.

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>

* refactor(CodeTool): use hook for codeblock tools rather than context (#6273)

* refactor(CodeTool): use hook for codeblock tools rather than context

* fix: codeblock overflow behaviour

* fix: CodePreview scrollbar

* refactor: move margin to CodeHeader

* refactor: add min-width to codeblock

* 修复DMXAPI文生成画bug

* feat: enhance citation handling in message export functionality (#6422)

* feat: enhance citation handling in message export functionality

- Refactored message export functions to include citation content in markdown output.
- Introduced a new utility function to extract and format citations from messages.
- Updated related imports and adjusted existing markdown generation logic for improved clarity and maintainability.

* feat: enhance message export tests to include citation and reasoning content

- Added tests to verify inclusion of citation content in markdown output when citation blocks exist.
- Ensured proper formatting with double newlines between sections in exported markdown.
- Updated existing tests to handle cases with reasoning content and no main text block gracefully.

* fix: update citation mapping in export tests for consistency

- Modified the citation mapping in export tests to use the index parameter directly, improving clarity and consistency in the generated markdown output.

* fix: enhance ExportService to support nested bold and italic formatting (#6420)

* fix: enhance ExportService to support nested bold and italic formatting

- Added tracking for nested bold and italic tags in the ExportService.
- Updated text rendering logic to apply bold and italic styles based on the nesting level of the tags.

* fix: remove unused citation variable in messageToMarkdown function

* fix: enhance web search recognization in AI providers (#6423)

* fix: escape special characters in search pattern for improved filtering

* feat: throttle updateTranslationBlock dispatch for improved performance (#6442)

- Introduced throttling to the updateTranslationBlock dispatch function to limit the frequency of updates, enhancing performance during message operations.
- Utilized lodash's throttle function to ensure efficient handling of accumulated text updates.

* fix: Chinese input issue in AddProviderPopup (#6445)

* chore: remove postinstall script from package.json

* fix: improve header styling in CustomCollapse component (#6449)

* fix: return value from appUpdater.checkForUpdates in IPC handler

* docs: update README files to reflect new roadmap and feature enhancements

- Revised the TODO section to a comprehensive roadmap outlining core features, knowledge management, platform support, and advanced features.
- Added links to the project board and GitHub Discussions for community engagement and feedback.

* fix: update popup content to improve user interaction in MessageGroup component

* fix: change display style of .shiki class to flex for improved layout in CodePreview component

* docs: update README files to enhance feature listings and organization

- Renumbered feature sections for clarity and consistency across English, Japanese, and Chinese README files.
- Improved formatting by removing unnecessary bullet points for a cleaner presentation of core features, knowledge management, platform support, and advanced features.

* fix: 修复Nutstore设置中的自动同步状态和错误消息内容 (#6452)

- 在NutstoreSettings组件中,添加了设置Nutstore自动同步状态的逻辑。
- 更新NutstoreService中的错误消息内容,确保使用正确的国际化键。

* feat: add dark mode support for DMXAPI logo in settings

- Introduced a new dark mode logo for DMXAPI and updated the logo rendering logic in the DMXAPISettings component to switch between light and dark logos based on the current theme.

* chore: update electron configuration and add debug script (#6361)

* chore: update electron configuration and add debug script

- Added sourcemap generation for development builds in electron.vite.config.ts.
- Introduced a new debug script in package.json for easier debugging with remote inspection.

* docs: add debug section to development documentation

- Introduced a new section for debugging instructions, including the command to run the debug script and how to access the Chrome inspect tool.

* Feat: Supports sorting of textarea function buttons by dragging (#6268)

* feat(inputbar): add collapsible tools and localization for tool actions

* refactor(inputbar): simplify tool rendering logic in InputbarTools

* refactor(inputbar): enhance tool visibility logic and improve rendering structure in InputbarTools

* fix(inputbar): correct tooltip text for collapse/expand action in InputbarTools

* refactor(Inputbar): simplify Toolbar structure and improve styling

* chore: update release notes and fix various issues

- Updated release notes to include new DMXAPI service and fixed knowledge base search results issue.
- Enhanced drag-and-drop functionality for message selection and resolved memory exceptions in translation replies.
- Added styling adjustments for context menu and improved layout in CodeBlockView and MessageGroup components.

* chore: remove gitee provider

* refactor: streamline provider menu logic in settings

- Consolidated edit and delete menu items for providers into separate constants for improved readability and maintainability.
- Enhanced the logic for displaying menus based on provider status, ensuring correct options are presented for system providers and initial providers.

* chore(version): 1.3.12

* refactor: remove early return for empty MCP servers in MCPToolsButton

- Eliminated the conditional return for empty active MCP servers to streamline the component rendering logic.

* chore: update electron-builder configuration and package dependencies

- Modified electron-builder.yml to refine file inclusion/exclusion patterns.
- Removed and re-added dependencies in package.json for consistency and updated yarn.lock to reflect these changes.
- Cleaned up unnecessary entries in yarn.lock to streamline the dependency tree.

* test: more unit tests (#5130)

* test: more unit tests

- Adjust vitest configuration to handle main process and renderer process tests separately
- Add unit tests for main process utils
- Add unit tests for the renderer process
- Add three component tests to verify vitest usage: `DragableList`, `Scrollbar`, `QuickPanelView`
- Add an e2e startup test to verify playwright usage
- Extract `splitApiKeyString` and add tests for it
- Add and format some comments

* fix: mock individual properties

* test: add tests for CustomTag

* test: add tests for ExpandableText

* test: conditional rendering tooltip of tag

* chore: update dependencies

* feat: Selection Assistant / 划词助手 (#5900)

* feat(selection): implement selection assistant with toolbar and action management

- Added selection assistant functionality including a toolbar for actions.
- Introduced new settings for enabling/disabling the selection assistant and configuring its behavior.
- Implemented action items for built-in functionalities like translate, explain, and copy.
- Integrated selection service to manage selection events and actions.
- Updated localization files to support new selection assistant features in multiple languages.
- Added new components for action management and user interaction within the selection assistant.

* chore: update selection-hook to version 0.9.10 and exclude prebuilds from packaging

* fix: toolbar hiding

* feat: enhance error handling and service management in main index

* fix: improve logical coordinate handling in SelectionService

* fix: update URL loading and coordinate conversion in SelectionService

* fix: replace console.error with Logger for error handling in SelectionService

* refactor(SelectionService): enhance preloaded action window management

* chore(electron-builder): add filter for .node build files in configuration

* fix: toolbar position calculating for multi monitor

* fix: update selection assistant configuration and improve error handling in SelectionService

* fix: SelectionActionUserModal layout

* feat: add hints for custom search URL in multiple languages

* fix: update calculateToolbarPosition to ensure integer return type and round position values

* feat: add action window opacity setting and update related UI components

refactor: SelectionActionsList

* chore: enhance tooltip for trigger mode settings

* fix: console.log

* chore: update selection-hook to version 0.9.12

* fix: integrate language settings into selection components

* fix: filter out default assistant from user predefined assistants in selection modal

* chore: update selection-hook package version to 0.9.13

* chore: update selection-hook package version to 0.9.14

* chore:  removed unused dependencies to reduce size (#6464)

* chore: update package dependencies and refactor BackupManager to use fs.promises

- Removed unused dependencies: fetch-socks and fs-extra from package.json and yarn.lock.
- Updated BackupManager to utilize fs.promises for file system operations, improving consistency and modernizing the codebase.
- Ensured all file operations in BackupManager are handled with promises for better error handling and readability.

* chore: add fs-extra dependency and refactor BackupManager for improved file handling

- Added fs-extra to package.json and updated yarn.lock to enhance file system operations.
- Refactored BackupManager to utilize fs-extra methods for better readability and functionality, replacing fs.promises with fs-extra equivalents for directory and file operations.

---------

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>

* fix: cannot run from yarn dev (#6468)

* fix[SelectionAssistant]: remove console.log (#6474)

fix: remove console.log

* feat: integrate custom CSS support in SelectionAssistant

* fix: adjust order of tools in CodeToolbar constants for correct display

* chore: update @google/genai to version 1.0.1 and remove GeminiService references

- Updated the @google/genai dependency in package.json and yarn.lock to version 1.0.1.
- Removed the GeminiService and its related references from the codebase to streamline functionality.
- Introduced a new CacheService for managing cached data effectively.

* chore: update electron-builder configuration to refine file exclusion patterns

- Added exclusions for various distribution directories and module types to optimize the build process.
- Updated license file exclusions to be more inclusive of different casing variations.

* fix(MainTextBlock): adjust whiteSpace style for user role messages (#6501)

* feat: add title to selection action button in compact mode (#6498)

* feat: add title prototype to selection action button in compact mode

* fix: optimize the display name logic for action buttons in the selection toolbar

* fix: remove tiktoken

* chore: remove electron-icon-builder

* feat: painting aihubmix support model: gpt-image-1 (#6486)

* update select style

* add openai painting

* support base64 response

* update config

* fix upload preview bug

* fix remix default model

* fix history data

* feat: optimize structure

* fix history data

---------

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

* feat: add default painting provider support and update routing

- Introduced defaultPaintingProvider in settings to manage selected painting provider.
- Updated Sidebar component to reflect the selected painting provider in the route.
- Enhanced PaintingsRoutePage to dispatch the default painting provider based on URL parameters.
- Added PaintingProvider type to define available options for painting providers.

* feat: aihubmix painting support imagen (#6525)

* add imagen

* feat: support imagen model

* update proxy notice

---------

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

* refactor: TrayService & ConfigManager (#6526)

* refactor: TrayService

- Removed the App_RestartTray channel from IpcChannel and its usage in ipc.ts and preload/index.ts.
- Updated TrayService to handle configuration changes without the need for a restart.
- Enhanced ConfigManager to notify subscribers on language and quick assistant settings changes.
- Adjusted QuickAssistantSettings to close the mini window based on the quick assistant's enable state.

* refactor: enhance configuration management

- Updated ConfigManager to consolidate setting and notification logic into a single method, setAndNotify.
- Modified IPC handler to accept an additional parameter for notification control.
- Adjusted QuickAssistantSettings to utilize the new parameter for enabling notifications during configuration changes.

* fix: Optimize error message formatting (#5988)

* fix: Optimize error message formatting

* fix: improve error unit test

* refactor: simplify error handling in ErrorBlock component

- Replaced custom StyledAlert with a more streamlined Alert component for error messages.
- Reduced complexity by removing unnecessary JSX wrappers and improving readability.
- Adjusted styling for the Alert component to maintain visual consistency.

* fix: update error handling in ErrorBlock component

- Removed unnecessary message prop from Alert component to simplify error display.
- Maintained existing error handling logic while improving code clarity.

* feat[SelectionAssistant]: add faq&feedback link (#6531)

feat: add FAQ button to Selection Assistant settings

* chore: refine file exclusion patterns in electron-builder configuration (#6502)

- Updated exclusion patterns to ensure more comprehensive filtering of unnecessary files and directories during the build process.
- Added additional file types and configurations to the exclusion list for better optimization.

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>

* feat: support system prompt variables (#5995)

* feat: support system prompt variables

* feat: add tip

* fix ci test fail

* feat: Assistant add tag (#6065)

* feat: 添加助手标签显示逻辑
-增加助手的标签属性
-能够删除,修改,调整助手的标签
Signed-off-by: LeeSH <shuhao_lin@fzzixun.com>

* fix: 修复不能输入新增标签的问题

* feat: 完善不同状态下,提示文本展示

* feat: 调整标签展示逻辑
1,左键调整列表页展示逻辑
2,新增标签改为使用+号提示

* feat: 移除搜索栏可以直接增加tag值的功能
Signed-off-by: LeeSH <shuhao_lin@fzzixun.com>

* fix: 修复点击不能切换话题的bug

* feat:  调整了标签修改的交互
1,添加和管理分开处理
2,可以点击标签之间切换
3,点击删除可以之间移除所有关联助手的标签

tips:为了简单实现,标签本身不具有具体类,都是助手的子属性。所以如果关联的所有助手都没了该属性,标签会直接消失,而且标签目前无法排序

Signed-off-by: LeeSH <shuhao_lin@fzzixun.com>

* feat:优化标签管理
1,列表状态管理向上提,切换左侧列表不会影响原来的列表状态
2,标签名称增加最大宽度
3,标签内的助手顺序,参照原顺序排列
4,增加标签ui,提示语调整
5,标签管理ui,提示语调整
6,标签管理增加标签暂时态,防止误删没有其他助手的标签项的时候,标签在弹窗内整个消失(如果关闭弹窗那标签就无法找回)
7,如果没有标签的时候,右键仅展示添加标签

Signed-off-by: LeeSH <shuhao_lin@fzzixun.com>

---------

Signed-off-by: LeeSH <shuhao_lin@fzzixun.com>
Co-authored-by: linshuhao <nmnm1996>
Co-authored-by: Lee SH <shuhao_lin@fzzixun.com>

* fix: increase max cache limit and update slider marks in MiniAppSettings (#6414)

* fix: increase max cache limit and update slider marks in MiniAppSettings

* fix: adjust max cache limit and update slider marks in MiniAppSettings

* Update MiniAppSettings.tsx

---------

Co-authored-by: George Zhao <georgezhao@SKJLAB>

* fix: update TikToken implementation and remove js-tiktoken dependency

- Replaced the existing TikToken implementation with a placeholder error message indicating it is not implemented.
- Removed the js-tiktoken dependency from package.json to streamline the project.
- Updated yarn.lock to reflect changes in dependencies and checksums.

* fix: update token limits for Claude-4 models and refine reasoning checks in OpenAIProvider

- Adjusted max token limit for 'claude-sonnet-4' and 'claude-opus-4' models from 64000 to 32000.
- Simplified reasoning checks in OpenAIProvider to combine conditions for supported models, enhancing code clarity.

* fix[SelectionAssistant]: interrupting in terminal apps (#6549)

fix: interrupting in terminal apps

* fix: add custom parameters to OpenAI generateImageByChat requests

* chore(version): 1.4.0-rc.1

* fix: update artifact patterns in release workflow

- Modified the artifact patterns in the GitHub Actions release workflow to include 'dist/rc*.yml' for better versioning support.

* fix(OpenAIProvider): adjust reasoning effort setting to default to 'medium' when set to 'auto'

* fix: suppress exhaustive-deps warnings in multiple components

- Added eslint-disable comments for react-hooks/exhaustive-deps in CustomCollapse, DmxapiPage, SelectionActionApp, ActionGeneral, and ActionTranslate components to prevent warnings related to missing dependencies in useEffect hooks.

* feat(SelectionAssistant): App Filter / 应用筛选 (#6519)

feat: add filter mode and list functionality to selection assistant

- Introduced new filter mode options (default, whitelist, blacklist) for the selection assistant.
- Added methods to set and get filter mode and filter list in ConfigManager.
- Enhanced SelectionService to manage filter mode and list, affecting text selection processing.
- Updated UI components to allow users to configure filter settings.
- Localized new filter settings in multiple languages.

* fix: Repair abnormal line break display

* Revert "fix: Repair abnormal line break display"

This reverts commit 3d7fd5a30c.

* fix(HealthCheck): add a disclaimer (#6570)

* fix(HealthCheck): add a disclaimer

* fix: remove duplicates in zh-tw.json

* refactor: chat navigation triggering (#6576)

* refactor(ChatNavigation): move down the navigation bar

* refactor: attach listeners to MessagesContainer for better triggering experience

* refactor: add delay to Tooltips

* refactor: exclude some toolbars areas from triggering

* fix(style): global cursor style for scrollbar thumb

* fix(SvgPreview): dragging and sanitizing (#6568)

* fix(SvgPreview): dragging

* fix(SvgPreview): sanitize svg content

* feat(SelectionAssistant): support Shift+Click & enhance Ctrl key mode (#6566)

* feat: add filter mode and list functionality to selection assistant

- Introduced new filter mode options (default, whitelist, blacklist) for the selection assistant.
- Added methods to set and get filter mode and filter list in ConfigManager.
- Enhanced SelectionService to manage filter mode and list, affecting text selection processing.
- Updated UI components to allow users to configure filter settings.
- Localized new filter settings in multiple languages.

* feat: support Shift+Click & enhance Ctrl key method

* fix: remove comments

* fix(PROVIDER_CONFIG): update website URLs from ppinfra.com to ppio.cn

* fix(provider): update Qiniu's name and logo, fix gitee typo (#6593)

* fix(provider): update Qiniu's name and logo

* fix(provider): typo

* feat(theme): 用户自定义主题色 (#4613)

* feat(theme): 用户自定义主题色

* refactor(QuickPanel): integrate user theme for dynamic color handling

* refactor(ThemeProvider): separate user theme initialization into its own useEffect

* refactor(useUserTheme): move theme initialization logic into a dedicated function

* feat(settings): enhance color picker with presets and update styles for ant-collapse

* feat: Refactor theme management to use userTheme object for colorPrimary

* refactor(Chat, Messages): simplify maxWidth calculations and remove unused showAssistants variable

* refactor(Scrollbar, Chat, Messages): improve scroll handling and clean up component structure

* refactor(Messages): enhance message rendering and navigation exclusions

- Updated styles for message content and group containers to improve layout.
- Added new selectors to exclude additional elements from navigation.
- Implemented conditional rendering for mentions in message content.
- Simplified token display logic in message tokens component.

* feat(DisplaySettings): add theme color presets and zoom settings

- Introduced a new color selection feature in DisplaySettings, allowing users to choose from predefined theme color presets.
- Added a dedicated section for zoom settings in the DisplaySettings component, enhancing user customization options.
- Updated localization files to include new zoom settings titles in multiple languages.

* feat: 调整分组的效果 (#6561)

1,未分组标签改为未分组
2,列表展示效果持久化
3,增加一个管理列表展示效过的store

Co-authored-by: linshuhao <nmnm1996>

* refactor: standardize variable naming and improve tag calculation logic

- Renamed variables for consistency, changing `AssistantsTabSortType` to `assistantsTabSortType`.
- Refactored tag calculation in `useTags` to utilize `uniq` and `flatMap` for better performance and readability.
- Updated localization files to remove unnecessary characters in quick trigger messages.
- Enhanced the `AssistantItem` component by extracting menu item creation logic and sorting functions for better maintainability.

* test(QuickPanelView): integrate Redux store into tests and refactor rendering logic

- Added a mock Redux store to the QuickPanelView tests for better state management.
- Refactored test rendering to use a wrapper function for consistent provider usage.
- Updated Scrollbar test to verify throttle behavior with new delay and options.

* revert: fix: english serif font rendering issue #6224

This reverts commit 5dd508b4f4.

* feat(SelectionAssistant): add "Remember Window Size" functionality

- Introduced a new setting to remember the last adjusted size of the action window.
- Updated ConfigManager, SelectionService, and IPC channels to handle the new feature.
- Enhanced UI components to allow users to toggle the "Remember Size" option.
- Localized the new setting in multiple languages.

* feat: add "Regenerate" in action window

* chore(version): 1.4.0-rc.2

* feat(SelectionAssistant): improve selection in browsers and pdf readers (#6618)

fix: improve browsers and pdf readers selection

* refactor(Scrollbar, Messages): clean up scrollbar component and styles

- Removed unused 'right' prop from Scrollbar component.
- Increased scrolling timeout duration for better user experience.
- Updated scrollbar styles to simplify color handling.
- Adjusted Messages component to remove unnecessary props and added margin for better layout.
- Added responsive styles to CitationBlock for improved mobile display.

* fix: setting tab font size

* feat: dmxapi generate multiple image (#6632)

* chore(version): 1.3.8

* 新增自动添加

* 图片自增功能优化

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>

* refactor(SvgPreview): use shadow dom

* test(scrollbar): fix snapshot mismatched

* fix(MainTextBlock): update whiteSpace style for user messages to 'pre-wrap'

* fix(Messages, WebSearchProviderSetting): remove unused variables and update provider logo styling

* lint: fix eslint error and build:check

* feat: improve translation setting logic (#6463)

* feat: add auto-detect language option and improve translation logic

* feat: remove auto-detect language option and add bidirectional translation settings

* fix: remove unused model removal function from TranslatePage component

* feat: add language detection and bidirectional translation utilities

* feat: update translation settings to include bidirectional translation tips and remove deprecated options

* fix: improve interaction

* fix: change cld3-asm to franc

* fix: ui/ux

* fix: change eslint

* fix: update

* Revert "fix: update"

This reverts commit 1126a5cce9.

* Reapply "fix: update"

This reverts commit 82b7890f92.

* fix: setloading missing

* refactor: Theme improve (#6619)

* refactor(IpcChannel): rename theme change event and streamline theme handling

- Updated the IpcChannel enum to rename 'theme:change' to 'theme:updated' for clarity.
- Refactored theme handling in ipc.ts to utilize a new ThemeService, simplifying theme updates and event broadcasting.
- Adjusted various components to consistently use the updated theme variable naming convention.

* refactor(Theme): standardize theme handling across components

- Updated theme retrieval to use 'actualTheme' instead of 'theme' for consistency.
- Changed default theme setting from 'auto' to 'system' in ConfigManager and related components.
- Adjusted theme handling in various components to reflect the new naming convention and ensure proper theme application.

* fix(Theme): improve theme handling and migration logic

- Added a console log for debugging theme transitions in ThemeProvider.
- Updated ThemeService to ensure theme is set correctly when changed.
- Incremented version number in store configuration to reflect changes.
- Enhanced migration logic to convert 'auto' theme setting to 'system' for better consistency.

* feat(Theme): add getTheme IPC channel and improve theme management

- Introduced a new IPC channel 'App_GetTheme' to retrieve the current theme.
- Updated ThemeService to include a method for getting the current theme.
- Refactored theme initialization in WindowService to ensure proper theme setup.
- Enhanced theme handling in various components to utilize the new theme retrieval method.

* fix(ThemeService): improve theme initialization and retrieval logic

- Set default theme to 'system' and updated theme initialization to handle legacy versions.
- Enhanced getTheme method to return both the current theme and the actual theme based on nativeTheme settings.
- Removed redundant initTheme method from ThemeService and ensured themeService is imported in WindowService for proper initialization.
- Updated ThemeProvider to handle the new structure of the theme retrieval response.

* refactor(Settings): remove theme management from settings

- Eliminated theme-related state and actions from the settings slice.
- Updated useSettings hook to remove theme handling functionality.
- Cleaned up imports by removing unused ThemeMode type.

* refactor(Theme): update theme retrieval in GeneralSettings and HomeWindow

- Restored theme retrieval in GeneralSettings and HomeWindow components.
- Adjusted imports to ensure proper theme management.
- Updated theme condition checks to utilize the ThemeMode enumeration for consistency.

* refactor(Theme): update theme terminology and retrieval in Sidebar and DisplaySettings

- Changed theme label from 'auto' to 'system' in multiple localization files for consistency.
- Updated Sidebar component to reflect the new theme terminology.
- Adjusted DisplaySettings to display the updated theme label.

* refactor(ThemeProvider): initialize theme state from API response

* refactor(ThemeProvider): reset theme state to default values and streamline initialization logic

* refactor(Theme): enhance theme management by incorporating 'system' mode and updating state handling

- Updated ThemeService to include 'system' as a valid theme option.
- Refactored ThemeProvider to utilize useSettings for theme state management and ensure proper initialization.
- Adjusted useSettings to include theme setting functionality.
- Modified settings slice to manage theme state effectively.

* refactor(WindowService, ThemeProvider, Messages, HomeWindow): streamline imports and clean up unused variables

- Removed duplicate import of ThemeService in WindowService.
- Adjusted import order in ThemeProvider for clarity.
- Simplified useSettings destructuring in Messages component.
- Cleaned up unused ThemeMode import in HomeWindow.

* refactor(Theme): standardize theme usage across components by replacing 'actualTheme' with 'theme'

- Updated components to consistently use 'theme' instead of 'actualTheme' for better clarity and maintainability.
- Adjusted ThemeProvider to reflect changes in theme state management.
- Ensured all relevant components are aligned with the new theme structure.

* refactor(Theme): remove unused theme retrieval functionality

- Eliminated the App_GetTheme channel and associated methods from ThemeService and IPC handling.
- Updated components to use the new theme structure, replacing 'actualTheme' with 'settedTheme' for consistency.
- Ensured all theme-related functionalities are streamlined and aligned with the latest changes.

* refactor(Theme): update theme variable usage in ChatFlowHistory and GeneralSettings

- Replaced 'theme' with 'settedTheme' in ChatFlowHistory for consistency with recent theme structure changes.
- Simplified theme destructuring in GeneralSettings by removing unused 'themeMode' variable.
- Ensured alignment with the latest theme management updates across components.

* refactor(Theme): update theme variable in GeneralSettings component

- Replaced 'themeMode' with 'theme' in GeneralSettings for consistency with recent theme structure changes.
- Ensured alignment with the latest theme management updates across components.

---------

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>

* fix: interrupting in shell and improve pdf readers

* fix: The edit button cannot be used after using MCP. 修复对话中使用 MCP 后编辑按钮消失的问题 (#6623)

fix: The edit button cannot be used after using MCP.

* chore(version): 1.4.0-rc.3

* fix: thinking time reset (#6665)

* fix: thinking time reset

* fix: update theme listener to properly handle theme updates

---------

Co-authored-by: Pleasurecruise <3196812536@qq.com>

* feat(SelectionAssistant): predefined apps filter list (#6662)

* feat: predefined app blacklist

* fix

* fix

* fix

* fix: improve filter list processing in SelectionFilterListModal

* chore(version): 1.4.0

* refactor: sort mentioned models in QuickPanel (#6666)

* fix(OpenAIProvider): prevent atob error with non-base64 image URLs (#6673)

Signed-off-by: Chan Lee <Leetimemp@gmail.com>

* fix: replace franc with franc-min for improved performance

* fix: provider o3 docs not found

* feat(SelectionService): enhance trigger mode handling and update predefined blacklist

* fix: adjust sidebar icon margins based on fullscreen state

* test: more unit tests for message rendering (#6663)

* refactor(encodeHTML): remove duplicate definition

* test(Scrollbar): update snapshot

* test: add more tests

Add tests for
- MainTextBlock
- ThinkingBlock
- Markdown
- CitationTooltip

* fix: token usage not updated after editing message (#6725)

fix: update token usage when edit message

* fix: qwen3 cannot name a topic (#6722)

* fix: qwen3 cannot name a topic

* feat: Display error message when topic naming fails

* fix: assistant emoji displaying incorrectly in specific situations #6243 (#6280)

* fix:  ssistant emoji displaying incorrectly in specific situations

* chore: remove unuse import

* fix: ensure default emoji

* fix: remove redundant min-width in AssistantItem and EmojiIcon components; enhance emoji click handling

---------

Co-authored-by: 自由的世界人 <3196812536@qq.com>

* fix: mcp uv&bun installation status icon in nav bar not updated after… (#6654)

fix: mcp uv&bun installation status icon in nav bar not updated after installed

Signed-off-by: aprilandjan <merlin.ye@qq.com>

* hotfix(OpenAIProvider): remove redundant 'unkown' chunk (#6737)

fix(OpenAIProvider): remove redundant 'unknown' yield case in chunk processing

* hotfix: update qwen3 model identification logic to use startsWith for im… (#6738)

fix: update qwen3 model identification logic to use startsWith for improved accuracy

* feat(AppUpdater): implement localized update dialog (#6742)

feat(AppUpdater): implement localized update dialog with new translations for multiple languages

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>

* Fix: outdated provider websites and models (#6766)

* fix: inappropriate provider websites (openrouter, grok)

* fix: outdated model list (grok)

* chore(gitignore): exclude cursor settings (#6779)

* fix: prevent message overflow when minimized width (#6775)

* refactor(BackupManager, WebDav): streamline WebDAV client initialization and enhance directory listing functionality (#6784)

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>

* fix(SelectionAssistant): customCSS should not override background (#6746)

fix: customCSS should not override background

* feat(SelectionAssistant): Smart Translation ( aka BiDirectionTranslate) (#6715)

* feat(Translation): enhance translation functionality and UI improvements

- Added secondary text color variables in color.scss for better UI contrast.
- Updated translation configuration to include language codes for better language handling.
- Enhanced translation UI with new language selection options and improved loading indicators.
- Implemented smart translation tips in multiple language JSON files for user guidance.
- Refactored translation logic to streamline message processing and error handling.

* feat(Translation): expand language options and update localization files

- Added new languages (Polish, Turkish, Thai, Vietnamese, Indonesian, Urdu, Malay) to translation options in translate.ts.
- Updated localization JSON files (en-us, ja-jp, ru-ru, zh-cn, zh-tw) to include translations for the new languages.
- Enhanced language detection logic in translate.ts to support new language codes.

* fix: chat navigation triggering (#6774)

* fix: exclude MessageEditor

* fix: more accurate triggering area

* fix(ci): Update the nightly-build workflow (#6791)

Update the branch name from `develop` to `main`

* fix: optimize multilingual display of documents (#6793)

Update Sidebar.tsx

* fix: correct variable name obsidianVault in Obsidian export (#6796)

* fix: transparent window flashing when show (#6755)

* fix: avoid SelectionAssistant toolbar flashing

* add comments

* feat(SelectionAssistant): fullscreen game/presentation mode

* fix: codeblock overflow in bubble style (#6773)

* refactor: revert CodeBlockView style change

* fix: codeblock width and overflow

* refactor: improve CodeEditor border

* revert: context-menu-container width for message group

* fix(MermaidPreview): debounce mermaid rendering to alleviate flickering (#6675)

* hotfix: gemini auto thinking (#6810)

* fix(SelectionAssistant): JetBrains IDEs, Remote desktop, Gaming, PDF views, etc (#6809)

fix: jetbrains ides, remote desktop, pdf views, etcs

* fix: use monospace font for theme colorpicker (#6816)

* fix(AnthropicProvider): update usage and metrics handling to prevent TypeError  (#6813)

* fix: sync active topic after rename (#6804)

Co-authored-by: Chen Tao <70054568+eeee0717@users.noreply.github.com>

* fix: OpenAI provider api check doesn't handle error (#6769)

* feat(constants): expand supported file extensions and categorize text… (#6815)

* feat(constants): expand supported file extensions and categorize text file types

* refactor(constants): remove binary file extensions

* refactor(constants): remove Xcode project

* feat(Settings): Add token count display toggle (#6772)

* feat(Settings): add token count toggle

* fix(i18n): update token usage messages for zh-cn and zh-tw locales

* fix(InstallNpxUv): optimize checkBinaries function with useCallback for better performance

---------

Co-authored-by: Pleasurecruise <3196812536@qq.com>

* hotfix: ensure show token usage setting defaults to true (#6828)

Hotfix: ensure show token usage setting defaults to true

* fix(SelectionAssistant): ignore ctrl pressing when user is zooming in/out (#6822)

* fix(SelectionService): ignore ctrl pressing when user is zomming in/out

* chore: rename function

* fix: reset listener status

* refactor: enhance export functions (#5854)

* feat(markdown-export): add option to show model name in export

* refactor(export): Refactor the Obsidian export modal to Ant Design style

* refactor(obsidian-export): export to obsidian using markdown interface & support COT

* feat(markdown-export): optimize COT export style, support export model & provider name

Add a new setting to toggle displaying the model provider alongside the
model name in markdown exports. Update the export logic to include the
provider name when enabled, improving context and clarity of exported
messages. Also fix invalid filename character removal regex for Mac.

* feat(export): add option to export reasoning in Joplin notes

Introduce a new setting to toggle exporting reasoning details when
exporting topics or messages to Joplin. Update the export function to
handle raw messages and convert them to markdown with or without
reasoning based on the setting. This improves the export feature by
allowing users to include more detailed context in their Joplin notes.

* feat(export): update i18n for new export options

* fix(settings): remove duplicate showModelNameInMarkdown state

* feat(export): add CoT export for notion & optmize notion export

* feat(export): update Notion settings i18n

* fix(utils): correct citation markdown formatting

Swap citation title and URL positions in markdown links to ensure
the link text displays the title (or URL if title is missing) and
the link points to the correct URL. This improves citation clarity.

* fix: add blank lines between reasoning summary parts (#6827)

Co-authored-by: Chen Tao <70054568+eeee0717@users.noreply.github.com>

* support tokenflux image generation for [Flux.1 Kontext] (#6705)

* Add support for TokenFlux image generation service

This commit integrates TokenFlux as a new painting provider with dynamic
form generation based on model schemas, real-time generation polling,
and full painting history management.

Key features:
- Dynamic form rendering from JSON schema input parameters
- Model selection with pricing display
- Real-time generation status polling
- Integration with existing painting workflow and file management
- Provider-specific painting state management

The implementation follows existing patterns from other painting pages
while adding TokenFlux-specific functionality like schema-based form
generation and asynchronous polling for generation results.

* Add image upload support and comparison view to TokenFlux

Implements file upload handling for image parameters with base64
conversion, random seed generation, and side-by-side comparison
layout when input images are present.

* Refactor TokenFlux to use service class and components

Extract form rendering logic to DynamicFormRender component and API
logic to TokenFluxService class. Simplifies the main component by
removing duplicate code for model fetching, image generation polling,
and form field rendering.

* Refactor TokenFlux to fix state management and polling

- Change painting field from modelId to model for consistency
- Fix updatePaintingState to use functional state updates
- Add automatic polling for in-progress generations on mount
- Group models by provider in the selection dropdown
- Separate prompt from other input params in form data handling
- Improve error handling in the paintings store

* Auto-select first model when models are loaded

* Add image generation UI localization strings

Add translation keys for model selection, input parameters, image
labels, pricing display, and form validation across all supported
locales (en-us, ja-jp, ru-ru, zh-cn, zh-tw). Update TokenFluxPage
component to use localized strings instead of hardcoded English text.

* fix: Add a right border to the first child of the ImageComparisonSection

* style: Remove padding from UploadedImageContainer in TokenFluxPage

* feat: Implement caching for TokenFlux model fetching and update image upload handling

* feat: Enhance localization support by adding language context handling in TokenFluxPage

* refactor: Simplify layout structure in TokenFluxPage by removing unnecessary SectionGroup components and improving section title styling

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>

* feat: optimize UI interface display

* refactor(i18n): reorganize Notion settings localization strings in ja-jp.json

This commit restructures the localization strings for Notion settings in the Japanese language file, moving them from a nested structure to a more accessible format. This change improves clarity and maintainability of the localization data.

* chore: update OpenAI package to version 5.1.0 and adjust related patches (#6838)

* chore: update OpenAI package to version 5.1.0 and adjust related patches

- Updated OpenAI dependency from version 4.96.0 to 5.1.0 in package.json and yarn.lock.
- Removed obsolete patch for OpenAI 4.96.0 and added new patch for OpenAI 5.1.0.
- Adjusted types for image handling in OpenAIResponseProvider to use Uploadable instead of FileLike.
- Minor code refactoring for better clarity and maintainability.

* refactor(OpenAIResponseProvider): remove logging for image generation process

* fix(AssistantsTab): remove untagged group title

This commit updates the AssistantsTab component to only display group titles for tagged assistants, excluding the 'untagged' category. This change enhances the UI by reducing clutter and improving clarity in the display of assistant groups.

* chore: remove unused Delete tokenflux_painting_page.md (#6840)

Delete tokenflux_painting_page.md

* fix: Improve the switching logic in multi-tab state

* chore(version): 1.4.1

* 更新版本

* 解决冲突

---------

Signed-off-by: jtsang4 <info@jtsang.me>
Signed-off-by: LeeSH <shuhao_lin@fzzixun.com>
Signed-off-by: Chan Lee <Leetimemp@gmail.com>
Signed-off-by: aprilandjan <merlin.ye@qq.com>
Co-authored-by: Morax <100508620+fzlzjerry@users.noreply.github.com>
Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
Co-authored-by: suyao <sy20010504@gmail.com>
Co-authored-by: jwcrystal <121911854+jwcrystal@users.noreply.github.com>
Co-authored-by: beyondkmp <beyondkmp@gmail.com>
Co-authored-by: beyondkmp <beyondkmkp@gmail.com>
Co-authored-by: one <wangan.cs@gmail.com>
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
Co-authored-by: Wei <huangwei@burncloud.com>
Co-authored-by: karl <jialongkou@163.com>
Co-authored-by: Zhaker <0x149527@gmail.com>
Co-authored-by: George Zhao <georgezhao@SKJLAB>
Co-authored-by: LiuVaayne <10231735+vaayne@users.noreply.github.com>
Co-authored-by: 自由的世界人 <3196812536@qq.com>
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: chenxue <DDU1222@users.noreply.github.com>
Co-authored-by: zhaochenxue <zhaochenxue@bixin.cn>
Co-authored-by: MyPrototypeWhat <43230886+MyPrototypeWhat@users.noreply.github.com>
Co-authored-by: hutchisr <42283663+hutchisr@users.noreply.github.com>
Co-authored-by: jtsang4 <info@jtsang.me>
Co-authored-by: Caelan <79105826+jin-wang-c@users.noreply.github.com>
Co-authored-by: w <772814125@qq.com>
Co-authored-by: iola1999 <iola1999@foxmail.com>
Co-authored-by: fullex <0xfullex@gmail.com>
Co-authored-by: Konv Suu <2583695112@qq.com>
Co-authored-by: kanweiwei <kanweiwei@nutstore.net>
Co-authored-by: Roland <shlroland1995@gmail.com>
Co-authored-by: Teo <cheesen.xu@gmail.com>
Co-authored-by: shiquda <czy1847259360@gmail.com>
Co-authored-by: purefkh <purefkh@gmail.com>
Co-authored-by: nmnmtttt <34001432+nmnmtttt@users.noreply.github.com>
Co-authored-by: Lee SH <shuhao_lin@fzzixun.com>
Co-authored-by: George Zhao <38124587+CreatorZZY@users.noreply.github.com>
Co-authored-by: FunJim <xunjin.zheng@outlook.com>
Co-authored-by: stevending1st <stevending1st@163.com>
Co-authored-by: Alain <yinxulai@hotmail.com>
Co-authored-by: MyPrototypeWhat <daoquqiexing@gmail.com>
Co-authored-by: Rudbeckia.hirta.L <83773602@qq.com>
Co-authored-by: Doekin <105162544+Doekin@users.noreply.github.com>
Co-authored-by: icarus <eurfelux@gmail.com>
Co-authored-by: Wang Jiyuan <59059173+EurFelux@users.noreply.github.com>
Co-authored-by: May <aprilandjan@users.noreply.github.com>
Co-authored-by: Lucas <lucas04@foxmail.com>
Co-authored-by: Murphy <69335326+MurphyLo@users.noreply.github.com>
Co-authored-by: Chen Tao <70054568+eeee0717@users.noreply.github.com>
Co-authored-by: 熊可狸 <me@korin.im>
Co-authored-by: George·Dong <98630204+GeorgeDong32@users.noreply.github.com>
2025-06-24 13:32:15 +08:00
kangfenmao
8fd57d0271 refactor: windows7 support 2025-05-07 10:14:27 +08:00
581 changed files with 57394 additions and 20444 deletions

86
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,86 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
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
commit-message:
prefix: "ci"
include: "scope"
groups:
github-actions:
patterns:
- "*"
update-types:
- "minor"
- "patch"

View File

@@ -54,5 +54,5 @@ jobs:
days-before-pr-close: -1 # Completely disable closing for PRs
# Temporary to reduce the huge issues number
operations-per-run: 100
operations-per-run: 1000
debug-only: false

View File

@@ -52,6 +52,8 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
with:
ref: main
- name: Install Node.js
uses: actions/setup-node@v4

View File

@@ -5,6 +5,7 @@ on:
pull_request:
branches:
- main
- develop
jobs:
build:

View File

@@ -26,6 +26,8 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
with:
ref: main
- name: Get release tag
id: get-tag
@@ -111,5 +113,5 @@ jobs:
allowUpdates: true
makeLatest: false
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/*.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 }}

10
.gitignore vendored
View File

@@ -45,9 +45,15 @@ stats.html
local
.aider*
.cursorrules
.cursor/rules
.cursor/*
# test
# vitest
coverage
.vitest-cache
vitest.config.*.timestamp-*
# playwright
playwright-report
test-results
YOUR_MEMORY_FILE_PATH

View File

@@ -0,0 +1,71 @@
diff --git a/dist/utils/tiktoken.cjs b/dist/utils/tiktoken.cjs
index 973b0d0e75aeaf8de579419af31b879b32975413..f23c7caa8b9dc8bd404132725346a4786f6b278b 100644
--- a/dist/utils/tiktoken.cjs
+++ b/dist/utils/tiktoken.cjs
@@ -1,25 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.encodingForModel = exports.getEncoding = void 0;
-const lite_1 = require("js-tiktoken/lite");
const async_caller_js_1 = require("./async_caller.cjs");
const cache = {};
const caller = /* #__PURE__ */ new async_caller_js_1.AsyncCaller({});
async function getEncoding(encoding) {
- if (!(encoding in cache)) {
- cache[encoding] = caller
- .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`)
- .then((res) => res.json())
- .then((data) => new lite_1.Tiktoken(data))
- .catch((e) => {
- delete cache[encoding];
- throw e;
- });
- }
- return await cache[encoding];
+ throw new Error("TikToken Not implemented");
}
exports.getEncoding = getEncoding;
async function encodingForModel(model) {
- return getEncoding((0, lite_1.getEncodingNameForModel)(model));
+ throw new Error("TikToken Not implemented");
}
exports.encodingForModel = encodingForModel;
diff --git a/dist/utils/tiktoken.js b/dist/utils/tiktoken.js
index 8e41ee6f00f2f9c7fa2c59fa2b2f4297634b97aa..aa5f314a6349ad0d1c5aea8631a56aad099176e0 100644
--- a/dist/utils/tiktoken.js
+++ b/dist/utils/tiktoken.js
@@ -1,20 +1,9 @@
-import { Tiktoken, getEncodingNameForModel, } from "js-tiktoken/lite";
import { AsyncCaller } from "./async_caller.js";
const cache = {};
const caller = /* #__PURE__ */ new AsyncCaller({});
export async function getEncoding(encoding) {
- if (!(encoding in cache)) {
- cache[encoding] = caller
- .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`)
- .then((res) => res.json())
- .then((data) => new Tiktoken(data))
- .catch((e) => {
- delete cache[encoding];
- throw e;
- });
- }
- return await cache[encoding];
+ throw new Error("TikToken Not implemented");
}
export async function encodingForModel(model) {
- return getEncoding(getEncodingNameForModel(model));
+ throw new Error("TikToken Not implemented");
}
diff --git a/package.json b/package.json
index 36072aecf700fca1bc49832a19be832eca726103..90b8922fba1c3d1b26f78477c891b07816d6238a 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,6 @@
"ansi-styles": "^5.0.0",
"camelcase": "6",
"decamelize": "1.2.0",
- "js-tiktoken": "^1.0.12",
"langsmith": ">=0.2.8 <0.4.0",
"mustache": "^4.2.0",
"p-queue": "^6.6.2",

View File

@@ -1,92 +0,0 @@
diff --git a/out/electron/ElectronFramework.js b/out/electron/ElectronFramework.js
index 5a4b4546870ee9e770d5a50d79790d39baabd268..3f0ac05dfd6bbaeaf5f834341a823718bd10f55c 100644
--- a/out/electron/ElectronFramework.js
+++ b/out/electron/ElectronFramework.js
@@ -55,26 +55,27 @@ async function removeUnusedLanguagesIfNeeded(options) {
if (!wantedLanguages.length) {
return;
}
- const { dir, langFileExt } = getLocalesConfig(options);
+ const { dirs, langFileExt } = getLocalesConfig(options);
// noinspection SpellCheckingInspection
- await (0, tiny_async_pool_1.default)(builder_util_1.MAX_FILE_REQUESTS, await (0, fs_extra_1.readdir)(dir), async (file) => {
- if (!file.endsWith(langFileExt)) {
+ const deletedFiles = async (dir) => {
+ await (0, tiny_async_pool_1.default)(builder_util_1.MAX_FILE_REQUESTS, await (0, fs_extra_1.readdir)(dir), async (file) => {
+ if (!file.endsWith(langFileExt)) {
+ return;
+ }
+ const language = file.substring(0, file.length - langFileExt.length);
+ if (!wantedLanguages.includes(language)) {
+ return fs.rm(path.join(dir, file), { recursive: true, force: true });
+ }
return;
- }
- const language = file.substring(0, file.length - langFileExt.length);
- if (!wantedLanguages.includes(language)) {
- return fs.rm(path.join(dir, file), { recursive: true, force: true });
- }
- return;
- });
+ });
+ };
+ await Promise.all(dirs.map(deletedFiles));
function getLocalesConfig(options) {
const { appOutDir, packager } = options;
if (packager.platform === index_1.Platform.MAC) {
- return { dir: packager.getResourcesDir(appOutDir), langFileExt: ".lproj" };
- }
- else {
- return { dir: path.join(packager.getResourcesDir(appOutDir), "..", "locales"), langFileExt: ".pak" };
+ return { dirs: [packager.getResourcesDir(appOutDir), packager.getMacOsElectronFrameworkResourcesDir(appOutDir)], langFileExt: ".lproj" };
}
+ return { dirs: [path.join(packager.getResourcesDir(appOutDir), "..", "locales")], langFileExt: ".pak" };
}
}
class ElectronFramework {
diff --git a/out/node-module-collector/index.d.ts b/out/node-module-collector/index.d.ts
index 8e808be0fa0d5971b9f9605c8eb88f71630e34b7..1b97dccd8a150a67c4312d2ba4757960e624045b 100644
--- a/out/node-module-collector/index.d.ts
+++ b/out/node-module-collector/index.d.ts
@@ -2,6 +2,6 @@ import { NpmNodeModulesCollector } from "./npmNodeModulesCollector";
import { PnpmNodeModulesCollector } from "./pnpmNodeModulesCollector";
import { detect, PM, getPackageManagerVersion } from "./packageManager";
import { NodeModuleInfo } from "./types";
-export declare function getCollectorByPackageManager(rootDir: string): Promise<NpmNodeModulesCollector | PnpmNodeModulesCollector>;
+export declare function getCollectorByPackageManager(rootDir: string): Promise<PnpmNodeModulesCollector | NpmNodeModulesCollector>;
export declare function getNodeModules(rootDir: string): Promise<NodeModuleInfo[]>;
export { detect, getPackageManagerVersion, PM };
diff --git a/out/platformPackager.d.ts b/out/platformPackager.d.ts
index 2df1ba2725c54c7b0e8fed67ab52e94f0cdb17bc..c7ff756564cfd216d2c7d8f72f367527010c06f9 100644
--- a/out/platformPackager.d.ts
+++ b/out/platformPackager.d.ts
@@ -67,6 +67,7 @@ export declare abstract class PlatformPackager<DC extends PlatformSpecificBuildO
getElectronSrcDir(dist: string): string;
getElectronDestinationDir(appOutDir: string): string;
getResourcesDir(appOutDir: string): string;
+ getMacOsElectronFrameworkResourcesDir(appOutDir: string): string;
getMacOsResourcesDir(appOutDir: string): string;
private checkFileInPackage;
private sanityCheckPackage;
diff --git a/out/platformPackager.js b/out/platformPackager.js
index 6f799ce0d1cdb5f0b18a9c8187b2db84b3567aa9..879248e6c6786d3473e1a80e3930d3a8d0190aab 100644
--- a/out/platformPackager.js
+++ b/out/platformPackager.js
@@ -465,12 +465,13 @@ class PlatformPackager {
if (this.platform === index_1.Platform.MAC) {
return this.getMacOsResourcesDir(appOutDir);
}
- else if ((0, Framework_1.isElectronBased)(this.info.framework)) {
+ if ((0, Framework_1.isElectronBased)(this.info.framework)) {
return path.join(appOutDir, "resources");
}
- else {
- return appOutDir;
- }
+ return appOutDir;
+ }
+ getMacOsElectronFrameworkResourcesDir(appOutDir) {
+ return path.join(appOutDir, `${this.appInfo.productFilename}.app`, "Contents", "Frameworks", "Electron Framework.framework", "Resources");
}
getMacOsResourcesDir(appOutDir) {
return path.join(appOutDir, `${this.appInfo.productFilename}.app`, "Contents", "Resources");

View File

@@ -0,0 +1,159 @@
diff --git a/out/macPackager.js b/out/macPackager.js
index 852f6c4d16f86a7bb8a78bf1ed5a14647a279aa1..60e7f5f16a844541eb1909b215fcda1811e924b8 100644
--- a/out/macPackager.js
+++ b/out/macPackager.js
@@ -423,7 +423,7 @@ class MacPackager extends platformPackager_1.PlatformPackager {
}
appPlist.CFBundleName = appInfo.productName;
appPlist.CFBundleDisplayName = appInfo.productName;
- const minimumSystemVersion = this.platformSpecificBuildOptions.minimumSystemVersion;
+ const minimumSystemVersion = this.platformSpecificBuildOptions.LSMinimumSystemVersion;
if (minimumSystemVersion != null) {
appPlist.LSMinimumSystemVersion = minimumSystemVersion;
}
diff --git a/out/publish/updateInfoBuilder.js b/out/publish/updateInfoBuilder.js
index 7924c5b47d01f8dfccccb8f46658015fa66da1f7..1a1588923c3939ae1297b87931ba83f0ebc052d8 100644
--- a/out/publish/updateInfoBuilder.js
+++ b/out/publish/updateInfoBuilder.js
@@ -133,6 +133,7 @@ async function createUpdateInfo(version, event, releaseInfo) {
const customUpdateInfo = event.updateInfo;
const url = path.basename(event.file);
const sha512 = (customUpdateInfo == null ? null : customUpdateInfo.sha512) || (await (0, hash_1.hashFile)(event.file));
+ const minimumSystemVersion = customUpdateInfo == null ? null : customUpdateInfo.minimumSystemVersion;
const files = [{ url, sha512 }];
const result = {
// @ts-ignore
@@ -143,9 +144,13 @@ async function createUpdateInfo(version, event, releaseInfo) {
path: url /* backward compatibility, electron-updater 1.x - electron-updater 2.15.0 */,
// @ts-ignore
sha512 /* backward compatibility, electron-updater 1.x - electron-updater 2.15.0 */,
+ minimumSystemVersion,
...releaseInfo,
};
if (customUpdateInfo != null) {
+ if (customUpdateInfo.minimumSystemVersion) {
+ delete customUpdateInfo.minimumSystemVersion;
+ }
// file info or nsis web installer packages info
Object.assign("sha512" in customUpdateInfo ? files[0] : result, customUpdateInfo);
}
diff --git a/out/targets/ArchiveTarget.js b/out/targets/ArchiveTarget.js
index e1f52a5fa86fff6643b2e57eaf2af318d541f865..47cc347f154a24b365e70ae5e1f6d309f3582ed0 100644
--- a/out/targets/ArchiveTarget.js
+++ b/out/targets/ArchiveTarget.js
@@ -69,6 +69,9 @@ class ArchiveTarget extends core_1.Target {
}
}
}
+ if (updateInfo != null && this.packager.platformSpecificBuildOptions.minimumSystemVersion) {
+ updateInfo.minimumSystemVersion = this.packager.platformSpecificBuildOptions.minimumSystemVersion;
+ }
await packager.info.emitArtifactBuildCompleted({
updateInfo,
file: artifactPath,
diff --git a/out/targets/nsis/NsisTarget.js b/out/targets/nsis/NsisTarget.js
index e8bd7bb46c8a54b3f55cf3a853ef924195271e01..f956e9f3fe9eb903c78aef3502553b01de4b89b1 100644
--- a/out/targets/nsis/NsisTarget.js
+++ b/out/targets/nsis/NsisTarget.js
@@ -305,6 +305,9 @@ class NsisTarget extends core_1.Target {
if (updateInfo != null && isPerMachine && (oneClick || options.packElevateHelper)) {
updateInfo.isAdminRightsRequired = true;
}
+ if (updateInfo != null && this.packager.platformSpecificBuildOptions.minimumSystemVersion) {
+ updateInfo.minimumSystemVersion = this.packager.platformSpecificBuildOptions.minimumSystemVersion;
+ }
await packager.info.emitArtifactBuildCompleted({
file: installerPath,
updateInfo,
diff --git a/scheme.json b/scheme.json
index 433e2efc9cef156ff5444f0c4520362ed2ef9ea7..a89c7a9b0b608fef67902c49106a43ebd0fa8b61 100644
--- a/scheme.json
+++ b/scheme.json
@@ -1975,6 +1975,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."
},
+ "minimumSystemVersion": {
+ "description": "The minimum os kernel version required to install the application.",
+ "type": [
+ "null",
+ "string"
+ ]
+ },
"packageCategory": {
"description": "backward compatibility + to allow specify fpm-only category for all possible fpm targets in one place",
"type": [
@@ -2327,6 +2334,13 @@
"MacConfiguration": {
"additionalProperties": false,
"properties": {
+ "LSMinimumSystemVersion": {
+ "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.",
+ "type": [
+ "null",
+ "string"
+ ]
+ },
"additionalArguments": {
"anyOf": [
{
@@ -2737,7 +2751,7 @@
"type": "boolean"
},
"minimumSystemVersion": {
- "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.",
+ "description": "The minimum os kernel version required to install the application.",
"type": [
"null",
"string"
@@ -2959,6 +2973,13 @@
"MasConfiguration": {
"additionalProperties": false,
"properties": {
+ "LSMinimumSystemVersion": {
+ "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.",
+ "type": [
+ "null",
+ "string"
+ ]
+ },
"additionalArguments": {
"anyOf": [
{
@@ -3369,7 +3390,7 @@
"type": "boolean"
},
"minimumSystemVersion": {
- "description": "The minimum version of macOS required for the app to run. Corresponds to `LSMinimumSystemVersion`.",
+ "description": "The minimum os kernel version required to install the application.",
"type": [
"null",
"string"
@@ -6507,6 +6528,13 @@
"string"
]
},
+ "minimumSystemVersion": {
+ "description": "The minimum os kernel version required to install the application.",
+ "type": [
+ "null",
+ "string"
+ ]
+ },
"protocols": {
"anyOf": [
{
@@ -7376,6 +7404,13 @@
],
"description": "MAS (Mac Application Store) development options (`mas-dev` target)."
},
+ "minimumSystemVersion": {
+ "description": "The minimum os kernel version required to install the application.",
+ "type": [
+ "null",
+ "string"
+ ]
+ },
"msi": {
"anyOf": [
{

View File

@@ -1,51 +0,0 @@
diff --git a/out/MacUpdater.js b/out/MacUpdater.js
index 8f18dc5416c91835ded4e47f2358fba680c129ac..a3fb43c2450dc3484bf099b5ea79a362a3b372cc 100644
--- a/out/MacUpdater.js
+++ b/out/MacUpdater.js
@@ -74,7 +74,7 @@ class MacUpdater extends AppUpdater_1.AppUpdater {
else {
files = files.filter(file => !isArm64(file));
}
- const zipFileInfo = (0, Provider_1.findFile)(files, "zip", ["pkg", "dmg"]);
+ const zipFileInfo = (0, Provider_1.findFile)(files, "zip", ["pkg", "dmg"], false /*has been filtered by myself*/);
if (zipFileInfo == null) {
throw (0, builder_util_runtime_1.newError)(`ZIP file not provided: ${(0, builder_util_runtime_1.safeStringifyJson)(files)}`, "ERR_UPDATER_ZIP_FILE_NOT_FOUND");
}
diff --git a/out/providers/Provider.js b/out/providers/Provider.js
index 9829dff7e95aa9baa0bfdf29f52e6f761c9b7243..6ecaade9e294c87c03bb42e77ff5463f2782cb3c 100644
--- a/out/providers/Provider.js
+++ b/out/providers/Provider.js
@@ -61,11 +61,18 @@ class Provider {
}
}
exports.Provider = Provider;
-function findFile(files, extension, not) {
+function findFile(files, extension, not, filterByArch = true) {
if (files.length === 0) {
throw (0, builder_util_runtime_1.newError)("No files provided", "ERR_UPDATER_NO_FILES_PROVIDED");
}
- const result = files.find(it => it.url.pathname.toLowerCase().endsWith(`.${extension.toLowerCase()}`));
+ const result = files
+ .filter(file => {
+ if (!filterByArch) {
+ return true;
+ }
+ return (process.arch == "arm64") === (file.url.pathname.includes("arm64") || file.info.url.includes("arm64"));
+ })
+ .find(it => it.url.pathname.toLowerCase().endsWith(`.${extension.toLowerCase()}`));
if (result != null) {
return result;
}
diff --git a/out/differentialDownloader/multipleRangeDownloader.js b/out/differentialDownloader/multipleRangeDownloader.js
index bf7d3a2982c62b94054fed4ef60455b20b26d622..3a924eddc946ec446654a112a33be4e2cea311d2 100644
--- a/out/differentialDownloader/multipleRangeDownloader.js
+++ b/out/differentialDownloader/multipleRangeDownloader.js
@@ -75,7 +75,7 @@ function doExecuteTasks(differentialDownloader, options, out, resolve, reject) {
return;
}
const contentType = (0, builder_util_runtime_1.safeGetHeader)(response, "content-type");
- const m = /^multipart\/.+?(?:; boundary=(?:(?:"(.+)")|(?:([^\s]+))))$/i.exec(contentType);
+ const m = /^multipart\/.+?\s*;\s*boundary=(?:"([^"]+)"|([^\s";]+))\s*$/i.exec(contentType);
if (m == null) {
reject(new Error(`Content-Type "multipart/byteranges" is expected, but got "${contentType}"`));
return;

View File

@@ -1,39 +0,0 @@
diff --git a/core.js b/core.js
index ebb071d31cd5a14792b62814df072c5971e83300..31e1062d4a7f2422ffec79cf96a35dbb69fe89cb 100644
--- a/core.js
+++ b/core.js
@@ -157,7 +157,7 @@ class APIClient {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': this.getUserAgent(),
- ...getPlatformHeaders(),
+ // ...getPlatformHeaders(),
...this.authHeaders(opts),
};
}
diff --git a/core.mjs b/core.mjs
index 9c1a0264dcd73a85de1cf81df4efab9ce9ee2ab7..33f9f1f237f2eb2667a05dae1a7e3dc916f6bfff 100644
--- a/core.mjs
+++ b/core.mjs
@@ -150,7 +150,7 @@ export class APIClient {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': this.getUserAgent(),
- ...getPlatformHeaders(),
+ // ...getPlatformHeaders(),
...this.authHeaders(opts),
};
}
diff --git a/error.mjs b/error.mjs
index 7d19f5578040afa004bc887aab1725e8703d2bac..59ec725b6142299a62798ac4bdedb63ba7d9932c 100644
--- a/error.mjs
+++ b/error.mjs
@@ -36,7 +36,7 @@ export class APIError extends OpenAIError {
if (!status || !headers) {
return new APIConnectionError({ message, cause: castToError(errorResponse) });
}
- const error = errorResponse?.['error'];
+ const error = errorResponse?.['error'] || errorResponse;
if (status === 400) {
return new BadRequestError(status, error, message, headers);
}

View File

@@ -0,0 +1,279 @@
diff --git a/client.js b/client.js
index 33b4ff6309d5f29187dab4e285d07dac20340bab..8f568637ee9e4677585931fb0284c8165a933f69 100644
--- a/client.js
+++ b/client.js
@@ -433,7 +433,7 @@ class OpenAI {
'User-Agent': this.getUserAgent(),
'X-Stainless-Retry-Count': String(retryCount),
...(options.timeout ? { 'X-Stainless-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}),
- ...(0, detect_platform_1.getPlatformHeaders)(),
+ // ...(0, detect_platform_1.getPlatformHeaders)(),
'OpenAI-Organization': this.organization,
'OpenAI-Project': this.project,
},
diff --git a/client.mjs b/client.mjs
index c34c18213073540ebb296ea540b1d1ad39527906..1ce1a98256d7e90e26ca963582f235b23e996e73 100644
--- a/client.mjs
+++ b/client.mjs
@@ -430,7 +430,7 @@ export class OpenAI {
'User-Agent': this.getUserAgent(),
'X-Stainless-Retry-Count': String(retryCount),
...(options.timeout ? { 'X-Stainless-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}),
- ...getPlatformHeaders(),
+ // ...getPlatformHeaders(),
'OpenAI-Organization': this.organization,
'OpenAI-Project': this.project,
},
diff --git a/core/error.js b/core/error.js
index a12d9d9ccd242050161adeb0f82e1b98d9e78e20..fe3a5462480558bc426deea147f864f12b36f9bd 100644
--- a/core/error.js
+++ b/core/error.js
@@ -40,7 +40,7 @@ class APIError extends OpenAIError {
if (!status || !headers) {
return new APIConnectionError({ message, cause: (0, errors_1.castToError)(errorResponse) });
}
- const error = errorResponse?.['error'];
+ const error = errorResponse?.['error'] || errorResponse;
if (status === 400) {
return new BadRequestError(status, error, message, headers);
}
diff --git a/core/error.mjs b/core/error.mjs
index 83cefbaffeb8c657536347322d8de9516af479a2..63334b7972ec04882aa4a0800c1ead5982345045 100644
--- a/core/error.mjs
+++ b/core/error.mjs
@@ -36,7 +36,7 @@ export class APIError extends OpenAIError {
if (!status || !headers) {
return new APIConnectionError({ message, cause: castToError(errorResponse) });
}
- const error = errorResponse?.['error'];
+ const error = errorResponse?.['error'] || errorResponse;
if (status === 400) {
return new BadRequestError(status, error, message, headers);
}
diff --git a/resources/embeddings.js b/resources/embeddings.js
index 2404264d4ba0204322548945ebb7eab3bea82173..8f1bc45cc45e0797d50989d96b51147b90ae6790 100644
--- a/resources/embeddings.js
+++ b/resources/embeddings.js
@@ -5,52 +5,64 @@ exports.Embeddings = void 0;
const resource_1 = require("../core/resource.js");
const utils_1 = require("../internal/utils.js");
class Embeddings extends resource_1.APIResource {
- /**
- * Creates an embedding vector representing the input text.
- *
- * @example
- * ```ts
- * const createEmbeddingResponse =
- * await client.embeddings.create({
- * input: 'The quick brown fox jumped over the lazy dog',
- * model: 'text-embedding-3-small',
- * });
- * ```
- */
- create(body, options) {
- const hasUserProvidedEncodingFormat = !!body.encoding_format;
- // No encoding_format specified, defaulting to base64 for performance reasons
- // See https://github.com/openai/openai-node/pull/1312
- let encoding_format = hasUserProvidedEncodingFormat ? body.encoding_format : 'base64';
- if (hasUserProvidedEncodingFormat) {
- (0, utils_1.loggerFor)(this._client).debug('embeddings/user defined encoding_format:', body.encoding_format);
- }
- const response = this._client.post('/embeddings', {
- body: {
- ...body,
- encoding_format: encoding_format,
- },
- ...options,
- });
- // if the user specified an encoding_format, return the response as-is
- if (hasUserProvidedEncodingFormat) {
- return response;
- }
- // in this stage, we are sure the user did not specify an encoding_format
- // and we defaulted to base64 for performance reasons
- // we are sure then that the response is base64 encoded, let's decode it
- // the returned result will be a float32 array since this is OpenAI API's default encoding
- (0, utils_1.loggerFor)(this._client).debug('embeddings/decoding base64 embeddings from base64');
- return response._thenUnwrap((response) => {
- if (response && response.data) {
- response.data.forEach((embeddingBase64Obj) => {
- const embeddingBase64Str = embeddingBase64Obj.embedding;
- embeddingBase64Obj.embedding = (0, utils_1.toFloat32Array)(embeddingBase64Str);
- });
- }
- return response;
- });
- }
+ /**
+ * Creates an embedding vector representing the input text.
+ *
+ * @example
+ * ```ts
+ * const createEmbeddingResponse =
+ * await client.embeddings.create({
+ * input: 'The quick brown fox jumped over the lazy dog',
+ * model: 'text-embedding-3-small',
+ * });
+ * ```
+ */
+ create(body, options) {
+ const hasUserProvidedEncodingFormat = !!body.encoding_format;
+ // No encoding_format specified, defaulting to base64 for performance reasons
+ // See https://github.com/openai/openai-node/pull/1312
+ let encoding_format = hasUserProvidedEncodingFormat
+ ? body.encoding_format
+ : "base64";
+ if (body.model.includes("jina")) {
+ encoding_format = undefined;
+ }
+ if (hasUserProvidedEncodingFormat) {
+ (0, utils_1.loggerFor)(this._client).debug(
+ "embeddings/user defined encoding_format:",
+ body.encoding_format
+ );
+ }
+ const response = this._client.post("/embeddings", {
+ body: {
+ ...body,
+ encoding_format: encoding_format,
+ },
+ ...options,
+ });
+ // if the user specified an encoding_format, return the response as-is
+ if (hasUserProvidedEncodingFormat || body.model.includes("jina")) {
+ return response;
+ }
+ // in this stage, we are sure the user did not specify an encoding_format
+ // and we defaulted to base64 for performance reasons
+ // we are sure then that the response is base64 encoded, let's decode it
+ // the returned result will be a float32 array since this is OpenAI API's default encoding
+ (0, utils_1.loggerFor)(this._client).debug(
+ "embeddings/decoding base64 embeddings from base64"
+ );
+ return response._thenUnwrap((response) => {
+ if (response && response.data) {
+ response.data.forEach((embeddingBase64Obj) => {
+ const embeddingBase64Str = embeddingBase64Obj.embedding;
+ embeddingBase64Obj.embedding = (0, utils_1.toFloat32Array)(
+ embeddingBase64Str
+ );
+ });
+ }
+ return response;
+ });
+ }
}
exports.Embeddings = Embeddings;
//# sourceMappingURL=embeddings.js.map
diff --git a/resources/embeddings.mjs b/resources/embeddings.mjs
index 19dcaef578c194a89759c4360073cfd4f7dd2cbf..0284e9cc615c900eff508eb595f7360a74bd9200 100644
--- a/resources/embeddings.mjs
+++ b/resources/embeddings.mjs
@@ -2,51 +2,61 @@
import { APIResource } from "../core/resource.mjs";
import { loggerFor, toFloat32Array } from "../internal/utils.mjs";
export class Embeddings extends APIResource {
- /**
- * Creates an embedding vector representing the input text.
- *
- * @example
- * ```ts
- * const createEmbeddingResponse =
- * await client.embeddings.create({
- * input: 'The quick brown fox jumped over the lazy dog',
- * model: 'text-embedding-3-small',
- * });
- * ```
- */
- create(body, options) {
- const hasUserProvidedEncodingFormat = !!body.encoding_format;
- // No encoding_format specified, defaulting to base64 for performance reasons
- // See https://github.com/openai/openai-node/pull/1312
- let encoding_format = hasUserProvidedEncodingFormat ? body.encoding_format : 'base64';
- if (hasUserProvidedEncodingFormat) {
- loggerFor(this._client).debug('embeddings/user defined encoding_format:', body.encoding_format);
- }
- const response = this._client.post('/embeddings', {
- body: {
- ...body,
- encoding_format: encoding_format,
- },
- ...options,
- });
- // if the user specified an encoding_format, return the response as-is
- if (hasUserProvidedEncodingFormat) {
- return response;
- }
- // in this stage, we are sure the user did not specify an encoding_format
- // and we defaulted to base64 for performance reasons
- // we are sure then that the response is base64 encoded, let's decode it
- // the returned result will be a float32 array since this is OpenAI API's default encoding
- loggerFor(this._client).debug('embeddings/decoding base64 embeddings from base64');
- return response._thenUnwrap((response) => {
- if (response && response.data) {
- response.data.forEach((embeddingBase64Obj) => {
- const embeddingBase64Str = embeddingBase64Obj.embedding;
- embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str);
- });
- }
- return response;
- });
- }
+ /**
+ * Creates an embedding vector representing the input text.
+ *
+ * @example
+ * ```ts
+ * const createEmbeddingResponse =
+ * await client.embeddings.create({
+ * input: 'The quick brown fox jumped over the lazy dog',
+ * model: 'text-embedding-3-small',
+ * });
+ * ```
+ */
+ create(body, options) {
+ const hasUserProvidedEncodingFormat = !!body.encoding_format;
+ // No encoding_format specified, defaulting to base64 for performance reasons
+ // See https://github.com/openai/openai-node/pull/1312
+ let encoding_format = hasUserProvidedEncodingFormat
+ ? body.encoding_format
+ : "base64";
+ if (body.model.includes("jina")) {
+ encoding_format = undefined;
+ }
+ if (hasUserProvidedEncodingFormat) {
+ loggerFor(this._client).debug(
+ "embeddings/user defined encoding_format:",
+ body.encoding_format
+ );
+ }
+ const response = this._client.post("/embeddings", {
+ body: {
+ ...body,
+ encoding_format: encoding_format,
+ },
+ ...options,
+ });
+ // if the user specified an encoding_format, return the response as-is
+ if (hasUserProvidedEncodingFormat || body.model.includes("jina")) {
+ return response;
+ }
+ // in this stage, we are sure the user did not specify an encoding_format
+ // and we defaulted to base64 for performance reasons
+ // we are sure then that the response is base64 encoded, let's decode it
+ // the returned result will be a float32 array since this is OpenAI API's default encoding
+ loggerFor(this._client).debug(
+ "embeddings/decoding base64 embeddings from base64"
+ );
+ return response._thenUnwrap((response) => {
+ if (response && response.data) {
+ response.data.forEach((embeddingBase64Obj) => {
+ const embeddingBase64Str = embeddingBase64Obj.embedding;
+ embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str);
+ });
+ }
+ return response;
+ });
+ }
}
//# sourceMappingURL=embeddings.mjs.map

File diff suppressed because one or more lines are too long

948
.yarn/releases/yarn-4.9.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -4,4 +4,4 @@ httpTimeout: 300000
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.6.0.cjs
yarnPath: .yarn/releases/yarn-4.9.1.cjs

View File

@@ -19,13 +19,15 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
# 📖 Guide
https://docs.cherry-ai.com
<https://docs.cherry-ai.com>
# 🌠 Screenshot
![](https://github.com/user-attachments/assets/082efa42-c4df-4863-a9cb-80435cecce0f)
![](https://github.com/user-attachments/assets/f8411a65-c51f-47d3-9273-62ae384cc6f1)
![](https://github.com/user-attachments/assets/0d235b3e-65ae-45ab-987f-8dbe003c52be)
![](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)
# 🌟 Key Features
@@ -65,28 +67,50 @@ https://docs.cherry-ai.com
- 📝 Complete Markdown Rendering
- 🤲 Easy Content Sharing
# 📝 TODO
# 📝 Roadmap
- [x] Quick popup (read clipboard, quick question, explain, translate, summarize)
- [x] Comparison of multi-model answers
- [x] Support login using SSO provided by service providers
- [x] All models support networking
- [x] Launch of the first official version
- [x] Bug fixes and improvements (In progress...)
- [ ] Plugin functionality (JavaScript)
- [ ] Browser extension (highlight text to translate, summarize, add to knowledge base)
- [ ] iOS & Android client
- [ ] AI notes
- [ ] Voice input and output (AI call)
- [ ] Data backup supports custom backup content
We're actively working on the following features and improvements:
1. 🎯 **Core Features**
- Selection Assistant - Smart content selection enhancement
- Deep Research - Advanced research capabilities
- Memory System - Global context awareness
- Document Preprocessing - Improved document handling
- MCP Marketplace - Model Context Protocol ecosystem
2. 🗂 **Knowledge Management**
- Notes and Collections
- Dynamic Canvas visualization
- OCR capabilities
- TTS (Text-to-Speech) support
3. 📱 **Platform Support**
- HarmonyOS Edition (PC)
- Android App (Phase 1)
- iOS App (Phase 1)
- Multi-Window support
- Window Pinning functionality
4. 🔌 **Advanced Features**
- Plugin System
- ASR (Automatic Speech Recognition)
- Assistant and Topic Interaction Refactoring
Track our progress and contribute on our [project board](https://github.com/orgs/CherryHQ/projects/7).
Want to influence our roadmap? Join our [GitHub Discussions](https://github.com/CherryHQ/cherry-studio/discussions) to share your ideas and feedback!
# 🌈 Theme
- Theme Gallery: https://cherrycss.com
- Aero Theme: https://github.com/hakadao/CherryStudio-Aero
- PaperMaterial Theme: https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
- Claude dynamic-style: https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
- Maple Neon Theme: https://github.com/BoningtonChen/CherryStudio_themes
- Theme Gallery: <https://cherrycss.com>
- Aero Theme: <https://github.com/hakadao/CherryStudio-Aero>
- PaperMaterial Theme: <https://github.com/rainoffallingstar/CherryStudio-PaperMaterial>
- Claude dynamic-style: <https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic>
- Maple Neon Theme: <https://github.com/BoningtonChen/CherryStudio_themes>
Welcome PR for more themes
@@ -94,6 +118,10 @@ Welcome PR for more themes
Refer to the [development documentation](docs/dev.md)
Refer to the [Architecture overview documentation](https://deepwiki.com/CherryHQ/cherry-studio)
Refer to the [Branching Strategy](docs/branching-strategy-en.md) for contribution guidelines
# 🤝 Contributing
We welcome contributions to Cherry Studio! Here are some ways you can contribute:
@@ -117,7 +145,7 @@ For more detailed guidelines, please refer to our [Contributing Guide](./CONTRIB
Thank you for your support and contributions!
## 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.
@@ -126,7 +154,7 @@ Thank you for your support and contributions!
# 🚀 Contributors
<a href="https://github.com/CherryHQ/cherry-studio/graphs/contributors">
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
<img src="https://contrib.rocks/image?repo=CherryHQ/cherry-studio" />
</a>
<br /><br />
@@ -144,7 +172,7 @@ Thank you for your support and contributions!
# ✉️ Contact
yinsenho@cherry-ai.com
<yinsenho@cherry-ai.com>
# ⭐️ Star History

View File

@@ -6,17 +6,19 @@
<p align="center">
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | <a href="./README.zh.md">中文</a> | 日本語 <br>
</p>
<div align="center">
<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: 250px; height: 54px;" width="250" height="54" /></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) をして開発をサポートしてください!❤️
❤️ Cherry Studio をお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!
# 📖 ガイド
@@ -24,9 +26,11 @@ https://docs.cherry-ai.com
# 🌠 スクリーンショット
![](https://github.com/user-attachments/assets/082efa42-c4df-4863-a9cb-80435cecce0f)
![](https://github.com/user-attachments/assets/f8411a65-c51f-47d3-9273-62ae384cc6f1)
![](https://github.com/user-attachments/assets/0d235b3e-65ae-45ab-987f-8dbe003c52be)
![](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)
# 🌟 主な機能
@@ -66,75 +70,104 @@ https://docs.cherry-ai.com
- 📝 完全な Markdown レンダリング
- 🤲 簡単な共有機能
# 📝 TODO
# 📝 開発計画
- [x] クイックポップアップ(クリップボードの読み取り、簡単な質問、説明、翻訳、要約)
- [x] 複数モデルの回答の比較
- [x] サービスプロバイダーが提供する SSO を使用したログインをサポート
- [x] すべてのモデルがネットワークをサポート
- [x] 最初の公式バージョンのリリース
- [ ] 錯誤修復と改善 (開発中...)
- [ ] プラグイン機能JavaScript
- [ ] ブラウザ拡張機能(テキストをハイライトして翻訳、要約、ナレッジベースに追加)
- [ ] iOS & Android クライアント
- [ ] AIート
- [ ] 音声入出力AI コール)
- [ ] データバックアップはカスタムバックアップコンテンツをサポート
以下の機能と改善に積極的に取り組んでいます:
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
- テーマギャラリー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 を歓迎します
# 🖥️ 開発
参考[開発ドキュメント](dev.md)
[開発ドキュメント](dev.md)を参照してください
[アーキテクチャ概要ドキュメント](https://deepwiki.com/CherryHQ/cherry-studio)を参照してください
[ブランチ戦略](branching-strategy-en.md)を参照して貢献ガイドラインを確認してください
# 🤝 貢献
Cherry Studio への貢献を歓迎します!以下の方法で貢献できます:
1. **コードの貢献**:新機能を開発するか、既存のコードを最適化します
2. **バグの修正**:見つけたバグを修正します
3. **問題の管理**GitHub の問題を管理するのを手伝います
4. **製品デザイン**:デザインの議論に参加します
5. **ドキュメントの作成**:ユーザーマニュアルやガイドを改善します
6. **コミュニティの参加**:ディスカッションに参加し、ユーザーを支援します
7. **使用の促進**Cherry Studio を広めます
1. **コードの貢献**:新機能を開発するか、既存のコードを最適化します
2. **バグの修正**:見つけたバグを修正します
3. **問題の管理**GitHub の問題を管理するのを手伝います
4. **製品デザイン**:デザインの議論に参加します
5. **ドキュメントの作成**:ユーザーマニュアルやガイドを改善します
6. **コミュニティの参加**:ディスカッションに参加し、ユーザーを支援します
7. **使用の促進**Cherry Studio を広めます
## 始め方
1. **リポジトリをフォーク**:フォークしてローカルマシンにクローンします
2. **ブランチを作成**:変更のためのブランチを作成します
3. **変更を提出**:変更をコミットしてプッシュします
4. **プルリクエストを開く**:変更内容と理由を説明します
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=kangfenmao/cherry-studio" />
<img src="https://contrib.rocks/image?repo=CherryHQ/cherry-studio" />
</a>
<br /><br />
# コミュニティ
# 🌐 コミュニティ
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
# スポンサー
# スポンサー
[Buy Me a Coffee](sponsor.md)
[開発者を支援する](sponsor.md)
# 📃 ライセンス

View File

@@ -4,7 +4,8 @@
</a>
</h1>
<p align="center">
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="./README.ja.md">日本語</a><br></p>
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="./README.ja.md">日本語</a><br>
</p>
<div align="center">
<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: 250px; height: 54px;" width="250" height="54" /></a>
@@ -18,15 +19,25 @@ Cherry Studio 是一款支持多个大语言模型LLM服务商的桌面客
❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️
# GitCode✖Cherry Studio【新源力】贡献挑战赛
<p align="center">
<a href="https://gitcode.com/CherryHQ/cherry-studio/discussion/2">
<img src="https://raw.gitcode.com/user-images/assets/5007375/8d8d7559-1141-4691-b90f-d154558c6896/cherry-studio-gitcode.jpg" width="100%" alt="banner" />
</a>
</p>
# 📖 使用教程
https://docs.cherry-ai.com
# 🌠 界面
![](https://github.com/user-attachments/assets/082efa42-c4df-4863-a9cb-80435cecce0f)
![](https://github.com/user-attachments/assets/f8411a65-c51f-47d3-9273-62ae384cc6f1)
![](https://github.com/user-attachments/assets/0d235b3e-65ae-45ab-987f-8dbe003c52be)
![](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)
# 🌟 主要特性
@@ -66,28 +77,50 @@ https://docs.cherry-ai.com
- 📝 完整的 Markdown 渲染
- 🤲 便捷的内容分享功能
# 📝 待辦事項
# 📝 开发计划
- [x] 快捷弹窗(读取剪贴板、快速提问、解释、翻译、总结)
- [x] 多模型回答对比
- [x] 支持使用服务供应商提供的 SSO 进行登入
- [x] 全部模型支持连网(开发中...
- [x] 推出第一个正式版
- [x] 错误修复和改进(开发中...
- [ ] 插件功能JavaScript
- [ ] 浏览器插件(划词翻译、总结、新增至知识库)
- [ ] iOS & Android 客户端
- [ ] AI 笔记
- [ ] 语音输入输出AI 通话)
- [ ] 数据备份支持自定义备份内容
我们正在积极开发以下功能和改进:
1. 🎯 **核心功能**
- 选择助手 - 智能内容选择增强
- 深度研究 - 高级研究能力
- 全局记忆 - 全局上下文感知
- 文档预处理 - 改进文档处理能力
- MCP 市场 - 模型上下文协议生态系统
2. 🗂 **知识管理**
- 笔记与收藏功能
- 动态画布可视化
- OCR 光学字符识别
- TTS 文本转语音支持
3. 📱 **平台支持**
- 鸿蒙版本 (PC)
- Android 应用(第一期)
- iOS 应用(第一期)
- 多窗口支持
- 窗口置顶功能
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
- PaperMaterial 主题https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
- 仿 Claude 主题https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
- 霓虹枫叶主题:https://github.com/BoningtonChen/CherryStudio_themes
欢迎 PR 更多主题
@@ -95,37 +128,43 @@ https://docs.cherry-ai.com
参考[开发文档](dev.md)
参考[架构概览文档](https://deepwiki.com/CherryHQ/cherry-studio)
参考[分支策略](branching-strategy-zh.md)了解贡献指南
# 🤝 贡献
我们欢迎对 Cherry Studio 的贡献!您可以通过以下方式贡献:
1. **贡献代码**:开发新功能或优化现有代码
2. **修复错误**:提交您发现的错误修复
3. **维护问题**:帮助管理 GitHub 问题
4. **产品设计**:参与设计讨论
5. **撰写文档**:改进用户手册和指南
6. **社区参与**:加入讨论并帮助用户
7. **推广使用**:宣传 Cherry Studio
1. **贡献代码**:开发新功能或优化现有代码
2. **修复错误**:提交您发现的错误修复
3. **维护问题**:帮助管理 GitHub 问题
4. **产品设计**:参与设计讨论
5. **撰写文档**:改进用户手册和指南
6. **社区参与**:加入讨论并帮助用户
7. **推广使用**:宣传 Cherry Studio
## 入门
1. **Fork 仓库**Fork 并克隆到您的本地机器
2. **创建分支**:为您的更改创建分支
3. **提交更改**:提交并推送您的更改
4. **打开 Pull Request**:描述您的更改和原因
1. **Fork 仓库**Fork 并克隆到您的本地机器
2. **创建分支**:为您的更改创建分支
3. **提交更改**:提交并推送您的更改
4. **打开 Pull Request**:描述您的更改和原因
有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.zh.md)
有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.zh.md)
感谢您的支持和贡献!
## 相关项目
# 🔗 相关项目
- [one-api](https://github.com/songquanpeng/one-api)LLM API 管理及分发系统,支持 OpenAI、Azure、Anthropic 等主流模型,统一 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=kangfenmao/cherry-studio" />
<img src="https://contrib.rocks/image?repo=CherryHQ/cherry-studio" />
</a>
<br /><br />
@@ -135,7 +174,7 @@ https://docs.cherry-ai.com
# ☕ 赞助
[微信赞赏码](sponsor.md)
[赞助开发者](sponsor.md)
# 📃 许可证

View File

@@ -0,0 +1,71 @@
# 🌿 Branching Strategy
Cherry Studio implements a structured branching strategy to maintain code quality and streamline the development process.
## Main Branches
- `main`: Main development branch
- Contains the latest development code
- Direct commits are not allowed - changes must come through pull requests
- Code may contain features in development and might not be fully stable
- `release/*`: Release branches
- Created from `main` branch
- Contains stable code ready for release
- Only accepts documentation updates and bug fixes
- Thoroughly tested before production deployment
## Contributing Branches
When contributing to Cherry Studio, please follow these guidelines:
1. **Feature Branches:**
- Create from `main` branch
- Naming format: `feature/issue-number-brief-description`
- Submit PR back to `main`
2. **Bug Fix Branches:**
- Create from `main` branch
- Naming format: `fix/issue-number-brief-description`
- Submit PR back to `main`
3. **Documentation Branches:**
- Create from `main` branch
- Naming format: `docs/brief-description`
- Submit PR back to `main`
4. **Hotfix Branches:**
- Create from `main` branch
- Naming format: `hotfix/issue-number-brief-description`
- Submit PR to both `main` and relevant `release` branches
5. **Release Branches:**
- Create from `main` branch
- Naming format: `release/version-number`
- Used for final preparation work before version release
- Only accepts bug fixes and documentation updates
- After testing and preparation, merge back to `main` and tag with version
## Workflow Diagram
![](https://github.com/user-attachments/assets/61db64a2-fab1-4a16-8253-0c64c9df1a63)
## Pull Request Guidelines
- All PRs should be submitted to the `main` branch unless fixing a critical production issue
- Ensure your branch is up to date with the latest `main` changes before submitting
- Include relevant issue numbers in your PR description
- Make sure all tests pass and code meets our quality standards
- Add before/after screenshots if you add a new feature or modify a UI component
## Version Tag Management
- Major releases: v1.0.0, v2.0.0, etc.
- Feature releases: v1.1.0, v1.2.0, etc.
- Patch releases: v1.0.1, v1.0.2, etc.
- Hotfix releases: v1.0.1-hotfix, etc.

View File

@@ -0,0 +1,71 @@
# 🌿 分支策略
Cherry Studio 采用结构化的分支策略来维护代码质量并简化开发流程。
## 主要分支
- `main`:主开发分支
- 包含最新的开发代码
- 禁止直接提交 - 所有更改必须通过拉取请求Pull Request
- 此分支上的代码可能包含正在开发的功能,不一定完全稳定
- `release/*`:发布分支
-`main` 分支创建
- 包含准备发布的稳定代码
- 只接受文档更新和 bug 修复
- 经过完整测试后可以发布到生产环境
## 贡献分支
在为 Cherry Studio 贡献代码时,请遵循以下准则:
1. **功能开发分支:**
-`main` 分支创建
- 命名格式:`feature/issue-number-brief-description`
- 完成后提交 PR 到 `main` 分支
2. **Bug 修复分支:**
-`main` 分支创建
- 命名格式:`fix/issue-number-brief-description`
- 完成后提交 PR 到 `main` 分支
3. **文档更新分支:**
-`main` 分支创建
- 命名格式:`docs/brief-description`
- 完成后提交 PR 到 `main` 分支
4. **紧急修复分支:**
-`main` 分支创建
- 命名格式:`hotfix/issue-number-brief-description`
- 完成后需要同时合并到 `main` 和相关的 `release` 分支
5. **发布分支:**
-`main` 分支创建
- 命名格式:`release/version-number`
- 用于版本发布前的最终准备工作
- 只允许合并 bug 修复和文档更新
- 完成测试和准备工作后,将代码合并回 `main` 分支并打上版本标签
## 工作流程
![](https://github.com/user-attachments/assets/61db64a2-fab1-4a16-8253-0c64c9df1a63)
## 拉取请求PR指南
- 除非是修复生产环境的关键问题,否则所有 PR 都应该提交到 `main` 分支
- 提交 PR 前确保你的分支已经同步了最新的 `main` 分支内容
- 在 PR 描述中包含相关的 issue 编号
- 确保所有测试通过,且代码符合我们的质量标准
- 如果你添加了新功能或修改了 UI 组件,请附上更改前后的截图
## 版本标签管理
- 主要版本发布v1.0.0、v2.0.0 等
- 功能更新发布v1.1.0、v1.2.0 等
- 补丁修复发布v1.0.1、v1.0.2 等
- 紧急修复发布v1.0.1-hotfix 等

View File

@@ -37,6 +37,14 @@ yarn install
yarn dev
```
### Debug
```bash
yarn debug
```
Then input chrome://inspect in browser
### Test
```bash

View File

@@ -0,0 +1,3 @@
# 消息的生命周期
![image](./message-lifecycle.png)

View File

@@ -0,0 +1,127 @@
# messageBlock.ts 使用指南
该文件定义了用于管理应用程序中所有 `MessageBlock` 实体的 Redux Slice。它使用 Redux Toolkit 的 `createSlice``createEntityAdapter` 来高效地处理规范化的状态,并提供了一系列 actions 和 selectors 用于与消息块数据交互。
## 核心目标
- **状态管理**: 集中管理所有 `MessageBlock` 的状态。`MessageBlock` 代表消息中的不同内容单元(如文本、代码、图片、引用等)。
- **规范化**: 使用 `createEntityAdapter``MessageBlock` 数据存储在规范化的结构中(`{ ids: [], entities: {} }`),这有助于提高性能和简化更新逻辑。
- **可预测性**: 提供明确的 actions 来修改状态,并通过 selectors 安全地访问状态。
## 关键概念
- **Slice (`createSlice`)**: Redux Toolkit 的核心 API用于创建包含 reducer 逻辑、action creators 和初始状态的 Redux 模块。
- **Entity Adapter (`createEntityAdapter`)**: Redux Toolkit 提供的工具,用于简化对规范化数据的 CRUD创建、读取、更新、删除操作。它会自动生成 reducer 函数和 selectors。
- **Selectors**: 用于从 Redux store 中派生和计算数据的函数。Selectors 可以被记忆化memoized以提高性能。
## State 结构
`messageBlocks` slice 的状态结构由 `createEntityAdapter` 定义,大致如下:
```typescript
{
ids: string[]; // 存储所有 MessageBlock ID 的有序列表
entities: { [id: string]: MessageBlock }; // 按 ID 存储 MessageBlock 对象的字典
loadingState: 'idle' | 'loading' | 'succeeded' | 'failed'; // (可选) 其他状态,如加载状态
error: string | null; // (可选) 错误信息
}
```
## Actions
该 slice 导出以下 actions (由 `createSlice``createEntityAdapter` 自动生成或自定义)
- **`upsertOneBlock(payload: MessageBlock)`**:
- 添加一个新的 `MessageBlock` 或更新一个已存在的 `MessageBlock`。如果 payload 中的 `id` 已存在,则执行更新;否则执行插入。
- **`upsertManyBlocks(payload: MessageBlock[])`**:
- 添加或更新多个 `MessageBlock`。常用于批量加载数据(例如,加载一个 Topic 的所有消息块)。
- **`removeOneBlock(payload: string)`**:
- 根据提供的 `id` (payload) 移除单个 `MessageBlock`
- **`removeManyBlocks(payload: string[])`**:
- 根据提供的 `id` 数组 (payload) 移除多个 `MessageBlock`。常用于删除消息或清空 Topic 时清理相关的块。
- **`removeAllBlocks()`**:
- 移除 state 中的所有 `MessageBlock` 实体。
- **`updateOneBlock(payload: { id: string; changes: Partial<MessageBlock> })`**:
- 更新一个已存在的 `MessageBlock``payload` 需要包含块的 `id` 和一个包含要更改的字段的 `changes` 对象。
- **`setMessageBlocksLoading(payload: 'idle' | 'loading')`**:
- (自定义) 设置 `loadingState` 属性。
- **`setMessageBlocksError(payload: string)`**:
- (自定义) 设置 `loadingState``'failed'` 并记录错误信息。
**使用示例 (在 Thunk 或其他 Dispatch 的地方):**
```typescript
import { upsertOneBlock, removeManyBlocks, updateOneBlock } from './messageBlock'
import store from './store' // 假设这是你的 Redux store 实例
// 添加或更新一个块
const newBlock: MessageBlock = {
/* ... block data ... */
}
store.dispatch(upsertOneBlock(newBlock))
// 更新一个块的内容
store.dispatch(updateOneBlock({ id: blockId, changes: { content: 'New content' } }))
// 删除多个块
const blockIdsToRemove = ['id1', 'id2']
store.dispatch(removeManyBlocks(blockIdsToRemove))
```
## Selectors
该 slice 导出由 `createEntityAdapter` 生成的基础 selectors并通过 `messageBlocksSelectors` 对象访问:
- **`messageBlocksSelectors.selectIds(state: RootState): string[]`**: 返回包含所有块 ID 的数组。
- **`messageBlocksSelectors.selectEntities(state: RootState): { [id: string]: MessageBlock }`**: 返回块 ID 到块对象的映射字典。
- **`messageBlocksSelectors.selectAll(state: RootState): MessageBlock[]`**: 返回包含所有块对象的数组。
- **`messageBlocksSelectors.selectTotal(state: RootState): number`**: 返回块的总数。
- **`messageBlocksSelectors.selectById(state: RootState, id: string): MessageBlock | undefined`**: 根据 ID 返回单个块对象,如果找不到则返回 `undefined`
**此外,还提供了一个自定义的、记忆化的 selector**
- **`selectFormattedCitationsByBlockId(state: RootState, blockId: string | undefined): Citation[]`**:
- 接收一个 `blockId`
- 如果该 ID 对应的块是 `CITATION` 类型,则提取并格式化其包含的引用信息(来自网页搜索、知识库等),进行去重和重新编号,最后返回一个 `Citation[]` 数组,用于在 UI 中显示。
- 如果块不存在或类型不匹配,返回空数组 `[]`
- 这个 selector 封装了处理不同引用来源Gemini, OpenAI, OpenRouter, Zhipu 等)的复杂逻辑。
**使用示例 (在 React 组件或 `useSelector` 中):**
```typescript
import { useSelector } from 'react-redux'
import { messageBlocksSelectors, selectFormattedCitationsByBlockId } from './messageBlock'
import type { RootState } from './store'
// 获取所有块
const allBlocks = useSelector(messageBlocksSelectors.selectAll)
// 获取特定 ID 的块
const specificBlock = useSelector((state: RootState) => messageBlocksSelectors.selectById(state, someBlockId))
// 获取特定引用块格式化后的引用列表
const formattedCitations = useSelector((state: RootState) => selectFormattedCitationsByBlockId(state, citationBlockId))
// 在组件中使用引用数据
// {formattedCitations.map(citation => ...)}
```
## 集成
`messageBlock.ts` slice 通常与 `messageThunk.ts` 中的 Thunks 紧密协作。Thunks 负责处理异步逻辑(如 API 调用、数据库操作),并在需要时 dispatch `messageBlock` slice 的 actions 来更新状态。例如,当 `messageThunk` 接收到流式响应时,它会 dispatch `upsertOneBlock``updateOneBlock` 来实时更新对应的 `MessageBlock`。同样,删除消息的 Thunk 会 dispatch `removeManyBlocks`
理解 `messageBlock.ts` 的职责是管理**状态本身**,而 `messageThunk.ts` 负责**触发状态变更**的异步流程,这对于维护清晰的应用架构至关重要。

View File

@@ -0,0 +1,105 @@
# messageThunk.ts 使用指南
该文件包含用于管理应用程序中消息流、处理助手交互以及同步 Redux 状态与 IndexedDB 数据库的核心 Thunk Action Creators。主要围绕 `Message``MessageBlock` 对象进行操作。
## 核心功能
1. **发送/接收消息**: 处理用户消息的发送,触发助手响应,并流式处理返回的数据,将其解析为不同的 `MessageBlock`
2. **状态管理**: 确保 Redux store 中的消息和消息块状态与 IndexedDB 中的持久化数据保持一致。
3. **消息操作**: 提供删除、重发、重新生成、编辑后重发、追加响应、克隆等消息生命周期管理功能。
4. **Block 处理**: 动态创建、更新和保存各种类型的 `MessageBlock`(文本、思考过程、工具调用、引用、图片、错误、翻译等)。
## 主要 Thunks
以下是一些关键的 Thunk 函数及其用途:
1. **`sendMessage(userMessage, userMessageBlocks, assistant, topicId)`**
- **用途**: 发送一条新的用户消息。
- **流程**:
- 保存用户消息 (`userMessage`) 及其块 (`userMessageBlocks`) 到 Redux 和 DB。
- 检查 `@mentions` 以确定是单模型响应还是多模型响应。
- 创建助手消息(们)的存根 (Stub)。
- 将存根添加到 Redux 和 DB。
- 将核心处理逻辑 `fetchAndProcessAssistantResponseImpl` 添加到该 `topicId` 的队列中以获取实际响应。
- **Block 相关**: 主要处理用户消息的初始 `MessageBlock` 保存。
2. **`fetchAndProcessAssistantResponseImpl(dispatch, getState, topicId, assistant, assistantMessage)`**
- **用途**: (内部函数) 获取并处理单个助手响应的核心逻辑,被 `sendMessage`, `resend...`, `regenerate...`, `append...` 等调用。
- **流程**:
- 设置 Topic 加载状态。
- 准备上下文消息。
- 调用 `fetchChatCompletion` API 服务。
- 使用 `createStreamProcessor` 处理流式响应。
- 通过各种回调 (`onTextChunk`, `onThinkingChunk`, `onToolCallComplete`, `onImageGenerated`, `onError`, `onComplete` 等) 处理不同类型的事件。
- **Block 相关**:
- 根据流事件创建初始 `UNKNOWN` 块。
- 实时创建和更新 `MAIN_TEXT``THINKING` 块,使用 `throttledBlockUpdate``throttledBlockDbUpdate` 进行节流更新。
- 创建 `TOOL`, `CITATION`, `IMAGE`, `ERROR` 等类型的块。
- 在事件完成时(如 `onTextComplete`, `onToolCallComplete`)将块状态标记为 `SUCCESS``ERROR`,并使用 `saveUpdatedBlockToDB` 保存最终状态。
- 使用 `handleBlockTransition` 管理非流式块(如 `TOOL`, `CITATION`)的添加和状态更新。
3. **`loadTopicMessagesThunk(topicId, forceReload)`**
- **用途**: 从数据库加载指定主题的所有消息及其关联的 `MessageBlock`
- **流程**:
- 从 DB 获取 `Topic` 及其 `messages` 列表。
- 根据消息 ID 列表从 DB 获取所有相关的 `MessageBlock`
- 使用 `upsertManyBlocks` 将块更新到 Redux。
- 将消息更新到 Redux。
- **Block 相关**: 负责将持久化的 `MessageBlock` 加载到 Redux 状态。
4. **删除 Thunks**
- `deleteSingleMessageThunk(topicId, messageId)`: 删除单个消息及其所有 `MessageBlock`
- `deleteMessageGroupThunk(topicId, askId)`: 删除一个用户消息及其所有相关的助手响应消息和它们的所有 `MessageBlock`
- `clearTopicMessagesThunk(topicId)`: 清空主题下的所有消息及其所有 `MessageBlock`
- **Block 相关**: 从 Redux 和 DB 中移除指定的 `MessageBlock`
5. **重发/重新生成 Thunks**
- `resendMessageThunk(topicId, userMessageToResend, assistant)`: 重发用户消息。会重置(清空 Block 并标记为 PENDING所有与该用户消息关联的助手响应然后重新请求生成。
- `resendUserMessageWithEditThunk(topicId, originalMessage, mainTextBlockId, editedContent, assistant)`: 用户编辑消息内容后重发。先更新用户消息的 `MAIN_TEXT` 块内容,然后调用 `resendMessageThunk`
- `regenerateAssistantResponseThunk(topicId, assistantMessageToRegenerate, assistant)`: 重新生成单个助手响应。重置该助手消息(清空 Block 并标记为 PENDING然后重新请求生成。
- **Block 相关**: 删除旧的 `MessageBlock`,并在重新生成过程中创建新的 `MessageBlock`
6. **`appendAssistantResponseThunk(topicId, existingAssistantMessageId, newModel, assistant)`**
- **用途**: 在已有的对话上下文中,针对同一个用户问题,使用新选择的模型追加一个新的助手响应。
- **流程**:
- 找到现有助手消息以获取原始 `askId`
- 创建使用 `newModel` 的新助手消息存根(使用相同的 `askId`)。
- 添加新存根到 Redux 和 DB。
-`fetchAndProcessAssistantResponseImpl` 添加到队列以生成新响应。
- **Block 相关**: 为新的助手响应创建全新的 `MessageBlock`
7. **`cloneMessagesToNewTopicThunk(sourceTopicId, branchPointIndex, newTopic)`**
- **用途**: 将源主题的部分消息(及其 Block克隆到一个**已存在**的新主题中。
- **流程**:
- 复制指定索引前的消息。
- 为所有克隆的消息和 Block 生成新的 UUID。
- 正确映射克隆消息之间的 `askId` 关系。
- 复制 `MessageBlock` 内容,更新其 `messageId` 指向新的消息 ID。
- 更新文件引用计数(如果 Block 是文件或图片)。
- 将克隆的消息和 Block 保存到新主题的 Redux 状态和 DB 中。
- **Block 相关**: 创建 `MessageBlock` 的副本,并更新其 ID 和 `messageId`
8. **`initiateTranslationThunk(messageId, topicId, targetLanguage, sourceBlockId?, sourceLanguage?)`**
- **用途**: 为指定消息启动翻译流程,创建一个初始的 `TRANSLATION` 类型的 `MessageBlock`
- **流程**:
- 创建一个状态为 `STREAMING``TranslationMessageBlock`
- 将其添加到 Redux 和 DB。
- 更新原消息的 `blocks` 列表以包含新的翻译块 ID。
- **Block 相关**: 创建并保存一个占位的 `TranslationMessageBlock`。实际翻译内容的获取和填充需要后续步骤。
## 内部机制和注意事项
- **数据库交互**: 通过 `saveMessageAndBlocksToDB`, `updateExistingMessageAndBlocksInDB`, `saveUpdatesToDB`, `saveUpdatedBlockToDB`, `throttledBlockDbUpdate` 等辅助函数与 IndexedDB (`db`) 交互,确保数据持久化。
- **状态同步**: Thunks 负责协调 Redux Store 和 IndexedDB 之间的数据一致性。
- **队列 (`getTopicQueue`)**: 使用 `AsyncQueue` 确保对同一主题的操作(尤其是 API 请求)按顺序执行,避免竞态条件。
- **节流 (`throttle`)**: 对流式响应中频繁的 Block 更新(文本、思考)使用 `lodash.throttle` 优化性能,减少 Redux dispatch 和 DB 写入次数。
- **错误处理**: `fetchAndProcessAssistantResponseImpl` 内的回调函数(特别是 `onError`)处理流处理和 API 调用中可能出现的错误,并创建 `ERROR` 类型的 `MessageBlock`
开发者在使用这些 Thunks 时,通常需要提供 `dispatch`, `getState` (由 Redux Thunk 中间件注入),以及如 `topicId`, `assistant` 配置对象, 相关的 `Message``MessageBlock` 对象/ID 等参数。理解每个 Thunk 的职责和它如何影响消息及块的状态至关重要。

View File

@@ -0,0 +1,156 @@
# useMessageOperations.ts 使用指南
该文件定义了一个名为 `useMessageOperations` 的自定义 React Hook。这个 Hook 的主要目的是为 React 组件提供一个便捷的接口用于执行与特定主题Topic相关的各种消息操作。它封装了调用 Redux Thunks (`messageThunk.ts`) 和 Actions (`newMessage.ts`, `messageBlock.ts`) 的逻辑,简化了组件与消息数据交互的代码。
## 核心目标
- **封装**: 将复杂的消息操作逻辑(如删除、重发、重新生成、编辑、翻译等)封装在易于使用的函数中。
- **简化**: 让组件可以直接调用这些操作函数,而无需直接与 Redux `dispatch` 或 Thunks 交互。
- **上下文关联**: 所有操作都与传入的 `topic` 对象相关联,确保操作作用于正确的主题。
## 如何使用
在你的 React 函数组件中,导入并调用 `useMessageOperations` Hook并传入当前活动的 `Topic` 对象。
```typescript
import React from 'react';
import { useMessageOperations } from '@renderer/hooks/useMessageOperations';
import type { Topic, Message, Assistant, Model } from '@renderer/types';
interface MyComponentProps {
currentTopic: Topic;
currentAssistant: Assistant;
}
function MyComponent({ currentTopic, currentAssistant }: MyComponentProps) {
const {
deleteMessage,
resendMessage,
regenerateAssistantMessage,
appendAssistantResponse,
getTranslationUpdater,
createTopicBranch,
// ... 其他操作函数
} = useMessageOperations(currentTopic);
const handleDelete = (messageId: string) => {
deleteMessage(messageId);
};
const handleResend = (message: Message) => {
resendMessage(message, currentAssistant);
};
const handleAppend = (existingMsg: Message, newModel: Model) => {
appendAssistantResponse(existingMsg, newModel, currentAssistant);
}
// ... 在组件中使用其他操作函数
return (
<div>
{/* Component UI */}
<button onClick={() => handleDelete('some-message-id')}>Delete Message</button>
{/* ... */}
</div>
);
}
```
## 返回值
`useMessageOperations(topic)` Hook 返回一个包含以下函数和值的对象:
- **`deleteMessage(id: string)`**:
- 删除指定 `id` 的单个消息。
- 内部调用 `deleteSingleMessageThunk`
- **`deleteGroupMessages(askId: string)`**:
- 删除与指定 `askId` 相关联的一组消息(通常是用户提问及其所有助手回答)。
- 内部调用 `deleteMessageGroupThunk`
- **`editMessage(messageId: string, updates: Partial<Message>)`**:
- 更新指定 `messageId` 的消息的部分属性。
- **注意**: 目前主要用于更新 Redux 状态
- 内部调用 `newMessagesActions.updateMessage`
- **`resendMessage(message: Message, assistant: Assistant)`**:
- 重新发送指定的用户消息 (`message`),这将触发其所有关联助手响应的重新生成。
- 内部调用 `resendMessageThunk`
- **`resendUserMessageWithEdit(message: Message, editedContent: string, assistant: Assistant)`**:
- 在用户消息的主要文本块被编辑后,重新发送该消息。
- 会先查找消息的 `MAIN_TEXT` 块 ID然后调用 `resendUserMessageWithEditThunk`
- **`clearTopicMessages(_topicId?: string)`**:
- 清除当前主题(或可选的指定 `_topicId`)下的所有消息。
- 内部调用 `clearTopicMessagesThunk`
- **`createNewContext()`**:
- 发出一个全局事件 (`EVENT_NAMES.NEW_CONTEXT`),通常用于通知 UI 清空显示,准备新的上下文。不直接修改 Redux 状态。
- **`displayCount`**:
- (非操作函数) 从 Redux store 中获取当前的 `displayCount` 值。
- **`pauseMessages()`**:
- 尝试中止当前主题中正在进行的消息生成(状态为 `processing``pending`)。
- 通过查找相关的 `askId` 并调用 `abortCompletion` 来实现。
- 同时会 dispatch `setTopicLoading` action 将加载状态设为 `false`
- **`resumeMessage(message: Message, assistant: Assistant)`**:
- 恢复/重新发送一个用户消息。目前实现为直接调用 `resendMessage`
- **`regenerateAssistantMessage(message: Message, assistant: Assistant)`**:
- 重新生成指定的**助手**消息 (`message`) 的响应。
- 内部调用 `regenerateAssistantResponseThunk`
- **`appendAssistantResponse(existingAssistantMessage: Message, newModel: Model, assistant: Assistant)`**:
- 针对 `existingAssistantMessage` 所回复的**同一用户提问**,使用 `newModel` 追加一个新的助手响应。
- 内部调用 `appendAssistantResponseThunk`
- **`getTranslationUpdater(messageId: string, targetLanguage: string, sourceBlockId?: string, sourceLanguage?: string)`**:
- **用途**: 获取一个用于逐步更新翻译块内容的函数。
- **流程**:
1. 内部调用 `initiateTranslationThunk` 来创建或获取一个 `TRANSLATION` 类型的 `MessageBlock`,并获取其 `blockId`
2. 返回一个**异步更新函数**。
- **返回的更新函数 `(accumulatedText: string, isComplete?: boolean) => void`**:
- 接收累积的翻译文本和完成状态。
- 调用 `updateOneBlock` 更新 Redux 中的翻译块内容和状态 (`STREAMING``SUCCESS`)。
- 调用 `throttledBlockDbUpdate` 将更新(节流地)保存到数据库。
- 如果初始化失败Thunk 返回 `undefined`),则此函数返回 `null`
- **`createTopicBranch(sourceTopicId: string, branchPointIndex: number, newTopic: Topic)`**:
- 创建一个主题分支,将 `sourceTopicId` 主题中 `branchPointIndex` 索引之前的消息克隆到 `newTopic` 中。
- **注意**: `newTopic` 对象必须是调用此函数**之前**已经创建并添加到 Redux 和数据库中的。
- 内部调用 `cloneMessagesToNewTopicThunk`
## 依赖
- **`topic: Topic`**: 必须传入当前操作上下文的主题对象。Hook 返回的操作函数将始终作用于这个主题的 `topic.id`
- **Redux `dispatch`**: Hook 内部使用 `useAppDispatch` 获取 `dispatch` 函数来调用 actions 和 thunks。
## 相关 Hooks
在同一文件中还定义了两个辅助 Hook
- **`useTopicMessages(topic: Topic)`**:
- 使用 `selectMessagesForTopic` selector 来获取并返回指定主题的消息列表。
- **`useTopicLoading(topic: Topic)`**:
- 使用 `selectNewTopicLoading` selector 来获取并返回指定主题的加载状态。
这些 Hook 可以与 `useMessageOperations` 结合使用,方便地在组件中获取消息数据、加载状态,并执行相关操作。

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

View File

@@ -38,24 +38,21 @@ files:
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
asarUnpack:
- resources/**
- '**/*.{metal,exp,lib}'
- '**/*.{node,dll,metal,exp,lib}'
win:
executableName: Cherry Studio
artifactName: ${productName}-${version}-${arch}-setup.${ext}
artifactName: ${productName}-${version}-portable.${ext}
target:
- target: nsis
- target: portable
nsis:
artifactName: ${productName}-${version}-${arch}-setup.${ext}
artifactName: ${productName}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
allowToChangeInstallationDirectory: true
oneClick: false
include: build/nsis-installer.nsh
buildUniversalInstaller: false
portable:
artifactName: ${productName}-${version}-${arch}-portable.${ext}
mac:
entitlementsInherit: build/entitlements.mac.plist
notarize: false
@@ -67,11 +64,20 @@ mac:
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
target:
- target: dmg
arch:
- arm64
- x64
- target: zip
arch:
- arm64
- x64
linux:
artifactName: ${productName}-${version}-${arch}.${ext}
target:
- target: AppImage
arch:
- arm64
- x64
maintainer: electronjs.org
category: Utility
desktop:
@@ -80,18 +86,19 @@ linux:
mimeTypes:
- x-scheme-handler/cherrystudio
publish:
provider: generic
url: https://releases.cherry-ai.com
# provider: generic
# url: https://cherrystudio.ocool.online
provider: github
repo: cherry-studio
owner: CherryHQ
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/
afterPack: scripts/after-pack.js
afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
新增对 grok-2-image 和 gpt-4o-image 图像支持
支持 Windows 便携版使用 data 目录存储数据
MCP 界面改版,新增描述信息显示
Mermaid 渲染逻辑优化
支持关闭公示渲染
修复 OpenAI 类型渲染错误
新增划词助手
助手支持分组
支持主题颜色切换
划词助手支持应用过滤
翻译模块功能改进

View File

@@ -1,5 +1,6 @@
import react from '@vitejs/plugin-react-swc'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import fs from 'fs'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
@@ -9,8 +10,7 @@ const visualizerPlugin = (type: 'renderer' | 'main') => {
export default defineConfig({
main: {
plugins: [
externalizeDepsPlugin({
plugins: [externalizeDepsPlugin({
exclude: [
'@cherrystudio/embedjs',
'@cherrystudio/embedjs-openai',
@@ -25,9 +25,7 @@ export default defineConfig({
'p-queue',
'webdav'
]
}),
...visualizerPlugin('main')
],
}), ...visualizerPlugin('main')],
resolve: {
alias: {
'@main': resolve('src/main'),
@@ -37,7 +35,27 @@ export default defineConfig({
},
build: {
rollupOptions: {
external: ['@libsql/client']
external: ['@libsql/client', 'bufferutil', 'utf-8-validate'],
plugins: [
{
name: 'inject-windows7-polyfill',
generateBundle(_, bundle) {
// 遍历所有生成的文件
for (const fileName in bundle) {
const chunk = bundle[fileName]
if (
chunk.type === 'chunk' &&
chunk.isEntry &&
chunk.fileName.includes('index.js') // 匹配主进程入口文件
) {
const code = fs.readFileSync('src/main/polyfill/windows7-patch.js', 'utf-8')
// 在文件末尾插入自定义代码
chunk.code = code + '\r\n' + chunk.code
}
}
}
}
]
}
}
},
@@ -47,9 +65,16 @@ export default defineConfig({
alias: {
'@shared': resolve('packages/shared')
}
},
build: {
sourcemap: process.env.NODE_ENV === 'development'
}
},
renderer: {
define: {
// 使用方法 (Windows CMD): set CUSTOM_APP_NAME=AppName && yarn run dev
'process.env.CUSTOM_APP_NAME': JSON.stringify(process.env.CUSTOM_APP_NAME)
},
plugins: [
react({
plugins: [
@@ -73,7 +98,20 @@ export default defineConfig({
}
},
optimizeDeps: {
exclude: []
exclude: ['pyodide']
},
worker: {
format: 'es'
},
build: {
rollupOptions: {
input: {
index: resolve(__dirname, 'src/renderer/index.html'),
miniWindow: resolve(__dirname, 'src/renderer/miniWindow.html'),
selectionToolbar: resolve(__dirname, 'src/renderer/selectionToolbar.html'),
selectionAction: resolve(__dirname, 'src/renderer/selectionAction.html')
}
}
}
}
})

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.2.10",
"version": "1.4.1",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -20,12 +20,14 @@
"scripts": {
"start": "electron-vite preview",
"dev": "electron-vite dev",
"debug": "electron-vite -- --inspect --sourcemap --remote-debugging-port=9222",
"build": "npm run typecheck && electron-vite build",
"build:check": "yarn test && yarn typecheck && yarn check:i18n",
"build:check": "yarn typecheck && yarn check:i18n && yarn test",
"build:unpack": "dotenv npm run build && electron-builder --dir",
"build:win": "dotenv npm run build && electron-builder --win --x64 --arm64",
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
"build:win:arm64": "dotenv npm run build && electron-builder --win --arm64",
"build:win7": "xcopy \"src\\patch\\windows7\\\" \"node_modules\\\" /E /Y && dotenv npm run build && electron-builder --win --x64",
"build:mac": "dotenv electron-vite build && electron-builder --mac --arm64 --x64",
"build:mac:arm64": "dotenv electron-vite build && electron-builder --mac --arm64",
"build:mac:x64": "dotenv electron-vite build && electron-builder --mac --x64",
@@ -37,70 +39,78 @@
"publish": "yarn build:check && yarn release patch push",
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
"generate:agents": "yarn workspace @cherry-studio/database agents",
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
"analyze:main": "VISUALIZER_MAIN=true yarn build",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"check:i18n": "node scripts/check-i18n.js",
"test": "yarn test:renderer",
"test:coverage": "yarn test:renderer:coverage",
"test:node": "npx -y tsx --test src/**/*.test.ts",
"test:renderer": "vitest run",
"test:renderer:ui": "vitest --ui",
"test:renderer:coverage": "vitest run --coverage",
"test": "vitest run --silent",
"test:main": "vitest run --project main",
"test:renderer": "vitest run --project renderer",
"test:update": "yarn test:renderer --update",
"test:coverage": "vitest run --coverage --silent",
"test:ui": "vitest --ui",
"test:watch": "vitest",
"test:e2e": "yarn playwright test",
"test:lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"postinstall": "electron-builder install-app-deps",
"prepare": "husky"
},
"dependencies": {
"@cherrystudio/embedjs": "^0.1.28",
"@cherrystudio/embedjs-libsql": "^0.1.28",
"@cherrystudio/embedjs-loader-csv": "^0.1.28",
"@cherrystudio/embedjs-loader-image": "^0.1.28",
"@cherrystudio/embedjs-loader-markdown": "^0.1.28",
"@cherrystudio/embedjs-loader-msoffice": "^0.1.28",
"@cherrystudio/embedjs-loader-pdf": "^0.1.28",
"@cherrystudio/embedjs-loader-sitemap": "^0.1.28",
"@cherrystudio/embedjs-loader-web": "^0.1.28",
"@cherrystudio/embedjs-loader-xml": "^0.1.28",
"@cherrystudio/embedjs-openai": "^0.1.28",
"@cherrystudio/embedjs": "^0.1.31",
"@cherrystudio/embedjs-libsql": "^0.1.31",
"@cherrystudio/embedjs-loader-csv": "^0.1.31",
"@cherrystudio/embedjs-loader-image": "^0.1.31",
"@cherrystudio/embedjs-loader-markdown": "^0.1.31",
"@cherrystudio/embedjs-loader-msoffice": "^0.1.31",
"@cherrystudio/embedjs-loader-pdf": "^0.1.31",
"@cherrystudio/embedjs-loader-sitemap": "^0.1.31",
"@cherrystudio/embedjs-loader-web": "^0.1.31",
"@cherrystudio/embedjs-loader-xml": "^0.1.31",
"@cherrystudio/embedjs-openai": "^0.1.31",
"@electron-toolkit/utils": "^3.0.0",
"@electron/notarize": "^2.5.0",
"@langchain/community": "^0.3.36",
"@libsql/client": "^0.15.2",
"@libsql/win32-x64-msvc": "^0.5.4",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@peculiar/webcrypto": "^1.5.0",
"@strongtz/win32-arm64-msvc": "^0.4.7",
"@tanstack/react-query": "^5.27.0",
"@types/react-infinite-scroll-component": "^5.0.0",
"adm-zip": "^0.5.16",
"archiver": "^7.0.1",
"async-mutex": "^0.5.0",
"blob-polyfill": "^9.0.20240710",
"bufferutil": "^4.0.9",
"color": "^5.0.0",
"diff": "^7.0.0",
"docx": "^9.0.2",
"domexception": "^4.0.0",
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "patch:electron-updater@npm%3A6.6.3#~/.yarn/patches/electron-updater-npm-6.6.3-9269dbaf84.patch",
"electron-updater": "6.6.2",
"electron-window-state": "^5.0.3",
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
"extract-zip": "^2.0.1",
"fast-xml-parser": "^5.2.0",
"fetch-socks": "^1.3.2",
"franc-min": "^6.2.0",
"fs-extra": "^11.2.0",
"got-scraping": "^4.1.1",
"jsdom": "^26.0.0",
"libsql": "^0.5.4",
"markdown-it": "^14.1.0",
"node-fetch": "2",
"node-stream-zip": "^1.15.0",
"officeparser": "^4.1.1",
"os-proxy-config": "^1.1.2",
"proxy-agent": "^6.5.0",
"selection-hook": "^0.9.21",
"tar": "^7.4.3",
"turndown": "^7.2.0",
"turndown-plugin-gfm": "^1.0.2",
"undici": "^7.4.0",
"web-streams-polyfill": "^4.1.0",
"webdav": "^5.8.0",
"ws": "^8.18.1",
"zipread": "^1.3.3"
},
"devDependencies": {
@@ -108,26 +118,29 @@
"@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@anthropic-ai/sdk": "^0.38.0",
"@anthropic-ai/sdk": "^0.41.0",
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@electron/notarize": "^2.5.0",
"@emotion/is-prop-valid": "^1.3.1",
"@eslint-react/eslint-plugin": "^1.36.1",
"@eslint/js": "^9.22.0",
"@google/genai": "^0.10.0",
"@google/genai": "^1.0.1",
"@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0",
"@modelcontextprotocol/sdk": "^1.10.2",
"@modelcontextprotocol/sdk": "^1.11.4",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@peculiar/webcrypto": "^1.5.0",
"@reduxjs/toolkit": "^2.2.5",
"@shikijs/markdown-it": "^3.2.2",
"@swc/plugin-styled-components": "^7.1.3",
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
"@shikijs/markdown-it": "^3.4.2",
"@swc/plugin-styled-components": "^7.1.5",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@tryfabric/martian": "^1.2.4",
"@types/adm-zip": "^0",
"@types/diff": "^7",
"@types/fs-extra": "^11",
"@types/lodash": "^4.17.5",
@@ -138,45 +151,53 @@
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@types/react-infinite-scroll-component": "^5.0.0",
"@types/react-window": "^1",
"@types/tinycolor2": "^1",
"@types/ws": "^8",
"@uiw/codemirror-extensions-langs": "^4.23.12",
"@uiw/codemirror-themes-all": "^4.23.12",
"@uiw/react-codemirror": "^4.23.12",
"@vitejs/plugin-react-swc": "^3.9.0",
"@vitest/coverage-v8": "^3.1.1",
"@vitest/ui": "^3.1.1",
"@vitest/browser": "^3.1.4",
"@vitest/coverage-v8": "^3.1.4",
"@vitest/ui": "^3.1.4",
"@vitest/web-worker": "^3.1.4",
"@xyflow/react": "^12.4.4",
"antd": "^5.22.5",
"applescript": "^1.0.0",
"axios": "^1.7.3",
"babel-plugin-styled-components": "^2.1.4",
"browser-image-compression": "^2.0.2",
"color": "^5.0.0",
"dayjs": "^1.11.11",
"dexie": "^4.0.8",
"dexie-react-hooks": "^1.1.7",
"dotenv-cli": "^7.4.2",
"electron": "31.7.6",
"electron-builder": "26.0.13",
"electron": "22.3.23",
"electron-builder": "^24.9.1",
"electron-devtools-installer": "^3.2.0",
"electron-icon-builder": "^2.0.1",
"electron-vite": "^2.3.0",
"electron-vite": "^3.1.0",
"emittery": "^1.0.3",
"emoji-picker-element": "^1.22.1",
"eslint": "^9.22.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4",
"fast-diff": "^1.3.0",
"html-to-image": "^1.11.13",
"husky": "^9.1.7",
"i18next": "^23.11.5",
"jest-styled-components": "^7.2.0",
"lint-staged": "^15.5.0",
"lodash": "^4.17.21",
"lru-cache": "^11.1.0",
"lucide-react": "^0.487.0",
"mermaid": "^11.6.0",
"mime": "^4.0.4",
"motion": "^12.10.5",
"npx-scope-finder": "^1.2.0",
"openai": "patch:openai@npm%3A4.87.3#~/.yarn/patches/openai-npm-4.87.3-2b30a7685f.patch",
"openai": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
"p-queue": "^8.1.0",
"playwright": "^1.52.0",
"prettier": "^3.5.3",
"rc-virtual-list": "^3.18.5",
"rc-virtual-list": "^3.18.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hotkeys-hook": "^4.6.1",
@@ -187,6 +208,7 @@
"react-router": "6",
"react-router-dom": "6",
"react-spinners": "^0.14.1",
"react-window": "^1.8.11",
"redux": "^5.0.1",
"redux-persist": "^6.0.0",
"rehype-katex": "^7.0.1",
@@ -196,30 +218,33 @@
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.2",
"shiki": "^3.2.2",
"sass": "^1.88.0",
"shiki": "^3.4.2",
"string-width": "^7.2.0",
"styled-components": "^6.1.11",
"tiny-pinyin": "^1.3.2",
"tinycolor2": "^1.6.0",
"tokenx": "^0.4.1",
"typescript": "^5.6.2",
"uuid": "^10.0.0",
"vite": "6.2.6",
"vitest": "^3.1.1"
"vitest": "^3.1.4"
},
"resolutions": {
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
"node-gyp": "^9.1.0",
"libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch",
"openai@npm:^4.77.0": "patch:openai@npm%3A4.87.3#~/.yarn/patches/openai-npm-4.87.3-2b30a7685f.patch",
"openai@npm:^4.77.0": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
"@types/domexception": "^4",
"electron": "22.3.23",
"electron-builder": "^24.9.1",
"app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch",
"shiki": "3.2.2"
"openai@npm:^4.87.3": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
"app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch",
"@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A0.3.44#~/.yarn/patches/@langchain-core-npm-0.3.44-41d5c3cb0a.patch"
},
"packageManager": "yarn@4.6.0",
"packageManager": "yarn@4.9.1",
"lint-staged": {
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts}": [
"prettier --write",

View File

@@ -1 +0,0 @@
# Cherry Studio Artifacts

View File

@@ -1,19 +0,0 @@
{
"name": "@cherry-studio/artifacts",
"version": "0.1.0",
"description": "Cherry Studio Artifacts",
"main": "index.js",
"homepage": "https://github.com/kangfenmao/cherry-studio/blob/main/npm/artifacts",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"artifacts"
],
"author": "kangfenmao",
"license": "ISC"
}

View File

@@ -1,108 +0,0 @@
:root {
/* 莫兰迪色系:使用柔和、低饱和度的颜色 */
--primary-color: #b6b5a7; /* 莫兰迪灰褐色,用于背景文字 */
--secondary-color: #9a8f8f; /* 莫兰迪灰棕色,用于标题背景 */
--accent-color: #c5b4a0; /* 莫兰迪淡棕色,用于强调元素 */
--background-color: #e8e3de; /* 莫兰迪米色,用于页面背景 */
--text-color: #5b5b5b; /* 莫兰迪深灰色,用于主要文字 */
--light-text-color: #8c8c8c; /* 莫兰迪中灰色,用于次要文字 */
--divider-color: #d1cbc3; /* 莫兰迪浅灰色,用于分隔线 */
}
body,
html {
margin: 0;
padding: 0;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--background-color); /* 使用莫兰迪米色作为页面背景 */
font-family: 'Noto Sans SC', sans-serif;
color: var(--text-color); /* 使用莫兰迪深灰色作为主要文字颜色 */
}
.card {
width: 300px;
height: 500px;
background-color: #f2ede9; /* 莫兰迪浅米色,用于卡片背景 */
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
}
.header {
background-color: var(--secondary-color); /* 使用莫兰迪灰棕色作为标题背景 */
color: #f2ede9; /* 浅色文字与深色背景形成对比 */
padding: 20px;
text-align: left;
position: relative;
z-index: 1;
}
h1 {
font-family: 'Noto Serif SC', serif;
font-size: 20px;
margin: 0;
font-weight: 700;
}
.content {
padding: 30px 20px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.word {
text-align: left;
margin-bottom: 20px;
}
.word-main {
font-family: 'Noto Serif SC', serif;
font-size: 36px;
color: var(--text-color); /* 使用莫兰迪深灰色作为主要词汇颜色 */
margin-bottom: 10px;
position: relative;
}
.word-main::after {
content: '';
position: absolute;
left: 0;
bottom: -5px;
width: 50px;
height: 3px;
background-color: var(--accent-color); /* 使用莫兰迪淡棕色作为下划线 */
}
.word-sub {
font-size: 14px;
color: var(--light-text-color); /* 使用莫兰迪中灰色作为次要文字颜色 */
margin: 5px 0;
}
.divider {
width: 100%;
height: 1px;
background-color: var(--divider-color); /* 使用莫兰迪浅灰色作为分隔线 */
margin: 20px 0;
}
.explanation {
font-size: 18px;
line-height: 1.6;
text-align: left;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.quote {
position: relative;
padding-left: 20px;
border-left: 3px solid var(--accent-color); /* 使用莫兰迪淡棕色作为引用边框 */
}
.background-text {
position: absolute;
font-size: 150px;
color: rgba(182, 181, 167, 0.15); /* 使用莫兰迪灰褐色的透明版本作为背景文字 */
z-index: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
}

View File

@@ -1,3 +0,0 @@
data/*
!data/.gitkeep

View File

@@ -1,3 +0,0 @@
# Cherry Studio Database
Cherry Studio 依赖的数据文件由这个数据库来生成,数据库文件请联系开发者获取

View File

@@ -1,13 +0,0 @@
{
"name": "@cherry-studio/database",
"packageManager": "yarn@4.6.0",
"dependencies": {
"csv-parser": "^3.0.0",
"sqlite3": "^5.1.7"
},
"scripts": {
"agents": "node src/agents.js",
"email": "yarn csv && node src/email.js",
"csv": "node src/csv.js"
}
}

View File

@@ -1,47 +0,0 @@
const sqlite3 = require('sqlite3').verbose()
const fs = require('fs')
// 连接到数据库
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
if (err) {
console.error('Error connecting to the database:', err.message)
return
}
console.log('Connected to the database.')
})
// 查询数据并转换为JSON
db.all('SELECT * FROM agents', [], (err, rows) => {
if (err) {
console.error('Error querying the database:', err.message)
return
}
// 将 ID 类型转换为字符串
for (const row of rows) {
row.id = row.id.toString()
row.group = row.group.toString().split(',')
row.group = row.group.map((item) => item.trim().replace('\r\n', ''))
}
// 将查询结果转换为JSON字符串
const jsonData = JSON.stringify(rows, null, 2)
// 将JSON数据写入文件
fs.writeFile('../../src/renderer/src/config/agents.json', jsonData, (err) => {
if (err) {
console.error('Error writing to file:', err.message)
return
}
console.log('Data has been written to agents.json')
})
// 关闭数据库连接
db.close((err) => {
if (err) {
console.error('Error closing the database:', err.message)
return
}
console.log('Database connection closed.')
})
})

View File

@@ -1,77 +0,0 @@
const fs = require('fs')
const csv = require('csv-parser')
const sqlite3 = require('sqlite3').verbose()
// 连接到 SQLite 数据库
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
if (err) {
console.error('Error opening database', err)
return
}
console.log('Connected to the SQLite database.')
})
// 创建一个数组来存储 CSV 数据
const results = []
// 读取 CSV 文件
fs.createReadStream('./data/data.csv')
.pipe(csv())
.on('data', (data) => results.push(data))
.on('end', () => {
// 准备 SQL 插入语句,使用 INSERT OR IGNORE
const stmt = db.prepare('INSERT OR IGNORE INTO emails (email, github, sent) VALUES (?, ?, ?)')
// 插入每一行数据
let inserted = 0
let skipped = 0
let emptyEmail = 0
db.serialize(() => {
// 开始一个事务以提高性能
db.run('BEGIN TRANSACTION')
results.forEach((row) => {
// 检查 email 是否为空
if (!row.email || row.email.trim() === '') {
emptyEmail++
return // 跳过这一行
}
stmt.run(row.email, row['user-href'], 0, function (err) {
if (err) {
console.error('Error inserting row', err)
} else {
if (this.changes === 1) {
inserted++
} else {
skipped++
}
}
})
})
// 提交事务
db.run('COMMIT', (err) => {
if (err) {
console.error('Error committing transaction', err)
} else {
console.log(
`Insertion complete. Inserted: ${inserted}, Skipped (duplicate): ${skipped}, Skipped (empty email): ${emptyEmail}`
)
}
// 完成插入
stmt.finalize()
// 关闭数据库连接
db.close((err) => {
if (err) {
console.error('Error closing database', err)
} else {
console.log('Database connection closed.')
}
})
})
})
})

View File

@@ -1,36 +0,0 @@
const sqlite3 = require('sqlite3').verbose()
// 连接到数据库
const db = new sqlite3.Database('./data/CherryStudio.sqlite3', (err) => {
if (err) {
console.error('Error connecting to the database:', err.message)
return
}
})
// 查询数据并转换为JSON
db.all('SELECT * FROM emails WHERE sent = 0', [], (err, rows) => {
if (err) {
console.error('Error querying the database:', err.message)
return
}
for (const row of rows) {
console.log(row.email)
// Update row set sent = 1
db.run('UPDATE emails SET sent = 1 WHERE id = ?', [row.id], (err) => {
if (err) {
console.error('Error updating the database:', err.message)
return
}
})
}
// 关闭数据库连接
db.close((err) => {
if (err) {
console.error('Error closing the database:', err.message)
return
}
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
export enum IpcChannel {
App_GetCacheSize = 'app:get-cache-size',
App_ClearCache = 'app:clear-cache',
App_SetLaunchOnBoot = 'app:set-launch-on-boot',
App_SetLanguage = 'app:set-language',
@@ -10,16 +11,18 @@ export enum IpcChannel {
App_SetLaunchToTray = 'app:set-launch-to-tray',
App_SetTray = 'app:set-tray',
App_SetTrayOnClose = 'app:set-tray-on-close',
App_RestartTray = 'app:restart-tray',
App_SetTheme = 'app:set-theme',
App_SetCustomCss = 'app:set-custom-css',
App_SetAutoUpdate = 'app:set-auto-update',
App_HandleZoomFactor = 'app:handle-zoom-factor',
App_IsBinaryExist = 'app:is-binary-exist',
App_GetBinaryPath = 'app:get-binary-path',
App_InstallUvBinary = 'app:install-uv-binary',
App_InstallBunBinary = 'app:install-bun-binary',
Notification_Send = 'notification:send',
Notification_OnClick = 'notification:on-click',
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
// Open
@@ -51,6 +54,7 @@ export enum IpcChannel {
Mcp_GetInstallInfo = 'mcp:get-install-info',
Mcp_ServersChanged = 'mcp:servers-changed',
Mcp_ServersUpdated = 'mcp:servers-updated',
Mcp_CheckConnectivity = 'mcp:check-connectivity',
//copilot
Copilot_GetAuthMessage = 'copilot:get-auth-message',
@@ -82,8 +86,6 @@ export enum IpcChannel {
Windows_ResetMinimumSize = 'window:reset-minimum-size',
Windows_SetMinimumSize = 'window:set-minimum-size',
SelectionMenu_Action = 'selection-menu:action',
KnowledgeBase_Create = 'knowledge-base:create',
KnowledgeBase_Reset = 'knowledge-base:reset',
KnowledgeBase_Delete = 'knowledge-base:delete',
@@ -105,12 +107,14 @@ export enum IpcChannel {
File_SelectFolder = 'file:selectFolder',
File_Create = 'file:create',
File_Write = 'file:write',
File_WriteWithId = 'file:writeWithId',
File_SaveImage = 'file:saveImage',
File_Base64Image = 'file:base64Image',
File_SaveBase64Image = 'file:saveBase64Image',
File_Download = 'file:download',
File_Copy = 'file:copy',
File_BinaryFile = 'file:binaryFile',
File_BinaryImage = 'file:binaryImage',
File_Base64File = 'file:base64File',
Fs_Read = 'fs:read',
Export_Word = 'export:word',
@@ -135,10 +139,12 @@ export enum IpcChannel {
System_GetDeviceType = 'system:getDeviceType',
System_GetHostname = 'system:getHostname',
// DevTools
System_ToggleDevTools = 'system:toggleDevTools',
// events
SelectionAction = 'selection-action',
BackupProgress = 'backup-progress',
ThemeChange = 'theme:change',
ThemeUpdated = 'theme:updated',
UpdateDownloadedCancelled = 'update-downloaded-cancelled',
RestoreProgress = 'restore-progress',
UpdateError = 'update-error',
@@ -154,7 +160,6 @@ export enum IpcChannel {
HideMiniWindow = 'hide-mini-window',
ShowMiniWindow = 'show-mini-window',
MiniWindowReload = 'miniwindow-reload',
ReduxStateChange = 'redux-state-change',
ReduxStoreReady = 'redux-store-ready',
@@ -162,5 +167,32 @@ export enum IpcChannel {
// Search Window
SearchWindow_Open = 'search-window:open',
SearchWindow_Close = 'search-window:close',
SearchWindow_OpenUrl = 'search-window:open-url'
SearchWindow_OpenUrl = 'search-window:open-url',
//Store Sync
StoreSync_Subscribe = 'store-sync:subscribe',
StoreSync_Unsubscribe = 'store-sync:unsubscribe',
StoreSync_OnUpdate = 'store-sync:on-update',
StoreSync_BroadcastSync = 'store-sync:broadcast-sync',
// Provider
Provider_AddKey = 'provider:add-key',
//Selection Assistant
Selection_TextSelected = 'selection:text-selected',
Selection_ToolbarHide = 'selection:toolbar-hide',
Selection_ToolbarVisibilityChange = 'selection:toolbar-visibility-change',
Selection_ToolbarDetermineSize = 'selection:toolbar-determine-size',
Selection_WriteToClipboard = 'selection:write-to-clipboard',
Selection_SetEnabled = 'selection:set-enabled',
Selection_SetTriggerMode = 'selection:set-trigger-mode',
Selection_SetFilterMode = 'selection:set-filter-mode',
Selection_SetFilterList = 'selection:set-filter-list',
Selection_SetFollowToolbar = 'selection:set-follow-toolbar',
Selection_SetRemeberWinSize = 'selection:set-remeber-win-size',
Selection_ActionWindowClose = 'selection:action-window-close',
Selection_ActionWindowMinimize = 'selection:action-window-minimize',
Selection_ActionWindowPin = 'selection:action-window-pin',
Selection_ProcessAction = 'selection:process-action',
Selection_UpdateActionData = 'selection:update-action-data'
}

View File

@@ -4,135 +4,376 @@ export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const thirdPartyApplicationExts = ['.draftsExport']
export const bookExts = ['.epub']
export const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件
'.mdx', // Markdown 文件
'.html', // HTML 文件
'.htm', // HTML 文件的另一种扩展名
'.xml', // XML 文件
'.json', // JSON 文件
'.yaml', // YAML 文件
'.yml', // YAML 文件的另一种扩展名
'.csv', // 逗号分隔值文件
'.tsv', // 制表符分隔值文件
'.ini', // 配置文件
'.log', // 日志文件
'.rtf', // 富文本格式文件
'.org', // org-mode 文件
'.wiki', // VimWiki 文件
'.tex', // LaTeX 文件
'.bib', // BibTeX 文件
'.srt', // 字幕文件
'.xhtml', // XHTML 文件
'.nfo', // 信息文件(主要用于场景发布)
'.conf', // 配置文件
'.config', // 配置文件
'.env', // 环境变量文件
'.rst', // reStructuredText 文件
'.php', // PHP 脚本文件,包含嵌入的 HTML
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
'.ts', // TypeScript 文件
'.jsp', // JavaServer Pages 文件
'.aspx', // ASP.NET 文件
'.bat', // Windows 批处理文件
'.sh', // Unix/Linux Shell 脚本文件
'.py', // Python 脚本文件
'.ipynb', // Jupyter 笔记本格式
'.rb', // Ruby 脚本文件
'.pl', // Perl 脚本文件
'.sql', // SQL 脚本文件
'.css', // Cascading Style Sheets 文件
'.less', // Less CSS 预处理器文件
'.scss', // Sass CSS 预处理器文件
'.sass', // Sass 文件
'.styl', // Stylus CSS 预处理器文件
'.coffee', // CoffeeScript 文件
'.ino', // Arduino 代码文件
'.asm', // Assembly 语言文件
'.go', // Go 语言文件
'.scala', // Scala 语言文件
'.swift', // Swift 语言文件
'.kt', // Kotlin 语言文件
'.rs', // Rust 语言文件
'.lua', // Lua 语言文件
'.groovy', // Groovy 语言文件
'.dart', // Dart 语言文件
'.hs', // Haskell 语言文件
'.clj', // Clojure 语言文件
'.cljs', // ClojureScript 语言文件
'.elm', // Elm 语言文件
'.erl', // Erlang 语言文件
'.ex', // Elixir 语言文件
'.exs', // Elixir 脚本文件
'.pug', // Pug (formerly Jade) 模板文件
'.haml', // Haml 模板文件
'.slim', // Slim 模板文件
'.tpl', // 模板文件(通用)
'.ejs', // Embedded JavaScript 模板文件
'.hbs', // Handlebars 模板文件
'.mustache', // Mustache 模板文件
'.jade', // Jade 模板文件 (已重命名为 Pug)
'.twig', // Twig 模板文件
'.blade', // Blade 模板文件 (Laravel)
'.vue', // Vue.js 单文件组件
'.jsx', // React JSX 文件
'.tsx', // React TSX 文件
'.graphql', // GraphQL 查询语言文件
'.gql', // GraphQL 查询语言文件
'.proto', // Protocol Buffers 文件
'.thrift', // Thrift 文件
'.toml', // TOML 配置文件
'.edn', // Clojure 数据表示文件
'.cake', // CakePHP 配置文件
'.ctp', // CakePHP 视图文件
'.cfm', // ColdFusion 标记语言文件
'.cfc', // ColdFusion 组件文件
'.m', // Objective-C 或 MATLAB 源文件
'.mm', // Objective-C++ 源文件
'.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件
'.kts', // Kotlin Script 文件
'.java', // Java 代码文件
'.cs', // C# 代码文件
'.cpp', // C++ 代码文件
'.c', // C++ 代码文件
'.h', // C++ 头文件
'.hpp', // C++ 头文件
'.cc', // C++ 源文件
'.cxx', // C++ 源文件
'.cppm', // C++20 模块接口文件
'.ipp', // 模板实现文件
'.ixx', // C++20 模块实现文件
'.f90', // Fortran 90 源文件
'.f', // Fortran 固定格式源代码文件
'.f03', // Fortran 2003+ 源代码文件
'.ahk', // AutoHotKey 语言文件
'.tcl', // Tcl 脚本
'.do', // Questa 或 Modelsim Tcl 脚本
'.v', // Verilog 源文件
'.sv', // SystemVerilog 源文件
'.svh', // SystemVerilog 头文件
'.vhd', // VHDL 源文件
'.vhdl', // VHDL 源文件
'.lef', // Library Exchange Format
'.def', // Design Exchange Format
'.edif', // Electronic Design Interchange Format
'.sdf', // Standard Delay Format
'.sdc', // Synopsys Design Constraints
'.xdc', // Xilinx Design Constraints
'.rpt', // 报告文件
'.lisp', // Lisp 脚本
'.il', // Cadence SKILL 脚本
'.ils', // Cadence SKILL++ 脚本
'.sp', // SPICE netlist 文件
'.spi', // SPICE netlist 文件
'.cir', // SPICE netlist 文件
'.net', // SPICE netlist 文件
'.scs', // Spectre netlist 文件
'.asc', // LTspice netlist schematic 文件
'.tf' // Technology File
const textExtsByCategory = new Map([
[
'language',
[
'.js',
'.mjs',
'.cjs',
'.ts',
'.jsx',
'.tsx', // JavaScript/TypeScript
'.py', // Python
'.java', // Java
'.cs', // C#
'.cpp',
'.c',
'.h',
'.hpp',
'.cc',
'.cxx',
'.cppm',
'.ipp',
'.ixx', // C/C++
'.php', // PHP
'.rb', // Ruby
'.pl', // Perl
'.go', // Go
'.rs', // Rust
'.swift', // Swift
'.kt',
'.kts', // Kotlin
'.scala', // Scala
'.lua', // Lua
'.groovy', // Groovy
'.dart', // Dart
'.hs', // Haskell
'.clj',
'.cljs', // Clojure
'.elm', // Elm
'.erl', // Erlang
'.ex',
'.exs', // Elixir
'.ml',
'.mli', // OCaml
'.fs', // F#
'.r',
'.R', // R
'.sol', // Solidity
'.awk', // AWK
'.cob', // COBOL
'.asm',
'.s', // Assembly
'.lisp',
'.lsp', // Lisp
'.coffee', // CoffeeScript
'.ino', // Arduino
'.jl', // Julia
'.nim', // Nim
'.zig', // Zig
'.d', // D语言
'.pas', // Pascal
'.vb', // Visual Basic
'.rkt', // Racket
'.scm', // Scheme
'.hx', // Haxe
'.as', // ActionScript
'.pde', // Processing
'.f90',
'.f',
'.f03',
'.for',
'.f95', // Fortran
'.adb',
'.ads', // Ada
'.pro', // Prolog
'.m',
'.mm', // Objective-C/MATLAB
'.rpy', // Ren'Py
'.ets', // OpenHarmony,
'.uniswap', // DeFi
'.vy', // Vyper
'.shader',
'.glsl',
'.frag',
'.vert',
'.gd' // Godot
]
],
[
'script',
[
'.sh', // Shell
'.bat',
'.cmd', // Windows批处理
'.ps1', // PowerShell
'.tcl',
'.do', // Tcl
'.ahk', // AutoHotkey
'.zsh', // Zsh
'.fish', // Fish shell
'.csh', // C shell
'.vbs', // VBScript
'.applescript', // AppleScript
'.au3', // AutoIt
'.bash',
'.nu'
]
],
[
'style',
[
'.css', // CSS
'.less', // Less
'.scss',
'.sass', // Sass
'.styl', // Stylus
'.pcss', // PostCSS
'.postcss' // PostCSS
]
],
[
'template',
[
'.vue', // Vue.js
'.pug',
'.jade', // Pug/Jade
'.haml', // Haml
'.slim', // Slim
'.tpl', // 通用模板
'.ejs', // EJS
'.hbs', // Handlebars
'.mustache', // Mustache
'.twig', // Twig
'.blade', // Blade (Laravel)
'.liquid', // Liquid
'.jinja',
'.jinja2',
'.j2', // Jinja
'.erb', // ERB
'.vm', // Velocity
'.ftl', // FreeMarker
'.svelte', // Svelte
'.astro' // Astro
]
],
[
'config',
[
'.ini', // INI配置
'.conf',
'.config', // 通用配置
'.env', // 环境变量
'.toml', // TOML
'.cfg', // 通用配置
'.properties', // Java属性
'.desktop', // Linux桌面文件
'.service', // systemd服务
'.rc',
'.bashrc',
'.zshrc', // Shell配置
'.fishrc', // Fish shell配置
'.vimrc', // Vim配置
'.htaccess', // Apache配置
'.robots', // robots.txt
'.editorconfig', // EditorConfig
'.eslintrc', // ESLint
'.prettierrc', // Prettier
'.babelrc', // Babel
'.npmrc', // npm
'.dockerignore', // Docker ignore
'.npmignore',
'.yarnrc',
'.prettierignore',
'.eslintignore',
'.browserslistrc',
'.json5',
'.tfvars'
]
],
[
'document',
[
'.txt',
'.text', // 纯文本
'.md',
'.mdx', // Markdown
'.html',
'.htm',
'.xhtml', // HTML
'.xml', // XML
'.org', // Org-mode
'.wiki', // Wiki
'.tex',
'.bib', // LaTeX
'.rst', // reStructuredText
'.rtf', // 富文本
'.nfo', // 信息文件
'.adoc',
'.asciidoc', // AsciiDoc
'.pod', // Perl文档
'.1',
'.2',
'.3',
'.4',
'.5',
'.6',
'.7',
'.8',
'.9', // man页面
'.man', // man页面
'.texi',
'.texinfo', // Texinfo
'.readme',
'.me', // README
'.changelog', // 变更日志
'.license', // 许可证
'.authors', // 作者文件
'.po',
'.pot'
]
],
[
'data',
[
'.json', // JSON
'.jsonc', // JSON with comments
'.yaml',
'.yml', // YAML
'.csv',
'.tsv', // 分隔值文件
'.edn', // Clojure数据
'.jsonl',
'.ndjson', // 换行分隔JSON
'.geojson', // GeoJSON
'.gpx', // GPS Exchange
'.kml', // Keyhole Markup
'.rss',
'.atom', // Feed格式
'.vcf', // vCard
'.ics', // iCalendar
'.ldif', // LDAP数据交换
'.pbtxt',
'.map'
]
],
[
'build',
[
'.gradle', // Gradle
'.make',
'.mk', // Make
'.cmake', // CMake
'.sbt', // SBT
'.rake', // Rake
'.spec', // RPM spec
'.pom',
'.build', // Meson
'.bazel' // Bazel
]
],
[
'database',
[
'.sql', // SQL
'.ddl',
'.dml', // DDL/DML
'.plsql', // PL/SQL
'.psql', // PostgreSQL
'.cypher', // Cypher
'.sparql' // SPARQL
]
],
[
'web',
[
'.graphql',
'.gql', // GraphQL
'.proto', // Protocol Buffers
'.thrift', // Thrift
'.wsdl', // WSDL
'.raml', // RAML
'.swagger',
'.openapi' // API文档
]
],
[
'version',
[
'.gitignore', // Git ignore
'.gitattributes', // Git attributes
'.gitconfig', // Git config
'.hgignore', // Mercurial ignore
'.bzrignore', // Bazaar ignore
'.svnignore', // SVN ignore
'.githistory' // Git history
]
],
[
'subtitle',
[
'.srt',
'.sub',
'.ass' // 字幕格式
]
],
[
'log',
[
'.log',
'.rpt' // 日志和报告 (移除了.out因为通常是二进制可执行文件)
]
],
[
'eda',
[
'.v',
'.sv',
'.svh', // Verilog/SystemVerilog
'.vhd',
'.vhdl', // VHDL
'.lef',
'.def', // LEF/DEF
'.edif', // EDIF
'.sdf', // SDF
'.sdc',
'.xdc', // 约束文件
'.sp',
'.spi',
'.cir',
'.net', // SPICE
'.scs', // Spectre
'.asc', // LTspice
'.tf', // Technology File
'.il',
'.ils' // SKILL
]
],
[
'game',
[
'.mtl', // Material Template Library
'.x3d', // X3D文件
'.gltf', // glTF JSON
'.prefab', // Unity预制体 (YAML格式)
'.meta' // Unity元数据文件 (YAML格式)
]
],
[
'other',
[
'.mcfunction', // Minecraft函数
'.jsp', // JSP
'.aspx', // ASP.NET
'.ipynb', // Jupyter Notebook
'.cake',
'.ctp', // CakePHP
'.cfm',
'.cfc' // ColdFusion
]
]
])
export const textExts = Array.from(textExtsByCategory.values()).flat()
export const ZOOM_LEVELS = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5]
// 从 ZOOM_LEVELS 生成 Ant Design Select 所需的 options 结构
export const ZOOM_OPTIONS = ZOOM_LEVELS.map((level) => ({
value: level,
label: `${Math.round(level * 100)}%`
}))
export const ZOOM_SHORTCUTS = [
{

42
playwright.config.ts Normal file
View File

@@ -0,0 +1,42 @@
import { defineConfig, devices } from '@playwright/test'
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
// Look for test files, relative to this configuration file.
testDir: './tests/e2e',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry'
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
]
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://localhost:3000',
// reuseExistingServer: !process.env.CI,
// },
})

View File

@@ -15,6 +15,8 @@ const BUN_PACKAGES = {
'darwin-x64': 'bun-darwin-x64.zip',
'win32-x64': 'bun-windows-x64.zip',
'win32-x64-baseline': 'bun-windows-x64-baseline.zip',
'win32-arm64': 'bun-windows-x64.zip',
'win32-arm64-baseline': 'bun-windows-x64-baseline.zip',
'linux-x64': 'bun-linux-x64.zip',
'linux-x64-baseline': 'bun-linux-x64-baseline.zip',
'linux-arm64': 'bun-linux-aarch64.zip',

View File

@@ -1,117 +0,0 @@
import Cocoa
import Foundation
class TextSelectionObserver: NSObject {
let workspace = NSWorkspace.shared
var lastSelectedText: String?
override init() {
super.init()
//
let observer = NSWorkspace.shared.notificationCenter
observer.addObserver(
self,
selector: #selector(handleSelectionChange),
name: NSWorkspace.didActivateApplicationNotification,
object: nil
)
//
var axObserver: AXObserver?
let error = AXObserverCreate(getpid(), { observer, element, notification, userData in
let selfPointer = userData!.load(as: TextSelectionObserver.self)
selfPointer.checkSelectedText()
}, &axObserver)
if error == .success, let axObserver = axObserver {
CFRunLoopAddSource(
RunLoop.main.getCFRunLoop(),
AXObserverGetRunLoopSource(axObserver),
.defaultMode
)
//
updateActiveAppObserver(axObserver)
}
}
@objc func handleSelectionChange(_ notification: Notification) {
//
var axObserver: AXObserver?
let error = AXObserverCreate(getpid(), { _, _, _, _ in }, &axObserver)
if error == .success, let axObserver = axObserver {
updateActiveAppObserver(axObserver)
}
}
func updateActiveAppObserver(_ axObserver: AXObserver) {
guard let app = workspace.frontmostApplication else { return }
let pid = app.processIdentifier
let element = AXUIElementCreateApplication(pid)
//
AXObserverAddNotification(
axObserver,
element,
kAXSelectedTextChangedNotification as CFString,
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
}
func checkSelectedText() {
if let text = getSelectedText() {
if text.count > 0 && text != lastSelectedText {
print(text)
fflush(stdout)
lastSelectedText = text
}
}
}
func getSelectedText() -> String? {
guard let app = NSWorkspace.shared.frontmostApplication else { return nil }
let pid = app.processIdentifier
let axApp = AXUIElementCreateApplication(pid)
var focusedElement: AnyObject?
// Get focused element
let result = AXUIElementCopyAttributeValue(axApp, kAXFocusedUIElementAttribute as CFString, &focusedElement)
guard result == .success else { return nil }
// Try different approaches to get selected text
var selectedText: AnyObject?
// First try: Direct selected text
var textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedText)
// Second try: Selected text in text area
if textResult != .success {
var selectedTextRange: AnyObject?
textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextRangeAttribute as CFString, &selectedTextRange)
if textResult == .success {
textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXValueAttribute as CFString, &selectedText)
}
}
// Third try: Get selected text from parent element
if textResult != .success {
var parent: AnyObject?
if AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXParentAttribute as CFString, &parent) == .success {
textResult = AXUIElementCopyAttributeValue(parent as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedText)
}
}
guard textResult == .success, let text = selectedText as? String else { return nil }
return text
}
}
let observer = TextSelectionObserver()
signal(SIGINT) { _ in
exit(0)
}
RunLoop.main.run()

View File

@@ -8,11 +8,6 @@ exports.default = function (buildResult) {
}
let oldFilePath = buildResult.file
if (oldFilePath.includes('-portable') && !oldFilePath.includes('-x64') && !oldFilePath.includes('-arm64')) {
console.log('[artifact build completed] delete portable file:', oldFilePath)
fs.unlinkSync(oldFilePath)
return
}
const newfilePath = oldFilePath.replace(/ /g, '-')
fs.renameSync(oldFilePath, newfilePath)
buildResult.file = newfilePath

View File

@@ -3,7 +3,7 @@ Object.defineProperty(exports, '__esModule', { value: true })
var fs = require('fs')
var path = require('path')
var translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
var baseLocale = 'zh-CN'
var baseLocale = 'en-us'
var baseFileName = ''.concat(baseLocale, '.json')
var baseFilePath = path.join(translationsDir, baseFileName)
/**

View File

@@ -7,7 +7,6 @@ exports.default = async function notarizing(context) {
}
if (!process.env.APPLE_ID || !process.env.APPLE_APP_SPECIFIC_PASSWORD || !process.env.APPLE_TEAM_ID) {
console.log('Skipping notarization')
return
}

View File

@@ -1,16 +1,16 @@
/**
* OCOOL_API_KEY=sk-abcxxxxxxxxxxxxxxxxxxxxxxx123 ts-node scripts/update-i18n.ts
* Paratera_API_KEY=sk-abcxxxxxxxxxxxxxxxxxxxxxxx123 ts-node scripts/update-i18n.ts
*/
// OCOOL API KEY
const OCOOL_API_KEY = process.env.OCOOL_API_KEY
const Paratera_API_KEY = process.env.Paratera_API_KEY
const INDEX = [
// 语言的名称 代码 用来翻译的模型
{ name: 'France', code: 'fr-fr', model: 'qwen2.5-32b-instruct' },
{ name: 'Spanish', code: 'es-es', model: 'qwen2.5-32b-instruct' },
{ name: 'Portuguese', code: 'pt-pt', model: 'qwen2.5-72b-instruct' },
{ name: 'Greek', code: 'el-gr', model: 'qwen-turbo' }
{ name: 'France', code: 'fr-fr', model: 'Qwen3-235B-A22B' },
{ name: 'Spanish', code: 'es-es', model: 'Qwen3-235B-A22B' },
{ name: 'Portuguese', code: 'pt-pt', model: 'Qwen3-235B-A22B' },
{ name: 'Greek', code: 'el-gr', model: 'Qwen3-235B-A22B' }
]
const fs = require('fs')
@@ -19,8 +19,8 @@ import OpenAI from 'openai'
const zh = JSON.parse(fs.readFileSync('src/renderer/src/i18n/locales/zh-cn.json', 'utf8')) as object
const openai = new OpenAI({
apiKey: OCOOL_API_KEY,
baseURL: 'https://one.ocoolai.com/v1'
apiKey: Paratera_API_KEY,
baseURL: 'https://llmapi.paratera.com/v1'
})
// 递归遍历翻译

19
scripts/win-sign.js Normal file
View File

@@ -0,0 +1,19 @@
const { execSync } = require('child_process')
exports.default = async function (configuration) {
if (process.env.WIN_SIGN) {
const { path } = configuration
if (configuration.path) {
try {
console.log('Start code signing...')
console.log('Signing file:', path)
const signCommand = `signtool sign /tr http://timestamp.comodoca.com /td sha256 /fd sha256 /a /v "${path}"`
execSync(signCommand, { stdio: 'inherit' })
console.log('Code signing completed')
} catch (error) {
console.error('Code signing failed:', error)
throw error
}
}
}
}

View File

@@ -1,6 +1,7 @@
import { app } from 'electron'
import { getDataPath } from './utils'
import { isWindows7 } from './utils/runtime'
const isDev = process.env.NODE_ENV === 'development'
@@ -12,12 +13,12 @@ export const DATA_PATH = getDataPath()
export const titleBarOverlayDark = {
height: 40,
color: 'rgba(0,0,0,0)',
color: isWindows7() ? '#1c1c1c' : 'rgba(0,0,0,0)',
symbolColor: '#ffffff'
}
export const titleBarOverlayLight = {
height: 40,
color: 'rgba(255,255,255,0)',
color: isWindows7() ? '#f4f4f4' : 'rgba(255,255,255,0)',
symbolColor: '#000000'
}

View File

@@ -0,0 +1,57 @@
interface IFilterList {
WINDOWS: string[]
MAC?: string[]
}
interface IFinetunedList {
EXCLUDE_CLIPBOARD_CURSOR_DETECT: IFilterList
INCLUDE_CLIPBOARD_DELAY_READ: IFilterList
}
/*************************************************************************
* 注意:请不要修改此配置,除非你非常清楚其含义、影响和行为的目的
* Note: Do not modify this configuration unless you fully understand its meaning, implications, and intended behavior.
* -----------------------------------------------------------------------
* A predefined application filter list to include commonly used software
* that does not require text selection but may conflict with it, and disable them in advance.
* Only available in the selected mode.
*
* Specification: must be all lowercase, need to accurately find the actual running program name
*************************************************************************/
export const SELECTION_PREDEFINED_BLACKLIST: IFilterList = {
WINDOWS: [
// Screenshot
'snipaste.exe',
'pixpin.exe',
'sharex.exe',
// Office
'excel.exe',
'powerpnt.exe',
// Image Editor
'photoshop.exe',
'illustrator.exe',
// Video Editor
'adobe premiere pro.exe',
'afterfx.exe',
// Audio Editor
'adobe audition.exe',
// 3D Editor
'blender.exe',
'3dsmax.exe',
'maya.exe',
// CAD
'acad.exe',
'sldworks.exe',
// Remote Desktop
'mstsc.exe'
]
}
export const SELECTION_FINETUNED_LIST: IFinetunedList = {
EXCLUDE_CLIPBOARD_CURSOR_DETECT: {
WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe']
},
INCLUDE_CLIPBOARD_DELAY_READ: {
WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe', 'foxitphantom.exe']
}
}

View File

@@ -11,7 +11,6 @@ export default class VoyageEmbeddings extends BaseEmbeddings {
if (!this.configuration.outputDimension) {
throw new Error('You need to pass in the optional dimensions parameter for this model')
}
console.log('VoyageEmbeddings', this.configuration)
this.model = new _VoyageEmbeddings(this.configuration)
}
override async getDimensions(): Promise<number> {

View File

@@ -1,10 +1,12 @@
import '@main/config'
import { electronApp, optimizer } from '@electron-toolkit/utils'
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
import { IpcChannel } from '@shared/IpcChannel'
import { app, ipcMain } from 'electron'
import { app } from 'electron'
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
import Logger from 'electron-log'
import { isDev, isWin } from './constant'
import { registerIpc } from './ipc'
import { configManager } from './services/ConfigManager'
import mcpService from './services/MCPService'
@@ -14,16 +16,45 @@ import {
registerProtocolClient,
setupAppImageDeepLink
} from './services/ProtocolClient'
import selectionService, { initSelectionService } from './services/SelectionService'
import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService'
import { setUserDataDir } from './utils/file'
Logger.initialize()
/**
* Disable chromium's window animations
* main purpose for this is to avoid the transparent window flashing when it is shown
* (especially on Windows for SelectionAssistant Toolbar)
* Know Issue: https://github.com/electron/electron/issues/12130#issuecomment-627198990
*/
if (isWin) {
app.commandLine.appendSwitch('wm-window-animations-disabled')
}
// in production mode, handle uncaught exception and unhandled rejection globally
if (!isDev) {
// handle uncaught exception
process.on('uncaughtException', (error) => {
Logger.error('Uncaught Exception:', error)
})
// handle unhandled rejection
process.on('unhandledRejection', (reason, promise) => {
Logger.error('Unhandled Rejection at:', promise, 'reason:', reason)
})
}
// Check for single instance lock
if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
} else {
// Portable dir must be setup before app ready
setUserDataDir()
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
@@ -56,23 +87,17 @@ if (!app.requestSingleInstanceLock()) {
replaceDevtoolsFont(mainWindow)
setUserDataDir()
// Setup deep link for AppImage on Linux
await setupAppImageDeepLink()
if (process.env.NODE_ENV === 'development') {
if (isDev) {
installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS])
.then((name) => console.log(`Added Extension: ${name}`))
.catch((err) => console.log('An error occurred: ', err))
}
ipcMain.handle(IpcChannel.System_GetDeviceType, () => {
return process.platform === 'darwin' ? 'mac' : process.platform === 'win32' ? 'windows' : 'linux'
})
ipcMain.handle(IpcChannel.System_GetHostname, () => {
return require('os').hostname()
})
//start selection assistant service
initSelectionService()
})
registerProtocolClient(app)
@@ -99,6 +124,11 @@ if (!app.requestSingleInstanceLock()) {
app.on('before-quit', () => {
app.isQuitting = true
// quit selection service
if (selectionService) {
selectionService.quit()
}
})
app.on('will-quit', async () => {

File diff suppressed because one or more lines are too long

View File

@@ -3,12 +3,13 @@ import { arch } from 'node:os'
import { isMac, isWin } from '@main/constant'
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
import { handleZoomFactor } from '@main/utils/zoom'
import { IpcChannel } from '@shared/IpcChannel'
import { Shortcut, ThemeMode } from '@types'
import { BrowserWindow, ipcMain, nativeTheme, session, shell } from 'electron'
import { BrowserWindow, ipcMain, session, shell } from 'electron'
import log from 'electron-log'
import { Notification } from 'src/renderer/src/types/notification'
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
import AppUpdater from './services/AppUpdater'
import BackupManager from './services/BackupManager'
import { configManager } from './services/ConfigManager'
@@ -16,20 +17,22 @@ import CopilotService from './services/CopilotService'
import { ExportService } from './services/ExportService'
import FileService from './services/FileService'
import FileStorage from './services/FileStorage'
import { GeminiService } from './services/GeminiService'
import KnowledgeService from './services/KnowledgeService'
import mcpService from './services/MCPService'
import NotificationService from './services/NotificationService'
import * as NutstoreService from './services/NutstoreService'
import ObsidianVaultService from './services/ObsidianVaultService'
import { ProxyConfig, proxyManager } from './services/ProxyManager'
import { searchService } from './services/SearchService'
import { SelectionService } from './services/SelectionService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import storeSyncService from './services/StoreSyncService'
import { themeService } from './services/ThemeService'
import { setOpenLinkExternal } from './services/WebviewService'
import { windowService } from './services/WindowService'
import { getResourcePath } from './utils'
import { calculateDirectorySize, getResourcePath } from './utils'
import { decrypt, encrypt } from './utils/aes'
import { getConfigDir, getFilesDir } from './utils/file'
import { getCacheDir, getConfigDir, getFilesDir } from './utils/file'
import { compress, decompress } from './utils/zip'
const fileManager = new FileStorage()
@@ -39,6 +42,7 @@ const obsidianVaultService = new ObsidianVaultService()
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const appUpdater = new AppUpdater(mainWindow)
const notificationService = new NotificationService(mainWindow)
ipcMain.handle(IpcChannel.App_Info, () => ({
version: app.getVersion(),
@@ -108,10 +112,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
configManager.setAutoUpdate(isActive)
})
ipcMain.handle(IpcChannel.App_RestartTray, () => TrayService.getInstance().restartTray())
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any) => {
configManager.set(key, value)
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => {
configManager.set(key, value, isNotify)
})
ipcMain.handle(IpcChannel.Config_Get, (_, key: string) => {
@@ -120,41 +122,13 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// theme
ipcMain.handle(IpcChannel.App_SetTheme, (_, theme: ThemeMode) => {
const notifyThemeChange = () => {
const windows = BrowserWindow.getAllWindows()
windows.forEach((win) =>
win.webContents.send(IpcChannel.ThemeChange, nativeTheme.shouldUseDarkColors ? ThemeMode.dark : ThemeMode.light)
)
}
if (theme === ThemeMode.auto) {
nativeTheme.themeSource = 'system'
nativeTheme.on('updated', notifyThemeChange)
} else {
nativeTheme.themeSource = theme
nativeTheme.removeAllListeners('updated')
}
mainWindow?.setTitleBarOverlay &&
mainWindow.setTitleBarOverlay(nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight)
configManager.setTheme(theme)
notifyThemeChange()
themeService.setTheme(theme)
})
// custom css
ipcMain.handle(IpcChannel.App_SetCustomCss, (event, css: string) => {
if (css === configManager.getCustomCss()) return
configManager.setCustomCss(css)
// Broadcast to all windows including the mini window
const senderWindowId = event.sender.id
ipcMain.handle(IpcChannel.App_HandleZoomFactor, (_, delta: number, reset: boolean = false) => {
const windows = BrowserWindow.getAllWindows()
// 向其他窗口广播主题变化
windows.forEach((win) => {
if (win.webContents.id !== senderWindowId) {
win.webContents.send('custom-css:update', css)
}
})
handleZoomFactor(windows, delta, reset)
return configManager.getZoomFactor()
})
// clear cache
@@ -179,15 +153,46 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}
})
// get cache size
ipcMain.handle(IpcChannel.App_GetCacheSize, async () => {
const cachePath = getCacheDir()
log.info(`Calculating cache size for path: ${cachePath}`)
try {
const sizeInBytes = await calculateDirectorySize(cachePath)
const sizeInMB = (sizeInBytes / (1024 * 1024)).toFixed(2)
return `${sizeInMB}`
} catch (error: any) {
log.error(`Failed to calculate cache size for ${cachePath}: ${error.message}`)
return '0'
}
})
// check for update
ipcMain.handle(IpcChannel.App_CheckForUpdate, async () => {
await appUpdater.checkForUpdates()
return await appUpdater.checkForUpdates()
})
// notification
ipcMain.handle(IpcChannel.Notification_Send, async (_, notification: Notification) => {
await notificationService.sendNotification(notification)
})
ipcMain.handle(IpcChannel.Notification_OnClick, (_, notification: Notification) => {
mainWindow.webContents.send('notification-click', notification)
})
// zip
ipcMain.handle(IpcChannel.Zip_Compress, (_, text: string) => compress(text))
ipcMain.handle(IpcChannel.Zip_Decompress, (_, text: Buffer) => decompress(text))
// system
ipcMain.handle(IpcChannel.System_GetDeviceType, () => (isMac ? 'mac' : isWin ? 'windows' : 'linux'))
ipcMain.handle(IpcChannel.System_GetHostname, () => require('os').hostname())
ipcMain.handle(IpcChannel.System_ToggleDevTools, (e) => {
const win = BrowserWindow.fromWebContents(e.sender)
win && win.webContents.toggleDevTools()
})
// backup
ipcMain.handle(IpcChannel.Backup_Backup, backupManager.backup)
ipcMain.handle(IpcChannel.Backup_Restore, backupManager.restore)
@@ -211,11 +216,14 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.File_SelectFolder, fileManager.selectFolder)
ipcMain.handle(IpcChannel.File_Create, fileManager.createTempFile)
ipcMain.handle(IpcChannel.File_Write, fileManager.writeFile)
ipcMain.handle(IpcChannel.File_WriteWithId, fileManager.writeFileWithId)
ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage)
ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image)
ipcMain.handle(IpcChannel.File_SaveBase64Image, fileManager.saveBase64Image)
ipcMain.handle(IpcChannel.File_Base64File, fileManager.base64File)
ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile)
ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile)
ipcMain.handle(IpcChannel.File_BinaryFile, fileManager.binaryFile)
ipcMain.handle(IpcChannel.File_BinaryImage, fileManager.binaryImage)
// fs
ipcMain.handle(IpcChannel.Fs_Read, FileService.readFile)
@@ -260,13 +268,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}
})
// gemini
ipcMain.handle(IpcChannel.Gemini_UploadFile, GeminiService.uploadFile)
ipcMain.handle(IpcChannel.Gemini_Base64File, GeminiService.base64File)
ipcMain.handle(IpcChannel.Gemini_RetrieveFile, GeminiService.retrieveFile)
ipcMain.handle(IpcChannel.Gemini_ListFiles, GeminiService.listFiles)
ipcMain.handle(IpcChannel.Gemini_DeleteFile, GeminiService.deleteFile)
// mini window
ipcMain.handle(IpcChannel.MiniWindow_Show, () => windowService.showMiniWindow())
ipcMain.handle(IpcChannel.MiniWindow_Hide, () => windowService.hideMiniWindow())
@@ -293,6 +294,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.Mcp_ListResources, mcpService.listResources)
ipcMain.handle(IpcChannel.Mcp_GetResource, mcpService.getResource)
ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo)
ipcMain.handle(IpcChannel.Mcp_CheckConnectivity, mcpService.checkMcpConnectivity)
ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name))
@@ -338,4 +340,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
setOpenLinkExternal(webviewId, isExternal)
)
// store sync
storeSyncService.registerIpcHandler()
// selection assistant
SelectionService.registerIpcHandler()
}

View File

@@ -237,8 +237,7 @@ async function getPoisData(apiKey: string, ids: string[]): Promise<BravePoiRespo
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
}
const poisResponse = (await response.json()) as BravePoiResponse
return poisResponse
return (await response.json()) as BravePoiResponse
}
async function getDescriptionsData(apiKey: string, ids: string[]): Promise<BraveDescription> {
@@ -257,8 +256,7 @@ async function getDescriptionsData(apiKey: string, ids: string[]): Promise<Brave
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
}
const descriptionsData = (await response.json()) as BraveDescription
return descriptionsData
return (await response.json()) as BraveDescription
}
function formatLocalResults(poisData: BravePoiResponse, descData: BraveDescription): string {

View File

@@ -56,7 +56,6 @@ class DifyKnowledgeServer {
private config: DifyKnowledgeServerConfig
constructor(difyKey: string, args: string[]) {
console.log('DifyKnowledgeServer args', args)
if (args.length === 0) {
throw new Error('DifyKnowledgeServer requires at least one argument')
}
@@ -113,8 +112,6 @@ class DifyKnowledgeServer {
const errorDetails = JSON.stringify(parsed.error.format(), null, 2)
throw new Error(`无效的参数:\n${errorDetails}`)
}
console.log('DifyKnowledgeServer search_knowledge parsed', parsed.data)
return await this.performSearchKnowledge(
parsed.data.id,
parsed.data.query,

View File

@@ -209,20 +209,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const validatedArgs = RequestPayloadSchema.parse(args)
if (request.params.name === 'fetch_html') {
const fetchResult = await Fetcher.html(validatedArgs)
return fetchResult
return await Fetcher.html(validatedArgs)
}
if (request.params.name === 'fetch_json') {
const fetchResult = await Fetcher.json(validatedArgs)
return fetchResult
return await Fetcher.json(validatedArgs)
}
if (request.params.name === 'fetch_txt') {
const fetchResult = await Fetcher.txt(validatedArgs)
return fetchResult
return await Fetcher.txt(validatedArgs)
}
if (request.params.name === 'fetch_markdown') {
const fetchResult = await Fetcher.markdown(validatedArgs)
return fetchResult
return await Fetcher.markdown(validatedArgs)
}
throw new Error('Tool not found')
})

View File

@@ -183,7 +183,6 @@ async function searchFiles(
}
} catch (error) {
// Skip invalid paths during search
continue
}
}
}

View File

@@ -2,6 +2,7 @@ import { getConfigDir } from '@main/utils/file'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'
import { Mutex } from 'async-mutex' // 引入 Mutex
import Logger from 'electron-log'
import { promises as fs } from 'fs'
import path from 'path'
@@ -355,9 +356,9 @@ class MemoryServer {
private async _initializeManager(memoryPath: string): Promise<void> {
try {
this.knowledgeGraphManager = await KnowledgeGraphManager.create(memoryPath)
console.log('KnowledgeGraphManager initialized successfully.')
Logger.log('KnowledgeGraphManager initialized successfully.')
} catch (error) {
console.error('Failed to initialize KnowledgeGraphManager:', error)
Logger.error('Failed to initialize KnowledgeGraphManager:', error)
// Server might be unusable, consider how to handle this state
// Maybe set a flag and return errors for all tool calls?
this.knowledgeGraphManager = null // Ensure it's null if init fails

View File

@@ -55,8 +55,8 @@ class SequentialThinkingServer {
const { thoughtNumber, totalThoughts, thought, isRevision, revisesThought, branchFromThought, branchId } =
thoughtData
let prefix = ''
let context = ''
let prefix: string
let context: string
if (isRevision) {
prefix = chalk.yellow('🔄 Revision')

View File

@@ -0,0 +1,36 @@
console.info('inject polyfill win7')
// fix for node_modules\@libsql\isomorphic-fetch\node.cjs
if (!globalThis.fetch) {
globalThis.fetch = require('node-fetch')
}
if (!globalThis.Request) {
const { Request, Headers } = require('node-fetch')
globalThis.Request = Request
globalThis.Headers = Headers
}
// fix for node_modules/undici/lib/web/fetch/webidl.js
if (!globalThis.Blob) {
const { Blob } = require('blob-polyfill')
globalThis.Blob = Blob
}
// fix for node_modules/undici/lib/web/fetch/webidl.js
if (!globalThis.ReadableStream) {
const { ReadableStream, TransformStream } = require('web-streams-polyfill')
globalThis.ReadableStream = ReadableStream
globalThis.TransformStream = TransformStream
console.log('ReadableStream', ReadableStream)
}
if (!globalThis.DOMException) {
globalThis.DOMException = require('domexception')
}
if (!globalThis.crypto) {
const { Crypto } = require('@peculiar/webcrypto')
globalThis.crypto = new Crypto()
}
console.info('inject polyfill win7 ok')

View File

@@ -17,6 +17,10 @@ export default abstract class BaseReranker {
* Get Rerank Request Url
*/
protected getRerankUrl() {
if (this.base.rerankModelProvider === 'dashscope') {
return 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank'
}
let baseURL = this.base?.rerankBaseURL?.endsWith('/')
? this.base.rerankBaseURL.slice(0, -1)
: this.base.rerankBaseURL
@@ -28,6 +32,56 @@ export default abstract class BaseReranker {
return `${baseURL}/rerank`
}
/**
* Get Rerank Request Body
*/
protected getRerankRequestBody(query: string, searchResults: ExtractChunkData[]) {
const provider = this.base.rerankModelProvider
const documents = searchResults.map((doc) => doc.pageContent)
const topN = this.base.documentCount
if (provider === 'voyageai') {
return {
model: this.base.rerankModel,
query,
documents,
top_k: topN
}
} else if (provider === 'dashscope') {
return {
model: this.base.rerankModel,
input: {
query,
documents
},
parameters: {
top_n: topN
}
}
} else {
return {
model: this.base.rerankModel,
query,
documents,
top_n: topN
}
}
}
/**
* Extract Rerank Result
*/
protected extractRerankResult(data: any) {
const provider = this.base.rerankModelProvider
if (provider === 'dashscope') {
return data.output.results
} else if (provider === 'voyageai') {
return data.data
} else {
return data.results
}
}
/**
* Get Rerank Result
* @param searchResults

View File

@@ -1,14 +0,0 @@
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { KnowledgeBaseParams } from '@types'
import BaseReranker from './BaseReranker'
export default class DefaultReranker extends BaseReranker {
constructor(base: KnowledgeBaseParams) {
super(base)
}
async rerank(): Promise<ExtractChunkData[]> {
throw new Error('Method not implemented.')
}
}

View File

@@ -4,7 +4,7 @@ import { KnowledgeBaseParams } from '@types'
import BaseReranker from './BaseReranker'
export default class JinaReranker extends BaseReranker {
export default class GeneralReranker extends BaseReranker {
constructor(base: KnowledgeBaseParams) {
super(base)
}
@@ -12,21 +12,15 @@ export default class JinaReranker extends BaseReranker {
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
const url = this.getRerankUrl()
const requestBody = {
model: this.base.rerankModel,
query,
documents: searchResults.map((doc) => doc.pageContent),
top_n: this.base.topN
}
const requestBody = this.getRerankRequestBody(query, searchResults)
try {
const { data } = await AxiosProxy.axios.post(url, requestBody, { headers: this.defaultHeaders() })
const rerankResults = data.results
const rerankResults = this.extractRerankResult(data)
return this.getRerankResult(searchResults, rerankResults)
} catch (error: any) {
const errorDetails = this.formatErrorMessage(url, error, requestBody)
console.error('Jina Reranker API Error:', errorDetails)
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
}
}

View File

@@ -1,13 +1,12 @@
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { KnowledgeBaseParams } from '@types'
import BaseReranker from './BaseReranker'
import RerankerFactory from './RerankerFactory'
import GeneralReranker from './GeneralReranker'
export default class Reranker {
private sdk: BaseReranker
private sdk: GeneralReranker
constructor(base: KnowledgeBaseParams) {
this.sdk = RerankerFactory.create(base)
this.sdk = new GeneralReranker(base)
}
public async rerank(query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> {
return this.sdk.rerank(query, searchResults)

View File

@@ -1,20 +0,0 @@
import { KnowledgeBaseParams } from '@types'
import BaseReranker from './BaseReranker'
import DefaultReranker from './DefaultReranker'
import JinaReranker from './JinaReranker'
import SiliconFlowReranker from './SiliconFlowReranker'
import VoyageReranker from './VoyageReranker'
export default class RerankerFactory {
static create(base: KnowledgeBaseParams): BaseReranker {
if (base.rerankModelProvider === 'silicon') {
return new SiliconFlowReranker(base)
} else if (base.rerankModelProvider === 'jina') {
return new JinaReranker(base)
} else if (base.rerankModelProvider === 'voyageai') {
return new VoyageReranker(base)
}
return new DefaultReranker(base)
}
}

View File

@@ -1,36 +0,0 @@
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import axiosProxy from '@main/services/AxiosProxy'
import { KnowledgeBaseParams } from '@types'
import BaseReranker from './BaseReranker'
export default class SiliconFlowReranker extends BaseReranker {
constructor(base: KnowledgeBaseParams) {
super(base)
}
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
const url = this.getRerankUrl()
const requestBody = {
model: this.base.rerankModel,
query,
documents: searchResults.map((doc) => doc.pageContent),
top_n: this.base.topN,
max_chunks_per_doc: this.base.chunkSize,
overlap_tokens: this.base.chunkOverlap
}
try {
const { data } = await axiosProxy.axios.post(url, requestBody, { headers: this.defaultHeaders() })
const rerankResults = data.results
return this.getRerankResult(searchResults, rerankResults)
} catch (error: any) {
const errorDetails = this.formatErrorMessage(url, error, requestBody)
console.error('SiliconFlow Reranker API 错误:', errorDetails)
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
}
}
}

View File

@@ -1,40 +0,0 @@
import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import axiosProxy from '@main/services/AxiosProxy'
import { KnowledgeBaseParams } from '@types'
import BaseReranker from './BaseReranker'
export default class VoyageReranker extends BaseReranker {
constructor(base: KnowledgeBaseParams) {
super(base)
}
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
const url = this.getRerankUrl()
const requestBody = {
model: this.base.rerankModel,
query,
documents: searchResults.map((doc) => doc.pageContent),
top_k: this.base.topN,
return_documents: false,
truncation: true
}
try {
const { data } = await axiosProxy.axios.post(url, requestBody, {
headers: {
...this.defaultHeaders()
}
})
const rerankResults = data.data
return this.getRerankResult(searchResults, rerankResults)
} catch (error: any) {
const errorDetails = this.formatErrorMessage(url, error, requestBody)
console.error('Voyage Reranker API Error:', errorDetails)
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
}
}
}

View File

@@ -1,4 +1,5 @@
import { isWin } from '@main/constant'
import { locales } from '@main/utils/locales'
import { IpcChannel } from '@shared/IpcChannel'
import { UpdateInfo } from 'builder-util-runtime'
import { app, BrowserWindow, dialog } from 'electron'
@@ -94,15 +95,22 @@ export default class AppUpdater {
if (!this.releaseInfo) {
return
}
const locale = locales[configManager.getLanguage()]
const { update: updateLocale } = locale.translation
let detail = this.formatReleaseNotes(this.releaseInfo.releaseNotes)
if (detail === '') {
detail = updateLocale.noReleaseNotes
}
dialog
.showMessageBox({
type: 'info',
title: '安装更新',
title: updateLocale.title,
icon,
message: `新版本 ${this.releaseInfo.version} 已准备就绪`,
detail: this.formatReleaseNotes(this.releaseInfo.releaseNotes),
buttons: ['稍后安装', '立即安装'],
message: updateLocale.message.replace('{{version}}', this.releaseInfo.version),
detail,
buttons: [updateLocale.later, updateLocale.install],
defaultId: 1,
cancelId: 0
})
@@ -118,7 +126,7 @@ export default class AppUpdater {
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
if (!releaseNotes) {
return '暂无更新说明'
return ''
}
if (typeof releaseNotes === 'string') {

View File

@@ -4,10 +4,10 @@ import archiver from 'archiver'
import { exec } from 'child_process'
import { app } from 'electron'
import Logger from 'electron-log'
import extract from 'extract-zip'
import * as fs from 'fs-extra'
import StreamZip from 'node-stream-zip'
import * as path from 'path'
import { createClient, CreateDirectoryOptions, FileStat } from 'webdav'
import { CreateDirectoryOptions, FileStat } from 'webdav'
import WebDav from './WebDav'
import { windowService } from './WindowService'
@@ -77,7 +77,8 @@ class BackupManager {
_: Electron.IpcMainInvokeEvent,
fileName: string,
data: string,
destinationPath: string = this.backupDir
destinationPath: string = this.backupDir,
skipBackupFile: boolean = false
): Promise<string> {
const mainWindow = windowService.getMainWindow()
@@ -104,6 +105,9 @@ class BackupManager {
onProgress({ stage: 'writing_data', progress: 20, total: 100 })
Logger.log('[BackupManager IPC] ', skipBackupFile)
if (!skipBackupFile) {
// 复制 Data 目录到临时目录
const sourcePath = path.join(app.getPath('userData'), 'Data')
const tempDataDir = path.join(this.tempDir, 'Data')
@@ -121,6 +125,10 @@ class BackupManager {
await this.setWritableRecursive(tempDataDir)
onProgress({ stage: 'preparing_compression', progress: 50, total: 100 })
} else {
Logger.log('[BackupManager] Skip the backup of the file')
await fs.promises.mkdir(path.join(this.tempDir, 'Data')) // 不创建空 Data 目录会导致 restore 失败
}
// 创建输出文件流
const backupedFilePath = path.join(destinationPath, fileName)
@@ -231,15 +239,10 @@ class BackupManager {
Logger.log('[backup] step 1: unzip backup file', this.tempDir)
// 使用 extract-zip 解压
await extract(backupPath, {
dir: this.tempDir,
onEntry: () => {
// 这里可以处理进度,但 extract-zip 不提供总条目数信息
const zip = new StreamZip.async({ file: backupPath })
onProgress({ stage: 'extracting', progress: 15, total: 100 })
}
})
onProgress({ stage: 'extracting', progress: 25, total: 100 })
await zip.extract(null, this.tempDir)
onProgress({ stage: 'extracted', progress: 25, total: 100 })
Logger.log('[backup] step 2: read data.json')
// 读取 data.json
@@ -252,6 +255,10 @@ class BackupManager {
const sourcePath = path.join(this.tempDir, 'Data')
const destPath = path.join(app.getPath('userData'), 'Data')
const dataExists = await fs.pathExists(sourcePath)
const dataFiles = dataExists ? await fs.readdir(sourcePath) : []
if (dataExists && dataFiles.length > 0) {
// 获取源目录总大小
const totalSize = await this.getDirSize(sourcePath)
let copiedSize = 0
@@ -265,6 +272,9 @@ class BackupManager {
const progress = Math.min(85, 35 + Math.floor((copiedSize / totalSize) * 50))
onProgress({ stage: 'copying_files', progress, total: 100 })
})
} else {
Logger.log('[backup] skipBackupFile is true, skip restoring Data directory')
}
Logger.log('[backup] step 4: clean up temp directory')
// 清理临时目录
@@ -284,11 +294,20 @@ class BackupManager {
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
const backupedFilePath = await this.backup(_, filename, data)
const backupedFilePath = await this.backup(_, filename, data, undefined, webdavConfig.skipBackupFile)
const webdavClient = new WebDav(webdavConfig)
return await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
try {
const result = await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
overwrite: true
})
// 上传成功后删除本地备份文件
await fs.remove(backupedFilePath)
return result
} catch (error) {
// 上传失败时也删除本地临时文件
await fs.remove(backupedFilePath).catch(() => {})
throw error
}
}
async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
@@ -321,12 +340,8 @@ class BackupManager {
listWebdavFiles = async (_: Electron.IpcMainInvokeEvent, config: WebDavConfig) => {
try {
const client = createClient(config.webdavHost, {
username: config.webdavUser,
password: config.webdavPass
})
const response = await client.getDirectoryContents(config.webdavPath)
const client = new WebDav(config)
const response = await client.getDirectoryContents()
const files = Array.isArray(response) ? response : response.data
return files

View File

@@ -1,118 +0,0 @@
import { debounce, getResourcePath } from '@main/utils'
import { exec } from 'child_process'
import { screen } from 'electron'
import path from 'path'
import { windowService } from './WindowService'
export default class ClipboardMonitor {
private platform: string
private lastText: string
private user32: any
private observer: any
public onTextSelected: (text: string) => void
constructor() {
this.platform = process.platform
this.lastText = ''
this.onTextSelected = debounce((text: string) => this.handleTextSelected(text), 550)
if (this.platform === 'win32') {
this.setupWindows()
} else if (this.platform === 'darwin') {
this.setupMacOS()
}
}
setupMacOS() {
// 使用 Swift 脚本来监听文本选择
const scriptPath = path.join(getResourcePath(), 'textMonitor.swift')
// 启动 Swift 进程来监听文本选择
const process = exec(`swift ${scriptPath}`)
process?.stdout?.on('data', (data: string) => {
console.log('[ClipboardMonitor] MacOS data:', data)
const text = data.toString().trim()
if (text && text !== this.lastText) {
this.lastText = text
this.onTextSelected(text)
}
})
process.on('error', (error) => {
console.error('[ClipboardMonitor] MacOS error:', error)
})
}
setupWindows() {
// 使用 Windows API 监听文本选择事件
const ffi = require('ffi-napi')
const ref = require('ref-napi')
this.user32 = new ffi.Library('user32', {
SetWinEventHook: ['pointer', ['uint32', 'uint32', 'pointer', 'pointer', 'uint32', 'uint32', 'uint32']],
UnhookWinEvent: ['bool', ['pointer']]
})
// 定义事件常量
const EVENT_OBJECT_SELECTION = 0x8006
const WINEVENT_OUTOFCONTEXT = 0x0000
const WINEVENT_SKIPOWNTHREAD = 0x0001
const WINEVENT_SKIPOWNPROCESS = 0x0002
// 创建回调函数
const callback = ffi.Callback('void', ['pointer', 'uint32', 'pointer', 'long', 'long', 'uint32', 'uint32'], () => {
this.getSelectedText()
})
// 设置事件钩子
this.observer = this.user32.SetWinEventHook(
EVENT_OBJECT_SELECTION,
EVENT_OBJECT_SELECTION,
ref.NULL,
callback,
0,
0,
WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNTHREAD | WINEVENT_SKIPOWNPROCESS
)
}
getSelectedText() {
// Get selected text
if (this.platform === 'win32') {
const ref = require('ref-napi')
if (this.user32.OpenClipboard(ref.NULL)) {
// Get clipboard content
const text = this.user32.GetClipboardData(1) // CF_TEXT = 1
this.user32.CloseClipboard()
if (text && text !== this.lastText) {
this.lastText = text
this.onTextSelected(text)
}
}
}
}
private handleTextSelected(text: string) {
if (!text) return
console.log('[ClipboardMonitor] handleTextSelected', text)
windowService.setLastSelectedText(text)
const mousePosition = screen.getCursorScreenPoint()
windowService.showSelectionMenu({
x: mousePosition.x,
y: mousePosition.y + 10
})
}
dispose() {
if (this.platform === 'win32' && this.observer) {
this.user32.UnhookWinEvent(this.observer)
}
}
}

View File

@@ -5,7 +5,7 @@ import Store from 'electron-store'
import { locales } from '../utils/locales'
enum ConfigKeys {
export enum ConfigKeys {
Language = 'language',
Theme = 'theme',
LaunchToTray = 'launchToTray',
@@ -16,7 +16,13 @@ enum ConfigKeys {
ClickTrayToShowQuickAssistant = 'clickTrayToShowQuickAssistant',
EnableQuickAssistant = 'enableQuickAssistant',
AutoUpdate = 'autoUpdate',
EnableDataCollection = 'enableDataCollection'
EnableDataCollection = 'enableDataCollection',
SelectionAssistantEnabled = 'selectionAssistantEnabled',
SelectionAssistantTriggerMode = 'selectionAssistantTriggerMode',
SelectionAssistantFollowToolbar = 'selectionAssistantFollowToolbar',
SelectionAssistantRemeberWinSize = 'selectionAssistantRemeberWinSize',
SelectionAssistantFilterMode = 'selectionAssistantFilterMode',
SelectionAssistantFilterList = 'selectionAssistantFilterList'
}
export class ConfigManager {
@@ -32,26 +38,18 @@ export class ConfigManager {
return this.get(ConfigKeys.Language, locale) as LanguageVarious
}
setLanguage(theme: LanguageVarious) {
this.set(ConfigKeys.Language, theme)
setLanguage(lang: LanguageVarious) {
this.setAndNotify(ConfigKeys.Language, lang)
}
getTheme(): ThemeMode {
return this.get(ConfigKeys.Theme, ThemeMode.auto)
return this.get(ConfigKeys.Theme, ThemeMode.system)
}
setTheme(theme: ThemeMode) {
this.set(ConfigKeys.Theme, theme)
}
getCustomCss(): string {
return this.store.get('customCss', '') as string
}
setCustomCss(css: string) {
this.store.set('customCss', css)
}
getLaunchToTray(): boolean {
return !!this.get(ConfigKeys.LaunchToTray, false)
}
@@ -65,8 +63,7 @@ export class ConfigManager {
}
setTray(value: boolean) {
this.set(ConfigKeys.Tray, value)
this.notifySubscribers(ConfigKeys.Tray, value)
this.setAndNotify(ConfigKeys.Tray, value)
}
getTrayOnClose(): boolean {
@@ -82,8 +79,7 @@ export class ConfigManager {
}
setZoomFactor(factor: number) {
this.set(ConfigKeys.ZoomFactor, factor)
this.notifySubscribers(ConfigKeys.ZoomFactor, factor)
this.setAndNotify(ConfigKeys.ZoomFactor, factor)
}
subscribe<T>(key: string, callback: (newValue: T) => void) {
@@ -115,11 +111,10 @@ export class ConfigManager {
}
setShortcuts(shortcuts: Shortcut[]) {
this.set(
this.setAndNotify(
ConfigKeys.Shortcuts,
shortcuts.filter((shortcut) => shortcut.system)
)
this.notifySubscribers(ConfigKeys.Shortcuts, shortcuts)
}
getClickTrayToShowQuickAssistant(): boolean {
@@ -135,7 +130,7 @@ export class ConfigManager {
}
setEnableQuickAssistant(value: boolean) {
this.set(ConfigKeys.EnableQuickAssistant, value)
this.setAndNotify(ConfigKeys.EnableQuickAssistant, value)
}
getAutoUpdate(): boolean {
@@ -154,8 +149,64 @@ export class ConfigManager {
this.set(ConfigKeys.EnableDataCollection, value)
}
set(key: string, value: unknown) {
// Selection Assistant: is enabled the selection assistant
getSelectionAssistantEnabled(): boolean {
return this.get<boolean>(ConfigKeys.SelectionAssistantEnabled, true)
}
setSelectionAssistantEnabled(value: boolean) {
this.setAndNotify(ConfigKeys.SelectionAssistantEnabled, value)
}
// Selection Assistant: trigger mode (selected, ctrlkey)
getSelectionAssistantTriggerMode(): string {
return this.get<string>(ConfigKeys.SelectionAssistantTriggerMode, 'selected')
}
setSelectionAssistantTriggerMode(value: string) {
this.setAndNotify(ConfigKeys.SelectionAssistantTriggerMode, value)
}
// Selection Assistant: if action window position follow toolbar
getSelectionAssistantFollowToolbar(): boolean {
return this.get<boolean>(ConfigKeys.SelectionAssistantFollowToolbar, true)
}
setSelectionAssistantFollowToolbar(value: boolean) {
this.setAndNotify(ConfigKeys.SelectionAssistantFollowToolbar, value)
}
getSelectionAssistantRemeberWinSize(): boolean {
return this.get<boolean>(ConfigKeys.SelectionAssistantRemeberWinSize, false)
}
setSelectionAssistantRemeberWinSize(value: boolean) {
this.setAndNotify(ConfigKeys.SelectionAssistantRemeberWinSize, value)
}
getSelectionAssistantFilterMode(): string {
return this.get<string>(ConfigKeys.SelectionAssistantFilterMode, 'default')
}
setSelectionAssistantFilterMode(value: string) {
this.setAndNotify(ConfigKeys.SelectionAssistantFilterMode, value)
}
getSelectionAssistantFilterList(): string[] {
return this.get<string[]>(ConfigKeys.SelectionAssistantFilterList, [])
}
setSelectionAssistantFilterList(value: string[]) {
this.setAndNotify(ConfigKeys.SelectionAssistantFilterList, value)
}
setAndNotify(key: string, value: unknown) {
this.set(key, value, true)
}
set(key: string, value: unknown, isNotify: boolean = false) {
this.store.set(key, value)
isNotify && this.notifySubscribers(key, value)
}
get<T>(key: string, defaultValue?: T) {

View File

@@ -0,0 +1,77 @@
import { Menu, MenuItemConstructorOptions } from 'electron'
import { locales } from '../utils/locales'
import { configManager } from './ConfigManager'
class ContextMenu {
public contextMenu(w: Electron.BrowserWindow) {
w.webContents.on('context-menu', (_event, properties) => {
const template: MenuItemConstructorOptions[] = this.createEditMenuItems(properties)
const filtered = template.filter((item) => item.visible !== false)
if (filtered.length > 0) {
const menu = Menu.buildFromTemplate([...filtered, ...this.createInspectMenuItems(w)])
menu.popup()
}
})
}
private createInspectMenuItems(w: Electron.BrowserWindow): MenuItemConstructorOptions[] {
const locale = locales[configManager.getLanguage()]
const { common } = locale.translation
const template: MenuItemConstructorOptions[] = [
{
id: 'inspect',
label: common.inspect,
click: () => {
w.webContents.toggleDevTools()
},
enabled: true
}
]
return template
}
private createEditMenuItems(properties: Electron.ContextMenuParams): MenuItemConstructorOptions[] {
const locale = locales[configManager.getLanguage()]
const { common } = locale.translation
const hasText = properties.selectionText.trim().length > 0
const can = (type: string) => properties.editFlags[`can${type}`] && hasText
const template: MenuItemConstructorOptions[] = [
{
id: 'copy',
label: common.copy,
role: 'copy',
enabled: can('Copy'),
visible: properties.isEditable || hasText
},
{
id: 'paste',
label: common.paste,
role: 'paste',
enabled: properties.editFlags.canPaste,
visible: properties.isEditable
},
{
id: 'cut',
label: common.cut,
role: 'cut',
enabled: can('Cut'),
visible: properties.isEditable
}
]
// remove role from items that are not enabled
// https://github.com/electron/electron/issues/13554
template.forEach((item) => {
if (item.enabled === false) {
item.role = undefined
}
})
return template
}
}
export const contextMenu = new ContextMenu()

View File

@@ -1,5 +1,6 @@
import { AxiosRequestConfig } from 'axios'
import { app, safeStorage } from 'electron'
import Logger from 'electron-log'
import fs from 'fs/promises'
import path from 'path'
@@ -227,10 +228,10 @@ class CopilotService {
try {
await fs.access(this.tokenFilePath)
await fs.unlink(this.tokenFilePath)
console.log('Successfully logged out from Copilot')
Logger.log('Successfully logged out from Copilot')
} catch (error) {
// 文件不存在不是错误,只是记录一下
console.log('Token file not found, nothing to delete')
Logger.log('Token file not found, nothing to delete')
}
} catch (error) {
console.error('Failed to logout:', error)

View File

@@ -47,6 +47,8 @@ export class ExportService {
let linkText = ''
let linkUrl = ''
let insideLink = false
let boldStack = 0 // 跟踪嵌套的粗体标记
let italicStack = 0 // 跟踪嵌套的斜体标记
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]
@@ -82,17 +84,37 @@ export class ExportService {
insideLink = false
}
break
case 'strong_open':
boldStack++
break
case 'strong_close':
boldStack--
break
case 'em_open':
italicStack++
break
case 'em_close':
italicStack--
break
case 'text':
runs.push(new TextRun({ text: token.content, bold: isHeaderRow ? true : false }))
break
case 'strong':
runs.push(new TextRun({ text: token.content, bold: true }))
break
case 'em':
runs.push(new TextRun({ text: token.content, italics: true }))
runs.push(
new TextRun({
text: token.content,
bold: isHeaderRow || boldStack > 0,
italics: italicStack > 0
})
)
break
case 'code_inline':
runs.push(new TextRun({ text: token.content, font: 'Consolas', size: 20 }))
runs.push(
new TextRun({
text: token.content,
font: 'Consolas',
size: 20,
bold: isHeaderRow || boldStack > 0,
italics: italicStack > 0
})
)
break
}
}

View File

@@ -28,12 +28,17 @@ class FileStorage {
}
private initStorageDir = (): void => {
try {
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true })
}
if (!fs.existsSync(this.tempDir)) {
fs.mkdirSync(this.tempDir, { recursive: true })
}
} catch (error) {
logger.error('[FileStorage] Failed to initialize storage directories:', error)
throw error
}
}
private getFileHash = async (filePath: string): Promise<string> => {
@@ -263,7 +268,60 @@ class FileStorage {
}
}
public binaryFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: Buffer; mime: string }> => {
public saveBase64Image = async (_: Electron.IpcMainInvokeEvent, base64Data: string): Promise<FileType> => {
try {
if (!base64Data) {
throw new Error('Base64 data is required')
}
// 移除 base64 头部信息(如果存在)
const base64String = base64Data.replace(/^data:.*;base64,/, '')
const buffer = Buffer.from(base64String, 'base64')
const uuid = uuidv4()
const ext = '.png'
const destPath = path.join(this.storageDir, uuid + ext)
logger.info('[FileStorage] Saving base64 image:', {
storageDir: this.storageDir,
destPath,
bufferSize: buffer.length
})
// 确保目录存在
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true })
}
await fs.promises.writeFile(destPath, buffer)
const fileMetadata: FileType = {
id: uuid,
origin_name: uuid + ext,
name: uuid + ext,
path: destPath,
created_at: new Date().toISOString(),
size: buffer.length,
ext: ext.slice(1),
type: getFileType(ext),
count: 1
}
return fileMetadata
} catch (error) {
logger.error('[FileStorage] Failed to save base64 image:', error)
throw error
}
}
public base64File = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: string; mime: string }> => {
const filePath = path.join(this.storageDir, id)
const buffer = await fs.promises.readFile(filePath)
const base64 = buffer.toString('base64')
const mime = `application/${path.extname(filePath).slice(1)}`
return { data: base64, mime }
}
public binaryImage = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: Buffer; mime: string }> => {
const filePath = path.join(this.storageDir, id)
const data = await fs.promises.readFile(filePath)
const mime = `image/${path.extname(filePath).slice(1)}`
@@ -315,7 +373,7 @@ class FileStorage {
fileName: string,
content: string,
options?: SaveDialogOptions
): Promise<string | null> => {
): Promise<string | null | undefined> => {
try {
const result: SaveDialogReturnValue = await dialog.showSaveDialog({
title: '保存文件',
@@ -323,14 +381,18 @@ class FileStorage {
...options
})
if (result.canceled) {
return Promise.reject(new Error('User canceled the save dialog'))
}
if (!result.canceled && result.filePath) {
await writeFileSync(result.filePath, content, { encoding: 'utf-8' })
}
return result.filePath
} catch (err) {
} catch (err: any) {
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
return null
return Promise.reject('An error occurred saving the file: ' + err?.message)
}
}
@@ -369,7 +431,11 @@ class FileStorage {
}
}
public downloadFile = async (_: Electron.IpcMainInvokeEvent, url: string): Promise<FileType> => {
public downloadFile = async (
_: Electron.IpcMainInvokeEvent,
url: string,
isUseContentType?: boolean
): Promise<FileType> => {
try {
const response = await fetch(url)
if (!response.ok) {
@@ -394,7 +460,7 @@ class FileStorage {
}
// 如果文件名没有后缀根据Content-Type添加后缀
if (!filename.includes('.')) {
if (isUseContentType || !filename.includes('.')) {
const contentType = response.headers.get('Content-Type')
const ext = this.getExtensionFromMimeType(contentType)
filename += ext
@@ -467,6 +533,25 @@ class FileStorage {
throw error
}
}
public writeFileWithId = async (_: Electron.IpcMainInvokeEvent, id: string, content: string): Promise<void> => {
try {
const filePath = path.join(this.storageDir, id)
logger.info('[FileStorage] Writing file:', filePath)
// 确保目录存在
if (!fs.existsSync(this.storageDir)) {
logger.info('[FileStorage] Creating storage directory:', this.storageDir)
fs.mkdirSync(this.storageDir, { recursive: true })
}
await fs.promises.writeFile(filePath, content, 'utf8')
logger.info('[FileStorage] File written successfully:', filePath)
} catch (error) {
logger.error('[FileStorage] Failed to write file:', error)
throw error
}
}
}
export default FileStorage

View File

@@ -1,69 +0,0 @@
import { File, FileState, GoogleGenAI, Pager } from '@google/genai'
import { FileType } from '@types'
import fs from 'fs'
import { CacheService } from './CacheService'
export class GeminiService {
private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list'
private static readonly CACHE_DURATION = 3000
static async uploadFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string): Promise<File> {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
const uploadResult = await sdk.files.upload({
file: file.path,
config: {
mimeType: 'application/pdf',
name: file.id,
displayName: file.origin_name
}
})
return uploadResult
}
static async base64File(_: Electron.IpcMainInvokeEvent, file: FileType) {
return {
data: Buffer.from(fs.readFileSync(file.path)).toString('base64'),
mimeType: 'application/pdf'
}
}
static async retrieveFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string): Promise<File | undefined> {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
const cachedResponse = CacheService.get<any>(GeminiService.FILE_LIST_CACHE_KEY)
if (cachedResponse) {
return GeminiService.processResponse(cachedResponse, file)
}
const response = await sdk.files.list()
CacheService.set(GeminiService.FILE_LIST_CACHE_KEY, response, GeminiService.CACHE_DURATION)
return GeminiService.processResponse(response, file)
}
private static async processResponse(response: Pager<File>, file: FileType) {
for await (const f of response) {
if (f.state === FileState.ACTIVE) {
if (f.displayName === file.origin_name && Number(f.sizeBytes) === file.size) {
return f
}
}
}
return undefined
}
static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string): Promise<File[]> {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
const files: File[] = []
for await (const f of await sdk.files.list()) {
files.push(f)
}
return files
}
static async deleteFile(_: Electron.IpcMainInvokeEvent, fileId: string, apiKey: string) {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
await sdk.files.delete({ name: fileId })
}
}

View File

@@ -459,7 +459,7 @@ class KnowledgeService {
{ uniqueId, uniqueIds, base }: { uniqueId: string; uniqueIds: string[]; base: KnowledgeBaseParams }
): Promise<void> => {
const ragApplication = await this.getRagApplication(base)
console.log(`[ KnowledgeService Remove Item UniqueId: ${uniqueId}]`)
Logger.log(`[ KnowledgeService Remove Item UniqueId: ${uniqueId}]`)
for (const id of uniqueIds) {
await ragApplication.deleteLoader(id)
}

View File

@@ -1,15 +1,14 @@
import crypto from 'node:crypto'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { isLinux, isMac, isWin } from '@main/constant'
import { createInMemoryMCPServer } from '@main/mcpServers/factory'
import { makeSureDirExists } from '@main/utils'
import { buildFunctionCallToolName } from '@main/utils/mcp'
import { getBinaryName, getBinaryPath } from '@main/utils/process'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { SSEClientTransport, SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js'
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import {
StreamableHTTPClientTransport,
type StreamableHTTPClientTransportOptions
@@ -33,6 +32,7 @@ import { memoize } from 'lodash'
import { CacheService } from './CacheService'
import { CallBackServer } from './mcp/oauth/callback'
import { McpOAuthClientProvider } from './mcp/oauth/provider'
import getLoginShellEnvironment from './mcp/shell-env'
// Generic type for caching wrapped functions
type CachedFunction<T extends unknown[], R> = (...args: T) => Promise<R>
@@ -70,17 +70,7 @@ function withCache<T extends unknown[], R>(
class McpService {
private clients: Map<string, Client> = new Map()
private getServerKey(server: MCPServer): string {
return JSON.stringify({
baseUrl: server.baseUrl,
command: server.command,
args: server.args,
registryUrl: server.registryUrl,
env: server.env,
id: server.id
})
}
private pendingClients: Map<string, Promise<Client>> = new Map()
constructor() {
this.initClient = this.initClient.bind(this)
@@ -97,9 +87,26 @@ class McpService {
this.cleanup = this.cleanup.bind(this)
}
private getServerKey(server: MCPServer): string {
return JSON.stringify({
baseUrl: server.baseUrl,
command: server.command,
args: Array.isArray(server.args) ? server.args : [],
registryUrl: server.registryUrl,
env: server.env,
id: server.id
})
}
async initClient(server: MCPServer): Promise<Client> {
const serverKey = this.getServerKey(server)
// If there's a pending initialization, wait for it
const pendingClient = this.pendingClients.get(serverKey)
if (pendingClient) {
return pendingClient
}
// Check if we already have a client for this server configuration
const existingClient = this.clients.get(serverKey)
if (existingClient) {
@@ -114,11 +121,15 @@ class McpService {
} else {
return existingClient
}
} catch (error) {
Logger.error(`[MCP] Error pinging server ${server.name}:`, error)
} catch (error: any) {
Logger.error(`[MCP] Error pinging server ${server.name}:`, error?.message)
this.clients.delete(serverKey)
}
}
// Create a promise for the initialization process
const initPromise = (async () => {
try {
// Create new client instance for each connection
const client = new Client({ name: 'Cherry Studio', version: app.getVersion() }, { capabilities: {} })
@@ -162,7 +173,23 @@ class McpService {
} else if (server.type === 'sse') {
const options: SSEClientTransportOptions = {
eventSourceInit: {
fetch: (url, init) => fetch(url, { ...init, headers: server.headers || {} })
fetch: async (url, init) => {
const headers = { ...(server.headers || {}), ...(init?.headers || {}) }
// Get tokens from authProvider to make sure using the latest tokens
if (authProvider && typeof authProvider.tokens === 'function') {
try {
const tokens = await authProvider.tokens()
if (tokens && tokens.access_token) {
headers['Authorization'] = `Bearer ${tokens.access_token}`
}
} catch (error) {
Logger.error('Failed to fetch tokens:', error)
}
}
return fetch(url, { ...init, headers })
}
},
requestInit: {
headers: server.headers || {}
@@ -183,7 +210,7 @@ class McpService {
// add -x to args if args exist
if (args && args.length > 0) {
if (!args.includes('-y')) {
!args.includes('-y') && args.unshift('-y')
args.unshift('-y')
}
if (!args.includes('x')) {
args.unshift('x')
@@ -215,13 +242,18 @@ class McpService {
Logger.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`)
// Logger.info(`[MCP] Environment variables for server:`, server.env)
const loginShellEnv = await this.getLoginShellEnv()
// Bun not support proxy https://github.com/oven-sh/bun/issues/16812
if (cmd.includes('bun')) {
this.removeProxyEnv(loginShellEnv)
}
const stdioTransport = new StdioClientTransport({
command: cmd,
args,
env: {
...getDefaultEnvironment(),
PATH: await this.getEnhancedPath(process.env.PATH || ''),
...loginShellEnv,
...server.env
},
stderr: 'pipe'
@@ -285,7 +317,10 @@ class McpService {
try {
await client.connect(transport)
} catch (error: Error | any) {
if (error instanceof Error && (error.name === 'UnauthorizedError' || error.message.includes('Unauthorized'))) {
if (
error instanceof Error &&
(error.name === 'UnauthorizedError' || error.message.includes('Unauthorized'))
) {
Logger.info(`[MCP] Authentication required for server: ${server.name}`)
await handleAuth(client, transport as SSEClientTransport | StreamableHTTPClientTransport)
} else {
@@ -299,9 +334,19 @@ class McpService {
Logger.info(`[MCP] Activated server: ${server.name}`)
return client
} catch (error: any) {
Logger.error(`[MCP] Error activating server ${server.name}:`, error)
Logger.error(`[MCP] Error activating server ${server.name}:`, error?.message)
throw new Error(`[MCP] Error activating server ${server.name}: ${error.message}`)
}
} finally {
// Clean up the pending promise when done
this.pendingClients.delete(serverKey)
}
})()
// Store the pending promise
this.pendingClients.set(serverKey, initPromise)
return initPromise
}
async closeClient(serverKey: string) {
@@ -343,12 +388,32 @@ class McpService {
for (const [key] of this.clients) {
try {
await this.closeClient(key)
} catch (error) {
Logger.error(`[MCP] Failed to close client: ${error}`)
} catch (error: any) {
Logger.error(`[MCP] Failed to close client: ${error?.message}`)
}
}
}
/**
* Check connectivity for an MCP server
*/
public async checkMcpConnectivity(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise<boolean> {
Logger.info(`[MCP] Checking connectivity for server: ${server.name}`)
try {
const client = await this.initClient(server)
// Attempt to list tools as a way to check connectivity
await client.listTools()
Logger.info(`[MCP] Connectivity check successful for server: ${server.name}`)
return true
} catch (error) {
Logger.error(`[MCP] Connectivity check failed for server: ${server.name}`, error)
// Close the client if connectivity check fails to ensure a clean state for the next attempt
const serverKey = this.getServerKey(server)
await this.closeClient(serverKey)
return false
}
}
private async listToolsImpl(server: MCPServer): Promise<MCPTool[]> {
Logger.info(`[MCP] Listing tools for server: ${server.name}`)
const client = await this.initClient(server)
@@ -358,15 +423,15 @@ class McpService {
tools.map((tool: any) => {
const serverTool: MCPTool = {
...tool,
id: `f${nanoid()}`,
id: buildFunctionCallToolName(server.name, tool.name),
serverId: server.id,
serverName: server.name
}
serverTools.push(serverTool)
})
return serverTools
} catch (error) {
Logger.error(`[MCP] Failed to list tools for server: ${server.name}`, error)
} catch (error: any) {
Logger.error(`[MCP] Failed to list tools for server: ${server.name}`, error?.message)
return []
}
}
@@ -425,19 +490,21 @@ class McpService {
* List prompts available on an MCP server
*/
private async listPromptsImpl(server: MCPServer): Promise<MCPPrompt[]> {
Logger.info(`[MCP] Listing prompts for server: ${server.name}`)
const client = await this.initClient(server)
Logger.info(`[MCP] Listing prompts for server: ${server.name}`)
try {
const { prompts } = await client.listPrompts()
const serverPrompts = prompts.map((prompt: any) => ({
return prompts.map((prompt: any) => ({
...prompt,
id: `p${nanoid()}`,
serverId: server.id,
serverName: server.name
}))
return serverPrompts
} catch (error) {
Logger.error(`[MCP] Failed to list prompts for server: ${server.name}`, error)
} catch (error: any) {
// -32601 is the code for the method not found
if (error?.code !== -32601) {
Logger.error(`[MCP] Failed to list prompts for server: ${server.name}`, error?.message)
}
return []
}
}
@@ -495,19 +562,21 @@ class McpService {
* List resources available on an MCP server (implementation)
*/
private async listResourcesImpl(server: MCPServer): Promise<MCPResource[]> {
Logger.info(`[MCP] Listing resources for server: ${server.name}`)
const client = await this.initClient(server)
Logger.info(`[MCP] Listing resources for server: ${server.name}`)
try {
const result = await client.listResources()
const resources = result.resources || []
const serverResources = (Array.isArray(resources) ? resources : []).map((resource: any) => ({
return (Array.isArray(resources) ? resources : []).map((resource: any) => ({
...resource,
serverId: server.id,
serverName: server.name
}))
return serverResources
} catch (error) {
Logger.error(`[MCP] Failed to list resources for server: ${server.name}`, error)
} catch (error: any) {
// -32601 is the code for the method not found
if (error?.code !== -32601) {
Logger.error(`[MCP] Failed to list resources for server: ${server.name}`, error?.message)
}
return []
}
}
@@ -550,7 +619,7 @@ class McpService {
contents: contents
}
} catch (error: Error | any) {
Logger.error(`[MCP] Failed to get resource ${uri} from server: ${server.name}`, error)
Logger.error(`[MCP] Failed to get resource ${uri} from server: ${server.name}`, error.message)
throw new Error(`Failed to get resource ${uri} from server: ${server.name}: ${error.message}`)
}
}
@@ -574,164 +643,27 @@ class McpService {
return await cachedGetResource(server, uri)
}
private findPowerShellExecutable() {
const psPath = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' // Standard WinPS path
const pwshPath = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
if (fs.existsSync(psPath)) {
return psPath
}
if (fs.existsSync(pwshPath)) {
return pwshPath
}
return 'powershell.exe'
}
private getSystemPath = memoize(async (): Promise<string> => {
return new Promise((resolve, reject) => {
let command: string
let shell: string
if (process.platform === 'win32') {
shell = this.findPowerShellExecutable()
command = '$env:PATH'
} else {
// 尝试获取当前用户的默认 shell
let userShell = process.env.SHELL
if (!userShell) {
if (fs.existsSync('/bin/zsh')) {
userShell = '/bin/zsh'
} else if (fs.existsSync('/bin/bash')) {
userShell = '/bin/bash'
} else if (fs.existsSync('/bin/fish')) {
userShell = '/bin/fish'
} else {
userShell = '/bin/sh'
}
}
shell = userShell
// 根据不同的 shell 构建不同的命令
if (userShell.includes('zsh')) {
command =
'source /etc/zshenv 2>/dev/null || true; source ~/.zshenv 2>/dev/null || true; source /etc/zprofile 2>/dev/null || true; source ~/.zprofile 2>/dev/null || true; source /etc/zshrc 2>/dev/null || true; source ~/.zshrc 2>/dev/null || true; source /etc/zlogin 2>/dev/null || true; source ~/.zlogin 2>/dev/null || true; echo $PATH'
} else if (userShell.includes('bash')) {
command =
'source /etc/profile 2>/dev/null || true; source ~/.bash_profile 2>/dev/null || true; source ~/.bash_login 2>/dev/null || true; source ~/.profile 2>/dev/null || true; source ~/.bashrc 2>/dev/null || true; echo $PATH'
} else if (userShell.includes('fish')) {
command =
'source /etc/fish/config.fish 2>/dev/null || true; source ~/.config/fish/config.fish 2>/dev/null || true; source ~/.config/fish/config.local.fish 2>/dev/null || true; echo $PATH'
} else {
// 默认使用 zsh
shell = '/bin/zsh'
command =
'source /etc/zshenv 2>/dev/null || true; source ~/.zshenv 2>/dev/null || true; source /etc/zprofile 2>/dev/null || true; source ~/.zprofile 2>/dev/null || true; source /etc/zshrc 2>/dev/null || true; source ~/.zshrc 2>/dev/null || true; source /etc/zlogin 2>/dev/null || true; source ~/.zlogin 2>/dev/null || true; echo $PATH'
}
}
console.log(`Using shell: ${shell} with command: ${command}`)
const child = require('child_process').spawn(shell, ['-c', command], {
env: { ...process.env },
cwd: app.getPath('home')
})
let path = ''
child.stdout.on('data', (data: Buffer) => {
path += data.toString()
})
child.stderr.on('data', (data: Buffer) => {
console.error('Error getting PATH:', data.toString())
})
child.on('error', (error: Error) => {
reject(new Error(`Failed to get system PATH, ${error.message}`))
})
child.on('close', (code: number) => {
if (code === 0) {
const trimmedPath = path.trim()
resolve(trimmedPath)
} else {
reject(new Error(`Failed to get system PATH, exit code: ${code}`))
}
})
})
})
/**
* Get enhanced PATH including common tool locations
*/
private async getEnhancedPath(originalPath: string): Promise<string> {
let systemPath = ''
private getLoginShellEnv = memoize(async (): Promise<Record<string, string>> => {
try {
systemPath = await this.getSystemPath()
} catch (error) {
Logger.error('[MCP] Failed to get system PATH:', error)
}
// 将原始 PATH 按分隔符分割成数组
const loginEnv = await getLoginShellEnvironment()
const pathSeparator = process.platform === 'win32' ? ';' : ':'
const existingPaths = new Set(
[...systemPath.split(pathSeparator), ...originalPath.split(pathSeparator)].filter(Boolean)
)
const homeDir = process.env.HOME || process.env.USERPROFILE || ''
// 定义要添加的新路径
const newPaths: string[] = []
if (isMac) {
newPaths.push(
'/bin',
'/usr/bin',
'/usr/local/bin',
'/usr/local/sbin',
'/opt/homebrew/bin',
'/opt/homebrew/sbin',
'/usr/local/opt/node/bin',
`${homeDir}/.nvm/current/bin`,
`${homeDir}/.npm-global/bin`,
`${homeDir}/.yarn/bin`,
`${homeDir}/.cargo/bin`,
`${homeDir}/.cherrystudio/bin`,
'/opt/local/bin'
)
}
if (isLinux) {
newPaths.push(
'/bin',
'/usr/bin',
'/usr/local/bin',
`${homeDir}/.nvm/current/bin`,
`${homeDir}/.npm-global/bin`,
`${homeDir}/.yarn/bin`,
`${homeDir}/.cargo/bin`,
`${homeDir}/.cherrystudio/bin`,
'/snap/bin'
)
}
if (isWin) {
newPaths.push(
`${process.env.APPDATA}\\npm`,
`${homeDir}\\AppData\\Local\\Yarn\\bin`,
`${homeDir}\\.cargo\\bin`,
`${homeDir}\\.cherrystudio\\bin`
)
}
// 只添加不存在的路径
newPaths.forEach((path) => {
if (path && !existingPaths.has(path)) {
existingPaths.add(path)
const cherryBinPath = path.join(os.homedir(), '.cherrystudio', 'bin')
loginEnv.PATH = `${loginEnv.PATH}${pathSeparator}${cherryBinPath}`
Logger.info('[MCP] Successfully fetched login shell environment variables:')
return loginEnv
} catch (error) {
Logger.error('[MCP] Failed to fetch login shell environment variables:', error)
return {}
}
})
// 转换回字符串
return Array.from(existingPaths).join(pathSeparator)
private removeProxyEnv(env: Record<string, string>) {
delete env.HTTPS_PROXY
delete env.HTTP_PROXY
delete env.grpc_proxy
delete env.http_proxy
delete env.https_proxy
}
}
const mcpService = new McpService()
export default mcpService
export default new McpService()

View File

@@ -0,0 +1,31 @@
import { BrowserWindow, Notification as ElectronNotification } from 'electron'
import { Notification } from 'src/renderer/src/types/notification'
import icon from '../../../build/icon.png?asset'
class NotificationService {
private window: BrowserWindow
constructor(window: BrowserWindow) {
// Initialize the service
this.window = window
}
public async sendNotification(notification: Notification) {
// 使用 Electron Notification API
const electronNotification = new ElectronNotification({
title: notification.title,
body: notification.message,
icon: icon
})
electronNotification.on('click', () => {
this.window.show()
this.window.webContents.send('notification-click', notification)
})
electronNotification.show()
}
}
export default NotificationService

View File

@@ -32,10 +32,9 @@ interface WebDAVResponse {
}
export async function getNutstoreSSOUrl() {
const url = await createOAuthUrl({
return await createOAuthUrl({
app: 'cherrystudio'
})
return url
}
export async function decryptToken(token: string) {

View File

@@ -1,4 +1,5 @@
import { app } from 'electron'
import Logger from 'electron-log'
import fs from 'fs'
import path from 'path'
@@ -155,7 +156,7 @@ class ObsidianVaultService {
return []
}
console.log('获取Vault文件结构:', vault.name, vault.path)
Logger.log('获取Vault文件结构:', vault.name, vault.path)
return this.getVaultStructure(vault.path)
} catch (error) {
console.error('获取Vault文件结构时发生错误:', error)

View File

@@ -6,6 +6,7 @@ import { promisify } from 'node:util'
import { app } from 'electron'
import Logger from 'electron-log'
import { handleProvidersProtocolUrl } from './urlschema/handle-providers'
import { handleMcpProtocolUrl } from './urlschema/mcp-install'
import { windowService } from './WindowService'
@@ -25,7 +26,6 @@ export function handleProtocolUrl(url: string) {
if (!url) return
// Process the URL that was used to open the app
// The url will be in the format: cherrystudio://data?param1=value1&param2=value2
console.log('Received URL:', url)
// Parse the URL and extract parameters
const urlObj = new URL(url)
@@ -35,6 +35,9 @@ export function handleProtocolUrl(url: string) {
case 'mcp':
handleMcpProtocolUrl(urlObj)
return
case 'providers':
handleProvidersProtocolUrl(urlObj)
return
}
// You can send the data to your renderer process

View File

@@ -1,8 +1,12 @@
import { ProxyConfig as _ProxyConfig, session } from 'electron'
import { socksDispatcher } from 'fetch-socks'
// import { ProxyConfig as _ProxyConfig, session } from 'electron'
import { session } from 'electron'
declare type _ProxyConfig = any
// import { socksDispatcher } from 'fetch-socks'
import { getSystemProxy } from 'os-proxy-config'
import { ProxyAgent as GeneralProxyAgent } from 'proxy-agent'
import { ProxyAgent, setGlobalDispatcher } from 'undici'
// import { ProxyAgent, setGlobalDispatcher } from 'undici'
type ProxyMode = 'system' | 'custom' | 'none'
@@ -121,23 +125,22 @@ export class ProxyManager {
return this.config.url || ''
}
setGlobalProxy() {
const proxyUrl = this.config.url
if (proxyUrl) {
const [protocol, address] = proxyUrl.split('://')
const [host, port] = address.split(':')
if (!protocol.includes('socks')) {
setGlobalDispatcher(new ProxyAgent(proxyUrl))
} else {
const dispatcher = socksDispatcher({
port: parseInt(port),
type: protocol === 'socks5' ? 5 : 4,
host: host
})
global[Symbol.for('undici.globalDispatcher.1')] = dispatcher
}
}
}
// setGlobalProxy() {
// const proxyUrl = this.config.url
// if (proxyUrl) {
// const [protocol, address] = proxyUrl.split('://')
// const [host, port] = address.split(':')
// if (!protocol.includes('socks')) {
// setGlobalDispatcher(new ProxyAgent(proxyUrl))
// } else {
// global[Symbol.for('undici.globalDispatcher.1')] = socksDispatcher({
// port: parseInt(port),
// type: protocol === 'socks5' ? 5 : 4,
// host: host
// })
// }
// }
// }
}
export const proxyManager = new ProxyManager()

View File

@@ -191,7 +191,7 @@ export const reduxService = new ReduxService()
try {
// 读取状态
const settings = await reduxService.select('state.settings')
console.log('settings', settings)
Logger.log('settings', settings)
// 派发 action
await reduxService.dispatch({
@@ -201,7 +201,7 @@ export const reduxService = new ReduxService()
// 订阅状态变化
const unsubscribe = await reduxService.subscribe('state.settings.apiKey', (newValue) => {
console.log('API key changed:', newValue)
Logger.log('API key changed:', newValue)
})
// 批量执行 actions
@@ -212,16 +212,16 @@ export const reduxService = new ReduxService()
// 同步方法虽然可能不是最新的数据,但响应更快
const apiKey = reduxService.selectSync('state.settings.apiKey')
console.log('apiKey', apiKey)
Logger.log('apiKey', apiKey)
// 处理保证是最新的数据
const apiKey1 = await reduxService.select('state.settings.apiKey')
console.log('apiKey1', apiKey1)
Logger.log('apiKey1', apiKey1)
// 取消订阅
unsubscribe()
} catch (error) {
console.error('Error:', error)
Logger.error('Error:', error)
}
}
*/

View File

@@ -0,0 +1,57 @@
// 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

@@ -74,8 +74,7 @@ export class SearchService {
})
// Get the page content after ensuring it's fully loaded
const content = await window.webContents.executeJavaScript('document.documentElement.outerHTML')
return content
return await window.webContents.executeJavaScript('document.documentElement.outerHTML')
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
import { handleZoomFactor } from '@main/utils/zoom'
import { Shortcut } from '@types'
import { BrowserWindow, globalShortcut } from 'electron'
import Logger from 'electron-log'
@@ -14,14 +15,11 @@ const windowOnHandlers = new Map<BrowserWindow, { onFocusHandler: () => void; on
function getShortcutHandler(shortcut: Shortcut) {
switch (shortcut.key) {
case 'zoom_in':
return (window: BrowserWindow) => handleZoom(0.1)(window)
return (window: BrowserWindow) => handleZoomFactor([window], 0.1)
case 'zoom_out':
return (window: BrowserWindow) => handleZoom(-0.1)(window)
return (window: BrowserWindow) => handleZoomFactor([window], -0.1)
case 'zoom_reset':
return (window: BrowserWindow) => {
window.webContents.setZoomFactor(1)
configManager.setZoomFactor(1)
}
return (window: BrowserWindow) => handleZoomFactor([window], 0, true)
case 'show_app':
return () => {
windowService.toggleMainWindow()
@@ -39,17 +37,6 @@ function formatShortcutKey(shortcut: string[]): string {
return shortcut.join('+')
}
function handleZoom(delta: number) {
return (window: BrowserWindow) => {
const currentZoom = configManager.getZoomFactor()
const newZoom = Number((currentZoom + delta).toFixed(1))
if (newZoom >= 0.1 && newZoom <= 5.0) {
window.webContents.setZoomFactor(newZoom)
configManager.setZoomFactor(newZoom)
}
}
}
const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat = (
shortcut: string | string[]
): string => {

View File

@@ -0,0 +1,116 @@
import { IpcChannel } from '@shared/IpcChannel'
import type { StoreSyncAction } from '@types'
import { BrowserWindow, ipcMain } from 'electron'
/**
* StoreSyncService class manages Redux store synchronization between multiple windows in the main process
* It uses singleton pattern to ensure only one sync service instance exists in the application
*
* Main features:
* 1. Manages window subscriptions for store sync
* 2. Handles IPC communication for store sync between windows
* 3. Broadcasts Redux actions from one window to all other windows
* 4. Adds metadata to synced actions to prevent infinite sync loops
*/
export class StoreSyncService {
private static instance: StoreSyncService
private windowIds: number[] = []
private isIpcHandlerRegistered = false
private constructor() {
return
}
/**
* Get the singleton instance of StoreSyncService
*/
public static getInstance(): StoreSyncService {
if (!StoreSyncService.instance) {
StoreSyncService.instance = new StoreSyncService()
}
return StoreSyncService.instance
}
/**
* Subscribe a window to store sync
* @param windowId ID of the window to subscribe
*/
public subscribe(windowId: number): void {
if (!this.windowIds.includes(windowId)) {
this.windowIds.push(windowId)
}
}
/**
* Unsubscribe a window from store sync
* @param windowId ID of the window to unsubscribe
*/
public unsubscribe(windowId: number): void {
this.windowIds = this.windowIds.filter((id) => id !== windowId)
}
/**
* Register IPC handlers for store sync communication
* Handles window subscription, unsubscription and action broadcasting
*/
public registerIpcHandler(): void {
if (this.isIpcHandlerRegistered) return
ipcMain.handle(IpcChannel.StoreSync_Subscribe, (event) => {
const windowId = BrowserWindow.fromWebContents(event.sender)?.id
if (windowId) {
this.subscribe(windowId)
}
})
ipcMain.handle(IpcChannel.StoreSync_Unsubscribe, (event) => {
const windowId = BrowserWindow.fromWebContents(event.sender)?.id
if (windowId) {
this.unsubscribe(windowId)
}
})
ipcMain.handle(IpcChannel.StoreSync_OnUpdate, (event, action: StoreSyncAction) => {
const sourceWindowId = BrowserWindow.fromWebContents(event.sender)?.id
if (!sourceWindowId) return
// Broadcast the action to all other windows
this.broadcastToOtherWindows(sourceWindowId, action)
})
this.isIpcHandlerRegistered = true
}
/**
* Broadcast a Redux action to all other windows except the source
* @param sourceWindowId ID of the window that originated the action
* @param action Redux action to broadcast
*/
private broadcastToOtherWindows(sourceWindowId: number, action: StoreSyncAction): void {
// Add metadata to indicate this action came from sync
const syncAction = {
...action,
meta: {
...action.meta,
fromSync: true,
source: `windowId:${sourceWindowId}`
}
}
// Send to all windows except the source
this.windowIds.forEach((windowId) => {
if (windowId !== sourceWindowId) {
const targetWindow = BrowserWindow.fromId(windowId)
if (targetWindow && !targetWindow.isDestroyed()) {
targetWindow.webContents.send(IpcChannel.StoreSync_BroadcastSync, syncAction)
} else {
this.unsubscribe(windowId)
}
}
})
}
}
// Export singleton instance
export default StoreSyncService.getInstance()

View File

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

View File

@@ -5,16 +5,17 @@ import { app, Menu, MenuItemConstructorOptions, nativeImage, nativeTheme, Tray }
import icon from '../../../build/tray_icon.png?asset'
import iconDark from '../../../build/tray_icon_dark.png?asset'
import iconLight from '../../../build/tray_icon_light.png?asset'
import { configManager } from './ConfigManager'
import { ConfigKeys, configManager } from './ConfigManager'
import { windowService } from './WindowService'
export class TrayService {
private static instance: TrayService
private tray: Tray | null = null
private contextMenu: Menu | null = null
constructor() {
this.watchConfigChanges()
this.updateTray()
this.watchTrayChanges()
TrayService.instance = this
}
@@ -43,6 +44,30 @@ export class TrayService {
this.tray = tray
this.updateContextMenu()
if (process.platform === 'linux') {
this.tray.setContextMenu(this.contextMenu)
}
this.tray.setToolTip('Cherry Studio')
this.tray.on('right-click', () => {
if (this.contextMenu) {
this.tray?.popUpContextMenu(this.contextMenu)
}
})
this.tray.on('click', () => {
if (configManager.getEnableQuickAssistant() && configManager.getClickTrayToShowQuickAssistant()) {
windowService.showMiniWindow()
} else {
windowService.showMainWindow()
}
})
}
private updateContextMenu() {
const locale = locales[configManager.getLanguage()]
const { tray: trayLocale } = locale.translation
@@ -64,25 +89,7 @@ export class TrayService {
}
].filter(Boolean) as MenuItemConstructorOptions[]
const contextMenu = Menu.buildFromTemplate(template)
if (process.platform === 'linux') {
this.tray.setContextMenu(contextMenu)
}
this.tray.setToolTip('Cherry Studio')
this.tray.on('right-click', () => {
this.tray?.popUpContextMenu(contextMenu)
})
this.tray.on('click', () => {
if (enableQuickAssistant && configManager.getClickTrayToShowQuickAssistant()) {
windowService.showMiniWindow()
} else {
windowService.showMainWindow()
}
})
this.contextMenu = Menu.buildFromTemplate(template)
}
private updateTray() {
@@ -94,13 +101,6 @@ export class TrayService {
}
}
public restartTray() {
if (configManager.getTray()) {
this.destroyTray()
this.createTray()
}
}
private destroyTray() {
if (this.tray) {
this.tray.destroy()
@@ -108,8 +108,16 @@ export class TrayService {
}
}
private watchTrayChanges() {
configManager.subscribe<boolean>('tray', () => this.updateTray())
private watchConfigChanges() {
configManager.subscribe(ConfigKeys.Tray, () => this.updateTray())
configManager.subscribe(ConfigKeys.Language, () => {
this.updateContextMenu()
})
configManager.subscribe(ConfigKeys.EnableQuickAssistant, () => {
this.updateContextMenu()
})
}
private quit() {

View File

@@ -1,6 +1,7 @@
import { WebDavConfig } from '@types'
import Logger from 'electron-log'
import Stream from 'stream'
import https from 'https'
import {
BufferLike,
createClient,
@@ -20,7 +21,8 @@ export default class WebDav {
username: params.webdavUser,
password: params.webdavPass,
maxBodyLength: Infinity,
maxContentLength: Infinity
maxContentLength: Infinity,
httpsAgent: new https.Agent({ rejectUnauthorized: false })
})
this.putFileContents = this.putFileContents.bind(this)
@@ -74,6 +76,19 @@ export default class WebDav {
}
}
public getDirectoryContents = async () => {
if (!this.instance) {
throw new Error('WebDAV client not initialized')
}
try {
return await this.instance.getDirectoryContents(this.webdavPath)
} catch (error) {
Logger.error('[WebDAV] Error getting directory contents on WebDAV:', error)
throw error
}
}
public checkConnection = async () => {
if (!this.instance) {
throw new Error('WebDAV client not initialized')

View File

@@ -6,12 +6,8 @@ import { session, shell, webContents } from 'electron'
*/
export function initSessionUserAgent() {
const wvSession = session.fromPartition('persist:webview')
const newChromeVersion = '135.0.7049.96'
const originUA = wvSession.getUserAgent()
const newUA = originUA
.replace(/CherryStudio\/\S+\s/, '')
.replace(/Electron\/\S+\s/, '')
.replace(/Chrome\/\d+\.\d+\.\d+\.\d+/, `Chrome/${newChromeVersion}`)
const newUA = originUA.replace(/CherryStudio\/\S+\s/, '').replace(/Electron\/\S+\s/, '')
wvSession.setUserAgent(newUA)
}

View File

@@ -1,17 +1,19 @@
// just import the themeService to ensure the theme is initialized
import './ThemeService'
import { is } from '@electron-toolkit/utils'
import { isDev, isLinux, isMac, isWin } from '@main/constant'
import { getFilesDir } from '@main/utils/file'
import { IpcChannel } from '@shared/IpcChannel'
import { ThemeMode } from '@types'
import { app, BrowserWindow, ipcMain, Menu, MenuItem, nativeTheme, shell } from 'electron'
import { app, BrowserWindow, nativeTheme, shell } from 'electron'
import Logger from 'electron-log'
import windowStateKeeper from 'electron-window-state'
import { join } from 'path'
import icon from '../../../build/icon.png?asset'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
import { locales } from '../utils/locales'
import { configManager } from './ConfigManager'
import { contextMenu } from './ContextMenu'
import { initSessionUserAgent } from './WebviewService'
export class WindowService {
@@ -22,9 +24,7 @@ export class WindowService {
//hacky-fix: store the focused status of mainWindow before miniWindow shows
//to restore the focus status when miniWindow hides
private wasMainWindowFocused: boolean = false
private selectionMenuWindow: BrowserWindow | null = null
private lastSelectedText: string = ''
private contextMenu: Menu | null = null
private lastRendererProcessCrashTime: number = 0
public static getInstance(): WindowService {
if (!WindowService.instance) {
@@ -47,13 +47,6 @@ export class WindowService {
maximize: false
})
const theme = configManager.getTheme()
if (theme === ThemeMode.auto) {
nativeTheme.themeSource = 'system'
} else {
nativeTheme.themeSource = theme
}
this.mainWindow = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
@@ -66,7 +59,7 @@ export class WindowService {
transparent: isMac,
vibrancy: 'sidebar',
visualEffectState: 'active',
titleBarStyle: isLinux ? 'default' : 'hidden',
titleBarStyle: 'hidden',
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
darkTheme: nativeTheme.shouldUseDarkColors,
@@ -77,7 +70,9 @@ export class WindowService {
sandbox: false,
webSecurity: false,
webviewTag: true,
allowRunningInsecureContent: true
allowRunningInsecureContent: true,
zoomFactor: configManager.getZoomFactor(),
backgroundThrottling: false
}
})
@@ -103,9 +98,32 @@ export class WindowService {
this.setupWindowEvents(mainWindow)
this.setupWebContentsHandlers(mainWindow)
this.setupWindowLifecycleEvents(mainWindow)
this.setupMainWindowMonitor(mainWindow)
this.loadMainWindowContent(mainWindow)
}
private setupMainWindowMonitor(mainWindow: BrowserWindow) {
mainWindow.webContents.on('render-process-gone', (_, details) => {
Logger.error(`Renderer process crashed with: ${JSON.stringify(details)}`)
const currentTime = Date.now()
const lastCrashTime = this.lastRendererProcessCrashTime
this.lastRendererProcessCrashTime = currentTime
if (currentTime - lastCrashTime > 60 * 1000) {
// 如果大于1分钟则重启渲染进程
mainWindow.webContents.reload()
} else {
// 如果小于1分钟则退出应用, 可能是连续crash需要退出应用
app.exit(1)
}
})
mainWindow.webContents.on('unresponsive', () => {
// 在升级到electron 34后可以获取具体js stack trace,目前只打个日志监控下
// https://www.electronjs.org/blog/electron-34-0#unresponsive-renderer-javascript-call-stacks
Logger.error('Renderer process unresponsive')
})
}
private setupMaximize(mainWindow: BrowserWindow, isMaximized: boolean) {
if (isMaximized) {
// 如果是从托盘启动,则需要延迟最大化,否则显示的就不是重启前的最大化窗口了
@@ -118,18 +136,9 @@ export class WindowService {
}
private setupContextMenu(mainWindow: BrowserWindow) {
if (!this.contextMenu) {
const locale = locales[configManager.getLanguage()]
const { common } = locale.translation
this.contextMenu = new Menu()
this.contextMenu.append(new MenuItem({ label: common.copy, role: 'copy' }))
this.contextMenu.append(new MenuItem({ label: common.paste, role: 'paste' }))
this.contextMenu.append(new MenuItem({ label: common.cut, role: 'cut' }))
}
mainWindow.webContents.on('context-menu', () => {
this.contextMenu?.popup()
contextMenu.contextMenu(mainWindow)
app.on('browser-window-created', (_, win) => {
contextMenu.contextMenu(win)
})
// Dangerous API
@@ -138,13 +147,6 @@ export class WindowService {
webPreferences.preload = join(__dirname, '../preload/index.js')
})
}
// Handle webview context menu
mainWindow.webContents.on('did-attach-webview', (_, webContents) => {
webContents.on('context-menu', () => {
this.contextMenu?.popup()
})
})
}
private setupWindowEvents(mainWindow: BrowserWindow) {
@@ -179,6 +181,12 @@ export class WindowService {
mainWindow.webContents.setZoomFactor(configManager.getZoomFactor())
})
// set the zoom factor again when the window is going to restore
// minimize and restore will cause zoom reset
mainWindow.on('restore', () => {
mainWindow.webContents.setZoomFactor(configManager.getZoomFactor())
})
// ARCH: as `will-resize` is only for Win & Mac,
// linux has the same problem, use `resize` listener instead
// but `resize` will fliker the ui
@@ -193,10 +201,21 @@ export class WindowService {
// 当按下Escape键且窗口处于全屏状态时退出全屏
if (input.key === 'Escape' && !input.alt && !input.control && !input.meta && !input.shift) {
if (mainWindow.isFullScreen()) {
// 获取 shortcuts 配置
const shortcuts = configManager.getShortcuts()
const exitFullscreenShortcut = shortcuts.find((s) => s.key === 'exit_fullscreen')
if (exitFullscreenShortcut == undefined) {
mainWindow.setFullScreen(false)
return
}
if (exitFullscreenShortcut?.enabled) {
event.preventDefault()
mainWindow.setFullScreen(false)
return
}
}
}
return
})
}
@@ -301,7 +320,7 @@ export class WindowService {
/**
* 上述逻辑以下:
* win/linux: 是开启托盘+设置关闭时最小化到托盘的情况
* win/linux: 是"开启托盘+设置关闭时最小化到托盘"的情况
* mac: 任何情况都会到这里因此需要单独处理mac
*/
@@ -424,7 +443,8 @@ export class WindowService {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: false,
webviewTag: true
webviewTag: true,
backgroundThrottling: false
}
})
@@ -462,16 +482,10 @@ export class WindowService {
this.miniWindow?.webContents.send(IpcChannel.ShowMiniWindow)
})
ipcMain.on(IpcChannel.MiniWindowReload, () => {
this.miniWindow?.reload()
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
this.miniWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/mini')
this.miniWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/miniWindow.html')
} else {
this.miniWindow.loadFile(join(__dirname, '../renderer/index.html'), {
hash: '#/mini'
})
this.miniWindow.loadFile(join(__dirname, '../renderer/miniWindow.html'))
}
return this.miniWindow
@@ -484,10 +498,6 @@ export class WindowService {
return
}
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
this.selectionMenuWindow.hide()
}
if (this.miniWindow && !this.miniWindow.isDestroyed()) {
this.wasMainWindowFocused = this.mainWindow?.isFocused() || false
@@ -534,74 +544,6 @@ export class WindowService {
public setPinMiniWindow(isPinned) {
this.isPinnedMiniWindow = isPinned
}
public showSelectionMenu(bounds: { x: number; y: number }) {
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
this.selectionMenuWindow.setPosition(bounds.x, bounds.y)
this.selectionMenuWindow.show()
return
}
const theme = configManager.getTheme()
this.selectionMenuWindow = new BrowserWindow({
width: 280,
height: 40,
x: bounds.x,
y: bounds.y,
show: true,
autoHideMenuBar: true,
transparent: true,
frame: false,
alwaysOnTop: false,
skipTaskbar: true,
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
resizable: false,
vibrancy: 'popover',
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: false
}
})
// 点击其他地方时隐藏窗口
this.selectionMenuWindow.on('blur', () => {
this.selectionMenuWindow?.hide()
this.miniWindow?.webContents.send(IpcChannel.SelectionAction, {
action: 'home',
selectedText: this.lastSelectedText
})
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
this.selectionMenuWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/src/windows/menu/menu.html')
} else {
this.selectionMenuWindow.loadFile(join(__dirname, '../renderer/src/windows/menu/menu.html'))
}
this.setupSelectionMenuEvents()
}
private setupSelectionMenuEvents() {
if (!this.selectionMenuWindow) return
ipcMain.removeHandler(IpcChannel.SelectionMenu_Action)
ipcMain.handle(IpcChannel.SelectionMenu_Action, (_, action) => {
this.selectionMenuWindow?.hide()
this.showMiniWindow()
setTimeout(() => {
this.miniWindow?.webContents.send(IpcChannel.SelectionAction, {
action,
selectedText: this.lastSelectedText
})
}, 100)
})
}
public setLastSelectedText(text: string) {
this.lastSelectedText = text
}
}
export const windowService = WindowService.getInstance()

View File

@@ -44,7 +44,7 @@ export class CallBackServer {
Logger.error('OAuth callback server error:', error)
})
const runningServer = new Promise<http.Server>((resolve, reject) => {
return new Promise<http.Server>((resolve, reject) => {
server.listen(port, () => {
Logger.info(`OAuth callback server listening on port ${port}`)
resolve(server)
@@ -54,7 +54,6 @@ export class CallBackServer {
reject(error)
})
})
return runningServer
}
get getServer(): Promise<http.Server> {

View File

@@ -0,0 +1,120 @@
import { spawn } from 'child_process'
import Logger from 'electron-log'
import os from 'os'
/**
* Spawns a login shell in the user's home directory to capture its environment variables.
* @returns {Promise<Object>} A promise that resolves with an object containing
* the environment variables, or rejects with an error.
*/
function getLoginShellEnvironment(): Promise<Record<string, string>> {
return new Promise((resolve, reject) => {
const homeDirectory = os.homedir()
if (!homeDirectory) {
return reject(new Error("Could not determine user's home directory."))
}
let shellPath = process.env.SHELL
let commandArgs
let shellCommandToGetEnv
const platform = os.platform()
if (platform === 'win32') {
// On Windows, 'cmd.exe' is the common shell.
// The 'set' command lists environment variables.
// We don't typically talk about "login shells" in the same way,
// but cmd will load the user's environment.
shellPath = process.env.COMSPEC || 'cmd.exe'
shellCommandToGetEnv = 'set'
commandArgs = ['/c', shellCommandToGetEnv] // /c Carries out the command specified by string and then terminates
} else {
// For POSIX systems (Linux, macOS)
if (!shellPath) {
// Fallback if process.env.SHELL is not set (less common for interactive users)
// Defaulting to bash, but this might not be the user's actual login shell.
// A more robust solution might involve checking /etc/passwd or similar,
// but that's more complex and often requires higher privileges or native modules.
Logger.warn("process.env.SHELL is not set. Defaulting to /bin/bash. This might not be the user's login shell.")
shellPath = '/bin/bash' // A common default
}
// -l: Make it a login shell. This sources profile files like .profile, .bash_profile, .zprofile etc.
// -i: Make it interactive. Some shells or profile scripts behave differently.
// 'env': The command to print environment variables.
// Using 'env -0' would be more robust for parsing if values contain newlines,
// but requires splitting by null character. For simplicity, we'll use 'env'.
shellCommandToGetEnv = 'env'
commandArgs = ['-ilc', shellCommandToGetEnv] // -i for interactive, -l for login, -c to execute command
}
Logger.log(`[ShellEnv] Spawning shell: ${shellPath} with args: ${commandArgs.join(' ')} in ${homeDirectory}`)
const child = spawn(shellPath, commandArgs, {
cwd: homeDirectory, // Run the command in the user's home directory
detached: true, // Allows the parent to exit independently of the child
stdio: ['ignore', 'pipe', 'pipe'], // stdin, stdout, stderr
shell: false // We are specifying the shell command directly
})
let output = ''
let errorOutput = ''
child.stdout.on('data', (data) => {
output += data.toString()
})
child.stderr.on('data', (data) => {
errorOutput += data.toString()
})
child.on('error', (error) => {
Logger.error(`Failed to start shell process: ${shellPath}`, error)
reject(new Error(`Failed to start shell: ${error.message}`))
})
child.on('close', (code) => {
if (code !== 0) {
const errorMessage = `Shell process exited with code ${code}. Shell: ${shellPath}. Args: ${commandArgs.join(' ')}. CWD: ${homeDirectory}. Stderr: ${errorOutput.trim()}`
Logger.error(errorMessage)
return reject(new Error(errorMessage))
}
if (errorOutput.trim()) {
// Some shells might output warnings or non-fatal errors to stderr
// during profile loading. Log it, but proceed if exit code is 0.
Logger.warn(`Shell process stderr output (even with exit code 0):\n${errorOutput.trim()}`)
}
const env: Record<string, string> = {}
const lines = output.split('\n')
lines.forEach((line) => {
const trimmedLine = line.trim()
if (trimmedLine) {
const separatorIndex = trimmedLine.indexOf('=')
if (separatorIndex > 0) {
// Ensure '=' is present and it's not the first character
const key = trimmedLine.substring(0, separatorIndex)
const value = trimmedLine.substring(separatorIndex + 1)
env[key] = value
}
}
})
if (Object.keys(env).length === 0 && output.length < 100) {
// Arbitrary small length check
// This might indicate an issue if no env vars were parsed or output was minimal
Logger.warn(
'Parsed environment is empty or output was very short. This might indicate an issue with shell execution or environment variable retrieval.'
)
Logger.warn('Raw output from shell:\n', output)
}
env.PATH = env.Path || env.PATH || ''
resolve(env)
})
})
}
export default getLoginShellEnvironment

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