Compare commits

..

138 Commits

Author SHA1 Message Date
Teo
d618cdf19e feat(icons): add new lightbulb icons and update reasoning effort settings in translations
- Introduced new SVG icons for lightbulb states (off, on at 10%, 50%, and 90%).
- Added "Off" reasoning effort option in English, Japanese, Russian, Simplified Chinese, and Traditional Chinese translations.
- Refactored Inputbar and ThinkingButton components to integrate new reasoning effort logic and icon display.
2025-05-01 12:59:32 +08:00
suyao
9120136f45 Merge branch 'main' into feat/qwen3-support 2025-04-30 15:29:41 +08:00
suyao
8747974359 refactor: enhance ThinkingPanel and related components for improved reasoning effort management
- Updated ThinkingPanel to streamline token mapping and error messaging.
- Refactored ThinkingSelect to utilize a list for better UI interaction.
- Enhanced ThinkingSlider with styled components for a more intuitive user experience.
- Adjusted model checks in the configuration to support new reasoning models.
- Improved translations for clarity and consistency across languages.
2025-04-30 15:26:51 +08:00
Akey Zhang
ecd7518505 style: optimize mcp arg name display layout when tool or prompt descr… (#5467)
* style: optimize mcp arg name display layout when tool or prompt description is short

* style: optimize mcp arg name display layout, make it simple

* fix: remove unused import
2025-04-30 11:22:39 +08:00
suyao
69e9b9855e feat: implement ThinkingPanel for managing reasoning effort and token limits
- Added ThinkingPanel component to handle user settings for reasoning effort and thinking budget.
- Introduced ThinkingSelect and ThinkingSlider components for selecting reasoning effort and adjusting token limits.
- Updated models and hooks to support new reasoning effort and thinking budget features.
- Enhanced Inputbar to integrate ThinkingPanel and provide a toggle for enabling thinking features.
- Updated translations and styles for new components.
2025-04-30 02:52:18 +08:00
MyPrototypeWhat
08ee877676 fix: update webSearch type and results structure in upgrade logic (#5512) 2025-04-30 00:16:58 +08:00
one
6ee8a72823 fix: determining thinking process using block status (#5509)
* fix: determining thinking process using block status

* refactor: merge MessageThought to ThinkingBlock

* style: fix typos

* fix: error handling

* refactor: set collapsed status as default

* refactor: remove pending status

* refactor: better collapsing behaviour

* refactor: remove processing status
2025-04-30 00:04:13 +08:00
SuYao
b0d6f209d7 feat(messageThunk): integrate autoRenameTopic functionality to update topic names based on assistant responses (#5504) 2025-04-29 23:07:09 +08:00
Chen Tao
1c5526c020 feat: optimize extract logic (#5470)
* feat: optimize extract logic

* chore
2025-04-29 20:43:50 +08:00
karl
f29b83faab fix: The Error display of the failed mcp call shows that the Error type cannot be displayed (#5492)
* fix: The Error display of the failed mcp call shows that the Error type cannot be displayed

* Update src/renderer/src/utils/mcp-tools.ts

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-04-29 17:40:50 +08:00
SuYao
57e8b9a592 fix(GeminiProvider): relocate grounding metadata handling (#5490)
* fix(GeminiProvider): relocate grounding metadata handling to improve code clarity and maintainability

* refactor(GeminiProvider): enhance conditional checks for chunk processing
2025-04-29 17:36:35 +08:00
karl
0782b24790 perf: <tool_use> display (#5489) 2025-04-29 16:39:19 +08:00
Teo
0a7bf99f9c refactor: network search module, support quick menu (#5291)
* refactor: Reconstruct the network search module to support quick menu switching between different suppliers.

* refactor(GeminiProvider): simplify web search enablement logic

* refactor(settings): remove unnecessary SettingDivider for cleaner layout

* refactor(SelectModelButton): remove unused setModel function for improved performance

* refactor(ApiService): simplify web search condition by removing redundant check
2025-04-29 16:31:41 +08:00
Chen Tao
7be6ddfb59 fix: message do not use knowledge (#5485) 2025-04-29 16:19:19 +08:00
SuYao
0413884021 refactor(MainTextBlock): enhance content processing by ignoring tooluse (#5483) 2025-04-29 16:16:34 +08:00
MyPrototypeWhat
4225d20760 feat: 添加 messageBlock、messageThunk 和 useMessageOperations 使用指南文档
- 新增 `how-to-use-messageBlock.md`,详细介绍 `messageBlock.ts` 的 Redux Slice 及其状态管理、actions 和 selectors。
- 新增 `how-to-use-messageThunk.md`,概述 `messageThunk.ts` 的核心功能和主要 Thunks 的使用。
- 新增 `how-to-use-useMessageOperations.md`,提供 `useMessageOperations` Hook 的使用示例和功能说明,简化组件与消息数据的交互。
2025-04-29 15:14:55 +08:00
chenxue
efad8f9ad0 feat: add painting aihubmix provider (#4503)
* add painting aihubmix provider

* fix: Cannot read properties of undefined (reading 'unshift')

* fix: painting redux data

---------

Co-authored-by: zhaochenxue <zhaochenxue@bixin.cn>
Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-04-29 14:38:59 +08:00
MyPrototypeWhat
a6822d4037 refactor: message block structure (#4660)
* feat(message-blocks): introduce new message block types and middleware for handling message updates

- Added new types for message blocks including MainText, Thinking, Translation, Code, Image, ToolCall, ToolResult, KnowledgeCitation, WebSearch, File, and Error blocks.
- Implemented middleware to manage message block mapping and updates in Redux, enhancing the handling of message states and blocks.
- Introduced LRU caching for efficient message block retrieval and management.

* feat(messages): implement message management slice and utility functions

- Introduced a new Redux slice for managing messages, including actions for setting the current topic, loading state, error handling, and message updates.
- Added utility functions for creating various message block types, enhancing the structure and management of message content.
- Updated message type definitions to include timestamped block references, improving the tracking of message block states.

* feat(store): add messageBlocks reducer and integrate into rootReducer

- Introduced a new messageBlocks reducer to manage message block state.
- Updated rootReducer to include messageBlocks alongside existing reducers.
- Refactored newMessage slice to accommodate changes in message structure and block handling.
- Removed obsolete messageBlockMap utility file to streamline codebase.

* feat(database): implement version 7 migration and enhance message structure

- Added new message_blocks table to the database schema for improved message handling.
- Introduced upgradeToV7 function to migrate existing topics and messages to the new structure.
- Refactored message creation utilities to support new message block types and improved error handling.
- Implemented various message filtering utilities to streamline message processing.
- Updated Redux store to accommodate new message structure and loading states.

* feat(message): refactor message handling and introduce messageStreamProcessor

- Updated database schema to correct types for topics and message blocks.
- Introduced messageStreamProcessor to handle API responses and transform them into application-specific data structures.
- Refactored messageThunk to streamline message creation and updates, ensuring consistency with the new message structure.
- Enhanced error handling and state management during message processing.

* refactor(message): update message handling with new types and utility functions

- Refactored message operations to utilize new message types and improved utility functions for content retrieval.
- Introduced helper functions for finding message blocks, enhancing the structure and readability of message processing.
- Updated various providers to align with the new message structure, ensuring consistent handling of message content and blocks.
- Enhanced error handling and state management during message processing, improving overall reliability.

* refactor(message): wip create XxxBlock components

- Refactored message handling to utilize new message types, enhancing the overall structure and readability.
- Introduced new message blocks including CitationBlock, CodeBlock, ErrorBlock, FileBlock, ImageBlock, ThinkingBlock, ToolBlock, and TranslationBlock.
- Updated existing components to align with the new message structure, ensuring consistent handling of message content and blocks.
- Improved utility functions for finding and processing message blocks, enhancing the reliability of message operations.

* refactor(message): enhance message operations with new selectors and blocks

- Updated useMessageOperations to integrate new message block types and selectors for improved state management.
- Introduced new selectors for fetching topic messages and loading states, enhancing the efficiency of message retrieval.
- Refactored message handling in Inputbar and MessageContent components to align with the new message structure.
- Added CitationBlock and improved rendering logic for message blocks, ensuring consistent display of message content.
- Enhanced error handling and logging for message operations, improving overall reliability.

* refactor(message): enhance message update logic with block instructions

- Updated the updateMessage reducer to handle block instructions, allowing for more flexible message updates.
- Improved the fetchAndProcessAssistantResponseImpl function to track the last added block, ensuring proper handling of streaming content.
- Refactored stream processing to accommodate new callback signatures and improve error handling.
- Removed unused console logs and cleaned up code for better readability and maintainability.

* merge origin/main

* refactor(message): update message handling and block structure

- Replaced `prepareTopicMessages` with `loadTopicMessagesThunk` for improved message loading logic.
- Updated `MessageTools` to accept `ToolBlock` instead of `Message`, enhancing type safety.
- Introduced `resetMessage` utility to streamline message object creation and reset mutable fields.
- Refactored `findCitationBlocks` and related types for consistency in message block handling.
- Enhanced error handling in message update logic to ensure robustness during database operations.

* refactor(message): update message and block handling for improved clarity and performance

- Refactored message handling by removing unnecessary cloning of messages in `MessageContent`.
- Updated `Inputbar` to import `Message` from the new message types module.
- Simplified block rendering logic in `MessageBlockRenderer` by removing redundant checks.
- Adjusted `StreamProcessingService` to use the updated import path for `GroundingMetadata`.
- Enhanced message status handling in `newMessage` to include 'streaming' status.
- Removed deprecated `createUserMessageThunk` to streamline thunk actions.
- Updated type definitions in `newMessageTypes` for better clarity and consistency.

* refactor(message): enhance message block structure and type handling

- Updated `Markdown` component to utilize `MainTextMessageBlock` for improved type safety.
- Refactored `MessageContent` to remove error handling logic and streamline rendering.
- Adjusted `MessageError` to accept `ErrorMessageBlock` and simplified error display logic.
- Enhanced `MessageTools` to work with `ToolMessageBlock`, improving clarity in tool response handling.
- Updated `MainTextBlock` to pass the correct props to `Markdown`, ensuring consistent rendering.
- Refactored utility functions to align with new message block types, enhancing overall code clarity and maintainability.

* refactor: update message types import paths and enhance message handling logic

- Changed import paths for message types from 'newMessageTypes' to 'newMessage' across multiple files.
- Refactored message handling functions to utilize the new message structure, ensuring compatibility with the updated types.
- Improved logic for finding and processing main text blocks in messages.
- Updated related components and hooks to reflect the new message structure, enhancing overall code maintainability.

* refactor: improve stream processing and message handling logic

- Updated the `createStreamProcessor` function to handle null and undefined chunks more robustly.
- Introduced a helper function `handleBlockTransition` to streamline state transitions between message blocks.
- Enhanced error handling in the `onComplete` function to specifically manage abort errors and create error blocks when necessary.
- Improved final block status updates to ensure accurate tracking of message processing states.

* refactor: use rehype-sanitize for html tags

# Conflicts:
#	src/renderer/src/pages/home/Markdown/Markdown.tsx

* refactor: merge rehype plugins

* refactor(ModelList): extract NameSpan component and adjust styling for better layout

* feat: update Z.ai app configuration with additional styling and increment store version to 97

# Conflicts:
#	src/renderer/src/store/index.ts

* feat(FeatureMenus, Footer): replace Ant Design icons with Lucide icons and enhance layout

- Updated icons in FeatureMenus from Ant Design to Lucide for improved visual consistency.
- Refactored Footer component to use Lucide icons and adjusted layout for better alignment and spacing.
- Enhanced styling of Tag components for a more cohesive design.

* lint: fix code format

* chore(version): 1.2.5

* feat(locales): add locale cleanup functionality to after-pack script

- Introduced a new `remove-locales.js` script to handle the removal of unnecessary locale files based on the platform.
- Integrated the locale cleanup process into the `after-pack.js` script to ensure locales are managed during packaging.

* feat: support escaping the comma character in the API key. (#5088)

feat: support escaping the comma character in the API key.

* feat(Citations): enhance CitationsList with title and info icon, and update styling

* Revert "feat: add chat message translate copy button (#4620)"

This reverts commit 8b462935b4.

# Conflicts:
#	src/renderer/src/hooks/useMessageOperations.ts
#	src/renderer/src/pages/home/Inputbar/Inputbar.tsx

* refactor: update message status handling and improve message creation logic

- Introduced `AssistantMessageStatus` to standardize message statuses across the application.
- Updated various components and services to utilize the new status enum, replacing string literals for better type safety.
- Refactored message creation and processing functions to align with the new status definitions.
- Adjusted filtering logic in `useMessageOperations` to reflect changes in message status handling.
- Cleaned up unused code and comments for improved readability.

* refactor: enhance citation handling and web search integration

- Updated CitationBlock and MainTextBlock components to improve citation processing logic.
- Refactored citation data handling to accommodate new web search sources and formats.
- Introduced new chunk types for better handling of streaming responses, including text and web search results.
- Enhanced the integration of web search results into message blocks, ensuring accurate citation references.
- Updated related types and interfaces to reflect changes in citation and web search structures.

* refactor: improve message handling and citation integration

- Added debug logging to various components for better traceability during message processing.
- Refactored CitationBlock and MainTextBlock to streamline citation handling and improve integration with web search results.
- Updated Inputbar to include debug logs for message sending and dispatching actions.
- Enhanced the message block rendering logic to decouple citation references from main text blocks.
- Improved the handling of citation data and ensured accurate formatting across different sources.

* feat: add regenerate assistant response functionality

- Introduced `regenerateAssistantResponseThunk` to allow regeneration of assistant messages.
- Updated `useMessageOperations` to include the new `regenerateAssistantMessage` function.
- Refactored `MessageMenubar` to utilize the new regeneration feature.
- Adjusted `MessageImage` and `ImageBlock` components to align with updated props.
- Cleaned up unused code and comments across various files for improved readability.

* fix: deepseek-reasoner does not support successive user or assistant messages in MCP scenario (#5112)

* fix: deepseek-reasoner does not support successive user or assistant messages in MCP scenario.

* fix: @ts-ignore

* refactor: remove google analytics

* feat: add PostHogProvider for analytics integration

- Introduced PostHogProvider to manage data collection based on user settings.
- Wrapped the main application in PostHogProvider to enable analytics when data collection is allowed.

* refactor(AxiosProxy): improve proxy handling and initialization logic

- Changed cacheAxios from undefined to null for better initialization.
- Updated proxy handling to use ProxyAgent, ensuring axios instance is recreated when the proxy changes.
- Simplified axios instance creation by directly using the current proxy agent.

* refactor: remove search enhanceMode

* fix: 知识库和网络搜索使用输出语言问题 (#5129)

* feat(proxy): use os-proxy-config to get system proxy info instead of resolveProxy (#5123)

* feat(proxy): integrate os-proxy-config for system proxy management

- Added os-proxy-config dependency to manage system proxy settings.
- Refactored setSystemProxy method to utilize getSystemProxy for improved proxy handling.

* fix lint error

* chore(version): 1.2.6

* fix: zipfile dependencies

* chore(release): update default release tag to v1.0.0 and install setuptools for Mac build

* disable auto update in portable exe

* chore(electron-builder): add StartupWMClass for CherryStudio in liunx desktop configuration (#5158)

chore(electron-builder): add StartupWMClass for CherryStudio in desktop configuration

* fix(MinApp): integrate dynamic background color for MinappPopupContainer (#5142)

* fix(models): 更新OpenRouter模型ID和名称,简化模型组分类 (#5172)

* fix: purify minapp user agent tag (#5173)

* fix: electron-builder 新增配置导致的无法构建的问题  (#5175)

fix: electron-builder 新增配置导致的无法构建的问题

当前 electron-builder 的版本为 "26.0.13",但在 v26 之后,StartupWMClass 等配置标签要在 desktop > entry 下,而不是直接在 desktop 下,否则会导致无法构建打包

* Revert "fix(minapps): remove AI Studio entry from default mini apps list" (#5177)

This reverts commit aed9c04c20.

* refactor(Markdown): remove rehype-sanitize and implement custom element filtering

- Removed rehype-sanitize dependency and its related configuration.
- Introduced ALLOWED_ELEMENTS and DISALLOWED_ELEMENTS for custom HTML element filtering.
- Updated rehypePlugins logic to conditionally apply plugins based on message content.
- Added encodeHTML utility function for HTML entity encoding.

# Conflicts:
#	src/renderer/src/pages/home/Markdown/Markdown.tsx

* refactor: mcp buttons and mcp settings

* refactor: add MessageTranslate.tsx & MessageCitations.tsx

* refactor: add MessageContent.main.tsx { getModelUniqId } from '@renderer/services/ModelService' import { Message, Model } from '@renderer/types' import { getBriefInfo } from '@renderer/utils' import { formatCitations, withMessageThought } from '@renderer/utils/formats' import { encodeHTML } from '@renderer/utils/markdown' import { Flex } from 'antd' import { clone } from 'lodash' import { Search } from 'lucide-react' import React, { Fragment, useMemo } from 'react' import { useTranslation } from 'react-i18next' import BarLoader from 'react-spinners/BarLoader' import styled, { css } from 'styled-components'

* refactor: enhance translation handling by integrating TranslationMessageBlock into Markdown and MessageTranslate components, and streamline TranslationBlock to utilize MessageTranslate for rendering.

* refactor: introduce citation processing optimization checklist and enhance citation handling

- Added a new refactoring checklist for optimizing citation processing logic.
- Implemented a lookup map for citations in MainTextBlock to improve performance.
- Updated Citation interface to include optional content field.
- Modified chunk types for web and knowledge search responses to improve type safety.
- Enhanced citation formatting to include content from web search results.

* refactor: update web search handling in GeminiProvider and OpenAIProvider

* refactor: update prompt handling and citation processing logic

* refactor: standardize chunk type usage across providers and improve image handling in MessageImage component

* refactor: update message operations and API service for improved message handling

- Translated comment in useMessageOperations.ts for clarity.
- Refactored ApiService to utilize findMainTextBlocks for knowledge base checks.
- Enhanced messageThunk with multi-model dispatch logic for better assistant response handling.

* refactor: optimize knowledge base ID handling in ApiService

- Removed unused store import and streamlined knowledge base ID extraction logic.
- Enhanced fetchExternalTool function to utilize mainTextBlocks for knowledge base checks, improving clarity and efficiency.

* feat: add message lifecycle documentation and enhance citation handling in CitationBlock component

* feat: add SearchingSpinner component and enhance message handling with ThinkingMessageBlock

- Introduced a new SearchingSpinner component for better user feedback during processing.
- Updated Markdown and MessageThought components to support ThinkingMessageBlock.
- Enhanced CitationBlock to display the SearchingSpinner when processing citations.
- Refactored message handling to include thinking time metrics across various components and services.

* refactor: enhance ApiService and message handling for improved search functionality

- Streamlined knowledge base ID extraction using flatMap for better performance.
- Added early checks for extractResults in search functions to prevent errors and improve logging.
- Updated chunk types to include SEARCH_IN_PROGRESS_UNION and SEARCH_COMPLETE_UNION for better state management during searches.
- Refactored search execution logic to handle different search scenarios more efficiently.

* refactor: streamline message update logic and enhance block handling

- Simplified message update dispatching by using the store's dispatch directly.
- Improved block transition handling by replacing direct calls with a dedicated function.
- Refactored conditional logic for updating blocks to ensure accurate state management.

* feat: enhance StreamProcessingService with tool call progress handling

- Added onToolCallInProgress callback to StreamProcessorCallbacks for handling tool call progress updates.
- Updated createStreamProcessor function to accept an empty object as default for callbacks.
- Implemented logic to process MCP_TOOL_IN_PROGRESS and MCP_TOOL_COMPLETE chunks in message handling.
- Improved type definitions for MCPToolInProgressChunk to include responses.

* refactor: enhance AI providers with abort controller integration for improved request handling

- Added abort controller functionality to AnthropicProvider, GeminiProvider, and OpenAIProvider to manage request cancellations effectively.
- Updated API calls to include signal for aborting requests, ensuring better control over ongoing operations.
- Improved cleanup logic to handle abort scenarios gracefully.

* feat: add TODO for pause capability in WebSearchService

- Added a TODO comment in WebSearchService to implement pause functionality for network searches, enhancing future service capabilities.

* fix(WebdavBackupManager): update modal confirmation to use window.modal and center content

* refactor: improve message operations and API service for enhanced functionality

- Updated useMessageOperations to dispatch resendMessageThunk with topic ID instead of object for better clarity.
- Refactored ApiService to streamline knowledge base ID extraction using flatMap and added early checks for improved error handling.
- Enhanced message handling in messageThunk by integrating topic queue management and simplifying error handling logic.
- Cleaned up commented-out code for better readability and maintainability.

* fix: enhance message resending functionality and integrate abort signal for web searches

- Updated resendMessageThunk to reset assistant messages without deleting other messages, improving user experience.
- Integrated abort signal handling in WebSearchService and fetch functions to manage request cancellations effectively.
- Refactored fetchWebContents and fetchWebContent to accept an optional abort signal for better control over fetch operations.
- Added resetAssistantMessage utility to streamline the resetting of assistant messages for regeneration.

* fix: enhance chunk handling in AI providers for improved message processing

- Added onChunk calls in AnthropicProvider and GeminiProvider to ensure complete text messages are processed correctly.
- Updated OpenAIProvider to handle finish reasons more accurately, improving message completion handling.
- Removed outdated TODO comment in WebSearchService for better code clarity.

* feat: enhance SearchingSpinner component and add processing text localization

- Updated SearchingSpinner to accept a text prop for dynamic message rendering.
- Added "processing" text localization in English, Japanese, Russian, Chinese (Simplified and Traditional) for improved user experience.
- Integrated updated SearchingSpinner in CitationBlock and MainTextBlock components to display appropriate loading messages.

* fix(WebdavBackupManager): update modal confirmation to use window.modal and center content

fix sse no headers

add eventSourceInit

refactor: switch from @vitejs/plugin-react to @vitejs/plugin-react-swc for improved performance

perf: improve streaming performance (#4986)

feat(ProviderSettings): move model provider to the top when toggled

When the model provider is toggled (OFF to ON), it is moved to the top of the provider setting for convenience. The change is minimal.

fix(settings): handle undefined content limit in BasicSettings component (#5252)

feat: update os-proxy-config to 1.1.2 and delete the patch (#5255)

updte os-proxy-config to 1.1.2 and delete the patch

feat: 添加嵌入维度配置 (#3947)

fix(ci): Remove a deleted step which make the nightly build pipeline fail

These lines were deleted in `release.yml` in commit 75f98608.

build: fix nightly build error

build: remove sentry integration

refactor(GeminiProvider): streamline abort signal handling and improve stream processing #5276

需要处理 GeminiProvider processStream 函数代码

https://github.com/CherryHQ/cherry-studio/pull/5276/files

Update @modelcontextprotocol/sdk to v1.10.2 (#5266)

- Removed MCPStreamableHttpClient.ts as it is now provided by the SDK.
- Adjusted imports in MCPService.ts to use the SDK's implementation.
- Updated yarn.lock to reflect the new SDK version.

feat: add cherry-text-logo.svg and remove npm.svg; update MCPSettings and NpxSearch components

- Introduced a new cherry-text-logo.svg file for branding.
- Removed the deprecated npm.svg file.
- Refactored MCPSettings and NpxSearch components to enhance functionality and UI, including state management and layout adjustments.
- Updated translations in multiple locales to include new types for MCP servers.

style(settings): update border-radius to use CSS variable for consistency

feat(mcp): mcp setting add service description page

chore: update dependencies and clean up code

- Reintroduced @mozilla/readability, @shikijs/markdown-it, and @xyflow/react to package.json.
- Updated shiki version to 3.2.2 in both package.json and yarn.lock.
- Removed trailing whitespace in IpcChannel.ts and index.ts for code cleanliness.
- Added outline style to .ant-tabs-tab-btn in ant.scss for improved UI consistency.

feat(WindowService): add maximize functionality and disable electron-window-state maxmize (#5292)

* feat(WindowService): add maximize functionality and clean up window close logic

- Introduced a new `maximize` option in the window state configuration.
- Added `setupMaximize` method to handle window maximization based on the launch state.
- Removed redundant logic from the window close event handler for clarity.

* add code

* update code

Create pull_request_template.md

feat: enhance MinAppIcon component with sidebar prop

- Added optional sidebar prop to MinAppIcon for conditional styling.
- Updated Sidebar component to pass sidebar prop to MinAppIcon for consistent appearance in sidebar context.

refactor(MessageAttachments): move styled component definition inside the component for better encapsulation

feat: support portable config dir (#5039)

* feat: support portable config dir

* fix: remove redundant mkdir

feat(image): support grok-2-image image and gpt-4o-image (#4767)

* feat(image): support grok image

* feat: add gpt-4o-image

* feat: 添加 gpt-image-1 到生成图像模型列表

* refactor(GeminiProvider): remove redundant onChunk call in processStream function

* refactor(OpenAIProvider): update image generation response format and improve prompt handling

* feat(AiProvider): implement thought processing for incremental reasoning and update MessageContent component

* refactor(useMessageOperations): update topic handling in resendUserMessageWithEditThunk and improve MessageMenubar component structure

* refactor(Messages): streamline message rendering and update type imports for better clarity

* refactor(Messages):  improve message block rendering logic

* refactor(Messages): enhance thinking time calculations

* refactor(Messages): enhance message usage estimation logic

* refactor(OpenAIProvider): get image generation usage

* refactor(Messages): optimize citation handling and improve main text block rendering

* refactor(Messages): improve clipboard copy functionality and streamline API response handling

* refactor(Messages): update message state management and improve selector usage for topic messages

* refactor(upgrades): update citation data structure in upgradeToV7 function and remove unused comments in messageBlock.ts

* feat(OpenAIProvider): enhance link conversion for web search results and refactor related logic in ApiService and messageBlock

* feat(translation): implement streaming translation updates and refactor translation handling

- Added a new `getTranslationUpdater` function to manage streaming translation updates.
- Refactored `translate` methods across various providers to support incremental updates.
- Updated `fetchTranslate` to accept content directly instead of a message object.
- Removed the `MessageContent.main.tsx` file as part of the cleanup.
- Enhanced error handling and logging during translation processes.

* feat(message-operations): add appendAssistantResponse functionality and enhance message operations

- Introduced `appendAssistantResponse` to allow appending new assistant responses using a specified model.
- Updated `useMessageOperations` hook to include the new function and improved documentation for existing methods.
- Refactored `MessageMenubar` to utilize the new `appendAssistantResponse` function for message handling.
- Enhanced error handling and logging in message-related thunks for better debugging and state management.

* feat(message): refactor message handling and enhance file block integration

- Updated `message_blocks` schema to include `file.id` for better file association.
- Refactored `FilesPage` to improve file deletion logic, ensuring related message blocks are updated or deleted accordingly.
- Enhanced `Inputbar` and `MessageAttachments` components to utilize new message structure and improve file handling.
- Removed deprecated `MessageCitations` component to streamline message management.
- Updated various components to use the new `MessageInputBaseParams` type for consistency across message operations.

* refactor(tests): clean up and organize formats test suite

- Removed commented-out code and unnecessary imports to enhance readability.
- Organized test cases for `escapeDollarNumber`, `escapeBrackets`, `extractTitle`, and `removeSvgEmptyLines` for better structure.
- Maintained existing test functionality while improving overall code clarity.

* refactor(tests): comment out unused tests in formats test suite

- Commented out the `withGeminiGrounding` test suite to improve clarity and focus on active tests.
- Removed unnecessary imports and organized the test structure for better readability.
- Maintained existing functionality while enhancing overall code organization.

* refactor(components): remove role prop from Markdown component in MessageThought and MessageTranslate

- Removed the `role` prop from the `Markdown` component in both `MessageThought` and `MessageTranslate` for consistency and to simplify the component usage.
- Updated import statements in `export.ts` to use type imports for `Message` and `Topic`, enhancing type safety.
- Commented out unused mock dependencies in the formats test suite to improve clarity and focus on active tests.

* refactor(messages): update message selection and handling for improved consistency

- Replaced legacy message selectors with new message handling methods in `ChatFlowHistory`, `ChatNavigation`, and `MessageAnchorLine` components.
- Utilized `getMainTextContent` utility for consistent message content retrieval across components.
- Updated state management in `messageThunk` to set the current topic ID correctly.
- Enhanced markdown export functions to utilize new message structure for better content handling.

* fix(databases): correct syntax in message_blocks schema for proper key separation

- Updated the `message_blocks` schema to include a comma separator between `messageId` and `file.id` for accurate primary key definition.
- Ensured consistency in database schema definitions to prevent potential issues during data retrieval.

* refactor(messages): enhance loading state handling and improve message block rendering

- Introduced a new `LoadingBlock` component to manage loading states for different message block types using `BeatLoader`.
- Updated `MessageContent` to display loading indicators when messages are pending.
- Cleaned up commented-out code and improved the structure of message block rendering logic.
- Adjusted `throttledBlockUpdate` and `throttledBlockDbUpdate` to prevent unnecessary updates when block statuses are already successful.
- Added error handling improvements in `fetchExternalTool` and `fetchAndProcessAssistantResponseImpl` for better robustness.

* refactor(messages): improve loading state handling in message block rendering

- Integrated `MessageBlockStatus` for better management of message block statuses.
- Added `LoadingBlock` component to handle loading states during message processing.
- Updated `fetchAndProcessAssistantResponseImpl` to set the status of tool blocks to `PROCESSING` for improved state tracking.
- Cleaned up commented-out code to enhance readability and maintainability of the rendering logic.

* refactor(messages): streamline message handling for clearing user messages

* feat(message-operations): add createTopicBranch functionality to clone messages to a new topic

- Implemented `createTopicBranch` in `useMessageOperations` to facilitate cloning messages from a source topic to a new topic.
- Introduced `cloneMessagesToNewTopicThunk` for handling the cloning process, including unique ID generation and database updates.
- Updated `Messages` component to utilize the new cloning functionality, ensuring proper topic management and error handling.
- Cleaned up unused imports and commented-out code in `MessageMenubar` for improved readability.

* fix(Messages): remove unused message operations in Messages component

- Removed `createNewContext` from the destructured message operations in the `Messages` component to clean up unused functionality.
- Added `getUserMessage` import to enhance message handling capabilities.

* 优化格式化和测试:重构消息处理和格式化功能

- 在 `formats.ts` 中移除未使用的 `withGeminiGrounding` 函数,并更新相关类型导入。
- 在测试文件中添加了对 `withGenerateImage` 和 `addImageFileToContents` 函数的测试,确保它们正确处理消息块和图像元数据。
- 通过创建辅助函数来简化测试数据的生成,提高测试的可读性和一致性。
- 清理了测试中的注释代码,提升了代码的整洁性。

* 优化消息处理和类型导入:更新消息相关组件以使用新消息类型

- 在多个组件中更新消息导入,确保使用 `newMessage` 类型以提高类型安全性。
- 移除未使用的 `CodeBlock` 组件,简化代码结构。
- 在 `SearchResults` 组件中引入 `getMainTextContent` 函数,以改进消息内容处理。
- 清理 `Suggestions` 组件中的冗余代码,提升可读性。
- 更新 `Message` 组件以支持新的消息处理逻辑,确保与助手消息状态的兼容性。

* feat(PlaceholderBlock): introduce PlaceholderBlock and Spinner component for loading states

- Added a new `Spinner` component to provide a visual loading indicator using `BarLoader` and `Search` icon.
- Replaced the deprecated `SearchingSpinner` with the new `Spinner` component in `CitationBlock` and `PlaceholderBlock` for improved consistency in loading states.
- Removed the unused `LoadingBlock` component to streamline the codebase.
- Updated `MessageContent` to enhance rendering logic by removing commented-out code and improving readability.

* feat:upgradeToV7 del catch

* fix:mini/message lint error

* feat(CitationsList): refactor citations rendering with Collapse component

- Replaced the direct rendering of citations with a Collapse component for better UI organization.
- Utilized useMemo for optimized rendering of citation items.
- Updated styling in CitationsContainer for improved layout.
- Enhanced PlaceholderBlock to use BeatLoader for loading state indication.

* fix(messageThunk): improve logging and cloneMessagesToNewTopicThunk functionality

- Updated debug logging to provide clearer information about topic retrieval and cloning process.
- Enhanced the cloning logic to correctly map askId for assistant messages, ensuring proper linkage in the new topic.
- Added checks to ensure file modifications only occur if the file exists, improving robustness.
- Cleaned up comments and improved readability in the cloneMessagesToNewTopicThunk function.

* fix(GeminiProvider): enhance image generation logic and response configuration (#5447)

* feat(GeminiProvider): enhance image generation logic and response configuration

* refactor(GeminiProvider): improve image generation logic readability

* feat(Message): enhance message handling and introduce MessageContent component

- Updated createAssistantMessage to remove unnecessary 'blocks' property from overrides.
- Refactored MessageItem to manage MainTextMessageBlock state and improve message processing logic.
- Added new MessageContent component for rendering message content with mentions and Markdown support.

---------

Co-authored-by: lizhixuan <zhixuan.li@banosuperapp.com>
Co-authored-by: one <wangan.cs@gmail.com>
Co-authored-by: ousugo <dkzyxh@gmail.com>
Co-authored-by: kangfenmao <kangfenmao@qq.com>
Co-authored-by: chenxi <16267732+chenxi-null@users.noreply.github.com>
Co-authored-by: suyao <sy20010504@gmail.com>
Co-authored-by: beyondkmp <beyondkmp@gmail.com>
Co-authored-by: Chen Tao <70054568+eeee0717@users.noreply.github.com>
Co-authored-by: Asurada <43401755+ousugo@users.noreply.github.com>
Co-authored-by: Roland <shlroland1995@gmail.com>
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
Co-authored-by: tchigher <34847046+tchigher@users.noreply.github.com>
2025-04-29 14:12:07 +08:00
Teo
8423fb5610 refactor(TopicsTab): Use onContextMenu instead of onMouseEnter (#5459)
refactor(TopicsTab): change mouse event from onMouseEnter to onContextMenu for setting target topic
2025-04-29 10:14:19 +08:00
kangfenmao
350c2735e4 refactor(Sidebar, McpSettingsNavbar): update icons and improve layout for better UI consistency 2025-04-29 09:35:02 +08:00
kangfenmao
f844a7e024 chore(version): 1.2.10 2025-04-29 09:12:13 +08:00
SuYao
74e7c6a327 fix(GeminiProvider): enhance image generation logic and response configuration (#5447)
* feat(GeminiProvider): enhance image generation logic and response configuration

* refactor(GeminiProvider): improve image generation logic readability
2025-04-29 09:02:39 +08:00
Akey Zhang
8c05b4f067 refactor: use the existing hook 2025-04-29 09:00:08 +08:00
kangfenmao
449d74040f refactor(McpSettings): streamline form layout and enhance advanced settings toggle functionality 2025-04-29 08:58:42 +08:00
xixiaxixi
712bd11274 feat: 允许功能键(F1-F19)可以被设置为快捷键而不需要修饰键 2025-04-29 08:01:21 +08:00
kangfenmao
cc89f5330a fix: update API key URL in provider configuration 2025-04-29 07:58:11 +08:00
SuYao
db7a7e2c2b fix(McpOAuthClientProvider): update redirect URL (#5449)
fix(McpOAuthClientProvider): update redirect URL to use 127.0.0.1 instead of localhost
2025-04-29 01:40:08 +08:00
Camol
d64ae18bfc chore: Update runtime version in VSCode launch configuration (#5434)
Co-authored-by: kanweiwei <kanweiwei@nutstore.net>
2025-04-28 18:48:00 +08:00
Akey Zhang
9f2ac4aa81 style: optimize mcp arg name display layout 2025-04-28 18:11:33 +08:00
Teo
e227fbf821 feat(TopicsTab): 简单方式优化话题列表切换卡顿问题 (#5436)
* feat(TopicsTab): refactor topic menu item handling and improve state management

* refactor(TopicsTab): enhance state management with useDeferredValue for target topic
2025-04-28 17:55:42 +08:00
beyondkmp
6595dc9a1e feat: update theme handling in ConfigManager and WindowService (#5433)
- Change default theme return value to 'auto' in ConfigManager.
- Adjust WindowService to set nativeTheme based on the updated theme configuration.
2025-04-28 16:52:56 +08:00
kangfenmao
d16235d916 feat: Add model notes functionality to provider context menu 2025-04-28 09:38:28 +08:00
africa1207
f5a7258229 feat: 增加模型备注功能 (#5392)
* feat: 增加模型备注功能

* fix: 移除未使用变量

---------

Co-authored-by: liutao <>
2025-04-28 09:11:55 +08:00
karl
dfd957434c fix(MCPService): Tool call failure caused by incorrect tool parameters 2025-04-28 09:05:49 +08:00
fullex
2e0d315ce4 refactor: simplify window.api definition (#5412)
* refactor(preload): remove unused index.d.ts file and export WindowApiType from index.ts

* fix: type def errors in original index.ts file
2025-04-28 09:02:14 +08:00
beyondkmp
47bde9eb36 fix(AppUpdater): update condition for checking available updates (#5417) 2025-04-27 22:22:31 +08:00
Chen Tao
506af6cfb9 revert: generate image button (#5414)
* revert: generate image button

* chore
2025-04-27 21:19:27 +08:00
beyondkmp
4e07faf520 fix: update electron-updater patch and refine content-type regex (#5416)
fix: update electron-updater patch and refine content-type regex in multipleRangeDownloader.js
2025-04-27 21:17:33 +08:00
木子不是木子狸
381377c0eb #4288 Added a "Copy" button next to each search result. (#5389)
Co-authored-by: eeee0717 <chentao020717Work@outlook.com>
Co-authored-by: Chen Tao <70054568+eeee0717@users.noreply.github.com>
2025-04-27 19:41:30 +08:00
Chen Tao
24f59047a5 chore: add grok image alias (#5408) 2025-04-27 17:07:36 +08:00
SuYao
e1a97ccd50 fix(OpenAIProvider): refine system message handling for specific model IDs (#5397)
* fix(OpenAIProvider): refine system message handling for specific model IDs

* refactor(OpenAIProvider): introduce OPENAI_NO_SUPPORT_DEV_ROLE_MODELS for model ID checks
2025-04-27 15:10:01 +08:00
LiuVaayne
f2929d44d8 feat(MCP): Add MCP resources dropdown in settings navbar (#5394)
* Add MCP resources dropdown in settings navbar

* Refactor MCP settings navbar with inline styles

- Replace styled-components with inline styles
- Update resource URLs and capitalize "Servers" in title
- Add noopener,noreferrer to external links
- Simplify component structure with direct Menu.Item usage
2025-04-27 14:32:27 +08:00
beyondkmp
bb02dca7e9 feat(MCPService): add method to find PowerShell executable path (#5393)
- Implemented `findPowerShellExecutable` to determine the correct PowerShell executable path on Windows.
- Updated `getSystemPath` to utilize the new method for improved path retrieval.
2025-04-27 14:25:37 +08:00
LiuVaayne
54cb4d7ac1 Feat/mcp enhancement (#5386) 2025-04-27 12:15:00 +08:00
karl
b76a609b97 fix(MCPSettings): fix mcp setting state error,fix mcp setting save searchKey lose (#5384) 2025-04-27 11:42:19 +08:00
AO2233
9cea0166e6 fix: Fix the image support for the GitHub Copilot models (#5379) 2025-04-27 01:55:19 +08:00
beyondkmp
e5e04c8132 feat: Enhance theme management with auto mode support (#5374)
- Updated IPC channel to handle 'auto' theme mode, allowing dynamic theme changes based on system preferences.
- Modified theme setting functions in preload scripts to accommodate the new 'auto' option.
- Adjusted Sidebar component to display an icon for 'auto' theme mode.
- Refactored ThemeProvider to manage theme state more effectively and listen for theme changes across windows.
2025-04-26 22:16:12 +08:00
Chen Tao
6b113c19a3 feat: Enable image generation in assistant based on model selection (#5364)
* feat: Enable image generation in assistant based on model selection

* chore: remove generate image button
2025-04-26 22:15:48 +08:00
kangfenmao
bcbb3f294e chore(version): 1.2.9 2025-04-26 15:45:48 +08:00
Song
e17765f1bf fix: more robust portable data dir setup logic (#5347) 2025-04-26 15:29:54 +08:00
kangfenmao
3064b7a3e3 feat: Update MCP server configurations with environment variables
- Adjusted MCP server definitions to include environment variable placeholders for MEMORY_FILE_PATH, BRAVE_API_KEY, and DIFY_KEY.
- Modified styling in SyncServersPopup to reduce gap between elements and removed unnecessary margin.
2025-04-26 11:29:51 +08:00
emaryn
da72a5706f feat: Implement deep linking for AppImage on Linux (#5360)
Signed-off-by: emaryn <197520219+emaryn@users.noreply.github.com>
Co-authored-by: emaryn <emaryn@users.noreply.github.com>
Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-04-26 11:19:53 +08:00
Chen Tao
261eeb097a feat: add dify knowledge mcp (#5290)
feat: add dify mcp
2025-04-26 11:19:22 +08:00
LiuVaayne
bf17e71445 feat: Add MCP server installation via URL protocol (#5351)
* Add MCP server installation via URL protocol

Implement handler for cherrystudio://mcp/install URLs to add MCP servers
from encoded configuration data. Supports multiple server configuration
formats and adds a new IPC channel for server addition.

* feat: Enhance MCP protocol handling and navigation

- Implemented navigation to the '/settings/mcp' page using executeJavaScript in the MCP protocol URL handler.
- Updated NavigationService to expose the navigate function globally for easier access in the application.
- Added NavigateFunction type to the global environment for improved type safety in navigation operations.

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>
2025-04-26 11:09:58 +08:00
SuYao
0744e42be9 feat(models): add new search models (#5349)
* feat(models): add new Perplexity search models and update Gemini search models

* fix(models): update Perplexity search model names for consistency
2025-04-25 22:57:39 +08:00
LiuVaayne
2833c377fa feat(McpServersList): add ModelScope sync functionality (#5250)
* feat: add sync servers functionality and UI components for MCP settings

* Rename 'Discover Models' to 'Discover MCP Servers'

Replace Radio button group with Select dropdown for provider selection
in the sync servers popup UI and update related styling
Rename 'Discover Models' to 'Discover MCP Servers'

* Add error messages for MCP server sync

Improve error handling in modelscopeSyncUtils with new localization
strings for unauthorized access and no available servers. Update error
message handling in all language files and use nanoid for unique server
naming.
2025-04-25 22:11:23 +08:00
eeee0717
ad5769f6b7 feat: add document count in create popup 2025-04-25 21:40:13 +08:00
木子不是木子狸
a6a4a32159 fix #5127
Signed-off-by: 木子不是木子狸 <richardllleeeee@gmail.com>
2025-04-25 21:23:58 +08:00
beyondkmp
a112b143e7 refactor(auto-update): should triggle download when checking for update manually (#5262)
refactor(auto-update): streamline update check logic and enhance error handling

- Moved update check logic from IPC handler to AppUpdater class for better organization.
- Improved error handling during update checks to ensure graceful failure.
- Removed redundant conditions in the AboutSettings component for cleaner rendering.
2025-04-25 21:21:44 +08:00
自由的世界人
a7ee3cbd02 Update issue-management.yml (#5314)
* Update issue-management.yml

* feat: enhance issue management with new templates and automated stale issue handling

* fix: update issue templates for clarity and consistency in descriptions

* fix: improve clarity in issue templates and add area labels for better categorization

* fix: reorder question template title for improved clarity and update issue-checker to remove redundant label handling

* fix: add skip and remove labels for kind/bug, kind/enhancement, and kind/question in issue-checker

* fix: remove redundant handling for needs-more-info labels in issue-checker
2025-04-25 21:18:17 +08:00
Miter
32f160444b 获取searxng搜索结果url的内容作为大模型回答的参考,而不是使用searxng搜索结果的摘要内容 2025-04-25 18:13:43 +08:00
beyondkmp
a546c265ee fix: update electron-updater to patch to support window arm upgrade (#5337) 2025-04-25 18:13:07 +08:00
kangfenmao
ea89a37b1d feat: update licensing terms and UI components
- Revised the licensing agreement to introduce a User-Segmented Dual Licensing model, detailing conditions for individual and organizational use.
- Enhanced UI components to display a specific empty state image in various popups and pages.
- Adjusted styles in MiniAppIconsManager and MiniAppSettings for improved layout and appearance.
- Updated default web search provider to 'local-bing' and modified font size in ProviderOAuth description for better readability.
- Changed window style setting from 'transparent' to 'opaque' for a more consistent user experience.
2025-04-25 17:50:33 +08:00
kangfenmao
c288b4a8d0 refactor: reorder llm providers 2025-04-25 17:16:40 +08:00
kangfenmao
e8384db91a refactor: remove ollama settings 2025-04-25 17:16:27 +08:00
kangfenmao
36c87451d9 feat: enhance OAuth provider settings with new functionality
- Added a new ProviderOAuth component to manage OAuth authentication and billing for providers.
- Updated existing components to integrate the new ProviderOAuth functionality.
- Enhanced internationalization support for OAuth-related texts across multiple languages.
- Introduced new utility functions for handling provider billing.
2025-04-25 16:55:36 +08:00
fullex
eaa37fe674 feat: open popup url in external browser (#4446)
* feat: open popup url in external browser

* fix: allow google auth popup internal

* feat: add functionality(including settings) to open links in external browser for webviews

* fix: set useragent globally

* fix: remove setUserAgent in webview

* fix: set Chrome version to newest
2025-04-25 09:45:54 +08:00
kangfenmao
308ad9f68f feat: update release notes with new features and fixes
- Added support for grok-2-image and gpt-4o-image.
- Enabled portable version of Windows to use the data directory for storage.
- Revamped MCP interface with new description display.
- Optimized Mermaid rendering logic.
- Added option to disable public rendering.
- Fixed OpenAI type rendering errors.
2025-04-25 09:44:13 +08:00
shiquda
62b6584d65 feat: support preview of MCP call results (#5236) 2025-04-25 08:59:41 +08:00
Chen Tao
5a44f6aca8 fix: image abort error and message render error (#5303) 2025-04-24 21:55:47 +08:00
dcai
4c6a904929 fix: Resolve unsafe map call in MessageContent.tsx (#5311) 2025-04-24 21:39:35 +08:00
kangfenmao
be4ef2990f chore(version): 1.2.8 2025-04-24 18:16:00 +08:00
kangfenmao
2807e71f1a docs: contributor guide 2025-04-24 18:14:21 +08:00
SuYao
a5f8ac8587 feat: add English contributor guide and update issue templates (#5300)
- Introduced a new English version of the contributor guide (CONTRIBUTING_EN.md) to enhance accessibility for non-Chinese speakers.
- Updated issue templates to use more specific labels (kind/bug, kind/enhancement, kind/question) for better categorization.
- Added a testing section in the developer guide to clarify testing procedures.
2025-04-24 18:04:45 +08:00
Chen Tao
4bd50251ff fix(openai): 修复OpenAI类型渲染错误 (#5263) 2025-04-24 17:51:13 +08:00
Chen Tao
f0d60052c4 feat(image): support grok-2-image image and gpt-4o-image (#4767)
* feat(image): support grok image

* feat: add gpt-4o-image

* feat: 添加 gpt-image-1 到生成图像模型列表
2025-04-24 17:26:15 +08:00
Song
84f4b565f3 feat: support portable config dir (#5039)
* feat: support portable config dir

* fix: remove redundant mkdir
2025-04-24 17:23:56 +08:00
kanweiwei
ebdacdde3e refactor(MessageAttachments): move styled component definition inside the component for better encapsulation 2025-04-24 17:06:35 +08:00
kangfenmao
aeb66195a0 feat: enhance MinAppIcon component with sidebar prop
- Added optional sidebar prop to MinAppIcon for conditional styling.
- Updated Sidebar component to pass sidebar prop to MinAppIcon for consistent appearance in sidebar context.
2025-04-24 17:03:12 +08:00
HuiZhang
53ef8b0f32 Create pull_request_template.md 2025-04-24 16:56:43 +08:00
beyondkmp
794c23f296 feat(WindowService): add maximize functionality and disable electron-window-state maxmize (#5292)
* feat(WindowService): add maximize functionality and clean up window close logic

- Introduced a new `maximize` option in the window state configuration.
- Added `setupMaximize` method to handle window maximization based on the launch state.
- Removed redundant logic from the window close event handler for clarity.

* add code

* update code
2025-04-24 16:55:51 +08:00
kangfenmao
62440cbfa1 chore: update dependencies and clean up code
- Reintroduced @mozilla/readability, @shikijs/markdown-it, and @xyflow/react to package.json.
- Updated shiki version to 3.2.2 in both package.json and yarn.lock.
- Removed trailing whitespace in IpcChannel.ts and index.ts for code cleanliness.
- Added outline style to .ant-tabs-tab-btn in ant.scss for improved UI consistency.
2025-04-24 16:39:09 +08:00
寇佳龙
39b723f143 feat(mcp): mcp setting add service description page 2025-04-24 15:55:43 +08:00
kangfenmao
bdc75f2f4e style(settings): update border-radius to use CSS variable for consistency 2025-04-24 15:18:53 +08:00
kangfenmao
eb3f136997 feat: add cherry-text-logo.svg and remove npm.svg; update MCPSettings and NpxSearch components
- Introduced a new cherry-text-logo.svg file for branding.
- Removed the deprecated npm.svg file.
- Refactored MCPSettings and NpxSearch components to enhance functionality and UI, including state management and layout adjustments.
- Updated translations in multiple locales to include new types for MCP servers.
2025-04-24 15:17:20 +08:00
LiuVaayne
6ba5768650 Update @modelcontextprotocol/sdk to v1.10.2 (#5266)
- Removed MCPStreamableHttpClient.ts as it is now provided by the SDK.
- Adjusted imports in MCPService.ts to use the SDK's implementation.
- Updated yarn.lock to reflect the new SDK version.
2025-04-24 15:05:42 +08:00
SuYao
1f588d242e refactor(GeminiProvider): streamline abort signal handling and improve stream processing (#5276) 2025-04-24 13:52:29 +08:00
kangfenmao
25b1e309ed build: remove sentry integration 2025-04-24 11:48:53 +08:00
kangfenmao
7a7b24fe2f build: fix nightly build error 2025-04-24 10:59:52 +08:00
Lucas
0686b2d813 fix(ci): Remove a deleted step which make the nightly build pipeline fail
These lines were deleted in `release.yml` in commit 75f98608.
2025-04-24 10:29:05 +08:00
Chen Tao
4a027892b9 feat: 添加嵌入维度配置 (#3947) 2025-04-24 10:18:23 +08:00
beyondkmp
be323b6304 feat: update os-proxy-config to 1.1.2 and delete the patch (#5255)
updte os-proxy-config to 1.1.2 and delete the patch
2025-04-24 09:24:08 +08:00
Asurada
2f5cfc0162 fix(settings): handle undefined content limit in BasicSettings component (#5252) 2025-04-24 02:58:36 +08:00
kabu1204
4a00eb57ad feat(ProviderSettings): move model provider to the top when toggled
When the model provider is toggled (OFF to ON), it is moved to the top of the provider setting for convenience. The change is minimal.
2025-04-24 00:04:30 +08:00
one
784b02e62e perf: improve streaming performance (#4986) 2025-04-24 00:02:04 +08:00
suyao
4c2c026f6d refactor: switch from @vitejs/plugin-react to @vitejs/plugin-react-swc for improved performance 2025-04-23 23:56:02 +08:00
Rocky LIU Yan
07b2c6f169 fix sse no headers
add eventSourceInit
2025-04-23 22:35:10 +08:00
kangfenmao
aafd04090e fix(WebdavBackupManager): update modal confirmation to use window.modal and center content 2025-04-23 22:33:18 +08:00
Teo
af10ae3f37 style: fix animation (#5243)
* style: fix animation

* fix(animation): correct animation name typos in multiple components
2025-04-23 22:31:26 +08:00
kangfenmao
3df4680c7b feat(mermaid): update Mermaid integration and improve rendering logic
- Upgraded Mermaid script to version 11.6.0.
- Refactored rendering logic to use a debounced function for improved performance.
- Added event listener for 'mermaid-loaded' to trigger rendering.
- Enhanced error handling during Mermaid chart rendering in both main and popup components.
- Removed unnecessary initialization calls and streamlined the use of theme settings.
2025-04-23 21:12:54 +08:00
kangfenmao
5d04ef2508 chore(release): increase Node.js memory limit in release workflow
- Added NODE_OPTIONS to set max-old-space-size to 8192 in the release workflow for Mac, Windows, and Linux builds.
2025-04-23 18:59:53 +08:00
beyondkmp
7fb6eb1949 feat(auto-update): improve auto-update toggle functionality (#5215)
* feat(auto-update): improve auto-update toggle functionality

- Added setAutoUpdate method in AppUpdater to control auto-update behavior.
- Updated IPC handler to set auto-update status based on user preference.
- Modified AboutSettings component to conditionally display update options based on auto-check setting.

* update autoupdate position
2025-04-23 16:13:55 +08:00
fullex
4fe99cddce fix: should give more time to init autosync (#5219) 2025-04-23 16:01:02 +08:00
beyondkmp
a84763def6 fix(proxy): update os-proxy-config patch to correct proxy URL handling (#5222)
* fix(proxy): update os-proxy-config patch to correct proxy URL handling

- Modified the os-proxy-config dependency in package.json to apply a patch.
- The patch updates the logic in getSystemProxy to correctly handle HTTP and SOCKS proxy settings.

* use http instead of https for https proxy
2025-04-23 15:53:25 +08:00
PilgrimLyieu
8125fac309 feat: add support for 'none' option in math engine settings (#5122) 2025-04-23 14:53:19 +08:00
kangfenmao
6c6b2f0b9e fix(sentry): update Sentry configuration and initialization logic
- Changed the organization in the Sentry Vite plugin configuration.
- Modified Sentry initialization in the main process to always check data collection settings.
- Simplified Sentry initialization in the renderer process by removing the packaged check.
2025-04-23 10:59:17 +08:00
kangfenmao
314be9b198 feat: add sentry integration 2025-04-22 22:05:56 +08:00
kangfenmao
409e0096d8 chore(version): 1.2.7 2025-04-22 20:39:14 +08:00
Chen Tao
a1ffabae41 fix(knowledge): fix citation bug and optimize extract logic (#5195)
* fix(knowledge): change search ui and fix search bug

* fix: knowledge citation

* feat: optimize extract logic
2025-04-22 20:17:11 +08:00
kangfenmao
0fa10627bc feat(BackupManager): replace AdmZip with archiver for improved backup compression and add extract-zip for unzipping functionality
- Updated BackupManager to use archiver for creating ZIP files, enabling better performance and support for large files.
- Integrated extract-zip for unzipping backup files, enhancing the backup restoration process.
- Adjusted progress reporting during backup and restore operations for better user feedback.
- Updated package.json and yarn.lock to include archiver and extract-zip dependencies.
2025-04-22 18:03:16 +08:00
eeee0717
80618b2331 fix(knowledge): change search ui and fix search bug 2025-04-22 16:10:03 +08:00
kangfenmao
bf8baedfcf feat(Messages): add MessageCitations and MessageTranslate components for citation and translation display
- Introduced MessageCitations component to handle and display citations from messages.
- Added MessageTranslate component to show translated content with loading state.
- Updated MessageContent to integrate new components and streamline citation formatting.
- Refactored citation handling logic in formats utility for improved performance and clarity.
- Enhanced MessageImage component to manage image download and clipboard copy functionality.

refactor(MCP): optimize MCP server handling in Inputbar and MCPToolsButton

wip

refactor(MCPSettings): streamline MCP server management and enhance UI components

- Removed unused imports and optimized state management for selected MCP servers.
- Introduced McpServersList component to encapsulate server listing and management logic.
- Updated routing to accommodate the new component structure.
- Adjusted styles for better layout and user experience in MCP settings.
2025-04-22 15:40:39 +08:00
SuYao
98f2c8a0b6 Revert "fix(minapps): remove AI Studio entry from default mini apps list" (#5177)
This reverts commit aed9c04c20.
2025-04-22 15:40:33 +08:00
tchigher
3887cf2a6f fix: electron-builder 新增配置导致的无法构建的问题 (#5175)
fix: electron-builder 新增配置导致的无法构建的问题

当前 electron-builder 的版本为 "26.0.13",但在 v26 之后,StartupWMClass 等配置标签要在 desktop > entry 下,而不是直接在 desktop 下,否则会导致无法构建打包
2025-04-22 15:40:33 +08:00
fullex
eb89c6ea21 fix: purify minapp user agent tag (#5173) 2025-04-22 15:40:33 +08:00
Roland
fd09edc2b9 fix(models): 更新OpenRouter模型ID和名称,简化模型组分类 (#5172) 2025-04-22 15:40:33 +08:00
kangfenmao
55a9447a7b refactor(Markdown): remove rehype-sanitize and implement custom element filtering
- Removed rehype-sanitize dependency and its related configuration.
- Introduced ALLOWED_ELEMENTS and DISALLOWED_ELEMENTS for custom HTML element filtering.
- Updated rehypePlugins logic to conditionally apply plugins based on message content.
- Added encodeHTML utility function for HTML entity encoding.
2025-04-22 11:00:27 +08:00
SuYao
c576aa5cb4 fix(MinApp): integrate dynamic background color for MinappPopupContainer (#5142) 2025-04-21 23:44:15 +08:00
Asurada
ca553a2454 chore(electron-builder): add StartupWMClass for CherryStudio in liunx desktop configuration (#5158)
chore(electron-builder): add StartupWMClass for CherryStudio in desktop configuration
2025-04-21 23:40:20 +08:00
beyondkmp
ef9c8fd037 disable auto update in portable exe 2025-04-21 20:14:07 +08:00
kangfenmao
234a5e085f chore(release): update default release tag to v1.0.0 and install setuptools for Mac build 2025-04-21 20:02:37 +08:00
kangfenmao
cb22b80ead fix: zipfile dependencies 2025-04-21 19:25:09 +08:00
kangfenmao
a6d9ad6716 chore(version): 1.2.6 2025-04-21 18:52:01 +08:00
beyondkmp
185900ada6 feat(proxy): use os-proxy-config to get system proxy info instead of resolveProxy (#5123)
* feat(proxy): integrate os-proxy-config for system proxy management

- Added os-proxy-config dependency to manage system proxy settings.
- Refactored setSystemProxy method to utilize getSystemProxy for improved proxy handling.

* fix lint error
2025-04-21 12:45:01 +08:00
Chen Tao
288ebe5222 fix: 知识库和网络搜索使用输出语言问题 (#5129) 2025-04-21 11:42:48 +08:00
kangfenmao
6e91066e5d refactor: remove search enhanceMode 2025-04-21 11:35:06 +08:00
beyondkmp
49a7b2dc8b refactor(AxiosProxy): improve proxy handling and initialization logic
- Changed cacheAxios from undefined to null for better initialization.
- Updated proxy handling to use ProxyAgent, ensuring axios instance is recreated when the proxy changes.
- Simplified axios instance creation by directly using the current proxy agent.
2025-04-21 11:26:13 +08:00
kangfenmao
4789ba3e8f feat: add PostHogProvider for analytics integration
- Introduced PostHogProvider to manage data collection based on user settings.
- Wrapped the main application in PostHogProvider to enable analytics when data collection is allowed.
2025-04-21 11:18:11 +08:00
kangfenmao
cc18f0f0c3 refactor: remove google analytics 2025-04-21 11:01:22 +08:00
chenxi
9bb96c212d fix: deepseek-reasoner does not support successive user or assistant messages in MCP scenario (#5112)
* fix: deepseek-reasoner does not support successive user or assistant messages in MCP scenario.

* fix: @ts-ignore
2025-04-21 09:04:47 +08:00
one
81eab1179b test: add vitest (#5085)
* test: migrate to vitest

* test: update vitest config

* test: updates tests for utils

* ci: fix test command

* test: add tests for format.ts

* test: add snapshots

* test: update snapshots

* test: add tests for linkConverter

* test: add tests for error.ts

* test: update test coverage script name

* test: update tests for prompt.ts

* test: re-group utils, add tests

* test: add tests for export.ts

* test: add tests for sort.ts
2025-04-20 22:44:01 +08:00
beyondkmp
24c9a8e8f1 refactor(locales): fix locales errors (#5080) 2025-04-20 21:27:49 +08:00
Pleasurecruise
c4d0f8e950 fix: language error 2025-04-20 19:03:06 +08:00
kangfenmao
3bdf0be4ad update(README): replace outdated screenshots in English, Japanese, and Chinese documentation 2025-04-20 15:50:58 +08:00
kangfenmao
9e4ebf7c6f Revert "refactor(ipc): remove Windows ARM update check from IPC handler and AboutSettings component"
This reverts commit d1c2bbed1b.
2025-04-20 11:32:59 +08:00
fullex
2408566d34 fix(AssistantSettings): temporarily disable transitionName to resolve modal closing issues in production 2025-04-20 11:20:15 +08:00
lossercode
cf61ae927c fix(MCPService):修复MCP server 请求头不生效 (#5072) 2025-04-20 11:18:34 +08:00
chenxi
60680936d3 feat: support escaping the comma character in the API key. (#5088)
feat: support escaping the comma character in the API key.
2025-04-20 10:25:28 +08:00
357 changed files with 19178 additions and 61033 deletions

View File

@@ -1,7 +1,7 @@
name: 🐛 错误报告 (中文)
description: 创建一个报告以帮助我们改进
title: '[错误]: '
labels: ['bug']
labels: ['kind/bug']
body:
- type: markdown
attributes:

View File

@@ -1,7 +1,7 @@
name: 💡 功能建议 (中文)
description: 为项目提出新的想法
title: '[功能]: '
labels: ['enhancement']
labels: ['kind/enhancement']
body:
- type: markdown
attributes:

View File

@@ -1,7 +1,7 @@
name: 讨论 & 提问 (中文)
name: 提问 & 讨论 (中文)
description: 寻求帮助、讨论问题、提出疑问等...
title: '[讨论]: '
labels: ['question']
labels: ['kind/question']
body:
- type: markdown
attributes:

76
.github/ISSUE_TEMPLATE/#3_others.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
name: 🤔 其他问题 (中文)
description: 提交不属于错误报告或功能需求的问题
title: '[其他]: '
body:
- type: markdown
attributes:
value: |
感谢您花时间提出问题!
在提交此问题之前,请确保您已经了解了[常见问题](https://docs.cherry-ai.com/question-contact/questions)和[知识科普](https://docs.cherry-ai.com/question-contact/knowledge)
- type: checkboxes
id: checklist
attributes:
label: 提交前检查
description: |
在提交 Issue 前请确保您已经完成了以下所有步骤
options:
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
required: true
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的问题。
required: true
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是"一个问题"、"求助"等。
required: true
- label: 我的问题不属于错误报告或功能需求类别。
required: true
- type: dropdown
id: platform
attributes:
label: 平台
description: 您正在使用哪个平台?
options:
- Windows
- macOS
- Linux
validations:
required: true
- type: input
id: version
attributes:
label: 版本
description: 您正在运行的 Cherry Studio 版本是什么?
placeholder: 例如 v1.0.0
validations:
required: true
- type: textarea
id: question
attributes:
label: 问题描述
description: 请详细描述您的问题或疑问
placeholder: 我想了解有关...的更多信息
validations:
required: true
- type: textarea
id: context
attributes:
label: 相关背景
description: 请提供与您的问题相关的任何背景信息或上下文
placeholder: 我尝试实现...时遇到了疑问
validations:
required: true
- type: textarea
id: attempts
attributes:
label: 您已尝试的方法
description: 请描述您为解决问题已经尝试过的方法(如果有)
- type: textarea
id: additional
attributes:
label: 附加信息
description: 任何能让我们对您的问题有更多了解的信息,包括截图或相关链接

View File

@@ -1,7 +1,7 @@
name: 🐛 Bug Report (English)
description: Create a report to help us improve
title: '[Bug]: '
labels: ['bug']
labels: ['kind/bug']
body:
- type: markdown
attributes:

View File

@@ -1,7 +1,7 @@
name: 💡 Feature Request (English)
description: Suggest an idea for this project
title: '[Feature]: '
labels: ['enhancement']
labels: ['kind/enhancement']
body:
- type: markdown
attributes:

View File

@@ -1,7 +1,7 @@
name: Discussion & Questions
name: Questions & Discussion
description: Seeking help, discussing issues, asking questions, etc...
title: '[Discussion]: '
labels: ['question']
labels: ['kind/question']
body:
- type: markdown
attributes:

76
.github/ISSUE_TEMPLATE/3_others.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
name: 🤔 Other Questions (English)
description: Submit questions that don't fit into bug reports or feature requests
title: '[Other]: '
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to ask a question!
Before submitting this issue, please make sure you've reviewed the [FAQ](https://docs.cherry-ai.com/question-contact/questions) and [Knowledge Base](https://docs.cherry-ai.com/question-contact/knowledge)
- type: checkboxes
id: checklist
attributes:
label: Pre-submission Checklist
description: |
Please ensure you've completed all the steps below before submitting your issue
options:
- label: I understand that Issues are for feedback and problem-solving, not for complaints, and I will provide as much information as possible to help resolve the issue.
required: true
- label: I have checked the pinned Issues and searched through existing [open Issues](https://github.com/CherryHQ/cherry-studio/issues) and [closed Issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20) and didn't find similar questions.
required: true
- label: I have written a short and clear title that helps developers quickly understand the nature of my question, rather than vague titles like "A question" or "Help needed".
required: true
- label: My question doesn't fall under bug reports or feature requests categories.
required: true
- type: dropdown
id: platform
attributes:
label: Platform
description: Which platform are you using?
options:
- Windows
- macOS
- Linux
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of Cherry Studio are you running?
placeholder: e.g., v1.0.0
validations:
required: true
- type: textarea
id: question
attributes:
label: Question Description
description: Please describe your question or inquiry in detail
placeholder: I would like to know more about...
validations:
required: true
- type: textarea
id: context
attributes:
label: Relevant Context
description: Please provide any background information or context related to your question
placeholder: I encountered this question while trying to implement...
validations:
required: true
- type: textarea
id: attempts
attributes:
label: Attempted Solutions
description: Please describe any methods you've already tried to resolve your question (if applicable)
- type: textarea
id: additional
attributes:
label: Additional Information
description: Any other information that could help us better understand your question, including screenshots or relevant links

252
.github/issue-checker.yml vendored Normal file
View File

@@ -0,0 +1,252 @@
default-mode:
add:
remove: [pull_request_target, issues]
labels:
# <!-- [Ss]kip `LABEL` --> 跳过一个 label
# <!-- [Rr]emove `LABEL` --> 去掉一个 label
# skips and removes
- name: skip all
content:
regexes: "[Ss]kip (?:[Aa]ll |)[Ll]abels?"
- name: remove all
content:
regexes: "[Rr]emove (?:[Aa]ll |)[Ll]abels?"
- name: skip kind/bug
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)"
- name: remove kind/bug
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)"
- name: skip kind/enhancement
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)"
- name: remove kind/enhancement
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)"
- name: skip kind/question
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/question(?:`|)"
- name: remove kind/question
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/question(?:`|)"
- name: skip area/Connectivity
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)"
- name: remove area/Connectivity
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)"
- name: skip area/UI/UX
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)"
- name: remove area/UI/UX
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)"
- name: skip kind/documentation
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)"
- name: remove kind/documentation
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)"
- name: skip client:linux
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:linux(?:`|)"
- name: remove client:linux
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:linux(?:`|)"
- name: skip client:mac
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:mac(?:`|)"
- name: remove client:mac
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:mac(?:`|)"
- name: skip client:win
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:win(?:`|)"
- name: remove client:win
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:win(?:`|)"
- name: skip sig/Assistant
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)"
- name: remove sig/Assistant
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)"
- name: skip sig/Data
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)"
- name: remove sig/Data
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)"
- name: skip sig/MCP
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)"
- name: remove sig/MCP
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)"
- name: skip sig/RAG
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)"
- name: remove sig/RAG
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)"
- name: skip lgtm
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)lgtm(?:`|)"
- name: remove lgtm
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)lgtm(?:`|)"
- name: skip License
content:
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)License(?:`|)"
- name: remove License
content:
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)License(?:`|)"
# `Dev Team`
- name: Dev Team
mode:
add: [pull_request_target, issues]
author_association:
- COLLABORATOR
# Area labels
- name: area/Connectivity
content: area/Connectivity
regexes: "代理|[Pp]roxy"
skip-if:
- skip all
- skip area/Connectivity
remove-if:
- remove all
- remove area/Connectivity
- name: area/UI/UX
content: area/UI/UX
regexes: "界面|[Uu][Ii]|重叠|按钮|图标|组件|渲染|菜单|栏目|头像|主题|样式|[Cc][Ss][Ss]"
skip-if:
- skip all
- skip area/UI/UX
remove-if:
- remove all
- remove area/UI/UX
# Kind labels
- name: kind/documentation
content: kind/documentation
regexes: "文档|教程|[Dd]oc(s|umentation)|[Rr]eadme"
skip-if:
- skip all
- skip kind/documentation
remove-if:
- remove all
- remove kind/documentation
# Client labels
- name: client:linux
content: client:linux
regexes: "(?:[Ll]inux|[Uu]buntu|[Dd]ebian)"
skip-if:
- skip all
- skip client:linux
remove-if:
- remove all
- remove client:linux
- name: client:mac
content: client:mac
regexes: "(?:[Mm]ac|[Mm]acOS|[Oo]SX)"
skip-if:
- skip all
- skip client:mac
remove-if:
- remove all
- remove client:mac
- name: client:win
content: client:win
regexes: "(?:[Ww]in|[Ww]indows)"
skip-if:
- skip all
- skip client:win
remove-if:
- remove all
- remove client:win
# SIG labels
- name: sig/Assistant
content: sig/Assistant
regexes: "快捷助手|[Aa]ssistant"
skip-if:
- skip all
- skip sig/Assistant
remove-if:
- remove all
- remove sig/Assistant
- name: sig/Data
content: sig/Data
regexes: "[Ww]ebdav|坚果云|备份|同步|数据|Obsidian|Notion|Joplin|思源"
skip-if:
- skip all
- skip sig/Data
remove-if:
- remove all
- remove sig/Data
- name: sig/MCP
content: sig/MCP
regexes: "[Mm][Cc][Pp]"
skip-if:
- skip all
- skip sig/MCP
remove-if:
- remove all
- remove sig/MCP
- name: sig/RAG
content: sig/RAG
regexes: "知识库|[Rr][Aa][Gg]"
skip-if:
- skip all
- skip sig/RAG
remove-if:
- remove all
- remove sig/RAG
# Other labels
- name: lgtm
content: lgtm
regexes: "(?:[Ll][Gg][Tt][Mm]|[Ll]ooks [Gg]ood [Tt]o [Mm]e)"
skip-if:
- skip all
- skip lgtm
remove-if:
- remove all
- remove lgtm
- name: License
content: License
regexes: "(?:[Ll]icense|[Cc]opyright|[Mm][Ii][Tt]|[Aa]pache)"
skip-if:
- skip all
- skip License
remove-if:
- remove all
- remove License

54
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,54 @@
<!-- Template from https://github.com/kubevirt/kubevirt/blob/main/.github/PULL_REQUEST_TEMPLATE.md?-->
<!-- Thanks for sending a pull request! Here are some tips for you:
1. Consider creating this PR as draft: https://github.com/CherryHQ/cherry-studio/blob/main/CONTRIBUTING.md
-->
### What this PR does
Before this PR:
After this PR:
<!-- (optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: -->
Fixes #
### Why we need it and why it was done in this way
The following tradeoffs were made:
The following alternatives were considered:
Links to places where the discussion took place: <!-- optional: slack, other GH issue, mailinglist, ... -->
### Breaking changes
<!-- optional -->
If this PR introduces breaking changes, please describe the changes and the impact on users.
### Special notes for your reviewer
<!-- optional -->
### Checklist
This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR.
Approvers are expected to review this list.
- [ ] PR: The PR description is expressive enough and will help future contributors
- [ ] Code: [Write code that humans can understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans) and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
- [ ] Refactor: You have [left the code cleaner than you found it (Boy Scout Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
- [ ] Upgrade: Impact of this change on upgrade flows was considered and addressed if required
- [ ] Documentation: A [user-guide update](https://docs.cherry-ai.com) was considered and is present (link) or not required. You want a user-guide update if it's a user facing feature.
### Release note
<!-- Write your release note:
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
2. If no release note is required, just write "NONE".
-->
```release-note
```

25
.github/workflows/issue-checker.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: "Issue Checker"
on:
issues:
types: [opened, edited]
pull_request_target:
types: [opened, edited]
issue_comment:
types: [created, edited]
permissions:
contents: read
issues: write
pull-requests: write
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: MaaAssistantArknights/issue-checker@v1.14
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/issue-checker.yml
not-before: 2022-08-05T00:00:00Z
include-title: 1

View File

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

View File

@@ -7,9 +7,41 @@ on:
permissions:
contents: write
actions: write # Required for deleting artifacts
jobs:
cleanup-artifacts:
runs-on: ubuntu-latest
steps:
- name: Delete old artifacts
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
# Calculate the date 14 days ago
cutoff_date=$(date -d "14 days ago" +%Y-%m-%d)
# List and delete artifacts older than cutoff date
gh api repos/$REPO/actions/artifacts --paginate | \
jq -r '.artifacts[] | select(.name | startswith("cherry-studio-nightly-")) | select(.created_at < "'$cutoff_date'") | .id' | \
while read artifact_id; do
echo "Deleting artifact $artifact_id"
gh api repos/$REPO/actions/artifacts/$artifact_id -X DELETE
done
check-repository:
runs-on: ubuntu-latest
outputs:
should_run: ${{ github.repository == 'CherryHQ/cherry-studio' }}
steps:
- name: Check if running in main repository
run: |
echo "Running in repository: ${{ github.repository }}"
echo "Should run: ${{ github.repository == 'CherryHQ/cherry-studio' }}"
nightly-build:
needs: check-repository
if: needs.check-repository.outputs.should_run == 'true'
runs-on: ${{ matrix.os }}
strategy:
@@ -26,6 +58,11 @@ jobs:
with:
node-version: 20
- name: macos-latest dependencies fix
if: matrix.os == 'macos-latest'
run: |
brew install python-setuptools
- name: Install corepack
run: corepack enable && corepack prepare yarn@4.6.0 --activate
@@ -59,6 +96,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
NODE_OPTIONS: --max-old-space-size=8192
- name: Build Mac
if: matrix.os == 'macos-latest'
@@ -73,19 +111,17 @@ jobs:
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: --max-old-space-size=8192
- name: Build Windows
if: matrix.os == 'windows-latest'
run: |
yarn build:npm windows
yarn build:win:x64
yarn build:win:arm64
yarn build:win
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
- name: Replace spaces in filenames
run: node scripts/replace-spaces.js
NODE_OPTIONS: --max-old-space-size=8192
- name: Rename artifacts with nightly format
shell: bash
@@ -96,39 +132,24 @@ jobs:
# Windows artifacts - based on actual file naming pattern
if [ "${{ matrix.os }}" == "windows-latest" ]; then
# Setup installer
find dist -name "*setup.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-setup.exe \;
# Portable exe
find dist -name "*portable.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-portable.exe \;
find dist -name "*-x64-setup.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64-setup.exe \;
find dist -name "*-arm64-setup.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64-setup.exe \;
# Rename blockmap files to match the new exe names
if [ -f "dist/*setup.exe.blockmap" ]; then
cp dist/*setup.exe.blockmap renamed-artifacts/cherry-studio-nightly-${DATE}-setup.exe.blockmap || true
fi
# Portable exe
find dist -name "*-x64-portable.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64-portable.exe \;
find dist -name "*-arm64-portable.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64-portable.exe \;
fi
# macOS artifacts
if [ "${{ matrix.os }}" == "macos-latest" ]; then
# 处理arm64架构文件
find dist -name "*-arm64.dmg" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.dmg \;
find dist -name "*-arm64.dmg.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.dmg.blockmap \;
find dist -name "*-arm64.zip" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.zip \;
find dist -name "*-arm64.zip.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.zip.blockmap \;
# 处理x64架构文件
find dist -name "*-x64.dmg" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.dmg \;
find dist -name "*-x64.dmg.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.dmg.blockmap \;
find dist -name "*-x64.zip" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.zip \;
find dist -name "*-x64.zip.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.zip.blockmap \;
fi
# Linux artifacts
if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
find dist -name "*.AppImage" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.AppImage \;
find dist -name "*.snap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.snap \;
find dist -name "*.deb" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.deb \;
find dist -name "*.rpm" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.rpm \;
find dist -name "*.tar.gz" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.tar.gz \;
find dist -name "*-x86_64.AppImage" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x86_64.AppImage \;
find dist -name "*-arm64.AppImage" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.AppImage \;
fi
# Copy update files

View File

@@ -6,7 +6,7 @@ on:
tag:
description: 'Release tag (e.g. v1.0.0)'
required: true
default: 'v0.9.18'
default: 'v1.0.0'
push:
tags:
- v*.*.*
@@ -42,6 +42,11 @@ jobs:
with:
node-version: 20
- name: macos-latest dependencies fix
if: matrix.os == 'macos-latest'
run: |
brew install python-setuptools
- name: Install corepack
run: corepack enable && corepack prepare yarn@4.6.0 --activate
@@ -71,10 +76,12 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
NODE_OPTIONS: --max-old-space-size=8192
- name: Build Mac
if: matrix.os == 'macos-latest'
run: |
sudo -H pip install setuptools
yarn build:npm mac
yarn build:mac
env:
@@ -85,6 +92,7 @@ jobs:
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: --max-old-space-size=8192
- name: Build Windows
if: matrix.os == 'windows-latest'
@@ -94,6 +102,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
NODE_OPTIONS: --max-old-space-size=8192
- name: Release
uses: ncipollo/release-action@v1

5
.gitignore vendored
View File

@@ -46,3 +46,8 @@ local
.aider*
.cursorrules
.cursor/rules
# test
coverage
.vitest-cache
vitest.config.*.timestamp-*

1
.vscode/launch.json vendored
View File

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,92 @@
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,51 @@
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,8 +1,8 @@
diff --git a/core.js b/core.js
index ebb071d31cd5a14792b62814df072c5971e83300..31e1062d4a7f2422ffec79cf96a35dbb69fe89cb 100644
index 862d66101f441fb4f47dfc8cff5e2d39e1f5a11e..6464bebbf696c39d35f0368f061ea4236225c162 100644
--- a/core.js
+++ b/core.js
@@ -157,7 +157,7 @@ class APIClient {
@@ -159,7 +159,7 @@ class APIClient {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': this.getUserAgent(),
@@ -12,10 +12,10 @@ index ebb071d31cd5a14792b62814df072c5971e83300..31e1062d4a7f2422ffec79cf96a35dbb
};
}
diff --git a/core.mjs b/core.mjs
index 9c1a0264dcd73a85de1cf81df4efab9ce9ee2ab7..33f9f1f237f2eb2667a05dae1a7e3dc916f6bfff 100644
index 05dbc6cfde51589a2b100d4e4b5b3c1a33b32b89..789fbb4985eb952a0349b779fa83b1a068af6e7e 100644
--- a/core.mjs
+++ b/core.mjs
@@ -150,7 +150,7 @@ export class APIClient {
@@ -152,7 +152,7 @@ export class APIClient {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': this.getUserAgent(),

View File

@@ -1,45 +1,73 @@
# Cherry Studio 贡献者指南
[中文](./docs/CONTRIBUTING.zh.md) | [English](./CONTRIBUTING.md)
欢迎来到 Cherry Studio 的贡献者社区!我们致力于将 Cherry Studio 打造成一个长期提供价值的项目,并希望邀请更多的开发者加入我们的行列。无论您是经验丰富的开发者还是刚刚起步的初学者,您的贡献都将帮助我们更好地服务用户,提升软件质量。
# Cherry Studio Contributor Guide
## 如何贡献
Welcome to the Cherry Studio contributor community! We are committed to making Cherry Studio a project that provides long-term value and hope to invite more developers to join us. Whether you are an experienced developer or a beginner just starting out, your contributions will help us better serve users and improve software quality.
以下是您可以参与的几种方式:
## How to Contribute
1. **贡献代码**:帮助我们开发新功能或优化现有代码。请确保您的代码符合我们的编码标准,并通过所有测试。
Here are several ways you can participate:
2. **修复 BUG**:如果您发现了 BUG欢迎提交修复方案。请在提交前确认问题已被解决并附上相关测试。
1. **Contribute Code**: Help us develop new features or optimize existing code. Please ensure your code adheres to our coding standards and passes all tests.
3. **维护 Issue**:协助我们管理 GitHub 上的 issue帮助标记、分类和解决问题。
2. **Fix Bugs**: If you find a bug, you are welcome to submit a fix. Please confirm the issue is resolved before submitting and include relevant tests.
4. **产品设计**:参与产品设计讨论,帮助我们改进用户体验和界面设计。
3. **Maintain Issues**: Help us manage issues on GitHub by assisting with tagging, classifying, and resolving problems.
5. **编写文档**帮助我们完善用户手册、API 文档和开发者指南。
4. **Product Design**: Participate in product design discussions to help us improve user experience and interface design.
6. **社区维护**:参与社区讨论,帮助解答用户问题,促进社区活跃。
5. **Write Documentation**: Help us improve the user manual, API documentation, and developer guides.
7. **推广使用**:通过博客、社交媒体等渠道推广 Cherry Studio吸引更多用户和开发者。
6. **Community Maintenance**: Participate in community discussions, help answer user questions, and promote community activity.
## 开始贡献
7. **Promote Usage**: Promote Cherry Studio through blogs, social media, and other channels to attract more users and developers.
1. **Fork 仓库**:在 GitHub 上 fork 我们的仓库,并将其克隆到本地。
## Before You Start
2. **创建分支**:为您要进行的更改创建一个新的分支。
Please make sure you have read the [Code of Conduct](CODE_OF_CONDUCT.md) and the [LICENSE](LICENSE).
3. **提交更改**:在本地进行更改并提交。请确保您的提交信息清晰明了。
## Getting Started
4. **发起 Pull Request**:将您的更改推送到 GitHub,并发起 Pull Request。请描述您的更改内容和原因。
To help you get familiar with the codebase, we recommend tackling issues tagged with one or more of the following labels: [good-first-issue](https://github.com/CherryHQ/cherry-studio/labels/good%20first%20issue), [help-wanted](https://github.com/CherryHQ/cherry-studio/labels/help%20wanted), or [kind/bug](https://github.com/CherryHQ/cherry-studio/labels/kind%2Fbug). Any help is welcome.
### 其他建议
### Testing
- **联系开发者**:在提交 PR 之前,您可以先和开发者进行联系,共同探讨或者获取帮助。
- **成为核心开发者**:如果您能够稳定为项目贡献,恭喜您可以成为项目核心开发者,获取到项目成员身份。
Features without tests are considered non-existent. To ensure code is truly effective, relevant processes should be covered by unit tests and functional tests. Therefore, when considering contributions, please also consider testability. All tests can be run locally without dependency on CI. Please refer to the "Testing" section in the [Developer Guide](docs/dev.md).
## 联系我们
### Automated Testing for Pull Requests
如果您有任何问题或建议,欢迎通过以下方式联系我们:
Automated tests are triggered on pull requests (PRs) opened by members of the Cherry Studio organization, except for draft PRs. PRs opened by new contributors will initially be marked with the `needs-ok-to-test` label and will not be automatically tested. Once a Cherry Studio organization member adds `/ok-to-test` to the PR, the test pipeline will be created.
- 微信kangfenmao
### Consider Opening Your Pull Request as a Draft
Not all pull requests are ready for review when created. This might be because the author wants to start a discussion, they are not entirely sure if the changes are heading in the right direction, or the changes are not yet complete. Please consider creating these PRs as [draft pull requests](https://github.blog/2019-02-14-introducing-draft-pull-requests/). Draft PRs are skipped by CI, thus saving CI resources. This also means reviewers will not be automatically assigned, and the community will understand that this PR is not yet ready for review.
Reviewers will be assigned after you mark the draft pull request as ready for review.
### Contributor Compliance with Project Terms
We require every contributor to certify that they have the right to legally contribute to our project. Contributors express this by consciously signing their commits, thereby indicating their compliance with the [LICENSE](LICENSE).
A signed commit is one where the commit message includes the following:
You can generate a signed commit using the following command [git commit --signoff](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---signoff):
```
git commit --signoff -m "Your commit message"
```
### Getting Code Reviewed/Merged
Maintainers are here to help you implement your use case within a reasonable timeframe. They will do their best to review your code and provide constructive feedback promptly. However, if you get stuck during the review process or feel your Pull Request is not receiving the attention it deserves, please contact us via comments in the Issue or through the [Community](README.md#-community).
### Other Suggestions
- **Contact Developers**: Before submitting a PR, you can contact the developers first to discuss or get help.
- **Become a Core Developer**: If you contribute to the project consistently, congratulations, you can become a core developer and gain project membership status. Please check our [Membership Guide](https://github.com/CherryHQ/community/blob/main/docs/membership.en.md).
## Contact Us
If you have any questions or suggestions, feel free to contact us through the following ways:
- WeChat: kangfenmao
- [GitHub Issues](https://github.com/CherryHQ/cherry-studio/issues)
感谢您的支持和贡献!我们期待与您一起将 Cherry Studio 打造成更好的产品。
Thank you for your support and contributions! We look forward to working with you to make Cherry Studio a better product.

View File

@@ -23,14 +23,12 @@ https://docs.cherry-ai.com
# 🌠 Screenshot
![](https://github.com/user-attachments/assets/28585d83-4bf0-4714-b561-8c7bf57cc600)
![](https://github.com/user-attachments/assets/8576863a-f632-4776-bc12-657eeced9da3)
![](https://github.com/user-attachments/assets/790790d7-b462-48dd-bde1-91c1697a4648)
![](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)
# 🌟 Key Features
![](https://github.com/user-attachments/assets/7b4f2f78-5cbe-4be8-9aec-f98d8405a505)
1. **Diverse LLM Provider Support**:
- ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more

View File

@@ -1,123 +0,0 @@
/**
* 内置的ASR服务器模块
* 这个文件可以直接在Electron中运行不需要外部依赖
*/
// 使用Electron内置的Node.js模块
const http = require('http')
const path = require('path')
const fs = require('fs')
// 输出环境信息
console.log('ASR Server (Embedded) starting...')
console.log('Node.js version:', process.version)
console.log('Current directory:', __dirname)
console.log('Current working directory:', process.cwd())
console.log('Command line arguments:', process.argv)
// 创建HTTP服务器
const server = http.createServer((req, res) => {
try {
if (req.url === '/' || req.url === '/index.html') {
// 尝试多个可能的路径
const possiblePaths = [
// 当前目录
path.join(__dirname, 'index.html'),
// 上级目录
path.join(__dirname, '..', 'index.html'),
// 应用根目录
path.join(process.cwd(), 'index.html')
]
console.log('Possible index.html paths:', possiblePaths)
// 查找第一个存在的文件
let indexPath = null
for (const p of possiblePaths) {
try {
if (fs.existsSync(p)) {
indexPath = p
console.log(`Found index.html at: ${p}`)
break
}
} catch (e) {
console.error(`Error checking existence of ${p}:`, e)
}
}
if (indexPath) {
// 读取文件内容并发送
fs.readFile(indexPath, (err, data) => {
if (err) {
console.error('Error reading index.html:', err)
res.writeHead(500)
res.end('Error reading index.html')
return
}
res.writeHead(200, { 'Content-Type': 'text/html' })
res.end(data)
})
} else {
// 如果找不到文件返回一个简单的HTML页面
console.error('Could not find index.html, serving fallback page')
res.writeHead(200, { 'Content-Type': 'text/html' })
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>ASR Server</title>
<style>
body { font-family: sans-serif; padding: 2em; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>ASR Server is running</h1>
<p>This is a fallback page because the index.html file could not be found.</p>
<p>Server is running at: http://localhost:34515</p>
<p>Current directory: ${__dirname}</p>
<p>Working directory: ${process.cwd()}</p>
</body>
</html>
`)
}
} else {
// 处理其他请求
res.writeHead(404)
res.end('Not found')
}
} catch (error) {
console.error('Error handling request:', error)
res.writeHead(500)
res.end('Server error')
}
})
// 添加进程错误处理
process.on('uncaughtException', (error) => {
console.error('[Server] Uncaught exception:', error)
// 不立即退出,给日志输出的时间
setTimeout(() => process.exit(1), 1000)
})
process.on('unhandledRejection', (reason, promise) => {
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
})
// 尝试启动服务器
try {
const port = 34515
server.listen(port, () => {
console.log(`[Server] Server running at http://localhost:${port}`)
})
// 处理服务器错误
server.on('error', (error) => {
console.error(`[Server] Failed to start server:`, error)
process.exit(1) // Exit if server fails to start
})
} catch (error) {
console.error('[Server] Critical error starting server:', error)
process.exit(1)
}

View File

@@ -1,425 +0,0 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cherry Studio ASR</title>
<style>
body {
font-family: sans-serif;
padding: 1em;
}
#status {
margin-top: 1em;
font-style: italic;
color: #555;
}
#result {
margin-top: 0.5em;
border: 1px solid #ccc;
padding: 0.5em;
min-height: 50px;
background: #f9f9f9;
}
</style>
</head>
<body>
<h1>浏览器语音识别中继页面</h1>
<p>这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。</p>
<div id="status">正在连接到服务器...</div>
<div id="result"></div>
<script>
const statusDiv = document.getElementById('status');
const resultDiv = document.getElementById('result');
// 尝试连接到WebSocket服务器
let ws;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
const reconnectInterval = 2000; // 2秒
function connectWebSocket() {
try {
ws = new WebSocket('ws://localhost:34515');
ws.onopen = () => {
reconnectAttempts = 0;
updateStatus('已连接到服务器,等待指令...');
ws.send(JSON.stringify({ type: 'identify', role: 'browser' }));
};
ws.onmessage = handleMessage;
ws.onerror = (error) => {
console.error('[Browser Page] WebSocket Error:', error);
updateStatus('WebSocket 连接错误!请检查服务器是否运行。');
};
ws.onclose = () => {
console.log('[Browser Page] WebSocket Connection Closed');
updateStatus('与服务器断开连接。尝试重新连接...');
stopRecognition();
// 尝试重新连接
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
updateStatus(`与服务器断开连接。尝试重新连接 (${reconnectAttempts}/${maxReconnectAttempts})...`);
setTimeout(connectWebSocket, reconnectInterval);
} else {
updateStatus('无法连接到服务器。请刷新页面或重启应用。');
}
};
} catch (error) {
console.error('[Browser Page] Error creating WebSocket:', error);
updateStatus('创建WebSocket连接时出错。请刷新页面或重启应用。');
}
}
// 初始连接
connectWebSocket();
let recognition = null;
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
function updateStatus(message) {
console.log(`[Browser Page Status] ${message}`);
statusDiv.textContent = message;
}
function handleMessage(event) {
let data;
try {
data = JSON.parse(event.data);
console.log('[Browser Page] Received command:', data);
} catch (e) {
console.error('[Browser Page] Received non-JSON message:', event.data);
return;
}
if (data.type === 'start') {
startRecognition();
} else if (data.type === 'stop') {
stopRecognition();
} else if (data.type === 'reset') {
// 强制重置语音识别
forceResetRecognition();
} else {
console.warn('[Browser Page] Received unknown command type:', data.type);
}
};
function setupRecognition() {
if (!SpeechRecognition) {
updateStatus('错误:此浏览器不支持 Web Speech API。');
return false;
}
if (recognition && recognition.recognizing) {
console.log('[Browser Page] Recognition already active.');
return true;
}
recognition = new SpeechRecognition();
recognition.lang = 'zh-CN';
recognition.continuous = true;
recognition.interimResults = true;
// 增加以下设置提高语音识别的可靠性
recognition.maxAlternatives = 3; // 返回多个可能的识别结果
// 设置较短的语音识别时间,使用户能更快地看到结果
// 注意:这个属性不是标准的,可能不是所有浏览器都支持
try {
// @ts-ignore
recognition.audioStart = 0.1; // 尝试设置较低的起始音量阈值
} catch (e) {
console.log('[Browser Page] audioStart property not supported');
}
recognition.onstart = () => {
updateStatus("🎤 正在识别...");
console.log('[Browser Page] SpeechRecognition started.');
};
recognition.onresult = (event) => {
console.log('[Browser Page] Recognition result event:', event);
let interim_transcript = '';
let final_transcript = '';
// 输出识别结果的详细信息便于调试
for (let i = event.resultIndex; i < event.results.length; ++i) {
const confidence = event.results[i][0].confidence;
console.log(`[Browser Page] Result ${i}: ${event.results[i][0].transcript} (Confidence: ${confidence.toFixed(2)})`);
if (event.results[i].isFinal) {
final_transcript += event.results[i][0].transcript;
} else {
interim_transcript += event.results[i][0].transcript;
}
}
const resultText = final_transcript || interim_transcript;
resultDiv.textContent = resultText;
// 更新状态显示
if (resultText) {
updateStatus(`🎤 正在识别... (已捕捉到语音)`);
}
if (ws.readyState === WebSocket.OPEN) {
console.log(`[Browser Page] Sending ${final_transcript ? 'final' : 'interim'} result to server:`, resultText);
ws.send(JSON.stringify({ type: 'result', data: { text: resultText, isFinal: !!final_transcript } }));
}
};
recognition.onerror = (event) => {
console.error(`[Browser Page] SpeechRecognition Error - Type: ${event.error}, Message: ${event.message}`);
// 根据错误类型提供更友好的错误提示
let errorMessage = '';
switch (event.error) {
case 'no-speech':
errorMessage = '未检测到语音,请确保麦克风工作正常并尝试说话。';
// 尝试重新启动语音识别
setTimeout(() => {
if (recognition) {
try {
recognition.start();
console.log('[Browser Page] Restarting recognition after no-speech error');
} catch (e) {
console.error('[Browser Page] Failed to restart recognition:', e);
}
}
}, 1000);
break;
case 'audio-capture':
errorMessage = '无法捕获音频,请确保麦克风已连接并已授权。';
break;
case 'not-allowed':
errorMessage = '浏览器不允许使用麦克风,请检查权限设置。';
break;
case 'network':
errorMessage = '网络错误导致语音识别失败。';
break;
case 'aborted':
errorMessage = '语音识别被用户或系统中止。';
break;
default:
errorMessage = `识别错误: ${event.error}`;
}
updateStatus(`错误: ${errorMessage}`);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'error',
data: {
error: event.error,
message: errorMessage || event.message || `Recognition error: ${event.error}`
}
}));
}
};
recognition.onend = () => {
console.log('[Browser Page] SpeechRecognition ended.');
// 检查是否是由于错误或用户手动停止导致的结束
const isErrorOrStopped = statusDiv.textContent.includes('错误') || statusDiv.textContent.includes('停止');
if (!isErrorOrStopped) {
// 如果不是由于错误或手动停止,则自动重新启动语音识别
updateStatus("识别暂停,正在重新启动...");
// 保存当前的recognition对象
const currentRecognition = recognition;
// 尝试重新启动语音识别
setTimeout(() => {
try {
if (currentRecognition && currentRecognition === recognition) {
currentRecognition.start();
console.log('[Browser Page] Automatically restarting recognition');
} else {
// 如果recognition对象已经变化重新创建一个
setupRecognition();
if (recognition) {
recognition.start();
console.log('[Browser Page] Created new recognition instance and started');
}
}
} catch (e) {
console.error('[Browser Page] Failed to restart recognition:', e);
updateStatus("识别已停止。等待指令...");
}
}, 300);
} else {
updateStatus("识别已停止。等待指令...");
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'status', message: 'stopped' }));
}
// 只有在手动停止或错误时才重置recognition对象
recognition = null;
}
};
return true;
}
function startRecognition() {
if (!SpeechRecognition) {
updateStatus('错误:浏览器不支持 Web Speech API。');
return;
}
// 显示正在准备的状态
updateStatus('正在准备麦克风...');
if (recognition) {
console.log('[Browser Page] Recognition already exists, stopping first.');
stopRecognition();
}
if (!setupRecognition()) return;
console.log('[Browser Page] Attempting to start recognition...');
try {
// 设置更长的超时时间,确保有足够的时间获取麦克风权限
const micPermissionTimeout = setTimeout(() => {
updateStatus('获取麦克风权限超时,请刷新页面重试。');
}, 10000); // 10秒超时
navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
})
.then(stream => {
clearTimeout(micPermissionTimeout);
console.log('[Browser Page] Microphone access granted.');
// 检查麦克风音量级别
const audioContext = new AudioContext();
const analyser = audioContext.createAnalyser();
const microphone = audioContext.createMediaStreamSource(stream);
const javascriptNode = audioContext.createScriptProcessor(2048, 1, 1);
analyser.smoothingTimeConstant = 0.8;
analyser.fftSize = 1024;
microphone.connect(analyser);
analyser.connect(javascriptNode);
javascriptNode.connect(audioContext.destination);
javascriptNode.onaudioprocess = function () {
const array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);
let values = 0;
const length = array.length;
for (let i = 0; i < length; i++) {
values += (array[i]);
}
const average = values / length;
console.log('[Browser Page] Microphone volume level:', average);
// 如果音量太低,显示提示
if (average < 5) {
updateStatus('麦克风音量很低,请说话或检查麦克风设置。');
} else {
updateStatus('🎤 正在识别...');
}
// 只检查一次就断开连接
microphone.disconnect();
analyser.disconnect();
javascriptNode.disconnect();
};
// 释放测试用的音频流
setTimeout(() => {
stream.getTracks().forEach(track => track.stop());
audioContext.close();
}, 1000);
// 启动语音识别
if (recognition) {
recognition.start();
updateStatus('🎤 正在识别...');
} else {
updateStatus('错误Recognition 实例丢失。');
console.error('[Browser Page] Recognition instance lost before start.');
}
})
.catch(err => {
clearTimeout(micPermissionTimeout);
console.error('[Browser Page] Microphone access error:', err);
let errorMsg = `无法访问麦克风 (${err.name})`;
if (err.name === 'NotAllowedError') {
errorMsg = '麦克风访问被拒绝。请在浏览器设置中允许麦克风访问权限。';
} else if (err.name === 'NotFoundError') {
errorMsg = '未找到麦克风设备。请确保麦克风已连接。';
}
updateStatus(`错误: ${errorMsg}`);
recognition = null;
});
} catch (e) {
console.error('[Browser Page] Error calling recognition.start():', e);
updateStatus(`启动识别时出错: ${e.message}`);
recognition = null;
}
}
function stopRecognition() {
if (recognition) {
console.log('[Browser Page] Stopping recognition...');
updateStatus("正在停止识别...");
try {
recognition.stop();
} catch (e) {
console.error('[Browser Page] Error calling recognition.stop():', e);
recognition = null;
updateStatus("停止时出错,已强制重置。");
}
} else {
console.log('[Browser Page] Recognition not active, nothing to stop.');
updateStatus("识别未运行。");
}
}
function forceResetRecognition() {
console.log('[Browser Page] Force resetting recognition...');
updateStatus("强制重置语音识别...");
// 先尝试停止当前的识别
if (recognition) {
try {
recognition.stop();
} catch (e) {
console.error('[Browser Page] Error stopping recognition during reset:', e);
}
}
// 强制设置为null丢弃所有后续结果
recognition = null;
// 通知服务器已重置
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'status', message: 'reset_complete' }));
}
updateStatus("语音识别已重置,等待新指令。");
}
</script>
</body>
</html>

View File

@@ -1,854 +0,0 @@
{
"name": "cherry-asr-server",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cherry-asr-server",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.2",
"ws": "^8.13.0"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@@ -1,10 +0,0 @@
{
"name": "cherry-asr-server",
"version": "1.0.0",
"description": "Cherry Studio ASR Server",
"main": "server.js",
"dependencies": {
"express": "^4.18.2",
"ws": "^8.13.0"
}
}

View File

@@ -1,269 +0,0 @@
// 检查依赖项
try {
console.log('ASR Server starting...')
console.log('Node.js version:', process.version)
console.log('Current directory:', __dirname)
console.log('Current working directory:', process.cwd())
console.log('Command line arguments:', process.argv)
// 检查必要的依赖项
const checkDependency = (name) => {
try {
require(name) // Removed unused variable 'module'
console.log(`Successfully loaded dependency: ${name}`)
return true
} catch (error) {
console.error(`Failed to load dependency: ${name}`, error.message)
return false
}
}
// 检查所有必要的依赖项
const dependencies = ['http', 'ws', 'express', 'path', 'fs']
const missingDeps = dependencies.filter((dep) => !checkDependency(dep))
if (missingDeps.length > 0) {
console.error(`Missing dependencies: ${missingDeps.join(', ')}. Server cannot start.`)
process.exit(1)
}
} catch (error) {
console.error('Error during dependency check:', error)
process.exit(1)
}
// 加载依赖项
const http = require('http')
const WebSocket = require('ws')
const express = require('express')
const path = require('path') // Need path module
// const fs = require('fs') // Commented out unused import 'fs'
const app = express()
const port = 34515 // Define the port
// 获取index.html文件的路径
function getIndexHtmlPath() {
const fs = require('fs')
console.log('Current directory:', __dirname)
console.log('Current working directory:', process.cwd())
// 尝试多个可能的路径
const possiblePaths = [
// 开发环境路径
path.join(__dirname, 'index.html'),
// 当前目录
path.join(process.cwd(), 'index.html'),
// 相对于可执行文件的路径
path.join(path.dirname(process.execPath), 'index.html'),
// 相对于可执行文件的上级目录的路径
path.join(path.dirname(path.dirname(process.execPath)), 'index.html'),
// 相对于可执行文件的resources目录的路径
path.join(path.dirname(process.execPath), 'resources', 'index.html'),
// 相对于可执行文件的resources/asr-server目录的路径
path.join(path.dirname(process.execPath), 'resources', 'asr-server', 'index.html'),
// 相对于可执行文件的asr-server目录的路径
path.join(path.dirname(process.execPath), 'asr-server', 'index.html'),
// 如果是pkg打包环境
process.pkg ? path.join(path.dirname(process.execPath), 'index.html') : null
].filter(Boolean) // 过滤掉null值
console.log('Possible index.html paths:', possiblePaths)
// 检查每个路径,返回第一个存在的文件
for (const p of possiblePaths) {
try {
if (fs.existsSync(p)) {
console.log(`Found index.html at: ${p}`)
return p
}
} catch (e) {
console.error(`Error checking existence of ${p}:`, e)
}
}
// 如果没有找到文件,返回默认路径并记录错误
console.error('Could not find index.html in any of the expected locations')
return path.join(__dirname, 'index.html') // 返回默认路径,即使它可能不存在
}
// 提供网页给浏览器
app.get('/', (req, res) => {
try {
const indexPath = getIndexHtmlPath()
console.log(`Serving index.html from: ${indexPath}`)
// 检查文件是否存在
const fs = require('fs')
if (!fs.existsSync(indexPath)) {
console.error(`Error: index.html not found at ${indexPath}`)
return res.status(404).send(`Error: index.html not found at ${indexPath}. <br>Please check the server logs.`)
}
res.sendFile(indexPath, (err) => {
if (err) {
console.error('Error sending index.html:', err)
res.status(500).send(`Error serving index.html: ${err.message}`)
}
})
} catch (error) {
console.error('Error in route handler:', error)
res.status(500).send(`Server error: ${error.message}`)
}
})
const server = http.createServer(app)
const wss = new WebSocket.Server({ server })
let browserConnection = null
let electronConnection = null
wss.on('connection', (ws) => {
console.log('[Server] WebSocket client connected') // Add log
ws.on('message', (message) => {
let data
try {
// Ensure message is treated as string before parsing
data = JSON.parse(message.toString())
console.log('[Server] Received message:', data) // Log parsed data
} catch (e) {
console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e)
return // Ignore non-JSON messages
}
// 识别客户端类型
if (data.type === 'identify') {
if (data.role === 'browser') {
browserConnection = ws
console.log('[Server] Browser identified and connected')
// Notify Electron that the browser is ready
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
console.log('[Server] Sent browser_ready status to Electron')
}
// Notify Electron if it's already connected
if (electronConnection) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' }))
}
ws.on('close', () => {
console.log('[Server] Browser disconnected')
browserConnection = null
// Notify Electron
if (electronConnection) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' }))
}
})
ws.on('error', (error) => {
console.error('[Server] Browser WebSocket error:', error)
browserConnection = null // Assume disconnected on error
if (electronConnection) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
}
})
} else if (data.role === 'electron') {
electronConnection = ws
console.log('[Server] Electron identified and connected')
// If browser is already connected when Electron connects, notify Electron immediately
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
console.log('[Server] Sent initial browser_ready status to Electron')
}
ws.on('close', () => {
console.log('[Server] Electron disconnected')
electronConnection = null
// Maybe send stop to browser if electron disconnects?
// if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' }));
})
ws.on('error', (error) => {
console.error('[Server] Electron WebSocket error:', error)
electronConnection = null // Assume disconnected on error
})
}
}
// Electron 控制开始/停止
else if (data.type === 'start' && ws === electronConnection) {
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying START command to browser')
browserConnection.send(JSON.stringify({ type: 'start' }))
} else {
console.log('[Server] Cannot relay START: Browser not connected')
// Optionally notify Electron back
electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' }))
}
} else if (data.type === 'stop' && ws === electronConnection) {
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying STOP command to browser')
browserConnection.send(JSON.stringify({ type: 'stop' }))
} else {
console.log('[Server] Cannot relay STOP: Browser not connected')
}
} else if (data.type === 'reset' && ws === electronConnection) {
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying RESET command to browser')
browserConnection.send(JSON.stringify({ type: 'reset' }))
} else {
console.log('[Server] Cannot relay RESET: Browser not connected')
}
}
// 浏览器发送识别结果
else if (data.type === 'result' && ws === browserConnection) {
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
// console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed
electronConnection.send(JSON.stringify({ type: 'result', data: data.data }))
} else {
// console.log('[Server] Cannot relay RESULT: Electron not connected');
}
}
// 浏览器发送状态更新 (例如 'stopped')
else if (data.type === 'status' && ws === browserConnection) {
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed
electronConnection.send(JSON.stringify({ type: 'status', message: data.message }))
} else {
console.log('[Server] Cannot relay STATUS: Electron not connected')
}
} else {
console.log('[Server] Received unknown message type or from unknown source:', data)
}
})
ws.on('error', (error) => {
// Generic error handling for connection before identification
console.error('[Server] Initial WebSocket connection error:', error)
// Attempt to clean up based on which connection it might be (if identified)
if (ws === browserConnection) {
browserConnection = null
if (electronConnection)
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
} else if (ws === electronConnection) {
electronConnection = null
}
})
})
// 添加进程错误处理
process.on('uncaughtException', (error) => {
console.error('[Server] Uncaught exception:', error)
// 不立即退出,给日志输出的时间
setTimeout(() => process.exit(1), 1000)
})
process.on('unhandledRejection', (reason, promise) => {
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
})
// 尝试启动服务器
try {
server.listen(port, () => {
console.log(`[Server] Server running at http://localhost:${port}`)
})
// Handle server errors
server.on('error', (error) => {
console.error(`[Server] Failed to start server:`, error)
process.exit(1) // Exit if server fails to start
})
} catch (error) {
console.error('[Server] Critical error starting server:', error)
process.exit(1)
}

View File

@@ -1,114 +0,0 @@
/**
* 独立的ASR服务器
* 这个文件是一个简化版的server.js用于在打包后的应用中运行
*/
// 基本依赖
const http = require('http')
const express = require('express')
const path = require('path')
const fs = require('fs')
// 输出环境信息
console.log('ASR Server starting...')
console.log('Node.js version:', process.version)
console.log('Current directory:', __dirname)
console.log('Current working directory:', process.cwd())
console.log('Command line arguments:', process.argv)
// 创建Express应用
const app = express()
const port = 34515
// 提供静态文件
app.use(express.static(__dirname))
// 提供网页给浏览器
app.get('/', (req, res) => {
try {
// 尝试多个可能的路径
const possiblePaths = [
// 当前目录
path.join(__dirname, 'index.html'),
// 上级目录
path.join(__dirname, '..', 'index.html'),
// 应用根目录
path.join(process.cwd(), 'index.html')
]
console.log('Possible index.html paths:', possiblePaths)
// 查找第一个存在的文件
let indexPath = null
for (const p of possiblePaths) {
try {
if (fs.existsSync(p)) {
indexPath = p
console.log(`Found index.html at: ${p}`)
break
}
} catch (e) {
console.error(`Error checking existence of ${p}:`, e)
}
}
if (indexPath) {
res.sendFile(indexPath)
} else {
// 如果找不到文件返回一个简单的HTML页面
console.error('Could not find index.html, serving fallback page')
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>ASR Server</title>
<style>
body { font-family: sans-serif; padding: 2em; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>ASR Server is running</h1>
<p>This is a fallback page because the index.html file could not be found.</p>
<p>Server is running at: http://localhost:${port}</p>
<p>Current directory: ${__dirname}</p>
<p>Working directory: ${process.cwd()}</p>
</body>
</html>
`)
}
} catch (error) {
console.error('Error serving index.html:', error)
res.status(500).send(`Server error: ${error.message}`)
}
})
// 创建HTTP服务器
const server = http.createServer(app)
// 添加进程错误处理
process.on('uncaughtException', (error) => {
console.error('[Server] Uncaught exception:', error)
// 不立即退出,给日志输出的时间
setTimeout(() => process.exit(1), 1000)
})
process.on('unhandledRejection', (reason, promise) => {
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
})
// 尝试启动服务器
try {
server.listen(port, () => {
console.log(`[Server] Server running at http://localhost:${port}`)
})
// 处理服务器错误
server.on('error', (error) => {
console.error(`[Server] Failed to start server:`, error)
process.exit(1) // Exit if server fails to start
})
} catch (error) {
console.error('[Server] Critical error starting server:', error)
process.exit(1)
}

View File

@@ -1,5 +0,0 @@
@echo off
echo Starting ASR Server...
cd /d %~dp0
node standalone.js
pause

77
docs/CONTRIBUTING.zh.md Normal file
View File

@@ -0,0 +1,77 @@
# Cherry Studio 贡献者指南
[**English**](../CONTRIBUTING.md) | [**中文**](./CONTRIBUTING.zh.md)
欢迎来到 Cherry Studio 的贡献者社区!我们致力于将 Cherry Studio 打造成一个长期提供价值的项目,并希望邀请更多的开发者加入我们的行列。无论您是经验丰富的开发者还是刚刚起步的初学者,您的贡献都将帮助我们更好地服务用户,提升软件质量。
## 如何贡献
以下是您可以参与的几种方式:
1. **贡献代码**:帮助我们开发新功能或优化现有代码。请确保您的代码符合我们的编码标准,并通过所有测试。
2. **修复 BUG**:如果您发现了 BUG欢迎提交修复方案。请在提交前确认问题已被解决并附上相关测试。
3. **维护 Issue**:协助我们管理 GitHub 上的 issue帮助标记、分类和解决问题。
4. **产品设计**:参与产品设计讨论,帮助我们改进用户体验和界面设计。
5. **编写文档**帮助我们完善用户手册、API 文档和开发者指南。
6. **社区维护**:参与社区讨论,帮助解答用户问题,促进社区活跃。
7. **推广使用**:通过博客、社交媒体等渠道推广 Cherry Studio吸引更多用户和开发者。
## 开始之前
请确保阅读了[行为准则](CODE_OF_CONDUCT.md)和[LICENSE](LICENSE)。
## 开始贡献
为了让您更熟悉代码,建议您处理一些标记有以下标签之一或多个的问题:[good-first-issue](https://github.com/CherryHQ/cherry-studio/labels/good%20first%20issue)、[help-wanted](https://github.com/CherryHQ/cherry-studio/labels/help%20wanted) 或 [kind/bug](https://github.com/CherryHQ/cherry-studio/labels/kind%2Fbug)。任何帮助都会收到欢迎。
### 测试
未经测试的功能等同于不存在。为确保代码真正有效,应通过单元测试和功能测试覆盖相关流程。因此,在考虑贡献时,也请考虑可测试性。所有测试均可本地运行,无需依赖 CI。请参阅[开发者指南](docs/dev.md#test)中的“Test”部分。
### 拉取请求的自动化测试
自动化测试会在 Cherry Studio 组织成员开启的拉取请求PR上触发草稿 PR 除外。新贡献者开启的 PR 最初会标记为 needs-ok-to-test 标签且不自动测试。待 Cherry Studio 组织成员在 PR 上添加 /ok-to-test 后,测试通道将被创建。
### 考虑将您的拉取请求作为草稿打开
并非所有拉取请求在创建时就准备好接受审查。这可能是因为作者想发起讨论,或者他们不完全确定更改是否朝着正确的方向发展,甚至可能是因为更改尚未完成。请考虑将这些 PR 创建为[草稿拉取请求](https://github.blog/2019-02-14-introducing-draft-pull-requests/)。草稿 PR 会被CI跳过从而节省CI资源。这也意味着审阅者不会被自动分配社区会理解此 PR 尚未准备好接受审阅。
在您将草稿拉取请求标记为准备审核后,审核人员将被分配
### 贡献者遵守项目条款
我们要求每位贡献者证明他们有权合法地为我们的项目做出贡献。贡献者通过有意识地签署他们的提交来表达这一点,并通过这一行为表明他们遵守许可证[LICENSE](LICENSE)。
签名提交是指提交信息中包含以下内容的提交:
```
Signed-off-by: Your Name <your.email@example.com>
```
您可以通过以下命令[git commit --signoff](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---signoff)生成签名提交:
```
git commit --signoff -m "Your commit message"
```
### 获取代码审查/合并
维护者在此帮助您在合理时间内实现您的用例。他们会尽力在合理时间内审查您的代码并提供建设性反馈。但如果您在审查过程中受阻,或认为您的 Pull Request 未得到应有的关注,请通过 Issue 中的评论或者[社群](README.md#-community)联系我们
### 其他建议
- **联系开发者**:在提交 PR 之前,您可以先和开发者进行联系,共同探讨或者获取帮助。
- **成为核心开发者**:如果您能够稳定为项目贡献,恭喜您可以成为项目核心开发者,获取到项目成员身份。请查看我们的[成员指南](https://github.com/CherryHQ/community/blob/main/membership.md)
## 联系我们
如果您有任何问题或建议,欢迎通过以下方式联系我们:
- 微信kangfenmao
- [GitHub Issues](https://github.com/CherryHQ/cherry-studio/issues)
感谢您的支持和贡献!我们期待与您一起将 Cherry Studio 打造成更好的产品。

View File

@@ -24,14 +24,12 @@ https://docs.cherry-ai.com
# 🌠 スクリーンショット
![](https://github.com/user-attachments/assets/28585d83-4bf0-4714-b561-8c7bf57cc600)
![](https://github.com/user-attachments/assets/8576863a-f632-4776-bc12-657eeced9da3)
![](https://github.com/user-attachments/assets/790790d7-b462-48dd-bde1-91c1697a4648)
![](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/7b4f2f78-5cbe-4be8-9aec-f98d8405a505)
1. **多様な LLM サービス対応**
- ☁️ 主要な LLM クラウドサービス対応OpenAI、Gemini、Anthropic など

View File

@@ -24,14 +24,12 @@ https://docs.cherry-ai.com
# 🌠 界面
![](https://github.com/user-attachments/assets/28585d83-4bf0-4714-b561-8c7bf57cc600)
![](https://github.com/user-attachments/assets/8576863a-f632-4776-bc12-657eeced9da3)
![](https://github.com/user-attachments/assets/790790d7-b462-48dd-bde1-91c1697a4648)
![](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/995910f3-177a-4d1e-97ea-04e3b009ba36)
1. **多样化 LLM 服务支持**
- ☁️ 支持主流 LLM 云服务OpenAI、Gemini、Anthropic、硅基流动等
@@ -116,7 +114,7 @@ https://docs.cherry-ai.com
3. **提交更改**:提交并推送您的更改。
4. **打开 Pull Request**:描述您的更改和原因。
有关更详细的指南,请参阅我们的 [贡献指南](../CONTRIBUTING.md)。
有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.zh.md)。
感谢您的支持和贡献!

View File

@@ -37,6 +37,12 @@ yarn install
yarn dev
```
### Test
```bash
yarn test
```
### Build
```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

@@ -3,14 +3,15 @@ productName: Cherry Studio
electronLanguages:
- zh-CN
- zh-TW
- en-GB
- en-US
- ru
- ja # macOS/linux/win
- ru # macOS/linux/win
- zh_CN # for macOS
- zh_TW # for macOS
- en # for macOS
directories:
buildResources: build
files:
- out/**/*
- package.json
- '!{.vscode,.yarn,.github}'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
@@ -35,18 +36,9 @@ files:
- '!node_modules/@tavily/core/node_modules/js-tiktoken'
- '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}'
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
asarUnpack: # Removed ASR server rules from 'files' section
asarUnpack:
- resources/**
- '**/*.{metal,exp,lib}'
extraResources: # Add extraResources to copy the prepared asr-server directory
- from: asr-server # Copy the folder from project root
to: app/asr-server # Copy TO the 'app' subfolder within resources
filter:
- '**/*' # Include everything inside
- from: resources/data # Copy the data folder with agents.json
to: data # Copy TO the 'data' subfolder within resources
filter:
- '**/*' # Include everything inside
win:
executableName: Cherry Studio
artifactName: ${productName}-${version}-${arch}-setup.${ext}
@@ -82,6 +74,11 @@ linux:
- target: AppImage
maintainer: electronjs.org
category: Utility
desktop:
entry:
StartupWMClass: CherryStudio
mimeTypes:
- x-scheme-handler/cherrystudio
publish:
provider: generic
url: https://releases.cherry-ai.com
@@ -92,6 +89,9 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
全新图标风格
新的智能体界面
WebDAV 增加文件管理功能
新增对 grok-2-image 和 gpt-4o-image 图像支持
支持 Windows 便携版使用 data 目录存储数据
MCP 界面改版,新增描述信息显示
Mermaid 渲染逻辑优化
支持关闭公示渲染
修复 OpenAI 类型渲染错误

View File

@@ -1,4 +1,4 @@
import viteReact from '@vitejs/plugin-react'
import react from '@vitejs/plugin-react-swc'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
@@ -6,7 +6,7 @@ import { visualizer } from 'rollup-plugin-visualizer'
const visualizerPlugin = (type: 'renderer' | 'main') => {
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
}
// const viteReact = await import('@vitejs/plugin-react')
export default defineConfig({
main: {
plugins: [
@@ -51,20 +51,18 @@ export default defineConfig({
},
renderer: {
plugins: [
viteReact({
babel: {
plugins: [
[
'styled-components',
{
displayName: true, // 开发环境下启用组件名
fileName: false, // 不在类名中包含文件名
pure: true, // 优化性能
ssr: false // 不需要服务端渲染
}
]
react({
plugins: [
[
'@swc/plugin-styled-components',
{
displayName: true, // 开发环境下启用组件名称
fileName: false, // 不在类名中包含文件名
pure: true, // 优化性能
ssr: false // 不需要服务端渲染
}
]
}
]
}),
...visualizerPlugin('renderer')
],
@@ -76,17 +74,6 @@ export default defineConfig({
},
optimizeDeps: {
exclude: []
},
build: {
rollupOptions: {
input: {
index: resolve('src/renderer/index.html')
}
},
// 复制ASR服务器文件
assetsInlineLimit: 0,
// 确保复制assets目录下的所有文件
copyPublicDir: true
}
}
})

View File

@@ -1,425 +0,0 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cherry Studio ASR</title>
<style>
body {
font-family: sans-serif;
padding: 1em;
}
#status {
margin-top: 1em;
font-style: italic;
color: #555;
}
#result {
margin-top: 0.5em;
border: 1px solid #ccc;
padding: 0.5em;
min-height: 50px;
background: #f9f9f9;
}
</style>
</head>
<body>
<h1>浏览器语音识别中继页面</h1>
<p>这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。</p>
<div id="status">正在连接到服务器...</div>
<div id="result"></div>
<script>
const statusDiv = document.getElementById('status');
const resultDiv = document.getElementById('result');
// 尝试连接到WebSocket服务器
let ws;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
const reconnectInterval = 2000; // 2秒
function connectWebSocket() {
try {
ws = new WebSocket('ws://localhost:34515');
ws.onopen = () => {
reconnectAttempts = 0;
updateStatus('已连接到服务器,等待指令...');
ws.send(JSON.stringify({ type: 'identify', role: 'browser' }));
};
ws.onmessage = handleMessage;
ws.onerror = (error) => {
console.error('[Browser Page] WebSocket Error:', error);
updateStatus('WebSocket 连接错误!请检查服务器是否运行。');
};
ws.onclose = () => {
console.log('[Browser Page] WebSocket Connection Closed');
updateStatus('与服务器断开连接。尝试重新连接...');
stopRecognition();
// 尝试重新连接
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
updateStatus(`与服务器断开连接。尝试重新连接 (${reconnectAttempts}/${maxReconnectAttempts})...`);
setTimeout(connectWebSocket, reconnectInterval);
} else {
updateStatus('无法连接到服务器。请刷新页面或重启应用。');
}
};
} catch (error) {
console.error('[Browser Page] Error creating WebSocket:', error);
updateStatus('创建WebSocket连接时出错。请刷新页面或重启应用。');
}
}
// 初始连接
connectWebSocket();
let recognition = null;
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
function updateStatus(message) {
console.log(`[Browser Page Status] ${message}`);
statusDiv.textContent = message;
}
function handleMessage(event) {
let data;
try {
data = JSON.parse(event.data);
console.log('[Browser Page] Received command:', data);
} catch (e) {
console.error('[Browser Page] Received non-JSON message:', event.data);
return;
}
if (data.type === 'start') {
startRecognition();
} else if (data.type === 'stop') {
stopRecognition();
} else if (data.type === 'reset') {
// 强制重置语音识别
forceResetRecognition();
} else {
console.warn('[Browser Page] Received unknown command type:', data.type);
}
};
function setupRecognition() {
if (!SpeechRecognition) {
updateStatus('错误:此浏览器不支持 Web Speech API。');
return false;
}
if (recognition && recognition.recognizing) {
console.log('[Browser Page] Recognition already active.');
return true;
}
recognition = new SpeechRecognition();
recognition.lang = 'zh-CN';
recognition.continuous = true;
recognition.interimResults = true;
// 增加以下设置提高语音识别的可靠性
recognition.maxAlternatives = 3; // 返回多个可能的识别结果
// 设置较短的语音识别时间,使用户能更快地看到结果
// 注意:这个属性不是标准的,可能不是所有浏览器都支持
try {
// @ts-ignore
recognition.audioStart = 0.1; // 尝试设置较低的起始音量阈值
} catch (e) {
console.log('[Browser Page] audioStart property not supported');
}
recognition.onstart = () => {
updateStatus("🎤 正在识别...");
console.log('[Browser Page] SpeechRecognition started.');
};
recognition.onresult = (event) => {
console.log('[Browser Page] Recognition result event:', event);
let interim_transcript = '';
let final_transcript = '';
// 输出识别结果的详细信息便于调试
for (let i = event.resultIndex; i < event.results.length; ++i) {
const confidence = event.results[i][0].confidence;
console.log(`[Browser Page] Result ${i}: ${event.results[i][0].transcript} (Confidence: ${confidence.toFixed(2)})`);
if (event.results[i].isFinal) {
final_transcript += event.results[i][0].transcript;
} else {
interim_transcript += event.results[i][0].transcript;
}
}
const resultText = final_transcript || interim_transcript;
resultDiv.textContent = resultText;
// 更新状态显示
if (resultText) {
updateStatus(`🎤 正在识别... (已捕捉到语音)`);
}
if (ws.readyState === WebSocket.OPEN) {
console.log(`[Browser Page] Sending ${final_transcript ? 'final' : 'interim'} result to server:`, resultText);
ws.send(JSON.stringify({ type: 'result', data: { text: resultText, isFinal: !!final_transcript } }));
}
};
recognition.onerror = (event) => {
console.error(`[Browser Page] SpeechRecognition Error - Type: ${event.error}, Message: ${event.message}`);
// 根据错误类型提供更友好的错误提示
let errorMessage = '';
switch (event.error) {
case 'no-speech':
errorMessage = '未检测到语音,请确保麦克风工作正常并尝试说话。';
// 尝试重新启动语音识别
setTimeout(() => {
if (recognition) {
try {
recognition.start();
console.log('[Browser Page] Restarting recognition after no-speech error');
} catch (e) {
console.error('[Browser Page] Failed to restart recognition:', e);
}
}
}, 1000);
break;
case 'audio-capture':
errorMessage = '无法捕获音频,请确保麦克风已连接并已授权。';
break;
case 'not-allowed':
errorMessage = '浏览器不允许使用麦克风,请检查权限设置。';
break;
case 'network':
errorMessage = '网络错误导致语音识别失败。';
break;
case 'aborted':
errorMessage = '语音识别被用户或系统中止。';
break;
default:
errorMessage = `识别错误: ${event.error}`;
}
updateStatus(`错误: ${errorMessage}`);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'error',
data: {
error: event.error,
message: errorMessage || event.message || `Recognition error: ${event.error}`
}
}));
}
};
recognition.onend = () => {
console.log('[Browser Page] SpeechRecognition ended.');
// 检查是否是由于错误或用户手动停止导致的结束
const isErrorOrStopped = statusDiv.textContent.includes('错误') || statusDiv.textContent.includes('停止');
if (!isErrorOrStopped) {
// 如果不是由于错误或手动停止,则自动重新启动语音识别
updateStatus("识别暂停,正在重新启动...");
// 保存当前的recognition对象
const currentRecognition = recognition;
// 尝试重新启动语音识别
setTimeout(() => {
try {
if (currentRecognition && currentRecognition === recognition) {
currentRecognition.start();
console.log('[Browser Page] Automatically restarting recognition');
} else {
// 如果recognition对象已经变化重新创建一个
setupRecognition();
if (recognition) {
recognition.start();
console.log('[Browser Page] Created new recognition instance and started');
}
}
} catch (e) {
console.error('[Browser Page] Failed to restart recognition:', e);
updateStatus("识别已停止。等待指令...");
}
}, 300);
} else {
updateStatus("识别已停止。等待指令...");
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'status', message: 'stopped' }));
}
// 只有在手动停止或错误时才重置recognition对象
recognition = null;
}
};
return true;
}
function startRecognition() {
if (!SpeechRecognition) {
updateStatus('错误:浏览器不支持 Web Speech API。');
return;
}
// 显示正在准备的状态
updateStatus('正在准备麦克风...');
if (recognition) {
console.log('[Browser Page] Recognition already exists, stopping first.');
stopRecognition();
}
if (!setupRecognition()) return;
console.log('[Browser Page] Attempting to start recognition...');
try {
// 设置更长的超时时间,确保有足够的时间获取麦克风权限
const micPermissionTimeout = setTimeout(() => {
updateStatus('获取麦克风权限超时,请刷新页面重试。');
}, 10000); // 10秒超时
navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
})
.then(stream => {
clearTimeout(micPermissionTimeout);
console.log('[Browser Page] Microphone access granted.');
// 检查麦克风音量级别
const audioContext = new AudioContext();
const analyser = audioContext.createAnalyser();
const microphone = audioContext.createMediaStreamSource(stream);
const javascriptNode = audioContext.createScriptProcessor(2048, 1, 1);
analyser.smoothingTimeConstant = 0.8;
analyser.fftSize = 1024;
microphone.connect(analyser);
analyser.connect(javascriptNode);
javascriptNode.connect(audioContext.destination);
javascriptNode.onaudioprocess = function () {
const array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);
let values = 0;
const length = array.length;
for (let i = 0; i < length; i++) {
values += (array[i]);
}
const average = values / length;
console.log('[Browser Page] Microphone volume level:', average);
// 如果音量太低,显示提示
if (average < 5) {
updateStatus('麦克风音量很低,请说话或检查麦克风设置。');
} else {
updateStatus('🎤 正在识别...');
}
// 只检查一次就断开连接
microphone.disconnect();
analyser.disconnect();
javascriptNode.disconnect();
};
// 释放测试用的音频流
setTimeout(() => {
stream.getTracks().forEach(track => track.stop());
audioContext.close();
}, 1000);
// 启动语音识别
if (recognition) {
recognition.start();
updateStatus('🎤 正在识别...');
} else {
updateStatus('错误Recognition 实例丢失。');
console.error('[Browser Page] Recognition instance lost before start.');
}
})
.catch(err => {
clearTimeout(micPermissionTimeout);
console.error('[Browser Page] Microphone access error:', err);
let errorMsg = `无法访问麦克风 (${err.name})`;
if (err.name === 'NotAllowedError') {
errorMsg = '麦克风访问被拒绝。请在浏览器设置中允许麦克风访问权限。';
} else if (err.name === 'NotFoundError') {
errorMsg = '未找到麦克风设备。请确保麦克风已连接。';
}
updateStatus(`错误: ${errorMsg}`);
recognition = null;
});
} catch (e) {
console.error('[Browser Page] Error calling recognition.start():', e);
updateStatus(`启动识别时出错: ${e.message}`);
recognition = null;
}
}
function stopRecognition() {
if (recognition) {
console.log('[Browser Page] Stopping recognition...');
updateStatus("正在停止识别...");
try {
recognition.stop();
} catch (e) {
console.error('[Browser Page] Error calling recognition.stop():', e);
recognition = null;
updateStatus("停止时出错,已强制重置。");
}
} else {
console.log('[Browser Page] Recognition not active, nothing to stop.');
updateStatus("识别未运行。");
}
}
function forceResetRecognition() {
console.log('[Browser Page] Force resetting recognition...');
updateStatus("强制重置语音识别...");
// 先尝试停止当前的识别
if (recognition) {
try {
recognition.stop();
} catch (e) {
console.error('[Browser Page] Error stopping recognition during reset:', e);
}
}
// 强制设置为null丢弃所有后续结果
recognition = null;
// 通知服务器已重置
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'status', message: 'reset_complete' }));
}
updateStatus("语音识别已重置,等待新指令。");
}
</script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.2.5",
"version": "1.2.10",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -44,7 +44,12 @@
"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": "npx -y tsx --test src/**/*.test.ts",
"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",
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"postinstall": "electron-builder install-app-deps",
@@ -65,32 +70,30 @@
"@electron-toolkit/utils": "^3.0.0",
"@electron/notarize": "^2.5.0",
"@langchain/community": "^0.3.36",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@strongtz/win32-arm64-msvc": "^0.4.7",
"@tryfabric/martian": "^1.2.4",
"@types/react-infinite-scroll-component": "^5.0.0",
"@xyflow/react": "^12.4.4",
"adm-zip": "^0.5.16",
"archiver": "^7.0.1",
"async-mutex": "^0.5.0",
"bufferutil": "^4.0.9",
"color": "^5.0.0",
"diff": "^7.0.0",
"docx": "^9.0.2",
"edge-tts-node": "^1.5.7",
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "^6.3.9",
"electron-updater": "patch:electron-updater@npm%3A6.6.3#~/.yarn/patches/electron-updater-npm-6.6.3-9269dbaf84.patch",
"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",
"fs-extra": "^11.2.0",
"got-scraping": "^4.1.1",
"jsdom": "^26.0.0",
"markdown-it": "^14.1.0",
"node-edge-tts": "^1.2.8",
"node-stream-zip": "^1.15.0",
"officeparser": "^4.1.1",
"os-proxy-config": "^1.1.2",
"proxy-agent": "^6.5.0",
"tar": "^7.4.3",
"turndown": "^7.2.0",
@@ -104,7 +107,6 @@
"@agentic/exa": "^7.3.3",
"@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3",
"@analytics/google-analytics": "^1.1.0",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@anthropic-ai/sdk": "^0.38.0",
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
@@ -114,19 +116,21 @@
"@emotion/is-prop-valid": "^1.3.1",
"@eslint-react/eslint-plugin": "^1.36.1",
"@eslint/js": "^9.22.0",
"@google/genai": "patch:@google/genai@npm%3A0.8.0#~/.yarn/patches/@google-genai-npm-0.8.0-450d0d9a7d.patch",
"@google/genai": "^0.10.0",
"@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0",
"@modelcontextprotocol/sdk": "^1.9.0",
"@modelcontextprotocol/sdk": "^1.10.2",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@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",
"@tryfabric/martian": "^1.2.4",
"@types/adm-zip": "^0",
"@types/diff": "^7",
"@types/fs-extra": "^11",
"@types/js-yaml": "^4",
"@types/lodash": "^4.17.16",
"@types/lodash": "^4.17.5",
"@types/markdown-it": "^14",
"@types/md5": "^2.3.5",
"@types/node": "^18.19.9",
@@ -136,8 +140,10 @@
"@types/react-infinite-scroll-component": "^5.0.0",
"@types/tinycolor2": "^1",
"@types/ws": "^8",
"@vitejs/plugin-react": "4.3.4",
"analytics": "^0.8.16",
"@vitejs/plugin-react-swc": "^3.9.0",
"@vitest/coverage-v8": "^3.1.1",
"@vitest/ui": "^3.1.1",
"@xyflow/react": "^12.4.4",
"antd": "^5.22.5",
"applescript": "^1.0.0",
"axios": "^1.7.3",
@@ -167,7 +173,7 @@
"lucide-react": "^0.487.0",
"mime": "^4.0.4",
"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%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
"p-queue": "^8.1.0",
"prettier": "^3.5.3",
"rc-virtual-list": "^3.18.5",
@@ -186,13 +192,12 @@
"rehype-katex": "^7.0.1",
"rehype-mathjax": "^7.0.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-cjk-friendly": "^1.1.0",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.2",
"shiki": "^3.2.1",
"shiki": "^3.2.2",
"string-width": "^7.2.0",
"styled-components": "^6.1.11",
"tiny-pinyin": "^1.3.2",
@@ -200,7 +205,8 @@
"tokenx": "^0.4.1",
"typescript": "^5.6.2",
"uuid": "^10.0.0",
"vite": "6.2.6"
"vite": "6.2.6",
"vitest": "^3.1.1"
},
"resolutions": {
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
@@ -208,8 +214,11 @@
"@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",
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch"
"openai@npm:^4.77.0": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
"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%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch"
},
"packageManager": "yarn@4.6.0",
"lint-staged": {

View File

@@ -20,16 +20,7 @@ export enum IpcChannel {
App_InstallUvBinary = 'app:install-uv-binary',
App_InstallBunBinary = 'app:install-bun-binary',
// ASR Server
Asr_StartServer = 'start-asr-server',
Asr_StopServer = 'stop-asr-server',
// MsTTS
MsTTS_GetVoices = 'mstts:get-voices',
MsTTS_Synthesize = 'mstts:synthesize',
MsTTS_SynthesizeStream = 'mstts:synthesize-stream',
MsTTS_StreamData = 'mstts:stream-data',
MsTTS_StreamEnd = 'mstts:stream-end',
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
// Open
Open_Path = 'open:path',
@@ -47,6 +38,7 @@ export enum IpcChannel {
MiniWindow_SetPin = 'miniwindow:set-pin',
// Mcp
Mcp_AddServer = 'mcp:add-server',
Mcp_RemoveServer = 'mcp:remove-server',
Mcp_RestartServer = 'mcp:restart-server',
Mcp_StopServer = 'mcp:stop-server',

View File

@@ -1,123 +0,0 @@
/**
* 内置的ASR服务器模块
* 这个文件可以直接在Electron中运行不需要外部依赖
*/
// 使用Electron内置的Node.js模块
const http = require('http')
const path = require('path')
const fs = require('fs')
// 输出环境信息
console.log('ASR Server (Embedded) starting...')
console.log('Node.js version:', process.version)
console.log('Current directory:', __dirname)
console.log('Current working directory:', process.cwd())
console.log('Command line arguments:', process.argv)
// 创建HTTP服务器
const server = http.createServer((req, res) => {
try {
if (req.url === '/' || req.url === '/index.html') {
// 尝试多个可能的路径
const possiblePaths = [
// 当前目录
path.join(__dirname, 'index.html'),
// 上级目录
path.join(__dirname, '..', 'index.html'),
// 应用根目录
path.join(process.cwd(), 'index.html')
]
console.log('Possible index.html paths:', possiblePaths)
// 查找第一个存在的文件
let indexPath = null
for (const p of possiblePaths) {
try {
if (fs.existsSync(p)) {
indexPath = p
console.log(`Found index.html at: ${p}`)
break
}
} catch (e) {
console.error(`Error checking existence of ${p}:`, e)
}
}
if (indexPath) {
// 读取文件内容并发送
fs.readFile(indexPath, (err, data) => {
if (err) {
console.error('Error reading index.html:', err)
res.writeHead(500)
res.end('Error reading index.html')
return
}
res.writeHead(200, { 'Content-Type': 'text/html' })
res.end(data)
})
} else {
// 如果找不到文件返回一个简单的HTML页面
console.error('Could not find index.html, serving fallback page')
res.writeHead(200, { 'Content-Type': 'text/html' })
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>ASR Server</title>
<style>
body { font-family: sans-serif; padding: 2em; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>ASR Server is running</h1>
<p>This is a fallback page because the index.html file could not be found.</p>
<p>Server is running at: http://localhost:34515</p>
<p>Current directory: ${__dirname}</p>
<p>Working directory: ${process.cwd()}</p>
</body>
</html>
`)
}
} else {
// 处理其他请求
res.writeHead(404)
res.end('Not found')
}
} catch (error) {
console.error('Error handling request:', error)
res.writeHead(500)
res.end('Server error')
}
})
// 添加进程错误处理
process.on('uncaughtException', (error) => {
console.error('[Server] Uncaught exception:', error)
// 不立即退出,给日志输出的时间
setTimeout(() => process.exit(1), 1000)
})
process.on('unhandledRejection', (reason, promise) => {
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
})
// 尝试启动服务器
try {
const port = 34515
server.listen(port, () => {
console.log(`[Server] Server running at http://localhost:${port}`)
})
// 处理服务器错误
server.on('error', (error) => {
console.error(`[Server] Failed to start server:`, error)
process.exit(1) // Exit if server fails to start
})
} catch (error) {
console.error('[Server] Critical error starting server:', error)
process.exit(1)
}

View File

@@ -1,425 +0,0 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cherry Studio ASR</title>
<style>
body {
font-family: sans-serif;
padding: 1em;
}
#status {
margin-top: 1em;
font-style: italic;
color: #555;
}
#result {
margin-top: 0.5em;
border: 1px solid #ccc;
padding: 0.5em;
min-height: 50px;
background: #f9f9f9;
}
</style>
</head>
<body>
<h1>浏览器语音识别中继页面</h1>
<p>这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。</p>
<div id="status">正在连接到服务器...</div>
<div id="result"></div>
<script>
const statusDiv = document.getElementById('status');
const resultDiv = document.getElementById('result');
// 尝试连接到WebSocket服务器
let ws;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
const reconnectInterval = 2000; // 2秒
function connectWebSocket() {
try {
ws = new WebSocket('ws://localhost:34515');
ws.onopen = () => {
reconnectAttempts = 0;
updateStatus('已连接到服务器,等待指令...');
ws.send(JSON.stringify({ type: 'identify', role: 'browser' }));
};
ws.onmessage = handleMessage;
ws.onerror = (error) => {
console.error('[Browser Page] WebSocket Error:', error);
updateStatus('WebSocket 连接错误!请检查服务器是否运行。');
};
ws.onclose = () => {
console.log('[Browser Page] WebSocket Connection Closed');
updateStatus('与服务器断开连接。尝试重新连接...');
stopRecognition();
// 尝试重新连接
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
updateStatus(`与服务器断开连接。尝试重新连接 (${reconnectAttempts}/${maxReconnectAttempts})...`);
setTimeout(connectWebSocket, reconnectInterval);
} else {
updateStatus('无法连接到服务器。请刷新页面或重启应用。');
}
};
} catch (error) {
console.error('[Browser Page] Error creating WebSocket:', error);
updateStatus('创建WebSocket连接时出错。请刷新页面或重启应用。');
}
}
// 初始连接
connectWebSocket();
let recognition = null;
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
function updateStatus(message) {
console.log(`[Browser Page Status] ${message}`);
statusDiv.textContent = message;
}
function handleMessage(event) {
let data;
try {
data = JSON.parse(event.data);
console.log('[Browser Page] Received command:', data);
} catch (e) {
console.error('[Browser Page] Received non-JSON message:', event.data);
return;
}
if (data.type === 'start') {
startRecognition();
} else if (data.type === 'stop') {
stopRecognition();
} else if (data.type === 'reset') {
// 强制重置语音识别
forceResetRecognition();
} else {
console.warn('[Browser Page] Received unknown command type:', data.type);
}
};
function setupRecognition() {
if (!SpeechRecognition) {
updateStatus('错误:此浏览器不支持 Web Speech API。');
return false;
}
if (recognition && recognition.recognizing) {
console.log('[Browser Page] Recognition already active.');
return true;
}
recognition = new SpeechRecognition();
recognition.lang = 'zh-CN';
recognition.continuous = true;
recognition.interimResults = true;
// 增加以下设置提高语音识别的可靠性
recognition.maxAlternatives = 3; // 返回多个可能的识别结果
// 设置较短的语音识别时间,使用户能更快地看到结果
// 注意:这个属性不是标准的,可能不是所有浏览器都支持
try {
// @ts-ignore
recognition.audioStart = 0.1; // 尝试设置较低的起始音量阈值
} catch (e) {
console.log('[Browser Page] audioStart property not supported');
}
recognition.onstart = () => {
updateStatus("🎤 正在识别...");
console.log('[Browser Page] SpeechRecognition started.');
};
recognition.onresult = (event) => {
console.log('[Browser Page] Recognition result event:', event);
let interim_transcript = '';
let final_transcript = '';
// 输出识别结果的详细信息便于调试
for (let i = event.resultIndex; i < event.results.length; ++i) {
const confidence = event.results[i][0].confidence;
console.log(`[Browser Page] Result ${i}: ${event.results[i][0].transcript} (Confidence: ${confidence.toFixed(2)})`);
if (event.results[i].isFinal) {
final_transcript += event.results[i][0].transcript;
} else {
interim_transcript += event.results[i][0].transcript;
}
}
const resultText = final_transcript || interim_transcript;
resultDiv.textContent = resultText;
// 更新状态显示
if (resultText) {
updateStatus(`🎤 正在识别... (已捕捉到语音)`);
}
if (ws.readyState === WebSocket.OPEN) {
console.log(`[Browser Page] Sending ${final_transcript ? 'final' : 'interim'} result to server:`, resultText);
ws.send(JSON.stringify({ type: 'result', data: { text: resultText, isFinal: !!final_transcript } }));
}
};
recognition.onerror = (event) => {
console.error(`[Browser Page] SpeechRecognition Error - Type: ${event.error}, Message: ${event.message}`);
// 根据错误类型提供更友好的错误提示
let errorMessage = '';
switch (event.error) {
case 'no-speech':
errorMessage = '未检测到语音,请确保麦克风工作正常并尝试说话。';
// 尝试重新启动语音识别
setTimeout(() => {
if (recognition) {
try {
recognition.start();
console.log('[Browser Page] Restarting recognition after no-speech error');
} catch (e) {
console.error('[Browser Page] Failed to restart recognition:', e);
}
}
}, 1000);
break;
case 'audio-capture':
errorMessage = '无法捕获音频,请确保麦克风已连接并已授权。';
break;
case 'not-allowed':
errorMessage = '浏览器不允许使用麦克风,请检查权限设置。';
break;
case 'network':
errorMessage = '网络错误导致语音识别失败。';
break;
case 'aborted':
errorMessage = '语音识别被用户或系统中止。';
break;
default:
errorMessage = `识别错误: ${event.error}`;
}
updateStatus(`错误: ${errorMessage}`);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'error',
data: {
error: event.error,
message: errorMessage || event.message || `Recognition error: ${event.error}`
}
}));
}
};
recognition.onend = () => {
console.log('[Browser Page] SpeechRecognition ended.');
// 检查是否是由于错误或用户手动停止导致的结束
const isErrorOrStopped = statusDiv.textContent.includes('错误') || statusDiv.textContent.includes('停止');
if (!isErrorOrStopped) {
// 如果不是由于错误或手动停止,则自动重新启动语音识别
updateStatus("识别暂停,正在重新启动...");
// 保存当前的recognition对象
const currentRecognition = recognition;
// 尝试重新启动语音识别
setTimeout(() => {
try {
if (currentRecognition && currentRecognition === recognition) {
currentRecognition.start();
console.log('[Browser Page] Automatically restarting recognition');
} else {
// 如果recognition对象已经变化重新创建一个
setupRecognition();
if (recognition) {
recognition.start();
console.log('[Browser Page] Created new recognition instance and started');
}
}
} catch (e) {
console.error('[Browser Page] Failed to restart recognition:', e);
updateStatus("识别已停止。等待指令...");
}
}, 300);
} else {
updateStatus("识别已停止。等待指令...");
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'status', message: 'stopped' }));
}
// 只有在手动停止或错误时才重置recognition对象
recognition = null;
}
};
return true;
}
function startRecognition() {
if (!SpeechRecognition) {
updateStatus('错误:浏览器不支持 Web Speech API。');
return;
}
// 显示正在准备的状态
updateStatus('正在准备麦克风...');
if (recognition) {
console.log('[Browser Page] Recognition already exists, stopping first.');
stopRecognition();
}
if (!setupRecognition()) return;
console.log('[Browser Page] Attempting to start recognition...');
try {
// 设置更长的超时时间,确保有足够的时间获取麦克风权限
const micPermissionTimeout = setTimeout(() => {
updateStatus('获取麦克风权限超时,请刷新页面重试。');
}, 10000); // 10秒超时
navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
})
.then(stream => {
clearTimeout(micPermissionTimeout);
console.log('[Browser Page] Microphone access granted.');
// 检查麦克风音量级别
const audioContext = new AudioContext();
const analyser = audioContext.createAnalyser();
const microphone = audioContext.createMediaStreamSource(stream);
const javascriptNode = audioContext.createScriptProcessor(2048, 1, 1);
analyser.smoothingTimeConstant = 0.8;
analyser.fftSize = 1024;
microphone.connect(analyser);
analyser.connect(javascriptNode);
javascriptNode.connect(audioContext.destination);
javascriptNode.onaudioprocess = function () {
const array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);
let values = 0;
const length = array.length;
for (let i = 0; i < length; i++) {
values += (array[i]);
}
const average = values / length;
console.log('[Browser Page] Microphone volume level:', average);
// 如果音量太低,显示提示
if (average < 5) {
updateStatus('麦克风音量很低,请说话或检查麦克风设置。');
} else {
updateStatus('🎤 正在识别...');
}
// 只检查一次就断开连接
microphone.disconnect();
analyser.disconnect();
javascriptNode.disconnect();
};
// 释放测试用的音频流
setTimeout(() => {
stream.getTracks().forEach(track => track.stop());
audioContext.close();
}, 1000);
// 启动语音识别
if (recognition) {
recognition.start();
updateStatus('🎤 正在识别...');
} else {
updateStatus('错误Recognition 实例丢失。');
console.error('[Browser Page] Recognition instance lost before start.');
}
})
.catch(err => {
clearTimeout(micPermissionTimeout);
console.error('[Browser Page] Microphone access error:', err);
let errorMsg = `无法访问麦克风 (${err.name})`;
if (err.name === 'NotAllowedError') {
errorMsg = '麦克风访问被拒绝。请在浏览器设置中允许麦克风访问权限。';
} else if (err.name === 'NotFoundError') {
errorMsg = '未找到麦克风设备。请确保麦克风已连接。';
}
updateStatus(`错误: ${errorMsg}`);
recognition = null;
});
} catch (e) {
console.error('[Browser Page] Error calling recognition.start():', e);
updateStatus(`启动识别时出错: ${e.message}`);
recognition = null;
}
}
function stopRecognition() {
if (recognition) {
console.log('[Browser Page] Stopping recognition...');
updateStatus("正在停止识别...");
try {
recognition.stop();
} catch (e) {
console.error('[Browser Page] Error calling recognition.stop():', e);
recognition = null;
updateStatus("停止时出错,已强制重置。");
}
} else {
console.log('[Browser Page] Recognition not active, nothing to stop.');
updateStatus("识别未运行。");
}
}
function forceResetRecognition() {
console.log('[Browser Page] Force resetting recognition...');
updateStatus("强制重置语音识别...");
// 先尝试停止当前的识别
if (recognition) {
try {
recognition.stop();
} catch (e) {
console.error('[Browser Page] Error stopping recognition during reset:', e);
}
}
// 强制设置为null丢弃所有后续结果
recognition = null;
// 通知服务器已重置
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'status', message: 'reset_complete' }));
}
updateStatus("语音识别已重置,等待新指令。");
}
</script>
</body>
</html>

View File

@@ -1,854 +0,0 @@
{
"name": "cherry-asr-server",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cherry-asr-server",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.2",
"ws": "^8.13.0"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@@ -1,10 +0,0 @@
{
"name": "cherry-asr-server",
"version": "1.0.0",
"description": "Cherry Studio ASR Server",
"main": "server.js",
"dependencies": {
"express": "^4.18.2",
"ws": "^8.13.0"
}
}

View File

@@ -1,269 +0,0 @@
// 检查依赖项
try {
console.log('ASR Server starting...')
console.log('Node.js version:', process.version)
console.log('Current directory:', __dirname)
console.log('Current working directory:', process.cwd())
console.log('Command line arguments:', process.argv)
// 检查必要的依赖项
const checkDependency = (name) => {
try {
require(name) // Removed unused variable 'module'
console.log(`Successfully loaded dependency: ${name}`)
return true
} catch (error) {
console.error(`Failed to load dependency: ${name}`, error.message)
return false
}
}
// 检查所有必要的依赖项
const dependencies = ['http', 'ws', 'express', 'path', 'fs']
const missingDeps = dependencies.filter((dep) => !checkDependency(dep))
if (missingDeps.length > 0) {
console.error(`Missing dependencies: ${missingDeps.join(', ')}. Server cannot start.`)
process.exit(1)
}
} catch (error) {
console.error('Error during dependency check:', error)
process.exit(1)
}
// 加载依赖项
const http = require('http')
const WebSocket = require('ws')
const express = require('express')
const path = require('path') // Need path module
// const fs = require('fs') // Commented out unused import 'fs'
const app = express()
const port = 34515 // Define the port
// 获取index.html文件的路径
function getIndexHtmlPath() {
const fs = require('fs')
console.log('Current directory:', __dirname)
console.log('Current working directory:', process.cwd())
// 尝试多个可能的路径
const possiblePaths = [
// 开发环境路径
path.join(__dirname, 'index.html'),
// 当前目录
path.join(process.cwd(), 'index.html'),
// 相对于可执行文件的路径
path.join(path.dirname(process.execPath), 'index.html'),
// 相对于可执行文件的上级目录的路径
path.join(path.dirname(path.dirname(process.execPath)), 'index.html'),
// 相对于可执行文件的resources目录的路径
path.join(path.dirname(process.execPath), 'resources', 'index.html'),
// 相对于可执行文件的resources/asr-server目录的路径
path.join(path.dirname(process.execPath), 'resources', 'asr-server', 'index.html'),
// 相对于可执行文件的asr-server目录的路径
path.join(path.dirname(process.execPath), 'asr-server', 'index.html'),
// 如果是pkg打包环境
process.pkg ? path.join(path.dirname(process.execPath), 'index.html') : null
].filter(Boolean) // 过滤掉null值
console.log('Possible index.html paths:', possiblePaths)
// 检查每个路径,返回第一个存在的文件
for (const p of possiblePaths) {
try {
if (fs.existsSync(p)) {
console.log(`Found index.html at: ${p}`)
return p
}
} catch (e) {
console.error(`Error checking existence of ${p}:`, e)
}
}
// 如果没有找到文件,返回默认路径并记录错误
console.error('Could not find index.html in any of the expected locations')
return path.join(__dirname, 'index.html') // 返回默认路径,即使它可能不存在
}
// 提供网页给浏览器
app.get('/', (req, res) => {
try {
const indexPath = getIndexHtmlPath()
console.log(`Serving index.html from: ${indexPath}`)
// 检查文件是否存在
const fs = require('fs')
if (!fs.existsSync(indexPath)) {
console.error(`Error: index.html not found at ${indexPath}`)
return res.status(404).send(`Error: index.html not found at ${indexPath}. <br>Please check the server logs.`)
}
res.sendFile(indexPath, (err) => {
if (err) {
console.error('Error sending index.html:', err)
res.status(500).send(`Error serving index.html: ${err.message}`)
}
})
} catch (error) {
console.error('Error in route handler:', error)
res.status(500).send(`Server error: ${error.message}`)
}
})
const server = http.createServer(app)
const wss = new WebSocket.Server({ server })
let browserConnection = null
let electronConnection = null
wss.on('connection', (ws) => {
console.log('[Server] WebSocket client connected') // Add log
ws.on('message', (message) => {
let data
try {
// Ensure message is treated as string before parsing
data = JSON.parse(message.toString())
console.log('[Server] Received message:', data) // Log parsed data
} catch (e) {
console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e)
return // Ignore non-JSON messages
}
// 识别客户端类型
if (data.type === 'identify') {
if (data.role === 'browser') {
browserConnection = ws
console.log('[Server] Browser identified and connected')
// Notify Electron that the browser is ready
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
console.log('[Server] Sent browser_ready status to Electron')
}
// Notify Electron if it's already connected
if (electronConnection) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' }))
}
ws.on('close', () => {
console.log('[Server] Browser disconnected')
browserConnection = null
// Notify Electron
if (electronConnection) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' }))
}
})
ws.on('error', (error) => {
console.error('[Server] Browser WebSocket error:', error)
browserConnection = null // Assume disconnected on error
if (electronConnection) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
}
})
} else if (data.role === 'electron') {
electronConnection = ws
console.log('[Server] Electron identified and connected')
// If browser is already connected when Electron connects, notify Electron immediately
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
console.log('[Server] Sent initial browser_ready status to Electron')
}
ws.on('close', () => {
console.log('[Server] Electron disconnected')
electronConnection = null
// Maybe send stop to browser if electron disconnects?
// if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' }));
})
ws.on('error', (error) => {
console.error('[Server] Electron WebSocket error:', error)
electronConnection = null // Assume disconnected on error
})
}
}
// Electron 控制开始/停止
else if (data.type === 'start' && ws === electronConnection) {
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying START command to browser')
browserConnection.send(JSON.stringify({ type: 'start' }))
} else {
console.log('[Server] Cannot relay START: Browser not connected')
// Optionally notify Electron back
electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' }))
}
} else if (data.type === 'stop' && ws === electronConnection) {
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying STOP command to browser')
browserConnection.send(JSON.stringify({ type: 'stop' }))
} else {
console.log('[Server] Cannot relay STOP: Browser not connected')
}
} else if (data.type === 'reset' && ws === electronConnection) {
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying RESET command to browser')
browserConnection.send(JSON.stringify({ type: 'reset' }))
} else {
console.log('[Server] Cannot relay RESET: Browser not connected')
}
}
// 浏览器发送识别结果
else if (data.type === 'result' && ws === browserConnection) {
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
// console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed
electronConnection.send(JSON.stringify({ type: 'result', data: data.data }))
} else {
// console.log('[Server] Cannot relay RESULT: Electron not connected');
}
}
// 浏览器发送状态更新 (例如 'stopped')
else if (data.type === 'status' && ws === browserConnection) {
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed
electronConnection.send(JSON.stringify({ type: 'status', message: data.message }))
} else {
console.log('[Server] Cannot relay STATUS: Electron not connected')
}
} else {
console.log('[Server] Received unknown message type or from unknown source:', data)
}
})
ws.on('error', (error) => {
// Generic error handling for connection before identification
console.error('[Server] Initial WebSocket connection error:', error)
// Attempt to clean up based on which connection it might be (if identified)
if (ws === browserConnection) {
browserConnection = null
if (electronConnection)
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
} else if (ws === electronConnection) {
electronConnection = null
}
})
})
// 添加进程错误处理
process.on('uncaughtException', (error) => {
console.error('[Server] Uncaught exception:', error)
// 不立即退出,给日志输出的时间
setTimeout(() => process.exit(1), 1000)
})
process.on('unhandledRejection', (reason, promise) => {
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
})
// 尝试启动服务器
try {
server.listen(port, () => {
console.log(`[Server] Server running at http://localhost:${port}`)
})
// Handle server errors
server.on('error', (error) => {
console.error(`[Server] Failed to start server:`, error)
process.exit(1) // Exit if server fails to start
})
} catch (error) {
console.error('[Server] Critical error starting server:', error)
process.exit(1)
}

View File

@@ -1,114 +0,0 @@
/**
* 独立的ASR服务器
* 这个文件是一个简化版的server.js用于在打包后的应用中运行
*/
// 基本依赖
const http = require('http')
const express = require('express')
const path = require('path')
const fs = require('fs')
// 输出环境信息
console.log('ASR Server starting...')
console.log('Node.js version:', process.version)
console.log('Current directory:', __dirname)
console.log('Current working directory:', process.cwd())
console.log('Command line arguments:', process.argv)
// 创建Express应用
const app = express()
const port = 34515
// 提供静态文件
app.use(express.static(__dirname))
// 提供网页给浏览器
app.get('/', (req, res) => {
try {
// 尝试多个可能的路径
const possiblePaths = [
// 当前目录
path.join(__dirname, 'index.html'),
// 上级目录
path.join(__dirname, '..', 'index.html'),
// 应用根目录
path.join(process.cwd(), 'index.html')
]
console.log('Possible index.html paths:', possiblePaths)
// 查找第一个存在的文件
let indexPath = null
for (const p of possiblePaths) {
try {
if (fs.existsSync(p)) {
indexPath = p
console.log(`Found index.html at: ${p}`)
break
}
} catch (e) {
console.error(`Error checking existence of ${p}:`, e)
}
}
if (indexPath) {
res.sendFile(indexPath)
} else {
// 如果找不到文件返回一个简单的HTML页面
console.error('Could not find index.html, serving fallback page')
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>ASR Server</title>
<style>
body { font-family: sans-serif; padding: 2em; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>ASR Server is running</h1>
<p>This is a fallback page because the index.html file could not be found.</p>
<p>Server is running at: http://localhost:${port}</p>
<p>Current directory: ${__dirname}</p>
<p>Working directory: ${process.cwd()}</p>
</body>
</html>
`)
}
} catch (error) {
console.error('Error serving index.html:', error)
res.status(500).send(`Server error: ${error.message}`)
}
})
// 创建HTTP服务器
const server = http.createServer(app)
// 添加进程错误处理
process.on('uncaughtException', (error) => {
console.error('[Server] Uncaught exception:', error)
// 不立即退出,给日志输出的时间
setTimeout(() => process.exit(1), 1000)
})
process.on('unhandledRejection', (reason, promise) => {
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
})
// 尝试启动服务器
try {
server.listen(port, () => {
console.log(`[Server] Server running at http://localhost:${port}`)
})
// 处理服务器错误
server.on('error', (error) => {
console.error(`[Server] Failed to start server:`, error)
process.exit(1) // Exit if server fails to start
})
} catch (error) {
console.error('[Server] Critical error starting server:', error)
process.exit(1)
}

View File

@@ -1,5 +0,0 @@
@echo off
echo Starting ASR Server...
cd /d %~dp0
node standalone.js
pause

View File

@@ -1,123 +0,0 @@
/**
* 内置的ASR服务器模块
* 这个文件可以直接在Electron中运行不需要外部依赖
*/
// 使用Electron内置的Node.js模块
const http = require('http')
const path = require('path')
const fs = require('fs')
// 输出环境信息
console.log('ASR Server (Embedded) starting...')
console.log('Node.js version:', process.version)
console.log('Current directory:', __dirname)
console.log('Current working directory:', process.cwd())
console.log('Command line arguments:', process.argv)
// 创建HTTP服务器
const server = http.createServer((req, res) => {
try {
if (req.url === '/' || req.url === '/index.html') {
// 尝试多个可能的路径
const possiblePaths = [
// 当前目录
path.join(__dirname, 'index.html'),
// 上级目录
path.join(__dirname, '..', 'index.html'),
// 应用根目录
path.join(process.cwd(), 'index.html')
]
console.log('Possible index.html paths:', possiblePaths)
// 查找第一个存在的文件
let indexPath = null
for (const p of possiblePaths) {
try {
if (fs.existsSync(p)) {
indexPath = p
console.log(`Found index.html at: ${p}`)
break
}
} catch (e) {
console.error(`Error checking existence of ${p}:`, e)
}
}
if (indexPath) {
// 读取文件内容并发送
fs.readFile(indexPath, (err, data) => {
if (err) {
console.error('Error reading index.html:', err)
res.writeHead(500)
res.end('Error reading index.html')
return
}
res.writeHead(200, { 'Content-Type': 'text/html' })
res.end(data)
})
} else {
// 如果找不到文件返回一个简单的HTML页面
console.error('Could not find index.html, serving fallback page')
res.writeHead(200, { 'Content-Type': 'text/html' })
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>ASR Server</title>
<style>
body { font-family: sans-serif; padding: 2em; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>ASR Server is running</h1>
<p>This is a fallback page because the index.html file could not be found.</p>
<p>Server is running at: http://localhost:34515</p>
<p>Current directory: ${__dirname}</p>
<p>Working directory: ${process.cwd()}</p>
</body>
</html>
`)
}
} else {
// 处理其他请求
res.writeHead(404)
res.end('Not found')
}
} catch (error) {
console.error('Error handling request:', error)
res.writeHead(500)
res.end('Server error')
}
})
// 添加进程错误处理
process.on('uncaughtException', (error) => {
console.error('[Server] Uncaught exception:', error)
// 不立即退出,给日志输出的时间
setTimeout(() => process.exit(1), 1000)
})
process.on('unhandledRejection', (reason, promise) => {
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
})
// 尝试启动服务器
try {
const port = 34515
server.listen(port, () => {
console.log(`[Server] Server running at http://localhost:${port}`)
})
// 处理服务器错误
server.on('error', (error) => {
console.error(`[Server] Failed to start server:`, error)
process.exit(1) // Exit if server fails to start
})
} catch (error) {
console.error('[Server] Critical error starting server:', error)
process.exit(1)
}

View File

@@ -1,425 +0,0 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cherry Studio ASR</title>
<style>
body {
font-family: sans-serif;
padding: 1em;
}
#status {
margin-top: 1em;
font-style: italic;
color: #555;
}
#result {
margin-top: 0.5em;
border: 1px solid #ccc;
padding: 0.5em;
min-height: 50px;
background: #f9f9f9;
}
</style>
</head>
<body>
<h1>浏览器语音识别中继页面</h1>
<p>这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。</p>
<div id="status">正在连接到服务器...</div>
<div id="result"></div>
<script>
const statusDiv = document.getElementById('status');
const resultDiv = document.getElementById('result');
// 尝试连接到WebSocket服务器
let ws;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
const reconnectInterval = 2000; // 2秒
function connectWebSocket() {
try {
ws = new WebSocket('ws://localhost:34515');
ws.onopen = () => {
reconnectAttempts = 0;
updateStatus('已连接到服务器,等待指令...');
ws.send(JSON.stringify({ type: 'identify', role: 'browser' }));
};
ws.onmessage = handleMessage;
ws.onerror = (error) => {
console.error('[Browser Page] WebSocket Error:', error);
updateStatus('WebSocket 连接错误!请检查服务器是否运行。');
};
ws.onclose = () => {
console.log('[Browser Page] WebSocket Connection Closed');
updateStatus('与服务器断开连接。尝试重新连接...');
stopRecognition();
// 尝试重新连接
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
updateStatus(`与服务器断开连接。尝试重新连接 (${reconnectAttempts}/${maxReconnectAttempts})...`);
setTimeout(connectWebSocket, reconnectInterval);
} else {
updateStatus('无法连接到服务器。请刷新页面或重启应用。');
}
};
} catch (error) {
console.error('[Browser Page] Error creating WebSocket:', error);
updateStatus('创建WebSocket连接时出错。请刷新页面或重启应用。');
}
}
// 初始连接
connectWebSocket();
let recognition = null;
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
function updateStatus(message) {
console.log(`[Browser Page Status] ${message}`);
statusDiv.textContent = message;
}
function handleMessage(event) {
let data;
try {
data = JSON.parse(event.data);
console.log('[Browser Page] Received command:', data);
} catch (e) {
console.error('[Browser Page] Received non-JSON message:', event.data);
return;
}
if (data.type === 'start') {
startRecognition();
} else if (data.type === 'stop') {
stopRecognition();
} else if (data.type === 'reset') {
// 强制重置语音识别
forceResetRecognition();
} else {
console.warn('[Browser Page] Received unknown command type:', data.type);
}
};
function setupRecognition() {
if (!SpeechRecognition) {
updateStatus('错误:此浏览器不支持 Web Speech API。');
return false;
}
if (recognition && recognition.recognizing) {
console.log('[Browser Page] Recognition already active.');
return true;
}
recognition = new SpeechRecognition();
recognition.lang = 'zh-CN';
recognition.continuous = true;
recognition.interimResults = true;
// 增加以下设置提高语音识别的可靠性
recognition.maxAlternatives = 3; // 返回多个可能的识别结果
// 设置较短的语音识别时间,使用户能更快地看到结果
// 注意:这个属性不是标准的,可能不是所有浏览器都支持
try {
// @ts-ignore
recognition.audioStart = 0.1; // 尝试设置较低的起始音量阈值
} catch (e) {
console.log('[Browser Page] audioStart property not supported');
}
recognition.onstart = () => {
updateStatus("🎤 正在识别...");
console.log('[Browser Page] SpeechRecognition started.');
};
recognition.onresult = (event) => {
console.log('[Browser Page] Recognition result event:', event);
let interim_transcript = '';
let final_transcript = '';
// 输出识别结果的详细信息便于调试
for (let i = event.resultIndex; i < event.results.length; ++i) {
const confidence = event.results[i][0].confidence;
console.log(`[Browser Page] Result ${i}: ${event.results[i][0].transcript} (Confidence: ${confidence.toFixed(2)})`);
if (event.results[i].isFinal) {
final_transcript += event.results[i][0].transcript;
} else {
interim_transcript += event.results[i][0].transcript;
}
}
const resultText = final_transcript || interim_transcript;
resultDiv.textContent = resultText;
// 更新状态显示
if (resultText) {
updateStatus(`🎤 正在识别... (已捕捉到语音)`);
}
if (ws.readyState === WebSocket.OPEN) {
console.log(`[Browser Page] Sending ${final_transcript ? 'final' : 'interim'} result to server:`, resultText);
ws.send(JSON.stringify({ type: 'result', data: { text: resultText, isFinal: !!final_transcript } }));
}
};
recognition.onerror = (event) => {
console.error(`[Browser Page] SpeechRecognition Error - Type: ${event.error}, Message: ${event.message}`);
// 根据错误类型提供更友好的错误提示
let errorMessage = '';
switch (event.error) {
case 'no-speech':
errorMessage = '未检测到语音,请确保麦克风工作正常并尝试说话。';
// 尝试重新启动语音识别
setTimeout(() => {
if (recognition) {
try {
recognition.start();
console.log('[Browser Page] Restarting recognition after no-speech error');
} catch (e) {
console.error('[Browser Page] Failed to restart recognition:', e);
}
}
}, 1000);
break;
case 'audio-capture':
errorMessage = '无法捕获音频,请确保麦克风已连接并已授权。';
break;
case 'not-allowed':
errorMessage = '浏览器不允许使用麦克风,请检查权限设置。';
break;
case 'network':
errorMessage = '网络错误导致语音识别失败。';
break;
case 'aborted':
errorMessage = '语音识别被用户或系统中止。';
break;
default:
errorMessage = `识别错误: ${event.error}`;
}
updateStatus(`错误: ${errorMessage}`);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'error',
data: {
error: event.error,
message: errorMessage || event.message || `Recognition error: ${event.error}`
}
}));
}
};
recognition.onend = () => {
console.log('[Browser Page] SpeechRecognition ended.');
// 检查是否是由于错误或用户手动停止导致的结束
const isErrorOrStopped = statusDiv.textContent.includes('错误') || statusDiv.textContent.includes('停止');
if (!isErrorOrStopped) {
// 如果不是由于错误或手动停止,则自动重新启动语音识别
updateStatus("识别暂停,正在重新启动...");
// 保存当前的recognition对象
const currentRecognition = recognition;
// 尝试重新启动语音识别
setTimeout(() => {
try {
if (currentRecognition && currentRecognition === recognition) {
currentRecognition.start();
console.log('[Browser Page] Automatically restarting recognition');
} else {
// 如果recognition对象已经变化重新创建一个
setupRecognition();
if (recognition) {
recognition.start();
console.log('[Browser Page] Created new recognition instance and started');
}
}
} catch (e) {
console.error('[Browser Page] Failed to restart recognition:', e);
updateStatus("识别已停止。等待指令...");
}
}, 300);
} else {
updateStatus("识别已停止。等待指令...");
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'status', message: 'stopped' }));
}
// 只有在手动停止或错误时才重置recognition对象
recognition = null;
}
};
return true;
}
function startRecognition() {
if (!SpeechRecognition) {
updateStatus('错误:浏览器不支持 Web Speech API。');
return;
}
// 显示正在准备的状态
updateStatus('正在准备麦克风...');
if (recognition) {
console.log('[Browser Page] Recognition already exists, stopping first.');
stopRecognition();
}
if (!setupRecognition()) return;
console.log('[Browser Page] Attempting to start recognition...');
try {
// 设置更长的超时时间,确保有足够的时间获取麦克风权限
const micPermissionTimeout = setTimeout(() => {
updateStatus('获取麦克风权限超时,请刷新页面重试。');
}, 10000); // 10秒超时
navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
})
.then(stream => {
clearTimeout(micPermissionTimeout);
console.log('[Browser Page] Microphone access granted.');
// 检查麦克风音量级别
const audioContext = new AudioContext();
const analyser = audioContext.createAnalyser();
const microphone = audioContext.createMediaStreamSource(stream);
const javascriptNode = audioContext.createScriptProcessor(2048, 1, 1);
analyser.smoothingTimeConstant = 0.8;
analyser.fftSize = 1024;
microphone.connect(analyser);
analyser.connect(javascriptNode);
javascriptNode.connect(audioContext.destination);
javascriptNode.onaudioprocess = function () {
const array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);
let values = 0;
const length = array.length;
for (let i = 0; i < length; i++) {
values += (array[i]);
}
const average = values / length;
console.log('[Browser Page] Microphone volume level:', average);
// 如果音量太低,显示提示
if (average < 5) {
updateStatus('麦克风音量很低,请说话或检查麦克风设置。');
} else {
updateStatus('🎤 正在识别...');
}
// 只检查一次就断开连接
microphone.disconnect();
analyser.disconnect();
javascriptNode.disconnect();
};
// 释放测试用的音频流
setTimeout(() => {
stream.getTracks().forEach(track => track.stop());
audioContext.close();
}, 1000);
// 启动语音识别
if (recognition) {
recognition.start();
updateStatus('🎤 正在识别...');
} else {
updateStatus('错误Recognition 实例丢失。');
console.error('[Browser Page] Recognition instance lost before start.');
}
})
.catch(err => {
clearTimeout(micPermissionTimeout);
console.error('[Browser Page] Microphone access error:', err);
let errorMsg = `无法访问麦克风 (${err.name})`;
if (err.name === 'NotAllowedError') {
errorMsg = '麦克风访问被拒绝。请在浏览器设置中允许麦克风访问权限。';
} else if (err.name === 'NotFoundError') {
errorMsg = '未找到麦克风设备。请确保麦克风已连接。';
}
updateStatus(`错误: ${errorMsg}`);
recognition = null;
});
} catch (e) {
console.error('[Browser Page] Error calling recognition.start():', e);
updateStatus(`启动识别时出错: ${e.message}`);
recognition = null;
}
}
function stopRecognition() {
if (recognition) {
console.log('[Browser Page] Stopping recognition...');
updateStatus("正在停止识别...");
try {
recognition.stop();
} catch (e) {
console.error('[Browser Page] Error calling recognition.stop():', e);
recognition = null;
updateStatus("停止时出错,已强制重置。");
}
} else {
console.log('[Browser Page] Recognition not active, nothing to stop.');
updateStatus("识别未运行。");
}
}
function forceResetRecognition() {
console.log('[Browser Page] Force resetting recognition...');
updateStatus("强制重置语音识别...");
// 先尝试停止当前的识别
if (recognition) {
try {
recognition.stop();
} catch (e) {
console.error('[Browser Page] Error stopping recognition during reset:', e);
}
}
// 强制设置为null丢弃所有后续结果
recognition = null;
// 通知服务器已重置
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'status', message: 'reset_complete' }));
}
updateStatus("语音识别已重置,等待新指令。");
}
</script>
</body>
</html>

View File

@@ -1,854 +0,0 @@
{
"name": "cherry-asr-server",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cherry-asr-server",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.2",
"ws": "^8.13.0"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@@ -1,10 +0,0 @@
{
"name": "cherry-asr-server",
"version": "1.0.0",
"description": "Cherry Studio ASR Server",
"main": "server.js",
"dependencies": {
"express": "^4.18.2",
"ws": "^8.13.0"
}
}

View File

@@ -1,269 +0,0 @@
// 检查依赖项
try {
console.log('ASR Server starting...')
console.log('Node.js version:', process.version)
console.log('Current directory:', __dirname)
console.log('Current working directory:', process.cwd())
console.log('Command line arguments:', process.argv)
// 检查必要的依赖项
const checkDependency = (name) => {
try {
require(name) // Removed unused variable 'module'
console.log(`Successfully loaded dependency: ${name}`)
return true
} catch (error) {
console.error(`Failed to load dependency: ${name}`, error.message)
return false
}
}
// 检查所有必要的依赖项
const dependencies = ['http', 'ws', 'express', 'path', 'fs']
const missingDeps = dependencies.filter((dep) => !checkDependency(dep))
if (missingDeps.length > 0) {
console.error(`Missing dependencies: ${missingDeps.join(', ')}. Server cannot start.`)
process.exit(1)
}
} catch (error) {
console.error('Error during dependency check:', error)
process.exit(1)
}
// 加载依赖项
const http = require('http')
const WebSocket = require('ws')
const express = require('express')
const path = require('path') // Need path module
// const fs = require('fs') // Commented out unused import 'fs'
const app = express()
const port = 34515 // Define the port
// 获取index.html文件的路径
function getIndexHtmlPath() {
const fs = require('fs')
console.log('Current directory:', __dirname)
console.log('Current working directory:', process.cwd())
// 尝试多个可能的路径
const possiblePaths = [
// 开发环境路径
path.join(__dirname, 'index.html'),
// 当前目录
path.join(process.cwd(), 'index.html'),
// 相对于可执行文件的路径
path.join(path.dirname(process.execPath), 'index.html'),
// 相对于可执行文件的上级目录的路径
path.join(path.dirname(path.dirname(process.execPath)), 'index.html'),
// 相对于可执行文件的resources目录的路径
path.join(path.dirname(process.execPath), 'resources', 'index.html'),
// 相对于可执行文件的resources/asr-server目录的路径
path.join(path.dirname(process.execPath), 'resources', 'asr-server', 'index.html'),
// 相对于可执行文件的asr-server目录的路径
path.join(path.dirname(process.execPath), 'asr-server', 'index.html'),
// 如果是pkg打包环境
process.pkg ? path.join(path.dirname(process.execPath), 'index.html') : null
].filter(Boolean) // 过滤掉null值
console.log('Possible index.html paths:', possiblePaths)
// 检查每个路径,返回第一个存在的文件
for (const p of possiblePaths) {
try {
if (fs.existsSync(p)) {
console.log(`Found index.html at: ${p}`)
return p
}
} catch (e) {
console.error(`Error checking existence of ${p}:`, e)
}
}
// 如果没有找到文件,返回默认路径并记录错误
console.error('Could not find index.html in any of the expected locations')
return path.join(__dirname, 'index.html') // 返回默认路径,即使它可能不存在
}
// 提供网页给浏览器
app.get('/', (req, res) => {
try {
const indexPath = getIndexHtmlPath()
console.log(`Serving index.html from: ${indexPath}`)
// 检查文件是否存在
const fs = require('fs')
if (!fs.existsSync(indexPath)) {
console.error(`Error: index.html not found at ${indexPath}`)
return res.status(404).send(`Error: index.html not found at ${indexPath}. <br>Please check the server logs.`)
}
res.sendFile(indexPath, (err) => {
if (err) {
console.error('Error sending index.html:', err)
res.status(500).send(`Error serving index.html: ${err.message}`)
}
})
} catch (error) {
console.error('Error in route handler:', error)
res.status(500).send(`Server error: ${error.message}`)
}
})
const server = http.createServer(app)
const wss = new WebSocket.Server({ server })
let browserConnection = null
let electronConnection = null
wss.on('connection', (ws) => {
console.log('[Server] WebSocket client connected') // Add log
ws.on('message', (message) => {
let data
try {
// Ensure message is treated as string before parsing
data = JSON.parse(message.toString())
console.log('[Server] Received message:', data) // Log parsed data
} catch (e) {
console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e)
return // Ignore non-JSON messages
}
// 识别客户端类型
if (data.type === 'identify') {
if (data.role === 'browser') {
browserConnection = ws
console.log('[Server] Browser identified and connected')
// Notify Electron that the browser is ready
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
console.log('[Server] Sent browser_ready status to Electron')
}
// Notify Electron if it's already connected
if (electronConnection) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' }))
}
ws.on('close', () => {
console.log('[Server] Browser disconnected')
browserConnection = null
// Notify Electron
if (electronConnection) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' }))
}
})
ws.on('error', (error) => {
console.error('[Server] Browser WebSocket error:', error)
browserConnection = null // Assume disconnected on error
if (electronConnection) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
}
})
} else if (data.role === 'electron') {
electronConnection = ws
console.log('[Server] Electron identified and connected')
// If browser is already connected when Electron connects, notify Electron immediately
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
console.log('[Server] Sent initial browser_ready status to Electron')
}
ws.on('close', () => {
console.log('[Server] Electron disconnected')
electronConnection = null
// Maybe send stop to browser if electron disconnects?
// if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' }));
})
ws.on('error', (error) => {
console.error('[Server] Electron WebSocket error:', error)
electronConnection = null // Assume disconnected on error
})
}
}
// Electron 控制开始/停止
else if (data.type === 'start' && ws === electronConnection) {
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying START command to browser')
browserConnection.send(JSON.stringify({ type: 'start' }))
} else {
console.log('[Server] Cannot relay START: Browser not connected')
// Optionally notify Electron back
electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' }))
}
} else if (data.type === 'stop' && ws === electronConnection) {
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying STOP command to browser')
browserConnection.send(JSON.stringify({ type: 'stop' }))
} else {
console.log('[Server] Cannot relay STOP: Browser not connected')
}
} else if (data.type === 'reset' && ws === electronConnection) {
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying RESET command to browser')
browserConnection.send(JSON.stringify({ type: 'reset' }))
} else {
console.log('[Server] Cannot relay RESET: Browser not connected')
}
}
// 浏览器发送识别结果
else if (data.type === 'result' && ws === browserConnection) {
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
// console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed
electronConnection.send(JSON.stringify({ type: 'result', data: data.data }))
} else {
// console.log('[Server] Cannot relay RESULT: Electron not connected');
}
}
// 浏览器发送状态更新 (例如 'stopped')
else if (data.type === 'status' && ws === browserConnection) {
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed
electronConnection.send(JSON.stringify({ type: 'status', message: data.message }))
} else {
console.log('[Server] Cannot relay STATUS: Electron not connected')
}
} else {
console.log('[Server] Received unknown message type or from unknown source:', data)
}
})
ws.on('error', (error) => {
// Generic error handling for connection before identification
console.error('[Server] Initial WebSocket connection error:', error)
// Attempt to clean up based on which connection it might be (if identified)
if (ws === browserConnection) {
browserConnection = null
if (electronConnection)
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
} else if (ws === electronConnection) {
electronConnection = null
}
})
})
// 添加进程错误处理
process.on('uncaughtException', (error) => {
console.error('[Server] Uncaught exception:', error)
// 不立即退出,给日志输出的时间
setTimeout(() => process.exit(1), 1000)
})
process.on('unhandledRejection', (reason, promise) => {
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
})
// 尝试启动服务器
try {
server.listen(port, () => {
console.log(`[Server] Server running at http://localhost:${port}`)
})
// Handle server errors
server.on('error', (error) => {
console.error(`[Server] Failed to start server:`, error)
process.exit(1) // Exit if server fails to start
})
} catch (error) {
console.error('[Server] Critical error starting server:', error)
process.exit(1)
}

View File

@@ -1,114 +0,0 @@
/**
* 独立的ASR服务器
* 这个文件是一个简化版的server.js用于在打包后的应用中运行
*/
// 基本依赖
const http = require('http')
const express = require('express')
const path = require('path')
const fs = require('fs')
// 输出环境信息
console.log('ASR Server starting...')
console.log('Node.js version:', process.version)
console.log('Current directory:', __dirname)
console.log('Current working directory:', process.cwd())
console.log('Command line arguments:', process.argv)
// 创建Express应用
const app = express()
const port = 34515
// 提供静态文件
app.use(express.static(__dirname))
// 提供网页给浏览器
app.get('/', (req, res) => {
try {
// 尝试多个可能的路径
const possiblePaths = [
// 当前目录
path.join(__dirname, 'index.html'),
// 上级目录
path.join(__dirname, '..', 'index.html'),
// 应用根目录
path.join(process.cwd(), 'index.html')
]
console.log('Possible index.html paths:', possiblePaths)
// 查找第一个存在的文件
let indexPath = null
for (const p of possiblePaths) {
try {
if (fs.existsSync(p)) {
indexPath = p
console.log(`Found index.html at: ${p}`)
break
}
} catch (e) {
console.error(`Error checking existence of ${p}:`, e)
}
}
if (indexPath) {
res.sendFile(indexPath)
} else {
// 如果找不到文件返回一个简单的HTML页面
console.error('Could not find index.html, serving fallback page')
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>ASR Server</title>
<style>
body { font-family: sans-serif; padding: 2em; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>ASR Server is running</h1>
<p>This is a fallback page because the index.html file could not be found.</p>
<p>Server is running at: http://localhost:${port}</p>
<p>Current directory: ${__dirname}</p>
<p>Working directory: ${process.cwd()}</p>
</body>
</html>
`)
}
} catch (error) {
console.error('Error serving index.html:', error)
res.status(500).send(`Server error: ${error.message}`)
}
})
// 创建HTTP服务器
const server = http.createServer(app)
// 添加进程错误处理
process.on('uncaughtException', (error) => {
console.error('[Server] Uncaught exception:', error)
// 不立即退出,给日志输出的时间
setTimeout(() => process.exit(1), 1000)
})
process.on('unhandledRejection', (reason, promise) => {
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
})
// 尝试启动服务器
try {
server.listen(port, () => {
console.log(`[Server] Server running at http://localhost:${port}`)
})
// 处理服务器错误
server.on('error', (error) => {
console.error(`[Server] Failed to start server:`, error)
process.exit(1) // Exit if server fails to start
})
} catch (error) {
console.error('[Server] Critical error starting server:', error)
process.exit(1)
}

View File

@@ -1,5 +0,0 @@
@echo off
echo Starting ASR Server...
cd /d %~dp0
node standalone.js
pause

View File

@@ -14,35 +14,76 @@
<div class="mb-12">
<h1 class="text-3xl font-bold mb-8 text-gray-900">许可协议</h1>
<p class="mb-6 text-gray-700">采用 Apache License 2.0 修改版许可,并附加以下条件:</p>
<p class="mb-6 text-gray-700">本项目采用<strong>区分用户的双重许可 (User-Segmented Dual Licensing)</strong> 模式。</p>
<section class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-gray-900">一. 商用许可</h2>
<p class="mb-4 text-gray-700">在以下任何一种情况下,您需要联系我们并获得明确的书面商业授权后,方可继续使用 Cherry Studio 材料:</p>
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
<li><strong>修改与衍生</strong> 您对 Cherry Studio 材料进行修改或基于其进行衍生开发包括但不限于修改应用名称、Logo、代码、功能、界面数据等</li>
<li><strong>企业服务</strong> 在您的企业内部,或为企业客户提供基于 Cherry Studio 的服务,且该服务支持 10 人及以上累计用户使用。</li>
<li><strong>硬件捆绑销售</strong> 您将 Cherry Studio 预装或集成到硬件设备或产品中进行捆绑销售。</li>
<li><strong>政府或教育机构大规模采购</strong> 您的使用场景属于政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。</li>
<li><strong>面向公众的公有云服务</strong>:基于 Cherry Studio提供面向公众的公有云服务。</li>
</ol>
<h2 class="text-xl font-semibold mb-4 text-gray-900">核心原则</h2>
<ul class="list-disc pl-6 space-y-2 text-gray-700">
<li><strong>个人用户 和 10人及以下企业/组织:</strong> 默认适用 <strong>GNU Affero 通用公共许可证 v3.0 (AGPLv3)</strong></li>
<li><strong>超过10人的企业/组织:</strong> <strong>必须</strong> 获取 <strong>商业许可证 (Commercial License)</strong></li>
</ul>
</section>
<section class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-gray-900">二. 贡献者协议</h2>
<p class="mb-4 text-gray-700">作为 Cherry Studio 的贡献者,您应当同意以下条款:</p>
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
<li><strong>许可调整</strong>:生产者有权根据需要对开源协议进行调整,使其更加严格或宽松。</li>
<li><strong>商业用途</strong>:您贡献的代码可能会被用于商业用途,包括但不限于云业务运营。</li>
</ol>
<h2 class="text-xl font-semibold mb-4 text-gray-900">定义:"10人及以下"</h2>
<p class="text-gray-700">
指在您的组织包括公司、非营利组织、政府机构、教育机构等任何实体能够访问、使用或以任何方式直接或间接受益于本软件Cherry
Studio功能的个人总数不超过10人。这包括但不限于开发者、测试人员、运营人员、最终用户、通过集成系统间接使用者等。
</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-gray-900">. 其他条款</h2>
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
<li>本协议条款的解释权归 Cherry Studio 开发者所有。</li>
<li>本协议可能根据实际情况进行更新,更新时将通过本软件通知用户。</li>
</ol>
<h2 class="text-xl font-semibold mb-4 text-gray-900">1. 开源许可证 (Open Source License): AGPLv3 - 适用于个人及10人及以下组织
</h2>
<ul class="list-disc pl-6 space-y-2 text-gray-700">
<li>如果您是个人用户,或者您的组织满足上述"10人及以下"的定义,您可以在 <strong>AGPLv3</strong> 的条款下自由使用、修改和分发 Cherry Studio。AGPLv3 的完整文本可以访问
<a href="https://www.gnu.org/licenses/agpl-3.0.html"
class="text-blue-600 hover:underline">https://www.gnu.org/licenses/agpl-3.0.html</a> 获取。
</li>
<li><strong>核心义务:</strong> AGPLv3 的一个关键要求是,如果您修改了 Cherry Studio 并通过网络提供服务,或者分发了修改后的版本,您必须以 AGPLv3
许可证向接收者提供相应的<strong>完整源代码</strong>。即使您符合"10人及以下"的标准,如果您希望避免此源代码公开义务,您也需要考虑获取商业许可证(见下文)。</li>
<li>使用前请务必仔细阅读并理解 AGPLv3 的所有条款。</li>
</ul>
</section>
<section class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-gray-900">2. 商业许可证 (Commercial License) - 适用于超过10人的组织或希望规避 AGPLv3
义务的用户</h2>
<ul class="list-disc pl-6 space-y-2 text-gray-700">
<li><strong>强制要求:</strong>
如果您的组织<strong></strong>满足上述"10人及以下"的定义即有11人或更多人可以访问、使用或受益于本软件<strong>必须</strong>联系我们获取并签署一份商业许可证才能使用
Cherry Studio。</li>
<li><strong>自愿选择:</strong> 即使您的组织满足"10人及以下"的条件,但如果您的使用场景<strong>无法满足 AGPLv3
的条款要求</strong>(特别是关于<strong>源代码公开</strong>的义务),或者您需要 AGPLv3 <strong>未提供</strong>的特定商业条款(如保证、赔偿、无 Copyleft
限制等),您也<strong>必须</strong>联系我们获取并签署一份商业许可证。</li>
<li><strong>需要商业许可证的常见情况包括(但不限于):</strong>
<ul class="list-disc pl-6 mt-2 space-y-1">
<li>您的组织规模超过10人。</li>
<li>(无论组织规模)您希望分发修改过的 Cherry Studio 版本,但<strong>不希望</strong>根据 AGPLv3 公开您修改部分的源代码。</li>
<li>(无论组织规模)您希望基于修改过的 Cherry Studio 提供网络服务SaaS<strong>不希望</strong>根据 AGPLv3 向服务使用者提供修改后的源代码。</li>
<li>(无论组织规模)您的公司政策、客户合同或项目要求不允许使用 AGPLv3 许可的软件,或要求闭源分发及保密。</li>
</ul>
</li>
<li><strong>获取商业许可:</strong> 请通过邮箱 <a href="mailto:bd@cherry-ai.com"
class="text-blue-600 hover:underline">bd@cherry-ai.com</a> 联系 Cherry Studio 开发团队洽谈商业授权事宜。</li>
</ul>
</section>
<section class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-gray-900">3. 贡献 (Contributions)</h2>
<ul class="list-disc pl-6 space-y-2 text-gray-700">
<li>我们欢迎社区对 Cherry Studio 的贡献。所有向本项目提交的贡献都将被视为在 <strong>AGPLv3</strong> 许可证下提供。</li>
<li>通过向本项目提交贡献(例如通过 Pull Request即表示您同意您的代码以 AGPLv3 许可证授权给本项目及所有后续使用者(无论这些使用者最终遵循 AGPLv3 还是商业许可)。</li>
<li>您也理解并同意,您的贡献可能会被包含在根据商业许可证分发的 Cherry Studio 版本中。</li>
</ul>
</section>
<section class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-gray-900">4. 其他条款 (Other Terms)</h2>
<ul class="list-disc pl-6 space-y-2 text-gray-700">
<li>关于商业许可证的具体条款和条件,以双方签署的正式商业许可协议为准。</li>
<li>项目维护者保留根据需要更新本许可政策(包括用户规模定义和阈值)的权利。相关更新将通过项目官方渠道(如代码仓库、官方网站)进行通知。</li>
</ul>
</section>
</div>
@@ -50,58 +91,107 @@
<!-- English Version -->
<div>
<h1 class="text-3xl font-bold mb-8 text-gray-900">License Agreement</h1>
<h1 class="text-3xl font-bold mb-8 text-gray-900">Licensing</h1>
<p class="mb-6 text-gray-700">This software is licensed under a modified version of the Apache License 2.0, with
the following additional conditions.</p>
<p class="mb-6 text-gray-700">This project employs a <strong>User-Segmented Dual Licensing</strong> model.</p>
<section class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-gray-900">I. Commercial Licensing</h2>
<p class="mb-4 text-gray-700">You must contact us and obtain explicit written commercial authorization to
continue using Cherry Studio materials under any of the following circumstances:</p>
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
<li><strong>Modifications and Derivatives:</strong> You modify Cherry Studio materials or perform derivative
development based on them (including but not limited to changing the application's name, logo, code,
functionality, user interface, data, etc.).</li>
<li><strong>Enterprise Services:</strong> You use Cherry Studio internally within your enterprise, or you
provide Cherry Studio-based services for enterprise customers, and such services support cumulative usage by
10 or more users.</li>
<li><strong>Hardware Bundling and Sales:</strong> You pre-install or integrate Cherry Studio into hardware
devices or products for bundled sale.</li>
<li><strong>Large-scale Procurement by Government or Educational Institutions:</strong> Your usage scenario
involves large-scale procurement projects by government or educational institutions, especially in cases
involving sensitive requirements such as security and data privacy.</li>
<li><strong>Public Cloud Services:</strong> You provide public cloud-based product services utilizing Cherry
<h2 class="text-xl font-semibold mb-4 text-gray-900">Core Principle</h2>
<ul class="list-disc pl-6 space-y-2 text-gray-700">
<li><strong>Individual Users and Organizations with 10 or Fewer Individuals:</strong> Governed by default
under the <strong>GNU Affero General Public License v3.0 (AGPLv3)</strong>.</li>
<li><strong>Organizations with More Than 10 Individuals:</strong> <strong>Must</strong> obtain a
<strong>Commercial License</strong>.
</li>
</ul>
</section>
<section class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-gray-900">Definition: "10 or Fewer Individuals"</h2>
<p class="text-gray-700">
Refers to any organization (including companies, non-profits, government agencies, educational institutions,
etc.) where the total number of individuals who can access, use, or in any way directly or indirectly benefit
from the functionality of this software (Cherry Studio) does not exceed 10. This includes, but is not limited
to, developers, testers, operations staff, end-users, and indirect users via integrated systems.
</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-gray-900">1. Open Source License: AGPLv3 - For Individuals and
Organizations of 10 or Fewer</h2>
<ul class="list-disc pl-6 space-y-2 text-gray-700">
<li>If you are an individual user, or if your organization meets the "10 or Fewer Individuals" definition
above, you are free to use, modify, and distribute Cherry Studio under the terms of the
<strong>AGPLv3</strong>. The full text of the AGPLv3 can be found at <a
href="https://www.gnu.org/licenses/agpl-3.0.html"
class="text-blue-600 hover:underline">https://www.gnu.org/licenses/agpl-3.0.html</a>.
</li>
<li><strong>Core Obligation:</strong> A key requirement of the AGPLv3 is that if you modify Cherry Studio and
make it available over a network, or distribute the modified version, you must provide the <strong>complete
corresponding source code</strong> under the AGPLv3 license to the recipients. Even if you qualify under
the "10 or Fewer Individuals" rule, if you wish to avoid this source code disclosure obligation, you will
need to obtain a Commercial License (see below).</li>
<li>Please read and understand the full terms of the AGPLv3 carefully before use.</li>
</ul>
</section>
<section class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-gray-900">2. Commercial License - For Organizations with More Than 10
Individuals, or Users Needing to Avoid AGPLv3 Obligations</h2>
<ul class="list-disc pl-6 space-y-2 text-gray-700">
<li><strong>Mandatory Requirement:</strong> If your organization does <strong>not</strong> meet the "10 or
Fewer Individuals" definition above (i.e., 11 or more individuals can access, use, or benefit from the
software), you <strong>must</strong> contact us to obtain and execute a Commercial License to use Cherry
Studio.</li>
</ol>
<li><strong>Voluntary Option:</strong> Even if your organization meets the "10 or Fewer Individuals"
condition, if your intended use case <strong>cannot comply with the terms of the AGPLv3</strong>
(particularly the obligations regarding <strong>source code disclosure</strong>), or if you require specific
commercial terms <strong>not offered</strong> by the AGPLv3 (such as warranties, indemnities, or freedom
from copyleft restrictions), you also <strong>must</strong> contact us to obtain and execute a Commercial
License.</li>
<li><strong>Common scenarios requiring a Commercial License include (but are not limited to):</strong>
<ul class="list-disc pl-6 mt-2 space-y-1">
<li>Your organization has more than 10 individuals who can access, use, or benefit from the software.</li>
<li>(Regardless of organization size) You wish to distribute a modified version of Cherry Studio but
<strong>do not want</strong> to disclose the source code of your modifications under AGPLv3.
</li>
<li>(Regardless of organization size) You wish to provide a network service (SaaS) based on a modified
version of Cherry Studio but <strong>do not want</strong> to provide the modified source code to users
of the service under AGPLv3.</li>
<li>(Regardless of organization size) Your corporate policies, client contracts, or project requirements
prohibit the use of AGPLv3-licensed software or mandate closed-source distribution and confidentiality.
</li>
</ul>
</li>
<li><strong>Obtaining a Commercial License:</strong> Please contact the Cherry Studio development team via
email at <a href="mailto:bd@cherry-ai.com" class="text-blue-600 hover:underline">bd@cherry-ai.com</a> to
discuss commercial licensing options.</li>
</ul>
</section>
<section class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-gray-900">II. Contributor Agreement</h2>
<p class="mb-4 text-gray-700">As a contributor to Cherry Studio, you must agree to the following terms:</p>
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
<li><strong>License Adjustments:</strong> The producer reserves the right to adjust the open-source license as
necessary, making it more strict or permissive.</li>
<li><strong>Commercial Usage:</strong> Your contributed code may be used commercially, including but not
limited to cloud business operations.</li>
</ol>
<h2 class="text-xl font-semibold mb-4 text-gray-900">3. Contributions</h2>
<ul class="list-disc pl-6 space-y-2 text-gray-700">
<li>We welcome community contributions to Cherry Studio. All contributions submitted to this project are
considered to be offered under the <strong>AGPLv3</strong> license.</li>
<li>By submitting a contribution to this project (e.g., via a Pull Request), you agree to license your code
under the AGPLv3 to the project and all its downstream users (regardless of whether those users ultimately
operate under AGPLv3 or a Commercial License).</li>
<li>You also understand and agree that your contribution may be included in distributions of Cherry Studio
offered under our commercial license.</li>
</ul>
</section>
<section class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-gray-900">III. Other Terms</h2>
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
<li>Cherry Studio developers reserve the right of final interpretation of these agreement terms.</li>
<li>This agreement may be updated according to practical circumstances, and users will be notified of updates
through this software.</li>
</ol>
<h2 class="text-xl font-semibold mb-4 text-gray-900">4. Other Terms</h2>
<ul class="list-disc pl-6 space-y-2 text-gray-700">
<li>The specific terms and conditions of the Commercial License are governed by the formal commercial license
agreement signed by both parties.</li>
<li>The project maintainers reserve the right to update this licensing policy (including the definition and
threshold for user count) as needed. Updates will be communicated through official project channels (e.g.,
code repository, official website).</li>
</ul>
</section>
<p class="mt-8 text-gray-700">
Other than these specific conditions, all remaining rights and restrictions follow the Apache License 2.0. For
more detailed information regarding Apache License 2.0, please visit
<a href="http://www.apache.org/licenses/LICENSE-2.0"
class="text-blue-600 hover:underline">http://www.apache.org/licenses/LICENSE-2.0</a>.
</p>
</div>
</div>
</body>

View File

@@ -1,10 +1,8 @@
const { Arch } = require('electron-builder')
const { default: removeLocales } = require('./remove-locales')
const fs = require('fs')
const path = require('path')
exports.default = async function (context) {
await removeLocales(context)
const platform = context.packager.platform.name
const arch = context.arch

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 = 'zh-CN'
var baseFileName = ''.concat(baseLocale, '.json')
var baseFilePath = path.join(translationsDir, baseFileName)
/**

View File

@@ -2,7 +2,7 @@ import * as fs from 'fs'
import * as path from 'path'
const translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
const baseLocale = 'zh-cn'
const baseLocale = 'zh-CN'
const baseFileName = `${baseLocale}.json`
const baseFilePath = path.join(translationsDir, baseFileName)

View File

@@ -1,58 +0,0 @@
const fs = require('fs')
const path = require('path')
exports.default = async function (context) {
const platform = context.packager.platform.name
// 根据平台确定 locales 目录位置
let resourceDirs = []
if (platform === 'mac') {
// macOS 的语言文件位置
resourceDirs = [
path.join(context.appOutDir, 'Cherry Studio.app', 'Contents', 'Resources'),
path.join(
context.appOutDir,
'Cherry Studio.app',
'Contents',
'Frameworks',
'Electron Framework.framework',
'Resources'
)
]
} else {
// Windows 和 Linux 的语言文件位置
resourceDirs = [path.join(context.appOutDir, 'locales')]
}
// 处理每个资源目录
for (const resourceDir of resourceDirs) {
if (!fs.existsSync(resourceDir)) {
console.log(`Resource directory not found: ${resourceDir}, skipping...`)
continue
}
// 读取所有文件和目录
const items = fs.readdirSync(resourceDir)
// 遍历并删除不需要的语言文件
for (const item of items) {
if (platform === 'mac') {
// 在 macOS 上检查 .lproj 目录
if (item.endsWith('.lproj') && !item.match(/^(en|zh|ru)/)) {
const dirPath = path.join(resourceDir, item)
fs.rmSync(dirPath, { recursive: true, force: true })
console.log(`Removed locale directory: ${item} from ${resourceDir}`)
}
} else {
// 其他平台处理 .pak 文件
if (!item.match(/^(en|zh|ru)/)) {
const filePath = path.join(resourceDir, item)
fs.unlinkSync(filePath)
console.log(`Removed locale file: ${item} from ${resourceDir}`)
}
}
}
}
console.log('Locale cleanup completed!')
}

View File

@@ -2,3 +2,4 @@ export const isMac = process.platform === 'darwin'
export const isWin = process.platform === 'win32'
export const isLinux = process.platform === 'linux'
export const isDev = process.env.NODE_ENV === 'development'
export const isPortable = isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env

View File

@@ -11,6 +11,7 @@ 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

@@ -8,10 +8,16 @@ import Logger from 'electron-log'
import { registerIpc } from './ipc'
import { configManager } from './services/ConfigManager'
import mcpService from './services/MCPService'
import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient'
import {
CHERRY_STUDIO_PROTOCOL,
handleProtocolUrl,
registerProtocolClient,
setupAppImageDeepLink
} from './services/ProtocolClient'
import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService'
import { setUserDataDir } from './utils/file'
// Check for single instance lock
if (!app.requestSingleInstanceLock()) {
@@ -48,11 +54,13 @@ if (!app.requestSingleInstanceLock()) {
registerIpc(mainWindow, app)
// 注意: MsTTS IPC处理程序已在ipc.ts中注册
// 不需要再次调用registerMsTTSIpcHandlers()
replaceDevtoolsFont(mainWindow)
setUserDataDir()
// Setup deep link for AppImage on Linux
await setupAppImageDeepLink()
if (process.env.NODE_ENV === 'development') {
installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS])
.then((name) => console.log(`Added Extension: ${name}`))
@@ -75,14 +83,6 @@ if (!app.requestSingleInstanceLock()) {
handleProtocolUrl(url)
})
registerProtocolClient(app)
// macOS specific: handle protocol when app is already running
app.on('open-url', (event, url) => {
event.preventDefault()
handleProtocolUrl(url)
})
// Listen for second instance
app.on('second-instance', (_event, argv) => {
windowService.showMainWindow()

View File

@@ -5,12 +5,11 @@ import { isMac, isWin } from '@main/constant'
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
import { IpcChannel } from '@shared/IpcChannel'
import { Shortcut, ThemeMode } from '@types'
import { BrowserWindow, ipcMain, session, shell } from 'electron'
import { BrowserWindow, ipcMain, nativeTheme, session, shell } from 'electron'
import log from 'electron-log'
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
import AppUpdater from './services/AppUpdater'
import { asrServerService } from './services/ASRServerService'
import BackupManager from './services/BackupManager'
import { configManager } from './services/ConfigManager'
import CopilotService from './services/CopilotService'
@@ -20,13 +19,13 @@ import FileStorage from './services/FileStorage'
import { GeminiService } from './services/GeminiService'
import KnowledgeService from './services/KnowledgeService'
import mcpService from './services/MCPService'
import * as MsTTSService from './services/MsTTSService'
import * as NutstoreService from './services/NutstoreService'
import ObsidianVaultService from './services/ObsidianVaultService'
import { ProxyConfig, proxyManager } from './services/ProxyManager'
import { searchService } from './services/SearchService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { setOpenLinkExternal } from './services/WebviewService'
import { windowService } from './services/WindowService'
import { getResourcePath } from './utils'
import { decrypt, encrypt } from './utils/aes'
@@ -50,7 +49,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
appDataPath: app.getPath('userData'),
resourcesPath: getResourcePath(),
logsPath: log.transports.file.getFile().path,
arch: arch()
arch: arch(),
isPortable: isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env
}))
ipcMain.handle(IpcChannel.App_Proxy, async (_, proxy: string) => {
@@ -104,6 +104,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// auto update
ipcMain.handle(IpcChannel.App_SetAutoUpdate, (_, isActive: boolean) => {
appUpdater.setAutoUpdate(isActive)
configManager.setAutoUpdate(isActive)
})
@@ -118,23 +119,26 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
})
// theme
ipcMain.handle(IpcChannel.App_SetTheme, (event, theme: ThemeMode) => {
if (theme === configManager.getTheme()) return
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)
)
}
configManager.setTheme(theme)
// should sync theme change to all windows
const senderWindowId = event.sender.id
const windows = BrowserWindow.getAllWindows()
// 向其他窗口广播主题变化
windows.forEach((win) => {
if (win.webContents.id !== senderWindowId) {
win.webContents.send(IpcChannel.ThemeChange, theme)
}
})
if (theme === ThemeMode.auto) {
nativeTheme.themeSource = 'system'
nativeTheme.on('updated', notifyThemeChange)
} else {
nativeTheme.themeSource = theme
nativeTheme.removeAllListeners('updated')
}
mainWindow?.setTitleBarOverlay &&
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
mainWindow.setTitleBarOverlay(nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight)
configManager.setTheme(theme)
notifyThemeChange()
})
// custom css
@@ -167,7 +171,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
})
)
await fileManager.clearTemp()
fs.writeFileSync(log.transports.file.getFile().path, '')
await fs.writeFileSync(log.transports.file.getFile().path, '')
return { success: true }
} catch (error: any) {
log.error('Failed to clear cache:', error)
@@ -177,12 +181,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// check for update
ipcMain.handle(IpcChannel.App_CheckForUpdate, async () => {
const update = await appUpdater.autoUpdater.checkForUpdates()
return {
currentVersion: appUpdater.autoUpdater.currentVersion,
updateInfo: update?.updateInfo
}
await appUpdater.checkForUpdates()
})
// zip
@@ -325,18 +324,18 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
)
// search window
ipcMain.handle(IpcChannel.SearchWindow_Open, (_, uid: string) => searchService.openSearchWindow(uid))
ipcMain.handle(IpcChannel.SearchWindow_Close, (_, uid: string) => searchService.closeSearchWindow(uid))
ipcMain.handle(IpcChannel.SearchWindow_OpenUrl, (_, uid: string, url: string) =>
searchService.openUrlInSearchWindow(uid, url)
)
ipcMain.handle(IpcChannel.SearchWindow_Open, async (_, uid: string) => {
await searchService.openSearchWindow(uid)
})
ipcMain.handle(IpcChannel.SearchWindow_Close, async (_, uid: string) => {
await searchService.closeSearchWindow(uid)
})
ipcMain.handle(IpcChannel.SearchWindow_OpenUrl, async (_, uid: string, url: string) => {
return await searchService.openUrlInSearchWindow(uid, url)
})
// 注册ASR服务器IPC处理程序
asrServerService.registerIpcHandlers()
// 注册MsTTS IPC处理程序
ipcMain.handle(IpcChannel.MsTTS_GetVoices, MsTTSService.getVoices)
ipcMain.handle(IpcChannel.MsTTS_Synthesize, (_, text: string, voice: string, outputFormat: string) =>
MsTTSService.synthesize(text, voice, outputFormat)
// webview
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
setOpenLinkExternal(webviewId, isExternal)
)
}

View File

@@ -0,0 +1,263 @@
// inspired by https://dify.ai/blog/turn-your-dify-app-into-an-mcp-server
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
import { zodToJsonSchema } from 'zod-to-json-schema'
interface DifyKnowledgeServerConfig {
difyKey: string
apiHost: string
}
interface DifyListKnowledgeResponse {
id: string
name: string
description: string
}
interface DifySearchKnowledgeResponse {
query: {
content: string
}
records: Array<{
segment: {
id: string
position: number
document_id: string
content: string
keywords: string[]
document?: {
id: string
data_source_type: string
name: string
}
}
score: number
}>
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ToolInputSchema = ToolSchema.shape.inputSchema
type ToolInput = z.infer<typeof ToolInputSchema>
const SearchKnowledgeArgsSchema = z.object({
id: z.string().describe('Knowledge ID'),
query: z.string().describe('Query string'),
topK: z.number().optional().describe('Number of top results to return')
})
type McpResponse = {
content: Array<{ type: 'text'; text: string }>
isError?: boolean
}
class DifyKnowledgeServer {
public server: Server
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')
}
this.config = {
difyKey: difyKey,
apiHost: args[0]
}
this.server = new Server(
{
name: '@cherry/dify-knowledge-server',
version: '0.1.0'
},
{
capabilities: {
tools: {}
}
}
)
this.initialize()
}
initialize() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'list_knowledges',
description: 'List all knowledges',
inputSchema: {
type: 'object',
properties: {},
required: []
}
},
{
name: 'search_knowledge',
description: 'Search knowledge by id and query',
inputSchema: zodToJsonSchema(SearchKnowledgeArgsSchema) as ToolInput
}
]
}
})
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params
switch (name) {
case 'list_knowledges': {
return await this.performListKnowledges(this.config.difyKey, this.config.apiHost)
}
case 'search_knowledge': {
const parsed = SearchKnowledgeArgsSchema.safeParse(args)
if (!parsed.success) {
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,
parsed.data.topK || 6,
this.config.difyKey,
this.config.apiHost
)
}
default:
throw new Error(`Unknown tool: ${name}`)
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
return {
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
isError: true
}
}
})
}
private async performListKnowledges(difyKey: string, apiHost: string): Promise<McpResponse> {
try {
const url = `${apiHost.replace(/\/$/, '')}/datasets`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${difyKey}`
}
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`API 请求失败,状态码 ${response.status}: ${errorText}`)
}
const apiResponse = await response.json()
const knowledges: DifyListKnowledgeResponse[] =
apiResponse?.data?.map((item: any) => ({
id: item.id,
name: item.name,
description: item.description || ''
})) || []
const listText =
knowledges.length > 0
? knowledges.map((k) => `- **${k.name}** (ID: ${k.id})\n ${k.description || 'No Description'}`).join('\n')
: '- No knowledges found.'
const formattedText = `### 可用知识库:\n\n${listText}`
return {
content: [{ type: 'text', text: formattedText }]
}
} catch (error) {
console.error('获取知识库列表时出错:', error)
const errorMessage = error instanceof Error ? error.message : String(error)
// 返回包含错误信息的 MCP 响应
return {
content: [{ type: 'text', text: `Accessing Knowledge Error: ${errorMessage}` }],
isError: true
}
}
}
private async performSearchKnowledge(
id: string,
query: string,
topK: number,
difyKey: string,
apiHost: string
): Promise<McpResponse> {
try {
const url = `${apiHost.replace(/\/$/, '')}/datasets/${id}/retrieve`
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${difyKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: query,
retrieval_model: {
top_k: topK,
// will be error if not set
reranking_enable: null,
score_threshold_enabled: null
}
})
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`API 请求失败,状态码 ${response.status}: ${errorText}`)
}
const searchResponse: DifySearchKnowledgeResponse = await response.json()
if (!searchResponse || !Array.isArray(searchResponse.records)) {
throw new Error(`从 Dify API 收到的响应格式无效: ${JSON.stringify(searchResponse)}`)
}
const header = `### Query: ${query}\n\n`
let body: string
if (searchResponse.records.length === 0) {
body = 'No results found.'
} else {
const resultsText = searchResponse.records
.map((record, index) => {
const docName = record.segment.document?.name || 'Unknown Document'
const content = record.segment.content.trim()
const score = record.score
const keywords = record.segment.keywords || []
let resultEntry = `#### ${index + 1}. ${docName} (Relevant Score: ${(score * 100).toFixed(1)}%)`
resultEntry += `\n${content}`
if (keywords.length > 0) {
resultEntry += `\n*Keywords: ${keywords.join(', ')}*`
}
return resultEntry
})
.join('\n\n')
body = `Found ${searchResponse.records.length} results:\n\n${resultsText}`
}
const formattedText = header + body
return {
content: [{ type: 'text', text: formattedText }]
}
} catch (error) {
console.error('搜索知识库时出错:', error)
const errorMessage = error instanceof Error ? error.message : String(error)
return {
content: [{ type: 'text', text: `Search Knowledge Error: ${errorMessage}` }],
isError: true
}
}
}
}
export default DifyKnowledgeServer

View File

@@ -2,6 +2,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import Logger from 'electron-log'
import BraveSearchServer from './brave-search'
import DifyKnowledgeServer from './dify-knowledge'
import FetchServer from './fetch'
import FileSystemServer from './filesystem'
import MemoryServer from './memory'
@@ -26,6 +27,10 @@ export function createInMemoryMCPServer(name: string, args: string[] = [], envs:
case '@cherry/filesystem': {
return new FileSystemServer(args).server
}
case '@cherry/dify-knowledge': {
const difyKey = envs.DIFY_KEY
return new DifyKnowledgeServer(difyKey, args).server
}
default:
throw new Error(`Unknown in-memory MCP server: ${name}`)
}

View File

@@ -1,131 +0,0 @@
import { ChildProcess, spawn } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import { IpcChannel } from '@shared/IpcChannel'
import { app, ipcMain } from 'electron'
import log from 'electron-log'
/**
* ASR服务器服务用于管理ASR服务器进程
*/
class ASRServerService {
private asrServerProcess: ChildProcess | null = null
/**
* 注册IPC处理程序
*/
public registerIpcHandlers(): void {
// 启动ASR服务器
ipcMain.handle(IpcChannel.Asr_StartServer, this.startServer.bind(this))
// 停止ASR服务器
ipcMain.handle(IpcChannel.Asr_StopServer, this.stopServer.bind(this))
}
/**
* 启动ASR服务器
* @returns Promise<{success: boolean, pid?: number, error?: string}>
*/
private async startServer(): Promise<{ success: boolean; pid?: number; error?: string }> {
try {
if (this.asrServerProcess) {
return { success: true, pid: this.asrServerProcess.pid }
}
// 获取服务器文件路径
log.info('App path:', app.getAppPath())
// 在开发环境和生产环境中使用不同的路径
let serverPath = ''
const isPackaged = app.isPackaged
if (isPackaged) {
// 生产环境 (打包后) - 使用 extraResources 复制的路径
// 注意: 'app' 是 extraResources 配置中 'to' 字段的一部分
serverPath = path.join(process.resourcesPath, 'app', 'asr-server', 'server.js')
log.info('生产环境ASR 服务器路径:', serverPath)
} else {
// 开发环境 - 指向项目根目录的 asr-server
serverPath = path.join(app.getAppPath(), 'asr-server', 'server.js')
log.info('开发环境ASR 服务器路径:', serverPath)
}
// 注意:删除了 isExeFile 检查逻辑, 假设总是用 node 启动
// Removed unused variable 'isExeFile'
log.info('ASR服务器路径:', serverPath)
// 检查文件是否存在
if (!fs.existsSync(serverPath)) {
return { success: false, error: '服务器文件不存在' }
}
// 启动服务器进程
// 始终使用 node 启动 server.js
log.info(`尝试使用 node 启动: ${serverPath}`)
this.asrServerProcess = spawn('node', [serverPath], {
stdio: 'pipe', // 'pipe' 用于捕获输出, 如果需要调试可以临时改为 'inherit'
detached: false // false 通常足够
})
// 处理服务器输出
this.asrServerProcess.stdout?.on('data', (data) => {
log.info(`[ASR Server] ${data.toString()}`)
})
this.asrServerProcess.stderr?.on('data', (data) => {
log.error(`[ASR Server Error] ${data.toString()}`)
})
// 处理服务器退出
this.asrServerProcess.on('close', (code) => {
log.info(`[ASR Server] 进程退出,退出码: ${code}`)
this.asrServerProcess = null
})
// 等待一段时间确保服务器启动
await new Promise((resolve) => setTimeout(resolve, 1000))
return { success: true, pid: this.asrServerProcess.pid }
} catch (error) {
log.error('启动ASR服务器失败:', error)
return { success: false, error: (error as Error).message }
}
}
/**
* 停止ASR服务器
* @param _event IPC事件
* @param pid 进程ID
* @returns Promise<{success: boolean, error?: string}>
*/
private async stopServer(
_event: Electron.IpcMainInvokeEvent,
pid?: number
): Promise<{ success: boolean; error?: string }> {
try {
if (!this.asrServerProcess) {
return { success: true }
}
// 检查PID是否匹配
if (pid && this.asrServerProcess.pid !== pid) {
log.warn(`请求停止的PID (${pid}) 与当前运行的ASR服务器PID (${this.asrServerProcess.pid}) 不匹配`)
}
// 杀死进程
this.asrServerProcess.kill()
// 等待一段时间确保进程已经退出
await new Promise((resolve) => setTimeout(resolve, 500))
this.asrServerProcess = null
return { success: true }
} catch (error) {
log.error('停止ASR服务器失败:', error)
return { success: false, error: (error as Error).message }
}
}
}
// 导出单例实例
export const asrServerService = new ASRServerService()

View File

@@ -1,3 +1,4 @@
import { isWin } from '@main/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { UpdateInfo } from 'builder-util-runtime'
import { app, BrowserWindow, dialog } from 'electron'
@@ -55,6 +56,40 @@ export default class AppUpdater {
this.autoUpdater = autoUpdater
}
public setAutoUpdate(isActive: boolean) {
autoUpdater.autoDownload = isActive
autoUpdater.autoInstallOnAppQuit = isActive
}
public async checkForUpdates() {
if (isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env) {
return {
currentVersion: app.getVersion(),
updateInfo: null
}
}
try {
const update = await this.autoUpdater.checkForUpdates()
if (update?.isUpdateAvailable && !this.autoUpdater.autoDownload) {
// 如果 autoDownload 为 false则需要再调用下面的函数触发下
// do not use await, because it will block the return of this function
this.autoUpdater.downloadUpdate()
}
return {
currentVersion: this.autoUpdater.currentVersion,
updateInfo: update?.updateInfo
}
} catch (error) {
logger.error('Failed to check for update:', error)
return {
currentVersion: app.getVersion(),
updateInfo: null
}
}
}
public async showUpdateDialog(mainWindow: BrowserWindow) {
if (!this.releaseInfo) {
return

View File

@@ -1,25 +1,27 @@
import { AxiosInstance, default as axios_ } from 'axios'
import { ProxyAgent } from 'proxy-agent'
import { proxyManager } from './ProxyManager'
class AxiosProxy {
private cacheAxios: AxiosInstance | undefined
private proxyURL: string | undefined
private cacheAxios: AxiosInstance | null = null
private proxyAgent: ProxyAgent | null = null
get axios(): AxiosInstance {
const currentProxyURL = proxyManager.getProxyUrl()
if (this.proxyURL !== currentProxyURL) {
this.proxyURL = currentProxyURL
const agent = proxyManager.getProxyAgent()
const currentProxyAgent = proxyManager.getProxyAgent()
// 如果代理发生变化或尚未初始化,则重新创建 axios 实例
if (this.cacheAxios === null || (currentProxyAgent !== null && this.proxyAgent !== currentProxyAgent)) {
this.proxyAgent = currentProxyAgent
// 创建带有代理配置的 axios 实例
this.cacheAxios = axios_.create({
proxy: false,
...(agent && { httpAgent: agent, httpsAgent: agent })
httpAgent: currentProxyAgent || undefined,
httpsAgent: currentProxyAgent || undefined
})
}
if (this.cacheAxios === undefined) {
this.cacheAxios = axios_.create({ proxy: false })
}
return this.cacheAxios
}
}

View File

@@ -1,9 +1,10 @@
import { IpcChannel } from '@shared/IpcChannel'
import { WebDavConfig } from '@types'
import AdmZip from 'adm-zip'
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 * as path from 'path'
import { createClient, CreateDirectoryOptions, FileStat } from 'webdav'
@@ -91,6 +92,7 @@ class BackupManager {
// 使用流的方式写入 data.json
const tempDataPath = path.join(this.tempDir, 'data.json')
await new Promise<void>((resolve, reject) => {
const writeStream = fs.createWriteStream(tempDataPath)
writeStream.write(data)
@@ -99,6 +101,7 @@ class BackupManager {
writeStream.on('finish', () => resolve())
writeStream.on('error', (error) => reject(error))
})
onProgress({ stage: 'writing_data', progress: 20, total: 100 })
// 复制 Data 目录到临时目录
@@ -112,18 +115,92 @@ class BackupManager {
// 使用流式复制
await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => {
copiedSize += size
const progress = Math.min(80, 20 + Math.floor((copiedSize / totalSize) * 60))
const progress = Math.min(50, Math.floor((copiedSize / totalSize) * 50))
onProgress({ stage: 'copying_files', progress, total: 100 })
})
await this.setWritableRecursive(tempDataDir)
onProgress({ stage: 'compressing', progress: 80, total: 100 })
onProgress({ stage: 'preparing_compression', progress: 50, total: 100 })
// 使用 adm-zip 创建压缩文件
const zip = new AdmZip()
zip.addLocalFolder(this.tempDir)
// 创建输出文件
const backupedFilePath = path.join(destinationPath, fileName)
zip.writeZip(backupedFilePath)
const output = fs.createWriteStream(backupedFilePath)
// 创建 archiver 实例,启用 ZIP64 支持
const archive = archiver('zip', {
zlib: { level: 1 }, // 使用最低压缩级别以提高速度
zip64: true // 启用 ZIP64 支持以处理大文件
})
let lastProgress = 50
let totalEntries = 0
let processedEntries = 0
let totalBytes = 0
let processedBytes = 0
// 首先计算总文件数和总大小
const calculateTotals = async (dirPath: string) => {
const items = await fs.readdir(dirPath, { withFileTypes: true })
for (const item of items) {
const fullPath = path.join(dirPath, item.name)
if (item.isDirectory()) {
await calculateTotals(fullPath)
} else {
totalEntries++
const stats = await fs.stat(fullPath)
totalBytes += stats.size
}
}
}
await calculateTotals(this.tempDir)
// 监听文件添加事件
archive.on('entry', () => {
processedEntries++
if (totalEntries > 0) {
const progressPercent = Math.min(55, 50 + Math.floor((processedEntries / totalEntries) * 5))
if (progressPercent > lastProgress) {
lastProgress = progressPercent
onProgress({ stage: 'compressing', progress: progressPercent, total: 100 })
}
}
})
// 监听数据写入事件
archive.on('data', (chunk) => {
processedBytes += chunk.length
if (totalBytes > 0) {
const progressPercent = Math.min(99, 55 + Math.floor((processedBytes / totalBytes) * 44))
if (progressPercent > lastProgress) {
lastProgress = progressPercent
onProgress({ stage: 'compressing', progress: progressPercent, total: 100 })
}
}
})
// 使用 Promise 等待压缩完成
await new Promise<void>((resolve, reject) => {
output.on('close', () => {
onProgress({ stage: 'compressing', progress: 100, total: 100 })
resolve()
})
archive.on('error', reject)
archive.on('warning', (err: any) => {
if (err.code !== 'ENOENT') {
Logger.warn('[BackupManager] Archive warning:', err)
}
})
// 将输出流连接到压缩器
archive.pipe(output)
// 添加整个临时目录到压缩文件
archive.directory(this.tempDir, false)
// 完成压缩
archive.finalize()
})
// 清理临时目录
await fs.remove(this.tempDir)
@@ -133,6 +210,8 @@ class BackupManager {
return backupedFilePath
} catch (error) {
Logger.error('[BackupManager] Backup failed:', error)
// 确保清理临时目录
await fs.remove(this.tempDir).catch(() => {})
throw error
}
}
@@ -151,16 +230,22 @@ class BackupManager {
onProgress({ stage: 'preparing', progress: 0, total: 100 })
Logger.log('[backup] step 1: unzip backup file', this.tempDir)
// 使用 adm-zip 解压
const zip = new AdmZip(backupPath)
zip.extractAllTo(this.tempDir, true) // true 表示覆盖已存在的文件
onProgress({ stage: 'extracting', progress: 20, total: 100 })
// 使用 extract-zip 解压
await extract(backupPath, {
dir: this.tempDir,
onEntry: () => {
// 这里可以处理进度,但 extract-zip 不提供总条目数信息
onProgress({ stage: 'extracting', progress: 15, total: 100 })
}
})
onProgress({ stage: 'extracting', progress: 25, total: 100 })
Logger.log('[backup] step 2: read data.json')
// 读取 data.json
const dataPath = path.join(this.tempDir, 'data.json')
const data = await fs.readFile(dataPath, 'utf-8')
onProgress({ stage: 'reading_data', progress: 40, total: 100 })
onProgress({ stage: 'reading_data', progress: 35, total: 100 })
Logger.log('[backup] step 3: restore Data directory')
// 恢复 Data 目录
@@ -177,7 +262,7 @@ class BackupManager {
// 使用流式复制
await this.copyDirWithProgress(sourcePath, destPath, (size) => {
copiedSize += size
const progress = Math.min(90, 40 + Math.floor((copiedSize / totalSize) * 50))
const progress = Math.min(85, 35 + Math.floor((copiedSize / totalSize) * 50))
onProgress({ stage: 'copying_files', progress, total: 100 })
})

View File

@@ -15,7 +15,8 @@ enum ConfigKeys {
Shortcuts = 'shortcuts',
ClickTrayToShowQuickAssistant = 'clickTrayToShowQuickAssistant',
EnableQuickAssistant = 'enableQuickAssistant',
AutoUpdate = 'autoUpdate'
AutoUpdate = 'autoUpdate',
EnableDataCollection = 'enableDataCollection'
}
export class ConfigManager {
@@ -36,7 +37,7 @@ export class ConfigManager {
}
getTheme(): ThemeMode {
return this.get(ConfigKeys.Theme, ThemeMode.light)
return this.get(ConfigKeys.Theme, ThemeMode.auto)
}
setTheme(theme: ThemeMode) {
@@ -145,6 +146,14 @@ export class ConfigManager {
this.set(ConfigKeys.AutoUpdate, value)
}
getEnableDataCollection(): boolean {
return this.get<boolean>(ConfigKeys.EnableDataCollection, true)
}
setEnableDataCollection(value: boolean) {
this.set(ConfigKeys.EnableDataCollection, value)
}
set(key: string, value: unknown) {
this.store.set(key, value)
}

View File

@@ -1,12 +1,7 @@
import fs from 'node:fs'
export default class FileService {
public static async readFile(_: Electron.IpcMainInvokeEvent, path: string, encoding?: BufferEncoding) {
// 如果指定了编码,则返回字符串,否则返回二进制数据
if (encoding) {
return fs.readFileSync(path, encoding)
} else {
return fs.readFileSync(path)
}
public static async readFile(_: Electron.IpcMainInvokeEvent, path: string) {
return fs.readFileSync(path, 'utf8')
}
}

View File

@@ -10,6 +10,10 @@ 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 {
StreamableHTTPClientTransport,
type StreamableHTTPClientTransportOptions
} from '@modelcontextprotocol/sdk/client/streamableHttp'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'
import { nanoid } from '@reduxjs/toolkit'
import {
@@ -29,7 +33,6 @@ import { memoize } from 'lodash'
import { CacheService } from './CacheService'
import { CallBackServer } from './mcp/oauth/callback'
import { McpOAuthClientProvider } from './mcp/oauth/provider'
import { StreamableHTTPClientTransport, type StreamableHTTPClientTransportOptions } from './MCPStreamableHttpClient'
// Generic type for caching wrapped functions
type CachedFunction<T extends unknown[], R> = (...args: T) => Promise<R>
@@ -158,6 +161,9 @@ class McpService {
return new StreamableHTTPClientTransport(new URL(server.baseUrl!), options)
} else if (server.type === 'sse') {
const options: SSEClientTransportOptions = {
eventSourceInit: {
fetch: (url, init) => fetch(url, { ...init, headers: server.headers || {} })
},
requestInit: {
headers: server.headers || {}
},
@@ -388,8 +394,17 @@ class McpService {
): Promise<MCPCallToolResponse> {
try {
Logger.info('[MCP] Calling:', server.name, name, args)
if (typeof args === 'string') {
try {
args = JSON.parse(args)
} catch (e) {
Logger.error('[MCP] args parse error', args)
}
}
const client = await this.initClient(server)
const result = await client.callTool({ name, arguments: args })
const result = await client.callTool({ name, arguments: args }, undefined, {
timeout: server.timeout ? server.timeout * 1000 : 60000 // Default timeout of 1 minute
})
return result as MCPCallToolResponse
} catch (error) {
Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error)
@@ -559,13 +574,26 @@ 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 = 'powershell.exe'
shell = this.findPowerShellExecutable()
command = '$env:PATH'
} else {
// 尝试获取当前用户的默认 shell
@@ -617,6 +645,10 @@ class McpService {
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()

View File

@@ -1,365 +0,0 @@
import { auth, AuthResult, OAuthClientProvider, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
import { JSONRPCMessage, JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js'
export class StreamableHTTPError extends Error {
constructor(
public readonly code: number | undefined,
message: string | undefined,
public readonly event: ErrorEvent
) {
super(`Streamable HTTP error: ${message}`)
}
}
/**
* Configuration options for the `StreamableHTTPClientTransport`.
*/
export type StreamableHTTPClientTransportOptions = {
/**
* An OAuth client provider to use for authentication.
*
* When an `authProvider` is specified and the connection is started:
* 1. The connection is attempted with any existing access token from the `authProvider`.
* 2. If the access token has expired, the `authProvider` is used to refresh the token.
* 3. If token refresh fails or no access token exists, and auth is required, `OAuthClientProvider.redirectToAuthorization` is called, and an `UnauthorizedError` will be thrown from `connect`/`start`.
*
* After the user has finished authorizing via their user agent, and is redirected back to the MCP client application, call `StreamableHTTPClientTransport.finishAuth` with the authorization code before retrying the connection.
*
* If an `authProvider` is not provided, and auth is required, an `UnauthorizedError` will be thrown.
*
* `UnauthorizedError` might also be thrown when sending any message over the transport, indicating that the session has expired, and needs to be re-authed and reconnected.
*/
authProvider?: OAuthClientProvider
/**
* Customizes HTTP requests to the server.
*/
requestInit?: RequestInit
}
/**
* Client transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.
* It will connect to a server using HTTP POST for sending messages and HTTP GET with Server-Sent Events
* for receiving messages.
*/
export class StreamableHTTPClientTransport implements Transport {
private _activeStreams: Map<string, ReadableStreamDefaultReader<Uint8Array>> = new Map()
private _abortController?: AbortController
private _url: URL
private _requestInit?: RequestInit
private _authProvider?: OAuthClientProvider
private _sessionId?: string
private _lastEventId?: string
onclose?: () => void
onerror?: (error: Error) => void
onmessage?: (message: JSONRPCMessage) => void
constructor(url: URL, opts?: StreamableHTTPClientTransportOptions) {
this._url = url
this._requestInit = opts?.requestInit
this._authProvider = opts?.authProvider
}
private async _authThenStart(): Promise<void> {
if (!this._authProvider) {
throw new UnauthorizedError('No auth provider')
}
let result: AuthResult
try {
result = await auth(this._authProvider, { serverUrl: this._url })
} catch (error) {
this.onerror?.(error as Error)
throw error
}
if (result !== 'AUTHORIZED') {
throw new UnauthorizedError()
}
return await this._startOrAuth()
}
private async _commonHeaders(): Promise<HeadersInit> {
const headers: HeadersInit = {}
if (this._authProvider) {
const tokens = await this._authProvider.tokens()
if (tokens) {
headers['Authorization'] = `Bearer ${tokens.access_token}`
}
}
if (this._sessionId) {
headers['mcp-session-id'] = this._sessionId
}
return headers
}
private async _startOrAuth(): Promise<void> {
try {
// Try to open an initial SSE stream with GET to listen for server messages
// This is optional according to the spec - server may not support it
const commonHeaders = await this._commonHeaders()
const headers = new Headers(commonHeaders)
headers.set('Accept', 'text/event-stream')
// Include Last-Event-ID header for resumable streams
if (this._lastEventId) {
headers.set('last-event-id', this._lastEventId)
}
const response = await fetch(this._url, {
method: 'GET',
headers,
signal: this._abortController?.signal
})
if (response.status === 405) {
// Server doesn't support GET for SSE, which is allowed by the spec
// We'll rely on SSE responses to POST requests for communication
return
}
if (!response.ok) {
if (response.status === 401 && this._authProvider) {
// Need to authenticate
return await this._authThenStart()
}
const error = new Error(`Failed to open SSE stream: ${response.status} ${response.statusText}`)
this.onerror?.(error)
throw error
}
// Successful connection, handle the SSE stream as a standalone listener
const streamId = `initial-${Date.now()}`
this._handleSseStream(response.body, streamId)
} catch (error) {
this.onerror?.(error as Error)
throw error
}
}
async start() {
if (this._activeStreams.size > 0) {
throw new Error(
'StreamableHTTPClientTransport already started! If using Client class, note that connect() calls start() automatically.'
)
}
this._abortController = new AbortController()
return await this._startOrAuth()
}
/**
* Call this method after the user has finished authorizing via their user agent and is redirected back to the MCP client application. This will exchange the authorization code for an access token, enabling the next connection attempt to successfully auth.
*/
async finishAuth(authorizationCode: string): Promise<void> {
if (!this._authProvider) {
throw new UnauthorizedError('No auth provider')
}
const result = await auth(this._authProvider, { serverUrl: this._url, authorizationCode })
if (result !== 'AUTHORIZED') {
throw new UnauthorizedError('Failed to authorize')
}
}
async close(): Promise<void> {
// Close all active streams
for (const reader of this._activeStreams.values()) {
try {
reader.cancel()
} catch (error) {
this.onerror?.(error as Error)
}
}
this._activeStreams.clear()
// Abort any pending requests
this._abortController?.abort()
// If we have a session ID, send a DELETE request to explicitly terminate the session
if (this._sessionId) {
try {
const commonHeaders = await this._commonHeaders()
const response = await fetch(this._url, {
method: 'DELETE',
headers: commonHeaders,
signal: this._abortController?.signal
})
if (!response.ok) {
// Server might respond with 405 if it doesn't support explicit session termination
// We don't throw an error in that case
if (response.status !== 405) {
const text = await response.text().catch(() => null)
throw new Error(`Error terminating session (HTTP ${response.status}): ${text}`)
}
}
} catch (error) {
// We still want to invoke onclose even if the session termination fails
this.onerror?.(error as Error)
}
}
this.onclose?.()
}
async send(message: JSONRPCMessage | JSONRPCMessage[]): Promise<void> {
try {
const commonHeaders = await this._commonHeaders()
const headers = new Headers({ ...commonHeaders, ...this._requestInit?.headers })
headers.set('content-type', 'application/json')
headers.set('accept', 'application/json, text/event-stream')
const init = {
...this._requestInit,
method: 'POST',
headers,
body: JSON.stringify(message),
signal: this._abortController?.signal
}
const response = await fetch(this._url, init)
// Handle session ID received during initialization
const sessionId = response.headers.get('mcp-session-id')
if (sessionId) {
this._sessionId = sessionId
}
if (!response.ok) {
if (response.status === 401 && this._authProvider) {
const result = await auth(this._authProvider, { serverUrl: this._url })
if (result !== 'AUTHORIZED') {
throw new UnauthorizedError()
}
// Purposely _not_ awaited, so we don't call onerror twice
return this.send(message)
}
const text = await response.text().catch(() => null)
throw new Error(`Error POSTing to endpoint (HTTP ${response.status}): ${text}`)
}
// If the response is 202 Accepted, there's no body to process
if (response.status === 202) {
return
}
// Get original message(s) for detecting request IDs
const messages = Array.isArray(message) ? message : [message]
// Extract IDs from request messages for tracking responses
const requestIds = messages
.filter((msg) => 'method' in msg && 'id' in msg)
.map((msg) => ('id' in msg ? msg.id : undefined))
.filter((id) => id !== undefined)
// If we have request IDs and an SSE response, create a unique stream ID
const hasRequests = requestIds.length > 0
// Check the response type
const contentType = response.headers.get('content-type')
if (hasRequests) {
if (contentType?.includes('text/event-stream')) {
// For streaming responses, create a unique stream ID based on request IDs
const streamId = `req-${requestIds.join('-')}-${Date.now()}`
this._handleSseStream(response.body, streamId)
} else if (contentType?.includes('application/json')) {
// For non-streaming servers, we might get direct JSON responses
const data = await response.json()
const responseMessages = Array.isArray(data)
? data.map((msg) => JSONRPCMessageSchema.parse(msg))
: [JSONRPCMessageSchema.parse(data)]
for (const msg of responseMessages) {
this.onmessage?.(msg)
}
}
}
} catch (error) {
this.onerror?.(error as Error)
throw error
}
}
private _handleSseStream(stream: ReadableStream<Uint8Array> | null, streamId: string): void {
if (!stream) {
return
}
// Set up stream handling for server-sent events
const reader = stream.getReader()
this._activeStreams.set(streamId, reader)
const decoder = new TextDecoder()
let buffer = ''
const processStream = async () => {
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
// Stream closed by server
this._activeStreams.delete(streamId)
break
}
buffer += decoder.decode(value, { stream: true })
// Process SSE messages in the buffer
const events = buffer.split('\n\n')
buffer = events.pop() || ''
for (const event of events) {
const lines = event.split('\n')
let id: string | undefined
let eventType: string | undefined
let data: string | undefined
// Parse SSE message according to the format
for (const line of lines) {
if (line.startsWith('id:')) {
id = line.slice(3).trim()
} else if (line.startsWith('event:')) {
eventType = line.slice(6).trim()
} else if (line.startsWith('data:')) {
data = line.slice(5).trim()
}
}
// Update last event ID if provided by server
// As per spec: the ID MUST be globally unique across all streams within that session
if (id) {
this._lastEventId = id
}
// Handle message event
if (data) {
// Default event type is 'message' per SSE spec if not specified
if (!eventType || eventType === 'message') {
try {
const message = JSONRPCMessageSchema.parse(JSON.parse(data))
this.onmessage?.(message)
} catch (error) {
this.onerror?.(error as Error)
}
}
}
}
}
} catch (error) {
this._activeStreams.delete(streamId)
this.onerror?.(error as Error)
}
}
processStream()
}
}

View File

@@ -1,137 +0,0 @@
import fs from 'node:fs'
import path from 'node:path'
import { app } from 'electron'
import log from 'electron-log'
import { EdgeTTS } from 'node-edge-tts'
/**
* Microsoft Edge TTS服务
* 使用Microsoft Edge的在线TTS服务不需要API密钥
*/
class MsEdgeTTSService {
private static instance: MsEdgeTTSService
private tempDir: string
private constructor() {
this.tempDir = path.join(app.getPath('temp'), 'cherry-tts')
// 确保临时目录存在
if (!fs.existsSync(this.tempDir)) {
fs.mkdirSync(this.tempDir, { recursive: true })
}
}
/**
* 获取单例实例
*/
public static getInstance(): MsEdgeTTSService {
if (!MsEdgeTTSService.instance) {
MsEdgeTTSService.instance = new MsEdgeTTSService()
}
return MsEdgeTTSService.instance
}
/**
* 获取可用的语音列表
* @returns 语音列表
*/
public async getVoices(): Promise<any[]> {
try {
// 返回预定义的中文语音列表
return [
{ name: 'zh-CN-XiaoxiaoNeural', locale: 'zh-CN', gender: 'Female' },
{ name: 'zh-CN-YunxiNeural', locale: 'zh-CN', gender: 'Male' },
{ name: 'zh-CN-YunyangNeural', locale: 'zh-CN', gender: 'Male' },
{ name: 'zh-CN-XiaohanNeural', locale: 'zh-CN', gender: 'Female' },
{ name: 'zh-CN-XiaomoNeural', locale: 'zh-CN', gender: 'Female' },
{ name: 'zh-CN-XiaoxuanNeural', locale: 'zh-CN', gender: 'Female' },
{ name: 'zh-CN-XiaoruiNeural', locale: 'zh-CN', gender: 'Female' },
{ name: 'zh-CN-YunfengNeural', locale: 'zh-CN', gender: 'Male' }
]
} catch (error) {
log.error('获取Microsoft Edge TTS语音列表失败:', error)
throw error
}
}
/**
* 合成语音
* @param text 要合成的文本
* @param voice 语音
* @param outputFormat 输出格式
* @returns 音频文件路径
*/
public async synthesize(text: string, voice: string, outputFormat: string): Promise<string> {
try {
log.info(`Microsoft Edge TTS合成语音: 文本="${text.substring(0, 30)}...", 语音=${voice}, 格式=${outputFormat}`)
// 验证输入参数
if (!text || text.trim() === '') {
throw new Error('要合成的文本不能为空')
}
if (!voice || voice.trim() === '') {
throw new Error('语音名称不能为空')
}
// 创建一个新的EdgeTTS实例并设置参数
const tts = new EdgeTTS({
voice: voice,
outputFormat: outputFormat,
timeout: 30000, // 30秒超时
rate: '+0%', // 正常语速
pitch: '+0Hz', // 正常音调
volume: '+0%' // 正常音量
})
// 生成临时文件路径
const timestamp = Date.now()
const fileExtension = outputFormat.includes('mp3') ? 'mp3' : outputFormat.split('-').pop() || 'audio'
const outputPath = path.join(this.tempDir, `tts_${timestamp}.${fileExtension}`)
log.info(`开始生成语音文件: ${outputPath}`)
// 使用ttsPromise方法生成文件
await tts.ttsPromise(text, outputPath)
// 验证生成的文件是否存在且大小大于0
if (!fs.existsSync(outputPath)) {
throw new Error(`生成的语音文件不存在: ${outputPath}`)
}
const stats = fs.statSync(outputPath)
if (stats.size === 0) {
throw new Error(`生成的语音文件大小为0: ${outputPath}`)
}
log.info(`Microsoft Edge TTS合成成功: ${outputPath}, 文件大小: ${stats.size} 字节`)
return outputPath
} catch (error: any) {
// 记录详细的错误信息
log.error(`Microsoft Edge TTS语音合成失败 (语音=${voice}):`, error)
// 尝试提供更有用的错误信息
if (error.message && typeof error.message === 'string') {
if (error.message.includes('Timed out')) {
throw new Error(`语音合成超时,请检查网络连接或尝试其他语音`)
} else if (error.message.includes('ENOTFOUND')) {
throw new Error(`无法连接到Microsoft语音服务请检查网络连接`)
} else if (error.message.includes('ECONNREFUSED')) {
throw new Error(`连接被拒绝,请检查网络设置或代理配置`)
}
}
throw error
}
}
}
// 导出单例方法
export const getVoices = async () => {
return await MsEdgeTTSService.getInstance().getVoices()
}
export const synthesize = async (text: string, voice: string, outputFormat: string) => {
return await MsEdgeTTSService.getInstance().synthesize(text, voice, outputFormat)
}

View File

@@ -1,50 +0,0 @@
import { IpcChannel } from '@shared/IpcChannel'
import { BrowserWindow, ipcMain } from 'electron'
import * as MsTTSService from './MsTTSService'
/**
* 注册MsTTS相关的IPC处理程序
*/
export function registerMsTTSIpcHandlers(): void {
// 获取可用的语音列表
ipcMain.handle(IpcChannel.MsTTS_GetVoices, MsTTSService.getVoices)
// 合成语音
ipcMain.handle(IpcChannel.MsTTS_Synthesize, (_, text: string, voice: string, outputFormat: string) =>
MsTTSService.synthesize(text, voice, outputFormat)
)
// 流式合成语音
ipcMain.handle(
IpcChannel.MsTTS_SynthesizeStream,
async (event, requestId: string, text: string, voice: string, outputFormat: string) => {
const window = BrowserWindow.fromWebContents(event.sender)
if (!window) return
try {
await MsTTSService.synthesizeStream(
text,
voice,
outputFormat,
(chunk: Uint8Array) => {
// 发送音频数据块
if (!window.isDestroyed()) {
window.webContents.send(IpcChannel.MsTTS_StreamData, requestId, chunk)
}
},
() => {
// 发送流结束信号
if (!window.isDestroyed()) {
window.webContents.send(IpcChannel.MsTTS_StreamEnd, requestId)
}
}
)
return { success: true }
} catch (error) {
console.error('流式TTS合成失败:', error)
return { success: false, error: error instanceof Error ? error.message : String(error) }
}
}
)
}

View File

@@ -1,643 +0,0 @@
import fs from 'node:fs'
import path from 'node:path'
import { MsEdgeTTS, OUTPUT_FORMAT } from 'edge-tts-node' // 新版支持流式的TTS库
import { app } from 'electron'
import log from 'electron-log'
import { EdgeTTS } from 'node-edge-tts' // 旧版TTS库
// --- START OF HARDCODED VOICE LIST ---
// WARNING: This list is static and may become outdated.
// It's generally recommended to use listVoices() for the most up-to-date list.
const hardcodedVoices = [
{
Name: 'Microsoft Server Speech Text to Speech Voice (af-ZA, AdriNeural)',
ShortName: 'af-ZA-AdriNeural',
Gender: 'Female',
Locale: 'af-ZA'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (am-ET, MekdesNeural)',
ShortName: 'am-ET-MekdesNeural',
Gender: 'Female',
Locale: 'am-ET'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (ar-AE, FatimaNeural)',
ShortName: 'ar-AE-FatimaNeural',
Gender: 'Female',
Locale: 'ar-AE'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (ar-AE, HamdanNeural)',
ShortName: 'ar-AE-HamdanNeural',
Gender: 'Male',
Locale: 'ar-AE'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (ar-BH, AliNeural)',
ShortName: 'ar-BH-AliNeural',
Gender: 'Male',
Locale: 'ar-BH'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (ar-BH, LailaNeural)',
ShortName: 'ar-BH-LailaNeural',
Gender: 'Female',
Locale: 'ar-BH'
},
// ... (Many other Arabic locales/voices) ...
{
Name: 'Microsoft Server Speech Text to Speech Voice (ar-SA, ZariyahNeural)',
ShortName: 'ar-SA-ZariyahNeural',
Gender: 'Female',
Locale: 'ar-SA'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (az-AZ, BabekNeural)',
ShortName: 'az-AZ-BabekNeural',
Gender: 'Male',
Locale: 'az-AZ'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (az-AZ, BanuNeural)',
ShortName: 'az-AZ-BanuNeural',
Gender: 'Female',
Locale: 'az-AZ'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (bg-BG, BorislavNeural)',
ShortName: 'bg-BG-BorislavNeural',
Gender: 'Male',
Locale: 'bg-BG'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (bg-BG, KalinaNeural)',
ShortName: 'bg-BG-KalinaNeural',
Gender: 'Female',
Locale: 'bg-BG'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (bn-BD, NabanitaNeural)',
ShortName: 'bn-BD-NabanitaNeural',
Gender: 'Female',
Locale: 'bn-BD'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (bn-BD, PradeepNeural)',
ShortName: 'bn-BD-PradeepNeural',
Gender: 'Male',
Locale: 'bn-BD'
},
// ... (Catalan, Czech, Welsh, Danish, German, Greek, English variants) ...
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-AU, NatashaNeural)',
ShortName: 'en-AU-NatashaNeural',
Gender: 'Female',
Locale: 'en-AU'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-AU, WilliamNeural)',
ShortName: 'en-AU-WilliamNeural',
Gender: 'Male',
Locale: 'en-AU'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-CA, ClaraNeural)',
ShortName: 'en-CA-ClaraNeural',
Gender: 'Female',
Locale: 'en-CA'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-CA, LiamNeural)',
ShortName: 'en-CA-LiamNeural',
Gender: 'Male',
Locale: 'en-CA'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, LibbyNeural)',
ShortName: 'en-GB-LibbyNeural',
Gender: 'Female',
Locale: 'en-GB'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, MaisieNeural)',
ShortName: 'en-GB-MaisieNeural',
Gender: 'Female',
Locale: 'en-GB'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, RyanNeural)',
ShortName: 'en-GB-RyanNeural',
Gender: 'Male',
Locale: 'en-GB'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, SoniaNeural)',
ShortName: 'en-GB-SoniaNeural',
Gender: 'Female',
Locale: 'en-GB'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, ThomasNeural)',
ShortName: 'en-GB-ThomasNeural',
Gender: 'Male',
Locale: 'en-GB'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-HK, SamNeural)',
ShortName: 'en-HK-SamNeural',
Gender: 'Male',
Locale: 'en-HK'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-HK, YanNeural)',
ShortName: 'en-HK-YanNeural',
Gender: 'Female',
Locale: 'en-HK'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-IE, ConnorNeural)',
ShortName: 'en-IE-ConnorNeural',
Gender: 'Male',
Locale: 'en-IE'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-IE, EmilyNeural)',
ShortName: 'en-IE-EmilyNeural',
Gender: 'Female',
Locale: 'en-IE'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-IN, NeerjaNeural)',
ShortName: 'en-IN-NeerjaNeural',
Gender: 'Female',
Locale: 'en-IN'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-IN, PrabhatNeural)',
ShortName: 'en-IN-PrabhatNeural',
Gender: 'Male',
Locale: 'en-IN'
},
// ... (Many more English variants: KE, NG, NZ, PH, SG, TZ, US, ZA) ...
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, AriaNeural)',
ShortName: 'en-US-AriaNeural',
Gender: 'Female',
Locale: 'en-US'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, AnaNeural)',
ShortName: 'en-US-AnaNeural',
Gender: 'Female',
Locale: 'en-US'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, ChristopherNeural)',
ShortName: 'en-US-ChristopherNeural',
Gender: 'Male',
Locale: 'en-US'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, EricNeural)',
ShortName: 'en-US-EricNeural',
Gender: 'Male',
Locale: 'en-US'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, GuyNeural)',
ShortName: 'en-US-GuyNeural',
Gender: 'Male',
Locale: 'en-US'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, JennyNeural)',
ShortName: 'en-US-JennyNeural',
Gender: 'Female',
Locale: 'en-US'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, MichelleNeural)',
ShortName: 'en-US-MichelleNeural',
Gender: 'Female',
Locale: 'en-US'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, RogerNeural)',
ShortName: 'en-US-RogerNeural',
Gender: 'Male',
Locale: 'en-US'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, SteffanNeural)',
ShortName: 'en-US-SteffanNeural',
Gender: 'Male',
Locale: 'en-US'
},
// ... (Spanish variants) ...
{
Name: 'Microsoft Server Speech Text to Speech Voice (es-MX, DaliaNeural)',
ShortName: 'es-MX-DaliaNeural',
Gender: 'Female',
Locale: 'es-MX'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (es-MX, JorgeNeural)',
ShortName: 'es-MX-JorgeNeural',
Gender: 'Male',
Locale: 'es-MX'
},
// ... (Estonian, Basque, Persian, Finnish, Filipino, French, Irish, Galician, Gujarati, Hebrew, Hindi, Croatian, Hungarian, Indonesian, Icelandic, Italian, Japanese) ...
{
Name: 'Microsoft Server Speech Text to Speech Voice (ja-JP, KeitaNeural)',
ShortName: 'ja-JP-KeitaNeural',
Gender: 'Male',
Locale: 'ja-JP'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)',
ShortName: 'ja-JP-NanamiNeural',
Gender: 'Female',
Locale: 'ja-JP'
},
// ... (Javanese, Georgian, Kazakh, Khmer, Kannada, Korean) ...
{
Name: 'Microsoft Server Speech Text to Speech Voice (ko-KR, InJoonNeural)',
ShortName: 'ko-KR-InJoonNeural',
Gender: 'Male',
Locale: 'ko-KR'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (ko-KR, SunHiNeural)',
ShortName: 'ko-KR-SunHiNeural',
Gender: 'Female',
Locale: 'ko-KR'
},
// ... (Lao, Lithuanian, Latvian, Macedonian, Malayalam, Mongolian, Marathi, Malay, Maltese, Burmese, Norwegian, Dutch, Polish, Pashto, Portuguese) ...
{
Name: 'Microsoft Server Speech Text to Speech Voice (pt-BR, AntonioNeural)',
ShortName: 'pt-BR-AntonioNeural',
Gender: 'Male',
Locale: 'pt-BR'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (pt-BR, FranciscaNeural)',
ShortName: 'pt-BR-FranciscaNeural',
Gender: 'Female',
Locale: 'pt-BR'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (pt-PT, DuarteNeural)',
ShortName: 'pt-PT-DuarteNeural',
Gender: 'Male',
Locale: 'pt-PT'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (pt-PT, RaquelNeural)',
ShortName: 'pt-PT-RaquelNeural',
Gender: 'Female',
Locale: 'pt-PT'
},
// ... (Romanian, Russian, Sinhala, Slovak, Slovenian, Somali, Albanian, Serbian, Sundanese, Swedish, Swahili, Tamil, Telugu, Thai) ...
{
Name: 'Microsoft Server Speech Text to Speech Voice (th-TH, NiwatNeural)',
ShortName: 'th-TH-NiwatNeural',
Gender: 'Male',
Locale: 'th-TH'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (th-TH, PremwadeeNeural)',
ShortName: 'th-TH-PremwadeeNeural',
Gender: 'Female',
Locale: 'th-TH'
},
// ... (Turkish, Ukrainian, Urdu, Uzbek, Vietnamese) ...
{
Name: 'Microsoft Server Speech Text to Speech Voice (vi-VN, HoaiMyNeural)',
ShortName: 'vi-VN-HoaiMyNeural',
Gender: 'Female',
Locale: 'vi-VN'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (vi-VN, NamMinhNeural)',
ShortName: 'vi-VN-NamMinhNeural',
Gender: 'Male',
Locale: 'vi-VN'
},
// ... (Chinese variants) ...
{
Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, XiaoxiaoNeural)',
ShortName: 'zh-CN-XiaoxiaoNeural',
Gender: 'Female',
Locale: 'zh-CN'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, YunxiNeural)',
ShortName: 'zh-CN-YunxiNeural',
Gender: 'Male',
Locale: 'zh-CN'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, YunjianNeural)',
ShortName: 'zh-CN-YunjianNeural',
Gender: 'Male',
Locale: 'zh-CN'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, YunxiaNeural)',
ShortName: 'zh-CN-YunxiaNeural',
Gender: 'Male',
Locale: 'zh-CN'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, YunyangNeural)',
ShortName: 'zh-CN-YunyangNeural',
Gender: 'Male',
Locale: 'zh-CN'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN-liaoning, XiaobeiNeural)',
ShortName: 'zh-CN-liaoning-XiaobeiNeural',
Gender: 'Female',
Locale: 'zh-CN-liaoning'
},
// { Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN-shaanxi, XiaoniNeural)', ShortName: 'zh-CN-shaanxi-XiaoniNeural', Gender: 'Female', Locale: 'zh-CN-shaanxi' }, // Example regional voice
{
Name: 'Microsoft Server Speech Text to Speech Voice (zh-HK, HiuGaaiNeural)',
ShortName: 'zh-HK-HiuGaaiNeural',
Gender: 'Female',
Locale: 'zh-HK'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (zh-HK, HiuMaanNeural)',
ShortName: 'zh-HK-HiuMaanNeural',
Gender: 'Female',
Locale: 'zh-HK'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (zh-HK, WanLungNeural)',
ShortName: 'zh-HK-WanLungNeural',
Gender: 'Male',
Locale: 'zh-HK'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (zh-TW, HsiaoChenNeural)',
ShortName: 'zh-TW-HsiaoChenNeural',
Gender: 'Female',
Locale: 'zh-TW'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (zh-TW, HsiaoYuNeural)',
ShortName: 'zh-TW-HsiaoYuNeural',
Gender: 'Female',
Locale: 'zh-TW'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (zh-TW, YunJheNeural)',
ShortName: 'zh-TW-YunJheNeural',
Gender: 'Male',
Locale: 'zh-TW'
},
// ... (Zulu) ...
{
Name: 'Microsoft Server Speech Text to Speech Voice (zu-ZA, ThandoNeural)',
ShortName: 'zu-ZA-ThandoNeural',
Gender: 'Female',
Locale: 'zu-ZA'
},
{
Name: 'Microsoft Server Speech Text to Speech Voice (zu-ZA, ThembaNeural)',
ShortName: 'zu-ZA-ThembaNeural',
Gender: 'Male',
Locale: 'zu-ZA'
}
]
// --- END OF HARDCODED VOICE LIST ---
/**
* 免费在线TTS服务
* 使用免费的在线TTS服务不需要API密钥
*/
class MsTTSService {
private static instance: MsTTSService
private tempDir: string
private constructor() {
this.tempDir = path.join(app.getPath('temp'), 'cherry-tts')
if (!fs.existsSync(this.tempDir)) {
fs.mkdirSync(this.tempDir, { recursive: true })
}
log.info('初始化免费在线TTS服务 (使用硬编码语音列表)')
}
public static getInstance(): MsTTSService {
if (!MsTTSService.instance) {
MsTTSService.instance = new MsTTSService()
}
return MsTTSService.instance
}
/**
* 流式合成语音
* @param text 要合成的文本
* @param voice 语音的 ShortName (例如 'zh-CN-XiaoxiaoNeural')
* @param outputFormat 输出格式 (例如 'audio-24khz-48kbitrate-mono-mp3')
* @param onData 数据块回调
* @param onEnd 结束回调
*/
public async synthesizeStream(
text: string,
voice: string,
outputFormat: string,
onData: (chunk: Uint8Array) => void,
onEnd: () => void
): Promise<void> {
try {
// 记录详细的请求信息
log.info(`流式微软在线TTS合成语音: 文本="${text.substring(0, 30)}...", 语音=${voice}, 格式=${outputFormat}`)
// 验证输入参数
if (!text || text.trim() === '') {
throw new Error('要合成的文本不能为空')
}
if (!voice || voice.trim() === '') {
throw new Error('语音名称不能为空')
}
// 创建一个新的MsEdgeTTS实例
const tts = new MsEdgeTTS({
enableLogger: false // 禁用内部日志
})
// 设置元数据
let msOutputFormat: OUTPUT_FORMAT
if (outputFormat.includes('mp3')) {
msOutputFormat = OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3
} else if (outputFormat.includes('webm')) {
msOutputFormat = OUTPUT_FORMAT.WEBM_24KHZ_16BIT_MONO_OPUS
} else {
msOutputFormat = OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3
}
await tts.setMetadata(voice, msOutputFormat)
// 创建流
const audioStream = tts.toStream(text)
// 监听数据事件
audioStream.on('data', (data: Buffer) => {
onData(data)
})
// 监听结束事件
audioStream.on('end', () => {
log.info(`流式微软在线TTS合成成功`)
onEnd()
})
// 监听错误事件
audioStream.on('error', (error: Error) => {
log.error(`流式微软在线TTS语音合成失败:`, error)
throw error
})
} catch (error: any) {
// 记录详细的错误信息
log.error(`流式微软在线TTS语音合成失败 (语音=${voice}):`, error)
throw error
}
}
/**
* 获取可用的语音列表 (返回硬编码列表)
* @returns 语音列表
*/
public async getVoices(): Promise<any[]> {
try {
log.info(`返回硬编码的 ${hardcodedVoices.length} 个语音列表`)
// 直接返回硬编码的列表
// 注意:保持 async 是为了接口兼容性,虽然这里没有实际的异步操作
return hardcodedVoices
} catch (error) {
// 这个 try/catch 在这里意义不大了,因为返回静态数据不会出错
// 但保留结构以防未来改动
log.error('获取硬编码语音列表时出错 (理论上不应发生):', error)
return [] // 返回空列表以防万一
}
}
/**
* 合成语音
* @param text 要合成的文本
* @param voice 语音的 ShortName (例如 'zh-CN-XiaoxiaoNeural')
* @param outputFormat 输出格式 (例如 'audio-24khz-48kbitrate-mono-mp3')
* @returns 音频文件路径
*/
public async synthesize(text: string, voice: string, outputFormat: string): Promise<string> {
try {
// 记录详细的请求信息
log.info(`微软在线TTS合成语音: 文本="${text.substring(0, 30)}...", 语音=${voice}, 格式=${outputFormat}`)
// 验证输入参数
if (!text || text.trim() === '') {
throw new Error('要合成的文本不能为空')
}
if (!voice || voice.trim() === '') {
throw new Error('语音名称不能为空')
}
// 创建一个新的EdgeTTS实例并设置参数
// 添加超时设置默认为30秒
const tts = new EdgeTTS({
voice: voice,
outputFormat: outputFormat,
timeout: 30000, // 30秒超时
rate: '+0%', // 正常语速
pitch: '+0Hz', // 正常音调
volume: '+0%' // 正常音量
})
// 生成临时文件路径
const timestamp = Date.now()
const fileExtension = outputFormat.includes('mp3') ? 'mp3' : outputFormat.split('-').pop() || 'audio'
const outputPath = path.join(this.tempDir, `tts_${timestamp}.${fileExtension}`)
log.info(`开始生成语音文件: ${outputPath}`)
// 使用ttsPromise方法生成文件
await tts.ttsPromise(text, outputPath)
// 验证生成的文件是否存在且大小大于0
if (!fs.existsSync(outputPath)) {
throw new Error(`生成的语音文件不存在: ${outputPath}`)
}
const stats = fs.statSync(outputPath)
if (stats.size === 0) {
throw new Error(`生成的语音文件大小为0: ${outputPath}`)
}
log.info(`微软在线TTS合成成功: ${outputPath}, 文件大小: ${stats.size} 字节`)
return outputPath
} catch (error: any) {
// 记录详细的错误信息
log.error(`微软在线TTS语音合成失败 (语音=${voice}):`, error)
// 尝试提供更有用的错误信息
if (error.message && typeof error.message === 'string') {
if (error.message.includes('Timed out')) {
throw new Error(`语音合成超时,请检查网络连接或尝试其他语音`)
} else if (error.message.includes('ENOTFOUND')) {
throw new Error(`无法连接到微软语音服务,请检查网络连接`)
} else if (error.message.includes('ECONNREFUSED')) {
throw new Error(`连接被拒绝,请检查网络设置或代理配置`)
}
}
throw error
}
}
/**
* (可选) 清理临时文件目录
*/
public async cleanupTempDir(): Promise<void> {
// (Cleanup method remains the same)
try {
const files = await fs.promises.readdir(this.tempDir)
for (const file of files) {
if (file.startsWith('tts_')) {
await fs.promises.unlink(path.join(this.tempDir, file))
}
}
log.info('TTS 临时文件已清理')
} catch (error) {
log.error('清理 TTS 临时文件失败:', error)
}
}
}
// 导出单例方法 (保持不变)
export const getVoices = async () => {
return await MsTTSService.getInstance().getVoices()
}
export const synthesize = async (text: string, voice: string, outputFormat: string) => {
return await MsTTSService.getInstance().synthesize(text, voice, outputFormat)
}
export const synthesizeStream = async (
text: string,
voice: string,
outputFormat: string,
onData: (chunk: Uint8Array) => void,
onEnd: () => void
) => {
return await MsTTSService.getInstance().synthesizeStream(text, voice, outputFormat, onData, onEnd)
}
export const cleanupTtsTempFiles = async () => {
await MsTTSService.getInstance().cleanupTempDir()
}

View File

@@ -1,3 +1,12 @@
import { exec } from 'node:child_process'
import fs from 'node:fs/promises'
import path from 'node:path'
import { promisify } from 'node:util'
import { app } from 'electron'
import Logger from 'electron-log'
import { handleMcpProtocolUrl } from './urlschema/mcp-install'
import { windowService } from './WindowService'
export const CHERRY_STUDIO_PROTOCOL = 'cherrystudio'
@@ -22,6 +31,12 @@ export function handleProtocolUrl(url: string) {
const urlObj = new URL(url)
const params = new URLSearchParams(urlObj.search)
switch (urlObj.hostname.toLowerCase()) {
case 'mcp':
handleMcpProtocolUrl(urlObj)
return
}
// You can send the data to your renderer process
const mainWindow = windowService.getMainWindow()
@@ -32,3 +47,78 @@ export function handleProtocolUrl(url: string) {
})
}
}
const execAsync = promisify(exec)
const DESKTOP_FILE_NAME = 'cherrystudio-url-handler.desktop'
/**
* Sets up deep linking for the AppImage build on Linux by creating a .desktop file.
* This allows the OS to open cherrystudio:// URLs with this App.
*/
export async function setupAppImageDeepLink(): Promise<void> {
// Only run on Linux and when packaged as an AppImage
if (process.platform !== 'linux' || !process.env.APPIMAGE) {
return
}
Logger.info('AppImage environment detected on Linux, setting up deep link.')
try {
const appPath = app.getPath('exe')
if (!appPath) {
Logger.error('Could not determine App path.')
return
}
const homeDir = app.getPath('home')
const applicationsDir = path.join(homeDir, '.local', 'share', 'applications')
const desktopFilePath = path.join(applicationsDir, DESKTOP_FILE_NAME)
// Ensure the applications directory exists
await fs.mkdir(applicationsDir, { recursive: true })
// Content of the .desktop file
// %U allows passing the URL to the application
// NoDisplay=true hides it from the regular application menu
const desktopFileContent = `[Desktop Entry]
Name=Cherry Studio
Exec=${escapePathForExec(appPath)} %U
Terminal=false
Type=Application
MimeType=x-scheme-handler/${CHERRY_STUDIO_PROTOCOL};
NoDisplay=true
`
// Write the .desktop file (overwrite if exists)
await fs.writeFile(desktopFilePath, desktopFileContent, 'utf-8')
Logger.info(`Created/Updated desktop file: ${desktopFilePath}`)
// Update the desktop database
// It's important to update the database for the changes to take effect
try {
const { stdout, stderr } = await execAsync(`update-desktop-database ${escapePathForExec(applicationsDir)}`)
if (stderr) {
Logger.warn(`update-desktop-database stderr: ${stderr}`)
}
Logger.info(`update-desktop-database stdout: ${stdout}`)
Logger.info('Desktop database updated successfully.')
} catch (updateError) {
Logger.error('Failed to update desktop database:', updateError)
// Continue even if update fails, as the file is still created.
}
} catch (error) {
// Log the error but don't prevent the app from starting
Logger.error('Failed to setup AppImage deep link:', error)
}
}
/**
* Escapes a path for safe use within the Exec field of a .desktop file
* and for shell commands. Handles spaces and potentially other special characters
* by quoting.
*/
function escapePathForExec(filePath: string): string {
// Simple quoting for paths with spaces.
return `'${filePath.replace(/'/g, "'\\''")}'`
}

View File

@@ -1,5 +1,6 @@
import { ProxyConfig as _ProxyConfig, session } from 'electron'
import { socksDispatcher } from 'fetch-socks'
import { getSystemProxy } from 'os-proxy-config'
import { ProxyAgent as GeneralProxyAgent } from 'proxy-agent'
import { ProxyAgent, setGlobalDispatcher } from 'undici'
@@ -70,15 +71,14 @@ export class ProxyManager {
private async setSystemProxy(): Promise<void> {
try {
await this.setSessionsProxy({ mode: 'system' })
const proxyString = await session.defaultSession.resolveProxy('https://dummy.com')
const [protocol, address] = proxyString.split(';')[0].split(' ')
const url = protocol === 'PROXY' ? `http://${address}` : null
if (url && url !== this.config.url) {
this.config.url = url.toLowerCase()
this.setEnvironment(this.config.url)
this.proxyAgent = new GeneralProxyAgent()
const currentProxy = await getSystemProxy()
if (!currentProxy || currentProxy.proxyUrl === this.config.url) {
return
}
await this.setSessionsProxy({ mode: 'system' })
this.config.url = currentProxy.proxyUrl.toLowerCase()
this.setEnvironment(this.config.url)
this.proxyAgent = new GeneralProxyAgent()
} catch (error) {
console.error('Failed to set system proxy:', error)
throw error

View File

@@ -0,0 +1,35 @@
import { session, shell, webContents } from 'electron'
/**
* init the useragent of the webview session
* remove the CherryStudio and Electron from the useragent
*/
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}`)
wvSession.setUserAgent(newUA)
}
/**
* WebviewService handles the behavior of links opened from webview elements
* It controls whether links should be opened within the application or in an external browser
*/
export function setOpenLinkExternal(webviewId: number, isExternal: boolean) {
const webview = webContents.fromId(webviewId)
if (!webview) return
webview.setWindowOpenHandler(({ url }) => {
if (isExternal) {
shell.openExternal(url)
return { action: 'deny' }
} else {
return { action: 'allow' }
}
})
}

View File

@@ -2,7 +2,8 @@ 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 { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
import { ThemeMode } from '@types'
import { app, BrowserWindow, ipcMain, Menu, MenuItem, nativeTheme, shell } from 'electron'
import Logger from 'electron-log'
import windowStateKeeper from 'electron-window-state'
import { join } from 'path'
@@ -11,6 +12,7 @@ import icon from '../../../build/icon.png?asset'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
import { locales } from '../utils/locales'
import { configManager } from './ConfigManager'
import { initSessionUserAgent } from './WebviewService'
export class WindowService {
private static instance: WindowService | null = null
@@ -41,10 +43,16 @@ export class WindowService {
const mainWindowState = windowStateKeeper({
defaultWidth: 1080,
defaultHeight: 670,
fullScreen: false
fullScreen: false,
maximize: false
})
const theme = configManager.getTheme()
if (theme === ThemeMode.auto) {
nativeTheme.themeSource = 'system'
} else {
nativeTheme.themeSource = theme
}
this.mainWindow = new BrowserWindow({
x: mainWindowState.x,
@@ -59,8 +67,9 @@ export class WindowService {
vibrancy: 'sidebar',
visualEffectState: 'active',
titleBarStyle: isLinux ? 'default' : 'hidden',
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
darkTheme: nativeTheme.shouldUseDarkColors,
trafficLightPosition: { x: 8, y: 12 },
...(isLinux ? { icon } : {}),
webPreferences: {
@@ -80,12 +89,16 @@ export class WindowService {
this.miniWindow = this.createMiniWindow(true)
}
//init the MinApp webviews' useragent
initSessionUserAgent()
return this.mainWindow
}
private setupMainWindow(mainWindow: BrowserWindow, mainWindowState: any) {
mainWindowState.manage(mainWindow)
this.setupMaximize(mainWindow, mainWindowState.isMaximized)
this.setupContextMenu(mainWindow)
this.setupWindowEvents(mainWindow)
this.setupWebContentsHandlers(mainWindow)
@@ -93,6 +106,17 @@ export class WindowService {
this.loadMainWindowContent(mainWindow)
}
private setupMaximize(mainWindow: BrowserWindow, isMaximized: boolean) {
if (isMaximized) {
// 如果是从托盘启动,则需要延迟最大化,否则显示的就不是重启前的最大化窗口了
configManager.getLaunchToTray()
? mainWindow.once('show', () => {
mainWindow.maximize()
})
: mainWindow.maximize()
}
}
private setupContextMenu(mainWindow: BrowserWindow) {
if (!this.contextMenu) {
const locale = locales[configManager.getLanguage()]
@@ -191,9 +215,11 @@ export class WindowService {
const oauthProviderUrls = [
'https://account.siliconflow.cn/oauth',
'https://cloud.siliconflow.cn/bills',
'https://cloud.siliconflow.cn/expensebill',
'https://aihubmix.com/token',
'https://aihubmix.com/topup'
'https://aihubmix.com/topup',
'https://aihubmix.com/statistics'
]
if (oauthProviderUrls.some((link) => url.startsWith(link))) {

View File

@@ -21,7 +21,7 @@ export class CallBackServer {
if (req.url?.startsWith(path)) {
try {
// Parse the URL to extract the authorization code
const url = new URL(req.url, `http://localhost:${port}`)
const url = new URL(req.url, `http://127.0.0.1:${port}`)
const code = url.searchParams.get('code')
if (code) {
// Emit the code event

View File

@@ -27,7 +27,7 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
}
get redirectUrl(): string {
return `http://localhost:${this.config.callbackPort}${this.config.callbackPath}`
return `http://127.0.0.1:${this.config.callbackPort}${this.config.callbackPath}`
}
get clientMetadata() {

View File

@@ -0,0 +1,76 @@
import { nanoid } from '@reduxjs/toolkit'
import { IpcChannel } from '@shared/IpcChannel'
import { MCPServer } from '@types'
import Logger from 'electron-log'
import { windowService } from '../WindowService'
function installMCPServer(server: MCPServer) {
const mainWindow = windowService.getMainWindow()
if (!server.id) {
server.id = nanoid()
}
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(IpcChannel.Mcp_AddServer, server)
}
}
function installMCPServers(servers: Record<string, MCPServer>) {
for (const name in servers) {
const server = servers[name]
if (!server.name) {
server.name = name
}
installMCPServer(server)
}
}
export function handleMcpProtocolUrl(url: URL) {
const params = new URLSearchParams(url.search)
switch (url.pathname) {
case '/install': {
// jsonConfig example:
// {
// "mcpServers": {
// "everything": {
// "command": "npx",
// "args": [
// "-y",
// "@modelcontextprotocol/server-everything"
// ]
// }
// }
// }
// cherrystudio://mcp/install?servers={base64Encode(JSON.stringify(jsonConfig))}
const data = params.get('servers')
if (data) {
const stringify = Buffer.from(data, 'base64').toString('utf8')
Logger.info('install MCP servers from urlschema: ', stringify)
const jsonConfig = JSON.parse(stringify)
Logger.info('install MCP servers from urlschema: ', jsonConfig)
// support both {mcpServers: [servers]}, [servers] and {server}
if (jsonConfig.mcpServers) {
installMCPServers(jsonConfig.mcpServers)
} else if (Array.isArray(jsonConfig)) {
for (const server of jsonConfig) {
installMCPServer(server)
}
} else {
installMCPServer(jsonConfig)
}
}
const mainWindow = windowService.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.executeJavaScript("window.navigate('/settings/mcp')")
}
break
}
default:
console.error(`Unknown MCP protocol URL: ${url}`)
break
}
}

View File

@@ -2,6 +2,7 @@ import * as fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { isMac } from '@main/constant'
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant'
import { FileType, FileTypes } from '@types'
import { app } from 'electron'
@@ -83,3 +84,12 @@ export function getConfigDir() {
export function getAppConfigDir(name: string) {
return path.join(getConfigDir(), name)
}
export function setUserDataDir() {
if (!isMac) {
const dir = path.join(path.dirname(app.getPath('exe')), 'data')
if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
app.setPath('userData', dir)
}
}
}

View File

@@ -4,8 +4,7 @@ import path from 'node:path'
import { app } from 'electron'
export function getResourcePath() {
// 在打包环境中使用process.resourcesPath否则使用app.getAppPath()/resources
return app.isPackaged ? process.resourcesPath : path.join(app.getAppPath(), 'resources')
return path.join(app.getAppPath(), 'resources')
}
export function getDataPath() {

209
src/preload/index.d.ts vendored
View File

@@ -1,209 +0,0 @@
import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { ElectronAPI } from '@electron-toolkit/preload'
import type { File } from '@google/genai'
import type { GetMCPPromptResponse, MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
import { AppInfo, FileType, KnowledgeBaseParams, KnowledgeItem, LanguageVarious, WebDavConfig } from '@renderer/types'
import type { LoaderReturn } from '@shared/config/types'
import type { OpenDialogOptions } from 'electron'
import type { UpdateInfo } from 'electron-updater'
interface BackupFile {
fileName: string
modifiedTime: string
size: number
}
declare global {
interface Window {
electron: ElectronAPI
api: {
getAppInfo: () => Promise<AppInfo>
checkForUpdate: () => Promise<{ currentVersion: string; updateInfo: UpdateInfo | null }>
showUpdateDialog: () => Promise<void>
openWebsite: (url: string) => void
setProxy: (proxy: string | undefined) => void
setLanguage: (theme: LanguageVarious) => void
setLaunchOnBoot: (isActive: boolean) => void
setLaunchToTray: (isActive: boolean) => void
setTray: (isActive: boolean) => void
setTrayOnClose: (isActive: boolean) => void
restartTray: () => void
setTheme: (theme: 'light' | 'dark') => void
setCustomCss: (css: string) => void
setAutoUpdate: (isActive: boolean) => void
reload: () => void
clearCache: () => Promise<{ success: boolean; error?: string }>
system: {
getDeviceType: () => Promise<'mac' | 'windows' | 'linux'>
getHostname: () => Promise<string>
}
zip: {
compress: (text: string) => Promise<Buffer>
decompress: (text: Buffer) => Promise<string>
}
backup: {
backup: (fileName: string, data: string, destinationPath?: string) => Promise<Readable>
restore: (backupPath: string) => Promise<string>
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise<boolean>
restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise<string>
listWebdavFiles: (webdavConfig: WebDavConfig) => Promise<BackupFile[]>
checkConnection: (webdavConfig: WebDavConfig) => Promise<boolean>
createDirectory: (webdavConfig: WebDavConfig, path: string, options?: CreateDirectoryOptions) => Promise<void>
deleteWebdavFile: (fileName: string, webdavConfig: WebDavConfig) => Promise<boolean>
}
file: {
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
upload: (file: FileType) => Promise<FileType>
delete: (fileId: string) => Promise<void>
read: (fileId: string) => Promise<string>
clear: () => Promise<void>
get: (filePath: string) => Promise<FileType | null>
selectFolder: () => Promise<string | null>
create: (fileName: string) => Promise<string>
write: (filePath: string, data: Uint8Array | string) => Promise<void>
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null>
openPath: (path: string) => Promise<void>
save: (
path: string,
content: string | NodeJS.ArrayBufferView,
options?: SaveDialogOptions
) => Promise<string | null>
saveImage: (name: string, data: string) => void
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
download: (url: string) => Promise<FileType | null>
copy: (fileId: string, destPath: string) => Promise<void>
binaryFile: (fileId: string) => Promise<{ data: Buffer; mime: string }>
}
fs: {
read: (path: string) => Promise<string>
}
export: {
toWord: (markdown: string, fileName: string) => Promise<void>
}
openPath: (path: string) => Promise<void>
shortcuts: {
update: (shortcuts: Shortcut[]) => Promise<void>
}
knowledgeBase: {
create: (base: KnowledgeBaseParams) => Promise<void>
reset: (base: KnowledgeBaseParams) => Promise<void>
delete: (id: string) => Promise<void>
add: ({
base,
item,
forceReload = false
}: {
base: KnowledgeBaseParams
item: KnowledgeItem
forceReload?: boolean
}) => Promise<LoaderReturn>
remove: ({
uniqueId,
uniqueIds,
base
}: {
uniqueId: string
uniqueIds: string[]
base: KnowledgeBaseParams
}) => Promise<void>
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => Promise<ExtractChunkData[]>
rerank: ({
search,
base,
results
}: {
search: string
base: KnowledgeBaseParams
results: ExtractChunkData[]
}) => Promise<ExtractChunkData[]>
}
window: {
setMinimumSize: (width: number, height: number) => Promise<void>
resetMinimumSize: () => Promise<void>
}
gemini: {
uploadFile: (file: FileType, apiKey: string) => Promise<File>
retrieveFile: (file: FileType, apiKey: string) => Promise<File | undefined>
base64File: (file: FileType) => Promise<{ data: string; mimeType: string }>
listFiles: (apiKey: string) => Promise<File[]>
deleteFile: (fileId: string, apiKey: string) => Promise<void>
}
selectionMenu: {
action: (action: string) => Promise<void>
}
config: {
set: (key: string, value: any) => Promise<void>
get: (key: string) => Promise<any>
}
miniWindow: {
show: () => Promise<void>
hide: () => Promise<void>
close: () => Promise<void>
toggle: () => Promise<void>
setPin: (isPinned: boolean) => Promise<void>
}
aes: {
encrypt: (text: string, secretKey: string, iv: string) => Promise<{ iv: string; encryptedData: string }>
decrypt: (encryptedData: string, iv: string, secretKey: string) => Promise<string>
}
shell: {
openExternal: (url: string, options?: OpenExternalOptions) => Promise<void>
}
mcp: {
removeServer: (server: MCPServer) => Promise<void>
restartServer: (server: MCPServer) => Promise<void>
stopServer: (server: MCPServer) => Promise<void>
listTools: (server: MCPServer) => Promise<MCPTool[]>
callTool: ({
server,
name,
args
}: {
server: MCPServer
name: string
args: any
}) => Promise<MCPCallToolResponse>
listPrompts: (server: MCPServer) => Promise<MCPPrompt[]>
getPrompt: ({
server,
name,
args
}: {
server: MCPServer
name: string
args?: Record<string, any>
}) => Promise<GetMCPPromptResponse>
listResources: (server: MCPServer) => Promise<MCPResource[]>
getResource: ({ server, uri }: { server: MCPServer; uri: string }) => Promise<GetResourceResponse>
getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }>
}
copilot: {
getAuthMessage: (
headers?: Record<string, string>
) => Promise<{ device_code: string; user_code: string; verification_uri: string }>
getCopilotToken: (device_code: string, headers?: Record<string, string>) => Promise<{ access_token: string }>
saveCopilotToken: (access_token: string) => Promise<void>
getToken: (headers?: Record<string, string>) => Promise<{ token: string }>
logout: () => Promise<void>
getUser: (token: string) => Promise<{ login: string; avatar: string }>
}
isBinaryExist: (name: string) => Promise<boolean>
getBinaryPath: (name: string) => Promise<string>
installUVBinary: () => Promise<void>
installBunBinary: () => Promise<void>
protocol: {
onReceiveData: (callback: (data: { url: string; params: any }) => void) => () => void
}
nutstore: {
getSSOUrl: () => Promise<string>
decryptToken: (token: string) => Promise<{ username: string; access_token: string }>
getDirectoryContents: (token: string, path: string) => Promise<any>
}
searchService: {
openSearchWindow: (uid: string) => Promise<string>
closeSearchWindow: (uid: string) => Promise<string>
openUrlInSearchWindow: (uid: string, url: string) => Promise<string>
}
}
}
}

View File

@@ -9,7 +9,7 @@ import { CreateDirectoryOptions } from 'webdav'
const api = {
getAppInfo: () => ipcRenderer.invoke(IpcChannel.App_Info),
reload: () => ipcRenderer.invoke(IpcChannel.App_Reload),
setProxy: (proxy: string) => ipcRenderer.invoke(IpcChannel.App_Proxy, proxy),
setProxy: (proxy: string | undefined) => ipcRenderer.invoke(IpcChannel.App_Proxy, proxy),
checkForUpdate: () => ipcRenderer.invoke(IpcChannel.App_CheckForUpdate),
showUpdateDialog: () => ipcRenderer.invoke(IpcChannel.App_ShowUpdateDialog),
setLanguage: (lang: string) => ipcRenderer.invoke(IpcChannel.App_SetLanguage, lang),
@@ -18,7 +18,7 @@ const api = {
setTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTray, isActive),
setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive),
restartTray: () => ipcRenderer.invoke(IpcChannel.App_RestartTray),
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
setTheme: (theme: 'light' | 'dark' | 'auto') => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
setCustomCss: (css: string) => ipcRenderer.invoke(IpcChannel.App_SetCustomCss, css),
setAutoUpdate: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetAutoUpdate, isActive),
openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
@@ -50,16 +50,16 @@ const api = {
},
file: {
select: (options?: OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.File_Select, options),
upload: (filePath: string) => ipcRenderer.invoke(IpcChannel.File_Upload, filePath),
upload: (file: FileType) => ipcRenderer.invoke(IpcChannel.File_Upload, file),
delete: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Delete, fileId),
read: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Read, fileId),
clear: () => ipcRenderer.invoke(IpcChannel.File_Clear),
get: (filePath: string) => ipcRenderer.invoke(IpcChannel.File_Get, filePath),
create: (fileName: string) => ipcRenderer.invoke(IpcChannel.File_Create, fileName),
write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke(IpcChannel.File_Write, filePath, data),
open: (options?: { decompress: boolean }) => ipcRenderer.invoke(IpcChannel.File_Open, options),
open: (options?: OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.File_Open, options),
openPath: (path: string) => ipcRenderer.invoke(IpcChannel.File_OpenPath, path),
save: (path: string, content: string, options?: { compress: boolean }) =>
save: (path: string, content: string | NodeJS.ArrayBufferView, options?: any) =>
ipcRenderer.invoke(IpcChannel.File_Save, path, content, options),
selectFolder: () => ipcRenderer.invoke(IpcChannel.File_SelectFolder),
saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
@@ -69,7 +69,7 @@ const api = {
binaryFile: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryFile, fileId)
},
fs: {
read: (path: string, encoding?: BufferEncoding) => ipcRenderer.invoke(IpcChannel.Fs_Read, path, encoding)
read: (path: string) => ipcRenderer.invoke(IpcChannel.Fs_Read, path)
},
export: {
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_Word, markdown, fileName)
@@ -108,7 +108,7 @@ const api = {
base64File: (file: FileType) => ipcRenderer.invoke(IpcChannel.Gemini_Base64File, file),
retrieveFile: (file: FileType, apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_RetrieveFile, file, apiKey),
listFiles: (apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_ListFiles, apiKey),
deleteFile: (apiKey: string, fileId: string) => ipcRenderer.invoke(IpcChannel.Gemini_DeleteFile, apiKey, fileId)
deleteFile: (fileId: string, apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_DeleteFile, fileId, apiKey)
},
selectionMenu: {
action: (action: string) => ipcRenderer.invoke(IpcChannel.SelectionMenu_Action, action)
@@ -124,11 +124,6 @@ const api = {
toggle: () => ipcRenderer.invoke(IpcChannel.MiniWindow_Toggle),
setPin: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.MiniWindow_SetPin, isPinned)
},
msTTS: {
getVoices: () => ipcRenderer.invoke(IpcChannel.MsTTS_GetVoices),
synthesize: (text: string, voice: string, outputFormat: string) =>
ipcRenderer.invoke(IpcChannel.MsTTS_Synthesize, text, voice, outputFormat)
},
aes: {
encrypt: (text: string, secretKey: string, iv: string) =>
ipcRenderer.invoke(IpcChannel.Aes_Encrypt, text, secretKey, iv),
@@ -140,7 +135,7 @@ const api = {
restartServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RestartServer, server),
stopServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_StopServer, server),
listTools: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListTools, server),
callTool: ({ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }) =>
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) =>
ipcRenderer.invoke(IpcChannel.Mcp_CallTool, { server, name, args }),
listPrompts: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListPrompts, server),
getPrompt: ({ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }) =>
@@ -151,7 +146,7 @@ const api = {
getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo)
},
shell: {
openExternal: shell.openExternal
openExternal: (url: string, options?: Electron.OpenExternalOptions) => shell.openExternal(url, options)
},
copilot: {
getAuthMessage: (headers?: Record<string, string>) =>
@@ -191,9 +186,9 @@ const api = {
closeSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Close, uid),
openUrlInSearchWindow: (uid: string, url: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_OpenUrl, uid, url)
},
asrServer: {
startServer: () => ipcRenderer.invoke(IpcChannel.Asr_StartServer),
stopServer: (pid: number) => ipcRenderer.invoke(IpcChannel.Asr_StopServer, pid)
webview: {
setOpenLinkExternal: (webviewId: number, isExternal: boolean) =>
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal)
}
}
@@ -218,3 +213,5 @@ if (process.contextIsolated) {
// @ts-ignore (define in dts)
window.api = api
}
export type WindowApiType = typeof api

11
src/preload/preload.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import type { WindowApiType } from './index'
/** you don't need to declare this in your code, it's automatically generated */
declare global {
interface Window {
electron: ElectronAPI
api: WindowApiType
}
}

View File

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

View File

@@ -17,7 +17,7 @@ import AppsPage from './pages/apps/AppsPage'
import FilesPage from './pages/files/FilesPage'
import HomePage from './pages/home/HomePage'
import KnowledgePage from './pages/knowledge/KnowledgePage'
import PaintingsPage from './pages/paintings/PaintingsPage'
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
@@ -36,7 +36,7 @@ function App(): React.ReactElement {
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings" element={<PaintingsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />

View File

@@ -1,395 +0,0 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Browser ASR (External)</title>
<style>
body {
font-family: sans-serif;
padding: 1em;
}
#status {
margin-top: 1em;
font-style: italic;
color: #555;
}
#result {
margin-top: 0.5em;
border: 1px solid #ccc;
padding: 0.5em;
min-height: 50px;
background: #f9f9f9;
}
</style>
</head>
<body>
<h1>浏览器语音识别中继页面</h1>
<p>这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。</p>
<div id="status">正在连接到服务器...</div>
<div id="result"></div>
<script>
const statusDiv = document.getElementById('status');
const resultDiv = document.getElementById('result');
const ws = new WebSocket('ws://localhost:8080'); // Use the defined port
let recognition = null;
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
function updateStatus(message) {
console.log(`[Browser Page Status] ${message}`);
statusDiv.textContent = message;
}
ws.onopen = () => {
updateStatus('已连接到服务器,等待指令...');
ws.send(JSON.stringify({ type: 'identify', role: 'browser' }));
};
ws.onmessage = (event) => {
let data;
try {
data = JSON.parse(event.data);
console.log('[Browser Page] Received command:', data);
} catch (e) {
console.error('[Browser Page] Received non-JSON message:', event.data);
return;
}
if (data.type === 'start') {
startRecognition();
} else if (data.type === 'stop') {
stopRecognition();
} else if (data.type === 'reset') {
// 强制重置语音识别
forceResetRecognition();
} else {
console.warn('[Browser Page] Received unknown command type:', data.type);
}
};
ws.onerror = (error) => {
console.error('[Browser Page] WebSocket Error:', error);
updateStatus('WebSocket 连接错误!请检查服务器是否运行。');
};
ws.onclose = () => {
console.log('[Browser Page] WebSocket Connection Closed');
updateStatus('与服务器断开连接。请刷新页面或重启服务器。');
stopRecognition();
};
function setupRecognition() {
if (!SpeechRecognition) {
updateStatus('错误:此浏览器不支持 Web Speech API。');
return false;
}
if (recognition && recognition.recognizing) {
console.log('[Browser Page] Recognition already active.');
return true;
}
recognition = new SpeechRecognition();
recognition.lang = 'zh-CN';
recognition.continuous = true;
recognition.interimResults = true;
// 增加以下设置提高语音识别的可靠性
recognition.maxAlternatives = 3; // 返回多个可能的识别结果
// 设置较短的语音识别时间,使用户能更快地看到结果
// 注意:这个属性不是标准的,可能不是所有浏览器都支持
try {
// @ts-ignore
recognition.audioStart = 0.1; // 尝试设置较低的起始音量阈值
} catch (e) {
console.log('[Browser Page] audioStart property not supported');
}
recognition.onstart = () => {
updateStatus("🎤 正在识别...");
console.log('[Browser Page] SpeechRecognition started.');
};
recognition.onresult = (event) => {
console.log('[Browser Page] Recognition result event:', event);
let interim_transcript = '';
let final_transcript = '';
// 输出识别结果的详细信息便于调试
for (let i = event.resultIndex; i < event.results.length; ++i) {
const confidence = event.results[i][0].confidence;
console.log(`[Browser Page] Result ${i}: ${event.results[i][0].transcript} (Confidence: ${confidence.toFixed(2)})`);
if (event.results[i].isFinal) {
final_transcript += event.results[i][0].transcript;
} else {
interim_transcript += event.results[i][0].transcript;
}
}
const resultText = final_transcript || interim_transcript;
resultDiv.textContent = resultText;
// 更新状态显示
if (resultText) {
updateStatus(`🎤 正在识别... (已捕捉到语音)`);
}
if (ws.readyState === WebSocket.OPEN) {
console.log(`[Browser Page] Sending ${final_transcript ? 'final' : 'interim'} result to server:`, resultText);
ws.send(JSON.stringify({ type: 'result', data: { text: resultText, isFinal: !!final_transcript } }));
}
};
recognition.onerror = (event) => {
console.error(`[Browser Page] SpeechRecognition Error - Type: ${event.error}, Message: ${event.message}`);
// 根据错误类型提供更友好的错误提示
let errorMessage = '';
switch (event.error) {
case 'no-speech':
errorMessage = '未检测到语音,请确保麦克风工作正常并尝试说话。';
// 尝试重新启动语音识别
setTimeout(() => {
if (recognition) {
try {
recognition.start();
console.log('[Browser Page] Restarting recognition after no-speech error');
} catch (e) {
console.error('[Browser Page] Failed to restart recognition:', e);
}
}
}, 1000);
break;
case 'audio-capture':
errorMessage = '无法捕获音频,请确保麦克风已连接并已授权。';
break;
case 'not-allowed':
errorMessage = '浏览器不允许使用麦克风,请检查权限设置。';
break;
case 'network':
errorMessage = '网络错误导致语音识别失败。';
break;
case 'aborted':
errorMessage = '语音识别被用户或系统中止。';
break;
default:
errorMessage = `识别错误: ${event.error}`;
}
updateStatus(`错误: ${errorMessage}`);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'error',
data: {
error: event.error,
message: errorMessage || event.message || `Recognition error: ${event.error}`
}
}));
}
};
recognition.onend = () => {
console.log('[Browser Page] SpeechRecognition ended.');
// 检查是否是由于错误或用户手动停止导致的结束
const isErrorOrStopped = statusDiv.textContent.includes('错误') || statusDiv.textContent.includes('停止');
if (!isErrorOrStopped) {
// 如果不是由于错误或手动停止,则自动重新启动语音识别
updateStatus("识别暂停,正在重新启动...");
// 保存当前的recognition对象
const currentRecognition = recognition;
// 尝试重新启动语音识别
setTimeout(() => {
try {
if (currentRecognition && currentRecognition === recognition) {
currentRecognition.start();
console.log('[Browser Page] Automatically restarting recognition');
} else {
// 如果recognition对象已经变化重新创建一个
setupRecognition();
if (recognition) {
recognition.start();
console.log('[Browser Page] Created new recognition instance and started');
}
}
} catch (e) {
console.error('[Browser Page] Failed to restart recognition:', e);
updateStatus("识别已停止。等待指令...");
}
}, 300);
} else {
updateStatus("识别已停止。等待指令...");
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'status', message: 'stopped' }));
}
// 只有在手动停止或错误时才重置recognition对象
recognition = null;
}
};
return true;
}
function startRecognition() {
if (!SpeechRecognition) {
updateStatus('错误:浏览器不支持 Web Speech API。');
return;
}
// 显示正在准备的状态
updateStatus('正在准备麦克风...');
if (recognition) {
console.log('[Browser Page] Recognition already exists, stopping first.');
stopRecognition();
}
if (!setupRecognition()) return;
console.log('[Browser Page] Attempting to start recognition...');
try {
// 设置更长的超时时间,确保有足够的时间获取麦克风权限
const micPermissionTimeout = setTimeout(() => {
updateStatus('获取麦克风权限超时,请刷新页面重试。');
}, 10000); // 10秒超时
navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
})
.then(stream => {
clearTimeout(micPermissionTimeout);
console.log('[Browser Page] Microphone access granted.');
// 检查麦克风音量级别
const audioContext = new AudioContext();
const analyser = audioContext.createAnalyser();
const microphone = audioContext.createMediaStreamSource(stream);
const javascriptNode = audioContext.createScriptProcessor(2048, 1, 1);
analyser.smoothingTimeConstant = 0.8;
analyser.fftSize = 1024;
microphone.connect(analyser);
analyser.connect(javascriptNode);
javascriptNode.connect(audioContext.destination);
javascriptNode.onaudioprocess = function () {
const array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);
let values = 0;
const length = array.length;
for (let i = 0; i < length; i++) {
values += (array[i]);
}
const average = values / length;
console.log('[Browser Page] Microphone volume level:', average);
// 如果音量太低,显示提示
if (average < 5) {
updateStatus('麦克风音量很低,请说话或检查麦克风设置。');
} else {
updateStatus('🎤 正在识别...');
}
// 只检查一次就断开连接
microphone.disconnect();
analyser.disconnect();
javascriptNode.disconnect();
};
// 释放测试用的音频流
setTimeout(() => {
stream.getTracks().forEach(track => track.stop());
audioContext.close();
}, 1000);
// 启动语音识别
if (recognition) {
recognition.start();
updateStatus('🎤 正在识别...');
} else {
updateStatus('错误Recognition 实例丢失。');
console.error('[Browser Page] Recognition instance lost before start.');
}
})
.catch(err => {
clearTimeout(micPermissionTimeout);
console.error('[Browser Page] Microphone access error:', err);
let errorMsg = `无法访问麦克风 (${err.name})`;
if (err.name === 'NotAllowedError') {
errorMsg = '麦克风访问被拒绝。请在浏览器设置中允许麦克风访问权限。';
} else if (err.name === 'NotFoundError') {
errorMsg = '未找到麦克风设备。请确保麦克风已连接。';
}
updateStatus(`错误: ${errorMsg}`);
recognition = null;
});
} catch (e) {
console.error('[Browser Page] Error calling recognition.start():', e);
updateStatus(`启动识别时出错: ${e.message}`);
recognition = null;
}
}
function stopRecognition() {
if (recognition) {
console.log('[Browser Page] Stopping recognition...');
updateStatus("正在停止识别...");
try {
recognition.stop();
} catch (e) {
console.error('[Browser Page] Error calling recognition.stop():', e);
recognition = null;
updateStatus("停止时出错,已强制重置。");
}
} else {
console.log('[Browser Page] Recognition not active, nothing to stop.');
updateStatus("识别未运行。");
}
}
function forceResetRecognition() {
console.log('[Browser Page] Force resetting recognition...');
updateStatus("强制重置语音识别...");
// 先尝试停止当前的识别
if (recognition) {
try {
recognition.stop();
} catch (e) {
console.error('[Browser Page] Error stopping recognition during reset:', e);
}
}
// 强制设置为null丢弃所有后续结果
recognition = null;
// 通知服务器已重置
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'status', message: 'reset_complete' }));
}
updateStatus("语音识别已重置,等待新指令。");
}
</script>
</body>
</html>

View File

@@ -1,27 +0,0 @@
{
"name": "cherry-asr-server",
"version": "1.0.0",
"description": "Cherry Studio ASR Server",
"main": "server.js",
"bin": "server.js",
"scripts": {
"start": "node server.js",
"build": "pkg ."
},
"pkg": {
"targets": [
"node16-win-x64"
],
"outputPath": "dist",
"assets": [
"index.html"
]
},
"dependencies": {
"express": "^4.18.2",
"ws": "^8.13.0"
},
"devDependencies": {
"pkg": "^5.8.1"
}
}

View File

@@ -1,179 +0,0 @@
const http = require('http')
const WebSocket = require('ws')
const express = require('express')
const path = require('path') // Need path module
const app = express()
const port = 34515 // Define the port
// 获取index.html文件的路径
function getIndexHtmlPath() {
// 在开发环境中,直接使用相对路径
const devPath = path.join(__dirname, 'index.html')
// 在pkg打包后文件会被包含在可执行文件中
// 使用process.pkg检测是否是打包环境
if (process.pkg) {
// 在打包环境中,使用绝对路径
return path.join(path.dirname(process.execPath), 'index.html')
}
// 如果文件存在,返回开发路径
try {
if (require('fs').existsSync(devPath)) {
return devPath
}
} catch (e) {
console.error('Error checking file existence:', e)
}
// 如果都不存在,尝试使用当前目录
return path.join(process.cwd(), 'index.html')
}
// 提供网页给浏览器
app.get('/', (req, res) => {
const indexPath = getIndexHtmlPath()
console.log(`Serving index.html from: ${indexPath}`)
res.sendFile(indexPath)
})
const server = http.createServer(app)
const wss = new WebSocket.Server({ server })
let browserConnection = null
let electronConnection = null
wss.on('connection', (ws) => {
console.log('[Server] WebSocket client connected') // Add log
ws.on('message', (message) => {
let data
try {
// Ensure message is treated as string before parsing
data = JSON.parse(message.toString())
console.log('[Server] Received message:', data) // Log parsed data
} catch (e) {
console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e)
return // Ignore non-JSON messages
}
// 识别客户端类型
if (data.type === 'identify') {
if (data.role === 'browser') {
browserConnection = ws
console.log('[Server] Browser identified and connected')
// Notify Electron that the browser is ready
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
console.log('[Server] Sent browser_ready status to Electron')
}
// Notify Electron if it's already connected
if (electronConnection) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' }))
}
ws.on('close', () => {
console.log('[Server] Browser disconnected')
browserConnection = null
// Notify Electron
if (electronConnection) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' }))
}
})
ws.on('error', (error) => {
console.error('[Server] Browser WebSocket error:', error)
browserConnection = null // Assume disconnected on error
if (electronConnection) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
}
})
} else if (data.role === 'electron') {
electronConnection = ws
console.log('[Server] Electron identified and connected')
// If browser is already connected when Electron connects, notify Electron immediately
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
console.log('[Server] Sent initial browser_ready status to Electron')
}
ws.on('close', () => {
console.log('[Server] Electron disconnected')
electronConnection = null
// Maybe send stop to browser if electron disconnects?
// if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' }));
})
ws.on('error', (error) => {
console.error('[Server] Electron WebSocket error:', error)
electronConnection = null // Assume disconnected on error
})
}
}
// Electron 控制开始/停止
else if (data.type === 'start' && ws === electronConnection) {
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying START command to browser')
browserConnection.send(JSON.stringify({ type: 'start' }))
} else {
console.log('[Server] Cannot relay START: Browser not connected')
// Optionally notify Electron back
electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' }))
}
} else if (data.type === 'stop' && ws === electronConnection) {
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying STOP command to browser')
browserConnection.send(JSON.stringify({ type: 'stop' }))
} else {
console.log('[Server] Cannot relay STOP: Browser not connected')
}
} else if (data.type === 'reset' && ws === electronConnection) {
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying RESET command to browser')
browserConnection.send(JSON.stringify({ type: 'reset' }))
} else {
console.log('[Server] Cannot relay RESET: Browser not connected')
}
}
// 浏览器发送识别结果
else if (data.type === 'result' && ws === browserConnection) {
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
// console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed
electronConnection.send(JSON.stringify({ type: 'result', data: data.data }))
} else {
// console.log('[Server] Cannot relay RESULT: Electron not connected');
}
}
// 浏览器发送状态更新 (例如 'stopped')
else if (data.type === 'status' && ws === browserConnection) {
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed
electronConnection.send(JSON.stringify({ type: 'status', message: data.message }))
} else {
console.log('[Server] Cannot relay STATUS: Electron not connected')
}
} else {
console.log('[Server] Received unknown message type or from unknown source:', data)
}
})
ws.on('error', (error) => {
// Generic error handling for connection before identification
console.error('[Server] Initial WebSocket connection error:', error)
// Attempt to clean up based on which connection it might be (if identified)
if (ws === browserConnection) {
browserConnection = null
if (electronConnection)
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
} else if (ws === electronConnection) {
electronConnection = null
}
})
})
server.listen(port, () => {
console.log(`[Server] Server running at http://localhost:${port}`)
})
// Handle server errors
server.on('error', (error) => {
console.error(`[Server] Failed to start server:`, error)
process.exit(1) // Exit if server fails to start
})

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.45 66.73">
<defs>
<style>
.cls-1 {
fill: #ea5e5d;
}
.cls-2 {
fill: #23af69;
}
.cls-3 {
fill: #ea5756;
}
</style>
</defs>
<g id="_图层_1-2" data-name="图层_1">
<g>
<g>
<g>
<path class="cls-1" d="M16.72,51.21c-4.45,0-8.64-1.78-11.81-5.01-3.17-3.23-4.91-7.51-4.91-12.04s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.71,1.82,11.82,4.99c2.32,2.36,2.32,6.2,0,8.56-2.32,2.36-6.08,2.36-8.4,0-.9-.92-2.15-1.45-3.43-1.45-2.63,0-4.85,2.26-4.85,4.94s2.22,4.94,4.85,4.94c1.28,0,2.52-.53,3.43-1.45,2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-3.11,3.17-7.42,4.99-11.82,4.99Z"/>
<path class="cls-1" d="M32.05,66.73c-4.45,0-8.64-1.78-11.81-5.01s-4.91-7.51-4.91-12.04,1.79-8.88,4.9-12.06c2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-.9.92-1.42,2.19-1.42,3.49,0,2.68,2.22,4.94,4.85,4.94s4.85-2.26,4.85-4.94c0-.95-.23-2.31-1.32-3.43-3.13-3.19-4.92-7.6-4.92-12.09s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.64,1.78,11.81,5.01,4.91,7.51,4.91,12.04-1.79,8.88-4.9,12.06c-2.32,2.36-6.08,2.36-8.4,0-2.32-2.36-2.32-6.2,0-8.56.9-.92,1.42-2.19,1.42-3.49,0-2.68-2.22-4.94-4.85-4.94s-4.85,2.26-4.85,4.94c0,1.31.53,2.6,1.45,3.53,3.1,3.16,4.8,7.42,4.8,11.99s-1.74,8.81-4.91,12.04c-3.17,3.23-7.36,5.01-11.81,5.01Z"/>
</g>
<path class="cls-2" d="M32.05,19.09l-9.72-9.12c-1.5-1.4-1.57-3.75-.17-5.25,1.4-1.49,3.75-1.57,5.25-.17l3.89,3.65,5.53-6.83c1.29-1.59,3.63-1.84,5.22-.55,1.59,1.29,1.84,3.63.55,5.22l-10.56,13.05Z"/>
</g>
<g>
<path class="cls-3" d="M93.93,24.6l.55-.39c.69-.4,1.17-.61,1.46-.61.63,0,1.3.57,2.03,1.7.44.71.67,1.27.67,1.7s-.14.78-.41,1.06c-.27.28-.59.54-.96.76-.36.22-.71.43-1.05.64-.33.2-1.02.47-2.05.79-1.03.32-2.03.49-2.99.49s-1.93-.13-2.91-.38c-.98-.25-1.99-.68-3.03-1.27-1.04-.6-1.98-1.32-2.81-2.18-.83-.86-1.51-1.96-2.05-3.31-.54-1.35-.8-2.81-.8-4.38s.26-3.01.79-4.29c.53-1.28,1.2-2.35,2.02-3.19.82-.84,1.75-1.54,2.81-2.11,1.98-1.09,3.97-1.64,5.98-1.64.95,0,1.92.15,2.9.44.98.29,1.72.59,2.23.9l.73.42c.36.22.65.4.85.55.53.42.79.91.79,1.44s-.21,1.1-.64,1.68c-.79,1.09-1.5,1.64-2.12,1.64-.36,0-.88-.22-1.55-.67-.85-.69-1.98-1.03-3.4-1.03-1.31,0-2.61.46-3.88,1.36-.61.44-1.11,1.07-1.52,1.88-.4.81-.61,1.72-.61,2.75s.2,1.94.61,2.75c.4.81.92,1.45,1.55,1.91,1.23.89,2.52,1.34,3.85,1.34.63,0,1.22-.08,1.77-.24.56-.16.96-.32,1.2-.49Z"/>
<path class="cls-3" d="M114.38,9.07c.16-.3.43-.52.82-.64.38-.12.87-.18,1.46-.18s1.05.05,1.4.15c.34.1.61.22.79.36.18.14.32.34.42.61.1.34.15.87.15,1.58v16.84c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58v-6.16h-8.04v6.19c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V10.92c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v6.19h8.04v-6.22c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8Z"/>
<path class="cls-3" d="M127.21,25.1h9.34c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-12.01c-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55V10.9c0-1.03.19-1.73.58-2.11.38-.37,1.11-.56,2.18-.56h11.95c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-9.31v3.06h6.01c.46,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.38,2.25-1.15,2.49-.34.12-.87.18-1.58.18h-5.95v3.06Z"/>
<path class="cls-3" d="M196.96,8.79c.99.69,1.49,1.35,1.49,2,0,.38-.23.92-.7,1.61l-6.55,9.8v5.79c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.16.3-.43.52-.82.64-.38.12-.9.18-1.55.18s-1.16-.06-1.55-.18c-.38-.12-.66-.34-.82-.65-.16-.31-.26-.59-.29-.82-.03-.23-.05-.59-.05-1.08v-5.73l-6.55-9.8c-.47-.69-.7-1.22-.7-1.61,0-.65.44-1.27,1.33-1.87.89-.6,1.53-.9,1.91-.9s.69.08.91.24c.34.22.71.64,1.09,1.24l4.7,7.52,4.7-7.52c.38-.61.72-1.01,1-1.2s.61-.29.99-.29.97.25,1.77.76Z"/>
<g>
<path class="cls-3" d="M81.93,56.63c-.53-.65-.79-1.23-.79-1.74s.43-1.2,1.3-2.05c.51-.49,1.04-.73,1.61-.73s1.36.51,2.37,1.52c.28.34.69.67,1.21.99.53.31,1.01.47,1.46.47,1.88,0,2.82-.77,2.82-2.31,0-.46-.26-.85-.77-1.17-.52-.31-1.16-.54-1.93-.68-.77-.14-1.6-.37-2.49-.68-.89-.31-1.72-.68-2.49-1.11-.77-.42-1.41-1.1-1.93-2.02-.52-.92-.77-2.03-.77-3.32,0-1.78.66-3.33,1.99-4.66s3.13-1.99,5.42-1.99c1.21,0,2.32.16,3.32.47,1,.31,1.69.63,2.08.96l.76.58c.63.59.94,1.08.94,1.49s-.24.96-.73,1.67c-.69,1.01-1.4,1.52-2.12,1.52-.42,0-.95-.2-1.58-.61-.06-.04-.18-.14-.35-.3-.17-.16-.33-.29-.47-.39-.42-.26-.97-.39-1.62-.39s-1.2.16-1.64.47c-.43.31-.65.75-.65,1.3s.26,1.01.77,1.35c.52.34,1.16.58,1.93.7.77.12,1.61.31,2.52.56.91.25,1.75.56,2.52.93.77.36,1.41,1,1.93,1.9.52.9.77,2.01.77,3.32s-.26,2.47-.79,3.47c-.53,1-1.21,1.77-2.06,2.32-1.64,1.07-3.39,1.61-5.25,1.61-.95,0-1.85-.12-2.7-.35-.85-.23-1.54-.52-2.06-.86-1.07-.65-1.82-1.27-2.24-1.88l-.27-.33Z"/>
<path class="cls-3" d="M100.74,37.49h16.87c.65,0,1.12.08,1.43.23.3.15.51.39.61.71.1.32.15.75.15,1.27s-.05.95-.15,1.26c-.1.31-.27.53-.52.65-.36.18-.88.27-1.55.27h-5.79v15.26c0,.47-.02.81-.05,1.03s-.12.48-.27.77c-.15.29-.42.5-.8.62-.38.12-.89.18-1.52.18s-1.13-.06-1.5-.18c-.37-.12-.64-.33-.79-.62-.15-.29-.24-.56-.27-.79-.03-.23-.05-.58-.05-1.05v-15.23h-5.82c-.65,0-1.12-.08-1.43-.23-.3-.15-.51-.39-.61-.71-.1-.32-.15-.75-.15-1.27s.05-.95.15-1.26c.1-.31.27-.53.52-.65.36-.18.88-.27,1.55-.27Z"/>
<path class="cls-3" d="M135.99,38.34c.2-.32.5-.55.88-.67.38-.12.86-.18,1.44-.18s1.04.05,1.38.15c.34.1.61.22.79.36.18.14.31.35.39.64.12.34.18.87.18,1.58v9.16c0,2.67-.83,5.1-2.49,7.28-.81,1.03-1.85,1.87-3.12,2.5s-2.68.96-4.23.96-2.95-.32-4.22-.97c-1.26-.65-2.29-1.5-3.08-2.55-1.64-2.14-2.46-4.57-2.46-7.28v-9.13c0-.49.02-.84.05-1.08.03-.23.13-.5.29-.8.16-.3.43-.52.82-.64.38-.12.9-.18,1.55-.18s1.16.06,1.55.18c.38.12.65.33.79.64.24.47.36,1.1.36,1.91v9.1c0,1.23.3,2.41.91,3.52.3.57.76,1.02,1.37,1.36.61.34,1.32.52,2.15.52,1.48,0,2.58-.55,3.31-1.64.73-1.09,1.09-2.36,1.09-3.79v-9.28c0-.79.1-1.34.3-1.67Z"/>
<path class="cls-3" d="M146.18,37.49l5.61.03c2.93,0,5.51,1.06,7.74,3.17,2.22,2.11,3.34,4.71,3.34,7.8s-1.09,5.73-3.26,7.93c-2.17,2.2-4.81,3.31-7.9,3.31h-5.55c-1.23,0-2-.25-2.31-.76-.24-.42-.36-1.07-.36-1.94v-16.87c0-.49.02-.84.05-1.06s.13-.49.29-.79c.28-.55,1.07-.82,2.37-.82ZM151.79,54.35c1.46,0,2.77-.54,3.94-1.62,1.17-1.08,1.76-2.44,1.76-4.08s-.57-3.01-1.71-4.11c-1.14-1.1-2.48-1.65-4.02-1.65h-2.91v11.47h2.94Z"/>
<path class="cls-3" d="M164.84,40.19c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v16.87c0,.49-.02.84-.05,1.06s-.13.49-.29.79c-.28.55-1.07.82-2.37.82-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55v-16.87Z"/>
<path class="cls-3" d="M183.07,37.24c2.99,0,5.59,1.08,7.8,3.25,2.2,2.16,3.31,4.85,3.31,8.05s-1.05,5.94-3.16,8.19c-2.1,2.26-4.69,3.38-7.77,3.38s-5.69-1.11-7.84-3.34c-2.15-2.22-3.23-4.87-3.23-7.95,0-1.68.3-3.25.91-4.72.61-1.47,1.42-2.7,2.43-3.69,1.01-.99,2.17-1.77,3.49-2.34,1.31-.57,2.67-.85,4.07-.85ZM177.55,48.68c0,1.8.58,3.26,1.74,4.38,1.16,1.12,2.46,1.68,3.9,1.68s2.73-.55,3.88-1.64c1.15-1.09,1.73-2.56,1.73-4.4s-.58-3.32-1.74-4.43c-1.16-1.11-2.46-1.67-3.9-1.67s-2.73.56-3.88,1.68c-1.15,1.12-1.73,2.58-1.73,4.38Z"/>
</g>
<g>
<path class="cls-3" d="M176.92,11.06c-.03-.23-.13-.5-.29-.8-.28-.55-1.07-.82-2.37-.82h-6.55c-1.78,0-3.51.65-5.19,1.94-.81.63-1.48,1.48-2,2.55-.53,1.07-.79,2.27-.79,3.58,0,2.29.76,4.17,2.28,5.64-.44,1.07-1.13,2.66-2.06,4.76-.3.73-.45,1.25-.45,1.58,0,.77.63,1.42,1.88,1.94.65.28,1.17.43,1.56.43s.72-.1.97-.29c.25-.19.44-.39.56-.59.2-.38.99-2.21,2.37-5.49l.94.06h3.82v3.43c0,.47.02.81.05,1.05.03.23.13.5.29.8.28.55,1.07.82,2.37.82,1.42,0,2.25-.37,2.49-1.12.12-.34.18-.87.18-1.58V12.11c0-.46-.02-.81-.05-1.05ZM172.81,19.44c-.09.14-.48.77-1.24.91-.2.04-.37.03-.48.02-.02.14-.04.26-.06.38-.16.83-.38,1.05-.57,1.07-.29.05-.51-.35-.93-.9-.23.01-.46.02-.69.02-.51,0-1.01-.03-1.49-.09-.25-.03-.5-.07-.74-.11-1.18-.32-2.03-1.27-2.03-2.4v-1.37c0-1.13.86-2.08,2.03-2.4.24-.04.49-.08.74-.11.48-.06.98-.09,1.49-.09s1.01.03,1.49.09c.25.03.5.07.74.11.6.16,1.12.49,1.49.93.34.41.55.92.55,1.47v1.37c0,.23-.01.66-.29,1.1Z"/>
<circle class="cls-2" cx="167.24" cy="17.67" r=".49"/>
<circle class="cls-2" cx="168.88" cy="17.71" r=".49"/>
<circle class="cls-2" cx="170.59" cy="17.71" r=".49"/>
</g>
<g>
<path class="cls-3" d="M141.01,8.24c.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82h6.55c1.78,0,3.51.65,5.19,1.94.81.63,1.48,1.48,2,2.55.53,1.07.79,2.27.79,3.58,0,2.29-.76,4.17-2.28,5.64.44,1.07,1.13,2.66,2.06,4.76.3.73.45,1.25.45,1.58,0,.77-.63,1.42-1.88,1.94-.65.28-1.17.43-1.56.43s-.72-.1-.97-.29c-.25-.19-.44-.39-.56-.59-.2-.38-.99-2.21-2.37-5.49l-.94.06h-3.82v3.43c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V9.28c0-.46.02-.81.05-1.05ZM145.12,16.62c.09.14.48.77,1.24.91.2.04.37.03.48.02.02.14.04.26.06.38.16.83.38,1.05.57,1.07.29.05.51-.35.93-.9.23.01.46.02.69.02.51,0,1.01-.03,1.49-.09.25-.03.5-.07.74-.11,1.18-.32,2.03-1.27,2.03-2.4v-1.37c0-1.13-.86-2.08-2.03-2.4-.24-.04-.49-.08-.74-.11-.48-.06-.98-.09-1.49-.09s-1.01.03-1.49.09c-.25.03-.5.07-.74.11-.6.16-1.12.49-1.49.93-.34.41-.55.92-.55,1.47v1.37c0,.23.01.66.29,1.1Z"/>
<circle class="cls-2" cx="150.69" cy="14.84" r=".49"/>
<circle class="cls-2" cx="149.05" cy="14.89" r=".49"/>
<circle class="cls-2" cx="147.35" cy="14.89" r=".49"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744456106953" class="icon" viewBox="0 0 2633 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1486" xmlns:xlink="http://www.w3.org/1999/xlink" width="514.2578125" height="200"><path d="M0 0v877.843607h731.440328v146.156393H1316.724263v-146.156393h1316.724262V0z m731.440328 731.196014h-146.156393V292.312786h-146.485574v438.883228H146.485574V146.238688h584.954754z m438.880656 0v146.567869h-292.405369V146.238688H1463.209837v585.037049H1170.320984z m1316.888853 0H2341.30033V292.312786h-146.56787v438.883228h-146.485574V292.312786h-145.909508v438.883228H1609.283935V146.238688h878.008197zM1170.238688 292.477377H1316.724263v292.644539h-146.485575z" fill="#CB3837" p-id="1487"></path></svg>

Before

Width:  |  Height:  |  Size: 845 B

View File

@@ -0,0 +1,8 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="ic_ImageUp">
<path id="Vector" d="M10.8 21.5H5.5C4.96957 21.5 4.46086 21.2893 4.08579 20.9142C3.71071 20.5391 3.5 20.0304 3.5 19.5V5.5C3.5 4.96957 3.71071 4.46086 4.08579 4.08579C4.46086 3.71071 4.96957 3.5 5.5 3.5H19.5C20.0304 3.5 20.5391 3.71071 20.9142 4.08579C21.2893 4.46086 21.5 4.96957 21.5 5.5V15.5L18.4 12.4C18.0237 12.0312 17.517 11.8258 16.9901 11.8284C16.4632 11.831 15.9586 12.0415 15.586 12.414L6.5 21.5" stroke="#353941" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M14.5 20L17.5 17L20.5 20" stroke="#353941" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M17.5 22.5V17" stroke="#353941" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_4" d="M9.5 11.5C10.6046 11.5 11.5 10.6046 11.5 9.5C11.5 8.39543 10.6046 7.5 9.5 7.5C8.39543 7.5 7.5 8.39543 7.5 9.5C7.5 10.6046 8.39543 11.5 9.5 11.5Z" stroke="#353941" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

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