Compare commits
35 Commits
hlink/deep
...
feat/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d98c7e7405 | ||
|
|
779d4c4787 | ||
|
|
32f160444b | ||
|
|
a546c265ee | ||
|
|
ea89a37b1d | ||
|
|
c288b4a8d0 | ||
|
|
e8384db91a | ||
|
|
36c87451d9 | ||
|
|
eaa37fe674 | ||
|
|
308ad9f68f | ||
|
|
62b6584d65 | ||
|
|
5a44f6aca8 | ||
|
|
4c6a904929 | ||
|
|
be4ef2990f | ||
|
|
2807e71f1a | ||
|
|
a5f8ac8587 | ||
|
|
4bd50251ff | ||
|
|
f0d60052c4 | ||
|
|
84f4b565f3 | ||
|
|
ebdacdde3e | ||
|
|
aeb66195a0 | ||
|
|
53ef8b0f32 | ||
|
|
794c23f296 | ||
|
|
62440cbfa1 | ||
|
|
39b723f143 | ||
|
|
bdc75f2f4e | ||
|
|
eb3f136997 | ||
|
|
6ba5768650 | ||
|
|
1f588d242e | ||
|
|
25b1e309ed | ||
|
|
7a7b24fe2f | ||
|
|
0686b2d813 | ||
|
|
4a027892b9 | ||
|
|
be323b6304 | ||
|
|
2f5cfc0162 |
2
.github/ISSUE_TEMPLATE/#0_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/#0_bug_report.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: 🐛 错误报告 (中文)
|
||||
description: 创建一个报告以帮助我们改进
|
||||
title: '[错误]: '
|
||||
labels: ['bug']
|
||||
labels: ['kind/bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: 💡 功能建议 (中文)
|
||||
description: 为项目提出新的想法
|
||||
title: '[功能]: '
|
||||
labels: ['enhancement']
|
||||
labels: ['kind/enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/#2_question.yml
vendored
2
.github/ISSUE_TEMPLATE/#2_question.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: ❓ 讨论 & 提问 (中文)
|
||||
description: 寻求帮助、讨论问题、提出疑问等...
|
||||
title: '[讨论]: '
|
||||
labels: ['question']
|
||||
labels: ['kind/question']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/2_question.yml
vendored
2
.github/ISSUE_TEMPLATE/2_question.yml
vendored
@@ -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
54
.github/pull_request_template.md
vendored
Normal 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
|
||||
|
||||
```
|
||||
75
.github/workflows/nightly-build.yml
vendored
75
.github/workflows/nightly-build.yml
vendored
@@ -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
3
.gitignore
vendored
@@ -51,6 +51,3 @@ local
|
||||
coverage
|
||||
.vitest-cache
|
||||
vitest.config.*.timestamp-*
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
|
||||
37698
.yarn/patches/@google-genai-npm-0.8.0-450d0d9a7d.patch
vendored
37698
.yarn/patches/@google-genai-npm-0.8.0-450d0d9a7d.patch
vendored
File diff suppressed because one or more lines are too long
38
.yarn/patches/electron-updater-npm-6.6.3-9269dbaf84.patch
vendored
Normal file
38
.yarn/patches/electron-updater-npm-6.6.3-9269dbaf84.patch
vendored
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
77
docs/CONTRIBUTING.zh.md
Normal 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 打造成更好的产品。
|
||||
@@ -114,7 +114,7 @@ https://docs.cherry-ai.com
|
||||
3. **提交更改**:提交并推送您的更改。
|
||||
4. **打开 Pull Request**:描述您的更改和原因。
|
||||
|
||||
有关更详细的指南,请参阅我们的 [贡献指南](../CONTRIBUTING.md)。
|
||||
有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.zh.md)。
|
||||
|
||||
感谢您的支持和贡献!
|
||||
|
||||
|
||||
@@ -37,6 +37,12 @@ yarn install
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
yarn test
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
|
||||
@@ -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 类型渲染错误
|
||||
|
||||
@@ -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: {
|
||||
|
||||
25
package.json
25
package.json
@@ -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": {
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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
157
scripts/auto-i18n.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 || {}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
35
src/main/services/WebviewService.ts
Normal file
35
src/main/services/WebviewService.ts
Normal 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' }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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))) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
src/preload/index.d.ts
vendored
6
src/preload/index.d.ts
vendored
@@ -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>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
55
src/renderer/src/assets/images/cherry-text-logo.svg
Normal file
55
src/renderer/src/assets/images/cherry-text-logo.svg
Normal 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 |
@@ -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 |
@@ -16,6 +16,10 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ant-tabs-tab-btn {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ant-segmented-group {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ const MinAppsPopover: FC<Props> = ({ children }) => {
|
||||
))}
|
||||
{isEmpty(minapps) && (
|
||||
<Center>
|
||||
<Empty />
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Center>
|
||||
)}
|
||||
</AppsContainer>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
119
src/renderer/src/i18n/locales/i18n.py
Normal file
119
src/renderer/src/i18n/locales/i18n.py
Normal 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()
|
||||
@@ -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の認証が成功しました",
|
||||
|
||||
@@ -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认证成功",
|
||||
|
||||
@@ -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": "选择一个搜索服务商",
|
||||
|
||||
@@ -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 認證成功",
|
||||
|
||||
@@ -503,7 +503,9 @@
|
||||
"switch.disabled": "Παρακαλείστε να περιμένετε τη λήξη της τρέχουσας απάντησης",
|
||||
"tools": {
|
||||
"completed": "Ολοκληρώθηκε",
|
||||
"invoking": "κλήση σε εξέλιξη"
|
||||
"invoking": "κλήση σε εξέλιξη",
|
||||
"raw": "Ακατέργαστο",
|
||||
"preview": "Προεπισκόπηση"
|
||||
},
|
||||
"topic.added": "Η θεματική προστέθηκε επιτυχώς",
|
||||
"upgrade.success.button": "Επανεκκίνηση",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />}>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 来提高性能
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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)')};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
|
||||
|
||||
@@ -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.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`
|
||||
)
|
||||
}
|
||||
|
||||
35
src/renderer/src/utils/shiki.ts
Normal file
35
src/renderer/src/utils/shiki.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user