diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c3ab3d803..bea18d50b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -115,3 +115,38 @@ jobs:
tag: ${{ steps.get-tag.outputs.tag }}
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/*.blockmap'
token: ${{ secrets.GITHUB_TOKEN }}
+
+ dispatch-docs-update:
+ needs: release
+ if: success() && github.repository == 'CherryHQ/cherry-studio' # 确保所有构建成功且在主仓库中运行
+ runs-on: ubuntu-latest
+ steps:
+ - name: Get release tag
+ id: get-tag
+ shell: bash
+ run: |
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
+ else
+ echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Check if tag is pre-release
+ id: check-tag
+ shell: bash
+ run: |
+ TAG="${{ steps.get-tag.outputs.tag }}"
+ if [[ "$TAG" == *"rc"* || "$TAG" == *"pre-release"* ]]; then
+ echo "is_pre_release=true" >> $GITHUB_OUTPUT
+ else
+ echo "is_pre_release=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Dispatch update-download-version workflow to cherry-studio-docs
+ if: steps.check-tag.outputs.is_pre_release == 'false'
+ uses: peter-evans/repository-dispatch@v3
+ with:
+ token: ${{ secrets.REPO_DISPATCH_TOKEN }}
+ repository: CherryHQ/cherry-studio-docs
+ event-type: update-download-version
+ client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}'
\ No newline at end of file
diff --git a/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch b/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch
index dbf07cb47..ef9e74c73 100644
--- a/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch
+++ b/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch
@@ -151,7 +151,7 @@ index 2404264d4ba0204322548945ebb7eab3bea82173..8f1bc45cc45e0797d50989d96b51147b
+ "embeddings/decoding base64 embeddings from base64"
+ );
+ return response._thenUnwrap((response) => {
-+ if (response && response.data) {
++ if (response && response.data && typeof response.data[0]?.embedding === 'string') {
+ response.data.forEach((embeddingBase64Obj) => {
+ const embeddingBase64Str = embeddingBase64Obj.embedding;
+ embeddingBase64Obj.embedding = (0, utils_1.toFloat32Array)(
@@ -266,7 +266,7 @@ index 19dcaef578c194a89759c4360073cfd4f7dd2cbf..0284e9cc615c900eff508eb595f7360a
+ "embeddings/decoding base64 embeddings from base64"
+ );
+ return response._thenUnwrap((response) => {
-+ if (response && response.data) {
++ if (response && response.data && typeof response.data[0]?.embedding === 'string') {
+ response.data.forEach((embeddingBase64Obj) => {
+ const embeddingBase64Str = embeddingBase64Obj.embedding;
+ embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str);
diff --git a/README.md b/README.md
index 3cf67d836..b589376b7 100644
--- a/README.md
+++ b/README.md
@@ -3,10 +3,42 @@
-
English | 中文 | 日本語
+ English | 中文 | 日本語 | Official Site | Documents | Development | Feedback
+
+
+
+
+[![][deepwiki-shield]][deepwiki-link]
+[![][twitter-shield]][twitter-link]
+[![][discord-shield]][discord-link]
+[![][telegram-shield]][telegram-link]
+
+
+
+
+
+
+
+[![][github-stars-shield]][github-stars-link]
+[![][github-forks-shield]][github-forks-link]
+[![][github-release-shield]][github-release-link]
+[![][github-contributors-shield]][github-contributors-link]
+
+
+
+
+
+[![][license-shield]][license-link]
+[![][commercial-shield]][commercial-link]
+[![][sponsor-shield]][sponsor-link]
+
+
+
+
# 🍒 Cherry Studio
@@ -17,10 +49,6 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
❤️ Like Cherry Studio? Give it a star 🌟 or [Sponsor](docs/sponsor.md) to support the development!
-# 📖 Guide
-
-
-
# 🌠 Screenshot

@@ -114,14 +142,6 @@ Want to influence our roadmap? Join our [GitHub Discussions](https://github.com/
Welcome PR for more themes
-# 🖥️ Develop
-
-Refer to the [development documentation](docs/dev.md)
-
-Refer to the [Architecture overview documentation](https://deepwiki.com/CherryHQ/cherry-studio)
-
-Refer to the [Branching Strategy](docs/branching-strategy-en.md) for contribution guidelines
-
# 🤝 Contributing
We welcome contributions to Cherry Studio! Here are some ways you can contribute:
@@ -134,6 +154,8 @@ We welcome contributions to Cherry Studio! Here are some ways you can contribute
6. **Community Engagement**: Join discussions and help users.
7. **Promote Usage**: Spread the word about Cherry Studio.
+Refer to the [Branching Strategy](docs/branching-strategy-en.md) for contribution guidelines
+
## Getting Started
1. **Fork the Repository**: Fork and clone it to your local machine.
@@ -158,22 +180,34 @@ Thank you for your support and contributions!
-# 🌐 Community
-
-[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
-
-# ☕ Sponsor
-
-[Buy Me a Coffee](docs/sponsor.md)
-
-# 📃 License
-
-[LICENSE](./LICENSE)
-
-# ✉️ Contact
-
-
-
# ⭐️ Star History
-[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
+[](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
+
+
+[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
+[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
+[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
+[twitter-link]: https://twitter.com/CherryStudioHQ
+[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
+[discord-link]: https://discord.gg/wez8HtpxqQ
+[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
+[telegram-link]: https://t.me/CherryStudioAI
+
+
+[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
+[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
+[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
+[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
+[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
+[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
+[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
+[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
+
+
+[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
+[license-link]: https://www.gnu.org/licenses/agpl-3.0
+[commercial-shield]: https://img.shields.io/badge/License-Contact-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
+[commercial-link]: mailto:license@cherry-ai.com?subject=Commercial%20License%20Inquiry
+[sponsor-shield]: https://img.shields.io/badge/Sponsor-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
+[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md
diff --git a/docs/README.ja.md b/docs/README.ja.md
index 2a88cf8e5..1278edec8 100644
--- a/docs/README.ja.md
+++ b/docs/README.ja.md
@@ -1,15 +1,46 @@
- English | 中文 | 日本語
+ English | 中文 | 日本語 | 公式サイト | ドキュメント | 開発 | フィードバック
+
+
+
+[![][deepwiki-shield]][deepwiki-link]
+[![][twitter-shield]][twitter-link]
+[![][discord-shield]][discord-link]
+[![][telegram-shield]][telegram-link]
+
+
+
+
+
+
+
+[![][github-stars-shield]][github-stars-link]
+[![][github-forks-shield]][github-forks-link]
+[![][github-release-shield]][github-release-link]
+[![][github-contributors-shield]][github-contributors-link]
+
+
+
+
+
+[![][license-shield]][license-link]
+[![][commercial-shield]][commercial-link]
+[![][sponsor-shield]][sponsor-link]
+
+
+
+
# 🍒 Cherry Studio
@@ -20,10 +51,6 @@ Cherry Studio は、複数の LLM プロバイダーをサポートするデス
❤️ Cherry Studio をお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!
-# 📖 ガイド
-
-https://docs.cherry-ai.com
-
# 🌠 スクリーンショット

@@ -117,14 +144,6 @@ https://docs.cherry-ai.com
より多くのテーマの PR を歓迎します
-# 🖥️ 開発
-
-[開発ドキュメント](dev.md)を参照してください
-
-[アーキテクチャ概要ドキュメント](https://deepwiki.com/CherryHQ/cherry-studio)を参照してください
-
-[ブランチ戦略](branching-strategy-en.md)を参照して貢献ガイドラインを確認してください
-
# 🤝 貢献
Cherry Studio への貢献を歓迎します!以下の方法で貢献できます:
@@ -137,6 +156,8 @@ Cherry Studio への貢献を歓迎します!以下の方法で貢献できま
6. **コミュニティの参加**:ディスカッションに参加し、ユーザーを支援します
7. **使用の促進**:Cherry Studio を広めます
+[ブランチ戦略](branching-strategy-en.md)を参照して貢献ガイドラインを確認してください
+
## 始め方
1. **リポジトリをフォーク**:フォークしてローカルマシンにクローンします
@@ -161,22 +182,34 @@ Cherry Studio への貢献を歓迎します!以下の方法で貢献できま
-# 🌐 コミュニティ
-
-[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
-
-# ☕ スポンサー
-
-[開発者を支援する](sponsor.md)
-
-# 📃 ライセンス
-
-[LICENSE](../LICENSE)
-
-# ✉️ お問い合わせ
-
-yinsenho@cherry-ai.com
-
# ⭐️ スター履歴
-[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
+[](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
+
+
+[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
+[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
+[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
+[twitter-link]: https://twitter.com/CherryStudioHQ
+[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
+[discord-link]: https://discord.gg/wez8HtpxqQ
+[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
+[telegram-link]: https://t.me/CherryStudioAI
+
+
+[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
+[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
+[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
+[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
+[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
+[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
+[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
+[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
+
+
+[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
+[license-link]: https://www.gnu.org/licenses/agpl-3.0
+[commercial-shield]: https://img.shields.io/badge/商用ライセンス-お問い合わせ-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
+[commercial-link]: mailto:license@cherry-ai.com?subject=商業ライセンスについて
+[sponsor-shield]: https://img.shields.io/badge/スポンサー-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
+[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md
diff --git a/docs/README.zh.md b/docs/README.zh.md
index f4a8feda6..1ca483fd1 100644
--- a/docs/README.zh.md
+++ b/docs/README.zh.md
@@ -1,14 +1,46 @@
- English | 中文 | 日本語
+ English | 中文 | 日本語 | 官方网站 | 文档 | 开发 | 反馈
+
+
+
+
+[![][deepwiki-shield]][deepwiki-link]
+[![][twitter-shield]][twitter-link]
+[![][discord-shield]][discord-link]
+[![][telegram-shield]][telegram-link]
+
+
+
+
+
+
+
+[![][github-stars-shield]][github-stars-link]
+[![][github-forks-shield]][github-forks-link]
+[![][github-release-shield]][github-release-link]
+[![][github-contributors-shield]][github-contributors-link]
+
+
+
+
+
+[![][license-shield]][license-link]
+[![][commercial-shield]][commercial-link]
+[![][sponsor-shield]][sponsor-link]
+
+
+
+
# 🍒 Cherry Studio
@@ -124,14 +156,6 @@ https://docs.cherry-ai.com
欢迎 PR 更多主题
-# 🖥️ 开发
-
-参考[开发文档](dev.md)
-
-参考[架构概览文档](https://deepwiki.com/CherryHQ/cherry-studio)
-
-参考[分支策略](branching-strategy-zh.md)了解贡献指南
-
# 🤝 贡献
我们欢迎对 Cherry Studio 的贡献!您可以通过以下方式贡献:
@@ -144,6 +168,8 @@ https://docs.cherry-ai.com
6. **社区参与**:加入讨论并帮助用户
7. **推广使用**:宣传 Cherry Studio
+参考[分支策略](branching-strategy-zh.md)了解贡献指南
+
## 入门
1. **Fork 仓库**:Fork 并克隆到您的本地机器
@@ -168,22 +194,34 @@ https://docs.cherry-ai.com
-# 🌐 社区
-
-[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
-
-# ☕ 赞助
-
-[赞助开发者](sponsor.md)
-
-# 📃 许可证
-
-[LICENSE](../LICENSE)
-
-# ✉️ 联系我们
-
-yinsenho@cherry-ai.com
-
# ⭐️ Star 记录
-[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
+[](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
+
+
+[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
+[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
+[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
+[twitter-link]: https://twitter.com/CherryStudioHQ
+[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
+[discord-link]: https://discord.gg/wez8HtpxqQ
+[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
+[telegram-link]: https://t.me/CherryStudioAI
+
+
+[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
+[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
+[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
+[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
+[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
+[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
+[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
+[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
+
+
+[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
+[license-link]: https://www.gnu.org/licenses/agpl-3.0
+[commercial-shield]: https://img.shields.io/badge/商用授权-联系-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
+[commercial-link]: mailto:license@cherry-ai.com?subject=商业授权咨询
+[sponsor-shield]: https://img.shields.io/badge/赞助支持-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
+[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md
diff --git a/package.json b/package.json
index 9d0cd8c5b..52ef7f4c8 100644
--- a/package.json
+++ b/package.json
@@ -68,9 +68,11 @@
"@cherrystudio/embedjs-loader-sitemap": "^0.1.31",
"@cherrystudio/embedjs-loader-web": "^0.1.31",
"@cherrystudio/embedjs-loader-xml": "^0.1.31",
+ "@cherrystudio/embedjs-ollama": "^0.1.31",
"@cherrystudio/embedjs-openai": "^0.1.31",
"@electron-toolkit/utils": "^3.0.0",
"@langchain/community": "^0.3.36",
+ "@langchain/ollama": "^0.2.1",
"@mistralai/mistralai": "^1.6.0",
"@strongtz/win32-arm64-msvc": "^0.4.7",
"@tanstack/react-query": "^5.27.0",
@@ -96,7 +98,8 @@
"pdf-to-img": "^4.4.0",
"pdfjs-dist": "4.2.67",
"proxy-agent": "^6.5.0",
- "selection-hook": "^0.9.21",
+ "remove-markdown": "^0.6.2",
+ "selection-hook": "^0.9.23",
"tar": "^7.4.3",
"turndown": "^7.2.0",
"webdav": "^5.8.0",
diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts
index f331877b4..30583b904 100644
--- a/packages/shared/IpcChannel.ts
+++ b/packages/shared/IpcChannel.ts
@@ -13,6 +13,7 @@ export enum IpcChannel {
App_SetTrayOnClose = 'app:set-tray-on-close',
App_SetTheme = 'app:set-theme',
App_SetAutoUpdate = 'app:set-auto-update',
+ App_SetFeedUrl = 'app:set-feed-url',
App_HandleZoomFactor = 'app:handle-zoom-factor',
App_IsBinaryExist = 'app:is-binary-exist',
@@ -20,6 +21,8 @@ export enum IpcChannel {
App_InstallUvBinary = 'app:install-uv-binary',
App_InstallBunBinary = 'app:install-bun-binary',
+ App_QuoteToMain = 'app:quote-to-main',
+
Notification_Send = 'notification:send',
Notification_OnClick = 'notification:on-click',
diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts
index e1fca4e6d..cfba46df7 100644
--- a/packages/shared/config/constant.ts
+++ b/packages/shared/config/constant.ts
@@ -403,3 +403,8 @@ export const KB = 1024
export const MB = 1024 * KB
export const GB = 1024 * MB
export const defaultLanguage = 'en-US'
+
+export enum FeedUrl {
+ PRODUCTION = 'https://releases.cherry-ai.com',
+ EARLY_ACCESS = 'https://github.com/CherryHQ/cherry-studio/releases/latest/download'
+}
diff --git a/src/main/configs/SelectionConfig.ts b/src/main/configs/SelectionConfig.ts
index 1d0a3850c..59988ded7 100644
--- a/src/main/configs/SelectionConfig.ts
+++ b/src/main/configs/SelectionConfig.ts
@@ -20,6 +20,7 @@ interface IFinetunedList {
*************************************************************************/
export const SELECTION_PREDEFINED_BLACKLIST: IFilterList = {
WINDOWS: [
+ 'explorer.exe',
// Screenshot
'snipaste.exe',
'pixpin.exe',
diff --git a/src/main/embeddings/Embeddings.ts b/src/main/embeddings/Embeddings.ts
index cf354450e..0701e7db2 100644
--- a/src/main/embeddings/Embeddings.ts
+++ b/src/main/embeddings/Embeddings.ts
@@ -5,8 +5,15 @@ import EmbeddingsFactory from './EmbeddingsFactory'
export default class Embeddings {
private sdk: BaseEmbeddings
- constructor({ model, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams) {
- this.sdk = EmbeddingsFactory.create({ model, apiKey, apiVersion, baseURL, dimensions } as KnowledgeBaseParams)
+ constructor({ model, provider, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams) {
+ this.sdk = EmbeddingsFactory.create({
+ model,
+ provider,
+ apiKey,
+ apiVersion,
+ baseURL,
+ dimensions
+ } as KnowledgeBaseParams)
}
public async init(): Promise {
return this.sdk.init()
diff --git a/src/main/embeddings/EmbeddingsFactory.ts b/src/main/embeddings/EmbeddingsFactory.ts
index 5924d00d7..808db0579 100644
--- a/src/main/embeddings/EmbeddingsFactory.ts
+++ b/src/main/embeddings/EmbeddingsFactory.ts
@@ -1,20 +1,49 @@
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
+import { OllamaEmbeddings } from '@cherrystudio/embedjs-ollama'
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings'
import { getInstanceName } from '@main/utils'
import { KnowledgeBaseParams } from '@types'
-import VoyageEmbeddings from './VoyageEmbeddings'
+import { SUPPORTED_DIM_MODELS as VOYAGE_SUPPORTED_DIM_MODELS, VoyageEmbeddings } from './VoyageEmbeddings'
export default class EmbeddingsFactory {
- static create({ model, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams): BaseEmbeddings {
+ static create({ model, provider, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams): BaseEmbeddings {
const batchSize = 10
- if (model.includes('voyage')) {
- return new VoyageEmbeddings({
- modelName: model,
- apiKey,
- outputDimension: dimensions,
- batchSize: 8
+ if (provider === 'voyageai') {
+ if (VOYAGE_SUPPORTED_DIM_MODELS.includes(model)) {
+ return new VoyageEmbeddings({
+ modelName: model,
+ apiKey,
+ outputDimension: dimensions,
+ batchSize: 8
+ })
+ } else {
+ return new VoyageEmbeddings({
+ modelName: model,
+ apiKey,
+ batchSize: 8
+ })
+ }
+ }
+ if (provider === 'ollama') {
+ if (baseURL.includes('v1/')) {
+ return new OllamaEmbeddings({
+ model: model,
+ baseUrl: baseURL.replace('v1/', ''),
+ requestOptions: {
+ // @ts-ignore expected
+ 'encoding-format': 'float'
+ }
+ })
+ }
+ return new OllamaEmbeddings({
+ model: model,
+ baseUrl: baseURL,
+ requestOptions: {
+ // @ts-ignore expected
+ 'encoding-format': 'float'
+ }
})
}
if (apiVersion !== undefined) {
diff --git a/src/main/embeddings/VoyageEmbeddings.ts b/src/main/embeddings/VoyageEmbeddings.ts
index ce21afe58..edec32dc5 100644
--- a/src/main/embeddings/VoyageEmbeddings.ts
+++ b/src/main/embeddings/VoyageEmbeddings.ts
@@ -1,16 +1,20 @@
import { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { VoyageEmbeddings as _VoyageEmbeddings } from '@langchain/community/embeddings/voyage'
-export default class VoyageEmbeddings extends BaseEmbeddings {
+/**
+ * 支持设置嵌入维度的模型
+ */
+export const SUPPORTED_DIM_MODELS = ['voyage-3-large', 'voyage-3.5', 'voyage-3.5-lite', 'voyage-code-3']
+export class VoyageEmbeddings extends BaseEmbeddings {
private model: _VoyageEmbeddings
constructor(private readonly configuration?: ConstructorParameters[0]) {
super()
if (!this.configuration) this.configuration = {}
if (!this.configuration.modelName) this.configuration.modelName = 'voyage-3'
-
- if (!this.configuration.outputDimension) {
- throw new Error('You need to pass in the optional dimensions parameter for this model')
+ if (!SUPPORTED_DIM_MODELS.includes(this.configuration.modelName) && this.configuration.outputDimension) {
+ throw new Error(`VoyageEmbeddings only supports ${SUPPORTED_DIM_MODELS.join(', ')}`)
}
+
this.model = new _VoyageEmbeddings(this.configuration)
}
override async getDimensions(): Promise {
diff --git a/src/main/index.ts b/src/main/index.ts
index e53fbb4b4..3272887aa 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -34,6 +34,26 @@ if (isWin) {
app.commandLine.appendSwitch('wm-window-animations-disabled')
}
+// Enable features for unresponsive renderer js call stacks
+app.commandLine.appendSwitch('enable-features', 'DocumentPolicyIncludeJSCallStacksInCrashReports')
+app.on('web-contents-created', (_, webContents) => {
+ webContents.session.webRequest.onHeadersReceived((details, callback) => {
+ callback({
+ responseHeaders: {
+ ...details.responseHeaders,
+ 'Document-Policy': ['include-js-call-stacks-in-crash-reports']
+ }
+ })
+ })
+
+ webContents.on('unresponsive', async () => {
+ // Interrupt execution and collect call stack from unresponsive renderer
+ Logger.error('Renderer unresponsive start')
+ const callStack = await webContents.mainFrame.collectJavaScriptCallStack()
+ Logger.error('Renderer unresponsive js call stack\n', callStack)
+ })
+})
+
// in production mode, handle uncaught exception and unhandled rejection globally
if (!isDev) {
// handle uncaught exception
diff --git a/src/main/ipc.ts b/src/main/ipc.ts
index 3f291f994..77c6384ee 100644
--- a/src/main/ipc.ts
+++ b/src/main/ipc.ts
@@ -35,6 +35,7 @@ import { calculateDirectorySize, getResourcePath } from './utils'
import { decrypt, encrypt } from './utils/aes'
import { getCacheDir, getConfigDir, getFilesDir } from './utils/file'
import { compress, decompress } from './utils/zip'
+import { FeedUrl } from '@shared/config/constant'
const fileManager = new FileStorage()
const backupManager = new BackupManager()
@@ -113,6 +114,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
configManager.setAutoUpdate(isActive)
})
+ ipcMain.handle(IpcChannel.App_SetFeedUrl, (_, feedUrl: FeedUrl) => {
+ appUpdater.setFeedUrl(feedUrl)
+ })
+
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => {
configManager.set(key, value, isNotify)
})
@@ -369,4 +374,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// selection assistant
SelectionService.registerIpcHandler()
+
+ ipcMain.handle(IpcChannel.App_QuoteToMain, (_, text: string) => windowService.quoteToMainWindow(text))
}
diff --git a/src/main/services/AppUpdater.ts b/src/main/services/AppUpdater.ts
index 1733bc606..772c885a0 100644
--- a/src/main/services/AppUpdater.ts
+++ b/src/main/services/AppUpdater.ts
@@ -1,6 +1,7 @@
import { isWin } from '@main/constant'
import { locales } from '@main/utils/locales'
import { IpcChannel } from '@shared/IpcChannel'
+import { FeedUrl } from '@shared/config/constant'
import { UpdateInfo } from 'builder-util-runtime'
import { app, BrowserWindow, dialog } from 'electron'
import logger from 'electron-log'
@@ -20,6 +21,7 @@ export default class AppUpdater {
autoUpdater.forceDevUpdateConfig = !app.isPackaged
autoUpdater.autoDownload = configManager.getAutoUpdate()
autoUpdater.autoInstallOnAppQuit = configManager.getAutoUpdate()
+ autoUpdater.setFeedURL(configManager.getFeedUrl())
// 检测下载错误
autoUpdater.on('error', (error) => {
@@ -62,6 +64,11 @@ export default class AppUpdater {
autoUpdater.autoInstallOnAppQuit = isActive
}
+ public setFeedUrl(feedUrl: FeedUrl) {
+ autoUpdater.setFeedURL(feedUrl)
+ configManager.setFeedUrl(feedUrl)
+ }
+
public async checkForUpdates() {
if (isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env) {
return {
diff --git a/src/main/services/BackupManager.ts b/src/main/services/BackupManager.ts
index 63a3303c0..f23809f1e 100644
--- a/src/main/services/BackupManager.ts
+++ b/src/main/services/BackupManager.ts
@@ -295,10 +295,12 @@ class BackupManager {
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
const backupedFilePath = await this.backup(_, filename, data, undefined, webdavConfig.skipBackupFile)
+ const contentLength = (await fs.stat(backupedFilePath)).size
const webdavClient = new WebDav(webdavConfig)
try {
const result = await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
- overwrite: true
+ overwrite: true,
+ contentLength
})
// 上传成功后删除本地备份文件
await fs.remove(backupedFilePath)
diff --git a/src/main/services/ConfigManager.ts b/src/main/services/ConfigManager.ts
index fbe871cbb..573674bd7 100644
--- a/src/main/services/ConfigManager.ts
+++ b/src/main/services/ConfigManager.ts
@@ -1,4 +1,4 @@
-import { defaultLanguage, ZOOM_SHORTCUTS } from '@shared/config/constant'
+import { defaultLanguage, FeedUrl, ZOOM_SHORTCUTS } from '@shared/config/constant'
import { LanguageVarious, Shortcut, ThemeMode } from '@types'
import { app } from 'electron'
import Store from 'electron-store'
@@ -16,6 +16,7 @@ export enum ConfigKeys {
ClickTrayToShowQuickAssistant = 'clickTrayToShowQuickAssistant',
EnableQuickAssistant = 'enableQuickAssistant',
AutoUpdate = 'autoUpdate',
+ FeedUrl = 'feedUrl',
EnableDataCollection = 'enableDataCollection',
SelectionAssistantEnabled = 'selectionAssistantEnabled',
SelectionAssistantTriggerMode = 'selectionAssistantTriggerMode',
@@ -141,6 +142,14 @@ export class ConfigManager {
this.set(ConfigKeys.AutoUpdate, value)
}
+ getFeedUrl(): string {
+ return this.get(ConfigKeys.FeedUrl, FeedUrl.PRODUCTION)
+ }
+
+ setFeedUrl(value: FeedUrl) {
+ this.set(ConfigKeys.FeedUrl, value)
+ }
+
getEnableDataCollection(): boolean {
return this.get(ConfigKeys.EnableDataCollection, true)
}
@@ -151,7 +160,7 @@ export class ConfigManager {
// Selection Assistant: is enabled the selection assistant
getSelectionAssistantEnabled(): boolean {
- return this.get(ConfigKeys.SelectionAssistantEnabled, true)
+ return this.get(ConfigKeys.SelectionAssistantEnabled, false)
}
setSelectionAssistantEnabled(value: boolean) {
diff --git a/src/main/services/FileSystemService.ts b/src/main/services/FileSystemService.ts
index afb3794df..a964d43a8 100644
--- a/src/main/services/FileSystemService.ts
+++ b/src/main/services/FileSystemService.ts
@@ -1,7 +1,9 @@
-import fs from 'node:fs'
+import fs from 'fs/promises'
export default class FileService {
- public static async readFile(_: Electron.IpcMainInvokeEvent, path: string) {
- return fs.promises.readFile(path, 'utf8')
+ public static async readFile(_: Electron.IpcMainInvokeEvent, pathOrUrl: string, encoding?: BufferEncoding) {
+ const path = pathOrUrl.startsWith('file://') ? new URL(pathOrUrl) : pathOrUrl
+ if (encoding) return fs.readFile(path, { encoding })
+ return fs.readFile(path)
}
}
diff --git a/src/main/services/KnowledgeService.ts b/src/main/services/KnowledgeService.ts
index e02fca9fc..71978ee51 100644
--- a/src/main/services/KnowledgeService.ts
+++ b/src/main/services/KnowledgeService.ts
@@ -118,13 +118,21 @@ class KnowledgeService {
private getRagApplication = async ({
id,
model,
+ provider,
apiKey,
apiVersion,
baseURL,
dimensions
}: KnowledgeBaseParams): Promise => {
let ragApplication: RAGApplication
- const embeddings = new Embeddings({ model, apiKey, apiVersion, baseURL, dimensions } as KnowledgeBaseParams)
+ const embeddings = new Embeddings({
+ model,
+ provider,
+ apiKey,
+ apiVersion,
+ baseURL,
+ dimensions
+ } as KnowledgeBaseParams)
try {
ragApplication = await new RAGApplicationBuilder()
.setModel('NO_MODEL')
diff --git a/src/main/services/SelectionService.ts b/src/main/services/SelectionService.ts
index ea5bede2d..0719c2b26 100644
--- a/src/main/services/SelectionService.ts
+++ b/src/main/services/SelectionService.ts
@@ -14,6 +14,7 @@ import type {
import type { ActionItem } from '../../renderer/src/types/selectionTypes'
import { ConfigKeys, configManager } from './ConfigManager'
+import storeSyncService from './StoreSyncService'
let SelectionHook: SelectionHookConstructor | null = null
try {
@@ -39,7 +40,8 @@ type RelativeOrientation =
enum TriggerMode {
Selected = 'selected',
- Ctrlkey = 'ctrlkey'
+ Ctrlkey = 'ctrlkey',
+ Shortcut = 'shortcut'
}
/** SelectionService is a singleton class that manages the selection hook and the toolbar window
@@ -314,6 +316,8 @@ export class SelectionService {
this.toolbarWindow.close()
this.toolbarWindow = null
}
+ this.closePreloadedActionWindows()
+
this.started = false
this.logInfo('SelectionService Stopped')
return true
@@ -334,6 +338,21 @@ export class SelectionService {
this.logInfo('SelectionService Quitted')
}
+ /**
+ * Toggle the enabled state of the selection service
+ * Will sync the new enabled store to all renderer windows
+ */
+ public toggleEnabled(enabled: boolean | undefined = undefined) {
+ if (!this.selectionHook) return
+
+ const newEnabled = enabled === undefined ? !configManager.getSelectionAssistantEnabled() : enabled
+
+ configManager.setSelectionAssistantEnabled(newEnabled)
+
+ //sync the new enabled state to all renderer windows
+ storeSyncService.syncToRenderer('selectionStore/setSelectionEnabled', newEnabled)
+ }
+
/**
* Create and configure the toolbar window
* Sets up window properties, event handlers, and loads the toolbar UI
@@ -378,6 +397,9 @@ export class SelectionService {
// Clean up when closed
this.toolbarWindow.on('closed', () => {
+ if (!this.toolbarWindow?.isDestroyed()) {
+ this.toolbarWindow?.destroy()
+ }
this.toolbarWindow = null
})
@@ -563,6 +585,21 @@ export class SelectionService {
return startTop.y === endTop.y && startBottom.y === endBottom.y
}
+ /**
+ * Get the user selected text and process it (trigger by shortcut)
+ *
+ * it's a public method used by shortcut service
+ */
+ public processSelectTextByShortcut(): void {
+ if (!this.selectionHook || this.triggerMode !== TriggerMode.Shortcut) return
+
+ const selectionData = this.selectionHook.getCurrentSelection()
+
+ if (selectionData) {
+ this.processTextSelection(selectionData)
+ }
+ }
+
/**
* Determine if the text selection should be processed by filter mode&list
* @param selectionData Text selection information and coordinates
@@ -812,8 +849,8 @@ export class SelectionService {
if (this.triggerMode === TriggerMode.Ctrlkey && this.isCtrlkey(data.vkCode)) {
return
}
- //dont hide toolbar when shiftkey is pressed, because it's used for selection
- if (this.isShiftkey(data.vkCode)) {
+ //dont hide toolbar when shiftkey or altkey is pressed, because it's used for selection
+ if (this.isShiftkey(data.vkCode) || this.isAltkey(data.vkCode)) {
return
}
@@ -854,7 +891,6 @@ export class SelectionService {
this.lastCtrlkeyDownTime = -1
const selectionData = this.selectionHook!.getCurrentSelection()
-
if (selectionData) {
this.processTextSelection(selectionData)
}
@@ -901,6 +937,11 @@ export class SelectionService {
return vkCode === 160 || vkCode === 161
}
+ //check if the key is alt key
+ private isAltkey(vkCode: number) {
+ return vkCode === 164 || vkCode === 165
+ }
+
/**
* Create a preloaded action window for quick response
* Action windows handle specific operations on selected text
@@ -953,6 +994,17 @@ export class SelectionService {
}
}
+ /**
+ * Close all preloaded action windows
+ */
+ private closePreloadedActionWindows() {
+ for (const actionWindow of this.preloadedActionWindows) {
+ if (!actionWindow.isDestroyed()) {
+ actionWindow.destroy()
+ }
+ }
+ }
+
/**
* Preload a new action window asynchronously
* This method is called after popping a window to ensure we always have windows ready
@@ -1101,24 +1153,38 @@ export class SelectionService {
* Manages appropriate event listeners for each mode
*/
private processTriggerMode() {
- if (this.triggerMode === TriggerMode.Selected) {
- if (this.isCtrlkeyListenerActive) {
- this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
- this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
+ switch (this.triggerMode) {
+ case TriggerMode.Selected:
+ if (this.isCtrlkeyListenerActive) {
+ this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
+ this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
- this.isCtrlkeyListenerActive = false
- }
+ this.isCtrlkeyListenerActive = false
+ }
- this.selectionHook!.setSelectionPassiveMode(false)
- } else if (this.triggerMode === TriggerMode.Ctrlkey) {
- if (!this.isCtrlkeyListenerActive) {
- this.selectionHook!.on('key-down', this.handleKeyDownCtrlkeyMode)
- this.selectionHook!.on('key-up', this.handleKeyUpCtrlkeyMode)
+ this.selectionHook!.setSelectionPassiveMode(false)
+ break
+ case TriggerMode.Ctrlkey:
+ if (!this.isCtrlkeyListenerActive) {
+ this.selectionHook!.on('key-down', this.handleKeyDownCtrlkeyMode)
+ this.selectionHook!.on('key-up', this.handleKeyUpCtrlkeyMode)
- this.isCtrlkeyListenerActive = true
- }
+ this.isCtrlkeyListenerActive = true
+ }
- this.selectionHook!.setSelectionPassiveMode(true)
+ this.selectionHook!.setSelectionPassiveMode(true)
+ break
+ case TriggerMode.Shortcut:
+ //remove the ctrlkey listener, don't need any key listener for shortcut mode
+ if (this.isCtrlkeyListenerActive) {
+ this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
+ this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
+
+ this.isCtrlkeyListenerActive = false
+ }
+
+ this.selectionHook!.setSelectionPassiveMode(true)
+ break
}
}
diff --git a/src/main/services/ShortcutService.ts b/src/main/services/ShortcutService.ts
index d69c80b32..24ea2324f 100644
--- a/src/main/services/ShortcutService.ts
+++ b/src/main/services/ShortcutService.ts
@@ -4,10 +4,16 @@ import { BrowserWindow, globalShortcut } from 'electron'
import Logger from 'electron-log'
import { configManager } from './ConfigManager'
+import selectionService from './SelectionService'
import { windowService } from './WindowService'
let showAppAccelerator: string | null = null
let showMiniWindowAccelerator: string | null = null
+let selectionAssistantToggleAccelerator: string | null = null
+let selectionAssistantSelectTextAccelerator: string | null = null
+
+//indicate if the shortcuts are registered on app boot time
+let isRegisterOnBoot = true
// store the focus and blur handlers for each window to unregister them later
const windowOnHandlers = new Map void; onBlurHandler: () => void }>()
@@ -28,6 +34,18 @@ function getShortcutHandler(shortcut: Shortcut) {
return () => {
windowService.toggleMiniWindow()
}
+ case 'selection_assistant_toggle':
+ return () => {
+ if (selectionService) {
+ selectionService.toggleEnabled()
+ }
+ }
+ case 'selection_assistant_select_text':
+ return () => {
+ if (selectionService) {
+ selectionService.processSelectTextByShortcut()
+ }
+ }
default:
return null
}
@@ -37,9 +55,8 @@ function formatShortcutKey(shortcut: string[]): string {
return shortcut.join('+')
}
-const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat = (
- shortcut: string | string[]
-): string => {
+// convert the shortcut recorded by keyboard event key value to electron global shortcut format
+const convertShortcutFormat = (shortcut: string | string[]): string => {
const accelerator = (() => {
if (Array.isArray(shortcut)) {
return shortcut
@@ -93,11 +110,14 @@ const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutForm
}
export function registerShortcuts(window: BrowserWindow) {
- window.once('ready-to-show', () => {
- if (configManager.getLaunchToTray()) {
- registerOnlyUniversalShortcuts()
- }
- })
+ if (isRegisterOnBoot) {
+ window.once('ready-to-show', () => {
+ if (configManager.getLaunchToTray()) {
+ registerOnlyUniversalShortcuts()
+ }
+ })
+ isRegisterOnBoot = false
+ }
//only for clearer code
const registerOnlyUniversalShortcuts = () => {
@@ -124,7 +144,12 @@ export function registerShortcuts(window: BrowserWindow) {
}
// only register universal shortcuts when needed
- if (onlyUniversalShortcuts && !['show_app', 'mini_window'].includes(shortcut.key)) {
+ if (
+ onlyUniversalShortcuts &&
+ !['show_app', 'mini_window', 'selection_assistant_toggle', 'selection_assistant_select_text'].includes(
+ shortcut.key
+ )
+ ) {
return
}
@@ -146,6 +171,14 @@ export function registerShortcuts(window: BrowserWindow) {
showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut)
break
+ case 'selection_assistant_toggle':
+ selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut)
+ break
+
+ case 'selection_assistant_select_text':
+ selectionAssistantSelectTextAccelerator = formatShortcutKey(shortcut.shortcut)
+ break
+
//the following ZOOMs will register shortcuts seperately, so will return
case 'zoom_in':
globalShortcut.register('CommandOrControl+=', () => handler(window))
@@ -162,9 +195,7 @@ export function registerShortcuts(window: BrowserWindow) {
return
}
- const accelerator = convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(
- shortcut.shortcut
- )
+ const accelerator = convertShortcutFormat(shortcut.shortcut)
globalShortcut.register(accelerator, () => handler(window))
} catch (error) {
@@ -181,15 +212,25 @@ export function registerShortcuts(window: BrowserWindow) {
if (showAppAccelerator) {
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
- const accelerator =
- convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showAppAccelerator)
+ const accelerator = convertShortcutFormat(showAppAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
}
if (showMiniWindowAccelerator) {
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
- const accelerator =
- convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showMiniWindowAccelerator)
+ const accelerator = convertShortcutFormat(showMiniWindowAccelerator)
+ handler && globalShortcut.register(accelerator, () => handler(window))
+ }
+
+ if (selectionAssistantToggleAccelerator) {
+ const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut)
+ const accelerator = convertShortcutFormat(selectionAssistantToggleAccelerator)
+ handler && globalShortcut.register(accelerator, () => handler(window))
+ }
+
+ if (selectionAssistantSelectTextAccelerator) {
+ const handler = getShortcutHandler({ key: 'selection_assistant_select_text' } as Shortcut)
+ const accelerator = convertShortcutFormat(selectionAssistantSelectTextAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
}
} catch (error) {
@@ -217,6 +258,8 @@ export function unregisterAllShortcuts() {
try {
showAppAccelerator = null
showMiniWindowAccelerator = null
+ selectionAssistantToggleAccelerator = null
+ selectionAssistantSelectTextAccelerator = null
windowOnHandlers.forEach((handlers, window) => {
window.off('focus', handlers.onFocusHandler)
window.off('blur', handlers.onBlurHandler)
diff --git a/src/main/services/StoreSyncService.ts b/src/main/services/StoreSyncService.ts
index 84d84d1ad..57f07195b 100644
--- a/src/main/services/StoreSyncService.ts
+++ b/src/main/services/StoreSyncService.ts
@@ -49,6 +49,23 @@ export class StoreSyncService {
this.windowIds = this.windowIds.filter((id) => id !== windowId)
}
+ /**
+ * Sync an action to all renderer windows
+ * @param type Action type, like 'settings/setTray'
+ * @param payload Action payload
+ *
+ * NOTICE: DO NOT use directly in ConfigManager, may cause infinite sync loop
+ */
+ public syncToRenderer(type: string, payload: any): void {
+ const action: StoreSyncAction = {
+ type,
+ payload
+ }
+
+ //-1 means the action is from the main process, will be broadcast to all windows
+ this.broadcastToOtherWindows(-1, action)
+ }
+
/**
* Register IPC handlers for store sync communication
* Handles window subscription, unsubscription and action broadcasting
diff --git a/src/main/services/WebDav.ts b/src/main/services/WebDav.ts
index ad1a127b3..fae0e2da3 100644
--- a/src/main/services/WebDav.ts
+++ b/src/main/services/WebDav.ts
@@ -1,7 +1,8 @@
import { WebDavConfig } from '@types'
import Logger from 'electron-log'
-import Stream from 'stream'
import https from 'https'
+import path from 'path'
+import Stream from 'stream'
import {
BufferLike,
createClient,
@@ -15,7 +16,7 @@ export default class WebDav {
private webdavPath: string
constructor(params: WebDavConfig) {
- this.webdavPath = params.webdavPath
+ this.webdavPath = params.webdavPath || '/'
this.instance = createClient(params.webdavHost, {
username: params.webdavUser,
@@ -51,7 +52,7 @@ export default class WebDav {
throw error
}
- const remoteFilePath = `${this.webdavPath}/${filename}`
+ const remoteFilePath = path.posix.join(this.webdavPath, filename)
try {
return await this.instance.putFileContents(remoteFilePath, data, options)
@@ -66,7 +67,7 @@ export default class WebDav {
throw new Error('WebDAV client not initialized')
}
- const remoteFilePath = `${this.webdavPath}/${filename}`
+ const remoteFilePath = path.posix.join(this.webdavPath, filename)
try {
return await this.instance.getFileContents(remoteFilePath, options)
@@ -120,7 +121,7 @@ export default class WebDav {
throw new Error('WebDAV client not initialized')
}
- const remoteFilePath = `${this.webdavPath}/${filename}`
+ const remoteFilePath = path.posix.join(this.webdavPath, filename)
try {
return await this.instance.deleteFile(remoteFilePath)
diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts
index 1cdcc2754..3f37d7c40 100644
--- a/src/main/services/WindowService.ts
+++ b/src/main/services/WindowService.ts
@@ -116,12 +116,6 @@ export class WindowService {
app.exit(1)
}
})
-
- mainWindow.webContents.on('unresponsive', () => {
- // 在升级到electron 34后,可以获取具体js stack trace,目前只打个日志监控下
- // https://www.electronjs.org/blog/electron-34-0#unresponsive-renderer-javascript-call-stacks
- Logger.error('Renderer process unresponsive')
- })
}
private setupMaximize(mainWindow: BrowserWindow, isMaximized: boolean) {
@@ -544,6 +538,25 @@ export class WindowService {
public setPinMiniWindow(isPinned) {
this.isPinnedMiniWindow = isPinned
}
+
+ /**
+ * 引用文本到主窗口
+ * @param text 原始文本(未格式化)
+ */
+ public quoteToMainWindow(text: string): void {
+ try {
+ this.showMainWindow()
+
+ const mainWindow = this.getMainWindow()
+ if (mainWindow && !mainWindow.isDestroyed()) {
+ setTimeout(() => {
+ mainWindow.webContents.send(IpcChannel.App_QuoteToMain, text)
+ }, 100)
+ }
+ } catch (error) {
+ Logger.error('Failed to quote to main window:', error as Error)
+ }
+ }
}
export const windowService = WindowService.getInstance()
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 6cf72d437..9f3b893d7 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -1,5 +1,6 @@
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { electronAPI } from '@electron-toolkit/preload'
+import { FeedUrl } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import {
FileListResponse,
@@ -31,6 +32,7 @@ const api = {
setLaunchToTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetLaunchToTray, isActive),
setTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTray, isActive),
setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive),
+ setFeedUrl: (feedUrl: FeedUrl) => ipcRenderer.invoke(IpcChannel.App_SetFeedUrl, feedUrl),
setTheme: (theme: ThemeMode) => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
handleZoomFactor: (delta: number, reset: boolean = false) =>
ipcRenderer.invoke(IpcChannel.App_HandleZoomFactor, delta, reset),
@@ -107,7 +109,7 @@ const api = {
getPathForFile: (file: File) => webUtils.getPathForFile(file)
},
fs: {
- read: (path: string) => ipcRenderer.invoke(IpcChannel.Fs_Read, path)
+ read: (pathOrUrl: string, encoding?: BufferEncoding) => ipcRenderer.invoke(IpcChannel.Fs_Read, pathOrUrl, encoding)
},
export: {
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_Word, markdown, fileName)
@@ -252,7 +254,8 @@ const api = {
closeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowClose),
minimizeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowMinimize),
pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned)
- }
+ },
+ quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text)
}
// Use `contextBridge` APIs to expose Electron APIs to
diff --git a/src/renderer/src/assets/images/providers/netease-youdao.svg b/src/renderer/src/assets/images/providers/netease-youdao.svg
new file mode 100644
index 000000000..959c48e23
--- /dev/null
+++ b/src/renderer/src/assets/images/providers/netease-youdao.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/renderer/src/assets/images/providers/nomic.png b/src/renderer/src/assets/images/providers/nomic.png
new file mode 100644
index 000000000..350377877
Binary files /dev/null and b/src/renderer/src/assets/images/providers/nomic.png differ
diff --git a/src/renderer/src/assets/styles/container.scss b/src/renderer/src/assets/styles/container.scss
index ab5e8a7de..8be402798 100644
--- a/src/renderer/src/assets/styles/container.scss
+++ b/src/renderer/src/assets/styles/container.scss
@@ -4,13 +4,3 @@
border-top-left-radius: 10px;
border-left: 0.5px solid var(--color-border);
}
-
-.group-container {
- .context-menu-container {
- width: 100%;
- }
-}
-
-.context-menu-container {
- max-width: 100%;
-}
diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss
index 91778848b..b91b3c3a5 100644
--- a/src/renderer/src/assets/styles/index.scss
+++ b/src/renderer/src/assets/styles/index.scss
@@ -129,22 +129,29 @@ ul {
.message-content-container {
margin: 5px 0;
border-radius: 8px;
- padding: 10px 15px 0 15px;
+ padding: 0.5rem 1rem;
}
+
+ .block-wrapper {
+ display: flow-root;
+ }
+
+ .message-content-container > *:last-child {
+ margin-bottom: 0;
+ }
+
.message-thought-container {
margin-top: 8px;
}
+
.message-user {
color: var(--chat-text-user);
- .markdown,
- .anticon,
- .iconfont,
- .lucide,
- .message-tokens {
+ .message-content-container-user .anticon {
color: var(--chat-text-user) !important;
}
- .message-action-button:hover {
- background-color: var(--color-white-soft);
+
+ .markdown {
+ color: var(--chat-text-user);
}
}
.group-grid-container.horizontal,
@@ -165,6 +172,12 @@ ul {
code {
color: var(--color-text);
}
+ .markdown {
+ display: flow-root;
+ *:last-child {
+ margin-bottom: 0;
+ }
+ }
}
.lucide {
diff --git a/src/renderer/src/components/CodeBlockView/CodePreview.tsx b/src/renderer/src/components/CodeBlockView/CodePreview.tsx
index c0ebcd9a7..d3c56f295 100644
--- a/src/renderer/src/components/CodeBlockView/CodePreview.tsx
+++ b/src/renderer/src/components/CodeBlockView/CodePreview.tsx
@@ -134,26 +134,31 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => {
return () => cleanupTokenizers(callerId)
}, [callerId, cleanupTokenizers])
- // 处理第二次开始的代码高亮
+ // 触发代码高亮
+ // - 进入视口后触发第一次高亮
+ // - 内容变化后触发之后的高亮
useEffect(() => {
- if (prevCodeLengthRef.current > 0) {
- setTimeout(highlightCode, 0)
- }
- }, [highlightCode])
-
- // 视口检测逻辑,只处理第一次代码高亮
- useEffect(() => {
- const codeElement = codeContentRef.current
- if (!codeElement || prevCodeLengthRef.current > 0) return
-
let isMounted = true
- const observer = new IntersectionObserver((entries) => {
- if (entries[0].isIntersecting && isMounted) {
- setTimeout(highlightCode, 0)
- observer.disconnect()
+ if (prevCodeLengthRef.current > 0) {
+ setTimeout(highlightCode, 0)
+ return
+ }
+
+ const codeElement = codeContentRef.current
+ if (!codeElement) return
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ if (entries[0].intersectionRatio > 0 && isMounted) {
+ setTimeout(highlightCode, 0)
+ observer.disconnect()
+ }
+ },
+ {
+ rootMargin: '50px 0px 50px 0px'
}
- })
+ )
observer.observe(codeElement)
diff --git a/src/renderer/src/components/CodeEditor/index.tsx b/src/renderer/src/components/CodeEditor/index.tsx
index c10fc5e8a..d92fd91e8 100644
--- a/src/renderer/src/components/CodeEditor/index.tsx
+++ b/src/renderer/src/components/CodeEditor/index.tsx
@@ -26,6 +26,7 @@ interface Props {
onSave?: (newContent: string) => void
onChange?: (newContent: string) => void
setTools?: (value: React.SetStateAction) => void
+ height?: string
minHeight?: string
maxHeight?: string
/** 用于覆写编辑器的某些设置 */
@@ -54,6 +55,7 @@ const CodeEditor = ({
onSave,
onChange,
setTools,
+ height,
minHeight,
maxHeight,
options,
@@ -193,6 +195,7 @@ const CodeEditor = ({
value={initialContent.current}
placeholder={placeholder}
width="100%"
+ height={height}
minHeight={minHeight}
maxHeight={collapsible && !isExpanded ? (maxHeight ?? '350px') : 'none'}
editable={true}
diff --git a/src/renderer/src/components/ContextMenu/index.tsx b/src/renderer/src/components/ContextMenu/index.tsx
index 61d51f370..195fcb2a3 100644
--- a/src/renderer/src/components/ContextMenu/index.tsx
+++ b/src/renderer/src/components/ContextMenu/index.tsx
@@ -1,4 +1,3 @@
-import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Dropdown } from 'antd'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -7,12 +6,12 @@ import styled from 'styled-components'
interface ContextMenuProps {
children: React.ReactNode
onContextMenu?: (e: React.MouseEvent) => void
+ style?: React.CSSProperties
}
-const ContextMenu: React.FC = ({ children, onContextMenu }) => {
+const ContextMenu: React.FC = ({ children, onContextMenu, style }) => {
const { t } = useTranslation()
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null)
- const [selectedQuoteText, setSelectedQuoteText] = useState('')
const [selectedText, setSelectedText] = useState('')
const handleContextMenu = useCallback(
@@ -20,12 +19,6 @@ const ContextMenu: React.FC = ({ children, onContextMenu }) =>
e.preventDefault()
const _selectedText = window.getSelection()?.toString()
if (_selectedText) {
- const quotedText =
- _selectedText
- .split('\n')
- .map((line) => `> ${line}`)
- .join('\n') + '\n-------------'
- setSelectedQuoteText(quotedText)
setContextMenuPosition({ x: e.clientX, y: e.clientY })
setSelectedText(_selectedText)
}
@@ -45,7 +38,7 @@ const ContextMenu: React.FC = ({ children, onContextMenu }) =>
}, [])
// 获取右键菜单项
- const getContextMenuItems = (t: (key: string) => string, selectedQuoteText: string, selectedText: string) => [
+ const getContextMenuItems = (t: (key: string) => string, selectedText: string) => [
{
key: 'copy',
label: t('common.copy'),
@@ -66,19 +59,19 @@ const ContextMenu: React.FC = ({ children, onContextMenu }) =>
key: 'quote',
label: t('chat.message.quote'),
onClick: () => {
- if (selectedQuoteText) {
- EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText)
+ if (selectedText) {
+ window.api?.quoteToMainWindow(selectedText)
}
}
}
]
return (
-
+
{contextMenuPosition && (
diff --git a/src/renderer/src/components/ImageViewer.tsx b/src/renderer/src/components/ImageViewer.tsx
new file mode 100644
index 000000000..e9f9be169
--- /dev/null
+++ b/src/renderer/src/components/ImageViewer.tsx
@@ -0,0 +1,141 @@
+import {
+ CopyOutlined,
+ DownloadOutlined,
+ FileImageOutlined,
+ RotateLeftOutlined,
+ RotateRightOutlined,
+ SwapOutlined,
+ UndoOutlined,
+ ZoomInOutlined,
+ ZoomOutOutlined
+} from '@ant-design/icons'
+import { download } from '@renderer/utils/download'
+import { Dropdown, Image as AntImage, ImageProps as AntImageProps, Space } from 'antd'
+import { Base64 } from 'js-base64'
+import mime from 'mime'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import styled from 'styled-components'
+
+interface ImageViewerProps extends AntImageProps {
+ src: string
+}
+
+const ImageViewer: React.FC = ({ src, style, ...props }) => {
+ const { t } = useTranslation()
+
+ // 复制图片到剪贴板
+ const handleCopyImage = async (src: string) => {
+ try {
+ if (src.startsWith('data:')) {
+ // 处理 base64 格式的图片
+ const match = src.match(/^data:(image\/\w+);base64,(.+)$/)
+ if (!match) throw new Error('无效的 base64 图片格式')
+ const mimeType = match[1]
+ const byteArray = Base64.toUint8Array(match[2])
+ const blob = new Blob([byteArray], { type: mimeType })
+ await navigator.clipboard.write([new ClipboardItem({ [mimeType]: blob })])
+ } else if (src.startsWith('file://')) {
+ // 处理本地文件路径
+ const bytes = await window.api.fs.read(src)
+ const mimeType = mime.getType(src) || 'application/octet-stream'
+ const blob = new Blob([bytes], { type: mimeType })
+ await navigator.clipboard.write([
+ new ClipboardItem({
+ [mimeType]: blob
+ })
+ ])
+ } else {
+ // 处理 URL 格式的图片
+ const response = await fetch(src)
+ const blob = await response.blob()
+
+ await navigator.clipboard.write([
+ new ClipboardItem({
+ [blob.type]: blob
+ })
+ ])
+ }
+
+ window.message.success(t('message.copy.success'))
+ } catch (error) {
+ console.error('复制图片失败:', error)
+ window.message.error(t('message.copy.failed'))
+ }
+ }
+
+ const getContextMenuItems = (src: string) => {
+ return [
+ {
+ key: 'copy-url',
+ label: t('common.copy'),
+ icon: ,
+ onClick: () => {
+ navigator.clipboard.writeText(src)
+ window.message.success(t('message.copy.success'))
+ }
+ },
+ {
+ key: 'download',
+ label: t('common.download'),
+ icon: ,
+ onClick: () => download(src)
+ },
+ {
+ key: 'copy-image',
+ label: t('code_block.preview.copy.image'),
+ icon: ,
+ onClick: () => handleCopyImage(src)
+ }
+ ]
+ }
+
+ return (
+
+ (
+
+
+
+
+
+
+
+
+ handleCopyImage(src)} />
+ download(src)} />
+
+ )
+ }}
+ />
+
+ )
+}
+
+const ToolbarWrapper = styled(Space)`
+ padding: 0px 24px;
+ color: #fff;
+ font-size: 20px;
+ background-color: rgba(0, 0, 0, 0.1);
+ border-radius: 100px;
+ .anticon {
+ padding: 12px;
+ cursor: pointer;
+ }
+ .anticon:hover {
+ opacity: 0.3;
+ }
+`
+
+export default ImageViewer
diff --git a/src/renderer/src/components/ObsidianExportDialog.tsx b/src/renderer/src/components/ObsidianExportDialog.tsx
index 631c085ca..dfc4cc09e 100644
--- a/src/renderer/src/components/ObsidianExportDialog.tsx
+++ b/src/renderer/src/components/ObsidianExportDialog.tsx
@@ -20,10 +20,16 @@ interface FileInfo {
name: string
}
+const ObsidianProcessingMethod = {
+ APPEND: '1',
+ PREPEND: '2',
+ NEW_OR_OVERWRITE: '3'
+} as const
+
interface PopupContainerProps {
title: string
obsidianTags: string | null
- processingMethod: string | '3'
+ processingMethod: (typeof ObsidianProcessingMethod)[keyof typeof ObsidianProcessingMethod]
open: boolean
resolve: (success: boolean) => void
message?: Message
@@ -230,10 +236,10 @@ const PopupContainer: React.FC = ({
markdown = ''
}
let content = ''
- if (state.processingMethod !== '3') {
+ if (state.processingMethod !== ObsidianProcessingMethod.NEW_OR_OVERWRITE) {
content = `\n---\n${markdown}`
} else {
- content = `---\n\ntitle: ${state.title}\ncreated: ${state.createdAt}\nsource: ${state.source}\ntags: ${state.tags}\n---\n${markdown}`
+ content = `---\ntitle: ${state.title}\ncreated: ${state.createdAt}\nsource: ${state.source}\ntags: ${state.tags}\n---\n${markdown}`
}
if (content === '') {
window.message.error(i18n.t('chat.topics.export.obsidian_export_failed'))
@@ -280,9 +286,9 @@ const PopupContainer: React.FC = ({
const titleWithoutExt = fileName.endsWith('.md') ? fileName.substring(0, fileName.length - 3) : fileName
handleChange('title', titleWithoutExt)
setHasTitleBeenManuallyEdited(false)
- handleChange('processingMethod', '1')
+ handleChange('processingMethod', ObsidianProcessingMethod.APPEND)
} else {
- handleChange('processingMethod', '3')
+ handleChange('processingMethod', ObsidianProcessingMethod.NEW_OR_OVERWRITE)
if (!hasTitleBeenManuallyEdited) {
handleChange('title', title)
}
@@ -390,9 +396,15 @@ const PopupContainer: React.FC = ({
onChange={(value) => handleChange('processingMethod', value)}
placeholder={i18n.t('chat.topics.export.obsidian_operate_placeholder')}
allowClear>
- {i18n.t('chat.topics.export.obsidian_operate_append')}
- {i18n.t('chat.topics.export.obsidian_operate_prepend')}
- {i18n.t('chat.topics.export.obsidian_operate_new_or_overwrite')}
+
+ {i18n.t('chat.topics.export.obsidian_operate_append')}
+
+
+ {i18n.t('chat.topics.export.obsidian_operate_prepend')}
+
+
+ {i18n.t('chat.topics.export.obsidian_operate_new_or_overwrite')}
+
@@ -403,4 +415,4 @@ const PopupContainer: React.FC = ({
)
}
-export { PopupContainer }
+export { ObsidianProcessingMethod, PopupContainer }
diff --git a/src/renderer/src/components/Popups/ObsidianExportPopup.tsx b/src/renderer/src/components/Popups/ObsidianExportPopup.tsx
index 49dc320c7..aec5fcbaa 100644
--- a/src/renderer/src/components/Popups/ObsidianExportPopup.tsx
+++ b/src/renderer/src/components/Popups/ObsidianExportPopup.tsx
@@ -1,11 +1,11 @@
-import { PopupContainer } from '@renderer/components/ObsidianExportDialog'
+import { ObsidianProcessingMethod, PopupContainer } from '@renderer/components/ObsidianExportDialog'
import { TopView } from '@renderer/components/TopView'
import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
interface ObsidianExportOptions {
title: string
- processingMethod: string | '3'
+ processingMethod: (typeof ObsidianProcessingMethod)[keyof typeof ObsidianProcessingMethod]
topic?: Topic
message?: Message
messages?: Message[]
diff --git a/src/renderer/src/components/TopView/index.tsx b/src/renderer/src/components/TopView/index.tsx
index 4638e0db0..91d5cc42c 100644
--- a/src/renderer/src/components/TopView/index.tsx
+++ b/src/renderer/src/components/TopView/index.tsx
@@ -65,7 +65,7 @@ const TopViewContainer: React.FC = ({ children }) => {
const FullScreenContainer: React.FC = useCallback(({ children }) => {
return (
-
+
{children}
diff --git a/src/renderer/src/components/WebdavBackupManager.tsx b/src/renderer/src/components/WebdavBackupManager.tsx
index 1c589736b..a434e3c63 100644
--- a/src/renderer/src/components/WebdavBackupManager.tsx
+++ b/src/renderer/src/components/WebdavBackupManager.tsx
@@ -14,9 +14,9 @@ interface BackupFile {
interface WebdavConfig {
webdavHost: string
- webdavUser: string
- webdavPass: string
- webdavPath: string
+ webdavUser?: string
+ webdavPass?: string
+ webdavPath?: string
}
interface WebdavBackupManagerProps {
@@ -47,7 +47,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
const { webdavHost, webdavUser, webdavPass, webdavPath } = webdavConfig
const fetchBackupFiles = useCallback(async () => {
- if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
+ if (!webdavHost) {
message.error(t('message.error.invalid.webdav'))
return
}
@@ -93,7 +93,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
return
}
- if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
+ if (!webdavHost) {
message.error(t('message.error.invalid.webdav'))
return
}
@@ -132,7 +132,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
}
const handleDeleteSingle = async (fileName: string) => {
- if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
+ if (!webdavHost) {
message.error(t('message.error.invalid.webdav'))
return
}
@@ -165,7 +165,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
}
const handleRestore = async (fileName: string) => {
- if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
+ if (!webdavHost) {
message.error(t('message.error.invalid.webdav'))
return
}
diff --git a/src/renderer/src/components/WebdavModals.tsx b/src/renderer/src/components/WebdavModals.tsx
index 2dc8f1d3b..a96c96fde 100644
--- a/src/renderer/src/components/WebdavModals.tsx
+++ b/src/renderer/src/components/WebdavModals.tsx
@@ -123,7 +123,7 @@ export function useWebdavRestoreModal({
const [backupFiles, setBackupFiles] = useState([])
const showRestoreModal = useCallback(async () => {
- if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
+ if (!webdavHost) {
window.message.error({ content: t('message.error.invalid.webdav'), key: 'webdav-error' })
return
}
@@ -146,7 +146,7 @@ export function useWebdavRestoreModal({
}, [webdavHost, webdavUser, webdavPass, webdavPath, t])
const handleRestore = useCallback(async () => {
- if (!selectedFile || !webdavHost || !webdavUser || !webdavPass || !webdavPath) {
+ if (!selectedFile || !webdavHost) {
window.message.error({
content: !selectedFile ? t('message.error.no.file.selected') : t('message.error.invalid.webdav'),
key: 'restore-error'
@@ -170,7 +170,7 @@ export function useWebdavRestoreModal({
}
}
})
- }, [selectedFile, webdavHost, webdavUser, webdavPass, webdavPath, t, restoreMethod])
+ }, [selectedFile, webdavHost, t, restoreMethod])
const handleCancel = () => {
setIsRestoreModalVisible(false)
diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx
index e5fe83b98..e6176c2a0 100644
--- a/src/renderer/src/components/app/Navbar.tsx
+++ b/src/renderer/src/components/app/Navbar.tsx
@@ -34,6 +34,15 @@ export const NavbarRight: FC = ({ children, ...props }) => {
)
}
+export const NavbarMain: FC = ({ children, ...props }) => {
+ const isFullscreen = useFullscreen()
+ return (
+
+ {children}
+
+ )
+}
+
const NavbarContainer = styled.div`
min-width: 100%;
display: flex;
@@ -72,3 +81,15 @@ const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
justify-content: flex-end;
`
+
+const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
+ flex: 1;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 ${isMac ? '20px' : 0};
+ font-weight: bold;
+ color: var(--color-text-1);
+ padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
+`
diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts
index 0b0afc398..07581eadb 100644
--- a/src/renderer/src/config/models.ts
+++ b/src/renderer/src/config/models.ts
@@ -140,6 +140,8 @@ import XirangModelLogo from '@renderer/assets/images/models/xirang.png'
import XirangModelLogoDark from '@renderer/assets/images/models/xirang_dark.png'
import YiModelLogo from '@renderer/assets/images/models/yi.png'
import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png'
+import YoudaoLogo from '@renderer/assets/images/providers/netease-youdao.svg'
+import NomicLogo from '@renderer/assets/images/providers/nomic.png'
import { getProviderByModel } from '@renderer/services/AssistantService'
import { Assistant, Model } from '@renderer/types'
import OpenAI from 'openai'
@@ -297,7 +299,7 @@ export function getModelLogo(modelId: string) {
'davinci-': isLight ? ChatGptModelLogo : ChatGptModelLogoDakr,
glm: isLight ? ChatGLMModelLogo : ChatGLMModelLogoDark,
deepseek: isLight ? DeepSeekModelLogo : DeepSeekModelLogoDark,
- '(qwen|qwq-|qvq-)': isLight ? QwenModelLogo : QwenModelLogoDark,
+ '(qwen|qwq|qwq-|qvq-)': isLight ? QwenModelLogo : QwenModelLogoDark,
gemma: isLight ? GemmaModelLogo : GemmaModelLogoDark,
'yi-': isLight ? YiModelLogo : YiModelLogoDark,
llama: isLight ? LlamaModelLogo : LlamaModelLogoDark,
@@ -376,12 +378,14 @@ export function getModelLogo(modelId: string) {
'google/': isLight ? GoogleModelLogo : GoogleModelLogoDark,
xirang: isLight ? XirangModelLogo : XirangModelLogoDark,
hugging: isLight ? HuggingfaceModelLogo : HuggingfaceModelLogoDark,
+ youdao: YoudaoLogo,
embedding: isLight ? EmbeddingModelLogo : EmbeddingModelLogoDark,
perplexity: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
sonar: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
'bge-': BgeModelLogo,
'voyage-': VoyageModelLogo,
- tokenflux: isLight ? TokenFluxModelLogo : TokenFluxModelLogoDark
+ tokenflux: isLight ? TokenFluxModelLogo : TokenFluxModelLogoDark,
+ 'nomic-': NomicLogo
}
for (const key in logoMap) {
@@ -395,6 +399,37 @@ export function getModelLogo(modelId: string) {
}
export const SYSTEM_MODELS: Record = {
+ defaultModel: [
+ {
+ // 默认助手模型
+ id: 'deepseek-ai/DeepSeek-V3',
+ name: 'deepseek-ai/DeepSeek-V3',
+ provider: 'silicon',
+ group: 'deepseek-ai'
+ },
+ {
+ // 默认话题命名模型
+ id: 'Qwen/Qwen3-8B',
+ name: 'Qwen/Qwen3-8B',
+ provider: 'silicon',
+ group: 'Qwen'
+ },
+ {
+ // 默认翻译模型
+ id: 'deepseek-ai/DeepSeek-V3',
+ name: 'deepseek-ai/DeepSeek-V3',
+ provider: 'silicon',
+ group: 'deepseek-ai'
+ },
+ {
+ // 默认快捷助手模型
+ id: 'deepseek-ai/DeepSeek-V3',
+ name: 'deepseek-ai/DeepSeek-V3',
+ provider: 'silicon',
+ group: 'deepseek-ai'
+ }
+ ],
+
aihubmix: [
{
id: 'gpt-4o',
@@ -600,17 +635,17 @@ export const SYSTEM_MODELS: Record = {
name: 'Qwen2.5-7B-Instruct',
group: 'Qwen'
},
- {
- id: 'meta-llama/Llama-3.3-70B-Instruct',
- name: 'meta-llama/Llama-3.3-70B-Instruct',
- provider: 'silicon',
- group: 'meta-llama'
- },
{
id: 'BAAI/bge-m3',
name: 'BAAI/bge-m3',
provider: 'silicon',
group: 'BAAI'
+ },
+ {
+ id: 'Qwen/Qwen3-8B',
+ name: 'Qwen/Qwen3-8B',
+ provider: 'silicon',
+ group: 'Qwen'
}
],
ppio: [
@@ -1709,24 +1744,6 @@ export const SYSTEM_MODELS: Record = {
name: 'ERNIE-Speed-128K',
group: '免费模型'
},
- {
- id: 'THUDM/glm-4-9b-chat',
- provider: 'dmxapi',
- name: 'THUDM/glm-4-9b-chat',
- group: '免费模型'
- },
- {
- id: 'glm-4-flash',
- provider: 'dmxapi',
- name: 'glm-4-flash',
- group: '免费模型'
- },
- {
- id: 'hunyuan-lite',
- provider: 'dmxapi',
- name: 'hunyuan-lite',
- group: '免费模型'
- },
{
id: 'gpt-4o',
provider: 'dmxapi',
@@ -2631,7 +2648,8 @@ export function groupQwenModels(models: Model[]): Record {
export const THINKING_TOKEN_MAP: Record = {
// Gemini models
- 'gemini-.*$': { min: 0, max: 24576 },
+ 'gemini-.*-flash.*$': { min: 0, max: 24576 },
+ 'gemini-.*-pro.*$': { min: 128, max: 32768 },
// Qwen models
'qwen-plus-.*$': { min: 0, max: 38912 },
diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts
index f1a1f7568..c83522f35 100644
--- a/src/renderer/src/config/providers.ts
+++ b/src/renderer/src/config/providers.ts
@@ -169,7 +169,7 @@ export const PROVIDER_CONFIG = {
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'
+ models: 'https://cloud.siliconflow.cn/models'
}
},
'gitee-ai': {
diff --git a/src/renderer/src/databases/upgrades.ts b/src/renderer/src/databases/upgrades.ts
index fae8a7471..cb1e770db 100644
--- a/src/renderer/src/databases/upgrades.ts
+++ b/src/renderer/src/databases/upgrades.ts
@@ -281,7 +281,6 @@ export async function upgradeToV7(tx: Transaction): Promise {
modelId: oldMessage.modelId,
model: oldMessage.model,
type: oldMessage.type === 'clear' ? 'clear' : undefined,
- isPreset: oldMessage.isPreset,
useful: oldMessage.useful,
askId: oldMessage.askId,
mentions: oldMessage.mentions,
diff --git a/src/renderer/src/hooks/useSettings.ts b/src/renderer/src/hooks/useSettings.ts
index c058a9523..10f9ee900 100644
--- a/src/renderer/src/hooks/useSettings.ts
+++ b/src/renderer/src/hooks/useSettings.ts
@@ -4,6 +4,7 @@ import {
SendMessageShortcut,
setAssistantIconType,
setAutoCheckUpdate as _setAutoCheckUpdate,
+ setEarlyAccess as _setEarlyAccess,
setLaunchOnBoot,
setLaunchToTray,
setPinTopicsToTop,
@@ -19,6 +20,7 @@ import {
setWindowStyle
} from '@renderer/store/settings'
import { SidebarIcon, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
+import { FeedUrl } from '@shared/config/constant'
export function useSettings() {
const settings = useAppSelector((state) => state.settings)
@@ -58,6 +60,11 @@ export function useSettings() {
window.api.setAutoUpdate(isAutoUpdate)
},
+ setEarlyAccess(isEarlyAccess: boolean) {
+ dispatch(_setEarlyAccess(isEarlyAccess))
+ window.api.setFeedUrl(isEarlyAccess ? FeedUrl.EARLY_ACCESS : FeedUrl.PRODUCTION)
+ },
+
setTheme(theme: ThemeMode) {
dispatch(setTheme(theme))
},
diff --git a/src/renderer/src/hooks/useTags.ts b/src/renderer/src/hooks/useTags.ts
index 50b3f2a78..44d43377c 100644
--- a/src/renderer/src/hooks/useTags.ts
+++ b/src/renderer/src/hooks/useTags.ts
@@ -1,27 +1,65 @@
+import { createSelector } from '@reduxjs/toolkit'
+import { RootState, useAppDispatch, useAppSelector } from '@renderer/store'
+import { setTagsOrder, updateAssistants } from '@renderer/store/assistants'
import { flatMap, groupBy, uniq } from 'lodash'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useAssistants } from './useAssistant'
+// 基础选择器
+const selectAssistantsState = (state: RootState) => state.assistants
+// 记忆化 tagsOrder 选择器(自动处理默认值)--- 这是一个选择器,用于从 store 中获取 tagsOrder 的值。因为之前的tagsOrder是后面新加的,不这样做会报错,所以这里需要处理一下默认值
+const selectTagsOrder = createSelector([selectAssistantsState], (assistants) => assistants.tagsOrder ?? [])
+
// 定义useTags的返回类型,包含所有标签和获取特定标签的助手函数
// 为了不增加新的概念,标签直接作为助手的属性,所以这里的标签是指助手的标签属性
// 但是为了方便管理,增加了一个获取特定标签的助手函数
-
export const useTags = () => {
const { assistants } = useAssistants()
const { t } = useTranslation()
+ const dispatch = useAppDispatch()
+ const savedTagsOrder = useAppSelector(selectTagsOrder)
// 计算所有标签
const allTags = useMemo(() => {
- return uniq(flatMap(assistants, (assistant) => assistant.tags || []))
- }, [assistants])
+ const tags = uniq(flatMap(assistants, (assistant) => assistant.tags || []))
+ if (savedTagsOrder.length > 0) {
+ return [
+ ...savedTagsOrder.filter((tag) => tags.includes(tag)),
+ ...tags.filter((tag) => !savedTagsOrder.includes(tag))
+ ]
+ }
+ return tags
+ }, [assistants, savedTagsOrder])
const getAssistantsByTag = useCallback(
(tag: string) => assistants.filter((assistant) => assistant.tags?.includes(tag)),
[assistants]
)
+ const updateTagsOrder = useCallback(
+ (newOrder: string[]) => {
+ dispatch(setTagsOrder(newOrder))
+ updateAssistants(
+ assistants.map((assistant) => {
+ if (!assistant.tags || assistant.tags.length === 0) {
+ return assistant
+ }
+ const newTags = [...assistant.tags]
+ newTags.sort((a, b) => {
+ return newOrder.indexOf(a) - newOrder.indexOf(b)
+ })
+ return {
+ ...assistant,
+ tags: newTags
+ }
+ })
+ )
+ },
+ [assistants, dispatch]
+ )
+
const getGroupedAssistants = useMemo(() => {
// 按标签分组,处理多标签的情况
const assistantsByTags = flatMap(assistants, (assistant) => {
@@ -42,12 +80,30 @@ export const useTags = () => {
grouped.unshift(untagged)
}
+ // 根据savedTagsOrder对标签组进行排序
+ if (savedTagsOrder.length > 0) {
+ const untagged = grouped.length > 0 && grouped[0].tag === t('assistants.tags.untagged') ? grouped.shift() : null
+ grouped.sort((a, b) => {
+ const indexA = savedTagsOrder.indexOf(a.tag)
+ const indexB = savedTagsOrder.indexOf(b.tag)
+ if (indexA === -1 && indexB === -1) return 0
+ if (indexA === -1) return 1
+ if (indexB === -1) return -1
+
+ return indexA - indexB
+ })
+ if (untagged) {
+ grouped.unshift(untagged)
+ }
+ }
+
return grouped
- }, [assistants, t])
+ }, [assistants, t, savedTagsOrder])
return {
allTags,
getAssistantsByTag,
- getGroupedAssistants
+ getGroupedAssistants,
+ updateTagsOrder
}
}
diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json
index 00e4bc70b..b042b1346 100644
--- a/src/renderer/src/i18n/locales/en-us.json
+++ b/src/renderer/src/i18n/locales/en-us.json
@@ -8,7 +8,10 @@
"add.name.placeholder": "Enter name",
"add.prompt": "Prompt",
"add.prompt.placeholder": "Enter prompt",
- "add.prompt.variables.tip": "Available variables: {{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
+ "add.prompt.variables.tip": {
+ "title": "Available variables",
+ "content": "{{date}}:\tDate\n{{time}}:\tTime\n{{datetime}}:\tDate and time\n{{system}}:\tOperating system\n{{arch}}:\tCPU architecture\n{{language}}:\tLanguage\n{{model_name}}:\tModel name"
+ },
"add.title": "Create Agent",
"import": {
"title": "Import from External",
@@ -30,16 +33,7 @@
"agent": "Export Agent"
},
"delete.popup.content": "Are you sure you want to delete this agent?",
- "edit.message.add.title": "Add",
- "edit.message.assistant.placeholder": "Enter assistant message",
- "edit.message.assistant.title": "Assistant",
- "edit.message.empty.content": "Conversation input content cannot be empty",
- "edit.message.group.title": "Message Group",
- "edit.message.title": "Preset messages",
- "edit.message.user.placeholder": "Enter user message",
- "edit.message.user.title": "User",
"edit.model.select.title": "Select Model",
- "edit.settings.hide_preset_messages": "Hide Preset Message",
"edit.title": "Edit Agent",
"manage.title": "Manage Agents",
"my_agents": "My Agents",
@@ -76,7 +70,6 @@
"settings.mcp.noServersAvailable": "No MCP servers available. Add servers in settings",
"settings.mcp.description": "Default enabled MCP servers",
"settings.model": "Model Settings",
- "settings.preset_messages": "Preset Messages",
"settings.prompt": "Prompt Settings",
"settings.reasoning_effort": "Reasoning effort",
"settings.reasoning_effort.off": "Off",
@@ -268,6 +261,7 @@
"topics.clear.title": "Clear Messages",
"topics.copy.image": "Copy as image",
"topics.copy.md": "Copy as markdown",
+ "topics.copy.plain_text": "Copy as plain text (remove Markdown)",
"topics.copy.title": "Copy",
"topics.delete.shortcut": "Hold {{key}} to delete directly",
"topics.edit.placeholder": "Enter new name",
@@ -580,8 +574,12 @@
"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}})."
+ "dimensions_size_placeholder": " Embedding dimension size, e.g. 1024",
+ "dimensions_auto_set": "Auto-set embedding dimensions",
+ "dimensions_error_invalid": "Please enter embedding dimension size",
+ "dimensions_size_too_large": "The embedding dimension cannot exceed the model's context limit ({{max_context}}).",
+ "dimensions_set_right": "⚠️ Please ensure the model supports the set embedding dimension size",
+ "dimensions_default": "The model will use default embedding dimensions"
},
"languages": {
"arabic": "Arabic",
@@ -958,7 +956,10 @@
"magic_prompt_option_tip": "Intelligently enhances upscaling prompts"
},
"text_desc_required": "Please enter image description first",
+ "image_handle_required": "Please upload an image first.",
"req_error_text": "Operation failed. Please try again. Avoid using 'copyrighted' or 'sensitive' words in your prompt.",
+ "req_error_token": "Please check the validity of the token",
+ "req_error_no_balance": "Please check the validity of the token",
"auto_create_paint": "Auto-create image",
"auto_create_paint_tip": "After the image is generated, a new image will be created automatically.",
"select_model": "Select Model",
@@ -1353,6 +1354,8 @@
"general.emoji_picker": "Emoji Picker",
"general.image_upload": "Image Upload",
"general.auto_check_update.title": "Auto Update",
+ "general.early_access.title": "Early Access",
+ "general.early_access.tooltip": "Enable to use the latest version from GitHub, which may be slower. Please backup your data in advance.",
"general.reset.button": "Reset",
"general.reset.title": "Data Reset",
"general.restore.button": "Restore",
@@ -1522,6 +1525,7 @@
"messages.prompt": "Show prompt",
"messages.tokens": "Show token usage",
"messages.divider": "Show divider between messages",
+ "messages.divider.tooltip": "Not applicable to bubble-style message",
"messages.grid_columns": "Message grid display columns",
"messages.grid_popover_trigger": "Grid detail trigger",
"messages.grid_popover_trigger.click": "Click to display",
@@ -1554,6 +1558,7 @@
"models.add.model_id.select.placeholder": "Select Model",
"models.add.model_id.tooltip": "Example: gpt-3.5-turbo",
"models.add.model_name": "Model Name",
+ "models.add.model_name.tooltip": "Optional e.g. GPT-4",
"models.add.model_name.placeholder": "Optional e.g. GPT-4",
"models.check.all": "All",
"models.check.all_models_passed": "All models check passed",
@@ -1705,6 +1710,8 @@
"exit_fullscreen": "Exit Fullscreen",
"key": "Key",
"mini_window": "Quick Assistant",
+ "selection_assistant_toggle": "Toggle Selection Assistant",
+ "selection_assistant_select_text": "Selection Assistant: Select Text",
"new_topic": "New Topic",
"press_shortcut": "Press Shortcut",
"reset_defaults": "Reset Defaults",
@@ -1852,7 +1859,7 @@
"close": "Close",
"closed": "Translation closed",
"copied": "Translation content copied",
- "detected.language": "Detected Language",
+ "detected.language": "Auto Detect",
"empty": "Translation content is empty",
"not.found": "Translation content not found",
"confirm": {
@@ -1914,7 +1921,8 @@
"summary": "Summarize",
"search": "Search",
"refine": "Refine",
- "copy": "Copy"
+ "copy": "Copy",
+ "quote": "Quote"
},
"window": {
"pin": "Pin",
@@ -1942,10 +1950,15 @@
"title": "Toolbar",
"trigger_mode": {
"title": "Trigger Mode",
- "description": "Show toolbar immediately when text is selected, or show only when Ctrl key is held after selection.",
- "description_note": "The Ctrl key may not work in some apps. If you use AHK or other tools to remap the Ctrl key, it may not work.",
+ "description": "The way to trigger the selection assistant and show the toolbar",
+ "description_note": "Some applications do not support selecting text with the Ctrl key. If you have remapped the Ctrl key using tools like AHK, it may cause some applications to fail to select text.",
"selected": "Selection",
- "ctrlkey": "Ctrl Key"
+ "selected_note": "Show toolbar immediately when text is selected",
+ "ctrlkey": "Ctrl Key",
+ "ctrlkey_note": "After selection, hold down the Ctrl key to show the toolbar",
+ "shortcut": "Shortcut",
+ "shortcut_note": "After selection, use shortcut to show the toolbar. Please set the shortcut in the shortcut settings page and enable it. ",
+ "shortcut_link": "Go to Shortcut Settings"
},
"compact_mode": {
"title": "Compact Mode",
diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json
index fb45a779f..ee4cbc6a4 100644
--- a/src/renderer/src/i18n/locales/ja-jp.json
+++ b/src/renderer/src/i18n/locales/ja-jp.json
@@ -8,7 +8,10 @@
"add.name.placeholder": "名前を入力",
"add.prompt": "プロンプト",
"add.prompt.placeholder": "プロンプトを入力",
- "add.prompt.variables.tip": "利用可能な変数:{{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
+ "add.prompt.variables.tip": {
+ "title": "利用可能な変数",
+ "content": "{{date}}:\t日付\n{{time}}:\t時間\n{{datetime}}:\t日付と時間\n{{system}}:\tオペレーティングシステム\n{{arch}}:\tCPUアーキテクチャ\n{{language}}:\t言語\n{{model_name}}:\tモデル名"
+ },
"add.title": "エージェントを作成",
"import": {
"title": "外部からインポート",
@@ -30,16 +33,7 @@
"agent": "エージェントをエクスポート"
},
"delete.popup.content": "このエージェントを削除してもよろしいですか?",
- "edit.message.add.title": "追加",
- "edit.message.assistant.placeholder": "アシスタントのメッセージを入力",
- "edit.message.assistant.title": "アシスタント",
- "edit.message.empty.content": "会話の入力内容が空です",
- "edit.message.group.title": "メッセージグループ",
- "edit.message.title": "プリセットメッセージ",
- "edit.message.user.placeholder": "ユーザーメッセージを入力",
- "edit.message.user.title": "ユーザー",
"edit.model.select.title": "モデルを選択",
- "edit.settings.hide_preset_messages": "プリセットメッセージを非表示",
"edit.title": "エージェントを編集",
"manage.title": "エージェントを管理",
"my_agents": "マイエージェント",
@@ -76,7 +70,6 @@
"settings.default_model": "デフォルトモデル",
"settings.knowledge_base": "ナレッジベース設定",
"settings.model": "モデル設定",
- "settings.preset_messages": "プリセットメッセージ",
"settings.prompt": "プロンプト設定",
"settings.reasoning_effort": "思考連鎖の長さ",
"settings.reasoning_effort.off": "オフ",
@@ -268,6 +261,7 @@
"topics.clear.title": "メッセージをクリア",
"topics.copy.image": "画像としてコピー",
"topics.copy.md": "Markdownとしてコピー",
+ "topics.copy.plain_text": "プレーンテキストとしてコピー(Markdownを除去)",
"topics.copy.title": "コピー",
"topics.delete.shortcut": "{{key}}キーを押しながらで直接削除",
"topics.edit.placeholder": "新しい名前を入力",
@@ -576,12 +570,16 @@
"urls": "URL",
"dimensions": "埋め込み次元",
"dimensions_size_tooltip": "埋め込み次元のサイズは、数値が大きいほど埋め込み次元も大きくなりますが、消費するトークンも増えます。",
- "dimensions_size_placeholder": "デフォルト値(変更はお勧めしません)",
- "dimensions_size_too_large": "埋め込み次元はモデルのコンテキスト制限({{max_context}})を超えてはなりません。",
"status_embedding_completed": "埋め込み完了",
"status_preprocess_completed": "前処理完了",
"status_embedding_failed": "埋め込み失敗",
- "status_preprocess_failed": "前処理に失敗しました"
+ "status_preprocess_failed": "前処理に失敗しました",
+ "dimensions_size_placeholder": " 埋め込み次元のサイズ(例:1024)",
+ "dimensions_auto_set": "埋め込み次元を自動設定",
+ "dimensions_error_invalid": "埋め込み次元のサイズを入力してください",
+ "dimensions_size_too_large": "埋め込み次元はモデルのコンテキスト制限({{max_context}})を超えてはなりません。",
+ "dimensions_set_right": "⚠️ モデルが設定した埋め込み次元のサイズをサポートしていることを確認してください",
+ "dimensions_default": "モデルはデフォルトの埋め込み次元を使用します"
},
"languages": {
"arabic": "アラビア語",
@@ -958,7 +956,10 @@
"rendering_speed": "レンダリング速度",
"translating": "翻訳中...",
"text_desc_required": "画像の説明を先に入力してください",
+ "image_handle_required": "最初に画像をアップロードしてください。",
"req_error_text": "実行に失敗しました。もう一度お試しください。プロンプトに「著作権用語」や「センシティブな用語」を含めないでください。",
+ "req_error_token": "トークンの有効性を確認してください",
+ "req_error_no_balance": "トークンの有効性を確認してください",
"auto_create_paint": "画像を自動作成",
"auto_create_paint_tip": "画像が生成された後、自動的に新しい画像が作成されます。",
"select_model": "モデルを選択",
@@ -1518,6 +1519,7 @@
"messages.prompt": "プロンプト表示",
"messages.tokens": "トークン使用量を表示",
"messages.divider": "メッセージ間に区切り線を表示",
+ "messages.divider.tooltip": "バブルスタイルのメッセージには適用されません",
"messages.grid_columns": "メッセージグリッドの表示列数",
"messages.grid_popover_trigger": "グリッド詳細トリガー",
"messages.grid_popover_trigger.click": "クリックで表示",
@@ -1550,7 +1552,8 @@
"models.add.model_id.select.placeholder": "モデルを選択",
"models.add.model_id.tooltip": "例:gpt-3.5-turbo",
"models.add.model_name": "モデル名",
- "models.add.model_name.placeholder": "例:GPT-3.5",
+ "models.add.model_name.tooltip": "例:GPT-4",
+ "models.add.model_name.placeholder": "例:GPT-4",
"models.check.all": "すべて",
"models.check.all_models_passed": "すべてのモデルチェックが成功しました",
"models.check.button_caption": "健康チェック",
@@ -1695,6 +1698,8 @@
"exit_fullscreen": "フルスクリーンを終了",
"key": "キー",
"mini_window": "クイックアシスタント",
+ "selection_assistant_toggle": "選択アシスタントを切り替え",
+ "selection_assistant_select_text": "選択アシスタント:テキストを選択",
"new_topic": "新しいトピック",
"press_shortcut": "ショートカットを押す",
"reset_defaults": "デフォルトのショートカットをリセット",
@@ -1788,6 +1793,8 @@
"tray.show": "トレイアイコンを表示",
"tray.title": "トレイ",
"general.auto_check_update.title": "自動更新",
+ "general.early_access.title": "早期アクセス",
+ "general.early_access.tooltip": "有効にすると、GitHub の最新バージョンを使用します。ダウンロード速度が遅く、不安定な場合があります。データを事前にバックアップしてください。",
"quickPhrase": {
"title": "クイックフレーズ",
"add": "フレーズを追加",
@@ -1885,7 +1892,7 @@
"menu": {
"description": "對當前輸入框內容進行翻譯"
},
- "detected.language": "検出された言語"
+ "detected.language": "自動検出"
},
"tray": {
"quit": "終了",
@@ -1914,7 +1921,8 @@
"summary": "要約",
"search": "検索",
"refine": "最適化",
- "copy": "コピー"
+ "copy": "コピー",
+ "quote": "引用"
},
"window": {
"pin": "最前面に固定",
@@ -1941,11 +1949,16 @@
"toolbar": {
"title": "ツールバー",
"trigger_mode": {
- "title": "表示方法",
- "description": "テキスト選択時に即時表示、またはCtrlキー押下時のみ表示",
- "description_note": "一部のアプリはCtrlキーでのテキスト選択に対応していません。AHKなどでCtrlキーをリマップすると、選択できなくなる場合があります。",
+ "title": "単語の取り出し方",
+ "description": "テキスト選択後、取詞ツールバーを表示する方法",
+ "description_note": "一部のアプリケーションでは、Ctrl キーでテキストを選択できません。AHK などのツールを使用して Ctrl キーを再マップした場合、一部のアプリケーションでテキスト選択が失敗する可能性があります。",
"selected": "選択時",
- "ctrlkey": "Ctrlキー"
+ "selected_note": "テキスト選択時に即時表示",
+ "ctrlkey": "Ctrlキー",
+ "ctrlkey_note": "テキスト選択後、Ctrlキーを押下して表示",
+ "shortcut": "ショートカットキー",
+ "shortcut_note": "テキスト選択後、ショートカットキーを押下して表示。ショートカットキーを設定するには、ショートカット設定ページで有効にしてください。",
+ "shortcut_link": "ショートカット設定ページに移動"
},
"compact_mode": {
"title": "コンパクトモード",
diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json
index f2321ddd9..ec9036f31 100644
--- a/src/renderer/src/i18n/locales/ru-ru.json
+++ b/src/renderer/src/i18n/locales/ru-ru.json
@@ -8,19 +8,13 @@
"add.name.placeholder": "Введите имя",
"add.prompt": "Промпт",
"add.prompt.placeholder": "Введите промпт",
- "add.prompt.variables.tip": "Доступные переменные: {{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
+ "add.prompt.variables.tip": {
+ "title": "Доступные переменные",
+ "content": "{{date}}:\tДата\n{{time}}:\tВремя\n{{datetime}}:\tДата и время\n{{system}}:\tОперационная система\n{{arch}}:\tАрхитектура процессора\n{{language}}:\tЯзык\n{{model_name}}:\tНазвание модели"
+ },
"add.title": "Создать агента",
"delete.popup.content": "Вы уверены, что хотите удалить этого агента?",
- "edit.message.add.title": "Добавить",
- "edit.message.assistant.placeholder": "Введите сообщение ассистента",
- "edit.message.assistant.title": "Ассистент",
- "edit.message.empty.content": "Содержание вводимого сообщения не может быть пустым",
- "edit.message.group.title": "Группа сообщений",
- "edit.message.title": "Предустановленные сообщения",
- "edit.message.user.placeholder": "Введите сообщение пользователя",
- "edit.message.user.title": "Пользователь",
"edit.model.select.title": "Выбрать модель",
- "edit.settings.hide_preset_messages": "Скрыть предустановленные сообщения",
"edit.title": "Редактировать агента",
"manage.title": "Редактировать агентов",
"my_agents": "Мои агенты",
@@ -76,7 +70,6 @@
"settings.default_model": "Модель по умолчанию",
"settings.knowledge_base": "Настройки базы знаний",
"settings.model": "Настройки модели",
- "settings.preset_messages": "Предустановленные сообщения",
"settings.prompt": "Настройки промптов",
"settings.reasoning_effort.off": "Выключить",
"settings.reasoning_effort.high": "Стараюсь думать",
@@ -268,6 +261,7 @@
"topics.clear.title": "Очистить сообщения",
"topics.copy.image": "Скопировать как изображение",
"topics.copy.md": "Скопировать как Markdown",
+ "topics.copy.plain_text": "Копировать как обычный текст (удалить Markdown)",
"topics.copy.title": "Скопировать",
"topics.delete.shortcut": "Удерживайте {{key}} для мгновенного удаления",
"topics.edit.placeholder": "Введите новый заголовок",
@@ -576,12 +570,16 @@
"urls": "URL-адреса",
"dimensions": "векторное пространство",
"dimensions_size_tooltip": "Размерность вложения, чем больше значение, тем больше размерность вложения, но и потребляемых токенов также становится больше.",
- "dimensions_size_placeholder": "Значение по умолчанию (не рекомендуется изменять)",
- "dimensions_size_too_large": "Размерность вложения не может превышать ограничение контекста модели ({{max_context}})",
"status_embedding_completed": "Вложение завершено",
"status_preprocess_completed": "Предварительная обработка завершена",
"status_embedding_failed": "Не удалось встроить",
- "status_preprocess_failed": "Предварительная обработка не удалась"
+ "status_preprocess_failed": "Предварительная обработка не удалась",
+ "dimensions_size_placeholder": " Размерность эмбеддинга, например 1024",
+ "dimensions_auto_set": "Автоматическая установка размерности эмбеддинга",
+ "dimensions_error_invalid": "Пожалуйста, введите размерность эмбеддинга",
+ "dimensions_size_too_large": "Размерность вложения не может превышать ограничение контекста модели ({{max_context}})",
+ "dimensions_set_right": "⚠️ Убедитесь, что модель поддерживает заданный размер эмбеддинга",
+ "dimensions_default": "Модель будет использовать размер эмбеддинга по умолчанию"
},
"languages": {
"arabic": "Арабский",
@@ -958,7 +956,10 @@
"magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов"
},
"text_desc_required": "Пожалуйста, сначала введите описание изображения",
+ "image_handle_required": "Пожалуйста, сначала загрузите изображение.",
"req_error_text": "Операция не удалась, повторите попытку. Пожалуйста, избегайте защищенных авторским правом терминов и конфиденциальных слов в запросах.",
+ "req_error_token": "Пожалуйста, проверьте действительность токена",
+ "req_error_no_balance": "Пожалуйста, проверьте действительность токена",
"auto_create_paint": "Автоматическое создание изображения",
"auto_create_paint_tip": "После генерации изображения будет автоматически создано новое.",
"select_model": "Выбрать модель",
@@ -1518,6 +1519,7 @@
"messages.prompt": "Показывать подсказки",
"messages.tokens": "Показать использование токенов",
"messages.divider": "Показывать разделитель между сообщениями",
+ "messages.divider.tooltip": "Не применимо к сообщениям в стиле пузырей",
"messages.grid_columns": "Количество столбцов сетки сообщений",
"messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке",
"messages.grid_popover_trigger.click": "Нажатие для отображения",
@@ -1550,6 +1552,7 @@
"models.add.model_id.select.placeholder": "Выберите модель",
"models.add.model_id.tooltip": "Пример: gpt-3.5-turbo",
"models.add.model_name": "Имя модели",
+ "models.add.model_name.tooltip": "Необязательно, например, GPT-4",
"models.add.model_name.placeholder": "Необязательно, например, GPT-4",
"models.check.all": "Все",
"models.check.all_models_passed": "Все модели прошли проверку",
@@ -1695,6 +1698,8 @@
"exit_fullscreen": "Выйти из полноэкранного режима",
"key": "Клавиша",
"mini_window": "Быстрый помощник",
+ "selection_assistant_toggle": "Переключить помощник выделения",
+ "selection_assistant_select_text": "Помощник выделения: выделить текст",
"new_topic": "Новый топик",
"press_shortcut": "Нажмите сочетание клавиш",
"reset_defaults": "Сбросить настройки по умолчанию",
@@ -1787,7 +1792,45 @@
"tray.onclose": "Свернуть в трей при закрытии",
"tray.show": "Показать значок в трее",
"tray.title": "Трей",
- "general.auto_check_update.title": "Включить автообновление",
+ "websearch": {
+ "blacklist": "Черный список",
+ "blacklist_description": "Результаты из следующих веб-сайтов не будут отображаться в результатах поиска",
+ "check": "проверка",
+ "check_failed": "Проверка не прошла",
+ "check_success": "Проверка успешна",
+ "get_api_key": "Получить ключ API",
+ "no_provider_selected": "Пожалуйста, выберите поставщика поисковых услуг, затем проверьте.",
+ "search_max_result": "Количество результатов поиска",
+ "search_provider": "поиск сервисного провайдера",
+ "search_provider_placeholder": "Выберите поставщика поисковых услуг",
+ "search_result_default": "По умолчанию",
+ "search_with_time": "Поиск, содержащий дату",
+ "tavily": {
+ "api_key": "Ключ API Tavily",
+ "api_key.placeholder": "Введите ключ API Tavily",
+ "description": "Tavily — это поисковая система, специально разработанная для ИИ-агентов, предоставляющая актуальные результаты, умные предложения по запросам и глубокие исследовательские возможности",
+ "title": "Tavily"
+ },
+ "title": "Поиск в Интернете",
+ "blacklist_tooltip": "Шаблон: *://*.example.com/*\nРегулярное выражение: /example\\.(net|org)/",
+ "subscribe": "Подписка на черный список",
+ "subscribe_update": "Обновить",
+ "subscribe_add": "Добавить",
+ "subscribe_url": "URL подписки",
+ "subscribe_name": "Альтернативное имя",
+ "subscribe_name.placeholder": "Альтернативное имя, если в подписке нет названия.",
+ "subscribe_add_success": "Подписка успешно добавлена!",
+ "subscribe_delete": "Удалить",
+ "overwrite": "Переопределить провайдера поиска",
+ "overwrite_tooltip": "Использовать провайдера поиска вместо LLM",
+ "apikey": "API ключ",
+ "free": "Бесплатно",
+ "content_limit": "Ограничение длины текста",
+ "content_limit_tooltip": "Ограничьте длину содержимого результатов поиска, контент, превышающий ограничение, будет обрезан."
+ },
+ "general.auto_check_update.title": "Автоматическое обновление",
+ "general.early_access.title": "Ранний доступ",
+ "general.early_access.tooltip": "Включить для использования последней версии из GitHub, что может быть медленнее и нестабильно. Пожалуйста, сделайте резервную копию данных заранее.",
"quickPhrase": {
"title": "Быстрые фразы",
"add": "Добавить фразу",
@@ -1885,7 +1928,7 @@
"menu": {
"description": "Перевести содержимое текущего ввода"
},
- "detected.language": "Обнаруженный язык"
+ "detected.language": "Автоматическое обнаружение"
},
"tray": {
"quit": "Выйти",
@@ -1914,7 +1957,8 @@
"summary": "Суммаризировать",
"search": "Поиск",
"refine": "Уточнить",
- "copy": "Копировать"
+ "copy": "Копировать",
+ "quote": "Цитировать"
},
"window": {
"pin": "Закрепить",
@@ -1942,10 +1986,15 @@
"title": "Панель инструментов",
"trigger_mode": {
"title": "Режим активации",
- "description": "Показывать панель сразу при выделении или только при удержании Ctrl.",
+ "description": "Показывать панель сразу при выделении, или только при удержании Ctrl, или только при нажатии на сочетание клавиш",
"description_note": "В некоторых приложениях Ctrl может не работать. Если вы используете AHK или другие инструменты для переназначения Ctrl, это может привести к тому, что некоторые приложения не смогут выделить текст.",
"selected": "При выделении",
- "ctrlkey": "По Ctrl"
+ "selected_note": "После выделения",
+ "ctrlkey": "По Ctrl",
+ "ctrlkey_note": "После выделения, удерживайте Ctrl для показа панели. Пожалуйста, установите Ctrl в настройках клавиатуры и активируйте его.",
+ "shortcut": "По сочетанию клавиш",
+ "shortcut_note": "После выделения, используйте сочетание клавиш для показа панели. Пожалуйста, установите сочетание клавиш в настройках клавиатуры и активируйте его.",
+ "shortcut_link": "Перейти к настройкам клавиатуры"
},
"compact_mode": {
"title": "Компактный режим",
diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json
index 745ad4148..39a95dcd2 100644
--- a/src/renderer/src/i18n/locales/zh-cn.json
+++ b/src/renderer/src/i18n/locales/zh-cn.json
@@ -8,7 +8,10 @@
"add.name.placeholder": "输入名称",
"add.prompt": "提示词",
"add.prompt.placeholder": "输入提示词",
- "add.prompt.variables.tip": "可用的变量:{{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
+ "add.prompt.variables.tip": {
+ "title": "可用的变量",
+ "content": "{{date}}:\t日期\n{{time}}:\t时间\n{{datetime}}:\t日期和时间\n{{system}}:\t操作系统\n{{arch}}:\tCPU架构\n{{language}}:\t语言\n{{model_name}}:\t模型名称"
+ },
"add.title": "创建智能体",
"import": {
"title": "从外部导入",
@@ -30,16 +33,7 @@
"agent": "导出智能体"
},
"delete.popup.content": "确定要删除此智能体吗?",
- "edit.message.add.title": "添加",
- "edit.message.assistant.placeholder": "输入助手消息",
- "edit.message.assistant.title": "助手",
- "edit.message.empty.content": "会话输入内容不能为空",
- "edit.message.group.title": "消息组",
- "edit.message.title": "预设消息",
- "edit.message.user.placeholder": "输入用户消息",
- "edit.message.user.title": "用户",
"edit.model.select.title": "选择模型",
- "edit.settings.hide_preset_messages": "隐藏预设消息",
"edit.title": "编辑智能体",
"manage.title": "管理智能体",
"my_agents": "我的智能体",
@@ -83,7 +77,6 @@
"settings.tool_use_mode.function": "函数",
"settings.tool_use_mode.prompt": "提示词",
"settings.model": "模型设置",
- "settings.preset_messages": "预设消息",
"settings.prompt": "提示词设置",
"settings.reasoning_effort": "思维链长度",
"settings.reasoning_effort.off": "关闭",
@@ -286,6 +279,7 @@
"topics.clear.title": "清空消息",
"topics.copy.image": "复制为图片",
"topics.copy.md": "复制为 Markdown",
+ "topics.copy.plain_text": "复制为纯文本(去除 Markdown)",
"topics.copy.title": "复制",
"topics.delete.shortcut": "按住 {{key}} 可直接删除",
"topics.edit.placeholder": "输入新名称",
@@ -525,7 +519,11 @@
"delete_confirm": "确定要删除此知识库吗?",
"dimensions": "嵌入维度",
"dimensions_size_tooltip": "嵌入维度大小,数值越大,嵌入维度越大,但消耗的 Token 也越多",
- "dimensions_size_placeholder": " 默认值(不建议修改)",
+ "dimensions_set_right": "⚠️ 请确保模型支持所设置的嵌入维度大小",
+ "dimensions_default": "模型将使用默认嵌入维度",
+ "dimensions_size_placeholder": " 嵌入维度大小,如 1024",
+ "dimensions_auto_set": "自动设置嵌入维度",
+ "dimensions_error_invalid": "请输入嵌入维度大小",
"dimensions_size_too_large": "嵌入维度不能超过模型上下文限制({{max_context}})",
"directories": "目录",
"directory_placeholder": "请输入目录路径",
@@ -959,6 +957,9 @@
},
"text_desc_required": "请先输入图片描述",
"req_error_text": "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。",
+ "req_error_token": "请检查令牌有效性",
+ "req_error_no_balance": "请检查令牌有效性",
+ "image_handle_required": "请先上传图片",
"auto_create_paint": "自动新建图片",
"auto_create_paint_tip": "在图片生成后,会自动新建图片",
"select_model": "选择模型",
@@ -1353,6 +1354,8 @@
"general.emoji_picker": "表情选择器",
"general.image_upload": "图片上传",
"general.auto_check_update.title": "自动更新",
+ "general.early_access.title": "抢先体验",
+ "general.early_access.tooltip": "开启后,将使用 GitHub 的最新版本,下载速度可能较慢,请务必提前备份数据",
"general.reset.button": "重置",
"general.reset.title": "重置数据",
"general.restore.button": "恢复",
@@ -1522,6 +1525,7 @@
"messages.prompt": "显示提示词",
"messages.tokens": "显示Token用量",
"messages.divider": "消息分割线",
+ "messages.divider.tooltip": "不适用于气泡样式消息",
"messages.grid_columns": "消息网格展示列数",
"messages.grid_popover_trigger": "网格详情触发",
"messages.grid_popover_trigger.click": "点击显示",
@@ -1554,7 +1558,8 @@
"models.add.model_id.select.placeholder": "选择模型",
"models.add.model_id.tooltip": "例如 gpt-3.5-turbo",
"models.add.model_name": "模型名称",
- "models.add.model_name.placeholder": "例如 GPT-3.5",
+ "models.add.model_name.placeholder": "例如 GPT-4",
+ "models.add.model_name.tooltip": "例如 GPT-4",
"models.check.all": "所有",
"models.check.all_models_passed": "所有模型检测通过",
"models.check.button_caption": "健康检测",
@@ -1705,6 +1710,8 @@
"exit_fullscreen": "退出全屏",
"key": "按键",
"mini_window": "快捷助手",
+ "selection_assistant_toggle": "开关划词助手",
+ "selection_assistant_select_text": "划词助手:取词",
"new_topic": "新建话题",
"press_shortcut": "按下快捷键",
"reset_defaults": "重置默认快捷键",
@@ -1714,7 +1721,7 @@
"search_message_in_chat": "在当前对话中搜索消息",
"show_app": "显示/隐藏应用",
"show_settings": "打开设置",
- "title": "快捷方式",
+ "title": "快捷键",
"toggle_new_context": "清除上下文",
"toggle_show_assistants": "切换助手显示",
"toggle_show_topics": "切换话题显示",
@@ -1885,7 +1892,7 @@
},
"title": "翻译",
"tooltip.newline": "换行",
- "detected.language": "检测到的语言"
+ "detected.language": "自动检测"
},
"tray": {
"quit": "退出",
@@ -1914,7 +1921,8 @@
"summary": "总结",
"search": "搜索",
"refine": "优化",
- "copy": "复制"
+ "copy": "复制",
+ "quote": "引用"
},
"window": {
"pin": "置顶",
@@ -1941,11 +1949,16 @@
"toolbar": {
"title": "工具栏",
"trigger_mode": {
- "title": "触发方式",
- "description": "划词立即显示工具栏,或者划词后按住 Ctrl 键才显示工具栏。",
+ "title": "取词方式",
+ "description": "划词后,触发取词并显示工具栏的方式",
"description_note": "少数应用不支持通过 Ctrl 键划词。若使用了AHK等工具对 Ctrl 键进行了重映射,可能导致部分应用无法划词。",
"selected": "划词",
- "ctrlkey": "Ctrl 键"
+ "selected_note": "划词后立即显示工具栏",
+ "ctrlkey": "Ctrl 键",
+ "ctrlkey_note": "划词后,再 按住 Ctrl键,才显示工具栏",
+ "shortcut": "快捷键",
+ "shortcut_note": "划词后,使用快捷键显示工具栏。请在快捷键设置页面中设置取词快捷键并启用。",
+ "shortcut_link": "前往快捷键设置"
},
"compact_mode": {
"title": "紧凑模式",
diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json
index 2bd7c7c37..4f592f3da 100644
--- a/src/renderer/src/i18n/locales/zh-tw.json
+++ b/src/renderer/src/i18n/locales/zh-tw.json
@@ -8,7 +8,10 @@
"add.name.placeholder": "輸入名稱",
"add.prompt": "提示詞",
"add.prompt.placeholder": "輸入提示詞",
- "add.prompt.variables.tip": "可用的變數:{{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
+ "add.prompt.variables.tip": {
+ "title": "可用的變數",
+ "content": "{{date}}:\t日期\n{{time}}:\t時間\n{{datetime}}:\t日期和時間\n{{system}}:\t作業系統\n{{arch}}:\tCPU架構\n{{language}}:\t語言\n{{model_name}}:\t模型名稱"
+ },
"add.title": "建立智慧代理人",
"import": {
"title": "從外部導入",
@@ -30,16 +33,7 @@
"agent": "匯出智慧代理人"
},
"delete.popup.content": "確定要刪除此智慧代理人嗎?",
- "edit.message.add.title": "新增",
- "edit.message.assistant.placeholder": "輸入助手訊息",
- "edit.message.assistant.title": "助手",
- "edit.message.empty.content": "會話輸入內容不能為空",
- "edit.message.group.title": "訊息分組",
- "edit.message.title": "預設訊息",
- "edit.message.user.placeholder": "輸入使用者訊息",
- "edit.message.user.title": "使用者",
"edit.model.select.title": "選擇模型",
- "edit.settings.hide_preset_messages": "隱藏預設訊息",
"edit.title": "編輯智慧代理人",
"manage.title": "管理智慧代理人",
"my_agents": "我的智慧代理人",
@@ -76,7 +70,6 @@
"settings.default_model": "預設模型",
"settings.knowledge_base": "知識庫設定",
"settings.model": "模型設定",
- "settings.preset_messages": "預設訊息",
"settings.prompt": "提示詞設定",
"settings.reasoning_effort": "思維鏈長度",
"settings.reasoning_effort.off": "關閉",
@@ -268,6 +261,7 @@
"topics.clear.title": "清空訊息",
"topics.copy.image": "複製為圖片",
"topics.copy.md": "複製為 Markdown",
+ "topics.copy.plain_text": "複製為純文字(移除 Markdown)",
"topics.copy.title": "複製",
"topics.delete.shortcut": "按住 {{key}} 可直接刪除",
"topics.edit.placeholder": "輸入新名稱",
@@ -576,12 +570,16 @@
"urls": "網址",
"dimensions": "嵌入維度",
"dimensions_size_tooltip": "嵌入維度大小,數值越大,嵌入維度越大,但消耗的 Token 也越多",
- "dimensions_size_placeholder": "預設值(不建議修改)",
- "dimensions_size_too_large": "嵌入維度不能超過模型上下文限制({{max_context}})",
"status_embedding_completed": "嵌入完成",
"status_preprocess_completed": "預處理完成",
"status_embedding_failed": "嵌入失敗",
- "status_preprocess_failed": "預處理失敗"
+ "status_preprocess_failed": "預處理失敗",
+ "dimensions_size_placeholder": " 嵌入維度大小,例如 1024",
+ "dimensions_auto_set": "自動設定嵌入維度",
+ "dimensions_error_invalid": "請輸入嵌入維度大小",
+ "dimensions_size_too_large": "嵌入維度不能超過模型上下文限制({{max_context}})",
+ "dimensions_set_right": "⚠️ 請確保模型支援所設置的嵌入維度大小",
+ "dimensions_default": "模型將使用預設嵌入維度"
},
"languages": {
"arabic": "阿拉伯文",
@@ -958,7 +956,10 @@
},
"rendering_speed": "渲染速度",
"text_desc_required": "請先輸入圖片描述",
+ "image_handle_required": "請先上傳圖片。",
"req_error_text": "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。",
+ "req_error_token": "請檢查令牌的有效性",
+ "req_error_no_balance": "請檢查令牌的有效性",
"auto_create_paint": "自動新增圖片",
"auto_create_paint_tip": "圖片生成後,會自動新增圖片",
"select_model": "選擇模型",
@@ -1521,6 +1522,7 @@
"messages.prompt": "提示詞顯示",
"messages.tokens": "Token用量顯示",
"messages.divider": "訊息間顯示分隔線",
+ "messages.divider.tooltip": "不適用於氣泡樣式消息",
"messages.grid_columns": "訊息網格展示列數",
"messages.grid_popover_trigger": "網格詳細資訊觸發",
"messages.grid_popover_trigger.click": "點選顯示",
@@ -1554,6 +1556,7 @@
"models.add.model_id.tooltip": "例如 gpt-3.5-turbo",
"models.add.model_name": "模型名稱",
"models.add.model_name.placeholder": "選填,例如 GPT-4",
+ "models.add.model_name.tooltip": "例如 GPT-4",
"models.check.all": "所有",
"models.check.all_models_passed": "所有模型檢查通過",
"models.check.button_caption": "健康檢查",
@@ -1697,6 +1700,8 @@
"copy_last_message": "複製上一則訊息",
"key": "按鍵",
"mini_window": "快捷助手",
+ "selection_assistant_toggle": "開關劃詞助手",
+ "selection_assistant_select_text": "劃詞助手:取词",
"new_topic": "新增話題",
"press_shortcut": "按下快捷鍵",
"reset_defaults": "重設預設快捷鍵",
@@ -1706,7 +1711,7 @@
"search_message_in_chat": "在當前對話中搜尋訊息",
"show_app": "顯示/隱藏應用程式",
"show_settings": "開啟設定",
- "title": "快速方式",
+ "title": "快捷鍵",
"toggle_new_context": "清除上下文",
"toggle_show_assistants": "切換助手顯示",
"toggle_show_topics": "切換話題顯示",
@@ -1790,7 +1795,45 @@
"tray.onclose": "關閉時最小化到系统匣",
"tray.show": "顯示系统匣圖示",
"tray.title": "系统匣",
- "general.auto_check_update.title": "啟用自動更新",
+ "websearch": {
+ "check_success": "驗證成功",
+ "get_api_key": "點選這裡取得金鑰",
+ "search_with_time": "搜尋包含日期",
+ "tavily": {
+ "api_key": "Tavily API 金鑰",
+ "api_key.placeholder": "請輸入 Tavily API 金鑰",
+ "description": "Tavily 是一個為 AI 代理量身訂製的搜尋引擎,提供即時、準確的結果、智慧查詢建議和深入的研究能力",
+ "title": "Tavily"
+ },
+ "blacklist": "黑名單",
+ "blacklist_description": "以下網站不會出現在搜索結果中",
+ "search_max_result": "搜尋結果個數",
+ "search_result_default": "預設",
+ "check": "檢查",
+ "search_provider": "搜尋服務商",
+ "search_provider_placeholder": "選擇一個搜尋服務商",
+ "no_provider_selected": "請選擇搜索服務商後再檢查",
+ "check_failed": "驗證失敗",
+ "blacklist_tooltip": "匹配模式: *://*.example.com/*\n正则表达式: /example\\.(net|org)/",
+ "subscribe": "黑名單訂閱",
+ "subscribe_update": "更新",
+ "subscribe_add": "添加訂閱",
+ "subscribe_url": "訂閱源地址",
+ "subscribe_name": "替代名稱",
+ "subscribe_name.placeholder": "當下載的訂閱源沒有名稱時所使用的替代名稱",
+ "subscribe_add_success": "訂閱源添加成功!",
+ "subscribe_delete": "刪除",
+ "title": "網路搜尋",
+ "overwrite": "覆蓋搜尋服務商",
+ "overwrite_tooltip": "強制使用搜尋服務商而不是大語言模型進行搜尋",
+ "apikey": "API 金鑰",
+ "free": "免費",
+ "content_limit": "內容長度限制",
+ "content_limit_tooltip": "限制搜尋結果的內容長度,超過限制的內容將被截斷"
+ },
+ "general.auto_check_update.title": "自動更新",
+ "general.early_access.title": "搶先體驗",
+ "general.early_access.tooltip": "開啟後,將使用 GitHub 的最新版本,下載速度可能較慢,請務必提前備份數據",
"quickPhrase": {
"title": "快捷短語",
"add": "新增短語",
@@ -1885,7 +1928,7 @@
"menu": {
"description": "對當前輸入框內容進行翻譯"
},
- "detected.language": "檢測到的語言"
+ "detected.language": "自動檢測"
},
"tray": {
"quit": "結束",
@@ -1914,7 +1957,8 @@
"summary": "總結",
"search": "搜尋",
"refine": "優化",
- "copy": "複製"
+ "copy": "複製",
+ "quote": "引用"
},
"window": {
"pin": "置頂",
@@ -1941,11 +1985,16 @@
"toolbar": {
"title": "工具列",
"trigger_mode": {
- "title": "觸發方式",
- "description": "劃詞立即顯示工具列,或者劃詞後按住 Ctrl 鍵才顯示工具列。",
+ "title": "取詞方式",
+ "description": "劃詞後,觸發取詞並顯示工具列的方式",
"description_note": "在某些應用中可能無法透過 Ctrl 鍵劃詞。若使用了AHK等工具對Ctrl鍵進行了重新對應,可能導致部分應用程式無法劃詞。",
"selected": "劃詞",
- "ctrlkey": "Ctrl 鍵"
+ "selected_note": "劃詞後,立即顯示工具列",
+ "ctrlkey": "Ctrl 鍵",
+ "ctrlkey_note": "劃詞後,再 按住 Ctrl鍵,才顯示工具列",
+ "shortcut": "快捷鍵",
+ "shortcut_note": "劃詞後,使用快捷鍵顯示工具列。請在快捷鍵設定頁面中設置取詞快捷鍵並啟用。",
+ "shortcut_link": "前往快捷鍵設定"
},
"compact_mode": {
"title": "緊湊模式",
diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json
index 5ea7d99b9..125e73132 100644
--- a/src/renderer/src/i18n/translate/el-gr.json
+++ b/src/renderer/src/i18n/translate/el-gr.json
@@ -8,18 +8,13 @@
"add.name.placeholder": "Εισαγάγετε όνομα",
"add.prompt": "Φράση προκαλέσεως",
"add.prompt.placeholder": "Εισαγάγετε φράση προκαλέσεως",
+ "add.prompt.variables.tip": {
+ "title": "Διαθέσιμες μεταβλητές",
+ "content": "{{date}}:\tΗμερομηνία\n{{time}}:\tΏρα\n{{datetime}}:\tΗμερομηνία και ώρα\n{{system}}:\tΛειτουργικό σύστημα\n{{arch}}:\tΑρχιτεκτονική CPU\n{{language}}:\tΓλώσσα\n{{model_name}}:\tΌνομα μοντέλου"
+ },
"add.title": "Δημιουργία νέου ειδικού",
"delete.popup.content": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον ειδικό;",
- "edit.message.add.title": "Προσθήκη",
- "edit.message.assistant.placeholder": "Εισαγάγετε μήνυμα βοηθού",
- "edit.message.assistant.title": "Βοηθός",
- "edit.message.empty.content": "Το περιεχόμενο του συνομιλητή δεν μπορεί να είναι κενό.",
- "edit.message.group.title": "Ομάδα μηνυμάτων",
- "edit.message.title": "Προεπιλογές μηνυμάτων",
- "edit.message.user.placeholder": "Εισαγάγετε μήνυμα χρήστη",
- "edit.message.user.title": "Χρήστης",
"edit.model.select.title": "Επιλογή μοντέλου",
- "edit.settings.hide_preset_messages": "Απόκρυψη προεπιλογών μηνυμάτων",
"edit.title": "Επεξεργασία ειδικού",
"manage.title": "Διαχείριση ειδικών",
"my_agents": "Οι ειδικοί μου",
@@ -64,7 +59,6 @@
"settings.default_model": "Προεπιλεγμένο μοντέλο",
"settings.knowledge_base": "Ρυθμίσεις βάσης γνώσεων",
"settings.model": "Ρυθμίσεις μοντέλου",
- "settings.preset_messages": "Προεπιλεγμένα μηνύματα",
"settings.prompt": "Ρυθμίσεις προκαλύμματος",
"settings.reasoning_effort": "Μήκος λογισμικού αλυσίδας",
"settings.reasoning_effort.high": "Μεγάλο",
@@ -204,6 +198,7 @@
"topics.clear.title": "Καθαρισμός μηνυμάτων",
"topics.copy.image": "Αντιγραφή ως εικόνα",
"topics.copy.md": "Αντιγραφή ως Markdown",
+ "topics.copy.plain_text": "Αντιγραφή ως απλό κείμενο (αφαίρεση Markdown)",
"topics.copy.title": "Αντιγραφή",
"topics.delete.shortcut": "Πατήστε {{key}} για να διαγράψετε αμέσως",
"topics.edit.placeholder": "Εισαγάγετε το νέο όνομα",
@@ -496,8 +491,12 @@
"urls": "Διευθύνσεις",
"dimensions": "Διαστάσεις ενσωμάτωσης",
"dimensions_size_tooltip": "Το μέγεθος των διαστάσεων ενσωμάτωσης. Όσο μεγαλύτερη η τιμή, τόσο περισσότερες οι διαστάσεις ενσωμάτωσης, αλλά και οι απαιτούμενες μονάδες (Tokens).",
- "dimensions_size_placeholder": "Προεπιλεγμένη τιμή (δεν συνιστάται να τροποποιηθεί)",
- "dimensions_size_too_large": "Οι διαστάσεις ενσωμάτωσης δεν μπορούν να υπερβούν το όριο περιεχομένου του μοντέλου ({{max_context}})"
+ "dimensions_size_placeholder": " Μέγεθος διαστάσεων ενσωμάτωσης, π.χ. 1024",
+ "dimensions_auto_set": "Αυτόματη ρύθμιση διαστάσεων ενσωμάτωσης",
+ "dimensions_error_invalid": "Παρακαλώ εισάγετε μέγεθος διαστάσεων ενσωμάτωσης",
+ "dimensions_size_too_large": "Οι διαστάσεις ενσωμάτωσης δεν μπορούν να υπερβούν το όριο περιεχομένου του μοντέλου ({{max_context}})",
+ "dimensions_set_right": "⚠️ Βεβαιωθείτε ότι το μοντέλο υποστηρίζει το καθορισμένο μέγεθος διαστάσεων ενσωμάτωσης",
+ "dimensions_default": "Το μοντέλο θα χρησιμοποιήσει τις προεπιλεγμένες διαστάσεις ενσωμάτωσης"
},
"languages": {
"arabic": "Αραβικά",
@@ -1306,6 +1305,7 @@
"advancedSettings": "Προχωρημένες Ρυθμίσεις"
},
"messages.divider": "Διαχωριστική γραμμή μηνυμάτων",
+ "messages.divider.tooltip": "Δεν ισχύει για μηνύματα με στυλ φυσαλίδας",
"messages.grid_columns": "Αριθμός στήλων γριλ μηνυμάτων",
"messages.grid_popover_trigger": "Καταγραφή στοιχείων στο grid",
"messages.grid_popover_trigger.click": "Εμφάνιση κλικ",
diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json
index d0a1c588d..ec48af732 100644
--- a/src/renderer/src/i18n/translate/es-es.json
+++ b/src/renderer/src/i18n/translate/es-es.json
@@ -8,18 +8,13 @@
"add.name.placeholder": "Ingrese el nombre",
"add.prompt": "Palabra clave",
"add.prompt.placeholder": "Ingrese la palabra clave",
+ "add.prompt.variables.tip": {
+ "title": "Variables disponibles",
+ "content": "{{date}}:\tFecha\n{{time}}:\tHora\n{{datetime}}:\tFecha y hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitectura de CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNombre del modelo"
+ },
"add.title": "Crear agente inteligente",
"delete.popup.content": "¿Está seguro de que desea eliminar este agente inteligente?",
- "edit.message.add.title": "Agregar",
- "edit.message.assistant.placeholder": "Ingrese el mensaje del asistente",
- "edit.message.assistant.title": "Asistente",
- "edit.message.empty.content": "El contenido de la sesión de chat no puede estar vacío",
- "edit.message.group.title": "Grupo de mensajes",
- "edit.message.title": "Mensaje predeterminado",
- "edit.message.user.placeholder": "Ingrese el mensaje del usuario",
- "edit.message.user.title": "Usuario",
"edit.model.select.title": "Seleccionar modelo",
- "edit.settings.hide_preset_messages": "Ocultar mensajes predeterminados",
"edit.title": "Editar agente inteligente",
"manage.title": "Administrar agentes inteligentes",
"my_agents": "Mis agentes inteligentes",
@@ -64,7 +59,6 @@
"settings.default_model": "Modelo Predeterminado",
"settings.knowledge_base": "Configuración de Base de Conocimientos",
"settings.model": "Configuración de Modelo",
- "settings.preset_messages": "Mensajes Preestablecidos",
"settings.prompt": "Configuración de Palabras Clave",
"settings.reasoning_effort": "Longitud de Cadena de Razonamiento",
"settings.reasoning_effort.high": "Largo",
@@ -205,6 +199,7 @@
"topics.clear.title": "Limpiar mensajes",
"topics.copy.image": "Copiar como imagen",
"topics.copy.md": "Copiar como Markdown",
+ "topics.copy.plain_text": "Copiar como texto sin formato (eliminar Markdown)",
"topics.copy.title": "Copiar",
"topics.delete.shortcut": "Mantén presionada {{key}} para eliminar directamente",
"topics.edit.placeholder": "Introduce nuevo nombre",
@@ -497,8 +492,12 @@
"urls": "URLs",
"dimensions": "Dimensión de incrustación",
"dimensions_size_tooltip": "Tamaño de la dimensión de incrustación, cuanto mayor sea el valor, mayor será la dimensión de incrustación, pero también consumirá más Tokens",
- "dimensions_size_placeholder": "Valor predeterminado (no recomendado modificar)",
- "dimensions_size_too_large": "La dimensión de incrustación no puede exceder el límite del contexto del modelo ({{max_context}})"
+ "dimensions_size_placeholder": " Tamaño de dimensión de incrustación, ej. 1024",
+ "dimensions_auto_set": "Configuración automática de dimensiones de incrustación",
+ "dimensions_error_invalid": "Por favor ingrese el tamaño de dimensión de incrustación",
+ "dimensions_size_too_large": "La dimensión de incrustación no puede exceder el límite del contexto del modelo ({{max_context}})",
+ "dimensions_set_right": "⚠️ Asegúrese de que el modelo admita el tamaño de dimensión de incrustación establecido",
+ "dimensions_default": "El modelo utilizará las dimensiones de incrustación predeterminadas"
},
"languages": {
"arabic": "Árabe",
@@ -1305,6 +1304,7 @@
"advancedSettings": "Configuración avanzada"
},
"messages.divider": "Separador de mensajes",
+ "messages.divider.tooltip": "No aplicable para mensajes de estilo burbuja",
"messages.grid_columns": "Número de columnas en la cuadrícula de mensajes",
"messages.grid_popover_trigger": "Desencadenante de detalles de cuadrícula",
"messages.grid_popover_trigger.click": "Mostrar al hacer clic",
diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json
index 272954f0e..1d3f5332d 100644
--- a/src/renderer/src/i18n/translate/fr-fr.json
+++ b/src/renderer/src/i18n/translate/fr-fr.json
@@ -8,18 +8,13 @@
"add.name.placeholder": "Entrer le nom",
"add.prompt": "Mot-clé",
"add.prompt.placeholder": "Entrer le mot-clé",
+ "add.prompt.variables.tip": {
+ "title": "Variables disponibles",
+ "content": "{{date}}:\tDate\n{{time}}:\tHeure\n{{datetime}}:\tDate et heure\n{{system}}:\tSystème d'exploitation\n{{arch}}:\tArchitecture du processeur\n{{language}}:\tLangue\n{{model_name}}:\tNom du modèle"
+ },
"add.title": "Créer un agent intelligent",
"delete.popup.content": "Êtes-vous sûr de vouloir supprimer cet agent intelligent ?",
- "edit.message.add.title": "Ajouter",
- "edit.message.assistant.placeholder": "Entrer le message de l'assistant",
- "edit.message.assistant.title": "Assistant",
- "edit.message.empty.content": "Le contenu de la session ne peut pas être vide",
- "edit.message.group.title": "Groupe de messages",
- "edit.message.title": "Messages prédéfinis",
- "edit.message.user.placeholder": "Entrer le message de l'utilisateur",
- "edit.message.user.title": "Utilisateur",
"edit.model.select.title": "Sélectionner un modèle",
- "edit.settings.hide_preset_messages": "Masquer les messages prédéfinis",
"edit.title": "Modifier l'agent intelligent",
"manage.title": "Gérer les agents intelligents",
"my_agents": "Mes agents intelligents",
@@ -64,7 +59,6 @@
"settings.default_model": "Modèle par défaut",
"settings.knowledge_base": "Paramètres de la base de connaissances",
"settings.model": "Paramètres du modèle",
- "settings.preset_messages": "Messages prédéfinis",
"settings.prompt": "Paramètres de l'invite",
"settings.reasoning_effort": "Longueur de la chaîne de raisonnement",
"settings.reasoning_effort.high": "Long",
@@ -204,6 +198,7 @@
"topics.clear.title": "Effacer le message",
"topics.copy.image": "Copier sous forme d'image",
"topics.copy.md": "Copier sous forme de Markdown",
+ "topics.copy.plain_text": "Copier en tant que texte brut (supprimer Markdown)",
"topics.copy.title": "Copier",
"topics.delete.shortcut": "Maintenez {{key}} pour supprimer directement",
"topics.edit.placeholder": "Entrez un nouveau nom",
@@ -496,8 +491,12 @@
"urls": "URLs",
"dimensions": "Размерность встраивания",
"dimensions_size_tooltip": "Размерность встраивания. Чем больше значение, тем выше размерность, но тем больше токенов требуется",
- "dimensions_size_placeholder": "Значение по умолчанию (не рекомендуется изменять)",
- "dimensions_size_too_large": "Размерность встраивания не может превышать ограничение контекста модели ({{max_context}})"
+ "dimensions_size_placeholder": " Taille de dimension d'incorporation, ex. 1024",
+ "dimensions_auto_set": "Réglage automatique des dimensions d'incorporation",
+ "dimensions_error_invalid": "Veuillez saisir la taille de dimension d'incorporation",
+ "dimensions_size_too_large": "Размерность встраивания не может превышать ограничение контекста модели ({{max_context}})",
+ "dimensions_set_right": "⚠️ Assurez-vous que le modèle prend en charge la taille de dimension d'incorporation définie",
+ "dimensions_default": "Le modèle utilisera les dimensions d'incorporation par défaut"
},
"languages": {
"arabic": "Arabe",
@@ -1306,6 +1305,7 @@
"advancedSettings": "Расширенные настройки"
},
"messages.divider": "Séparateur de messages",
+ "messages.divider.tooltip": "Non applicable aux messages de style bulle",
"messages.grid_columns": "Nombre de colonnes de la grille de messages",
"messages.grid_popover_trigger": "Déclencheur de popover de la grille",
"messages.grid_popover_trigger.click": "Afficher au clic",
diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json
index 5e97076f6..fe5f0c6dd 100644
--- a/src/renderer/src/i18n/translate/pt-pt.json
+++ b/src/renderer/src/i18n/translate/pt-pt.json
@@ -8,18 +8,13 @@
"add.name.placeholder": "Digite o Nome",
"add.prompt": "Prompt",
"add.prompt.placeholder": "Digite o Prompt",
+ "add.prompt.variables.tip": {
+ "title": "Variáveis disponíveis",
+ "content": "{{date}}:\tData\n{{time}}:\tHora\n{{datetime}}:\tData e hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitetura da CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNome do modelo"
+ },
"add.title": "Criar Agente Inteligente",
"delete.popup.content": "Tem certeza de que deseja excluir este agente inteligente?",
- "edit.message.add.title": "Adicionar",
- "edit.message.assistant.placeholder": "Digite a Mensagem do Assistente",
- "edit.message.assistant.title": "Assistente",
- "edit.message.empty.content": "O conteúdo da sessão não pode estar vazio",
- "edit.message.group.title": "Grupo de Mensagens",
- "edit.message.title": "Mensagens Padrão",
- "edit.message.user.placeholder": "Digite a Mensagem do Usuário",
- "edit.message.user.title": "Usuário",
"edit.model.select.title": "Selecionar Modelo",
- "edit.settings.hide_preset_messages": "Ocultar Mensagens Padrão",
"edit.title": "Editar Agente Inteligente",
"manage.title": "Gerenciar Agentes Inteligentes",
"my_agents": "Meus Agentes Inteligentes",
@@ -64,7 +59,6 @@
"settings.default_model": "Modelo Padrão",
"settings.knowledge_base": "Configurações da Base de Conhecimento",
"settings.model": "Configurações do Modelo",
- "settings.preset_messages": "Mensagens Pré-definidas",
"settings.prompt": "Configurações de Prompt",
"settings.reasoning_effort": "Comprimento da Cadeia de Raciocínio",
"settings.reasoning_effort.high": "Longo",
@@ -205,6 +199,7 @@
"topics.clear.title": "Limpar mensagens",
"topics.copy.image": "Copiar como imagem",
"topics.copy.md": "Copiar como Markdown",
+ "topics.copy.plain_text": "Copiar como texto simples (remover Markdown)",
"topics.copy.title": "Copiar",
"topics.delete.shortcut": "Pressione {{key}} para deletar diretamente",
"topics.edit.placeholder": "Digite novo nome",
@@ -498,8 +493,12 @@
"urls": "URLs",
"dimensions": "Dimensão de incorporação",
"dimensions_size_tooltip": "Tamanho da dimensão de incorporação, quanto maior o valor, maior a dimensão de incorporação, mas também maior o consumo de tokens",
- "dimensions_size_placeholder": "Valor padrão (não recomendado alterar)",
- "dimensions_size_too_large": "A dimensão de incorporação não pode exceder o limite do contexto do modelo ({{max_context}})"
+ "dimensions_size_placeholder": " Tamanho da dimensão de incorporação, ex. 1024",
+ "dimensions_auto_set": "Definição automática de dimensões de incorporação",
+ "dimensions_error_invalid": "Por favor insira o tamanho da dimensão de incorporação",
+ "dimensions_size_too_large": "A dimensão de incorporação não pode exceder o limite do contexto do modelo ({{max_context}})",
+ "dimensions_set_right": "⚠️ Certifique-se de que o modelo suporta o tamanho da dimensão de incorporação definido",
+ "dimensions_default": "O modelo utilizará as dimensões de incorporação padrão"
},
"languages": {
"arabic": "Árabe",
@@ -1308,6 +1307,7 @@
"advancedSettings": "Configurações Avançadas"
},
"messages.divider": "Divisor de mensagens",
+ "messages.divider.tooltip": "Não aplicável a mensagens de estilo bolha",
"messages.grid_columns": "Número de colunas da grade de mensagens",
"messages.grid_popover_trigger": "Disparador de detalhes da grade",
"messages.grid_popover_trigger.click": "Clique para mostrar",
diff --git a/src/renderer/src/pages/agents/index.ts b/src/renderer/src/pages/agents/index.ts
index 3bc31bd1b..711ec59a6 100644
--- a/src/renderer/src/pages/agents/index.ts
+++ b/src/renderer/src/pages/agents/index.ts
@@ -45,7 +45,7 @@ export function useSystemAgents() {
// 如果没有远程配置或获取失败,加载本地代理
if (resourcesPath && _agents.length === 0) {
- const localAgentsData = await window.api.fs.read(resourcesPath + '/data/agents.json')
+ const localAgentsData = await window.api.fs.read(resourcesPath + '/data/agents.json', 'utf-8')
_agents = JSON.parse(localAgentsData) as Agent[]
}
diff --git a/src/renderer/src/pages/apps/AppsPage.tsx b/src/renderer/src/pages/apps/AppsPage.tsx
index 5348ac436..31cb3f239 100644
--- a/src/renderer/src/pages/apps/AppsPage.tsx
+++ b/src/renderer/src/pages/apps/AppsPage.tsx
@@ -1,18 +1,22 @@
-import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
+import { Navbar, NavbarMain } from '@renderer/components/app/Navbar'
import { useMinapps } from '@renderer/hooks/useMinapps'
-import { Input } from 'antd'
-import { Search } from 'lucide-react'
-import React, { FC, useState } from 'react'
+import { Button, Input } from 'antd'
+import { Search, SettingsIcon, X } from 'lucide-react'
+import React, { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
+import { useLocation } from 'react-router'
import styled from 'styled-components'
import App from './App'
+import MiniAppSettings from './MiniappSettings/MiniAppSettings'
import NewAppButton from './NewAppButton'
const AppsPage: FC = () => {
const { t } = useTranslation()
const [search, setSearch] = useState('')
const { minapps } = useMinapps()
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false)
+ const location = useLocation()
const filteredApps = search
? minapps.filter(
@@ -31,31 +35,51 @@ const AppsPage: FC = () => {
e.preventDefault()
}
+ useEffect(() => {
+ setIsSettingsOpen(false)
+ }, [location.key])
+
return (
-
+
{t('minapp.title')}
}
value={search}
onChange={(e) => setSearch(e.target.value)}
+ disabled={isSettingsOpen}
/>
-
-
+ : }
+ onClick={() => setIsSettingsOpen(!isSettingsOpen)}
+ />
+
-
- {filteredApps.map((app) => (
-
- ))}
-
-
+ {isSettingsOpen && }
+ {!isSettingsOpen && (
+
+ {filteredApps.map((app) => (
+
+ ))}
+
+
+ )}
)
diff --git a/src/renderer/src/pages/settings/MiniappSettings/MiniAppIconsManager.tsx b/src/renderer/src/pages/apps/MiniappSettings/MiniAppIconsManager.tsx
similarity index 100%
rename from src/renderer/src/pages/settings/MiniappSettings/MiniAppIconsManager.tsx
rename to src/renderer/src/pages/apps/MiniappSettings/MiniAppIconsManager.tsx
diff --git a/src/renderer/src/pages/settings/MiniappSettings/MiniAppSettings.tsx b/src/renderer/src/pages/apps/MiniappSettings/MiniAppSettings.tsx
similarity index 57%
rename from src/renderer/src/pages/settings/MiniappSettings/MiniAppSettings.tsx
rename to src/renderer/src/pages/apps/MiniappSettings/MiniAppSettings.tsx
index 740599abf..30d0496ff 100644
--- a/src/renderer/src/pages/settings/MiniappSettings/MiniAppSettings.tsx
+++ b/src/renderer/src/pages/apps/MiniappSettings/MiniAppSettings.tsx
@@ -1,8 +1,8 @@
import { UndoOutlined } from '@ant-design/icons' // 导入重置图标
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
-import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useSettings } from '@renderer/hooks/useSettings'
+import { SettingDescription, SettingDivider, SettingRowTitle, SettingTitle } from '@renderer/pages/settings'
import { useAppDispatch } from '@renderer/store'
import {
setMaxKeepAliveMinapps,
@@ -12,9 +12,9 @@ import {
import { Button, message, Slider, Switch, Tooltip } from 'antd'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
+import { useNavigate } from 'react-router'
import styled from 'styled-components'
-import { SettingContainer, SettingDescription, SettingDivider, SettingGroup, SettingRowTitle, SettingTitle } from '..'
import MiniAppIconsManager from './MiniAppIconsManager'
// 默认小程序缓存数量
@@ -22,10 +22,10 @@ const DEFAULT_MAX_KEEPALIVE = 3
const MiniAppSettings: FC = () => {
const { t } = useTranslation()
- const { theme } = useTheme()
const dispatch = useAppDispatch()
const { maxKeepAliveMinapps, showOpenedMinappsInSidebar, minappsOpenLinkExternal } = useSettings()
const { minapps, disabled, updateMinapps, updateDisabledMinapps } = useMinapps()
+ const navigate = useNavigate()
const [visibleMiniApps, setVisibleMiniApps] = useState(minapps)
const [disabledMiniApps, setDisabledMiniApps] = useState(disabled || [])
@@ -72,83 +72,87 @@ const MiniAppSettings: FC = () => {
}, [])
return (
-
+
{contextHolder} {/* 添加消息上下文 */}
-
- {t('settings.miniapps.title')}
-
-
-
- {t('settings.miniapps.display_title')}
-
- {t('common.reset')}
-
-
-
-
-
-
-
-
- {t('settings.miniapps.open_link_external.title')}
-
- dispatch(setMinappsOpenLinkExternal(checked))}
- />
-
-
-
- {/* 缓存小程序数量设置 */}
-
-
- {t('settings.miniapps.cache_title')}
- {t('settings.miniapps.cache_description')}
-
-
-
-
-
-
-
-
- `${value}` }}
- />
-
-
-
-
-
-
- {t('settings.miniapps.sidebar_title')}
- {t('settings.miniapps.sidebar_description')}
-
- dispatch(setShowOpenedMinappsInSidebar(checked))}
- />
-
-
-
+
+ {t('settings.miniapps.display_title')}
+
+ {t('common.reset')}
+
+
+
+
+
+
+
+
+ {t('settings.miniapps.open_link_external.title')}
+
+ dispatch(setMinappsOpenLinkExternal(checked))}
+ />
+
+
+ {/* 缓存小程序数量设置 */}
+
+
+ {t('settings.miniapps.cache_title')}
+ {t('settings.miniapps.cache_description')}
+
+
+
+
+
+
+
+
+ `${value}` }}
+ />
+
+
+
+
+
+
+ {t('settings.miniapps.sidebar_title')}
+ {t('settings.miniapps.sidebar_description')}
+
+ dispatch(setShowOpenedMinappsInSidebar(checked))}
+ />
+
+
+
+ navigate('/apps')}>{t('common.close')}
+
+
)
}
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+`
+
// 修改和新增样式
const SettingRow = styled.div`
display: flex;
diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx
index d4196fe66..337f923ee 100644
--- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx
+++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx
@@ -18,7 +18,7 @@ import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
-import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
+import { getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import FileManager from '@renderer/services/FileManager'
import { checkRateLimit, getUserMessage } from '@renderer/services/MessagesService'
@@ -33,8 +33,10 @@ import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk'
import { Assistant, FileMetadata, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types'
import type { MessageInputBaseParams } from '@renderer/types/newMessage'
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
+import { formatQuotedText } from '@renderer/utils/formats'
import { getFilesFromDropEvent } from '@renderer/utils/input'
import { documentExts, imageExts, textExts } from '@shared/config/constant'
+import { IpcChannel } from '@shared/IpcChannel'
import { Button, Tooltip } from 'antd'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import dayjs from 'dayjs'
@@ -408,7 +410,6 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) =
const topic = getDefaultTopic(assistant.id)
await db.topics.add({ id: topic.id, messages: [] })
- await addAssistantMessagesToTopic({ assistant, topic })
// Clear previous state
// Reset to assistant default model
@@ -420,6 +421,19 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) =
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
}, [addTopic, assistant, setActiveTopic, setModel])
+ const onQuote = useCallback(
+ (text: string) => {
+ const quotedText = formatQuotedText(text)
+ setText((prevText) => {
+ const newText = prevText ? `${prevText}\n${quotedText}\n` : `${quotedText}\n`
+ setTimeout(() => resizeTextArea(), 0)
+ return newText
+ })
+ textareaRef.current?.focus()
+ },
+ [resizeTextArea]
+ )
+
const onPause = async () => {
await pauseMessages()
}
@@ -624,21 +638,25 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) =
_setEstimateTokenCount(tokensCount)
setContextCount({ current: contextCount.current, max: contextCount.max }) // 现在contextCount是一个对象而不是单个数值
}),
- EventEmitter.on(EVENT_NAMES.ADD_NEW_TOPIC, addNewTopic),
- EventEmitter.on(EVENT_NAMES.QUOTE_TEXT, (quotedText: string) => {
- setText((prevText) => {
- const newText = prevText ? `${prevText}\n${quotedText}\n` : `${quotedText}\n`
- setTimeout(() => resizeTextArea(), 0)
- return newText
- })
- textareaRef.current?.focus()
- })
+ EventEmitter.on(EVENT_NAMES.ADD_NEW_TOPIC, addNewTopic)
]
- return () => unsubscribes.forEach((unsub) => unsub())
- }, [addNewTopic, resizeTextArea])
+
+ // 监听引用事件
+ const quoteFromAnywhereRemover = window.electron?.ipcRenderer.on(
+ IpcChannel.App_QuoteToMain,
+ (_, selectedText: string) => onQuote(selectedText)
+ )
+
+ return () => {
+ unsubscribes.forEach((unsub) => unsub())
+ quoteFromAnywhereRemover?.()
+ }
+ }, [addNewTopic, onQuote])
useEffect(() => {
- textareaRef.current?.focus()
+ if (!document.querySelector('.topview-fullscreen-container')) {
+ textareaRef.current?.focus()
+ }
}, [assistant, topic])
useEffect(() => {
@@ -723,50 +741,28 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) =
}, [])
const onToggleExpended = () => {
- if (textareaHeight) {
- const textArea = textareaRef.current?.resizableTextArea?.textArea
- if (textArea) {
- textArea.style.height = 'auto'
- setTextareaHeight(undefined)
- setTimeout(() => {
- textArea.style.height = `${textArea.scrollHeight}px`
- }, 200)
- return
- }
- }
-
- const isExpended = !expended
- setExpend(isExpended)
+ const currentlyExpanded = expended || !!textareaHeight
+ const shouldExpand = !currentlyExpanded
+ setExpend(shouldExpand)
const textArea = textareaRef.current?.resizableTextArea?.textArea
-
- if (textArea) {
- if (isExpended) {
- textArea.style.height = '70vh'
- } else {
- resetHeight()
- }
+ if (!textArea) return
+ if (shouldExpand) {
+ textArea.style.height = '70vh'
+ setTextareaHeight(window.innerHeight * 0.7)
+ } else {
+ textArea.style.height = 'auto'
+ setTextareaHeight(undefined)
+ requestAnimationFrame(() => {
+ if (textArea) {
+ const contentHeight = textArea.scrollHeight
+ textArea.style.height = contentHeight > 400 ? '400px' : `${contentHeight}px`
+ }
+ })
}
textareaRef.current?.focus()
}
- const resetHeight = () => {
- if (expended) {
- setExpend(false)
- }
-
- setTextareaHeight(undefined)
-
- requestAnimationFrame(() => {
- const textArea = textareaRef.current?.resizableTextArea?.textArea
- if (textArea) {
- textArea.style.height = 'auto'
- const contentHeight = textArea.scrollHeight
- textArea.style.height = contentHeight > 400 ? '400px' : `${contentHeight}px`
- }
- })
- }
-
const isExpended = expended || !!textareaHeight
const showThinkingButton = isSupportedThinkingTokenModel(model) || isSupportedReasoningEffortModel(model)
@@ -949,11 +945,11 @@ const Textarea = styled(TextArea)`
padding: 0;
border-radius: 0;
display: flex;
- flex: 1;
resize: none !important;
overflow: auto;
width: 100%;
box-sizing: border-box;
+ transition: height 0.2s ease;
&.ant-input {
line-height: 1.4;
}
@@ -968,6 +964,9 @@ const Toolbar = styled.div`
margin-bottom: 4px;
height: 30px;
gap: 16px;
+ position: relative;
+ z-index: 2;
+ flex-shrink: 0;
`
const ToolbarMenu = styled.div`
diff --git a/src/renderer/src/pages/home/Markdown/Markdown.tsx b/src/renderer/src/pages/home/Markdown/Markdown.tsx
index 8e2d64177..d82905cf7 100644
--- a/src/renderer/src/pages/home/Markdown/Markdown.tsx
+++ b/src/renderer/src/pages/home/Markdown/Markdown.tsx
@@ -2,6 +2,7 @@ import 'katex/dist/katex.min.css'
import 'katex/dist/contrib/copy-tex'
import 'katex/dist/contrib/mhchem'
+import ImageViewer from '@renderer/components/ImageViewer'
import MarkdownShadowDOMRenderer from '@renderer/components/MarkdownShadowDOMRenderer'
import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
@@ -12,7 +13,7 @@ import { findCitationInChildren, getCodeBlockId } from '@renderer/utils/markdown
import { isEmpty } from 'lodash'
import { type FC, memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
-import ReactMarkdown, { type Components } from 'react-markdown'
+import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown'
import rehypeKatex from 'rehype-katex'
// @ts-ignore rehype-mathjax is not typed
import rehypeMathjax from 'rehype-mathjax'
@@ -22,7 +23,6 @@ import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import CodeBlock from './CodeBlock'
-import ImagePreview from './ImagePreview'
import Link from './Link'
const ALLOWED_ELEMENTS =
@@ -83,19 +83,25 @@ const Markdown: FC = ({ block }) => {
code: (props: any) => (
),
- img: ImagePreview,
- pre: (props: any) =>
+ img: (props: any) => ,
+ pre: (props: any) => ,
+ p: (props) => {
+ const hasImage = props?.node?.children?.some((child: any) => child.tagName === 'img')
+ if (hasImage) return
+ return
+ }
} as Partial
}, [onSaveCodeBlock])
- // if (role === 'user' && !renderInputMessageAsMarkdown) {
- // return {messageContent}
- // }
-
if (messageContent.includes('