Compare commits

..

424 Commits

Author SHA1 Message Date
1600822305
41ef576d0a 6 2025-04-24 04:24:42 +08:00
1600822305
4d605005a7 升级了下筛选 2025-04-24 01:41:10 +08:00
1600822305
d2019a32aa Merge branch 'deepsearch-2' of https://github.com/CherryHQ/cherry-studio into deepsearch-2 2025-04-24 01:35:57 +08:00
1600822305
05d110e4af 升级了下筛选 2025-04-24 01:34:35 +08:00
1600822305
e2a140a99a Delete index.html 2025-04-24 01:12:31 +08:00
1600822305
d33e16fa81 deepsearch 2025-04-24 01:04:27 +08:00
1600822305
1b2d15f2e8 Add files via upload 2025-04-24 00:49:47 +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
kangfenmao
5bfa13112a 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.
2025-04-19 19:22:36 +08:00
kangfenmao
7f7db748a7 chore(version): 1.2.5 2025-04-19 18:58:53 +08:00
kangfenmao
55aac1cb9b lint: fix code format 2025-04-19 18:58:26 +08:00
kangfenmao
4663794ba6 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.
2025-04-19 18:54:17 +08:00
kangfenmao
e4de5331e0 Revert "feat: add chat message translate copy button (#4620)"
This reverts commit 8b462935b4.
2025-04-19 18:09:44 +08:00
kangfenmao
c7ed15684a feat(Messages): enhance citations display with improved styling and translation support
- Added a title for the citations list with translation using `useTranslation`.
- Introduced an `Info` icon next to the citations title.
- Updated the `CitationsContainer` styling for better visual appeal.
- Refactored citation rendering logic in `MessageContent` to streamline citation handling.
2025-04-19 17:44:44 +08:00
kangfenmao
1bb27ee3f9 feat: update Z.ai app configuration with additional styling and increment store version to 97 2025-04-19 17:28:23 +08:00
ousugo
579d7d1e5d refactor(ModelList): extract NameSpan component and adjust styling for better layout 2025-04-19 13:58:21 +08:00
one
36aa13c4f1 refactor: merge rehype plugins 2025-04-19 13:55:33 +08:00
one
c5161b9da4 refactor: use rehype-sanitize for html tags 2025-04-19 13:55:33 +08:00
karl
32c96daf1f feat: mcp configuration extraction logic optimization (#4918)
Co-authored-by: 寇佳龙 <koujialong@bonc.com.cn>
2025-04-19 10:45:22 +08:00
寇佳龙
30309c29ff fix: mcp search field redundancy 2025-04-19 10:39:44 +08:00
自由的世界人
8b462935b4 feat: add chat message translate copy button (#4620)
* feat: add chat message translate copy button

* Update MessageMenubar.tsx

* fix: copy button display
2025-04-19 10:36:57 +08:00
Simple4
d907344ca7 fix(obsidian): update title update logic 2025-04-19 10:24:45 +08:00
缘生
9b21c334cc feat: implement maximum backups feature in WebDAV settings (#5060)
* feat: implement maximum backups feature in WebDAV settings

- Add maximum backups feature to WebDAV settings

Signed-off-by: ysicing <i@ysicing.me>

* refactor: refactor backup file management for device-specific handling

- Change the order of hostname and device type in the backup file name
- Add filtering for backup files to manage device-specific backups
- Update logic to delete the oldest backup files based on the specific device count

Signed-off-by: ysicing <i@ysicing.me>

---------

Signed-off-by: ysicing <i@ysicing.me>
2025-04-19 10:22:03 +08:00
Asurada
3e1e814004 fix (UI): Resolve the issue of overly long file names and path names not being displayed correctly (#5054)
* feat(Messages): enhance file upload display with styled component for better UI

* feat(Inputbar): add file name truncation for better display in attachment preview

* feat(DataSettings): replace Typography.Text with PathText for improved path display and add text truncation styling

* feat(DataSettings): refactor path display with PathRow component for better layout and styling
2025-04-19 02:00:11 +08:00
Hantong Chen
1c5adc1329 feat(websearch): HTTP basic auth support for Searxng (#5009)
* feat(websearch): HTTP basic auth support for Searxng

* fix(websearch): schema migration

* refactor(i18n): update translations for basic authentication

* fix(websearch): 修正 `HTTP 认证` 相关翻译

* feat(websearch): 为 `HTTP 认证` 添加注释

---------

Co-authored-by: suyao <sy20010504@gmail.com>
2025-04-19 01:58:03 +08:00
SuYao
3360905275 feat: gemini reasoning budget support (#5052)
* feat(models): add Gemini 2.5 reasoning model identification and integrate reasoning effort logic in GeminiProvider

* feat(AiProvider): enhance usage tracking by adding thoughts_tokens and updating usage types
2025-04-19 01:27:20 +08:00
chenxi
0a28df132d fix(deepseek-reasoner) doesn't support successive user or assistant messages (#5051)
fix(deepseek-reasoner) does not support successive user or assistant messages
2025-04-19 01:21:27 +08:00
fullex
fd3d9f17b8 fix: should not download when autoupdate is false (#5029) 2025-04-18 16:28:20 +08:00
ousugo
73f8148a94 feat(models): update gemini model identification logic to be more general 2025-04-18 09:58:44 +08:00
fullex
456f0657a6 fix: Nutstore auto-sync when app starts (#5005)
feat: integrate Nutstore auto-sync functionality in initialization process
2025-04-17 21:57:24 +08:00
缘生
53f74725ed feat: add hostname retrieval for backup webdav (#5004)
fix #4705

Signed-off-by: ysicing <i@ysicing.me>
2025-04-17 21:21:39 +08:00
SuYao
4fa04a801a feat(UI): Support custcom css in mini window (#4255)
* feat(UI): enable custom CSS functionality with miniWindow

* feat(UI): implement custom CSS handling in IPC and update related components
2025-04-17 20:54:34 +08:00
Chen Tao
c5580f5b71 fix: knowledge citations (#4988) 2025-04-17 15:43:30 +08:00
one
f8f808c9f4 refactor: improve error boundary messsage (#4987) 2025-04-17 15:39:40 +08:00
ousugo
dbbd539207 fix(AddAgentPopup): update form handling and simplify prompt input layout 2025-04-17 15:30:42 +08:00
Asurada
703eae5777 fix(models): simplify OpenAI o-series model identification logic (#4985)
* fix(models): simplify OpenAI o-series model identification logic

* Update OpenAIProvider.ts

---------

Co-authored-by: Pleasurecruise <3196812536@qq.com>
2025-04-17 15:19:14 +08:00
Chen Tao
9438c8e6ff feat: LLM可以根据需求自行选择使用知识库或者网络搜索 (#4806) 2025-04-17 13:11:43 +08:00
beyondkmp
75f986087a chore(electron-builder): Simplify file renaming logic and remove space (#4919)
* chore(electron-builder): Disable universal installer option in NSIS configuration

* refactor(after-build): Change file handling to delete files with spaces and rename files in YAML data

- Updated the function to delete files containing spaces instead of renaming them.
- Enhanced YAML processing to rename files and their blockmaps, ensuring proper handling of setup and portable versions.
- Adjusted the final YAML output to reflect the new file names.

* refactor(after-build): Simplify file renaming logic and remove space handling script

- Updated the after-build script to rename artifact files by replacing spaces with hyphens.
- Removed the replace-spaces.js script as its functionality is now integrated into the after-build process.
- Adjusted the build process in package.json to reflect the changes in file handling.

* refactor(electron-builder): Update artifact build script reference and remove obsolete after-build script

- Changed the artifactBuildCompleted script reference in electron-builder.yml to point to the new script.
- Deleted the outdated after-build.js script, which is no longer needed for file handling.

* delete js-yml
2025-04-17 10:05:48 +08:00
Asurada
7ac8f480bb feat(models): add support for o3 and o4-mini models in vision and logo configurations (#4963) 2025-04-17 02:13:53 +08:00
SuYao
676ac21804 fix(GeminiProvider): update content configuration based on model type (#4960)
* fix(GeminiProvider): update content configuration based on model type

* chore(ApiService): comment out debug log for message output
2025-04-17 00:00:35 +08:00
SuYao
24ddd69cd5 refactor(Gemini): migrate generative-ai sdk to genai sdk (#4939)
* refactor(GeminiService): migrate to new Google GenAI SDK and update file handling methods

- Updated import statements to use the new Google GenAI SDK.
- Refactored file upload, retrieval, and deletion methods to align with the new SDK's API.
- Adjusted type definitions and response handling for improved type safety and clarity.
- Enhanced file listing and processing logic to utilize async iteration for better performance.

* refactor(GeminiProvider): update message handling and integrate abort signal support

- Refactored message content handling to align with updated type definitions, ensuring consistent use of Content type.
- Enhanced abort signal management for chat requests, allowing for better control over ongoing operations.
- Improved message processing logic to streamline user message history handling and response generation.
- Adjusted type definitions for message contents to enhance type safety and clarity.

* refactor(electron.vite.config): replace direct import of Vite React plugin with dynamic import

* fix(Gemini): clean up unused methods and improve property access

* fix(typecheck): update color properties to use CSS variables

* feat: 修改画图逻辑

* fix: import viteReact

---------

Co-authored-by: eeee0717 <chentao020717Work@outlook.com>
2025-04-16 23:13:22 +08:00
LiuVaayne
35c50b54a8 Feat/mcp oauth (#4837)
* feat: implement OAuth client provider and lockfile management

* feat: implement OAuth callback server and refactor authentication flow

* fix(McpService): restrict command handling to 'npx' for improved clarity

* refactor: make callbackPort optional in OAuthProviderOptions and clean up MCPService

* refactor: restructure OAuth handling by creating separate callback and provider classes, and remove unused utility functions
2025-04-16 22:07:32 +08:00
beyondkmp
ac0fe75078 chore: Update electron-builder configuration to remove architecture specifications for targets and adjust build scripts for Windows, macOS, and Linux to include both x64 and arm64 architectures. 2025-04-16 10:49:27 +08:00
nutstore-dev
f0c25f8108 refractor: nutstore sdk changed to JS version (#4913)
Co-authored-by: shlroland <shlroland1995@gmail.com>
2025-04-16 10:47:08 +08:00
W
c10e5a9ca4 fix: 快捷助手发起询问后没有清理掉输入框内的内容 (#4907) 2025-04-16 10:42:34 +08:00
Asurada
444abc9b88 fix(OpenAIProvider): Filter empty system prompts (#4896) 2025-04-16 02:15:06 +08:00
Cle2ment
2d130a8526 fix(ReadMe): the redirection error of the Maple Neon theme (#4893)
* 修复Maple Neon Theme的链接重定向错误.

* 修复中文和日语主题的排版错误,改为和英文README统一的列表.
2025-04-16 00:03:11 +08:00
liqihao
5061ee5c4d Fix(MCPService): 修复 getSystemPath 因硬编码 Shell 路径导致的兼容性问题 (#4853)
Fix(MCPService): Prioritize process.env.SHELL for PATH retrieval

Co-authored-by: liqihao <liqiha0@outlook.com>
2025-04-15 23:23:28 +08:00
心如止水自在如风
9e913f531c fix: MCP服务器添加server-filesystem时填写参数后启用时报错 (#4872)
fix: MCP服务器添加server-filesystem时填写地址后启用时报错

Co-authored-by: annan01 <annan01@qianxin.com>
2025-04-15 23:19:22 +08:00
Cle2ment
98130de8ac chore(README): 为各语言的README都添加了Maple-Neon主题. (#4891) 2025-04-15 23:14:14 +08:00
SuYao
b339b7b6d4 fix(OpenAIProvider): remove unnecessary o-series model stream restriction (#4889)
* fix(OpenAIProvider): remove unnecessary o-series model stream restriction

* lint(OpenAIProvider): remove redundant code
2025-04-15 22:53:34 +08:00
Hao He
eb8ee5ec02 feat(MessageContent): Add Collapsible Citations Display (#4285)
* feat(MessageContent): 添加引用内容折叠功能,优化用户界面交互

* feat(Citations): add hideTitle prop to control title visibility in CitationsList

* feat(Messages): add message update functionality and manage UI state for citations and web search

* fix: web search citation

---------

Co-authored-by: 自由的世界人 <3196812536@qq.com>
2025-04-15 22:47:20 +08:00
牡丹凤凰
a34e10cb0d Update README.ja.md 2025-04-15 19:45:40 +08:00
牡丹凤凰
fcae5b097b Update README.zh.md 2025-04-15 19:44:23 +08:00
牡丹凤凰
3cb339e480 Update README.md 2025-04-15 19:42:16 +08:00
SuYao
35224f5213 feat(model): add ModelCard schema and related types for input/output capabilities (#4812)
* feat(model): add ModelCard schema and related types for input/output capabilities

* refactor(model): make limits and price properties optional in ModelSchema

* feat(model): add textGeneration capability to ModelSchema
2025-04-15 19:40:13 +08:00
beyondkmp
b8b37fcd11 feat: integrate AxiosProxy for HTTP requests in rerankers and Copilot… (#4858)
* feat: integrate AoxisProxy for HTTP requests in rerankers and CopilotService

- Replaced direct axios calls with aoxisProxy in JinaReranker, SiliconFlowReranker, and VoyageReranker to utilize proxy settings.
- Introduced AoxisProxy service to manage axios instances with proxy configurations.
- Updated CopilotService to use aoxisProxy for API requests, ensuring consistent proxy handling across services.

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

* fix tyop

* fix tyop
2025-04-15 18:42:31 +08:00
ousugo
615fda0547 fix(styles): add support for lucide icons in global styles 2025-04-15 17:44:50 +08:00
karl
0585d28312 feat/search mcp auto config (#4780)
feat: mcp search carry basic configuration

Co-authored-by: 寇佳龙 <koujialong@bonc.com.cn>
2025-04-15 16:05:17 +08:00
Teo
9ad40b9219 feat(QuickPanel): enhance pinyin filtering and improve input handling in QuickPanel 2025-04-15 15:58:44 +08:00
Asurada
18e99dee67 chore(issue-template): improve clarity and requirements in bug report checklist (#4847) 2025-04-15 13:29:16 +08:00
Asurada
43ef1d6815 feat(models): add gpt-4.1 model to visionAllowedModels (#4843) 2025-04-15 12:14:51 +08:00
Asurada
141904e61a feat(models): update GLM model list and add new GLM-Z1 reasoning models (#4836) 2025-04-15 11:07:17 +08:00
Chen Tao
247501c26c fix: websearch ui (#4840)
fix(ui)
2025-04-15 11:06:28 +08:00
自由的世界人
748252febc Update issue-management.yml (#4830)
* Update issue-management.yml

* Update issue-management.yml
2025-04-15 09:59:37 +08:00
自由的世界人
8ac4d07d6b feat: create issue-management.yml (#4822) 2025-04-15 04:03:01 +08:00
Asurada
5fbff8c1fe feat(Grok): add isGrokModel function and update systemMessage handling for Grok models (#4823) 2025-04-15 03:15:07 +08:00
Asurada
f0f44d5768 feat(Miniapp): add Z.ai mini app with logo and migration support (#4820) 2025-04-15 01:43:56 +08:00
beyondkmp
8c20bd6d8f update electron-builder to 26.0.13 2025-04-14 21:52:58 +08:00
kangfenmao
d1c2bbed1b refactor(ipc): remove Windows ARM update check from IPC handler and AboutSettings component 2025-04-14 20:04:48 +08:00
kangfenmao
f372ebe485 ci(after-build): move renameFilesWithSpaces call to the end of the process 2025-04-14 18:42:18 +08:00
kangfenmao
833873b95e chore(version): 1.2.4 2025-04-14 17:30:41 +08:00
LiuVaayne
1d211ee9f7 feat: mcp custom headers (#4800)
* Add support for custom HTTP headers in MCP servers

Allow users to configure custom HTTP headers for SSE and streamable HTTP
MCP server connections. This enables authentication and other API
requirements.

* Add custom headers i18n strings
2025-04-14 17:17:13 +08:00
LiuVaayne
c7ef2c5791 Feat/mcp tool response support image (#4787)
* Add typed MCPCallToolResponse interface and format converters

The commit introduces a structured response type for MCP tool calls with
proper handling for different content types (text, image, audio). It adds
provider-specific converter functions to transform tool responses into
the appropriate message format for OpenAI, Anthropic, and Gemini.

* Support vision models in tool call responses

Add isVisionModel parameter to tool response formatters to conditionally
handle content based on model capabilities. Non-vision models now receive
JSON stringified content, while vision models get proper multimedia parts.
2025-04-14 17:16:42 +08:00
kangfenmao
6e66721688 feat: add after-build script for renaming files and updating latest.yml
- Introduced a new script to rename files with spaces in the 'dist' directory.
- Updated 'latest.yml' to remove the first file entry and adjust paths accordingly.
- Enhanced build process for Windows to include the new script execution.
- Added js-yaml dependency for YAML file manipulation.
2025-04-14 17:14:45 +08:00
kangfenmao
ffc8a33ccf fix: emoji icon empty 2025-04-14 17:10:55 +08:00
Teo
81538a5446 feat: update icons in Inputbar and related components for consistency 2025-04-14 16:06:17 +08:00
fullex
e6b325dd88 fix: quickAssistant transalte not work by key select 2025-04-14 10:31:52 +08:00
kangfenmao
0e8c053cee feat: enhance styling and icon consistency across components 2025-04-14 09:34:13 +08:00
kangfenmao
24e46efa0c feat(migrate, websearch): enable enhanceMode in websearch and update migration logic 2025-04-14 09:34:13 +08:00
fullex
64200b00a9 fix: mac fullscreen changed when switch back through clicking dock icon 2025-04-13 23:34:55 +08:00
fullex
ab4fb7d1d6 fix: numpad enter not work 2025-04-13 23:34:20 +08:00
Reamd7
352731827c fix(MCPService): 增加获取系统 PATH 的功能, 修复 process.env.PATH 无法获取系统PATH的问题 (#4766) 2025-04-13 22:45:02 +08:00
LiuVaayne
e13a43d82a feat: Enhance web search with XML-based query extraction (#4770)
Add support for webpage summarization, direct URL references, and
better query processing using a structured XML format. Move web content
fetching to dedicated utility functions with improved error handling
and format options.
2025-04-13 22:42:14 +08:00
kangfenmao
e51de5b492 fix(Sidebar): rename Sparkle icon to Sparkles for consistency 2025-04-13 22:05:32 +08:00
africa1207
a54360cc69 feat: 优化webdav备份文件恢复管理功能 (#4699)
* feat: 优化webdav备份文件恢复管理功能

* fix: 恢复和删除操作更改为文字而非图标

* feat: 统一坚果云与webdav备份恢复功能
2025-04-13 21:52:16 +08:00
kangfenmao
88cbb27557 style(Sidebar, Messages, ModelSettings): update icon styles and clean up unused imports
- Added 'icon' class to various icons in Sidebar for consistent styling.
- Removed unused loading state from Messages component.
- Cleaned up iconStyle variable in ModelSettings as it was no longer needed.
2025-04-13 21:29:48 +08:00
LiuVaayne
e22d076d67 feat(MCP): add resource management features and localization support (#4746)
* feat(MCP): add resource management features and localization support

* feat(MCP): enhance resource handling with improved error messages and response structure

* fix(MCPToolsButton): add missing useEffect import for resource handling

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-04-13 21:08:57 +08:00
Teo
de7f806bbc hotfix: 优化一些issue反馈 (#4758)
feat(Inputbar, Settings): add backspace delete model functionality and localization updates

- Implemented a new setting to enable backspace key functionality for deleting models/attachments in the Inputbar.
- Added corresponding localization strings for English, Japanese, Russian, Chinese (Simplified and Traditional) in the i18n files.
- Updated the QuickPanelBody styling to inherit border-radius.
- Migrated the new setting to the state management for persistence.

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-04-13 21:07:00 +08:00
kangfenmao
5f7d8652bc feat: new icon style 2025-04-13 21:03:19 +08:00
bjl101501
39c614e4db docs(README): Add 仿Claude样式主题 (#4751)
* Update README.md

* Add 仿Claude样式主题 in README.zh.md

* Add 仿Claude样式主题 in README.ja.md
2025-04-13 20:40:13 +08:00
George·Dong
412e8b03fc fix: Update dashscoop provider configuration and enhance model editing functionality (#4748)
* fix(provider config): update dashscoop new links

* feat(EditModelsPopup): add grouping function for bailian

* fix(isWebSearchModel): Correctly handle the priority of manually setting model support for web search
2025-04-13 20:38:25 +08:00
kangfenmao
486ccc1a15 fix(EditModelsPopup, ModelList): adjust avatar size and streamline model description rendering
- Reduced avatar size in ModelList for better alignment.
- Simplified rendering logic for model descriptions in EditModelsPopup to enhance readability.
2025-04-13 13:34:48 +08:00
kangfenmao
c6ab7b9326 style(MCPSettings): adjust layout and spacing in NpxSearch and MainContainer
- Updated MainContainer to use flex display for better layout.
- Increased margin in NpxSearch component for improved spacing.
- Adjusted ResultList to use two columns instead of three for better content presentation.
2025-04-13 11:00:07 +08:00
kangfenmao
41981acd77 feat(Settings): implement assistant icon type selection and localization updates 2025-04-13 10:45:47 +08:00
Teo
97ef7016d3 feat(AssistantItem): add emoji support and improve icon display logic 2025-04-13 10:01:03 +08:00
Teo
e4514bd04c refactor(AgentPage): Refactor AgentPage UI (#4737)
* refactor(AgentPage): Refactor AgentPage UI

* style(AgentCard): update HeaderInfoEmoji styling for improved layout and visual consistency

* fix(AgentCard): conditionally render HeaderInfoEmoji to prevent rendering of undefined

* feat(AgentsPage): add handleAddAgent function to streamline agent addition process

* style(AgentsPage): remove unnecessary whitespace in title rendering
2025-04-13 09:58:46 +08:00
王叔叔
f39bb9869b Update LICENSE (#4744) 2025-04-13 08:00:41 +08:00
purefkh
fc884d72af fix(UI): Correct citation tooltip style in light theme (#4738) 2025-04-12 21:48:14 +08:00
kangfenmao
883bdd6283 chore(version): 1.2.3 2025-04-12 20:47:08 +08:00
kangfenmao
fa19f41385 feat(Ipc): add architecture information and update check logic for Windows arm64 2025-04-12 20:42:20 +08:00
kangfenmao
8b95a131ec fix(SettingsTab): refine reasoning effort handling for Grok models
fix: #4735
2025-04-12 20:23:31 +08:00
kangfenmao
72e18fbcc1 feat(MCPSettings): enhance MCP server management and localization updates
- Added a new SVG icon for npm in the MCP settings.
- Introduced a custom hook `useMCPServer` for retrieving a specific MCP server by ID.
- Updated localization files to include new error messages for tool and prompt loading in English, Japanese, Russian, and Chinese.
- Refactored MCP settings components for improved navigation and state management, including the use of React Router for routing.
- Enhanced the Npx search functionality and UI for better user experience.
2025-04-12 19:47:36 +08:00
kangfenmao
b62c59eb52 style(SelectModelPopup): update background color animation for improved visual consistency 2025-04-12 17:02:17 +08:00
kangfenmao
ffe7702c1c style(QuickPanel): update font sizes and line height for improved readability 2025-04-12 16:41:39 +08:00
kangfenmao
1ed6320caf refactor(license.html): update structure and styling for improved readability and consistency 2025-04-12 16:41:26 +08:00
kangfenmao
315271ac35 Revert "fix(ChatNavigation): improve navigation button collapse functionality"
This reverts commit fb5ddaf9d5.
2025-04-12 16:12:34 +08:00
kangfenmao
0bd24f652d feat(NewContextButton): add styled container for responsive design
- Introduced a styled container to the NewContextButton component to hide it on smaller screens (max-width: 800px).
- Ensured the tooltip and button functionality remain intact while enhancing the component's layout.
2025-04-12 16:11:00 +08:00
kangfenmao
0e7c4e4bdd refactor(Inputbar, Messages): simplify clear topic functionality and improve message display logic
- Removed unused QuestionCircleOutlined icon and Popconfirm from Inputbar, replacing it with a direct button click for clearing topics.
- Refactored message display logic in Messages component to enhance clarity and maintainability, while preserving existing functionality.
2025-04-12 16:07:40 +08:00
kangfenmao
d4bf8da225 feat(CustomCollapse): enhance component with customizable styles and improve usage in EditModelsPopup 2025-04-12 15:57:50 +08:00
LiuVaayne
8eb6632620 Feat/improve UI mcp settings (#4717)
* feat(MCPSettings): implement server selection and navigation with back button

* chore(ui)

* chore(UI): npx search padding

* feat(NpxSearch): add server selection and navigation; update styles

---------

Co-authored-by: eeee0717 <chentao020717Work@outlook.com>
2025-04-12 15:31:52 +08:00
王叔叔
10225512f4 docs: Update LICENSE (#4723) 2025-04-12 15:31:33 +08:00
Hao He
76058bd749 feat(MessageTools): add error handling and status indicator for tool responses (#4712)
* feat(MessageTools): add error handling and status indicator for tool responses

* feat(i18n): add error message for tool invocation in English, Japanese, and Russian locales
2025-04-12 10:33:14 +08:00
Herio
a692ae7e9d fix(Messages): 调整ScrollContainer和Container的样式以减少底部空间 2025-04-12 10:28:06 +08:00
LiuVaayne
a70ca190ba Feat/mcp support MCP prompt (#4675)
* Add MCP prompt listing and retrieval functionality

* Add generic caching mechanism for MCP service methods

Refactor caching strategy by implementing a higher-order withCache function
to centralize cache logic and reduce code duplication. Separate implementation
details from caching concerns in listTools, listPrompts and getPrompt methods.

# Conflicts:
#	src/main/services/MCPService.ts

* Add MCP prompts listing feature

- Add IPC handlers for listing and getting prompts
- Create UI component to display available prompts in settings tab
- Improve error handling in MCP service methods

* fix(McpService): add error handling for tool and prompt listing methods

* feat(MCPSettings): enhance prompts and tools sections with improved UI and reset functionality

* feat(i18n): add tabs and prompts sections to localization files

* feat(MCPToolsButton): add MCP prompt list functionality to Inputbar

* feat(McpSettings, NpxSearch): improve user feedback with success messages on server addition

* feat(MCPService, MCPToolsButton): enhance prompt handling with caching and improved selection logic

* feat(MCPToolsButton): enhance prompt handling with argument support and error management

---------

Co-authored-by: Teo <cheesen.xu@gmail.com>
2025-04-12 10:27:48 +08:00
robot-AI
7c39116351 重构了memory.ts,增加了文件写入锁,解决了并行写入导致记忆文件错误的问题; (#4671)
优化了memory.json文件的加载过程,只加载一次,其它涉及图谱的操作均在内存中完成,提高效率;
注意新引入了async-mutex软件包,需要yarn install安装。
2025-04-11 22:03:57 +08:00
kangfenmao
04333535dd chore(version): 1.2.2 2025-04-11 14:43:02 +08:00
kangfenmao
a1dba93d27 feat(websearch): initialize subscribeSources in migrateConfig and update WebSearchState interface 2025-04-11 14:42:35 +08:00
Chen Tao
0842b7e84d fix(llm): rename settingsSlice to llmSlice for clarity (#4688) 2025-04-11 11:32:30 +08:00
kangfenmao
24d6d146c0 fix(scripts): update download URLs and default versions for bun and uv binaries 2025-04-11 11:25:37 +08:00
kangfenmao
978c3ea3cf feat(i18n): update subscription terminology in multiple languages for consistency 2025-04-10 22:12:27 +08:00
ousugo
a9eb235c43 refactor(SettingsTab): update reasoning effort change handler to use useCallback for performance optimization 2025-04-10 21:47:14 +08:00
ousugo
e0a47de8f7 feat(CodeBlock): add tooltips for collapse and copy buttons 2025-04-10 21:47:14 +08:00
ousugo
78a4696327 feat(models): add grok-3 support to FUNCTION_CALLING_MODELS 2025-04-10 21:46:48 +08:00
Asurada
57fa0aad38 feat(xAI): Add support for Grok-3-mini and update reasoning effort logic (#4657)
* feat(models): add grok-3-mini support and update reasoning effort logic in SettingsTab and OpenAIProvider

* feat(settings): update reasoning effort logic for Grok models and enhance localization in multiple languages

* fix(models): correct spelling of reasoning in model support functions

* fix(settings): correct spelling of reasoning_effort in OpenAIProvider
2025-04-10 18:43:20 +08:00
Chen Tao
2e0251aed7 feat: support ublacklist subscribe (#2974)
* feat: support ublacklist subscribe

* Merge branch 'main' into feat-ublacklist

* chore

* chore
2025-04-10 17:25:38 +08:00
ousugo
afd1381d7f refactor(CodeBlock): simplify header layout and adjust CollapseIcon position 2025-04-10 17:22:20 +08:00
LiuVaayne
c3b5cbee8f Clean up MCPService connections on app quit (#4647)
* Clean up MCPService connections on app quit

* Improve application shutdown error handling
2025-04-10 17:19:02 +08:00
kangfenmao
e1f255048e feat(models): add Qiniu models to SYSTEM_MODELS and update migration logic to initialize provider models
- Introduced new models for the Qiniu provider in SYSTEM_MODELS.
- Updated migration logic to populate Qiniu provider models if they are empty during state initialization.
2025-04-10 13:42:03 +08:00
kangfenmao
8a579be4c1 refactor(after-pack): rename function to keepArchNodeFiles and update logic for retaining architecture-specific node modules
close PR#4522
2025-04-10 13:15:41 +08:00
kangfenmao
efcffbaa30 feat(websearch): enhance web search provider settings and localization
- Updated web search provider settings to include API key and free status indicators.
- Improved localization for English, Japanese, Russian, Chinese, and Taiwanese languages to reflect new API key and free status fields.
- Refactored web search provider management to prevent duplicates and streamline provider addition during state migration.
- Adjusted UI components to conditionally render based on provider type, enhancing user experience.
2025-04-10 13:07:55 +08:00
LiuVaayne
f9c6bddae5 feat(search): support using google as default search provider (#4569)
* feat(websearch): implement search window functionality and enhance search service

* feat(DefaultProvider): integrate @mozilla/readability for improved content parsing

* Add LocalSearchProvider for web page scraping

AI: Change `provider` from private to protected in BaseWebSearchProvider and implement LocalSearchProvider for web searching with browser-based content extraction.

* Add web search provider management features

Implement addWebSearchProvider function to prevent duplicates,
automatically load default providers on initialization, fix
LocalSearchProvider implementation, and update local provider
identification logic.

* Improve web search with specialized search engine parsers

Add dedicated parsers for Google, Bing, and Baidu search results,
replacing the generic URL extraction approach. Enhance page loading
with proper wait mechanisms and window cleanup. Remove DuckDuckGo
provider as it's no longer supported.

* Simplify DefaultProvider to unimplemented placeholder

* Remove default search engine from initial state

* Improve web search providers config and display

Add configuration for local search providers, remove empty apiKey fields,
and enhance the UI by sorting providers alphabetically and showing
whether they require an API key.

* Add stderr logging for MCP servers

* Make search window initially hidden
2025-04-10 12:29:09 +08:00
司马琦昂
5e086a1686 fix: O3 config text-embedding-3-small duplicate 2025-04-10 10:20:34 +08:00
fullex
0db4c8b475 fix: [mac] issues related to fullscreen mode (#4618) 2025-04-10 09:02:53 +08:00
自由的世界人
d5fcef39d3 feat: add model provider logo upload (#4408)
* feat: add model provider logo upload

* Update index.tsx

* fix: upload image delete
2025-04-09 23:52:42 +08:00
kangfenmao
5c44f71684 refactor(ModelList): replace FileItem with ListItem and HStack for improved layout and styling 2025-04-09 20:42:36 +08:00
fullex
3462be2a2a fix:[mac] window level to show py input 2025-04-09 20:12:33 +08:00
Teo
a0be911dc9 feat: Optimize QuickPanel (#4604)
* feat(QuickPanel): enhance close action options and improve input handling

- Added 'enter_empty' as a new close action option for QuickPanel.
- Refactored input handling to include a delay before clearing search text after panel closure.
- Updated keyboard event handling to prevent default actions for specific keys.
- Improved styling for selected and focused states in QuickPanel components.
- Enhanced AttachmentPreview to utilize a separate FileNameRender component for better readability and functionality.

* feat(AttachmentPreview): enhance file icon rendering and styling

* feat(CustomTag): add closable functionality and improve styling

- Enhanced CustomTag component to support closable tags with an onClose callback.
- Updated styling for better visual integration and added hover effects for the close icon.
- Refactored usage of CustomTag in AttachmentPreview, KnowledgeBaseInput, and MentionModelsInput components for consistency.

* feat(SelectModelPopup, QuickPanel): update tag component and enhance search functionality

* feat(Inputbar, SettingsTab): add enable quick panel triggers setting and update translations

* feat(QuickPanel): integrate color library for dynamic styling and update package dependencies
2025-04-09 17:00:34 +08:00
magicdmer
f7f7d2bde8 fix: 解决聊天页面图片复制失败的问题和点击编辑回复的时候,不显示图片url的问题 (#4496)
* fix: 解决聊天页面图片复制失败的问题和点击编辑回复的时候,不显示图片url的问题

* fix: 解决chat模式,gemini-2.0-flash-exp-image-generation返回base64图片,无法复制的问题

* fix(MessageImage): Update the image copying feature to process base64 and URL formatted images based on their type

---------

Co-authored-by: magicdmer <magicdmer@163.com>
Co-authored-by: eeee0717 <chentao020717Work@outlook.com>
2025-04-09 16:23:25 +08:00
fullex
10efa444bf fix: missing ExportMenuOptions in persist leads to useSelector re-render a lot (#4593)
* fix: missing ExportMenuOptions in persist leads to useSelector re-render

* chore: cleanup
2025-04-09 13:54:15 +08:00
LiuVaayne
24e28b86cf feat(mcp): support MCP by prompt (#4476)
* feat: implement tool usage handling and system prompt building for AI providers

* refactor: streamline tool usage handling and remove unused code in OpenAIProvider and formats

* refactor: simplify tool usage handling in Anthropic and Gemini providers, and update prompt instructions

* refactor: remove unused function calling model checks and simplify MCP tools handling in Inputbar

* hidden tool use in message

* revert  import

* Add idx parameter to parseAndCallTools for unique tool IDs
2025-04-09 11:22:14 +08:00
Hao He
fa66d048d7 feat(AssistantsTab): add sorting functionality by Pinyin and update translations (#4507) 2025-04-09 09:23:11 +08:00
Vaayne
fe7a392116 fix(NpxSearch): update SearchResult type to use MCPServer type definition 2025-04-09 09:21:35 +08:00
Vaayne
c883fd85d8 feat(MCP): add StreamableHTTPClientTransport and update server type handling 2025-04-09 09:21:35 +08:00
suyao
aa73025568 feat(websearch): improve web search enablement logic 2025-04-09 09:19:42 +08:00
ZhuangYumin
9689f00214 fix: fix main-window fake show up in Wayland KDE 2025-04-09 00:12:33 +08:00
kangfenmao
3674cc4afe chore(version): 1.2.1 2025-04-08 23:19:52 +08:00
kangfenmao
1f21b99820 feat(ModelTagsWithLabel): enhance tag component styling and update layout
- Improved icon size handling in the ModelTagsWithLabel component for better visual consistency.
- Adjusted the layout of the tags to prevent wrapping and added horizontal scrolling for better usability.
- Updated the EditModelsPopup to increase its width for improved content display.
- Removed unnecessary CustomTag usage in ModelList for cleaner code.
2025-04-08 21:32:17 +08:00
kangfenmao
a3e10dd116 chore(version): 1.2.0 2025-04-08 20:29:54 +08:00
SuYao
ab1a5f18c9 feat(websearch): add overwrite functionality for search service (#4530)
* feat(websearch): add overwrite functionality for search service

- Introduced new settings to allow users to override the default search service.
- Updated localization files for English, Japanese, Russian, Simplified Chinese, and Traditional Chinese to include new overwrite options and tooltips.
- Modified relevant components and services to support the new overwrite feature in the web search settings.

* feat(websearch): enhance web search model integration

* chore(websearch): unnecessary return
2025-04-08 20:07:00 +08:00
kangfenmao
2a0d6eb08a feat: update dangbei miniapp integration and version bump
- Added `bodered` property to the dangbei miniapp configuration.
- Refactored migration logic to utilize a new `addMiniApp` function for cleaner code.
- Incremented store version from 91 to 92 for migration compatibility.
2025-04-08 19:59:32 +08:00
DemonJun
f78663f815 feat: add dangbei miniapp (#4552)
* feat: add dangbei miniapp

* compressed logo file

---------

Co-authored-by: demonjun <demonjun@foxmail.com>
2025-04-08 19:46:26 +08:00
kangfenmao
8bcb31071e feat(CustomCollapse, KnowledgeContent): enhance collapsible behavior and UI updates
- Added `activeKey` prop to `CustomCollapse` for better control over active panels.
- Updated styles in `CustomCollapse` for improved visual consistency.
- Refactored `KnowledgeContent` to include expand/collapse functionality for file and directory sections, enhancing user experience.
- Added translations for "collapse" in multiple languages.
- Minor style adjustments across various components for better UI consistency.
2025-04-08 19:38:15 +08:00
Teo
3aaa1848f0 style(ProviderSettings): Refactor ProviderSettings UI (#4475)
* chore(version): 1.1.19

* style(ProviderSettings): Refactor ProviderSettings UI

* style(CustomTag, ModelTagsWithLabel): enhance layout and styling for better UI consistency

* refactor(CustomTag, ModelTagsWithLabel, MentionModelsButton): update props handling and improve component usage

* feat(CustomTag, ModelTagsWithLabel): add tooltip support and improve label visibility based on container size

* fix(ModelTagsWithLabel): adjust maxWidth for non-Chinese languages to improve layout

* style(ModelList): add text overflow handling for list item names

* feat(ModelList): enhance group label with item count using CustomTag

* feat(FileItem): add style prop for customizable background color in FileItem component

* style(index.scss): update border color variables for improved UI consistency

* style(EditModelsPopup): update background color for model items to enhance visual distinction

* style(HealthCheckPopup): update button size for improved usability

* feat(CustomCollapse): add collapsible prop to customize collapse behavior

* chore: remove hover models color

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>
Co-authored-by: eeee0717 <chentao020717Work@outlook.com>
2025-04-08 19:37:11 +08:00
Vaayne
037027f1f4 fix(McpService): improve client connection handling with error logging 2025-04-08 17:28:09 +08:00
suyao
97c1d67cbf fix(formats): add optional chaining for grounding support properties to prevent errors 2025-04-08 17:27:06 +08:00
suyao
d38c4c7368 fix(MessageContent): handle optional chaining for grounding metadata and citations 2025-04-08 17:27:06 +08:00
Hamm
b1bd5d0531 refactor(reranker): 重构重排序功能以提高可维护性 (#4539)
* refactor(reranker): 重构重排序功能以提高可维护性

- 将 BaseReranker 类中的公共逻辑提取到受保护的方法中
- 优化了 JinaReranker、SiliconFlowReranker 和 VoyageReranker 的实现
- 新增 getRerankUrl 和 getRerankResult 方法以提高代码复用性
- 简化了重排序结果的处理逻辑

* refactor(reranker): 将 formatErrorMessage 方法的访问权限改为受保护

- 将 formatErrorMessage 方法的访问权限从公共 (public) 改为受保护 (protected)
- 这一更改限制了方法的访问范围,仅允许子类访问该方法
- 有助于提高代码的封装性和安全性
2025-04-08 16:53:31 +08:00
one
1fcee6c829 fix: wrap inline code 2025-04-08 09:26:37 +08:00
MyPrototypeWhat
cbcebdc87a fix(NpxSearch): update search result mapping to use record name as ke… (#4491)
fix(NpxSearch): update search result mapping to use record name as key and handle optional search results

Co-authored-by: lizhixuan <zhixuan.li@banosuperapp.com>
2025-04-07 19:45:40 +08:00
kangfenmao
96df9f6979 chore(version): 1.1.19 2025-04-07 16:04:48 +08:00
kangfenmao
4ef9d52694 Revert "refactor(Navbar): replace FormOutlined with SearchOutlined and update tooltip functionality"
This reverts commit d8baf378ea.
2025-04-07 14:34:12 +08:00
kangfenmao
41b9f8dbd5 feat(mcp): add automatic MCP server registration for auto-install tool
- Implemented functionality to automatically register MCP servers when the '@cherry/mcp-auto-install' tool is called.
- Utilized nanoid for unique server ID generation and dispatched the new server to the Redux store.
2025-04-07 14:33:43 +08:00
kangfenmao
8191791036 refactor(mcp): move inMemory servers to npx search scope
- Updated error handling in FileSystemServer to throw errors instead of exiting the process.
- Enhanced MCPService to use a more flexible server name check and updated the path for MCP_REGISTRY_PATH.
- Refined Inputbar to conditionally set enabled MCPs only if they are not empty.
- Cleaned up MCPToolsButton and MCPSettings by removing unused imports and effects.
- Updated NpxSearch to include a new npm scope and improved search result handling.
- Enhanced builtin MCP server descriptions for better clarity.
2025-04-07 14:07:01 +08:00
fullex
99b37f2782 perf: remove unused codes related to minapp (#4444) 2025-04-07 09:51:10 +08:00
fullex
da49c3ddd3 fix: remove sidebar minapp animation to stop gpu high load 2025-04-07 09:37:10 +08:00
one
6891068ca1 perf: prevent unnecessary reflow 2025-04-07 09:36:46 +08:00
kangfenmao
b361001f39 fix(WindowService): conditionally hide dock icon for macOS when closing to tray 2025-04-06 21:32:56 +08:00
kangfenmao
3823912b3e feat(i18n): add user and system labels to multiple language files 2025-04-06 21:27:37 +08:00
one
b5ad77e70c fix: LRU cache import 2025-04-06 21:11:37 +08:00
eeee0717
3491eec86b fix(websearch): improve error handling and response validation 2025-04-06 21:03:38 +08:00
kangfenmao
581ad5fbda feat: add qiniu ai provider 2025-04-06 18:50:35 +08:00
Teo
90424808ab refactor(Inputbar): streamline Backspace handling and update knowledge base management 2025-04-06 18:49:50 +08:00
kangfenmao
c884b11f01 feat(MCPSettings): enhance server management with segmented control and improved layout 2025-04-06 16:59:09 +08:00
kangfenmao
a530ce652e fix(useAssistant): ensure safe access to assistant ID in setModel callback 2025-04-06 14:40:32 +08:00
Shelly
9c052dee5c fix: 🐛 LRUCache export name error (#4420)
BREAKING CHANGE: 🧨 SyntaxError: The requested module
'/@fs/Users/bary/code/tools/cherry-studio/node_modules/.vite/deps/lru-cache.js?v=9139ab94'
does not provide an export named 'LRUCache' (at
CodeCacheService.ts:2:10)
2025-04-06 12:41:30 +08:00
LiuVaayne
1085c11240 bugfix(MCP): ensure memory path exists on initialization and remove unused… (#4418)
* bugfix: ensure memory path exists on initialization and remove unused everything mcp server

* refactor(factory): remove unused EverythingServer import and case

* fix(CodeCacheService): update import statement for LRUCache to default import

Error: src/renderer/src/services/CodeCacheService.ts(2,10): error TS2595: 'LRUCache' can only be imported by using a default import.
2025-04-06 12:40:51 +08:00
kangfenmao
ae6097a29e chore(dependencies): update libsql and add sindresorhus/is package
- Updated libsql patch reference in package.json.
- Added sindresorhus/is package to yarn.lock with version 7.0.1.
- Removed duplicate sindresorhus/is entry from yarn.lock.
2025-04-06 10:51:41 +08:00
kangfenmao
d74f05f27e refactor(Inputbar, Markdown): optimize citation handling and improve performance
- Refactored AttachmentButton to use useMemo for extensions calculation.
- Simplified citation extraction in Markdown by moving logic to a new utility function.
- Updated TopicsTab dependencies for better performance and reactivity.
2025-04-06 10:49:52 +08:00
Sophon
8501ab82c6 build: Add support for Windows ARM64 platform (#3431)
Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-04-06 09:35:08 +08:00
SuYao
f2ca56a088 feat(UI, OpenAI): support OpenAI-4o-web-search add support for web search citations (#3524)
* feat(UI, OpenAI): support  OpenAI 4o web search add support for web search citations

- refactor: Introduced a new CitationsList component to display citations in MessageContent.
- feat: Enhanced message handling to support web search results and annotations from OpenAI.
- refactor: Removed the deprecated MessageSearchResults component for cleaner code structure.
- refactor: Added utility functions for link conversion and URL extraction from Markdown.

* chore: remove debug logging from ProxyManager

* revert(OpenAIProvider): streamline reasoning check for stream output handling

* chore(OpenAIProvider): correct placement of webSearch in response object

* fix(patches): update OpenAI package version and remove patch references

- Integrated dayjs for dynamic date formatting in prompts.ts.

* feat(Citation, Favicon): enhance OpenAI web search support and citation handling

- Improved FallbackFavicon component to cache failed favicon URLs.
- Support all web search citation preview
- Added support for Hunyuan search model in OpenAIProvider and ApiService.

* refactor(provider/AI): move additional search parameters to AI Provider
2025-04-06 09:11:59 +08:00
kangfenmao
e02c967f5b fix(Markdown): Conditionally apply MarkdownShadowDOMRenderer for style components based on message content 2025-04-06 09:11:23 +08:00
PilgrimLyieu
7284679907 feat: Support bottom anchor in message anchor line 2025-04-06 08:50:05 +08:00
George·Dong
56e9a7371a refactor(export): 添加导出菜单选项设置、思维链导出功能 (#4168)
* refactor(settings): Add export menu setting & optimize data settings page

* feat: add dynamic export menu options from Redux state in MessageMenubar and TopicsTab

* feat(export): Add export to markdown with reasoning method

* feat(export): optimize reasoning style

* feat(export): Add export to markdown with reasoning to export menu

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

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-04-06 08:43:54 +08:00
one
95639df35c perf(CodeBlock): improve long codeblock loading experience (#4167)
* perf(CodeBlock): improve long codeblock loading experience

* refactor: use requestIdleCallback rather than observer

* refactor: simplify setting expanded and unwrapped

* refactor: simplify logic

* refactor: revert to observer

* fix: turn mermaid listener to passive to avoid scrolling performance downgrade

* feat: add lru cache for syntax highlighting

* refactor: adjust cache options

* feat: add highlighter cache

* fix: highlighter should be loaded before highlighting

* refactor: reduce cache time

* refactor: adjust cache size and hash

* refactor: decrease cache size

* fix: restore the behaviour of ShowExpandButton

* fix: check streaming status

* fix: empty code

* refactor: improve streaming check

* fix: optimizeDeps excludes

* refactor: adjust cache policy

* feat: add a setting for code caching

* feat: add more settings for code cache

* fix: initialize service

* refactor: prevent accident cache reset, update settings

* refactor: update code cache service

* fix: revert unecessary changes

* refactor: adjust cache settings

* fix: update migrate version

* chore: update to shiki v3

* fix: import path

* refactor: remove highlighter cache, improve fallbacks

* fix: revert path changes

* style: fix lint errors

* style: improve readability

* style: improve readability

* chore: update migrate version

* chore: update packages
2025-04-06 08:38:02 +08:00
PeterWang-dev
641dfc60b0 feat(icons): explicitly add icons in multiple resolutions to correct appimage icon path 2025-04-06 08:28:10 +08:00
kangfenmao
46c7df6f5b refactor(GraphRAG): Remove GraphRAG related files and references from the project 2025-04-06 08:11:10 +08:00
one
b828d1f54f perf(QuickPanel): improve search responsiveness (#4406) 2025-04-06 01:06:54 +08:00
kangfenmao
d9abfc5443 feat(database): Add version 6 with new 'files' store and update existing stores 2025-04-05 20:59:36 +08:00
kangfenmao
7364646caa feat(MCPSettings): Reset form change state on server ID change and disable server type selection for inMemory servers 2025-04-05 20:58:36 +08:00
kangfenmao
5fa7465174 refactor(Inputbar): Update file name handling in AttachmentPreview, adjust padding in Inputbar, and enhance textarea expansion logic 2025-04-05 20:33:44 +08:00
Teo
bc02727633 feat(QuickPanel): Optimize QuickPanel (#4404)
feat(QuickPanel): Add footer resizing and improve item action handling
2025-04-05 20:19:43 +08:00
kangfenmao
c76f274562 feat: google analytics 2025-04-05 16:07:04 +08:00
Teo
f9be0e0d26 feat(QuickPanel): Add new feature QuickPanel, unify input box operation. (#4356)
* feat(QuickPanel): Add new feature QuickPanel, unify input box operation.

* refactor(Inputbar): Remove unused quickPanelSymbol reference and update navigation action in KnowledgeBaseButton

* fix(Inputbar): Prevent translation action when input text is empty and reorder MentionModelsInput component

* refactor(Inputbar): Add resizeTextArea prop to QuickPhrasesButton for better text area management

* feat(i18n): Add translation strings for input actions and quick phrases in multiple languages

* feat(Inputbar): Enhance AttachmentButton to support ref forwarding and quick panel opening

* feat(i18n, Inputbar): Add upload file translation strings and enhance file count display in multiple languages

* style(QuickPanel): Update background color for QuickPanelBody and add dark theme support

* fix(Inputbar): Update upload label for vision model support

* feat(QuickPanel): Add outside click handling and update close action type

* feat(QuickPanel): Improve scrolling behavior with key press handling and add PageUp/PageDown support

* feat(i18n): Add translation strings for menu description in multiple languages

* refactor(QuickPhrasesButton): simplify phrase mapping by removing index-based disabling

* fix(QuickPanel): correct regex pattern for search functionality

* refactor(QuickPanel): remove searchText state and related logic for cleaner context management

* refactor(QuickPanel): enhance search text handling and input management

* refactor(Inputbar): update file name handling in AttachmentPreview and Inputbar components
2025-04-05 16:05:28 +08:00
LiuVaayne
ea059d5517 feat(mcp): add in-memory MCP server support and configuration management (#4359) 2025-04-05 14:17:56 +08:00
kangfenmao
9c6de71fbb lint: fix eslint error 2025-04-05 10:52:45 +08:00
Hamm
3290ac4b1b refactor(Constants): 优化一些常量和枚举值 (#3773)
* refactor(main): 使用枚举管理 IPC 通道

- 新增 IpcChannel 枚举,用于统一管理所有的 IPC 通道
- 修改相关代码,使用 IpcChannel 枚举替代硬编码的字符串通道名称
- 此改动有助于提高代码的可维护性和可读性,避免因通道名称变更导致的错误

* refactor(ipc): 将字符串通道名称替换为 IpcChannel 枚举

- 在多个文件中将硬编码的字符串通道名称替换为 IpcChannel 枚举值
- 更新了相关文件的导入,增加了对 IpcChannel 的引用
- 通过使用枚举来管理 IPC 通道名称,提高了代码的可维护性和可读性

* refactor(ipc): 调整 IPC 通道枚举和预加载脚本

- 移除了 IpcChannel 枚举中的未使用注释
- 更新了预加载脚本中 IpcChannel 的导入路径

* refactor(ipc): 更新 IpcChannel导入路径

- 将 IpcChannel 的导入路径从 @main/enum/IpcChannel 修改为 @shared/IpcChannel
- 此修改涉及多个文件,包括 AppUpdater、BackupManager、EditMcpJsonPopup 等
- 同时移除了 tsconfig.web.json 中对 src/main/**/* 的引用

* refactor(ipc): 添加 ReduxStoreReady 事件并更新事件监听

- 在 IpcChannel 枚举中添加 ReduxStoreReady 事件
- 更新 ReduxService 中的事件监听,使用新的枚举值

* refactor(main): 重构 ReduxService 中的状态变化事件处理

- 将状态变化事件名称定义为常量 STATUS_CHANGE_EVENT
- 更新事件监听和触发使用新的常量
- 优化了代码结构,提高了可维护性

* refactor(i18n): 优化国际化配置和语言选择逻辑

- 在多个文件中引入 defaultLanguage 常量,统一默认语言设置
- 调整 i18n 初始化和语言变更逻辑,使用新配置
- 更新相关组件和 Hook 中的语言选择逻辑

* refactor(ConfigManager): 重构配置管理器

- 添加 ConfigKeys 枚举,用于统一配置项的键名
- 引入 defaultLanguage,作为默认语言设置
- 重构 get 和 set 方法,使用 ConfigKeys 枚举作为键名
- 优化类型定义和方法签名,提高代码可读性和可维护性

* refactor(ConfigManager): 重命名配置键 ZoomFactor

将配置键 zoomFactor 重命名为 ZoomFactor,以符合命名规范。
更新了相关方法和属性以反映这一变更。

* refactor(shared): 重构常量定义并优化文件大小格式化逻辑

- 在 constant.ts 中添加 KB、MB、GB 常量定义
- 将 defaultLanguage 移至 constant.ts
- 更新 ConfigManager、useAppInit、i18n、GeneralSettings 等文件中的导入路径
- 优化 formatFileSize 函数,使用新定义的常量

* refactor(FileSize): 使用 GB/MB/KB 等常量处理文件大小计算

* refactor(ipc): 将字符串通道名称替换为 IpcChannel 枚举

- 在多个文件中将硬编码的字符串通道名称替换为 IpcChannel 枚举值
- 更新了相关文件的导入,增加了对 IpcChannel 的引用
- 通过使用枚举来管理 IPC 通道名称,提高了代码的可维护性和可读性

* refactor(ipc): 更新 IpcChannel导入路径

- 将 IpcChannel 的导入路径从 @main/enum/IpcChannel 修改为 @shared/IpcChannel
- 此修改涉及多个文件,包括 AppUpdater、BackupManager、EditMcpJsonPopup 等
- 同时移除了 tsconfig.web.json 中对 src/main/**/* 的引用

* refactor(i18n): 优化国际化配置和语言选择逻辑

- 在多个文件中引入 defaultLanguage 常量,统一默认语言设置
- 调整 i18n 初始化和语言变更逻辑,使用新配置
- 更新相关组件和 Hook 中的语言选择逻辑

* refactor(shared): 重构常量定义并优化文件大小格式化逻辑

- 在 constant.ts 中添加 KB、MB、GB 常量定义
- 将 defaultLanguage 移至 constant.ts
- 更新 ConfigManager、useAppInit、i18n、GeneralSettings 等文件中的导入路径
- 优化 formatFileSize 函数,使用新定义的常量

* refactor: 移除重复的导入语句

- 在 HomeWindow.tsx 和 useAppInit.ts 文件中移除了重复的 defaultLanguage导入语句
- 这个改动简化了代码结构,提高了代码的可读性和维护性
2025-04-04 19:07:23 +08:00
lizhixuan
ef8250ab72 refactor(MCPSettings): replace MainContent callback with useMemo for performance optimization 2025-04-04 18:59:54 +08:00
kangfenmao
773c0da9ef chore(version): 1.1.18 2025-04-04 12:07:05 +08:00
kangfenmao
a3d124b9fd feat(migrate): update migration logic to remove specific mini app from state and increment version to 89 2025-04-04 11:54:24 +08:00
kangfenmao
c11adc01dc fix(SelectModelPopup): set popup width to 600 for improved layout 2025-04-04 11:49:59 +08:00
kangfenmao
d8baf378ea refactor(Navbar): replace FormOutlined with SearchOutlined and update tooltip functionality 2025-04-04 10:54:21 +08:00
kangfenmao
3768c135d8 fix(Inputbar): enhance message usage estimation and clean up code structure 2025-04-04 10:34:08 +08:00
LiuVaayne
10848f7a45 feat: mcp tool permissions (#4348)
* feat(MCPSettings): add tool toggle functionality and update server configuration

* fix(McpSettings): improve server type handling and tool fetching logic
2025-04-04 09:57:54 +08:00
fullex
4d5cfe06f5 feat: render markdown when show assistant prompt (#4365)
* feat: render markdown when show assistant prompt

* fix: polish user experience
2025-04-04 09:37:29 +08:00
George·Dong
fb5ddaf9d5 fix(ChatNavigation): improve navigation button collapse functionality 2025-04-04 00:07:36 +08:00
fullex
8cb11e6d55 fix: webdav backup resume and modal problem 2025-04-03 23:09:59 +08:00
ousugo
d0cb333f3c fix(TopicsTab): Topic prompt word modification does not take effect immediately 2025-04-03 23:03:32 +08:00
MyPrototypeWhat
23de48ecbd feat(mcp): add registryUrl to initialState for improved package manag… (#4279)
* feat(mcp): add registryUrl to initialState for improved package management

* feat(mcp): initialize registryUrl in MCP server state for future enhancements

* fix(inputbar):mcp server list

* refactor(Inputbar): remove unused MCP server variable and console log for cleaner code

* fix(Inputbar): 还原

* fix(Inputbar): Add activedMcpServers to Inputbar component props
2025-04-03 18:51:29 +08:00
BlBana
06c730aaf6 fix(Inputbar): Solve the problem that the initial state of assistant mcpServers is empty, and can not get enable mcp servers. 2025-04-03 17:45:14 +08:00
ousugo
aed9c04c20 fix(minapps): remove AI Studio entry from default mini apps list 2025-04-03 17:41:13 +08:00
Asurada
d067d21561 feat(Anthropic): Enable Anthropic 128k context beta feature (#2887) 2025-04-03 17:16:36 +08:00
Camol
515721239f fix(nutstore): restore from nutstore #4318 (#4334)
Co-authored-by: kanweiwei <kanweiwei@nutstore.net>
2025-04-03 10:56:05 +08:00
Yuzhong Zhang
5cdf4eff77 fix(CodeBlock): incorrect behavior of message in multiple models (#4328)
* 修复多模型对比时的复制按钮sticky行为,并添加注释

* 同时修复横向滚动条消失的问题

* 增加布局判断
2025-04-03 01:01:04 +08:00
LiuVaayne
b53dbcbb30 fix(mcp-tools): enhance tool lookup to match by name in addition to ID (#4323) 2025-04-02 23:20:07 +08:00
Bowie He
a42283e789 feat:add default doubao model to model list 2025-04-02 18:56:30 +08:00
kanweiwei
d2ed9972bd fix(NutstoreService): Fix slash handling in path processing #4208 2025-04-02 13:38:29 +08:00
fullex
0fd9b6e56c feat: miniWindow Pin/Resize (#3201)
feat: [#2030] miniWindow pin/resizable/copy toast/move optimized
2025-04-02 10:26:56 +08:00
shiquda
91b9a48c48 fix(Knowledge): enable text selection in knowledge base search results (#4281) 2025-04-01 23:16:34 +08:00
kangfenmao
e572b3801b lint: Added an eslint disable comment in MinappPopupContainer to address 2025-04-01 21:05:36 +08:00
亢奋猫
4bf15aed25 refactor(MCPService, process): Updated MCPService to conditionally set the NPM_CONFIG_REGISTRY
* refactor(MCPService, process): enhance registry URL handling and improve getBinaryPath function

- Updated MCPService to conditionally set the NPM_CONFIG_REGISTRY based on server name, improving flexibility for auto-install scenarios.
- Modified getBinaryPath function to handle optional name parameter, returning a default path when no name is provided, enhancing usability.

* refactor(MCPService, utils): add directory existence check for registry file

- Introduced makeSureDirExists utility function to ensure the specified directory exists, enhancing robustness.
- Updated MCPService to utilize this function when setting the registry URL for the mcp-auto-install server, improving error handling.

* feat:change MCP_REGISTRY_PATH

* refactor(MCPService): streamline environment variable setup for mcp-auto-install

- Updated MCPService to conditionally set NPM_CONFIG_REGISTRY and MCP_REGISTRY_PATH in a more concise manner.
- Enhanced readability by removing redundant code while maintaining functionality.

---------

Co-authored-by: lizhixuan <zhixuan.li@banosuperapp.com>
2025-04-01 20:57:56 +08:00
Chen Tao
6d568688ed fix(KnowledgeContent): adjust VirtualList height based on item count (#4270) 2025-04-01 19:47:58 +08:00
kangfenmao
f20cbf31a8 refactor(McpSettings): simplify args form item and adjust navbar padding for Windows
- Removed unnecessary validation rules from the args Form.Item in McpSettings for cleaner code.
- Updated McpSettingsNavbar to conditionally adjust padding based on the operating system.

close #4244
2025-04-01 17:03:27 +08:00
kangfenmao
bfbfba13fe feat(Inputbar, MCPToolsButton, AssistantMCPSettings): integrate active MCP server handling and UI updates
- Added active MCP server filtering in Inputbar for message sending.
- Updated MCPToolsButton to reflect availability of enabled MCPs.
- Refactored AssistantMCPSettings to streamline MCP server updates and adjusted UI styles for consistency.
2025-04-01 16:57:12 +08:00
Hobee Liu
8b9929cc7b feat: add chat navigation bar close (#4019)
* feat(聊天导航): 新增关闭、置顶和置底按钮并更新图标

在聊天导航组件中新增了关闭、置顶和置底按钮,并更新了相关图标以提升用户体验。同时,添加了点击关闭按钮时隐藏导航的功能。

* feat(消息导航): 添加手动关闭状态以避免误触

在 ChatNavigation 组件中添加 `manuallyClosedUntil` 状态,用于在用户手动关闭导航后,1分钟内不响应鼠标靠近事件。这可以防止用户在操作时误触导航栏,提升用户体验。

* refactor(ChatNavigation): 重命名函数并添加滚动处理逻辑

重命名 handleChatNavigationClick 为 handleCloseChatNavigation 以提高代码可读性,并添加 handleScrollToTop 和 handleScrollToBottom 函数以处理滚动逻辑

* fix: 修复滚动到顶部时位置不正确的问题

将 `scrollToTop` 函数中的 `top` 值从 `0` 改为 `-container.scrollHeight`,以确保滚动到顶部时位置正确

* docs(i18n): 添加新的翻译字符串以支持更多操作

在多个语言文件中添加了“回到顶部”、“回到底部”和“关闭”的翻译字符串,以支持更多用户界面操作。

* refactor: 移除未使用的变量以简化代码
```

解释:
- **类型**: `refactor`,因为这是代码重构,移除了未使用的变量,没有改变功能行为。
- **描述**: 移除了未使用的变量以简化代码,符合简洁和可维护性的原则。
2025-04-01 16:17:57 +08:00
Cherry
a90be7e83f feat: One-click copy model id (#4190)
* feat:One-click copy model id

* fix:model id消失问题,样式问题
2025-04-01 16:11:06 +08:00
fullex
efa68c8519 fix: chat history dark theme (#4254) 2025-04-01 14:55:24 +08:00
LiuVaayne
d7bd240a9a refactor: enhance mcp init (#4238)
* fix(MCPService): extend command support to include 'bun' and 'bunx', and improve environment variable handling

* fix(MCPService): enhance environment variable handling by incorporating default environment settings

* fix(hooks): simplify active MCP servers selection logic
2025-04-01 13:08:25 +08:00
kangfenmao
95df69ff82 refactor: move websearch provider code to providers folder 2025-04-01 11:05:31 +08:00
kangfenmao
e41df917b4 feat(i18n): add topic naming model support for message title generation in Japanese, Russian, and Traditional Chinese locales 2025-04-01 10:41:30 +08:00
kangfenmao
0a33649b3c fix(settings): enable clickAssistantToShowTopic by default 2025-04-01 10:06:24 +08:00
kangfenmao
d1cb7258d2 fix(Inputbar): simplify assistant state reset logic in useEffect 2025-04-01 09:57:21 +08:00
shiquda
8fbedb2bd0 feat: add support for title generation when exporting single message
#3992
2025-04-01 07:34:57 +08:00
MyPrototypeWhat
750247aef8 feat: add React Developer Tools extension support and optimize CodeBlock component 2025-04-01 07:33:16 +08:00
suyao
32e1f428e7 fix(styles): set dropdown menu width to max-content for better layout 2025-03-31 23:25:10 +08:00
suyao
aee6219a75 fix(styles): improve scrollbar visibility by adjusting opacity and background color on hover 2025-03-31 23:25:10 +08:00
suyao
5329fa7ede fix(UI): enhance scrollbar visibility and dropdown menu overflow handling 2025-03-31 23:25:10 +08:00
SuYao
ba640d4070 refactor(MCP): enhance schema validation for gemini (#4153) 2025-03-31 21:13:59 +08:00
LiuVaayne
8c5f61d407 feat(MCP): add registryUrl support for package management (#4200) 2025-03-31 21:13:20 +08:00
one
b43ecb75f5 perf: improve modellist search bar responsiveness (and memorization) (#4221) 2025-03-31 21:11:46 +08:00
Yuzhong Zhang
3dc4947e26 optimize: Sticky CopyButton in CodeBlock (#4205) 2025-03-31 21:11:28 +08:00
LiuVaayne
a5b0480418 Feat/assistant level mcp (#4220) 2025-03-31 21:10:33 +08:00
fullex
8a7db19e73 fix: Resolve a series of miniWindow display issues and improve app behavior across platforms (#3072) 2025-03-31 21:07:16 +08:00
MyPrototypeWhat
2da8a73124 feat(MCP): add auto-install server configuration and migration for ve… (#4156)
* feat(MCP): add auto-install server configuration and migration for version 87

* update persistReducer version
2025-03-31 18:07:50 +08:00
fullex
5223a3c5a6 feat: minapp show&copy current REAL url and can open it 2025-03-31 18:01:10 +08:00
fullex
72c5de3b81 optimize: reduce animation gpu load of sidebar minapp 2025-03-31 17:47:41 +08:00
one
9f11e7c22b perf(Tabs): improve responsiveness when switching items rapidly 2025-03-31 09:33:17 +08:00
fullex
1ce86c11ca fix: zoomfactor should not change when resize (#4159)
* fix: zoomfactor should not change when resize

* add linux fallback support
2025-03-31 09:24:49 +08:00
suyao
57c1b59a51 fix(models): reorganize gemini websearch model lists 2025-03-30 23:52:58 +08:00
kangfenmao
a2f9067908 chore(version): 1.1.17 2025-03-30 14:39:43 +08:00
kangfenmao
2a4c512e49 refactor(BackupManager): switch to stream-based file writing for improved performance
* Updated BackupManager to use streams for writing data to temporary and backup files, enhancing efficiency and error handling.
* Replaced synchronous file writing with asynchronous stream operations to prevent blocking the event loop.
2025-03-30 14:37:20 +08:00
kangfenmao
94eb7f3a34 refactor(knowledge): enhance CustomCollapse component and improve UI consistency
* Updated CustomCollapse to accept React nodes for labels, allowing for more flexible content.
* Replaced static labels with CollapseLabel component to display item counts.
* Introduced EmptyView component for consistent empty state representation across collapsible sections.
* Removed unnecessary styles and improved button click handling to prevent event propagation.
2025-03-30 14:32:57 +08:00
kangfenmao
b363cb06a4 chore(store): update migration logic and increment version to 87
* Updated migration functions to include error handling for provider additions.
* Incremented the version number in the persisted reducer configuration.
2025-03-30 14:08:14 +08:00
Hao He
9e977f4b35 feat: Add keyboard navigation and selection highlighting for AddAssistantPopup (#4022)
* feat(AddAssistantPopup): 添加键盘导航和选中项高亮功能

* feat(AddAssistantPopup): 为所有项添加相同宽度的透明边框,避免布局跳动。
2025-03-30 13:58:52 +08:00
Teo
00de616958 refactor(files): Reconstruct file system UI (#4100)
* refactor(files): Reconstruct file system UI

* refactor(knowledge): replace Card components with CustomCollapse for better UI structure

* refactor(files): update folder icon from FolderOpenOutlined to FolderOpenFilled

* feat(components): add CustomCollapse component for enhanced collapsible UI

* refactor(files): implement virtual scrolling in FileList and KnowledgeContent components
2025-03-30 13:56:34 +08:00
George·Dong
22b0bd54b4 refactor(settings): 重构小程序设置 (#4092) 2025-03-30 08:48:23 +08:00
kangfenmao
be39c5f40c feat(MCPService): enhance PATH management with platform-specific directories 2025-03-30 08:32:45 +08:00
kangfenmao
8b00ff4b93 fix(MCPSettings): ensure server name is set when missing and reorder radio options 2025-03-30 08:26:01 +08:00
Vaayne
f5b675b356 fix(MCPService): clear cache on server close and refactor tool fetching logic 2025-03-30 07:42:27 +08:00
LiuVaayne
de8dbb2646 fix(MCPService): prefix tool IDs with 'f' for consistency (#4121) 2025-03-30 00:26:47 +08:00
fullex
7e67005e70 fix(UI/markdown): markdown not recognized ** as emphasis marks in CJK (#4119) 2025-03-29 23:56:41 +08:00
yangtb2024
d6e66f3a4d feat(config): 增强模型支持 (#4085)
* feat(config): 添加对新模型的支持

- 新增 gemini-2.5 到 visionAllowedModels
- 新增 gpt-4.5 到 visionAllowedModels 和 FUNCTION_CALLING_MODELS
- 新增 o1 到 FUNCTION_CALLING_MODELS
- 从 visionExcludedModels 和 FUNCTION_CALLING_EXCLUDED_MODELS 中排除 o1-mini, o1-preview, AIDC-AI/Marco-o1

* feat(config): 添加对 deepseek-ai 函数调用的支持

- 新增 deepseek-ai 到 FUNCTION_CALLING_MODELS
2025-03-29 23:04:51 +08:00
one
e5aaec2129 fix: race condition in topic auto renaming 2025-03-29 22:58:38 +08:00
Herio
464634d051 feat(ApiCheckPopup): 使用Promise.all并行处理API验证请求并更新状态 2025-03-29 22:00:21 +08:00
fullex
3698238e9e fix: one-off minapp should not show minimize button 2025-03-29 21:59:03 +08:00
George·Dong
ae2a661201 fix(ApiService): context clear failed 2025-03-29 21:43:29 +08:00
Chen Tao
d6dbe357fb fix: add base url for gemini (#4109) 2025-03-29 21:35:04 +08:00
Catwine
e9dd795f9a docs(config): fix typo in electron-builder.yml 2025-03-29 21:08:55 +08:00
kangfenmao
03a18c1f3b fix(WebviewContainer): update webview partition to use a generic identifier 2025-03-29 19:00:08 +08:00
kangfenmao
e3ba44fc2c chore(version): 1.1.16 2025-03-29 15:29:28 +08:00
kangfenmao
9976ad9ed0 fix(migrate): add error handling to migration functions and ensure state integrity during updates 2025-03-29 15:28:57 +08:00
kangfenmao
3bb294e698 chore(version): 1.1.15 2025-03-29 15:00:02 +08:00
kangfenmao
990b1651a9 chore(version): 1.1.14 2025-03-29 08:05:56 +08:00
kangfenmao
11c070a1d7 feat(i18n): add delete server confirmation messages in multiple languages 2025-03-29 08:00:51 +08:00
fullex
57ba91072d feat: MinApp tabs on sidebar, we can keep MinApps alive and re-open it without loading again. 2025-03-29 07:29:45 +08:00
fullex
433d562599 refactor: MinAppType id required and only string 2025-03-29 07:29:45 +08:00
Chen Tao
194ba1baa0 feat: support gpt-4o image generation (#4054)
* feat: support gpt-4o image generation

* clean code
2025-03-29 07:18:42 +08:00
suyao
53ae427f2f feat(models): add support for new Gemini models 2025-03-29 07:17:40 +08:00
LiuVaayne
3f40cc28ac feat: mcp tools (#4069)
* feat(McpSettings): add MCP tools section and fetch tools on server activation

* refactor(McpService): improve client management and connection handling

* feat(McpService): add server management functions for restart and stop

* feat(McpTool): add tools section with input schema and availability messages

* feat(McpService): add unique IDs to tools and update function name mapping

* feat(McpService): implement caching for tool listings and enhance tool structure

* feat(McpToolsButton): streamline active server handling and update dropdown rendering

* fix(mcp-tools): update tool lookup to use unique IDs and add warning for missing tools
2025-03-29 07:16:59 +08:00
kangfenmao
d3584d2d39 chore(version): 1.1.13 2025-03-28 21:49:09 +08:00
kangfenmao
da0db73916 feat(Markdown): disallow iframe elements in Markdown rendering #4059 2025-03-28 21:46:37 +08:00
Teo
21f1b8b373 fix: fix fold selected (#4058)
* fix: 修复foldSelected问题

* refactor: 优化布局定位
2025-03-28 21:22:45 +08:00
fullex
f1a03916e7 remove unnecessary css 2025-03-28 18:11:21 +08:00
fullex
45f0bfa0f9 fix: code block selection abnormal
- the reason is using display: table/table-row, which makes the selection behavior become table style.
- use display: flex/block to solve this problem, meanwhile the line number css also modified to fit the adjust
2025-03-28 18:11:21 +08:00
kangfenmao
f2102daf00 feat(electron-builder): update release notes to include Nutstore login, SiYuan note export, and MCP improvements 2025-03-28 18:01:09 +08:00
kangfenmao
8f5c4483fc chore(version): 1.1.12 2025-03-28 15:17:33 +08:00
Chris Wan
43adac3f74 feat(MessagesService): two or more adjacent messages have the same role as user, then only the last one should be kept 2025-03-28 15:11:52 +08:00
ousugo
7b8c5f185c fix(TopicsTab): Topic prompts cannot be cleared 2025-03-28 15:09:01 +08:00
Teo
eeb537048b refactor(mcp settings): enhance NpxSearch component layout and styling (#4053)
* refactor: mcp setting ui refactor

* refactor(mcp settings): enhance NpxSearch component layout and styling
2025-03-28 15:08:24 +08:00
nutstore-dev
5712a58a5e fix(nutstore): fix the issue of not being able to customize the name of nutstore backup files. (#4050)
Co-authored-by: shlroland <shlroland1995@gmail.com>
2025-03-28 15:06:15 +08:00
kangfenmao
c4162bd9e3 feat(i18n): add "New Folder" button label to multiple locales 2025-03-28 13:42:14 +08:00
Teo
eddbae6f5e refactor: mcp setting ui refactor 2025-03-28 13:21:32 +08:00
kangfenmao
29f7da1a4c chore(version): 1.1.11 2025-03-28 11:30:20 +08:00
kangfenmao
403ed8cbf4 fix: mcp install ui 2025-03-28 11:15:49 +08:00
kangfenmao
7263a682b7 chore: remove useless code 2025-03-28 09:24:54 +08:00
kangfenmao
29b5ba787b refactor: mcp service 2025-03-28 04:24:10 +08:00
kangfenmao
bb6fdd2db7 Revert "feat: Use logo instead of avatar"
This reverts commit aee0f9ea3f.
2025-03-28 04:14:20 +08:00
Chen Tao
710171278a fix(Reranker): 修复rerank 400 and 完善错误信息 (#4013)
feat(Reranker): enhance error handling with detailed error messages and early return for empty results
2025-03-27 20:04:37 +08:00
MyPrototypeWhat
41191f6132 feat: mcp auto server (#3996)
* feat: add configuration file management to MCPService

- Introduced methods to ensure the existence of a configuration file, load configurations from it, and save server configurations.
- Updated the MCPService class to handle server configurations more effectively, improving initialization and error handling.
- Added dependency on chokidar for file system watching.

* feat: enhance MCPService configuration handling

- Improved configuration management by adding compatibility for both old and new server formats.
- Updated methods to ensure configuration file existence, load configurations, and save server data more effectively.
- Refined server initialization logic to handle updates and notifications to Redux more efficiently.
- Removed unnecessary waiting for server data from Redux during initialization.

* feat: enhance MCPService default configuration handling

- Added logic to create a default configuration if none exists, improving the initialization process.
- Implemented migration of server configurations from Redux to file, ensuring data consistency.
- Updated methods to handle nested server structures and improved error handling during server updates.

* refactor: clean up MCPService by removing redundant console logs and unused updateServerInRedux method

- Eliminated unnecessary console log statements to improve code readability.
- Removed the unused updateServerInRedux method, streamlining the MCPService class.
- Maintained existing functionality while enhancing code clarity.
2025-03-27 17:15:16 +08:00
kangfenmao
bbc7b20183 lint: fix code format 2025-03-27 15:15:15 +08:00
kangfenmao
8bb8081f31 feat(Messages): add foldSelected property to assistant messages for improved message handling 2025-03-27 15:15:01 +08:00
kangfenmao
7ddd2cb9d5 refactor(Messages): update message group styling and improve grouped message handling 2025-03-27 14:07:19 +08:00
kangfenmao
06ff44f97c refactor(Scrollbar, Tabs): simplify component structure and improve styling 2025-03-27 13:40:09 +08:00
kangfenmao
1a85b8bd5d feat(i18n): update assistant settings titles and add new translations for multiple languages 2025-03-27 13:19:04 +08:00
MyPrototypeWhat
fb9c23c500 Perf/optimize rendering (#3923)
* optimize useMessageOperations

* chore: update dependencies and refactor React imports

- Added @ant-design/v5-patch-for-react-19 and rc-virtual-list to package.json.
- Updated React and ReactDOM types to version 19 in package.json and yarn.lock.
- Refactored ReactDOM usage to createRoot in main.tsx for better compatibility with React 18+.
- Changed useContext to use in SyntaxHighlighterProvider and ThemeProvider components.
- Adjusted flex-direction in Messages components to column for improved layout.
- Removed unused state in CodeBlock component.

* refactor(Messages): enhance scrolling behavior and introduce scroll utilities

- Added createScrollHandler and scrollToBottom utilities for improved scroll management.
- Updated Messages component to utilize new scroll utilities for better user experience.
- Refactored scroll handling logic to ensure smooth scrolling when new messages are added.
- Changed containerRef type to HTMLElement for better type safety.

* refactor(Messages): streamline message handling and introduce useTopicMessages hook

- Removed direct message selection from useMessageOperations and created a new useTopicMessages hook for better separation of concerns.
- Updated Messages component to utilize the new useTopicMessages hook for fetching messages.
- Enhanced message display logic with computeDisplayMessages function for improved message rendering.
- Refactored scrolling behavior to maintain a smooth user experience during message updates.

* refactor(Message Operations): introduce useTopicLoading hook for improved loading state management

- Removed loading state from useMessageOperations and created a new useTopicLoading hook for better separation of concerns.
- Updated components to utilize the new useTopicLoading hook for fetching loading states related to topics.
- Enhanced code organization and readability by streamlining message operations and loading state handling.

* refactor(Messages): replace updateMessage with updateMessageThunk for improved async handling

- Updated useMessageOperations and MessageAnchorLine components to utilize updateMessageThunk for message updates.
- Enhanced error handling and database synchronization in the new thunk implementation.
- Streamlined message update logic to improve code clarity and maintainability.

* refactor(SyntaxHighlighterProvider, MessageTools, AddMcpServerPopup): update styles and improve type safety

- Changed import statements to use TypeScript's type imports for better clarity and type safety.
- Updated MessageTools and AddMcpServerPopup components to replace bodyStyle with styles prop for consistent styling approach.
- Enhanced overall code organization and maintainability by adhering to TypeScript best practices.

* refactor(Messages): update layout and remove unnecessary prop

- Removed the hasChildren prop from the Messages component for cleaner code.
- Adjusted flex-direction in the mini chat Messages component to column-reverse for improved layout consistency.

* refactor: enhance type safety and component return types

- Updated functional components to return React.ReactElement instead of JSX.Element for better type consistency.
- Changed import statements to use TypeScript's type imports for improved clarity.
- Initialized useRef hooks with null for better type safety in various components.
- Adjusted props types to use HTMLAttributes for more accurate type definitions.

* chore: update package dependencies

- Removed outdated dependencies: @agentic/exa, @agentic/searxng, @agentic/tavily, and rc-virtual-list.
- Added back @ant-design/v5-patch-for-react-19 and rc-virtual-list with specified versions for improved compatibility.

* fix(useMessageOperations): ensure message retrieval from store when updating content
2025-03-27 12:06:47 +08:00
ousugo
7fb85dc311 feat(message): calculate token usage when message content is updated 2025-03-27 08:32:46 +08:00
ousugo
2af15e4172 feat(models): add 'qwen2.5-omni' to allowed vision models 2025-03-27 08:31:58 +08:00
one
415f991143 fix: open 'data' page by default after routing to data settings 2025-03-26 22:15:27 +08:00
one
c162242433 fix(HealthCheck): exclude rerank models from being checked (#3969)
* fix(HealthCheck): exclude rerank models from being checked

* fix: info in en
2025-03-26 21:44:26 +08:00
OrzMiku
487d7a502e refactor(AgentCard): unify dropmenus 2025-03-26 21:17:24 +08:00
Sorades
d64d6969ae feat(message): 将fold display mode的状态持久化 2025-03-26 19:21:39 +08:00
kangfenmao
cc32c36222 build: replace @llm-tools/embedjs with @cherrystudio/embedjs 2025-03-26 18:14:04 +08:00
purefkh
0d320120a4 fix(UI): exclude rerank models from mention dropdown (#3958) 2025-03-26 16:17:07 +08:00
Yanhua Zheng
3cbe45fc8d docs(README): Add PaperMaterial Theme in README (#3955)
* Add PaperMaterial Theme

* Update README.md
2025-03-26 16:10:08 +08:00
africa1207
917943386e feat: 优化导出obsidian,自动选择库路径,不再需要手动配置 (#3854)
* feat: 优化导出obsidian,自动选择库路径,不再需要手动配置

* fix: eslint报错

* feat: 增加预设置默认仓库

* fix: 解决合并冲突
2025-03-25 21:31:22 +08:00
Teo
aee0f9ea3f feat: Use logo instead of avatar 2025-03-25 21:30:49 +08:00
kangfenmao
2055615aca feat: update model identifiers and names in configuration
- Changed model ID from 'mixtral-8x7b-32768' to 'mistral-saba-24b' and updated its name to 'Mistral Saba 24B'.
- Updated model ID from 'gemma-7b-it' to 'gemma-9b-it' and changed its name to 'Gemma 9B'.
- Enhanced clarity and consistency in model naming conventions.
2025-03-25 20:18:14 +08:00
kangfenmao
40cac47136 Revert "feat: support acrylic effect for Windows"
This reverts commit c8b2e8dd79.
2025-03-25 20:13:54 +08:00
kangfenmao
40d9629681 ci: fix eslint slow 2025-03-25 18:34:20 +08:00
kangfenmao
8acefaa907 feat: update migration for settings auto-check update
Incremented version to 85 and updated migration logic to transition from manual to automatic update checks in settings, enhancing user experience.
2025-03-25 13:16:18 +08:00
wangxiaolong
e2d8b89ffd feat: 更新自动检查更新功能易读性
将手动检查更新的设置更改为自动检查更新,更新相关的状态管理和界面文本,以提升用户体验。
2025-03-25 13:06:25 +08:00
d5v
8d48824981 feat: add Siyuan Note export functionality and configuration (#3845)
* feat(i18n): add Siyuan Note export functionality and configuration

- 增加导出到思源笔记。

* feat/Add document address

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-03-25 13:05:21 +08:00
nutstore-dev
fd66881022 feat: nutstore integration (#3461)
* feat(protocol): add custom protocol

* feat(webdav): add handler for checking webdav connection

* feat(webdav): abstract WebDAV modal components

* feat(nutstore): add nutstore sso

---------

Co-authored-by: shlroland <shlroland1995@gmail.com>
2025-03-25 11:40:11 +08:00
z-zeechung
b321169ca2 feat: Katex and MathJax blocks horizontally auto-scroll (#3806) 2025-03-25 09:44:35 +08:00
ousugo
123362b493 fix(thinking): Claude think label recognition error problem 2025-03-25 08:57:49 +08:00
Chen Tao
a1568808d4 feat(genmini): enhance (#3849) 2025-03-25 08:52:43 +08:00
RarityBrown
6dff8b2725 feat: Update text-based file extensions for EDAs
远期可以进一步考虑直接自动使用 https://github.com/github-linguist/linguist/blob/main/lib/linguist/languages.yml 加上自定义扩展集的方式,减轻维护负担

Should consider directly and automatically using https://github.com/github-linguist/linguist/blob/main/lib/linguist/languages.yml along with a custom extension set to reduce the maintenance burden.
2025-03-25 08:51:30 +08:00
Hakadao
c8b2e8dd79 feat: support acrylic effect for Windows 2025-03-25 08:50:24 +08:00
hobee
8ac18934e9 feat(主题): 添加settingTheme字段以增强主题切换功能
在ThemeProvider中添加settingTheme字段,用于在Sidebar组件中显示当前主题状态。这样用户可以更直观地了解当前主题设置
2025-03-25 08:50:03 +08:00
Teo
6699b0902f feat(pending-animation): 当消息处于后台pending时,助手头像跟话题显示脉冲动画效果 (#3867) 2025-03-25 08:48:26 +08:00
fullex
9b98312775 fix: some shortcuts not enabled 2025-03-25 00:48:35 +08:00
hobee
1e14dd6ea2 feat: 添加隐藏小程序功能
可以直接在小程序界面隐藏小程序
2025-03-25 00:32:39 +08:00
hobee
0d612cb827 feat(i18n): 在侧边栏添加隐藏小工具的翻译
为多种语言添加了“隐藏小工具”的翻译,以支持新的功能需求。
2025-03-25 00:32:39 +08:00
kangfenmao
ccfac25a04 feat(docs): add theme section to README files in multiple languages 2025-03-24 09:53:59 +08:00
kangfenmao
7447dfe771 feat(docs): add guide section and update contact email in README files 2025-03-24 09:41:47 +08:00
kangfenmao
0fe45a203c feat(docs): add Product Hunt badge to README files in multiple languages 2025-03-24 09:34:21 +08:00
kangfenmao
94942141b9 feat(docs): add contact information to README files in multiple languages 2025-03-24 09:31:41 +08:00
Asurada
f08856ae42 feat(SettingTab): add support for reasoning effort model check (#3842) 2025-03-24 09:23:32 +08:00
Pleasurecruise
a606f4b6c5 fix: apikey input flickering 2025-03-23 22:45:33 +08:00
kangfenmao
a5318ebefa chore(version): 1.1.10 2025-03-23 19:31:49 +08:00
Chen Tao
ae8869e1b6 feat(knowledge): support Voyage AI (#3810)
* feat(knowledge): support Voyage AI

* chore
2025-03-23 19:31:18 +08:00
kangfenmao
32b8fa7e63 fix(prompts): enhance FOOTNOTE_PROMPT for clarity and completeness
- Updated the FOOTNOTE_PROMPT to instruct the model to provide answers based on its knowledge when reference materials are irrelevant, ensuring responses are clearly structured and complete.
2025-03-23 14:25:04 +08:00
kangfenmao
c299d615fc fix(ApiService): remove quotes from message summaries
- Updated the fetchMessagesSummary function to remove all quotes from the generated summaries, ensuring cleaner output for the user.
2025-03-23 14:19:28 +08:00
kangfenmao
8628dc188b feat(i18n): add chat history localization for multiple languages
- Added chat history localization entries for English, Japanese, Russian, Simplified Chinese, Traditional Chinese, ensuring consistent user experience across languages.
- Removed redundant history entries from previous versions to streamline localization files.
2025-03-23 13:38:38 +08:00
africa1207
eba746a3bc feat:增加聊天记录流程图,方便查看 (#3772)
* feat: 聊天记录流程图

* fix: 修复偶尔多标签切换不滚动的问题
2025-03-23 13:35:16 +08:00
kangfenmao
640ca19cba refactor(ipc): simplify launch on boot handling and improve WindowService logic
- Updated the IPC handler for setting launch on boot to directly use the boolean parameter for openAtLogin.
- Cleaned up the WindowService logic to enhance readability and maintainability, including minor adjustments to the tray behavior on macOS.
2025-03-23 13:22:54 +08:00
kangfenmao
f9941a6858 feat(i18n): add machine translations for Greek, Spanish, French, and Portuguese
- Updated the translation script to output machine-generated translations for Greek, Spanish, French, and Portuguese.
- Adjusted file paths for translation outputs and ensured proper formatting in the translation prompts.
- Added a README file to indicate that the translations are machine-generated and should not be edited.
2025-03-22 23:41:17 +08:00
kangfenmao
17bd66259d chore(version): 1.1.9 2025-03-22 23:22:41 +08:00
kangfenmao
9112ecc79b fix: i18n check 2025-03-22 23:21:45 +08:00
kangfenmao
e75cfac8d8 chore: update package dependencies and clean up yarn.lock
- Reintroduced several dependencies in devDependencies that were previously removed from dependencies.
- Removed unused dependencies from both package.json and yarn.lock to streamline the project.
- Updated tslib version in yarn.lock to ensure compatibility.
2025-03-22 22:47:10 +08:00
kangfenmao
a9b2b32c5a feat: add GenerateImageButton component to Inputbar for image generation functionality 2025-03-22 22:45:45 +08:00
kangfenmao
16ac419b9b fix: i18n and lint 2025-03-22 22:31:43 +08:00
z-zeechung
13747a585a i18n: 新增自动i18n脚本。新增阿拉伯文,希腊文,西班牙文,法文,葡萄牙文翻译 (#3792)
* added auto i18n script. added arab, greek, spanish, france, portuguese translation

* remove arabian
2025-03-22 22:20:09 +08:00
Chen Tao
d56774fd59 fix(knowledge): show more info (#3790)
* feat: remove rerank model from embedded model

* feat: add rerank model info

* fix: embedding and rerank model end without `/v1` bug
2025-03-22 22:14:25 +08:00
Chen Tao
6c6af2a12b feat(provider): gemini-2.0-flash-exp image (#3421)
* feat: finish basic gemini-2.0-flash-exp generate image

* feat: support edit image

* chore

* fix: package https-proxy-agent-v5 version

* feat: throw finish message and add history messages

* feat: update generate image models

* chore

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-03-22 22:10:11 +08:00
one
ae7b94b01e feat: add a search bar to model list (#3788)
* feat: add a search bar to model list

* feat: make the search bar collapsible
2025-03-22 21:44:00 +08:00
kangfenmao
36824c20f8 fix: update placeholder text in localization files for tag input fields
- Removed unnecessary information about using pure numbers in tag placeholders across multiple languages.
2025-03-22 21:42:55 +08:00
one
07b6d5ce1d fix: make the setting icon style of non-system providers consistent with system providers 2025-03-22 19:44:39 +08:00
kangfenmao
43a6428653 refactor: enhance model tags and localization for new model types
- Added support for rerank models in ModelTags component.
- Updated localization keys for model types in various icon components.
- Modified SelectModelPopup to filter out rerank models appropriately.
- Improved model identification logic in models configuration.
- Enhanced UI elements to reflect updated model types and their respective tooltips.
2025-03-22 19:38:00 +08:00
kangfenmao
404ec095d4 refactor: update rerank model support and configuration
- Changed provider configuration in dev-app-update.yml to use GitHub.
- Added SUPPORTED_REANK_PROVIDERS constant to filter available rerank models.
- Updated tooltip messages in localization files to indicate supported providers.
- Enhanced AddKnowledgePopup and KnowledgeSettingsPopup components to display supported providers in the UI.
2025-03-22 16:30:54 +08:00
kangfenmao
ed731db56a fix: remove unnecessary dependency from useEffect in MessageAnchorLine component 2025-03-22 10:12:04 +08:00
Dawn-spring
1e4d6f196f refactor: 改进 Obsidian 导出,不再依赖 Obsidian 第三方插件 (#3637)
改进 Obsidian 导出,不在依赖 Obsidian 第三方插件
2025-03-22 10:11:49 +08:00
MyPrototypeWhat
e0f1768c4f refactor(NpxSearch): improve type safety and error handling
- Changed import of MCPServer to type import for better clarity.
- Enhanced error handling in async operations to manage unknown error types.
- Updated Table component to use ellipsis for long text in description and npm link.
- Adjusted column width for actions and ensured consistent styling in the component.
2025-03-22 10:09:40 +08:00
Xiangfang Chen
c9d640770a add GLM-4V-Flash Models. 2025-03-22 10:06:19 +08:00
ousugo
56207d5617 feat: add search input focus handling in EditModelsPopup 2025-03-22 10:05:34 +08:00
ousugo
2e2ed664d0 fix: update REASONING_REGEX to include 'hunyuan-t1' model 2025-03-22 10:04:57 +08:00
one
183f1310e5 fix: reset topicId for branched messages 2025-03-22 03:08:50 +08:00
one
b7ee0ea7b3 fix: take messages with empty tool_calls as normal messages 2025-03-22 00:30:12 +08:00
suyao
117cf548fe chore(ProxyManager): remove unnecessary console log 2025-03-22 00:28:07 +08:00
eeee0717
bddec81402 fix 2025-03-21 20:21:19 +08:00
one
a0ccc4e661 fix: use messagesRef to avoid empty new branch 2025-03-21 19:39:03 +08:00
fullex
25c166cb8e feat: Launch on boot, Minimize to tray on launch & on close / fix: Mac: don't show dock when close to tray (#2871)
* launch/tray feature enhance stashed

* feature: Issue #2754. launch on boot(win&mac, linux not supported now), min to tray when launch(not only boot), min to tray when close
bug-fix: Issue #2576. In Mac, if tray-on-close is set, MainWindow will not show on the dock when closed
bug-fix: MiniWindow will hide MainWindow when it shows first time and won't hide MainWindow later. The user will not open the MainWindow again if the tray is set to not show. The bug fixed by not hiding the MainWindow anytime the MiniWindow showed.

* migration version fix

* fix: enable universal shortcuts when launch to tray

*  feat: add Model Context Protocol (MCP) support (#2809)

*  feat: add Model Context Protocol (MCP) server configuration (main)

- Added `@modelcontextprotocol/sdk` dependency for MCP integration.
- Introduced MCP server configuration UI in settings with add, edit, delete, and activation functionalities.
- Created `useMCPServers` hook to manage MCP server state and actions.
- Added i18n support for MCP settings with translation keys.
- Integrated MCP settings into the application's settings navigation and routing.
- Implemented Redux state management for MCP servers.
- Updated `yarn.lock` with new dependencies and their resolutions.

* 🌟 feat: implement mcp service and integrate with ipc handlers

- Added `MCPService` class to manage Model Context Protocol servers.
- Implemented various handlers in `ipc.ts` for managing MCP servers including listing, adding, updating, deleting, and activating/deactivating servers.
- Integrated MCP related types into existing type declarations for consistency across the application.
- Updated `preload` to expose new MCP related APIs to the renderer process.
- Enhanced `MCPSettings` component to interact directly with the new MCP service for adding, updating, deleting servers and setting their active states.
- Introduced selectors in the MCP Redux slice for fetching active and all servers from the store.
- Moved MCP types to a centralized location in `@renderer/types` for reuse across different parts of the application.

* feat: enhance MCPService initialization to prevent recursive calls and improve error handling

* feat: enhance MCP integration by adding MCPTool type and updating related methods

* feat: implement streaming support for tool calls in OpenAIProvider and enhance message processing

* fix: finish_reason undefined

---------

Co-authored-by: LiuVaayne <10231735+vaayne@users.noreply.github.com>
Co-authored-by: kangfenmao <kangfenmao@qq.com>
2025-03-21 16:48:19 +08:00
one
bc1d6157f6 fix(HealthCheck): health checking local models without API keys 2025-03-21 16:27:07 +08:00
Teo
852274b4b1 feat(Messages): 新增消息锚点功能,在右侧显示消息大纲 (#3674)
* feat(Messages): 添加消息线和分页按钮的设置选项

* feat(Messages): 将“消息线”更名为“对话锚点”,并更新相关设置和国际化文本

* fix(Messages): 调整消息透明度和缩放效果,优化消息项的样式和交互

* feat(Messages): 添加容器高度自适应功能,优化消息线样式和交互效果

* fix(Messages): 调整消息容器高度计算和样式,优化交互效果

* feat(settings): 更新消息导航相关翻译和设置
2025-03-21 16:21:12 +08:00
自由的世界人
998c4bc459 fix: select and copy the translation part in the chat box (#3710)
* Update TranslatePage.tsx

* Update TranslatePage.tsx

我再也不点`webstorm`的quick fix了

* Update TranslatePage.tsx
2025-03-21 16:16:50 +08:00
ousugo
55bb4530c0 fix(MessageMenubar): handle assistant messages from reasoning models when copying and editing content 2025-03-21 16:16:10 +08:00
ousugo
27a384b0c8 feat(MessageMenubar): Automatically hide tooltip when secondary popups appear
- Introduced state management for tooltips related to regenerate and delete actions in the MessageMenubar component.
- Updated Tooltip components to control visibility based on user interactions.
2025-03-21 16:09:27 +08:00
SuYao
4927f98e59 revert(Proxy): remove proxyManager usage from multiple services (#3720)
* revert(Proxy): remove proxyManager usage from multiple services

* refactor(ProxyManager): streamline proxy configuration and management
2025-03-21 16:04:45 +08:00
kangfenmao
36966cfc14 refactor(Inputbar): reposition MentionModelsButton for improved accessibility
- Moved MentionModelsButton to a new position within the Inputbar component for better user experience.
- Ensured consistent functionality while enhancing the layout of the input toolbar.
2025-03-21 13:55:40 +08:00
kangfenmao
9ebc20882b feat: update WebDAV integration and dependencies
- Added 'webdav' to the list of plugins in electron.vite.config.ts.
- Upgraded 'webdav' package from version 4.11.4 to 5.8.0 in package.json and yarn.lock.
- Introduced a utility function for formatting file sizes in WebDavSettings component.
- Updated file size display logic to use the new formatting utility.
2025-03-21 13:53:01 +08:00
451 changed files with 87383 additions and 18374 deletions

View File

@@ -18,7 +18,9 @@ body:
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),没有找到类似的问题
- label: 的问题不是 [常见问题](https://github.com/CherryHQ/cherry-studio/issues/3881) 中的内容
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
@@ -48,8 +50,8 @@ body:
id: description
attributes:
label: 错误描述
description: 描述问题时请尽可能详细
placeholder: 告诉我们发生了什么...
description: 描述问题时请尽可能详细。请尽可能提供截图或屏幕录制,以帮助我们更好地理解问题。
placeholder: 告诉我们发生了什么...(记得附上截图/录屏,如果适用)
validations:
required: true
@@ -57,12 +59,14 @@ body:
id: reproduction
attributes:
label: 重现步骤
description: 提供详细的重现步骤,以便于我们可以准确地重现问题
description: 提供详细的重现步骤,以便于我们的开发人员可以准确地重现问题。请尽可能为每个步骤提供截图或屏幕录制。
placeholder: |
1. 转到 '...'
2. 点击 '....'
3. 向下滚动到 '....'
4. 看到错误
记得尽可能为每个步骤附上截图/录屏!
validations:
required: true

View File

@@ -6,8 +6,8 @@ body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out this bug report!
Before submitting this issue, please make sure that you have understood the [FAQ](https://docs.cherry-ai.com/question-contact/questions) and [Knowledge Science](https://docs.cherry-ai.com/question-contact/knowledge)
Thank you for taking the time to fill out this bug report!
Before submitting this issue, please make sure that you have understood the [FAQ](https://docs.cherry-ai.com/question-contact/questions) and [Knowledge Science](https://docs.cherry-ai.com/question-contact/knowledge)
- type: checkboxes
id: checklist
@@ -18,7 +18,9 @@ body:
options:
- label: I understand that issues are for feedback and problem solving, not for complaining in the comment section, and will provide as much information as possible to help solve the problem.
required: true
- label: I've looked at pinned issues and searched for existing [Open Issues](https://github.com/CherryHQ/cherry-studio/issues), [Closed Issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [Discussions](https://github.com/CherryHQ/cherry-studio/discussions), no similar issue or discussion was found.
- label: My issue is not listed in the [FAQ](https://github.com/CherryHQ/cherry-studio/issues/3881).
required: true
- label: I've looked at **pinned issues** and searched for existing [Open Issues](https://github.com/CherryHQ/cherry-studio/issues), [Closed Issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [Discussions](https://github.com/CherryHQ/cherry-studio/discussions), no similar issue or discussion was found.
required: true
- label: I've filled in short, clear headings so that developers can quickly identify a rough idea of what to expect when flipping through the list of issues. And not "a suggestion", "stuck", etc.
required: true

39
.github/workflows/issue-management.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: "Stale Issue Management"
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
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
jobs:
stale:
if: github.repository_owner == 'CherryHQ'
runs-on: ubuntu-latest
permissions:
actions: write # Workaround for https://github.com/actions/stale/issues/1090
issues: write
# Completely disable stalling for PRs
pull-requests: none
contents: none
steps:
- name: Close inactive issues
uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: ${{ env.daysBeforeStale }}
days-before-close: ${{ env.daysBeforeClose }}
stale-issue-label: "inactive"
stale-issue-message: |
This issue has been inactive for a prolonged period and will be closed automatically in ${{ env.daysBeforeClose }} days.
该问题已长时间处于闲置状态,${{ env.daysBeforeClose }} 天后将自动关闭。
exempt-issue-labels: "pending, Dev Team, enhancement"
days-before-pr-stale: -1 # Completely disable stalling for PRs
days-before-pr-close: -1 # Completely disable closing for PRs
# Temporary to reduce the huge issues number
operations-per-run: 100
debug-only: false

View File

@@ -76,7 +76,10 @@ jobs:
- name: Build Windows
if: matrix.os == 'windows-latest'
run: yarn build:win
run: |
yarn build:npm windows
yarn build:win:x64
yarn build:win:arm64
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}

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,16 +92,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:win
run: |
yarn build:npm windows
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: Release
uses: ncipollo/release-action@v1

9
.gitignore vendored
View File

@@ -35,7 +35,6 @@ Thumbs.db
node_modules
dist
out
build/icons
stats.html
# ENV
@@ -47,3 +46,11 @@ local
.aider*
.cursorrules
.cursor/rules
# test
coverage
.vitest-cache
vitest.config.*.timestamp-*
# Sentry Config File
.env.sentry-build-plugin

View File

@@ -6,3 +6,4 @@ tsconfig.json
tsconfig.*.json
CHANGELOG*.md
agents.json
src/renderer/src/integration/nutstore/sso/lib

10
.vscode/settings.json vendored
View File

@@ -31,5 +31,13 @@
"[markdown]": {
"files.trimTrailingWhitespace": false
},
"i18n-ally.localesPaths": ["src/renderer/src/i18n"]
"i18n-ally.localesPaths": ["src/renderer/src/i18n/locales"],
"i18n-ally.enabledFrameworks": ["react-i18next", "i18next"],
"i18n-ally.keystyle": "nested", // 翻译路径格式
"i18n-ally.sortKeys": true, // 排序
"i18n-ally.namespace": true, // 开启命名空间
"i18n-ally.enabledParsers": ["ts", "js", "json"], // 解析语言
"i18n-ally.sourceLanguage": "en-us", // 翻译源语言
"i18n-ally.displayLanguage": "zh-cn",
"i18n-ally.fullReloadOnChanged": true // 界面显示语言
}

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +0,0 @@
diff --git a/src/markdown-loader.js b/src/markdown-loader.js
index eaf30b114a273e68abbb92c8b07018495e63f4cb..4b06519bdb51845e4693fe877da9de01c7a81039 100644
--- a/src/markdown-loader.js
+++ b/src/markdown-loader.js
@@ -21,7 +21,7 @@ export class MarkdownLoader extends BaseLoader {
? (await getSafe(this.filePathOrUrl, { format: 'buffer' })).body
: await streamToBuffer(fs.createReadStream(this.filePathOrUrl));
this.debug('MarkdownLoader stream created');
- const result = micromark(buffer, { extensions: [gfm(), mdxJsx()], htmlExtensions: [gfmHtml()] });
+ const result = micromark(buffer, { extensions: [gfm()], htmlExtensions: [gfmHtml()] });
this.debug('Markdown parsed...');
const webLoader = new WebLoader({
urlOrContent: result,

View File

@@ -1,158 +0,0 @@
diff --git a/src/loaders/local-path-loader.d.ts b/src/loaders/local-path-loader.d.ts
index 48c20e68c469cd309be2dc8f28e44c1bd04a26e9..1c16d83bcbf9b7140292793d6cbb8c04281949d9 100644
--- a/src/loaders/local-path-loader.d.ts
+++ b/src/loaders/local-path-loader.d.ts
@@ -4,8 +4,10 @@ export declare class LocalPathLoader extends BaseLoader<{
}> {
private readonly debug;
private readonly path;
- constructor({ path }: {
+ constructor({ path, chunkSize, chunkOverlap }: {
path: string;
+ chunkSize?: number;
+ chunkOverlap?: number;
});
getUnfilteredChunks(): AsyncGenerator<{
metadata: {
diff --git a/src/loaders/local-path-loader.js b/src/loaders/local-path-loader.js
index 4cf8a6bd1d890244c8ec49d4a05ee3bd58861c79..ec8215b01195a21ef20f3c5d56ecc99f186bb596 100644
--- a/src/loaders/local-path-loader.js
+++ b/src/loaders/local-path-loader.js
@@ -8,8 +8,8 @@ import { BaseLoader } from '@llm-tools/embedjs-interfaces';
export class LocalPathLoader extends BaseLoader {
debug = createDebugMessages('embedjs:loader:LocalPathLoader');
path;
- constructor({ path }) {
- super(`LocalPathLoader_${md5(path)}`, { path });
+ constructor({ path, chunkSize, chunkOverlap }) {
+ super(`LocalPathLoader_${md5(path)}`, { path }, chunkSize ?? 1000, chunkOverlap ?? 0);
this.path = path;
}
async *getUnfilteredChunks() {
@@ -36,10 +36,12 @@ export class LocalPathLoader extends BaseLoader {
const extension = currentPath.split('.').pop().toLowerCase();
if (extension === 'md' || extension === 'mdx')
mime = 'text/markdown';
+ if (extension === 'txt')
+ mime = 'text/plain';
this.debug(`File '${this.path}' mime type updated to 'text/markdown'`);
}
try {
- const loader = await createLoaderFromMimeType(currentPath, mime);
+ const loader = await createLoaderFromMimeType(currentPath, mime, this.chunkSize, this.chunkOverlap);
for await (const result of await loader.getUnfilteredChunks()) {
yield {
pageContent: result.pageContent,
diff --git a/src/util/mime.d.ts b/src/util/mime.d.ts
index 57f56a1b8edc98366af9f84d671676c41c2f01ca..14be3b5727cff6eb1978838045e9a788f8f53bfb 100644
--- a/src/util/mime.d.ts
+++ b/src/util/mime.d.ts
@@ -1,2 +1,2 @@
import { BaseLoader } from '@llm-tools/embedjs-interfaces';
-export declare function createLoaderFromMimeType(loaderData: string, mimeType: string): Promise<BaseLoader>;
+export declare function createLoaderFromMimeType(loaderData: string, mimeType: string, chunkSize?: number, chunkOverlap?: number): Promise<BaseLoader>;
diff --git a/src/util/mime.js b/src/util/mime.js
index b6426a859968e2bf6206795f70333e90ae27aeb7..16ae2adb863f8d7abfa757f1c5cc39f6bb1c44fa 100644
--- a/src/util/mime.js
+++ b/src/util/mime.js
@@ -1,7 +1,9 @@
import mime from 'mime';
import createDebugMessages from 'debug';
import { TextLoader } from '../loaders/text-loader.js';
-export async function createLoaderFromMimeType(loaderData, mimeType) {
+import fs from 'node:fs'
+
+export async function createLoaderFromMimeType(loaderData, mimeType, chunkSize, chunkOverlap) {
createDebugMessages('embedjs:util:createLoaderFromMimeType')(`Incoming mime type '${mimeType}'`);
switch (mimeType) {
case 'application/msword':
@@ -10,7 +12,7 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load docx files');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported DocxLoader');
- return new DocxLoader({ filePathOrUrl: loaderData });
+ return new DocxLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
case 'application/vnd.ms-excel':
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': {
@@ -18,21 +20,21 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load excel files');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported ExcelLoader');
- return new ExcelLoader({ filePathOrUrl: loaderData });
+ return new ExcelLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
case 'application/pdf': {
const { PdfLoader } = await import('@llm-tools/embedjs-loader-pdf').catch(() => {
throw new Error('Package `@llm-tools/embedjs-loader-pdf` needs to be installed to load PDF files');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported PdfLoader');
- return new PdfLoader({ filePathOrUrl: loaderData });
+ return new PdfLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': {
const { PptLoader } = await import('@llm-tools/embedjs-loader-msoffice').catch(() => {
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load pptx files');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported PptLoader');
- return new PptLoader({ filePathOrUrl: loaderData });
+ return new PptLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
case 'text/plain': {
const fineType = mime.getType(loaderData);
@@ -42,24 +44,24 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
throw new Error('Package `@llm-tools/embedjs-loader-csv` needs to be installed to load CSV files');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported CsvLoader');
- return new CsvLoader({ filePathOrUrl: loaderData });
+ return new CsvLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
- else
- return new TextLoader({ text: loaderData });
+ const content = fs.readFileSync(loaderData, 'utf-8');
+ return new TextLoader({ text: content, chunkSize, chunkOverlap });
}
case 'application/csv': {
const { CsvLoader } = await import('@llm-tools/embedjs-loader-csv').catch(() => {
throw new Error('Package `@llm-tools/embedjs-loader-csv` needs to be installed to load CSV files');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported CsvLoader');
- return new CsvLoader({ filePathOrUrl: loaderData });
+ return new CsvLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
case 'text/html': {
const { WebLoader } = await import('@llm-tools/embedjs-loader-web').catch(() => {
throw new Error('Package `@llm-tools/embedjs-loader-web` needs to be installed to load web documents');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported WebLoader');
- return new WebLoader({ urlOrContent: loaderData });
+ return new WebLoader({ urlOrContent: loaderData, chunkSize, chunkOverlap });
}
case 'text/xml': {
const { SitemapLoader } = await import('@llm-tools/embedjs-loader-sitemap').catch(() => {
@@ -67,14 +69,14 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported SitemapLoader');
if (await SitemapLoader.test(loaderData)) {
- return new SitemapLoader({ url: loaderData });
+ return new SitemapLoader({ url: loaderData, chunkSize, chunkOverlap });
}
//This is not a Sitemap but is still XML
const { XmlLoader } = await import('@llm-tools/embedjs-loader-xml').catch(() => {
throw new Error('Package `@llm-tools/embedjs-loader-xml` needs to be installed to load XML documents');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported XmlLoader');
- return new XmlLoader({ filePathOrUrl: loaderData });
+ return new XmlLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
case 'text/x-markdown':
case 'text/markdown': {
@@ -82,7 +84,7 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
throw new Error('Package `@llm-tools/embedjs-loader-markdown` needs to be installed to load markdown files');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported MarkdownLoader');
- return new MarkdownLoader({ filePathOrUrl: loaderData });
+ return new MarkdownLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
case 'image/png':
case 'image/jpeg': {

View File

@@ -1,26 +0,0 @@
diff --git a/dist/cjs/client/stdio.js b/dist/cjs/client/stdio.js
index 2ada8771c5f76673b5021d6453c6bdd7e0b88013..89f6ea9ca7de86294d3d966f3454b98e19bfe534 100644
--- a/dist/cjs/client/stdio.js
+++ b/dist/cjs/client/stdio.js
@@ -68,7 +68,7 @@ class StdioClientTransport {
this._process = (0, node_child_process_1.spawn)(this._serverParams.command, (_a = this._serverParams.args) !== null && _a !== void 0 ? _a : [], {
env: (_b = this._serverParams.env) !== null && _b !== void 0 ? _b : getDefaultEnvironment(),
stdio: ["pipe", "pipe", (_c = this._serverParams.stderr) !== null && _c !== void 0 ? _c : "inherit"],
- shell: false,
+ shell: process.platform === 'win32' ? true : false,
signal: this._abortController.signal,
windowsHide: node_process_1.default.platform === "win32" && isElectron(),
cwd: this._serverParams.cwd,
diff --git a/dist/esm/client/stdio.js b/dist/esm/client/stdio.js
index 387c982fd40fd8db9790a78e1a05c9ecb81501c0..7b7e60a306bca73149609015a27e904a0a68ca02 100644
--- a/dist/esm/client/stdio.js
+++ b/dist/esm/client/stdio.js
@@ -61,7 +61,7 @@ export class StdioClientTransport {
this._process = spawn(this._serverParams.command, (_a = this._serverParams.args) !== null && _a !== void 0 ? _a : [], {
env: (_b = this._serverParams.env) !== null && _b !== void 0 ? _b : getDefaultEnvironment(),
stdio: ["pipe", "pipe", (_c = this._serverParams.stderr) !== null && _c !== void 0 ? _c : "inherit"],
- shell: false,
+ shell: process.platform === 'win32' ? true : false,
signal: this._abortController.signal,
windowsHide: process.platform === "win32" && isElectron(),
cwd: this._serverParams.cwd,

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,17 @@
diff --git a/index.js b/index.js
index 4e8423491ab51a9eb9fee22182e4ea0fcc3d3d3b..2846c5d4354c130d478dc99565b3ecd6d85b7d2e 100644
--- a/index.js
+++ b/index.js
@@ -19,7 +19,11 @@ function requireNative() {
break;
}
}
- return require(`@libsql/${target}`);
+ if (target === "win32-arm64-msvc") {
+ return require(`@strongtz/win32-arm64-msvc`);
+ } else {
+ return require(`@libsql/${target}`);
+ }
}
const {

View File

@@ -1,8 +1,8 @@
diff --git a/core.js b/core.js
index e75a18281ce8f051990c5a50bc1076afdddf91a3..e62f796791a155f23d054e74a429516c14d6e11b 100644
index ebb071d31cd5a14792b62814df072c5971e83300..31e1062d4a7f2422ffec79cf96a35dbb69fe89cb 100644
--- a/core.js
+++ b/core.js
@@ -156,7 +156,7 @@ class APIClient {
@@ -157,7 +157,7 @@ class APIClient {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': this.getUserAgent(),
@@ -12,10 +12,10 @@ index e75a18281ce8f051990c5a50bc1076afdddf91a3..e62f796791a155f23d054e74a429516c
};
}
diff --git a/core.mjs b/core.mjs
index fcef58eb502664c41a77483a00db8adaf29b2817..18c5d6ed4be86b3640931277bdc27700006764d7 100644
index 9c1a0264dcd73a85de1cf81df4efab9ce9ee2ab7..33f9f1f237f2eb2667a05dae1a7e3dc916f6bfff 100644
--- a/core.mjs
+++ b/core.mjs
@@ -149,7 +149,7 @@ export class APIClient {
@@ -150,7 +150,7 @@ export class APIClient {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': this.getUserAgent(),

View File

@@ -0,0 +1,32 @@
diff --git a/dist/index.js b/dist/index.js
index 663919ac5bb4f9147c5c1b09bd2e379586266a4b..88ff8873ac5beb5eb293f7e741a92fb15b00960c 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -20,21 +20,21 @@ function getSystemProxy() {
else if (process.platform === 'darwin') {
const proxySettings = yield mac_system_proxy_1.getMacSystemProxy();
const noProxy = proxySettings.ExceptionsList || [];
- if (proxySettings.HTTPSEnable && proxySettings.HTTPSProxy && proxySettings.HTTPSPort) {
+ if (proxySettings.HTTPEnable && proxySettings.HTTPProxy && proxySettings.HTTPPort) {
return {
- proxyUrl: `https://${proxySettings.HTTPSProxy}:${proxySettings.HTTPSPort}`,
+ proxyUrl: `http://${proxySettings.HTTPProxy}:${proxySettings.HTTPPort}`,
noProxy
};
}
- else if (proxySettings.HTTPEnable && proxySettings.HTTPProxy && proxySettings.HTTPPort) {
+ else if (proxySettings.SOCKSEnable && proxySettings.SOCKSProxy && proxySettings.SOCKSPort) {
return {
- proxyUrl: `http://${proxySettings.HTTPProxy}:${proxySettings.HTTPPort}`,
+ proxyUrl: `socks://${proxySettings.SOCKSProxy}:${proxySettings.SOCKSPort}`,
noProxy
};
}
- else if (proxySettings.SOCKSEnable && proxySettings.SOCKSProxy && proxySettings.SOCKSPort) {
+ else if (proxySettings.HTTPSEnable && proxySettings.HTTPSProxy && proxySettings.HTTPSPort) {
return {
- proxyUrl: `socks://${proxySettings.SOCKSProxy}:${proxySettings.SOCKSPort}`,
+ proxyUrl: `http://${proxySettings.HTTPSProxy}:${proxySettings.HTTPSPort}`,
noProxy
};
}

View File

@@ -0,0 +1,18 @@
diff --git a/dist/index.node.js b/dist/index.node.js
index bb108cbc210af5b99e864fd1dd8c555e948ecf7a..8ef8c1aab59215c21d161c0e52125724528ecab8 100644
--- a/dist/index.node.js
+++ b/dist/index.node.js
@@ -1,8 +1,11 @@
let crypto;
crypto =
globalThis.crypto?.webcrypto ?? // Node.js 16 REPL has globalThis.crypto as node:crypto
- globalThis.crypto ?? // Node.js 18+
- (await import("node:crypto")).webcrypto; // Node.js 16 non-REPL
+ globalThis.crypto ?? // Node.js 18+
+ (async() => {
+ const crypto = await import("node:crypto");
+ return crypto.webcrypto;
+ })();
/**
* Creates an array of length `size` of random bytes
* @param size

File diff suppressed because one or more lines are too long

111
LICENSE
View File

@@ -1,62 +1,87 @@
**许可协议**
**许可协议 (Licensing)**
软件采用 Apache License 2.0 许可。除 Apache License 2.0 规定的条款外,您在使用 Cherry Studio 时还应遵守以下附加条款:
项目采用**区分用户的双重许可 (User-Segmented Dual Licensing)** 模式。
**一. 商用许可**
**核心原则:**
在以下任何一种情况下,您需要联系我们并获得明确的书面商业授权后,方可继续使用 Cherry Studio 材料:
* **个人用户 和 10人及以下企业/组织:** 默认适用 **GNU Affero 通用公共许可证 v3.0 (AGPLv3)**。
* **超过10人的企业/组织:** **必须** 获取 **商业许可证 (Commercial License)**。
1. **修改与衍生** 您对 Cherry Studio 材料进行修改或基于其进行衍生开发包括但不限于修改应用名称、Logo、代码、功能、界面数据等
2. **企业服务** 在您的企业内部,或为企业客户提供基于 CherryStudio 的服务,且该服务支持 10 人及以上累计用户使用
3. **硬件捆绑销售** 您将 Cherry Studio 预装或集成到硬件设备或产品中进行捆绑销售。
4. **政府或教育机构大规模采购** 您的使用场景属于政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。
5. **面向公众的公有云服务**:基于 Cherry Studio提供面向公众的公有云服务。
**二. 贡献者协议**
作为 Cherry Studio 的贡献者,您应当同意以下条款:
1. **许可调整**:生产者有权根据需要对开源协议进行调整,使其更加严格或宽松。
2. **商业用途**:您贡献的代码可能会被用于商业用途,包括但不限于云业务运营。
**三. 其他条款**
1. 本协议条款的解释权归 Cherry Studio 开发者所有。
2. 本协议可能根据实际情况进行更新,更新时将通过本软件通知用户。
如有任何问题或需申请商业授权,请联系 Cherry Studio 开发团队。
除上述特定条件外,其他所有权利和限制均遵循 Apache License 2.0。有关 Apache License 2.0 的详细信息,请访问 http://www.apache.org/licenses/LICENSE-2.0。
定义“10人及以下”
指在您的组织(包括公司、非营利组织、政府机构、教育机构等任何实体)中,能够访问、使用或以任何方式直接或间接受益于本软件(Cherry Studio功能的个人总数不超过10人。这包括但不限于开发者、测试人员、运营人员、最终用户、通过集成系统间接使用者等
---
**1. 开源许可证 (Open Source License): AGPLv3 - 适用于个人及10人及以下组织**
**License Agreement**
* 如果您是个人用户或者您的组织满足上述“10人及以下”的定义您可以在 **AGPLv3** 的条款下自由使用、修改和分发 Cherry Studio。AGPLv3 的完整文本可以访问 [https://www.gnu.org/licenses/agpl-3.0.html](https://www.gnu.org/licenses/agpl-3.0.html) 获取。
* **核心义务:** AGPLv3 的一个关键要求是,如果您修改了 Cherry Studio 并通过网络提供服务,或者分发了修改后的版本,您必须以 AGPLv3 许可证向接收者提供相应的**完整源代码**。即使您符合“10人及以下”的标准如果您希望避免此源代码公开义务您也需要考虑获取商业许可证见下文
* 使用前请务必仔细阅读并理解 AGPLv3 的所有条款。
This software is licensed under the Apache License 2.0. In addition to the terms stipulated by the Apache License 2.0, you must comply with the following supplementary terms when using Cherry Studio:
**2. 商业许可证 (Commercial License) - 适用于超过10人的组织或希望规避 AGPLv3 义务的用户**
**I. Commercial Licensing**
* **强制要求:** 如果您的组织**不**满足上述“10人及以下”的定义即有11人或更多人可以访问、使用或受益于本软件您**必须**联系我们获取并签署一份商业许可证才能使用 Cherry Studio。
* **自愿选择:** 即使您的组织满足“10人及以下”的条件但如果您的使用场景**无法满足 AGPLv3 的条款要求**(特别是关于**源代码公开**的义务),或者您需要 AGPLv3 **未提供**的特定商业条款(如保证、赔偿、无 Copyleft 限制等),您也**必须**联系我们获取并签署一份商业许可证。
* **需要商业许可证的常见情况包括(但不限于):**
* 您的组织规模超过10人。
* (无论组织规模)您希望分发修改过的 Cherry Studio 版本,但**不希望**根据 AGPLv3 公开您修改部分的源代码。
* (无论组织规模)您希望基于修改过的 Cherry Studio 提供网络服务SaaS但**不希望**根据 AGPLv3 向服务使用者提供修改后的源代码。
* (无论组织规模)您的公司政策、客户合同或项目要求不允许使用 AGPLv3 许可的软件,或要求闭源分发及保密。
* 商业许可证将为您提供豁免 AGPLv3 义务(如源代码公开)的权利,并可能包含额外的商业保障条款。
* **获取商业许可:** 请通过邮箱 **bd@cherry-ai.com** 联系 Cherry Studio 开发团队洽谈商业授权事宜。
You must contact us and obtain explicit written commercial authorization to continue using Cherry Studio materials under any of the following circumstances:
**3. 贡献 (Contributions)**
1. **Modifications and Derivatives:** You modify Cherry Studio materials or perform derivative development based on them (including but not limited to changing the applications name, logo, code, functionality, user interface, data, etc.).
2. **Enterprise Services:** 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.
3. **Hardware Bundling and Sales:** You pre-install or integrate Cherry Studio into hardware devices or products for bundled sale.
4. **Large-scale Procurement by Government or Educational Institutions:** 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.
5. **Public Cloud Services:** You provide public cloud-based product services utilizing Cherry Studio.
* 我们欢迎社区对 Cherry Studio 的贡献。所有向本项目提交的贡献都将被视为在 **AGPLv3** 许可证下提供。
* 通过向本项目提交贡献(例如通过 Pull Request即表示您同意您的代码以 AGPLv3 许可证授权给本项目及所有后续使用者(无论这些使用者最终遵循 AGPLv3 还是商业许可)。
* 您也理解并同意,您的贡献可能会被包含在根据商业许可证分发的 Cherry Studio 版本中。
**II. Contributor Agreement**
**4. 其他条款 (Other Terms)**
As a contributor to Cherry Studio, you must agree to the following terms:
* 关于商业许可证的具体条款和条件,以双方签署的正式商业许可协议为准。
* 项目维护者保留根据需要更新本许可政策(包括用户规模定义和阈值)的权利。相关更新将通过项目官方渠道(如代码仓库、官方网站)进行通知。
1. **License Adjustments:** The producer reserves the right to adjust the open-source license as necessary, making it more strict or permissive.
2. **Commercial Usage:** Your contributed code may be used commercially, including but not limited to cloud business operations.
---
**III. Other Terms**
**Licensing**
1. Cherry Studio developers reserve the right of final interpretation of these agreement terms.
2. This agreement may be updated according to practical circumstances, and users will be notified of updates through this software.
This project employs a **User-Segmented Dual Licensing** model.
If you have any questions or need to apply for commercial authorization, please contact the Cherry Studio development team.
**Core Principle:**
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 http://www.apache.org/licenses/LICENSE-2.0.
* **Individual Users and Organizations with 10 or Fewer Individuals:** Governed by default under the **GNU Affero General Public License v3.0 (AGPLv3)**.
* **Organizations with More Than 10 Individuals:** **Must** obtain a **Commercial License**.
Definition: "10 or Fewer Individuals"
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.
---
**1. Open Source License: AGPLv3 - For Individuals and Organizations of 10 or Fewer**
* 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 **AGPLv3**. The full text of the AGPLv3 can be found in the LICENSE file at [https://www.gnu.org/licenses/agpl-3.0.html](https://www.gnu.org/licenses/agpl-3.0.html).
* **Core Obligation:** 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 **complete corresponding source code** 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).
* Please read and understand the full terms of the AGPLv3 carefully before use.
**2. Commercial License - For Organizations with More Than 10 Individuals, or Users Needing to Avoid AGPLv3 Obligations**
* **Mandatory Requirement:** If your organization does **not** meet the "10 or Fewer Individuals" definition above (i.e., 11 or more individuals can access, use, or benefit from the software), you **must** contact us to obtain and execute a Commercial License to use Cherry Studio.
* **Voluntary Option:** Even if your organization meets the "10 or Fewer Individuals" condition, if your intended use case **cannot comply with the terms of the AGPLv3** (particularly the obligations regarding **source code disclosure**), or if you require specific commercial terms **not offered** by the AGPLv3 (such as warranties, indemnities, or freedom from copyleft restrictions), you also **must** contact us to obtain and execute a Commercial License.
* **Common scenarios requiring a Commercial License include (but are not limited to):**
* Your organization has more than 10 individuals who can access, use, or benefit from the software.
* (Regardless of organization size) You wish to distribute a modified version of Cherry Studio but **do not want** to disclose the source code of your modifications under AGPLv3.
* (Regardless of organization size) You wish to provide a network service (SaaS) based on a modified version of Cherry Studio but **do not want** to provide the modified source code to users of the service under AGPLv3.
* (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.
* The Commercial License grants you rights exempting you from AGPLv3 obligations (like source code disclosure) and may include additional commercial assurances.
* **Obtaining a Commercial License:** Please contact the Cherry Studio development team via email at **bd@cherry-ai.com** to discuss commercial licensing options.
**3. Contributions**
* We welcome community contributions to Cherry Studio. All contributions submitted to this project are considered to be offered under the **AGPLv3** license.
* 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).
* You also understand and agree that your contribution may be included in distributions of Cherry Studio offered under our commercial license.
**4. Other Terms**
* The specific terms and conditions of the Commercial License are governed by the formal commercial license agreement signed by both parties.
* 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).

View File

@@ -6,26 +6,29 @@
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="./docs/README.ja.md">日本語</a><br></p>
<div align="center">
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
# 🍒 Cherry Studio
Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux.
👏 Join [Telegram Group](https://t.me/CherryStudioAI)[Discord](https://discord.gg/wez8HtpxqQ) | [QQ Group(472019156)](https://qm.qq.com/q/CbZiBWwCXu)
👏 Join [Telegram Group](https://t.me/CherryStudioAI)[Discord](https://discord.gg/wez8HtpxqQ) | [QQ Group(575014769)](https://qm.qq.com/q/lo0D4qVZKi)
❤️ Like Cherry Studio? Give it a star 🌟 or [Sponsor](docs/sponsor.md) to support the development!
# 📖 Guide
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
@@ -77,6 +80,16 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
- [ ] Voice input and output (AI call)
- [ ] Data backup supports custom backup content
# 🌈 Theme
- Theme Gallery: https://cherrycss.com
- Aero Theme: https://github.com/hakadao/CherryStudio-Aero
- PaperMaterial Theme: https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
- Claude dynamic-style: https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
- Maple Neon Theme: https://github.com/BoningtonChen/CherryStudio_themes
Welcome PR for more themes
# 🖥️ Develop
Refer to the [development documentation](docs/dev.md)
@@ -119,11 +132,7 @@ Thank you for your support and contributions!
# 🌐 Community
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:kangfenmao@gmail.com) | [Twitter](https://x.com/kangfenmao)
# 📣 Product Hunt
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
# ☕ Sponsor
@@ -133,6 +142,10 @@ Thank you for your support and contributions!
[LICENSE](./LICENSE)
# ✉️ Contact
yinsenho@cherry-ai.com
# ⭐️ Star History
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)

BIN
build/icons/1024x1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
build/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
build/icons/16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

BIN
build/icons/24x24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
build/icons/256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
build/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
build/icons/48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
build/icons/512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
build/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -5,4 +5,4 @@
# repo: cherry-studio
# owner: kangfenmao
provider: generic
url: https://cherrystudio.ocool.online
url: https://releases.cherry-ai.com

View File

@@ -8,25 +8,28 @@
</p>
<div align="center">
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
# 🍒 Cherry Studio
Cherry Studio は、複数の LLM プロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linux で利用可能です。
👏 [Telegram](https://t.me/CherryStudioAI)[Discord](https://discord.gg/wez8HtpxqQ) | [QQグループ(472019156)](https://qm.qq.com/q/CbZiBWwCXu)
👏 [Telegram](https://t.me/CherryStudioAI)[Discord](https://discord.gg/wez8HtpxqQ) | [QQグループ(575014769)](https://qm.qq.com/q/lo0D4qVZKi)
❤️ Cherry Studio をお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!❤️
# 📖 ガイド
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 など
@@ -78,6 +81,16 @@ Cherry Studio は、複数の LLM プロバイダーをサポートするデス
- [ ] 音声入出力AI コール)
- [ ] データバックアップはカスタムバックアップコンテンツをサポート
# 🌈 テーマ
- テーマギャラリー: https://cherrycss.com
- Aero テーマ: https://github.com/hakadao/CherryStudio-Aero
- PaperMaterial テーマ: https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
- Claude テーマ: https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
- メープルネオンテーマ: https://github.com/BoningtonChen/CherryStudio_themes
より多くのテーマのPRを歓迎します
# 🖥️ 開発
参考[開発ドキュメント](dev.md)
@@ -117,11 +130,7 @@ Cherry Studio への貢献を歓迎します!以下の方法で貢献できま
# コミュニティ
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:kangfenmao@gmail.com) | [Twitter](https://x.com/kangfenmao)
# 📣 プロダクトハント
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
# スポンサー
@@ -131,6 +140,10 @@ Cherry Studio への貢献を歓迎します!以下の方法で貢献できま
[LICENSE](../LICENSE)
# ✉️ お問い合わせ
yinsenho@cherry-ai.com
# ⭐️ スター履歴
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)

View File

@@ -7,25 +7,29 @@
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="./README.ja.md">日本語</a><br></p>
<div align="center">
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
# 🍒 Cherry Studio
Cherry Studio 是一款支持多个大语言模型LLM服务商的桌面客户端兼容 Windows、Mac 和 Linux 系统。
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)[Discord](https://discord.gg/wez8HtpxqQ) | [QQ群(472019156)](https://qm.qq.com/q/CbZiBWwCXu)
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)[Discord](https://discord.gg/wez8HtpxqQ) | [QQ群(575014769)](https://qm.qq.com/q/lo0D4qVZKi)
❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️
# 📖 使用教程
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、硅基流动等
@@ -77,6 +81,16 @@ Cherry Studio 是一款支持多个大语言模型LLM服务商的桌面客
- [ ] 语音输入输出AI 通话)
- [ ] 数据备份支持自定义备份内容
# 🌈 主题
- 主题库https://cherrycss.com
- Aero 主题https://github.com/hakadao/CherryStudio-Aero
- PaperMaterial 主题: https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
- 仿Claude 主题: https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
- 霓虹枫叶字体主题: https://github.com/BoningtonChen/CherryStudio_themes
欢迎 PR 更多主题
# 🖥️ 开发
参考[开发文档](dev.md)
@@ -117,11 +131,7 @@ Cherry Studio 是一款支持多个大语言模型LLM服务商的桌面客
# 🌐 社区
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:kangfenmao@gmail.com) | [Twitter](https://x.com/kangfenmao)
# 📣 产品猎人
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
# ☕ 赞助
@@ -131,6 +141,10 @@ Cherry Studio 是一款支持多个大语言模型LLM服务商的桌面客
[LICENSE](../LICENSE)
# ✉️ 联系我们
yinsenho@cherry-ai.com
# ⭐️ Star 记录
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)

View File

@@ -1,5 +1,14 @@
appId: com.kangfenmao.CherryStudio
productName: Cherry Studio
electronLanguages:
- zh-CN
- zh-TW
- en-US
- ja # macOS/linux/win
- ru # macOS/linux/win
- zh_CN # for macOS
- zh_TW # for macOS
- en # for macOS
directories:
buildResources: build
files:
@@ -29,21 +38,24 @@ files:
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
asarUnpack:
- resources/**
- '**/*.{node,dll,metal,exp,lib}'
- '**/*.{metal,exp,lib}'
win:
executableName: Cherry Studio
artifactName: ${productName}-${version}-portable.${ext}
artifactName: ${productName}-${version}-${arch}-setup.${ext}
target:
- target: nsis
- target: portable
nsis:
artifactName: ${productName}-${version}-setup.${ext}
artifactName: ${productName}-${version}-${arch}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
allowToChangeInstallationDirectory: true
oneClick: false
include: build/nsis-installer.nsh
buildUniversalInstaller: false
portable:
artifactName: ${productName}-${version}-${arch}-portable.${ext}
mac:
entitlementsInherit: build/entitlements.mac.plist
notarize: false
@@ -55,36 +67,31 @@ mac:
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
target:
- target: dmg
arch:
- arm64
- x64
- target: zip
arch:
- arm64
- x64
linux:
artifactName: ${productName}-${version}-${arch}.${ext}
target:
- target: AppImage
arch:
- arm64
- x64
maintainer: electronjs.org
category: Utility
desktop:
entry:
StartupWMClass: CherryStudio
publish:
# provider: generic
# url: https://cherrystudio.ocool.online
provider: github
repo: cherry-studio
owner: CherryHQ
provider: generic
url: https://releases.cherry-ai.com
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/
afterPack: scripts/after-pack.js
afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
知识库设置增加重排模型,提升知识库的准确性
自定义服务商增加兼容模式
增加 Github Copilot 服务商
PlantUML 预览支持放大和缩小
联网模式支持增强模式
修正语言及本地化错误
Windows ARM 更新跳转到官网下载
改进系统代理处理和初始化逻辑
修复 MCP 服务请求头不生效问题
移除搜索增强模式
优化消息渲染速度
修复备份大文件失败问题
修复网络搜索导致卡顿问题

View File

@@ -1,4 +1,5 @@
import react from '@vitejs/plugin-react'
import { sentryVitePlugin } from '@sentry/vite-plugin'
import react from '@vitejs/plugin-react-swc'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
@@ -12,17 +13,18 @@ export default defineConfig({
plugins: [
externalizeDepsPlugin({
exclude: [
'@llm-tools/embedjs',
'@llm-tools/embedjs-openai',
'@llm-tools/embedjs-loader-web',
'@llm-tools/embedjs-loader-markdown',
'@llm-tools/embedjs-loader-msoffice',
'@llm-tools/embedjs-loader-xml',
'@llm-tools/embedjs-loader-pdf',
'@llm-tools/embedjs-loader-sitemap',
'@llm-tools/embedjs-libsql',
'@llm-tools/embedjs-loader-image',
'p-queue'
'@cherrystudio/embedjs',
'@cherrystudio/embedjs-openai',
'@cherrystudio/embedjs-loader-web',
'@cherrystudio/embedjs-loader-markdown',
'@cherrystudio/embedjs-loader-msoffice',
'@cherrystudio/embedjs-loader-xml',
'@cherrystudio/embedjs-loader-pdf',
'@cherrystudio/embedjs-loader-sitemap',
'@cherrystudio/embedjs-libsql',
'@cherrystudio/embedjs-loader-image',
'p-queue',
'webdav'
]
}),
...visualizerPlugin('main')
@@ -41,24 +43,32 @@ export default defineConfig({
}
},
preload: {
plugins: [externalizeDepsPlugin()]
plugins: [externalizeDepsPlugin()],
resolve: {
alias: {
'@shared': resolve('packages/shared')
}
}
},
renderer: {
plugins: [
react({
babel: {
plugins: [
[
'styled-components',
{
displayName: true, // 开发环境下启用组件名
fileName: false, // 不在类名中包含文件名
pure: true, // 优化性能
ssr: false // 不需要服务端渲染
}
]
plugins: [
[
'@swc/plugin-styled-components',
{
displayName: true, // 开发环境下启用组件名称
fileName: false, // 不在类名中包含文件名
pure: true, // 优化性能
ssr: false // 不需要服务端渲染
}
]
}
]
}),
sentryVitePlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: 'cherry-ai',
project: 'cherry-studio'
}),
...visualizerPlugin('renderer')
],
@@ -69,7 +79,7 @@ export default defineConfig({
}
},
optimizeDeps: {
exclude: ['chunk-PZ64DZKH.js', 'chunk-JMKENWIY.js', 'chunk-UXYB6GHG.js', 'chunk-ALDIEZMG.js', 'chunk-4X6ZJEXY.js']
exclude: []
}
}
})

View File

@@ -53,6 +53,16 @@ export default defineConfig([
}
],
{
ignores: ['node_modules/**', 'dist/**', 'out/**', 'local/**', '.gitignore', 'scripts/cloudflare-worker.js']
ignores: [
'node_modules/**',
'build/**',
'dist/**',
'out/**',
'local/**',
'.yarn/**',
'.gitignore',
'scripts/cloudflare-worker.js',
'src/main/integration/nutstore/sso/lib/**'
]
}
])

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.1.8",
"version": "1.2.7",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -23,12 +23,13 @@
"build": "npm run typecheck && electron-vite build",
"build:check": "yarn test && yarn typecheck && yarn check:i18n",
"build:unpack": "dotenv npm run build && electron-builder --dir",
"build:win": "dotenv npm run build && electron-builder --win",
"build:win": "dotenv npm run build && electron-builder --win --x64 --arm64",
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
"build:mac": "dotenv electron-vite build && electron-builder --mac",
"build:win:arm64": "dotenv npm run build && electron-builder --win --arm64",
"build:mac": "dotenv electron-vite build && electron-builder --mac --arm64 --x64",
"build:mac:arm64": "dotenv electron-vite build && electron-builder --mac --arm64",
"build:mac:x64": "dotenv electron-vite build && electron-builder --mac --x64",
"build:linux": "dotenv electron-vite build && electron-builder --linux",
"build:linux": "dotenv electron-vite build && electron-builder --linux --x64 --arm64",
"build:linux:arm64": "dotenv electron-vite build && electron-builder --linux --arm64",
"build:linux:x64": "dotenv electron-vite build && electron-builder --linux --x64",
"build:npm": "node scripts/build-npm.js",
@@ -43,80 +44,110 @@
"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",
"prepare": "husky"
},
"dependencies": {
"@agentic/exa": "^7.3.3",
"@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3",
"@cherrystudio/embedjs": "^0.1.28",
"@cherrystudio/embedjs-libsql": "^0.1.28",
"@cherrystudio/embedjs-loader-csv": "^0.1.28",
"@cherrystudio/embedjs-loader-image": "^0.1.28",
"@cherrystudio/embedjs-loader-markdown": "^0.1.28",
"@cherrystudio/embedjs-loader-msoffice": "^0.1.28",
"@cherrystudio/embedjs-loader-pdf": "^0.1.28",
"@cherrystudio/embedjs-loader-sitemap": "^0.1.28",
"@cherrystudio/embedjs-loader-web": "^0.1.28",
"@cherrystudio/embedjs-loader-xml": "^0.1.28",
"@cherrystudio/embedjs-openai": "^0.1.28",
"@electron-toolkit/utils": "^3.0.0",
"@electron/notarize": "^2.5.0",
"@emotion/is-prop-valid": "^1.3.1",
"@google/generative-ai": "^0.21.0",
"@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.28#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.28-8e4393fa2d.patch",
"@llm-tools/embedjs-libsql": "^0.1.28",
"@llm-tools/embedjs-loader-csv": "^0.1.28",
"@llm-tools/embedjs-loader-markdown": "patch:@llm-tools/embedjs-loader-markdown@npm%3A0.1.28#~/.yarn/patches/@llm-tools-embedjs-loader-markdown-npm-0.1.28-81647ffac6.patch",
"@llm-tools/embedjs-loader-msoffice": "^0.1.28",
"@llm-tools/embedjs-loader-pdf": "^0.1.28",
"@llm-tools/embedjs-loader-sitemap": "^0.1.28",
"@llm-tools/embedjs-loader-web": "^0.1.28",
"@llm-tools/embedjs-loader-xml": "^0.1.28",
"@llm-tools/embedjs-openai": "^0.1.28",
"@modelcontextprotocol/sdk": "patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch",
"@langchain/community": "^0.3.36",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@sentry/electron": "^6.5.0",
"@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",
"apache-arrow": "^18.1.0",
"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",
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "^6.3.9",
"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",
"npx-scope-finder": "^1.2.0",
"node-stream-zip": "^1.15.0",
"officeparser": "^4.1.1",
"p-queue": "^8.1.0",
"socks-proxy-agent": "^8.0.3",
"os-proxy-config": "patch:os-proxy-config@npm%3A1.1.1#~/.yarn/patches/os-proxy-config-npm-1.1.1-af9c7574cc.patch",
"proxy-agent": "^6.5.0",
"tar": "^7.4.3",
"tokenx": "^0.4.1",
"turndown": "^7.2.0",
"turndown-plugin-gfm": "^1.0.2",
"undici": "^7.4.0",
"webdav": "4.11.4",
"webdav": "^5.8.0",
"ws": "^8.18.1",
"zipread": "^1.3.3"
},
"devDependencies": {
"@agentic/exa": "^7.3.3",
"@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@anthropic-ai/sdk": "^0.38.0",
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@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",
"@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0",
"@llm-tools/embedjs-loader-image": "^0.1.28",
"@modelcontextprotocol/sdk": "^1.9.0",
"@notionhq/client": "^2.2.15",
"@reduxjs/toolkit": "^2.2.5",
"@sentry/react": "^9.13.0",
"@sentry/vite-plugin": "^3.3.1",
"@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/lodash": "^4.17.5",
"@types/markdown-it": "^14",
"@types/md5": "^2.3.5",
"@types/node": "^18.19.9",
"@types/pako": "^1.0.2",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@types/react-infinite-scroll-component": "^5.0.0",
"@types/tinycolor2": "^1",
"@vitejs/plugin-react": "^4.2.1",
"@types/ws": "^8",
"@vitejs/plugin-react-swc": "^3.9.0",
"@vitest/coverage-v8": "^3.1.1",
"@vitest/ui": "^3.1.1",
"antd": "^5.22.5",
"applescript": "^1.0.0",
"axios": "^1.7.3",
@@ -127,7 +158,7 @@
"dexie-react-hooks": "^1.1.7",
"dotenv-cli": "^7.4.2",
"electron": "31.7.6",
"electron-builder": "^24.13.3",
"electron-builder": "26.0.13",
"electron-devtools-installer": "^3.2.0",
"electron-icon-builder": "^2.0.1",
"electron-vite": "^2.3.0",
@@ -142,11 +173,16 @@
"i18next": "^23.11.5",
"lint-staged": "^15.5.0",
"lodash": "^4.17.21",
"lru-cache": "^11.1.0",
"lucide-react": "^0.487.0",
"mime": "^4.0.4",
"openai": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch",
"npx-scope-finder": "^1.2.0",
"openai": "patch:openai@npm%3A4.87.3#~/.yarn/patches/openai-npm-4.87.3-2b30a7685f.patch",
"p-queue": "^8.1.0",
"prettier": "^3.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rc-virtual-list": "^3.18.5",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hotkeys-hook": "^4.6.1",
"react-i18next": "^14.1.2",
"react-infinite-scroll-component": "^6.1.0",
@@ -160,27 +196,31 @@
"rehype-katex": "^7.0.1",
"rehype-mathjax": "^7.0.0",
"rehype-raw": "^7.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": "^1.22.2",
"shiki": "^3.2.1",
"string-width": "^7.2.0",
"styled-components": "^6.1.11",
"tiny-pinyin": "^1.3.2",
"tinycolor2": "^1.6.0",
"tokenx": "^0.4.1",
"typescript": "^5.6.2",
"uuid": "^10.0.0",
"vite": "^5.0.12"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
"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",
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
"openai@npm:^4.77.0": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.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",
"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"
},
"packageManager": "yarn@4.6.0",
"lint-staged": {

View File

@@ -1,13 +0,0 @@
{
"name": "@cherry-studio/artifacts",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@cherry-studio/artifacts",
"version": "0.1.0",
"license": "ISC"
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,166 @@
export enum IpcChannel {
App_ClearCache = 'app:clear-cache',
App_SetLaunchOnBoot = 'app:set-launch-on-boot',
App_SetLanguage = 'app:set-language',
App_ShowUpdateDialog = 'app:show-update-dialog',
App_CheckForUpdate = 'app:check-for-update',
App_Reload = 'app:reload',
App_Info = 'app:info',
App_Proxy = 'app:proxy',
App_SetLaunchToTray = 'app:set-launch-to-tray',
App_SetTray = 'app:set-tray',
App_SetTrayOnClose = 'app:set-tray-on-close',
App_RestartTray = 'app:restart-tray',
App_SetTheme = 'app:set-theme',
App_SetCustomCss = 'app:set-custom-css',
App_SetAutoUpdate = 'app:set-auto-update',
App_IsBinaryExist = 'app:is-binary-exist',
App_GetBinaryPath = 'app:get-binary-path',
App_InstallUvBinary = 'app:install-uv-binary',
App_InstallBunBinary = 'app:install-bun-binary',
// Open
Open_Path = 'open:path',
Open_Website = 'open:website',
Minapp = 'minapp',
Config_Set = 'config:set',
Config_Get = 'config:get',
MiniWindow_Show = 'miniwindow:show',
MiniWindow_Hide = 'miniwindow:hide',
MiniWindow_Close = 'miniwindow:close',
MiniWindow_Toggle = 'miniwindow:toggle',
MiniWindow_SetPin = 'miniwindow:set-pin',
// Mcp
Mcp_RemoveServer = 'mcp:remove-server',
Mcp_RestartServer = 'mcp:restart-server',
Mcp_StopServer = 'mcp:stop-server',
Mcp_ListTools = 'mcp:list-tools',
Mcp_CallTool = 'mcp:call-tool',
Mcp_ListPrompts = 'mcp:list-prompts',
Mcp_GetPrompt = 'mcp:get-prompt',
Mcp_ListResources = 'mcp:list-resources',
Mcp_GetResource = 'mcp:get-resource',
Mcp_GetInstallInfo = 'mcp:get-install-info',
Mcp_ServersChanged = 'mcp:servers-changed',
Mcp_ServersUpdated = 'mcp:servers-updated',
//copilot
Copilot_GetAuthMessage = 'copilot:get-auth-message',
Copilot_GetCopilotToken = 'copilot:get-copilot-token',
Copilot_SaveCopilotToken = 'copilot:save-copilot-token',
Copilot_GetToken = 'copilot:get-token',
Copilot_Logout = 'copilot:logout',
Copilot_GetUser = 'copilot:get-user',
// obsidian
Obsidian_GetVaults = 'obsidian:get-vaults',
Obsidian_GetFiles = 'obsidian:get-files',
// nutstore
Nutstore_GetSsoUrl = 'nutstore:get-sso-url',
Nutstore_DecryptToken = 'nutstore:decrypt-token',
Nutstore_GetDirectoryContents = 'nutstore:get-directory-contents',
//aes
Aes_Encrypt = 'aes:encrypt',
Aes_Decrypt = 'aes:decrypt',
Gemini_UploadFile = 'gemini:upload-file',
Gemini_Base64File = 'gemini:base64-file',
Gemini_RetrieveFile = 'gemini:retrieve-file',
Gemini_ListFiles = 'gemini:list-files',
Gemini_DeleteFile = 'gemini:delete-file',
Windows_ResetMinimumSize = 'window:reset-minimum-size',
Windows_SetMinimumSize = 'window:set-minimum-size',
SelectionMenu_Action = 'selection-menu:action',
KnowledgeBase_Create = 'knowledge-base:create',
KnowledgeBase_Reset = 'knowledge-base:reset',
KnowledgeBase_Delete = 'knowledge-base:delete',
KnowledgeBase_Add = 'knowledge-base:add',
KnowledgeBase_Remove = 'knowledge-base:remove',
KnowledgeBase_Search = 'knowledge-base:search',
KnowledgeBase_Rerank = 'knowledge-base:rerank',
//file
File_Open = 'file:open',
File_OpenPath = 'file:openPath',
File_Save = 'file:save',
File_Select = 'file:select',
File_Upload = 'file:upload',
File_Clear = 'file:clear',
File_Read = 'file:read',
File_Delete = 'file:delete',
File_Get = 'file:get',
File_SelectFolder = 'file:selectFolder',
File_Create = 'file:create',
File_Write = 'file:write',
File_SaveImage = 'file:saveImage',
File_Base64Image = 'file:base64Image',
File_Download = 'file:download',
File_Copy = 'file:copy',
File_BinaryFile = 'file:binaryFile',
Fs_Read = 'fs:read',
Export_Word = 'export:word',
Shortcuts_Update = 'shortcuts:update',
// backup
Backup_Backup = 'backup:backup',
Backup_Restore = 'backup:restore',
Backup_BackupToWebdav = 'backup:backupToWebdav',
Backup_RestoreFromWebdav = 'backup:restoreFromWebdav',
Backup_ListWebdavFiles = 'backup:listWebdavFiles',
Backup_CheckConnection = 'backup:checkConnection',
Backup_CreateDirectory = 'backup:createDirectory',
Backup_DeleteWebdavFile = 'backup:deleteWebdavFile',
// zip
Zip_Compress = 'zip:compress',
Zip_Decompress = 'zip:decompress',
// system
System_GetDeviceType = 'system:getDeviceType',
System_GetHostname = 'system:getHostname',
// events
SelectionAction = 'selection-action',
BackupProgress = 'backup-progress',
ThemeChange = 'theme:change',
UpdateDownloadedCancelled = 'update-downloaded-cancelled',
RestoreProgress = 'restore-progress',
UpdateError = 'update-error',
UpdateAvailable = 'update-available',
UpdateNotAvailable = 'update-not-available',
DownloadProgress = 'download-progress',
UpdateDownloaded = 'update-downloaded',
DownloadUpdate = 'download-update',
DirectoryProcessingPercent = 'directory-processing-percent',
FullscreenStatusChanged = 'fullscreen-status-changed',
HideMiniWindow = 'hide-mini-window',
ShowMiniWindow = 'show-mini-window',
MiniWindowReload = 'miniwindow-reload',
ReduxStateChange = 'redux-state-change',
ReduxStoreReady = 'redux-store-ready',
// Search Window
SearchWindow_Open = 'search-window:open',
SearchWindow_Close = 'search-window:close',
SearchWindow_OpenUrl = 'search-window:open-url',
// sentry
Sentry_Init = 'sentry:init'
}

View File

@@ -88,7 +88,7 @@ export const textExts = [
'.ctp', // CakePHP 视图文件
'.cfm', // ColdFusion 标记语言文件
'.cfc', // ColdFusion 组件文件
'.m', // Objective-C 源文件
'.m', // Objective-C 或 MATLAB 源文件
'.mm', // Objective-C++ 源文件
'.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件
@@ -106,7 +106,32 @@ export const textExts = [
'.ixx', // C++20 模块实现文件
'.f90', // Fortran 90 源文件
'.f', // Fortran 固定格式源代码文件
'.f03' // Fortran 2003+ 源代码文件
'.f03', // Fortran 2003+ 源代码文件
'.ahk', // AutoHotKey 语言文件
'.tcl', // Tcl 脚本
'.do', // Questa 或 Modelsim Tcl 脚本
'.v', // Verilog 源文件
'.sv', // SystemVerilog 源文件
'.svh', // SystemVerilog 头文件
'.vhd', // VHDL 源文件
'.vhdl', // VHDL 源文件
'.lef', // Library Exchange Format
'.def', // Design Exchange Format
'.edif', // Electronic Design Interchange Format
'.sdf', // Standard Delay Format
'.sdc', // Synopsys Design Constraints
'.xdc', // Xilinx Design Constraints
'.rpt', // 报告文件
'.lisp', // Lisp 脚本
'.il', // Cadence SKILL 脚本
'.ils', // Cadence SKILL++ 脚本
'.sp', // SPICE netlist 文件
'.spi', // SPICE netlist 文件
'.cir', // SPICE netlist 文件
'.net', // SPICE netlist 文件
'.scs', // Spectre netlist 文件
'.asc', // LTspice netlist schematic 文件
'.tf' // Technology File
]
export const ZOOM_SHORTCUTS = [
@@ -132,3 +157,8 @@ export const ZOOM_SHORTCUTS = [
system: true
}
]
export const KB = 1024
export const MB = 1024 * KB
export const GB = 1024 * MB
export const defaultLanguage = 'en-US'

View File

@@ -0,0 +1 @@
export const NUTSTORE_HOST = 'https://dav.jianguoyun.com/dav'

View File

@@ -1,115 +1,106 @@
<!doctype html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CherryStudio 许可协议-ZH/EN</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet" />
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>许可协议 | License Agreement</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 p-8">
<div class="container mx-auto bg-white p-6 rounded shadow-lg">
<h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio 许可协议</h1>
<div class="mb-8">
<h2 class="text-2xl font-semibold mb-4">许可协议</h2>
<p class="mb-4">
本软件采用 <strong>Apache License 2.0</strong> 许可。除 Apache License 2.0 规定的条款外,您在使用 Cherry
Studio 时还应遵守以下附加条款:
</p>
<h3 class="text-xl font-semibold mb-2">一. 商用许可</h3>
<ol class="list-decimal list-inside mb-4">
<li><strong>免费商用</strong>:用户在不修改代码的情况下,可以免费用于商业目的。</li>
<li>
<strong>商业授权</strong>如果您满足以下任意条件之一,需取得商业授权:
<ol class="list-decimal list-inside ml-4">
<li>对本软件进行二次修改、开发包括但不限于修改应用名称、logo、代码以及功能</li>
<li>为企业客户提供多租户服务,且该服务支持 10 人或以上的使用</li>
<li>预装或集成到硬件设备或产品中进行捆绑销售。</li>
<li>政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。</li>
</ol>
</li>
</ol>
<h3 class="text-xl font-semibold mb-2">二. 贡献者协议</h3>
<ol class="list-decimal list-inside mb-4">
<li><strong>许可调整</strong>:生产者有权根据需要对开源协议进行调整,使其更加严格或宽松。</li>
<li><strong>商业用途</strong>:您贡献的代码可能会被用于商业用途,包括但不限于云业务运营。</li>
</ol>
<h3 class="text-xl font-semibold mb-2">三. 其他条款</h3>
<ol class="list-decimal list-inside mb-4">
<li>本协议条款的解释权归 Cherry Studio 开发者所有。</li>
<li>本协议可能根据实际情况进行更新,更新时将通过本软件通知用户。</li>
</ol>
<p class="mb-4">如有任何问题或需申请商业授权,请联系 Cherry Studio 开发团队</p>
<p>
除上述特定条件外,其他所有权利和限制均遵循 Apache License 2.0。有关 Apache License 2.0 的详细信息,请访问
<a href="http://www.apache.org/licenses/LICENSE-2.0"
class="text-blue-500 underline">http://www.apache.org/licenses/LICENSE-2.0</a>
</p>
<body class="bg-gray-50">
<div class="max-w-4xl mx-auto px-4 py-8">
<!-- 中文版本 -->
<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>
<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>
</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>
</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>
</section>
</div>
<h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio License</h1>
<div class="mb-8">
<h2 class="text-2xl font-semibold mb-4">License Agreement</h2>
<p class="mb-4">
This software is licensed under the <strong>Apache License 2.0</strong>. In addition to the terms of the
Apache License 2.0, the following additional terms apply to the use of Cherry Studio:
</p>
<h3 class="text-xl font-semibold mb-2">I. Commercial Use License</h3>
<ol class="list-decimal list-inside mb-4">
<li>
<strong>Free Commercial Use</strong>: Users can use the software for commercial purposes without
modifying
the code.
</li>
<li>
<strong>Commercial License Required</strong>: A commercial license is required if any of the
following
conditions are met:
<ol class="list-decimal list-inside ml-4">
<li>
You modify, develop, or alter the software, including but not limited to changes to the
application
name, logo, code, or functionality.
</li>
<li>You provide multi-tenant services to enterprise customers with 10 or more users.</li>
<li>
You pre-install or integrate the software into hardware devices or products and bundle it
for sale.
</li>
<li>
You are engaging in large-scale procurement for government or educational institutions,
especially
involving security, data privacy, or other sensitive requirements.
</li>
</ol>
</li>
</ol>
<h3 class="text-xl font-semibold mb-2">II. Contributor Agreement</h3>
<ol class="list-decimal list-inside mb-4">
<li>
<strong>License Adjustment</strong>: The producer reserves the right to adjust the open-source
license as
needed, making it stricter or more lenient.
</li>
<li>
<strong>Commercial Use</strong>: Any code you contribute may be used for commercial purposes,
including but
not limited to cloud business operations.
</li>
</ol>
<h3 class="text-xl font-semibold mb-2">III. Other Terms</h3>
<ol class="list-decimal list-inside mb-4">
<li>The interpretation of these terms is subject to the discretion of Cherry Studio developers.</li>
<li>These terms may be updated, and users will be notified through the software when changes occur.</li>
</ol>
<p class="mb-4">
For any questions or to request a commercial license, please contact the Cherry Studio development team.
</p>
<p>
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache
License 2.0. Detailed information about the Apache License 2.0 can be found at
<hr class="my-12 border-gray-300">
<!-- English Version -->
<div>
<h1 class="text-3xl font-bold mb-8 text-gray-900">License Agreement</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>
<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
Studio.</li>
</ol>
</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>
</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>
</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-500 underline">http://www.apache.org/licenses/LICENSE-2.0</a>
class="text-blue-600 hover:underline">http://www.apache.org/licenses/LICENSE-2.0</a>.
</p>
</div>
</div>

View File

@@ -1,7 +1,6 @@
<!doctype html>
<html lang="en">
<head>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Github Releases Timeline</title>
@@ -9,194 +8,201 @@
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/typography@0.5.10/dist/typography.min.css"></script>
</head>
</head>
<body id="app">
<body id="app">
<div :class="isDark ? 'dark-bg' : 'bg'" class="min-h-screen">
<div class="max-w-3xl mx-auto py-12 px-4">
<h1 class="text-3xl font-bold mb-8" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
<div class="max-w-3xl mx-auto py-12 px-4">
<h1 class="text-3xl font-bold mb-8" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
<!-- Loading状态 -->
<div v-if="loading" class="text-center py-8">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4"
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
</div>
<!-- Error 状态 -->
<div v-else-if="error" class="text-red-500 text-center py-8">{{ error }}</div>
<!-- Release 列表 -->
<div v-else class="space-y-8">
<div v-for="release in releases" :key="release.id" class="relative pl-8"
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
<div class="rounded-lg shadow-sm p-6 transition-shadow"
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
<div class="flex items-start justify-between mb-4">
<div>
<h2 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900'">
{{ release.name || release.tag_name }}
</h2>
<p class="text-sm mt-1" :class="isDark ? 'text-gray-400' : 'text-gray-500'">
{{ formatDate(release.published_at) }}
</p>
</div>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
{{ release.tag_name }}
</span>
</div>
<div class="prose" :class="isDark ? 'text-gray-300 dark-prose' : 'text-gray-600'"
v-html="renderMarkdown(release.body)"></div>
</div>
</div>
</div>
<!-- Loading状态 -->
<div v-if="loading" class="text-center py-8">
<div
class="inline-block animate-spin rounded-full h-8 w-8 border-4"
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
</div>
<!-- Error 状态 -->
<div v-else-if="error" class="text-red-500 text-center py-8">{{ error }}</div>
<!-- Release 列表 -->
<div v-else class="space-y-8">
<div
v-for="release in releases"
:key="release.id"
class="relative pl-8"
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
<div
class="rounded-lg shadow-sm p-6 transition-shadow"
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
<div class="flex items-start justify-between mb-4">
<div>
<h2 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900'">
{{ release.name || release.tag_name }}
</h2>
<p class="text-sm mt-1" :class="isDark ? 'text-gray-400' : 'text-gray-500'">
{{ formatDate(release.published_at) }}
</p>
</div>
<span
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
{{ release.tag_name }}
</span>
</div>
<div
class="prose"
:class="isDark ? 'text-gray-300 dark-prose' : 'text-gray-600'"
v-html="renderMarkdown(release.body)"></div>
</div>
</div>
</div>
</div>
</div>
<script>
const md = window.markdownit({
breaks: true,
linkify: true
})
const md = window.markdownit({
breaks: true,
linkify: true
})
const { createApp } = Vue
const { createApp } = Vue
createApp({
data() {
return {
releases: [],
loading: true,
error: null,
isDark: false
}
},
methods: {
async fetchReleases() {
try {
this.loading = true
this.error = null
const response = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases')
if (!response.ok) {
throw new Error('Failed to fetch releases')
}
this.releases = await response.json()
} catch (err) {
this.error = 'Error loading releases: ' + err.message
} finally {
this.loading = false
}
},
formatDate(dateString) {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
},
renderMarkdown(content) {
if (!content) return ''
return md.render(content)
},
initTheme() {
// 从 URL 参数获取主题设置
const url = new URL(window.location.href)
const theme = url.searchParams.get('theme')
this.isDark = theme === 'dark'
}
},
mounted() {
this.initTheme()
this.fetchReleases()
createApp({
data() {
return {
releases: [],
loading: true,
error: null,
isDark: false
}
},
methods: {
async fetchReleases() {
try {
this.loading = true
this.error = null
const response = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases')
if (!response.ok) {
throw new Error('Failed to fetch releases')
}
this.releases = await response.json()
} catch (err) {
this.error = 'Error loading releases: ' + err.message
} finally {
this.loading = false
}
}).mount('#app')
},
formatDate(dateString) {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
},
renderMarkdown(content) {
if (!content) return ''
return md.render(content)
},
initTheme() {
// 从 URL 参数获取主题设置
const url = new URL(window.location.href)
const theme = url.searchParams.get('theme')
this.isDark = theme === 'dark'
}
},
mounted() {
this.initTheme()
this.fetchReleases()
}
}).mount('#app')
</script>
<style>
/* 基础的 Markdown 样式 */
.prose {
line-height: 1.6;
}
/* 基础的 Markdown 样式 */
.prose {
line-height: 1.6;
}
.prose h1 {
font-size: 1.5em;
margin: 1em 0;
}
.prose h1 {
font-size: 1.5em;
margin: 1em 0;
}
.prose h2 {
font-size: 1.3em;
margin: 0.8em 0;
}
.prose h2 {
font-size: 1.3em;
margin: 0.8em 0;
}
.prose h3 {
font-size: 1.1em;
margin: 0.6em 0;
}
.prose h3 {
font-size: 1.1em;
margin: 0.6em 0;
}
.prose ul {
list-style-type: disc;
margin-left: 1.5em;
margin-bottom: 1em;
}
.prose ul {
list-style-type: disc;
margin-left: 1.5em;
margin-bottom: 1em;
}
.prose ol {
list-style-type: decimal;
margin-left: 1.5em;
margin-bottom: 1em;
}
.prose ol {
list-style-type: decimal;
margin-left: 1.5em;
margin-bottom: 1em;
}
.prose code {
padding: 0.2em 0.4em;
border-radius: 0.2em;
font-size: 0.9em;
}
.prose code {
padding: 0.2em 0.4em;
border-radius: 0.2em;
font-size: 0.9em;
}
.dark .prose code {
background-color: #1f2937;
}
.dark .prose code {
background-color: #1f2937;
}
.prose code {
background-color: #f3f4f6;
}
.prose code {
background-color: #f3f4f6;
}
.prose pre code {
display: block;
padding: 1em;
overflow-x: auto;
}
.prose pre code {
display: block;
padding: 1em;
overflow-x: auto;
}
.prose a {
color: #3b82f6;
text-decoration: underline;
}
.prose a {
color: #3b82f6;
text-decoration: underline;
}
.dark .prose a {
color: #60a5fa;
}
.dark .prose a {
color: #60a5fa;
}
.prose blockquote {
border-left: 4px solid #e5e7eb;
padding-left: 1em;
margin: 1em 0;
}
.prose blockquote {
border-left: 4px solid #e5e7eb;
padding-left: 1em;
margin: 1em 0;
}
.dark .prose blockquote {
border-left-color: #374151;
color: #9ca3af;
}
.dark .prose blockquote {
border-left-color: #374151;
color: #9ca3af;
}
.dark .prose {
color: #e5e7eb;
}
.dark .prose {
color: #e5e7eb;
}
.dark-bg {
background-color: #151515;
}
.dark-bg {
background-color: #151515;
}
.bg {
background-color: #f2f2f2;
}
.bg {
background-color: #f2f2f2;
}
</style>
</body>
</html>
</body>
</html>

View File

@@ -1,68 +0,0 @@
<head>
<style>
body {
margin: 0;
}
</style>
<script src="https://unpkg.com/3d-force-graph"></script>
</head>
<body>
<div id="3d-graph"></div>
<script src="./js/bridge.js"></script>
<script type="module">
import { getQueryParam } from './js/utils.js'
const apiUrl = getQueryParam('apiUrl')
const modelId = getQueryParam('modelId')
const jsonUrl = `${apiUrl}/v1/global_graph/${modelId}`
const infoCard = document.createElement('div')
infoCard.style.position = 'fixed'
infoCard.style.backgroundColor = 'rgba(255, 255, 255, 0.9)'
infoCard.style.padding = '8px'
infoCard.style.borderRadius = '4px'
infoCard.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'
infoCard.style.fontSize = '12px'
infoCard.style.maxWidth = '200px'
infoCard.style.display = 'none'
infoCard.style.zIndex = '1000'
document.body.appendChild(infoCard)
document.addEventListener('mousemove', (event) => {
infoCard.style.left = `${event.clientX + 10}px`
infoCard.style.top = `${event.clientY + 10}px`
})
const elem = document.getElementById('3d-graph')
const Graph = ForceGraph3D()(elem)
.jsonUrl(jsonUrl)
.nodeAutoColorBy((node) => node.properties.type || 'default')
.nodeVal((node) => node.properties.degree)
.linkWidth((link) => link.properties.weight)
.onNodeHover((node) => {
if (node) {
infoCard.innerHTML = `
<div style="font-weight: bold; margin-bottom: 4px; color: #333;">
${node.properties.title}
</div>
<div style="color: #666;">
${node.properties.description}
</div>`
infoCard.style.display = 'block'
} else {
infoCard.style.display = 'none'
}
})
.onNodeClick((node) => {
const url = `${apiUrl}/v1/references/${modelId}/entities/${node.properties.human_readable_id}`
window.api.minApp({
url,
windowOptions: {
title: node.properties.title,
width: 500,
height: 800
}
})
})
</script>
</body>

View File

@@ -1,8 +1,5 @@
const { ProxyAgent } = require('undici')
const { SocksProxyAgent } = require('socks-proxy-agent')
const https = require('https')
const fs = require('fs')
const { pipeline } = require('stream/promises')
/**
* Downloads a file from a URL with redirect handling
@@ -11,42 +8,28 @@ const { pipeline } = require('stream/promises')
* @returns {Promise<void>} Promise that resolves when download is complete
*/
async function downloadWithRedirects(url, destinationPath) {
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY
if (proxyUrl.startsWith('socks')) {
const proxyAgent = new SocksProxyAgent(proxyUrl)
return new Promise((resolve, reject) => {
const request = (url) => {
https
.get(url, { agent: proxyAgent }, (response) => {
if (response.statusCode == 301 || response.statusCode == 302) {
request(response.headers.location)
return
}
if (response.statusCode !== 200) {
reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`))
return
}
const file = fs.createWriteStream(destinationPath)
response.pipe(file)
file.on('finish', () => resolve())
})
.on('error', (err) => {
reject(err)
})
}
request(url)
})
} else {
const proxyAgent = new ProxyAgent(proxyUrl)
const response = await fetch(url, {
dispatcher: proxyAgent
})
if (!response.ok) {
throw new Error(`Download failed: ${response.status} ${response.statusText}`)
return new Promise((resolve, reject) => {
const request = (url) => {
https
.get(url, (response) => {
if (response.statusCode == 301 || response.statusCode == 302) {
request(response.headers.location)
return
}
if (response.statusCode !== 200) {
reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`))
return
}
const file = fs.createWriteStream(destinationPath)
response.pipe(file)
file.on('finish', () => resolve())
})
.on('error', (err) => {
reject(err)
})
}
const file = fs.createWriteStream(destinationPath)
await pipeline(response.body, file)
}
request(url)
})
}
module.exports = { downloadWithRedirects }

View File

@@ -6,8 +6,8 @@ const AdmZip = require('adm-zip')
const { downloadWithRedirects } = require('./download')
// Base URL for downloading bun binaries
const BUN_RELEASE_BASE_URL = 'https://github.com/oven-sh/bun/releases/download'
const DEFAULT_BUN_VERSION = '1.2.5' // Default fallback version
const BUN_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/bun/releases/download'
const DEFAULT_BUN_VERSION = '1.2.9' // Default fallback version
// Mapping of platform+arch to binary package name
const BUN_PACKAGES = {

View File

@@ -1,314 +0,0 @@
const fs = require('fs')
const path = require('path')
const os = require('os')
const https = require('https')
const { execSync } = require('child_process')
// 配置
const NODE_VERSION = process.env.NODE_VERSION || '18.18.0' // 默认版本
const NODE_RELEASE_BASE_URL = 'https://nodejs.org/dist'
// 平台映射
const NODE_PACKAGES = {
'darwin-arm64': `node-v${NODE_VERSION}-darwin-arm64.tar.gz`,
'darwin-x64': `node-v${NODE_VERSION}-darwin-x64.tar.gz`,
'win32-x64': `node-v${NODE_VERSION}-win32-x64.zip`,
'win32-ia32': `node-v${NODE_VERSION}-win32-x86.zip`,
'linux-x64': `node-v${NODE_VERSION}-linux-x64.tar.gz`,
'linux-arm64': `node-v${NODE_VERSION}-linux-arm64.tar.gz`,
}
// 辅助函数 - 递归复制目录
function copyFolderRecursiveSync(source, target) {
// 检查目标目录是否存在,不存在则创建
if (!fs.existsSync(target)) {
fs.mkdirSync(target, { recursive: true });
}
// 读取源目录中的所有文件和文件夹
const files = fs.readdirSync(source);
// 循环处理每个文件/文件夹
for (const file of files) {
const sourcePath = path.join(source, file);
const targetPath = path.join(target, file);
// 检查是文件还是文件夹
if (fs.statSync(sourcePath).isDirectory()) {
// 如果是文件夹,递归复制
copyFolderRecursiveSync(sourcePath, targetPath);
} else {
// 如果是文件,直接复制
fs.copyFileSync(sourcePath, targetPath);
}
}
}
// 二进制文件存放目录
const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin')
// 创建二进制文件存放目录
async function createBinariesDir() {
if (!fs.existsSync(binariesDir)) {
console.log(`Creating binaries directory at ${binariesDir}`)
fs.mkdirSync(binariesDir, { recursive: true })
}
}
// 获取当前平台对应的包名
function getPackageForPlatform() {
const platform = os.platform()
const arch = os.arch()
const key = `${platform}-${arch}`
console.log(`Current platform: ${platform}, architecture: ${arch}`)
if (!NODE_PACKAGES[key]) {
throw new Error(`Unsupported platform/architecture: ${key}`)
}
return NODE_PACKAGES[key]
}
// 下载 Node.js
async function downloadNodeJs() {
const packageName = getPackageForPlatform()
const downloadUrl = `${NODE_RELEASE_BASE_URL}/v${NODE_VERSION}/${packageName}`
const tempFilePath = path.join(os.tmpdir(), packageName)
console.log(`Downloading Node.js v${NODE_VERSION} from ${downloadUrl}`)
console.log(`Temp file path: ${tempFilePath}`)
// 如果临时文件已存在,先删除
if (fs.existsSync(tempFilePath)) {
fs.unlinkSync(tempFilePath)
}
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(tempFilePath)
https.get(downloadUrl, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`))
return
}
console.log(`Download started, status code: ${response.statusCode}`)
response.pipe(file)
file.on('finish', () => {
file.close()
console.log('Download completed')
resolve(tempFilePath)
})
file.on('error', (err) => {
fs.unlinkSync(tempFilePath)
reject(err)
})
}).on('error', (err) => {
if (fs.existsSync(tempFilePath)) {
fs.unlinkSync(tempFilePath)
}
reject(err)
})
})
}
// 解压 Node.js 包
async function extractNodeJs(filePath) {
const platform = os.platform()
const extractDir = path.join(os.tmpdir(), `node-v${NODE_VERSION}-extract`)
if (fs.existsSync(extractDir)) {
console.log(`Removing existing extract directory: ${extractDir}`)
fs.rmSync(extractDir, { recursive: true, force: true })
}
console.log(`Creating extract directory: ${extractDir}`)
fs.mkdirSync(extractDir, { recursive: true })
console.log(`Extracting to ${extractDir}`)
if (platform === 'win32') {
// Windows 使用内置的解压工具
try {
const AdmZip = require('adm-zip')
console.log(`Using adm-zip to extract ${filePath}`)
const zip = new AdmZip(filePath)
zip.extractAllTo(extractDir, true)
console.log(`Extraction completed using adm-zip`)
} catch (error) {
console.error(`Error using adm-zip: ${error}`)
throw error
}
} else {
// Linux/Mac 使用 tar
try {
console.log(`Using tar to extract ${filePath} to ${extractDir}`)
execSync(`tar -xzf "${filePath}" -C "${extractDir}"`, { stdio: 'inherit' })
console.log(`Extraction completed using tar`)
} catch (error) {
console.error(`Error using tar: ${error}`)
throw error
}
}
return extractDir
}
// 安装 Node.js
async function installNodeJs(extractDir) {
const platform = os.platform()
console.log(`Finding extracted Node.js directory in ${extractDir}`)
const items = fs.readdirSync(extractDir)
console.log(`Found items in extract directory: ${items.join(', ')}`)
// 找到包含"node-v"的目录名
const folderName = items.find(item => item.startsWith('node-v'))
if (!folderName) {
throw new Error(`Could not find Node.js directory in ${extractDir}`)
}
console.log(`Found Node.js directory: ${folderName}`)
const nodeBinPath = path.join(extractDir, folderName, 'bin')
console.log(`Node.js bin path: ${nodeBinPath}`)
// 复制 node 和 npm
if (platform === 'win32') {
// Windows
console.log('Installing Node.js binaries for Windows')
fs.copyFileSync(
path.join(extractDir, folderName, 'node.exe'),
path.join(binariesDir, 'node.exe')
)
console.log(`Copied node.exe to ${path.join(binariesDir, 'node.exe')}`)
fs.copyFileSync(
path.join(extractDir, folderName, 'npm.cmd'),
path.join(binariesDir, 'npm.cmd')
)
console.log(`Copied npm.cmd to ${path.join(binariesDir, 'npm.cmd')}`)
fs.copyFileSync(
path.join(extractDir, folderName, 'npx.cmd'),
path.join(binariesDir, 'npx.cmd')
)
console.log(`Copied npx.cmd to ${path.join(binariesDir, 'npx.cmd')}`)
} else {
// Linux/Mac
console.log('Installing Node.js binaries for Linux/Mac')
fs.copyFileSync(
path.join(nodeBinPath, 'node'),
path.join(binariesDir, 'node')
)
console.log(`Copied node to ${path.join(binariesDir, 'node')}`)
// 创建npm脚本指向正确路径
const npmScript = `#!/usr/bin/env node
require("./node_modules/npm/lib/cli.js")(process)`;
fs.writeFileSync(path.join(binariesDir, 'npm'), npmScript);
console.log(`Created npm script at ${path.join(binariesDir, 'npm')}`);
// 创建npx脚本指向正确路径
const npxScript = `#!/usr/bin/env node
require("./node_modules/npm/bin/npx-cli.js")`;
fs.writeFileSync(path.join(binariesDir, 'npx'), npxScript);
console.log(`Created npx script at ${path.join(binariesDir, 'npx')}`);
// 设置执行权限
execSync(`chmod +x "${path.join(binariesDir, 'node')}"`)
execSync(`chmod +x "${path.join(binariesDir, 'npm')}"`)
execSync(`chmod +x "${path.join(binariesDir, 'npx')}"`)
console.log('Set executable permissions for Node.js binaries')
}
// 复制 npm 相关文件和目录
const npmDir = path.join(binariesDir, 'node_modules', 'npm')
fs.mkdirSync(npmDir, { recursive: true })
console.log(`Created npm directory at ${npmDir}`)
// 复制 npm 目录的内容
const srcNpmDir = path.join(extractDir, folderName, 'lib', 'node_modules', 'npm')
console.log(`Copying npm files from ${srcNpmDir} to ${npmDir}`)
const files = fs.readdirSync(srcNpmDir)
for (const file of files) {
const srcPath = path.join(srcNpmDir, file)
const destPath = path.join(npmDir, file)
if (fs.lstatSync(srcPath).isDirectory()) {
// 使用自定义函数代替fs.cpSync确保兼容性
console.log(`Copying directory: ${file}`)
copyFolderRecursiveSync(srcPath, destPath)
} else {
console.log(`Copying file: ${file}`)
fs.copyFileSync(srcPath, destPath)
}
}
console.log('Node.js installation completed successfully')
}
// 清理临时文件
async function cleanup(filePath, extractDir) {
try {
if (fs.existsSync(filePath)) {
console.log(`Cleaning up temp file: ${filePath}`)
fs.unlinkSync(filePath)
}
if (fs.existsSync(extractDir)) {
console.log(`Cleaning up extract directory: ${extractDir}`)
fs.rmSync(extractDir, { recursive: true, force: true })
}
console.log('Cleaned up temporary files')
} catch (error) {
console.error('Error during cleanup:', error)
}
}
// 主安装函数
async function install() {
try {
console.log(`Starting Node.js v${NODE_VERSION} installation...`)
await createBinariesDir()
console.log('Binary directory created/verified')
const filePath = await downloadNodeJs()
console.log(`Downloaded Node.js to ${filePath}`)
const extractDir = await extractNodeJs(filePath)
console.log(`Extracted Node.js to ${extractDir}`)
await installNodeJs(extractDir)
console.log('Installed Node.js binaries')
await cleanup(filePath, extractDir)
console.log('Cleanup completed')
console.log(`Node.js v${NODE_VERSION} has been installed successfully at ${binariesDir}`)
return true
} catch (error) {
console.error('Installation failed:', error)
throw error
}
}
// 执行安装
install()
.then(() => {
console.log('Installation process completed successfully')
process.exit(0)
})
.catch((error) => {
console.error('Fatal error during installation:', error)
process.exit(1)
})

View File

@@ -7,8 +7,8 @@ const AdmZip = require('adm-zip')
const { downloadWithRedirects } = require('./download')
// Base URL for downloading uv binaries
const UV_RELEASE_BASE_URL = 'https://github.com/astral-sh/uv/releases/download'
const DEFAULT_UV_VERSION = '0.6.6'
const UV_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/uv/releases/download'
const DEFAULT_UV_VERSION = '0.6.14'
// Mapping of platform+arch to binary package name
const UV_PACKAGES = {

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
@@ -18,28 +16,48 @@ exports.default = async function (context) {
'node_modules'
)
removeDifferentArchNodeFiles(node_modules_path, '@libsql', arch === Arch.arm64 ? ['darwin-arm64'] : ['darwin-x64'])
keepPackageNodeFiles(node_modules_path, '@libsql', arch === Arch.arm64 ? ['darwin-arm64'] : ['darwin-x64'])
}
if (platform === 'linux') {
const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules')
const _arch = arch === Arch.arm64 ? ['linux-arm64-gnu', 'linux-arm64-musl'] : ['linux-x64-gnu', 'linux-x64-musl']
removeDifferentArchNodeFiles(node_modules_path, '@libsql', _arch)
keepPackageNodeFiles(node_modules_path, '@libsql', _arch)
}
if (platform === 'windows') {
const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules')
removeDifferentArchNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
if (arch === Arch.arm64) {
keepPackageNodeFiles(node_modules_path, '@strongtz', ['win32-arm64-msvc'])
keepPackageNodeFiles(node_modules_path, '@libsql', ['win32-arm64-msvc'])
}
if (arch === Arch.x64) {
keepPackageNodeFiles(node_modules_path, '@strongtz', ['win32-x64-msvc'])
keepPackageNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
}
}
}
function removeDifferentArchNodeFiles(nodeModulesPath, packageName, arch) {
/**
* 使用指定架构的 node_modules 文件
* @param {*} nodeModulesPath
* @param {*} packageName
* @param {*} arch
* @returns
*/
function keepPackageNodeFiles(nodeModulesPath, packageName, arch) {
const modulePath = path.join(nodeModulesPath, packageName)
if (!fs.existsSync(modulePath)) {
console.log(`[After Pack] Directory does not exist: ${modulePath}`)
return
}
const dirs = fs.readdirSync(modulePath)
dirs
.filter((dir) => !arch.includes(dir))
.forEach((dir) => {
fs.rmSync(path.join(modulePath, dir), { recursive: true, force: true })
console.log(`Removed dir: ${dir}`, arch)
console.log(`[After Pack] Removed dir: ${dir}`, arch)
})
}

View File

@@ -0,0 +1,23 @@
const fs = require('fs')
exports.default = function (buildResult) {
try {
console.log('[artifact build completed] rename artifact file...')
if (!buildResult.file.includes(' ')) {
return
}
let oldFilePath = buildResult.file
if (oldFilePath.includes('-portable') && !oldFilePath.includes('-x64') && !oldFilePath.includes('-arm64')) {
console.log('[artifact build completed] delete portable file:', oldFilePath)
fs.unlinkSync(oldFilePath)
return
}
const newfilePath = oldFilePath.replace(/ /g, '-')
fs.renameSync(oldFilePath, newfilePath)
buildResult.file = newfilePath
console.log(`[artifact build completed] rename file ${oldFilePath} to ${newfilePath} `)
} catch (error) {
console.error('Error renaming file:', error)
}
}

View File

@@ -33,6 +33,10 @@ async function downloadNpm(platform) {
'@libsql/win32-x64-msvc',
'https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.4.7.tgz'
)
downloadNpmPackage(
'@strongtz/win32-arm64-msvc',
'https://registry.npmjs.org/@strongtz/win32-arm64-msvc/-/win32-arm64-msvc-0.4.7.tgz'
)
}
}

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

@@ -1,58 +0,0 @@
// replaceSpaces.js
const fs = require('fs')
const path = require('path')
const directory = 'dist'
// 处理文件名中的空格
function replaceFileNames() {
fs.readdir(directory, (err, files) => {
if (err) throw err
files.forEach((file) => {
const oldPath = path.join(directory, file)
const newPath = path.join(directory, file.replace(/ /g, '-'))
fs.stat(oldPath, (err, stats) => {
if (err) throw err
if (stats.isFile() && oldPath !== newPath) {
fs.rename(oldPath, newPath, (err) => {
if (err) throw err
console.log(`Renamed: ${oldPath} -> ${newPath}`)
})
}
})
})
})
}
function replaceYmlContent() {
fs.readdir(directory, (err, files) => {
if (err) throw err
files.forEach((file) => {
if (path.extname(file).toLowerCase() === '.yml') {
const filePath = path.join(directory, file)
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) throw err
// 替换内容
const newContent = data.replace(/Cherry Studio-/g, 'Cherry-Studio-')
// 写回文件
fs.writeFile(filePath, newContent, 'utf8', (err) => {
if (err) throw err
console.log(`Updated content in: ${filePath}`)
})
})
}
})
})
}
// 执行两个操作
replaceFileNames()
replaceYmlContent()

130
scripts/update-i18n.ts Normal file
View File

@@ -0,0 +1,130 @@
/**
* OCOOL_API_KEY=sk-abcxxxxxxxxxxxxxxxxxxxxxxx123 ts-node scripts/update-i18n.ts
*/
// OCOOL API KEY
const OCOOL_API_KEY = process.env.OCOOL_API_KEY
const INDEX = [
// 语言的名称 代码 用来翻译的模型
{ name: 'France', code: 'fr-fr', model: 'qwen2.5-32b-instruct' },
{ name: 'Spanish', code: 'es-es', model: 'qwen2.5-32b-instruct' },
{ name: 'Portuguese', code: 'pt-pt', model: 'qwen2.5-72b-instruct' },
{ name: 'Greek', code: 'el-gr', model: 'qwen-turbo' }
]
const fs = require('fs')
import OpenAI from 'openai'
const zh = JSON.parse(fs.readFileSync('src/renderer/src/i18n/locales/zh-cn.json', 'utf8')) as object
const openai = new OpenAI({
apiKey: OCOOL_API_KEY,
baseURL: 'https://one.ocoolai.com/v1'
})
// 递归遍历翻译
async function translate(zh: object, obj: object, target: string, model: string, updateFile) {
const texts: { [key: string]: string } = {}
for (const e in zh) {
if (typeof zh[e] == 'object') {
// 遍历下一层
if (!obj[e] || typeof obj[e] != 'object') obj[e] = {}
await translate(zh[e], obj[e], target, model, updateFile)
} else {
// 加入到本层待翻译列表
if (!obj[e] || typeof obj[e] != 'string') {
texts[e] = zh[e]
}
}
}
if (Object.keys(texts).length > 0) {
const completion = await openai.chat.completions.create({
model: model,
response_format: { type: 'json_object' },
messages: [
{
role: 'user',
content: `
You are a robot specifically designed for translation tasks. As a model that has been extensively fine-tuned on Russian language corpora, you are proficient in using the Russian language.
Now, please output the translation based on the input content. The input will include both Chinese and English key values, and you should output the corresponding key values in the Russian language.
When translating, ensure that no key value is omitted, and maintain the accuracy and fluency of the translation. Pay attention to the capitalization rules in the output to match the source text, and especially pay attention to whether to capitalize the first letter of each word except for prepositions. For strings containing \`{{value}}\`, ensure that the format is not disrupted.
Output in JSON.
######################################################
INPUT
######################################################
${JSON.stringify({
confirm: '确定要备份数据吗?',
select_model: '选择模型',
title: '文件',
deeply_thought: '已深度思考(用时 {{secounds}} 秒)'
})}
######################################################
MAKE SURE TO OUTPUT IN Russian. DO NOT OUTPUT IN UNSPECIFIED LANGUAGE.
######################################################
`
},
{
role: 'assistant',
content: JSON.stringify({
confirm: 'Подтвердите резервное копирование данных?',
select_model: 'Выберите Модель',
title: 'Файл',
deeply_thought: 'Глубоко продумано (заняло {{seconds}} секунд)'
})
},
{
role: 'user',
content: `
You are a robot specifically designed for translation tasks. As a model that has been extensively fine-tuned on ${target} language corpora, you are proficient in using the ${target} language.
Now, please output the translation based on the input content. The input will include both Chinese and English key values, and you should output the corresponding key values in the ${target} language.
When translating, ensure that no key value is omitted, and maintain the accuracy and fluency of the translation. Pay attention to the capitalization rules in the output to match the source text, and especially pay attention to whether to capitalize the first letter of each word except for prepositions. For strings containing \`{{value}}\`, ensure that the format is not disrupted.
Output in JSON.
######################################################
INPUT
######################################################
${JSON.stringify(texts)}
######################################################
MAKE SURE TO OUTPUT IN ${target}. DO NOT OUTPUT IN UNSPECIFIED LANGUAGE.
######################################################
`
}
]
})
// 添加翻译后的键值,并打印错译漏译内容
try {
const result = JSON.parse(completion.choices[0].message.content!)
for (const e in texts) {
if (result[e] && typeof result[e] === 'string') {
obj[e] = result[e]
} else {
console.log('[warning]', `missing value "${e}" in ${target} translation`)
}
}
} catch (e) {
console.log('[error]', e)
for (const e in texts) {
console.log('[warning]', `missing value "${e}" in ${target} translation`)
}
}
}
// 删除多余的键值
for (const e in obj) {
if (!zh[e]) {
delete obj[e]
}
}
// 更新文件
updateFile()
}
;(async () => {
for (const { name, code, model } of INDEX) {
const obj = fs.existsSync(`src/renderer/src/i18n/translate/${code}.json`)
? JSON.parse(fs.readFileSync(`src/renderer/src/i18n/translate/${code}.json`, 'utf8'))
: {}
await translate(zh, obj, name, model, () => {
fs.writeFileSync(`src/renderer/src/i18n/translate/${code}.json`, JSON.stringify(obj, null, 2), 'utf8')
})
}
})()

View File

@@ -22,7 +22,8 @@ function downloadNpmPackage(packageName, url) {
console.log(`Extracting ${filename}...`)
execSync(`tar -xvf ${filename}`)
execSync(`rm -rf ${filename}`)
execSync(`mv package ${targetDir}`)
execSync(`mkdir -p ${targetDir}`)
execSync(`mv package/* ${targetDir}/`)
} catch (error) {
console.error(`Error processing ${packageName}: ${error.message}`)
if (fs.existsSync(filename)) {

16
src/@types/index.d.ts vendored
View File

@@ -1,16 +0,0 @@
export interface NodeAppType {
id: string
name: string
type: string
description?: string
author?: string
homepage?: string
repositoryUrl?: string
port?: number
installCommand?: string
buildCommand?: string
startCommand?: string
isInstalled: boolean
isRunning: boolean
url?: string
}

View File

@@ -0,0 +1,24 @@
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { KnowledgeBaseParams } from '@types'
import EmbeddingsFactory from './EmbeddingsFactory'
export default class Embeddings {
private sdk: BaseEmbeddings
constructor({ model, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams) {
this.sdk = EmbeddingsFactory.create({ model, apiKey, apiVersion, baseURL, dimensions } as KnowledgeBaseParams)
}
public async init(): Promise<void> {
return this.sdk.init()
}
public async getDimensions(): Promise<number> {
return this.sdk.getDimensions()
}
public async embedDocuments(texts: string[]): Promise<number[][]> {
return this.sdk.embedDocuments(texts)
}
public async embedQuery(text: string): Promise<number[]> {
return this.sdk.embedQuery(text)
}
}

View File

@@ -0,0 +1,38 @@
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings'
import { getInstanceName } from '@main/utils'
import { KnowledgeBaseParams } from '@types'
import VoyageEmbeddings from './VoyageEmbeddings'
export default class EmbeddingsFactory {
static create({ model, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams): BaseEmbeddings {
const batchSize = 10
if (model.includes('voyage')) {
return new VoyageEmbeddings({
modelName: model,
apiKey,
outputDimension: dimensions,
batchSize: 8
})
}
if (apiVersion !== undefined) {
return new AzureOpenAiEmbeddings({
azureOpenAIApiKey: apiKey,
azureOpenAIApiVersion: apiVersion,
azureOpenAIApiDeploymentName: model,
azureOpenAIApiInstanceName: getInstanceName(baseURL),
dimensions,
batchSize
})
}
return new OpenAiEmbeddings({
model,
apiKey,
dimensions,
batchSize,
configuration: { baseURL }
})
}
}

View File

@@ -0,0 +1,30 @@
import { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { VoyageEmbeddings as _VoyageEmbeddings } from '@langchain/community/embeddings/voyage'
export default class VoyageEmbeddings extends BaseEmbeddings {
private model: _VoyageEmbeddings
constructor(private readonly configuration?: ConstructorParameters<typeof _VoyageEmbeddings>[0]) {
super()
if (!this.configuration) this.configuration = {}
if (!this.configuration.modelName) this.configuration.modelName = 'voyage-3'
if (!this.configuration.outputDimension) {
throw new Error('You need to pass in the optional dimensions parameter for this model')
}
this.model = new _VoyageEmbeddings(this.configuration)
}
override async getDimensions(): Promise<number> {
if (!this.configuration?.outputDimension) {
throw new Error('You need to pass in the optional dimensions parameter for this model')
}
return this.configuration?.outputDimension
}
override async embedDocuments(texts: string[]): Promise<number[][]> {
return this.model.embedDocuments(texts)
}
override async embedQuery(text: string): Promise<number[]> {
return this.model.embedQuery(text)
}
}

View File

@@ -1,9 +1,15 @@
import { electronApp, optimizer } from '@electron-toolkit/utils'
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
import { IpcChannel } from '@shared/IpcChannel'
import { app, ipcMain } from 'electron'
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
import Logger from 'electron-log'
import { initSentry } from './integration/sentry'
import { registerIpc } from './ipc'
import { configManager } from './services/ConfigManager'
import mcpService from './services/MCPService'
import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient'
import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService'
@@ -21,6 +27,12 @@ if (!app.requestSingleInstanceLock()) {
// Set app user model id for windows
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
// Mac: Hide dock icon before window creation when launch to tray is set
const isLaunchToTray = configManager.getLaunchToTray()
if (isLaunchToTray) {
app.dock?.hide()
}
const mainWindow = windowService.createMainWindow()
new TrayService()
@@ -40,18 +52,43 @@ if (!app.requestSingleInstanceLock()) {
replaceDevtoolsFont(mainWindow)
if (process.env.NODE_ENV === 'development') {
installExtension(REDUX_DEVTOOLS)
installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS])
.then((name) => console.log(`Added Extension: ${name}`))
.catch((err) => console.log('An error occurred: ', err))
}
ipcMain.handle('system:getDeviceType', () => {
ipcMain.handle(IpcChannel.System_GetDeviceType, () => {
return process.platform === 'darwin' ? 'mac' : process.platform === 'win32' ? 'windows' : 'linux'
})
ipcMain.handle(IpcChannel.System_GetHostname, () => {
return require('os').hostname()
})
})
registerProtocolClient(app)
// macOS specific: handle protocol when app is already running
app.on('open-url', (event, url) => {
event.preventDefault()
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', () => {
app.on('second-instance', (_event, argv) => {
windowService.showMainWindow()
// Protocol handler for Windows/Linux
// The commandLine is an array of strings where the last item might be the URL
const url = argv.find((arg) => arg.startsWith(CHERRY_STUDIO_PROTOCOL + '://'))
if (url) handleProtocolUrl(url)
})
app.on('browser-window-created', (_, window) => {
@@ -62,6 +99,17 @@ if (!app.requestSingleInstanceLock()) {
app.isQuitting = true
})
app.on('will-quit', async () => {
// event.preventDefault()
try {
await mcpService.cleanup()
} catch (error) {
Logger.error('Error cleaning up MCP service:', error)
}
})
// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.
}
initSentry()

View File

@@ -0,0 +1,14 @@
interface CreateOAuthUrlArgs {
app: string;
}
declare function createOAuthUrl({ app }: CreateOAuthUrlArgs): Promise<string>;
declare function _dont_use_in_prod_createOAuthUrl({ app, }: CreateOAuthUrlArgs): Promise<string>;
interface DecryptSecretArgs {
app: string;
s: string;
}
declare function decryptSecret({ app, s }: DecryptSecretArgs): Promise<string>;
declare function _dont_use_in_prod_decryptSecret({ app, s, }: DecryptSecretArgs): Promise<string>;
export { type CreateOAuthUrlArgs, type DecryptSecretArgs, _dont_use_in_prod_createOAuthUrl, _dont_use_in_prod_decryptSecret, createOAuthUrl, decryptSecret };

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
import { configManager } from '@main/services/ConfigManager'
import * as Sentry from '@sentry/electron/main'
import { app } from 'electron'
export function initSentry() {
if (configManager.getEnableDataCollection()) {
Sentry.init({
dsn: 'https://194ceab3bd44e686bd3ebda9de3c20fd@o4509184559218688.ingest.us.sentry.io/4509184569442304',
environment: app.isPackaged ? 'production' : 'development'
})
}
}

View File

@@ -1,11 +1,15 @@
import fs from 'node:fs'
import { arch } from 'node:os'
import { isMac, isWin } from '@main/constant'
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
import { MCPServer, Shortcut, ThemeMode } from '@types'
import { IpcChannel } from '@shared/IpcChannel'
import { Shortcut, ThemeMode } from '@types'
import { BrowserWindow, ipcMain, session, shell } from 'electron'
import log from 'electron-log'
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
import { initSentry } from './integration/sentry'
import AppUpdater from './services/AppUpdater'
import BackupManager from './services/BackupManager'
import { configManager } from './services/ConfigManager'
@@ -15,36 +19,41 @@ import FileService from './services/FileService'
import FileStorage from './services/FileStorage'
import { GeminiService } from './services/GeminiService'
import KnowledgeService from './services/KnowledgeService'
import MCPService from './services/MCPService'
import mcpService from './services/MCPService'
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 { windowService } from './services/WindowService'
import { getResourcePath } from './utils'
import { decrypt, encrypt } from './utils/aes'
import { getFilesDir } from './utils/file'
import { getConfigDir, getFilesDir } from './utils/file'
import { compress, decompress } from './utils/zip'
import NodeAppService from './services/NodeAppService'
const fileManager = new FileStorage()
const backupManager = new BackupManager()
const exportService = new ExportService(fileManager)
const mcpService = new MCPService()
const obsidianVaultService = new ObsidianVaultService()
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const appUpdater = new AppUpdater(mainWindow)
ipcMain.handle('app:info', () => ({
ipcMain.handle(IpcChannel.App_Info, () => ({
version: app.getVersion(),
isPackaged: app.isPackaged,
appPath: app.getAppPath(),
filesPath: getFilesDir(),
configPath: getConfigDir(),
appDataPath: app.getPath('userData'),
resourcesPath: getResourcePath(),
logsPath: log.transports.file.getFile().path
logsPath: log.transports.file.getFile().path,
arch: arch(),
isPortable: isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env
}))
ipcMain.handle('app:proxy', async (_, proxy: string) => {
ipcMain.handle(IpcChannel.App_Proxy, async (_, proxy: string) => {
let proxyConfig: ProxyConfig
if (proxy === 'system') {
@@ -58,34 +67,59 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
await proxyManager.configureProxy(proxyConfig)
})
ipcMain.handle('app:reload', () => mainWindow.reload())
ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url))
ipcMain.handle(IpcChannel.App_Reload, () => mainWindow.reload())
ipcMain.handle(IpcChannel.Open_Website, (_, url: string) => shell.openExternal(url))
// Update
ipcMain.handle('app:show-update-dialog', () => appUpdater.showUpdateDialog(mainWindow))
ipcMain.handle(IpcChannel.App_ShowUpdateDialog, () => appUpdater.showUpdateDialog(mainWindow))
// language
ipcMain.handle('app:set-language', (_, language) => {
ipcMain.handle(IpcChannel.App_SetLanguage, (_, language) => {
configManager.setLanguage(language)
})
// launch on boot
ipcMain.handle(IpcChannel.App_SetLaunchOnBoot, (_, openAtLogin: boolean) => {
// Set login item settings for windows and mac
// linux is not supported because it requires more file operations
if (isWin || isMac) {
app.setLoginItemSettings({ openAtLogin })
}
})
// launch to tray
ipcMain.handle(IpcChannel.App_SetLaunchToTray, (_, isActive: boolean) => {
configManager.setLaunchToTray(isActive)
})
// tray
ipcMain.handle('app:set-tray', (_, isActive: boolean) => {
ipcMain.handle(IpcChannel.App_SetTray, (_, isActive: boolean) => {
configManager.setTray(isActive)
})
ipcMain.handle('app:restart-tray', () => TrayService.getInstance().restartTray())
// to tray on close
ipcMain.handle(IpcChannel.App_SetTrayOnClose, (_, isActive: boolean) => {
configManager.setTrayOnClose(isActive)
})
ipcMain.handle('config:set', (_, key: string, value: any) => {
// auto update
ipcMain.handle(IpcChannel.App_SetAutoUpdate, (_, isActive: boolean) => {
appUpdater.setAutoUpdate(isActive)
configManager.setAutoUpdate(isActive)
})
ipcMain.handle(IpcChannel.App_RestartTray, () => TrayService.getInstance().restartTray())
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any) => {
configManager.set(key, value)
})
ipcMain.handle('config:get', (_, key: string) => {
ipcMain.handle(IpcChannel.Config_Get, (_, key: string) => {
return configManager.get(key)
})
// theme
ipcMain.handle('app:set-theme', (event, theme: ThemeMode) => {
ipcMain.handle(IpcChannel.App_SetTheme, (event, theme: ThemeMode) => {
if (theme === configManager.getTheme()) return
configManager.setTheme(theme)
@@ -96,7 +130,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// 向其他窗口广播主题变化
windows.forEach((win) => {
if (win.webContents.id !== senderWindowId) {
win.webContents.send('theme:change', theme)
win.webContents.send(IpcChannel.ThemeChange, theme)
}
})
@@ -104,8 +138,24 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
})
// custom css
ipcMain.handle(IpcChannel.App_SetCustomCss, (event, css: string) => {
if (css === configManager.getCustomCss()) return
configManager.setCustomCss(css)
// Broadcast to all windows including the mini window
const senderWindowId = event.sender.id
const windows = BrowserWindow.getAllWindows()
// 向其他窗口广播主题变化
windows.forEach((win) => {
if (win.webContents.id !== senderWindowId) {
win.webContents.send('custom-css:update', css)
}
})
})
// clear cache
ipcMain.handle('app:clear-cache', async () => {
ipcMain.handle(IpcChannel.App_ClearCache, async () => {
const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
try {
@@ -127,8 +177,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
})
// check for update
ipcMain.handle('app:check-for-update', async () => {
ipcMain.handle(IpcChannel.App_CheckForUpdate, async () => {
// 在 Windows 上,如果架构是 arm64则不检查更新
if (isWin && (arch().includes('arm') || 'PORTABLE_EXECUTABLE_DIR' in process.env)) {
return {
currentVersion: app.getVersion(),
updateInfo: null
}
}
const update = await appUpdater.autoUpdater.checkForUpdates()
return {
currentVersion: appUpdater.autoUpdater.currentVersion,
updateInfo: update?.updateInfo
@@ -136,60 +195,51 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
})
// zip
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
ipcMain.handle(IpcChannel.Zip_Compress, (_, text: string) => compress(text))
ipcMain.handle(IpcChannel.Zip_Decompress, (_, text: Buffer) => decompress(text))
// backup
ipcMain.handle('backup:backup', backupManager.backup)
ipcMain.handle('backup:restore', backupManager.restore)
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
ipcMain.handle('backup:listWebdavFiles', backupManager.listWebdavFiles)
ipcMain.handle(IpcChannel.Backup_Backup, backupManager.backup)
ipcMain.handle(IpcChannel.Backup_Restore, backupManager.restore)
ipcMain.handle(IpcChannel.Backup_BackupToWebdav, backupManager.backupToWebdav)
ipcMain.handle(IpcChannel.Backup_RestoreFromWebdav, backupManager.restoreFromWebdav)
ipcMain.handle(IpcChannel.Backup_ListWebdavFiles, backupManager.listWebdavFiles)
ipcMain.handle(IpcChannel.Backup_CheckConnection, backupManager.checkConnection)
ipcMain.handle(IpcChannel.Backup_CreateDirectory, backupManager.createDirectory)
ipcMain.handle(IpcChannel.Backup_DeleteWebdavFile, backupManager.deleteWebdavFile)
// file
ipcMain.handle('file:open', fileManager.open)
ipcMain.handle('file:openPath', fileManager.openPath)
ipcMain.handle('file:save', fileManager.save)
ipcMain.handle('file:select', fileManager.selectFile)
ipcMain.handle('file:upload', fileManager.uploadFile)
ipcMain.handle('file:clear', fileManager.clear)
ipcMain.handle('file:read', fileManager.readFile)
ipcMain.handle('file:delete', fileManager.deleteFile)
ipcMain.handle('file:get', fileManager.getFile)
ipcMain.handle('file:selectFolder', fileManager.selectFolder)
ipcMain.handle('file:create', fileManager.createTempFile)
ipcMain.handle('file:write', fileManager.writeFile)
ipcMain.handle('file:saveImage', fileManager.saveImage)
ipcMain.handle('file:base64Image', fileManager.base64Image)
ipcMain.handle('file:download', fileManager.downloadFile)
ipcMain.handle('file:copy', fileManager.copyFile)
ipcMain.handle('file:binaryFile', fileManager.binaryFile)
ipcMain.handle(IpcChannel.File_Open, fileManager.open)
ipcMain.handle(IpcChannel.File_OpenPath, fileManager.openPath)
ipcMain.handle(IpcChannel.File_Save, fileManager.save)
ipcMain.handle(IpcChannel.File_Select, fileManager.selectFile)
ipcMain.handle(IpcChannel.File_Upload, fileManager.uploadFile)
ipcMain.handle(IpcChannel.File_Clear, fileManager.clear)
ipcMain.handle(IpcChannel.File_Read, fileManager.readFile)
ipcMain.handle(IpcChannel.File_Delete, fileManager.deleteFile)
ipcMain.handle(IpcChannel.File_Get, fileManager.getFile)
ipcMain.handle(IpcChannel.File_SelectFolder, fileManager.selectFolder)
ipcMain.handle(IpcChannel.File_Create, fileManager.createTempFile)
ipcMain.handle(IpcChannel.File_Write, fileManager.writeFile)
ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage)
ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image)
ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile)
ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile)
ipcMain.handle(IpcChannel.File_BinaryFile, fileManager.binaryFile)
// fs
ipcMain.handle('fs:read', FileService.readFile)
// minapp
ipcMain.handle('minapp', (_, args) => {
windowService.createMinappWindow({
url: args.url,
parent: mainWindow,
windowOptions: {
...mainWindow.getBounds(),
...args.windowOptions
}
})
})
ipcMain.handle(IpcChannel.Fs_Read, FileService.readFile)
// export
ipcMain.handle('export:word', exportService.exportToWord)
ipcMain.handle(IpcChannel.Export_Word, exportService.exportToWord)
// open path
ipcMain.handle('open:path', async (_, path: string) => {
ipcMain.handle(IpcChannel.Open_Path, async (_, path: string) => {
await shell.openPath(path)
})
// shortcuts
ipcMain.handle('shortcuts:update', (_, shortcuts: Shortcut[]) => {
ipcMain.handle(IpcChannel.Shortcuts_Update, (_, shortcuts: Shortcut[]) => {
configManager.setShortcuts(shortcuts)
// Refresh shortcuts registration
if (mainWindow) {
@@ -199,20 +249,20 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
})
// knowledge base
ipcMain.handle('knowledge-base:create', KnowledgeService.create)
ipcMain.handle('knowledge-base:reset', KnowledgeService.reset)
ipcMain.handle('knowledge-base:delete', KnowledgeService.delete)
ipcMain.handle('knowledge-base:add', KnowledgeService.add)
ipcMain.handle('knowledge-base:remove', KnowledgeService.remove)
ipcMain.handle('knowledge-base:search', KnowledgeService.search)
ipcMain.handle('knowledge-base:rerank', KnowledgeService.rerank)
ipcMain.handle(IpcChannel.KnowledgeBase_Create, KnowledgeService.create)
ipcMain.handle(IpcChannel.KnowledgeBase_Reset, KnowledgeService.reset)
ipcMain.handle(IpcChannel.KnowledgeBase_Delete, KnowledgeService.delete)
ipcMain.handle(IpcChannel.KnowledgeBase_Add, KnowledgeService.add)
ipcMain.handle(IpcChannel.KnowledgeBase_Remove, KnowledgeService.remove)
ipcMain.handle(IpcChannel.KnowledgeBase_Search, KnowledgeService.search)
ipcMain.handle(IpcChannel.KnowledgeBase_Rerank, KnowledgeService.rerank)
// window
ipcMain.handle('window:set-minimum-size', (_, width: number, height: number) => {
ipcMain.handle(IpcChannel.Windows_SetMinimumSize, (_, width: number, height: number) => {
mainWindow?.setMinimumSize(width, height)
})
ipcMain.handle('window:reset-minimum-size', () => {
ipcMain.handle(IpcChannel.Windows_ResetMinimumSize, () => {
mainWindow?.setMinimumSize(1080, 600)
const [width, height] = mainWindow?.getSize() ?? [1080, 600]
if (width < 1080) {
@@ -221,120 +271,79 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
})
// gemini
ipcMain.handle('gemini:upload-file', GeminiService.uploadFile)
ipcMain.handle('gemini:base64-file', GeminiService.base64File)
ipcMain.handle('gemini:retrieve-file', GeminiService.retrieveFile)
ipcMain.handle('gemini:list-files', GeminiService.listFiles)
ipcMain.handle('gemini:delete-file', GeminiService.deleteFile)
ipcMain.handle(IpcChannel.Gemini_UploadFile, GeminiService.uploadFile)
ipcMain.handle(IpcChannel.Gemini_Base64File, GeminiService.base64File)
ipcMain.handle(IpcChannel.Gemini_RetrieveFile, GeminiService.retrieveFile)
ipcMain.handle(IpcChannel.Gemini_ListFiles, GeminiService.listFiles)
ipcMain.handle(IpcChannel.Gemini_DeleteFile, GeminiService.deleteFile)
// mini window
ipcMain.handle('miniwindow:show', () => windowService.showMiniWindow())
ipcMain.handle('miniwindow:hide', () => windowService.hideMiniWindow())
ipcMain.handle('miniwindow:close', () => windowService.closeMiniWindow())
ipcMain.handle('miniwindow:toggle', () => windowService.toggleMiniWindow())
ipcMain.handle(IpcChannel.MiniWindow_Show, () => windowService.showMiniWindow())
ipcMain.handle(IpcChannel.MiniWindow_Hide, () => windowService.hideMiniWindow())
ipcMain.handle(IpcChannel.MiniWindow_Close, () => windowService.closeMiniWindow())
ipcMain.handle(IpcChannel.MiniWindow_Toggle, () => windowService.toggleMiniWindow())
ipcMain.handle(IpcChannel.MiniWindow_SetPin, (_, isPinned) => windowService.setPinMiniWindow(isPinned))
// aes
ipcMain.handle('aes:encrypt', (_, text: string, secretKey: string, iv: string) => encrypt(text, secretKey, iv))
ipcMain.handle('aes:decrypt', (_, encryptedData: string, iv: string, secretKey: string) =>
ipcMain.handle(IpcChannel.Aes_Encrypt, (_, text: string, secretKey: string, iv: string) =>
encrypt(text, secretKey, iv)
)
ipcMain.handle(IpcChannel.Aes_Decrypt, (_, encryptedData: string, iv: string, secretKey: string) =>
decrypt(encryptedData, iv, secretKey)
)
// Register MCP handlers
ipcMain.on('mcp:servers-from-renderer', (_, servers) => mcpService.setServers(servers))
ipcMain.handle('mcp:list-servers', async () => mcpService.listAvailableServices())
ipcMain.handle('mcp:add-server', async (_, server: MCPServer) => mcpService.addServer(server))
ipcMain.handle('mcp:update-server', async (_, server: MCPServer) => mcpService.updateServer(server))
ipcMain.handle('mcp:delete-server', async (_, serverName: string) => mcpService.deleteServer(serverName))
ipcMain.handle('mcp:set-server-active', async (_, { name, isActive }) =>
mcpService.setServerActive({ name, isActive })
)
ipcMain.handle(IpcChannel.Mcp_RemoveServer, mcpService.removeServer)
ipcMain.handle(IpcChannel.Mcp_RestartServer, mcpService.restartServer)
ipcMain.handle(IpcChannel.Mcp_StopServer, mcpService.stopServer)
ipcMain.handle(IpcChannel.Mcp_ListTools, mcpService.listTools)
ipcMain.handle(IpcChannel.Mcp_CallTool, mcpService.callTool)
ipcMain.handle(IpcChannel.Mcp_ListPrompts, mcpService.listPrompts)
ipcMain.handle(IpcChannel.Mcp_GetPrompt, mcpService.getPrompt)
ipcMain.handle(IpcChannel.Mcp_ListResources, mcpService.listResources)
ipcMain.handle(IpcChannel.Mcp_GetResource, mcpService.getResource)
ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo)
// According to preload, this should take no parameters, but our implementation accepts
// an optional serverName for better flexibility
ipcMain.handle('mcp:list-tools', async (_, serverName?: string) => mcpService.listTools(serverName))
ipcMain.handle('mcp:call-tool', async (_, params: { client: string; name: string; args: any }) =>
mcpService.callTool(params)
)
ipcMain.handle('mcp:cleanup', async () => mcpService.cleanup())
// Shell API
ipcMain.handle('shell:openExternal', async (_, url: string) => {
try {
log.info(`Opening external URL: ${url}`)
return await shell.openExternal(url)
} catch (error) {
log.error('Error opening external URL:', error)
throw error
}
})
ipcMain.handle('app:is-binary-exist', (_, name: string) => isBinaryExists(name))
ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name))
ipcMain.handle('app:install-uv-binary', () => runInstallScript('install-uv.js'))
ipcMain.handle('app:install-bun-binary', () => runInstallScript('install-bun.js'))
// Listen for changes in MCP servers and notify renderer
mcpService.on('servers-updated', (servers) => {
mainWindow?.webContents.send('mcp:servers-updated', servers)
})
app.on('before-quit', () => mcpService.cleanup())
ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name))
ipcMain.handle(IpcChannel.App_InstallUvBinary, () => runInstallScript('install-uv.js'))
ipcMain.handle(IpcChannel.App_InstallBunBinary, () => runInstallScript('install-bun.js'))
//copilot
ipcMain.handle('copilot:get-auth-message', CopilotService.getAuthMessage)
ipcMain.handle('copilot:get-copilot-token', CopilotService.getCopilotToken)
ipcMain.handle('copilot:save-copilot-token', CopilotService.saveCopilotToken)
ipcMain.handle('copilot:get-token', CopilotService.getToken)
ipcMain.handle('copilot:logout', CopilotService.logout)
ipcMain.handle('copilot:get-user', CopilotService.getUser)
ipcMain.handle(IpcChannel.Copilot_GetAuthMessage, CopilotService.getAuthMessage)
ipcMain.handle(IpcChannel.Copilot_GetCopilotToken, CopilotService.getCopilotToken)
ipcMain.handle(IpcChannel.Copilot_SaveCopilotToken, CopilotService.saveCopilotToken)
ipcMain.handle(IpcChannel.Copilot_GetToken, CopilotService.getToken)
ipcMain.handle(IpcChannel.Copilot_Logout, CopilotService.logout)
ipcMain.handle(IpcChannel.Copilot_GetUser, CopilotService.getUser)
// Node app management
const nodeAppService = NodeAppService.getInstance()
ipcMain.handle('nodeapp:list', async () => await nodeAppService.getAllApps())
ipcMain.handle('nodeapp:add', async (_, app) => await nodeAppService.addApp(app))
ipcMain.handle('nodeapp:install', async (_, appId) => await nodeAppService.installApp(appId))
ipcMain.handle('nodeapp:update', async (_, appId) => await nodeAppService.updateApp(appId))
ipcMain.handle('nodeapp:start', async (_, appId) => await nodeAppService.startApp(appId))
ipcMain.handle('nodeapp:stop', async (_, appId) => await nodeAppService.stopApp(appId))
ipcMain.handle('nodeapp:uninstall', async (_, appId) => await nodeAppService.uninstallApp(appId))
ipcMain.handle('nodeapp:deploy-zip', async (_, zipPath, options) => await nodeAppService.deployFromZip(zipPath, options))
ipcMain.handle('nodeapp:deploy-git', async (_, repoUrl, options) => await nodeAppService.deployFromGit(repoUrl, options))
ipcMain.handle('nodeapp:check-node', async () => {
const isNodeInstalled = await isBinaryExists('node')
return isNodeInstalled
// Obsidian service
ipcMain.handle(IpcChannel.Obsidian_GetVaults, () => {
return obsidianVaultService.getVaults()
})
ipcMain.handle('nodeapp:install-node', async () => {
return await nodeAppService.installNodeJs()
ipcMain.handle(IpcChannel.Obsidian_GetFiles, (_event, vaultName) => {
return obsidianVaultService.getFilesByVaultName(vaultName)
})
// Listen for changes in Node.js apps and notify renderer
nodeAppService.on('apps-updated', (apps) => {
mainWindow?.webContents.send('nodeapp:updated', apps)
// nutstore
ipcMain.handle(IpcChannel.Nutstore_GetSsoUrl, NutstoreService.getNutstoreSSOUrl)
ipcMain.handle(IpcChannel.Nutstore_DecryptToken, (_, token: string) => NutstoreService.decryptToken(token))
ipcMain.handle(IpcChannel.Nutstore_GetDirectoryContents, (_, token: string, path: string) =>
NutstoreService.getDirectoryContents(token, path)
)
// search window
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)
})
app.on('before-quit', () => nodeAppService.cleanup())
// 运行简单命令
ipcMain.handle('app:run-command', async (_, command: string) => {
try {
const { execSync } = require('child_process')
const result = execSync(command).toString()
return result
} catch (error) {
log.error('Error running command:', error)
throw error
}
})
// sentry
ipcMain.handle(IpcChannel.Sentry_Init, () => initSentry())
}

View File

@@ -1,6 +1,6 @@
import * as fs from 'node:fs'
import { JsonLoader } from '@llm-tools/embedjs'
import { JsonLoader } from '@cherrystudio/embedjs'
/**
* Drafts 应用导出的笔记文件加载器

View File

@@ -1,6 +1,6 @@
import { BaseLoader } from '@cherrystudio/embedjs-interfaces'
import { cleanString } from '@cherrystudio/embedjs-utils'
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
import { BaseLoader } from '@llm-tools/embedjs-interfaces'
import { cleanString } from '@llm-tools/embedjs-utils'
import { getTempDir } from '@main/utils/file'
import Logger from 'electron-log'
import EPub from 'epub'

View File

@@ -1,8 +1,8 @@
import * as fs from 'node:fs'
import { JsonLoader, LocalPathLoader, RAGApplication, TextLoader } from '@llm-tools/embedjs'
import type { AddLoaderReturn } from '@llm-tools/embedjs-interfaces'
import { WebLoader } from '@llm-tools/embedjs-loader-web'
import { JsonLoader, LocalPathLoader, RAGApplication, TextLoader } from '@cherrystudio/embedjs'
import type { AddLoaderReturn } from '@cherrystudio/embedjs-interfaces'
import { WebLoader } from '@cherrystudio/embedjs-loader-web'
import { LoaderReturn } from '@shared/config/types'
import { FileType, KnowledgeBaseParams } from '@types'
import Logger from 'electron-log'

View File

@@ -1,6 +1,6 @@
import { BaseLoader } from '@cherrystudio/embedjs-interfaces'
import { cleanString } from '@cherrystudio/embedjs-utils'
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
import { BaseLoader } from '@llm-tools/embedjs-interfaces'
import { cleanString } from '@llm-tools/embedjs-utils'
import md5 from 'md5'
import { OfficeParserConfig, parseOfficeAsync } from 'officeparser'

View File

@@ -0,0 +1,374 @@
// Brave Search MCP Server
// port https://github.com/modelcontextprotocol/servers/blob/main/src/brave-search/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
const WEB_SEARCH_TOOL: Tool = {
name: 'brave_web_search',
description:
'Performs a web search using the Brave Search API, ideal for general queries, news, articles, and online content. ' +
'Use this for broad information gathering, recent events, or when you need diverse web sources. ' +
'Supports pagination, content filtering, and freshness controls. ' +
'Maximum 20 results per request, with offset for pagination. ',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query (max 400 chars, 50 words)'
},
count: {
type: 'number',
description: 'Number of results (1-20, default 10)',
default: 10
},
offset: {
type: 'number',
description: 'Pagination offset (max 9, default 0)',
default: 0
}
},
required: ['query']
}
}
const LOCAL_SEARCH_TOOL: Tool = {
name: 'brave_local_search',
description:
"Searches for local businesses and places using Brave's Local Search API. " +
'Best for queries related to physical locations, businesses, restaurants, services, etc. ' +
'Returns detailed information including:\n' +
'- Business names and addresses\n' +
'- Ratings and review counts\n' +
'- Phone numbers and opening hours\n' +
"Use this when the query implies 'near me' or mentions specific locations. " +
'Automatically falls back to web search if no local results are found.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: "Local search query (e.g. 'pizza near Central Park')"
},
count: {
type: 'number',
description: 'Number of results (1-20, default 5)',
default: 5
}
},
required: ['query']
}
}
const RATE_LIMIT = {
perSecond: 1,
perMonth: 15000
}
const requestCount = {
second: 0,
month: 0,
lastReset: Date.now()
}
function checkRateLimit() {
const now = Date.now()
if (now - requestCount.lastReset > 1000) {
requestCount.second = 0
requestCount.lastReset = now
}
if (requestCount.second >= RATE_LIMIT.perSecond || requestCount.month >= RATE_LIMIT.perMonth) {
throw new Error('Rate limit exceeded')
}
requestCount.second++
requestCount.month++
}
interface BraveWeb {
web?: {
results?: Array<{
title: string
description: string
url: string
language?: string
published?: string
rank?: number
}>
}
locations?: {
results?: Array<{
id: string // Required by API
title?: string
}>
}
}
interface BraveLocation {
id: string
name: string
address: {
streetAddress?: string
addressLocality?: string
addressRegion?: string
postalCode?: string
}
coordinates?: {
latitude: number
longitude: number
}
phone?: string
rating?: {
ratingValue?: number
ratingCount?: number
}
openingHours?: string[]
priceRange?: string
}
interface BravePoiResponse {
results: BraveLocation[]
}
interface BraveDescription {
descriptions: { [id: string]: string }
}
function isBraveWebSearchArgs(args: unknown): args is { query: string; count?: number } {
return (
typeof args === 'object' &&
args !== null &&
'query' in args &&
typeof (args as { query: string }).query === 'string'
)
}
function isBraveLocalSearchArgs(args: unknown): args is { query: string; count?: number } {
return (
typeof args === 'object' &&
args !== null &&
'query' in args &&
typeof (args as { query: string }).query === 'string'
)
}
async function performWebSearch(apiKey: string, query: string, count: number = 10, offset: number = 0) {
checkRateLimit()
const url = new URL('https://api.search.brave.com/res/v1/web/search')
url.searchParams.set('q', query)
url.searchParams.set('count', Math.min(count, 20).toString()) // API limit
url.searchParams.set('offset', offset.toString())
const response = await fetch(url, {
headers: {
Accept: 'application/json',
'Accept-Encoding': 'gzip',
'X-Subscription-Token': apiKey
}
})
if (!response.ok) {
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
}
const data = (await response.json()) as BraveWeb
// Extract just web results
const results = (data.web?.results || []).map((result) => ({
title: result.title || '',
description: result.description || '',
url: result.url || ''
}))
return results.map((r) => `Title: ${r.title}\nDescription: ${r.description}\nURL: ${r.url}`).join('\n\n')
}
async function performLocalSearch(apiKey: string, query: string, count: number = 5) {
checkRateLimit()
// Initial search to get location IDs
const webUrl = new URL('https://api.search.brave.com/res/v1/web/search')
webUrl.searchParams.set('q', query)
webUrl.searchParams.set('search_lang', 'en')
webUrl.searchParams.set('result_filter', 'locations')
webUrl.searchParams.set('count', Math.min(count, 20).toString())
const webResponse = await fetch(webUrl, {
headers: {
Accept: 'application/json',
'Accept-Encoding': 'gzip',
'X-Subscription-Token': apiKey
}
})
if (!webResponse.ok) {
throw new Error(`Brave API error: ${webResponse.status} ${webResponse.statusText}\n${await webResponse.text()}`)
}
const webData = (await webResponse.json()) as BraveWeb
const locationIds =
webData.locations?.results?.filter((r): r is { id: string; title?: string } => r.id != null).map((r) => r.id) || []
if (locationIds.length === 0) {
return performWebSearch(apiKey, query, count) // Fallback to web search
}
// Get POI details and descriptions in parallel
const [poisData, descriptionsData] = await Promise.all([
getPoisData(apiKey, locationIds),
getDescriptionsData(apiKey, locationIds)
])
return formatLocalResults(poisData, descriptionsData)
}
async function getPoisData(apiKey: string, ids: string[]): Promise<BravePoiResponse> {
checkRateLimit()
const url = new URL('https://api.search.brave.com/res/v1/local/pois')
ids.filter(Boolean).forEach((id) => url.searchParams.append('ids', id))
const response = await fetch(url, {
headers: {
Accept: 'application/json',
'Accept-Encoding': 'gzip',
'X-Subscription-Token': apiKey
}
})
if (!response.ok) {
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
}
const poisResponse = (await response.json()) as BravePoiResponse
return poisResponse
}
async function getDescriptionsData(apiKey: string, ids: string[]): Promise<BraveDescription> {
checkRateLimit()
const url = new URL('https://api.search.brave.com/res/v1/local/descriptions')
ids.filter(Boolean).forEach((id) => url.searchParams.append('ids', id))
const response = await fetch(url, {
headers: {
Accept: 'application/json',
'Accept-Encoding': 'gzip',
'X-Subscription-Token': apiKey
}
})
if (!response.ok) {
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`)
}
const descriptionsData = (await response.json()) as BraveDescription
return descriptionsData
}
function formatLocalResults(poisData: BravePoiResponse, descData: BraveDescription): string {
return (
(poisData.results || [])
.map((poi) => {
const address =
[
poi.address?.streetAddress ?? '',
poi.address?.addressLocality ?? '',
poi.address?.addressRegion ?? '',
poi.address?.postalCode ?? ''
]
.filter((part) => part !== '')
.join(', ') || 'N/A'
return `Name: ${poi.name}
Address: ${address}
Phone: ${poi.phone || 'N/A'}
Rating: ${poi.rating?.ratingValue ?? 'N/A'} (${poi.rating?.ratingCount ?? 0} reviews)
Price Range: ${poi.priceRange || 'N/A'}
Hours: ${(poi.openingHours || []).join(', ') || 'N/A'}
Description: ${descData.descriptions[poi.id] || 'No description available'}
`
})
.join('\n---\n') || 'No local results found'
)
}
class BraveSearchServer {
public server: Server
private apiKey: string
constructor(apiKey: string) {
if (!apiKey) {
throw new Error('BRAVE_API_KEY is required for Brave Search MCP server')
}
this.apiKey = apiKey
this.server = new Server(
{
name: 'brave-search-server',
version: '0.1.0'
},
{
capabilities: {
tools: {}
}
}
)
this.initialize()
}
initialize() {
// Tool handlers
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [WEB_SEARCH_TOOL, LOCAL_SEARCH_TOOL]
}))
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params
if (!args) {
throw new Error('No arguments provided')
}
switch (name) {
case 'brave_web_search': {
if (!isBraveWebSearchArgs(args)) {
throw new Error('Invalid arguments for brave_web_search')
}
const { query, count = 10 } = args
const results = await performWebSearch(this.apiKey, query, count)
return {
content: [{ type: 'text', text: results }],
isError: false
}
}
case 'brave_local_search': {
if (!isBraveLocalSearchArgs(args)) {
throw new Error('Invalid arguments for brave_local_search')
}
const { query, count = 5 } = args
const results = await performLocalSearch(this.apiKey, query, count)
return {
content: [{ type: 'text', text: results }],
isError: false
}
}
default:
return {
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
isError: true
}
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
}
}
})
}
}
export default BraveSearchServer

View File

@@ -0,0 +1,32 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import Logger from 'electron-log'
import BraveSearchServer from './brave-search'
import FetchServer from './fetch'
import FileSystemServer from './filesystem'
import MemoryServer from './memory'
import ThinkingServer from './sequentialthinking'
export function createInMemoryMCPServer(name: string, args: string[] = [], envs: Record<string, string> = {}): Server {
Logger.info(`[MCP] Creating in-memory MCP server: ${name} with args: ${args} and envs: ${JSON.stringify(envs)}`)
switch (name) {
case '@cherry/memory': {
const envPath = envs.MEMORY_FILE_PATH
return new MemoryServer(envPath).server
}
case '@cherry/sequentialthinking': {
return new ThinkingServer().server
}
case '@cherry/brave-search': {
return new BraveSearchServer(envs.BRAVE_API_KEY).server
}
case '@cherry/fetch': {
return new FetchServer().server
}
case '@cherry/filesystem': {
return new FileSystemServer(args).server
}
default:
throw new Error(`Unknown in-memory MCP server: ${name}`)
}
}

View File

@@ -0,0 +1,236 @@
// port https://github.com/zcaceres/fetch-mcp/blob/main/src/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import { JSDOM } from 'jsdom'
import TurndownService from 'turndown'
import { z } from 'zod'
export const RequestPayloadSchema = z.object({
url: z.string().url(),
headers: z.record(z.string()).optional()
})
export type RequestPayload = z.infer<typeof RequestPayloadSchema>
export class Fetcher {
private static async _fetch({ url, headers }: RequestPayload): Promise<Response> {
try {
const response = await fetch(url, {
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
...headers
}
})
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
return response
} catch (e: unknown) {
if (e instanceof Error) {
throw new Error(`Failed to fetch ${url}: ${e.message}`)
} else {
throw new Error(`Failed to fetch ${url}: Unknown error`)
}
}
}
static async html(requestPayload: RequestPayload) {
try {
const response = await this._fetch(requestPayload)
const html = await response.text()
return { content: [{ type: 'text', text: html }], isError: false }
} catch (error) {
return {
content: [{ type: 'text', text: (error as Error).message }],
isError: true
}
}
}
static async json(requestPayload: RequestPayload) {
try {
const response = await this._fetch(requestPayload)
const json = await response.json()
return {
content: [{ type: 'text', text: JSON.stringify(json) }],
isError: false
}
} catch (error) {
return {
content: [{ type: 'text', text: (error as Error).message }],
isError: true
}
}
}
static async txt(requestPayload: RequestPayload) {
try {
const response = await this._fetch(requestPayload)
const html = await response.text()
const dom = new JSDOM(html)
const document = dom.window.document
const scripts = document.getElementsByTagName('script')
const styles = document.getElementsByTagName('style')
Array.from(scripts).forEach((script: any) => script.remove())
Array.from(styles).forEach((style: any) => style.remove())
const text = document.body.textContent || ''
const normalizedText = text.replace(/\s+/g, ' ').trim()
return {
content: [{ type: 'text', text: normalizedText }],
isError: false
}
} catch (error) {
return {
content: [{ type: 'text', text: (error as Error).message }],
isError: true
}
}
}
static async markdown(requestPayload: RequestPayload) {
try {
const response = await this._fetch(requestPayload)
const html = await response.text()
const turndownService = new TurndownService()
const markdown = turndownService.turndown(html)
return { content: [{ type: 'text', text: markdown }], isError: false }
} catch (error) {
return {
content: [{ type: 'text', text: (error as Error).message }],
isError: true
}
}
}
}
const server = new Server(
{
name: 'zcaceres/fetch',
version: '0.1.0'
},
{
capabilities: {
resources: {},
tools: {}
}
}
)
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'fetch_html',
description: 'Fetch a website and return the content as HTML',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL of the website to fetch'
},
headers: {
type: 'object',
description: 'Optional headers to include in the request'
}
},
required: ['url']
}
},
{
name: 'fetch_markdown',
description: 'Fetch a website and return the content as Markdown',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL of the website to fetch'
},
headers: {
type: 'object',
description: 'Optional headers to include in the request'
}
},
required: ['url']
}
},
{
name: 'fetch_txt',
description: 'Fetch a website, return the content as plain text (no HTML)',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL of the website to fetch'
},
headers: {
type: 'object',
description: 'Optional headers to include in the request'
}
},
required: ['url']
}
},
{
name: 'fetch_json',
description: 'Fetch a JSON file from a URL',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL of the JSON to fetch'
},
headers: {
type: 'object',
description: 'Optional headers to include in the request'
}
},
required: ['url']
}
}
]
}
})
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { arguments: args } = request.params
const validatedArgs = RequestPayloadSchema.parse(args)
if (request.params.name === 'fetch_html') {
const fetchResult = await Fetcher.html(validatedArgs)
return fetchResult
}
if (request.params.name === 'fetch_json') {
const fetchResult = await Fetcher.json(validatedArgs)
return fetchResult
}
if (request.params.name === 'fetch_txt') {
const fetchResult = await Fetcher.txt(validatedArgs)
return fetchResult
}
if (request.params.name === 'fetch_markdown') {
const fetchResult = await Fetcher.markdown(validatedArgs)
return fetchResult
}
throw new Error('Tool not found')
})
class FetchServer {
public server: Server
constructor() {
this.server = server
}
}
export default FetchServer

View File

@@ -0,0 +1,655 @@
// port https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema } from '@modelcontextprotocol/sdk/types.js'
import { createTwoFilesPatch } from 'diff'
import fs from 'fs/promises'
import { minimatch } from 'minimatch'
import os from 'os'
import path from 'path'
import { z } from 'zod'
import { zodToJsonSchema } from 'zod-to-json-schema'
// Normalize all paths consistently
function normalizePath(p: string): string {
return path.normalize(p)
}
function expandHome(filepath: string): string {
if (filepath.startsWith('~/') || filepath === '~') {
return path.join(os.homedir(), filepath.slice(1))
}
return filepath
}
// Security utilities
async function validatePath(allowedDirectories: string[], requestedPath: string): Promise<string> {
const expandedPath = expandHome(requestedPath)
const absolute = path.isAbsolute(expandedPath)
? path.resolve(expandedPath)
: path.resolve(process.cwd(), expandedPath)
const normalizedRequested = normalizePath(absolute)
// Check if path is within allowed directories
const isAllowed = allowedDirectories.some((dir) => normalizedRequested.startsWith(dir))
if (!isAllowed) {
throw new Error(
`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`
)
}
// Handle symlinks by checking their real path
try {
const realPath = await fs.realpath(absolute)
const normalizedReal = normalizePath(realPath)
const isRealPathAllowed = allowedDirectories.some((dir) => normalizedReal.startsWith(dir))
if (!isRealPathAllowed) {
throw new Error('Access denied - symlink target outside allowed directories')
}
return realPath
} catch (error) {
// For new files that don't exist yet, verify parent directory
const parentDir = path.dirname(absolute)
try {
const realParentPath = await fs.realpath(parentDir)
const normalizedParent = normalizePath(realParentPath)
const isParentAllowed = allowedDirectories.some((dir) => normalizedParent.startsWith(dir))
if (!isParentAllowed) {
throw new Error('Access denied - parent directory outside allowed directories')
}
return absolute
} catch {
throw new Error(`Parent directory does not exist: ${parentDir}`)
}
}
}
// Schema definitions
const ReadFileArgsSchema = z.object({
path: z.string()
})
const ReadMultipleFilesArgsSchema = z.object({
paths: z.array(z.string())
})
const WriteFileArgsSchema = z.object({
path: z.string(),
content: z.string()
})
const EditOperation = z.object({
oldText: z.string().describe('Text to search for - must match exactly'),
newText: z.string().describe('Text to replace with')
})
const EditFileArgsSchema = z.object({
path: z.string(),
edits: z.array(EditOperation),
dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format')
})
const CreateDirectoryArgsSchema = z.object({
path: z.string()
})
const ListDirectoryArgsSchema = z.object({
path: z.string()
})
const DirectoryTreeArgsSchema = z.object({
path: z.string()
})
const MoveFileArgsSchema = z.object({
source: z.string(),
destination: z.string()
})
const SearchFilesArgsSchema = z.object({
path: z.string(),
pattern: z.string(),
excludePatterns: z.array(z.string()).optional().default([])
})
const GetFileInfoArgsSchema = z.object({
path: z.string()
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ToolInputSchema = ToolSchema.shape.inputSchema
type ToolInput = z.infer<typeof ToolInputSchema>
interface FileInfo {
size: number
created: Date
modified: Date
accessed: Date
isDirectory: boolean
isFile: boolean
permissions: string
}
// Tool implementations
async function getFileStats(filePath: string): Promise<FileInfo> {
const stats = await fs.stat(filePath)
return {
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
accessed: stats.atime,
isDirectory: stats.isDirectory(),
isFile: stats.isFile(),
permissions: stats.mode.toString(8).slice(-3)
}
}
async function searchFiles(
allowedDirectories: string[],
rootPath: string,
pattern: string,
excludePatterns: string[] = []
): Promise<string[]> {
const results: string[] = []
async function search(currentPath: string) {
const entries = await fs.readdir(currentPath, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name)
try {
// Validate each path before processing
await validatePath(allowedDirectories, fullPath)
// Check if path matches any exclude pattern
const relativePath = path.relative(rootPath, fullPath)
const shouldExclude = excludePatterns.some((pattern) => {
const globPattern = pattern.includes('*') ? pattern : `**/${pattern}/**`
return minimatch(relativePath, globPattern, { dot: true })
})
if (shouldExclude) {
continue
}
if (entry.name.toLowerCase().includes(pattern.toLowerCase())) {
results.push(fullPath)
}
if (entry.isDirectory()) {
await search(fullPath)
}
} catch (error) {
// Skip invalid paths during search
continue
}
}
}
await search(rootPath)
return results
}
// file editing and diffing utilities
function normalizeLineEndings(text: string): string {
return text.replace(/\r\n/g, '\n')
}
function createUnifiedDiff(originalContent: string, newContent: string, filepath: string = 'file'): string {
// Ensure consistent line endings for diff
const normalizedOriginal = normalizeLineEndings(originalContent)
const normalizedNew = normalizeLineEndings(newContent)
return createTwoFilesPatch(filepath, filepath, normalizedOriginal, normalizedNew, 'original', 'modified')
}
async function applyFileEdits(
filePath: string,
edits: Array<{ oldText: string; newText: string }>,
dryRun = false
): Promise<string> {
// Read file content and normalize line endings
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'))
// Apply edits sequentially
let modifiedContent = content
for (const edit of edits) {
const normalizedOld = normalizeLineEndings(edit.oldText)
const normalizedNew = normalizeLineEndings(edit.newText)
// If exact match exists, use it
if (modifiedContent.includes(normalizedOld)) {
modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew)
continue
}
// Otherwise, try line-by-line matching with flexibility for whitespace
const oldLines = normalizedOld.split('\n')
const contentLines = modifiedContent.split('\n')
let matchFound = false
for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
const potentialMatch = contentLines.slice(i, i + oldLines.length)
// Compare lines with normalized whitespace
const isMatch = oldLines.every((oldLine, j) => {
const contentLine = potentialMatch[j]
return oldLine.trim() === contentLine.trim()
})
if (isMatch) {
// Preserve original indentation of first line
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || ''
const newLines = normalizedNew.split('\n').map((line, j) => {
if (j === 0) return originalIndent + line.trimStart()
// For subsequent lines, try to preserve relative indentation
const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || ''
const newIndent = line.match(/^\s*/)?.[0] || ''
if (oldIndent && newIndent) {
const relativeIndent = newIndent.length - oldIndent.length
return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart()
}
return line
})
contentLines.splice(i, oldLines.length, ...newLines)
modifiedContent = contentLines.join('\n')
matchFound = true
break
}
}
if (!matchFound) {
throw new Error(`Could not find exact match for edit:\n${edit.oldText}`)
}
}
// Create unified diff
const diff = createUnifiedDiff(content, modifiedContent, filePath)
// Format diff with appropriate number of backticks
let numBackticks = 3
while (diff.includes('`'.repeat(numBackticks))) {
numBackticks++
}
const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`
if (!dryRun) {
await fs.writeFile(filePath, modifiedContent, 'utf-8')
}
return formattedDiff
}
class FileSystemServer {
public server: Server
private allowedDirectories: string[]
constructor(allowedDirs: string[]) {
if (!Array.isArray(allowedDirs) || allowedDirs.length === 0) {
throw new Error('No allowed directories provided, please specify at least one directory in args')
}
this.allowedDirectories = allowedDirs.map((dir) => normalizePath(path.resolve(expandHome(dir))))
// Validate that all directories exist and are accessible
this.validateDirs().catch((error) => {
console.error('Error validating allowed directories:', error)
throw new Error(`Error validating allowed directories: ${error}`)
})
this.server = new Server(
{
name: 'secure-filesystem-server',
version: '0.2.0'
},
{
capabilities: {
tools: {}
}
}
)
this.initialize()
}
async validateDirs() {
// Validate that all directories exist and are accessible
await Promise.all(
this.allowedDirectories.map(async (dir) => {
try {
const stats = await fs.stat(expandHome(dir))
if (!stats.isDirectory()) {
console.error(`Error: ${dir} is not a directory`)
throw new Error(`Error: ${dir} is not a directory`)
}
} catch (error: any) {
console.error(`Error accessing directory ${dir}:`, error)
throw new Error(`Error accessing directory ${dir}:`, error)
}
})
)
}
initialize() {
// Tool handlers
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'read_file',
description:
'Read the complete contents of a file from the file system. ' +
'Handles various text encodings and provides detailed error messages ' +
'if the file cannot be read. Use this tool when you need to examine ' +
'the contents of a single file. Only works within allowed directories.',
inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput
},
{
name: 'read_multiple_files',
description:
'Read the contents of multiple files simultaneously. This is more ' +
'efficient than reading files one by one when you need to analyze ' +
"or compare multiple files. Each file's content is returned with its " +
"path as a reference. Failed reads for individual files won't stop " +
'the entire operation. Only works within allowed directories.',
inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput
},
{
name: 'write_file',
description:
'Create a new file or completely overwrite an existing file with new content. ' +
'Use with caution as it will overwrite existing files without warning. ' +
'Handles text content with proper encoding. Only works within allowed directories.',
inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput
},
{
name: 'edit_file',
description:
'Make line-based edits to a text file. Each edit replaces exact line sequences ' +
'with new content. Returns a git-style diff showing the changes made. ' +
'Only works within allowed directories.',
inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput
},
{
name: 'create_directory',
description:
'Create a new directory or ensure a directory exists. Can create multiple ' +
'nested directories in one operation. If the directory already exists, ' +
'this operation will succeed silently. Perfect for setting up directory ' +
'structures for projects or ensuring required paths exist. Only works within allowed directories.',
inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput
},
{
name: 'list_directory',
description:
'Get a detailed listing of all files and directories in a specified path. ' +
'Results clearly distinguish between files and directories with [FILE] and [DIR] ' +
'prefixes. This tool is essential for understanding directory structure and ' +
'finding specific files within a directory. Only works within allowed directories.',
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput
},
{
name: 'directory_tree',
description:
'Get a recursive tree view of files and directories as a JSON structure. ' +
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
'Files have no children array, while directories always have a children array (which may be empty). ' +
'The output is formatted with 2-space indentation for readability. Only works within allowed directories.',
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput
},
{
name: 'move_file',
description:
'Move or rename files and directories. Can move files between directories ' +
'and rename them in a single operation. If the destination exists, the ' +
'operation will fail. Works across different directories and can be used ' +
'for simple renaming within the same directory. Both source and destination must be within allowed directories.',
inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput
},
{
name: 'search_files',
description:
'Recursively search for files and directories matching a pattern. ' +
'Searches through all subdirectories from the starting path. The search ' +
'is case-insensitive and matches partial names. Returns full paths to all ' +
"matching items. Great for finding files when you don't know their exact location. " +
'Only searches within allowed directories.',
inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput
},
{
name: 'get_file_info',
description:
'Retrieve detailed metadata about a file or directory. Returns comprehensive ' +
'information including size, creation time, last modified time, permissions, ' +
'and type. This tool is perfect for understanding file characteristics ' +
'without reading the actual content. Only works within allowed directories.',
inputSchema: zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput
},
{
name: 'list_allowed_directories',
description:
'Returns the list of directories that this server is allowed to access. ' +
'Use this to understand which directories are available before trying to access files.',
inputSchema: {
type: 'object',
properties: {},
required: []
}
}
]
}
})
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params
switch (name) {
case 'read_file': {
const parsed = ReadFileArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for read_file: ${parsed.error}`)
}
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
const content = await fs.readFile(validPath, 'utf-8')
return {
content: [{ type: 'text', text: content }]
}
}
case 'read_multiple_files': {
const parsed = ReadMultipleFilesArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for read_multiple_files: ${parsed.error}`)
}
const results = await Promise.all(
parsed.data.paths.map(async (filePath: string) => {
try {
const validPath = await validatePath(this.allowedDirectories, filePath)
const content = await fs.readFile(validPath, 'utf-8')
return `${filePath}:\n${content}\n`
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
return `${filePath}: Error - ${errorMessage}`
}
})
)
return {
content: [{ type: 'text', text: results.join('\n---\n') }]
}
}
case 'write_file': {
const parsed = WriteFileArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for write_file: ${parsed.error}`)
}
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
await fs.writeFile(validPath, parsed.data.content, 'utf-8')
return {
content: [{ type: 'text', text: `Successfully wrote to ${parsed.data.path}` }]
}
}
case 'edit_file': {
const parsed = EditFileArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for edit_file: ${parsed.error}`)
}
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun)
return {
content: [{ type: 'text', text: result }]
}
}
case 'create_directory': {
const parsed = CreateDirectoryArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for create_directory: ${parsed.error}`)
}
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
await fs.mkdir(validPath, { recursive: true })
return {
content: [{ type: 'text', text: `Successfully created directory ${parsed.data.path}` }]
}
}
case 'list_directory': {
const parsed = ListDirectoryArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for list_directory: ${parsed.error}`)
}
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
const entries = await fs.readdir(validPath, { withFileTypes: true })
const formatted = entries
.map((entry) => `${entry.isDirectory() ? '[DIR]' : '[FILE]'} ${entry.name}`)
.join('\n')
return {
content: [{ type: 'text', text: formatted }]
}
}
case 'directory_tree': {
const parsed = DirectoryTreeArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`)
}
interface TreeEntry {
name: string
type: 'file' | 'directory'
children?: TreeEntry[]
}
async function buildTree(allowedDirectories: string[], currentPath: string): Promise<TreeEntry[]> {
const validPath = await validatePath(allowedDirectories, currentPath)
const entries = await fs.readdir(validPath, { withFileTypes: true })
const result: TreeEntry[] = []
for (const entry of entries) {
const entryData: TreeEntry = {
name: entry.name,
type: entry.isDirectory() ? 'directory' : 'file'
}
if (entry.isDirectory()) {
const subPath = path.join(currentPath, entry.name)
entryData.children = await buildTree(allowedDirectories, subPath)
}
result.push(entryData)
}
return result
}
const treeData = await buildTree(this.allowedDirectories, parsed.data.path)
return {
content: [
{
type: 'text',
text: JSON.stringify(treeData, null, 2)
}
]
}
}
case 'move_file': {
const parsed = MoveFileArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for move_file: ${parsed.error}`)
}
const validSourcePath = await validatePath(this.allowedDirectories, parsed.data.source)
const validDestPath = await validatePath(this.allowedDirectories, parsed.data.destination)
await fs.rename(validSourcePath, validDestPath)
return {
content: [
{ type: 'text', text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}` }
]
}
}
case 'search_files': {
const parsed = SearchFilesArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for search_files: ${parsed.error}`)
}
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
const results = await searchFiles(
this.allowedDirectories,
validPath,
parsed.data.pattern,
parsed.data.excludePatterns
)
return {
content: [{ type: 'text', text: results.length > 0 ? results.join('\n') : 'No matches found' }]
}
}
case 'get_file_info': {
const parsed = GetFileInfoArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`Invalid arguments for get_file_info: ${parsed.error}`)
}
const validPath = await validatePath(this.allowedDirectories, parsed.data.path)
const info = await getFileStats(validPath)
return {
content: [
{
type: 'text',
text: Object.entries(info)
.map(([key, value]) => `${key}: ${value}`)
.join('\n')
}
]
}
}
case 'list_allowed_directories': {
return {
content: [
{
type: 'text',
text: `Allowed directories:\n${this.allowedDirectories.join('\n')}`
}
]
}
}
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
}
}
})
}
}
export default FileSystemServer

View File

@@ -0,0 +1,700 @@
import { getConfigDir } from '@main/utils/file'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'
import { Mutex } from 'async-mutex' // 引入 Mutex
import { promises as fs } from 'fs'
import path from 'path'
// Define memory file path
const defaultMemoryPath = path.join(getConfigDir(), 'memory.json')
// Interfaces remain the same
interface Entity {
name: string
entityType: string
observations: string[]
}
interface Relation {
from: string
to: string
relationType: string
}
// Structure for storing the graph in memory and in the file
interface KnowledgeGraph {
entities: Entity[]
relations: Relation[]
}
// The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
class KnowledgeGraphManager {
private memoryPath: string
private entities: Map<string, Entity> // Use Map for efficient entity lookup
private relations: Set<string> // Store stringified relations for easy Set operations
private fileMutex: Mutex // Mutex for file writing
private constructor(memoryPath: string) {
this.memoryPath = memoryPath
this.entities = new Map<string, Entity>()
this.relations = new Set<string>()
this.fileMutex = new Mutex()
}
// Static async factory method for initialization
public static async create(memoryPath: string): Promise<KnowledgeGraphManager> {
const manager = new KnowledgeGraphManager(memoryPath)
await manager._ensureMemoryPathExists()
await manager._loadGraphFromDisk()
return manager
}
private async _ensureMemoryPathExists(): Promise<void> {
try {
const directory = path.dirname(this.memoryPath)
await fs.mkdir(directory, { recursive: true })
try {
await fs.access(this.memoryPath)
} catch (error) {
// File doesn't exist, create an empty file with initial structure
await fs.writeFile(this.memoryPath, JSON.stringify({ entities: [], relations: [] }, null, 2))
}
} catch (error) {
console.error('Failed to ensure memory path exists:', error)
// Propagate the error or handle it more gracefully depending on requirements
throw new McpError(
ErrorCode.InternalError,
`Failed to ensure memory path: ${error instanceof Error ? error.message : String(error)}`
)
}
}
// Load graph from disk into memory (called once during initialization)
private async _loadGraphFromDisk(): Promise<void> {
try {
const data = await fs.readFile(this.memoryPath, 'utf-8')
// Handle empty file case
if (data.trim() === '') {
this.entities = new Map()
this.relations = new Set()
// Optionally write the initial empty structure back
await this._persistGraph()
return
}
const graph: KnowledgeGraph = JSON.parse(data)
this.entities.clear()
this.relations.clear()
graph.entities.forEach((entity) => this.entities.set(entity.name, entity))
graph.relations.forEach((relation) => this.relations.add(this._serializeRelation(relation)))
} catch (error) {
if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') {
// File doesn't exist (should have been created by _ensureMemoryPathExists, but handle defensively)
this.entities = new Map()
this.relations = new Set()
await this._persistGraph() // Create the file with empty structure
} else if (error instanceof SyntaxError) {
console.error('Failed to parse memory.json, initializing with empty graph:', error)
// If JSON is invalid, start fresh and overwrite the corrupted file
this.entities = new Map()
this.relations = new Set()
await this._persistGraph()
} else {
console.error('Failed to load knowledge graph from disk:', error)
throw new McpError(
ErrorCode.InternalError,
`Failed to load graph: ${error instanceof Error ? error.message : String(error)}`
)
}
}
}
// Persist the current in-memory graph to disk using a mutex
private async _persistGraph(): Promise<void> {
const release = await this.fileMutex.acquire()
try {
const graphData: KnowledgeGraph = {
entities: Array.from(this.entities.values()),
relations: Array.from(this.relations).map((rStr) => this._deserializeRelation(rStr))
}
await fs.writeFile(this.memoryPath, JSON.stringify(graphData, null, 2))
} catch (error) {
console.error('Failed to save knowledge graph:', error)
// Decide how to handle write errors - potentially retry or notify
throw new McpError(
ErrorCode.InternalError,
`Failed to save graph: ${error instanceof Error ? error.message : String(error)}`
)
} finally {
release()
}
}
// Helper to consistently serialize relations for Set storage
private _serializeRelation(relation: Relation): string {
// Simple serialization, ensure order doesn't matter if properties are consistent
return JSON.stringify({ from: relation.from, to: relation.to, relationType: relation.relationType })
}
// Helper to deserialize relations from Set storage
private _deserializeRelation(relationStr: string): Relation {
return JSON.parse(relationStr) as Relation
}
async createEntities(entities: Entity[]): Promise<Entity[]> {
const newEntities: Entity[] = []
entities.forEach((entity) => {
if (!this.entities.has(entity.name)) {
// Ensure observations is always an array
const newEntity = { ...entity, observations: Array.isArray(entity.observations) ? entity.observations : [] }
this.entities.set(entity.name, newEntity)
newEntities.push(newEntity)
}
})
if (newEntities.length > 0) {
await this._persistGraph()
}
return newEntities
}
async createRelations(relations: Relation[]): Promise<Relation[]> {
const newRelations: Relation[] = []
relations.forEach((relation) => {
// Ensure related entities exist before creating a relation
if (!this.entities.has(relation.from) || !this.entities.has(relation.to)) {
console.warn(`Skipping relation creation: Entity not found for relation ${relation.from} -> ${relation.to}`)
return // Skip this relation
}
const relationStr = this._serializeRelation(relation)
if (!this.relations.has(relationStr)) {
this.relations.add(relationStr)
newRelations.push(relation)
}
})
if (newRelations.length > 0) {
await this._persistGraph()
}
return newRelations
}
async addObservations(
observations: { entityName: string; contents: string[] }[]
): Promise<{ entityName: string; addedObservations: string[] }[]> {
const results: { entityName: string; addedObservations: string[] }[] = []
let changed = false
observations.forEach((o) => {
const entity = this.entities.get(o.entityName)
if (!entity) {
// Option 1: Throw error
throw new McpError(ErrorCode.InvalidParams, `Entity with name ${o.entityName} not found`)
// Option 2: Skip and warn
// console.warn(`Entity with name ${o.entityName} not found when adding observations. Skipping.`);
// return;
}
// Ensure observations array exists
if (!Array.isArray(entity.observations)) {
entity.observations = []
}
const newObservations = o.contents.filter((content) => !entity.observations.includes(content))
if (newObservations.length > 0) {
entity.observations.push(...newObservations)
results.push({ entityName: o.entityName, addedObservations: newObservations })
changed = true
} else {
// Still include in results even if nothing was added, to confirm processing
results.push({ entityName: o.entityName, addedObservations: [] })
}
})
if (changed) {
await this._persistGraph()
}
return results
}
async deleteEntities(entityNames: string[]): Promise<void> {
let changed = false
const namesToDelete = new Set(entityNames)
// Delete entities
namesToDelete.forEach((name) => {
if (this.entities.delete(name)) {
changed = true
}
})
// Delete relations involving deleted entities
const relationsToDelete = new Set<string>()
this.relations.forEach((relStr) => {
const rel = this._deserializeRelation(relStr)
if (namesToDelete.has(rel.from) || namesToDelete.has(rel.to)) {
relationsToDelete.add(relStr)
}
})
relationsToDelete.forEach((relStr) => {
if (this.relations.delete(relStr)) {
changed = true
}
})
if (changed) {
await this._persistGraph()
}
}
async deleteObservations(deletions: { entityName: string; observations: string[] }[]): Promise<void> {
let changed = false
deletions.forEach((d) => {
const entity = this.entities.get(d.entityName)
if (entity && Array.isArray(entity.observations)) {
const initialLength = entity.observations.length
const observationsToDelete = new Set(d.observations)
entity.observations = entity.observations.filter((o) => !observationsToDelete.has(o))
if (entity.observations.length !== initialLength) {
changed = true
}
}
})
if (changed) {
await this._persistGraph()
}
}
async deleteRelations(relations: Relation[]): Promise<void> {
let changed = false
relations.forEach((rel) => {
const relStr = this._serializeRelation(rel)
if (this.relations.delete(relStr)) {
changed = true
}
})
if (changed) {
await this._persistGraph()
}
}
// Read the current state from memory
async readGraph(): Promise<KnowledgeGraph> {
// Return a deep copy to prevent external modification of the internal state
return JSON.parse(
JSON.stringify({
entities: Array.from(this.entities.values()),
relations: Array.from(this.relations).map((rStr) => this._deserializeRelation(rStr))
})
)
}
// Search operates on the in-memory graph
async searchNodes(query: string): Promise<KnowledgeGraph> {
const lowerCaseQuery = query.toLowerCase()
const filteredEntities = Array.from(this.entities.values()).filter(
(e) =>
e.name.toLowerCase().includes(lowerCaseQuery) ||
e.entityType.toLowerCase().includes(lowerCaseQuery) ||
(Array.isArray(e.observations) && e.observations.some((o) => o.toLowerCase().includes(lowerCaseQuery)))
)
const filteredEntityNames = new Set(filteredEntities.map((e) => e.name))
const filteredRelations = Array.from(this.relations)
.map((rStr) => this._deserializeRelation(rStr))
.filter((r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to))
return {
entities: filteredEntities,
relations: filteredRelations
}
}
// Open operates on the in-memory graph
async openNodes(names: string[]): Promise<KnowledgeGraph> {
const nameSet = new Set(names)
const filteredEntities = Array.from(this.entities.values()).filter((e) => nameSet.has(e.name))
const filteredEntityNames = new Set(filteredEntities.map((e) => e.name))
const filteredRelations = Array.from(this.relations)
.map((rStr) => this._deserializeRelation(rStr))
.filter((r) => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to))
return {
entities: filteredEntities,
relations: filteredRelations
}
}
}
class MemoryServer {
public server: Server
// Hold the manager instance, initialized asynchronously
private knowledgeGraphManager: KnowledgeGraphManager | null = null
private initializationPromise: Promise<void> // To track initialization
constructor(envPath: string = '') {
const memoryPath = envPath
? path.isAbsolute(envPath)
? envPath
: path.resolve(envPath) // Use path.resolve for relative paths based on CWD
: defaultMemoryPath
this.server = new Server(
{
name: 'memory-server',
version: '1.1.0' // Incremented version for changes
},
{
capabilities: {
tools: {}
}
}
)
// Start initialization, but don't block constructor
this.initializationPromise = this._initializeManager(memoryPath)
this.setupRequestHandlers() // Setup handlers immediately
}
// Private async method to handle manager initialization
private async _initializeManager(memoryPath: string): Promise<void> {
try {
this.knowledgeGraphManager = await KnowledgeGraphManager.create(memoryPath)
console.log('KnowledgeGraphManager initialized successfully.')
} catch (error) {
console.error('Failed to initialize KnowledgeGraphManager:', error)
// Server might be unusable, consider how to handle this state
// Maybe set a flag and return errors for all tool calls?
this.knowledgeGraphManager = null // Ensure it's null if init fails
}
}
// Ensures the manager is initialized before handling tool calls
private async _getManager(): Promise<KnowledgeGraphManager> {
await this.initializationPromise // Wait for initialization to complete
if (!this.knowledgeGraphManager) {
throw new McpError(ErrorCode.InternalError, 'Memory server failed to initialize. Cannot process requests.')
}
return this.knowledgeGraphManager
}
// Setup handlers (can be called from constructor)
setupRequestHandlers() {
// ListTools remains largely the same, descriptions might be updated if needed
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
// Ensure manager is ready before listing tools that depend on it
// Although ListTools itself doesn't *call* the manager, it implies the
// manager is ready to handle calls for those tools.
try {
await this._getManager() // Wait for initialization before confirming tools are available
} catch (error) {
// If manager failed to init, maybe return an empty tool list or throw?
console.error('Cannot list tools, manager initialization failed:', error)
return { tools: [] } // Return empty list if server is not ready
}
return {
tools: [
{
name: 'create_entities',
description: 'Create multiple new entities in the knowledge graph. Skips existing entities.',
inputSchema: {
type: 'object',
properties: {
entities: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'The name of the entity' },
entityType: { type: 'string', description: 'The type of the entity' },
observations: {
type: 'array',
items: { type: 'string' },
description: 'An array of observation contents associated with the entity',
default: [] // Add default empty array
}
},
required: ['name', 'entityType'] // Observations are optional now on creation
}
}
},
required: ['entities']
}
},
{
name: 'create_relations',
description:
'Create multiple new relations between EXISTING entities. Skips existing relations or relations with non-existent entities.',
inputSchema: {
type: 'object',
properties: {
relations: {
type: 'array',
items: {
type: 'object',
properties: {
from: { type: 'string', description: 'The name of the entity where the relation starts' },
to: { type: 'string', description: 'The name of the entity where the relation ends' },
relationType: { type: 'string', description: 'The type of the relation' }
},
required: ['from', 'to', 'relationType']
}
}
},
required: ['relations']
}
},
{
name: 'add_observations',
description: 'Add new observations to existing entities. Skips duplicate observations.',
inputSchema: {
type: 'object',
properties: {
observations: {
type: 'array',
items: {
type: 'object',
properties: {
entityName: { type: 'string', description: 'The name of the entity to add the observations to' },
contents: {
type: 'array',
items: { type: 'string' },
description: 'An array of observation contents to add'
}
},
required: ['entityName', 'contents']
}
}
},
required: ['observations']
}
},
{
name: 'delete_entities',
description: 'Delete multiple entities and their associated relations.',
inputSchema: {
type: 'object',
properties: {
entityNames: {
type: 'array',
items: { type: 'string' },
description: 'An array of entity names to delete'
}
},
required: ['entityNames']
}
},
{
name: 'delete_observations',
description: 'Delete specific observations from entities.',
inputSchema: {
type: 'object',
properties: {
deletions: {
type: 'array',
items: {
type: 'object',
properties: {
entityName: { type: 'string', description: 'The name of the entity containing the observations' },
observations: {
type: 'array',
items: { type: 'string' },
description: 'An array of observations to delete'
}
},
required: ['entityName', 'observations']
}
}
},
required: ['deletions']
}
},
{
name: 'delete_relations',
description: 'Delete multiple specific relations.',
inputSchema: {
type: 'object',
properties: {
relations: {
type: 'array',
items: {
type: 'object',
properties: {
from: { type: 'string', description: 'The name of the entity where the relation starts' },
to: { type: 'string', description: 'The name of the entity where the relation ends' },
relationType: { type: 'string', description: 'The type of the relation' }
},
required: ['from', 'to', 'relationType']
},
description: 'An array of relations to delete'
}
},
required: ['relations']
}
},
{
name: 'read_graph',
description: 'Read the entire knowledge graph from memory.',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'search_nodes',
description: 'Search nodes (entities and relations) in memory based on a query.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The search query to match against entity names, types, and observation content'
}
},
required: ['query']
}
},
{
name: 'open_nodes',
description: 'Retrieve specific entities and their connecting relations from memory by name.',
inputSchema: {
type: 'object',
properties: {
names: {
type: 'array',
items: { type: 'string' },
description: 'An array of entity names to retrieve'
}
},
required: ['names']
}
}
]
}
})
// CallTool handler needs to await the manager and the async methods
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const manager = await this._getManager() // Ensure manager is ready
const { name, arguments: args } = request.params
if (!args) {
// Use McpError for standard errors
throw new McpError(ErrorCode.InvalidParams, `No arguments provided for tool: ${name}`)
}
try {
switch (name) {
case 'create_entities':
// Validate args structure if necessary, though SDK might do basic validation
if (!args.entities || !Array.isArray(args.entities)) {
throw new McpError(
ErrorCode.InvalidParams,
`Invalid arguments for ${name}: 'entities' array is required.`
)
}
return {
content: [
{ type: 'text', text: JSON.stringify(await manager.createEntities(args.entities as Entity[]), null, 2) }
]
}
case 'create_relations':
if (!args.relations || !Array.isArray(args.relations)) {
throw new McpError(
ErrorCode.InvalidParams,
`Invalid arguments for ${name}: 'relations' array is required.`
)
}
return {
content: [
{
type: 'text',
text: JSON.stringify(await manager.createRelations(args.relations as Relation[]), null, 2)
}
]
}
case 'add_observations':
if (!args.observations || !Array.isArray(args.observations)) {
throw new McpError(
ErrorCode.InvalidParams,
`Invalid arguments for ${name}: 'observations' array is required.`
)
}
return {
content: [
{
type: 'text',
text: JSON.stringify(
await manager.addObservations(args.observations as { entityName: string; contents: string[] }[]),
null,
2
)
}
]
}
case 'delete_entities':
if (!args.entityNames || !Array.isArray(args.entityNames)) {
throw new McpError(
ErrorCode.InvalidParams,
`Invalid arguments for ${name}: 'entityNames' array is required.`
)
}
await manager.deleteEntities(args.entityNames as string[])
return { content: [{ type: 'text', text: 'Entities deleted successfully' }] }
case 'delete_observations':
if (!args.deletions || !Array.isArray(args.deletions)) {
throw new McpError(
ErrorCode.InvalidParams,
`Invalid arguments for ${name}: 'deletions' array is required.`
)
}
await manager.deleteObservations(args.deletions as { entityName: string; observations: string[] }[])
return { content: [{ type: 'text', text: 'Observations deleted successfully' }] }
case 'delete_relations':
if (!args.relations || !Array.isArray(args.relations)) {
throw new McpError(
ErrorCode.InvalidParams,
`Invalid arguments for ${name}: 'relations' array is required.`
)
}
await manager.deleteRelations(args.relations as Relation[])
return { content: [{ type: 'text', text: 'Relations deleted successfully' }] }
case 'read_graph':
// No arguments expected or needed for read_graph based on original schema
return {
content: [{ type: 'text', text: JSON.stringify(await manager.readGraph(), null, 2) }]
}
case 'search_nodes':
if (typeof args.query !== 'string') {
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'query' string is required.`)
}
return {
content: [
{ type: 'text', text: JSON.stringify(await manager.searchNodes(args.query as string), null, 2) }
]
}
case 'open_nodes':
if (!args.names || !Array.isArray(args.names)) {
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for ${name}: 'names' array is required.`)
}
return {
content: [
{ type: 'text', text: JSON.stringify(await manager.openNodes(args.names as string[]), null, 2) }
]
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`)
}
} catch (error) {
// Catch errors from manager methods (like entity not found) or other issues
if (error instanceof McpError) {
throw error // Re-throw McpErrors directly
}
console.error(`Error executing tool ${name}:`, error)
// Throw a generic internal error for unexpected issues
throw new McpError(
ErrorCode.InternalError,
`Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}`
)
}
})
}
}
export default MemoryServer

View File

@@ -0,0 +1,289 @@
// Sequential Thinking MCP Server
// port https://github.com/modelcontextprotocol/servers/blob/main/src/sequentialthinking/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'
// Fixed chalk import for ESM
import chalk from 'chalk'
interface ThoughtData {
thought: string
thoughtNumber: number
totalThoughts: number
isRevision?: boolean
revisesThought?: number
branchFromThought?: number
branchId?: string
needsMoreThoughts?: boolean
nextThoughtNeeded: boolean
}
class SequentialThinkingServer {
private thoughtHistory: ThoughtData[] = []
private branches: Record<string, ThoughtData[]> = {}
private validateThoughtData(input: unknown): ThoughtData {
const data = input as Record<string, unknown>
if (!data.thought || typeof data.thought !== 'string') {
throw new Error('Invalid thought: must be a string')
}
if (!data.thoughtNumber || typeof data.thoughtNumber !== 'number') {
throw new Error('Invalid thoughtNumber: must be a number')
}
if (!data.totalThoughts || typeof data.totalThoughts !== 'number') {
throw new Error('Invalid totalThoughts: must be a number')
}
if (typeof data.nextThoughtNeeded !== 'boolean') {
throw new Error('Invalid nextThoughtNeeded: must be a boolean')
}
return {
thought: data.thought,
thoughtNumber: data.thoughtNumber,
totalThoughts: data.totalThoughts,
nextThoughtNeeded: data.nextThoughtNeeded,
isRevision: data.isRevision as boolean | undefined,
revisesThought: data.revisesThought as number | undefined,
branchFromThought: data.branchFromThought as number | undefined,
branchId: data.branchId as string | undefined,
needsMoreThoughts: data.needsMoreThoughts as boolean | undefined
}
}
private formatThought(thoughtData: ThoughtData): string {
const { thoughtNumber, totalThoughts, thought, isRevision, revisesThought, branchFromThought, branchId } =
thoughtData
let prefix = ''
let context = ''
if (isRevision) {
prefix = chalk.yellow('🔄 Revision')
context = ` (revising thought ${revisesThought})`
} else if (branchFromThought) {
prefix = chalk.green('🌿 Branch')
context = ` (from thought ${branchFromThought}, ID: ${branchId})`
} else {
prefix = chalk.blue('💭 Thought')
context = ''
}
const header = `${prefix} ${thoughtNumber}/${totalThoughts}${context}`
const border = '─'.repeat(Math.max(header.length, thought.length) + 4)
return `
${border}
${header}
${border}
${thought.padEnd(border.length - 2)}
${border}`
}
public processThought(input: unknown): { content: Array<{ type: string; text: string }>; isError?: boolean } {
try {
const validatedInput = this.validateThoughtData(input)
if (validatedInput.thoughtNumber > validatedInput.totalThoughts) {
validatedInput.totalThoughts = validatedInput.thoughtNumber
}
this.thoughtHistory.push(validatedInput)
if (validatedInput.branchFromThought && validatedInput.branchId) {
if (!this.branches[validatedInput.branchId]) {
this.branches[validatedInput.branchId] = []
}
this.branches[validatedInput.branchId].push(validatedInput)
}
const formattedThought = this.formatThought(validatedInput)
console.error(formattedThought)
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
thoughtNumber: validatedInput.thoughtNumber,
totalThoughts: validatedInput.totalThoughts,
nextThoughtNeeded: validatedInput.nextThoughtNeeded,
branches: Object.keys(this.branches),
thoughtHistoryLength: this.thoughtHistory.length
},
null,
2
)
}
]
}
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: error instanceof Error ? error.message : String(error),
status: 'failed'
},
null,
2
)
}
],
isError: true
}
}
}
}
const SEQUENTIAL_THINKING_TOOL: Tool = {
name: 'sequentialthinking',
description: `A detailed tool for dynamic and reflective problem-solving through thoughts.
This tool helps analyze problems through a flexible thinking process that can adapt and evolve.
Each thought can build on, question, or revise previous insights as understanding deepens.
When to use this tool:
- Breaking down complex problems into steps
- Planning and design with room for revision
- Analysis that might need course correction
- Problems where the full scope might not be clear initially
- Problems that require a multi-step solution
- Tasks that need to maintain context over multiple steps
- Situations where irrelevant information needs to be filtered out
Key features:
- You can adjust total_thoughts up or down as you progress
- You can question or revise previous thoughts
- You can add more thoughts even after reaching what seemed like the end
- You can express uncertainty and explore alternative approaches
- Not every thought needs to build linearly - you can branch or backtrack
- Generates a solution hypothesis
- Verifies the hypothesis based on the Chain of Thought steps
- Repeats the process until satisfied
- Provides a correct answer
Parameters explained:
- thought: Your current thinking step, which can include:
* Regular analytical steps
* Revisions of previous thoughts
* Questions about previous decisions
* Realizations about needing more analysis
* Changes in approach
* Hypothesis generation
* Hypothesis verification
- next_thought_needed: True if you need more thinking, even if at what seemed like the end
- thought_number: Current number in sequence (can go beyond initial total if needed)
- total_thoughts: Current estimate of thoughts needed (can be adjusted up/down)
- is_revision: A boolean indicating if this thought revises previous thinking
- revises_thought: If is_revision is true, which thought number is being reconsidered
- branch_from_thought: If branching, which thought number is the branching point
- branch_id: Identifier for the current branch (if any)
- needs_more_thoughts: If reaching end but realizing more thoughts needed
You should:
1. Start with an initial estimate of needed thoughts, but be ready to adjust
2. Feel free to question or revise previous thoughts
3. Don't hesitate to add more thoughts if needed, even at the "end"
4. Express uncertainty when present
5. Mark thoughts that revise previous thinking or branch into new paths
6. Ignore information that is irrelevant to the current step
7. Generate a solution hypothesis when appropriate
8. Verify the hypothesis based on the Chain of Thought steps
9. Repeat the process until satisfied with the solution
10. Provide a single, ideally correct answer as the final output
11. Only set next_thought_needed to false when truly done and a satisfactory answer is reached`,
inputSchema: {
type: 'object',
properties: {
thought: {
type: 'string',
description: 'Your current thinking step'
},
nextThoughtNeeded: {
type: 'boolean',
description: 'Whether another thought step is needed'
},
thoughtNumber: {
type: 'integer',
description: 'Current thought number',
minimum: 1
},
totalThoughts: {
type: 'integer',
description: 'Estimated total thoughts needed',
minimum: 1
},
isRevision: {
type: 'boolean',
description: 'Whether this revises previous thinking'
},
revisesThought: {
type: 'integer',
description: 'Which thought is being reconsidered',
minimum: 1
},
branchFromThought: {
type: 'integer',
description: 'Branching point thought number',
minimum: 1
},
branchId: {
type: 'string',
description: 'Branch identifier'
},
needsMoreThoughts: {
type: 'boolean',
description: 'If more thoughts are needed'
}
},
required: ['thought', 'nextThoughtNeeded', 'thoughtNumber', 'totalThoughts']
}
}
class ThinkingServer {
public server: Server
private thinkingServer: SequentialThinkingServer
constructor() {
this.thinkingServer = new SequentialThinkingServer()
this.server = new Server(
{
name: 'sequential-thinking-server',
version: '0.2.0'
},
{
capabilities: {
tools: {}
}
}
)
this.initialize()
}
initialize() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [SEQUENTIAL_THINKING_TOOL]
}))
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'sequentialthinking') {
return this.thinkingServer.processThought(request.params.arguments)
}
return {
content: [
{
type: 'text',
text: `Unknown tool: ${request.params.name}`
}
],
isError: true
}
})
}
}
export default ThinkingServer

View File

@@ -1,20 +1,77 @@
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { KnowledgeBaseParams } from '@types'
export default abstract class BaseReranker {
protected base: KnowledgeBaseParams
constructor(base: KnowledgeBaseParams) {
if (!base.rerankModel) {
throw new Error('Rerank model is required')
}
this.base = base
}
abstract rerank(query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]>
/**
* Get Rerank Request Url
*/
protected getRerankUrl() {
let baseURL = this.base?.rerankBaseURL?.endsWith('/')
? this.base.rerankBaseURL.slice(0, -1)
: this.base.rerankBaseURL
// 必须携带/v1否则会404
if (baseURL && !baseURL.endsWith('/v1')) {
baseURL = `${baseURL}/v1`
}
return `${baseURL}/rerank`
}
/**
* Get Rerank Result
* @param searchResults
* @param rerankResults
* @protected
*/
protected getRerankResult(
searchResults: ExtractChunkData[],
rerankResults: Array<{
index: number
relevance_score: number
}>
) {
const resultMap = new Map(rerankResults.map((result) => [result.index, result.relevance_score || 0]))
return searchResults
.map((doc: ExtractChunkData, index: number) => {
const score = resultMap.get(index)
if (score === undefined) return undefined
return {
...doc,
score
}
})
.filter((doc): doc is ExtractChunkData => doc !== undefined)
.sort((a, b) => b.score - a.score)
}
public defaultHeaders() {
return {
Authorization: `Bearer ${this.base.rerankApiKey}`,
'Content-Type': 'application/json'
}
}
protected formatErrorMessage(url: string, error: any, requestBody: any) {
const errorDetails = {
url: url,
message: error.message,
status: error.response?.status,
statusText: error.response?.statusText,
requestBody: requestBody
}
return JSON.stringify(errorDetails, null, 2)
}
}

View File

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

View File

@@ -1,6 +1,6 @@
import { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import AxiosProxy from '@main/services/AxiosProxy'
import { KnowledgeBaseParams } from '@types'
import axios from 'axios'
import BaseReranker from './BaseReranker'
@@ -10,10 +10,7 @@ export default class JinaReranker extends BaseReranker {
}
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
const baseURL = this.base?.rerankBaseURL?.endsWith('/')
? this.base.rerankBaseURL.slice(0, -1)
: this.base.rerankBaseURL
const url = `${baseURL}/rerank`
const url = this.getRerankUrl()
const requestBody = {
model: this.base.rerankModel,
@@ -23,26 +20,14 @@ export default class JinaReranker extends BaseReranker {
}
try {
const { data } = await axios.post(url, requestBody, { headers: this.defaultHeaders() })
const { data } = await AxiosProxy.axios.post(url, requestBody, { headers: this.defaultHeaders() })
const rerankResults = data.results
console.log(rerankResults)
const resultMap = new Map(rerankResults.map((result: any) => [result.index, result.relevance_score || 0]))
return searchResults
.map((doc: ExtractChunkData, index: number) => {
const score = resultMap.get(index)
if (score === undefined) return undefined
return {
...doc,
score
}
})
.filter((doc): doc is ExtractChunkData => doc !== undefined)
.sort((a, b) => b.score - a.score)
} catch (error) {
console.error('Jina Reranker API 错误:', error)
throw error
return this.getRerankResult(searchResults, rerankResults)
} catch (error: any) {
const errorDetails = this.formatErrorMessage(url, error, requestBody)
console.error('Jina Reranker API Error:', errorDetails)
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
}
}
}

View File

@@ -1,4 +1,4 @@
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { KnowledgeBaseParams } from '@types'
import BaseReranker from './BaseReranker'

View File

@@ -4,6 +4,7 @@ import BaseReranker from './BaseReranker'
import DefaultReranker from './DefaultReranker'
import JinaReranker from './JinaReranker'
import SiliconFlowReranker from './SiliconFlowReranker'
import VoyageReranker from './VoyageReranker'
export default class RerankerFactory {
static create(base: KnowledgeBaseParams): BaseReranker {
@@ -11,6 +12,8 @@ export default class RerankerFactory {
return new SiliconFlowReranker(base)
} else if (base.rerankModelProvider === 'jina') {
return new JinaReranker(base)
} else if (base.rerankModelProvider === 'voyageai') {
return new VoyageReranker(base)
}
return new DefaultReranker(base)
}

View File

@@ -1,6 +1,6 @@
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import axiosProxy from '@main/services/AxiosProxy'
import { KnowledgeBaseParams } from '@types'
import axios from 'axios'
import BaseReranker from './BaseReranker'
@@ -10,10 +10,7 @@ export default class SiliconFlowReranker extends BaseReranker {
}
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
const baseURL = this.base?.rerankBaseURL?.endsWith('/')
? this.base.rerankBaseURL.slice(0, -1)
: this.base.rerankBaseURL
const url = `${baseURL}/rerank`
const url = this.getRerankUrl()
const requestBody = {
model: this.base.rerankModel,
@@ -25,26 +22,15 @@ export default class SiliconFlowReranker extends BaseReranker {
}
try {
const { data } = await axios.post(url, requestBody, { headers: this.defaultHeaders() })
const { data } = await axiosProxy.axios.post(url, requestBody, { headers: this.defaultHeaders() })
const rerankResults = data.results
const resultMap = new Map(rerankResults.map((result: any) => [result.index, result.relevance_score || 0]))
return this.getRerankResult(searchResults, rerankResults)
} catch (error: any) {
const errorDetails = this.formatErrorMessage(url, error, requestBody)
return searchResults
.map((doc: ExtractChunkData, index: number) => {
const score = resultMap.get(index)
if (score === undefined) return undefined
return {
...doc,
score
}
})
.filter((doc): doc is ExtractChunkData => doc !== undefined)
.sort((a, b) => b.score - a.score)
} catch (error) {
console.error('SiliconFlow Reranker API 错误:', error)
throw error
console.error('SiliconFlow Reranker API 错误:', errorDetails)
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
}
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

View File

@@ -1,9 +1,11 @@
import { IpcChannel } from '@shared/IpcChannel'
import { UpdateInfo } from 'builder-util-runtime'
import { app, BrowserWindow, dialog } from 'electron'
import logger from 'electron-log'
import { AppUpdater as _AppUpdater, autoUpdater } from 'electron-updater'
import icon from '../../../build/icon.png?asset'
import { configManager } from './ConfigManager'
export default class AppUpdater {
autoUpdater: _AppUpdater = autoUpdater
@@ -14,7 +16,8 @@ export default class AppUpdater {
autoUpdater.logger = logger
autoUpdater.forceDevUpdateConfig = !app.isPackaged
autoUpdater.autoDownload = true
autoUpdater.autoDownload = configManager.getAutoUpdate()
autoUpdater.autoInstallOnAppQuit = configManager.getAutoUpdate()
// 检测下载错误
autoUpdater.on('error', (error) => {
@@ -24,27 +27,27 @@ export default class AppUpdater {
stack: error.stack,
time: new Date().toISOString()
})
mainWindow.webContents.send('update-error', error)
mainWindow.webContents.send(IpcChannel.UpdateError, error)
})
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
logger.info('检测到新版本', releaseInfo)
mainWindow.webContents.send('update-available', releaseInfo)
mainWindow.webContents.send(IpcChannel.UpdateAvailable, releaseInfo)
})
// 检测到不需要更新时
autoUpdater.on('update-not-available', () => {
mainWindow.webContents.send('update-not-available')
mainWindow.webContents.send(IpcChannel.UpdateNotAvailable)
})
// 更新下载进度
autoUpdater.on('download-progress', (progress) => {
mainWindow.webContents.send('download-progress', progress)
mainWindow.webContents.send(IpcChannel.DownloadProgress, progress)
})
// 当需要更新的内容下载完成后
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
mainWindow.webContents.send('update-downloaded', releaseInfo)
mainWindow.webContents.send(IpcChannel.UpdateDownloaded, releaseInfo)
this.releaseInfo = releaseInfo
logger.info('下载完成', releaseInfo)
})
@@ -52,6 +55,11 @@ export default class AppUpdater {
this.autoUpdater = autoUpdater
}
public setAutoUpdate(isActive: boolean) {
autoUpdater.autoDownload = isActive
autoUpdater.autoInstallOnAppQuit = isActive
}
public async showUpdateDialog(mainWindow: BrowserWindow) {
if (!this.releaseInfo) {
return
@@ -73,7 +81,7 @@ export default class AppUpdater {
app.isQuitting = true
setImmediate(() => autoUpdater.quitAndInstall())
} else {
mainWindow.webContents.send('update-downloaded-cancelled')
mainWindow.webContents.send(IpcChannel.UpdateDownloadedCancelled)
}
})
}

View File

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

View File

@@ -1,11 +1,13 @@
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, FileStat } from 'webdav'
import { createClient, CreateDirectoryOptions, FileStat } from 'webdav'
import WebDav from './WebDav'
import { windowService } from './WindowService'
@@ -15,11 +17,13 @@ class BackupManager {
private backupDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup')
constructor() {
this.checkConnection = this.checkConnection.bind(this)
this.backup = this.backup.bind(this)
this.restore = this.restore.bind(this)
this.backupToWebdav = this.backupToWebdav.bind(this)
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
this.listWebdavFiles = this.listWebdavFiles.bind(this)
this.deleteWebdavFile = this.deleteWebdavFile.bind(this)
}
private async setWritableRecursive(dirPath: string): Promise<void> {
@@ -78,7 +82,7 @@ class BackupManager {
const mainWindow = windowService.getMainWindow()
const onProgress = (processData: { stage: string; progress: number; total: number }) => {
mainWindow?.webContents.send('backup-progress', processData)
mainWindow?.webContents.send(IpcChannel.BackupProgress, processData)
Logger.log('[BackupManager] backup progress', processData)
}
@@ -86,9 +90,18 @@ class BackupManager {
await fs.ensureDir(this.tempDir)
onProgress({ stage: 'preparing', progress: 0, total: 100 })
// 将 data 写入临时文件
// 使用流的方式写入 data.json
const tempDataPath = path.join(this.tempDir, 'data.json')
await fs.writeFile(tempDataPath, data)
await new Promise<void>((resolve, reject) => {
const writeStream = fs.createWriteStream(tempDataPath)
writeStream.write(data)
writeStream.end()
writeStream.on('finish', () => resolve())
writeStream.on('error', (error) => reject(error))
})
onProgress({ stage: 'writing_data', progress: 20, total: 100 })
// 复制 Data 目录到临时目录
@@ -102,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)
@@ -123,6 +210,8 @@ class BackupManager {
return backupedFilePath
} catch (error) {
Logger.error('[BackupManager] Backup failed:', error)
// 确保清理临时目录
await fs.remove(this.tempDir).catch(() => {})
throw error
}
}
@@ -131,7 +220,7 @@ class BackupManager {
const mainWindow = windowService.getMainWindow()
const onProgress = (processData: { stage: string; progress: number; total: number }) => {
mainWindow?.webContents.send('restore-progress', processData)
mainWindow?.webContents.send(IpcChannel.RestoreProgress, processData)
Logger.log('[BackupManager] restore progress', processData)
}
@@ -141,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 目录
@@ -167,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 })
})
@@ -207,8 +302,15 @@ class BackupManager {
fs.mkdirSync(this.backupDir, { recursive: true })
}
// sync为同步写无须await
fs.writeFileSync(backupedFilePath, retrievedFile as Buffer)
// 使用流的方式写入文件
await new Promise<void>((resolve, reject) => {
const writeStream = fs.createWriteStream(backupedFilePath)
writeStream.write(retrievedFile as Buffer)
writeStream.end()
writeStream.on('finish', () => resolve())
writeStream.on('error', (error) => reject(error))
})
return await this.restore(_, backupedFilePath)
} catch (error: any) {
@@ -278,6 +380,31 @@ class BackupManager {
}
}
}
async checkConnection(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
const webdavClient = new WebDav(webdavConfig)
return await webdavClient.checkConnection()
}
async createDirectory(
_: Electron.IpcMainInvokeEvent,
webdavConfig: WebDavConfig,
path: string,
options?: CreateDirectoryOptions
) {
const webdavClient = new WebDav(webdavConfig)
return await webdavClient.createDirectory(path, options)
}
async deleteWebdavFile(_: Electron.IpcMainInvokeEvent, fileName: string, webdavConfig: WebDavConfig) {
try {
const webdavClient = new WebDav(webdavConfig)
return await webdavClient.deleteFile(fileName)
} catch (error: any) {
Logger.error('Failed to delete WebDAV file:', error)
throw new Error(error.message || 'Failed to delete backup file')
}
}
}
export default BackupManager

View File

@@ -1,10 +1,24 @@
import { ZOOM_SHORTCUTS } from '@shared/config/constant'
import { defaultLanguage, ZOOM_SHORTCUTS } from '@shared/config/constant'
import { LanguageVarious, Shortcut, ThemeMode } from '@types'
import { app } from 'electron'
import Store from 'electron-store'
import { locales } from '../utils/locales'
enum ConfigKeys {
Language = 'language',
Theme = 'theme',
LaunchToTray = 'launchToTray',
Tray = 'tray',
TrayOnClose = 'trayOnClose',
ZoomFactor = 'ZoomFactor',
Shortcuts = 'shortcuts',
ClickTrayToShowQuickAssistant = 'clickTrayToShowQuickAssistant',
EnableQuickAssistant = 'enableQuickAssistant',
AutoUpdate = 'autoUpdate',
EnableDataCollection = 'enableDataCollection'
}
export class ConfigManager {
private store: Store
private subscribers: Map<string, Array<(newValue: any) => void>> = new Map()
@@ -14,38 +28,62 @@ export class ConfigManager {
}
getLanguage(): LanguageVarious {
const locale = Object.keys(locales).includes(app.getLocale()) ? app.getLocale() : 'en-US'
return this.store.get('language', locale) as LanguageVarious
const locale = Object.keys(locales).includes(app.getLocale()) ? app.getLocale() : defaultLanguage
return this.get(ConfigKeys.Language, locale) as LanguageVarious
}
setLanguage(theme: LanguageVarious) {
this.store.set('language', theme)
this.set(ConfigKeys.Language, theme)
}
getTheme(): ThemeMode {
return this.store.get('theme', ThemeMode.light) as ThemeMode
return this.get(ConfigKeys.Theme, ThemeMode.light)
}
setTheme(theme: ThemeMode) {
this.store.set('theme', theme)
this.set(ConfigKeys.Theme, theme)
}
getCustomCss(): string {
return this.store.get('customCss', '') as string
}
setCustomCss(css: string) {
this.store.set('customCss', css)
}
getLaunchToTray(): boolean {
return !!this.get(ConfigKeys.LaunchToTray, false)
}
setLaunchToTray(value: boolean) {
this.set(ConfigKeys.LaunchToTray, value)
}
getTray(): boolean {
return !!this.store.get('tray', true)
return !!this.get(ConfigKeys.Tray, true)
}
setTray(value: boolean) {
this.store.set('tray', value)
this.notifySubscribers('tray', value)
this.set(ConfigKeys.Tray, value)
this.notifySubscribers(ConfigKeys.Tray, value)
}
getTrayOnClose(): boolean {
return !!this.get(ConfigKeys.TrayOnClose, true)
}
setTrayOnClose(value: boolean) {
this.set(ConfigKeys.TrayOnClose, value)
}
getZoomFactor(): number {
return this.store.get('zoomFactor', 1) as number
return this.get<number>(ConfigKeys.ZoomFactor, 1)
}
setZoomFactor(factor: number) {
this.store.set('zoomFactor', factor)
this.notifySubscribers('zoomFactor', factor)
this.set(ConfigKeys.ZoomFactor, factor)
this.notifySubscribers(ConfigKeys.ZoomFactor, factor)
}
subscribe<T>(key: string, callback: (newValue: T) => void) {
@@ -73,39 +111,55 @@ export class ConfigManager {
}
getShortcuts() {
return this.store.get('shortcuts', ZOOM_SHORTCUTS) as Shortcut[] | []
return this.get(ConfigKeys.Shortcuts, ZOOM_SHORTCUTS) as Shortcut[] | []
}
setShortcuts(shortcuts: Shortcut[]) {
this.store.set(
'shortcuts',
this.set(
ConfigKeys.Shortcuts,
shortcuts.filter((shortcut) => shortcut.system)
)
this.notifySubscribers('shortcuts', shortcuts)
this.notifySubscribers(ConfigKeys.Shortcuts, shortcuts)
}
getClickTrayToShowQuickAssistant(): boolean {
return this.store.get('clickTrayToShowQuickAssistant', false) as boolean
return this.get<boolean>(ConfigKeys.ClickTrayToShowQuickAssistant, false)
}
setClickTrayToShowQuickAssistant(value: boolean) {
this.store.set('clickTrayToShowQuickAssistant', value)
this.set(ConfigKeys.ClickTrayToShowQuickAssistant, value)
}
getEnableQuickAssistant(): boolean {
return this.store.get('enableQuickAssistant', false) as boolean
return this.get(ConfigKeys.EnableQuickAssistant, false)
}
setEnableQuickAssistant(value: boolean) {
this.store.set('enableQuickAssistant', value)
this.set(ConfigKeys.EnableQuickAssistant, value)
}
set(key: string, value: any) {
getAutoUpdate(): boolean {
return this.get<boolean>(ConfigKeys.AutoUpdate, true)
}
setAutoUpdate(value: boolean) {
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)
}
get(key: string) {
return this.store.get(key)
get<T>(key: string, defaultValue?: T) {
return this.store.get(key, defaultValue) as T
}
}

View File

@@ -1,8 +1,10 @@
import axios, { AxiosRequestConfig } from 'axios'
import { AxiosRequestConfig } from 'axios'
import { app, safeStorage } from 'electron'
import fs from 'fs/promises'
import path from 'path'
import aoxisProxy from './AxiosProxy'
// 配置常量,集中管理
const CONFIG = {
GITHUB_CLIENT_ID: 'Iv1.b507a08c87ecfe98',
@@ -93,7 +95,7 @@ class CopilotService {
}
}
const response = await axios.get(CONFIG.API_URLS.GITHUB_USER, config)
const response = await aoxisProxy.axios.get(CONFIG.API_URLS.GITHUB_USER, config)
return {
login: response.data.login,
avatar: response.data.avatar_url
@@ -114,7 +116,7 @@ class CopilotService {
try {
this.updateHeaders(headers)
const response = await axios.post<AuthResponse>(
const response = await aoxisProxy.axios.post<AuthResponse>(
CONFIG.API_URLS.GITHUB_DEVICE_CODE,
{
client_id: CONFIG.GITHUB_CLIENT_ID,
@@ -146,7 +148,7 @@ class CopilotService {
await this.delay(currentDelay)
try {
const response = await axios.post<TokenResponse>(
const response = await aoxisProxy.axios.post<TokenResponse>(
CONFIG.API_URLS.GITHUB_ACCESS_TOKEN,
{
client_id: CONFIG.GITHUB_CLIENT_ID,
@@ -208,7 +210,7 @@ class CopilotService {
}
}
const response = await axios.get<CopilotTokenResponse>(CONFIG.API_URLS.COPILOT_TOKEN, config)
const response = await aoxisProxy.axios.get<CopilotTokenResponse>(CONFIG.API_URLS.COPILOT_TOKEN, config)
return response.data
} catch (error) {

View File

@@ -1,5 +1,5 @@
import { getFilesDir, getFileType, getTempDir } from '@main/utils/file'
import { documentExts, imageExts } from '@shared/config/constant'
import { documentExts, imageExts, MB } from '@shared/config/constant'
import { FileType } from '@types'
import * as crypto from 'crypto'
import {
@@ -122,7 +122,7 @@ class FileStorage {
private async compressImage(sourcePath: string, destPath: string): Promise<void> {
try {
const stats = fs.statSync(sourcePath)
const fileSizeInMB = stats.size / (1024 * 1024)
const fileSizeInMB = stats.size / MB
// 如果图片大于1MB才进行压缩
if (fileSizeInMB > 1) {

View File

@@ -1,20 +1,22 @@
import { FileMetadataResponse, FileState, GoogleAIFileManager } from '@google/generative-ai/server'
import { File, FileState, GoogleGenAI, Pager } from '@google/genai'
import { FileType } from '@types'
import fs from 'fs'
import { CacheService } from './CacheService'
import { proxyManager } from './ProxyManager'
export class GeminiService {
private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list'
private static readonly CACHE_DURATION = 3000
static async uploadFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string) {
proxyManager.setGlobalProxy()
const fileManager = new GoogleAIFileManager(apiKey)
const uploadResult = await fileManager.uploadFile(file.path, {
mimeType: 'application/pdf',
displayName: file.origin_name
static async uploadFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string): Promise<File> {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
const uploadResult = await sdk.files.upload({
file: file.path,
config: {
mimeType: 'application/pdf',
name: file.id,
displayName: file.origin_name
}
})
return uploadResult
}
@@ -26,43 +28,42 @@ export class GeminiService {
}
}
static async retrieveFile(
_: Electron.IpcMainInvokeEvent,
file: FileType,
apiKey: string
): Promise<FileMetadataResponse | undefined> {
proxyManager.setGlobalProxy()
const fileManager = new GoogleAIFileManager(apiKey)
static async retrieveFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string): Promise<File | undefined> {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
const cachedResponse = CacheService.get<any>(GeminiService.FILE_LIST_CACHE_KEY)
if (cachedResponse) {
return GeminiService.processResponse(cachedResponse, file)
}
const response = await fileManager.listFiles()
const response = await sdk.files.list()
CacheService.set(GeminiService.FILE_LIST_CACHE_KEY, response, GeminiService.CACHE_DURATION)
return GeminiService.processResponse(response, file)
}
private static processResponse(response: any, file: FileType) {
if (response.files) {
return response.files
.filter((file) => file.state === FileState.ACTIVE)
.find((i) => i.displayName === file.origin_name && Number(i.sizeBytes) === file.size)
private static async processResponse(response: Pager<File>, file: FileType) {
for await (const f of response) {
if (f.state === FileState.ACTIVE) {
if (f.displayName === file.origin_name && Number(f.sizeBytes) === file.size) {
return f
}
}
}
return undefined
}
static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string) {
proxyManager.setGlobalProxy()
const fileManager = new GoogleAIFileManager(apiKey)
return await fileManager.listFiles()
static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string): Promise<File[]> {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
const files: File[] = []
for await (const f of await sdk.files.list()) {
files.push(f)
}
return files
}
static async deleteFile(_: Electron.IpcMainInvokeEvent, apiKey: string, fileId: string) {
proxyManager.setGlobalProxy()
const fileManager = new GoogleAIFileManager(apiKey)
await fileManager.deleteFile(fileId)
static async deleteFile(_: Electron.IpcMainInvokeEvent, fileId: string, apiKey: string) {
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
await sdk.files.delete({ name: fileId })
}
}

View File

@@ -16,19 +16,19 @@
import * as fs from 'node:fs'
import path from 'node:path'
import { RAGApplication, RAGApplicationBuilder, TextLoader } from '@llm-tools/embedjs'
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import { LibSqlDb } from '@llm-tools/embedjs-libsql'
import { SitemapLoader } from '@llm-tools/embedjs-loader-sitemap'
import { WebLoader } from '@llm-tools/embedjs-loader-web'
import { AzureOpenAiEmbeddings, OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
import { RAGApplication, RAGApplicationBuilder, TextLoader } from '@cherrystudio/embedjs'
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { LibSqlDb } from '@cherrystudio/embedjs-libsql'
import { SitemapLoader } from '@cherrystudio/embedjs-loader-sitemap'
import { WebLoader } from '@cherrystudio/embedjs-loader-web'
import Embeddings from '@main/embeddings/Embeddings'
import { addFileLoader } from '@main/loader'
import Reranker from '@main/reranker/Reranker'
import { proxyManager } from '@main/services/ProxyManager'
import { windowService } from '@main/services/WindowService'
import { getInstanceName } from '@main/utils'
import { getAllFiles } from '@main/utils/file'
import { MB } from '@shared/config/constant'
import type { LoaderReturn } from '@shared/config/types'
import { IpcChannel } from '@shared/IpcChannel'
import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types'
import { app } from 'electron'
import Logger from 'electron-log'
@@ -93,7 +93,7 @@ class KnowledgeService {
private workload = 0
private processingItemCount = 0
private knowledgeItemProcessingQueueMappingPromise: Map<LoaderTaskOfSet, () => void> = new Map()
private static MAXIMUM_WORKLOAD = 1024 * 1024 * 80
private static MAXIMUM_WORKLOAD = 80 * MB
private static MAXIMUM_PROCESSING_ITEM_COUNT = 30
private static ERROR_LOADER_RETURN: LoaderReturn = { entriesAdded: 0, uniqueId: '', uniqueIds: [''], loaderType: '' }
@@ -115,30 +115,20 @@ class KnowledgeService {
baseURL,
dimensions
}: KnowledgeBaseParams): Promise<RAGApplication> => {
const batchSize = 10
return new RAGApplicationBuilder()
.setModel('NO_MODEL')
.setEmbeddingModel(
apiVersion
? new AzureOpenAiEmbeddings({
azureOpenAIApiKey: apiKey,
azureOpenAIApiVersion: apiVersion,
azureOpenAIApiDeploymentName: model,
azureOpenAIApiInstanceName: getInstanceName(baseURL),
configuration: { httpAgent: proxyManager.getProxyAgent() },
dimensions,
batchSize
})
: new OpenAiEmbeddings({
model,
apiKey,
configuration: { baseURL, httpAgent: proxyManager.getProxyAgent() },
dimensions,
batchSize
})
)
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
.build()
let ragApplication: RAGApplication
const embeddings = new Embeddings({ model, apiKey, apiVersion, baseURL, dimensions } as KnowledgeBaseParams)
try {
ragApplication = await new RAGApplicationBuilder()
.setModel('NO_MODEL')
.setEmbeddingModel(embeddings)
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
.build()
} catch (e) {
Logger.error(e)
throw new Error(`Failed to create RAGApplication: ${e}`)
}
return ragApplication
}
public create = async (_: Electron.IpcMainInvokeEvent, base: KnowledgeBaseParams): Promise<void> => {
@@ -206,7 +196,7 @@ class KnowledgeService {
const sendDirectoryProcessingPercent = (totalFiles: number, processedFiles: number) => {
const mainWindow = windowService.getMainWindow()
mainWindow?.webContents.send('directory-processing-percent', {
mainWindow?.webContents.send(IpcChannel.DirectoryProcessingPercent, {
itemId: item.id,
percent: (processedFiles / totalFiles) * 100
})
@@ -282,7 +272,7 @@ class KnowledgeService {
return KnowledgeService.ERROR_LOADER_RETURN
})
},
evaluateTaskWorkload: { workload: 1024 * 1024 * 2 }
evaluateTaskWorkload: { workload: 2 * MB }
}
],
loaderDoneReturn: null
@@ -321,7 +311,7 @@ class KnowledgeService {
Logger.error(err)
return KnowledgeService.ERROR_LOADER_RETURN
}),
evaluateTaskWorkload: { workload: 1024 * 1024 * 20 }
evaluateTaskWorkload: { workload: 20 * MB }
}
],
loaderDoneReturn: null
@@ -426,7 +416,6 @@ class KnowledgeService {
}
public add = (_: Electron.IpcMainInvokeEvent, options: KnowledgeBaseAddItemOptions): Promise<LoaderReturn> => {
proxyManager.setGlobalProxy()
return new Promise((resolve) => {
const { base, item, forceReload = false } = options
const optionsNonNullableAttribute = { base, item, forceReload }
@@ -488,6 +477,9 @@ class KnowledgeService {
_: Electron.IpcMainInvokeEvent,
{ search, base, results }: { search: string; base: KnowledgeBaseParams; results: ExtractChunkData[] }
): Promise<ExtractChunkData[]> => {
if (results.length === 0) {
return results
}
return await new Reranker(base).rerank(search, results)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,365 @@
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()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,136 @@
import path from 'node:path'
import { NUTSTORE_HOST } from '@shared/config/nutstore'
import { XMLParser } from 'fast-xml-parser'
import { isNil, partial } from 'lodash'
import { type FileStat } from 'webdav'
import { createOAuthUrl, decryptSecret } from '../integration/nutstore/sso/lib/index.mjs'
interface OAuthResponse {
username: string
userid: string
access_token: string
}
interface WebDAVResponse {
multistatus: {
response: Array<{
href: string
propstat: {
prop: {
displayname: string
resourcetype: { collection?: any }
getlastmodified?: string
getcontentlength?: string
getcontenttype?: string
}
status: string
}
}>
}
}
export async function getNutstoreSSOUrl() {
const url = await createOAuthUrl({
app: 'cherrystudio'
})
return url
}
export async function decryptToken(token: string) {
try {
const decrypted = await decryptSecret({
app: 'cherrystudio',
s: token
})
return JSON.parse(decrypted) as OAuthResponse
} catch (error) {
console.error('解密失败:', error)
return null
}
}
export async function getDirectoryContents(token: string, target: string): Promise<FileStat[]> {
const contents: FileStat[] = []
if (!target.startsWith('/')) {
target = '/' + target
}
let currentUrl = `${NUTSTORE_HOST}${target}`
while (true) {
const response = await fetch(currentUrl, {
method: 'PROPFIND',
headers: {
Authorization: `Basic ${token}`,
'Content-Type': 'application/xml',
Depth: '1'
},
body: `<?xml version="1.0" encoding="utf-8"?>
<propfind xmlns="DAV:">
<prop>
<displayname/>
<resourcetype/>
<getlastmodified/>
<getcontentlength/>
<getcontenttype/>
</prop>
</propfind>`
})
const text = await response.text()
const result = parseXml<WebDAVResponse>(text)
const items = Array.isArray(result.multistatus.response)
? result.multistatus.response
: [result.multistatus.response]
// 跳过第一个条目(当前目录)
contents.push(...items.slice(1).map(partial(convertToFileStat, '/dav')))
const linkHeader = response.headers['link'] || response.headers['Link']
if (!linkHeader) {
break
}
const nextLink = extractNextLink(linkHeader)
if (!nextLink) {
break
}
currentUrl = decodeURI(nextLink)
}
return contents
}
function extractNextLink(linkHeader: string): string | null {
const matches = linkHeader.match(/<([^>]+)>;\s*rel="next"/)
return matches ? matches[1] : null
}
function convertToFileStat(serverBase: string, item: WebDAVResponse['multistatus']['response'][number]): FileStat {
const props = item.propstat.prop
const isDir = !isNil(props.resourcetype?.collection)
const href = decodeURIComponent(item.href)
const filename = serverBase === '/' ? href : path.posix.join('/', href.replace(serverBase, ''))
return {
filename: filename.endsWith('/') ? filename.slice(0, -1) : filename,
basename: path.basename(filename),
lastmod: props.getlastmodified || '',
size: props.getcontentlength ? parseInt(props.getcontentlength, 10) : 0,
type: isDir ? 'directory' : 'file',
etag: null,
mime: props.getcontenttype
}
}
function parseXml<T>(xml: string) {
const parser = new XMLParser({
attributeNamePrefix: '',
removeNSPrefix: true
})
return parser.parse(xml) as T
}

View File

@@ -0,0 +1,167 @@
import { app } from 'electron'
import fs from 'fs'
import path from 'path'
interface VaultInfo {
path: string
name: string
}
interface FileInfo {
path: string
type: 'folder' | 'markdown'
name: string
}
class ObsidianVaultService {
private obsidianConfigPath: string
constructor() {
// 根据操作系统获取Obsidian配置文件路径
if (process.platform === 'win32') {
this.obsidianConfigPath = path.join(app.getPath('appData'), 'obsidian', 'obsidian.json')
} else if (process.platform === 'darwin') {
this.obsidianConfigPath = path.join(
app.getPath('home'),
'Library',
'Application Support',
'obsidian',
'obsidian.json'
)
} else {
// Linux
this.obsidianConfigPath = path.join(app.getPath('home'), '.config', 'obsidian', 'obsidian.json')
}
}
/**
* 获取所有的Obsidian Vault
*/
getVaults(): VaultInfo[] {
try {
if (!fs.existsSync(this.obsidianConfigPath)) {
return []
}
const configContent = fs.readFileSync(this.obsidianConfigPath, 'utf8')
const config = JSON.parse(configContent)
if (!config.vaults) {
return []
}
return Object.entries(config.vaults).map(([, vault]: [string, any]) => ({
path: vault.path,
name: vault.name || path.basename(vault.path)
}))
} catch (error) {
console.error('获取Obsidian Vault失败:', error)
return []
}
}
/**
* 获取Vault中的文件夹和Markdown文件结构
*/
getVaultStructure(vaultPath: string): FileInfo[] {
const results: FileInfo[] = []
try {
// 检查vault路径是否存在
if (!fs.existsSync(vaultPath)) {
console.error('Vault路径不存在:', vaultPath)
return []
}
// 检查是否是目录
const stats = fs.statSync(vaultPath)
if (!stats.isDirectory()) {
console.error('Vault路径不是一个目录:', vaultPath)
return []
}
this.traverseDirectory(vaultPath, '', results)
} catch (error) {
console.error('读取Vault文件夹结构失败:', error)
}
return results
}
/**
* 递归遍历目录获取所有文件夹和Markdown文件
*/
private traverseDirectory(dirPath: string, relativePath: string, results: FileInfo[]) {
try {
// 首先添加当前文件夹
if (relativePath) {
results.push({
path: relativePath,
type: 'folder',
name: path.basename(relativePath)
})
}
// 确保目录存在且可访问
if (!fs.existsSync(dirPath)) {
console.error('目录不存在:', dirPath)
return
}
let items
try {
items = fs.readdirSync(dirPath, { withFileTypes: true })
} catch (err) {
console.error(`无法读取目录 ${dirPath}:`, err)
return
}
for (const item of items) {
// 忽略以.开头的隐藏文件夹和文件
if (item.name.startsWith('.')) {
continue
}
const newRelativePath = relativePath ? `${relativePath}/${item.name}` : item.name
const fullPath = path.join(dirPath, item.name)
if (item.isDirectory()) {
this.traverseDirectory(fullPath, newRelativePath, results)
} else if (item.isFile() && item.name.endsWith('.md')) {
// 收集.md文件
results.push({
path: newRelativePath,
type: 'markdown',
name: item.name
})
}
}
} catch (error) {
console.error(`遍历目录出错 ${dirPath}:`, error)
}
}
/**
* 获取指定Vault的文件夹和Markdown文件结构
* @param vaultName vault名称
*/
getFilesByVaultName(vaultName: string): FileInfo[] {
try {
const vaults = this.getVaults()
const vault = vaults.find((v) => v.name === vaultName)
if (!vault) {
console.error('未找到指定名称的Vault:', vaultName)
return []
}
console.log('获取Vault文件结构:', vault.name, vault.path)
return this.getVaultStructure(vault.path)
} catch (error) {
console.error('获取Vault文件结构时发生错误:', error)
return []
}
}
}
export default ObsidianVaultService

View File

@@ -0,0 +1,34 @@
import { windowService } from './WindowService'
export const CHERRY_STUDIO_PROTOCOL = 'cherrystudio'
export function registerProtocolClient(app: Electron.App) {
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(CHERRY_STUDIO_PROTOCOL, process.execPath, [process.argv[1]])
}
}
app.setAsDefaultProtocolClient('cherrystudio')
}
export function handleProtocolUrl(url: string) {
if (!url) return
// Process the URL that was used to open the app
// The url will be in the format: cherrystudio://data?param1=value1&param2=value2
console.log('Received URL:', url)
// Parse the URL and extract parameters
const urlObj = new URL(url)
const params = new URLSearchParams(urlObj.search)
// You can send the data to your renderer process
const mainWindow = windowService.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('protocol-data', {
url,
params: Object.fromEntries(params.entries())
})
}
}

View File

@@ -1,25 +1,24 @@
import { ProxyConfig as _ProxyConfig, session } from 'electron'
import { socksDispatcher } from 'fetch-socks'
import { HttpsProxyAgent } from 'https-proxy-agent'
import { getSystemProxy } from 'os-proxy-config'
import { ProxyAgent as GeneralProxyAgent } from 'proxy-agent'
import { ProxyAgent, setGlobalDispatcher } from 'undici'
type ProxyMode = 'system' | 'custom' | 'none'
export interface ProxyConfig {
mode: ProxyMode
url?: string | null
url?: string
}
export class ProxyManager {
private config: ProxyConfig
private proxyAgent: HttpsProxyAgent | null = null
private proxyUrl: string | null = null
private proxyAgent: GeneralProxyAgent | null = null
private systemProxyInterval: NodeJS.Timeout | null = null
constructor() {
this.config = {
mode: 'none',
url: ''
mode: 'none'
}
}
@@ -51,7 +50,7 @@ export class ProxyManager {
if (this.config.mode === 'system') {
await this.setSystemProxy()
this.monitorSystemProxy()
} else if (this.config.mode == 'custom') {
} else if (this.config.mode === 'custom') {
await this.setCustomProxy()
} else {
await this.clearProxy()
@@ -72,13 +71,14 @@ export class ProxyManager {
private async setSystemProxy(): Promise<void> {
try {
await this.setSessionsProxy({ mode: 'system' })
const url = await this.resolveSystemProxy()
if (url && url !== this.proxyUrl) {
this.proxyUrl = url.toLowerCase()
this.proxyAgent = new HttpsProxyAgent(this.proxyUrl)
this.setEnvironment(this.proxyUrl)
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
@@ -88,10 +88,9 @@ export class ProxyManager {
private async setCustomProxy(): Promise<void> {
try {
if (this.config.url) {
this.proxyUrl = this.config.url.toLowerCase()
this.proxyAgent = new HttpsProxyAgent(this.proxyUrl)
this.setEnvironment(this.proxyUrl)
await this.setSessionsProxy({ proxyRules: this.proxyUrl })
this.setEnvironment(this.config.url)
this.proxyAgent = new GeneralProxyAgent()
await this.setSessionsProxy({ proxyRules: this.config.url })
}
} catch (error) {
console.error('Failed to set custom proxy:', error)
@@ -99,45 +98,31 @@ export class ProxyManager {
}
}
private async clearProxy(): Promise<void> {
private clearEnvironment(): void {
delete process.env.HTTP_PROXY
delete process.env.HTTPS_PROXY
await this.setSessionsProxy({})
delete process.env.grpc_proxy
delete process.env.http_proxy
delete process.env.https_proxy
}
private async clearProxy(): Promise<void> {
this.clearEnvironment()
await this.setSessionsProxy({ mode: 'direct' })
this.config = { mode: 'none' }
this.proxyAgent = null
this.proxyUrl = null
}
private async resolveSystemProxy(): Promise<string | null> {
try {
return await this.resolveElectronProxy()
} catch (error) {
console.error('Failed to resolve system proxy:', error)
return null
}
}
private async resolveElectronProxy(): Promise<string | null> {
try {
const proxyString = await session.defaultSession.resolveProxy('https://dummy.com')
const [protocol, address] = proxyString.split(';')[0].split(' ')
return protocol === 'PROXY' ? `http://${address}` : null
} catch (error) {
console.error('Failed to resolve electron proxy:', error)
return null
}
}
getProxyAgent(): HttpsProxyAgent | null {
getProxyAgent(): GeneralProxyAgent | null {
return this.proxyAgent
}
getProxyUrl(): string | null {
return this.proxyUrl
getProxyUrl(): string {
return this.config.url || ''
}
setGlobalProxy() {
const proxyUrl = this.proxyUrl
const proxyUrl = this.config.url
if (proxyUrl) {
const [protocol, address] = proxyUrl.split('://')
const [host, port] = address.split(':')

View File

@@ -1,3 +1,4 @@
import { IpcChannel } from '@shared/IpcChannel'
import { ipcMain } from 'electron'
import { EventEmitter } from 'events'
@@ -10,6 +11,8 @@ export class ReduxService extends EventEmitter {
private stateCache: any = {}
private isReady = false
private readonly STATUS_CHANGE_EVENT = 'statusChange'
constructor() {
super()
this.setupIpcHandlers()
@@ -17,15 +20,15 @@ export class ReduxService extends EventEmitter {
private setupIpcHandlers() {
// 监听 store 就绪事件
ipcMain.handle('redux-store-ready', () => {
ipcMain.handle(IpcChannel.ReduxStoreReady, () => {
this.isReady = true
this.emit('ready')
})
// 监听 store 状态变化
ipcMain.on('redux-state-change', (_, newState) => {
ipcMain.on(IpcChannel.ReduxStateChange, (_, newState) => {
this.stateCache = newState
this.emit('stateChange', newState)
this.emit(this.STATUS_CHANGE_EVENT, newState)
})
}
@@ -122,19 +125,23 @@ export class ReduxService extends EventEmitter {
await this.waitForStoreReady(mainWindow.webContents)
// 在渲染进程中设置监听
await mainWindow.webContents.executeJavaScript(`
await mainWindow.webContents.executeJavaScript(
`
if (!window._storeSubscriptions) {
window._storeSubscriptions = new Set();
// 设置全局状态变化监听
const unsubscribe = window.store.subscribe(() => {
const state = window.store.getState();
window.electron.ipcRenderer.send('redux-state-change', state);
window.electron.ipcRenderer.send('` +
IpcChannel.ReduxStateChange +
`', state);
});
window._storeSubscriptions.add(unsubscribe);
}
`)
`
)
// 在主进程中处理回调
const handler = async () => {
@@ -146,9 +153,9 @@ export class ReduxService extends EventEmitter {
}
}
this.on('stateChange', handler)
this.on(this.STATUS_CHANGE_EVENT, handler)
return () => {
this.off('stateChange', handler)
this.off(this.STATUS_CHANGE_EVENT, handler)
}
}
@@ -180,41 +187,41 @@ export class ReduxService extends EventEmitter {
export const reduxService = new ReduxService()
/** example
async function example() {
try {
// 读取状态
const settings = await reduxService.select('state.settings')
console.log('settings', settings)
async function example() {
try {
// 读取状态
const settings = await reduxService.select('state.settings')
console.log('settings', settings)
// 派发 action
await reduxService.dispatch({
type: 'settings/updateApiKey',
payload: 'new-api-key'
})
// 派发 action
await reduxService.dispatch({
type: 'settings/updateApiKey',
payload: 'new-api-key'
})
// 订阅状态变化
const unsubscribe = await reduxService.subscribe('state.settings.apiKey', (newValue) => {
console.log('API key changed:', newValue)
})
// 订阅状态变化
const unsubscribe = await reduxService.subscribe('state.settings.apiKey', (newValue) => {
console.log('API key changed:', newValue)
})
// 批量执行 actions
await reduxService.batch([
{ type: 'action1', payload: 'data1' },
{ type: 'action2', payload: 'data2' }
])
// 批量执行 actions
await reduxService.batch([
{ type: 'action1', payload: 'data1' },
{ type: 'action2', payload: 'data2' }
])
// 同步方法虽然可能不是最新的数据,但响应更快
const apiKey = reduxService.selectSync('state.settings.apiKey')
console.log('apiKey', apiKey)
// 同步方法虽然可能不是最新的数据,但响应更快
const apiKey = reduxService.selectSync('state.settings.apiKey')
console.log('apiKey', apiKey)
// 处理保证是最新的数据
const apiKey1 = await reduxService.select('state.settings.apiKey')
console.log('apiKey1', apiKey1)
// 处理保证是最新的数据
const apiKey1 = await reduxService.select('state.settings.apiKey')
console.log('apiKey1', apiKey1)
// 取消订阅
unsubscribe()
} catch (error) {
console.error('Error:', error)
}
}
*/
// 取消订阅
unsubscribe()
} catch (error) {
console.error('Error:', error)
}
}
*/

View File

@@ -0,0 +1,82 @@
import { is } from '@electron-toolkit/utils'
import { BrowserWindow } from 'electron'
export class SearchService {
private static instance: SearchService | null = null
private searchWindows: Record<string, BrowserWindow> = {}
public static getInstance(): SearchService {
if (!SearchService.instance) {
SearchService.instance = new SearchService()
}
return SearchService.instance
}
constructor() {
// Initialize the service
}
private async createNewSearchWindow(uid: string): Promise<BrowserWindow> {
const newWindow = new BrowserWindow({
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
devTools: is.dev
}
})
newWindow.webContents.session.webRequest.onBeforeSendHeaders({ urls: ['*://*/*'] }, (details, callback) => {
const headers = {
...details.requestHeaders,
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
callback({ requestHeaders: headers })
})
this.searchWindows[uid] = newWindow
newWindow.on('closed', () => {
delete this.searchWindows[uid]
})
return newWindow
}
public async openSearchWindow(uid: string): Promise<void> {
await this.createNewSearchWindow(uid)
}
public async closeSearchWindow(uid: string): Promise<void> {
const window = this.searchWindows[uid]
if (window) {
window.close()
delete this.searchWindows[uid]
}
}
public async openUrlInSearchWindow(uid: string, url: string): Promise<any> {
let window = this.searchWindows[uid]
if (window) {
await window.loadURL(url)
} else {
window = await this.createNewSearchWindow(uid)
await window.loadURL(url)
}
// Get the page content after loading the URL
// Wait for the page to fully load before getting the content
await new Promise<void>((resolve) => {
const loadTimeout = setTimeout(() => resolve(), 10000) // 10 second timeout
window.webContents.once('did-finish-load', () => {
clearTimeout(loadTimeout)
// Small delay to ensure JavaScript has executed
setTimeout(resolve, 500)
})
})
// Get the page content after ensuring it's fully loaded
const content = await window.webContents.executeJavaScript('document.documentElement.outerHTML')
return content
}
}
export const searchService = SearchService.getInstance()

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