Compare commits

...

35 Commits

Author SHA1 Message Date
Vaayne
d98c7e7405 Move i18n.py to scripts/auto-i18n.py with usage examples
Add documentation showing how to set the API key and run the script
with uv. Also improve code formatting with consistent quote style and
spacing.
2025-04-25 21:24:59 +08:00
Vaayne
779d4c4787 Add i18n translation script
Implement Python utility for managing internationalization filesi1 with
support for en-us, zh-8cn, ja-jp, ru-ru,n and zh-tw languages. Uses Agno
agent with OpenRouter for AI-powered translations an
2025-04-25 21:08:24 +08:00
Miter
32f160444b 获取searxng搜索结果url的内容作为大模型回答的参考,而不是使用searxng搜索结果的摘要内容 2025-04-25 18:13:43 +08:00
beyondkmp
a546c265ee fix: update electron-updater to patch to support window arm upgrade (#5337) 2025-04-25 18:13:07 +08:00
kangfenmao
ea89a37b1d feat: update licensing terms and UI components
- Revised the licensing agreement to introduce a User-Segmented Dual Licensing model, detailing conditions for individual and organizational use.
- Enhanced UI components to display a specific empty state image in various popups and pages.
- Adjusted styles in MiniAppIconsManager and MiniAppSettings for improved layout and appearance.
- Updated default web search provider to 'local-bing' and modified font size in ProviderOAuth description for better readability.
- Changed window style setting from 'transparent' to 'opaque' for a more consistent user experience.
2025-04-25 17:50:33 +08:00
kangfenmao
c288b4a8d0 refactor: reorder llm providers 2025-04-25 17:16:40 +08:00
kangfenmao
e8384db91a refactor: remove ollama settings 2025-04-25 17:16:27 +08:00
kangfenmao
36c87451d9 feat: enhance OAuth provider settings with new functionality
- Added a new ProviderOAuth component to manage OAuth authentication and billing for providers.
- Updated existing components to integrate the new ProviderOAuth functionality.
- Enhanced internationalization support for OAuth-related texts across multiple languages.
- Introduced new utility functions for handling provider billing.
2025-04-25 16:55:36 +08:00
fullex
eaa37fe674 feat: open popup url in external browser (#4446)
* feat: open popup url in external browser

* fix: allow google auth popup internal

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

* fix: set useragent globally

* fix: remove setUserAgent in webview

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

* feat: add gpt-4o-image

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

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

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

* add code

* update code
2025-04-24 16:55:51 +08:00
kangfenmao
62440cbfa1 chore: update dependencies and clean up code
- Reintroduced @mozilla/readability, @shikijs/markdown-it, and @xyflow/react to package.json.
- Updated shiki version to 3.2.2 in both package.json and yarn.lock.
- Removed trailing whitespace in IpcChannel.ts and index.ts for code cleanliness.
- Added outline style to .ant-tabs-tab-btn in ant.scss for improved UI consistency.
2025-04-24 16:39:09 +08:00
寇佳龙
39b723f143 feat(mcp): mcp setting add service description page 2025-04-24 15:55:43 +08:00
kangfenmao
bdc75f2f4e style(settings): update border-radius to use CSS variable for consistency 2025-04-24 15:18:53 +08:00
kangfenmao
eb3f136997 feat: add cherry-text-logo.svg and remove npm.svg; update MCPSettings and NpxSearch components
- Introduced a new cherry-text-logo.svg file for branding.
- Removed the deprecated npm.svg file.
- Refactored MCPSettings and NpxSearch components to enhance functionality and UI, including state management and layout adjustments.
- Updated translations in multiple locales to include new types for MCP servers.
2025-04-24 15:17:20 +08:00
LiuVaayne
6ba5768650 Update @modelcontextprotocol/sdk to v1.10.2 (#5266)
- Removed MCPStreamableHttpClient.ts as it is now provided by the SDK.
- Adjusted imports in MCPService.ts to use the SDK's implementation.
- Updated yarn.lock to reflect the new SDK version.
2025-04-24 15:05:42 +08:00
SuYao
1f588d242e refactor(GeminiProvider): streamline abort signal handling and improve stream processing (#5276) 2025-04-24 13:52:29 +08:00
kangfenmao
25b1e309ed build: remove sentry integration 2025-04-24 11:48:53 +08:00
kangfenmao
7a7b24fe2f build: fix nightly build error 2025-04-24 10:59:52 +08:00
Lucas
0686b2d813 fix(ci): Remove a deleted step which make the nightly build pipeline fail
These lines were deleted in `release.yml` in commit 75f98608.
2025-04-24 10:29:05 +08:00
Chen Tao
4a027892b9 feat: 添加嵌入维度配置 (#3947) 2025-04-24 10:18:23 +08:00
beyondkmp
be323b6304 feat: update os-proxy-config to 1.1.2 and delete the patch (#5255)
updte os-proxy-config to 1.1.2 and delete the patch
2025-04-24 09:24:08 +08:00
Asurada
2f5cfc0162 fix(settings): handle undefined content limit in BasicSettings component (#5252) 2025-04-24 02:58:36 +08:00
96 changed files with 1862 additions and 39967 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

3
.gitignore vendored
View File

@@ -51,6 +51,3 @@ local
coverage
.vitest-cache
vitest.config.*.timestamp-*
# Sentry Config File
.env.sentry-build-plugin

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,38 @@
diff --git a/out/MacUpdater.js b/out/MacUpdater.js
index 8f18dc5416c91835ded4e47f2358fba680c129ac..a3fb43c2450dc3484bf099b5ea79a362a3b372cc 100644
--- a/out/MacUpdater.js
+++ b/out/MacUpdater.js
@@ -74,7 +74,7 @@ class MacUpdater extends AppUpdater_1.AppUpdater {
else {
files = files.filter(file => !isArm64(file));
}
- const zipFileInfo = (0, Provider_1.findFile)(files, "zip", ["pkg", "dmg"]);
+ const zipFileInfo = (0, Provider_1.findFile)(files, "zip", ["pkg", "dmg"], false /*has been filtered by myself*/);
if (zipFileInfo == null) {
throw (0, builder_util_runtime_1.newError)(`ZIP file not provided: ${(0, builder_util_runtime_1.safeStringifyJson)(files)}`, "ERR_UPDATER_ZIP_FILE_NOT_FOUND");
}
diff --git a/out/providers/Provider.js b/out/providers/Provider.js
index 9829dff7e95aa9baa0bfdf29f52e6f761c9b7243..6ecaade9e294c87c03bb42e77ff5463f2782cb3c 100644
--- a/out/providers/Provider.js
+++ b/out/providers/Provider.js
@@ -61,11 +61,18 @@ class Provider {
}
}
exports.Provider = Provider;
-function findFile(files, extension, not) {
+function findFile(files, extension, not, filterByArch = true) {
if (files.length === 0) {
throw (0, builder_util_runtime_1.newError)("No files provided", "ERR_UPDATER_NO_FILES_PROVIDED");
}
- const result = files.find(it => it.url.pathname.toLowerCase().endsWith(`.${extension.toLowerCase()}`));
+ const result = files
+ .filter(file => {
+ if (!filterByArch) {
+ return true;
+ }
+ return (process.arch == "arm64") === (file.url.pathname.includes("arm64") || file.info.url.includes("arm64"));
+ })
+ .find(it => it.url.pathname.toLowerCase().endsWith(`.${extension.toLowerCase()}`));
if (result != null) {
return result;
}

View File

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

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

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

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

View File

@@ -114,7 +114,7 @@ https://docs.cherry-ai.com
3. **提交更改**:提交并推送您的更改。
4. **打开 Pull Request**:描述您的更改和原因。
有关更详细的指南,请参阅我们的 [贡献指南](../CONTRIBUTING.md)。
有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.zh.md)。
感谢您的支持和贡献!

View File

@@ -37,6 +37,12 @@ yarn install
yarn dev
```
### Test
```bash
yarn test
```
### Build
```bash

View File

@@ -87,11 +87,9 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
修正语言及本地化错误
Windows ARM 更新跳转到官网下载
改进系统代理处理和初始化逻辑
修复 MCP 服务请求头不生效问题
移除搜索增强模式
优化消息渲染速度
修复备份大文件失败问题
修复网络搜索导致卡顿问题
新增对 grok-2-image 和 gpt-4o-image 图像支持
支持 Windows 便携版使用 data 目录存储数据
MCP 界面改版,新增描述信息显示
Mermaid 渲染逻辑优化
支持关闭公示渲染
修复 OpenAI 类型渲染错误

View File

@@ -1,4 +1,3 @@
import { sentryVitePlugin } from '@sentry/vite-plugin'
import react from '@vitejs/plugin-react-swc'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { resolve } from 'path'
@@ -65,11 +64,6 @@ export default defineConfig({
]
]
}),
sentryVitePlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: 'cherry-ai',
project: 'cherry-studio'
}),
...visualizerPlugin('renderer')
],
resolve: {

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.2.7",
"version": "1.2.8",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -70,13 +70,8 @@
"@electron-toolkit/utils": "^3.0.0",
"@electron/notarize": "^2.5.0",
"@langchain/community": "^0.3.36",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@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",
"archiver": "^7.0.1",
"async-mutex": "^0.5.0",
@@ -86,7 +81,7 @@
"docx": "^9.0.2",
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "^6.3.9",
"electron-updater": "patch:electron-updater@npm%3A6.6.3#~/.yarn/patches/electron-updater-npm-6.6.3-9269dbaf84.patch",
"electron-window-state": "^5.0.3",
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
"extract-zip": "^2.0.1",
@@ -98,7 +93,7 @@
"markdown-it": "^14.1.0",
"node-stream-zip": "^1.15.0",
"officeparser": "^4.1.1",
"os-proxy-config": "patch:os-proxy-config@npm%3A1.1.1#~/.yarn/patches/os-proxy-config-npm-1.1.1-af9c7574cc.patch",
"os-proxy-config": "^1.1.2",
"proxy-agent": "^6.5.0",
"tar": "^7.4.3",
"turndown": "^7.2.0",
@@ -121,14 +116,14 @@
"@emotion/is-prop-valid": "^1.3.1",
"@eslint-react/eslint-plugin": "^1.36.1",
"@eslint/js": "^9.22.0",
"@google/genai": "patch:@google/genai@npm%3A0.8.0#~/.yarn/patches/@google-genai-npm-0.8.0-450d0d9a7d.patch",
"@google/genai": "^0.10.0",
"@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0",
"@modelcontextprotocol/sdk": "^1.9.0",
"@modelcontextprotocol/sdk": "^1.10.2",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@reduxjs/toolkit": "^2.2.5",
"@sentry/react": "^9.13.0",
"@sentry/vite-plugin": "^3.3.1",
"@shikijs/markdown-it": "^3.2.2",
"@swc/plugin-styled-components": "^7.1.3",
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
"@tryfabric/martian": "^1.2.4",
@@ -148,6 +143,7 @@
"@vitejs/plugin-react-swc": "^3.9.0",
"@vitest/coverage-v8": "^3.1.1",
"@vitest/ui": "^3.1.1",
"@xyflow/react": "^12.4.4",
"antd": "^5.22.5",
"applescript": "^1.0.0",
"axios": "^1.7.3",
@@ -201,7 +197,7 @@
"remark-math": "^6.0.0",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.2",
"shiki": "^3.2.1",
"shiki": "^3.2.2",
"string-width": "^7.2.0",
"styled-components": "^6.1.11",
"tiny-pinyin": "^1.3.2",
@@ -220,7 +216,8 @@
"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"
"app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch",
"shiki": "3.2.2"
},
"packageManager": "yarn@4.6.0",
"lint-staged": {

View File

@@ -20,6 +20,8 @@ export enum IpcChannel {
App_InstallUvBinary = 'app:install-uv-binary',
App_InstallBunBinary = 'app:install-bun-binary',
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
// Open
Open_Path = 'open:path',
Open_Website = 'open:website',
@@ -159,8 +161,5 @@ export enum IpcChannel {
// Search Window
SearchWindow_Open = 'search-window:open',
SearchWindow_Close = 'search-window:close',
SearchWindow_OpenUrl = 'search-window:open-url',
// sentry
Sentry_Init = 'sentry:init'
SearchWindow_OpenUrl = 'search-window:open-url'
}

View File

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

157
scripts/auto-i18n.py Normal file
View File

@@ -0,0 +1,157 @@
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "agno",
# "openai",
# ]
# ///
#
# Example of how to run the script:
#
# 1. First, set the OpenRouter API key environment variable:
# ```
# export OPENROUTER_API_KEY=your-api-key
# ```
#
# 2. Then run the script using uv:
# ```
# uv run i18n.py --dir src/renderer/src/i18n/locales "settings.mcp.autoDescription='auto set i18n', settings.mcp.autoName='auto set i18n name'"
# ```
import json
import argparse
from pathlib import Path
from agno.agent import Agent
from agno.models.openrouter import OpenRouter
from agno.tools import tool
LANGUAGES = ["en-us", "zh-cn", "ja-jp", "ru-ru", "zh-tw"]
def ensure_json_files_exist(output_dir=None):
"""Ensure that all language JSON files exist with at least an empty object."""
output_dir = Path(output_dir) if output_dir else Path(".")
# Create the directory if it doesn't exist
output_dir.mkdir(parents=True, exist_ok=True)
for lang in LANGUAGES:
file_path = output_dir / f"{lang}.json"
if not file_path.exists():
with open(file_path, "w") as f:
json.dump({}, f, indent=4)
def set_nested_value(data, keys, value):
"""Recursively navigate through a nested dictionary and set the value."""
if len(keys) == 1:
data[keys[0]] = value
return
key = keys[0]
if key not in data:
data[key] = {}
set_nested_value(data[key], keys[1:], value)
@tool(show_result=True, stop_after_tool_call=True)
def set_i18n(key: str, translations: dict[str, str], output_dir=None):
"""
Set i18n translations for a key in all language files.
Args:
key: The i18n key (e.g., "settings.mcp.sync.title")
translations: Dictionary with translations for different languages
output_dir: Directory to store the i18n JSON files
Example:
set_i18n("settings.mcp.hello", {
"en-us": "Hello",
"zh-cn": "你好",
"ja-jp": "こんにちは",
"ru-ru": "Привет",
"zh-tw": "你好"
})
"""
ensure_json_files_exist(output_dir)
output_dir = Path(output_dir) if output_dir else Path(".")
results = {}
keys = key.split(".")
if keys[0] != "translation":
keys = ["translation"] + keys
for lang, text in translations.items():
if lang not in LANGUAGES:
continue
file_path = output_dir / f"{lang}.json"
try:
# Load existing data
with open(file_path, "r", encoding="utf-8") as f:
try:
data = json.load(f)
except json.JSONDecodeError:
data = {}
# Set the value at the nested path
set_nested_value(data, keys, text)
# Save the updated data
with open(file_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
results[lang] = f"Updated {key} in {file_path}"
except Exception as e:
results[lang] = f"Error updating {file_path}: {str(e)}"
return results
def main():
"""Main function to run the i18n translation agent."""
# Set up command line argument parser
parser = argparse.ArgumentParser(description="Translate i18n JSON content")
parser.add_argument("content", help="JSON content to translate")
parser.add_argument(
"-m",
"--model",
default="gpt-4.1-mini",
help="Model to use for translation (default: gpt-4.1-mini)",
)
parser.add_argument(
"--dir",
default=None,
help="Directory to store i18n JSON files (default: current directory)",
)
# Parse arguments
args = parser.parse_args()
# Initialize the agent with the specified model
agent = Agent(
model=OpenRouter(id=args.model),
tools=[set_i18n],
markdown=True,
)
# Create the prompt with the provided content
prompt = f"""Please help set i18n translations for the following content to all supported languages: {LANGUAGES}.
<content>
{args.content}
</content>
Use the provided directory {args.dir} for storing the i18n JSON files.
"""
# Call the agent with the tools context that includes the output directory
agent.print_response(
prompt, stream=True, tools_context={"set_i18n": {"output_dir": args.dir}}
)
if __name__ == "__main__":
main()

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import { app, ipcMain } from 'electron'
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'
@@ -13,6 +12,7 @@ import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } fro
import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService'
import { setAppDataDir } from './utils/file'
// Check for single instance lock
if (!app.requestSingleInstanceLock()) {
@@ -51,6 +51,8 @@ if (!app.requestSingleInstanceLock()) {
replaceDevtoolsFont(mainWindow)
setAppDataDir()
if (process.env.NODE_ENV === 'development') {
installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS])
.then((name) => console.log(`Added Extension: ${name}`))
@@ -73,14 +75,6 @@ if (!app.requestSingleInstanceLock()) {
handleProtocolUrl(url)
})
registerProtocolClient(app)
// macOS specific: handle protocol when app is already running
app.on('open-url', (event, url) => {
event.preventDefault()
handleProtocolUrl(url)
})
// Listen for second instance
app.on('second-instance', (_event, argv) => {
windowService.showMainWindow()
@@ -111,5 +105,3 @@ if (!app.requestSingleInstanceLock()) {
// 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

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

@@ -9,7 +9,6 @@ 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'
@@ -26,6 +25,7 @@ import { ProxyConfig, proxyManager } from './services/ProxyManager'
import { searchService } from './services/SearchService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { setOpenLinkExternal } from './services/WebviewService'
import { windowService } from './services/WindowService'
import { getResourcePath } from './utils'
import { decrypt, encrypt } from './utils/aes'
@@ -178,8 +178,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// check for update
ipcMain.handle(IpcChannel.App_CheckForUpdate, async () => {
// 在 Windows 上,如果架构是 arm64则不检查更新
if (isWin && (arch().includes('arm') || 'PORTABLE_EXECUTABLE_DIR' in process.env)) {
if (isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env) {
return {
currentVersion: app.getVersion(),
updateInfo: null
@@ -344,6 +343,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
return await searchService.openUrlInSearchWindow(uid, url)
})
// sentry
ipcMain.handle(IpcChannel.Sentry_Init, () => initSentry())
// webview
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
setOpenLinkExternal(webviewId, isExternal)
)
}

View File

@@ -10,6 +10,10 @@ import { getBinaryName, getBinaryPath } from '@main/utils/process'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { SSEClientTransport, SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js'
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import {
StreamableHTTPClientTransport,
type StreamableHTTPClientTransportOptions
} from '@modelcontextprotocol/sdk/client/streamableHttp'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'
import { nanoid } from '@reduxjs/toolkit'
import {
@@ -29,7 +33,6 @@ import { memoize } from 'lodash'
import { CacheService } from './CacheService'
import { CallBackServer } from './mcp/oauth/callback'
import { McpOAuthClientProvider } from './mcp/oauth/provider'
import { StreamableHTTPClientTransport, type StreamableHTTPClientTransportOptions } from './MCPStreamableHttpClient'
// Generic type for caching wrapped functions
type CachedFunction<T extends unknown[], R> = (...args: T) => Promise<R>
@@ -159,7 +162,7 @@ class McpService {
} else if (server.type === 'sse') {
const options: SSEClientTransportOptions = {
eventSourceInit: {
fetch: (url, init) => fetch(url, { ...init, headers: server.headers || {} }),
fetch: (url, init) => fetch(url, { ...init, headers: server.headers || {} })
},
requestInit: {
headers: server.headers || {}

View File

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

View File

@@ -0,0 +1,35 @@
import { session, shell, webContents } from 'electron'
/**
* init the useragent of the webview session
* remove the CherryStudio and Electron from the useragent
*/
export function initSessionUserAgent() {
const wvSession = session.fromPartition('persist:webview')
const newChromeVersion = '135.0.7049.96'
const originUA = wvSession.getUserAgent()
const newUA = originUA
.replace(/CherryStudio\/\S+\s/, '')
.replace(/Electron\/\S+\s/, '')
.replace(/Chrome\/\d+\.\d+\.\d+\.\d+/, `Chrome/${newChromeVersion}`)
wvSession.setUserAgent(newUA)
}
/**
* WebviewService handles the behavior of links opened from webview elements
* It controls whether links should be opened within the application or in an external browser
*/
export function setOpenLinkExternal(webviewId: number, isExternal: boolean) {
const webview = webContents.fromId(webviewId)
if (!webview) return
webview.setWindowOpenHandler(({ url }) => {
if (isExternal) {
shell.openExternal(url)
return { action: 'deny' }
} else {
return { action: 'allow' }
}
})
}

View File

@@ -11,6 +11,7 @@ import icon from '../../../build/icon.png?asset'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
import { locales } from '../utils/locales'
import { configManager } from './ConfigManager'
import { initSessionUserAgent } from './WebviewService'
export class WindowService {
private static instance: WindowService | null = null
@@ -41,7 +42,8 @@ export class WindowService {
const mainWindowState = windowStateKeeper({
defaultWidth: 1080,
defaultHeight: 670,
fullScreen: false
fullScreen: false,
maximize: false
})
const theme = configManager.getTheme()
@@ -80,12 +82,16 @@ export class WindowService {
this.miniWindow = this.createMiniWindow(true)
}
//init the MinApp webviews' useragent
initSessionUserAgent()
return this.mainWindow
}
private setupMainWindow(mainWindow: BrowserWindow, mainWindowState: any) {
mainWindowState.manage(mainWindow)
this.setupMaximize(mainWindow, mainWindowState.isMaximized)
this.setupContextMenu(mainWindow)
this.setupWindowEvents(mainWindow)
this.setupWebContentsHandlers(mainWindow)
@@ -93,6 +99,17 @@ export class WindowService {
this.loadMainWindowContent(mainWindow)
}
private setupMaximize(mainWindow: BrowserWindow, isMaximized: boolean) {
if (isMaximized) {
// 如果是从托盘启动,则需要延迟最大化,否则显示的就不是重启前的最大化窗口了
configManager.getLaunchToTray()
? mainWindow.once('show', () => {
mainWindow.maximize()
})
: mainWindow.maximize()
}
}
private setupContextMenu(mainWindow: BrowserWindow) {
if (!this.contextMenu) {
const locale = locales[configManager.getLanguage()]
@@ -191,9 +208,11 @@ export class WindowService {
const oauthProviderUrls = [
'https://account.siliconflow.cn/oauth',
'https://cloud.siliconflow.cn/bills',
'https://cloud.siliconflow.cn/expensebill',
'https://aihubmix.com/token',
'https://aihubmix.com/topup'
'https://aihubmix.com/topup',
'https://aihubmix.com/statistics'
]
if (oauthProviderUrls.some((link) => url.startsWith(link))) {

View File

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

View File

@@ -33,9 +33,6 @@ declare global {
setAutoUpdate: (isActive: boolean) => void
reload: () => void
clearCache: () => Promise<{ success: boolean; error?: string }>
sentry: {
init: () => Promise<void>
}
system: {
getDeviceType: () => Promise<'mac' | 'windows' | 'linux'>
getHostname: () => Promise<string>
@@ -207,6 +204,9 @@ declare global {
closeSearchWindow: (uid: string) => Promise<string>
openUrlInSearchWindow: (uid: string, url: string) => Promise<string>
}
webview: {
setOpenLinkExternal: (webviewId: number, isExternal: boolean) => Promise<void>
}
}
}
}

View File

@@ -23,9 +23,6 @@ const api = {
setAutoUpdate: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetAutoUpdate, isActive),
openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache),
sentry: {
init: () => ipcRenderer.invoke(IpcChannel.Sentry_Init)
},
system: {
getDeviceType: () => ipcRenderer.invoke(IpcChannel.System_GetDeviceType),
getHostname: () => ipcRenderer.invoke(IpcChannel.System_GetHostname)
@@ -188,6 +185,10 @@ const api = {
openSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Open, uid),
closeSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Close, uid),
openUrlInSearchWindow: (uid: string, url: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_OpenUrl, uid, url)
},
webview: {
setOpenLinkExternal: (webviewId: number, isExternal: boolean) =>
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal)
}
}

View File

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

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

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

Before

Width:  |  Height:  |  Size: 845 B

View File

@@ -16,6 +16,10 @@
outline: none;
}
.ant-tabs-tab-btn {
outline: none;
}
.ant-segmented-group {
gap: 4px;
}

View File

@@ -5,11 +5,12 @@ import styled from 'styled-components'
interface Props {
app: MinAppType
sidebar?: boolean
size?: number
style?: React.CSSProperties
}
const MinAppIcon: FC<Props> = ({ app, size = 48, style }) => {
const MinAppIcon: FC<Props> = ({ app, size = 48, style, sidebar = false }) => {
const _app = DEFAULT_MIN_APPS.find((item) => item.id === app.id)
if (!_app) {
@@ -24,7 +25,7 @@ const MinAppIcon: FC<Props> = ({ app, size = 48, style }) => {
width: `${size}px`,
height: `${size}px`,
backgroundColor: _app.background,
...app.style,
...(sidebar ? {} : app.style),
...style
}}
/>

View File

@@ -3,6 +3,7 @@ import {
CodeOutlined,
CopyOutlined,
ExportOutlined,
LinkOutlined,
MinusOutlined,
PushpinOutlined,
ReloadOutlined
@@ -14,6 +15,9 @@ import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { setMinappsOpenLinkExternal } from '@renderer/store/settings'
import { MinAppType } from '@renderer/types'
import { delay } from '@renderer/utils'
import { Avatar, Drawer, Tooltip } from 'antd'
@@ -40,6 +44,7 @@ const MinappPopupContainer: React.FC = () => {
const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation()
const backgroundColor = useNavBackgroundColor()
const dispatch = useAppDispatch()
/** control the drawer open or close */
const [isPopupShow, setIsPopupShow] = useState(true)
@@ -57,6 +62,8 @@ const MinappPopupContainer: React.FC = () => {
const webviewRefs = useRef<Map<string, WebviewTag | null>>(new Map())
/** indicate whether the webview has loaded */
const webviewLoadedRefs = useRef<Map<string, boolean>>(new Map())
/** whether the minapps open link external is enabled */
const { minappsOpenLinkExternal } = useSettings()
const isInDevelopment = process.env.NODE_ENV === 'development'
@@ -107,9 +114,14 @@ const MinappPopupContainer: React.FC = () => {
webviewLoadedRefs.current.forEach((_, appid) => {
if (!webviewRefs.current.has(appid)) {
webviewLoadedRefs.current.delete(appid)
} else if (appid === currentMinappId) {
const webviewId = webviewRefs.current.get(appid)?.getWebContentsId()
if (webviewId) {
window.api.webview.setOpenLinkExternal(webviewId, minappsOpenLinkExternal)
}
}
})
}, [currentMinappId])
}, [currentMinappId, minappsOpenLinkExternal])
/** only the keepalive minapp can be minimized */
const canMinimize = !(openedOneOffMinapp && openedOneOffMinapp.id == currentMinappId)
@@ -175,6 +187,10 @@ const MinappPopupContainer: React.FC = () => {
/** the callback function to set the webviews loaded indicator */
const handleWebviewLoaded = (appid: string) => {
webviewLoadedRefs.current.set(appid, true)
const webviewId = webviewRefs.current.get(appid)?.getWebContentsId()
if (webviewId) {
window.api.webview.setOpenLinkExternal(webviewId, minappsOpenLinkExternal)
}
if (appid == currentMinappId) {
setTimeout(() => setIsReady(true), 200)
}
@@ -220,6 +236,11 @@ const MinappPopupContainer: React.FC = () => {
updatePinnedMinapps(newPinned)
}
/** set the open external status */
const handleToggleOpenExternal = () => {
dispatch(setMinappsOpenLinkExternal(!minappsOpenLinkExternal))
}
/** Title bar of the popup */
const Title = ({ appInfo, url }: { appInfo: AppInfo | null; url: string | null }) => {
if (!appInfo) return null
@@ -238,7 +259,7 @@ const MinappPopupContainer: React.FC = () => {
}
return (
<TitleContainer style={{ backgroundColor: backgroundColor, justifyContent: 'space-between' }}>
<TitleContainer style={{ backgroundColor: backgroundColor }}>
<Tooltip
title={
<TitleTextTooltip>
@@ -256,6 +277,14 @@ const MinappPopupContainer: React.FC = () => {
}}>
<TitleText onContextMenu={(e) => handleCopyUrl(e, url ?? appInfo.url)}>{appInfo.name}</TitleText>
</Tooltip>
{appInfo.canOpenExternalLink && (
<Tooltip title={t('minapp.popup.openExternal')} mouseEnterDelay={0.8} placement="bottom">
<Button onClick={() => handleOpenLink(url ?? appInfo.url)}>
<ExportOutlined />
</Button>
</Tooltip>
)}
<Spacer />
<ButtonsGroup className={isWindows ? 'windows' : ''}>
<Tooltip title={t('minapp.popup.refresh')} mouseEnterDelay={0.8} placement="bottom">
<Button onClick={() => handleReload(appInfo.id)}>
@@ -272,13 +301,18 @@ const MinappPopupContainer: React.FC = () => {
</Button>
</Tooltip>
)}
{appInfo.canOpenExternalLink && (
<Tooltip title={t('minapp.popup.openExternal')} mouseEnterDelay={0.8} placement="bottom">
<Button onClick={() => handleOpenLink(url ?? appInfo.url)}>
<ExportOutlined />
</Button>
</Tooltip>
)}
<Tooltip
title={
minappsOpenLinkExternal
? t('minapp.popup.open_link_external_on')
: t('minapp.popup.open_link_external_off')
}
mouseEnterDelay={0.8}
placement="bottom">
<Button onClick={handleToggleOpenExternal} className={minappsOpenLinkExternal ? 'open-external' : ''}>
<LinkOutlined />
</Button>
</Tooltip>
{isInDevelopment && (
<Tooltip title={t('minapp.popup.devtools')} mouseEnterDelay={0.8} placement="bottom">
<Button onClick={() => handleOpenDevTools(appInfo.id)}>
@@ -367,8 +401,8 @@ const TitleText = styled.div`
font-weight: bold;
font-size: 14px;
color: var(--color-text-1);
margin-right: 10px;
-webkit-app-region: no-drag;
margin-right: 5px;
`
const TitleTextTooltip = styled.span`
@@ -407,6 +441,7 @@ const Button = styled.div`
color: var(--color-text-2);
transition: all 0.2s ease;
font-size: 14px;
-webkit-app-region: no-drag;
&:hover {
color: var(--color-text-1);
background-color: var(--color-background-mute);
@@ -415,6 +450,10 @@ const Button = styled.div`
color: var(--color-primary);
background-color: var(--color-primary-bg);
}
&.open-external {
color: var(--color-primary);
background-color: var(--color-primary-bg);
}
`
const EmptyView = styled.div`
@@ -428,4 +467,8 @@ const EmptyView = styled.div`
background-color: var(--color-background);
`
const Spacer = styled.div`
flex: 1;
`
export default MinappPopupContainer

View File

@@ -60,9 +60,6 @@ const WebviewContainer = memo(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appid, url])
//remove the tag of CherryStudio and Electron
const userAgent = navigator.userAgent.replace(/CherryStudio\/\S+\s/, '').replace(/Electron\/\S+\s/, '')
return (
<webview
key={appid}
@@ -70,7 +67,6 @@ const WebviewContainer = memo(
style={WebviewStyle}
allowpopups={'true' as any}
partition="persist:webview"
useragent={userAgent}
/>
)
}

View File

@@ -11,6 +11,7 @@ interface Props extends ButtonProps {
const OAuthButton: FC<Props> = ({ provider, onSuccess, ...buttonProps }) => {
const { t } = useTranslation()
const onAuth = () => {
const handleSuccess = (key: string) => {
if (key.trim()) {
@@ -29,8 +30,8 @@ const OAuthButton: FC<Props> = ({ provider, onSuccess, ...buttonProps }) => {
}
return (
<Button onClick={onAuth} {...buttonProps}>
{t('auth.get_key')}
<Button type="primary" onClick={onAuth} shape="round" {...buttonProps}>
{t('settings.provider.oauth.button', { provider: t(`provider.${provider.id}`) })}
</Button>
)
}

View File

@@ -48,7 +48,7 @@ const MinAppsPopover: FC<Props> = ({ children }) => {
))}
{isEmpty(minapps) && (
<Center>
<Empty />
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
</Center>
)}
</AppsContainer>

View File

@@ -248,7 +248,7 @@ const SidebarOpenedMinappTabs: FC = () => {
theme={theme}
onClick={() => handleOnClick(app)}
className={`${isActive ? 'opened-active' : ''}`}>
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} sidebar />
</Icon>
</Dropdown>
</StyledLink>
@@ -290,7 +290,7 @@ const PinnedApps: FC = () => {
theme={theme}
onClick={() => openMinappKeepAlive(app)}
className={`${isActive ? 'active' : ''} ${openedKeepAliveMinapps.some((item) => item.id === app.id) ? 'opened-minapp' : ''}`}>
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} sidebar />
</Icon>
</Dropdown>
</StyledLink>

View File

@@ -243,18 +243,6 @@ export const EMBEDDING_MODELS = [
id: 'mistral-embed',
max_context: 8000
},
{
id: 'voyage-3-large',
max_context: 1024
},
{
id: 'voyage-3-large',
max_context: 256
},
{
id: 'voyage-3-large',
max_context: 512
},
{
id: 'voyage-3-large',
max_context: 2048

View File

@@ -2145,7 +2145,13 @@ export const TEXT_TO_IMAGES_MODELS_SUPPORT_IMAGE_ENHANCEMENT = [
'stabilityai/stable-diffusion-xl-base-1.0'
]
export const GENERATE_IMAGE_MODELS = ['gemini-2.0-flash-exp-image-generation', 'gemini-2.0-flash-exp']
export const GENERATE_IMAGE_MODELS = [
'gemini-2.0-flash-exp-image-generation',
'gemini-2.0-flash-exp',
'grok-2-image-1212',
'gpt-4o-image',
'gpt-image-1'
]
export const GEMINI_SEARCH_MODELS = [
'gemini-2.0-flash',

View File

@@ -148,7 +148,7 @@ export const PROVIDER_CONFIG = {
url: 'https://api.siliconflow.cn'
},
websites: {
official: 'https://www.siliconflow.cn/',
official: 'https://www.siliconflow.cn',
apiKey: 'https://cloud.siliconflow.cn/i/d1nTBKXU',
docs: 'https://docs.siliconflow.cn/',
models: 'https://docs.siliconflow.cn/docs/model-names'

View File

@@ -3,7 +3,6 @@ import { isLocalAi } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import { initSentry } from '@renderer/init'
import { useAppDispatch } from '@renderer/store'
import { setAvatar, setFilesPath, setResourcesPath, setUpdateState } from '@renderer/store/runtime'
import { delay, runAsyncFunction } from '@renderer/utils'
@@ -106,6 +105,6 @@ export function useAppInit() {
}, [customCss])
useEffect(() => {
enableDataCollection && initSentry()
// TODO: init data collection
}, [enableDataCollection])
}

View File

@@ -29,8 +29,13 @@ export const useMCPServers = () => {
}
export const useMCPServer = (id: string) => {
const { mcpServers } = useMCPServers()
const server = useAppSelector((state) => (state.mcp.servers || []).find((server) => server.id === id))
const dispatch = useAppDispatch()
return {
server: mcpServers.find((server) => server.id === id)
server,
updateMCPServer: (server: MCPServer) => dispatch(updateMCPServer(server)),
setMCPServerActive: (server: MCPServer, isActive: boolean) => dispatch(updateMCPServer({ ...server, isActive })),
deleteMCPServer: (id: string) => dispatch(deleteMCPServer(id))
}
}

View File

@@ -1,18 +0,0 @@
import store, { useAppSelector } from '@renderer/store'
import { setOllamaKeepAliveTime } from '@renderer/store/llm'
import { useDispatch } from 'react-redux'
export function useOllamaSettings() {
const settings = useAppSelector((state) => state.llm.settings.ollama)
const dispatch = useDispatch()
return { ...settings, setKeepAliveTime: (time: number) => dispatch(setOllamaKeepAliveTime(time)) }
}
export function getOllamaSettings() {
return store.getState().llm.settings.ollama
}
export function getOllamaKeepAliveTime() {
return store.getState().llm.settings.ollama.keepAliveTime + 'm'
}

View File

@@ -445,7 +445,11 @@
"topN_tooltip": "The number of matching results returned; the larger the value, the more matching results, but also the more tokens consumed.",
"url_added": "URL added",
"url_placeholder": "Enter URL, multiple URLs separated by Enter",
"urls": "URLs"
"urls": "URLs",
"dimensions": "Embedding dimension",
"dimensions_size_tooltip": "The size of the embedding dimension; the larger the value, the larger the embedding dimension, but it also consumes more tokens.",
"dimensions_size_placeholder": "Default value (modification not recommended)",
"dimensions_size_too_large": "The embedding dimension cannot exceed the model's context limit ({{max_context}})."
},
"languages": {
"arabic": "Arabic",
@@ -558,7 +562,9 @@
"tools": {
"completed": "Completed",
"invoking": "Invoking",
"error": "Error occurred"
"error": "Error occurred",
"raw": "Raw",
"preview": "Preview"
},
"topic.added": "New topic added",
"upgrade.success.button": "Restart",
@@ -579,7 +585,9 @@
"minimize": "Minimize MinApp",
"devtools": "Developer Tools",
"openExternal": "Open in Browser",
"rightclick_copyurl": "Right-click to copy URL"
"rightclick_copyurl": "Right-click to copy URL",
"open_link_external_on": "Current: Open links in browser",
"open_link_external_off": "Current: Open links in default window"
},
"sidebar.add.title": "Add to sidebar",
"sidebar.remove.title": "Remove from sidebar",
@@ -1014,6 +1022,9 @@
"disabled": "Hidden Mini Apps",
"empty": "Drag mini apps from the left to hide them",
"visible": "Visible Mini Apps",
"open_link_external": {
"title": "Open new-window links in browser"
},
"cache_settings": "Cache Settings",
"cache_title": "Mini App Cache Limit",
"cache_description": "Set the maximum number of active mini apps to keep in memory",
@@ -1117,6 +1128,7 @@
"installHelp": "Get Installation Help",
"tabs": {
"general": "General",
"description": "Description",
"tools": "Tools",
"prompts": "Prompts",
"resources": "Resources"
@@ -1152,7 +1164,13 @@
"registryDefault": "Default",
"not_support": "Model not supported",
"user": "User",
"system": "System"
"system": "System",
"types": {
"inMemory": "In Memory",
"sse": "SSE",
"streamableHttp": "Streamable HTTP",
"stdio": "STDIO"
}
},
"messages.divider": "Show divider between messages",
"messages.grid_columns": "Message grid display columns",
@@ -1239,10 +1257,16 @@
"basic_auth.user_name.tip": "Left empty to disable",
"basic_auth.password": "Password",
"basic_auth.password.tip": "",
"charge": "Charge",
"charge": "Balance Recharge",
"bills": "Fee Bills",
"check": "Check",
"check_all_keys": "Check All Keys",
"check_multiple_keys": "Check Multiple API Keys",
"oauth": {
"button": "Login with {{provider}}",
"description": "This service is provided by <website>{{provider}}</website>",
"official_website": "Official Website"
},
"copilot": {
"auth_failed": "Github Copilot authentication failed.",
"auth_success": "GitHub Copilot authentication successful.",
@@ -1339,7 +1363,7 @@
"websearch": {
"blacklist": "Blacklist",
"blacklist_description": "Results from the following websites will not appear in search results",
"blacklist_tooltip": "Please use the following format (separated by line breaks)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
"blacklist_tooltip": "Please use the following format (separated by newlines)\nPattern matching: *://*.example.com/*\nRegular expression: /example\\.(net|org)/",
"check": "Check",
"check_failed": "Verification failed",
"check_success": "Verification successful",

View File

@@ -0,0 +1,119 @@
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "agno",
# "openai",
# ]
# ///
import json
from pathlib import Path
from agno.agent import Agent
from agno.models.openrouter import OpenRouter
from agno.tools import tool
LANGUAGES = ["en-us", "zh-cn", "ja-jp", "ru-ru", "zh-tw"]
def ensure_json_files_exist():
"""Ensure that all language JSON files exist with at least an empty object."""
for lang in LANGUAGES:
file_path = Path(f"{lang}.json")
if not file_path.exists():
with open(file_path, "w") as f:
json.dump({}, f, indent=4)
def set_nested_value(data, keys, value):
"""Recursively navigate through a nested dictionary and set the value."""
if len(keys) == 1:
data[keys[0]] = value
return
key = keys[0]
if key not in data:
data[key] = {}
set_nested_value(data[key], keys[1:], value)
@tool(show_result=True, stop_after_tool_call=True)
def set_i18n(key: str, translations: dict[str, str]):
"""
Set i18n translations for a key in all language files.
Args:
key: The i18n key (e.g., "settings.mcp.sync.title")
translations: Dictionary with translations for different languages
Example:
set_i18n("settings.mcp.hello", {
"en-us": "Hello",
"zh-cn": "你好",
"ja-jp": "こんにちは",
"ru-ru": "Привет",
"zh-tw": "你好"
})
"""
ensure_json_files_exist()
results = {}
keys = key.split(".")
if keys[0] != "translation":
keys = ["translation"] + keys
for lang, text in translations.items():
if lang not in LANGUAGES:
continue
file_path = f"{lang}.json"
try:
# Load existing data
with open(file_path, "r", encoding="utf-8") as f:
try:
data = json.load(f)
except json.JSONDecodeError:
data = {}
# Set the value at the nested path
set_nested_value(data, keys, text)
# Save the updated data
with open(file_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
results[lang] = f"Updated {key} in {file_path}"
except Exception as e:
results[lang] = f"Error updating {file_path}: {str(e)}"
return results
content = """
{
"settings.mcp.sync.unauthorized": "Sync Unauthorized",
"settings.mcp.sync.noServersAvailable": "No MCP servers available"
}
"""
def main():
"""Main function to run the i18n translation agent."""
agent = Agent(
model=OpenRouter(id="gpt-4.1-mini"),
tools=[set_i18n],
markdown=True,
)
prompt = f"""Please help set i18n translations for the following content to all supported languages: {LANGUAGES}.
<content>
{content}
</content>
"""
agent.print_response(prompt, stream=True)
if __name__ == "__main__":
main()

View File

@@ -445,7 +445,11 @@
"topN_tooltip": "返されるマッチ結果の数は、数値が大きいほどマッチ結果が多くなりますが、消費されるトークンも増えます。",
"url_added": "URLが追加されました",
"url_placeholder": "URLを入力, 複数のURLはEnterで区切る",
"urls": "URL"
"urls": "URL",
"dimensions": "埋め込み次元",
"dimensions_size_tooltip": "埋め込み次元のサイズは、数値が大きいほど埋め込み次元も大きくなりますが、消費するトークンも増えます。",
"dimensions_size_placeholder": "デフォルト値(変更はお勧めしません)",
"dimensions_size_too_large": "埋め込み次元はモデルのコンテキスト制限({{max_context}})を超えてはなりません。"
},
"languages": {
"arabic": "アラビア語",
@@ -557,7 +561,9 @@
"tools": {
"completed": "完了",
"invoking": "呼び出し中",
"error": "エラーが発生しました"
"error": "エラーが発生しました",
"raw": "生データ",
"preview": "プレビュー"
},
"topic.added": "新しいトピックが追加されました",
"upgrade.success.button": "再起動",
@@ -579,7 +585,9 @@
"minimize": "ミニアプリを最小化",
"devtools": "開発者ツール",
"openExternal": "ブラウザで開く",
"rightclick_copyurl": "右クリックでURLをコピー"
"rightclick_copyurl": "右クリックでURLをコピー",
"open_link_external_on": "現在:ブラウザで開く",
"open_link_external_off": "現在:デフォルトのウィンドウで開く"
},
"sidebar.add.title": "サイドバーに追加",
"sidebar.remove.title": "サイドバーから削除",
@@ -1014,6 +1022,9 @@
"disabled": "非表示のミニアプリ",
"empty": "非表示にするミニアプリを左側からここにドラッグしてください",
"visible": "表示するミニアプリ",
"open_link_external": {
"title": "新視窗のリンクをブラウザで開く"
},
"cache_settings": "キャッシュ設定",
"cache_title": "ミニアプリのキャッシュ数",
"cache_description": "メモリに保持するアクティブなミニアプリの最大数を設定します",
@@ -1116,6 +1127,7 @@
"installHelp": "インストールヘルプを取得",
"tabs": {
"general": "一般",
"description": "説明",
"tools": "ツール",
"prompts": "プロンプト",
"resources": "リソース"
@@ -1151,7 +1163,13 @@
"registryDefault": "デフォルト",
"not_support": "モデルはサポートされていません",
"user": "ユーザー",
"system": "システム"
"system": "システム",
"types": {
"inMemory": "組み込み",
"sse": "SSE",
"streamableHttp": "ストリーミング",
"stdio": "STDIO"
}
},
"messages.divider": "メッセージ間に区切り線を表示",
"messages.grid_columns": "メッセージグリッドの表示列数",
@@ -1238,10 +1256,16 @@
"basic_auth.user_name.tip": "空欄で無効化",
"basic_auth.password": "パスワード",
"basic_auth.password.tip": "",
"charge": "充電",
"charge": "残高充電",
"bills": "費用帳單",
"check": "チェック",
"check_all_keys": "すべてのキーをチェック",
"check_multiple_keys": "複数のAPIキーをチェック",
"oauth": {
"button": "{{provider}} アカウントでログイン",
"description": "本サービスは<website>{{provider}}</website>によって提供されます",
"official_website": "公式サイト"
},
"copilot": {
"auth_failed": "Github Copilotの認証に失敗しました。",
"auth_success": "Github Copilotの認証が成功しました",

View File

@@ -445,7 +445,11 @@
"topN_tooltip": "Количество возвращаемых совпадений; чем больше значение, тем больше совпадений, но и потребление токенов тоже возрастает.",
"url_added": "URL добавлен",
"url_placeholder": "Введите URL, несколько URL через Enter",
"urls": "URL-адреса"
"urls": "URL-адреса",
"dimensions": "векторное пространство",
"dimensions_size_tooltip": "Размерность вложения, чем больше значение, тем больше размерность вложения, но и потребляемых токенов также становится больше.",
"dimensions_size_placeholder": "Значение по умолчанию (не рекомендуется изменять)",
"dimensions_size_too_large": "Размерность вложения не может превышать ограничение контекста модели ({{max_context}})"
},
"languages": {
"arabic": "Арабский",
@@ -558,7 +562,9 @@
"tools": {
"completed": "Завершено",
"invoking": "Вызов",
"error": "Произошла ошибка"
"error": "Произошла ошибка",
"raw": "Исходный",
"preview": "Предпросмотр"
},
"topic.added": "Новый топик добавлен",
"upgrade.success.button": "Перезапустить",
@@ -579,7 +585,9 @@
"minimize": "Свернуть встроенное приложение",
"devtools": "Инструменты разработчика",
"openExternal": "Открыть в браузере",
"rightclick_copyurl": "ПКМ → Копировать URL"
"rightclick_copyurl": "ПКМ → Копировать URL",
"open_link_external_on": "Текущий: Открыть ссылки в браузере",
"open_link_external_off": "Текущий: Открыть ссылки в окне по умолчанию"
},
"sidebar.add.title": "Добавить в боковую панель",
"sidebar.remove.title": "Удалить из боковой панели",
@@ -1014,6 +1022,9 @@
"disabled": "Скрытые мини-приложения",
"empty": "Перетащите мини-приложения слева, чтобы скрыть их",
"visible": "Отображаемые мини-приложения",
"open_link_external": {
"title": "Открывать новые окна в браузере"
},
"cache_settings": "Настройки кэша",
"cache_title": "Количество кэшируемых мини-приложений",
"cache_description": "Установить максимальное количество активных мини-приложений в памяти",
@@ -1116,6 +1127,7 @@
"installHelp": "Получить помощь по установке",
"tabs": {
"general": "Общие",
"description": "Описание",
"tools": "Инструменты",
"prompts": "Подсказки",
"resources": "Ресурсы"
@@ -1151,7 +1163,13 @@
"registryDefault": "По умолчанию",
"not_support": "Модель не поддерживается",
"user": "Пользователь",
"system": "Система"
"system": "Система",
"types": {
"inMemory": "Встроенный",
"sse": "SSE",
"streamableHttp": "Потоковый HTTP",
"stdio": "STDIO"
}
},
"messages.divider": "Показывать разделитель между сообщениями",
"messages.grid_columns": "Количество столбцов сетки сообщений",
@@ -1238,10 +1256,16 @@
"basic_auth.user_name.tip": "Оставить пустым для отключения",
"basic_auth.password": "Пароль",
"basic_auth.password.tip": "",
"charge": "Пополнить",
"charge": "Пополнить баланс",
"bills": "Счета за услуги",
"check": "Проверить",
"check_all_keys": "Проверить все ключи",
"check_multiple_keys": "Проверить несколько ключей API",
"oauth": {
"button": "Войти с {{provider}}",
"description": "Сервис предоставляется <website>{{provider}}</website>",
"official_website": "Официальный сайт"
},
"copilot": {
"auth_failed": "Github Copilot认证失败",
"auth_success": "Github Copilot认证成功",

View File

@@ -399,6 +399,10 @@
"clear_selection": "清除选择",
"delete": "删除",
"delete_confirm": "确定要删除此知识库吗?",
"dimensions": "嵌入维度",
"dimensions_size_tooltip": "嵌入维度大小,数值越大,嵌入维度越大,但消耗的 Token 也越多",
"dimensions_size_placeholder": " 默认值(不建议修改)",
"dimensions_size_too_large": "嵌入维度不能超过模型上下文限制({{max_context}}",
"directories": "目录",
"directory_placeholder": "请输入目录路径",
"document_count": "请求文档片段数量",
@@ -558,7 +562,9 @@
"tools": {
"completed": "已完成",
"invoking": "调用中",
"error": "发生错误"
"error": "发生错误",
"raw": "原始",
"preview": "预览"
},
"topic.added": "话题添加成功",
"upgrade.success.button": "重启",
@@ -579,7 +585,9 @@
"minimize": "最小化小程序",
"devtools": "开发者工具",
"openExternal": "在浏览器中打开",
"rightclick_copyurl": "右键复制URL"
"rightclick_copyurl": "右键复制URL",
"open_link_external_on": "当前:在浏览器中打开链接",
"open_link_external_off": "当前:使用默认窗口打开链接"
},
"sidebar.add.title": "添加到侧边栏",
"sidebar.remove.title": "从侧边栏移除",
@@ -829,7 +837,7 @@
},
"joplin": {
"check": {
"button": "检",
"button": "检",
"empty_token": "请先输入 Joplin 授权令牌",
"empty_url": "请先输入 Joplin 剪裁服务监听 URL",
"fail": "Joplin 连接验证失败",
@@ -858,7 +866,7 @@
"notion.auto_split": "导出对话时自动分页",
"notion.auto_split_tip": "当要导出的话题过长时自动分页导出到Notion",
"notion.check": {
"button": "检",
"button": "检",
"empty_api_key": "未配置 API key",
"empty_database_id": "未配置 Database ID",
"error": "连接异常,请检查网络及 API key 和 Database ID 是否正确",
@@ -927,7 +935,7 @@
},
"yuque": {
"check": {
"button": "检",
"button": "检",
"empty_repo_url": "请先输入知识库URL",
"empty_token": "请先输入语雀Token",
"fail": "语雀连接验证失败",
@@ -961,8 +969,8 @@
"root_path": "文档根路径",
"root_path_placeholder": "例如:/CherryStudio",
"check": {
"title": "连接检",
"button": "检",
"title": "连接检",
"button": "检",
"empty_config": "请填写API地址和令牌",
"success": "连接成功",
"fail": "连接失败请检查API地址和令牌",
@@ -1014,6 +1022,9 @@
"disabled": "隐藏的小程序",
"empty": "把要隐藏的小程序从左侧拖拽到这里",
"visible": "显示的小程序",
"open_link_external": {
"title": "在浏览器中打开新窗口链接"
},
"cache_settings": "缓存设置",
"cache_title": "小程序缓存数量",
"cache_description": "设置同时保持活跃状态的小程序最大数量",
@@ -1117,6 +1128,7 @@
"installHelp": "获取安装帮助",
"tabs": {
"general": "通用",
"description": "描述",
"tools": "工具",
"prompts": "提示",
"resources": "资源"
@@ -1152,7 +1164,13 @@
"registryDefault": "默认",
"not_support": "模型不支持",
"user": "用户",
"system": "系统"
"system": "系统",
"types": {
"inMemory": "内置",
"sse": "SSE",
"streamableHttp": "流式",
"stdio": "STDIO"
}
},
"messages.divider": "消息分割线",
"messages.grid_columns": "消息网格展示列数",
@@ -1188,20 +1206,20 @@
"models.add.model_name": "模型名称",
"models.add.model_name.placeholder": "例如 GPT-3.5",
"models.check.all": "所有",
"models.check.all_models_passed": "所有模型检通过",
"models.check.button_caption": "健康检",
"models.check.all_models_passed": "所有模型检通过",
"models.check.button_caption": "健康检",
"models.check.disabled": "关闭",
"models.check.enable_concurrent": "并发检",
"models.check.enable_concurrent": "并发检",
"models.check.enabled": "开启",
"models.check.failed": "失败",
"models.check.keys_status_count": "通过:{{count_passed}}个密钥,失败:{{count_failed}}个密钥",
"models.check.model_status_summary": "{{provider}}: {{count_passed}} 个模型完成健康检(其中 {{count_partial}} 个模型用某些密钥无法访问),{{count_failed}} 个模型完全无法访问。",
"models.check.model_status_summary": "{{provider}}: {{count_passed}} 个模型完成健康检(其中 {{count_partial}} 个模型用某些密钥无法访问),{{count_failed}} 个模型完全无法访问。",
"models.check.no_api_keys": "未找到API密钥请先添加API密钥。",
"models.check.passed": "通过",
"models.check.select_api_key": "选择要使用的API密钥",
"models.check.single": "单个",
"models.check.start": "开始",
"models.check.title": "模型健康检",
"models.check.title": "模型健康检",
"models.check.use_all_keys": "使用密钥",
"models.default_assistant_model": "默认助手模型",
"models.default_assistant_model_description": "创建新助手时使用的模型,如果助手未设置模型,则使用此模型",
@@ -1239,10 +1257,16 @@
"basic_auth.user_name.tip": "留空以禁用",
"basic_auth.password": "密码",
"basic_auth.password.tip": "",
"charge": "充值",
"check": "检查",
"check_all_keys": "检查所有密钥",
"check_multiple_keys": "检查多个 API 密钥",
"charge": "余额充值",
"bills": "费用账单",
"check": "检",
"check_all_keys": "检测所有密钥",
"check_multiple_keys": "检测多个 API 密钥",
"oauth": {
"button": "使用{{provider}}账号登录",
"description": "本服务由<website>{{provider}}</website>提供",
"official_website": "官方网站"
},
"copilot": {
"auth_failed": "Github Copilot 认证失败",
"auth_success": "Github Copilot 认证成功",
@@ -1273,8 +1297,8 @@
"docs_more_details": "获取更多详情",
"get_api_key": "点击这里获取密钥",
"is_not_support_array_content": "开启兼容模式",
"no_models_for_check": "没有可以被检的模型(例如对话模型)",
"not_checked": "未检",
"no_models_for_check": "没有可以被检的模型(例如对话模型)",
"not_checked": "未检",
"remove_duplicate_keys": "移除重复密钥",
"remove_invalid_keys": "删除无效密钥",
"search": "搜索模型平台...",
@@ -1340,13 +1364,13 @@
"blacklist": "黑名单",
"blacklist_description": "在搜索结果中不会出现以下网站的结果",
"blacklist_tooltip": "请使用以下格式(换行分隔)\n匹配模式: *://*.example.com/*\n正则表达式: /example\\.(net|org)/",
"check": "检",
"check": "检",
"check_failed": "验证失败",
"check_success": "验证成功",
"overwrite": "覆盖服务商搜索",
"overwrite_tooltip": "强制使用搜索服务商而不是大语言模型进行搜索",
"get_api_key": "点击这里获取密钥",
"no_provider_selected": "请选择搜索服务商后再检",
"no_provider_selected": "请选择搜索服务商后再检",
"search_max_result": "搜索结果个数",
"search_provider": "搜索服务商",
"search_provider_placeholder": "选择一个搜索服务商",

View File

@@ -445,7 +445,11 @@
"topN_tooltip": "返回的匹配結果數量,數值越大,匹配結果越多,但消耗的 Token 也越多",
"url_added": "網址已新增",
"url_placeholder": "請輸入網址,多個網址用換行符號分隔",
"urls": "網址"
"urls": "網址",
"dimensions": "嵌入維度",
"dimensions_size_tooltip": "嵌入維度大小,數值越大,嵌入維度越大,但消耗的 Token 也越多",
"dimensions_size_placeholder": "預設值(不建議修改)",
"dimensions_size_too_large": "嵌入維度不能超過模型上下文限制({{max_context}}"
},
"languages": {
"arabic": "阿拉伯文",
@@ -558,7 +562,9 @@
"tools": {
"completed": "已完成",
"invoking": "調用中",
"error": "發生錯誤"
"error": "發生錯誤",
"raw": "原始碼",
"preview": "預覽"
},
"topic.added": "新話題已新增",
"upgrade.success.button": "重新啟動",
@@ -579,7 +585,9 @@
"minimize": "最小化小工具",
"devtools": "開發者工具",
"openExternal": "在瀏覽器中開啟",
"rightclick_copyurl": "右鍵複製URL"
"rightclick_copyurl": "右鍵複製URL",
"open_link_external_on": "当前:在瀏覽器中開啟連結",
"open_link_external_off": "当前:使用預設視窗開啟連結"
},
"sidebar.add.title": "新增到側邊欄",
"sidebar.remove.title": "從側邊欄移除",
@@ -1014,6 +1022,9 @@
"disabled": "隱藏的小程式",
"empty": "把要隱藏的小程式從左側拖拽到這裡",
"visible": "顯示的小程式",
"open_link_external": {
"title": "在瀏覽器中打開新視窗連結"
},
"cache_settings": "緩存設置",
"cache_title": "小程式緩存數量",
"cache_description": "設置同時保持活躍狀態的小程式最大數量",
@@ -1116,6 +1127,7 @@
"installHelp": "獲取安裝幫助",
"tabs": {
"general": "通用",
"description": "描述",
"tools": "工具",
"prompts": "提示",
"resources": "資源"
@@ -1151,7 +1163,13 @@
"registryDefault": "預設",
"not_support": "不支援此模型",
"user": "用戶",
"system": "系統"
"system": "系統",
"types": {
"inMemory": "內置",
"sse": "SSE",
"streamableHttp": "流式",
"stdio": "STDIO"
}
},
"messages.divider": "訊息間顯示分隔線",
"messages.grid_columns": "訊息網格展示列數",
@@ -1238,10 +1256,16 @@
"basic_auth.user_name.tip": "留空以停用",
"basic_auth.password": "密碼",
"basic_auth.password.tip": "",
"charge": "值",
"charge": "餘額充值",
"bills": "費用帳單",
"check": "檢查",
"check_all_keys": "檢查所有金鑰",
"check_multiple_keys": "檢查多個 API 金鑰",
"oauth": {
"button": "使用{{provider}}帳號登入",
"description": "本服務由<website>{{provider}}</website>提供",
"official_website": "官方網站"
},
"copilot": {
"auth_failed": "Github Copilot認證失敗",
"auth_success": "Github Copilot 認證成功",

View File

@@ -503,7 +503,9 @@
"switch.disabled": "Παρακαλείστε να περιμένετε τη λήξη της τρέχουσας απάντησης",
"tools": {
"completed": "Ολοκληρώθηκε",
"invoking": "κλήση σε εξέλιξη"
"invoking": "κλήση σε εξέλιξη",
"raw": "Ακατέργαστο",
"preview": "Προεπισκόπηση"
},
"topic.added": "Η θεματική προστέθηκε επιτυχώς",
"upgrade.success.button": "Επανεκκίνηση",

View File

@@ -503,7 +503,9 @@
"switch.disabled": "Espere a que se complete la respuesta actual antes de realizar la operación",
"tools": {
"completed": "Completado",
"invoking": "En llamada"
"invoking": "En llamada",
"raw": "Crudo",
"preview": "Vista previa"
},
"topic.added": "Tema agregado con éxito",
"upgrade.success.button": "Reiniciar",

View File

@@ -503,7 +503,9 @@
"switch.disabled": "Veuillez attendre la fin de la réponse actuelle avant de procéder",
"tools": {
"completed": "Terminé",
"invoking": "En cours d'exécution"
"invoking": "En cours d'exécution",
"raw": "Brut",
"preview": "Aperçu"
},
"topic.added": "Thème ajouté avec succès",
"upgrade.success.button": "Redémarrer",

View File

@@ -503,7 +503,9 @@
"switch.disabled": "Aguarde a conclusão da resposta atual antes de operar",
"tools": {
"completed": "Completo",
"invoking": "Em execução"
"invoking": "Em execução",
"raw": "Bruto",
"preview": "Pré-visualização"
},
"topic.added": "Tópico adicionado com sucesso",
"upgrade.success.button": "Reiniciar",

View File

@@ -1,6 +1,4 @@
import KeyvStorage from '@kangfenmao/keyv-storage'
import * as Sentry from '@sentry/electron/renderer'
import { init as reactInit } from '@sentry/react'
import { startAutoSync } from './services/BackupService'
import { startNutstoreAutoSync } from './services/NutstoreService'
@@ -31,17 +29,6 @@ function initAutoSync() {
}, 8000)
}
export function initSentry() {
Sentry.init(
{
sendDefaultPii: true,
tracesSampleRate: 1.0,
integrations: [Sentry.browserTracingIntegration()]
},
reactInit as any
)
}
initSpinner()
initKeyv()
initAutoSync()

View File

@@ -183,7 +183,7 @@ const FilesPage: FC = () => {
{dataSource && dataSource?.length > 0 ? (
<FileList id={fileType} list={dataSource} files={sortedFiles} />
) : (
<Empty />
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</MainContent>
</ContentContainer>

View File

@@ -54,7 +54,7 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
<Divider style={{ margin: '8px auto 15px' }} variant="dashed" />
</div>
))}
{isEmpty && <Empty />}
{isEmpty && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
{!isEmpty && (
<HStack justifyContent="center">
<Button onClick={() => onContinueChat(topic)} icon={<MessageOutlined />}>

View File

@@ -32,6 +32,7 @@ const Mermaid: React.FC<Props> = ({ chart }) => {
}
}, [chart, theme])
// eslint-disable-next-line react-hooks/exhaustive-deps
const renderMermaid = useCallback(debounce(renderMermaidBase, 1000), [renderMermaidBase])
useEffect(() => {

View File

@@ -19,6 +19,17 @@ interface Props {
message: Message
}
const StyledUpload = styled(Upload)`
.ant-upload-list-item-name {
max-width: 220px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
`
const MessageAttachments: FC<Props> = ({ message }) => {
const handleCopyImage = async (image: FileType) => {
const data = await FileManager.readFile(image)
@@ -66,17 +77,6 @@ const MessageAttachments: FC<Props> = ({ message }) => {
)
}
const StyledUpload = styled(Upload)`
.ant-upload-list-item-name {
max-width: 220px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
`
return (
<Container style={{ marginTop: 2, marginBottom: 8 }} className="message-attachments">
<StyledUpload

View File

@@ -62,12 +62,16 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
)
// 获取引用数据
// https://github.com/CherryHQ/cherry-studio/issues/5234#issuecomment-2824704499
const citationsData = useMemo(() => {
const citationUrls =
Array.isArray(message.metadata?.citations) &&
(message?.metadata?.annotations?.map((annotation) => annotation.url_citation) ?? [])
const searchResults =
message?.metadata?.webSearch?.results ||
message?.metadata?.webSearchInfo ||
message?.metadata?.groundingMetadata?.groundingChunks?.map((chunk) => chunk?.web) ||
message?.metadata?.annotations?.map((annotation) => annotation.url_citation) ||
citationUrls ||
[]
// 使用对象而不是 Map 来提高性能

View File

@@ -1,7 +1,7 @@
import { CheckOutlined, ExpandOutlined, LoadingOutlined, WarningOutlined } from '@ant-design/icons'
import { useSettings } from '@renderer/hooks/useSettings'
import { Message } from '@renderer/types'
import { Collapse, message as antdMessage, Modal, Tooltip } from 'antd'
import { Collapse, message as antdMessage, Modal, Tabs, Tooltip } from 'antd'
import { isEmpty } from 'lodash'
import { FC, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -116,6 +116,24 @@ const MessageTools: FC<Props> = ({ message }) => {
return items
}
const renderPreview = (content: string) => {
if (!content) return null
try {
const parsedResult = JSON.parse(content)
switch (parsedResult.content[0]?.type) {
case 'text':
return <PreviewBlock>{parsedResult.content[0].text}</PreviewBlock>
// TODO: support other types
default:
return <PreviewBlock>{content}</PreviewBlock>
}
} catch (e) {
console.error('failed to render the preview of mcp results:', e)
return <PreviewBlock>{content}</PreviewBlock>
}
}
return (
<>
<CollapseContainer
@@ -139,18 +157,42 @@ const MessageTools: FC<Props> = ({ message }) => {
styles={{ body: { maxHeight: '80vh', overflow: 'auto' } }}>
{expandedResponse && (
<ExpandedResponseContainer style={{ fontFamily, fontSize }}>
<ActionButton
className="copy-expanded-button"
onClick={() => {
if (expandedResponse) {
navigator.clipboard.writeText(expandedResponse.content)
antdMessage.success({ content: t('message.copied'), key: 'copy-expanded' })
{/* mode swtich tabs */}
<Tabs
tabBarExtraContent={
<ActionButton
className="copy-expanded-button"
onClick={() => {
navigator.clipboard.writeText(
typeof expandedResponse.content === 'string'
? expandedResponse.content
: JSON.stringify(expandedResponse.content, null, 2)
)
antdMessage.success({ content: t('message.copied'), key: 'copy-expanded' })
}}
aria-label={t('common.copy')}>
<i className="iconfont icon-copy"></i>
</ActionButton>
}
items={[
{
key: 'preview',
label: t('message.tools.preview'),
children: renderPreview(expandedResponse.content)
},
{
key: 'raw',
label: t('message.tools.raw'),
children: (
<CodeBlock>
{typeof expandedResponse.content === 'string'
? expandedResponse.content
: JSON.stringify(expandedResponse.content, null, 2)}
</CodeBlock>
)
}
}}
aria-label={t('common.copy')}>
<i className="iconfont icon-copy"></i>
</ActionButton>
<CodeBlock>{expandedResponse.content}</CodeBlock>
]}
/>
</ExpandedResponseContainer>
)}
</Modal>
@@ -267,6 +309,14 @@ const ToolResponseContainer = styled.div`
position: relative;
`
const PreviewBlock = styled.div`
margin: 0;
white-space: pre-wrap;
word-break: break-word;
color: var(--color-text);
user-select: text;
`
const CodeBlock = styled.pre`
margin: 0;
white-space: pre-wrap;

View File

@@ -23,6 +23,7 @@ interface FormData {
name: string
model: string
documentCount?: number
dimensions?: number
chunkSize?: number
chunkOverlap?: number
threshold?: number
@@ -87,6 +88,7 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
...base,
name: values.name,
documentCount: values.documentCount || DEFAULT_KNOWLEDGE_DOCUMENT_COUNT,
dimensions: values.dimensions || base.dimensions,
chunkSize: values.chunkSize,
chunkOverlap: values.chunkOverlap,
threshold: values.threshold ?? undefined,
@@ -185,6 +187,32 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
</AdvancedSettingsButton>
<div style={{ display: showAdvanced ? 'block' : 'none' }}>
<Form.Item
name="dimensions"
label={t('knowledge.dimensions')}
layout="horizontal"
initialValue={base.dimensions}
tooltip={{ title: t('knowledge.dimensions_size_tooltip') }}
rules={[
{
validator(_, value) {
const maxContext = getEmbeddingMaxContext(base.model.id)
if (value && maxContext && value > maxContext) {
return Promise.reject(
new Error(t('knowledge.dimensions_size_too_large', { max_context: maxContext }))
)
}
return Promise.resolve()
}
}
]}>
<InputNumber
style={{ width: '100%' }}
defaultValue={base.dimensions}
placeholder={t('knowledge.dimensions_size_placeholder')}
disabled={base.model.id !== 'voyage-3-large'}
/>
</Form.Item>
<Form.Item
name="chunkSize"
label={t('knowledge.chunk_size')}

View File

@@ -1,7 +1,6 @@
import { GithubOutlined } from '@ant-design/icons'
import IndicatorLight from '@renderer/components/IndicatorLight'
import { HStack } from '@renderer/components/Layout'
import { isWindows } from '@renderer/config/constant'
import { APP_NAME, AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
@@ -34,13 +33,6 @@ const AboutSettings: FC = () => {
const onCheckUpdate = debounce(
async () => {
const { arch } = await window.api.getAppInfo()
if (isWindows && arch.includes('arm')) {
window.open('https://cherry-ai.com/download', '_blank')
return
}
if (update.checking || update.downloading) {
return
}

View File

@@ -99,6 +99,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
<Alert
type={isUvInstalled ? 'success' : 'warning'}
banner
style={{ borderRadius: 'var(--list-item-border-radius)' }}
description={
<VStack>
<SettingRow style={{ width: '100%' }}>
@@ -129,6 +130,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
<Alert
type={isBunInstalled ? 'success' : 'warning'}
banner
style={{ borderRadius: 'var(--list-item-border-radius)' }}
description={
<VStack>
<SettingRow style={{ width: '100%' }}>
@@ -170,6 +172,7 @@ const Container = styled.div`
flex-direction: column;
margin-bottom: 20px;
gap: 12px;
padding-top: 50px;
`
export default InstallNpxUv

View File

@@ -0,0 +1,50 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { getShikiInstance } from '@renderer/utils/shiki'
import { Card } from 'antd'
import MarkdownIt from 'markdown-it'
import { npxFinder } from 'npx-scope-finder'
import { useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
interface McpDescriptionProps {
searchKey: string
}
const MCPDescription = ({ searchKey }: McpDescriptionProps) => {
const [renderedMarkdown, setRenderedMarkdown] = useState('')
const [loading, setLoading] = useState(false)
const md = useRef<MarkdownIt>(
new MarkdownIt({
linkify: true, // 自动转换 URL 为链接
typographer: true // 启用印刷格式优化
})
)
const { theme } = useTheme()
const getMcpInfo = useCallback(async () => {
setLoading(true)
const packages = await npxFinder(searchKey).finally(() => setLoading(false))
const readme = packages[0]?.original?.readme ?? '暂无描述'
setRenderedMarkdown(md.current.render(readme))
}, [md, searchKey])
useEffect(() => {
const sk = getShikiInstance(theme)
md.current.use(sk)
getMcpInfo()
}, [getMcpInfo, theme])
return (
<Section>
<Card loading={loading}>
<div className="markdown" dangerouslySetInnerHTML={{ __html: renderedMarkdown }} />
</Card>
</Section>
)
}
const Section = styled.div`
padding-top: 8px;
`
export default MCPDescription

View File

@@ -1,26 +1,22 @@
import { CodeOutlined, PlusOutlined } from '@ant-design/icons'
import { EditOutlined } from '@ant-design/icons'
import { nanoid } from '@reduxjs/toolkit'
import DragableList from '@renderer/components/DragableList'
import IndicatorLight from '@renderer/components/IndicatorLight'
import { HStack, VStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { MCPServer } from '@renderer/types'
import { Button, Empty, Tag } from 'antd'
import { MonitorCheck, Plus, Settings2 } from 'lucide-react'
import { FC, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingTitle } from '..'
import McpSettings from './McpSettings'
interface Props {
selectedMcpServer: MCPServer | null
setSelectedMcpServer: (server: MCPServer | null) => void
}
const McpServersList: FC<Props> = ({ selectedMcpServer, setSelectedMcpServer }) => {
import EditMcpJsonPopup from './EditMcpJsonPopup'
const McpServersList: FC = () => {
const { mcpServers, addMCPServer, updateMcpServers } = useMCPServers()
const { t } = useTranslation()
const navigate = useNavigate()
const onAddMcpServer = useCallback(async () => {
const newServer = {
@@ -33,78 +29,81 @@ const McpServersList: FC<Props> = ({ selectedMcpServer, setSelectedMcpServer })
env: {},
isActive: false
}
addMCPServer(newServer)
await addMCPServer(newServer)
navigate(`/settings/mcp/settings`, { state: { server: newServer } })
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' })
setSelectedMcpServer(newServer)
}, [addMCPServer, setSelectedMcpServer, t])
}, [addMCPServer, navigate, t])
return (
<Container>
<ServersList>
<ListHeader>
<SettingTitle>{t('settings.mcp.newServer')}</SettingTitle>
</ListHeader>
<AddServerCard onClick={onAddMcpServer}>
<PlusOutlined style={{ fontSize: 24 }} />
<AddServerText>{t('settings.mcp.addServer')}</AddServerText>
</AddServerCard>
<DragableList list={mcpServers} onUpdate={updateMcpServers}>
{(server) => (
<ServerCard
key={server.id}
onClick={() => setSelectedMcpServer(server)}
className={selectedMcpServer?.id === server.id ? 'active' : ''}>
<ServerHeader>
<ListHeader>
<SettingTitle style={{ gap: 3 }}>
<span>{t('settings.mcp.newServer')}</span>
<Button icon={<EditOutlined />} type="text" onClick={() => EditMcpJsonPopup.show()} shape="circle" />
</SettingTitle>
<Button icon={<Plus size={16} />} type="default" onClick={onAddMcpServer} shape="round">
{t('settings.mcp.addServer')}
</Button>
</ListHeader>
<DragableList style={{ width: '100%' }} list={mcpServers} onUpdate={updateMcpServers}>
{(server: MCPServer) => (
<ServerCard key={server.id} onClick={() => navigate(`/settings/mcp/settings`, { state: { server } })}>
<ServerHeader>
<ServerName>
<ServerNameText>{server.name}</ServerNameText>
<ServerIcon>
<CodeOutlined />
<MonitorCheck size={16} color={server.isActive ? 'var(--color-primary)' : 'var(--color-text-3)'} />
</ServerIcon>
<ServerName>{server.name}</ServerName>
<StatusIndicator>
<IndicatorLight
size={6}
color={server.isActive ? 'green' : 'var(--color-text-3)'}
animation={server.isActive}
shadow={false}
/>
</StatusIndicator>
</ServerHeader>
<ServerDescription>{server.description}</ServerDescription>
</ServerCard>
)}
</DragableList>
</ServersList>
<ServerSettings>{selectedMcpServer && <McpSettings server={selectedMcpServer} />}</ServerSettings>
</ServerName>
<StatusIndicator>
<Button
icon={<Settings2 size={16} />}
type="text"
onClick={() => navigate(`/settings/mcp/settings`, { state: { server } })}
/>
</StatusIndicator>
</ServerHeader>
<ServerDescription>{server.description}</ServerDescription>
<ServerFooter>
<Tag color="default" style={{ borderRadius: 20, margin: 0 }}>
{t(`settings.mcp.types.${server.type || 'stdio'}`)}
</Tag>
</ServerFooter>
</ServerCard>
)}
</DragableList>
{mcpServers.length === 0 && (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={t('settings.mcp.noServers')}
style={{ marginTop: 20 }}
/>
)}
</Container>
)
}
const Container = styled(HStack)`
const Container = styled(Scrollbar)`
display: flex;
flex: 1;
width: 350px;
flex-direction: column;
width: 100%;
height: calc(100vh - var(--navbar-height));
overflow: hidden;
`
const ServersList = styled(Scrollbar)`
gap: 16px;
display: flex;
flex-direction: column;
height: calc(100vh - var(--navbar-height));
width: 350px;
padding: 15px;
border-right: 0.5px solid var(--color-border);
`
const ServerSettings = styled(VStack)`
flex: 1;
height: calc(100vh - var(--navbar-height));
padding: 20px;
padding-top: 15px;
gap: 15px;
overflow-y: auto;
`
const ListHeader = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
h2 {
font-size: 20px;
font-size: 22px;
margin: 0;
}
`
@@ -112,19 +111,17 @@ const ListHeader = styled.div`
const ServerCard = styled.div`
display: flex;
flex-direction: column;
border: 1px solid var(--color-border);
border-radius: 8px;
border: 0.5px solid var(--color-border);
border-radius: var(--list-item-border-radius);
padding: 10px 16px;
cursor: pointer;
transition: all 0.2s ease;
height: 120px;
background-color: var(--color-background);
margin-bottom: 5px;
height: 125px;
cursor: pointer;
&:hover,
&.active {
&:hover {
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
`
@@ -136,16 +133,24 @@ const ServerHeader = styled.div`
const ServerIcon = styled.div`
font-size: 18px;
color: var(--color-primary);
margin-right: 8px;
display: flex;
`
const ServerName = styled.div`
font-weight: 500;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 10px;
`
const ServerNameText = styled.span`
font-size: 15px;
font-weight: 500;
font-family: Ubuntu;
`
const StatusIndicator = styled.div`
@@ -161,21 +166,14 @@ const ServerDescription = styled.div`
-webkit-box-orient: vertical;
width: 100%;
word-break: break-word;
height: 50px;
`
const AddServerCard = styled(ServerCard)`
const ServerFooter = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-style: dashed;
background-color: transparent;
color: var(--color-text-2);
`
const AddServerText = styled.div`
margin-top: 12px;
font-weight: 500;
justify-content: space-between;
margin-top: 10px;
`
export default McpServersList

View File

@@ -1,11 +1,13 @@
import { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription'
import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
import { Button, Flex, Form, Input, Radio, Switch, Tabs } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import { useLocation, useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
@@ -13,10 +15,6 @@ import MCPPromptsSection from './McpPrompt'
import MCPResourcesSection from './McpResource'
import MCPToolsSection from './McpTool'
interface Props {
server: MCPServer
}
interface MCPFormValues {
name: string
description?: string
@@ -61,8 +59,9 @@ const parseKeyValueString = (str: string): Record<string, string> => {
return result
}
const McpSettings: React.FC<Props> = ({ server }) => {
const McpSettings: React.FC = () => {
const { t } = useTranslation()
const { server } = useLocation().state as { server: MCPServer }
const { deleteMCPServer, updateMCPServer } = useMCPServers()
const [serverType, setServerType] = useState<MCPServer['type']>('stdio')
const [form] = Form.useForm<MCPFormValues>()
@@ -77,6 +76,8 @@ const McpSettings: React.FC<Props> = ({ server }) => {
const [isShowRegistry, setIsShowRegistry] = useState(false)
const [registry, setRegistry] = useState<Registry[]>()
const { theme } = useTheme()
const navigate = useNavigate()
useEffect(() => {
@@ -516,6 +517,13 @@ const McpSettings: React.FC<Props> = ({ server }) => {
)
}
]
if (server.searchKey) {
tabs.push({
key: 'description',
label: t('settings.mcp.tabs.description'),
children: <MCPDescription searchKey={server.searchKey} />
})
}
if (server.isActive) {
tabs.push(
@@ -538,8 +546,8 @@ const McpSettings: React.FC<Props> = ({ server }) => {
}
return (
<SettingContainer style={{ width: '100%' }}>
<SettingGroup style={{ marginBottom: 0 }}>
<SettingContainer theme={theme} style={{ width: '100%', paddingTop: 55, backgroundColor: 'transparent' }}>
<SettingGroup style={{ marginBottom: 0, borderRadius: 'var(--list-item-border-radius)' }}>
<SettingTitle>
<Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}>
<ServerName className="text-nowrap">{server?.name}</ServerName>
@@ -557,18 +565,18 @@ const McpSettings: React.FC<Props> = ({ server }) => {
icon={<SaveOutlined />}
onClick={onSave}
loading={loading}
shape="round"
disabled={!isFormChanged || activeTab !== 'settings'}>
{t('common.save')}
</Button>
</Flex>
</SettingTitle>
<SettingDivider />
<Tabs
defaultActiveKey="settings"
items={tabs}
onChange={(key) => setActiveTab(key as TabKey)}
style={{ marginTop: 8 }}
style={{ marginTop: 8, backgroundColor: 'transparent' }}
/>
</SettingGroup>
</SettingContainer>

View File

@@ -1,13 +1,11 @@
import { EditOutlined, ExportOutlined } from '@ant-design/icons'
import { NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import { isWindows } from '@renderer/config/constant'
import { Button } from 'antd'
import { Search } from 'lucide-react'
import { Search, SquareArrowOutUpRight } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import EditMcpJsonPopup from './EditMcpJsonPopup'
import InstallNpxUv from './InstallNpxUv'
export const McpSettingsNavbar = () => {
@@ -27,20 +25,11 @@ export const McpSettingsNavbar = () => {
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
{t('settings.mcp.searchNpx')}
</Button>
<Button
size="small"
type="text"
onClick={() => EditMcpJsonPopup.show()}
icon={<EditOutlined />}
className="nodrag"
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
{t('settings.mcp.editMcpJson')}
</Button>
<Button
size="small"
type="text"
onClick={onClick}
icon={<ExportOutlined />}
icon={<SquareArrowOutUpRight size={14} />}
className="nodrag"
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
{t('settings.mcp.findMore')}

View File

@@ -1,6 +1,6 @@
import { CheckOutlined, PlusOutlined } from '@ant-design/icons'
import { nanoid } from '@reduxjs/toolkit'
import npmLogo from '@renderer/assets/images/mcp/npm.svg'
import logo from '@renderer/assets/images/cherry-text-logo.svg'
import { Center, HStack } from '@renderer/components/Layout'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { builtinMCPServers } from '@renderer/store/mcp'
@@ -27,9 +27,7 @@ const npmScopes = ['@cherry', '@modelcontextprotocol', '@gongrzhe', '@mcpmarket'
let _searchResults: SearchResult[] = []
const NpxSearch: FC<{
setSelectedMcpServer: (server: MCPServer) => void
}> = ({ setSelectedMcpServer }) => {
const NpxSearch: FC = () => {
const { t } = useTranslation()
const { Text, Link } = Typography
@@ -126,7 +124,7 @@ const NpxSearch: FC<{
<Center>
<Space direction="vertical" style={{ marginBottom: 25, width: 500 }}>
<Center style={{ marginBottom: 15 }}>
<img src={npmLogo} alt="npm" width={100} />
<img src={logo} alt="npm" width={120} />
</Center>
<Space.Compact style={{ width: '100%' }}>
<Input
@@ -142,7 +140,6 @@ const NpxSearch: FC<{
{npmScopes.map((scope) => (
<Tag
key={scope}
bordered={false}
onClick={() => {
setNpmScope(scope)
handleNpmSearch(scope)
@@ -171,6 +168,7 @@ const NpxSearch: FC<{
<Card
size="small"
key={record.name}
style={{ borderRadius: 'var(--list-item-border-radius)' }}
title={
<Typography.Title level={5} style={{ margin: 0 }} className="selectable">
{record.name}
@@ -178,7 +176,7 @@ const NpxSearch: FC<{
}
extra={
<Flex>
<Tag bordered={false} color="processing">
<Tag color="success" style={{ borderRadius: 100 }}>
v{record.version}
</Tag>
<Button
@@ -197,7 +195,6 @@ const NpxSearch: FC<{
if (buildInServer) {
addMCPServer(buildInServer)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' })
setSelectedMcpServer(buildInServer)
return
}
@@ -209,12 +206,12 @@ const NpxSearch: FC<{
args: record.configSample?.args ?? ['-y', record.fullName],
env: record.configSample?.env,
isActive: false,
type: record.type
type: record.type,
searchKey: record.fullName
}
addMCPServer(newServer)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' })
setSelectedMcpServer(newServer)
}}
/>
</Flex>
@@ -242,6 +239,7 @@ const Container = styled.div`
flex: 1;
flex-direction: column;
gap: 8px;
padding-top: 20px;
`
const ResultList = styled.div`

View File

@@ -1,10 +1,7 @@
import { ArrowLeftOutlined } from '@ant-design/icons'
import { VStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { MCPServer } from '@renderer/types'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from 'antd'
import { FC } from 'react'
import { Route, Routes, useLocation } from 'react-router'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
@@ -12,58 +9,35 @@ import styled from 'styled-components'
import { SettingContainer } from '..'
import InstallNpxUv from './InstallNpxUv'
import McpServersList from './McpServersList'
import McpSettings from './McpSettings'
import NpxSearch from './NpxSearch'
const MCPSettings: FC = () => {
const { t } = useTranslation()
const { mcpServers } = useMCPServers()
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(mcpServers[0])
const { theme } = useTheme()
const location = useLocation()
const pathname = location.pathname
useEffect(() => {
const _selectedMcpServer = mcpServers.find((server) => server.id === selectedMcpServer?.id)
setSelectedMcpServer(_selectedMcpServer || mcpServers[0])
}, [mcpServers, selectedMcpServer])
// Check if the selected server still exists in the updated mcpServers list
useEffect(() => {
if (selectedMcpServer) {
const serverExists = mcpServers.some((server) => server.id === selectedMcpServer.id)
if (!serverExists) {
setSelectedMcpServer(mcpServers[0])
}
}
}, [mcpServers, selectedMcpServer])
const isHome = pathname === '/settings/mcp'
return (
<Container>
<SettingContainer theme={theme} style={{ padding: 0, position: 'relative' }}>
{!isHome && (
<BackButtonContainer>
<Link to="/settings/mcp">
<BackButton>
<ArrowLeftOutlined /> {t('common.back')}
</BackButton>
<Button type="default" icon={<ArrowLeftOutlined />} shape="circle" />
</Link>
</BackButtonContainer>
)}
<MainContainer>
<Routes>
<Route
path="/"
element={
<McpServersList selectedMcpServer={selectedMcpServer} setSelectedMcpServer={setSelectedMcpServer} />
}
/>
<Route path="/" element={<McpServersList />} />
<Route path="settings" element={<McpSettings />} />
<Route
path="npx-search"
element={
<SettingContainer theme={theme}>
<NpxSearch setSelectedMcpServer={setSelectedMcpServer} />
<NpxSearch />
</SettingContainer>
}
/>
@@ -77,18 +51,20 @@ const MCPSettings: FC = () => {
/>
</Routes>
</MainContainer>
</Container>
</SettingContainer>
)
}
const Container = styled(VStack)`
flex: 1;
`
const BackButtonContainer = styled.div`
padding: 12px 0 0 12px;
width: 100%;
background-color: var(--color-background);
display: flex;
align-items: center;
padding: 10px 20px;
background-color: transparent;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 1000;
`
const MainContainer = styled.div`
@@ -97,21 +73,4 @@ const MainContainer = styled.div`
width: 100%;
`
const BackButton = styled.div`
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--color-text-1);
cursor: pointer;
padding: 6px 12px;
border-radius: 4px;
margin-bottom: 10px;
background-color: var(--color-bg-1);
&:hover {
color: var(--color-primary);
background-color: var(--color-bg-2);
}
`
export default MCPSettings

View File

@@ -112,7 +112,7 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
return (
<DragDropContext onDragEnd={onDragEnd}>
<ProgramSection>
<ProgramSection style={{ background: 'transparent' }}>
{(['visible', 'disabled'] as const).map((listType) => (
<ProgramColumn key={listType}>
<h4>{t(`settings.miniapps.${listType}`)}</h4>

View File

@@ -4,7 +4,11 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { setMaxKeepAliveMinapps, setShowOpenedMinappsInSidebar } from '@renderer/store/settings'
import {
setMaxKeepAliveMinapps,
setMinappsOpenLinkExternal,
setShowOpenedMinappsInSidebar
} from '@renderer/store/settings'
import { Button, message, Slider, Switch, Tooltip } from 'antd'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -20,7 +24,7 @@ const MiniAppSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { maxKeepAliveMinapps, showOpenedMinappsInSidebar } = useSettings()
const { maxKeepAliveMinapps, showOpenedMinappsInSidebar, minappsOpenLinkExternal } = useSettings()
const { minapps, disabled, updateMinapps, updateDisabledMinapps } = useMinapps()
const [visibleMiniApps, setVisibleMiniApps] = useState(minapps)
@@ -89,9 +93,19 @@ const MiniAppSettings: FC = () => {
/>
</BorderedContainer>
<SettingDivider />
<SettingRow style={{ height: 40, alignItems: 'center' }}>
<SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.open_link_external.title')}</SettingRowTitle>
</SettingLabelGroup>
<Switch
checked={minappsOpenLinkExternal}
onChange={(checked) => dispatch(setMinappsOpenLinkExternal(checked))}
/>
</SettingRow>
<SettingDivider />
{/* 缓存小程序数量设置 */}
<CacheSettingRow>
<SettingRow>
<SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.cache_title')}</SettingRowTitle>
<SettingDescription>{t('settings.miniapps.cache_description')}</SettingDescription>
@@ -117,9 +131,9 @@ const MiniAppSettings: FC = () => {
/>
</SliderWithResetContainer>
</CacheSettingControls>
</CacheSettingRow>
</SettingRow>
<SettingDivider />
<SidebarSettingRow>
<SettingRow>
<SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.sidebar_title')}</SettingRowTitle>
<SettingDescription>{t('settings.miniapps.sidebar_description')}</SettingDescription>
@@ -128,14 +142,14 @@ const MiniAppSettings: FC = () => {
checked={showOpenedMinappsInSidebar}
onChange={(checked) => dispatch(setShowOpenedMinappsInSidebar(checked))}
/>
</SidebarSettingRow>
</SettingRow>
</SettingGroup>
</SettingContainer>
)
}
// 修改和新增样式
const CacheSettingRow = styled.div`
const SettingRow = styled.div`
display: flex;
align-items: flex-start;
justify-content: space-between;
@@ -206,14 +220,6 @@ const ResetButtonWrapper = styled.div`
justify-content: center;
`
// 新增侧边栏设置行样式
const SidebarSettingRow = styled.div`
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
`
// 新增: 带边框的容器组件
const BorderedContainer = styled.div`
border: 1px solid var(--color-border);

View File

@@ -1,34 +0,0 @@
import { useOllamaSettings } from '@renderer/hooks/useOllama'
import { InputNumber } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '..'
const OllamSettings: FC = () => {
const { keepAliveTime, setKeepAliveTime } = useOllamaSettings()
const [keepAliveMinutes, setKeepAliveMinutes] = useState(keepAliveTime)
const { t } = useTranslation()
return (
<Container>
<SettingSubtitle style={{ marginBottom: 5 }}>{t('ollama.keep_alive_time.title')}</SettingSubtitle>
<InputNumber
style={{ width: '100%' }}
value={keepAliveMinutes}
onChange={(e) => setKeepAliveMinutes(Number(e))}
onBlur={() => setKeepAliveTime(keepAliveMinutes)}
suffix={t('ollama.keep_alive_time.placeholder')}
step={5}
/>
<SettingHelpTextRow>
<SettingHelpText>{t('ollama.keep_alive_time.description')}</SettingHelpText>
</SettingHelpTextRow>
</Container>
)
}
const Container = styled.div``
export default OllamSettings

View File

@@ -0,0 +1,92 @@
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
import { HStack } from '@renderer/components/Layout'
import OAuthButton from '@renderer/components/OAuth/OAuthButton'
import { PROVIDER_CONFIG } from '@renderer/config/providers'
import { Provider } from '@renderer/types'
import { providerBills, providerCharge } from '@renderer/utils/oauth'
import { Button } from 'antd'
import { isEmpty } from 'lodash'
import { ReceiptText } from 'lucide-react'
import { CircleDollarSign } from 'lucide-react'
import { FC } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
provider: Provider
setApiKey: (apiKey: string) => void
}
const PROVIDER_LOGO_MAP = {
silicon: SiliconFlowProviderLogo,
aihubmix: AiHubMixProviderLogo
}
const ProviderOAuth: FC<Props> = ({ provider, setApiKey }) => {
const { t } = useTranslation()
const providerWebsite =
PROVIDER_CONFIG[provider.id]?.api?.url.replace('https://', '').replace('api.', '') || provider.name
return (
<Container>
<ProviderLogo src={PROVIDER_LOGO_MAP[provider.id]} />
{isEmpty(provider.apiKey) ? (
<OAuthButton provider={provider} onSuccess={setApiKey}>
{t('settings.provider.oauth.button', { provider: t(`provider.${provider.id}`) })}
</OAuthButton>
) : (
<HStack gap={10}>
<Button shape="round" icon={<CircleDollarSign size={16} />} onClick={() => providerCharge(provider.id)}>
{t('settings.provider.charge')}
</Button>
<Button shape="round" icon={<ReceiptText size={16} />} onClick={() => providerBills(provider.id)}>
{t('settings.provider.bills')}
</Button>
</HStack>
)}
<Description>
<Trans
i18nKey="settings.provider.oauth.description"
components={{
website: (
<OfficialWebsite href={PROVIDER_CONFIG[provider.id].websites.official} target="_blank" rel="noreferrer" />
)
}}
values={{ provider: providerWebsite }}
/>
</Description>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 15px;
padding: 20px;
`
const ProviderLogo = styled.img`
width: 60px;
height: 60px;
border-radius: 50%;
`
const Description = styled.div`
font-size: 11px;
color: var(--color-text-2);
display: flex;
align-items: center;
gap: 5px;
`
const OfficialWebsite = styled.a`
text-decoration: none;
color: var(--color-text-2);
`
export default ProviderOAuth

View File

@@ -1,7 +1,6 @@
import { CheckOutlined, LoadingOutlined } from '@ant-design/icons'
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
import { HStack } from '@renderer/components/Layout'
import OAuthButton from '@renderer/components/OAuth/OAuthButton'
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
import { PROVIDER_CONFIG } from '@renderer/config/providers'
import { useTheme } from '@renderer/context/ThemeProvider'
@@ -10,10 +9,9 @@ import i18n from '@renderer/i18n'
import { isOpenAIProvider } from '@renderer/providers/AiProvider/ProviderFactory'
import { checkApi, formatApiKeys } from '@renderer/services/ApiService'
import { checkModelsHealth, ModelCheckStatus } from '@renderer/services/HealthCheckService'
import { isProviderSupportAuth, isProviderSupportCharge } from '@renderer/services/ProviderService'
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
import { Provider } from '@renderer/types'
import { formatApiHost } from '@renderer/utils/api'
import { providerCharge } from '@renderer/utils/oauth'
import { Button, Divider, Flex, Input, Space, Switch, Tooltip } from 'antd'
import Link from 'antd/es/typography/Link'
import { debounce, isEmpty } from 'lodash'
@@ -37,7 +35,7 @@ import HealthCheckPopup from './HealthCheckPopup'
import LMStudioSettings from './LMStudioSettings'
import ModelList, { ModelStatus } from './ModelList'
import ModelListSearchBar from './ModelListSearchBar'
import OllamSettings from './OllamaSettings'
import ProviderOAuth from './ProviderOAuth'
import ProviderSettingsPopup from './ProviderSettingsPopup'
import SelectProviderModelPopup from './SelectProviderModelPopup'
@@ -323,6 +321,16 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
/>
</SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} />
{isProviderSupportAuth(provider) && (
<ProviderOAuth
provider={provider}
setApiKey={(v) => {
setApiKey(v)
setInputValue(v)
updateProvider({ ...provider, apiKey: v })
}}
/>
)}
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input.Password
@@ -339,10 +347,9 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
onUpdateApiKey()
}}
spellCheck={false}
autoFocus={provider.enabled && apiKey === ''}
autoFocus={provider.enabled && apiKey === '' && !isProviderSupportAuth(provider)}
disabled={provider.id === 'copilot'}
/>
{isProviderSupportAuth(provider) && <OAuthButton provider={provider} onSuccess={setApiKey} />}
<Button
type={apiValid ? 'primary' : 'default'}
ghost={apiValid}
@@ -357,11 +364,6 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
{t('settings.provider.get_api_key')}
</SettingHelpLink>
{isProviderSupportCharge(provider) && (
<SettingHelpLink onClick={() => providerCharge(provider.id)}>
{t('settings.provider.charge')}
</SettingHelpLink>
)}
</HStack>
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
</SettingHelpTextRow>
@@ -402,7 +404,6 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
</Space.Compact>
</>
)}
{provider.id === 'ollama' && <OllamSettings />}
{provider.id === 'lmstudio' && <LMStudioSettings />}
{provider.id === 'gpustack' && <GPUStackSettings />}
{provider.id === 'copilot' && <GithubCopilotSettings provider={provider} setApiKey={setApiKey} />}

View File

@@ -60,10 +60,12 @@ const BasicSettings: FC = () => {
<Input
style={{ width: '100px' }}
placeholder="2000"
value={contentLimit}
value={contentLimit === undefined ? '' : contentLimit}
onChange={(e) => {
const value = e.target.value
if (!isNaN(Number(value)) && Number(value) > 0) {
if (value === '') {
dispatch(setContentLimit(undefined))
} else if (!isNaN(Number(value)) && Number(value) > 0) {
dispatch(setContentLimit(Number(value)))
}
}}

View File

@@ -86,7 +86,7 @@ export const SettingHelpLink = styled(Link)`
export const SettingGroup = styled.div<{ theme?: ThemeMode; css?: CSSProp }>`
margin-bottom: 20px;
border-radius: 8px;
border-radius: var(--list-item-border-radius);
border: 0.5px solid var(--color-border);
padding: 16px;
background: ${(props) => (props.theme === 'dark' ? '#00000010' : 'var(--color-background)')};

View File

@@ -507,6 +507,10 @@ export default class AnthropicProvider extends BaseProvider {
return []
}
public async generateImageByChat(): Promise<void> {
throw new Error('Method not implemented.')
}
/**
* Generate suggestions
* @returns The suggestions

View File

@@ -1,6 +1,5 @@
import { REFERENCE_PROMPT } from '@renderer/config/prompts'
import { getLMStudioKeepAliveTime } from '@renderer/hooks/useLMStudio'
import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama'
import type {
Assistant,
GenerateImageParams,
@@ -39,6 +38,7 @@ export default abstract class BaseProvider {
abstract check(model: Model): Promise<{ valid: boolean; error: Error | null }>
abstract models(): Promise<OpenAI.Models.Model[]>
abstract generateImage(params: GenerateImageParams): Promise<string[]>
abstract generateImageByChat({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void>
abstract getEmbeddingDimensions(model: Model): Promise<number>
public getBaseURL(): string {
@@ -77,11 +77,7 @@ export default abstract class BaseProvider {
}
public get keepAliveTime() {
return this.provider.id === 'ollama'
? getOllamaKeepAliveTime()
: this.provider.id === 'lmstudio'
? getLMStudioKeepAliveTime()
: undefined
return this.provider.id === 'lmstudio' ? getLMStudioKeepAliveTime() : undefined
}
public async fakeCompletions({ onChunk }: CompletionsParams) {
@@ -148,10 +144,10 @@ export default abstract class BaseProvider {
const knowledgeReferences: KnowledgeReference[] = window.keyv.get(`knowledge-search-${message.id}`)
if (!isEmpty(knowledgeReferences)) {
console.log(`Found ${knowledgeReferences.length} knowledge base references in cache for ID: ${message.id}`)
// console.log(`Found ${knowledgeReferences.length} knowledge base references in cache for ID: ${message.id}`)
return knowledgeReferences
}
console.log(`No knowledge base references found in cache for ID: ${message.id}`)
// console.log(`No knowledge base references found in cache for ID: ${message.id}`)
return []
}

View File

@@ -320,32 +320,12 @@ export default class GeminiProvider extends BaseProvider {
const start_time_millsec = new Date().getTime()
const { cleanup, abortController } = this.createAbortController(userLastMessage?.id, true)
const signalProxy = {
_originalSignal: abortController.signal,
addEventListener: (eventName: string, listener: () => void) => {
if (eventName === 'abort') {
abortController.signal.addEventListener('abort', listener)
}
},
removeEventListener: (eventName: string, listener: () => void) => {
if (eventName === 'abort') {
abortController.signal.removeEventListener('abort', listener)
}
},
get aborted() {
return abortController.signal.aborted
}
}
if (!streamOutput) {
const response = await chat.sendMessage({
message: messageContents as PartUnion,
config: {
...generateContentConfig,
httpOptions: {
signal: signalProxy as any
}
abortSignal: abortController.signal
}
})
const time_completion_millsec = new Date().getTime() - start_time_millsec
@@ -371,9 +351,7 @@ export default class GeminiProvider extends BaseProvider {
message: messageContents as PartUnion,
config: {
...generateContentConfig,
httpOptions: {
signal: signalProxy as any
}
abortSignal: abortController.signal
}
})
let time_first_token_millsec = 0
@@ -399,9 +377,7 @@ export default class GeminiProvider extends BaseProvider {
message: flatten(toolResults.map((ts) => (ts as Content).parts)) as PartUnion,
config: {
...generateContentConfig,
httpOptions: {
signal: signalProxy as any
}
abortSignal: abortController.signal
}
})
await processStream(newStream, idx + 1)
@@ -410,38 +386,43 @@ export default class GeminiProvider extends BaseProvider {
const processStream = async (stream: AsyncGenerator<GenerateContentResponse>, idx: number) => {
let content = ''
for await (const chunk of stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
try {
for await (const chunk of stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
if (time_first_token_millsec == 0) {
time_first_token_millsec = new Date().getTime() - start_time_millsec
if (time_first_token_millsec == 0) {
time_first_token_millsec = new Date().getTime() - start_time_millsec
}
const time_completion_millsec = new Date().getTime() - start_time_millsec
if (chunk.text !== undefined) {
content += chunk.text
}
await processToolUses(content, idx)
const generateImage = this.processGeminiImageResponse(chunk)
onChunk({
text: chunk.text !== undefined ? chunk.text : '',
usage: {
prompt_tokens: chunk.usageMetadata?.promptTokenCount || 0,
completion_tokens: chunk.usageMetadata?.candidatesTokenCount || 0,
thoughts_tokens: chunk.usageMetadata?.thoughtsTokenCount || 0,
total_tokens: chunk.usageMetadata?.totalTokenCount || 0
},
metrics: {
completion_tokens: chunk.usageMetadata?.candidatesTokenCount,
time_completion_millsec,
time_first_token_millsec
},
search: chunk.candidates?.[0]?.groundingMetadata,
mcpToolResponse: toolResponses,
generateImage: generateImage
})
}
const time_completion_millsec = new Date().getTime() - start_time_millsec
if (chunk.text !== undefined) {
content += chunk.text
}
await processToolUses(content, idx)
const generateImage = this.processGeminiImageResponse(chunk)
onChunk({
text: chunk.text !== undefined ? chunk.text : '',
usage: {
prompt_tokens: chunk.usageMetadata?.promptTokenCount || 0,
completion_tokens: chunk.usageMetadata?.candidatesTokenCount || 0,
thoughts_tokens: chunk.usageMetadata?.thoughtsTokenCount || 0,
total_tokens: chunk.usageMetadata?.totalTokenCount || 0
},
metrics: {
completion_tokens: chunk.usageMetadata?.candidatesTokenCount,
time_completion_millsec,
time_first_token_millsec
},
search: chunk.candidates?.[0]?.groundingMetadata,
mcpToolResponse: toolResponses,
generateImage: generateImage
})
} catch (error) {
console.error('Error processing stream chunk:', error)
throw error
}
}
@@ -737,4 +718,8 @@ export default class GeminiProvider extends BaseProvider {
})
return data.embeddings?.[0]?.values?.length || 0
}
public generateImageByChat(): Promise<void> {
throw new Error('Method not implemented.')
}
}

View File

@@ -307,6 +307,10 @@ export default class OpenAIProvider extends BaseProvider {
* @returns The completions
*/
async completions({ messages, assistant, mcpTools, onChunk, onFilterMessages }: CompletionsParams): Promise<void> {
if (assistant.enableGenerateImage) {
await this.generateImageByChat({ messages, assistant, onChunk } as CompletionsParams)
return
}
const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel
const { contextCount, maxTokens, streamOutput } = getAssistantSettings(assistant)
@@ -893,4 +897,30 @@ export default class OpenAIProvider extends BaseProvider {
const { token } = await window.api.copilot.getToken(defaultHeaders)
this.sdk.apiKey = token
}
public async generateImageByChat({ messages, assistant, onChunk }: CompletionsParams): Promise<void> {
const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel
const lastUserMessage = messages.findLast((m) => m.role === 'user')
const { abortController } = this.createAbortController(lastUserMessage?.id, true)
const { signal } = abortController
const response = await this.sdk.images.generate(
{
model: model.id,
prompt: lastUserMessage?.content || '',
response_format: model.id.includes('gpt-image-1') ? undefined : 'b64_json'
},
{
signal
}
)
return onChunk({
text: '',
generateImage: {
type: 'base64',
images: response.data.map((item) => `data:image/png;base64,${item.b64_json}`)
}
})
}
}

View File

@@ -109,6 +109,15 @@ export default class AiProvider {
return this.sdk.generateImage(params)
}
public async generateImageByChat({
messages,
assistant,
onChunk,
onFilterMessages
}: CompletionsParams): Promise<void> {
return this.sdk.generateImageByChat({ messages, assistant, onChunk, onFilterMessages })
}
public async getEmbeddingDimensions(model: Model): Promise<number> {
return this.sdk.getEmbeddingDimensions(model)
}

View File

@@ -1,6 +1,7 @@
import { SearxngClient } from '@agentic/searxng'
import { WebSearchState } from '@renderer/store/websearch'
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
import { WebSearchProvider, WebSearchResponse, WebSearchResult } from '@renderer/types'
import { fetchWebContent, noContent } from '@renderer/utils/fetch'
import axios from 'axios'
import ky from 'ky'
@@ -112,20 +113,28 @@ export default class SearxngProvider extends BaseWebSearchProvider {
if (!result || !Array.isArray(result.results)) {
throw new Error('Invalid search results from SearxNG')
}
return {
query: result.query,
results: result.results.slice(0, websearch.maxResults).map((result) => {
let content = result.content || ''
if (websearch.contentLimit && content.length > websearch.contentLimit) {
content = content.slice(0, websearch.contentLimit) + '...'
}
return {
title: result.title || 'No title',
content: content,
url: result.url || ''
}
})
const validItems = result.results
.filter((item) => item.url.startsWith('http') || item.url.startsWith('https'))
.slice(0, websearch.maxResults)
// console.log('Valid search items:', validItems)
// Fetch content for each URL concurrently
const fetchPromises = validItems.map(async (item) => {
// console.log(`Fetching content for ${item.url}...`)
const result = await fetchWebContent(item.url, 'markdown', this.provider.usingBrowser)
if (websearch.contentLimit && result.content.length > websearch.contentLimit) {
result.content = result.content.slice(0, websearch.contentLimit) + '...'
}
return result
})
// Wait for all fetches to complete
const results: WebSearchResult[] = await Promise.all(fetchPromises)
return {
query: query,
results: results.filter((result) => result.content != noContent)
}
} catch (error) {
console.error('Searxng search failed:', error)

View File

@@ -228,7 +228,7 @@ export async function fetchChatCompletion({
if (generateImage && generateImage.images.length > 0) {
const existingImages = message.metadata?.generateImage?.images || []
generateImage.images = [...existingImages, ...generateImage.images]
console.log('generateImage', generateImage)
// console.log('generateImage', generateImage)
message.metadata = {
...message.metadata,
generateImage: generateImage

View File

@@ -46,16 +46,6 @@ export const INITIAL_PROVIDERS: Provider[] = [
isSystem: true,
enabled: false
},
{
id: 'o3',
name: 'O3',
type: 'openai',
apiKey: '',
apiHost: 'https://api.o3.fan',
models: SYSTEM_MODELS.o3,
isSystem: true,
enabled: false
},
{
id: 'ocoolai',
name: 'ocoolAI',
@@ -66,16 +56,6 @@ export const INITIAL_PROVIDERS: Provider[] = [
isSystem: true,
enabled: false
},
{
id: 'openrouter',
name: 'OpenRouter',
type: 'openai',
apiKey: '',
apiHost: 'https://openrouter.ai/api/v1/',
models: SYSTEM_MODELS.openrouter,
isSystem: true,
enabled: false
},
{
id: 'deepseek',
name: 'deepseek',
@@ -87,22 +67,12 @@ export const INITIAL_PROVIDERS: Provider[] = [
enabled: false
},
{
id: 'ollama',
name: 'Ollama',
id: 'openrouter',
name: 'OpenRouter',
type: 'openai',
apiKey: '',
apiHost: 'http://localhost:11434',
models: SYSTEM_MODELS.ollama,
isSystem: true,
enabled: false
},
{
id: 'lmstudio',
name: 'LM Studio',
type: 'openai',
apiKey: '',
apiHost: 'http://localhost:1234',
models: SYSTEM_MODELS.lmstudio,
apiHost: 'https://openrouter.ai/api/v1/',
models: SYSTEM_MODELS.openrouter,
isSystem: true,
enabled: false
},
@@ -147,12 +117,42 @@ export const INITIAL_PROVIDERS: Provider[] = [
enabled: false
},
{
id: 'baidu-cloud',
name: 'Baidu Cloud',
id: 'dmxapi',
name: 'DMXAPI',
type: 'openai',
apiKey: '',
apiHost: 'https://qianfan.baidubce.com/v2/',
models: SYSTEM_MODELS['baidu-cloud'],
apiHost: 'https://www.dmxapi.cn',
models: SYSTEM_MODELS.dmxapi,
isSystem: true,
enabled: false
},
{
id: 'o3',
name: 'O3',
type: 'openai',
apiKey: '',
apiHost: 'https://api.o3.fan',
models: SYSTEM_MODELS.o3,
isSystem: true,
enabled: false
},
{
id: 'ollama',
name: 'Ollama',
type: 'openai',
apiKey: '',
apiHost: 'http://localhost:11434',
models: SYSTEM_MODELS.ollama,
isSystem: true,
enabled: false
},
{
id: 'lmstudio',
name: 'LM Studio',
type: 'openai',
apiKey: '',
apiHost: 'http://localhost:1234',
models: SYSTEM_MODELS.lmstudio,
isSystem: true,
enabled: false
},
@@ -197,6 +197,16 @@ export const INITIAL_PROVIDERS: Provider[] = [
isSystem: true,
enabled: false
},
{
id: 'zhipu',
name: 'ZhiPu',
type: 'openai',
apiKey: '',
apiHost: 'https://open.bigmodel.cn/api/paas/v4/',
models: SYSTEM_MODELS.zhipu,
isSystem: true,
enabled: false
},
{
id: 'github',
name: 'Github Models',
@@ -218,16 +228,6 @@ export const INITIAL_PROVIDERS: Provider[] = [
enabled: false,
isAuthed: false
},
{
id: 'dmxapi',
name: 'DMXAPI',
type: 'openai',
apiKey: '',
apiHost: 'https://www.dmxapi.cn',
models: SYSTEM_MODELS.dmxapi,
isSystem: true,
enabled: false
},
{
id: 'yi',
name: 'Yi',
@@ -238,16 +238,6 @@ export const INITIAL_PROVIDERS: Provider[] = [
isSystem: true,
enabled: false
},
{
id: 'zhipu',
name: 'ZhiPu',
type: 'openai',
apiKey: '',
apiHost: 'https://open.bigmodel.cn/api/paas/v4/',
models: SYSTEM_MODELS.zhipu,
isSystem: true,
enabled: false
},
{
id: 'moonshot',
name: 'Moonshot AI',
@@ -458,6 +448,16 @@ export const INITIAL_PROVIDERS: Provider[] = [
isSystem: true,
enabled: false
},
{
id: 'baidu-cloud',
name: 'Baidu Cloud',
type: 'openai',
apiKey: '',
apiHost: 'https://qianfan.baidubce.com/v2/',
models: SYSTEM_MODELS['baidu-cloud'],
isSystem: true,
enabled: false
},
{
id: 'gpustack',
name: 'GPUStack',

View File

@@ -109,8 +109,10 @@ export interface SettingsState {
siyuanToken: string | null
siyuanBoxId: string | null
siyuanRootPath: string | null
// MinApps
maxKeepAliveMinapps: number
showOpenedMinappsInSidebar: boolean
minappsOpenLinkExternal: boolean
// 隐私设置
enableDataCollection: boolean
enableQuickPanelTriggers: boolean
@@ -147,7 +149,7 @@ export const initialState: SettingsState = {
trayOnClose: true,
tray: true,
theme: ThemeMode.auto,
windowStyle: 'transparent',
windowStyle: 'opaque',
fontSize: 14,
topicPosition: 'left',
showTopicTime: false,
@@ -211,8 +213,10 @@ export const initialState: SettingsState = {
siyuanToken: null,
siyuanBoxId: null,
siyuanRootPath: null,
// MinApps
maxKeepAliveMinapps: 3,
showOpenedMinappsInSidebar: true,
minappsOpenLinkExternal: false,
enableDataCollection: false,
enableQuickPanelTriggers: false,
enableBackspaceDeleteModel: true,
@@ -482,6 +486,9 @@ const settingsSlice = createSlice({
setShowOpenedMinappsInSidebar: (state, action: PayloadAction<boolean>) => {
state.showOpenedMinappsInSidebar = action.payload
},
setMinappsOpenLinkExternal: (state, action: PayloadAction<boolean>) => {
state.minappsOpenLinkExternal = action.payload
},
setEnableDataCollection: (state, action: PayloadAction<boolean>) => {
state.enableDataCollection = action.payload
},
@@ -579,6 +586,7 @@ export const {
setSiyuanRootPath,
setMaxKeepAliveMinapps,
setShowOpenedMinappsInSidebar,
setMinappsOpenLinkExternal,
setEnableDataCollection,
setEnableQuickPanelTriggers,
setExportMenuOptions,

View File

@@ -26,7 +26,7 @@ export interface WebSearchState {
}
const initialState: WebSearchState = {
defaultProvider: '',
defaultProvider: 'local-bing',
providers: [
{
id: 'tavily',
@@ -135,7 +135,7 @@ const websearchSlice = createSlice({
state.providers.push(action.payload)
}
},
setContentLimit: (state, action: PayloadAction<number>) => {
setContentLimit: (state, action: PayloadAction<number | undefined>) => {
state.contentLimit = action.payload
}
}

View File

@@ -403,6 +403,7 @@ export interface MCPServer {
disabledTools?: string[] // List of tool names that are disabled for this server
configSample?: MCPConfigSample
headers?: Record<string, string> // Custom headers to be sent with requests to this server
searchKey?: string
}
export interface MCPToolInputSchema {

View File

@@ -80,3 +80,26 @@ export const providerCharge = async (provider: string) => {
`width=${width},height=${height},toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,alwaysOnTop=yes,alwaysRaised=yes`
)
}
export const providerBills = async (provider: string) => {
const billsUrlMap = {
silicon: {
url: 'https://cloud.siliconflow.cn/bills',
width: 900,
height: 700
},
aihubmix: {
url: `https://aihubmix.com/statistics?client_id=cherry_studio_oauth&lang=${getLanguageCode()}&aff=SJyh`,
width: 900,
height: 700
}
}
const { url, width, height } = billsUrlMap[provider]
window.open(
url,
'oauth',
`width=${width},height=${height},toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,alwaysOnTop=yes,alwaysRaised=yes`
)
}

View File

@@ -0,0 +1,35 @@
import { ThemeMode } from '@renderer/types'
import { MarkdownItShikiOptions, setupMarkdownIt } from '@shikijs/markdown-it'
import MarkdownIt from 'markdown-it'
import { BuiltinLanguage, BuiltinTheme, bundledLanguages, createHighlighter } from 'shiki'
const defaultOptions = {
themes: {
light: 'one-light',
dark: 'material-theme-darker'
},
defaultColor: 'light'
}
const initHighlighter = async (options: MarkdownItShikiOptions) => {
const themeNames = ('themes' in options ? Object.values(options.themes) : [options.theme]).filter(
Boolean
) as BuiltinTheme[]
return await createHighlighter({
themes: themeNames,
langs: options.langs || (Object.keys(bundledLanguages) as BuiltinLanguage[])
})
}
const highlighter = await initHighlighter(defaultOptions)
export function getShikiInstance(theme: ThemeMode) {
const options = {
...defaultOptions,
defaultColor: theme
}
return function (markdownit: MarkdownIt) {
setupMarkdownIt(markdownit, highlighter, options)
}
}

1215
yarn.lock

File diff suppressed because it is too large Load Diff