From 582427663f59cc55a58600cb7cc56a19a061187f Mon Sep 17 00:00:00 2001
From: kangfenmao
Date: Mon, 19 May 2025 22:22:13 +0800
Subject: [PATCH 01/68] docs: update branching strategy documentation and add
English and Chinese versions
- Updated the README to link to the new English branching strategy document.
- Added a new English version of the branching strategy document.
- Removed the outdated branching strategy document.
- Added a Chinese version of the branching strategy document.
---
README.md | 2 +-
docs/README.ja.md | 69 +++++++++++++++++++---------------
docs/README.zh.md | 51 ++++++++++++++-----------
docs/branching-strategy-en.md | 71 +++++++++++++++++++++++++++++++++++
docs/branching-strategy-zh.md | 71 +++++++++++++++++++++++++++++++++++
docs/branching-strategy.md | 52 -------------------------
6 files changed, 211 insertions(+), 105 deletions(-)
create mode 100644 docs/branching-strategy-en.md
create mode 100644 docs/branching-strategy-zh.md
delete mode 100644 docs/branching-strategy.md
diff --git a/README.md b/README.md
index 96f727a96e..e5315f5ce9 100644
--- a/README.md
+++ b/README.md
@@ -96,7 +96,7 @@ Refer to the [development documentation](docs/dev.md)
Refer to the [Architecture overview documentation](https://deepwiki.com/CherryHQ/cherry-studio)
-Refer to the [Branching Strategy](docs/branching-strategy.md) for contribution guidelines
+Refer to the [Branching Strategy](docs/branching-strategy-en.md) for contribution guidelines
# 🤝 Contributing
diff --git a/docs/README.ja.md b/docs/README.ja.md
index 02983db685..ce5d2f6ef7 100644
--- a/docs/README.ja.md
+++ b/docs/README.ja.md
@@ -6,17 +6,19 @@
English | 中文 | 日本語
+
+
# 🍒 Cherry Studio
Cherry Studio は、複数の LLM プロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linux で利用可能です。
👏 [Telegram](https://t.me/CherryStudioAI)|[Discord](https://discord.gg/wez8HtpxqQ) | [QQグループ(575014769)](https://qm.qq.com/q/lo0D4qVZKi)
-❤️ Cherry Studio をお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!❤️
+❤️ Cherry Studio をお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!
# 📖 ガイド
@@ -56,7 +58,7 @@ https://docs.cherry-ai.com
- 🔤 AI による翻訳機能
- 🎯 ドラッグ&ドロップによる整理
- 🔌 ミニプログラム対応
-- ⚙️ MCP(モデルコンテキストプロトコル) サービス
+- ⚙️ MCP(モデルコンテキストプロトコル)サービス
5. **優れたユーザー体験**:
@@ -70,71 +72,78 @@ https://docs.cherry-ai.com
- [x] クイックポップアップ(クリップボードの読み取り、簡単な質問、説明、翻訳、要約)
- [x] 複数モデルの回答の比較
-- [x] サービスプロバイダーが提供する SSO を使用したログインをサポート
-- [x] すべてのモデルがネットワークをサポート
+- [x] サービスプロバイダーが提供する SSO を使用したログイン対応
+- [x] すべてのモデルのネットワーク対応
- [x] 最初の公式バージョンのリリース
-- [ ] 錯誤修復と改善 (開発中...)
+- [x] バグ修正と改善(進行中...)
- [ ] プラグイン機能(JavaScript)
- [ ] ブラウザ拡張機能(テキストをハイライトして翻訳、要約、ナレッジベースに追加)
- [ ] iOS & Android クライアント
-- [ ] AIノート
+- [ ] AI ノート
- [ ] 音声入出力(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
+- テーマギャラリー: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を歓迎します
+より多くのテーマの PR を歓迎します
# 🖥️ 開発
-参考[開発ドキュメント](dev.md)
+[開発ドキュメント](dev.md)を参照してください
+
+[アーキテクチャ概要ドキュメント](https://deepwiki.com/CherryHQ/cherry-studio)を参照してください
+
+[ブランチ戦略](branching-strategy-en.md)を参照して貢献ガイドラインを確認してください
# 🤝 貢献
Cherry Studio への貢献を歓迎します!以下の方法で貢献できます:
-1. **コードの貢献**:新機能を開発するか、既存のコードを最適化します。
-2. **バグの修正**:見つけたバグを修正します。
-3. **問題の管理**:GitHub の問題を管理するのを手伝います。
-4. **製品デザイン**:デザインの議論に参加します。
-5. **ドキュメントの作成**:ユーザーマニュアルやガイドを改善します。
-6. **コミュニティの参加**:ディスカッションに参加し、ユーザーを支援します。
-7. **使用の促進**:Cherry Studio を広めます。
+1. **コードの貢献**:新機能を開発するか、既存のコードを最適化します
+2. **バグの修正**:見つけたバグを修正します
+3. **問題の管理**:GitHub の問題を管理するのを手伝います
+4. **製品デザイン**:デザインの議論に参加します
+5. **ドキュメントの作成**:ユーザーマニュアルやガイドを改善します
+6. **コミュニティの参加**:ディスカッションに参加し、ユーザーを支援します
+7. **使用の促進**:Cherry Studio を広めます
## 始め方
-1. **リポジトリをフォーク**:フォークしてローカルマシンにクローンします。
-2. **ブランチを作成**:変更のためのブランチを作成します。
-3. **変更を提出**:変更をコミットしてプッシュします。
-4. **プルリクエストを開く**:変更内容と理由を説明します。
+1. **リポジトリをフォーク**:フォークしてローカルマシンにクローンします
+2. **ブランチを作成**:変更のためのブランチを作成します
+3. **変更を提出**:変更をコミットしてプッシュします
+4. **プルリクエストを開く**:変更内容と理由を説明します
詳細なガイドラインについては、[貢献ガイド](../CONTRIBUTING.md)をご覧ください。
ご支援と貢献に感謝します!
-## 関連頁版
+## 関連プロジェクト
- [one-api](https://github.com/songquanpeng/one-api):LLM API の管理・配信システム。OpenAI、Azure、Anthropic などの主要モデルに対応し、統一 API インターフェースを提供。API キー管理と再配布に利用可能。
+- [ublacklist](https://github.com/iorate/ublacklist):Google 検索結果から特定のサイトを非表示にします
+
# 🚀 コントリビューター
-
+
+
-# コミュニティ
+# 🌐 コミュニティ
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
-# スポンサー
+# ☕ スポンサー
-[Buy Me a Coffee](sponsor.md)
+[開発者を支援する](sponsor.md)
# 📃 ライセンス
diff --git a/docs/README.zh.md b/docs/README.zh.md
index 1e4876a820..ca85959dab 100644
--- a/docs/README.zh.md
+++ b/docs/README.zh.md
@@ -4,7 +4,8 @@
- English | 中文 | 日本語
+ English | 中文 | 日本語
+
@@ -74,12 +75,12 @@ https://docs.cherry-ai.com
- 📝 完整的 Markdown 渲染
- 🤲 便捷的内容分享功能
-# 📝 待辦事項
+# 📝 待办事项
- [x] 快捷弹窗(读取剪贴板、快速提问、解释、翻译、总结)
- [x] 多模型回答对比
-- [x] 支持使用服务供应商提供的 SSO 进行登入
-- [x] 全部模型支持连网(开发中...)
+- [x] 支持使用服务供应商提供的 SSO 进行登录
+- [x] 所有模型支持联网
- [x] 推出第一个正式版
- [x] 错误修复和改进(开发中...)
- [ ] 插件功能(JavaScript)
@@ -93,9 +94,9 @@ https://docs.cherry-ai.com
- 主题库:https://cherrycss.com
- Aero 主题:https://github.com/hakadao/CherryStudio-Aero
-- PaperMaterial 主题: https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
-- 仿Claude 主题: https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
-- 霓虹枫叶字体主题: https://github.com/BoningtonChen/CherryStudio_themes
+- PaperMaterial 主题:https://github.com/rainoffallingstar/CherryStudio-PaperMaterial
+- 仿 Claude 主题:https://github.com/bjl101501/CherryStudio-Claudestyle-dynamic
+- 霓虹枫叶主题:https://github.com/BoningtonChen/CherryStudio_themes
欢迎 PR 更多主题
@@ -103,26 +104,30 @@ https://docs.cherry-ai.com
参考[开发文档](dev.md)
+参考[架构概览文档](https://deepwiki.com/CherryHQ/cherry-studio)
+
+参考[分支策略](branching-strategy-zh.md)了解贡献指南
+
# 🤝 贡献
我们欢迎对 Cherry Studio 的贡献!您可以通过以下方式贡献:
-1. **贡献代码**:开发新功能或优化现有代码。
-2. **修复错误**:提交您发现的错误修复。
-3. **维护问题**:帮助管理 GitHub 问题。
-4. **产品设计**:参与设计讨论。
-5. **撰写文档**:改进用户手册和指南。
-6. **社区参与**:加入讨论并帮助用户。
-7. **推广使用**:宣传 Cherry Studio。
+1. **贡献代码**:开发新功能或优化现有代码
+2. **修复错误**:提交您发现的错误修复
+3. **维护问题**:帮助管理 GitHub 问题
+4. **产品设计**:参与设计讨论
+5. **撰写文档**:改进用户手册和指南
+6. **社区参与**:加入讨论并帮助用户
+7. **推广使用**:宣传 Cherry Studio
## 入门
-1. **Fork 仓库**:Fork 并克隆到您的本地机器。
-2. **创建分支**:为您的更改创建分支。
-3. **提交更改**:提交并推送您的更改。
-4. **打开 Pull Request**:描述您的更改和原因。
+1. **Fork 仓库**:Fork 并克隆到您的本地机器
+2. **创建分支**:为您的更改创建分支
+3. **提交更改**:提交并推送您的更改
+4. **打开 Pull Request**:描述您的更改和原因
-有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.zh.md)。
+有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.zh.md)
感谢您的支持和贡献!
@@ -130,10 +135,12 @@ https://docs.cherry-ai.com
- [one-api](https://github.com/songquanpeng/one-api):LLM API 管理及分发系统,支持 OpenAI、Azure、Anthropic 等主流模型,统一 API 接口,可用于密钥管理与二次分发。
+- [ublacklist](https://github.com/iorate/ublacklist):屏蔽特定网站在 Google 搜索结果中显示
+
# 🚀 贡献者
-
+
@@ -143,7 +150,7 @@ https://docs.cherry-ai.com
# ☕ 赞助
-[微信赞赏码](sponsor.md)
+[赞助开发者](sponsor.md)
# 📃 许可证
@@ -155,4 +162,4 @@ yinsenho@cherry-ai.com
# ⭐️ Star 记录
-[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
\ No newline at end of file
+[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
diff --git a/docs/branching-strategy-en.md b/docs/branching-strategy-en.md
new file mode 100644
index 0000000000..f3b7ddf508
--- /dev/null
+++ b/docs/branching-strategy-en.md
@@ -0,0 +1,71 @@
+# 🌿 Branching Strategy
+
+Cherry Studio implements a structured branching strategy to maintain code quality and streamline the development process.
+
+## Main Branches
+
+- `main`: Main development branch
+
+ - Contains the latest development code
+ - Direct commits are not allowed - changes must come through pull requests
+ - Code may contain features in development and might not be fully stable
+
+- `release/*`: Release branches
+ - Created from `main` branch
+ - Contains stable code ready for release
+ - Only accepts documentation updates and bug fixes
+ - Thoroughly tested before production deployment
+
+## Contributing Branches
+
+When contributing to Cherry Studio, please follow these guidelines:
+
+1. **Feature Branches:**
+
+ - Create from `main` branch
+ - Naming format: `feature/issue-number-brief-description`
+ - Submit PR back to `main`
+
+2. **Bug Fix Branches:**
+
+ - Create from `main` branch
+ - Naming format: `fix/issue-number-brief-description`
+ - Submit PR back to `main`
+
+3. **Documentation Branches:**
+
+ - Create from `main` branch
+ - Naming format: `docs/brief-description`
+ - Submit PR back to `main`
+
+4. **Hotfix Branches:**
+
+ - Create from `main` branch
+ - Naming format: `hotfix/issue-number-brief-description`
+ - Submit PR to both `main` and relevant `release` branches
+
+5. **Release Branches:**
+ - Create from `main` branch
+ - Naming format: `release/version-number`
+ - Used for final preparation work before version release
+ - Only accepts bug fixes and documentation updates
+ - After testing and preparation, merge back to `main` and tag with version
+
+## Workflow Diagram
+
+
+
+## Pull Request Guidelines
+
+- All PRs should be submitted to the `main` branch unless fixing a critical production issue
+- Ensure your branch is up to date with the latest `main` changes before submitting
+- Include relevant issue numbers in your PR description
+- Make sure all tests pass and code meets our quality standards
+- Add before/after screenshots if you add a new feature or modify a UI component
+
+## Version Tag Management
+
+- Major releases: v1.0.0, v2.0.0, etc.
+- Feature releases: v1.1.0, v1.2.0, etc.
+- Patch releases: v1.0.1, v1.0.2, etc.
+- Hotfix releases: v1.0.1-hotfix, etc.
diff --git a/docs/branching-strategy-zh.md b/docs/branching-strategy-zh.md
new file mode 100644
index 0000000000..b1379537a5
--- /dev/null
+++ b/docs/branching-strategy-zh.md
@@ -0,0 +1,71 @@
+# 🌿 分支策略
+
+Cherry Studio 采用结构化的分支策略来维护代码质量并简化开发流程。
+
+## 主要分支
+
+- `main`:主开发分支
+
+ - 包含最新的开发代码
+ - 禁止直接提交 - 所有更改必须通过拉取请求(Pull Request)
+ - 此分支上的代码可能包含正在开发的功能,不一定完全稳定
+
+- `release/*`:发布分支
+ - 从 `main` 分支创建
+ - 包含准备发布的稳定代码
+ - 只接受文档更新和 bug 修复
+ - 经过完整测试后可以发布到生产环境
+
+## 贡献分支
+
+在为 Cherry Studio 贡献代码时,请遵循以下准则:
+
+1. **功能开发分支:**
+
+ - 从 `main` 分支创建
+ - 命名格式:`feature/issue-number-brief-description`
+ - 完成后提交 PR 到 `main` 分支
+
+2. **Bug 修复分支:**
+
+ - 从 `main` 分支创建
+ - 命名格式:`fix/issue-number-brief-description`
+ - 完成后提交 PR 到 `main` 分支
+
+3. **文档更新分支:**
+
+ - 从 `main` 分支创建
+ - 命名格式:`docs/brief-description`
+ - 完成后提交 PR 到 `main` 分支
+
+4. **紧急修复分支:**
+
+ - 从 `main` 分支创建
+ - 命名格式:`hotfix/issue-number-brief-description`
+ - 完成后需要同时合并到 `main` 和相关的 `release` 分支
+
+5. **发布分支:**
+ - 从 `main` 分支创建
+ - 命名格式:`release/version-number`
+ - 用于版本发布前的最终准备工作
+ - 只允许合并 bug 修复和文档更新
+ - 完成测试和准备工作后,将代码合并回 `main` 分支并打上版本标签
+
+## 工作流程
+
+
+
+## 拉取请求(PR)指南
+
+- 除非是修复生产环境的关键问题,否则所有 PR 都应该提交到 `main` 分支
+- 提交 PR 前确保你的分支已经同步了最新的 `main` 分支内容
+- 在 PR 描述中包含相关的 issue 编号
+- 确保所有测试通过,且代码符合我们的质量标准
+- 如果你添加了新功能或修改了 UI 组件,请附上更改前后的截图
+
+## 版本标签管理
+
+- 主要版本发布:v1.0.0、v2.0.0 等
+- 功能更新发布:v1.1.0、v1.2.0 等
+- 补丁修复发布:v1.0.1、v1.0.2 等
+- 紧急修复发布:v1.0.1-hotfix 等
diff --git a/docs/branching-strategy.md b/docs/branching-strategy.md
deleted file mode 100644
index 897763af16..0000000000
--- a/docs/branching-strategy.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# 🌿 Branching Strategy
-
-Cherry Studio follows a structured branching strategy to maintain code quality and streamline the development process:
-
-## Main Branches
-
-- `main`: Production-ready branch containing stable releases
-
- - All code here is thoroughly tested and ready for production
- - Direct commits are not allowed - changes must come through pull requests
- - Each merge to main represents a new release
-
-- `develop` (default): Primary development branch
- - Contains the latest delivered development changes for the next release
- - Relatively stable but may contain features in progress
- - This is the default branch for development
-
-## Contributing Branches
-
-When contributing to Cherry Studio, please follow these guidelines:
-
-1. **For bug fixes:**
-
- - Create a branch from `develop`
- - Name format: `fix/issue-number-brief-description`
- - Submit pull request back to `develop`
-
-2. **For new features:**
-
- - Create a branch from `develop`
- - Name format: `feature/issue-number-brief-description`
- - Submit pull request back to `develop`
-
-3. **For documentation:**
-
- - Create a branch from `develop`
- - Name format: `docs/brief-description`
- - Submit pull request back to `develop`
-
-4. **For critical hotfixes:**
- - Create a branch from `main`
- - Name format: `hotfix/issue-number-brief-description`
- - Submit pull request to both `main` and `develop`
-
-## Pull Request Guidelines
-
-- Always create pull requests against the `develop` branch unless fixing a critical production issue
-- Ensure your branch is up to date with the latest `develop` changes before submitting
-- Include relevant issue numbers in your PR description
-- Make sure all tests pass and code meets our quality standards
-- Critical hotfixes may be submitted against `main` but must also be merged into `develop`
-- Add a photo to show what is different if you add a new feature or modify a component in the UI.
From a95ace3dc570e766c3d171bc6f72d44c583fba11 Mon Sep 17 00:00:00 2001
From: suyao
Date: Mon, 19 May 2025 22:45:59 +0800
Subject: [PATCH 02/68] fix: update file path resolution in new Electron
---
src/renderer/src/pages/knowledge/KnowledgeContent.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx
index 3afccbd43d..6d4671ab82 100644
--- a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx
+++ b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx
@@ -93,7 +93,7 @@ const KnowledgeContent: FC = ({ selectedBase }) => {
.map((file) => ({
id: file.name,
name: file.name,
- path: file.path,
+ path: window.api.file.getPathForFile(file),
size: file.size,
ext: `.${file.name.split('.').pop()}`.toLowerCase(),
count: 1,
From 47faa6edf2ffd67d5568f25b34c902ee1937529f Mon Sep 17 00:00:00 2001
From: kangfenmao
Date: Tue, 20 May 2025 00:20:02 +0800
Subject: [PATCH 03/68] fix: update default values and improve component
structure
- Changed default value for `getTrayOnClose` to true in `ConfigManager`.
- Removed fullscreen toggle logic from `WindowService`.
- Adjusted styles in `OpenAIAlert` for better spacing.
- Reorganized imports in `Navbar` and updated component paths in `AssistantsTab` and `SettingsTab`.
- Added new components `AssistantItem` and `OpenAISettingsGroup` for better modularity.
- Enhanced `SettingGroup` styles for improved UI consistency.
- Updated `QuickPhraseSettings` to utilize theme context.
- Minor fixes and refactoring across various services and components.
---
src/main/services/ConfigManager.ts | 2 +-
src/main/services/WindowService.ts | 5 -
.../src/components/Alert/OpenAIAlert.tsx | 2 +-
src/renderer/src/components/app/Navbar.tsx | 2 +-
src/renderer/src/i18n/locales/ru-ru.json | 4 +-
src/renderer/src/i18n/locales/zh-cn.json | 4 +-
src/renderer/src/pages/apps/App.tsx | 12 +-
.../src/pages/home/Messages/Messages.tsx | 1 +
.../src/pages/home/Tabs/AssistantsTab.tsx | 2 +-
.../src/pages/home/Tabs/SettingsTab.tsx | 36 +++---
.../Tabs/{ => components}/AssistantItem.tsx | 0
.../OpenAISettingsGroup.tsx} | 106 +++++++++---------
.../settings/QuickPhraseSettings/index.tsx | 6 +-
.../src/pages/settings/SettingGroup.tsx | 5 +-
src/renderer/src/services/BackupService.ts | 1 +
src/renderer/src/services/MessagesService.ts | 2 +-
src/renderer/src/store/migrate.ts | 2 +
17 files changed, 101 insertions(+), 91 deletions(-)
rename src/renderer/src/pages/home/Tabs/{ => components}/AssistantItem.tsx (100%)
rename src/renderer/src/pages/home/Tabs/{OpenAISettingsTab.tsx => components/OpenAISettingsGroup.tsx} (54%)
diff --git a/src/main/services/ConfigManager.ts b/src/main/services/ConfigManager.ts
index bb0cbfc422..6242709385 100644
--- a/src/main/services/ConfigManager.ts
+++ b/src/main/services/ConfigManager.ts
@@ -62,7 +62,7 @@ export class ConfigManager {
}
getTrayOnClose(): boolean {
- return !!this.get(ConfigKeys.TrayOnClose, false)
+ return !!this.get(ConfigKeys.TrayOnClose, true)
}
setTrayOnClose(value: boolean) {
diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts
index ba508d7048..b67cac03d6 100644
--- a/src/main/services/WindowService.ts
+++ b/src/main/services/WindowService.ts
@@ -331,11 +331,6 @@ export class WindowService {
event.preventDefault()
- if (mainWindow.isFullScreen()) {
- mainWindow.setFullScreen(false)
- return
- }
-
mainWindow.hide()
//for mac users, should hide dock icon if close to tray
diff --git a/src/renderer/src/components/Alert/OpenAIAlert.tsx b/src/renderer/src/components/Alert/OpenAIAlert.tsx
index 7f8695fa05..455ab62987 100644
--- a/src/renderer/src/components/Alert/OpenAIAlert.tsx
+++ b/src/renderer/src/components/Alert/OpenAIAlert.tsx
@@ -17,7 +17,7 @@ const OpenAIAlert = () => {
return (
{
diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx
index 85a5265560..e5fe83b98c 100644
--- a/src/renderer/src/components/app/Navbar.tsx
+++ b/src/renderer/src/components/app/Navbar.tsx
@@ -1,6 +1,6 @@
import { isLinux, isMac, isWindows } from '@renderer/config/constant'
-import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
+import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import type { FC, PropsWithChildren } from 'react'
import type { HTMLAttributes } from 'react'
import styled from 'styled-components'
diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json
index cb182f71a9..2f3a2e1baf 100644
--- a/src/renderer/src/i18n/locales/ru-ru.json
+++ b/src/renderer/src/i18n/locales/ru-ru.json
@@ -869,7 +869,8 @@
"number_images_tip": "Количество увеличенных результатов для генерации",
"seed_tip": "Контролирует случайный характер увеличения изображений для воспроизводимых результатов",
"magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов"
- }
+ },
+ "rendering_speed": "Скорость рендеринга"
},
"prompts": {
"explanation": "Объясните мне этот концепт",
@@ -927,7 +928,6 @@
"restore": {
"confirm": "Вы уверены, что хотите восстановить данные?",
"confirm.button": "Выбрать файл резервной копии",
-
"content": "Операция восстановления перезапишет все текущие данные приложения данными из резервной копии. Это может занять некоторое время.",
"progress": {
"completed": "Восстановление завершено",
diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json
index 1493d0d249..ca72417a8d 100644
--- a/src/renderer/src/i18n/locales/zh-cn.json
+++ b/src/renderer/src/i18n/locales/zh-cn.json
@@ -244,7 +244,7 @@
"settings.context_count": "上下文数",
"settings.context_count.tip": "要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10",
"settings.max": "不限",
- "settings.max_tokens": "最大 Token 数",
+ "settings.max_tokens": "最大 TOKEN 数",
"settings.max_tokens.confirm": "最大 Token 数",
"settings.max_tokens.confirm_content": "设置单次交互所用的最大 Token 数, 会影响返回结果的长度。要根据模型上下文限制来设置,否则会报错",
"settings.max_tokens.tip": "单次交互所用的最大 Token 数, 会影响返回结果的长度。要根据模型上下文限制来设置,否则会报错",
@@ -1678,7 +1678,7 @@
"reset": "重置"
},
"openai": {
- "title": "OpenAI设置",
+ "title": "OpenAI 设置",
"summary_text_mode.title": "摘要模式",
"summary_text_mode.tip": "模型执行的推理摘要",
"summary_text_mode.auto": "自动",
diff --git a/src/renderer/src/pages/apps/App.tsx b/src/renderer/src/pages/apps/App.tsx
index 506aaded1e..41c069f12d 100644
--- a/src/renderer/src/pages/apps/App.tsx
+++ b/src/renderer/src/pages/apps/App.tsx
@@ -156,14 +156,16 @@ const App: FC = ({ app, onClick, size = 60, isLast }) => {
: [])
]
- if (!isVisible && !isLast) return null
+ if (!isVisible && !isLast) {
+ return null
+ }
return (
<>
{isLast ? (
-
+
) : (
@@ -252,9 +254,9 @@ const AppTitle = styled.div`
white-space: nowrap;
`
-const AddButton = styled.div`
- width: 60px;
- height: 60px;
+const AddButton = styled.div<{ size?: number }>`
+ width: ${({ size }) => size || 60}px;
+ height: ${({ size }) => size || 60}px;
border-radius: 12px;
display: flex;
align-items: center;
diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx
index 58ee978fe2..84b89f0e43 100644
--- a/src/renderer/src/pages/home/Messages/Messages.tsx
+++ b/src/renderer/src/pages/home/Messages/Messages.tsx
@@ -256,6 +256,7 @@ const Messages: React.FC = ({ assistant, topic, setActiveTopic, o
useEffect(() => {
requestAnimationFrame(() => onComponentUpdate?.())
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const groupedMessages = useMemo(() => Object.entries(getGroupedMessages(displayMessages)), [displayMessages])
diff --git a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx
index b2f291a837..537c7583f8 100644
--- a/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx
+++ b/src/renderer/src/pages/home/Tabs/AssistantsTab.tsx
@@ -8,7 +8,7 @@ import { FC, useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
-import AssistantItem from './AssistantItem'
+import AssistantItem from './components/AssistantItem'
interface AssistantsTabProps {
activeAssistant: Assistant
diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx
index 6754ccdb43..a421a027f3 100644
--- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx
+++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx
@@ -63,7 +63,7 @@ import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
-import OpenAISettingsTab from './OpenAISettingsTab'
+import OpenAISettingsGroup from './components/OpenAISettingsGroup'
interface Props {
assistant: Assistant
@@ -218,8 +218,7 @@ const SettingsTab: FC = (props) => {
/>
}>
-
-
+
{t('chat.settings.temperature')}
@@ -227,7 +226,7 @@ const SettingsTab: FC = (props) => {
-
+
= (props) => {
value={typeof temperature === 'number' ? temperature : 0}
step={0.1}
/>
-
+
{t('chat.settings.context_count')}
@@ -245,7 +244,7 @@ const SettingsTab: FC = (props) => {
-
+
= (props) => {
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
/>
-
+
@@ -296,7 +295,7 @@ const SettingsTab: FC = (props) => {
/>
{enableMaxTokens && (
-
+
= (props) => {
)}
- {isOpenAI && (
-
- )}
+ {isOpenAI && (
+
+ )}
-
{t('settings.messages.prompt')}
dispatch(setShowPrompt(checked))} />
@@ -440,7 +440,6 @@ const SettingsTab: FC = (props) => {
-
{t('message.message.code_style')}
= (props) => {
-
{t('settings.messages.input.show_estimated_tokens')}
`
+const SettingGroup = styled.div<{ theme?: ThemeMode }>`
padding: 0 5px;
width: 100%;
margin-top: 0;
diff --git a/src/renderer/src/pages/home/Tabs/AssistantItem.tsx b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx
similarity index 100%
rename from src/renderer/src/pages/home/Tabs/AssistantItem.tsx
rename to src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx
diff --git a/src/renderer/src/pages/home/Tabs/OpenAISettingsTab.tsx b/src/renderer/src/pages/home/Tabs/components/OpenAISettingsGroup.tsx
similarity index 54%
rename from src/renderer/src/pages/home/Tabs/OpenAISettingsTab.tsx
rename to src/renderer/src/pages/home/Tabs/components/OpenAISettingsGroup.tsx
index 0fdd702d26..2aa25c5ff1 100644
--- a/src/renderer/src/pages/home/Tabs/OpenAISettingsTab.tsx
+++ b/src/renderer/src/pages/home/Tabs/components/OpenAISettingsGroup.tsx
@@ -1,4 +1,5 @@
-import { SettingDivider, SettingRow, SettingSubtitle } from '@renderer/pages/settings'
+import { SettingDivider, SettingRow } from '@renderer/pages/settings'
+import { CollapsibleSettingGroup } from '@renderer/pages/settings/SettingGroup'
import { RootState, useAppDispatch } from '@renderer/store'
import { setOpenAIServiceTier, setOpenAISummaryText } from '@renderer/store/settings'
import { OpenAIServiceTier, OpenAISummaryText } from '@renderer/types'
@@ -9,11 +10,11 @@ import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
-import { SettingGroup, SettingRowTitleSmall } from './SettingsTab'
-
interface Props {
isOpenAIReasoning: boolean
isSupportedFlexServiceTier: boolean
+ SettingGroup: FC<{ children: React.ReactNode }>
+ SettingRowTitleSmall: FC<{ children: React.ReactNode }>
}
const FALL_BACK_SERVICE_TIER: Record = {
@@ -22,7 +23,12 @@ const FALL_BACK_SERVICE_TIER: Record = {
flex: 'default'
}
-const OpenAISettingsTab: FC = (props) => {
+const OpenAISettingsGroup: FC = ({
+ isOpenAIReasoning,
+ isSupportedFlexServiceTier,
+ SettingGroup,
+ SettingRowTitleSmall
+}) => {
const { t } = useTranslation()
const summaryText = useSelector((state: RootState) => state.settings.openAI.summaryText)
const serviceTierMode = useSelector((state: RootState) => state.settings.openAI.serviceTier)
@@ -74,11 +80,11 @@ const OpenAISettingsTab: FC = (props) => {
]
return baseOptions.filter((option) => {
if (option.value === 'flex') {
- return props.isSupportedFlexServiceTier
+ return isSupportedFlexServiceTier
}
return true
})
- }, [props.isSupportedFlexServiceTier, t])
+ }, [isSupportedFlexServiceTier, t])
useEffect(() => {
if (serviceTierMode && !serviceTierOptions.some((option) => option.value === serviceTierMode)) {
@@ -87,49 +93,49 @@ const OpenAISettingsTab: FC = (props) => {
}, [serviceTierMode, serviceTierOptions, setServiceTierMode])
return (
-
- {t('settings.openai.title')}
-
-
-
- {t('settings.openai.service_tier.title')}{' '}
-
-
-
-
- {
- setServiceTierMode(value as OpenAIServiceTier)
- }}
- size="small"
- options={serviceTierOptions}
- />
-
- {props.isOpenAIReasoning && (
- <>
-
-
-
- {t('settings.openai.summary_text_mode.title')}{' '}
-
-
-
-
- {
- setSummaryText(value as OpenAISummaryText)
- }}
- size="small"
- options={summaryTextOptions}
- />
-
- >
- )}
-
+
+
+
+
+ {t('settings.openai.service_tier.title')}{' '}
+
+
+
+
+ {
+ setServiceTierMode(value as OpenAIServiceTier)
+ }}
+ size="small"
+ options={serviceTierOptions}
+ />
+
+ {isOpenAIReasoning && (
+ <>
+
+
+
+ {t('settings.openai.summary_text_mode.title')}{' '}
+
+
+
+
+ {
+ setSummaryText(value as OpenAISummaryText)
+ }}
+ size="small"
+ options={summaryTextOptions}
+ />
+
+ >
+ )}
+
+
)
}
@@ -141,4 +147,4 @@ const StyledSelect = styled(Select)`
}
`
-export default OpenAISettingsTab
+export default OpenAISettingsGroup
diff --git a/src/renderer/src/pages/settings/QuickPhraseSettings/index.tsx b/src/renderer/src/pages/settings/QuickPhraseSettings/index.tsx
index 1321a87db1..ea4a16a532 100644
--- a/src/renderer/src/pages/settings/QuickPhraseSettings/index.tsx
+++ b/src/renderer/src/pages/settings/QuickPhraseSettings/index.tsx
@@ -1,5 +1,6 @@
import { DeleteOutlined, EditOutlined, ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons'
import DragableList from '@renderer/components/DragableList'
+import { useTheme } from '@renderer/context/ThemeProvider'
import FileItem from '@renderer/pages/files/FileItem'
import QuickPhraseService from '@renderer/services/QuickPhraseService'
import { QuickPhrase } from '@renderer/types'
@@ -19,6 +20,7 @@ const QuickPhraseSettings: FC = () => {
const [editingPhrase, setEditingPhrase] = useState(null)
const [formData, setFormData] = useState({ title: '', content: '' })
const [dragging, setDragging] = useState(false)
+ const { theme } = useTheme()
const loadPhrases = async () => {
const data = await QuickPhraseService.getAll()
@@ -68,8 +70,8 @@ const QuickPhraseSettings: FC = () => {
const reversedPhrases = [...phrasesList].reverse()
return (
-
-
+
+
{t('settings.quickPhrase.title')}
} onClick={handleAdd} />
diff --git a/src/renderer/src/pages/settings/SettingGroup.tsx b/src/renderer/src/pages/settings/SettingGroup.tsx
index 7948c20390..bd5efc70cc 100644
--- a/src/renderer/src/pages/settings/SettingGroup.tsx
+++ b/src/renderer/src/pages/settings/SettingGroup.tsx
@@ -46,8 +46,11 @@ const GroupHeader = styled.div`
display: flex;
align-items: center;
cursor: pointer;
- padding: 6px 0;
+ padding: 8px 0;
+ padding-bottom: 12px;
user-select: none;
+ margin-bottom: 10px;
+ border-bottom: 0.5px solid var(--color-border);
`
const GroupTitle = styled.div`
diff --git a/src/renderer/src/services/BackupService.ts b/src/renderer/src/services/BackupService.ts
index 3d3bbf1e38..231ef49fc9 100644
--- a/src/renderer/src/services/BackupService.ts
+++ b/src/renderer/src/services/BackupService.ts
@@ -1,3 +1,4 @@
+/* eslint-disable simple-import-sort/imports */
import Logger from '@renderer/config/logger'
import db from '@renderer/databases'
import { upgradeToV7 } from '@renderer/databases/upgrades'
diff --git a/src/renderer/src/services/MessagesService.ts b/src/renderer/src/services/MessagesService.ts
index a20f481f6c..b27f24836f 100644
--- a/src/renderer/src/services/MessagesService.ts
+++ b/src/renderer/src/services/MessagesService.ts
@@ -217,7 +217,7 @@ export async function getMessageTitle(message: Message, length = 30): Promise
Date: Tue, 20 May 2025 10:41:25 +0800
Subject: [PATCH 04/68] feat: add MCP servers via JSON quickly (#6099)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: add MCP servers via JSON quickly
* refactor(MCPSettings): Extract JSON parsing logic into a helper function
* feat: json linter for EditMcpJsonPopup
* feat(mcp): confirm the MCP server status as connection
* refactor(AddMcpServerModal): 移除冗余注释并修复加载状态
* feat(MCPSettings): Add support for SSE and streamableHttp formats and optimize server configuration parsing
- Add server type validation to ensure the type is stdio, SSE, or streamableHttp
- Optimize JSON parsing logic to ensure server configuration completeness and validity
- Update example text to provide more detailed configuration examples
* fix(MCPSettings): fix AddMcpServerModal default baseUrl login
移除serverToAdd.url作为baseUrl的备选值,确保baseUrl仅使用serverToAdd.baseUrl的值
* feat(MCPSettings): support CodeEditor in AddMcpServerModal
* fix: Remove unnecessary type checks for JSON parsing login
* fix(MCPSettings): fix compatibility issues with the URL field when parsing server data
* refactor: remove unnessary cdoe
* chore: Add a server dropdown button to integrate new features in UI
- Integrate the two buttons for adding a server into a single dropdown menu to enhance user experience and simplify the interface
* chroe: modify the Dropdown items' name of addServer
* refactor(i18n): unify the translation for the MCP server import function
---------
Co-authored-by: one
---
packages/shared/IpcChannel.ts | 1 +
src/main/ipc.ts | 3 +
src/main/services/MCPService.ts | 20 ++
src/preload/index.ts | 3 +-
.../src/components/CodeEditor/index.tsx | 17 +-
src/renderer/src/i18n/locales/en-us.json | 8 +
src/renderer/src/i18n/locales/ja-jp.json | 8 +
src/renderer/src/i18n/locales/ru-ru.json | 8 +
src/renderer/src/i18n/locales/zh-cn.json | 8 +
src/renderer/src/i18n/locales/zh-tw.json | 8 +
.../MCPSettings/AddMcpServerModal.tsx | 282 ++++++++++++++++++
.../settings/MCPSettings/EditMcpJsonPopup.tsx | 21 +-
.../settings/MCPSettings/McpServersList.tsx | 50 +++-
13 files changed, 423 insertions(+), 14 deletions(-)
create mode 100644 src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx
diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts
index 7c536015a9..ce50f4680e 100644
--- a/packages/shared/IpcChannel.ts
+++ b/packages/shared/IpcChannel.ts
@@ -52,6 +52,7 @@ export enum IpcChannel {
Mcp_GetInstallInfo = 'mcp:get-install-info',
Mcp_ServersChanged = 'mcp:servers-changed',
Mcp_ServersUpdated = 'mcp:servers-updated',
+ Mcp_CheckConnectivity = 'mcp:check-connectivity',
//copilot
Copilot_GetAuthMessage = 'copilot:get-auth-message',
diff --git a/src/main/ipc.ts b/src/main/ipc.ts
index e40b19aca1..608d57534b 100644
--- a/src/main/ipc.ts
+++ b/src/main/ipc.ts
@@ -320,6 +320,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.Mcp_ListResources, (event, server) => getMcpInstance().listResources(event, server))
ipcMain.handle(IpcChannel.Mcp_GetResource, (event, params) => getMcpInstance().getResource(event, params))
ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, () => getMcpInstance().getInstallInfo())
+ ipcMain.handle(IpcChannel.Mcp_CheckConnectivity, (event, params) =>
+ getMcpInstance().checkMcpConnectivity(event, params)
+ )
ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name))
diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts
index 90e50ec65a..83cd082697 100644
--- a/src/main/services/MCPService.ts
+++ b/src/main/services/MCPService.ts
@@ -396,6 +396,26 @@ class McpService {
}
}
+ /**
+ * Check connectivity for an MCP server
+ */
+ public async checkMcpConnectivity(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise {
+ Logger.info(`[MCP] Checking connectivity for server: ${server.name}`)
+ try {
+ const client = await this.initClient(server)
+ // Attempt to list tools as a way to check connectivity
+ await client.listTools()
+ Logger.info(`[MCP] Connectivity check successful for server: ${server.name}`)
+ return true
+ } catch (error) {
+ Logger.error(`[MCP] Connectivity check failed for server: ${server.name}`, error)
+ // Close the client if connectivity check fails to ensure a clean state for the next attempt
+ const serverKey = this.getServerKey(server)
+ await this.closeClient(serverKey)
+ return false
+ }
+ }
+
private async listToolsImpl(server: MCPServer): Promise {
Logger.info(`[MCP] Listing tools for server: ${server.name}`)
const client = await this.initClient(server)
diff --git a/src/preload/index.ts b/src/preload/index.ts
index f70ca04d20..417055a29d 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -149,7 +149,8 @@ const api = {
listResources: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListResources, server),
getResource: ({ server, uri }: { server: MCPServer; uri: string }) =>
ipcRenderer.invoke(IpcChannel.Mcp_GetResource, { server, uri }),
- getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo)
+ getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo),
+ checkMcpConnectivity: (server: any) => ipcRenderer.invoke(IpcChannel.Mcp_CheckConnectivity, server)
},
shell: {
openExternal: (url: string, options?: Electron.OpenExternalOptions) => shell.openExternal(url, options)
diff --git a/src/renderer/src/components/CodeEditor/index.tsx b/src/renderer/src/components/CodeEditor/index.tsx
index 124912e277..6d1f059006 100644
--- a/src/renderer/src/components/CodeEditor/index.tsx
+++ b/src/renderer/src/components/CodeEditor/index.tsx
@@ -29,6 +29,8 @@ interface Props {
wrappable?: boolean
keymap?: boolean
} & BasicSetupOptions
+ /** 用于追加 extensions */
+ extensions?: Extension[]
/** 用于覆写编辑器的样式,会直接传给 CodeMirror 的 style 属性 */
style?: React.CSSProperties
}
@@ -38,7 +40,7 @@ interface Props {
*
* 目前必须和 CodeToolbar 配合使用。
*/
-const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options, style }: Props) => {
+const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options, extensions, style }: Props) => {
const {
fontSize,
codeShowLineNumbers: _lineNumbers,
@@ -177,9 +179,14 @@ const CodeEditor = ({ children, language, onSave, onChange, maxHeight, options,
])
}, [handleSave])
- const enabledExtensions = useMemo(() => {
- return [...langExtension, ...(isUnwrapped ? [] : [EditorView.lineWrapping]), ...(enableKeymap ? [saveKeymap] : [])]
- }, [enableKeymap, langExtension, isUnwrapped, saveKeymap])
+ const customExtensions = useMemo(() => {
+ return [
+ ...(extensions ?? []),
+ ...langExtension,
+ ...(isUnwrapped ? [] : [EditorView.lineWrapping]),
+ ...(enableKeymap ? [saveKeymap] : [])
+ ]
+ }, [extensions, langExtension, isUnwrapped, enableKeymap, saveKeymap])
return (
{
editorViewRef.current = view
setEditorReady(true)
diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json
index 0536ced621..d5adc09e64 100644
--- a/src/renderer/src/i18n/locales/en-us.json
+++ b/src/renderer/src/i18n/locales/en-us.json
@@ -1270,6 +1270,14 @@
"active": "Active",
"addError": "Failed to add server",
"addServer": "Add Server",
+ "addServer.create": "Quick Create",
+ "addServer.importFrom": "Import from JSON",
+ "addServer.importFrom.tooltip": "Please copy the configuration JSON (prioritizing\n NPX or UVX configurations) from the MCP Servers introduction page and paste it into the input box.",
+ "addServer.importFrom.placeholder": "Paste MCP server JSON config",
+ "addServer.importFrom.invalid": "Invalid input, please check JSON format",
+ "addServer.importFrom.nameExists": "Server already exists: {{name}}",
+ "addServer.importFrom.oneServer": "Only one MCP server configuration at a time",
+ "addServer.importFrom.connectionFailed": "Connection failed",
"addSuccess": "Server added successfully",
"args": "Arguments",
"argsTooltip": "Each argument on a new line",
diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json
index 3e16858f80..6bcd0ab9a1 100644
--- a/src/renderer/src/i18n/locales/ja-jp.json
+++ b/src/renderer/src/i18n/locales/ja-jp.json
@@ -1266,6 +1266,14 @@
"active": "有効",
"addError": "サーバーの追加に失敗しました",
"addServer": "サーバーを追加",
+ "addServer.create": "クイック作成",
+ "addServer.importFrom": "JSONからインポート",
+ "addServer.importFrom.tooltip": "MCPサーバー紹介ページから設定JSON(NPXまたはUVX設定を優先)をコピーし、入力ボックスに貼り付けてください。",
+ "addServer.importFrom.placeholder": "MCPサーバーJSON設定を貼り付け",
+ "addServer.importFrom.invalid": "無効な入力です。JSON形式を確認してください。",
+ "addServer.importFrom.nameExists": "サーバーはすでに存在します: {{name}}",
+ "addServer.importFrom.oneServer": "一度に1つのMCPサーバー設定のみを保存できます",
+ "addServer.importFrom.connectionFailed": "接続に失敗しました",
"addSuccess": "サーバーが正常に追加されました",
"args": "引数",
"argsTooltip": "1行に1つの引数を入力してください",
diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json
index 2f3a2e1baf..89c493361a 100644
--- a/src/renderer/src/i18n/locales/ru-ru.json
+++ b/src/renderer/src/i18n/locales/ru-ru.json
@@ -1266,6 +1266,14 @@
"active": "Активен",
"addError": "Ошибка добавления сервера",
"addServer": "Добавить сервер",
+ "addServer.create": "Быстрое создание",
+ "addServer.importFrom": "Импорт из JSON",
+ "addServer.importFrom.tooltip": "Скопируйте JSON-конфигурацию (приоритет NPX или UVX конфигураций) со страницы введения MCP Servers и вставьте ее в поле ввода.",
+ "addServer.importFrom.placeholder": "Вставьте JSON-конфигурацию сервера MCP",
+ "addServer.importFrom.invalid": "Неверный ввод, проверьте формат JSON",
+ "addServer.importFrom.nameExists": "Сервер уже существует: {{name}}",
+ "addServer.importFrom.oneServer": "Можно сохранить только один конфигурационный файл MCP",
+ "addServer.importFrom.connectionFailed": "Сбой подключения",
"addSuccess": "Сервер успешно добавлен",
"args": "Аргументы",
"argsTooltip": "Каждый аргумент с новой строки",
diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json
index ca72417a8d..37bfb50575 100644
--- a/src/renderer/src/i18n/locales/zh-cn.json
+++ b/src/renderer/src/i18n/locales/zh-cn.json
@@ -1270,6 +1270,14 @@
"active": "启用",
"addError": "添加服务器失败",
"addServer": "添加服务器",
+ "addServer.create": "快速创建",
+ "addServer.importFrom": "从 JSON 导入",
+ "addServer.importFrom.tooltip": "请从 MCP Servers 的介绍页面复制配置JSON(优先使用\n NPX或 UVX 配置),并粘贴到输入框中。",
+ "addServer.importFrom.placeholder": "粘贴 MCP 服务器 JSON 配置",
+ "addServer.importFrom.invalid": "无效输入,请检查 JSON 格式",
+ "addServer.importFrom.nameExists": "服务器已存在:{{name}}",
+ "addServer.importFrom.oneServer": "每次只能保存一個 MCP 伺服器配置",
+ "addServer.importFrom.connectionFailed": "連接失敗",
"addSuccess": "服务器添加成功",
"args": "参数",
"argsTooltip": "每个参数占一行",
diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json
index f4e1adbb65..3cc6777a88 100644
--- a/src/renderer/src/i18n/locales/zh-tw.json
+++ b/src/renderer/src/i18n/locales/zh-tw.json
@@ -1269,6 +1269,14 @@
"active": "啟用",
"addError": "添加伺服器失敗",
"addServer": "新增伺服器",
+ "addServer.create": "快速創建",
+ "addServer.importFrom": "從 JSON 導入",
+ "addServer.importFrom.tooltip": "請從 MCP Servers 的介紹頁面複製配置JSON(優先使用\n NPX或 UVX 配置),並粘貼到輸入框中。",
+ "addServer.importFrom.placeholder": "貼上 MCP 伺服器 JSON 設定",
+ "addServer.importFrom.invalid": "無效的輸入,請檢查 JSON 格式",
+ "addServer.importFrom.nameExists": "伺服器已存在:{{name}}",
+ "addServer.importFrom.oneServer": "每次只能保存一個 MCP 伺服器配置",
+ "addServer.importFrom.connectionFailed": "連線失敗",
"addSuccess": "伺服器新增成功",
"args": "參數",
"argsTooltip": "每個參數佔一行",
diff --git a/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx b/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx
new file mode 100644
index 0000000000..bc75508f14
--- /dev/null
+++ b/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx
@@ -0,0 +1,282 @@
+import { nanoid } from '@reduxjs/toolkit'
+import CodeEditor from '@renderer/components/CodeEditor'
+import { CodeToolbarProvider } from '@renderer/components/CodeToolbar'
+import { useAppDispatch } from '@renderer/store'
+import { setMCPServerActive } from '@renderer/store/mcp'
+import { MCPServer } from '@renderer/types'
+import { Extension } from '@uiw/react-codemirror'
+import { Form, Modal } from 'antd'
+import { FC, useCallback, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+
+interface AddMcpServerModalProps {
+ visible: boolean
+ onClose: () => void
+ onSuccess: (server: MCPServer) => void
+ existingServers: MCPServer[]
+}
+
+interface ParsedServerData extends MCPServer {
+ url?: string // JSON 可能包含此欄位,而不是 baseUrl
+}
+
+// 預設的 JSON 範例內容
+const initialJsonExample = `// 示例 JSON (stdio):
+// {
+// "mcpServers": {
+// "stdio-server-example": {
+// "command": "npx",
+// "args": ["-y", "mcp-server-example"]
+// }
+// }
+// }
+
+// 示例 JSON (sse):
+// {
+// "mcpServers": {
+// "sse-server-example": {
+// "type": "sse",
+// "url": "http://localhost:3000"
+// }
+// }
+// }
+
+// 示例 JSON (streamableHttp):
+// {
+// "mcpServers": {
+// "streamable-http-example": {
+// "type": "streamableHttp",
+// "url": "http://localhost:3001"
+// }
+// }
+// }
+`
+
+const AddMcpServerModal: FC = ({ visible, onClose, onSuccess, existingServers }) => {
+ const { t } = useTranslation()
+ const [form] = Form.useForm()
+ const [loading, setLoading] = useState(false)
+ const dispatch = useAppDispatch()
+ const [editorExtensions, setEditorExtensions] = useState([]) // 新增 editorExtensions 狀態
+
+ // 載入 CodeMirror JSON Linter 擴充功能
+ useEffect(() => {
+ let isMounted = true
+ Promise.all([
+ import('@codemirror/lang-json').then((mod) => mod.jsonParseLinter),
+ import('@codemirror/lint').then((mod) => mod.linter)
+ ]).then(([jsonParseLinter, linter]) => {
+ if (isMounted) {
+ setEditorExtensions([linter(jsonParseLinter())])
+ }
+ })
+ return () => {
+ isMounted = false
+ }
+ }, [])
+
+ const handleOk = async () => {
+ try {
+ const values = await form.validateFields()
+ const inputValue = values.serverConfig.trim()
+ setLoading(true)
+
+ const { serverToAdd, error } = parseAndExtractServer(inputValue, t)
+
+ if (error) {
+ form.setFields([
+ {
+ name: 'serverConfig',
+ errors: [error]
+ }
+ ])
+ setLoading(false)
+ return
+ }
+
+ // 檢查重複名稱
+ if (existingServers && existingServers.some((server) => server.name === serverToAdd!.name)) {
+ form.setFields([
+ {
+ name: 'serverConfig',
+ errors: [t('settings.mcp.addServer.importFrom.nameExists', { name: serverToAdd!.name })]
+ }
+ ])
+ setLoading(false)
+ return
+ }
+
+ // 如果成功解析並通過所有檢查,立即加入伺服器(非啟用狀態)並關閉對話框
+ const newServer: MCPServer = {
+ id: nanoid(),
+ name: serverToAdd!.name!,
+ description: serverToAdd!.description ?? '',
+ baseUrl: serverToAdd!.baseUrl ?? serverToAdd!.url ?? '',
+ command: serverToAdd!.command ?? '',
+ args: serverToAdd!.args || [],
+ env: serverToAdd!.env || {},
+ isActive: false,
+ type: serverToAdd!.type,
+ logoUrl: serverToAdd!.logoUrl,
+ provider: serverToAdd!.provider,
+ providerUrl: serverToAdd!.providerUrl,
+ tags: serverToAdd!.tags,
+ configSample: serverToAdd!.configSample
+ }
+
+ onSuccess(newServer)
+ form.resetFields()
+ onClose()
+
+ // 在背景非同步檢查伺服器可用性並更新狀態
+ window.api.mcp
+ .checkMcpConnectivity(newServer)
+ .then((isConnected) => {
+ console.log(`Connectivity check for ${newServer.name}: ${isConnected}`)
+ dispatch(setMCPServerActive({ id: newServer.id, isActive: isConnected }))
+ })
+ .catch((connError: any) => {
+ console.error(`Connectivity check failed for ${newServer.name}:`, connError)
+ window.message.error({
+ content: t(`${newServer.name} settings.mcp.addServer.importFrom.connectionFailed`),
+ key: 'mcp-quick-add-failed'
+ })
+ })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // CodeEditor 內容變更時的回呼函式
+ const handleEditorChange = useCallback(
+ (newContent: string) => {
+ form.setFieldsValue({ serverConfig: newContent })
+ // 可選:如果希望即時驗證,可以取消註解下一行
+ // form.validateFields(['serverConfig']);
+ },
+ [form]
+ )
+
+ const serverConfigValue = form.getFieldValue('serverConfig')
+
+ return (
+
+
+
+
+ {serverConfigValue ?? initialJsonExample}
+
+
+
+
+
+ )
+}
+
+// 解析 JSON 提取伺服器資料
+const parseAndExtractServer = (
+ inputValue: string,
+ t: (key: string, options?: any) => string
+): { serverToAdd: Partial | null; error: string | null } => {
+ const trimmedInput = inputValue.trim()
+
+ let parsedJson
+ try {
+ parsedJson = JSON.parse(trimmedInput)
+ } catch (e) {
+ // JSON 解析失敗,返回錯誤
+ return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') }
+ }
+
+ let serverToAdd: Partial | null = null
+
+ // 檢查是否包含多個伺服器配置 (適用於 JSON 格式)
+ if (
+ parsedJson.mcpServers &&
+ typeof parsedJson.mcpServers === 'object' &&
+ Object.keys(parsedJson.mcpServers).length > 1
+ ) {
+ return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.multipleServers') }
+ } else if (Array.isArray(parsedJson) && parsedJson.length > 1) {
+ return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.multipleServers') }
+ }
+
+ if (
+ parsedJson.mcpServers &&
+ typeof parsedJson.mcpServers === 'object' &&
+ Object.keys(parsedJson.mcpServers).length > 0
+ ) {
+ // Case 1: {"mcpServers": {"serverName": {...}}}
+ const firstServerKey = Object.keys(parsedJson.mcpServers)[0]
+ const potentialServer = parsedJson.mcpServers[firstServerKey]
+ if (typeof potentialServer === 'object' && potentialServer !== null) {
+ serverToAdd = { ...potentialServer }
+ serverToAdd!.name = potentialServer.name ?? firstServerKey
+ } else {
+ console.error('Invalid server data under mcpServers key:', potentialServer)
+ return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') }
+ }
+ } else if (Array.isArray(parsedJson) && parsedJson.length > 0) {
+ // Case 2: [{...}, ...] - 取第一個伺服器,確保它是物件
+ if (typeof parsedJson[0] === 'object' && parsedJson[0] !== null) {
+ serverToAdd = { ...parsedJson[0] }
+ serverToAdd!.name = parsedJson[0].name ?? t('settings.mcp.newServer')
+ } else {
+ console.error('Invalid server data in array:', parsedJson[0])
+ return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') }
+ }
+ } else if (
+ typeof parsedJson === 'object' &&
+ parsedJson !== null &&
+ !Array.isArray(parsedJson) &&
+ !parsedJson.mcpServers // 確保是直接的伺服器物件
+ ) {
+ // Case 3: {...} (單一伺服器物件)
+ // 檢查物件是否為空
+ if (Object.keys(parsedJson).length > 0) {
+ serverToAdd = { ...parsedJson }
+ serverToAdd!.name = parsedJson.name ?? t('settings.mcp.newServer')
+ } else {
+ // 空物件,視為無效
+ serverToAdd = null
+ }
+ } else {
+ // 無效結構或空的 mcpServers
+ serverToAdd = null
+ }
+
+ // 確保 serverToAdd 存在且 name 存在
+ if (!serverToAdd || !serverToAdd.name) {
+ console.error('Invalid JSON structure for server config or missing name:', parsedJson)
+ return { serverToAdd: null, error: t('settings.mcp.addServer.importFrom.invalid') }
+ }
+
+ return { serverToAdd, error: null }
+}
+
+export default AddMcpServerModal
diff --git a/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx b/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx
index c8726bfa15..32228dc96e 100644
--- a/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx
+++ b/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx
@@ -4,16 +4,17 @@ import { TopView } from '@renderer/components/TopView'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setMCPServers } from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types'
+import { Extension } from '@uiw/react-codemirror'
import { Modal, Typography } from 'antd'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
-
interface Props {
resolve: (data: any) => void
}
const PopupContainer: React.FC = ({ resolve }) => {
const [open, setOpen] = useState(true)
+ const [editorExtensions, setEditorExtensions] = useState([])
const [jsonConfig, setJsonConfig] = useState('')
const [jsonSaving, setJsonSaving] = useState(false)
const [jsonError, setJsonError] = useState('')
@@ -22,6 +23,21 @@ const PopupContainer: React.FC = ({ resolve }) => {
const dispatch = useAppDispatch()
const { t } = useTranslation()
+ useEffect(() => {
+ let isMounted = true
+ Promise.all([
+ import('@codemirror/lang-json').then((mod) => mod.jsonParseLinter),
+ import('@codemirror/lint').then((mod) => mod.linter)
+ ]).then(([jsonParseLinter, linter]) => {
+ if (isMounted) {
+ setEditorExtensions([linter(jsonParseLinter())])
+ }
+ })
+ return () => {
+ isMounted = false
+ }
+ }, [])
+
useEffect(() => {
try {
const mcpServersObj: Record = {}
@@ -137,7 +153,8 @@ const PopupContainer: React.FC = ({ resolve }) => {
foldGutter: true,
highlightActiveLine: true,
keymap: true
- }}>
+ }}
+ extensions={editorExtensions}>
{jsonConfig}
diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx
index 3f6b6eb8ae..ed2102530c 100644
--- a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx
+++ b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx
@@ -4,14 +4,15 @@ import DragableList from '@renderer/components/DragableList'
import Scrollbar from '@renderer/components/Scrollbar'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { MCPServer } from '@renderer/types'
-import { Button, Empty, Tag } from 'antd'
+import { Button, Dropdown, Empty, Tag } from 'antd'
import { MonitorCheck, Plus, RefreshCw, Settings2, SquareArrowOutUpRight } from 'lucide-react'
-import { FC, useCallback } from 'react'
+import { FC, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingTitle } from '..'
+import AddMcpServerModal from './AddMcpServerModal'
import EditMcpJsonPopup from './EditMcpJsonPopup'
import SyncServersPopup from './SyncServersPopup'
@@ -19,6 +20,7 @@ const McpServersList: FC = () => {
const { mcpServers, addMCPServer, updateMcpServers } = useMCPServers()
const { t } = useTranslation()
const navigate = useNavigate()
+ const [isAddModalVisible, setIsAddModalVisible] = useState(false)
const onAddMcpServer = useCallback(async () => {
const newServer = {
@@ -31,7 +33,7 @@ const McpServersList: FC = () => {
env: {},
isActive: false
}
- await addMCPServer(newServer)
+ addMCPServer(newServer)
navigate(`/settings/mcp/settings`, { state: { server: newServer } })
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' })
}, [addMCPServer, navigate, t])
@@ -40,6 +42,17 @@ const McpServersList: FC = () => {
SyncServersPopup.show(mcpServers)
}, [mcpServers])
+ const handleAddServerSuccess = useCallback(
+ async (server: MCPServer) => {
+ addMCPServer(server)
+ setIsAddModalVisible(false)
+ window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-quick-add' })
+ // Optionally navigate to the new server's settings page
+ // navigate(`/settings/mcp/settings`, { state: { server } })
+ },
+ [addMCPServer, t]
+ )
+
return (
@@ -48,9 +61,28 @@ const McpServersList: FC = () => {
} type="text" onClick={() => EditMcpJsonPopup.show()} shape="circle" />
- } type="default" onClick={onAddMcpServer} shape="round">
- {t('settings.mcp.addServer')}
-
+ {
+ onAddMcpServer()
+ }
+ },
+ {
+ key: 'quick',
+ label: t('settings.mcp.addServer.importFrom'),
+ onClick: () => setIsAddModalVisible(true)
+ }
+ ]
+ }}
+ trigger={['click']}>
+ } type="default" shape="round">
+ {t('settings.mcp.addServer')}
+
+
} type="default" onClick={onSyncServers} shape="round">
{t('settings.mcp.sync.title', 'Sync Servers')}
@@ -111,6 +143,12 @@ const McpServersList: FC = () => {
style={{ marginTop: 20 }}
/>
)}
+ setIsAddModalVisible(false)}
+ onSuccess={handleAddServerSuccess}
+ existingServers={mcpServers} // 傳遞現有的伺服器列表
+ />
)
}
From ab5ffe4e2e2cfbf2633b4a3da121eaf834a68bce Mon Sep 17 00:00:00 2001
From: kangfenmao
Date: Tue, 20 May 2025 11:31:35 +0800
Subject: [PATCH 05/68] fix: set default initial values for rendering speed in
aihubmixConfig
- Added initialValue 'DEFAULT' for renderingSpeed in multiple mode configurations to ensure consistent default behavior across the application.
---
src/renderer/src/pages/paintings/config/aihubmixConfig.tsx | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/renderer/src/pages/paintings/config/aihubmixConfig.tsx b/src/renderer/src/pages/paintings/config/aihubmixConfig.tsx
index 6e7aedb60a..b586186be2 100644
--- a/src/renderer/src/pages/paintings/config/aihubmixConfig.tsx
+++ b/src/renderer/src/pages/paintings/config/aihubmixConfig.tsx
@@ -60,6 +60,7 @@ export const createModeConfigs = (): Record => {
type: 'select',
key: 'renderingSpeed',
options: RENDERING_SPEED_OPTIONS,
+ initialValue: 'DEFAULT',
disabled: (_config, painting) => {
const model = painting?.model
return !model || !model.includes('V_3')
@@ -153,6 +154,7 @@ export const createModeConfigs = (): Record => {
type: 'select',
key: 'renderingSpeed',
options: RENDERING_SPEED_OPTIONS,
+ initialValue: 'DEFAULE',
disabled: (_config, painting) => {
const model = painting?.model
return !model || !model.includes('V_3')
@@ -227,6 +229,7 @@ export const createModeConfigs = (): Record => {
type: 'select',
key: 'renderingSpeed',
options: RENDERING_SPEED_OPTIONS,
+ initialValue: 'DEFAULT',
disabled: (_config, painting) => {
const model = painting?.model
return !model || !model.includes('V_3')
From 52559534c60821e3dc9b8cb3b729c18f0916ecce Mon Sep 17 00:00:00 2001
From: kangfenmao
Date: Tue, 20 May 2025 11:33:52 +0800
Subject: [PATCH 06/68] chore(version): 1.3.7
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index fb900e0650..fdcf75deda 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
- "version": "1.3.6",
+ "version": "1.3.7",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
From 98426e084af9a8360436f1468be7ab47a4bf6b81 Mon Sep 17 00:00:00 2001
From: beyondkmp
Date: Tue, 20 May 2025 13:09:37 +0800
Subject: [PATCH 07/68] refactor: centralize paste handling logic with
PasteService (#6199)
- Integrated PasteService for handling paste events in Inputbar and MessageEditor components.
- Removed redundant paste handling code and improved maintainability.
- Registered paste handlers and set last focused component for better user experience.
- Ensured consistent behavior for text and file pasting across components.
Co-authored-by: beyondkmp
---
.../src/pages/home/Inputbar/Inputbar.tsx | 96 +++-----
.../src/pages/home/Messages/MessageEditor.tsx | 101 ++++----
src/renderer/src/services/PasteService.ts | 215 ++++++++++++++++++
3 files changed, 284 insertions(+), 128 deletions(-)
create mode 100644 src/renderer/src/services/PasteService.ts
diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx
index f68f5fbbbf..22d972806d 100644
--- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx
+++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx
@@ -23,6 +23,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import FileManager from '@renderer/services/FileManager'
import { checkRateLimit, getUserMessage } from '@renderer/services/MessagesService'
import { getModelUniqId } from '@renderer/services/ModelService'
+import PasteService from '@renderer/services/PasteService'
import { estimateTextTokens as estimateTxtTokens, estimateUserPromptUsage } from '@renderer/services/TokenService'
import { translateText } from '@renderer/services/TranslateService'
import WebSearchService from '@renderer/services/WebSearchService'
@@ -581,72 +582,19 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) =
const onPaste = useCallback(
async (event: ClipboardEvent) => {
- // 优先处理文本粘贴
- const clipboardText = event.clipboardData?.getData('text')
- if (clipboardText) {
- // 1. 文本粘贴
- if (pasteLongTextAsFile && clipboardText.length > pasteLongTextThreshold) {
- // 长文本直接转文件,阻止默认粘贴
- event.preventDefault()
-
- const tempFilePath = await window.api.file.create('pasted_text.txt')
- await window.api.file.write(tempFilePath, clipboardText)
- const selectedFile = await window.api.file.get(tempFilePath)
- selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
- setText(text) // 保持输入框内容不变
- setTimeout(() => resizeTextArea(), 50)
- return
- }
- // 短文本走默认粘贴行为,直接返回
- return
- }
-
- // 2. 文件/图片粘贴(仅在无文本时处理)
- if (event.clipboardData?.files && event.clipboardData.files.length > 0) {
- event.preventDefault()
- for (const file of event.clipboardData.files) {
- try {
- // 使用新的API获取文件路径
- const filePath = window.api.file.getPathForFile(file)
-
- // 如果没有路径,可能是剪贴板中的图像数据
- if (!filePath) {
- // 图像生成也支持图像编辑
- if (file.type.startsWith('image/') && (isVisionModel(model) || isGenerateImageModel(model))) {
- const tempFilePath = await window.api.file.create(file.name)
- const arrayBuffer = await file.arrayBuffer()
- const uint8Array = new Uint8Array(arrayBuffer)
- await window.api.file.write(tempFilePath, uint8Array)
- const selectedFile = await window.api.file.get(tempFilePath)
- selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
- break
- } else {
- window.message.info({
- key: 'file_not_supported',
- content: t('chat.input.file_not_supported')
- })
- }
- continue
- }
-
- // 有路径的情况
- if (supportExts.includes(getFileExtension(filePath))) {
- const selectedFile = await window.api.file.get(filePath)
- selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
- } else {
- window.message.info({
- key: 'file_not_supported',
- content: t('chat.input.file_not_supported')
- })
- }
- } catch (error) {
- Logger.error('[src/renderer/src/pages/home/Inputbar/Inputbar.tsx] onPaste:', error)
- window.message.error(t('chat.input.file_error'))
- }
- }
- return
- }
- // 其他情况默认粘贴
+ return await PasteService.handlePaste(
+ event,
+ isVisionModel(model),
+ isGenerateImageModel(model),
+ supportExts,
+ setFiles,
+ setText,
+ pasteLongTextAsFile,
+ pasteLongTextThreshold,
+ text,
+ resizeTextArea,
+ t
+ )
},
[model, pasteLongTextAsFile, pasteLongTextThreshold, resizeTextArea, supportExts, t, text]
)
@@ -749,6 +697,20 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) =
}
}, [isDragging, handleDrag, handleDragEnd])
+ // 注册粘贴处理函数并初始化全局监听
+ useEffect(() => {
+ // 确保全局paste监听器仅初始化一次
+ PasteService.init()
+
+ // 注册当前组件的粘贴处理函数
+ PasteService.registerHandler('inputbar', onPaste)
+
+ // 卸载时取消注册
+ return () => {
+ PasteService.unregisterHandler('inputbar')
+ }
+ }, [onPaste])
+
useShortcut('new_topic', () => {
addNewTopic()
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
@@ -951,6 +913,8 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) =
styles={{ textarea: TextareaStyle }}
onFocus={(e: React.FocusEvent) => {
setInputFocus(true)
+ // 记录当前聚焦的组件
+ PasteService.setLastFocusedComponent('inputbar')
if (e.target.value.length === 0) {
e.target.setSelectionRange(0, 0)
}
diff --git a/src/renderer/src/pages/home/Messages/MessageEditor.tsx b/src/renderer/src/pages/home/Messages/MessageEditor.tsx
index 8b4cd32685..6b82ec625d 100644
--- a/src/renderer/src/pages/home/Messages/MessageEditor.tsx
+++ b/src/renderer/src/pages/home/Messages/MessageEditor.tsx
@@ -4,6 +4,7 @@ import { isGenerateImageModel, isVisionModel } from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import FileManager from '@renderer/services/FileManager'
+import PasteService from '@renderer/services/PasteService'
import { FileType, FileTypes } from '@renderer/types'
import { Message, MessageBlock, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
import { classNames, getFileExtension } from '@renderer/utils'
@@ -62,6 +63,39 @@ const MessageBlockEditor: FC = ({ message, onSave, onResend, onCancel })
}
}, [])
+ const onPaste = useCallback(
+ async (event: ClipboardEvent) => {
+ return await PasteService.handlePaste(
+ event,
+ isVisionModel(model),
+ isGenerateImageModel(model),
+ supportExts,
+ setFiles,
+ undefined, // 不需要setText
+ pasteLongTextAsFile,
+ pasteLongTextThreshold,
+ undefined, // 不需要text
+ resizeTextArea,
+ t
+ )
+ },
+ [model, pasteLongTextAsFile, pasteLongTextThreshold, resizeTextArea, supportExts, t]
+ )
+
+ // 添加全局粘贴事件处理
+ useEffect(() => {
+ // 注册当前组件的粘贴处理函数
+ PasteService.registerHandler('messageEditor', onPaste)
+
+ // 在组件加载时将焦点设置为当前组件
+ PasteService.setLastFocusedComponent('messageEditor')
+
+ // 卸载时取消注册
+ return () => {
+ PasteService.unregisterHandler('messageEditor')
+ }
+ }, [onPaste])
+
const handleTextChange = (blockId: string, content: string) => {
setEditedBlocks((prev) => prev.map((block) => (block.id === blockId ? { ...block, content } : block)))
}
@@ -131,67 +165,6 @@ const MessageBlockEditor: FC = ({ message, onSave, onResend, onCancel })
}
}
- const onPaste = useCallback(
- async (event: ClipboardEvent) => {
- // 1. 文本粘贴
- const clipboardText = event.clipboardData?.getData('text')
- if (clipboardText) {
- if (pasteLongTextAsFile && clipboardText.length > pasteLongTextThreshold) {
- // 长文本直接转文件,阻止默认粘贴
- event.preventDefault()
-
- const tempFilePath = await window.api.file.create('pasted_text.txt')
- await window.api.file.write(tempFilePath, clipboardText)
- const selectedFile = await window.api.file.get(tempFilePath)
- selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
- setTimeout(() => resizeTextArea(), 50)
- return
- }
- // 短文本走默认粘贴行为,直接返回
- return
- }
-
- // 2. 文件/图片粘贴
- if (event.clipboardData?.files && event.clipboardData.files.length > 0) {
- event.preventDefault()
- for (const file of event.clipboardData.files) {
- const filePath = window.api.file.getPathForFile(file)
- if (!filePath) {
- // 图像生成也支持图像编辑
- if (file.type.startsWith('image/') && (isVisionModel(model) || isGenerateImageModel(model))) {
- const tempFilePath = await window.api.file.create(file.name)
- const arrayBuffer = await file.arrayBuffer()
- const uint8Array = new Uint8Array(arrayBuffer)
- await window.api.file.write(tempFilePath, uint8Array)
- const selectedFile = await window.api.file.get(tempFilePath)
- selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
- break
- } else {
- window.message.info({
- key: 'file_not_supported',
- content: t('chat.input.file_not_supported')
- })
- }
- }
-
- if (supportExts.includes(getFileExtension(filePath))) {
- const selectedFile = await window.api.file.get(filePath)
- selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
- } else {
- window.message.info({
- key: 'file_not_supported',
- content: t('chat.input.file_not_supported')
- })
- }
- }
- return
- }
-
- // 短文本走默认粘贴行为
- },
- [model, pasteLongTextAsFile, pasteLongTextThreshold, resizeTextArea, supportExts, t]
- )
-
const autoResizeTextArea = (e: React.ChangeEvent) => {
const textarea = e.target
textarea.style.height = 'auto'
@@ -205,7 +178,7 @@ const MessageBlockEditor: FC = ({ message, onSave, onResend, onCancel })
.filter((block) => block.type === MessageBlockType.MAIN_TEXT)
.map((block) => (