diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index ce07892bc4..7f7100dc54 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -98,7 +98,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_OPTIONS: --max-old-space-size=8192 - MAIN_VITE_CHERRYIN_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYIN_CLIENT_SECRET }} + MAIN_VITE_CHERRYAI_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYAI_CLIENT_SECRET }} MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }} RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }} RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }} @@ -115,7 +115,7 @@ jobs: APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_OPTIONS: --max-old-space-size=8192 - MAIN_VITE_CHERRYIN_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYIN_CLIENT_SECRET }} + MAIN_VITE_CHERRYAI_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYAI_CLIENT_SECRET }} MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }} RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }} RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }} @@ -127,7 +127,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_OPTIONS: --max-old-space-size=8192 - MAIN_VITE_CHERRYIN_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYIN_CLIENT_SECRET }} + MAIN_VITE_CHERRYAI_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYAI_CLIENT_SECRET }} MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }} RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }} RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7428aa031e..c4a772ad6b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,7 +85,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_OPTIONS: --max-old-space-size=8192 - MAIN_VITE_CHERRYIN_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYIN_CLIENT_SECRET }} + MAIN_VITE_CHERRYAI_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYAI_CLIENT_SECRET }} MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }} RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }} RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }} @@ -103,7 +103,7 @@ jobs: APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_OPTIONS: --max-old-space-size=8192 - MAIN_VITE_CHERRYIN_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYIN_CLIENT_SECRET }} + MAIN_VITE_CHERRYAI_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYAI_CLIENT_SECRET }} MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }} RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }} RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }} @@ -115,7 +115,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_OPTIONS: --max-old-space-size=8192 - MAIN_VITE_CHERRYIN_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYIN_CLIENT_SECRET }} + MAIN_VITE_CHERRYAI_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYAI_CLIENT_SECRET }} MAIN_VITE_MINERU_API_KEY: ${{ vars.MAIN_VITE_MINERU_API_KEY }} RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }} RENDERER_VITE_PPIO_APP_SECRET: ${{ vars.RENDERER_VITE_PPIO_APP_SECRET }} diff --git a/.oxlintrc.json b/.oxlintrc.json index 6f4accbece..0a6a9764b7 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -15,7 +15,7 @@ ".gitignore", "scripts/cloudflare-worker.js", "src/main/integration/nutstore/sso/lib/**", - "src/main/integration/cherryin/index.js", + "src/main/integration/cherryai/index.js", "src/main/integration/nutstore/sso/lib/**", "src/renderer/src/ui/**", "packages/**/dist", diff --git a/electron-builder.yml b/electron-builder.yml index 8fd4b6a6f6..0fe2f2d3b9 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -126,17 +126,13 @@ artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | ✨ 新功能: - - 支持在对话中显示 AI 生成的图片 - - 代码编辑工具支持更多终端类型 - - 新增 Azure AI 服务支持 - - 新增通义千问 Plus 模型 + - 新增 CherryIN 服务商 + - 新增 AiOnly AI 服务商 + - 更新 MCP 服务器卡片布局和样式,改为列表视图 🐛 问题修复: - - 修复翻译功能中选中文本未正确使用的问题 - - 修复文件管理中空格键误删文件的问题 - - 修复部分 AI 服务连接不稳定的问题 - - 修复翻译页面长文本显示异常 - - 优化列表显示样式 + - 修复 QwenMT 模型的翻译内容处理逻辑 + - 修复无法将外部笔记添加到知识库的问题 🚀 性能优化: - 提升输入框响应速度 diff --git a/eslint.config.mjs b/eslint.config.mjs index f9e6c07501..be4a95cd60 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -59,7 +59,7 @@ export default defineConfig([ '.gitignore', 'scripts/cloudflare-worker.js', 'src/main/integration/nutstore/sso/lib/**', - 'src/main/integration/cherryin/index.js', + 'src/main/integration/cherryai/index.js', 'src/main/integration/nutstore/sso/lib/**', 'src/renderer/src/ui/**', 'packages/**/dist' diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 10de86024a..d066c6d3c9 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -8,6 +8,7 @@ export enum IpcChannel { App_ShowUpdateDialog = 'app:show-update-dialog', App_CheckForUpdate = 'app:check-for-update', App_Reload = 'app:reload', + App_Quit = 'app:quit', App_Info = 'app:info', App_Proxy = 'app:proxy', App_SetLaunchToTray = 'app:set-launch-to-tray', @@ -333,6 +334,6 @@ export enum IpcChannel { // OCR OCR_ocr = 'ocr:ocr', - // Cherryin - Cherryin_GetSignature = 'cherryin:get-signature' + // CherryAI + Cherryai_GetSignature = 'cherryai:get-signature' } diff --git a/resources/cherry-studio/privacy-en.html b/resources/cherry-studio/privacy-en.html new file mode 100644 index 0000000000..612f18a70f --- /dev/null +++ b/resources/cherry-studio/privacy-en.html @@ -0,0 +1,252 @@ + + + + + + + Privacy Policy + + + + + +
+

Privacy Policy

+ +

+ Welcome to Cherry Studio (hereinafter referred to as "the Software" or "we"). We highly value your privacy + protection. This Privacy Policy explains how we process and protect your personal information and data. + Please read and understand this policy carefully before using the Software: +

+ +

1. Information We Collect

+

To optimize user experience and improve software quality, we may only collect the following anonymous, + non-personal information:

+ +

The above information is completely anonymous, does not involve any personal identity data, and cannot be + linked to your personal information.

+ +

2. Information We Do Not Collect

+

To maximize the protection of your privacy and security, we explicitly commit that we:

+ + +

3. Data Interaction Description

+

+ The Software uses API Keys from third-party model service providers that you apply for and configure + yourself to complete model calls and conversation functions. The model services you use (such as large + models, API interfaces, etc.) are directly provided by third-party providers of your choice. We do not + intervene, monitor, or interfere with the data transmission process. +

+

+ Data interactions between you and third-party model services are governed by the privacy policies and user + agreements of third-party service providers. We recommend that you fully understand the privacy terms of + relevant service providers before use. +

+ +

4. Local Data Security Protection

+

The Software is a localized application, and all data is stored on your local device by default. We have + taken the following measures to ensure data security:

+ + +

5. Third-Party Services

+

+ When using the Software, you may access third-party services (such as AI model APIs, translation services, + etc.). The use of these third-party services is governed by their respective terms of service and privacy + policies. We strongly recommend that you carefully read and understand the relevant terms before use. +

+ +

6. User Rights

+

You have complete control over your data:

+ + +

7. Children's Privacy Protection

+

The Software is not intended for minors under 18 years of age. If you are a minor, please use the Software + under the guidance of a guardian.

+ +

8. Privacy Policy Updates

+

+ We may update this Privacy Policy based on legal requirements or changes in product features. The updated + policy will be published in the Software and you will be notified before it takes effect. If you do not + agree with the updated terms, you can choose to stop using the Software. +

+ +

9. Contact Us

+

If you have any questions, suggestions, or complaints about this Privacy Policy, please contact us through + the following methods:

+ + + +
+ + + \ No newline at end of file diff --git a/resources/cherry-studio/privacy-zh.html b/resources/cherry-studio/privacy-zh.html new file mode 100644 index 0000000000..db4195f68c --- /dev/null +++ b/resources/cherry-studio/privacy-zh.html @@ -0,0 +1,230 @@ + + + + + + + 隐私协议 + + + + + +
+

隐私协议

+ +

+ 欢迎使用 Cherry Studio(以下简称"本软件"或"我们")。我们高度重视您的隐私保护,本隐私协议将说明我们如何处理与保护您的个人信息和数据。请在使用本软件前仔细阅读并理解本协议: +

+ +

一、我们收集的信息范围

+

为了优化用户体验和提升软件质量,我们仅可能会匿名收集以下非个人化信息:

+ +

上述信息完全匿名,不会涉及任何个人身份数据,也无法关联到您的个人信息。

+ +

二、我们不会收集的任何信息

+

为了最大限度保护您的隐私安全,我们明确承诺:

+ + +

三、数据交互说明

+

+ 本软件采用您自行申请并配置的第三方模型服务提供商的 API Key,以完成相关模型的调用与对话功能。您使用的模型服务(例如大模型、API 接口等)由您选择的第三方提供商直接提供,我们不会介入、监控或干扰数据传输过程。 +

+

+ 您与第三方模型服务之间的数据交互受第三方服务提供商的隐私政策和用户协议约束,我们建议您在使用前充分了解相关服务商的隐私条款。 +

+ +

四、本地数据的安全保护

+

本软件为本地化应用程序,所有数据默认存储在您的本地设备上。我们采取了以下措施保障数据安全:

+ + +

五、第三方服务

+

+ 在使用本软件过程中,您可能会接入第三方服务(如 AI 模型 API、翻译服务等)。这些第三方服务的使用受其各自的服务条款和隐私政策约束。我们强烈建议您在使用前仔细阅读并理解相关条款。 +

+ +

六、用户权利

+

您对自己的数据拥有完全的控制权:

+ + +

七、儿童隐私保护

+

本软件不面向 18 岁以下的未成年人提供服务。如果您是未成年人,请在监护人的指导下使用本软件。

+ +

八、隐私政策的更新

+

+ 我们可能会根据法律法规要求或产品功能的变化更新本隐私协议。更新后的协议将在软件中发布,并在生效前通知您。如果您不同意更新后的条款,您可以选择停止使用本软件。 +

+ +

九、联系我们

+

如果您对本隐私协议有任何疑问、建议或投诉,请通过以下方式联系我们:

+ + + +
+ + + \ No newline at end of file diff --git a/src/main/config.ts b/src/main/config.ts index 0cffcd1768..b4859bafb0 100644 --- a/src/main/config.ts +++ b/src/main/config.ts @@ -21,4 +21,4 @@ export const titleBarOverlayLight = { symbolColor: '#000' } -global.CHERRYIN_CLIENT_SECRET = import.meta.env.MAIN_VITE_CHERRYIN_CLIENT_SECRET +global.CHERRYAI_CLIENT_SECRET = import.meta.env.MAIN_VITE_CHERRYAI_CLIENT_SECRET diff --git a/src/main/integration/cherryai/index.js b/src/main/integration/cherryai/index.js new file mode 100644 index 0000000000..eccd3b85bf --- /dev/null +++ b/src/main/integration/cherryai/index.js @@ -0,0 +1 @@ +var _0xe15d9a;const crypto=require("\u0063\u0072\u0079\u0070\u0074\u006F");_0xe15d9a=(988194^988194)+(417607^417603);var _0x9b_0x742=(247379^247387)+(371889^371892);const CLIENT_ID="\u0063\u0068\u0065\u0072\u0072\u0079\u002D\u0073\u0074\u0075\u0064\u0069\u006F";_0x9b_0x742=(202849^202856)+(796590^796585);var _0xa971e=(422203^422203)+(167917^167919);const CLIENT_SECRET_SUFFIX="\u0047\u0076\u0049\u0036\u0049\u0035\u005A\u0072\u0045\u0048\u0063\u0047\u004F\u0057\u006A\u004F\u0035\u0041\u004B\u0068\u004A\u004B\u0047\u006D\u006E\u0077\u0077\u0047\u0066\u004D\u0036\u0032\u0058\u004B\u0070\u0057\u0071\u006B\u006A\u0068\u0076\u007A\u0052\u0055\u0032\u004E\u005A\u0049\u0069\u006E\u004D\u0037\u0037\u0061\u0054\u0047\u0049\u0071\u0068\u0071\u0079\u0073\u0030\u0067";_0xa971e=(607707^607705)+(127822^127823);const CLIENT_SECRET=global['\u0043\u0048\u0045\u0052\u0052\u0059\u0041\u0049\u005F\u0043\u004C\u0049\u0045\u004E\u0054\u005F\u0053\u0045\u0043\u0052\u0045\u0054']+"\u002E"+CLIENT_SECRET_SUFFIX;class SignatureClient{constructor(clientId,clientSecret){this['\u0063\u006C\u0069\u0065\u006E\u0074\u0049\u0064']=clientId||CLIENT_ID;this['\u0063\u006C\u0069\u0065\u006E\u0074\u0053\u0065\u0063\u0072\u0065\u0074']=clientSecret||CLIENT_SECRET;this['\u0067\u0065\u006E\u0065\u0072\u0061\u0074\u0065\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065']=this['\u0067\u0065\u006E\u0065\u0072\u0061\u0074\u0065\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065']['\u0062\u0069\u006E\u0064'](this);}generateSignature(options){const{'\u006D\u0065\u0074\u0068\u006F\u0064':method,'\u0070\u0061\u0074\u0068':path,'\u0071\u0075\u0065\u0072\u0079':query='','\u0062\u006F\u0064\u0079':body=''}=options;var _0x99a7f=(735625^735624)+(520507^520508);const timestamp=Math['\u0066\u006C\u006F\u006F\u0072'](Date['\u006E\u006F\u0077']()/(351300^352172))['\u0074\u006F\u0053\u0074\u0072\u0069\u006E\u0067']();_0x99a7f=376728^376729;var _0x733a=(876666^876671)+(658949^658944);let bodyString='';_0x733a="kgclcd".split("").reverse().join("");if(body){if(typeof body==="tcejbo".split("").reverse().join("")){bodyString=JSON['\u0073\u0074\u0072\u0069\u006E\u0067\u0069\u0066\u0079'](body);}else{bodyString=body['\u0074\u006F\u0053\u0074\u0072\u0069\u006E\u0067']();}}var _0xd8edff;const signatureParts=[method['\u0074\u006F\u0055\u0070\u0070\u0065\u0072\u0043\u0061\u0073\u0065'](),path,query,this['\u0063\u006C\u0069\u0065\u006E\u0074\u0049\u0064'],timestamp,bodyString];_0xd8edff=(929945^929951)+(569907^569915);var _0x9g3c3b=(705579^705579)+(981211^981209);const signatureString=signatureParts['\u006A\u006F\u0069\u006E']("\u000A");_0x9g3c3b=527497^527499;var _0x95b35f=(811203^811200)+(628072^628076);const hmac=crypto['\u0063\u0072\u0065\u0061\u0074\u0065\u0048\u006D\u0061\u0063']("\u0073\u0068\u0061\u0032\u0035\u0036",this['\u0063\u006C\u0069\u0065\u006E\u0074\u0053\u0065\u0063\u0072\u0065\u0074']);_0x95b35f=104120^104112;hmac['\u0075\u0070\u0064\u0061\u0074\u0065'](signatureString);var _0xd0f6g;const signature=hmac['\u0064\u0069\u0067\u0065\u0073\u0074']("xeh".split("").reverse().join(""));_0xd0f6g=(615019^615018)+(266997^266992);return{'X-Client-ID':this['\u0063\u006C\u0069\u0065\u006E\u0074\u0049\u0064'],"\u0058\u002D\u0054\u0069\u006D\u0065\u0073\u0074\u0061\u006D\u0070":timestamp,'X-Signature':signature};}}const signatureClient=new SignatureClient();const generateSignature=signatureClient['\u0067\u0065\u006E\u0065\u0072\u0061\u0074\u0065\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065'];module['\u0065\u0078\u0070\u006F\u0072\u0074\u0073']={'\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065\u0043\u006C\u0069\u0065\u006E\u0074':SignatureClient,"generateSignature":generateSignature}; \ No newline at end of file diff --git a/src/main/integration/cherryin/index.js b/src/main/integration/cherryin/index.js deleted file mode 100644 index af185389eb..0000000000 --- a/src/main/integration/cherryin/index.js +++ /dev/null @@ -1 +0,0 @@ -var _0x6gg;const crypto=require("\u0063\u0072\u0079\u0070\u0074\u006F");_0x6gg='\u006D\u006F\u006C\u006A\u0065\u0065';var _0x111cbe;const CLIENT_ID="oiduts-yrrehc".split("").reverse().join("");_0x111cbe=(977158^977167)+(164595^164594);var _0x6d6adc=(756649^756650)+(497587^497587);const CLIENT_SECRET_SUFFIX="\u0047\u0076\u0049\u0036\u0049\u0035\u005A\u0072\u0045\u0048\u0063\u0047\u004F\u0057\u006A\u004F\u0035\u0041\u004B\u0068\u004A\u004B\u0047\u006D\u006E\u0077\u0077\u0047\u0066\u004D\u0036\u0032\u0058\u004B\u0070\u0057\u0071\u006B\u006A\u0068\u0076\u007A\u0052\u0055\u0032\u004E\u005A\u0049\u0069\u006E\u004D\u0037\u0037\u0061\u0054\u0047\u0049\u0071\u0068\u0071\u0079\u0073\u0030\u0067";_0x6d6adc=233169^233176;const CLIENT_SECRET=global['\u0043\u0048\u0045\u0052\u0052\u0059\u0049\u004E\u005F\u0043\u004C\u0049\u0045\u004E\u0054\u005F\u0053\u0045\u0043\u0052\u0045\u0054']+"\u002E"+CLIENT_SECRET_SUFFIX;class SignatureClient{constructor(clientId,clientSecret){this['\u0063\u006C\u0069\u0065\u006E\u0074\u0049\u0064']=clientId||CLIENT_ID;this['\u0063\u006C\u0069\u0065\u006E\u0074\u0053\u0065\u0063\u0072\u0065\u0074']=clientSecret||CLIENT_SECRET;this['\u0067\u0065\u006E\u0065\u0072\u0061\u0074\u0065\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065']=this['\u0067\u0065\u006E\u0065\u0072\u0061\u0074\u0065\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065']['\u0062\u0069\u006E\u0064'](this);}generateSignature(options){const{"method":method,"path":path,"query":query='',"body":body=''}=options;const timestamp=Math['\u0066\u006C\u006F\u006F\u0072'](Date['\u006E\u006F\u0077']()/(110765^111429))['\u0074\u006F\u0053\u0074\u0072\u0069\u006E\u0067']();var _0xe08cc=(212246^212244)+(773521^773523);let bodyString='';_0xe08cc=(606778^606776)+(962748^962740);if(body){if(typeof body==="\u006F\u0062\u006A\u0065\u0063\u0074"){bodyString=JSON['\u0073\u0074\u0072\u0069\u006E\u0067\u0069\u0066\u0079'](body);}else{bodyString=body['\u0074\u006F\u0053\u0074\u0072\u0069\u006E\u0067']();}}const signatureParts=[method['\u0074\u006F\u0055\u0070\u0070\u0065\u0072\u0043\u0061\u0073\u0065'](),path,query,this['\u0063\u006C\u0069\u0065\u006E\u0074\u0049\u0064'],timestamp,bodyString];var _0x5693g=(936664^936668)+(685268^685277);const signatureString=signatureParts['\u006A\u006F\u0069\u006E']("\u000A");_0x5693g=(266582^266576)+(337322^337315);const hmac=crypto['\u0063\u0072\u0065\u0061\u0074\u0065\u0048\u006D\u0061\u0063']("\u0073\u0068\u0061\u0032\u0035\u0036",this['\u0063\u006C\u0069\u0065\u006E\u0074\u0053\u0065\u0063\u0072\u0065\u0074']);hmac['\u0075\u0070\u0064\u0061\u0074\u0065'](signatureString);var _0x5fba=(354480^354481)+(537437^537434);const signature=hmac['\u0064\u0069\u0067\u0065\u0073\u0074']("\u0068\u0065\u0078");_0x5fba=(249614^249610)+(915906^915914);return{'X-Client-ID':this['\u0063\u006C\u0069\u0065\u006E\u0074\u0049\u0064'],'X-Timestamp':timestamp,'X-Signature':signature};}}const signatureClient=new SignatureClient();const generateSignature=signatureClient['\u0067\u0065\u006E\u0065\u0072\u0061\u0074\u0065\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065'];module['\u0065\u0078\u0070\u006F\u0072\u0074\u0073']={'\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065\u0043\u006C\u0069\u0065\u006E\u0074':SignatureClient,'\u0067\u0065\u006E\u0065\u0072\u0061\u0074\u0065\u0053\u0069\u0067\u006E\u0061\u0074\u0075\u0072\u0065':generateSignature}; \ No newline at end of file diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 9a8c3fddd2..dacc296112 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -4,7 +4,7 @@ import path from 'node:path' import { loggerService } from '@logger' import { isLinux, isMac, isPortable, isWin } from '@main/constant' -import { generateSignature } from '@main/integration/cherryin' +import { generateSignature } from '@main/integration/cherryai' import anthropicService from '@main/services/AnthropicService' import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process' import { handleZoomFactor } from '@main/utils/zoom' @@ -136,6 +136,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { }) ipcMain.handle(IpcChannel.App_Reload, () => mainWindow.reload()) + ipcMain.handle(IpcChannel.App_Quit, () => app.quit()) ipcMain.handle(IpcChannel.Open_Website, (_, url: string) => shell.openExternal(url)) // Update @@ -871,6 +872,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ocrService.ocr(file, provider) ) - // CherryIN - ipcMain.handle(IpcChannel.Cherryin_GetSignature, (_, params) => generateSignature(params)) + // CherryAI + ipcMain.handle(IpcChannel.Cherryai_GetSignature, (_, params) => generateSignature(params)) } diff --git a/src/main/services/AppUpdater.ts b/src/main/services/AppUpdater.ts index bdfb8e3cc8..3cb1558b0e 100644 --- a/src/main/services/AppUpdater.ts +++ b/src/main/services/AppUpdater.ts @@ -30,7 +30,8 @@ export default class AppUpdater { autoUpdater.autoInstallOnAppQuit = configManager.getAutoUpdate() autoUpdater.requestHeaders = { ...autoUpdater.requestHeaders, - 'User-Agent': generateUserAgent() + 'User-Agent': generateUserAgent(), + 'X-Client-Id': configManager.getClientId() } autoUpdater.on('error', (error) => { diff --git a/src/main/services/ConfigManager.ts b/src/main/services/ConfigManager.ts index 5f5be2c723..3cab0bf91d 100644 --- a/src/main/services/ConfigManager.ts +++ b/src/main/services/ConfigManager.ts @@ -2,6 +2,7 @@ import { defaultLanguage, UpgradeChannel, ZOOM_SHORTCUTS } from '@shared/config/ import { LanguageVarious, Shortcut, ThemeMode } from '@types' import { app } from 'electron' import Store from 'electron-store' +import { v4 as uuidv4 } from 'uuid' import { locales } from '../utils/locales' @@ -27,7 +28,8 @@ export enum ConfigKeys { SelectionAssistantFilterList = 'selectionAssistantFilterList', DisableHardwareAcceleration = 'disableHardwareAcceleration', Proxy = 'proxy', - EnableDeveloperMode = 'enableDeveloperMode' + EnableDeveloperMode = 'enableDeveloperMode', + ClientId = 'clientId' } export class ConfigManager { @@ -241,6 +243,17 @@ export class ConfigManager { this.set(ConfigKeys.EnableDeveloperMode, value) } + getClientId(): string { + let clientId = this.get(ConfigKeys.ClientId) + + if (!clientId) { + clientId = uuidv4() + this.set(ConfigKeys.ClientId, clientId) + } + + return clientId + } + set(key: string, value: unknown, isNotify: boolean = false) { this.store.set(key, value) isNotify && this.notifySubscribers(key, value) diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index 9b8a176a34..66aed098e7 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -256,7 +256,7 @@ export class WindowService { private setupWebContentsHandlers(mainWindow: BrowserWindow) { mainWindow.webContents.on('will-navigate', (event, url) => { - if (url.includes('localhost:5173')) { + if (url.includes('localhost:517')) { return } @@ -275,7 +275,8 @@ export class WindowService { 'https://aihubmix.com/topup', 'https://aihubmix.com/statistics', 'https://dash.302.ai/sso/login', - 'https://dash.302.ai/charge' + 'https://dash.302.ai/charge', + 'https://www.aiionly.com/login' ] if (oauthProviderUrls.some((link) => url.startsWith(link))) { diff --git a/src/preload/index.ts b/src/preload/index.ts index d302b08441..10b0c00ab3 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -47,6 +47,7 @@ const api = { getDiskInfo: (directoryPath: string): Promise<{ free: number; size: number } | null> => ipcRenderer.invoke(IpcChannel.App_GetDiskInfo, directoryPath), reload: () => ipcRenderer.invoke(IpcChannel.App_Reload), + quit: () => ipcRenderer.invoke(IpcChannel.App_Quit), setProxy: (proxy: string | undefined, bypassRules?: string) => ipcRenderer.invoke(IpcChannel.App_Proxy, proxy, bypassRules), checkForUpdate: () => ipcRenderer.invoke(IpcChannel.App_CheckForUpdate), @@ -455,9 +456,9 @@ const api = { ocr: (file: SupportedOcrFile, provider: OcrProvider): Promise => ipcRenderer.invoke(IpcChannel.OCR_ocr, file, provider) }, - cherryin: { + cherryai: { generateSignature: (params: { method: string; path: string; query: string; body: Record }) => - ipcRenderer.invoke(IpcChannel.Cherryin_GetSignature, params) + ipcRenderer.invoke(IpcChannel.Cherryai_GetSignature, params) }, windowControls: { minimize: (): Promise => ipcRenderer.invoke(IpcChannel.Windows_Minimize), diff --git a/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts b/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts index 31a911533e..b38ab59537 100644 --- a/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts +++ b/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts @@ -5,7 +5,7 @@ import { AihubmixAPIClient } from './aihubmix/AihubmixAPIClient' import { AnthropicAPIClient } from './anthropic/AnthropicAPIClient' import { AwsBedrockAPIClient } from './aws/AwsBedrockAPIClient' import { BaseApiClient } from './BaseApiClient' -import { CherryinAPIClient } from './cherryin/CherryinAPIClient' +import { CherryAiAPIClient } from './cherryai/CherryAiAPIClient' import { GeminiAPIClient } from './gemini/GeminiAPIClient' import { VertexAPIClient } from './gemini/VertexAPIClient' import { NewAPIClient } from './newapi/NewAPIClient' @@ -34,8 +34,8 @@ export class ApiClientFactory { let instance: BaseApiClient // 首先检查特殊的 Provider ID - if (provider.id === 'cherryin') { - instance = new CherryinAPIClient(provider) as BaseApiClient + if (provider.id === 'cherryai') { + instance = new CherryAiAPIClient(provider) as BaseApiClient return instance } diff --git a/src/renderer/src/aiCore/legacy/clients/__tests__/index.clientCompatibilityTypes.test.ts b/src/renderer/src/aiCore/legacy/clients/__tests__/index.clientCompatibilityTypes.test.ts index 343bc4d544..d70d9c58f9 100644 --- a/src/renderer/src/aiCore/legacy/clients/__tests__/index.clientCompatibilityTypes.test.ts +++ b/src/renderer/src/aiCore/legacy/clients/__tests__/index.clientCompatibilityTypes.test.ts @@ -35,10 +35,16 @@ vi.mock('@renderer/config/models', () => ({ findTokenLimit: vi.fn().mockReturnValue(4096), isFunctionCallingModel: vi.fn().mockReturnValue(false), DEFAULT_MAX_TOKENS: 4096, + qwen38bModel: { + id: 'Qwen/Qwen3-8B', + name: 'Qwen3-8B', + provider: 'cherryai', + group: 'Qwen' + }, glm45FlashModel: { id: 'glm-4.5-flash', name: 'GLM-4.5-Flash', - provider: 'cherryin', + provider: 'cherryai', group: 'GLM-4.5' } })) diff --git a/src/renderer/src/aiCore/legacy/clients/cherryin/CherryinAPIClient.ts b/src/renderer/src/aiCore/legacy/clients/cherryai/CherryAiAPIClient.ts similarity index 86% rename from src/renderer/src/aiCore/legacy/clients/cherryin/CherryinAPIClient.ts rename to src/renderer/src/aiCore/legacy/clients/cherryai/CherryAiAPIClient.ts index bf3ed7d718..8f8969bd59 100644 --- a/src/renderer/src/aiCore/legacy/clients/cherryin/CherryinAPIClient.ts +++ b/src/renderer/src/aiCore/legacy/clients/cherryai/CherryAiAPIClient.ts @@ -4,7 +4,7 @@ import OpenAI from 'openai' import { OpenAIAPIClient } from '../openai/OpenAIApiClient' -export class CherryinAPIClient extends OpenAIAPIClient { +export class CherryAiAPIClient extends OpenAIAPIClient { constructor(provider: Provider) { super(provider) } @@ -17,7 +17,7 @@ export class CherryinAPIClient extends OpenAIAPIClient { options = options || {} options.headers = options.headers || {} - const signature = await window.api.cherryin.generateSignature({ + const signature = await window.api.cherryai.generateSignature({ method: 'POST', path: '/chat/completions', query: '', @@ -34,7 +34,7 @@ export class CherryinAPIClient extends OpenAIAPIClient { } override getClientCompatibilityType(): string[] { - return ['CherryinAPIClient'] + return ['CherryAiAPIClient'] } public async listModels(): Promise { @@ -43,7 +43,7 @@ export class CherryinAPIClient extends OpenAIAPIClient { const created = Date.now() return models.map((id) => ({ id, - owned_by: 'cherryin', + owned_by: 'cherryai', object: 'model' as const, created })) diff --git a/src/renderer/src/aiCore/legacy/middleware/common/ErrorHandlerMiddleware.ts b/src/renderer/src/aiCore/legacy/middleware/common/ErrorHandlerMiddleware.ts index d80c9d2f83..dde98cbd1e 100644 --- a/src/renderer/src/aiCore/legacy/middleware/common/ErrorHandlerMiddleware.ts +++ b/src/renderer/src/aiCore/legacy/middleware/common/ErrorHandlerMiddleware.ts @@ -1,6 +1,6 @@ import { loggerService } from '@logger' import { isZhipuModel } from '@renderer/config/models' -import store from '@renderer/store' +import { getStoreProviders } from '@renderer/hooks/useStore' import { Chunk } from '@renderer/types/chunk' import { CompletionsParams, CompletionsResult } from '../schemas' @@ -87,7 +87,7 @@ function handleError(error: any, params: CompletionsParams): any { * 2. 绘画功能(enableGenerateImage为true)使用通用错误处理 */ function handleZhipuError(error: any): any { - const provider = store.getState().llm.providers.find((p) => p.id === 'zhipu') + const provider = getStoreProviders().find((p) => p.id === 'zhipu') const logger = loggerService.withContext('handleZhipuError') // 定义错误模式映射 diff --git a/src/renderer/src/aiCore/provider/providerConfig.ts b/src/renderer/src/aiCore/provider/providerConfig.ts index 33116272e8..eaaef15211 100644 --- a/src/renderer/src/aiCore/provider/providerConfig.ts +++ b/src/renderer/src/aiCore/provider/providerConfig.ts @@ -250,10 +250,10 @@ export async function prepareSpecialProviderConfig( config.options.apiKey = token break } - case 'cherryin': { + case 'cherryai': { config.options.fetch = async (url, options) => { // 在这里对最终参数进行签名 - const signature = await window.api.cherryin.generateSignature({ + const signature = await window.api.cherryai.generateSignature({ method: 'POST', path: '/chat/completions', query: '', diff --git a/src/renderer/src/assets/images/providers/aiOnly.webp b/src/renderer/src/assets/images/providers/aiOnly.webp new file mode 100644 index 0000000000..f86f74543b Binary files /dev/null and b/src/renderer/src/assets/images/providers/aiOnly.webp differ diff --git a/src/renderer/src/components/FreeTrialModelTag.tsx b/src/renderer/src/components/FreeTrialModelTag.tsx index 0ce63ade37..b86de99708 100644 --- a/src/renderer/src/components/FreeTrialModelTag.tsx +++ b/src/renderer/src/components/FreeTrialModelTag.tsx @@ -15,7 +15,7 @@ interface Props { } export const FreeTrialModelTag: FC = ({ model, showLabel = true }) => { - if (model.provider !== 'cherryin') { + if (model.provider !== 'cherryai') { return null } diff --git a/src/renderer/src/components/OAuth/OAuthButton.tsx b/src/renderer/src/components/OAuth/OAuthButton.tsx index fec3aae619..3368f60afe 100644 --- a/src/renderer/src/components/OAuth/OAuthButton.tsx +++ b/src/renderer/src/components/OAuth/OAuthButton.tsx @@ -3,6 +3,7 @@ import { Provider } from '@renderer/types' import { oauthWith302AI, oauthWithAihubmix, + oauthWithAiOnly, oauthWithPPIO, oauthWithSiliconFlow, oauthWithTokenFlux @@ -46,6 +47,10 @@ const OAuthButton: FC = ({ provider, onSuccess, ...buttonProps }) => { if (provider.id === '302ai') { oauthWith302AI(handleSuccess) } + + if (provider.id === 'aionly') { + oauthWithAiOnly(handleSuccess) + } } return ( diff --git a/src/renderer/src/components/Popups/PrivacyPopup.tsx b/src/renderer/src/components/Popups/PrivacyPopup.tsx new file mode 100644 index 0000000000..1ec9639bfc --- /dev/null +++ b/src/renderer/src/components/Popups/PrivacyPopup.tsx @@ -0,0 +1,137 @@ +import { TopView } from '@renderer/components/TopView' +import { useTheme } from '@renderer/context/ThemeProvider' +import { ThemeMode } from '@renderer/types' +import { runAsyncFunction } from '@renderer/utils' +import { Button, Modal } from 'antd' +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +const WebViewContainer = styled.div` + width: 100%; + height: 500px; + overflow: hidden; + + webview { + width: 100%; + height: 100%; + border: none; + background: transparent; + } +` + +interface ShowParams { + title?: string + showDeclineButton?: boolean +} + +interface Props extends ShowParams { + resolve: (data: any) => void +} + +const PopupContainer: React.FC = ({ title, showDeclineButton = true, resolve }) => { + const [open, setOpen] = useState(true) + const [privacyUrl, setPrivacyUrl] = useState('') + const { theme } = useTheme() + const { i18n } = useTranslation() + + const getTitle = () => { + if (title) return title + const isChinese = i18n.language.startsWith('zh') + return isChinese ? '隐私协议' : 'Privacy Policy' + } + + const handleAccept = () => { + setOpen(false) + localStorage.setItem('privacy-popup-accepted', 'true') + resolve({ accepted: true }) + } + + const handleDecline = () => { + setOpen(false) + window.api.quit() + resolve({ accepted: false }) + } + + const onClose = () => { + if (!showDeclineButton) { + handleAccept() + } else { + handleDecline() + } + } + + useEffect(() => { + runAsyncFunction(async () => { + const { appPath } = await window.api.getAppInfo() + const isChinese = i18n.language.startsWith('zh') + const htmlFile = isChinese ? 'privacy-zh.html' : 'privacy-en.html' + const url = `file://${appPath}/resources/cherry-studio/${htmlFile}?theme=${theme === ThemeMode.dark ? 'dark' : 'light'}` + setPrivacyUrl(url) + }) + }, [theme, i18n.language]) + + PrivacyPopup.hide = () => setOpen(false) + + return ( + + {i18n.language.startsWith('zh') ? '拒绝' : 'Decline'} + + ), + + ].filter(Boolean)}> + + {privacyUrl && } + + + ) +} + +const TopViewKey = 'PrivacyPopup' + +export default class PrivacyPopup { + static topviewId = 0 + static hide() { + TopView.hide(TopViewKey) + } + static async show(props?: ShowParams) { + const accepted = localStorage.getItem('privacy-popup-accepted') + + if (accepted) { + return + } + + return new Promise<{ accepted: boolean }>((resolve) => { + TopView.show( + { + resolve(v) + TopView.hide(TopViewKey) + }} + />, + TopViewKey + ) + }) + } +} diff --git a/src/renderer/src/components/Popups/SelectModelPopup/popup.tsx b/src/renderer/src/components/Popups/SelectModelPopup/popup.tsx index 1cd7926145..45560bcd6c 100644 --- a/src/renderer/src/components/Popups/SelectModelPopup/popup.tsx +++ b/src/renderer/src/components/Popups/SelectModelPopup/popup.tsx @@ -1,5 +1,6 @@ import { PushpinOutlined } from '@ant-design/icons' import { FreeTrialModelTag } from '@renderer/components/FreeTrialModelTag' +import { HStack } from '@renderer/components/Layout' import ModelTagsWithLabel from '@renderer/components/ModelTagsWithLabel' import { TopView } from '@renderer/components/TopView' import { DynamicVirtualList, type DynamicVirtualListRef } from '@renderer/components/VirtualList' @@ -102,16 +103,18 @@ const PopupContainer: React.FC = ({ model, filter: baseFilter, showTagFil (model: Model, provider: Provider, isPinned: boolean): FlatListModel => { const modelId = getModelUniqId(model) const groupName = getFancyProviderName(provider) - const isCherryin = provider.id === 'cherryin' + const isCherryAi = provider.id === 'cherryai' return { key: isPinned ? `${modelId}_pinned` : modelId, type: 'model', name: ( - {model.name} - {isPinned && | {groupName}} - {isCherryin && } + + {model.name} + {isPinned && | {groupName}} + + {isCherryAi && } ), tags: ( @@ -542,6 +545,7 @@ const ModelItemLeft = styled.div` const ModelName = styled.div` display: flex; flex-direction: row; + justify-content: space-between; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/src/renderer/src/components/QuickPanel/view.tsx b/src/renderer/src/components/QuickPanel/view.tsx index 59d72b2de2..52c33607c7 100644 --- a/src/renderer/src/components/QuickPanel/view.tsx +++ b/src/renderer/src/components/QuickPanel/view.tsx @@ -158,15 +158,22 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { const cursorPosition = textArea.selectionStart ?? 0 const textBeforeCursor = textArea.value.slice(0, cursorPosition) - // 查找最后一个 @ 或 / 符号的位置 - const lastAtIndex = textBeforeCursor.lastIndexOf('@') - const lastSlashIndex = textBeforeCursor.lastIndexOf('/') - const lastSymbolIndex = Math.max(lastAtIndex, lastSlashIndex) + // 查找末尾最近的触发符号(@ 或 /),允许位于文本起始或空格后 + const match = textBeforeCursor.match(/(^| )([@/][^\s]*)$/) + if (!match) return - if (lastSymbolIndex === -1) return + const matchIndex = match.index ?? -1 + if (matchIndex === -1) return + + const boundarySegment = match[1] ?? '' + const symbolSegment = match[2] ?? '' + if (!symbolSegment) return + + const boundaryStart = matchIndex + const symbolStart = boundaryStart + boundarySegment.length // 根据 includeSymbol 决定是否删除符号 - const deleteStart = includeSymbol ? lastSymbolIndex : lastSymbolIndex + 1 + const deleteStart = includeSymbol ? boundaryStart : symbolStart + 1 const deleteEnd = cursorPosition if (deleteStart >= deleteEnd) return @@ -203,7 +210,7 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { if (textArea) { setInputText(textArea.value) } - } else if (action && !['outsideclick', 'esc', 'enter_empty'].includes(action)) { + } else if (action && !['outsideclick', 'esc', 'enter_empty', 'no_result'].includes(action)) { clearSearchText(true) } }, @@ -533,6 +540,18 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { const visibleNonPinnedCount = useMemo(() => list.filter((i) => !i.alwaysVisible).length, [list]) const collapsed = hasSearchText && visibleNonPinnedCount === 0 + useEffect(() => { + if (!ctx.isVisible) return + if (!collapsed) return + if (ctx.triggerInfo?.type !== 'input') return + if (ctx.multiple) return + + const trimmedSearch = searchText.replace(/^[/@]/, '').trim() + if (!trimmedSearch) return + + handleClose('no_result') + }, [collapsed, ctx.isVisible, ctx.triggerInfo, ctx.multiple, handleClose, searchText]) + const estimateSize = useCallback(() => ITEM_HEIGHT, []) const rowRenderer = useCallback( diff --git a/src/renderer/src/config/__test__/models.test.ts b/src/renderer/src/config/__test__/models.test.ts index 547ff42efd..926ac3547d 100644 --- a/src/renderer/src/config/__test__/models.test.ts +++ b/src/renderer/src/config/__test__/models.test.ts @@ -16,7 +16,7 @@ describe('Qwen Model Detection', () => { initialState: {} })) vi.mock('@renderer/services/AssistantService', () => ({ - getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryin' }) + getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryai' }) })) }) test('isQwenReasoningModel', () => { @@ -52,7 +52,7 @@ describe('Vision Model Detection', () => { initialState: {} })) vi.mock('@renderer/services/AssistantService', () => ({ - getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryin' }) + getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryai' }) })) }) test('isVisionModel', () => { @@ -81,7 +81,7 @@ describe('Web Search Model Detection', () => { initialState: {} })) vi.mock('@renderer/services/AssistantService', () => ({ - getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryin' }) + getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryai' }) })) }) test('isWebSearchModel', () => { diff --git a/src/renderer/src/config/models/default.ts b/src/renderer/src/config/models/default.ts index 1b8e9ad1bc..02bf37af9e 100644 --- a/src/renderer/src/config/models/default.ts +++ b/src/renderer/src/config/models/default.ts @@ -3,14 +3,14 @@ import { Model, SystemProviderId } from '@renderer/types' export const glm45FlashModel: Model = { id: 'glm-4.5-flash', name: 'GLM-4.5-Flash', - provider: 'cherryin', + provider: 'cherryai', group: 'GLM-4.5' } export const qwen38bModel: Model = { id: 'Qwen/Qwen3-8B', name: 'Qwen3-8B', - provider: 'cherryin', + provider: 'cherryai', group: 'Qwen' } @@ -25,20 +25,7 @@ export const SYSTEM_MODELS: Record = // Default quick assistant model glm45FlashModel ], - cherryin: [ - { - id: 'glm-4.5-flash', - name: 'GLM-4.5-Flash', - provider: 'cherryin', - group: 'GLM-4.5' - }, - { - id: 'Qwen/Qwen3-8B', - name: 'Qwen3-8B', - provider: 'cherryin', - group: 'Qwen' - } - ], + cherryin: [], vertexai: [], '302ai': [ { @@ -1785,5 +1772,37 @@ export const SYSTEM_MODELS: Record = provider: 'poe', group: 'poe' } + ], + aionly: [ + { + id: 'claude-opus-4.1', + name: 'claude-opus-4.1', + provider: 'aionly', + group: 'claude' + }, + { + id: 'claude-sonnet4', + name: 'claude-sonnet4', + provider: 'aionly', + group: 'claude' + }, + { + id: 'claude-3.5-sonnet-v2', + name: 'claude-3.5-sonnet-v2', + provider: 'aionly', + group: 'claude' + }, + { + id: 'gpt-4.1', + name: 'gpt-4.1', + provider: 'aionly', + group: 'gpt' + }, + { + id: 'gemini-2.5-flash', + name: 'gemini-2.5-flash', + provider: 'aionly', + group: 'gemini' + } ] } diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index a710605b64..80ea9bdf7a 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -3,6 +3,7 @@ import HunyuanProviderLogo from '@renderer/assets/images/models/hunyuan.png' import AzureProviderLogo from '@renderer/assets/images/models/microsoft.png' import Ai302ProviderLogo from '@renderer/assets/images/providers/302ai.webp' import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp' +import AiOnlyProviderLogo from '@renderer/assets/images/providers/aiOnly.webp' import AlayaNewProviderLogo from '@renderer/assets/images/providers/alayanew.webp' import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.png' import AwsProviderLogo from '@renderer/assets/images/providers/aws-bedrock.webp' @@ -63,7 +64,18 @@ import { } from '@renderer/types' import { TOKENFLUX_HOST } from './constant' -import { SYSTEM_MODELS } from './models' +import { glm45FlashModel, qwen38bModel, SYSTEM_MODELS } from './models' + +export const CHERRYAI_PROVIDER: SystemProvider = { + id: 'cherryai' as SystemProviderId, + name: 'CherryAI', + type: 'openai', + apiKey: '', + apiHost: 'https://api.cherry-ai.com/', + models: [glm45FlashModel, qwen38bModel], + isSystem: true, + enabled: true +} export const SYSTEM_PROVIDERS_CONFIG: Record = { cherryin: { @@ -71,8 +83,8 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = name: 'CherryIN', type: 'openai', apiKey: '', - apiHost: 'https://api.cherry-ai.com/', - models: SYSTEM_MODELS.cherryin, + apiHost: 'https://open.cherryin.ai', + models: [], isSystem: true, enabled: true }, @@ -600,6 +612,16 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = models: SYSTEM_MODELS['poe'], isSystem: true, enabled: false + }, + aionly: { + id: 'aionly', + name: 'AIOnly', + type: 'openai', + apiKey: '', + apiHost: 'https://api.aiionly.com', + models: SYSTEM_MODELS.aionly, + isSystem: true, + enabled: false } } as const @@ -661,7 +683,8 @@ export const PROVIDER_LOGO_MAP: AtLeast = { vertexai: VertexAIProviderLogo, 'new-api': NewAPIProviderLogo, 'aws-bedrock': AwsProviderLogo, - poe: 'poe' // use svg icon component + poe: 'poe', // use svg icon component + aionly: AiOnlyProviderLogo } as const export function getProviderLogo(providerId: string) { @@ -687,12 +710,13 @@ type ProviderUrls = { export const PROVIDER_URLS: Record = { cherryin: { api: { - url: 'https://api.cherry-ai.com' + url: 'https://open.cherryin.ai' }, websites: { - official: 'https://cherry-ai.com', - docs: 'https://docs.cherry-ai.com', - models: 'https://docs.cherry-ai.com/pre-basic/providers/cherryin' + official: 'https://open.cherryin.ai', + apiKey: 'https://open.cherryin.ai/console/token', + docs: 'https://open.cherryin.ai', + models: 'https://open.cherryin.ai/pricing' } }, ph8: { @@ -1255,6 +1279,17 @@ export const PROVIDER_URLS: Record = { docs: 'https://creator.poe.com/docs/external-applications/openai-compatible-api', models: 'https://poe.com/' } + }, + aionly: { + api: { + url: 'https://api.aiionly.com' + }, + websites: { + official: 'https://www.aiionly.com', + apiKey: 'https://www.aiionly.com/keyApi', + docs: 'https://www.aiionly.com/document', + models: 'https://www.aiionly.com' + } } } diff --git a/src/renderer/src/hooks/useModel.ts b/src/renderer/src/hooks/useModel.ts index 27962e7cef..75119d0f89 100644 --- a/src/renderer/src/hooks/useModel.ts +++ b/src/renderer/src/hooks/useModel.ts @@ -1,6 +1,5 @@ -import store from '@renderer/store' - import { useProviders } from './useProvider' +import { getStoreProviders } from './useStore' export function useModel(id?: string, providerId?: string) { const { providers } = useProviders() @@ -15,7 +14,7 @@ export function useModel(id?: string, providerId?: string) { } export function getModel(id?: string, providerId?: string) { - const providers = store.getState().llm.providers + const providers = getStoreProviders() const allModels = providers.map((p) => p.models).flat() return allModels.find((m) => { if (providerId) { diff --git a/src/renderer/src/hooks/useProvider.ts b/src/renderer/src/hooks/useProvider.ts index 9182db3776..96120d4dc8 100644 --- a/src/renderer/src/hooks/useProvider.ts +++ b/src/renderer/src/hooks/useProvider.ts @@ -1,4 +1,5 @@ import { createSelector } from '@reduxjs/toolkit' +import { CHERRYAI_PROVIDER } from '@renderer/config/providers' import { getDefaultProvider } from '@renderer/services/AssistantService' import { useAppDispatch, useAppSelector } from '@renderer/store' import { @@ -16,7 +17,7 @@ import { useDefaultModel } from './useAssistant' const selectEnabledProviders = createSelector( (state) => state.llm.providers, - (providers) => providers.filter((p) => p.enabled) + (providers) => providers.filter((p) => p.enabled).concat(CHERRYAI_PROVIDER) ) export function useProviders() { @@ -24,7 +25,7 @@ export function useProviders() { const dispatch = useAppDispatch() return { - providers: providers || {}, + providers: providers || [], addProvider: (provider: Provider) => dispatch(addProvider(provider)), removeProvider: (provider: Provider) => dispatch(removeProvider(provider)), updateProvider: (updates: Partial & { id: string }) => dispatch(updateProvider(updates)), @@ -45,7 +46,9 @@ export function useAllProviders() { } export function useProvider(id: string) { - const provider = useAppSelector((state) => state.llm.providers.find((p) => p.id === id)) || getDefaultProvider() + const provider = + useAppSelector((state) => state.llm.providers.concat([CHERRYAI_PROVIDER]).find((p) => p.id === id)) || + getDefaultProvider() const dispatch = useAppDispatch() return { diff --git a/src/renderer/src/hooks/useStore.ts b/src/renderer/src/hooks/useStore.ts index 1b731e74c7..53e4646450 100644 --- a/src/renderer/src/hooks/useStore.ts +++ b/src/renderer/src/hooks/useStore.ts @@ -1,4 +1,5 @@ -import { useAppDispatch, useAppSelector } from '@renderer/store' +import { CHERRYAI_PROVIDER } from '@renderer/config/providers' +import store, { useAppDispatch, useAppSelector } from '@renderer/store' import { setAssistantsTabSortType, setShowAssistants, @@ -39,3 +40,7 @@ export function useAssistantsTabSortType() { setAssistantsTabSortType: (sortType: AssistantsSortType) => dispatch(setAssistantsTabSortType(sortType)) } } + +export function getStoreProviders() { + return store.getState().llm.providers.concat([CHERRYAI_PROVIDER]) +} diff --git a/src/renderer/src/i18n/label.ts b/src/renderer/src/i18n/label.ts index c0cc436de8..9f3834d771 100644 --- a/src/renderer/src/i18n/label.ts +++ b/src/renderer/src/i18n/label.ts @@ -86,7 +86,8 @@ const providerKeyMap = { yi: 'provider.yi', zhinao: 'provider.zhinao', zhipu: 'provider.zhipu', - poe: 'provider.poe' + poe: 'provider.poe', + aionly: 'provider.aionly' } as const /** diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index ed12fa512b..73bd6ccfac 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -527,7 +527,8 @@ }, "new_topic": "New Topic {{Command}}", "pause": "Pause", - "placeholder": "Type your message here, press {{key}} to send...", + "placeholder": "Type your message here, press {{key}} to send - @ to Select Model, / to Include Tools", + "placeholder_without_triggers": "Type your message here, press {{key}} to send", "send": "Send", "settings": "Settings", "thinking": { @@ -2231,6 +2232,7 @@ "provider": { "302ai": "302.AI", "aihubmix": "AiHubMix", + "aionly": "AiOnly", "alayanew": "Alaya NeW", "anthropic": "Anthropic", "aws-bedrock": "AWS Bedrock", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 88d2752510..8f08c40cbf 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -527,7 +527,8 @@ }, "new_topic": "新话题 {{Command}}", "pause": "暂停", - "placeholder": "在这里输入消息,按 {{key}} 发送...", + "placeholder": "在这里输入消息,按 {{key}} 发送 - @ 选择模型, / 选择工具", + "placeholder_without_triggers": "在这里输入消息,按 {{key}} 发送", "send": "发送", "settings": "设置", "thinking": { @@ -2231,6 +2232,7 @@ "provider": { "302ai": "302.AI", "aihubmix": "AiHubMix", + "aionly": "唯一AI (AiOnly)", "alayanew": "Alaya NeW", "anthropic": "Anthropic", "aws-bedrock": "AWS Bedrock", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 314955dd8a..fd1671a4ec 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -527,7 +527,8 @@ }, "new_topic": "新話題 {{Command}}", "pause": "暫停", - "placeholder": "在此輸入您的訊息,按 {{key}} 傳送...", + "placeholder": "在此輸入您的訊息,按 {{key}} 傳送 - @ 選擇模型,/ 包含工具", + "placeholder_without_triggers": "在此輸入您的訊息,按 {{key}} 傳送", "send": "傳送", "settings": "設定", "thinking": { @@ -2231,6 +2232,7 @@ "provider": { "302ai": "302.AI", "aihubmix": "AiHubMix", + "aionly": "唯一AI (AiOnly)", "alayanew": "Alaya NeW", "anthropic": "Anthropic", "aws-bedrock": "AWS Bedrock", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index e7e19fe7c4..a894d546c3 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -2232,6 +2232,7 @@ "provider": { "302ai": "302.AI", "aihubmix": "AiHubMix", + "aionly": "AiOnly", "alayanew": "Alaya NeW", "anthropic": "Anthropic", "aws-bedrock": "AWS Bedrock", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index e6a64e0dc8..b3ccb72677 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -2232,6 +2232,7 @@ "provider": { "302ai": "302.AI", "aihubmix": "AiHubMix", + "aionly": "AiOnly", "alayanew": "Alaya NeW", "anthropic": "Antropológico", "aws-bedrock": "AWS Bedrock", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index c60e674419..3f3353e277 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -2232,6 +2232,7 @@ "provider": { "302ai": "302.AI", "aihubmix": "AiHubMix", + "aionly": "AiOnly", "alayanew": "Alaya NeW", "anthropic": "Anthropic", "aws-bedrock": "AWS Bedrock", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 3661d13605..5e57226d96 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -2232,6 +2232,7 @@ "provider": { "302ai": "302.AI", "aihubmix": "AiHubMix", + "aionly": "AiOnly", "alayanew": "Alaya NeW", "anthropic": "Anthropic", "aws-bedrock": "AWS Bedrock", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index f70d79a7df..ff1c89ea6b 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -2232,6 +2232,7 @@ "provider": { "302ai": "302.AI", "aihubmix": "AiHubMix", + "aionly": "AiOnly", "alayanew": "Alaya NeW", "anthropic": "Antropológico", "aws-bedrock": "AWS Bedrock", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 1fcf5bed19..6ff78dd722 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -2232,6 +2232,7 @@ "provider": { "302ai": "302.AI", "aihubmix": "AiHubMix", + "aionly": "AiOnly", "alayanew": "Alaya NeW", "anthropic": "Anthropic", "aws-bedrock": "AWS Bedrock", diff --git a/src/renderer/src/pages/code/CodeToolsPage.tsx b/src/renderer/src/pages/code/CodeToolsPage.tsx index bb15871e44..44c1e323d9 100644 --- a/src/renderer/src/pages/code/CodeToolsPage.tsx +++ b/src/renderer/src/pages/code/CodeToolsPage.tsx @@ -70,7 +70,7 @@ const CodeToolsPage: FC = () => { if (isEmbeddingModel(m) || isRerankModel(m) || isTextToImageModel(m)) { return false } - if (m.provider === 'cherryin') { + if (m.provider === 'cherryai') { return false } if (selectedCliTool === codeTools.claudeCode) { diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 5897c4fe45..282f62656d 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -162,6 +162,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const [tokenCount, setTokenCount] = useState(0) const inputbarToolsRef = useRef(null) + const prevTextRef = useRef(text) // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedEstimate = useCallback( @@ -178,8 +179,21 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = debouncedEstimate(text) }, [text, debouncedEstimate]) + useEffect(() => { + prevTextRef.current = text + }, [text]) + const inputTokenCount = showInputEstimatedTokens ? tokenCount : 0 + const placeholderText = enableQuickPanelTriggers + ? t('chat.input.placeholder', { key: getSendMessageShortcutLabel(sendMessageShortcut) }) + : t('chat.input.placeholder_without_triggers', { + key: getSendMessageShortcutLabel(sendMessageShortcut), + defaultValue: t('chat.input.placeholder', { + key: getSendMessageShortcutLabel(sendMessageShortcut) + }) + }) + const inputEmpty = isEmpty(text.trim()) && files.length === 0 _text = text @@ -441,43 +455,91 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const newText = e.target.value setText(newText) + const prevText = prevTextRef.current + const isDeletion = newText.length < prevText.length + const textArea = textareaRef.current?.resizableTextArea?.textArea - const cursorPosition = textArea?.selectionStart ?? 0 + const cursorPosition = textArea?.selectionStart ?? newText.length const lastSymbol = newText[cursorPosition - 1] + const previousChar = newText[cursorPosition - 2] + const isCursorAtTextStart = cursorPosition <= 1 + const hasValidTriggerBoundary = previousChar === ' ' || isCursorAtTextStart + + const openRootPanelAt = (position: number) => { + const quickPanelMenu = + inputbarToolsRef.current?.getQuickPanelMenu({ + text: newText, + translate + }) || [] + + quickPanel.open({ + title: t('settings.quickPanel.title'), + list: quickPanelMenu, + symbol: QuickPanelReservedSymbol.Root, + triggerInfo: { + type: 'input', + position, + originalText: newText + } + }) + } + + const openMentionPanelAt = (position: number) => { + inputbarToolsRef.current?.openMentionModelsPanel({ + type: 'input', + position, + originalText: newText + }) + } + + if (enableQuickPanelTriggers && !quickPanel.isVisible) { + const textBeforeCursor = newText.slice(0, cursorPosition) + const lastRootIndex = textBeforeCursor.lastIndexOf(QuickPanelReservedSymbol.Root) + const lastMentionIndex = textBeforeCursor.lastIndexOf(QuickPanelReservedSymbol.MentionModels) + const lastTriggerIndex = Math.max(lastRootIndex, lastMentionIndex) + + if (lastTriggerIndex !== -1 && cursorPosition > lastTriggerIndex) { + const triggerChar = newText[lastTriggerIndex] + const boundaryChar = newText[lastTriggerIndex - 1] ?? '' + const hasBoundary = lastTriggerIndex === 0 || /\s/.test(boundaryChar) + const searchSegment = newText.slice(lastTriggerIndex + 1, cursorPosition) + const hasSearchContent = searchSegment.trim().length > 0 + + if (hasBoundary && (!hasSearchContent || isDeletion)) { + if (triggerChar === QuickPanelReservedSymbol.Root) { + openRootPanelAt(lastTriggerIndex) + } else if (triggerChar === QuickPanelReservedSymbol.MentionModels) { + openMentionPanelAt(lastTriggerIndex) + } + } + } + } // 触发符号为 '/':若当前未打开或符号不同,则切换/打开 - if (enableQuickPanelTriggers && lastSymbol === QuickPanelReservedSymbol.Root) { + if (enableQuickPanelTriggers && lastSymbol === QuickPanelReservedSymbol.Root && hasValidTriggerBoundary) { if (quickPanel.isVisible && quickPanel.symbol !== QuickPanelReservedSymbol.Root) { quickPanel.close('switch-symbol') } if (!quickPanel.isVisible || quickPanel.symbol !== QuickPanelReservedSymbol.Root) { - const quickPanelMenu = - inputbarToolsRef.current?.getQuickPanelMenu({ - text: newText, - translate - }) || [] - - quickPanel.open({ - title: t('settings.quickPanel.title'), - list: quickPanelMenu, - symbol: QuickPanelReservedSymbol.Root - }) + openRootPanelAt(cursorPosition - 1) } } // 触发符号为 '@':若当前未打开或符号不同,则切换/打开 - if (enableQuickPanelTriggers && lastSymbol === QuickPanelReservedSymbol.MentionModels) { + if ( + enableQuickPanelTriggers && + lastSymbol === QuickPanelReservedSymbol.MentionModels && + hasValidTriggerBoundary + ) { if (quickPanel.isVisible && quickPanel.symbol !== QuickPanelReservedSymbol.MentionModels) { quickPanel.close('switch-symbol') } if (!quickPanel.isVisible || quickPanel.symbol !== QuickPanelReservedSymbol.MentionModels) { - inputbarToolsRef.current?.openMentionModelsPanel({ - type: 'input', - position: cursorPosition - 1, - originalText: newText - }) + openMentionPanelAt(cursorPosition - 1) } } + + prevTextRef.current = newText }, [enableQuickPanelTriggers, quickPanel, t, translate] ) @@ -783,11 +845,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = value={text} onChange={onChange} onKeyDown={handleKeyDown} - placeholder={ - isTranslating - ? t('chat.input.translating') - : t('chat.input.placeholder', { key: getSendMessageShortcutLabel(sendMessageShortcut) }) - } + placeholder={isTranslating ? t('chat.input.translating') : placeholderText} autoFocus variant="borderless" spellCheck={enableSpellCheck} diff --git a/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx b/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx index ceaa748bf5..6bb36f988a 100644 --- a/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx @@ -89,7 +89,7 @@ const MentionModelsButton: FC = ({ // 兜底:使用打开时的 position(若存在),按空白边界删除 if (typeof fallbackPosition === 'number' && currentText[fallbackPosition] === '@') { let endPos = fallbackPosition + 1 - while (endPos < currentText.length && currentText[endPos] !== ' ' && currentText[endPos] !== '\n') { + while (endPos < currentText.length && !/\s/.test(currentText[endPos])) { endPos++ } return currentText.slice(0, fallbackPosition) + currentText.slice(endPos) @@ -98,7 +98,7 @@ const MentionModelsButton: FC = ({ } let endPos = start + 1 - while (endPos < currentText.length && currentText[endPos] !== ' ' && currentText[endPos] !== '\n') { + while (endPos < currentText.length && !/\s/.test(currentText[endPos])) { endPos++ } return currentText.slice(0, start) + currentText.slice(endPos) diff --git a/src/renderer/src/pages/home/components/SelectModelButton.tsx b/src/renderer/src/pages/home/components/SelectModelButton.tsx index bd6af86f7b..c9fa63d286 100644 --- a/src/renderer/src/pages/home/components/SelectModelButton.tsx +++ b/src/renderer/src/pages/home/components/SelectModelButton.tsx @@ -3,8 +3,8 @@ import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' import { isLocalAi } from '@renderer/config/env' import { isEmbeddingModel, isRerankModel, isWebSearchModel } from '@renderer/config/models' import { useAssistant } from '@renderer/hooks/useAssistant' +import { useProvider } from '@renderer/hooks/useProvider' import { getProviderName } from '@renderer/services/ProviderService' -import { useAppSelector } from '@renderer/store' import { Assistant, Model } from '@renderer/types' import { Button, Tag } from 'antd' import { ChevronsUpDown } from 'lucide-react' @@ -20,7 +20,7 @@ const SelectModelButton: FC = ({ assistant }) => { const { model, updateAssistant } = useAssistant(assistant.id) const { t } = useTranslation() const timerRef = useRef(undefined) - const provider = useAppSelector((state) => state.llm.providers.find((p) => p.id === model?.provider)) + const provider = useProvider(model?.provider) const modelFilter = (model: Model) => !isEmbeddingModel(model) && !isRerankModel(model) diff --git a/src/renderer/src/pages/paintings/SiliconPage.tsx b/src/renderer/src/pages/paintings/SiliconPage.tsx index eebd3ccb95..691aaa0d01 100644 --- a/src/renderer/src/pages/paintings/SiliconPage.tsx +++ b/src/renderer/src/pages/paintings/SiliconPage.tsx @@ -47,6 +47,12 @@ export const TEXT_TO_IMAGES_MODELS = [ provider: 'silicon', name: 'Kolors', group: 'Kwai-Kolors' + }, + { + id: 'Qwen/Qwen-Image', + provider: 'silicon', + name: 'Qwen-Image', + group: 'qwen' } ] diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServerCard.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServerCard.tsx index 518849d4f0..3d38255bf5 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpServerCard.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpServerCard.tsx @@ -183,6 +183,7 @@ const CardContainer = styled.div<{ $isActive: boolean }>` margin-bottom: 5px; height: 125px; opacity: ${(props) => (props.$isActive ? 1 : 0.6)}; + width: calc(100vw - var(--settings-width) - 40px); &:hover { opacity: 1; diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx index 67ae6231a8..7e18af264d 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx @@ -249,7 +249,9 @@ const McpServersList: FC = () => { items={filteredMcpServers} itemKey="id" onSortEnd={onSortEnd} - layout="grid" + layout="list" + horizontal={false} + listStyle={{ display: 'flex', flexDirection: 'column' }} gap="12px" restrictions={{ scrollableAncestor: true }} useDragOverlay diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx index d0e1304a2b..58468f09bb 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx @@ -50,7 +50,6 @@ const ModelList: React.FC = ({ providerId }) => { const providerConfig = PROVIDER_URLS[provider.id] const docsWebsite = providerConfig?.websites?.docs const modelsWebsite = providerConfig?.websites?.models - const editable = provider.id !== 'cherryin' const [searchText, _setSearchText] = useState('') const [displayedModelGroups, setDisplayedModelGroups] = useState(() => { @@ -113,17 +112,15 @@ const ModelList: React.FC = ({ providerId }) => { tooltip={t('models.search.tooltip')} /> - {editable && ( - - - - - - )} + + + + ) } diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderOAuth.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderOAuth.tsx index 5315110dc2..2d8b38be81 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderOAuth.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderOAuth.tsx @@ -1,5 +1,6 @@ import AI302ProviderLogo from '@renderer/assets/images/providers/302ai.webp' import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.webp' +import AiOnlyProviderLogo from '@renderer/assets/images/providers/aiOnly.webp' import PPIOProviderLogo from '@renderer/assets/images/providers/ppio.png' import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png' import TokenFluxProviderLogo from '@renderer/assets/images/providers/tokenflux.png' @@ -25,7 +26,8 @@ const PROVIDER_LOGO_MAP = { silicon: SiliconFlowProviderLogo, aihubmix: AiHubMixProviderLogo, ppio: PPIOProviderLogo, - tokenflux: TokenFluxProviderLogo + tokenflux: TokenFluxProviderLogo, + aionly: AiOnlyProviderLogo } const ProviderOAuth: FC = ({ providerId }) => { diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index 68bfc67ba2..ea40f6d9ac 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -68,7 +68,7 @@ const ProviderSetting: FC = ({ providerId }) => { const isAzureOpenAI = provider.id === 'azure-openai' || provider.type === 'azure-openai' const isDmxapi = provider.id === 'dmxapi' - const hideApiInput = ['vertexai', 'aws-bedrock', 'cherryin'].includes(provider.id) + const hideApiInput = ['vertexai', 'aws-bedrock'].includes(provider.id) const providerConfig = PROVIDER_URLS[provider.id] const officialWebsite = providerConfig?.websites?.official diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index d954fc1f85..64e2c1ae31 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -337,7 +337,7 @@ export async function fetchGenerate({ export function hasApiKey(provider: Provider) { if (!provider) return false - if (['ollama', 'lmstudio', 'vertexai', 'cherryin'].includes(provider.id)) return true + if (['ollama', 'lmstudio', 'vertexai', 'cherryai'].includes(provider.id)) return true return !isEmpty(provider.apiKey) } diff --git a/src/renderer/src/services/AssistantService.ts b/src/renderer/src/services/AssistantService.ts index 12753a3d20..2b083f48b9 100644 --- a/src/renderer/src/services/AssistantService.ts +++ b/src/renderer/src/services/AssistantService.ts @@ -6,7 +6,10 @@ import { MAX_CONTEXT_COUNT, UNLIMITED_CONTEXT_COUNT } from '@renderer/config/constant' +import { isQwenMTModel } from '@renderer/config/models' +import { CHERRYAI_PROVIDER } from '@renderer/config/providers' import { UNKNOWN } from '@renderer/config/translate' +import { getStoreProviders } from '@renderer/hooks/useStore' import i18n from '@renderer/i18n' import store from '@renderer/store' import { addAssistant } from '@renderer/store/assistants' @@ -69,11 +72,18 @@ export function getDefaultTranslateAssistant(targetLanguage: TranslateLanguage, temperature: 0.7 } - const content = store - .getState() - .settings.translateModelPrompt.replaceAll('{{target_language}}', targetLanguage.value) - .replaceAll('{{text}}', text) + const getTranslateContent = (model: Model, text: string, targetLanguage: TranslateLanguage): string => { + if (isQwenMTModel(model)) { + return text // QwenMT models handle raw text directly + } + return store + .getState() + .settings.translateModelPrompt.replaceAll('{{target_language}}', targetLanguage.value) + .replaceAll('{{text}}', text) + } + + const content = getTranslateContent(model, text, targetLanguage) const translateAssistant = { ...assistant, model, @@ -118,26 +128,25 @@ export function getTranslateModel() { } export function getAssistantProvider(assistant: Assistant): Provider { - const providers = store.getState().llm.providers + const providers = getStoreProviders() const provider = providers.find((p) => p.id === assistant.model?.provider) return provider || getDefaultProvider() } export function getProviderByModel(model?: Model): Provider { - const providers = store.getState().llm.providers + const providers = getStoreProviders() const provider = providers.find((p) => p.id === model?.provider) if (!provider) { const defaultProvider = providers.find((p) => p.id === getDefaultModel()?.provider) - const cherryinProvider = providers.find((p) => p.id === 'cherryin') - return defaultProvider || cherryinProvider || providers[0] + return defaultProvider || CHERRYAI_PROVIDER || providers[0] } return provider } export function getProviderByModelId(modelId?: string) { - const providers = store.getState().llm.providers + const providers = getStoreProviders() const _modelId = modelId || getDefaultModel().id return providers.find((p) => p.models.find((m) => m.id === _modelId)) as Provider } diff --git a/src/renderer/src/services/ModelService.ts b/src/renderer/src/services/ModelService.ts index c667a0eadd..266271e0bf 100644 --- a/src/renderer/src/services/ModelService.ts +++ b/src/renderer/src/services/ModelService.ts @@ -1,4 +1,4 @@ -import store from '@renderer/store' +import { getStoreProviders } from '@renderer/hooks/useStore' import { Model } from '@renderer/types' import { pick } from 'lodash' @@ -9,9 +9,8 @@ export const getModelUniqId = (m?: Model) => { } export const hasModel = (m?: Model) => { - const allModels = store - .getState() - .llm.providers.filter((p) => p.enabled) + const allModels = getStoreProviders() + .filter((p) => p.enabled) .map((p) => p.models) .flat() @@ -20,7 +19,8 @@ export const hasModel = (m?: Model) => { export function getModelName(model?: Model) { const modelName = model?.name || model?.id || '' - const provider = store.getState().llm.providers.find((p) => p.id === model?.provider) + const provider = getStoreProviders().find((p) => p.id === model?.provider) + if (provider) { const providerName = getProviderName(model as Model) return `${modelName} | ${providerName}` diff --git a/src/renderer/src/services/ProviderService.ts b/src/renderer/src/services/ProviderService.ts index 9e752df73b..5068ad8d97 100644 --- a/src/renderer/src/services/ProviderService.ts +++ b/src/renderer/src/services/ProviderService.ts @@ -1,4 +1,4 @@ -import store from '@renderer/store' +import { getStoreProviders } from '@renderer/hooks/useStore' import { Model, Provider } from '@renderer/types' import { getFancyProviderName } from '@renderer/utils' @@ -23,9 +23,9 @@ export function getProviderNameById(pid: string) { export function getProviderByModel(model?: Model) { const id = model?.provider - const provider = store.getState().llm.providers.find((p) => p.id === id) + const provider = getStoreProviders().find((p) => p.id === id) - if (provider?.id === 'cherryin') { + if (provider?.id === 'cherryai') { const map = { 'glm-4.5-flash': 'zhipu', 'Qwen/Qwen3-8B': 'silicon' @@ -42,7 +42,7 @@ export function getProviderByModel(model?: Model) { } export function isProviderSupportAuth(provider: Provider) { - const supportProviders = ['302ai', 'silicon', 'aihubmix', 'ppio', 'tokenflux'] + const supportProviders = ['302ai', 'silicon', 'aihubmix', 'ppio', 'tokenflux', 'aionly'] return supportProviders.includes(provider.id) } @@ -52,5 +52,5 @@ export function isProviderSupportCharge(provider: Provider) { } export function getProviderById(id: string) { - return store.getState().llm.providers.find((p) => p.id === id) + return getStoreProviders().find((p) => p.id === id) } diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 3b3cfa3f0c..ba532ecc65 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -67,7 +67,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 156, + version: 157, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 41fa441945..8063f7ec92 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2491,6 +2491,53 @@ const migrateConfig = { logger.error('migrate 156 error', error as Error) return state } + }, + '157': (state: RootState) => { + try { + addProvider(state, 'aionly') + + const cherryinProvider = state.llm.providers.find((provider) => provider.id === 'cherryin') + + if (cherryinProvider) { + updateProvider(state, 'cherryin', { apiHost: 'https://open.cherryin.ai', models: [] }) + } + + if (state.llm.defaultModel?.provider === 'cherryin') { + state.llm.defaultModel.provider = 'cherryai' + } + + if (state.llm.quickModel?.provider === 'cherryin') { + state.llm.quickModel.provider = 'cherryai' + } + + if (state.llm.translateModel?.provider === 'cherryin') { + state.llm.translateModel.provider = 'cherryai' + } + + state.assistants.assistants.forEach((assistant) => { + if (assistant.model?.provider === 'cherryin') { + assistant.model.provider = 'cherryai' + } + if (assistant.defaultModel?.provider === 'cherryin') { + assistant.defaultModel.provider = 'cherryai' + } + }) + + state.agents.agents.forEach((agent) => { + // @ts-ignore model is not defined in Agent + if (agent.model?.provider === 'cherryin') { + // @ts-ignore model is not defined in Agent + agent.model.provider = 'cherryai' + } + if (agent.defaultModel?.provider === 'cherryin') { + agent.defaultModel.provider = 'cherryai' + } + }) + return state + } catch (error) { + logger.error('migrate 157 error', error as Error) + return state + } } } diff --git a/src/renderer/src/types/provider.ts b/src/renderer/src/types/provider.ts index 02e15a4b66..b4796e0143 100644 --- a/src/renderer/src/types/provider.ts +++ b/src/renderer/src/types/provider.ts @@ -156,7 +156,8 @@ export const SystemProviderIds = { gpustack: 'gpustack', voyageai: 'voyageai', 'aws-bedrock': 'aws-bedrock', - poe: 'poe' + poe: 'poe', + aionly: 'aionly' } as const export type SystemProviderId = keyof typeof SystemProviderIds diff --git a/src/renderer/src/utils/model.ts b/src/renderer/src/utils/model.ts index 9795b72d26..e918c84572 100644 --- a/src/renderer/src/utils/model.ts +++ b/src/renderer/src/utils/model.ts @@ -64,7 +64,7 @@ export const getModelTags = (models: Model[]): Record => { } export function isFreeModel(model: Model) { - if (model.provider === 'cherryin') { + if (model.provider === 'cherryai') { return true } diff --git a/src/renderer/src/utils/oauth.ts b/src/renderer/src/utils/oauth.ts index 9fbb632a07..5d57547f69 100644 --- a/src/renderer/src/utils/oauth.ts +++ b/src/renderer/src/utils/oauth.ts @@ -172,6 +172,27 @@ export const oauthWith302AI = async (setKey) => { window.addEventListener('message', messageHandler) } +export const oauthWithAiOnly = async (setKey) => { + const authUrl = `https://www.aiionly.com/login?inviteCode=1755481173663DrZBBOC0&cherryCode=01` + + const popup = window.open( + authUrl, + 'login', + 'width=720,height=720,toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,alwaysOnTop=yes,alwaysRaised=yes' + ) + + const messageHandler = (event) => { + if (event.data.length > 0 && event.data[0]['secretKey'] !== undefined) { + setKey(event.data[0]['secretKey']) + popup?.close() + window.removeEventListener('message', messageHandler) + } + } + + window.removeEventListener('message', messageHandler) + window.addEventListener('message', messageHandler) +} + export const providerCharge = async (provider: string) => { const chargeUrlMap = { silicon: { @@ -198,6 +219,11 @@ export const providerCharge = async (provider: string) => { url: 'https://dash.302.ai/charge', width: 900, height: 700 + }, + aionly: { + url: `https://www.aiionly.com/recharge`, + width: 900, + height: 700 } } @@ -236,6 +262,11 @@ export const providerBills = async (provider: string) => { url: 'https://dash.302.ai/charge', width: 900, height: 700 + }, + aionly: { + url: `https://www.aiionly.com/billManagement`, + width: 900, + height: 700 } } diff --git a/tsconfig.web.json b/tsconfig.web.json index 07c1b41066..1204192253 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -8,7 +8,7 @@ "tests/__mocks__/**/*", "packages/mcp-trace/**/*", "packages/aiCore/src/**/*", - "src/main/integration/cherryin/index.js", + "src/main/integration/cherryai/index.js", "packages/extension-table-plus/**/*" ], "compilerOptions": {