Compare commits
48 Commits
refactor/a
...
feat/messa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7e79353fc | ||
|
|
93e972a5da | ||
|
|
12d08e4748 | ||
|
|
52a980f751 | ||
|
|
3b7ab2aec8 | ||
|
|
d41e239b89 | ||
|
|
b85040f579 | ||
|
|
8bcd229849 | ||
|
|
d12515ccb9 | ||
|
|
499cb52e28 | ||
|
|
05a318225c | ||
|
|
caad0bc005 | ||
|
|
067ecb5e8e | ||
|
|
0f8cbeed11 | ||
|
|
2ed99c0cb8 | ||
|
|
0a149e3d9e | ||
|
|
a3a26c69c5 | ||
|
|
2bafc53b25 | ||
|
|
09e9b95e08 | ||
|
|
bf2ffb7465 | ||
|
|
287c96ea2e | ||
|
|
adacb8c638 | ||
|
|
7a3d08672a | ||
|
|
ec4d106a59 | ||
|
|
fe0c0fac1e | ||
|
|
4a4a1686d3 | ||
|
|
37218eef4f | ||
|
|
3b34efd33a | ||
|
|
cc650b58d3 | ||
|
|
183b46be9e | ||
|
|
a847b74c32 | ||
|
|
25c5d671dc | ||
|
|
87d9c7b410 | ||
|
|
67a6a6a445 | ||
|
|
4f8507036a | ||
|
|
acf2f4758f | ||
|
|
abf368e558 | ||
|
|
0697c79daa | ||
|
|
3d6a82fb00 | ||
|
|
97e9e42173 | ||
|
|
5d8e706c0b | ||
|
|
a8cd2e2eac | ||
|
|
1e615d69e1 | ||
|
|
63be1d8cf2 | ||
|
|
f039aa253d | ||
|
|
5a7521e335 | ||
|
|
5dac1f5867 | ||
|
|
1d0fc26025 |
22
.github/workflows/delete-branch.yml
vendored
Normal file
22
.github/workflows/delete-branch.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Delete merged branch
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
delete-branch:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
if: github.event.pull_request.merged == true && github.event.pull_request.head.repo.full_name == github.repository
|
||||
steps:
|
||||
- name: Delete merged branch
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
github.rest.git.deleteRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `heads/${context.payload.pull_request.head.ref}`,
|
||||
})
|
||||
6
.github/workflows/nightly-build.yml
vendored
6
.github/workflows/nightly-build.yml
vendored
@@ -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 }}
|
||||
|
||||
2
.github/workflows/pr-ci.yml
vendored
2
.github/workflows/pr-ci.yml
vendored
@@ -10,12 +10,14 @@ on:
|
||||
- main
|
||||
- develop
|
||||
- v2
|
||||
types: [ready_for_review, synchronize, opened]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PRCI: true
|
||||
if: github.event.pull_request.draft == false
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -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 }}
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -54,6 +54,8 @@ local
|
||||
.qwen/*
|
||||
.trae/*
|
||||
.claude-code-router/*
|
||||
.codebuddy/*
|
||||
.zed/*
|
||||
CLAUDE.local.md
|
||||
|
||||
# vitest
|
||||
|
||||
@@ -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",
|
||||
|
||||
36
.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch
vendored
Normal file
36
.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index 110f37ec18c98b1d55ae2b73cc716194e6f9094d..91d0f336b318833c6cee9599fe91370c0ff75323 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -447,7 +447,10 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||
}
|
||||
|
||||
// src/get-model-path.ts
|
||||
-function getModelPath(modelId) {
|
||||
+function getModelPath(modelId, baseURL) {
|
||||
+ if (baseURL?.includes('cherryin')) {
|
||||
+ return `models/${modelId}`;
|
||||
+ }
|
||||
return modelId.includes("/") ? modelId : `models/${modelId}`;
|
||||
}
|
||||
|
||||
@@ -856,7 +859,8 @@ var GoogleGenerativeAILanguageModel = class {
|
||||
rawValue: rawResponse
|
||||
} = await postJsonToApi2({
|
||||
url: `${this.config.baseURL}/${getModelPath(
|
||||
- this.modelId
|
||||
+ this.modelId,
|
||||
+ this.config.baseURL
|
||||
)}:generateContent`,
|
||||
headers: mergedHeaders,
|
||||
body: args,
|
||||
@@ -962,7 +966,8 @@ var GoogleGenerativeAILanguageModel = class {
|
||||
);
|
||||
const { responseHeaders, value: response } = await postJsonToApi2({
|
||||
url: `${this.config.baseURL}/${getModelPath(
|
||||
- this.modelId
|
||||
+ this.modelId,
|
||||
+ this.config.baseURL
|
||||
)}:streamGenerateContent?alt=sse`,
|
||||
headers,
|
||||
body: args,
|
||||
@@ -125,16 +125,59 @@ afterSign: scripts/notarize.js
|
||||
artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
🐛 问题修复:
|
||||
- 修复 Anthropic API URL 处理,移除尾部斜杠并添加端点路径处理
|
||||
- 修复 MessageEditor 缺少 QuickPanelProvider 包装的问题
|
||||
- 修复 MiniWindow 高度问题
|
||||
<!--LANG:en-->
|
||||
🚀 New Features:
|
||||
- Refactored AI core engine for more efficient and stable content generation
|
||||
- Added support for multiple AI model providers: CherryIN, AiOnly
|
||||
- Added API server functionality for external application integration
|
||||
- Added PaddleOCR document recognition for enhanced document processing
|
||||
- Added Anthropic OAuth authentication support
|
||||
- Added data storage space limit notifications
|
||||
- Added font settings for global and code fonts customization
|
||||
- Added auto-copy feature after translation completion
|
||||
- Added keyboard shortcuts: rename topic, edit last message, etc.
|
||||
- Added text attachment preview for viewing file contents in messages
|
||||
- Added custom window control buttons (minimize, maximize, close)
|
||||
- Support for Qwen long-text (qwen-long) and document analysis (qwen-doc) models with native file uploads
|
||||
- Support for Qwen image recognition models (Qwen-Image)
|
||||
- Added iFlow CLI support
|
||||
- Converted knowledge base and web search to tool-calling approach for better flexibility
|
||||
|
||||
🚀 性能优化:
|
||||
- 优化输入栏提及模型状态缓存,在渲染间保持状态
|
||||
- 重构网络搜索参数支持模型内置搜索,新增 OpenAI Chat 和 OpenRouter 支持
|
||||
🎨 UI Improvements & Bug Fixes:
|
||||
- Integrated HeroUI and Tailwind CSS framework
|
||||
- Optimized message notification styles with unified toast component
|
||||
- Moved free models to bottom with fixed position for easier access
|
||||
- Refactored quick panel and input bar tools for smoother operation
|
||||
- Optimized responsive design for navbar and sidebar
|
||||
- Improved scrollbar component with horizontal scrolling support
|
||||
- Fixed multiple translation issues: paste handling, file processing, state management
|
||||
- Various UI optimizations and bug fixes
|
||||
<!--LANG:zh-CN-->
|
||||
🚀 新功能:
|
||||
- 重构 AI 核心引擎,提供更高效稳定的内容生成
|
||||
- 新增多个 AI 模型提供商支持:CherryIN、AiOnly
|
||||
- 新增 API 服务器功能,支持外部应用集成
|
||||
- 新增 PaddleOCR 文档识别,增强文档处理能力
|
||||
- 新增 Anthropic OAuth 认证支持
|
||||
- 新增数据存储空间限制提醒
|
||||
- 新增字体设置,支持全局字体和代码字体自定义
|
||||
- 新增翻译完成后自动复制功能
|
||||
- 新增键盘快捷键:重命名主题、编辑最后一条消息等
|
||||
- 新增文本附件预览,可查看消息中的文件内容
|
||||
- 新增自定义窗口控制按钮(最小化、最大化、关闭)
|
||||
- 支持通义千问长文本(qwen-long)和文档分析(qwen-doc)模型,原生文件上传
|
||||
- 支持通义千问图像识别模型(Qwen-Image)
|
||||
- 新增 iFlow CLI 支持
|
||||
- 知识库和网页搜索转换为工具调用方式,提升灵活性
|
||||
|
||||
🎨 界面改进与问题修复:
|
||||
- 集成 HeroUI 和 Tailwind CSS 框架
|
||||
- 优化消息通知样式,统一 toast 组件
|
||||
- 免费模型移至底部固定位置,便于访问
|
||||
- 重构快捷面板和输入栏工具,操作更流畅
|
||||
- 优化导航栏和侧边栏响应式设计
|
||||
- 改进滚动条组件,支持水平滚动
|
||||
- 修复多个翻译问题:粘贴处理、文件处理、状态管理
|
||||
- 各种界面优化和问题修复
|
||||
<!--LANG:END-->
|
||||
|
||||
🔧 重构改进:
|
||||
- 更新 HeroUIProvider 导入路径,改善上下文管理
|
||||
- 更新依赖项和 VSCode 开发环境配置
|
||||
- 升级 @cherrystudio/ai-core 到 v1.0.0-alpha.17
|
||||
|
||||
@@ -48,6 +48,27 @@ export default defineConfig([
|
||||
'@eslint-react/no-children-to-array': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'build/**',
|
||||
'dist/**',
|
||||
'out/**',
|
||||
'local/**',
|
||||
'.yarn/**',
|
||||
'.gitignore',
|
||||
'scripts/cloudflare-worker.js',
|
||||
'src/main/integration/nutstore/sso/lib/**',
|
||||
'src/main/integration/cherryai/index.js',
|
||||
'src/main/integration/nutstore/sso/lib/**',
|
||||
'src/renderer/src/ui/**',
|
||||
'packages/**/dist'
|
||||
]
|
||||
},
|
||||
// turn off oxlint supported rules.
|
||||
...oxlint.configs['flat/eslint'],
|
||||
...oxlint.configs['flat/typescript'],
|
||||
...oxlint.configs['flat/unicorn'],
|
||||
{
|
||||
// LoggerService Custom Rules - only apply to src directory
|
||||
files: ['src/**/*.{ts,tsx,js,jsx}'],
|
||||
@@ -110,25 +131,4 @@ export default defineConfig([
|
||||
'i18n/no-template-in-t': 'warn'
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'build/**',
|
||||
'dist/**',
|
||||
'out/**',
|
||||
'local/**',
|
||||
'.yarn/**',
|
||||
'.gitignore',
|
||||
'scripts/cloudflare-worker.js',
|
||||
'src/main/integration/nutstore/sso/lib/**',
|
||||
'src/main/integration/cherryin/index.js',
|
||||
'src/main/integration/nutstore/sso/lib/**',
|
||||
'src/renderer/src/ui/**',
|
||||
'packages/**/dist'
|
||||
]
|
||||
},
|
||||
// turn off oxlint supported rules.
|
||||
...oxlint.configs['flat/eslint'],
|
||||
...oxlint.configs['flat/typescript'],
|
||||
...oxlint.configs['flat/unicorn']
|
||||
])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.6.0-rc.2",
|
||||
"version": "1.6.1",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -368,7 +368,8 @@
|
||||
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
|
||||
"undici": "6.21.2",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch"
|
||||
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||
"@ai-sdk/google@npm:2.0.14": "patch:@ai-sdk/google@npm%3A2.0.14#~/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"lint-staged": {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@ai-sdk/anthropic": "^2.0.17",
|
||||
"@ai-sdk/azure": "^2.0.30",
|
||||
"@ai-sdk/deepseek": "^1.0.17",
|
||||
"@ai-sdk/google": "^2.0.14",
|
||||
"@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.14#~/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch",
|
||||
"@ai-sdk/openai": "^2.0.30",
|
||||
"@ai-sdk/openai-compatible": "^1.0.17",
|
||||
"@ai-sdk/provider": "^2.0.0",
|
||||
|
||||
@@ -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',
|
||||
@@ -321,10 +322,14 @@ export enum IpcChannel {
|
||||
|
||||
// CodeTools
|
||||
CodeTools_Run = 'code-tools:run',
|
||||
CodeTools_GetAvailableTerminals = 'code-tools:get-available-terminals',
|
||||
CodeTools_SetCustomTerminalPath = 'code-tools:set-custom-terminal-path',
|
||||
CodeTools_GetCustomTerminalPath = 'code-tools:get-custom-terminal-path',
|
||||
CodeTools_RemoveCustomTerminalPath = 'code-tools:remove-custom-terminal-path',
|
||||
|
||||
// OCR
|
||||
OCR_ocr = 'ocr:ocr',
|
||||
|
||||
// Cherryin
|
||||
Cherryin_GetSignature = 'cherryin:get-signature'
|
||||
// CherryAI
|
||||
Cherryai_GetSignature = 'cherryai:get-signature'
|
||||
}
|
||||
|
||||
@@ -219,3 +219,253 @@ export enum codeTools {
|
||||
openaiCodex = 'openai-codex',
|
||||
iFlowCli = 'iflow-cli'
|
||||
}
|
||||
|
||||
export enum terminalApps {
|
||||
systemDefault = 'Terminal',
|
||||
iterm2 = 'iTerm2',
|
||||
kitty = 'kitty',
|
||||
alacritty = 'Alacritty',
|
||||
wezterm = 'WezTerm',
|
||||
ghostty = 'Ghostty',
|
||||
tabby = 'Tabby',
|
||||
// Windows terminals
|
||||
windowsTerminal = 'WindowsTerminal',
|
||||
powershell = 'PowerShell',
|
||||
cmd = 'CMD',
|
||||
wsl = 'WSL'
|
||||
}
|
||||
|
||||
export interface TerminalConfig {
|
||||
id: string
|
||||
name: string
|
||||
bundleId?: string
|
||||
customPath?: string // For user-configured terminal paths on Windows
|
||||
}
|
||||
|
||||
export interface TerminalConfigWithCommand extends TerminalConfig {
|
||||
command: (directory: string, fullCommand: string) => { command: string; args: string[] }
|
||||
}
|
||||
|
||||
export const MACOS_TERMINALS: TerminalConfig[] = [
|
||||
{
|
||||
id: terminalApps.systemDefault,
|
||||
name: 'Terminal',
|
||||
bundleId: 'com.apple.Terminal'
|
||||
},
|
||||
{
|
||||
id: terminalApps.iterm2,
|
||||
name: 'iTerm2',
|
||||
bundleId: 'com.googlecode.iterm2'
|
||||
},
|
||||
{
|
||||
id: terminalApps.kitty,
|
||||
name: 'kitty',
|
||||
bundleId: 'net.kovidgoyal.kitty'
|
||||
},
|
||||
{
|
||||
id: terminalApps.alacritty,
|
||||
name: 'Alacritty',
|
||||
bundleId: 'org.alacritty'
|
||||
},
|
||||
{
|
||||
id: terminalApps.wezterm,
|
||||
name: 'WezTerm',
|
||||
bundleId: 'com.github.wez.wezterm'
|
||||
},
|
||||
{
|
||||
id: terminalApps.ghostty,
|
||||
name: 'Ghostty',
|
||||
bundleId: 'com.mitchellh.ghostty'
|
||||
},
|
||||
{
|
||||
id: terminalApps.tabby,
|
||||
name: 'Tabby',
|
||||
bundleId: 'org.tabby'
|
||||
}
|
||||
]
|
||||
|
||||
export const WINDOWS_TERMINALS: TerminalConfig[] = [
|
||||
{
|
||||
id: terminalApps.cmd,
|
||||
name: 'Command Prompt'
|
||||
},
|
||||
{
|
||||
id: terminalApps.powershell,
|
||||
name: 'PowerShell'
|
||||
},
|
||||
{
|
||||
id: terminalApps.windowsTerminal,
|
||||
name: 'Windows Terminal'
|
||||
},
|
||||
{
|
||||
id: terminalApps.wsl,
|
||||
name: 'WSL (Ubuntu/Debian)'
|
||||
},
|
||||
{
|
||||
id: terminalApps.alacritty,
|
||||
name: 'Alacritty'
|
||||
},
|
||||
{
|
||||
id: terminalApps.wezterm,
|
||||
name: 'WezTerm'
|
||||
}
|
||||
]
|
||||
|
||||
export const WINDOWS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
||||
{
|
||||
id: terminalApps.cmd,
|
||||
name: 'Command Prompt',
|
||||
command: (_: string, fullCommand: string) => ({
|
||||
command: 'cmd',
|
||||
args: ['/c', 'start', 'cmd', '/k', fullCommand]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: terminalApps.powershell,
|
||||
name: 'PowerShell',
|
||||
command: (_: string, fullCommand: string) => ({
|
||||
command: 'cmd',
|
||||
args: ['/c', 'start', 'powershell', '-NoExit', '-Command', `& '${fullCommand}'`]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: terminalApps.windowsTerminal,
|
||||
name: 'Windows Terminal',
|
||||
command: (_: string, fullCommand: string) => ({
|
||||
command: 'wt',
|
||||
args: ['cmd', '/k', fullCommand]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: terminalApps.wsl,
|
||||
name: 'WSL (Ubuntu/Debian)',
|
||||
command: (_: string, fullCommand: string) => {
|
||||
// Start WSL in a new window and execute the batch file from within WSL using cmd.exe
|
||||
// The batch file will run in Windows context but output will be in WSL terminal
|
||||
return {
|
||||
command: 'cmd',
|
||||
args: ['/c', 'start', 'wsl', '-e', 'bash', '-c', `cmd.exe /c '${fullCommand}' ; exec bash`]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: terminalApps.alacritty,
|
||||
name: 'Alacritty',
|
||||
customPath: '', // Will be set by user in settings
|
||||
command: (_: string, fullCommand: string) => ({
|
||||
command: 'alacritty', // Will be replaced with customPath if set
|
||||
args: ['-e', 'cmd', '/k', fullCommand]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: terminalApps.wezterm,
|
||||
name: 'WezTerm',
|
||||
customPath: '', // Will be set by user in settings
|
||||
command: (_: string, fullCommand: string) => ({
|
||||
command: 'wezterm', // Will be replaced with customPath if set
|
||||
args: ['start', 'cmd', '/k', fullCommand]
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
// Helper function to escape strings for AppleScript
|
||||
const escapeForAppleScript = (str: string): string => {
|
||||
// In AppleScript strings, backslashes and double quotes need to be escaped
|
||||
// When passed through osascript -e with single quotes, we need:
|
||||
// 1. Backslash: \ -> \\
|
||||
// 2. Double quote: " -> \"
|
||||
return str
|
||||
.replace(/\\/g, '\\\\') // Escape backslashes first
|
||||
.replace(/"/g, '\\"') // Then escape double quotes
|
||||
}
|
||||
|
||||
export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
||||
{
|
||||
id: terminalApps.systemDefault,
|
||||
name: 'Terminal',
|
||||
bundleId: 'com.apple.Terminal',
|
||||
command: (_directory: string, fullCommand: string) => ({
|
||||
command: 'sh',
|
||||
args: [
|
||||
'-c',
|
||||
`open -na Terminal && sleep 0.5 && osascript -e 'tell application "Terminal" to activate' -e 'tell application "Terminal" to do script "${escapeForAppleScript(fullCommand)}" in front window'`
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: terminalApps.iterm2,
|
||||
name: 'iTerm2',
|
||||
bundleId: 'com.googlecode.iterm2',
|
||||
command: (_directory: string, fullCommand: string) => ({
|
||||
command: 'sh',
|
||||
args: [
|
||||
'-c',
|
||||
`open -na iTerm && sleep 0.8 && osascript -e 'on waitUntilRunning()\n repeat 50 times\n tell application "System Events"\n if (exists process "iTerm2") then exit repeat\n end tell\n delay 0.1\n end repeat\nend waitUntilRunning\n\nwaitUntilRunning()\n\ntell application "iTerm2"\n if (count of windows) = 0 then\n create window with default profile\n delay 0.3\n else\n tell current window\n create tab with default profile\n end tell\n delay 0.3\n end if\n tell current session of current window to write text "${escapeForAppleScript(fullCommand)}"\n activate\nend tell'`
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: terminalApps.kitty,
|
||||
name: 'kitty',
|
||||
bundleId: 'net.kovidgoyal.kitty',
|
||||
command: (_directory: string, fullCommand: string) => ({
|
||||
command: 'sh',
|
||||
args: [
|
||||
'-c',
|
||||
`cd "${_directory}" && open -na kitty --args --directory="${_directory}" sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "kitty" to activate'`
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: terminalApps.alacritty,
|
||||
name: 'Alacritty',
|
||||
bundleId: 'org.alacritty',
|
||||
command: (_directory: string, fullCommand: string) => ({
|
||||
command: 'sh',
|
||||
args: [
|
||||
'-c',
|
||||
`open -na Alacritty --args --working-directory "${_directory}" -e sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "Alacritty" to activate'`
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: terminalApps.wezterm,
|
||||
name: 'WezTerm',
|
||||
bundleId: 'com.github.wez.wezterm',
|
||||
command: (_directory: string, fullCommand: string) => ({
|
||||
command: 'sh',
|
||||
args: [
|
||||
'-c',
|
||||
`open -na WezTerm --args start --new-tab --cwd "${_directory}" -- sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "WezTerm" to activate'`
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: terminalApps.ghostty,
|
||||
name: 'Ghostty',
|
||||
bundleId: 'com.mitchellh.ghostty',
|
||||
command: (_directory: string, fullCommand: string) => ({
|
||||
command: 'sh',
|
||||
args: [
|
||||
'-c',
|
||||
`cd "${_directory}" && open -na Ghostty --args --working-directory="${_directory}" -e sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "Ghostty" to activate'`
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: terminalApps.tabby,
|
||||
name: 'Tabby',
|
||||
bundleId: 'org.tabby',
|
||||
command: (_directory: string, fullCommand: string) => ({
|
||||
command: 'sh',
|
||||
args: [
|
||||
'-c',
|
||||
`if pgrep -x "Tabby" > /dev/null; then
|
||||
open -na Tabby --args open && sleep 0.3
|
||||
else
|
||||
open -na Tabby --args open && sleep 2
|
||||
fi && osascript -e 'tell application "Tabby" to activate' -e 'set the clipboard to "${escapeForAppleScript(fullCommand)}"' -e 'tell application "System Events" to tell process "Tabby" to keystroke "v" using {command down}' -e 'tell application "System Events" to key code 36'`
|
||||
]
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
252
resources/cherry-studio/privacy-en.html
Normal file
252
resources/cherry-studio/privacy-en.html
Normal file
@@ -0,0 +1,252 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Privacy Policy</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: transparent;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
body.dark {
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
body.dark h1 {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
color: #2c2c2c;
|
||||
}
|
||||
|
||||
body.dark h2 {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 12px 0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
body.dark p {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 12px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 6px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
body.dark li {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0066cc;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
body.dark a {
|
||||
color: #4da6ff;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
body.dark .footer {
|
||||
border-top-color: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
max-height: calc(100vh - 40px);
|
||||
overflow-y: auto;
|
||||
padding-right: 10px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Scrollbar styles - Light mode */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Scrollbar styles - Dark mode */
|
||||
body.dark ::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
body.dark ::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
body.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// Detect theme
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const theme = urlParams.get('theme');
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.body.classList.add('dark');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="content-wrapper">
|
||||
<h1>Privacy Policy</h1>
|
||||
|
||||
<p>
|
||||
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:
|
||||
</p>
|
||||
|
||||
<h2>1. Information We Collect</h2>
|
||||
<p>To optimize user experience and improve software quality, we may only collect the following anonymous,
|
||||
non-personal information:</p>
|
||||
<ul>
|
||||
<li>Software version information</li>
|
||||
<li>Activity and usage frequency of software features</li>
|
||||
<li>Anonymous crash and error log information</li>
|
||||
</ul>
|
||||
<p>The above information is completely anonymous, does not involve any personal identity data, and cannot be
|
||||
linked to your personal information.</p>
|
||||
|
||||
<h2>2. Information We Do Not Collect</h2>
|
||||
<p>To maximize the protection of your privacy and security, we explicitly commit that we:</p>
|
||||
<ul>
|
||||
<li>Will not collect, save, transmit, or process model service API Key information you enter into the
|
||||
Software</li>
|
||||
<li>Will not collect, save, transmit, or process any conversation data generated during your use of the
|
||||
Software, including but not limited to chat content, instruction information, knowledge base
|
||||
information, vector data, and other custom content</li>
|
||||
<li>Will not collect, save, transmit, or process any sensitive information that can identify personal
|
||||
identity</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Data Interaction Description</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>4. Local Data Security Protection</h2>
|
||||
<p>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:</p>
|
||||
<ul>
|
||||
<li>Conversation records, configuration information, and other data are only saved on your local device</li>
|
||||
<li>Data import/export functions are provided to facilitate your independent management and backup of data
|
||||
</li>
|
||||
<li>Your local data will not be uploaded to any server or cloud storage</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Third-Party Services</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>6. User Rights</h2>
|
||||
<p>You have complete control over your data:</p>
|
||||
<ul>
|
||||
<li>You can view, modify, and delete all locally stored data at any time</li>
|
||||
<li>You can choose whether to enable specific features or services</li>
|
||||
<li>You can stop using the Software and delete all related data at any time</li>
|
||||
</ul>
|
||||
|
||||
<h2>7. Children's Privacy Protection</h2>
|
||||
<p>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.</p>
|
||||
|
||||
<h2>8. Privacy Policy Updates</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h2>9. Contact Us</h2>
|
||||
<p>If you have any questions, suggestions, or complaints about this Privacy Policy, please contact us through
|
||||
the following methods:</p>
|
||||
<ul>
|
||||
<li>
|
||||
GitHub: <a href="https://github.com/CherryHQ/cherry-studio" target="_blank"
|
||||
rel="noopener noreferrer">https://github.com/CherryHQ/cherry-studio</a>
|
||||
</li>
|
||||
<li>Email: support@cherry-ai.com</li>
|
||||
</ul>
|
||||
|
||||
<div class="footer">
|
||||
Last Updated: December 2024
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
230
resources/cherry-studio/privacy-zh.html
Normal file
230
resources/cherry-studio/privacy-zh.html
Normal file
@@ -0,0 +1,230 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>隐私协议</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: transparent;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
body.dark {
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
body.dark h1 {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
color: #2c2c2c;
|
||||
}
|
||||
|
||||
body.dark h2 {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 12px 0;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
body.dark p {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 12px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 6px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
body.dark li {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0066cc;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
body.dark a {
|
||||
color: #4da6ff;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
body.dark .footer {
|
||||
border-top-color: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
overflow-y: auto;
|
||||
padding-right: 10px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 滚动条样式 - 亮色模式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 滚动条样式 - 暗色模式 */
|
||||
body.dark ::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
body.dark ::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
body.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// 检测主题
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const theme = urlParams.get('theme');
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.body.classList.add('dark');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="content-wrapper">
|
||||
<h1>隐私协议</h1>
|
||||
|
||||
<p>
|
||||
欢迎使用 Cherry Studio(以下简称"本软件"或"我们")。我们高度重视您的隐私保护,本隐私协议将说明我们如何处理与保护您的个人信息和数据。请在使用本软件前仔细阅读并理解本协议:
|
||||
</p>
|
||||
|
||||
<h2>一、我们收集的信息范围</h2>
|
||||
<p>为了优化用户体验和提升软件质量,我们仅可能会匿名收集以下非个人化信息:</p>
|
||||
<ul>
|
||||
<li>软件版本信息;</li>
|
||||
<li>软件功能的活跃度、使用频次;</li>
|
||||
<li>匿名的崩溃、错误日志信息;</li>
|
||||
</ul>
|
||||
<p>上述信息完全匿名,不会涉及任何个人身份数据,也无法关联到您的个人信息。</p>
|
||||
|
||||
<h2>二、我们不会收集的任何信息</h2>
|
||||
<p>为了最大限度保护您的隐私安全,我们明确承诺:</p>
|
||||
<ul>
|
||||
<li>不会收集、保存、传输或处理您输入到本软件中的模型服务 API Key 信息;</li>
|
||||
<li>不会收集、保存、传输或处理您在使用本软件过程中产生的任何对话数据,包括但不限于聊天内容、指令信息、知识库信息、向量数据及其他自定义内容;</li>
|
||||
<li>不会收集、保存、传输或处理任何可识别个人身份的敏感信息。</li>
|
||||
</ul>
|
||||
|
||||
<h2>三、数据交互说明</h2>
|
||||
<p>
|
||||
本软件采用您自行申请并配置的第三方模型服务提供商的 API Key,以完成相关模型的调用与对话功能。您使用的模型服务(例如大模型、API 接口等)由您选择的第三方提供商直接提供,我们不会介入、监控或干扰数据传输过程。
|
||||
</p>
|
||||
<p>
|
||||
您与第三方模型服务之间的数据交互受第三方服务提供商的隐私政策和用户协议约束,我们建议您在使用前充分了解相关服务商的隐私条款。
|
||||
</p>
|
||||
|
||||
<h2>四、本地数据的安全保护</h2>
|
||||
<p>本软件为本地化应用程序,所有数据默认存储在您的本地设备上。我们采取了以下措施保障数据安全:</p>
|
||||
<ul>
|
||||
<li>对话记录、配置信息等数据仅保存在您的本地设备中;</li>
|
||||
<li>提供数据导入/导出功能,方便您自主管理和备份数据;</li>
|
||||
<li>不会将您的本地数据上传至任何服务器或云端存储。</li>
|
||||
</ul>
|
||||
|
||||
<h2>五、第三方服务</h2>
|
||||
<p>
|
||||
在使用本软件过程中,您可能会接入第三方服务(如 AI 模型 API、翻译服务等)。这些第三方服务的使用受其各自的服务条款和隐私政策约束。我们强烈建议您在使用前仔细阅读并理解相关条款。
|
||||
</p>
|
||||
|
||||
<h2>六、用户权利</h2>
|
||||
<p>您对自己的数据拥有完全的控制权:</p>
|
||||
<ul>
|
||||
<li>您可以随时查看、修改、删除本地存储的所有数据;</li>
|
||||
<li>您可以选择是否启用特定功能或服务;</li>
|
||||
<li>您可以随时停止使用本软件并删除所有相关数据。</li>
|
||||
</ul>
|
||||
|
||||
<h2>七、儿童隐私保护</h2>
|
||||
<p>本软件不面向 18 岁以下的未成年人提供服务。如果您是未成年人,请在监护人的指导下使用本软件。</p>
|
||||
|
||||
<h2>八、隐私政策的更新</h2>
|
||||
<p>
|
||||
我们可能会根据法律法规要求或产品功能的变化更新本隐私协议。更新后的协议将在软件中发布,并在生效前通知您。如果您不同意更新后的条款,您可以选择停止使用本软件。
|
||||
</p>
|
||||
|
||||
<h2>九、联系我们</h2>
|
||||
<p>如果您对本隐私协议有任何疑问、建议或投诉,请通过以下方式联系我们:</p>
|
||||
<ul>
|
||||
<li>
|
||||
GitHub: <a href="https://github.com/CherryHQ/cherry-studio" target="_blank"
|
||||
rel="noopener noreferrer">https://github.com/CherryHQ/cherry-studio</a>
|
||||
</li>
|
||||
<li>Email: support@cherry-ai.com</li>
|
||||
</ul>
|
||||
|
||||
<div class="footer">
|
||||
最后更新日期:2024年12月
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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
|
||||
|
||||
1
src/main/integration/cherryai/index.js
Normal file
1
src/main/integration/cherryai/index.js
Normal file
@@ -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};
|
||||
@@ -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};
|
||||
@@ -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'
|
||||
@@ -126,6 +126,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
|
||||
@@ -824,12 +825,22 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
// CodeTools
|
||||
ipcMain.handle(IpcChannel.CodeTools_Run, codeToolsService.run)
|
||||
ipcMain.handle(IpcChannel.CodeTools_GetAvailableTerminals, () => codeToolsService.getAvailableTerminalsForPlatform())
|
||||
ipcMain.handle(IpcChannel.CodeTools_SetCustomTerminalPath, (_, terminalId: string, path: string) =>
|
||||
codeToolsService.setCustomTerminalPath(terminalId, path)
|
||||
)
|
||||
ipcMain.handle(IpcChannel.CodeTools_GetCustomTerminalPath, (_, terminalId: string) =>
|
||||
codeToolsService.getCustomTerminalPath(terminalId)
|
||||
)
|
||||
ipcMain.handle(IpcChannel.CodeTools_RemoveCustomTerminalPath, (_, terminalId: string) =>
|
||||
codeToolsService.removeCustomTerminalPath(terminalId)
|
||||
)
|
||||
|
||||
// OCR
|
||||
ipcMain.handle(IpcChannel.OCR_ocr, (_, file: SupportedOcrFile, provider: OcrProvider) =>
|
||||
ocrService.ocr(file, provider)
|
||||
)
|
||||
|
||||
// CherryIN
|
||||
ipcMain.handle(IpcChannel.Cherryin_GetSignature, (_, params) => generateSignature(params))
|
||||
// CherryAI
|
||||
ipcMain.handle(IpcChannel.Cherryai_GetSignature, (_, params) => generateSignature(params))
|
||||
}
|
||||
|
||||
@@ -17,6 +17,13 @@ import { windowService } from './WindowService'
|
||||
|
||||
const logger = loggerService.withContext('AppUpdater')
|
||||
|
||||
// Language markers constants for multi-language release notes
|
||||
const LANG_MARKERS = {
|
||||
EN_START: '<!--LANG:en-->',
|
||||
ZH_CN_START: '<!--LANG:zh-CN-->',
|
||||
END: '<!--LANG:END-->'
|
||||
} as const
|
||||
|
||||
export default class AppUpdater {
|
||||
autoUpdater: _AppUpdater = autoUpdater
|
||||
private releaseInfo: UpdateInfo | undefined
|
||||
@@ -30,7 +37,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) => {
|
||||
@@ -40,7 +48,8 @@ export default class AppUpdater {
|
||||
|
||||
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
||||
logger.info('update available', releaseInfo)
|
||||
windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateAvailable, releaseInfo)
|
||||
const processedReleaseInfo = this.processReleaseInfo(releaseInfo)
|
||||
windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateAvailable, processedReleaseInfo)
|
||||
})
|
||||
|
||||
// 检测到不需要更新时
|
||||
@@ -55,9 +64,10 @@ export default class AppUpdater {
|
||||
|
||||
// 当需要更新的内容下载完成后
|
||||
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
|
||||
windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateDownloaded, releaseInfo)
|
||||
this.releaseInfo = releaseInfo
|
||||
logger.info('update downloaded', releaseInfo)
|
||||
const processedReleaseInfo = this.processReleaseInfo(releaseInfo)
|
||||
windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateDownloaded, processedReleaseInfo)
|
||||
this.releaseInfo = processedReleaseInfo
|
||||
logger.info('update downloaded', processedReleaseInfo)
|
||||
})
|
||||
|
||||
if (isWin) {
|
||||
@@ -270,16 +280,99 @@ export default class AppUpdater {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if release notes contain multi-language markers
|
||||
*/
|
||||
private hasMultiLanguageMarkers(releaseNotes: string): boolean {
|
||||
return releaseNotes.includes(LANG_MARKERS.EN_START)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse multi-language release notes and return the appropriate language version
|
||||
* @param releaseNotes - Release notes string with language markers
|
||||
* @returns Parsed release notes for the user's language
|
||||
*
|
||||
* Expected format:
|
||||
* <!--LANG:en-->English content<!--LANG:zh-CN-->Chinese content<!--LANG:END-->
|
||||
*/
|
||||
private parseMultiLangReleaseNotes(releaseNotes: string): string {
|
||||
try {
|
||||
const language = configManager.getLanguage()
|
||||
const isChineseUser = language === 'zh-CN' || language === 'zh-TW'
|
||||
|
||||
// Create regex patterns using constants
|
||||
const enPattern = new RegExp(
|
||||
`${LANG_MARKERS.EN_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)${LANG_MARKERS.ZH_CN_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`
|
||||
)
|
||||
const zhPattern = new RegExp(
|
||||
`${LANG_MARKERS.ZH_CN_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)${LANG_MARKERS.END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`
|
||||
)
|
||||
|
||||
// Extract language sections
|
||||
const enMatch = releaseNotes.match(enPattern)
|
||||
const zhMatch = releaseNotes.match(zhPattern)
|
||||
|
||||
// Return appropriate language version with proper fallback
|
||||
if (isChineseUser && zhMatch) {
|
||||
return zhMatch[1].trim()
|
||||
} else if (enMatch) {
|
||||
return enMatch[1].trim()
|
||||
} else {
|
||||
// Clean fallback: remove all language markers
|
||||
logger.warn('Failed to extract language-specific release notes, using cleaned fallback')
|
||||
return releaseNotes
|
||||
.replace(new RegExp(`${LANG_MARKERS.EN_START}|${LANG_MARKERS.ZH_CN_START}|${LANG_MARKERS.END}`, 'g'), '')
|
||||
.trim()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse multi-language release notes', error as Error)
|
||||
// Return original notes as safe fallback
|
||||
return releaseNotes
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process release info to handle multi-language release notes
|
||||
* @param releaseInfo - Original release info from updater
|
||||
* @returns Processed release info with localized release notes
|
||||
*/
|
||||
private processReleaseInfo(releaseInfo: UpdateInfo): UpdateInfo {
|
||||
const processedInfo = { ...releaseInfo }
|
||||
|
||||
// Handle multi-language release notes in string format
|
||||
if (releaseInfo.releaseNotes && typeof releaseInfo.releaseNotes === 'string') {
|
||||
// Check if it contains multi-language markers
|
||||
if (this.hasMultiLanguageMarkers(releaseInfo.releaseNotes)) {
|
||||
processedInfo.releaseNotes = this.parseMultiLangReleaseNotes(releaseInfo.releaseNotes)
|
||||
}
|
||||
}
|
||||
|
||||
return processedInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* Format release notes for display
|
||||
* @param releaseNotes - Release notes in various formats
|
||||
* @returns Formatted string for display
|
||||
*/
|
||||
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
|
||||
if (!releaseNotes) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (typeof releaseNotes === 'string') {
|
||||
// Check if it contains multi-language markers
|
||||
if (this.hasMultiLanguageMarkers(releaseNotes)) {
|
||||
return this.parseMultiLangReleaseNotes(releaseNotes)
|
||||
}
|
||||
return releaseNotes
|
||||
}
|
||||
|
||||
return releaseNotes.map((note) => note.note).join('\n')
|
||||
if (Array.isArray(releaseNotes)) {
|
||||
return releaseNotes.map((note) => note.note).join('\n')
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
}
|
||||
interface GithubReleaseInfo {
|
||||
|
||||
@@ -3,11 +3,20 @@ import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { isWin } from '@main/constant'
|
||||
import { isMac, isWin } from '@main/constant'
|
||||
import { removeEnvProxy } from '@main/utils'
|
||||
import { isUserInChina } from '@main/utils/ipService'
|
||||
import { getBinaryName } from '@main/utils/process'
|
||||
import { codeTools } from '@shared/config/constant'
|
||||
import {
|
||||
codeTools,
|
||||
MACOS_TERMINALS,
|
||||
MACOS_TERMINALS_WITH_COMMANDS,
|
||||
terminalApps,
|
||||
TerminalConfig,
|
||||
TerminalConfigWithCommand,
|
||||
WINDOWS_TERMINALS,
|
||||
WINDOWS_TERMINALS_WITH_COMMANDS
|
||||
} from '@shared/config/constant'
|
||||
import { spawn } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
|
||||
@@ -22,7 +31,10 @@ interface VersionInfo {
|
||||
|
||||
class CodeToolsService {
|
||||
private versionCache: Map<string, { version: string; timestamp: number }> = new Map()
|
||||
private terminalsCache: { terminals: TerminalConfig[]; timestamp: number } | null = null
|
||||
private customTerminalPaths: Map<string, string> = new Map() // Store user-configured terminal paths
|
||||
private readonly CACHE_DURATION = 1000 * 60 * 30 // 30 minutes cache
|
||||
private readonly TERMINALS_CACHE_DURATION = 1000 * 60 * 5 // 5 minutes cache for terminals
|
||||
|
||||
constructor() {
|
||||
this.getBunPath = this.getBunPath.bind(this)
|
||||
@@ -32,6 +44,23 @@ class CodeToolsService {
|
||||
this.getVersionInfo = this.getVersionInfo.bind(this)
|
||||
this.updatePackage = this.updatePackage.bind(this)
|
||||
this.run = this.run.bind(this)
|
||||
|
||||
if (isMac || isWin) {
|
||||
this.preloadTerminals()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload available terminals in background
|
||||
*/
|
||||
private async preloadTerminals(): Promise<void> {
|
||||
try {
|
||||
logger.info('Preloading available terminals...')
|
||||
await this.getAvailableTerminals()
|
||||
logger.info('Terminal preloading completed')
|
||||
} catch (error) {
|
||||
logger.warn('Terminal preloading failed:', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
public async getBunPath() {
|
||||
@@ -75,10 +104,258 @@ class CodeToolsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a single terminal is available
|
||||
*/
|
||||
private async checkTerminalAvailability(terminal: TerminalConfig): Promise<TerminalConfig | null> {
|
||||
try {
|
||||
if (isMac && terminal.bundleId) {
|
||||
// macOS: Check if application is installed via bundle ID with timeout
|
||||
const { stdout } = await execAsync(`mdfind "kMDItemCFBundleIdentifier == '${terminal.bundleId}'"`, {
|
||||
timeout: 3000
|
||||
})
|
||||
if (stdout.trim()) {
|
||||
return terminal
|
||||
}
|
||||
} else if (isWin) {
|
||||
// Windows: Check terminal availability
|
||||
return await this.checkWindowsTerminalAvailability(terminal)
|
||||
} else {
|
||||
// TODO: Check if terminal is available in linux
|
||||
await execAsync(`which ${terminal.id}`, { timeout: 2000 })
|
||||
return terminal
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(`Terminal ${terminal.id} not available:`, error as Error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Windows terminal availability (simplified - user configured paths)
|
||||
*/
|
||||
private async checkWindowsTerminalAvailability(terminal: TerminalConfig): Promise<TerminalConfig | null> {
|
||||
try {
|
||||
switch (terminal.id) {
|
||||
case terminalApps.cmd:
|
||||
// CMD is always available on Windows
|
||||
return terminal
|
||||
|
||||
case terminalApps.powershell:
|
||||
// Check for PowerShell in PATH
|
||||
try {
|
||||
await execAsync('powershell -Command "Get-Host"', { timeout: 3000 })
|
||||
return terminal
|
||||
} catch {
|
||||
try {
|
||||
await execAsync('pwsh -Command "Get-Host"', { timeout: 3000 })
|
||||
return terminal
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
case terminalApps.windowsTerminal:
|
||||
// Check for Windows Terminal via where command (doesn't launch the terminal)
|
||||
try {
|
||||
await execAsync('where wt', { timeout: 3000 })
|
||||
return terminal
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
case terminalApps.wsl:
|
||||
// Check for WSL
|
||||
try {
|
||||
await execAsync('wsl --status', { timeout: 3000 })
|
||||
return terminal
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
default:
|
||||
// For other terminals (Alacritty, WezTerm), check if user has configured custom path
|
||||
return await this.checkCustomTerminalPath(terminal)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(`Windows terminal ${terminal.id} not available:`, error as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has configured custom path for terminal
|
||||
*/
|
||||
private async checkCustomTerminalPath(terminal: TerminalConfig): Promise<TerminalConfig | null> {
|
||||
// Check if user has configured custom path
|
||||
const customPath = this.customTerminalPaths.get(terminal.id)
|
||||
if (customPath && fs.existsSync(customPath)) {
|
||||
try {
|
||||
await execAsync(`"${customPath}" --version`, { timeout: 3000 })
|
||||
return { ...terminal, customPath }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to PATH check
|
||||
try {
|
||||
const command = terminal.id === terminalApps.alacritty ? 'alacritty' : 'wezterm'
|
||||
await execAsync(`${command} --version`, { timeout: 3000 })
|
||||
return terminal
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom path for a terminal (called from settings UI)
|
||||
*/
|
||||
public setCustomTerminalPath(terminalId: string, path: string): void {
|
||||
logger.info(`Setting custom path for terminal ${terminalId}: ${path}`)
|
||||
this.customTerminalPaths.set(terminalId, path)
|
||||
// Clear terminals cache to force refresh
|
||||
this.terminalsCache = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom path for a terminal
|
||||
*/
|
||||
public getCustomTerminalPath(terminalId: string): string | undefined {
|
||||
return this.customTerminalPaths.get(terminalId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove custom path for a terminal
|
||||
*/
|
||||
public removeCustomTerminalPath(terminalId: string): void {
|
||||
logger.info(`Removing custom path for terminal ${terminalId}`)
|
||||
this.customTerminalPaths.delete(terminalId)
|
||||
// Clear terminals cache to force refresh
|
||||
this.terminalsCache = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available terminals (with caching and parallel checking)
|
||||
*/
|
||||
private async getAvailableTerminals(): Promise<TerminalConfig[]> {
|
||||
const now = Date.now()
|
||||
|
||||
// Check cache first
|
||||
if (this.terminalsCache && now - this.terminalsCache.timestamp < this.TERMINALS_CACHE_DURATION) {
|
||||
logger.info(`Using cached terminals list (${this.terminalsCache.terminals.length} terminals)`)
|
||||
return this.terminalsCache.terminals
|
||||
}
|
||||
|
||||
logger.info('Checking available terminals in parallel...')
|
||||
const startTime = Date.now()
|
||||
|
||||
// Get terminal list based on platform
|
||||
const terminalList = isWin ? WINDOWS_TERMINALS : MACOS_TERMINALS
|
||||
|
||||
// Check all terminals in parallel
|
||||
const terminalPromises = terminalList.map((terminal) => this.checkTerminalAvailability(terminal))
|
||||
|
||||
try {
|
||||
// Wait for all checks to complete with a global timeout
|
||||
const results = await Promise.allSettled(
|
||||
terminalPromises.map((p) =>
|
||||
Promise.race([p, new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000))])
|
||||
)
|
||||
)
|
||||
|
||||
const availableTerminals: TerminalConfig[] = []
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled' && result.value) {
|
||||
availableTerminals.push(result.value as TerminalConfig)
|
||||
} else if (result.status === 'rejected') {
|
||||
logger.debug(`Terminal check failed for ${MACOS_TERMINALS[index].id}:`, result.reason)
|
||||
}
|
||||
})
|
||||
|
||||
const endTime = Date.now()
|
||||
logger.info(
|
||||
`Terminal availability check completed in ${endTime - startTime}ms, found ${availableTerminals.length} terminals`
|
||||
)
|
||||
|
||||
// Cache the results
|
||||
this.terminalsCache = {
|
||||
terminals: availableTerminals,
|
||||
timestamp: now
|
||||
}
|
||||
|
||||
return availableTerminals
|
||||
} catch (error) {
|
||||
logger.error('Error checking terminal availability:', error as Error)
|
||||
// Return cached result if available, otherwise empty array
|
||||
return this.terminalsCache?.terminals || []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get terminal config by ID, fallback to system default
|
||||
*/
|
||||
private async getTerminalConfig(terminalId?: string): Promise<TerminalConfigWithCommand> {
|
||||
const availableTerminals = await this.getAvailableTerminals()
|
||||
const terminalCommands = isWin ? WINDOWS_TERMINALS_WITH_COMMANDS : MACOS_TERMINALS_WITH_COMMANDS
|
||||
const defaultTerminal = isWin ? terminalApps.cmd : terminalApps.systemDefault
|
||||
|
||||
if (terminalId) {
|
||||
let requestedTerminal = terminalCommands.find(
|
||||
(t) => t.id === terminalId && availableTerminals.some((at) => at.id === t.id)
|
||||
)
|
||||
|
||||
if (requestedTerminal) {
|
||||
// Apply custom path if configured
|
||||
const customPath = this.customTerminalPaths.get(terminalId)
|
||||
if (customPath && isWin) {
|
||||
requestedTerminal = this.applyCustomPath(requestedTerminal, customPath)
|
||||
}
|
||||
return requestedTerminal
|
||||
} else {
|
||||
logger.warn(`Requested terminal ${terminalId} not available, falling back to system default`)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to system default Terminal
|
||||
const systemTerminal = terminalCommands.find(
|
||||
(t) => t.id === defaultTerminal && availableTerminals.some((at) => at.id === t.id)
|
||||
)
|
||||
if (systemTerminal) {
|
||||
return systemTerminal
|
||||
}
|
||||
|
||||
// If even system Terminal is not found, return the first available
|
||||
const firstAvailable = terminalCommands.find((t) => availableTerminals.some((at) => at.id === t.id))
|
||||
if (firstAvailable) {
|
||||
return firstAvailable
|
||||
}
|
||||
|
||||
// Last resort fallback
|
||||
return terminalCommands.find((t) => t.id === defaultTerminal)!
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply custom path to terminal configuration
|
||||
*/
|
||||
private applyCustomPath(terminal: TerminalConfigWithCommand, customPath: string): TerminalConfigWithCommand {
|
||||
return {
|
||||
...terminal,
|
||||
customPath,
|
||||
command: (directory: string, fullCommand: string) => {
|
||||
const originalCommand = terminal.command(directory, fullCommand)
|
||||
return {
|
||||
...originalCommand,
|
||||
command: customPath // Replace command with custom path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async isPackageInstalled(cliTool: string): Promise<boolean> {
|
||||
const executableName = await this.getCliExecutableName(cliTool)
|
||||
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const executablePath = path.join(binDir, executableName + (process.platform === 'win32' ? '.exe' : ''))
|
||||
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
|
||||
|
||||
// Ensure bin directory exists
|
||||
if (!fs.existsSync(binDir)) {
|
||||
@@ -105,7 +382,7 @@ class CodeToolsService {
|
||||
try {
|
||||
const executableName = await this.getCliExecutableName(cliTool)
|
||||
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const executablePath = path.join(binDir, executableName + (process.platform === 'win32' ? '.exe' : ''))
|
||||
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
|
||||
|
||||
const { stdout } = await execAsync(`"${executablePath}" --version`, { timeout: 10000 })
|
||||
// Extract version number from output (format may vary by tool)
|
||||
@@ -191,6 +468,17 @@ class CodeToolsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available terminals for the current platform
|
||||
*/
|
||||
public async getAvailableTerminalsForPlatform(): Promise<TerminalConfig[]> {
|
||||
if (isMac || isWin) {
|
||||
return this.getAvailableTerminals()
|
||||
}
|
||||
// For other platforms, return empty array for now
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a CLI tool to the latest version
|
||||
*/
|
||||
@@ -202,10 +490,9 @@ class CodeToolsService {
|
||||
const bunInstallPath = path.join(os.homedir(), '.cherrystudio')
|
||||
const registryUrl = await this.getNpmRegistryUrl()
|
||||
|
||||
const installEnvPrefix =
|
||||
process.platform === 'win32'
|
||||
? `set "BUN_INSTALL=${bunInstallPath}" && set "NPM_CONFIG_REGISTRY=${registryUrl}" &&`
|
||||
: `export BUN_INSTALL="${bunInstallPath}" && export NPM_CONFIG_REGISTRY="${registryUrl}" &&`
|
||||
const installEnvPrefix = isWin
|
||||
? `set "BUN_INSTALL=${bunInstallPath}" && set "NPM_CONFIG_REGISTRY=${registryUrl}" &&`
|
||||
: `export BUN_INSTALL="${bunInstallPath}" && export NPM_CONFIG_REGISTRY="${registryUrl}" &&`
|
||||
|
||||
const updateCommand = `${installEnvPrefix} "${bunPath}" install -g ${packageName}`
|
||||
logger.info(`Executing update command: ${updateCommand}`)
|
||||
@@ -241,7 +528,7 @@ class CodeToolsService {
|
||||
_model: string,
|
||||
directory: string,
|
||||
env: Record<string, string>,
|
||||
options: { autoUpdateToLatest?: boolean } = {}
|
||||
options: { autoUpdateToLatest?: boolean; terminal?: string } = {}
|
||||
) {
|
||||
logger.info(`Starting CLI tool launch: ${cliTool} in directory: ${directory}`)
|
||||
logger.debug(`Environment variables:`, Object.keys(env))
|
||||
@@ -251,7 +538,7 @@ class CodeToolsService {
|
||||
const bunPath = await this.getBunPath()
|
||||
const executableName = await this.getCliExecutableName(cliTool)
|
||||
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const executablePath = path.join(binDir, executableName + (process.platform === 'win32' ? '.exe' : ''))
|
||||
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
|
||||
|
||||
logger.debug(`Package name: ${packageName}`)
|
||||
logger.debug(`Bun path: ${bunPath}`)
|
||||
@@ -295,7 +582,13 @@ class CodeToolsService {
|
||||
|
||||
// Build environment variable prefix (based on platform)
|
||||
const buildEnvPrefix = (isWindows: boolean) => {
|
||||
if (Object.keys(env).length === 0) return ''
|
||||
if (Object.keys(env).length === 0) {
|
||||
logger.info('No environment variables to set')
|
||||
return ''
|
||||
}
|
||||
|
||||
logger.info('Setting environment variables:', Object.keys(env))
|
||||
logger.info('Environment variable values:', env)
|
||||
|
||||
if (isWindows) {
|
||||
// Windows uses set command
|
||||
@@ -304,13 +597,29 @@ class CodeToolsService {
|
||||
.join(' && ')
|
||||
} else {
|
||||
// Unix-like systems use export command
|
||||
return Object.entries(env)
|
||||
.map(([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`)
|
||||
const validEntries = Object.entries(env).filter(([key, value]) => {
|
||||
if (!key || key.trim() === '') {
|
||||
return false
|
||||
}
|
||||
if (value === undefined || value === null) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const envCommands = validEntries
|
||||
.map(([key, value]) => {
|
||||
const sanitizedValue = String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
||||
const exportCmd = `export ${key}="${sanitizedValue}"`
|
||||
logger.info(`Setting env var: ${key}="${sanitizedValue}"`)
|
||||
logger.info(`Export command: ${exportCmd}`)
|
||||
return exportCmd
|
||||
})
|
||||
.join(' && ')
|
||||
return envCommands
|
||||
}
|
||||
}
|
||||
|
||||
// Build command to execute
|
||||
let baseCommand = isWin ? `"${executablePath}"` : `"${bunPath}" "${executablePath}"`
|
||||
|
||||
// Add configuration parameters for OpenAI Codex
|
||||
@@ -351,20 +660,20 @@ class CodeToolsService {
|
||||
|
||||
switch (platform) {
|
||||
case 'darwin': {
|
||||
// macOS - Use osascript to launch terminal and execute command directly, without showing startup command
|
||||
// macOS - Support multiple terminals
|
||||
const envPrefix = buildEnvPrefix(false)
|
||||
const command = envPrefix ? `${envPrefix} && ${baseCommand}` : baseCommand
|
||||
// Combine directory change with the main command to ensure they execute in the same shell session
|
||||
const fullCommand = `cd '${directory.replace(/'/g, "\\'")}' && clear && ${command}`
|
||||
|
||||
terminalCommand = 'osascript'
|
||||
terminalArgs = [
|
||||
'-e',
|
||||
`tell application "Terminal"
|
||||
do script "${fullCommand.replace(/"/g, '\\"')}"
|
||||
activate
|
||||
end tell`
|
||||
]
|
||||
const command = envPrefix ? `${envPrefix} && ${baseCommand}` : baseCommand
|
||||
|
||||
// Combine directory change with the main command to ensure they execute in the same shell session
|
||||
const fullCommand = `cd "${directory.replace(/"/g, '\\"')}" && clear && ${command}`
|
||||
|
||||
const terminalConfig = await this.getTerminalConfig(options.terminal)
|
||||
logger.info(`Using terminal: ${terminalConfig.name} (${terminalConfig.id})`)
|
||||
|
||||
const { command: cmd, args } = terminalConfig.command(directory, fullCommand)
|
||||
terminalCommand = cmd
|
||||
terminalArgs = args
|
||||
break
|
||||
}
|
||||
case 'win32': {
|
||||
@@ -424,9 +733,23 @@ end tell`
|
||||
throw new Error(`Failed to create launch script: ${error}`)
|
||||
}
|
||||
|
||||
// Launch bat file - Use safest start syntax, no title parameter
|
||||
terminalCommand = 'cmd'
|
||||
terminalArgs = ['/c', 'start', batFilePath]
|
||||
// Use selected terminal configuration
|
||||
const terminalConfig = await this.getTerminalConfig(options.terminal)
|
||||
logger.info(`Using terminal: ${terminalConfig.name} (${terminalConfig.id})`)
|
||||
|
||||
// Get command and args from terminal configuration
|
||||
// Pass the bat file path as the command to execute
|
||||
const fullCommand = batFilePath
|
||||
const { command: cmd, args } = terminalConfig.command(directory, fullCommand)
|
||||
|
||||
// Override if it's a custom terminal with a custom path
|
||||
if (terminalConfig.customPath) {
|
||||
terminalCommand = terminalConfig.customPath
|
||||
terminalArgs = args
|
||||
} else {
|
||||
terminalCommand = cmd
|
||||
terminalArgs = args
|
||||
}
|
||||
|
||||
// Set cleanup task (delete temp file after 5 minutes)
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -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<string>(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)
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
319
src/main/services/__tests__/AppUpdater.test.ts
Normal file
319
src/main/services/__tests__/AppUpdater.test.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { UpdateInfo } from 'builder-util-runtime'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@logger', () => ({
|
||||
loggerService: {
|
||||
withContext: () => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn()
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('../ConfigManager', () => ({
|
||||
configManager: {
|
||||
getLanguage: vi.fn(),
|
||||
getAutoUpdate: vi.fn(() => false),
|
||||
getTestPlan: vi.fn(() => false),
|
||||
getTestChannel: vi.fn(),
|
||||
getClientId: vi.fn(() => 'test-client-id')
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('../WindowService', () => ({
|
||||
windowService: {
|
||||
getMainWindow: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@main/constant', () => ({
|
||||
isWin: false
|
||||
}))
|
||||
|
||||
vi.mock('@main/utils/ipService', () => ({
|
||||
getIpCountry: vi.fn(() => 'US')
|
||||
}))
|
||||
|
||||
vi.mock('@main/utils/locales', () => ({
|
||||
locales: {
|
||||
en: { translation: { update: {} } },
|
||||
'zh-CN': { translation: { update: {} } }
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@main/utils/systemInfo', () => ({
|
||||
generateUserAgent: vi.fn(() => 'test-user-agent')
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
isPackaged: true,
|
||||
getVersion: vi.fn(() => '1.0.0'),
|
||||
getPath: vi.fn(() => '/test/path')
|
||||
},
|
||||
dialog: {
|
||||
showMessageBox: vi.fn()
|
||||
},
|
||||
BrowserWindow: vi.fn(),
|
||||
net: {
|
||||
fetch: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('electron-updater', () => ({
|
||||
autoUpdater: {
|
||||
logger: null,
|
||||
forceDevUpdateConfig: false,
|
||||
autoDownload: false,
|
||||
autoInstallOnAppQuit: false,
|
||||
requestHeaders: {},
|
||||
on: vi.fn(),
|
||||
setFeedURL: vi.fn(),
|
||||
checkForUpdates: vi.fn(),
|
||||
downloadUpdate: vi.fn(),
|
||||
quitAndInstall: vi.fn(),
|
||||
channel: '',
|
||||
allowDowngrade: false,
|
||||
disableDifferentialDownload: false,
|
||||
currentVersion: '1.0.0'
|
||||
},
|
||||
Logger: vi.fn(),
|
||||
NsisUpdater: vi.fn(),
|
||||
AppUpdater: vi.fn()
|
||||
}))
|
||||
|
||||
// Import after mocks
|
||||
import AppUpdater from '../AppUpdater'
|
||||
import { configManager } from '../ConfigManager'
|
||||
|
||||
describe('AppUpdater', () => {
|
||||
let appUpdater: AppUpdater
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
appUpdater = new AppUpdater()
|
||||
})
|
||||
|
||||
describe('parseMultiLangReleaseNotes', () => {
|
||||
const sampleReleaseNotes = `<!--LANG:en-->
|
||||
🚀 New Features:
|
||||
- Feature A
|
||||
- Feature B
|
||||
|
||||
🎨 UI Improvements:
|
||||
- Improvement A
|
||||
<!--LANG:zh-CN-->
|
||||
🚀 新功能:
|
||||
- 功能 A
|
||||
- 功能 B
|
||||
|
||||
🎨 界面改进:
|
||||
- 改进 A
|
||||
<!--LANG:END-->`
|
||||
|
||||
it('should return Chinese notes for zh-CN users', () => {
|
||||
vi.mocked(configManager.getLanguage).mockReturnValue('zh-CN')
|
||||
|
||||
const result = (appUpdater as any).parseMultiLangReleaseNotes(sampleReleaseNotes)
|
||||
|
||||
expect(result).toContain('新功能')
|
||||
expect(result).toContain('功能 A')
|
||||
expect(result).not.toContain('New Features')
|
||||
})
|
||||
|
||||
it('should return Chinese notes for zh-TW users', () => {
|
||||
vi.mocked(configManager.getLanguage).mockReturnValue('zh-TW')
|
||||
|
||||
const result = (appUpdater as any).parseMultiLangReleaseNotes(sampleReleaseNotes)
|
||||
|
||||
expect(result).toContain('新功能')
|
||||
expect(result).toContain('功能 A')
|
||||
expect(result).not.toContain('New Features')
|
||||
})
|
||||
|
||||
it('should return English notes for non-Chinese users', () => {
|
||||
vi.mocked(configManager.getLanguage).mockReturnValue('en-US')
|
||||
|
||||
const result = (appUpdater as any).parseMultiLangReleaseNotes(sampleReleaseNotes)
|
||||
|
||||
expect(result).toContain('New Features')
|
||||
expect(result).toContain('Feature A')
|
||||
expect(result).not.toContain('新功能')
|
||||
})
|
||||
|
||||
it('should return English notes for other language users', () => {
|
||||
vi.mocked(configManager.getLanguage).mockReturnValue('ru-RU')
|
||||
|
||||
const result = (appUpdater as any).parseMultiLangReleaseNotes(sampleReleaseNotes)
|
||||
|
||||
expect(result).toContain('New Features')
|
||||
expect(result).not.toContain('新功能')
|
||||
})
|
||||
|
||||
it('should handle missing language sections gracefully', () => {
|
||||
const malformedNotes = 'Simple release notes without markers'
|
||||
|
||||
const result = (appUpdater as any).parseMultiLangReleaseNotes(malformedNotes)
|
||||
|
||||
expect(result).toBe('Simple release notes without markers')
|
||||
})
|
||||
|
||||
it('should handle malformed markers', () => {
|
||||
const malformedNotes = `<!--LANG:en-->English only`
|
||||
vi.mocked(configManager.getLanguage).mockReturnValue('zh-CN')
|
||||
|
||||
const result = (appUpdater as any).parseMultiLangReleaseNotes(malformedNotes)
|
||||
|
||||
// Should clean up markers and return cleaned content
|
||||
expect(result).toContain('English only')
|
||||
expect(result).not.toContain('<!--LANG:')
|
||||
})
|
||||
|
||||
it('should handle empty release notes', () => {
|
||||
const result = (appUpdater as any).parseMultiLangReleaseNotes('')
|
||||
|
||||
expect(result).toBe('')
|
||||
})
|
||||
|
||||
it('should handle errors gracefully', () => {
|
||||
// Force an error by mocking configManager to throw
|
||||
vi.mocked(configManager.getLanguage).mockImplementation(() => {
|
||||
throw new Error('Test error')
|
||||
})
|
||||
|
||||
const result = (appUpdater as any).parseMultiLangReleaseNotes(sampleReleaseNotes)
|
||||
|
||||
// Should return original notes as fallback
|
||||
expect(result).toBe(sampleReleaseNotes)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasMultiLanguageMarkers', () => {
|
||||
it('should return true when markers are present', () => {
|
||||
const notes = '<!--LANG:en-->Test'
|
||||
|
||||
const result = (appUpdater as any).hasMultiLanguageMarkers(notes)
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when no markers are present', () => {
|
||||
const notes = 'Simple text without markers'
|
||||
|
||||
const result = (appUpdater as any).hasMultiLanguageMarkers(notes)
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('processReleaseInfo', () => {
|
||||
it('should process multi-language release notes in string format', () => {
|
||||
vi.mocked(configManager.getLanguage).mockReturnValue('zh-CN')
|
||||
|
||||
const releaseInfo = {
|
||||
version: '1.0.0',
|
||||
files: [],
|
||||
path: '',
|
||||
sha512: '',
|
||||
releaseDate: new Date().toISOString(),
|
||||
releaseNotes: `<!--LANG:en-->English notes<!--LANG:zh-CN-->中文说明<!--LANG:END-->`
|
||||
} as UpdateInfo
|
||||
|
||||
const result = (appUpdater as any).processReleaseInfo(releaseInfo)
|
||||
|
||||
expect(result.releaseNotes).toBe('中文说明')
|
||||
})
|
||||
|
||||
it('should not process release notes without markers', () => {
|
||||
const releaseInfo = {
|
||||
version: '1.0.0',
|
||||
files: [],
|
||||
path: '',
|
||||
sha512: '',
|
||||
releaseDate: new Date().toISOString(),
|
||||
releaseNotes: 'Simple release notes'
|
||||
} as UpdateInfo
|
||||
|
||||
const result = (appUpdater as any).processReleaseInfo(releaseInfo)
|
||||
|
||||
expect(result.releaseNotes).toBe('Simple release notes')
|
||||
})
|
||||
|
||||
it('should handle array format release notes', () => {
|
||||
const releaseInfo = {
|
||||
version: '1.0.0',
|
||||
files: [],
|
||||
path: '',
|
||||
sha512: '',
|
||||
releaseDate: new Date().toISOString(),
|
||||
releaseNotes: [
|
||||
{ version: '1.0.0', note: 'Note 1' },
|
||||
{ version: '1.0.1', note: 'Note 2' }
|
||||
]
|
||||
} as UpdateInfo
|
||||
|
||||
const result = (appUpdater as any).processReleaseInfo(releaseInfo)
|
||||
|
||||
expect(result.releaseNotes).toEqual(releaseInfo.releaseNotes)
|
||||
})
|
||||
|
||||
it('should handle null release notes', () => {
|
||||
const releaseInfo = {
|
||||
version: '1.0.0',
|
||||
files: [],
|
||||
path: '',
|
||||
sha512: '',
|
||||
releaseDate: new Date().toISOString(),
|
||||
releaseNotes: null
|
||||
} as UpdateInfo
|
||||
|
||||
const result = (appUpdater as any).processReleaseInfo(releaseInfo)
|
||||
|
||||
expect(result.releaseNotes).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatReleaseNotes', () => {
|
||||
it('should format string release notes with markers', () => {
|
||||
vi.mocked(configManager.getLanguage).mockReturnValue('en-US')
|
||||
const notes = `<!--LANG:en-->English<!--LANG:zh-CN-->中文<!--LANG:END-->`
|
||||
|
||||
const result = (appUpdater as any).formatReleaseNotes(notes)
|
||||
|
||||
expect(result).toBe('English')
|
||||
})
|
||||
|
||||
it('should format string release notes without markers', () => {
|
||||
const notes = 'Simple notes'
|
||||
|
||||
const result = (appUpdater as any).formatReleaseNotes(notes)
|
||||
|
||||
expect(result).toBe('Simple notes')
|
||||
})
|
||||
|
||||
it('should format array release notes', () => {
|
||||
const notes = [
|
||||
{ version: '1.0.0', note: 'Note 1' },
|
||||
{ version: '1.0.1', note: 'Note 2' }
|
||||
]
|
||||
|
||||
const result = (appUpdater as any).formatReleaseNotes(notes)
|
||||
|
||||
expect(result).toBe('Note 1\nNote 2')
|
||||
})
|
||||
|
||||
it('should handle null release notes', () => {
|
||||
const result = (appUpdater as any).formatReleaseNotes(null)
|
||||
|
||||
expect(result).toBe('')
|
||||
})
|
||||
|
||||
it('should handle undefined release notes', () => {
|
||||
const result = (appUpdater as any).formatReleaseNotes(undefined)
|
||||
|
||||
expect(result).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
||||
import { SpanContext } from '@opentelemetry/api'
|
||||
import { UpgradeChannel } from '@shared/config/constant'
|
||||
import { TerminalConfig, UpgradeChannel } from '@shared/config/constant'
|
||||
import type { LogLevel, LogSourceWithContext } from '@shared/config/logger'
|
||||
import type { FileChangeEvent } from '@shared/config/types'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
@@ -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),
|
||||
@@ -439,16 +440,24 @@ const api = {
|
||||
model: string,
|
||||
directory: string,
|
||||
env: Record<string, string>,
|
||||
options?: { autoUpdateToLatest?: boolean }
|
||||
) => ipcRenderer.invoke(IpcChannel.CodeTools_Run, cliTool, model, directory, env, options)
|
||||
options?: { autoUpdateToLatest?: boolean; terminal?: string }
|
||||
) => ipcRenderer.invoke(IpcChannel.CodeTools_Run, cliTool, model, directory, env, options),
|
||||
getAvailableTerminals: (): Promise<TerminalConfig[]> =>
|
||||
ipcRenderer.invoke(IpcChannel.CodeTools_GetAvailableTerminals),
|
||||
setCustomTerminalPath: (terminalId: string, path: string): Promise<void> =>
|
||||
ipcRenderer.invoke(IpcChannel.CodeTools_SetCustomTerminalPath, terminalId, path),
|
||||
getCustomTerminalPath: (terminalId: string): Promise<string | undefined> =>
|
||||
ipcRenderer.invoke(IpcChannel.CodeTools_GetCustomTerminalPath, terminalId),
|
||||
removeCustomTerminalPath: (terminalId: string): Promise<void> =>
|
||||
ipcRenderer.invoke(IpcChannel.CodeTools_RemoveCustomTerminalPath, terminalId)
|
||||
},
|
||||
ocr: {
|
||||
ocr: (file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> =>
|
||||
ipcRenderer.invoke(IpcChannel.OCR_ocr, file, provider)
|
||||
},
|
||||
cherryin: {
|
||||
cherryai: {
|
||||
generateSignature: (params: { method: string; path: string; query: string; body: Record<string, any> }) =>
|
||||
ipcRenderer.invoke(IpcChannel.Cherryin_GetSignature, params)
|
||||
ipcRenderer.invoke(IpcChannel.Cherryai_GetSignature, params)
|
||||
},
|
||||
windowControls: {
|
||||
minimize: (): Promise<void> => ipcRenderer.invoke(IpcChannel.Windows_Minimize),
|
||||
|
||||
@@ -13,16 +13,6 @@ import { ToolCallChunkHandler } from './handleToolCallChunk'
|
||||
|
||||
const logger = loggerService.withContext('AiSdkToChunkAdapter')
|
||||
|
||||
export interface CherryStudioChunk {
|
||||
type: 'text-delta' | 'text-complete' | 'tool-call' | 'tool-result' | 'finish' | 'error'
|
||||
text?: string
|
||||
toolCall?: any
|
||||
toolResult?: any
|
||||
finishReason?: string
|
||||
usage?: any
|
||||
error?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* AI SDK 到 Cherry Studio Chunk 适配器类
|
||||
* 处理 fullStream 到 Cherry Studio chunk 的转换
|
||||
@@ -180,8 +170,7 @@ export class AiSdkToChunkAdapter {
|
||||
case 'reasoning-end':
|
||||
this.onChunk({
|
||||
type: ChunkType.THINKING_COMPLETE,
|
||||
text: (chunk.providerMetadata?.metadata?.thinking_content as string) || '',
|
||||
thinking_millsec: (chunk.providerMetadata?.metadata?.thinking_millsec as number) || 0
|
||||
text: (chunk.providerMetadata?.metadata?.thinking_content as string) || final.reasoningContent
|
||||
})
|
||||
final.reasoningContent = ''
|
||||
break
|
||||
|
||||
@@ -298,8 +298,29 @@ export class ToolCallChunkHandler {
|
||||
type: ChunkType.MCP_TOOL_COMPLETE,
|
||||
responses: [toolResponse]
|
||||
})
|
||||
|
||||
const images: string[] = []
|
||||
for (const content of toolResponse.response?.content || []) {
|
||||
if (content.type === 'image' && content.data) {
|
||||
images.push(`data:${content.mimeType};base64,${content.data}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (images.length) {
|
||||
this.onChunk({
|
||||
type: ChunkType.IMAGE_CREATED
|
||||
})
|
||||
this.onChunk({
|
||||
type: ChunkType.IMAGE_COMPLETE,
|
||||
image: {
|
||||
type: 'base64',
|
||||
images: images
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleToolError(
|
||||
chunk: {
|
||||
type: 'tool-error'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { isNewApiProvider } from '@renderer/config/providers'
|
||||
import { Provider } from '@renderer/types'
|
||||
|
||||
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 +35,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
|
||||
}
|
||||
|
||||
@@ -45,7 +46,7 @@ export class ApiClientFactory {
|
||||
return instance
|
||||
}
|
||||
|
||||
if (provider.id === 'new-api') {
|
||||
if (isNewApiProvider(provider)) {
|
||||
logger.debug(`Creating NewAPIClient for provider: ${provider.id}`)
|
||||
instance = new NewAPIClient(provider) as BaseApiClient
|
||||
return instance
|
||||
|
||||
@@ -67,7 +67,9 @@ vi.mock('@renderer/config/models', () => ({
|
||||
silicon: [],
|
||||
defaultModel: []
|
||||
},
|
||||
isOpenAIModel: vi.fn(() => false)
|
||||
isOpenAIModel: vi.fn(() => false),
|
||||
glm45FlashModel: {},
|
||||
qwen38bModel: {}
|
||||
}))
|
||||
|
||||
describe('ApiClientFactory', () => {
|
||||
|
||||
@@ -35,12 +35,8 @@ vi.mock('@renderer/config/models', () => ({
|
||||
findTokenLimit: vi.fn().mockReturnValue(4096),
|
||||
isFunctionCallingModel: vi.fn().mockReturnValue(false),
|
||||
DEFAULT_MAX_TOKENS: 4096,
|
||||
glm45FlashModel: {
|
||||
id: 'glm-4.5-flash',
|
||||
name: 'GLM-4.5-Flash',
|
||||
provider: 'cherryin',
|
||||
group: 'GLM-4.5'
|
||||
}
|
||||
qwen38bModel: {},
|
||||
glm45FlashModel: {}
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/services/AssistantService', () => ({
|
||||
|
||||
@@ -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<OpenAI.Models.Model[]> {
|
||||
@@ -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
|
||||
}))
|
||||
@@ -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')
|
||||
|
||||
// 定义错误模式映射
|
||||
|
||||
@@ -140,7 +140,19 @@ export function buildAiSdkMiddlewares(config: AiSdkMiddlewareConfig): LanguageMo
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
const tagNameArray = ['think', 'thought', 'reasoning']
|
||||
const tagName = {
|
||||
reasoning: 'reasoning',
|
||||
think: 'think',
|
||||
thought: 'thought',
|
||||
seedThink: 'seed:think'
|
||||
}
|
||||
|
||||
function getReasoningTagName(modelId: string | undefined): string {
|
||||
if (modelId?.includes('gpt-oss')) return tagName.reasoning
|
||||
if (modelId?.includes('gemini')) return tagName.thought
|
||||
if (modelId?.includes('seed-oss-36b')) return tagName.seedThink
|
||||
return tagName.think
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加provider特定的中间件
|
||||
@@ -156,7 +168,7 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config:
|
||||
case 'openai':
|
||||
case 'azure-openai': {
|
||||
if (config.enableReasoning) {
|
||||
const tagName = config.model?.id.includes('gemini') ? tagNameArray[1] : tagNameArray[0]
|
||||
const tagName = getReasoningTagName(config.model?.id.toLowerCase())
|
||||
builder.add({
|
||||
name: 'thinking-tag-extraction',
|
||||
middleware: extractReasoningMiddleware({ tagName })
|
||||
@@ -168,13 +180,6 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config:
|
||||
// Gemini特定中间件
|
||||
break
|
||||
case 'aws-bedrock': {
|
||||
if (config.model?.id.includes('gpt-oss')) {
|
||||
const tagName = tagNameArray[2]
|
||||
builder.add({
|
||||
name: 'thinking-tag-extraction',
|
||||
middleware: extractReasoningMiddleware({ tagName })
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -7,18 +7,14 @@ export default definePlugin({
|
||||
transformStream: () => () => {
|
||||
// === 时间跟踪状态 ===
|
||||
let thinkingStartTime = 0
|
||||
let hasStartedThinking = false
|
||||
let accumulatedThinkingContent = ''
|
||||
let reasoningBlockId = ''
|
||||
|
||||
return new TransformStream<TextStreamPart<ToolSet>, TextStreamPart<ToolSet>>({
|
||||
transform(chunk: TextStreamPart<ToolSet>, controller: TransformStreamDefaultController<TextStreamPart<ToolSet>>) {
|
||||
// === 处理 reasoning 类型 ===
|
||||
if (chunk.type === 'reasoning-start') {
|
||||
controller.enqueue(chunk)
|
||||
hasStartedThinking = true
|
||||
thinkingStartTime = performance.now()
|
||||
reasoningBlockId = chunk.id
|
||||
} else if (chunk.type === 'reasoning-delta') {
|
||||
accumulatedThinkingContent += chunk.text
|
||||
controller.enqueue({
|
||||
@@ -32,21 +28,6 @@ export default definePlugin({
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (chunk.type === 'reasoning-end' && hasStartedThinking) {
|
||||
controller.enqueue({
|
||||
type: 'reasoning-end',
|
||||
id: reasoningBlockId,
|
||||
providerMetadata: {
|
||||
metadata: {
|
||||
thinking_millsec: performance.now() - thinkingStartTime,
|
||||
thinking_content: accumulatedThinkingContent
|
||||
}
|
||||
}
|
||||
})
|
||||
accumulatedThinkingContent = ''
|
||||
hasStartedThinking = false
|
||||
thinkingStartTime = 0
|
||||
reasoningBlockId = ''
|
||||
} else {
|
||||
controller.enqueue(chunk)
|
||||
}
|
||||
|
||||
@@ -134,9 +134,10 @@ export async function buildStreamTextParams(
|
||||
if (aiSdkProviderId === 'google-vertex') {
|
||||
tools.google_search = vertex.tools.googleSearch({}) as ProviderDefinedTool
|
||||
} else if (aiSdkProviderId === 'google-vertex-anthropic') {
|
||||
const blockedDomains = mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
tools.web_search = vertexAnthropic.tools.webSearch_20250305({
|
||||
maxUses: webSearchConfig.maxResults,
|
||||
blockedDomains: mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
|
||||
}) as ProviderDefinedTool
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type ProviderSettingsMap
|
||||
} from '@cherrystudio/ai-core/provider'
|
||||
import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
|
||||
import { isNewApiProvider } from '@renderer/config/providers'
|
||||
import {
|
||||
getAwsBedrockAccessKeyId,
|
||||
getAwsBedrockRegion,
|
||||
@@ -65,7 +66,7 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
|
||||
if (provider.id === 'aihubmix') {
|
||||
return aihubmixProviderCreator(model, provider)
|
||||
}
|
||||
if (provider.id === 'new-api') {
|
||||
if (isNewApiProvider(provider)) {
|
||||
return newApiResolverCreator(model, provider)
|
||||
}
|
||||
if (provider.id === 'vertexai') {
|
||||
@@ -213,7 +214,8 @@ export function providerToAiSdkConfig(
|
||||
options: {
|
||||
...options,
|
||||
name: actualProvider.id,
|
||||
...extraOptions
|
||||
...extraOptions,
|
||||
includeUsage: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,10 +251,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: '',
|
||||
|
||||
@@ -84,6 +84,7 @@ export function buildProviderOptions(
|
||||
case 'openai':
|
||||
case 'openai-chat':
|
||||
case 'azure':
|
||||
case 'azure-responses':
|
||||
providerSpecificOptions = {
|
||||
...buildOpenAIProviderOptions(assistant, model, capabilities),
|
||||
serviceTier: serviceTierSetting
|
||||
|
||||
@@ -52,7 +52,7 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
return {}
|
||||
}
|
||||
// Don't disable reasoning for models that require it
|
||||
if (isGrokReasoningModel(model) || isOpenAIReasoningModel(model)) {
|
||||
if (isGrokReasoningModel(model) || isOpenAIReasoningModel(model) || model.id.includes('seed-oss')) {
|
||||
return {}
|
||||
}
|
||||
return { reasoning: { enabled: false, exclude: true } }
|
||||
@@ -112,6 +112,8 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
return {
|
||||
enable_thinking: true
|
||||
}
|
||||
case SystemProviderIds.hunyuan:
|
||||
case SystemProviderIds['tencent-cloud-ti']:
|
||||
case SystemProviderIds.doubao:
|
||||
return {
|
||||
thinking: {
|
||||
|
||||
@@ -44,7 +44,7 @@ function mapMaxResultToOpenAIContextSize(maxResults: number): OpenAISearchConfig
|
||||
export function buildProviderBuiltinWebSearchConfig(
|
||||
providerId: BaseProviderId,
|
||||
webSearchConfig: CherryWebSearchConfig
|
||||
): WebSearchPluginConfig {
|
||||
): WebSearchPluginConfig | undefined {
|
||||
switch (providerId) {
|
||||
case 'openai': {
|
||||
return {
|
||||
@@ -61,9 +61,10 @@ export function buildProviderBuiltinWebSearchConfig(
|
||||
}
|
||||
}
|
||||
case 'anthropic': {
|
||||
const blockedDomains = mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
const anthropicSearchOptions: AnthropicSearchConfig = {
|
||||
maxUses: webSearchConfig.maxResults,
|
||||
blockedDomains: mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
|
||||
}
|
||||
return {
|
||||
anthropic: anthropicSearchOptions
|
||||
@@ -99,7 +100,7 @@ export function buildProviderBuiltinWebSearchConfig(
|
||||
}
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported provider: ${providerId}`)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
src/renderer/src/assets/images/providers/aiOnly.webp
Normal file
BIN
src/renderer/src/assets/images/providers/aiOnly.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -76,6 +76,10 @@
|
||||
list-style: initial;
|
||||
}
|
||||
|
||||
.markdown ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.markdown ul,
|
||||
.markdown ol {
|
||||
padding-left: 1.5em;
|
||||
|
||||
@@ -163,6 +163,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
@keyframes fadeInWithBlur {
|
||||
from { opacity: 0; filter: blur(2px); }
|
||||
to { opacity: 1; filter: blur(0px); }
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const FreeTrialModelTag: FC<Props> = ({ model, showLabel = true }) => {
|
||||
if (model.provider !== 'cherryin') {
|
||||
if (model.provider !== 'cherryai') {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
app: MinAppType
|
||||
@@ -11,31 +10,52 @@ interface Props {
|
||||
}
|
||||
|
||||
const MinAppIcon: FC<Props> = ({ app, size = 48, style, sidebar = false }) => {
|
||||
// First try to find in DEFAULT_MIN_APPS for predefined styling
|
||||
const _app = DEFAULT_MIN_APPS.find((item) => item.id === app.id)
|
||||
|
||||
if (!_app) {
|
||||
return null
|
||||
// If found in DEFAULT_MIN_APPS, use predefined styling
|
||||
if (_app) {
|
||||
return (
|
||||
<img
|
||||
src={_app.logo}
|
||||
className="select-none rounded-2xl"
|
||||
style={{
|
||||
border: _app.bodered ? '0.5px solid var(--color-border)' : 'none',
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
backgroundColor: _app.background,
|
||||
userSelect: 'none',
|
||||
...(sidebar ? {} : app.style),
|
||||
...style
|
||||
}}
|
||||
draggable={false}
|
||||
alt={app.name || 'MinApp Icon'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
src={_app.logo}
|
||||
style={{
|
||||
border: _app.bodered ? '0.5px solid var(--color-border)' : 'none',
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
backgroundColor: _app.background,
|
||||
...(sidebar ? {} : app.style),
|
||||
...style
|
||||
}}
|
||||
/>
|
||||
)
|
||||
// If not found in DEFAULT_MIN_APPS but app has logo, use it (for temporary apps)
|
||||
if (app.logo) {
|
||||
return (
|
||||
<img
|
||||
src={app.logo}
|
||||
className="select-none rounded-2xl"
|
||||
style={{
|
||||
border: 'none',
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
backgroundColor: 'transparent',
|
||||
userSelect: 'none',
|
||||
...(sidebar ? {} : app.style),
|
||||
...style
|
||||
}}
|
||||
draggable={false}
|
||||
alt={app.name || 'MinApp Icon'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const Container = styled.img`
|
||||
border-radius: 16px;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
`
|
||||
|
||||
export default MinAppIcon
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`MinAppIcon > should render correctly with various props 1`] = `
|
||||
.c0 {
|
||||
border-radius: 16px;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
<img
|
||||
class="c0"
|
||||
alt="Test App"
|
||||
class="select-none rounded-2xl"
|
||||
draggable="false"
|
||||
src="/test-logo-1.png"
|
||||
style="border: 0.5px solid var(--color-border); width: 64px; height: 64px; background-color: rgb(240, 240, 240); opacity: 0.8; transform: scale(1.1); margin-top: 10px;"
|
||||
style="border: 0.5px solid var(--color-border); width: 64px; height: 64px; background-color: rgb(240, 240, 240); user-select: none; opacity: 0.8; transform: scale(1.1); margin-top: 10px;"
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Provider } from '@renderer/types'
|
||||
import {
|
||||
oauthWith302AI,
|
||||
oauthWithAihubmix,
|
||||
oauthWithAiOnly,
|
||||
oauthWithPPIO,
|
||||
oauthWithSiliconFlow,
|
||||
oauthWithTokenFlux
|
||||
@@ -46,6 +47,10 @@ const OAuthButton: FC<Props> = ({ provider, onSuccess, ...buttonProps }) => {
|
||||
if (provider.id === '302ai') {
|
||||
oauthWith302AI(handleSuccess)
|
||||
}
|
||||
|
||||
if (provider.id === 'aionly') {
|
||||
oauthWithAiOnly(handleSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
137
src/renderer/src/components/Popups/PrivacyPopup.tsx
Normal file
137
src/renderer/src/components/Popups/PrivacyPopup.tsx
Normal file
@@ -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<Props> = ({ title, showDeclineButton = true, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const [privacyUrl, setPrivacyUrl] = useState<string>('')
|
||||
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 (
|
||||
<Modal
|
||||
title={getTitle()}
|
||||
open={open}
|
||||
onCancel={showDeclineButton ? handleDecline : undefined}
|
||||
afterClose={onClose}
|
||||
transitionName=""
|
||||
maskTransitionName=""
|
||||
centered
|
||||
closable={false}
|
||||
maskClosable={false}
|
||||
styles={{
|
||||
mask: { backgroundColor: 'var(--color-background)' },
|
||||
header: { paddingLeft: 20 },
|
||||
body: { paddingLeft: 20 }
|
||||
}}
|
||||
width={900}
|
||||
footer={[
|
||||
showDeclineButton && (
|
||||
<Button key="decline" onClick={handleDecline}>
|
||||
{i18n.language.startsWith('zh') ? '拒绝' : 'Decline'}
|
||||
</Button>
|
||||
),
|
||||
<Button key="accept" type="primary" onClick={handleAccept}>
|
||||
{i18n.language.startsWith('zh') ? '同意并继续' : 'Accept and Continue'}
|
||||
</Button>
|
||||
].filter(Boolean)}>
|
||||
<WebViewContainer>
|
||||
{privacyUrl && <webview src={privacyUrl} style={{ width: '100%', height: '100%' }} />}
|
||||
</WebViewContainer>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
<PopupContainer
|
||||
{...(props || {})}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -255,7 +255,9 @@ const PopupContainer: React.FC<Props> = ({ source, title, resolve }) => {
|
||||
try {
|
||||
if (isNoteMode) {
|
||||
const note = source.data as NotesTreeNode
|
||||
const content = await window.api.file.read(note.id + '.md')
|
||||
const content = note.externalPath
|
||||
? await window.api.file.readExternal(note.externalPath)
|
||||
: await window.api.file.read(note.id + '.md')
|
||||
logger.debug('Note content:', content)
|
||||
await addNote(content)
|
||||
savedCount = 1
|
||||
|
||||
@@ -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<Props> = ({ 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: (
|
||||
<ModelName>
|
||||
{model.name}
|
||||
{isPinned && <span style={{ color: 'var(--color-text-3)' }}> | {groupName}</span>}
|
||||
{isCherryin && <FreeTrialModelTag model={model} showLabel={false} />}
|
||||
<HStack alignItems="center">
|
||||
{model.name}
|
||||
{isPinned && <span style={{ color: 'var(--color-text-3)' }}> | {groupName}</span>}
|
||||
</HStack>
|
||||
{isCherryAi && <FreeTrialModelTag model={model} showLabel={false} />}
|
||||
</ModelName>
|
||||
),
|
||||
tags: (
|
||||
@@ -177,7 +180,7 @@ const PopupContainer: React.FC<Props> = ({ model, filter: baseFilter, showTagFil
|
||||
key: `provider-${p.id}`,
|
||||
type: 'group',
|
||||
name: getFancyProviderName(p),
|
||||
actions: (
|
||||
actions: p.id !== 'cherryai' && (
|
||||
<Tooltip title={t('navigate.provider_settings')} mouseEnterDelay={0.5} mouseLeaveDelay={0}>
|
||||
<Settings2
|
||||
size={12}
|
||||
@@ -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;
|
||||
|
||||
@@ -18,7 +18,8 @@ export function renderSvgInShadowHost(svgContent: string, hostElement: HTMLEleme
|
||||
// Sanitize the SVG content
|
||||
const sanitizedContent = DOMPurify.sanitize(svgContent, {
|
||||
ADD_TAGS: ['animate', 'foreignObject', 'use'],
|
||||
ADD_ATTR: ['from', 'to']
|
||||
ADD_ATTR: ['from', 'to'],
|
||||
HTML_INTEGRATION_POINTS: { foreignobject: true }
|
||||
})
|
||||
|
||||
const shadowRoot = hostElement.shadowRoot || hostElement.attachShadow({ mode: 'open' })
|
||||
@@ -36,6 +37,7 @@ export function renderSvgInShadowHost(svgContent: string, hostElement: HTMLEleme
|
||||
border-radius: var(--shadow-host-border-radius);
|
||||
padding: 1em;
|
||||
overflow: hidden; /* Prevent scrollbars, as scaling is now handled */
|
||||
white-space: normal;
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
@@ -158,15 +158,22 @@ export const QuickPanelView: React.FC<Props> = ({ 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<Props> = ({ 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)
|
||||
}
|
||||
},
|
||||
@@ -450,7 +457,13 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
||||
|
||||
// 面板可见且未折叠时:拦截所有 Enter 变体;
|
||||
// 纯 Enter 选择项,带修饰键仅拦截不处理
|
||||
if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) {
|
||||
if (e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
// Don't prevent default or stop propagation - let it create a newline
|
||||
setIsMouseOver(false)
|
||||
break
|
||||
}
|
||||
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsMouseOver(false)
|
||||
@@ -533,6 +546,18 @@ export const QuickPanelView: React.FC<Props> = ({ 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(
|
||||
|
||||
@@ -87,6 +87,9 @@ const CommandListPopover = ({
|
||||
return true
|
||||
|
||||
case 'Enter':
|
||||
if (event.shiftKey) {
|
||||
return false
|
||||
}
|
||||
event.preventDefault()
|
||||
if (items[internalSelectedIndex]) {
|
||||
selectItem(internalSelectedIndex)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { commandSuggestion } from '../command'
|
||||
|
||||
describe('commandSuggestion render', () => {
|
||||
it('has render function', () => {
|
||||
expect(commandSuggestion.render).toBeDefined()
|
||||
expect(typeof commandSuggestion.render).toBe('function')
|
||||
})
|
||||
|
||||
it('render function returns object with onKeyDown', () => {
|
||||
const renderResult = commandSuggestion.render?.()
|
||||
expect(renderResult).toBeDefined()
|
||||
expect(renderResult?.onKeyDown).toBeDefined()
|
||||
expect(typeof renderResult?.onKeyDown).toBe('function')
|
||||
})
|
||||
})
|
||||
@@ -628,13 +628,34 @@ export const commandSuggestion: Omit<SuggestionOptions<Command, MentionNodeAttrs
|
||||
},
|
||||
|
||||
onKeyDown: (props) => {
|
||||
// Let CommandListPopover handle events first
|
||||
const popoverHandled = component.ref?.onKeyDown?.(props.event)
|
||||
if (popoverHandled) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle Shift+Enter for newline when popover doesn't handle it
|
||||
if (props.event.key === 'Enter' && props.event.shiftKey) {
|
||||
props.event.preventDefault()
|
||||
// Close the suggestion menu
|
||||
if (cleanup) cleanup()
|
||||
component.destroy()
|
||||
// Use the view from SuggestionKeyDownProps to insert newline
|
||||
const { view } = props
|
||||
const { state, dispatch } = view
|
||||
const { tr } = state
|
||||
tr.insertText('\n')
|
||||
dispatch(tr)
|
||||
return true
|
||||
}
|
||||
|
||||
if (props.event.key === 'Escape') {
|
||||
if (cleanup) cleanup()
|
||||
component.destroy()
|
||||
return true
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props.event)
|
||||
return false
|
||||
},
|
||||
|
||||
onExit: () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { Sortable, useDndReorder } from '@renderer/components/dnd'
|
||||
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
@@ -12,9 +13,10 @@ import tabsService from '@renderer/services/TabsService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import type { Tab } from '@renderer/store/tabs'
|
||||
import { addTab, removeTab, setActiveTab, setTabs } from '@renderer/store/tabs'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { MinAppType, ThemeMode } from '@renderer/types'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { Tooltip } from 'antd'
|
||||
import { LRUCache } from 'lru-cache'
|
||||
import {
|
||||
FileSearch,
|
||||
Folder,
|
||||
@@ -45,14 +47,40 @@ interface TabsContainerProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const getTabIcon = (tabId: string, minapps: any[]): React.ReactNode | undefined => {
|
||||
const logger = loggerService.withContext('TabContainer')
|
||||
|
||||
const getTabIcon = (
|
||||
tabId: string,
|
||||
minapps: MinAppType[],
|
||||
minAppsCache?: LRUCache<string, MinAppType>
|
||||
): React.ReactNode | undefined => {
|
||||
// Check if it's a minapp tab (format: apps:appId)
|
||||
if (tabId.startsWith('apps:')) {
|
||||
const appId = tabId.replace('apps:', '')
|
||||
const app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
|
||||
let app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
|
||||
|
||||
// If not found in permanent apps, search in temporary apps cache
|
||||
// The cache stores apps opened via openSmartMinapp() for top navbar mode
|
||||
// These are temporary MinApps that were opened but not yet saved to user's config
|
||||
// The cache is LRU (Least Recently Used) with max size from settings
|
||||
// Cache validity: Apps in cache are currently active/recently used, not outdated
|
||||
if (!app && minAppsCache) {
|
||||
app = minAppsCache.get(appId)
|
||||
|
||||
// Defensive programming: If app not found in cache but tab exists,
|
||||
// the cache entry may have been evicted due to LRU policy
|
||||
// Log warning for debugging potential sync issues
|
||||
if (!app) {
|
||||
logger.warn(`MinApp ${appId} not found in cache, using fallback icon`)
|
||||
}
|
||||
}
|
||||
|
||||
if (app) {
|
||||
return <MinAppIcon size={14} app={app} />
|
||||
}
|
||||
|
||||
// Fallback: If no app found (cache evicted), show default icon
|
||||
return <LayoutGrid size={14} />
|
||||
}
|
||||
|
||||
switch (tabId) {
|
||||
@@ -94,7 +122,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
||||
const activeTabId = useAppSelector((state) => state.tabs.activeTabId)
|
||||
const isFullscreen = useFullscreen()
|
||||
const { settedTheme, toggleTheme } = useTheme()
|
||||
const { hideMinappPopup } = useMinappPopup()
|
||||
const { hideMinappPopup, minAppsCache } = useMinappPopup()
|
||||
const { minapps } = useMinapps()
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -112,8 +140,23 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
||||
// Check if it's a minapp tab
|
||||
if (tabId.startsWith('apps:')) {
|
||||
const appId = tabId.replace('apps:', '')
|
||||
const app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
|
||||
return app ? app.name : 'MinApp'
|
||||
let app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
|
||||
|
||||
// If not found in permanent apps, search in temporary apps cache
|
||||
// This ensures temporary MinApps display proper titles while being used
|
||||
// The LRU cache automatically manages app lifecycle and prevents memory leaks
|
||||
if (!app && minAppsCache) {
|
||||
app = minAppsCache.get(appId)
|
||||
|
||||
// Defensive programming: If app not found in cache but tab exists,
|
||||
// the cache entry may have been evicted due to LRU policy
|
||||
if (!app) {
|
||||
logger.warn(`MinApp ${appId} not found in cache, using fallback title`)
|
||||
}
|
||||
}
|
||||
|
||||
// Return app name if found, otherwise use fallback with appId
|
||||
return app ? app.name : `MinApp-${appId}`
|
||||
}
|
||||
return getTitleLabel(tabId)
|
||||
}
|
||||
@@ -196,7 +239,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
||||
renderItem={(tab) => (
|
||||
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
|
||||
<TabHeader>
|
||||
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps)}</TabIcon>}
|
||||
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps, minAppsCache)}</TabIcon>}
|
||||
<TabTitle>{getTabTitle(tab.id)}</TabTitle>
|
||||
</TabHeader>
|
||||
{tab.id !== 'home' && (
|
||||
@@ -259,7 +302,7 @@ const TabsBar = styled.div<{ $isFullscreen: boolean }>`
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? 'env(titlebar-area-x)' : '15px')};
|
||||
padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? 'calc(env(titlebar-area-x) + 4px)' : '15px')};
|
||||
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : '0')};
|
||||
height: var(--navbar-height);
|
||||
min-height: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? 'env(titlebar-area-height)' : '')};
|
||||
|
||||
@@ -88,6 +88,7 @@ const NavbarCenterContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 ${isMac ? '20px' : 0};
|
||||
padding-left: 10px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
position: relative;
|
||||
@@ -108,7 +109,8 @@ const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 ${isMac ? '20px' : 0};
|
||||
padding-right: ${isMac ? '20px' : 0};
|
||||
padding-left: 10px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWin ? '140px' : isLinux ? '120px' : '12px')};
|
||||
|
||||
@@ -17,6 +17,7 @@ interface ItemRendererProps<T> {
|
||||
transform?: Transform | null
|
||||
transition?: string | null
|
||||
listeners?: DraggableSyntheticListeners
|
||||
itemStyle?: React.CSSProperties
|
||||
}
|
||||
|
||||
export function ItemRenderer<T>({
|
||||
@@ -30,6 +31,7 @@ export function ItemRenderer<T>({
|
||||
transform,
|
||||
transition,
|
||||
listeners,
|
||||
itemStyle,
|
||||
...props
|
||||
}: ItemRendererProps<T>) {
|
||||
useEffect(() => {
|
||||
@@ -44,7 +46,7 @@ export function ItemRenderer<T>({
|
||||
}
|
||||
}, [dragOverlay])
|
||||
|
||||
const wrapperStyle = {
|
||||
const style = {
|
||||
transition,
|
||||
transform: CSS.Transform.toString(transform ?? null)
|
||||
} as React.CSSProperties
|
||||
@@ -54,7 +56,7 @@ export function ItemRenderer<T>({
|
||||
ref={ref}
|
||||
data-index={index}
|
||||
className={classNames({ dragOverlay: dragOverlay })}
|
||||
style={{ ...wrapperStyle }}>
|
||||
style={{ ...style, ...itemStyle }}>
|
||||
<DraggableItem
|
||||
className={classNames({ dragging: dragging, dragOverlay: dragOverlay, ghost: ghost })}
|
||||
{...listeners}
|
||||
|
||||
@@ -61,6 +61,8 @@ interface SortableProps<T> {
|
||||
className?: string
|
||||
/** Item list style */
|
||||
listStyle?: React.CSSProperties
|
||||
/** Item style */
|
||||
itemStyle?: React.CSSProperties
|
||||
/** Item gap */
|
||||
gap?: number | string
|
||||
/** Restrictions, shortcuts for some modifiers */
|
||||
@@ -87,6 +89,7 @@ function Sortable<T>({
|
||||
showGhost = false,
|
||||
className,
|
||||
listStyle,
|
||||
itemStyle,
|
||||
gap,
|
||||
restrictions,
|
||||
modifiers: customModifiers
|
||||
@@ -195,19 +198,19 @@ function Sortable<T>({
|
||||
renderItem={renderItem}
|
||||
useDragOverlay={useDragOverlay}
|
||||
showGhost={showGhost}
|
||||
itemStyle={itemStyle}
|
||||
/>
|
||||
))}
|
||||
</ListWrapper>
|
||||
</SortableContext>
|
||||
|
||||
{useDragOverlay
|
||||
? createPortal(
|
||||
<DragOverlay adjustScale dropAnimation={dropAnimation}>
|
||||
{activeItem ? <ItemRenderer item={activeItem} renderItem={renderItem} dragOverlay /> : null}
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
)
|
||||
: null}
|
||||
{useDragOverlay &&
|
||||
createPortal(
|
||||
<DragOverlay adjustScale dropAnimation={dropAnimation}>
|
||||
{activeItem && <ItemRenderer item={activeItem} renderItem={renderItem} itemStyle={itemStyle} dragOverlay />}
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
)}
|
||||
</DndContext>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ interface SortableItemProps<T> {
|
||||
renderItem: RenderItemType<T>
|
||||
useDragOverlay?: boolean
|
||||
showGhost?: boolean
|
||||
itemStyle?: React.CSSProperties
|
||||
}
|
||||
|
||||
export function SortableItem<T>({
|
||||
@@ -18,7 +19,8 @@ export function SortableItem<T>({
|
||||
index,
|
||||
renderItem,
|
||||
useDragOverlay = true,
|
||||
showGhost = true
|
||||
showGhost = true,
|
||||
itemStyle
|
||||
}: SortableItemProps<T>) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id
|
||||
@@ -36,6 +38,7 @@ export function SortableItem<T>({
|
||||
transform={transform}
|
||||
transition={transition}
|
||||
listeners={listeners}
|
||||
itemStyle={itemStyle}
|
||||
{...attributes}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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<SystemProviderId | 'defaultModel', Model[]> =
|
||||
// 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<SystemProviderId | 'defaultModel', Model[]> =
|
||||
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'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -93,7 +93,17 @@ export function isSupportedThinkingTokenModel(model?: Model): boolean {
|
||||
// Specifically for DeepSeek V3.1. White list for now
|
||||
if (isDeepSeekHybridInferenceModel(model)) {
|
||||
return (
|
||||
['openrouter', 'dashscope', 'modelscope', 'doubao', 'silicon', 'nvidia', 'ppio'] satisfies SystemProviderId[]
|
||||
[
|
||||
'openrouter',
|
||||
'dashscope',
|
||||
'modelscope',
|
||||
'doubao',
|
||||
'silicon',
|
||||
'nvidia',
|
||||
'ppio',
|
||||
'hunyuan',
|
||||
'tencent-cloud-ti'
|
||||
] satisfies SystemProviderId[]
|
||||
).some((id) => id === model.provider)
|
||||
}
|
||||
|
||||
@@ -225,6 +235,8 @@ export function isSupportedThinkingTokenQwenModel(model?: Model): boolean {
|
||||
'qwen-plus-2025-04-28',
|
||||
'qwen-plus-0714',
|
||||
'qwen-plus-2025-07-14',
|
||||
'qwen-plus-2025-07-28',
|
||||
'qwen-plus-2025-09-11',
|
||||
'qwen-turbo',
|
||||
'qwen-turbo-latest',
|
||||
'qwen-turbo-0428',
|
||||
@@ -379,7 +391,8 @@ export function isReasoningModel(model?: Model): boolean {
|
||||
isDeepSeekHybridInferenceModel(model) ||
|
||||
modelId.includes('magistral') ||
|
||||
modelId.includes('minimax-m1') ||
|
||||
modelId.includes('pangu-pro-moe')
|
||||
modelId.includes('pangu-pro-moe') ||
|
||||
modelId.includes('seed-oss')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
@@ -410,13 +423,14 @@ export const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> =
|
||||
'gemini-.*-pro.*$': { min: 128, max: 32768 },
|
||||
|
||||
// Qwen models
|
||||
// qwen-plus-x 系列自 qwen-plus-2025-07-28 后模型最长思维链变为 81_920, qwen-plus 模型于 2025.9.16 同步变更
|
||||
'qwen3-235b-a22b-thinking-2507$': { min: 0, max: 81_920 },
|
||||
'qwen3-30b-a3b-thinking-2507$': { min: 0, max: 81_920 },
|
||||
'qwen-plus-2025-07-28$': { min: 0, max: 81_920 },
|
||||
'qwen-plus-latest$': { min: 0, max: 81_920 },
|
||||
'qwen-plus-2025-07-14$': { min: 0, max: 38_912 },
|
||||
'qwen-plus-2025-04-28$': { min: 0, max: 38_912 },
|
||||
'qwen3-1\\.7b$': { min: 0, max: 30_720 },
|
||||
'qwen3-0\\.6b$': { min: 0, max: 30_720 },
|
||||
'qwen-plus.*$': { min: 0, max: 38_912 },
|
||||
'qwen-plus.*$': { min: 0, max: 81_920 },
|
||||
'qwen-turbo.*$': { min: 0, max: 38_912 },
|
||||
'qwen-flash.*$': { min: 0, max: 81_920 },
|
||||
'qwen3-(?!max).*$': { min: 1024, max: 38_912 },
|
||||
|
||||
@@ -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,19 +64,30 @@ 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<SystemProviderId, SystemProvider> = {
|
||||
cherryin: {
|
||||
id: 'cherryin',
|
||||
name: 'CherryIN',
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.cherry-ai.com/',
|
||||
models: SYSTEM_MODELS.cherryin,
|
||||
isSystem: true,
|
||||
enabled: true
|
||||
},
|
||||
// cherryin: {
|
||||
// id: 'cherryin',
|
||||
// name: 'CherryIN',
|
||||
// type: 'openai',
|
||||
// apiKey: '',
|
||||
// apiHost: 'https://open.cherryin.ai',
|
||||
// models: [],
|
||||
// isSystem: true,
|
||||
// enabled: true
|
||||
// },
|
||||
silicon: {
|
||||
id: 'silicon',
|
||||
name: 'Silicon',
|
||||
@@ -126,16 +138,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
ppio: {
|
||||
id: 'ppio',
|
||||
name: 'PPIO',
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.ppinfra.com/v3/openai/',
|
||||
models: SYSTEM_MODELS.ppio,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
alayanew: {
|
||||
id: 'alayanew',
|
||||
name: 'AlayaNew',
|
||||
@@ -146,16 +148,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
qiniu: {
|
||||
id: 'qiniu',
|
||||
name: 'Qiniu',
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.qnaigc.com',
|
||||
models: SYSTEM_MODELS.qiniu,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
dmxapi: {
|
||||
id: 'dmxapi',
|
||||
name: 'DMXAPI',
|
||||
@@ -166,6 +158,16 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
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
|
||||
},
|
||||
burncloud: {
|
||||
id: 'burncloud',
|
||||
name: 'BurnCloud',
|
||||
@@ -226,6 +228,26 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
ppio: {
|
||||
id: 'ppio',
|
||||
name: 'PPIO',
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.ppinfra.com/v3/openai/',
|
||||
models: SYSTEM_MODELS.ppio,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
qiniu: {
|
||||
id: 'qiniu',
|
||||
name: 'Qiniu',
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.qnaigc.com',
|
||||
models: SYSTEM_MODELS.qiniu,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
openrouter: {
|
||||
id: 'openrouter',
|
||||
name: 'OpenRouter',
|
||||
@@ -661,7 +683,8 @@ export const PROVIDER_LOGO_MAP: AtLeast<SystemProviderId, string> = {
|
||||
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) {
|
||||
@@ -685,16 +708,17 @@ type ProviderUrls = {
|
||||
}
|
||||
|
||||
export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
||||
cherryin: {
|
||||
api: {
|
||||
url: 'https://api.cherry-ai.com'
|
||||
},
|
||||
websites: {
|
||||
official: 'https://cherry-ai.com',
|
||||
docs: 'https://docs.cherry-ai.com',
|
||||
models: 'https://docs.cherry-ai.com/pre-basic/providers/cherryin'
|
||||
}
|
||||
},
|
||||
// cherryin: {
|
||||
// api: {
|
||||
// url: 'https://open.cherryin.ai'
|
||||
// },
|
||||
// websites: {
|
||||
// official: 'https://open.cherryin.ai',
|
||||
// apiKey: 'https://open.cherryin.ai/console/token',
|
||||
// docs: 'https://open.cherryin.ai',
|
||||
// models: 'https://open.cherryin.ai/pricing'
|
||||
// }
|
||||
// },
|
||||
ph8: {
|
||||
api: {
|
||||
url: 'https://ph8.co'
|
||||
@@ -1255,6 +1279,17 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1340,3 +1375,7 @@ const SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS = ['gemini', 'vertexai'] as con
|
||||
export const isGeminiWebSearchProvider = (provider: Provider) => {
|
||||
return SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS.some((id) => id === provider.id)
|
||||
}
|
||||
|
||||
export const isNewApiProvider = (provider: Provider) => {
|
||||
return ['new-api', 'cherryin'].includes(provider.id)
|
||||
}
|
||||
|
||||
@@ -172,7 +172,10 @@ export function useAssistant(id: string) {
|
||||
(model: Model) => assistant && dispatch(setModel({ assistantId: assistant?.id, model })),
|
||||
[assistant, dispatch]
|
||||
),
|
||||
updateAssistant: useCallback((assistant: Partial<Assistant>) => dispatch(updateAssistant(assistant)), [dispatch]),
|
||||
updateAssistant: useCallback(
|
||||
(update: Partial<Omit<Assistant, 'id'>>) => dispatch(updateAssistant({ id, ...update })),
|
||||
[dispatch, id]
|
||||
),
|
||||
updateAssistantSettings
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
setCurrentDirectory,
|
||||
setEnvironmentVariables,
|
||||
setSelectedCliTool,
|
||||
setSelectedModel
|
||||
setSelectedModel,
|
||||
setSelectedTerminal
|
||||
} from '@renderer/store/codeTools'
|
||||
import { Model } from '@renderer/types'
|
||||
import { codeTools } from '@shared/config/constant'
|
||||
@@ -35,6 +36,14 @@ export const useCodeTools = () => {
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
// 设置选择的终端
|
||||
const setTerminal = useCallback(
|
||||
(terminal: string) => {
|
||||
dispatch(setSelectedTerminal(terminal))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
// 设置环境变量
|
||||
const setEnvVars = useCallback(
|
||||
(envVars: string) => {
|
||||
@@ -105,6 +114,7 @@ export const useCodeTools = () => {
|
||||
// 状态
|
||||
selectedCliTool: codeToolsState.selectedCliTool,
|
||||
selectedModel: selectedModel,
|
||||
selectedTerminal: codeToolsState.selectedTerminal,
|
||||
environmentVariables: environmentVariables,
|
||||
directories: codeToolsState.directories,
|
||||
currentDirectory: codeToolsState.currentDirectory,
|
||||
@@ -113,6 +123,7 @@ export const useCodeTools = () => {
|
||||
// 操作函数
|
||||
setCliTool,
|
||||
setModel,
|
||||
setTerminal,
|
||||
setEnvVars,
|
||||
addDir,
|
||||
removeDir,
|
||||
|
||||
@@ -67,7 +67,8 @@ export const useKnowledge = (baseId: string) => {
|
||||
// 添加笔记
|
||||
const addNote = async (content: string) => {
|
||||
await dispatch(addNoteThunk(baseId, content))
|
||||
checkAllBases()
|
||||
// 确保数据库写入完成后再触发队列检查
|
||||
setTimeout(() => KnowledgeQueue.checkAllBases(), 100)
|
||||
}
|
||||
|
||||
// 添加URL
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings' // 使用设置中的值
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
import TabsService from '@renderer/services/TabsService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
@@ -14,6 +15,8 @@ import { clearWebviewState } from '@renderer/utils/webviewStateManager'
|
||||
import { LRUCache } from 'lru-cache'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { useNavbarPosition } from './useSettings'
|
||||
|
||||
let minAppsCache: LRUCache<string, MinAppType>
|
||||
|
||||
/**
|
||||
@@ -34,6 +37,7 @@ export const useMinappPopup = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { openedKeepAliveMinapps, openedOneOffMinapp, minappShow } = useRuntime()
|
||||
const { maxKeepAliveMinapps } = useSettings() // 使用设置中的值
|
||||
const { isTopNavbar } = useNavbarPosition()
|
||||
|
||||
const createLRUCache = useCallback(() => {
|
||||
return new LRUCache<string, MinAppType>({
|
||||
@@ -165,6 +169,33 @@ export const useMinappPopup = () => {
|
||||
dispatch(setMinappShow(false))
|
||||
}, [dispatch, minappShow, openedOneOffMinapp])
|
||||
|
||||
/** Smart open minapp that adapts to navbar position */
|
||||
const openSmartMinapp = useCallback(
|
||||
(config: MinAppType, keepAlive: boolean = false) => {
|
||||
if (isTopNavbar) {
|
||||
// For top navbar mode, need to add to cache first for temporary apps
|
||||
const cacheApp = minAppsCache.get(config.id)
|
||||
if (!cacheApp) {
|
||||
// Add temporary app to cache so MinAppPage can find it
|
||||
minAppsCache.set(config.id, config)
|
||||
}
|
||||
|
||||
// Set current minapp and show state
|
||||
dispatch(setCurrentMinappId(config.id))
|
||||
dispatch(setMinappShow(true))
|
||||
|
||||
// Then navigate to the app tab using NavigationService
|
||||
if (NavigationService.navigate) {
|
||||
NavigationService.navigate(`/apps/${config.id}`)
|
||||
}
|
||||
} else {
|
||||
// For side navbar, use the traditional popup system
|
||||
openMinapp(config, keepAlive)
|
||||
}
|
||||
},
|
||||
[isTopNavbar, openMinapp, dispatch]
|
||||
)
|
||||
|
||||
return {
|
||||
openMinapp,
|
||||
openMinappKeepAlive,
|
||||
@@ -172,6 +203,7 @@ export const useMinappPopup = () => {
|
||||
closeMinapp,
|
||||
hideMinappPopup,
|
||||
closeAllMinapps,
|
||||
openSmartMinapp,
|
||||
// Expose cache instance for TabsService integration
|
||||
minAppsCache
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<Provider> & { 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 {
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -80,7 +80,8 @@ const providerKeyMap = {
|
||||
yi: 'provider.yi',
|
||||
zhinao: 'provider.zhinao',
|
||||
zhipu: 'provider.zhipu',
|
||||
poe: 'provider.poe'
|
||||
poe: 'provider.poe',
|
||||
aionly: 'provider.aionly'
|
||||
} as const
|
||||
|
||||
/**
|
||||
|
||||
@@ -332,7 +332,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": {
|
||||
@@ -672,6 +673,10 @@
|
||||
"bun_required_message": "Bun environment is required to run CLI tools",
|
||||
"cli_tool": "CLI Tool",
|
||||
"cli_tool_placeholder": "Select the CLI tool to use",
|
||||
"custom_path": "Custom path",
|
||||
"custom_path_error": "Failed to set custom terminal path",
|
||||
"custom_path_required": "Custom path required for this terminal",
|
||||
"custom_path_set": "Custom terminal path set successfully",
|
||||
"description": "Quickly launch multiple code CLI tools to improve development efficiency",
|
||||
"env_vars_help": "Enter custom environment variables (one per line, format: KEY=value)",
|
||||
"environment_variables": "Environment Variables",
|
||||
@@ -690,7 +695,10 @@
|
||||
"model_placeholder": "Select the model to use",
|
||||
"model_required": "Please select a model",
|
||||
"select_folder": "Select Folder",
|
||||
"set_custom_path": "Set custom terminal path",
|
||||
"supported_providers": "Supported Providers",
|
||||
"terminal": "Terminal",
|
||||
"terminal_placeholder": "Select terminal application",
|
||||
"title": "Code Tools",
|
||||
"update_options": "Update Options",
|
||||
"working_directory": "Working Directory"
|
||||
@@ -2010,6 +2018,7 @@
|
||||
"provider": {
|
||||
"302ai": "302.AI",
|
||||
"aihubmix": "AiHubMix",
|
||||
"aionly": "AiOnly",
|
||||
"alayanew": "Alaya NeW",
|
||||
"anthropic": "Anthropic",
|
||||
"aws-bedrock": "AWS Bedrock",
|
||||
|
||||
@@ -332,7 +332,8 @@
|
||||
},
|
||||
"new_topic": "新话题 {{Command}}",
|
||||
"pause": "暂停",
|
||||
"placeholder": "在这里输入消息,按 {{key}} 发送...",
|
||||
"placeholder": "在这里输入消息,按 {{key}} 发送 - @ 选择模型, / 选择工具",
|
||||
"placeholder_without_triggers": "在这里输入消息,按 {{key}} 发送",
|
||||
"send": "发送",
|
||||
"settings": "设置",
|
||||
"thinking": {
|
||||
@@ -672,6 +673,10 @@
|
||||
"bun_required_message": "运行 CLI 工具需要安装 Bun 环境",
|
||||
"cli_tool": "CLI 工具",
|
||||
"cli_tool_placeholder": "选择要使用的 CLI 工具",
|
||||
"custom_path": "自定义路径",
|
||||
"custom_path_error": "设置自定义终端路径失败",
|
||||
"custom_path_required": "此终端需要设置自定义路径",
|
||||
"custom_path_set": "自定义终端路径设置成功",
|
||||
"description": "快速启动多个代码 CLI 工具,提高开发效率",
|
||||
"env_vars_help": "输入自定义环境变量(每行一个,格式:KEY=value)",
|
||||
"environment_variables": "环境变量",
|
||||
@@ -690,7 +695,10 @@
|
||||
"model_placeholder": "选择要使用的模型",
|
||||
"model_required": "请选择模型",
|
||||
"select_folder": "选择文件夹",
|
||||
"set_custom_path": "设置自定义终端路径",
|
||||
"supported_providers": "支持的服务商",
|
||||
"terminal": "终端",
|
||||
"terminal_placeholder": "选择终端应用",
|
||||
"title": "代码工具",
|
||||
"update_options": "更新选项",
|
||||
"working_directory": "工作目录"
|
||||
@@ -2010,6 +2018,7 @@
|
||||
"provider": {
|
||||
"302ai": "302.AI",
|
||||
"aihubmix": "AiHubMix",
|
||||
"aionly": "唯一AI (AiOnly)",
|
||||
"alayanew": "Alaya NeW",
|
||||
"anthropic": "Anthropic",
|
||||
"aws-bedrock": "AWS Bedrock",
|
||||
|
||||
@@ -332,7 +332,8 @@
|
||||
},
|
||||
"new_topic": "新話題 {{Command}}",
|
||||
"pause": "暫停",
|
||||
"placeholder": "在此輸入您的訊息,按 {{key}} 傳送...",
|
||||
"placeholder": "在此輸入您的訊息,按 {{key}} 傳送 - @ 選擇模型,/ 包含工具",
|
||||
"placeholder_without_triggers": "在此輸入您的訊息,按 {{key}} 傳送",
|
||||
"send": "傳送",
|
||||
"settings": "設定",
|
||||
"thinking": {
|
||||
@@ -672,6 +673,10 @@
|
||||
"bun_required_message": "運行 CLI 工具需要安裝 Bun 環境",
|
||||
"cli_tool": "CLI 工具",
|
||||
"cli_tool_placeholder": "選擇要使用的 CLI 工具",
|
||||
"custom_path": "自訂路徑",
|
||||
"custom_path_error": "設定自訂終端機路徑失敗",
|
||||
"custom_path_required": "此終端機需要設定自訂路徑",
|
||||
"custom_path_set": "自訂終端機路徑設定成功",
|
||||
"description": "快速啟動多個程式碼 CLI 工具,提高開發效率",
|
||||
"env_vars_help": "輸入自定義環境變數(每行一個,格式:KEY=value)",
|
||||
"environment_variables": "環境變數",
|
||||
@@ -690,7 +695,10 @@
|
||||
"model_placeholder": "選擇要使用的模型",
|
||||
"model_required": "請選擇模型",
|
||||
"select_folder": "選擇資料夾",
|
||||
"set_custom_path": "設定自訂終端機路徑",
|
||||
"supported_providers": "支援的供應商",
|
||||
"terminal": "終端機",
|
||||
"terminal_placeholder": "選擇終端機應用程式",
|
||||
"title": "程式碼工具",
|
||||
"update_options": "更新選項",
|
||||
"working_directory": "工作目錄"
|
||||
@@ -2010,6 +2018,7 @@
|
||||
"provider": {
|
||||
"302ai": "302.AI",
|
||||
"aihubmix": "AiHubMix",
|
||||
"aionly": "唯一AI (AiOnly)",
|
||||
"alayanew": "Alaya NeW",
|
||||
"anthropic": "Anthropic",
|
||||
"aws-bedrock": "AWS Bedrock",
|
||||
|
||||
@@ -672,6 +672,10 @@
|
||||
"bun_required_message": "Για τη λειτουργία του εργαλείου CLI πρέπει να εγκαταστήσετε το περιβάλλον Bun",
|
||||
"cli_tool": "Εργαλείο CLI",
|
||||
"cli_tool_placeholder": "Επιλέξτε το CLI εργαλείο που θέλετε να χρησιμοποιήσετε",
|
||||
"custom_path": "Προσαρμοσμένη διαδρομή",
|
||||
"custom_path_error": "Η ρύθμιση της προσαρμοσμένης διαδρομής τερματικού απέτυχε",
|
||||
"custom_path_required": "Αυτό το τερματικό απαιτεί τη ρύθμιση προσαρμοσμένης διαδρομής",
|
||||
"custom_path_set": "Η προσαρμοσμένη διαδρομή τερματικού ορίστηκε με επιτυχία",
|
||||
"description": "Εκκίνηση γρήγορα πολλών εργαλείων CLI κώδικα, για αύξηση της αποδοτικότητας ανάπτυξης",
|
||||
"env_vars_help": "Εισαγάγετε προσαρμοσμένες μεταβλητές περιβάλλοντος (μία ανά γραμμή, με τη μορφή: KEY=value)",
|
||||
"environment_variables": "Μεταβλητές περιβάλλοντος",
|
||||
@@ -690,7 +694,10 @@
|
||||
"model_placeholder": "Επιλέξτε το μοντέλο που θα χρησιμοποιήσετε",
|
||||
"model_required": "Επιλέξτε μοντέλο",
|
||||
"select_folder": "Επιλογή φακέλου",
|
||||
"set_custom_path": "Ρύθμιση προσαρμοσμένης διαδρομής τερματικού",
|
||||
"supported_providers": "υποστηριζόμενοι πάροχοι",
|
||||
"terminal": "τερματικό",
|
||||
"terminal_placeholder": "Επιλέξτε εφαρμογή τερματικού",
|
||||
"title": "Εργαλεία κώδικα",
|
||||
"update_options": "Ενημέρωση επιλογών",
|
||||
"working_directory": "κατάλογος εργασίας"
|
||||
@@ -2010,6 +2017,7 @@
|
||||
"provider": {
|
||||
"302ai": "302.AI",
|
||||
"aihubmix": "AiHubMix",
|
||||
"aionly": "AiOnly",
|
||||
"alayanew": "Alaya NeW",
|
||||
"anthropic": "Anthropic",
|
||||
"aws-bedrock": "AWS Bedrock",
|
||||
|
||||
@@ -672,6 +672,10 @@
|
||||
"bun_required_message": "Se requiere instalar el entorno Bun para ejecutar la herramienta de línea de comandos",
|
||||
"cli_tool": "Herramienta de línea de comandos",
|
||||
"cli_tool_placeholder": "Seleccione la herramienta de línea de comandos que desea utilizar",
|
||||
"custom_path": "Ruta personalizada",
|
||||
"custom_path_error": "Error al establecer la ruta de terminal personalizada",
|
||||
"custom_path_required": "此终端需要设置自定义路径",
|
||||
"custom_path_set": "Configuración de ruta de terminal personalizada exitosa",
|
||||
"description": "Inicia rápidamente múltiples herramientas de línea de comandos para código, aumentando la eficiencia del desarrollo",
|
||||
"env_vars_help": "Introduzca variables de entorno personalizadas (una por línea, formato: CLAVE=valor)",
|
||||
"environment_variables": "Variables de entorno",
|
||||
@@ -690,7 +694,10 @@
|
||||
"model_placeholder": "Seleccionar el modelo que se va a utilizar",
|
||||
"model_required": "Seleccione el modelo",
|
||||
"select_folder": "Seleccionar carpeta",
|
||||
"set_custom_path": "Establecer ruta de terminal personalizada",
|
||||
"supported_providers": "Proveedores de servicios compatibles",
|
||||
"terminal": "terminal",
|
||||
"terminal_placeholder": "Seleccionar aplicación de terminal",
|
||||
"title": "Herramientas de código",
|
||||
"update_options": "Opciones de actualización",
|
||||
"working_directory": "directorio de trabajo"
|
||||
@@ -2010,6 +2017,7 @@
|
||||
"provider": {
|
||||
"302ai": "302.AI",
|
||||
"aihubmix": "AiHubMix",
|
||||
"aionly": "AiOnly",
|
||||
"alayanew": "Alaya NeW",
|
||||
"anthropic": "Antropológico",
|
||||
"aws-bedrock": "AWS Bedrock",
|
||||
|
||||
@@ -672,6 +672,10 @@
|
||||
"bun_required_message": "L'exécution de l'outil en ligne de commande nécessite l'installation de l'environnement Bun",
|
||||
"cli_tool": "Outil CLI",
|
||||
"cli_tool_placeholder": "Sélectionnez l'outil CLI à utiliser",
|
||||
"custom_path": "Chemin personnalisé",
|
||||
"custom_path_error": "Échec de la définition du chemin de terminal personnalisé",
|
||||
"custom_path_required": "Ce terminal nécessite la configuration d’un chemin personnalisé",
|
||||
"custom_path_set": "Paramétrage personnalisé du chemin du terminal réussi",
|
||||
"description": "Lancer rapidement plusieurs outils CLI de code pour améliorer l'efficacité du développement",
|
||||
"env_vars_help": "Saisissez les variables d'environnement personnalisées (une par ligne, format : KEY=value)",
|
||||
"environment_variables": "variables d'environnement",
|
||||
@@ -690,7 +694,10 @@
|
||||
"model_placeholder": "Sélectionnez le modèle à utiliser",
|
||||
"model_required": "Veuillez sélectionner le modèle",
|
||||
"select_folder": "Sélectionner le dossier",
|
||||
"set_custom_path": "Définir un chemin de terminal personnalisé",
|
||||
"supported_providers": "fournisseurs pris en charge",
|
||||
"terminal": "Terminal",
|
||||
"terminal_placeholder": "Choisir une application de terminal",
|
||||
"title": "Outils de code",
|
||||
"update_options": "Options de mise à jour",
|
||||
"working_directory": "répertoire de travail"
|
||||
@@ -2010,6 +2017,7 @@
|
||||
"provider": {
|
||||
"302ai": "302.AI",
|
||||
"aihubmix": "AiHubMix",
|
||||
"aionly": "AiOnly",
|
||||
"alayanew": "Alaya NeW",
|
||||
"anthropic": "Anthropic",
|
||||
"aws-bedrock": "AWS Bedrock",
|
||||
|
||||
@@ -672,6 +672,10 @@
|
||||
"bun_required_message": "CLI ツールを実行するには Bun 環境が必要です",
|
||||
"cli_tool": "CLI ツール",
|
||||
"cli_tool_placeholder": "使用する CLI ツールを選択してください",
|
||||
"custom_path": "カスタムパス",
|
||||
"custom_path_error": "カスタムターミナルパスの設定に失敗しました",
|
||||
"custom_path_required": "この端末にはカスタムパスを設定する必要があります",
|
||||
"custom_path_set": "カスタムターミナルパスの設定が成功しました",
|
||||
"description": "開発効率を向上させるために、複数のコード CLI ツールを迅速に起動します",
|
||||
"env_vars_help": "環境変数を設定して、CLI ツールの実行時に使用します。各変数は 1 行ごとに設定してください。",
|
||||
"environment_variables": "環境変数",
|
||||
@@ -690,7 +694,10 @@
|
||||
"model_placeholder": "使用するモデルを選択してください",
|
||||
"model_required": "モデルを選択してください",
|
||||
"select_folder": "フォルダを選択",
|
||||
"set_custom_path": "カスタムターミナルパスを設定",
|
||||
"supported_providers": "サポートされているプロバイダー",
|
||||
"terminal": "端末",
|
||||
"terminal_placeholder": "ターミナルアプリを選択",
|
||||
"title": "コードツール",
|
||||
"update_options": "更新オプション",
|
||||
"working_directory": "作業ディレクトリ"
|
||||
@@ -2010,6 +2017,7 @@
|
||||
"provider": {
|
||||
"302ai": "302.AI",
|
||||
"aihubmix": "AiHubMix",
|
||||
"aionly": "AiOnly",
|
||||
"alayanew": "Alaya NeW",
|
||||
"anthropic": "Anthropic",
|
||||
"aws-bedrock": "AWS Bedrock",
|
||||
|
||||
@@ -672,6 +672,10 @@
|
||||
"bun_required_message": "Executar a ferramenta CLI requer a instalação do ambiente Bun",
|
||||
"cli_tool": "Ferramenta de linha de comando",
|
||||
"cli_tool_placeholder": "Selecione a ferramenta de linha de comando a ser utilizada",
|
||||
"custom_path": "Caminho personalizado",
|
||||
"custom_path_error": "Falha ao definir caminho de terminal personalizado",
|
||||
"custom_path_required": "Este terminal requer a definição de um caminho personalizado",
|
||||
"custom_path_set": "Configuração personalizada do caminho do terminal bem-sucedida",
|
||||
"description": "Inicie rapidamente várias ferramentas de linha de comando de código, aumentando a eficiência do desenvolvimento",
|
||||
"env_vars_help": "Insira variáveis de ambiente personalizadas (uma por linha, formato: CHAVE=valor)",
|
||||
"environment_variables": "variáveis de ambiente",
|
||||
@@ -690,7 +694,10 @@
|
||||
"model_placeholder": "Selecione o modelo a ser utilizado",
|
||||
"model_required": "Selecione o modelo",
|
||||
"select_folder": "Selecionar pasta",
|
||||
"set_custom_path": "Definir caminho personalizado do terminal",
|
||||
"supported_providers": "Provedores de serviço suportados",
|
||||
"terminal": "terminal",
|
||||
"terminal_placeholder": "Selecionar aplicativo de terminal",
|
||||
"title": "Ferramenta de código",
|
||||
"update_options": "Opções de atualização",
|
||||
"working_directory": "diretório de trabalho"
|
||||
@@ -2010,6 +2017,7 @@
|
||||
"provider": {
|
||||
"302ai": "302.AI",
|
||||
"aihubmix": "AiHubMix",
|
||||
"aionly": "AiOnly",
|
||||
"alayanew": "Alaya NeW",
|
||||
"anthropic": "Antropológico",
|
||||
"aws-bedrock": "AWS Bedrock",
|
||||
|
||||
@@ -672,6 +672,10 @@
|
||||
"bun_required_message": "Запуск CLI-инструментов требует установки среды Bun",
|
||||
"cli_tool": "Инструмент",
|
||||
"cli_tool_placeholder": "Выберите CLI-инструмент для использования",
|
||||
"custom_path": "Пользовательский путь",
|
||||
"custom_path_error": "Не удалось задать пользовательский путь терминала",
|
||||
"custom_path_required": "Этот терминал требует установки пользовательского пути",
|
||||
"custom_path_set": "Пользовательский путь терминала успешно установлен",
|
||||
"description": "Быстро запускает несколько CLI-инструментов для кода, повышая эффективность разработки",
|
||||
"env_vars_help": "Установите переменные окружения для использования при запуске CLI-инструментов. Каждая переменная должна быть на отдельной строке в формате KEY=value",
|
||||
"environment_variables": "Переменные окружения",
|
||||
@@ -690,7 +694,10 @@
|
||||
"model_placeholder": "Выберите модель для использования",
|
||||
"model_required": "Пожалуйста, выберите модель",
|
||||
"select_folder": "Выберите папку",
|
||||
"set_custom_path": "Настройка пользовательского пути терминала",
|
||||
"supported_providers": "Поддерживаемые поставщики",
|
||||
"terminal": "терминал",
|
||||
"terminal_placeholder": "Выбор приложения терминала",
|
||||
"title": "Инструменты кода",
|
||||
"update_options": "Параметры обновления",
|
||||
"working_directory": "Рабочая директория"
|
||||
@@ -2010,6 +2017,7 @@
|
||||
"provider": {
|
||||
"302ai": "302.AI",
|
||||
"aihubmix": "AiHubMix",
|
||||
"aionly": "AiOnly",
|
||||
"alayanew": "Alaya NeW",
|
||||
"anthropic": "Anthropic",
|
||||
"aws-bedrock": "AWS Bedrock",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import ModelSelector from '@renderer/components/ModelSelector'
|
||||
import { isMac, isWin } from '@renderer/config/constant'
|
||||
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
|
||||
import { getProviderLogo } from '@renderer/config/providers'
|
||||
import { useCodeTools } from '@renderer/hooks/useCodeTools'
|
||||
@@ -12,10 +13,10 @@ import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setIsBunInstalled } from '@renderer/store/mcp'
|
||||
import { Model } from '@renderer/types'
|
||||
import { codeTools } from '@shared/config/constant'
|
||||
import { Alert, Avatar, Button, Checkbox, Input, Popover, Select, Space } from 'antd'
|
||||
import { ArrowUpRight, Download, HelpCircle, Terminal, X } from 'lucide-react'
|
||||
import { EndpointType, Model } from '@renderer/types'
|
||||
import { codeTools, terminalApps, TerminalConfig } from '@shared/config/constant'
|
||||
import { Alert, Avatar, Button, Checkbox, Input, Popover, Select, Space, Tooltip } from 'antd'
|
||||
import { ArrowUpRight, Download, FolderOpen, HelpCircle, Terminal, X } from 'lucide-react'
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
@@ -42,12 +43,14 @@ const CodeToolsPage: FC = () => {
|
||||
const {
|
||||
selectedCliTool,
|
||||
selectedModel,
|
||||
selectedTerminal,
|
||||
environmentVariables,
|
||||
directories,
|
||||
currentDirectory,
|
||||
canLaunch,
|
||||
setCliTool,
|
||||
setModel,
|
||||
setTerminal,
|
||||
setEnvVars,
|
||||
setCurrentDir,
|
||||
removeDir,
|
||||
@@ -58,24 +61,52 @@ const CodeToolsPage: FC = () => {
|
||||
const [isLaunching, setIsLaunching] = useState(false)
|
||||
const [isInstallingBun, setIsInstallingBun] = useState(false)
|
||||
const [autoUpdateToLatest, setAutoUpdateToLatest] = useState(false)
|
||||
const [availableTerminals, setAvailableTerminals] = useState<TerminalConfig[]>([])
|
||||
const [isLoadingTerminals, setIsLoadingTerminals] = useState(false)
|
||||
const [terminalCustomPaths, setTerminalCustomPaths] = useState<Record<string, string>>({})
|
||||
|
||||
const modelPredicate = useCallback(
|
||||
(m: Model) => {
|
||||
if (isEmbeddingModel(m) || isRerankModel(m) || isTextToImageModel(m)) {
|
||||
return false
|
||||
}
|
||||
if (m.provider === 'cherryin') {
|
||||
|
||||
if (m.provider === 'cherryai') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (selectedCliTool === codeTools.claudeCode) {
|
||||
if (m.supported_endpoint_types) {
|
||||
return m.supported_endpoint_types.includes('anthropic')
|
||||
}
|
||||
return m.id.includes('claude') || CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS.includes(m.provider)
|
||||
}
|
||||
|
||||
if (selectedCliTool === codeTools.geminiCli) {
|
||||
if (m.supported_endpoint_types) {
|
||||
return m.supported_endpoint_types.includes('gemini')
|
||||
}
|
||||
return m.id.includes('gemini')
|
||||
}
|
||||
|
||||
if (selectedCliTool === codeTools.openaiCodex) {
|
||||
if (m.supported_endpoint_types) {
|
||||
return ['openai', 'openai-response'].some((type) =>
|
||||
m.supported_endpoint_types?.includes(type as EndpointType)
|
||||
)
|
||||
}
|
||||
return m.id.includes('openai') || OPENAI_CODEX_SUPPORTED_PROVIDERS.includes(m.provider)
|
||||
}
|
||||
|
||||
if (selectedCliTool === codeTools.qwenCode || selectedCliTool === codeTools.iFlowCli) {
|
||||
if (m.supported_endpoint_types) {
|
||||
return ['openai', 'openai-response'].some((type) =>
|
||||
m.supported_endpoint_types?.includes(type as EndpointType)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
[selectedCliTool]
|
||||
@@ -119,6 +150,26 @@ const CodeToolsPage: FC = () => {
|
||||
}
|
||||
}, [dispatch])
|
||||
|
||||
// 获取可用终端
|
||||
const loadAvailableTerminals = useCallback(async () => {
|
||||
if (!isMac && !isWin) return // 仅 macOS 和 Windows 支持
|
||||
|
||||
try {
|
||||
setIsLoadingTerminals(true)
|
||||
const terminals = await window.api.codeTools.getAvailableTerminals()
|
||||
setAvailableTerminals(terminals)
|
||||
logger.info(
|
||||
`Found ${terminals.length} available terminals:`,
|
||||
terminals.map((t) => t.name)
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error('Failed to load available terminals:', error as Error)
|
||||
setAvailableTerminals([])
|
||||
} finally {
|
||||
setIsLoadingTerminals(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 安装 bun
|
||||
const handleInstallBun = async () => {
|
||||
try {
|
||||
@@ -179,11 +230,37 @@ const CodeToolsPage: FC = () => {
|
||||
// 执行启动操作
|
||||
const executeLaunch = async (env: Record<string, string>) => {
|
||||
window.api.codeTools.run(selectedCliTool, selectedModel?.id!, currentDirectory, env, {
|
||||
autoUpdateToLatest
|
||||
autoUpdateToLatest,
|
||||
terminal: selectedTerminal
|
||||
})
|
||||
window.toast.success(t('code.launch.success'))
|
||||
}
|
||||
|
||||
// 设置终端自定义路径
|
||||
const handleSetCustomPath = async (terminalId: string) => {
|
||||
try {
|
||||
const result = await window.api.file.select({
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{ name: 'Executable', extensions: ['exe'] },
|
||||
{ name: 'All Files', extensions: ['*'] }
|
||||
]
|
||||
})
|
||||
|
||||
if (result && result.length > 0) {
|
||||
const path = result[0].path
|
||||
await window.api.codeTools.setCustomTerminalPath(terminalId, path)
|
||||
setTerminalCustomPaths((prev) => ({ ...prev, [terminalId]: path }))
|
||||
window.toast.success(t('code.custom_path_set'))
|
||||
// Reload terminals to reflect changes
|
||||
loadAvailableTerminals()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to set custom terminal path:', error as Error)
|
||||
window.toast.error(t('code.custom_path_error'))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理启动
|
||||
const handleLaunch = async () => {
|
||||
const validation = validateLaunch()
|
||||
@@ -216,6 +293,11 @@ const CodeToolsPage: FC = () => {
|
||||
checkBunInstallation()
|
||||
}, [checkBunInstallation])
|
||||
|
||||
// 页面加载时获取可用终端
|
||||
useEffect(() => {
|
||||
loadAvailableTerminals()
|
||||
}, [loadAvailableTerminals])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
@@ -350,6 +432,47 @@ const CodeToolsPage: FC = () => {
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-3)', marginTop: 4 }}>{t('code.env_vars_help')}</div>
|
||||
</SettingsItem>
|
||||
|
||||
{/* 终端选择 (macOS 和 Windows) */}
|
||||
{(isMac || isWin) && availableTerminals.length > 0 && (
|
||||
<SettingsItem>
|
||||
<div className="settings-label">{t('code.terminal')}</div>
|
||||
<Space.Compact style={{ width: '100%', display: 'flex' }}>
|
||||
<Select
|
||||
style={{ flex: 1 }}
|
||||
placeholder={t('code.terminal_placeholder')}
|
||||
value={selectedTerminal}
|
||||
onChange={setTerminal}
|
||||
loading={isLoadingTerminals}
|
||||
options={availableTerminals.map((terminal) => ({
|
||||
value: terminal.id,
|
||||
label: terminal.name
|
||||
}))}
|
||||
/>
|
||||
{/* Show custom path button for Windows terminals except cmd/powershell */}
|
||||
{isWin &&
|
||||
selectedTerminal &&
|
||||
selectedTerminal !== terminalApps.cmd &&
|
||||
selectedTerminal !== terminalApps.powershell &&
|
||||
selectedTerminal !== terminalApps.windowsTerminal && (
|
||||
<Tooltip title={terminalCustomPaths[selectedTerminal] || t('code.set_custom_path')}>
|
||||
<Button icon={<FolderOpen size={16} />} onClick={() => handleSetCustomPath(selectedTerminal)} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space.Compact>
|
||||
{isWin &&
|
||||
selectedTerminal &&
|
||||
selectedTerminal !== terminalApps.cmd &&
|
||||
selectedTerminal !== terminalApps.powershell &&
|
||||
selectedTerminal !== terminalApps.windowsTerminal && (
|
||||
<div style={{ fontSize: 12, color: 'var(--color-text-3)', marginTop: 4 }}>
|
||||
{terminalCustomPaths[selectedTerminal]
|
||||
? `${t('code.custom_path')}: ${terminalCustomPaths[selectedTerminal]}`
|
||||
: t('code.custom_path_required')}
|
||||
</div>
|
||||
)}
|
||||
</SettingsItem>
|
||||
)}
|
||||
|
||||
<SettingsItem>
|
||||
<div className="settings-label">{t('code.update_options')}</div>
|
||||
<Checkbox checked={autoUpdateToLatest} onChange={(e) => setAutoUpdateToLatest(e.target.checked)}>
|
||||
|
||||
@@ -23,10 +23,16 @@ export const CLI_TOOLS = [
|
||||
{ value: codeTools.iFlowCli, label: 'iFlow CLI' }
|
||||
]
|
||||
|
||||
export const GEMINI_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api']
|
||||
export const GEMINI_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api', 'cherryin']
|
||||
export const CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS = ['deepseek', 'moonshot', 'zhipu', 'dashscope', 'modelscope']
|
||||
export const CLAUDE_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api', ...CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS]
|
||||
export const OPENAI_CODEX_SUPPORTED_PROVIDERS = ['openai', 'openrouter', 'aihubmix', 'new-api']
|
||||
export const CLAUDE_SUPPORTED_PROVIDERS = [
|
||||
'aihubmix',
|
||||
'dmxapi',
|
||||
'new-api',
|
||||
'cherryin',
|
||||
...CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS
|
||||
]
|
||||
export const OPENAI_CODEX_SUPPORTED_PROVIDERS = ['openai', 'openrouter', 'aihubmix', 'new-api', 'cherryin']
|
||||
|
||||
// Provider 过滤映射
|
||||
export const CLI_TOOL_PROVIDER_MAP: Record<string, (providers: Provider[]) => Provider[]> = {
|
||||
|
||||
@@ -162,6 +162,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
const [tokenCount, setTokenCount] = useState(0)
|
||||
|
||||
const inputbarToolsRef = useRef<InputbarToolsRef>(null)
|
||||
const prevTextRef = useRef(text)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const debouncedEstimate = useCallback(
|
||||
@@ -178,8 +179,21 @@ const Inputbar: FC<Props> = ({ 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
|
||||
@@ -377,7 +391,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === 'Backspace' && text.trim() === '' && files.length > 0) {
|
||||
if (event.key === 'Backspace' && text.length === 0 && files.length > 0) {
|
||||
setFiles((prev) => prev.slice(0, -1))
|
||||
return event.preventDefault()
|
||||
}
|
||||
@@ -441,43 +455,91 @@ const Inputbar: FC<Props> = ({ 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<Props> = ({ 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}
|
||||
|
||||
@@ -89,7 +89,7 @@ const MentionModelsButton: FC<Props> = ({
|
||||
// 兜底:使用打开时的 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<Props> = ({
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -12,6 +12,7 @@ import { removeSvgEmptyLines } from '@renderer/utils/formats'
|
||||
import { processLatexBrackets } from '@renderer/utils/markdown'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { type FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
@@ -64,6 +65,8 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
||||
initialText: block.content
|
||||
})
|
||||
|
||||
const isStreaming = block.status === 'streaming'
|
||||
|
||||
useEffect(() => {
|
||||
const newContent = block.content || ''
|
||||
const oldContent = prevContentRef.current || ''
|
||||
@@ -85,9 +88,8 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
||||
prevBlockIdRef.current = block.id
|
||||
|
||||
// 更新 stream 状态
|
||||
const isStreaming = block.status === 'streaming'
|
||||
setIsStreamDone(!isStreaming)
|
||||
}, [block.content, block.id, block.status, addChunk, reset])
|
||||
}, [block.content, block.id, block.status, addChunk, reset, isStreaming])
|
||||
|
||||
const remarkPlugins = useMemo(() => {
|
||||
const plugins = [
|
||||
@@ -130,14 +132,16 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
||||
table: (props: any) => <Table {...props} blockId={block.id} />,
|
||||
img: (props: any) => <ImageViewer style={{ maxWidth: 500, maxHeight: 500 }} {...props} />,
|
||||
pre: (props: any) => <pre style={{ overflow: 'visible' }} {...props} />,
|
||||
p: (props) => {
|
||||
p: SmoothFade((props) => {
|
||||
const hasImage = props?.node?.children?.some((child: any) => child.tagName === 'img')
|
||||
if (hasImage) return <div {...props} />
|
||||
return <p {...props} />
|
||||
},
|
||||
svg: MarkdownSvgRenderer
|
||||
}, isStreaming),
|
||||
svg: MarkdownSvgRenderer,
|
||||
li: SmoothFade((props) => <li {...props} />, isStreaming),
|
||||
span: SmoothFade((props) => <span {...props} />, isStreaming)
|
||||
} as Partial<Components>
|
||||
}, [block.id])
|
||||
}, [block.id, isStreaming])
|
||||
|
||||
if (/<style\b[^>]*>/i.test(messageContent)) {
|
||||
components.style = MarkdownShadowDOMRenderer as any
|
||||
@@ -168,3 +172,23 @@ const Markdown: FC<Props> = ({ block, postProcess }) => {
|
||||
}
|
||||
|
||||
export default memo(Markdown)
|
||||
|
||||
const SmoothFade = (Comp: React.ElementType, isStreaming: boolean) => {
|
||||
const handleAnimationEnd = (e: React.AnimationEvent) => {
|
||||
// 动画结束后移除类名
|
||||
if (e.animationName === 'fadeInWithBlur') {
|
||||
e.currentTarget.classList.remove('animate-[fadeInWithBlur_500ms_ease-out_forwards]')
|
||||
e.currentTarget.classList.remove('opacity-0')
|
||||
}
|
||||
}
|
||||
return ({ children, ...rest }) => {
|
||||
return (
|
||||
<Comp
|
||||
{...rest}
|
||||
className={isStreaming ? 'animate-[fadeInWithBlur_500ms_ease-out_forwards] opacity-0' : ''}
|
||||
onAnimationEnd={handleAnimationEnd}>
|
||||
{children}
|
||||
</Comp>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
import { LoadingIcon } from '@renderer/components/Icons'
|
||||
import { MessageBlockStatus, MessageBlockType, type PlaceholderMessageBlock } from '@renderer/types/newMessage'
|
||||
import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
|
||||
import { Loader } from '@renderer/ui/loader'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface PlaceholderBlockProps {
|
||||
block: PlaceholderMessageBlock
|
||||
status: MessageBlockStatus
|
||||
type: MessageBlockType
|
||||
}
|
||||
const PlaceholderBlock: React.FC<PlaceholderBlockProps> = ({ block }) => {
|
||||
if (block.status === MessageBlockStatus.PROCESSING && block.type === MessageBlockType.UNKNOWN) {
|
||||
const PlaceholderBlock: React.FC<PlaceholderBlockProps> = ({ status, type }) => {
|
||||
if (status === MessageBlockStatus.PROCESSING && type === MessageBlockType.UNKNOWN) {
|
||||
return (
|
||||
<MessageContentLoading>
|
||||
<LoadingIcon />
|
||||
</MessageContentLoading>
|
||||
<div className="-mt-2">
|
||||
<Loader variant="terminal" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
const MessageContentLoading = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
margin-top: -5px;
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
export default React.memo(PlaceholderBlock)
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { RootState } from '@renderer/store'
|
||||
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||
import type { ImageMessageBlock, Message, MessageBlock } from '@renderer/types/newMessage'
|
||||
import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
|
||||
import { isMainTextBlock, isVideoBlock } from '@renderer/utils/messageUtils/is'
|
||||
import { isMainTextBlock, isMessageProcessing, isVideoBlock } from '@renderer/utils/messageUtils/is'
|
||||
import { AnimatePresence, motion, type Variants } from 'motion/react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
@@ -107,6 +107,9 @@ const MessageBlockRenderer: React.FC<Props> = ({ blocks, message }) => {
|
||||
const renderedBlocks = blocks.map((blockId) => blockEntities[blockId]).filter(Boolean)
|
||||
const groupedBlocks = useMemo(() => groupSimilarBlocks(renderedBlocks), [renderedBlocks])
|
||||
|
||||
// Check if message is still processing
|
||||
const isProcessing = isMessageProcessing(message)
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="sync">
|
||||
{groupedBlocks.map((block) => {
|
||||
@@ -151,9 +154,6 @@ const MessageBlockRenderer: React.FC<Props> = ({ blocks, message }) => {
|
||||
|
||||
switch (block.type) {
|
||||
case MessageBlockType.UNKNOWN:
|
||||
if (block.status === MessageBlockStatus.PROCESSING) {
|
||||
blockComponent = <PlaceholderBlock key={block.id} block={block} />
|
||||
}
|
||||
break
|
||||
case MessageBlockType.MAIN_TEXT:
|
||||
case MessageBlockType.CODE: {
|
||||
@@ -213,6 +213,11 @@ const MessageBlockRenderer: React.FC<Props> = ({ blocks, message }) => {
|
||||
</AnimatedBlockWrapper>
|
||||
)
|
||||
})}
|
||||
{isProcessing && (
|
||||
<AnimatedBlockWrapper key="message-loading-placeholder" enableAnimation={true}>
|
||||
<PlaceholderBlock type={MessageBlockType.UNKNOWN} status={MessageBlockStatus.PROCESSING} />
|
||||
</AnimatedBlockWrapper>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { isLinux, isWin } from '@renderer/config/constant'
|
||||
import { isLinux, isMac, isWin } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
@@ -86,7 +86,14 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{!showAssistants && (
|
||||
<NavbarLeft style={{ justifyContent: 'flex-start', borderRight: 'none', padding: '0 10px', minWidth: 'auto' }}>
|
||||
<NavbarLeft
|
||||
style={{
|
||||
justifyContent: 'flex-start',
|
||||
borderRight: 'none',
|
||||
paddingLeft: 0,
|
||||
paddingRight: 10,
|
||||
minWidth: 'auto'
|
||||
}}>
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon onClick={() => toggleShowAssistants()}>
|
||||
<PanelRightClose size={18} />
|
||||
@@ -106,7 +113,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
</AnimatePresence>
|
||||
</NavbarLeft>
|
||||
)}
|
||||
<HStack alignItems="center" gap={6}>
|
||||
<HStack alignItems="center" gap={6} ml={!isMac ? 16 : 0}>
|
||||
<SelectModelButton assistant={assistant} />
|
||||
</HStack>
|
||||
<NavbarRight
|
||||
@@ -114,7 +121,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
justifyContent: 'flex-end',
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
paddingRight: isWin || isLinux ? '144px' : '6px'
|
||||
paddingRight: isWin || isLinux ? '144px' : '15px'
|
||||
}}
|
||||
className="home-navbar-right">
|
||||
<HStack alignItems="center" gap={6}>
|
||||
|
||||
@@ -410,7 +410,7 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
<SettingDivider />
|
||||
</SettingGroup>
|
||||
</CollapsibleSettingGroup>
|
||||
<CollapsibleSettingGroup title={t('settings.math.title')} defaultExpanded={true}>
|
||||
<CollapsibleSettingGroup title={t('settings.math.title')} defaultExpanded={false}>
|
||||
<SettingGroup>
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.math.engine.label')}</SettingRowTitleSmall>
|
||||
@@ -439,7 +439,7 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
<SettingDivider />
|
||||
</SettingGroup>
|
||||
</CollapsibleSettingGroup>
|
||||
<CollapsibleSettingGroup title={t('chat.settings.code.title')} defaultExpanded={true}>
|
||||
<CollapsibleSettingGroup title={t('chat.settings.code.title')} defaultExpanded={false}>
|
||||
<SettingGroup>
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
|
||||
@@ -583,7 +583,7 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
</SettingGroup>
|
||||
<SettingDivider />
|
||||
</CollapsibleSettingGroup>
|
||||
<CollapsibleSettingGroup title={t('settings.messages.input.title')} defaultExpanded={true}>
|
||||
<CollapsibleSettingGroup title={t('settings.messages.input.title')} defaultExpanded={false}>
|
||||
<SettingGroup>
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>
|
||||
|
||||
@@ -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<Props> = ({ assistant }) => {
|
||||
const { model, updateAssistant } = useAssistant(assistant.id)
|
||||
const { t } = useTranslation()
|
||||
const timerRef = useRef<NodeJS.Timeout>(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)
|
||||
|
||||
|
||||
@@ -44,11 +44,20 @@ const MinAppPage: FC = () => {
|
||||
}
|
||||
}, [isTopNavbar])
|
||||
|
||||
// Find the app from all available apps
|
||||
// Find the app from all available apps (including cached ones)
|
||||
const app = useMemo(() => {
|
||||
if (!appId) return null
|
||||
return [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
|
||||
}, [appId, minapps])
|
||||
|
||||
// First try to find in default and custom mini-apps
|
||||
let foundApp = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
|
||||
|
||||
// If not found and we have cache, try to find in cache (for temporary apps)
|
||||
if (!foundApp && minAppsCache) {
|
||||
foundApp = minAppsCache.get(appId)
|
||||
}
|
||||
|
||||
return foundApp
|
||||
}, [appId, minapps, minAppsCache])
|
||||
|
||||
useEffect(() => {
|
||||
// If app not found, redirect to apps list
|
||||
|
||||
@@ -111,7 +111,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
||||
const targetScrollTop = elementOffsetTop - (containerHeight - elementHeight) / 2
|
||||
scrollContainer.scrollTo({
|
||||
top: Math.max(0, targetScrollTop),
|
||||
behavior: 'smooth'
|
||||
behavior: 'instant'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const createNewPainting = () => {
|
||||
const handleAddPainting = () => {
|
||||
if (generating) return
|
||||
const newPainting = getNewPainting()
|
||||
const addedPainting = addPainting('zhipu_paintings', newPainting)
|
||||
@@ -342,12 +342,12 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter>
|
||||
<Title>{t('title.paintings')}</Title>
|
||||
</NavbarCenter>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
|
||||
{isMac && (
|
||||
<NavbarRight>
|
||||
<Button type="text" icon={<PlusOutlined />} onClick={createNewPainting} disabled={generating} />
|
||||
<NavbarRight style={{ justifyContent: 'flex-end' }}>
|
||||
<Button size="small" className="nodrag" icon={<PlusOutlined />} onClick={handleAddPainting}>
|
||||
{t('paintings.button.new.image')}
|
||||
</Button>
|
||||
</NavbarRight>
|
||||
)}
|
||||
</Navbar>
|
||||
@@ -482,7 +482,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
selectedPainting={painting}
|
||||
onSelectPainting={onSelectPainting}
|
||||
onDeletePainting={onDeletePainting}
|
||||
onNewPainting={createNewPainting}
|
||||
onNewPainting={handleAddPainting}
|
||||
/>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
@@ -556,12 +556,6 @@ const ToolbarMenu = styled.div`
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const Title = styled.h1`
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
const ProviderTitleContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -14,7 +14,7 @@ import { runAsyncFunction } from '@renderer/utils'
|
||||
import { UpgradeChannel } from '@shared/config/constant'
|
||||
import { Avatar, Button, Progress, Radio, Row, Switch, Tag, Tooltip } from 'antd'
|
||||
import { debounce } from 'lodash'
|
||||
import { Bug, FileCheck, Github, Globe, Mail, Rss } from 'lucide-react'
|
||||
import { Bug, FileCheck, Globe, Mail, Rss } from 'lucide-react'
|
||||
import { BadgeQuestionMark } from 'lucide-react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -32,7 +32,7 @@ const AboutSettings: FC = () => {
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const { update } = useRuntime()
|
||||
const { openMinapp } = useMinappPopup()
|
||||
const { openSmartMinapp } = useMinappPopup()
|
||||
|
||||
const onCheckUpdate = debounce(
|
||||
async () => {
|
||||
@@ -79,7 +79,7 @@ const AboutSettings: FC = () => {
|
||||
|
||||
const showLicense = async () => {
|
||||
const { appPath } = await window.api.getAppInfo()
|
||||
openMinapp({
|
||||
openSmartMinapp({
|
||||
id: 'cherrystudio-license',
|
||||
name: t('settings.about.license.title'),
|
||||
url: `file://${appPath}/resources/cherry-studio/license.html`,
|
||||
@@ -89,7 +89,7 @@ const AboutSettings: FC = () => {
|
||||
|
||||
const showReleases = async () => {
|
||||
const { appPath } = await window.api.getAppInfo()
|
||||
openMinapp({
|
||||
openSmartMinapp({
|
||||
id: 'cherrystudio-releases',
|
||||
name: t('settings.about.releases.title'),
|
||||
url: `file://${appPath}/resources/cherry-studio/releases.html?theme=${theme === ThemeMode.dark ? 'dark' : 'light'}`,
|
||||
@@ -273,7 +273,7 @@ const AboutSettings: FC = () => {
|
||||
<IndicatorLight color="green" />
|
||||
</SettingRowTitle>
|
||||
</SettingRow>
|
||||
<UpdateNotesWrapper>
|
||||
<UpdateNotesWrapper className="markdown">
|
||||
<Markdown>
|
||||
{typeof update.info.releaseNotes === 'string'
|
||||
? update.info.releaseNotes.replace(/\n/g, '\n\n')
|
||||
@@ -309,7 +309,7 @@ const AboutSettings: FC = () => {
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
<Github size={18} />
|
||||
<GithubOutlined size={18} />
|
||||
{t('settings.about.feedback.title')}
|
||||
</SettingRowTitle>
|
||||
<Button onClick={() => onOpenWebsite('https://github.com/CherryHQ/cherry-studio/issues/new/choose')}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
@@ -16,7 +17,7 @@ const JoplinSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const { openMinapp } = useMinappPopup()
|
||||
const { openSmartMinapp } = useMinappPopup()
|
||||
|
||||
const joplinToken = useSelector((state: RootState) => state.settings.joplinToken)
|
||||
const joplinUrl = useSelector((state: RootState) => state.settings.joplinUrl)
|
||||
@@ -66,10 +67,11 @@ const JoplinSettings: FC = () => {
|
||||
}
|
||||
|
||||
const handleJoplinHelpClick = () => {
|
||||
openMinapp({
|
||||
openSmartMinapp({
|
||||
id: 'joplin-help',
|
||||
name: 'Joplin Help',
|
||||
url: 'https://joplinapp.org/help/apps/clipper'
|
||||
url: 'https://joplinapp.org/help/apps/clipper',
|
||||
logo: AppLogo
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { Client } from '@notionhq/client'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
@@ -21,7 +22,7 @@ const NotionSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const { openMinapp } = useMinappPopup()
|
||||
const { openSmartMinapp } = useMinappPopup()
|
||||
|
||||
const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey)
|
||||
const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID)
|
||||
@@ -67,10 +68,11 @@ const NotionSettings: FC = () => {
|
||||
}
|
||||
|
||||
const handleNotionTitleClick = () => {
|
||||
openMinapp({
|
||||
openSmartMinapp({
|
||||
id: 'notion-help',
|
||||
name: 'Notion Help',
|
||||
url: 'https://docs.cherry-ai.com/advanced-basic/notion'
|
||||
url: 'https://docs.cherry-ai.com/advanced-basic/notion',
|
||||
logo: AppLogo
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { HStack } from '@renderer/components/Layout'
|
||||
import { S3BackupManager } from '@renderer/components/S3BackupManager'
|
||||
import { S3BackupModal, useS3BackupModal } from '@renderer/components/S3Modals'
|
||||
import Selector from '@renderer/components/Selector'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
@@ -47,7 +48,7 @@ const S3Settings: FC = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation()
|
||||
const { openMinapp } = useMinappPopup()
|
||||
const { openSmartMinapp } = useMinappPopup()
|
||||
|
||||
const { s3Sync } = useAppSelector((state) => state.backup)
|
||||
|
||||
@@ -62,10 +63,11 @@ const S3Settings: FC = () => {
|
||||
}
|
||||
|
||||
const handleTitleClick = () => {
|
||||
openMinapp({
|
||||
openSmartMinapp({
|
||||
id: 's3-help',
|
||||
name: 'S3 Compatible Storage Help',
|
||||
url: 'https://docs.cherry-ai.com/data-settings/s3-compatible'
|
||||
url: 'https://docs.cherry-ai.com/data-settings/s3-compatible',
|
||||
logo: AppLogo
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
@@ -16,7 +17,7 @@ import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle
|
||||
const logger = loggerService.withContext('SiyuanSettings')
|
||||
|
||||
const SiyuanSettings: FC = () => {
|
||||
const { openMinapp } = useMinappPopup()
|
||||
const { openSmartMinapp } = useMinappPopup()
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -43,10 +44,11 @@ const SiyuanSettings: FC = () => {
|
||||
}
|
||||
|
||||
const handleSiyuanHelpClick = () => {
|
||||
openMinapp({
|
||||
openSmartMinapp({
|
||||
id: 'siyuan-help',
|
||||
name: 'Siyuan Help',
|
||||
url: 'https://docs.cherry-ai.com/advanced-basic/siyuan'
|
||||
url: 'https://docs.cherry-ai.com/advanced-basic/siyuan',
|
||||
logo: AppLogo
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
@@ -16,7 +17,7 @@ const YuqueSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const { openMinapp } = useMinappPopup()
|
||||
const { openSmartMinapp } = useMinappPopup()
|
||||
|
||||
const yuqueToken = useSelector((state: RootState) => state.settings.yuqueToken)
|
||||
const yuqueUrl = useSelector((state: RootState) => state.settings.yuqueUrl)
|
||||
@@ -65,10 +66,11 @@ const YuqueSettings: FC = () => {
|
||||
}
|
||||
|
||||
const handleYuqueHelpClick = () => {
|
||||
openMinapp({
|
||||
openSmartMinapp({
|
||||
id: 'yuque-help',
|
||||
name: 'Yuque Help',
|
||||
url: 'https://www.yuque.com/settings/tokens'
|
||||
url: 'https://www.yuque.com/settings/tokens',
|
||||
logo: AppLogo
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user