Compare commits

...

12 Commits

Author SHA1 Message Date
beyondkmp 03c8b6b5b4 format imports 2025-10-30 13:18:01 +08:00
beyondkmp f5136a0adb format code 2025-10-30 13:09:58 +08:00
beyondkmp 99873a0767 📝 docs: refresh shortcut refactor design 2025-10-30 11:35:00 +08:00
beyondkmp 34affb4533 ♻️ refactor: migrate shortcuts to preferences 2025-10-30 11:28:41 +08:00
MyPrototypeWhat cf008ca22e feat: update migration status documentation for Cherry Studio UI
- Expanded the migration status document to outline the comprehensive plan for transitioning from antd + styled-components to shadcn/ui + Tailwind CSS.
- Introduced detailed migration strategies, principles, and component classification guidelines.
- Added extraction criteria and migration steps to ensure a structured approach to component migration and optimization.
- Emphasized collaboration with UI designers for maintaining design consistency throughout the migration process.
2025-10-29 18:06:30 +08:00
MyPrototypeWhat 851ff8992f style: format color variables in todocss.css for improved readability
- Reformatted color variable definitions in todocss.css to enhance readability by breaking long lines into multiple lines.
- Ensured consistency in the formatting of HSLA values across the file.
2025-10-29 16:56:31 +08:00
MyPrototypeWhat 91f9088436 feat: add design system documentation and todocss.css file
- Introduced a comprehensive design system document outlining integration strategies for Tailwind CSS v4, usage guidelines, and UI library balance strategies.
- Added todocss.css file containing typography, spacing, sizing, and color variables for the design system.
- Established naming conventions and core transformation rules for design tokens to enhance consistency and usability across the UI components.
2025-10-29 16:52:05 +08:00
fullex c971daf23c Merge branch 'main' of github.com:CherryHQ/cherry-studio into v2 2025-10-29 16:43:40 +08:00
fullex 0c7cee2700 Merge branch 'main' of github.com:CherryHQ/cherry-studio into v2 2025-10-29 16:37:54 +08:00
defi-failure dfbfc2869c fix: use session model when sending messages (#11028) 2025-10-29 16:32:04 +08:00
Phantom 1575e97168 fix: approved tools (#11025)
* refactor(agent): move permission mode types and constants to config

Move PermissionModeCard type definition to types/agent.ts and relocate permissionModeCards constant from constants/permissionModes.ts to config/agent.ts for better organization and maintainability

* refactor(AgentSettings): simplify state management in ToolingSettings

remove redundant state for selectedMode and derive it from configuration
consolidate permission mode constants import path

* docs(AgentSettings): add jsdoc for computeModeDefaults function

* refactor(AgentSettings): simplify tooling state management with useMemo

remove redundant state for autoToolIds and compute it directly using useMemo

* refactor(AgentSettings): simplify tool approval state management

- Replace useState with useMemo for approvedToolIds to prevent unnecessary state updates
- Remove redundant state transitions and simplify toggle logic
- Ensure consistent tool filtering and merging with defaults

* refactor(AgentSettings): replace useState with useMemo for configuration state

Optimize performance by memoizing agent configuration state to prevent unnecessary re-renders

* perf(AgentSettings): optimize permission_mode computation with useMemo

Prevent unnecessary recalculations of permission_mode by memoizing the value

* refactor(AgentSettings): simplify MCP selection logic and remove unused imports

Remove useEffect for MCP state synchronization and directly use memoized value
Clean up unused imports and simplify toggle handler logic

* refactor: remove unused useAgentClient hook from ToolingSettings
2025-10-29 16:21:29 +08:00
SuYao e0a2ed0481 Provider Config & anthropic-web-fetch (#10808)
* fix: update AI SDK dependencies to latest versions

* feat: Update provider configurations and API handling

- Refactor provider configuration to support new API types and enhance API host formatting.
- Introduce new utility functions for handling API versions and formatting Azure OpenAI hosts.
- Update system models to include new capabilities and adjust provider types for CherryIN and VertexAI.
- Enhance provider settings UI to accommodate new API types and improve user experience.
- Implement migration logic for provider type updates and default API host settings.
- Update translations for API host configuration tips across multiple languages.
- Fix various type checks and utility functions to ensure compatibility with new provider types.

* fix: update unsupported API version providers and add longcat to compatible provider IDs

* fix: 移除不再使用的 Azure OpenAI API 版本参数,优化 API 主机格式化逻辑
feat: 在选择器组件中添加样式属性,增强可定制性
feat: 更新提供者设置,支持动态选择 API 主机字段

* refactor: 优化测试用例

* 修复: 更新工具调用处理器以支持新的工具调用类型

* feat: 添加TODO注释以改进基于AI SDK的供应商内置工具展示和类型安全处理

* feat: 添加对Google SDK的支持,更新流式参数构建逻辑以包含Google工具的上下文

* feat: 更新web搜索模型判断逻辑,使用SystemProviderIds常量替代硬编码字符串

* feat: 添加对@renderer/store的mock以支持测试环境

* feat: 添加API主机地址验证功能,更新相关逻辑以支持端点提取

* fix: i18n

* fix(i18n): Auto update translations for PR #10808

* Apply suggestion from @EurFelux

Co-authored-by: Phantom <eurfelux@gmail.com>

* Apply suggestion from @EurFelux

Co-authored-by: Phantom <eurfelux@gmail.com>

* Apply suggestion from @EurFelux

Co-authored-by: Phantom <eurfelux@gmail.com>

* refactor: Simplify provider type migration logic and enhance API version validation

* fix: Correct variable name from configedApiHost to configuredApiHost for consistency

* fix: Update package.json to remove deprecated @ai-sdk/google version and streamline @ai-sdk/openai versioning

* fix: 更新 hasAPIVersion 函数中的正则表达式以更准确地匹配 API 版本路径

* fix(api): 简化 validateApiHost 函数逻辑以始终返回 true
fix(yarn): 更新 @ai-sdk/openai 版本至 2.0.53 并添加依赖项

* fix(api): 修正 validateApiHost 函数在使用哈希后缀时的验证逻辑

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Phantom <eurfelux@gmail.com>
2025-10-29 14:47:21 +08:00
76 changed files with 3667 additions and 1267 deletions
@@ -1,13 +0,0 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index 69ab1599c76801dc1167551b6fa283dded123466..f0af43bba7ad1196fe05338817e65b4ebda40955 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
// src/get-model-path.ts
function getModelPath(modelId) {
- return modelId.includes("/") ? modelId : `models/${modelId}`;
+ return modelId?.includes("models/") ? modelId : `models/${modelId}`;
}
// src/google-generative-ai-options.ts
@@ -0,0 +1,26 @@
diff --git a/dist/index.js b/dist/index.js
index 4cc66d83af1cef39f6447dc62e680251e05ddf9f..eb9819cb674c1808845ceb29936196c4bb355172 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -471,7 +471,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
// src/get-model-path.ts
function getModelPath(modelId) {
- return modelId.includes("/") ? modelId : `models/${modelId}`;
+ return modelId.includes("models/") ? modelId : `models/${modelId}`;
}
// src/google-generative-ai-options.ts
diff --git a/dist/index.mjs b/dist/index.mjs
index a032505ec54e132dc386dde001dc51f710f84c83..5efada51b9a8b56e3f01b35e734908ebe3c37043 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
// src/get-model-path.ts
function getModelPath(modelId) {
- return modelId.includes("/") ? modelId : `models/${modelId}`;
+ return modelId.includes("models/") ? modelId : `models/${modelId}`;
}
// src/google-generative-ai-options.ts
@@ -0,0 +1,76 @@
diff --git a/dist/index.js b/dist/index.js
index cc6652c4e7f32878a64a2614115bf7eeb3b7c890..76e989017549c89b45d633525efb1f318026d9b2 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -274,6 +274,7 @@ var openaiChatResponseSchema = (0, import_provider_utils3.lazyValidator)(
message: import_v42.z.object({
role: import_v42.z.literal("assistant").nullish(),
content: import_v42.z.string().nullish(),
+ reasoning_content: import_v42.z.string().nullish(),
tool_calls: import_v42.z.array(
import_v42.z.object({
id: import_v42.z.string().nullish(),
@@ -340,6 +341,7 @@ var openaiChatChunkSchema = (0, import_provider_utils3.lazyValidator)(
delta: import_v42.z.object({
role: import_v42.z.enum(["assistant"]).nullish(),
content: import_v42.z.string().nullish(),
+ reasoning_content: import_v42.z.string().nullish(),
tool_calls: import_v42.z.array(
import_v42.z.object({
index: import_v42.z.number(),
@@ -785,6 +787,14 @@ var OpenAIChatLanguageModel = class {
if (text != null && text.length > 0) {
content.push({ type: "text", text });
}
+ const reasoning =
+ choice.message.reasoning_content;
+ if (reasoning != null && reasoning.length > 0) {
+ content.push({
+ type: 'reasoning',
+ text: reasoning,
+ });
+ }
for (const toolCall of (_a = choice.message.tool_calls) != null ? _a : []) {
content.push({
type: "tool-call",
@@ -866,6 +876,7 @@ var OpenAIChatLanguageModel = class {
};
let isFirstChunk = true;
let isActiveText = false;
+ let isActiveReasoning = false;
const providerMetadata = { openai: {} };
return {
stream: response.pipeThrough(
@@ -920,6 +931,22 @@ var OpenAIChatLanguageModel = class {
return;
}
const delta = choice.delta;
+ const reasoningContent = delta.reasoning_content;
+ if (reasoningContent) {
+ if (!isActiveReasoning) {
+ controller.enqueue({
+ type: 'reasoning-start',
+ id: 'reasoning-0',
+ });
+ isActiveReasoning = true;
+ }
+
+ controller.enqueue({
+ type: 'reasoning-delta',
+ id: 'reasoning-0',
+ delta: reasoningContent,
+ });
+ }
if (delta.content != null) {
if (!isActiveText) {
controller.enqueue({ type: "text-start", id: "0" });
@@ -1032,6 +1059,9 @@ var OpenAIChatLanguageModel = class {
}
},
flush(controller) {
+ if (isActiveReasoning) {
+ controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' });
+ }
if (isActiveText) {
controller.enqueue({ type: "text-end", id: "0" });
}
+191
View File
@@ -0,0 +1,191 @@
# Cherry Studio 快捷键系统重构设计文档 v2.1
> 最近更新:2025-01-30
> 维护者:Architecture Team
## 目录
- [背景与目标](#背景与目标)
- [核心原则](#核心原则)
- [架构分层](#架构分层)
- [关键实现](#关键实现)
- [数据流](#数据流)
- [默认快捷键](#默认快捷键)
- [迁移与兼容性](#迁移与兼容性)
- [后续演进方向](#后续演进方向)
---
## 背景与目标
旧版快捷键系统存在以下问题:
1. 依赖已弃用的 `configManager`,与 v2 架构不兼容;
2. Redux store 与本地存储重复维护状态;
3. 处理器通过 `switch-case` 硬编码,可维护性差;
4. 快捷键定义分散,缺乏统一真相源;
5. 新增快捷键需要触达多处文件,易错且低效。
新版系统要实现:
- **单一真相源**:快捷键定义集中管理,保证一致性;
- **偏好服务优先**:所有运行时状态通过 `preferenceService` 管理;
- **处理器注册表**:解除 `switch-case` 依赖,改用 Map 注册;
- **类型安全**:从定义、存储到消费全链路具备 TypeScript 约束;
- **易扩展**:新增快捷键仅需「定义 → 注册处理器 → 使用」三步;
- **性能稳定**:支持 100+ 快捷键规模,主/渲染进程高效同步;
- **多窗口同步**:借助 `preferenceService` 自动推送变更。
---
## 核心原则
1. **关注点分离**
- 定义层:静态元数据(名称、默认绑定、作用域、分类等);
- 偏好层:用户可变配置(绑定、启用状态等);
- 服务层:主进程注册、电焦/失焦时的生命周期管理;
- UI 层:设置面板、快捷键提示等。
2. **复用基础设施**
- 所有持久化均依赖 `preferenceService`SQLite + 内存缓存 + IPC);
- 变更通过订阅自动广播至所有窗口;
- 新增键位无需改动主进程/渲染进程的底层框架代码。
---
## 架构分层
```
┌──────────────────────────────────────────────┐
│ Shortcut 系统 │
├──────────────────────────────────────────────┤
│ 📋 Definitions (packages/shared/shortcuts) │
│ - types.ts:类型、作用域、分类 │
│ - definitions.ts:静态定义(真相之源) │
│ - utils.ts:转换/校验工具 │
│ │
│ 💾 Preferences (preferenceService) │
│ - preferenceSchemas.ts 默认值 │
│ - preferenceTypes.ts 类型导出 │
│ │
│ ⚙️ Services │
│ - src/main/services/ShortcutService.ts │
│ · 处理器注册表、focus/blur 生命周期 │
│ · preference 订阅、主进程快捷键注册 │
│ - 渲染进程 useShortcut/useShortcutDisplay │
│ │
│ 🎨 UI │
│ - 设置页 ShortcutSettings │
│ - 各功能模块中的 useShortcut/useShortcutDisplay │
└──────────────────────────────────────────────┘
```
---
## 关键实现
### 1. 静态定义
- 所有快捷键在 `packages/shared/shortcuts/definitions.ts` 中集中维护;
- 包含 `scope`main / renderer / both)、`category``persistOnBlur` 等元信息;
- `enabledWhen` 支持动态启用(如 mini window 与 quick assistant 开关关联);
- 新增快捷键步骤:
1.`preferenceSchemas.ts` 中声明默认值;
2.`definitions.ts` 中补充静态定义;
3. 在主/渲染进程相关模块注册处理器或消费 Hook。
### 2. 偏好系统
- 所有运行时配置通过 `preferenceService` 读写;
- 默认值与 `PreferenceShortcutType` 结构保持一致;
- `ShortcutService` / `useShortcuts` 访问偏好时统一调用 `coerceShortcutPreference`,确保 fallback 与类型安全;
- 批量重置通过 `preferenceService.setMultiple` 实现。
### 3. 主进程服务
- `ShortcutService` 负责:
- 生命周期:随着窗口 focus/blur 注册或卸载快捷键;
- 处理器注册:Map 替换 `switch-case`
- 订阅偏好变更:自动重新注册;
- `persistOnBlur`:例如 `show_main_window` 在窗口失焦时仍可触发;
- `shortcut.app.show_settings` 会在需要时唤起窗口并调用 `window.navigate('/settings/provider')`,避免重复 blur/focus。
### 4. 渲染进程 Hook
- `useShortcut`:从偏好获取绑定 → 转为 `react-hotkeys-hook` 字符串 → 注册快捷键;
- `useShortcutDisplay`:转换为 UI 显示字符串(`⌘` / `Ctrl+` 等);
- `useAllShortcuts`:批量拉取配置 + diff 默认值,供设置面板使用;
- 新增 `enableOnContentEditable` 等配置支撑设置页和富文本场景。
### 5. 设置界面
- `ShortcutSettings` 直接消费 `useAllShortcuts`
- 支持录制、清空、重置默认、启用/禁用、冲突检测;
- 重新绑定时使用 `convertKeyToAccelerator` / `isValidShortcut` / `formatShortcutDisplay`
- “重置全部” 通过 `preferenceService.setMultiple` 一次性写入默认配置;
- 新增表格展示 `hasCustomBinding`,区分用户自定义与继承默认值。
---
## 数据流
### 启动阶段
1. `preferenceService.initialize()` 载入缓存;
2. `shortcutService` 构造时注册处理器与订阅;
3. 窗口创建后调用 `shortcutService.registerForWindow`,在 `focus` 时注册主进程快捷键。
### 运行时变更
1. 设置页或其他模块调用 `preferenceService.set` / `setMultiple`
2. 主进程订阅触发 → `globalShortcut.unregisterAll()` → 按新配置重注册;
3. 渲染进程通过 `usePreference`/`useMultiplePreferences` 自动收到更新,UI 即时刷新。
---
## 默认快捷键
| preference key | 默认绑定 | 描述 / 备注 |
|----------------------------------------|-----------------------------|--------------------------------------|
| `shortcut.app.show_main_window` | `Cmd/Ctrl + Shift + A` | 主窗口显示(失焦持久) |
| `shortcut.app.show_mini_window` | `Cmd/Ctrl + E` | Mini 窗口(与 quick assistant 联动) |
| `shortcut.app.show_settings` | `Cmd/Ctrl + ,` | 设置页入口 |
| `shortcut.app.toggle_show_assistants` | `Cmd/Ctrl + [` | 助手侧边栏 |
| `shortcut.app.exit_fullscreen` | `Escape` | 系统级,不可编辑 |
| `shortcut.app.zoom_in/out/reset` | `Cmd/Ctrl + = / - / 0` | 包含数字键盘变体 |
| `shortcut.app.search_message` | `Cmd/Ctrl + Shift + F` | 全局搜索 |
| `shortcut.chat.clear` | `Cmd/Ctrl + L` | 清空消息 |
| `shortcut.chat.search_message` | `Cmd/Ctrl + F` | 聊天内搜索 |
| `shortcut.chat.toggle_new_context` | `Cmd/Ctrl + K` | 新上下文 |
| `shortcut.chat.copy_last_message` | `Cmd/Ctrl + Shift + C` | 复制最后一条 |
| `shortcut.chat.edit_last_user_message` | `Cmd/Ctrl + Shift + E` | 编辑最后一条用户消息 |
| `shortcut.topic.new` | `Cmd/Ctrl + N` | 新增话题(默认启用) |
| `shortcut.topic.rename` | `Cmd/Ctrl + T` | 重命名话题(默认启用,自 2025-01 调整) |
| `shortcut.topic.toggle_show_topics` | `Cmd/Ctrl + ]` | 话题侧边栏 |
| `shortcut.selection.*` | 无默认绑定 | 划词助手开关、取词 |
> 具体配置以 `preferenceSchemas.ts` 为准,可在设置页查看或调整。
---
## 迁移与兼容性
- 已有用户偏好:沿用旧值;新增键(如 `shortcut.topic.rename`)在数据库不存在时继承新默认;
- 旧版 Redux store / `configManager` 已彻底移除;
- `IpcChannel.Shortcuts_Update``window.api.shortcuts.update` 相关逻辑已弃用;
- `PreferenceMigrator` 中保留与旧 keys 的映射,确保升级顺畅。
---
## 后续演进方向
1. **冲突检测增强**:主/渲染进程联动校验冲突并提示;
2. **导入导出**:允许用户批量备份/恢复自定义快捷键;
3. **多作用域绑定**:同一逻辑支持按窗口类型或上下文切换;
4. **可视化录制**:增加「录制模式」避免输入框手动录制;
5. **自动化测试**:补充主进程/渲染进程快捷键单元测试样板。
---
> 如需扩展或有疑问,请联系架构团队或在仓库中提交 Issue。
> 设计文档 v2.1 同步最新实现(2025-01),包含 `shortcut.topic.rename` 默认启用、`show_settings` 优化等补充说明。
+5 -4
View File
@@ -106,8 +106,8 @@
"@agentic/exa": "^7.3.3", "@agentic/exa": "^7.3.3",
"@agentic/searxng": "^7.3.3", "@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3", "@agentic/tavily": "^7.3.3",
"@ai-sdk/amazon-bedrock": "^3.0.35", "@ai-sdk/amazon-bedrock": "^3.0.42",
"@ai-sdk/google-vertex": "^3.0.40", "@ai-sdk/google-vertex": "^3.0.48",
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch", "@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch",
"@ai-sdk/mistral": "^2.0.19", "@ai-sdk/mistral": "^2.0.19",
"@ai-sdk/perplexity": "^2.0.13", "@ai-sdk/perplexity": "^2.0.13",
@@ -230,7 +230,7 @@
"@viz-js/lang-dot": "^1.0.5", "@viz-js/lang-dot": "^1.0.5",
"@viz-js/viz": "^3.14.0", "@viz-js/viz": "^3.14.0",
"@xyflow/react": "^12.4.4", "@xyflow/react": "^12.4.4",
"ai": "^5.0.68", "ai": "^5.0.76",
"antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch", "antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
@@ -393,7 +393,8 @@
"undici": "6.21.2", "undici": "6.21.2",
"vite": "npm:rolldown-vite@7.1.5", "vite": "npm:rolldown-vite@7.1.5",
"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.20": "patch:@ai-sdk/google@npm%3A2.0.20#~/.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch", "@ai-sdk/google@npm:2.0.23": "patch:@ai-sdk/google@npm%3A2.0.23#~/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch",
"@ai-sdk/openai@npm:^2.0.52": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch",
"@img/sharp-darwin-arm64": "0.34.3", "@img/sharp-darwin-arm64": "0.34.3",
"@img/sharp-darwin-x64": "0.34.3", "@img/sharp-darwin-x64": "0.34.3",
"@img/sharp-linux-arm": "0.34.3", "@img/sharp-linux-arm": "0.34.3",
+3 -3
View File
@@ -36,10 +36,10 @@
"ai": "^5.0.26" "ai": "^5.0.26"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "^2.0.27", "@ai-sdk/anthropic": "^2.0.32",
"@ai-sdk/azure": "^2.0.49", "@ai-sdk/azure": "^2.0.53",
"@ai-sdk/deepseek": "^1.0.23", "@ai-sdk/deepseek": "^1.0.23",
"@ai-sdk/openai": "^2.0.48", "@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch",
"@ai-sdk/openai-compatible": "^1.0.22", "@ai-sdk/openai-compatible": "^1.0.22",
"@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider": "^2.0.0",
"@ai-sdk/provider-utils": "^3.0.12", "@ai-sdk/provider-utils": "^3.0.12",
@@ -373,6 +373,8 @@ export interface PreferenceSchemas {
'shortcut.chat.clear': Record<string, unknown> 'shortcut.chat.clear': Record<string, unknown>
// redux/shortcuts/shortcuts.copy_last_message // redux/shortcuts/shortcuts.copy_last_message
'shortcut.chat.copy_last_message': Record<string, unknown> 'shortcut.chat.copy_last_message': Record<string, unknown>
// redux/shortcuts/shortcuts.edit_last_user_message
'shortcut.chat.edit_last_user_message': Record<string, unknown>
// redux/shortcuts/shortcuts.search_message_in_chat // redux/shortcuts/shortcuts.search_message_in_chat
'shortcut.chat.search_message': Record<string, unknown> 'shortcut.chat.search_message': Record<string, unknown>
// redux/shortcuts/shortcuts.toggle_new_context // redux/shortcuts/shortcuts.toggle_new_context
@@ -383,6 +385,10 @@ export interface PreferenceSchemas {
'shortcut.selection.toggle_enabled': Record<string, unknown> 'shortcut.selection.toggle_enabled': Record<string, unknown>
// redux/shortcuts/shortcuts.new_topic // redux/shortcuts/shortcuts.new_topic
'shortcut.topic.new': Record<string, unknown> 'shortcut.topic.new': Record<string, unknown>
// redux/shortcuts/shortcuts.rename_topic
'shortcut.topic.rename': Record<string, unknown>
// redux/shortcuts/shortcuts.toggle_show_topics
'shortcut.topic.toggle_show_topics': Record<string, unknown>
// redux/settings/enableTopicNaming // redux/settings/enableTopicNaming
'topic.naming.enabled': boolean 'topic.naming.enabled': boolean
// redux/settings/topicNamingPrompt // redux/settings/topicNamingPrompt
@@ -638,6 +644,12 @@ export const DefaultPreferences: PreferenceSchemas = {
key: ['CommandOrControl', 'Shift', 'C'], key: ['CommandOrControl', 'Shift', 'C'],
system: false system: false
}, },
'shortcut.chat.edit_last_user_message': {
editable: true,
enabled: false,
key: ['CommandOrControl', 'Shift', 'E'],
system: false
},
'shortcut.chat.search_message': { editable: true, enabled: true, key: ['CommandOrControl', 'F'], system: false }, 'shortcut.chat.search_message': { editable: true, enabled: true, key: ['CommandOrControl', 'F'], system: false },
'shortcut.chat.toggle_new_context': { 'shortcut.chat.toggle_new_context': {
editable: true, editable: true,
@@ -648,6 +660,18 @@ export const DefaultPreferences: PreferenceSchemas = {
'shortcut.selection.get_text': { editable: true, enabled: false, key: [], system: true }, 'shortcut.selection.get_text': { editable: true, enabled: false, key: [], system: true },
'shortcut.selection.toggle_enabled': { editable: true, enabled: false, key: [], system: true }, 'shortcut.selection.toggle_enabled': { editable: true, enabled: false, key: [], system: true },
'shortcut.topic.new': { editable: true, enabled: true, key: ['CommandOrControl', 'N'], system: false }, 'shortcut.topic.new': { editable: true, enabled: true, key: ['CommandOrControl', 'N'], system: false },
'shortcut.topic.rename': {
editable: true,
enabled: true,
key: ['CommandOrControl', 'T'],
system: false
},
'shortcut.topic.toggle_show_topics': {
editable: true,
enabled: true,
key: ['CommandOrControl', ']'],
system: false
},
'topic.naming.enabled': true, 'topic.naming.enabled': true,
'topic.naming_prompt': '', 'topic.naming_prompt': '',
'topic.position': 'left', 'topic.position': 'left',
+148
View File
@@ -0,0 +1,148 @@
import type { ShortcutCategory, ShortcutDefinition } from './types'
export const SHORTCUT_DEFINITIONS: readonly ShortcutDefinition[] = [
// ==================== 应用级快捷键 ====================
{
key: 'shortcut.app.show_main_window',
defaultKey: ['CommandOrControl', 'Shift', 'A'],
scope: 'main',
category: 'app',
persistOnBlur: true
},
{
key: 'shortcut.app.show_mini_window',
defaultKey: ['CommandOrControl', 'E'],
scope: 'main',
category: 'app',
persistOnBlur: true,
enabledWhen: (getPreference) => !!getPreference('feature.quick_assistant.enabled')
},
{
key: 'shortcut.app.show_settings',
defaultKey: ['CommandOrControl', ','],
scope: 'both',
category: 'app'
},
{
key: 'shortcut.app.toggle_show_assistants',
defaultKey: ['CommandOrControl', '['],
scope: 'renderer',
category: 'app'
},
{
key: 'shortcut.app.exit_fullscreen',
defaultKey: ['Escape'],
scope: 'renderer',
category: 'app'
},
{
key: 'shortcut.app.zoom_in',
defaultKey: ['CommandOrControl', '='],
scope: 'main',
category: 'app',
variants: [['CommandOrControl', 'numadd']]
},
{
key: 'shortcut.app.zoom_out',
defaultKey: ['CommandOrControl', '-'],
scope: 'main',
category: 'app',
variants: [['CommandOrControl', 'numsub']]
},
{
key: 'shortcut.app.zoom_reset',
defaultKey: ['CommandOrControl', '0'],
scope: 'main',
category: 'app'
},
{
key: 'shortcut.app.search_message',
defaultKey: ['CommandOrControl', 'Shift', 'F'],
scope: 'renderer',
category: 'app'
},
// ==================== 聊天相关快捷键 ====================
{
key: 'shortcut.chat.clear',
defaultKey: ['CommandOrControl', 'L'],
scope: 'renderer',
category: 'chat'
},
{
key: 'shortcut.chat.search_message',
defaultKey: ['CommandOrControl', 'F'],
scope: 'renderer',
category: 'chat'
},
{
key: 'shortcut.chat.toggle_new_context',
defaultKey: ['CommandOrControl', 'K'],
scope: 'renderer',
category: 'chat'
},
{
key: 'shortcut.chat.copy_last_message',
defaultKey: ['CommandOrControl', 'Shift', 'C'],
scope: 'renderer',
category: 'chat'
},
{
key: 'shortcut.chat.edit_last_user_message',
defaultKey: ['CommandOrControl', 'Shift', 'E'],
scope: 'renderer',
category: 'chat'
},
// ==================== 话题管理快捷键 ====================
{
key: 'shortcut.topic.new',
defaultKey: ['CommandOrControl', 'N'],
scope: 'renderer',
category: 'topic'
},
{
key: 'shortcut.topic.rename',
defaultKey: ['CommandOrControl', 'T'],
scope: 'renderer',
category: 'topic'
},
{
key: 'shortcut.topic.toggle_show_topics',
defaultKey: ['CommandOrControl', ']'],
scope: 'renderer',
category: 'topic'
},
// ==================== 划词助手快捷键 ====================
{
key: 'shortcut.selection.toggle_enabled',
defaultKey: [],
scope: 'main',
category: 'selection',
persistOnBlur: true
},
{
key: 'shortcut.selection.get_text',
defaultKey: [],
scope: 'main',
category: 'selection',
persistOnBlur: true
}
] as const
export const getShortcutsByCategory = () => {
const groups: Record<ShortcutCategory, ShortcutDefinition[]> = {
app: [],
chat: [],
topic: [],
selection: []
}
SHORTCUT_DEFINITIONS.forEach((definition) => {
groups[definition.category].push(definition)
})
return groups
}
export const findShortcutDefinition = (key: string): ShortcutDefinition | undefined => {
return SHORTCUT_DEFINITIONS.find((definition) => definition.key === key)
}
+40
View File
@@ -0,0 +1,40 @@
import type { PreferenceDefaultScopeType, PreferenceKeyType } from '@shared/data/preference/preferenceTypes'
import type { BrowserWindow } from 'electron'
export type ShortcutScope = 'main' | 'renderer' | 'both'
export type ShortcutCategory = 'app' | 'chat' | 'topic' | 'selection'
export type ShortcutPreferenceKey = Extract<PreferenceKeyType, `shortcut.${string}`>
export type GetPreferenceFn = <K extends PreferenceKeyType>(key: K) => PreferenceDefaultScopeType[K]
export type ShortcutEnabledPredicate = (getPreference: GetPreferenceFn) => boolean
export interface ShortcutDefinition {
key: ShortcutPreferenceKey
defaultKey: string[]
scope: ShortcutScope
category: ShortcutCategory
persistOnBlur?: boolean
variants?: string[][]
enabledWhen?: ShortcutEnabledPredicate
}
export interface ShortcutPreferenceValue {
binding: string[]
rawBinding: string[]
hasCustomBinding: boolean
enabled: boolean
editable: boolean
system: boolean
}
export interface ShortcutRuntimeConfig extends ShortcutDefinition {
binding: string[]
enabled: boolean
editable: boolean
system: boolean
}
export type ShortcutHandler = (window?: BrowserWindow) => void | Promise<void>
+137
View File
@@ -0,0 +1,137 @@
import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas'
import type { PreferenceShortcutType } from '@shared/data/preference/preferenceTypes'
import type { ShortcutDefinition, ShortcutPreferenceValue } from './types'
const modifierKeys = ['CommandOrControl', 'Ctrl', 'Alt', 'Shift', 'Meta', 'Command']
const specialSingleKeys = ['Escape', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12']
export const convertKeyToAccelerator = (key: string): string => {
const keyMap: Record<string, string> = {
Command: 'CommandOrControl',
Cmd: 'CommandOrControl',
Control: 'Ctrl',
Meta: 'Meta',
ArrowUp: 'Up',
ArrowDown: 'Down',
ArrowLeft: 'Left',
ArrowRight: 'Right',
AltGraph: 'AltGr',
Slash: '/',
Semicolon: ';',
BracketLeft: '[',
BracketRight: ']',
Backslash: '\\',
Quote: "'",
Comma: ',',
Minus: '-',
Equal: '='
}
return keyMap[key] || key
}
export const convertAcceleratorToHotkey = (accelerator: string[]): string => {
return accelerator
.map((key) => {
switch (key.toLowerCase()) {
case 'commandorcontrol':
return 'mod'
case 'command':
case 'cmd':
return 'meta'
case 'control':
case 'ctrl':
return 'ctrl'
case 'alt':
return 'alt'
case 'shift':
return 'shift'
case 'meta':
return 'meta'
default:
return key.toLowerCase()
}
})
.join('+')
}
export const formatShortcutDisplay = (keys: string[], isMac: boolean): string => {
return keys
.map((key) => {
switch (key.toLowerCase()) {
case 'ctrl':
case 'control':
return isMac ? '⌃' : 'Ctrl'
case 'command':
case 'cmd':
return isMac ? '⌘' : 'Win'
case 'commandorcontrol':
return isMac ? '⌘' : 'Ctrl'
case 'alt':
return isMac ? '⌥' : 'Alt'
case 'shift':
return isMac ? '⇧' : 'Shift'
case 'meta':
return isMac ? '⌘' : 'Win'
default:
return key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()
}
})
.join(isMac ? '' : '+')
}
export const isValidShortcut = (keys: string[]): boolean => {
if (!keys.length) {
return false
}
const hasModifier = keys.some((key) => modifierKeys.includes(key))
const isSpecialKey = keys.length === 1 && specialSingleKeys.includes(keys[0])
return hasModifier || isSpecialKey
}
const ensureArray = (value: unknown): string[] => {
if (Array.isArray(value)) {
return value.filter((item): item is string => typeof item === 'string')
}
return []
}
const ensureBoolean = (value: unknown, fallback: boolean): boolean => (typeof value === 'boolean' ? value : fallback)
export const getDefaultShortcutPreference = (definition: ShortcutDefinition): ShortcutPreferenceValue => {
const fallback = DefaultPreferences.default[definition.key] as PreferenceShortcutType
const rawBinding = ensureArray(fallback?.key)
const binding = rawBinding.length ? rawBinding : definition.defaultKey
return {
binding,
rawBinding: binding,
hasCustomBinding: false,
enabled: ensureBoolean(fallback?.enabled, true),
editable: ensureBoolean(fallback?.editable, true),
system: ensureBoolean(fallback?.system, false)
}
}
export const coerceShortcutPreference = (
definition: ShortcutDefinition,
value?: PreferenceShortcutType | null
): ShortcutPreferenceValue => {
const fallback = getDefaultShortcutPreference(definition)
const hasCustomBinding = Array.isArray((value as PreferenceShortcutType | undefined)?.key)
const rawBinding = hasCustomBinding ? ensureArray((value as PreferenceShortcutType).key) : fallback.binding
const binding = rawBinding.length > 0 ? rawBinding : fallback.binding
return {
binding,
rawBinding,
hasCustomBinding,
enabled: ensureBoolean(value?.enabled, fallback.enabled),
editable: ensureBoolean(value?.editable, fallback.editable),
system: ensureBoolean(value?.system, fallback.system)
}
}
+368
View File
@@ -0,0 +1,368 @@
# Cherry Studio Design System 集成方案
本文档聚焦三个核心问题:
1. **如何将 todocss.css 集成到 Tailwind CSS v4**
2. **如何在项目中使用集成后的设计系统**
3. **如何平衡 UI 库和主包的需求**
---
## 一、集成策略
### 1.1 文件架构
```
todocss.css (设计师提供)
↓ 转换 & 优化
design-tokens.css (--ds-* 变量)
↓ @theme inline 映射
globals.css (cs-* 工具类)
↓ 开发者使用
React Components
```
### 1.2 核心转换规则
#### 变量简化
```css
/* todocss.css */
--Brand--Base_Colors--Primary: hsla(84, 81%, 44%, 1);
/* ↓ 转换为 design-tokens.css */
--ds-primary: hsla(84, 81%, 44%, 1);
/* ↓ 映射到 globals.css */
@theme inline {
--color-cs-primary: var(--ds-primary);
}
/* ↓ 生成工具类 */
bg-cs-primary, text-cs-primary, border-cs-primary
```
#### 去除冗余
- **间距/尺寸合并**: `--Spacing--md``--Sizing--md` 值相同 → 统一为 `--ds-size-md`
- **透明度废弃**: `--Opacity--Red--Red-80` → 使用 `bg-cs-destructive/80`
- **错误修正**: `--Font_weight--Regular: 400px``--ds-font-weight-regular: 400`
### 1.3 命名规范
| 层级 | 前缀 | 示例 | 用途 |
|------|------|------|------|
| 设计令牌 | `--ds-*` | `--ds-primary` | 定义值 |
| Tailwind 映射 | `--color-cs-*` | `--color-cs-primary` | 生成工具类 |
| 工具类 | `cs-*` | `bg-cs-primary` | 开发者使用 |
#### Tailwind v4 映射规则
| 变量前缀 | 生成的工具类 |
|----------|-------------|
| `--color-cs-*` | `bg-*`, `text-*`, `border-*`, `fill-*` |
| `--spacing-cs-*` | `p-*`, `m-*`, `gap-*` |
| `--size-cs-*` | `w-*`, `h-*`, `size-*` |
| `--radius-cs-*` | `rounded-*` |
| `--font-size-cs-*` | `text-*` |
### 1.4 为什么使用 @theme inline
```css
/* ❌ @theme - 静态编译,不支持运行时主题切换 */
@theme {
--color-primary: var(--ds-primary);
}
/* ✅ @theme inline - 保留变量引用,支持运行时切换 */
@theme inline {
--color-cs-primary: var(--ds-primary);
}
```
**关键差异**`@theme inline` 使 CSS 变量在运行时动态解析,实现明暗主题切换。
---
## 二、项目使用指南
### 2.1 在 UI 库中使用
#### 文件结构
```
packages/ui/
├── src/styles/
│ ├── design-tokens.css # 核心变量定义
│ └── globals.css # Tailwind 集成
└── package.json # 导出配置
```
#### globals.css 示例
```css
@import 'tailwindcss';
@import './design-tokens.css';
@theme inline {
/* 颜色 */
--color-cs-primary: var(--ds-primary);
--color-cs-bg: var(--ds-background);
--color-cs-fg: var(--ds-foreground);
/* 间距 */
--spacing-cs-xs: var(--ds-size-xs);
--spacing-cs-sm: var(--ds-size-sm);
--spacing-cs-md: var(--ds-size-md);
/* 尺寸 */
--size-cs-xs: var(--ds-size-xs);
--size-cs-sm: var(--ds-size-sm);
/* 圆角 */
--radius-cs-sm: var(--ds-radius-sm);
--radius-cs-md: var(--ds-radius-md);
}
@custom-variant dark (&:is(.dark *));
```
#### 组件中使用
```tsx
// packages/ui/src/components/Button.tsx
export const Button = ({ children }) => (
<button className="
bg-cs-primary
text-white
px-cs-sm
py-cs-xs
rounded-cs-md
hover:bg-cs-primary/90
transition-colors
">
{children}
</button>
)
```
### 2.2 在主项目中使用
#### 导入 UI 库样式
```css
/* src/renderer/src/assets/styles/tailwind.css */
@import 'tailwindcss' source('../../../../renderer');
@import '@cherrystudio/ui/styles/globals.css';
@custom-variant dark (&:is(.dark *));
```
#### 覆盖或扩展变量
```css
/* src/renderer/src/assets/styles/tailwind.css */
@import '@cherrystudio/ui/styles/globals.css';
/* 主项目特定覆盖 */
:root {
--ds-primary: #custom-color; /* 覆盖 UI 库的主题色 */
}
```
#### 在主项目组件中使用
```tsx
// src/renderer/src/pages/Home.tsx
export const Home = () => (
<div className="
bg-cs-bg
p-cs-md
rounded-cs-lg
">
<Button></Button>
</div>
)
```
### 2.3 主题切换实现
```tsx
// App.tsx
import { useState } from 'react'
export function App() {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
return (
<div className={theme}>
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
</button>
{/* 所有子组件自动响应主题 */}
</div>
)
}
```
### 2.4 透明度修饰符
```tsx
<div className="
bg-cs-primary/10 /* 10% 透明度 */
bg-cs-primary/50 /* 50% 透明度 */
bg-cs-primary/[0.15] /* 自定义透明度 */
">
```
---
## 三、UI 库与主包平衡策略
### 3.1 UI 库职责
**目标**:提供可复用、可定制的基础设计系统
```json
// packages/ui/package.json
{
"exports": {
"./styles/design-tokens.css": "./src/styles/design-tokens.css",
"./styles/globals.css": "./src/styles/globals.css"
}
}
```
**原则**
- ✅ 定义通用的设计令牌(`--ds-*`
- ✅ 提供默认的 Tailwind 映射(`--color-cs-*`
- ✅ 保持变量语义化,不包含业务逻辑
- ❌ 不包含主项目特定的颜色或尺寸
### 3.2 主包职责
**目标**:导入 UI 库,根据业务需求扩展或覆盖
```css
/* src/renderer/src/assets/styles/tailwind.css */
@import '@cherrystudio/ui/styles/globals.css';
/* 主项目扩展 */
@theme inline {
--color-cs-brand-accent: #ff6b6b; /* 新增颜色 */
}
/* 主项目覆盖 */
:root {
--ds-primary: #custom-primary; /* 覆盖 UI 库的主题色 */
}
```
**原则**
- ✅ 导入 UI 库的 `globals.css`
- ✅ 通过覆盖 `--ds-*` 变量定制主题
- ✅ 添加项目特定的 `--color-cs-*` 映射
- ✅ 保留向后兼容的旧变量(如 `color.css`
### 3.3 向后兼容方案
#### 保留旧变量
```css
/* src/renderer/src/assets/styles/color.css */
:root {
--color-primary: #00b96b; /* 旧变量 */
--color-background: #181818; /* 旧变量 */
}
/* 映射到新系统 */
:root {
--ds-primary: var(--color-primary);
--ds-background: var(--color-background);
}
```
#### 渐进式迁移
```tsx
// 阶段 1:旧代码继续工作
<div style={{ color: 'var(--color-primary)' }}></div>
// 阶段 2:新代码使用工具类
<div className="text-cs-primary"></div>
// 阶段 3:逐步替换旧代码
```
### 3.4 冲突处理
| 场景 | 策略 |
|------|------|
| UI 库与 Tailwind 默认类冲突 | 使用 `cs-` 前缀隔离 |
| 主包需要覆盖 UI 库颜色 | 覆盖 `--ds-*` 变量 |
| 主包需要新增颜色 | 添加新的 `--color-cs-*` 映射 |
| 旧变量与新系统共存 | 通过 `var()` 映射到 `--ds-*` |
### 3.5 独立发布 UI 库
```json
// packages/ui/package.json
{
"name": "@cherrystudio/ui",
"exports": {
"./styles/design-tokens.css": "./src/styles/design-tokens.css",
"./styles/globals.css": "./src/styles/globals.css"
},
"peerDependencies": {
"tailwindcss": "^4.1.13"
}
}
```
**外部项目使用**
```css
/* 其他项目的 tailwind.css */
@import 'tailwindcss';
@import '@cherrystudio/ui/styles/globals.css';
/* 覆盖主题色 */
:root {
--ds-primary: #your-brand-color;
}
```
---
## 四、完整映射示例
### todocss.css → design-tokens.css
| todocss.css | design-tokens.css | 说明 |
|-------------|-------------------|------|
| `--Brand--Base_Colors--Primary` | `--ds-primary` | 简化命名 |
| `--Spacing--md` + `--Sizing--md` | `--ds-size-md` | 合并重复 |
| `--Opacity--Red--Red-80` | *(删除)* | 使用 `/80` 修饰符 |
| `--Font_weight--Regular: 400px` | `--ds-font-weight-regular: 400` | 修正错误 |
| `--Brand--UI_Element_Colors--Primary_Button--Background` | `--ds-btn-primary` | 简化语义 |
### design-tokens.css → globals.css → 工具类
| design-tokens.css | globals.css | 工具类 |
|-------------------|-------------|--------|
| `--ds-primary` | `--color-cs-primary` | `bg-cs-primary` |
| `--ds-size-md` | `--spacing-cs-md` | `p-cs-md` |
| `--ds-size-md` | `--size-cs-md` | `w-cs-md` |
| `--ds-radius-lg` | `--radius-cs-lg` | `rounded-cs-lg` |
---
## 五、关键决策记录
1. **使用 `@theme inline`** - 支持运行时主题切换
2. **`cs-` 前缀** - 命名空间隔离,避免冲突
3. **合并 Spacing/Sizing** - 消除冗余
4. **废弃 Opacity 变量** - 使用 Tailwind 的 `/modifier` 语法
5. **双层变量系统** - `--ds-*` (定义) → `--color-cs-*` (映射)
6. **共存策略** - Tailwind 默认类 + `cs-` 品牌类
+90 -97
View File
@@ -1,4 +1,26 @@
# UI Component Library Migration Status # Cherry Studio UI Migration Plan
## Overview
This document outlines the detailed plan for migrating Cherry Studio from antd + styled-components to shadcn/ui + Tailwind CSS. We will adopt a progressive migration strategy to ensure system stability and development efficiency, while gradually implementing UI refactoring in collaboration with UI designers.
## Migration Strategy
### Target Tech Stack
- **UI Component Library**: shadcn/ui (replacing antd and previously migrated HeroUI)
- **Styling Solution**: Tailwind CSS (replacing styled-components)
- **Design System**: Custom CSS variable system (see [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md))
- **Theme System**: CSS variables + shadcn/ui theme
### Migration Principles
1. **Backward Compatibility**: Old components continue working until new components are fully available
2. **Progressive Migration**: Migrate components one by one to avoid large-scale rewrites
3. **Feature Parity**: Ensure new components have all the functionality of old components
4. **Design Consistency**: Follow new design system specifications (see DESIGN_SYSTEM.md)
5. **Performance Priority**: Optimize bundle size and rendering performance
6. **Designer Collaboration**: Work with UI designers for gradual component encapsulation and UI optimization
## Usage Example ## Usage Example
@@ -24,115 +46,68 @@ function MyComponent() {
@packages/ui/ @packages/ui/
├── src/ ├── src/
│ ├── components/ # Main components directory │ ├── components/ # Main components directory
│ │ ├── base/ # Basic components (buttons, inputs, labels, etc.) │ │ ├── primitives/ # Basic/primitive components (Avatar, ErrorBoundary, Selector, etc.)
│ │ ├── display/ # Display components (cards, lists, tables, etc.) │ │ │ └── shadcn-io/ # shadcn/ui components (dropzone, etc.)
│ │ ├── layout/ # Layout components (containers, grids, spacing, etc.) │ │ ├── icons/ # Icon components (Icon, FileIcons, etc.)
│ │ ── icons/ # Icon components │ │ ── composites/ # Composite components (CodeEditor, ListItem, etc.)
│ │ ├── interactive/ # Interactive components (modals, tooltips, dropdowns, etc.)
│ │ └── composite/ # Composite components (made from multiple base components)
│ ├── hooks/ # Custom React Hooks │ ├── hooks/ # Custom React Hooks
── types/ # TypeScript type definitions ── styles/ # Global styles and CSS variables
│ ├── types/ # TypeScript type definitions
│ ├── utils/ # Utility functions
│ └── index.ts # Main export file
``` ```
### Component Classification Guide ### Component Classification Guide
When submitting PRs, please place components in the correct directory based on their function: When submitting PRs, please place components in the correct directory based on their function:
- **base**: Most basic UI elements like buttons, inputs, switches, labels, etc. - **primitives**: Basic and primitive UI elements, shadcn/ui components
- **display**: Components for displaying content like cards, lists, tables, tabs, etc. - `Avatar`: Avatar components
- **layout**: Components for page layout like containers, grid systems, dividers, etc. - `ErrorBoundary`: Error boundary components
- `Selector`: Selection components
- `shadcn-io/`: Direct shadcn/ui components or adaptations
- **icons**: All icon-related components - **icons**: All icon-related components
- **interactive**: Components requiring user interaction like modals, drawers, tooltips, dropdowns, etc. - `Icon`: Icon factory and basic icons
- **composite**: Composite components made from multiple base components - `FileIcons`: File-specific icons
- Loading/spinner icons (SvgSpinners180Ring, ToolsCallingIcon, etc.)
- **composites**: Complex components made from multiple primitives
- `CodeEditor`: Code editing components
- `ListItem`: List item components
- `ThinkingEffect`: Animation components
- Form and interaction components (DraggableList, EditableNumber, etc.)
## Migration Overview ## Component Extraction Criteria
- **Total Components**: 236 ### Extraction Standards
- **Migrated**: 34
- **Refactored**: 18
- **Pending Migration**: 184
## Component Status Table 1. **Usage Frequency**: Component is used in ≥ 3 places in the codebase
2. **Future Reusability**: Expected to be used in multiple scenarios in the future
3. **Business Complexity**: Component contains complex interaction logic or state management
4. **Maintenance Cost**: Centralized management can reduce maintenance overhead
5. **Design Consistency**: Components that require unified visual and interaction experience
6. **Test Coverage**: As common components, they facilitate unit test writing and maintenance
| Category | Component Name | Migration Status | Refactoring Status | Description | ### Extraction Principles
| ----------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **base** | | | | Base components | - **Single Responsibility**: Each component should only handle one clear function
| | CopyButton | ✅ | ✅ | Copy button | - **Highly Configurable**: Provide flexible configuration options through props
| | CustomTag | ✅ | ✅ | Custom tag | - **Backward Compatible**: New versions maintain API backward compatibility
| | DividerWithText | ✅ | ✅ | Divider with text | - **Complete Documentation**: Provide clear API documentation and usage examples
| | EmojiIcon | ✅ | ✅ | Emoji icon | - **Type Safety**: Use TypeScript to ensure type safety
| | ErrorBoundary | ✅ | ✅ | Error boundary (decoupled via props) |
| | StatusTag | ✅ | ✅ | Unified status tag (merged ErrorTag, SuccessTag, WarnTag, InfoTag) | ### Cases Not Recommended for Extraction
| | IndicatorLight | ✅ | ✅ | Indicator light |
| | Spinner | ✅ | ✅ | Loading spinner | - Simple display components used only on a single page
| | TextBadge | ✅ | ✅ | Text badge | - Overly customized business logic components
| | CustomCollapse | ✅ | ✅ | Custom collapse panel | - Components tightly coupled to specific data sources
| **display** | | | | Display components |
| | Ellipsis | ✅ | ✅ | Text ellipsis |
| | ExpandableText | ✅ | ✅ | Expandable text |
| | ThinkingEffect | ✅ | ✅ | Thinking effect animation |
| | EmojiAvatar | ✅ | ✅ | Emoji avatar |
| | ListItem | ✅ | ✅ | List item |
| | MaxContextCount | ✅ | ✅ | Max context count display |
| | ProviderAvatar | ✅ | ✅ | Provider avatar |
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
| | OGCard | ❌ | ❌ | OG card |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
| | Preview/* | ❌ | ❌ | Preview components |
| **layout** | | | | Layout components |
| | HorizontalScrollContainer | ✅ | ❌ | Horizontal scroll container |
| | Scrollbar | ✅ | ❌ | Scrollbar |
| | Layout/* | ✅ | ✅ | Layout components |
| | Tab/* | ❌ | ❌ | Tab (Redux dependency) |
| | TopView | ❌ | ❌ | Top view (window.api dependency) |
| **icons** | | | | Icon components |
| | Icon | ✅ | ✅ | Icon factory function and predefined icons (merged CopyIcon, DeleteIcon, EditIcon, RefreshIcon, ResetIcon, ToolIcon, VisionIcon, WebSearchIcon, WrapIcon, UnWrapIcon, OcrIcon) |
| | FileIcons | ✅ | ❌ | File icons (FileSvgIcon, FilePngIcon) |
| | ReasoningIcon | ✅ | ❌ | Reasoning icon |
| | SvgSpinners180Ring | ✅ | ❌ | Spinner loading icon |
| | ToolsCallingIcon | ✅ | ❌ | Tools calling icon |
| **interactive** | | | | Interactive components |
| | InfoTooltip | ✅ | ❌ | Info tooltip |
| | HelpTooltip | ✅ | ❌ | Help tooltip |
| | WarnTooltip | ✅ | ❌ | Warning tooltip |
| | EditableNumber | ✅ | ❌ | Editable number |
| | InfoPopover | ✅ | ❌ | Info popover |
| | CollapsibleSearchBar | ✅ | ❌ | Collapsible search bar |
| | ImageToolButton | ✅ | ❌ | Image tool button |
| | DraggableList | ✅ | ❌ | Draggable list |
| | CodeEditor | ✅ | ❌ | Code editor |
| | EmojiPicker | ❌ | ❌ | Emoji picker (useTheme dependency) |
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
| | LanguageSelect | ❌ | ❌ | Language select |
| | TranslateButton | ❌ | ❌ | Translate button (window.api dependency) |
| **composite** | | | | Composite components |
| | - | - | - | No composite components yet |
| **Uncategorized** | | | | Components needing categorization |
| | Popups/* (16+ files) | ❌ | ❌ | Popup components (business coupled) |
| | RichEditor/* (30+ files) | ❌ | ❌ | Rich text editor |
| | MarkdownEditor/* | ❌ | ❌ | Markdown editor |
| | MinApp/* | ❌ | ❌ | Mini app (Redux dependency) |
| | Avatar/* | ❌ | ❌ | Avatar components |
| | ActionTools/* | ❌ | ❌ | Action tools |
| | CodeBlockView/* | ❌ | ❌ | Code block view (window.api dependency) |
| | ContextMenu | ❌ | ❌ | Context menu (Electron API) |
| | WindowControls | ❌ | ❌ | Window controls (Electron API) |
| | ErrorBoundary | ❌ | ❌ | Error boundary (window.api dependency) |
## Migration Steps ## Migration Steps
### Phase 1: Copy Migration (Current Phase) | Phase | Status | Main Tasks | Description |
| --- | --- | --- | --- |
- Copy components as-is to @packages/ui | **Phase 1** | 🚧 **In Progress** | **Design System Integration** | • Integrate design system CSS variables (todocss.css → design-tokens.css → globals.css)<br>• Configure Tailwind CSS to use custom design tokens<br>• Establish basic style guidelines and theme system |
- Retain original dependencies (antd, styled-components, etc.) | **Phase 2** | ⏳ **To Start** | **Component Migration and Optimization** | • Filter components for migration based on extraction criteria<br>• Remove antd dependencies, replace with shadcn/ui<br>• Remove HeroUI dependencies, replace with shadcn/ui<br>• Remove styled-components, replace with Tailwind CSS + design system variables<br>• Optimize component APIs and type definitions |
- Add original path comment at file top | **Phase 3** | ⏳ **To Start** | **UI Refactoring and Optimization** | • Gradually implement UI refactoring with UI designers<br>• Ensure visual consistency and user experience<br>• Performance optimization and code quality improvement |
### Phase 2: Refactor and Optimize
- Remove antd dependencies, replace with HeroUI
- Remove styled-components, replace with Tailwind CSS
- Optimize component APIs and type definitions
## Notes ## Notes
@@ -143,9 +118,27 @@ When submitting PRs, please place components in the correct directory based on t
2. **Can migrate** but need decoupling later: 2. **Can migrate** but need decoupling later:
- Components using i18n (change i18n to props) - Components using i18n (change i18n to props)
- Components using antd (replace with HeroUI later) - Components using antd (replace with shadcn/ui later)
- Components using HeroUI (replace with shadcn/ui later)
3. **Submission Guidelines**: 3. **Submission Guidelines**:
- Each PR should focus on one category of components - Each PR should focus on one category of components
- Ensure all migrated components are exported - Ensure all migrated components are exported
- Update migration status in this document - Follow component extraction criteria, only migrate qualified components
## Design System Integration
### CSS Variable System
- Refer to [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md) for complete design system planning
- Design variables will be managed through CSS variable system, naming conventions TBD
- Support theme switching and responsive design
### Migration Priority Adjustment
1. **High Priority**: Basic components (buttons, inputs, tags, etc.)
2. **Medium Priority**: Display components (cards, lists, tables, etc.)
3. **Low Priority**: Composite components and business-coupled components
### UI Designer Collaboration
- All component designs need confirmation from UI designers
- Gradually implement UI refactoring to maintain visual consistency
- New components must comply with design system specifications
+870
View File
@@ -0,0 +1,870 @@
:root {
/* Typography: Desktop mode */
--Font_family--Heading: Inter;
--Font_weight--Regular: 400px;
--Font_size--Heading--2xl: 60px;
--Font_size--Heading--xl: 48px;
--Font_size--Heading--lg: 40px;
--Font_size--Heading--md: 32px;
--Font_size--Heading--sm: 24px;
--Font_size--Heading--xs: 20px;
--Line_height--Heading--xl: 80px;
--Line_height--Body--lg: 28px;
--Line_height--Body--md: 24px;
--Line_height--Body--sm: 24px;
--Line_height--Body--xs: 20px;
--Paragraph_spacing--Body--lg: 18px;
--Paragraph_spacing--Body--md: 16px;
--Paragraph_spacing--Body--sm: 14px;
--Paragraph_spacing--Body--xs: 12px;
--Line_height--Heading--lg: 60px;
--Line_height--Heading--md: 48px;
--Line_height--Heading--sm: 40px;
--Line_height--Heading--xs: 32px;
--Font_size--Body--lg: 18px;
--Font_size--Body--md: 16px;
--Font_size--Body--sm: 14px;
--Font_size--Body--xs: 12px;
--Font_weight--Italic: 400px;
--Font_weight--Medium: 500px;
--Font_weight--Bold: 700px;
--Font_family--Body: Inter;
--Paragraph_spacing--Heading--2xl: 60px;
--Paragraph_spacing--Heading--xl: 48px;
--Paragraph_spacing--Heading--lg: 40px;
--Paragraph_spacing--Heading--md: 32px;
--Paragraph_spacing--Heading--sm: 24px;
--Paragraph_spacing--Heading--xs: 20px;
--typography_components--h1--font-family: Inter;
--typography_components--h2--font-family: Inter;
--typography_components--h2--font-size: 30px;
--typography_components--h2--line-height: 36px;
--typography_components--h2--font-weight: 600;
--typography_components--h2--letter-spacing: -0.4000000059604645px;
--typography_components--h1--font-size: 36px;
--typography_components--h1--font-size-lg: 48px;
--typography_components--h1--line-height: 40px;
--typography_components--h1--font-weight: 800;
--typography_components--h1--letter-spacing: -0.4000000059604645px;
--typography_components--h3--font-family: Inter;
--typography_components--h3--font-size: 24px;
--typography_components--h3--line-height: 32px;
--typography_components--h3--font-weight: 600;
--typography_components--h3--letter-spacing: -0.4000000059604645px;
--typography_components--h4--font-family: Inter;
--typography_components--h4--font-size: 20px;
--typography_components--h4--line-height: 28px;
--typography_components--h4--font-weight: 600;
--typography_components--h4--letter-spacing: -0.4000000059604645px;
--typography_components--p--font-family: Inter;
--typography_components--p--font-size: 16px;
--typography_components--p--line-height: 28px;
--typography_components--p--font-weight: 400;
--typography_components--p--letter-spacing: 0px;
--typography_components--blockquote--font-family: Inter;
--typography_components--blockquote--font-size: 16px;
--typography_components--blockquote--line-height: 24px;
--typography_components--blockquote--letter-spacing: 0px;
--typography_components--blockquote--font-style: italic;
--typography_components--list--font-family: Inter;
--typography_components--list--font-size: 16px;
--typography_components--list--line-height: 28px;
--typography_components--list--letter-spacing: 0px;
--typography_components--inline_code--font-family: Menlo;
--typography_components--inline_code--font-size: 14px;
--typography_components--inline_code--line-height: 20px;
--typography_components--inline_code--font-weight: 600;
--typography_components--inline_code--letter-spacing: 0px;
--typography_components--lead--font-family: Inter;
--typography_components--lead--font-size: 20px;
--typography_components--lead--line-height: 28px;
--typography_components--lead--font-weight: 400;
--typography_components--lead--letter-spacing: 0px;
--typography_components--large--font-family: Inter;
--typography_components--large--font-size: 18px;
--typography_components--large--line-height: 28px;
--typography_components--large--font-weight: 600;
--typography_components--large--letter-spacing: 0px;
--typography_components--small--font-family: Inter;
--typography_components--small--font-size: 14px;
--typography_components--small--line-height: 14px;
--typography_components--small--font-weight: 500;
--typography_components--table--font-family: Inter;
--typography_components--table--font-size: 16px;
--typography_components--table--font-weight: 400;
--typography_components--table--font-weight-bold: 700;
--typography_components--table--letter-spacing: 0px;
/* Spacing and sizing: Desktop */
--Border_width--sm: 1px;
--Border_width--md: 2px;
--Border_width--lg: 3px;
--Radius--4xs: 4px;
--Radius--3xs: 8px;
--Radius--2xs: 12px;
--Radius--xs: 16px;
--Radius--sm: 24px;
--Radius--md: 32px;
--Radius--lg: 40px;
--Radius--xl: 48px;
--Radius--2xl: 56px;
--Radius--3xl: 64px;
--Radius--round: 999px;
--Spacing--5xs: 4px;
--Spacing--4xs: 8px;
--Spacing--3xs: 12px;
--Spacing--2xs: 16px;
--Spacing--xs: 24px;
--Spacing--sm: 32px;
--Spacing--md: 40px;
--Spacing--lg: 48px;
--Spacing--xl: 56px;
--Spacing--2xl: 64px;
--Spacing--3xl: 72px;
--Spacing--4xl: 80px;
--Spacing--5xl: 88px;
--Spacing--6xl: 96px;
--Spacing--7xl: 104px;
--Spacing--8xl: 112px;
--Sizing--5xs: 4px;
--Sizing--4xs: 8px;
--Sizing--3xs: 12px;
--Sizing--2xs: 16px;
--Sizing--xs: 24px;
--Sizing--sm: 32px;
--Sizing--md: 40px;
--Sizing--lg: 48px;
--Sizing--xl: 56px;
--Sizing--2xl: 64px;
--Sizing--3xl: 72px;
--Sizing--4xl: 80px;
--Sizing--5xl: 88px;
/* Color: Light mode */
--Opacity--Red--Red-100: var(--Primitive--Red--600);
--Opacity--Red--Red-80: hsla(0, 72%, 51%, 0.8);
--Opacity--Red--Red-60: hsla(0, 72%, 51%, 0.6);
--Opacity--Red--Red-40: hsla(0, 72%, 51%, 0.4);
--Opacity--Red--Red-20: hsla(0, 72%, 51%, 0.2);
--Opacity--Red--Red-10: hsla(0, 72%, 51%, 0.1);
--Opacity--Green--Green-100: var(--Primitive--Green--600);
--Opacity--Green--Green-80: hsla(142, 76%, 36%, 0.8);
--Opacity--Green--Green-60: hsla(142, 76%, 36%, 0.6);
--Opacity--Green--Green-40: hsla(142, 76%, 36%, 0.4);
--Opacity--Green--Green-20: hsla(142, 76%, 36%, 0.2);
--Opacity--Green--Green-10: hsla(142, 76%, 36%, 0.1);
--Opacity--Yellow--Yellow-100: var(--Primitive--Amber--400);
--Opacity--Yellow--Yellow-80: hsla(48, 96%, 53%, 0.8);
--Opacity--Yellow--Yellow-60: hsla(48, 96%, 53%, 0.6);
--Opacity--Yellow--Yellow-40: hsla(48, 96%, 53%, 0.4);
--Opacity--Yellow--Yellow-20: hsla(48, 96%, 53%, 0.2);
--Opacity--Yellow--Yellow-10: hsla(48, 96%, 53%, 0.1);
--Opacity--Violet--Violet-100: var(--Primitive--Violet--500);
--Opacity--Violet--Violet-80: hsla(258, 90%, 66%, 0.8);
--Opacity--Violet--Violet-60: hsla(258, 90%, 66%, 0.6);
--Opacity--Violet--Violet-40: hsla(258, 90%, 66%, 0.4);
--Opacity--Violet--Violet-20: hsla(258, 90%, 66%, 0.2);
--Opacity--Violet--Violet-10: hsla(258, 90%, 66%, 0.1);
--Opacity--Indigo--Indigo-100: var(--Primitive--Indigo--500);
--Opacity--Indigo--Indigo-80: hsla(239, 84%, 67%, 0.8);
--Opacity--Indigo--Indigo-60: hsla(239, 84%, 67%, 0.6);
--Opacity--Indigo--Indigo-40: hsla(239, 84%, 67%, 0.4);
--Opacity--Indigo--Indigo-20: hsla(239, 84%, 67%, 0.2);
--Opacity--Indigo--Indigo-10: hsla(239, 84%, 67%, 0.1);
--Opacity--Blue--Blue-100: var(--Primitive--Blue--500);
--Opacity--Blue--Blue-80: hsla(217, 91%, 60%, 0.8);
--Opacity--Blue--Blue-60: hsla(217, 91%, 60%, 0.6);
--Opacity--Blue--Blue-40: hsla(217, 91%, 60%, 0.4);
--Opacity--Blue--Blue-20: hsla(217, 91%, 60%, 0.2);
--Opacity--Blue--Blue-10: hsla(217, 91%, 60%, 0.1);
--Opacity--Grey--Grey-100: var(--Primitive--Gray--500);
--Opacity--Grey--Grey-80: hsla(220, 9%, 46%, 0.8);
--Opacity--Grey--Grey-60: hsla(220, 9%, 46%, 0.6);
--Opacity--Grey--Grey-40: hsla(220, 9%, 46%, 0.4);
--Opacity--Grey--Grey-20: hsla(220, 9%, 46%, 0.2);
--Opacity--Grey--Grey-10: hsla(220, 9%, 46%, 0.1);
--Opacity--White--White-100: var(--Primitive--White);
--Opacity--White--White-80: hsla(0, 0%, 100%, 0.8);
--Opacity--White--White-60: hsla(0, 0%, 100%, 0.6);
--Opacity--White--White-40: hsla(0, 0%, 100%, 0.4);
--Opacity--White--White-20: hsla(0, 0%, 100%, 0.2);
--Opacity--White--White-10: hsla(0, 0%, 100%, 0.1);
--Opacity--White--White-0: hsla(0, 0%, 100%, 0);
--Status--Error--colorErrorBg: var(--color--Red--50);
--Status--Error--colorErrorBgHover: var(--color--Red--100);
--Status--Error--colorErrorBorder: var(--color--Red--200);
--Status--Error--colorErrorBorderHover: var(--color--Red--300);
--Status--Error--colorErrorBase: var(--color--Red--500);
--Status--Error--colorErrorActive: var(--color--Red--600);
--Status--Error--colorErrorTextHover: var(--color--Red--700);
--Status--Error--colorErrorText: var(--color--Red--800);
--Status--Success--colorSuccessBg: var(--color--Green--50);
--Status--Success--colorSuccessBgHover: var(--color--Green--100);
--Status--Success--colorSuccessBase: var(--color--Green--500);
--Status--Success--colorSuccessTextHover: var(--color--Green--700);
--Status--Warning--colorWarningBg: var(--color--Yellow--50);
--Status--Warning--colorWarningBgHover: var(--color--Yellow--100);
--Status--Warning--colorWarningBase: var(--color--Yellow--500);
--Status--Warning--colorWarningActive: var(--color--Yellow--600);
--Status--Warning--colorWarningTextHover: var(--color--Yellow--700);
--Primitive--Black: hsla(0, 0%, 0%, 1);
--Primitive--White: hsla(0, 0%, 100%, 1);
--Brand--Base_Colors--Primary: var(--Primitive--Lime--500);
--Primitive--Neutral--50: hsla(0, 0%, 98%, 1);
--Primitive--Neutral--100: hsla(0, 0%, 96%, 1);
--Primitive--Neutral--200: hsla(0, 0%, 90%, 1);
--Primitive--Neutral--300: hsla(0, 0%, 83%, 1);
--Primitive--Neutral--400: hsla(0, 0%, 64%, 1);
--Primitive--Neutral--500: hsla(0, 0%, 45%, 1);
--Primitive--Neutral--600: hsla(215, 14%, 34%, 1);
--Primitive--Neutral--700: hsla(0, 0%, 25%, 1);
--Primitive--Neutral--800: hsla(0, 0%, 15%, 1);
--Primitive--Neutral--900: hsla(0, 0%, 9%, 1);
--Primitive--Neutral--950: hsla(0, 0%, 4%, 1);
--Primitive--Stone--50: hsla(60, 9%, 98%, 1);
--Primitive--Stone--100: hsla(60, 5%, 96%, 1);
--Primitive--Stone--200: hsla(20, 6%, 90%, 1);
--Primitive--Stone--300: hsla(24, 6%, 83%, 1);
--Primitive--Stone--400: hsla(24, 5%, 64%, 1);
--Primitive--Stone--500: hsla(25, 5%, 45%, 1);
--Primitive--Stone--600: hsla(33, 5%, 32%, 1);
--Primitive--Stone--700: hsla(30, 6%, 25%, 1);
--Primitive--Stone--800: hsla(12, 6%, 15%, 1);
--Primitive--Stone--900: hsla(24, 10%, 10%, 1);
--Primitive--Stone--950: hsla(20, 14%, 4%, 1);
--Primitive--Zinc--50: hsla(0, 0%, 98%, 1);
--Primitive--Zinc--100: hsla(240, 5%, 96%, 1);
--Primitive--Zinc--200: hsla(240, 6%, 90%, 1);
--Primitive--Zinc--300: hsla(240, 5%, 84%, 1);
--Primitive--Zinc--400: hsla(240, 5%, 65%, 1);
--Primitive--Zinc--500: hsla(240, 4%, 46%, 1);
--Primitive--Zinc--600: hsla(240, 5%, 34%, 1);
--Primitive--Zinc--700: hsla(240, 5%, 26%, 1);
--Primitive--Zinc--800: hsla(240, 4%, 16%, 1);
--Primitive--Zinc--900: hsla(240, 6%, 10%, 1);
--Primitive--Zinc--950: hsla(240, 10%, 4%, 1);
--Primitive--Slate--50: hsla(210, 40%, 98%, 1);
--Primitive--Slate--100: hsla(210, 40%, 96%, 1);
--Primitive--Slate--200: hsla(214, 32%, 91%, 1);
--Primitive--Slate--300: hsla(213, 27%, 84%, 1);
--Primitive--Slate--400: hsla(215, 20%, 65%, 1);
--Primitive--Slate--500: hsla(215, 16%, 47%, 1);
--Primitive--Slate--600: hsla(215, 19%, 35%, 1);
--Primitive--Slate--700: hsla(215, 25%, 27%, 1);
--Primitive--Slate--800: hsla(217, 33%, 17%, 1);
--Primitive--Slate--900: hsla(222, 47%, 11%, 1);
--Primitive--Slate--950: hsla(229, 84%, 5%, 1);
--Primitive--Gray--50: hsla(210, 20%, 98%, 1);
--Primitive--Gray--100: hsla(220, 14%, 96%, 1);
--Primitive--Gray--200: hsla(220, 13%, 91%, 1);
--Primitive--Gray--300: hsla(216, 12%, 84%, 1);
--Primitive--Gray--400: hsla(218, 11%, 65%, 1);
--Primitive--Gray--500: hsla(220, 9%, 46%, 1);
--Primitive--Gray--600: hsla(0, 0%, 32%, 1);
--Primitive--Gray--700: hsla(217, 19%, 27%, 1);
--Primitive--Gray--800: hsla(215, 28%, 17%, 1);
--Primitive--Gray--900: hsla(221, 39%, 11%, 1);
--Primitive--Gray--950: hsla(224, 71%, 4%, 1);
--Primitive--Red--50: hsla(0, 86%, 97%, 1);
--Primitive--Red--100: hsla(0, 93%, 94%, 1);
--Primitive--Red--200: hsla(0, 96%, 89%, 1);
--Primitive--Red--300: hsla(0, 94%, 82%, 1);
--Primitive--Red--400: hsla(0, 91%, 71%, 1);
--Primitive--Red--500: hsla(0, 84%, 60%, 1);
--Primitive--Red--600: hsla(0, 72%, 51%, 1);
--Primitive--Red--700: hsla(0, 74%, 42%, 1);
--Primitive--Red--800: hsla(0, 70%, 35%, 1);
--Primitive--Red--900: hsla(0, 63%, 31%, 1);
--Primitive--Red--950: hsla(0, 75%, 15%, 1);
--Primitive--Orange--50: hsla(33, 100%, 96%, 1);
--Primitive--Orange--100: hsla(34, 100%, 92%, 1);
--Primitive--Orange--200: hsla(32, 98%, 83%, 1);
--Primitive--Orange--300: hsla(31, 97%, 72%, 1);
--Primitive--Orange--400: hsla(27, 96%, 61%, 1);
--Primitive--Orange--500: hsla(25, 95%, 53%, 1);
--Primitive--Orange--600: hsla(21, 90%, 48%, 1);
--Primitive--Orange--700: hsla(17, 88%, 40%, 1);
--Primitive--Orange--800: hsla(15, 79%, 34%, 1);
--Primitive--Orange--900: hsla(15, 75%, 28%, 1);
--Primitive--Orange--950: hsla(13, 81%, 15%, 1);
--Primitive--Amber--50: hsla(48, 100%, 96%, 1);
--Primitive--Amber--100: hsla(48, 96%, 89%, 1);
--Primitive--Amber--200: hsla(48, 97%, 77%, 1);
--Primitive--Amber--300: hsla(46, 97%, 65%, 1);
--Primitive--Amber--400: hsla(43, 96%, 56%, 1);
--Primitive--Amber--500: hsla(38, 92%, 50%, 1);
--Primitive--Amber--600: hsla(32, 95%, 44%, 1);
--Primitive--Amber--700: hsla(26, 90%, 37%, 1);
--Primitive--Amber--800: hsla(23, 83%, 31%, 1);
--Primitive--Amber--900: hsla(22, 78%, 26%, 1);
--Primitive--Amber--950: hsla(21, 92%, 14%, 1);
--Primitive--Yellow--50: hsla(55, 92%, 95%, 1);
--Primitive--Yellow--100: hsla(55, 97%, 88%, 1);
--Primitive--Yellow--200: hsla(53, 98%, 77%, 1);
--Primitive--Yellow--300: hsla(50, 98%, 64%, 1);
--Primitive--Yellow--400: hsla(48, 96%, 53%, 1);
--Primitive--Yellow--500: hsla(45, 93%, 47%, 1);
--Primitive--Yellow--600: hsla(41, 96%, 40%, 1);
--Primitive--Yellow--700: hsla(35, 92%, 33%, 1);
--Primitive--Yellow--800: hsla(32, 81%, 29%, 1);
--Primitive--Yellow--900: hsla(28, 73%, 26%, 1);
--Primitive--Yellow--950: hsla(26, 83%, 14%, 1);
--Primitive--Lime--50: hsla(78, 92%, 95%, 1);
--Primitive--Lime--100: hsla(80, 89%, 89%, 1);
--Primitive--Lime--200: hsla(81, 88%, 80%, 1);
--Primitive--Lime--300: hsla(82, 85%, 67%, 1);
--Primitive--Lime--400: hsla(83, 78%, 55%, 1);
--Primitive--Lime--500: hsla(84, 81%, 44%, 1);
--Primitive--Lime--600: hsla(85, 85%, 35%, 1);
--Primitive--Lime--700: hsla(86, 78%, 27%, 1);
--Primitive--Lime--800: hsla(86, 69%, 23%, 1);
--Primitive--Lime--900: hsla(88, 61%, 20%, 1);
--Primitive--Lime--950: hsla(89, 80%, 10%, 1);
--Primitive--Green--50: hsla(138, 76%, 97%, 1);
--Primitive--Green--100: hsla(141, 84%, 93%, 1);
--Primitive--Green--200: hsla(141, 79%, 85%, 1);
--Primitive--Green--300: hsla(142, 77%, 73%, 1);
--Primitive--Green--400: hsla(142, 69%, 58%, 1);
--Primitive--Green--500: hsla(142, 71%, 45%, 1);
--Primitive--Green--600: hsla(142, 76%, 36%, 1);
--Primitive--Green--700: hsla(142, 72%, 29%, 1);
--Primitive--Green--800: hsla(143, 64%, 24%, 1);
--Primitive--Green--900: hsla(144, 61%, 20%, 1);
--Primitive--Green--950: hsla(145, 80%, 10%, 1);
--Primitive--Emerald--50: hsla(152, 81%, 96%, 1);
--Primitive--Emerald--100: hsla(149, 80%, 90%, 1);
--Primitive--Emerald--200: hsla(152, 76%, 80%, 1);
--Primitive--Emerald--300: hsla(156, 72%, 67%, 1);
--Primitive--Emerald--400: hsla(158, 64%, 52%, 1);
--Primitive--Emerald--500: hsla(160, 84%, 39%, 1);
--Primitive--Emerald--600: hsla(161, 94%, 30%, 1);
--Primitive--Emerald--700: hsla(163, 94%, 24%, 1);
--Primitive--Emerald--800: hsla(163, 88%, 20%, 1);
--Primitive--Emerald--900: hsla(164, 86%, 16%, 1);
--Primitive--Emerald--950: hsla(166, 91%, 9%, 1);
--Primitive--Teal--50: hsla(166, 76%, 97%, 1);
--Primitive--Teal--100: hsla(167, 85%, 89%, 1);
--Primitive--Teal--200: hsla(168, 84%, 78%, 1);
--Primitive--Teal--300: hsla(171, 77%, 64%, 1);
--Primitive--Teal--400: hsla(172, 66%, 50%, 1);
--Primitive--Teal--500: hsla(173, 80%, 40%, 1);
--Primitive--Teal--600: hsla(175, 84%, 32%, 1);
--Primitive--Teal--700: hsla(175, 77%, 26%, 1);
--Primitive--Teal--800: hsla(176, 69%, 22%, 1);
--Primitive--Teal--900: hsla(176, 61%, 19%, 1);
--Primitive--Teal--950: hsla(179, 84%, 10%, 1);
--Primitive--Cyan--50: hsla(183, 100%, 96%, 1);
--Primitive--Cyan--100: hsla(185, 96%, 90%, 1);
--Primitive--Cyan--200: hsla(186, 94%, 82%, 1);
--Primitive--Cyan--300: hsla(187, 92%, 69%, 1);
--Primitive--Cyan--400: hsla(188, 86%, 53%, 1);
--Primitive--Cyan--500: hsla(189, 94%, 43%, 1);
--Primitive--Cyan--600: hsla(192, 91%, 36%, 1);
--Primitive--Cyan--700: hsla(193, 82%, 31%, 1);
--Primitive--Cyan--800: hsla(194, 70%, 27%, 1);
--Primitive--Cyan--900: hsla(196, 64%, 24%, 1);
--Primitive--Cyan--950: hsla(197, 79%, 15%, 1);
--Primitive--Sky--50: hsla(204, 100%, 97%, 1);
--Primitive--Sky--100: hsla(204, 94%, 94%, 1);
--Primitive--Sky--200: hsla(201, 94%, 86%, 1);
--Primitive--Sky--300: hsla(199, 95%, 74%, 1);
--Primitive--Sky--400: hsla(198, 93%, 60%, 1);
--Primitive--Sky--500: hsla(199, 89%, 48%, 1);
--Primitive--Sky--600: hsla(200, 98%, 39%, 1);
--Primitive--Sky--700: hsla(201, 96%, 32%, 1);
--Primitive--Sky--800: hsla(201, 90%, 27%, 1);
--Primitive--Sky--900: hsla(202, 80%, 24%, 1);
--Primitive--Sky--950: hsla(204, 80%, 16%, 1);
--Primitive--Blue--50: hsla(214, 100%, 97%, 1);
--Primitive--Blue--100: hsla(214, 95%, 93%, 1);
--Primitive--Blue--200: hsla(213, 97%, 87%, 1);
--Primitive--Blue--300: hsla(212, 96%, 78%, 1);
--Primitive--Blue--400: hsla(213, 94%, 68%, 1);
--Primitive--Blue--500: hsla(217, 91%, 60%, 1);
--Primitive--Blue--600: hsla(221, 83%, 53%, 1);
--Primitive--Blue--700: hsla(224, 76%, 48%, 1);
--Primitive--Blue--800: hsla(226, 71%, 40%, 1);
--Primitive--Blue--900: hsla(224, 64%, 33%, 1);
--Primitive--Blue--950: hsla(226, 57%, 21%, 1);
--Primitive--Indigo--50: hsla(226, 100%, 97%, 1);
--Primitive--Indigo--100: hsla(226, 100%, 94%, 1);
--Primitive--Indigo--200: hsla(228, 96%, 89%, 1);
--Primitive--Indigo--300: hsla(230, 94%, 82%, 1);
--Primitive--Indigo--400: hsla(234, 89%, 74%, 1);
--Primitive--Indigo--500: hsla(239, 84%, 67%, 1);
--Primitive--Indigo--600: hsla(243, 75%, 59%, 1);
--Primitive--Indigo--700: hsla(245, 58%, 51%, 1);
--Primitive--Indigo--800: hsla(244, 55%, 41%, 1);
--Primitive--Indigo--900: hsla(242, 47%, 34%, 1);
--Primitive--Indigo--950: hsla(244, 47%, 20%, 1);
--Primitive--Violet--50: hsla(250, 100%, 98%, 1);
--Primitive--Violet--100: hsla(251, 91%, 95%, 1);
--Primitive--Violet--200: hsla(251, 95%, 92%, 1);
--Primitive--Violet--300: hsla(253, 95%, 85%, 1);
--Primitive--Violet--400: hsla(255, 92%, 76%, 1);
--Primitive--Violet--500: hsla(258, 90%, 66%, 1);
--Primitive--Violet--600: hsla(262, 83%, 58%, 1);
--Primitive--Violet--700: hsla(263, 70%, 50%, 1);
--Primitive--Violet--800: hsla(263, 69%, 42%, 1);
--Primitive--Violet--900: hsla(264, 67%, 35%, 1);
--Primitive--Violet--950: hsla(262, 78%, 23%, 1);
--Primitive--Purple--50: hsla(270, 100%, 98%, 1);
--Primitive--Purple--100: hsla(269, 100%, 95%, 1);
--Primitive--Purple--200: hsla(269, 100%, 92%, 1);
--Primitive--Purple--300: hsla(269, 97%, 85%, 1);
--Primitive--Purple--400: hsla(270, 95%, 75%, 1);
--Primitive--Purple--500: hsla(271, 91%, 65%, 1);
--Primitive--Purple--600: hsla(271, 81%, 56%, 1);
--Primitive--Purple--700: hsla(272, 72%, 47%, 1);
--Primitive--Purple--800: hsla(273, 67%, 39%, 1);
--Primitive--Purple--900: hsla(274, 66%, 32%, 1);
--Primitive--Purple--950: hsla(274, 87%, 21%, 1);
--Primitive--Fuchsia--50: hsla(289, 100%, 98%, 1);
--Primitive--Fuchsia--100: hsla(287, 100%, 95%, 1);
--Primitive--Fuchsia--200: hsla(288, 96%, 91%, 1);
--Primitive--Fuchsia--300: hsla(291, 93%, 83%, 1);
--Primitive--Fuchsia--400: hsla(292, 91%, 73%, 1);
--Primitive--Fuchsia--500: hsla(292, 84%, 61%, 1);
--Primitive--Fuchsia--600: hsla(293, 69%, 49%, 1);
--Primitive--Fuchsia--700: hsla(295, 72%, 40%, 1);
--Primitive--Fuchsia--800: hsla(295, 70%, 33%, 1);
--Primitive--Fuchsia--900: hsla(297, 64%, 28%, 1);
--Primitive--Fuchsia--950: hsla(297, 90%, 16%, 1);
--Primitive--Pink--50: hsla(327, 73%, 97%, 1);
--Primitive--Pink--100: hsla(326, 78%, 95%, 1);
--Primitive--Pink--200: hsla(326, 85%, 90%, 1);
--Primitive--Pink--300: hsla(327, 87%, 82%, 1);
--Primitive--Pink--400: hsla(329, 86%, 70%, 1);
--Primitive--Pink--500: hsla(330, 81%, 60%, 1);
--Primitive--Pink--600: hsla(333, 71%, 51%, 1);
--Primitive--Pink--700: hsla(335, 78%, 42%, 1);
--Primitive--Pink--800: hsla(336, 74%, 35%, 1);
--Primitive--Pink--900: hsla(336, 69%, 30%, 1);
--Primitive--Pink--950: hsla(336, 84%, 17%, 1);
--Primitive--Rose--50: hsla(356, 100%, 97%, 1);
--Primitive--Rose--100: hsla(356, 100%, 95%, 1);
--Primitive--Rose--200: hsla(353, 96%, 90%, 1);
--Primitive--Rose--300: hsla(353, 96%, 82%, 1);
--Primitive--Rose--400: hsla(351, 95%, 71%, 1);
--Primitive--Rose--500: hsla(350, 89%, 60%, 1);
--Primitive--Rose--600: hsla(347, 77%, 50%, 1);
--Primitive--Rose--700: hsla(345, 83%, 41%, 1);
--Primitive--Rose--800: hsla(343, 80%, 35%, 1);
--Primitive--Rose--900: hsla(342, 75%, 30%, 1);
--Primitive--Rose--950: hsla(343, 88%, 16%, 1);
--Brand--Base_Colors--Destructive: var(--Primitive--Red--500);
--Brand--Base_Colors--Success: var(--Primitive--Green--500);
--Brand--Base_Colors--Warning: var(--Primitive--Amber--500);
--Brand--Base_Colors--White: var(--Primitive--White);
--Brand--Base_Colors--Black: var(--Primitive--Black);
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--50); /*页面背景色:应用在整个页面的最底层。*/
--Brand--Semantic_Colors--Background-subtle: hsla(
0,
0%,
0%,
0.02
); /*细微背景色:用于需要与主背景有微弱区分的区域,如代码块背景。*/
--Brand--Semantic_Colors--Foreground: hsla(0, 0%, 0%, 0.9); /*主要前景/文字色:用于正文、标题等。*/
--Brand--Semantic_Colors--Foreground-secondary: hsla(0, 0%, 0%, 0.6); /*次要前景/文字色:用于辅助性文本、描述。*/
--Brand--Semantic_Colors--Foreground-muted: hsla(0, 0%, 0%, 0.4); /*静默前景/文字色:用于禁用状态的文字、占位符。*/
--Brand--Semantic_Colors--Border: hsla(0, 0%, 0%, 0.1); /*默认边框色:用于卡片、输入框、分隔线。*/
--Brand--Semantic_Colors--Border-hover: hsla(0, 0%, 0%, 0.2); /*激活边框色:用于元素被按下或激活时的边框。*/
--Brand--Semantic_Colors--Border-active: hsla(0, 0%, 0%, 0.3); /*激活边框色:用于元素被按下或激活时的边框。*/
--Brand--Semantic_Colors--Ring: hsla(
84,
81%,
44%,
0.4
); /*聚焦环颜色:用于输入框等元素在聚焦 (Focus) 状态下的外发光。*/
--Brand--UI_Element_Colors--Modal--Backdrop: hsla(0, 0%, 0%, 0.4);
--Brand--UI_Element_Colors--Modal--Thumb: hsla(0, 0%, 0%, 0.2);
--Brand--UI_Element_Colors--Modal--Thumb_Hover: hsla(0, 0%, 0%, 0.3);
--Brand--UI_Element_Colors--Icon--Default: var(--Brand--Semantic_Colors--Foreground-secondary);
--Brand--UI_Element_Colors--Icon--Hover: var(--Brand--Semantic_Colors--Foreground);
--Brand--UI_Element_Colors--Input_Select--Background: var(--Brand--Base_Colors--White);
--Brand--UI_Element_Colors--Input_Select--Border: var(--Brand--Semantic_Colors--Border);
--Brand--UI_Element_Colors--Input_Select--Border_Hover: var(--Brand--Semantic_Colors--Border-hover);
--Brand--UI_Element_Colors--Input_Select--Border_Focus: var(--Brand--Base_Colors--Primary);
--Brand--UI_Element_Colors--Primary_Button--Background: var(--Brand--Base_Colors--Primary);
--Brand--UI_Element_Colors--Card_Container--Background: var(--Brand--Base_Colors--White);
--Brand--UI_Element_Colors--Card_Container--Border: var(--Brand--Semantic_Colors--Border);
--Brand--UI_Element_Colors--Ghost_Button--Background: hsla(0, 0%, 0%, 0);
--Brand--UI_Element_Colors--Ghost_Button--Text: var(--Brand--Semantic_Colors--Foreground);
--Brand--UI_Element_Colors--Ghost_Button--Background_Hover: hsla(0, 0%, 0%, 0.05);
--Brand--UI_Element_Colors--Ghost_Button--Background_Active: hsla(0, 0%, 0%, 0.1);
--Brand--UI_Element_Colors--Secondary_Button--Background: hsla(0, 0%, 0%, 0.05);
--Brand--UI_Element_Colors--Secondary_Button--Text: var(--Brand--Semantic_Colors--Foreground);
--Brand--UI_Element_Colors--Secondary_Button--Background_Hover: hsla(0, 0%, 0%, 0.85);
--Brand--UI_Element_Colors--Secondary_Button--Background_Active: hsla(0, 0%, 0%, 0.7);
--Brand--UI_Element_Colors--Secondary_Button--Border: var(--Brand--Semantic_Colors--Border);
--Brand--UI_Element_Colors--Primary_Button--Text: var(--Brand--Base_Colors--White);
--Brand--UI_Element_Colors--Primary_Button--Background_Hover: hsla(84, 81%, 44%, 0.85);
--Brand--UI_Element_Colors--Primary_Button--2nd_Background: hsla(84, 81%, 44%, 0.1);
--Brand--UI_Element_Colors--Primary_Button--3rd_Background: hsla(84, 81%, 44%, 0.05);
--Brand--UI_Element_Colors--Primary_Button--Background_Active: hsla(84, 81%, 44%, 0.7);
--Boolean: false;
/* Color: Dark mode */
--Opacity--Red--Red-100: var(--Primitive--Red--600);
--Opacity--Red--Red-80: hsla(0, 72%, 51%, 0.8);
--Opacity--Red--Red-60: hsla(0, 72%, 51%, 0.6);
--Opacity--Red--Red-40: hsla(0, 72%, 51%, 0.4);
--Opacity--Red--Red-20: hsla(0, 72%, 51%, 0.2);
--Opacity--Red--Red-10: hsla(0, 72%, 51%, 0.1);
--Opacity--Green--Green-100: var(--Primitive--Green--600);
--Opacity--Green--Green-80: hsla(142, 76%, 36%, 0.8);
--Opacity--Green--Green-60: hsla(142, 76%, 36%, 0.6);
--Opacity--Green--Green-40: hsla(142, 76%, 36%, 0.4);
--Opacity--Green--Green-20: hsla(142, 76%, 36%, 0.2);
--Opacity--Green--Green-10: hsla(142, 76%, 36%, 0.1);
--Opacity--Yellow--Yellow-100: var(--Primitive--Yellow--400);
--Opacity--Yellow--Yellow-80: hsla(48, 96%, 53%, 0.8);
--Opacity--Yellow--Yellow-60: hsla(48, 96%, 53%, 0.6);
--Opacity--Yellow--Yellow-40: hsla(48, 96%, 53%, 0.4);
--Opacity--Yellow--Yellow-20: hsla(48, 96%, 53%, 0.2);
--Opacity--Yellow--Yellow-10: hsla(48, 96%, 53%, 0.1);
--Opacity--Violet--Violet-100: var(--Primitive--Violet--500);
--Opacity--Violet--Violet-80: hsla(258, 90%, 66%, 0.8);
--Opacity--Violet--Violet-60: hsla(258, 90%, 66%, 0.6);
--Opacity--Violet--Violet-40: hsla(258, 90%, 66%, 0.4);
--Opacity--Violet--Violet-20: hsla(258, 90%, 66%, 0.2);
--Opacity--Violet--Violet-10: hsla(258, 90%, 66%, 0.1);
--Opacity--Indigo--Indigo-100: var(--Primitive--Indigo--500);
--Opacity--Indigo--Indigo-80: hsla(239, 84%, 67%, 0.8);
--Opacity--Indigo--Indigo-60: hsla(239, 84%, 67%, 0.6);
--Opacity--Indigo--Indigo-40: hsla(239, 84%, 67%, 0.4);
--Opacity--Indigo--Indigo-20: hsla(239, 84%, 67%, 0.2);
--Opacity--Indigo--Indigo-10: hsla(239, 84%, 67%, 0.1);
--Opacity--Blue--Blue-100: var(--Primitive--Blue--500);
--Opacity--Blue--Blue-80: hsla(217, 91%, 60%, 0.8);
--Opacity--Blue--Blue-60: hsla(217, 91%, 60%, 0.6);
--Opacity--Blue--Blue-40: hsla(217, 91%, 60%, 0.4);
--Opacity--Blue--Blue-20: hsla(217, 91%, 60%, 0.2);
--Opacity--Blue--Blue-10: hsla(217, 91%, 60%, 0.1);
--Opacity--Grey--Grey-100: var(--Primitive--Gray--500);
--Opacity--Grey--Grey-80: hsla(220, 9%, 46%, 0.8);
--Opacity--Grey--Grey-60: hsla(220, 9%, 46%, 0.6);
--Opacity--Grey--Grey-40: hsla(220, 9%, 46%, 0.4);
--Opacity--Grey--Grey-20: hsla(220, 9%, 46%, 0.2);
--Opacity--Grey--Grey-10: hsla(220, 9%, 46%, 0.1);
--Opacity--White--White-100: var(--Primitive--White);
--Opacity--White--White-80: hsla(0, 0%, 100%, 0.8);
--Opacity--White--White-60: hsla(0, 0%, 100%, 0.6);
--Opacity--White--White-40: hsla(0, 0%, 100%, 0.4);
--Opacity--White--White-20: hsla(0, 0%, 100%, 0.2);
--Opacity--White--White-10: hsla(0, 0%, 100%, 0.1);
--Opacity--White--White-0: hsla(0, 0%, 100%, 0);
--Status--Error--colorErrorBg: var(--color--Red--900);
--Status--Error--colorErrorBgHover: var(--color--Red--800);
--Status--Error--colorErrorBorder: var(--color--Red--700);
--Status--Error--colorErrorBorderHover: var(--color--Red--600);
--Status--Error--colorErrorBase: var(--color--Red--400);
--Status--Error--colorErrorActive: var(--color--Red--300);
--Status--Error--colorErrorTextHover: var(--color--Red--200);
--Status--Error--colorErrorText: var(--color--Red--100);
--Status--Success--colorSuccessBg: var(--color--Green--900);
--Status--Success--colorSuccessBgHover: var(--color--Green--800);
--Status--Success--colorSuccessBase: var(--color--Green--400);
--Status--Success--colorSuccessTextHover: var(--color--Green--200);
--Status--Warning--colorWarningBg: var(--color--Yellow--900);
--Status--Warning--colorWarningBgHover: var(--color--Yellow--800);
--Status--Warning--colorWarningBase: var(--color--Yellow--400);
--Status--Warning--colorWarningActive: var(--color--Yellow--300);
--Status--Warning--colorWarningTextHover: var(--color--Yellow--200);
--Primitive--Black: hsla(0, 0%, 0%, 1);
--Primitive--White: hsla(0, 0%, 100%, 1);
--Brand--Base_Colors--Primary: var(--Primitive--Lime--500);
--Primitive--Neutral--50: hsla(0, 0%, 98%, 1);
--Primitive--Neutral--100: hsla(0, 0%, 96%, 1);
--Primitive--Neutral--200: hsla(0, 0%, 90%, 1);
--Primitive--Neutral--300: hsla(0, 0%, 83%, 1);
--Primitive--Neutral--400: hsla(0, 0%, 64%, 1);
--Primitive--Neutral--500: hsla(0, 0%, 45%, 1);
--Primitive--Neutral--600: hsla(215, 14%, 34%, 1);
--Primitive--Neutral--700: hsla(0, 0%, 25%, 1);
--Primitive--Neutral--800: hsla(0, 0%, 15%, 1);
--Primitive--Neutral--900: hsla(0, 0%, 9%, 1);
--Primitive--Neutral--950: hsla(0, 0%, 4%, 1);
--Primitive--Stone--50: hsla(60, 9%, 98%, 1);
--Primitive--Stone--100: hsla(60, 5%, 96%, 1);
--Primitive--Stone--200: hsla(20, 6%, 90%, 1);
--Primitive--Stone--300: hsla(24, 6%, 83%, 1);
--Primitive--Stone--400: hsla(24, 5%, 64%, 1);
--Primitive--Stone--500: hsla(25, 5%, 45%, 1);
--Primitive--Stone--600: hsla(33, 5%, 32%, 1);
--Primitive--Stone--700: hsla(30, 6%, 25%, 1);
--Primitive--Stone--800: hsla(12, 6%, 15%, 1);
--Primitive--Stone--900: hsla(24, 10%, 10%, 1);
--Primitive--Stone--950: hsla(20, 14%, 4%, 1);
--Primitive--Zinc--50: hsla(0, 0%, 98%, 1);
--Primitive--Zinc--100: hsla(240, 5%, 96%, 1);
--Primitive--Zinc--200: hsla(240, 6%, 90%, 1);
--Primitive--Zinc--300: hsla(240, 5%, 84%, 1);
--Primitive--Zinc--400: hsla(240, 5%, 65%, 1);
--Primitive--Zinc--500: hsla(240, 4%, 46%, 1);
--Primitive--Zinc--600: hsla(240, 5%, 34%, 1);
--Primitive--Zinc--700: hsla(240, 5%, 26%, 1);
--Primitive--Zinc--800: hsla(240, 4%, 16%, 1);
--Primitive--Zinc--900: hsla(240, 6%, 10%, 1);
--Primitive--Zinc--950: hsla(240, 10%, 4%, 1);
--Primitive--Slate--50: hsla(210, 40%, 98%, 1);
--Primitive--Slate--100: hsla(210, 40%, 96%, 1);
--Primitive--Slate--200: hsla(214, 32%, 91%, 1);
--Primitive--Slate--300: hsla(213, 27%, 84%, 1);
--Primitive--Slate--400: hsla(215, 20%, 65%, 1);
--Primitive--Slate--500: hsla(215, 16%, 47%, 1);
--Primitive--Slate--600: hsla(215, 19%, 35%, 1);
--Primitive--Slate--700: hsla(215, 25%, 27%, 1);
--Primitive--Slate--800: hsla(217, 33%, 17%, 1);
--Primitive--Slate--900: hsla(222, 47%, 11%, 1);
--Primitive--Slate--950: hsla(229, 84%, 5%, 1);
--Primitive--Gray--50: hsla(210, 20%, 98%, 1);
--Primitive--Gray--100: hsla(220, 14%, 96%, 1);
--Primitive--Gray--200: hsla(220, 13%, 91%, 1);
--Primitive--Gray--300: hsla(216, 12%, 84%, 1);
--Primitive--Gray--400: hsla(218, 11%, 65%, 1);
--Primitive--Gray--500: hsla(220, 9%, 46%, 1);
--Primitive--Gray--600: hsla(0, 0%, 32%, 1);
--Primitive--Gray--700: hsla(217, 19%, 27%, 1);
--Primitive--Gray--800: hsla(215, 28%, 17%, 1);
--Primitive--Gray--900: hsla(221, 39%, 11%, 1);
--Primitive--Gray--950: hsla(224, 71%, 4%, 1);
--Primitive--Red--50: hsla(0, 86%, 97%, 1);
--Primitive--Red--100: hsla(0, 93%, 94%, 1);
--Primitive--Red--200: hsla(0, 96%, 89%, 1);
--Primitive--Red--300: hsla(0, 94%, 82%, 1);
--Primitive--Red--400: hsla(0, 91%, 71%, 1);
--Primitive--Red--500: hsla(0, 84%, 60%, 1);
--Primitive--Red--600: hsla(0, 72%, 51%, 1);
--Primitive--Red--700: hsla(0, 74%, 42%, 1);
--Primitive--Red--800: hsla(0, 70%, 35%, 1);
--Primitive--Red--900: hsla(0, 63%, 31%, 1);
--Primitive--Red--950: hsla(0, 75%, 15%, 1);
--Primitive--Orange--50: hsla(33, 100%, 96%, 1);
--Primitive--Orange--100: hsla(34, 100%, 92%, 1);
--Primitive--Orange--200: hsla(32, 98%, 83%, 1);
--Primitive--Orange--300: hsla(31, 97%, 72%, 1);
--Primitive--Orange--400: hsla(27, 96%, 61%, 1);
--Primitive--Orange--500: hsla(25, 95%, 53%, 1);
--Primitive--Orange--600: hsla(21, 90%, 48%, 1);
--Primitive--Orange--700: hsla(17, 88%, 40%, 1);
--Primitive--Orange--800: hsla(15, 79%, 34%, 1);
--Primitive--Orange--900: hsla(15, 75%, 28%, 1);
--Primitive--Orange--950: hsla(13, 81%, 15%, 1);
--Primitive--Amber--50: hsla(48, 100%, 96%, 1);
--Primitive--Amber--100: hsla(48, 96%, 89%, 1);
--Primitive--Amber--200: hsla(48, 97%, 77%, 1);
--Primitive--Amber--300: hsla(46, 97%, 65%, 1);
--Primitive--Amber--400: hsla(43, 96%, 56%, 1);
--Primitive--Amber--500: hsla(38, 92%, 50%, 1);
--Primitive--Amber--600: hsla(32, 95%, 44%, 1);
--Primitive--Amber--700: hsla(26, 90%, 37%, 1);
--Primitive--Amber--800: hsla(23, 83%, 31%, 1);
--Primitive--Amber--900: hsla(22, 78%, 26%, 1);
--Primitive--Amber--950: hsla(21, 92%, 14%, 1);
--Primitive--Yellow--50: hsla(55, 92%, 95%, 1);
--Primitive--Yellow--100: hsla(55, 97%, 88%, 1);
--Primitive--Yellow--200: hsla(53, 98%, 77%, 1);
--Primitive--Yellow--300: hsla(50, 98%, 64%, 1);
--Primitive--Yellow--400: hsla(48, 96%, 53%, 1);
--Primitive--Yellow--500: hsla(45, 93%, 47%, 1);
--Primitive--Yellow--600: hsla(41, 96%, 40%, 1);
--Primitive--Yellow--700: hsla(35, 92%, 33%, 1);
--Primitive--Yellow--800: hsla(32, 81%, 29%, 1);
--Primitive--Yellow--900: hsla(28, 73%, 26%, 1);
--Primitive--Yellow--950: hsla(26, 83%, 14%, 1);
--Primitive--Lime--50: hsla(78, 92%, 95%, 1);
--Primitive--Lime--100: hsla(80, 89%, 89%, 1);
--Primitive--Lime--200: hsla(81, 88%, 80%, 1);
--Primitive--Lime--300: hsla(82, 85%, 67%, 1);
--Primitive--Lime--400: hsla(83, 78%, 55%, 1);
--Primitive--Lime--500: hsla(84, 81%, 44%, 1);
--Primitive--Lime--600: hsla(85, 85%, 35%, 1);
--Primitive--Lime--700: hsla(86, 78%, 27%, 1);
--Primitive--Lime--800: hsla(86, 69%, 23%, 1);
--Primitive--Lime--900: hsla(88, 61%, 20%, 1);
--Primitive--Lime--950: hsla(89, 80%, 10%, 1);
--Primitive--Green--50: hsla(138, 76%, 97%, 1);
--Primitive--Green--100: hsla(141, 84%, 93%, 1);
--Primitive--Green--200: hsla(141, 79%, 85%, 1);
--Primitive--Green--300: hsla(142, 77%, 73%, 1);
--Primitive--Green--400: hsla(142, 69%, 58%, 1);
--Primitive--Green--500: hsla(142, 71%, 45%, 1);
--Primitive--Green--600: hsla(142, 76%, 36%, 1);
--Primitive--Green--700: hsla(142, 72%, 29%, 1);
--Primitive--Green--800: hsla(143, 64%, 24%, 1);
--Primitive--Green--900: hsla(144, 61%, 20%, 1);
--Primitive--Green--950: hsla(145, 80%, 10%, 1);
--Primitive--Emerald--50: hsla(152, 81%, 96%, 1);
--Primitive--Emerald--100: hsla(149, 80%, 90%, 1);
--Primitive--Emerald--200: hsla(152, 76%, 80%, 1);
--Primitive--Emerald--300: hsla(156, 72%, 67%, 1);
--Primitive--Emerald--400: hsla(158, 64%, 52%, 1);
--Primitive--Emerald--500: hsla(160, 84%, 39%, 1);
--Primitive--Emerald--600: hsla(161, 94%, 30%, 1);
--Primitive--Emerald--700: hsla(163, 94%, 24%, 1);
--Primitive--Emerald--800: hsla(163, 88%, 20%, 1);
--Primitive--Emerald--900: hsla(164, 86%, 16%, 1);
--Primitive--Emerald--950: hsla(166, 91%, 9%, 1);
--Primitive--Teal--50: hsla(166, 76%, 97%, 1);
--Primitive--Teal--100: hsla(167, 85%, 89%, 1);
--Primitive--Teal--200: hsla(168, 84%, 78%, 1);
--Primitive--Teal--300: hsla(171, 77%, 64%, 1);
--Primitive--Teal--400: hsla(172, 66%, 50%, 1);
--Primitive--Teal--500: hsla(173, 80%, 40%, 1);
--Primitive--Teal--600: hsla(175, 84%, 32%, 1);
--Primitive--Teal--700: hsla(175, 77%, 26%, 1);
--Primitive--Teal--800: hsla(176, 69%, 22%, 1);
--Primitive--Teal--900: hsla(176, 61%, 19%, 1);
--Primitive--Teal--950: hsla(179, 84%, 10%, 1);
--Primitive--Cyan--50: hsla(183, 100%, 96%, 1);
--Primitive--Cyan--100: hsla(185, 96%, 90%, 1);
--Primitive--Cyan--200: hsla(186, 94%, 82%, 1);
--Primitive--Cyan--300: hsla(187, 92%, 69%, 1);
--Primitive--Cyan--400: hsla(188, 86%, 53%, 1);
--Primitive--Cyan--500: hsla(189, 94%, 43%, 1);
--Primitive--Cyan--600: hsla(192, 91%, 36%, 1);
--Primitive--Cyan--700: hsla(193, 82%, 31%, 1);
--Primitive--Cyan--800: hsla(194, 70%, 27%, 1);
--Primitive--Cyan--900: hsla(196, 64%, 24%, 1);
--Primitive--Cyan--950: hsla(197, 79%, 15%, 1);
--Primitive--Sky--50: hsla(204, 100%, 97%, 1);
--Primitive--Sky--100: hsla(204, 94%, 94%, 1);
--Primitive--Sky--200: hsla(201, 94%, 86%, 1);
--Primitive--Sky--300: hsla(199, 95%, 74%, 1);
--Primitive--Sky--400: hsla(198, 93%, 60%, 1);
--Primitive--Sky--500: hsla(199, 89%, 48%, 1);
--Primitive--Sky--600: hsla(200, 98%, 39%, 1);
--Primitive--Sky--700: hsla(201, 96%, 32%, 1);
--Primitive--Sky--800: hsla(201, 90%, 27%, 1);
--Primitive--Sky--900: hsla(202, 80%, 24%, 1);
--Primitive--Sky--950: hsla(204, 80%, 16%, 1);
--Primitive--Blue--50: hsla(214, 100%, 97%, 1);
--Primitive--Blue--100: hsla(214, 95%, 93%, 1);
--Primitive--Blue--200: hsla(213, 97%, 87%, 1);
--Primitive--Blue--300: hsla(212, 96%, 78%, 1);
--Primitive--Blue--400: hsla(213, 94%, 68%, 1);
--Primitive--Blue--500: hsla(217, 91%, 60%, 1);
--Primitive--Blue--600: hsla(221, 83%, 53%, 1);
--Primitive--Blue--700: hsla(224, 76%, 48%, 1);
--Primitive--Blue--800: hsla(226, 71%, 40%, 1);
--Primitive--Blue--900: hsla(224, 64%, 33%, 1);
--Primitive--Blue--950: hsla(226, 57%, 21%, 1);
--Primitive--Indigo--50: hsla(226, 100%, 97%, 1);
--Primitive--Indigo--100: hsla(226, 100%, 94%, 1);
--Primitive--Indigo--200: hsla(228, 96%, 89%, 1);
--Primitive--Indigo--300: hsla(230, 94%, 82%, 1);
--Primitive--Indigo--400: hsla(234, 89%, 74%, 1);
--Primitive--Indigo--500: hsla(239, 84%, 67%, 1);
--Primitive--Indigo--600: hsla(243, 75%, 59%, 1);
--Primitive--Indigo--700: hsla(245, 58%, 51%, 1);
--Primitive--Indigo--800: hsla(244, 55%, 41%, 1);
--Primitive--Indigo--900: hsla(242, 47%, 34%, 1);
--Primitive--Indigo--950: hsla(244, 47%, 20%, 1);
--Primitive--Violet--50: hsla(250, 100%, 98%, 1);
--Primitive--Violet--100: hsla(251, 91%, 95%, 1);
--Primitive--Violet--200: hsla(251, 95%, 92%, 1);
--Primitive--Violet--300: hsla(253, 95%, 85%, 1);
--Primitive--Violet--400: hsla(255, 92%, 76%, 1);
--Primitive--Violet--500: hsla(258, 90%, 66%, 1);
--Primitive--Violet--600: hsla(262, 83%, 58%, 1);
--Primitive--Violet--700: hsla(263, 70%, 50%, 1);
--Primitive--Violet--800: hsla(263, 69%, 42%, 1);
--Primitive--Violet--900: hsla(264, 67%, 35%, 1);
--Primitive--Violet--950: hsla(262, 78%, 23%, 1);
--Primitive--Purple--50: hsla(270, 100%, 98%, 1);
--Primitive--Purple--100: hsla(269, 100%, 95%, 1);
--Primitive--Purple--200: hsla(269, 100%, 92%, 1);
--Primitive--Purple--300: hsla(269, 97%, 85%, 1);
--Primitive--Purple--400: hsla(270, 95%, 75%, 1);
--Primitive--Purple--500: hsla(271, 91%, 65%, 1);
--Primitive--Purple--600: hsla(271, 81%, 56%, 1);
--Primitive--Purple--700: hsla(272, 72%, 47%, 1);
--Primitive--Purple--800: hsla(273, 67%, 39%, 1);
--Primitive--Purple--900: hsla(274, 66%, 32%, 1);
--Primitive--Purple--950: hsla(274, 87%, 21%, 1);
--Primitive--Fuchsia--50: hsla(289, 100%, 98%, 1);
--Primitive--Fuchsia--100: hsla(287, 100%, 95%, 1);
--Primitive--Fuchsia--200: hsla(288, 96%, 91%, 1);
--Primitive--Fuchsia--300: hsla(291, 93%, 83%, 1);
--Primitive--Fuchsia--400: hsla(292, 91%, 73%, 1);
--Primitive--Fuchsia--500: hsla(292, 84%, 61%, 1);
--Primitive--Fuchsia--600: hsla(293, 69%, 49%, 1);
--Primitive--Fuchsia--700: hsla(295, 72%, 40%, 1);
--Primitive--Fuchsia--800: hsla(295, 70%, 33%, 1);
--Primitive--Fuchsia--900: hsla(297, 64%, 28%, 1);
--Primitive--Fuchsia--950: hsla(297, 90%, 16%, 1);
--Primitive--Pink--50: hsla(327, 73%, 97%, 1);
--Primitive--Pink--100: hsla(326, 78%, 95%, 1);
--Primitive--Pink--200: hsla(326, 85%, 90%, 1);
--Primitive--Pink--300: hsla(327, 87%, 82%, 1);
--Primitive--Pink--400: hsla(329, 86%, 70%, 1);
--Primitive--Pink--500: hsla(330, 81%, 60%, 1);
--Primitive--Pink--600: hsla(333, 71%, 51%, 1);
--Primitive--Pink--700: hsla(335, 78%, 42%, 1);
--Primitive--Pink--800: hsla(336, 74%, 35%, 1);
--Primitive--Pink--900: hsla(336, 69%, 30%, 1);
--Primitive--Pink--950: hsla(336, 84%, 17%, 1);
--Primitive--Rose--50: hsla(356, 100%, 97%, 1);
--Primitive--Rose--100: hsla(356, 100%, 95%, 1);
--Primitive--Rose--200: hsla(353, 96%, 90%, 1);
--Primitive--Rose--300: hsla(353, 96%, 82%, 1);
--Primitive--Rose--400: hsla(351, 95%, 71%, 1);
--Primitive--Rose--500: hsla(350, 89%, 60%, 1);
--Primitive--Rose--600: hsla(347, 77%, 50%, 1);
--Primitive--Rose--700: hsla(345, 83%, 41%, 1);
--Primitive--Rose--800: hsla(343, 80%, 35%, 1);
--Primitive--Rose--900: hsla(342, 75%, 30%, 1);
--Primitive--Rose--950: hsla(343, 88%, 16%, 1);
--Brand--Base_Colors--Destructive: var(--Primitive--Red--500);
--Brand--Base_Colors--Success: var(--Primitive--Green--500);
--Brand--Base_Colors--Warning: var(--Primitive--Amber--500);
--Brand--Base_Colors--White: var(--Primitive--White);
--Brand--Base_Colors--Black: var(--Primitive--Black);
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--900); /*页面背景色:应用在整个页面的最底层。*/
--Brand--Semantic_Colors--Background-subtle: hsla(
0,
0%,
100%,
0.02
); /*细微背景色:用于需要与主背景有微弱区分的区域,如代码块背景。*/
--Brand--Semantic_Colors--Foreground: hsla(0, 0%, 100%, 0.9); /*主要前景/文字色:用于正文、标题等。*/
--Brand--Semantic_Colors--Foreground-secondary: hsla(0, 0%, 100%, 0.6); /*次要前景/文字色:用于辅助性文本、描述。*/
--Brand--Semantic_Colors--Foreground-muted: hsla(0, 0%, 100%, 0.4); /*静默前景/文字色:用于禁用状态的文字、占位符。*/
--Brand--Semantic_Colors--Border: hsla(0, 0%, 100%, 0.1); /*默认边框色:用于卡片、输入框、分隔线。*/
--Brand--Semantic_Colors--Border-hover: hsla(0, 0%, 100%, 0.2); /*激活边框色:用于元素被按下或激活时的边框。*/
--Brand--Semantic_Colors--Border-active: hsla(0, 0%, 100%, 0.3); /*激活边框色:用于元素被按下或激活时的边框。*/
--Brand--Semantic_Colors--Ring: hsla(
84,
81%,
44%,
0.4
); /*聚焦环颜色:用于输入框等元素在聚焦 (Focus) 状态下的外发光。*/
--Brand--UI_Element_Colors--Modal--Backdrop: hsla(0, 0%, 0%, 0.06);
--Brand--UI_Element_Colors--Modal--Thumb: hsla(0, 0%, 100%, 0.2);
--Brand--UI_Element_Colors--Modal--Thumb_Hover: hsla(0, 0%, 100%, 0.3);
--Brand--UI_Element_Colors--Icon--Default: var(--Brand--Semantic_Colors--Foreground-secondary);
--Brand--UI_Element_Colors--Icon--Hover: var(--Brand--Semantic_Colors--Foreground);
--Brand--UI_Element_Colors--Input_Select--Background: var(--Brand--Base_Colors--Black);
--Brand--UI_Element_Colors--Input_Select--Border: var(--Brand--Semantic_Colors--Border);
--Brand--UI_Element_Colors--Input_Select--Border_Hover: var(--Brand--Semantic_Colors--Border-hover);
--Brand--UI_Element_Colors--Input_Select--Border_Focus: var(--Brand--Base_Colors--Primary);
--Brand--UI_Element_Colors--Primary_Button--Background: var(--Brand--Base_Colors--Primary);
--Brand--UI_Element_Colors--Card_Container--Background: var(--Brand--Base_Colors--Black);
--Brand--UI_Element_Colors--Card_Container--Border: var(--Brand--Semantic_Colors--Border);
--Brand--UI_Element_Colors--Ghost_Button--Background: hsla(0, 0%, 100%, 0);
--Brand--UI_Element_Colors--Ghost_Button--Text: var(--Brand--Semantic_Colors--Foreground);
--Brand--UI_Element_Colors--Ghost_Button--Background_Hover: var(--Opacity--White--White-10);
--Brand--UI_Element_Colors--Ghost_Button--Background_Active: hsla(0, 0%, 100%, 0.15);
--Brand--UI_Element_Colors--Secondary_Button--Background: var(--Opacity--White--White-10);
--Brand--UI_Element_Colors--Secondary_Button--Text: var(--Brand--Semantic_Colors--Foreground);
--Brand--UI_Element_Colors--Secondary_Button--Background_Hover: var(--Opacity--White--White-20);
--Brand--UI_Element_Colors--Secondary_Button--Background_Active: hsla(0, 0%, 100%, 0.25);
--Brand--UI_Element_Colors--Secondary_Button--Border: var(--Brand--Semantic_Colors--Border);
--Brand--UI_Element_Colors--Primary_Button--Text: var(--Brand--Base_Colors--White);
--Brand--UI_Element_Colors--Primary_Button--Background_Hover: hsla(84, 81%, 44%, 0.85);
--Brand--UI_Element_Colors--Primary_Button--2nd_Background: hsla(84, 81%, 44%, 0.1);
--Brand--UI_Element_Colors--Primary_Button--3rd_Background: hsla(84, 81%, 44%, 0.05);
--Brand--UI_Element_Colors--Primary_Button--Background_Active: hsla(84, 81%, 44%, 0.7);
--Boolean: false;
}
+2 -2
View File
@@ -29,7 +29,7 @@ import {
setupAppImageDeepLink setupAppImageDeepLink
} from './services/ProtocolClient' } from './services/ProtocolClient'
import selectionService, { initSelectionService } from './services/SelectionService' import selectionService, { initSelectionService } from './services/SelectionService'
import { registerShortcuts } from './services/ShortcutService' import { shortcutService } from './services/ShortcutService'
import { TrayService } from './services/TrayService' import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService' import { windowService } from './services/WindowService'
import { dataRefactorMigrateService } from './data/migrate/dataRefactor/DataRefactorMigrateService' import { dataRefactorMigrateService } from './data/migrate/dataRefactor/DataRefactorMigrateService'
@@ -216,7 +216,7 @@ if (!app.requestSingleInstanceLock()) {
} }
}) })
registerShortcuts(mainWindow) shortcutService.registerForWindow(mainWindow)
registerIpc(mainWindow, app) registerIpc(mainWindow, app)
replaceDevtoolsFont(mainWindow) replaceDevtoolsFont(mainWindow)
-13
View File
@@ -21,7 +21,6 @@ import type {
OcrProvider, OcrProvider,
PluginError, PluginError,
Provider, Provider,
Shortcut,
SupportedOcrFile SupportedOcrFile
} from '@types' } from '@types'
import checkDiskSpace from 'check-disk-space' import checkDiskSpace from 'check-disk-space'
@@ -35,7 +34,6 @@ import appService from './services/AppService'
import AppUpdater from './services/AppUpdater' import AppUpdater from './services/AppUpdater'
import BackupManager from './services/BackupManager' import BackupManager from './services/BackupManager'
import { codeToolsService } from './services/CodeToolsService' import { codeToolsService } from './services/CodeToolsService'
import { configManager } from './services/ConfigManager'
import CopilotService from './services/CopilotService' import CopilotService from './services/CopilotService'
import DxtService from './services/DxtService' import DxtService from './services/DxtService'
import { ExportService } from './services/ExportService' import { ExportService } from './services/ExportService'
@@ -56,7 +54,6 @@ import { pythonService } from './services/PythonService'
import { FileServiceManager } from './services/remotefile/FileServiceManager' import { FileServiceManager } from './services/remotefile/FileServiceManager'
import { searchService } from './services/SearchService' import { searchService } from './services/SearchService'
import { SelectionService } from './services/SelectionService' import { SelectionService } from './services/SelectionService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import { import {
addEndMessage, addEndMessage,
addStreamMessage, addStreamMessage,
@@ -581,16 +578,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
await shell.openPath(path) await shell.openPath(path)
}) })
// shortcuts
ipcMain.handle(IpcChannel.Shortcuts_Update, (_, shortcuts: Shortcut[]) => {
configManager.setShortcuts(shortcuts)
// Refresh shortcuts registration
if (mainWindow) {
unregisterAllShortcuts()
registerShortcuts(mainWindow)
}
})
ipcMain.handle(IpcChannel.KnowledgeBase_Create, KnowledgeService.create.bind(KnowledgeService)) ipcMain.handle(IpcChannel.KnowledgeBase_Create, KnowledgeService.create.bind(KnowledgeService))
ipcMain.handle(IpcChannel.KnowledgeBase_Reset, KnowledgeService.reset.bind(KnowledgeService)) ipcMain.handle(IpcChannel.KnowledgeBase_Reset, KnowledgeService.reset.bind(KnowledgeService))
ipcMain.handle(IpcChannel.KnowledgeBase_Delete, KnowledgeService.delete.bind(KnowledgeService)) ipcMain.handle(IpcChannel.KnowledgeBase_Delete, KnowledgeService.delete.bind(KnowledgeService))
@@ -1,7 +1,6 @@
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces' import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { OllamaEmbeddings } from '@cherrystudio/embedjs-ollama' import { OllamaEmbeddings } from '@cherrystudio/embedjs-ollama'
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai' import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings'
import type { ApiClient } from '@types' import type { ApiClient } from '@types'
import { VoyageEmbeddings } from './VoyageEmbeddings' import { VoyageEmbeddings } from './VoyageEmbeddings'
@@ -9,7 +8,7 @@ import { VoyageEmbeddings } from './VoyageEmbeddings'
export default class EmbeddingsFactory { export default class EmbeddingsFactory {
static create({ embedApiClient, dimensions }: { embedApiClient: ApiClient; dimensions?: number }): BaseEmbeddings { static create({ embedApiClient, dimensions }: { embedApiClient: ApiClient; dimensions?: number }): BaseEmbeddings {
const batchSize = 10 const batchSize = 10
const { model, provider, apiKey, apiVersion, baseURL } = embedApiClient const { model, provider, apiKey, baseURL } = embedApiClient
if (provider === 'voyageai') { if (provider === 'voyageai') {
return new VoyageEmbeddings({ return new VoyageEmbeddings({
modelName: model, modelName: model,
@@ -38,16 +37,7 @@ export default class EmbeddingsFactory {
} }
}) })
} }
if (apiVersion !== undefined) { // NOTE: Azure OpenAI 也走 OpenAIEmbeddings, baseURL是https://xxxx.openai.azure.com/openai/v1
return new AzureOpenAiEmbeddings({
azureOpenAIApiKey: apiKey,
azureOpenAIApiVersion: apiVersion,
azureOpenAIApiDeploymentName: model,
azureOpenAIEndpoint: baseURL,
dimensions,
batchSize
})
}
return new OpenAiEmbeddings({ return new OpenAiEmbeddings({
model, model,
apiKey, apiKey,
+259 -250
View File
@@ -1,298 +1,307 @@
import { preferenceService } from '@data/PreferenceService' import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { handleZoomFactor } from '@main/utils/zoom' import { handleZoomFactor } from '@main/utils/zoom'
import type { Shortcut } from '@types' import type { PreferenceDefaultScopeType } from '@shared/data/preference/preferenceTypes'
import { SHORTCUT_DEFINITIONS } from '@shared/shortcuts/definitions'
import type {
ShortcutDefinition,
ShortcutHandler,
ShortcutPreferenceKey,
ShortcutPreferenceValue,
ShortcutRuntimeConfig
} from '@shared/shortcuts/types'
import { coerceShortcutPreference } from '@shared/shortcuts/utils'
import type { BrowserWindow } from 'electron' import type { BrowserWindow } from 'electron'
import { globalShortcut } from 'electron' import { globalShortcut } from 'electron'
import { configManager } from './ConfigManager'
import selectionService from './SelectionService' import selectionService from './SelectionService'
import { windowService } from './WindowService' import { windowService } from './WindowService'
const logger = loggerService.withContext('ShortcutService') const logger = loggerService.withContext('ShortcutService')
let showAppAccelerator: string | null = null const toAccelerator = (keys: string[]): string => keys.join('+')
let showMiniWindowAccelerator: string | null = null
let selectionAssistantToggleAccelerator: string | null = null
let selectionAssistantSelectTextAccelerator: string | null = null
//indicate if the shortcuts are registered on app boot time const relevantDefinitions = SHORTCUT_DEFINITIONS.filter((definition) => definition.scope !== 'renderer')
let isRegisterOnBoot = true
// store the focus and blur handlers for each window to unregister them later export class ShortcutService {
const windowOnHandlers = new Map<BrowserWindow, { onFocusHandler: () => void; onBlurHandler: () => void }>() private handlers = new Map<ShortcutPreferenceKey, ShortcutHandler>()
private windowLifecycleHandlers = new Map<
BrowserWindow,
{ onFocus: () => void; onBlur: () => void; onClosed: () => void }
>()
private currentWindow: BrowserWindow | null = null
private preferenceUnsubscribers: Array<() => void> = []
function getShortcutHandler(shortcut: Shortcut) { constructor() {
switch (shortcut.key) { this.registerBuiltInHandlers()
case 'zoom_in': this.subscribeToPreferenceChanges()
return (window: BrowserWindow) => handleZoomFactor([window], 0.1)
case 'zoom_out':
return (window: BrowserWindow) => handleZoomFactor([window], -0.1)
case 'zoom_reset':
return (window: BrowserWindow) => handleZoomFactor([window], 0, true)
case 'show_app':
return () => {
windowService.toggleMainWindow()
}
case 'mini_window':
return () => {
windowService.toggleMiniWindow()
}
case 'selection_assistant_toggle':
return () => {
if (selectionService) {
selectionService.toggleEnabled()
}
}
case 'selection_assistant_select_text':
return () => {
if (selectionService) {
selectionService.processSelectTextByShortcut()
}
}
default:
return null
} }
}
function formatShortcutKey(shortcut: string[]): string { public registerHandler(key: ShortcutPreferenceKey, handler: ShortcutHandler): void {
return shortcut.join('+') if (this.handlers.has(key)) {
} logger.warn(`Handler for ${key} is being overwritten`)
// convert the shortcut recorded by JS keyboard event key value to electron global shortcut format
// see: https://www.electronjs.org/zh/docs/latest/api/accelerator
const convertShortcutFormat = (shortcut: string | string[]): string => {
const accelerator = (() => {
if (Array.isArray(shortcut)) {
return shortcut
} else {
return shortcut.split('+').map((key) => key.trim())
} }
})() this.handlers.set(key, handler)
logger.debug(`Registered handler for ${key}`)
return accelerator
.map((key) => {
switch (key) {
// OLD WAY FOR MODIFIER KEYS, KEEP THEM HERE FOR REFERENCE
// case 'Command':
// return 'CommandOrControl'
// case 'Control':
// return 'Control'
// case 'Ctrl':
// return 'Control'
// NEW WAY FOR MODIFIER KEYS
// you can see all the modifier keys in the same
case 'CommandOrControl':
return 'CommandOrControl'
case 'Ctrl':
return 'Ctrl'
case 'Alt':
return 'Alt' // Use `Alt` instead of `Option`. The `Option` key only exists on macOS, whereas the `Alt` key is available on all platforms.
case 'Meta':
return 'Meta' // `Meta` key is mapped to the Windows key on Windows and Linux, `Cmd` on macOS.
case 'Shift':
return 'Shift'
// For backward compatibility with old data
case 'Command':
case 'Cmd':
return 'CommandOrControl'
case 'Control':
return 'Ctrl'
case 'ArrowUp':
return 'Up'
case 'ArrowDown':
return 'Down'
case 'ArrowLeft':
return 'Left'
case 'ArrowRight':
return 'Right'
case 'AltGraph':
return 'AltGr'
case 'Slash':
return '/'
case 'Semicolon':
return ';'
case 'BracketLeft':
return '['
case 'BracketRight':
return ']'
case 'Backslash':
return '\\'
case 'Quote':
return "'"
case 'Comma':
return ','
case 'Minus':
return '-'
case 'Equal':
return '='
default:
return key
}
})
.join('+')
}
export function registerShortcuts(window: BrowserWindow) {
if (isRegisterOnBoot) {
window.once('ready-to-show', () => {
if (preferenceService.get('app.tray.on_launch')) {
registerOnlyUniversalShortcuts()
}
})
isRegisterOnBoot = false
} }
//only for clearer code public registerForWindow(window: BrowserWindow): void {
const registerOnlyUniversalShortcuts = () => { if (this.windowLifecycleHandlers.has(window)) {
register(true) logger.warn(`Window ${window.id} already registered for shortcuts`)
return
}
const onFocus = () => {
logger.debug(`Window ${window.id} focused - registering shortcuts`)
this.currentWindow = window
this.registerAllShortcuts(window)
}
const onBlur = () => {
logger.debug(`Window ${window.id} blurred - unregistering non-persistent shortcuts`)
this.unregisterTransientShortcuts(window)
}
const onClosed = () => {
logger.debug(`Window ${window.id} closed - cleaning up shortcut registrations`)
this.unregisterWindow(window)
}
window.on('focus', onFocus)
window.on('blur', onBlur)
window.on('closed', onClosed)
this.windowLifecycleHandlers.set(window, { onFocus, onBlur, onClosed })
this.currentWindow = window
if (window.isFocused()) {
this.registerAllShortcuts(window)
} else {
this.unregisterTransientShortcuts(window)
}
logger.info(`ShortcutService attached to window ${window.id}`)
} }
//onlyUniversalShortcuts is used to register shortcuts that are not window specific, like show_app & mini_window public unregisterWindow(window: BrowserWindow): void {
//onlyUniversalShortcuts is needed when we launch to tray const lifecycle = this.windowLifecycleHandlers.get(window)
const register = (onlyUniversalShortcuts: boolean = false) => { if (!lifecycle) {
if (window.isDestroyed()) return return
}
const shortcuts = configManager.getShortcuts() window.off('focus', lifecycle.onFocus)
if (!shortcuts) return window.off('blur', lifecycle.onBlur)
window.off('closed', lifecycle.onClosed)
shortcuts.forEach((shortcut) => { this.windowLifecycleHandlers.delete(window)
try {
if (shortcut.shortcut.length === 0) { if (this.currentWindow === window) {
this.currentWindow = null
globalShortcut.unregisterAll()
}
}
public cleanup(): void {
this.windowLifecycleHandlers.forEach((_handlers, window) => this.unregisterWindow(window))
this.windowLifecycleHandlers.clear()
this.handlers.clear()
this.currentWindow = null
this.preferenceUnsubscribers.forEach((unsubscribe) => unsubscribe())
this.preferenceUnsubscribers = []
globalShortcut.unregisterAll()
logger.info('ShortcutService cleaned up')
}
private registerBuiltInHandlers(): void {
this.registerHandler('shortcut.app.show_main_window', () => {
windowService.toggleMainWindow()
})
this.registerHandler('shortcut.app.show_settings', () => {
let targetWindow = windowService.getMainWindow()
if (
!targetWindow ||
targetWindow.isDestroyed() ||
targetWindow.isMinimized() ||
!targetWindow.isVisible() ||
!targetWindow.isFocused()
) {
windowService.showMainWindow()
targetWindow = windowService.getMainWindow()
}
if (!targetWindow || targetWindow.isDestroyed()) {
return
}
void targetWindow.webContents
.executeJavaScript(`typeof window.navigate === 'function' && window.navigate('/settings/provider')`, true)
.catch((error) => {
logger.warn('Failed to navigate to settings from shortcut:', error as Error)
})
})
this.registerHandler('shortcut.app.show_mini_window', () => {
windowService.toggleMiniWindow()
})
this.registerHandler('shortcut.app.zoom_in', (window) => {
if (window) {
handleZoomFactor([window], 0.1)
}
})
this.registerHandler('shortcut.app.zoom_out', (window) => {
if (window) {
handleZoomFactor([window], -0.1)
}
})
this.registerHandler('shortcut.app.zoom_reset', (window) => {
if (window) {
handleZoomFactor([window], 0, true)
}
})
this.registerHandler('shortcut.selection.toggle_enabled', () => {
if (selectionService) {
selectionService.toggleEnabled()
}
})
this.registerHandler('shortcut.selection.get_text', () => {
if (selectionService) {
selectionService.processSelectTextByShortcut()
}
})
}
private subscribeToPreferenceChanges(): void {
this.preferenceUnsubscribers = relevantDefinitions.map((definition) =>
preferenceService.subscribeChange(definition.key, () => {
logger.debug(`Shortcut preference changed: ${definition.key}`)
this.reregisterShortcuts()
})
)
}
private registerAllShortcuts(window: BrowserWindow): void {
globalShortcut.unregisterAll()
relevantDefinitions.forEach((definition) => {
const runtimeConfig = this.getRuntimeConfig(definition)
if (!runtimeConfig.enabled) {
return
}
if (definition.enabledWhen && !definition.enabledWhen(this.getPreferenceValue)) {
logger.debug(`Skipping ${definition.key} - enabledWhen condition not met`)
return
}
const handler = this.handlers.get(definition.key)
if (!handler) {
logger.warn(`No handler registered for ${definition.key}`)
return
}
this.registerSingleShortcut(runtimeConfig.binding, handler, window)
if (definition.variants) {
definition.variants.forEach((variant) => {
this.registerSingleShortcut(variant, handler, window)
})
}
})
}
private unregisterTransientShortcuts(window: BrowserWindow): void {
globalShortcut.unregisterAll()
relevantDefinitions
.filter((definition) => definition.persistOnBlur)
.forEach((definition) => {
const runtimeConfig = this.getRuntimeConfig(definition)
if (!runtimeConfig.enabled) {
return return
} }
//if not enabled, exit early from the process. if (definition.enabledWhen && !definition.enabledWhen(this.getPreferenceValue)) {
if (!shortcut.enabled) {
return return
} }
// only register universal shortcuts when needed const handler = this.handlers.get(definition.key)
if (
onlyUniversalShortcuts &&
!['show_app', 'mini_window', 'selection_assistant_toggle', 'selection_assistant_select_text'].includes(
shortcut.key
)
) {
return
}
const handler = getShortcutHandler(shortcut)
if (!handler) { if (!handler) {
return return
} }
switch (shortcut.key) { this.registerSingleShortcut(runtimeConfig.binding, handler, window)
case 'show_app':
showAppAccelerator = formatShortcutKey(shortcut.shortcut)
break
case 'mini_window': if (definition.variants) {
//available only when QuickAssistant enabled definition.variants.forEach((variant) => {
if (!preferenceService.get('feature.quick_assistant.enabled')) { this.registerSingleShortcut(variant, handler, window)
return })
}
showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut)
break
case 'selection_assistant_toggle':
selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut)
break
case 'selection_assistant_select_text':
selectionAssistantSelectTextAccelerator = formatShortcutKey(shortcut.shortcut)
break
//the following ZOOMs will register shortcuts separately, so will return
case 'zoom_in':
globalShortcut.register('CommandOrControl+=', () => handler(window))
globalShortcut.register('CommandOrControl+numadd', () => handler(window))
return
case 'zoom_out':
globalShortcut.register('CommandOrControl+-', () => handler(window))
globalShortcut.register('CommandOrControl+numsub', () => handler(window))
return
case 'zoom_reset':
globalShortcut.register('CommandOrControl+0', () => handler(window))
return
} }
})
const accelerator = convertShortcutFormat(shortcut.shortcut)
globalShortcut.register(accelerator, () => handler(window))
} catch (error) {
logger.warn(`Failed to register shortcut ${shortcut.key}`)
}
})
} }
const unregister = () => { private registerSingleShortcut(keys: string[], handler: ShortcutHandler, window: BrowserWindow): void {
if (window.isDestroyed()) return if (!keys.length) {
return
}
const accelerator = toAccelerator(keys)
try { try {
globalShortcut.unregisterAll() globalShortcut.register(accelerator, () => {
logger.debug(`Shortcut triggered: ${accelerator}`)
if (showAppAccelerator) { const targetWindow = window?.isDestroyed?.() ? undefined : window
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut) handler(targetWindow)
const accelerator = convertShortcutFormat(showAppAccelerator) })
handler && globalShortcut.register(accelerator, () => handler(window)) logger.verbose(`Registered shortcut: ${accelerator}`)
}
if (showMiniWindowAccelerator) {
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
const accelerator = convertShortcutFormat(showMiniWindowAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
}
if (selectionAssistantToggleAccelerator) {
const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut)
const accelerator = convertShortcutFormat(selectionAssistantToggleAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
}
if (selectionAssistantSelectTextAccelerator) {
const handler = getShortcutHandler({ key: 'selection_assistant_select_text' } as Shortcut)
const accelerator = convertShortcutFormat(selectionAssistantSelectTextAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
}
} catch (error) { } catch (error) {
logger.warn('Failed to unregister shortcuts') logger.error(`Failed to register shortcut ${accelerator}:`, error as Error)
} }
} }
// only register the event handlers once private getRuntimeConfig(definition: ShortcutDefinition): ShortcutRuntimeConfig {
if (undefined === windowOnHandlers.get(window)) { const preference = this.getPreference(definition)
// pass register() directly to listener, the func will receive Event as argument, it's not expected return {
const registerHandler = () => { ...definition,
register() binding: preference.binding,
enabled: preference.enabled,
editable: preference.editable,
system: preference.system
} }
window.on('focus', registerHandler)
window.on('blur', unregister)
windowOnHandlers.set(window, { onFocusHandler: registerHandler, onBlurHandler: unregister })
} }
if (!window.isDestroyed() && window.isFocused()) { private getPreference(definition: ShortcutDefinition): ShortcutPreferenceValue {
register() const rawPreference = preferenceService.get(definition.key)
return coerceShortcutPreference(definition, rawPreference as any)
}
private getPreferenceValue = <K extends ShortcutPreferenceKey | keyof PreferenceDefaultScopeType>(
key: K
): PreferenceDefaultScopeType[K] => {
return preferenceService.get(key)
}
private reregisterShortcuts(): void {
if (!this.currentWindow || this.currentWindow.isDestroyed()) {
return
}
if (this.currentWindow.isFocused()) {
this.registerAllShortcuts(this.currentWindow)
return
}
this.unregisterTransientShortcuts(this.currentWindow)
} }
} }
export function unregisterAllShortcuts() { export const shortcutService = new ShortcutService()
try {
showAppAccelerator = null
showMiniWindowAccelerator = null
selectionAssistantToggleAccelerator = null
selectionAssistantSelectTextAccelerator = null
windowOnHandlers.forEach((handlers, window) => {
window.off('focus', handlers.onFocusHandler)
window.off('blur', handlers.onBlurHandler)
})
windowOnHandlers.clear()
globalShortcut.unregisterAll()
} catch (error) {
logger.warn('Failed to unregister all shortcuts')
}
}
@@ -6,7 +6,14 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { processKnowledgeReferences } from '@renderer/services/KnowledgeService' import { processKnowledgeReferences } from '@renderer/services/KnowledgeService'
import type { BaseTool, MCPTool, MCPToolResponse, NormalToolResponse } from '@renderer/types' import type {
BaseTool,
MCPCallToolResponse,
MCPTool,
MCPToolResponse,
MCPToolResultContent,
NormalToolResponse
} from '@renderer/types'
import type { Chunk } from '@renderer/types/chunk' import type { Chunk } from '@renderer/types/chunk'
import { ChunkType } from '@renderer/types/chunk' import { ChunkType } from '@renderer/types/chunk'
import type { ToolSet, TypedToolCall, TypedToolError, TypedToolResult } from 'ai' import type { ToolSet, TypedToolCall, TypedToolError, TypedToolResult } from 'ai'
@@ -255,6 +262,7 @@ export class ToolCallChunkHandler {
type: 'tool-result' type: 'tool-result'
} & TypedToolResult<ToolSet> } & TypedToolResult<ToolSet>
): void { ): void {
// TODO: 基于AI SDK为供应商内置工具做更好的展示和类型安全处理
const { toolCallId, output, input } = chunk const { toolCallId, output, input } = chunk
if (!toolCallId) { if (!toolCallId) {
@@ -300,12 +308,7 @@ export class ToolCallChunkHandler {
responses: [toolResponse] responses: [toolResponse]
}) })
const images: string[] = [] const images = extractImagesFromToolOutput(toolResponse.response)
for (const content of toolResponse.response?.content || []) {
if (content.type === 'image' && content.data) {
images.push(`data:${content.mimeType};base64,${content.data}`)
}
}
if (images.length) { if (images.length) {
this.onChunk({ this.onChunk({
@@ -352,3 +355,41 @@ export class ToolCallChunkHandler {
} }
export const addActiveToolCall = ToolCallChunkHandler.addActiveToolCall.bind(ToolCallChunkHandler) export const addActiveToolCall = ToolCallChunkHandler.addActiveToolCall.bind(ToolCallChunkHandler)
function extractImagesFromToolOutput(output: unknown): string[] {
if (!output) {
return []
}
const contents: unknown[] = []
if (isMcpCallToolResponse(output)) {
contents.push(...output.content)
} else if (Array.isArray(output)) {
contents.push(...output)
} else if (hasContentArray(output)) {
contents.push(...output.content)
}
return contents
.filter(isMcpImageContent)
.map((content) => `data:${content.mimeType ?? 'image/png'};base64,${content.data}`)
}
function isMcpCallToolResponse(value: unknown): value is MCPCallToolResponse {
return typeof value === 'object' && value !== null && Array.isArray((value as MCPCallToolResponse).content)
}
function hasContentArray(value: unknown): value is { content: unknown[] } {
return typeof value === 'object' && value !== null && Array.isArray((value as { content?: unknown }).content)
}
function isMcpImageContent(content: unknown): content is MCPToolResultContent & { data: string } {
if (typeof content !== 'object' || content === null) {
return false
}
const resultContent = content as MCPToolResultContent
return resultContent.type === 'image' && typeof resultContent.data === 'string'
}
+14 -10
View File
@@ -14,6 +14,7 @@ import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
import type { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity' import type { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types' import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types'
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes' import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
import { SUPPORTED_IMAGE_ENDPOINT_LIST } from '@renderer/utils'
import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic' import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic'
import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai' import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai'
@@ -78,7 +79,7 @@ export default class ModernAiProvider {
return this.actualProvider return this.actualProvider
} }
public async completions(modelId: string, params: StreamTextParams, config: ModernAiProviderConfig) { public async completions(modelId: string, params: StreamTextParams, providerConfig: ModernAiProviderConfig) {
// 检查model是否存在 // 检查model是否存在
if (!this.model) { if (!this.model) {
throw new Error('Model is required for completions. Please use constructor with model parameter.') throw new Error('Model is required for completions. Please use constructor with model parameter.')
@@ -86,7 +87,10 @@ export default class ModernAiProvider {
// 每次请求时重新生成配置以确保API key轮换生效 // 每次请求时重新生成配置以确保API key轮换生效
this.config = providerToAiSdkConfig(this.actualProvider, this.model) this.config = providerToAiSdkConfig(this.actualProvider, this.model)
logger.debug('Generated provider config for completions', this.config)
if (SUPPORTED_IMAGE_ENDPOINT_LIST.includes(this.config.options.endpoint)) {
providerConfig.isImageGenerationEndpoint = true
}
// 准备特殊配置 // 准备特殊配置
await prepareSpecialProviderConfig(this.actualProvider, this.config) await prepareSpecialProviderConfig(this.actualProvider, this.config)
@@ -97,13 +101,13 @@ export default class ModernAiProvider {
// 提前构建中间件 // 提前构建中间件
const middlewares = buildAiSdkMiddlewares({ const middlewares = buildAiSdkMiddlewares({
...config, ...providerConfig,
provider: this.actualProvider, provider: this.actualProvider,
assistant: config.assistant assistant: providerConfig.assistant
}) })
logger.debug('Built middlewares in completions', { logger.debug('Built middlewares in completions', {
middlewareCount: middlewares.length, middlewareCount: middlewares.length,
isImageGeneration: config.isImageGenerationEndpoint isImageGeneration: providerConfig.isImageGenerationEndpoint
}) })
if (!this.localProvider) { if (!this.localProvider) {
throw new Error('Local provider not created') throw new Error('Local provider not created')
@@ -111,7 +115,7 @@ export default class ModernAiProvider {
// 根据endpoint类型创建对应的模型 // 根据endpoint类型创建对应的模型
let model: AiSdkModel | undefined let model: AiSdkModel | undefined
if (config.isImageGenerationEndpoint) { if (providerConfig.isImageGenerationEndpoint) {
model = this.localProvider.imageModel(modelId) model = this.localProvider.imageModel(modelId)
} else { } else {
model = this.localProvider.languageModel(modelId) model = this.localProvider.languageModel(modelId)
@@ -127,15 +131,15 @@ export default class ModernAiProvider {
params.messages = [...claudeCodeSystemMessage, ...(params.messages || [])] params.messages = [...claudeCodeSystemMessage, ...(params.messages || [])]
} }
if (config.topicId && (await preferenceService.get('app.developer_mode.enabled'))) { if (providerConfig.topicId && (await preferenceService.get('app.developer_mode.enabled'))) {
// TypeScript类型窄化:确保topicId是string类型 // TypeScript类型窄化:确保topicId是string类型
const traceConfig = { const traceConfig = {
...config, ...providerConfig,
topicId: config.topicId topicId: providerConfig.topicId
} }
return await this._completionsForTrace(model, params, traceConfig) return await this._completionsForTrace(model, params, traceConfig)
} else { } else {
return await this._completionsOrImageGeneration(model, params, config) return await this._completionsOrImageGeneration(model, params, providerConfig)
} }
} }
@@ -1,5 +1,4 @@
import type { Provider } from '@renderer/types' import type { Provider } from '@renderer/types'
import { isOpenAIProvider } from '@renderer/utils'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AihubmixAPIClient } from '../aihubmix/AihubmixAPIClient' import { AihubmixAPIClient } from '../aihubmix/AihubmixAPIClient'
@@ -202,36 +201,4 @@ describe('ApiClientFactory', () => {
expect(client).toBeDefined() expect(client).toBeDefined()
}) })
}) })
describe('isOpenAIProvider', () => {
it('should return true for openai type', () => {
const provider = createTestProvider('openai', 'openai')
expect(isOpenAIProvider(provider)).toBe(true)
})
it('should return true for azure-openai type', () => {
const provider = createTestProvider('azure-openai', 'azure-openai')
expect(isOpenAIProvider(provider)).toBe(true)
})
it('should return true for unknown type (fallback to OpenAI)', () => {
const provider = createTestProvider('unknown', 'unknown')
expect(isOpenAIProvider(provider)).toBe(true)
})
it('should return false for vertexai type', () => {
const provider = createTestProvider('vertex', 'vertexai')
expect(isOpenAIProvider(provider)).toBe(false)
})
it('should return false for anthropic type', () => {
const provider = createTestProvider('anthropic', 'anthropic')
expect(isOpenAIProvider(provider)).toBe(false)
})
it('should return false for gemini type', () => {
const provider = createTestProvider('gemini', 'gemini')
expect(isOpenAIProvider(provider)).toBe(false)
})
})
}) })
@@ -1,5 +1,5 @@
import type { AiPlugin } from '@cherrystudio/ai-core' import type { AiPlugin } from '@cherrystudio/ai-core'
import { createPromptToolUsePlugin, googleToolsPlugin, webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins' import { createPromptToolUsePlugin, webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins'
import { preferenceService } from '@data/PreferenceService' import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import type { Assistant } from '@renderer/types' import type { Assistant } from '@renderer/types'
@@ -68,9 +68,9 @@ export async function buildPlugins(
) )
} }
if (middlewareConfig.enableUrlContext) { // if (middlewareConfig.enableUrlContext && middlewareConfig.) {
plugins.push(googleToolsPlugin({ urlContext: true })) // plugins.push(googleToolsPlugin({ urlContext: true }))
} // }
logger.debug( logger.debug(
'Final plugin list:', 'Final plugin list:',
@@ -114,7 +114,7 @@ export async function handleGeminiFileUpload(file: FileMetadata, model: Model):
} }
/** /**
* OpenAI大文件上传 * OpenAI兼容大文件上传
*/ */
export async function handleOpenAILargeFileUpload( export async function handleOpenAILargeFileUpload(
file: FileMetadata, file: FileMetadata,
@@ -3,6 +3,8 @@
* AI SDK的流式和非流式参数 * AI SDK的流式和非流式参数
*/ */
import { anthropic } from '@ai-sdk/anthropic'
import { google } from '@ai-sdk/google'
import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic/edge' import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic/edge'
import { vertex } from '@ai-sdk/google-vertex/edge' import { vertex } from '@ai-sdk/google-vertex/edge'
import type { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins' import type { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
@@ -97,10 +99,6 @@ export async function buildStreamTextParams(
let tools = setupToolsConfig(mcpTools) let tools = setupToolsConfig(mcpTools)
// if (webSearchProviderId) {
// tools['builtin_web_search'] = webSearchTool(webSearchProviderId)
// }
// 构建真正的 providerOptions // 构建真正的 providerOptions
const webSearchConfig: CherryWebSearchConfig = { const webSearchConfig: CherryWebSearchConfig = {
maxResults: store.getState().websearch.maxResults, maxResults: store.getState().websearch.maxResults,
@@ -143,12 +141,34 @@ export async function buildStreamTextParams(
} }
} }
// google-vertex if (enableUrlContext) {
if (enableUrlContext && aiSdkProviderId === 'google-vertex') {
if (!tools) { if (!tools) {
tools = {} tools = {}
} }
tools.url_context = vertex.tools.urlContext({}) as ProviderDefinedTool const blockedDomains = mapRegexToPatterns(webSearchConfig.excludeDomains)
switch (aiSdkProviderId) {
case 'google-vertex':
tools.url_context = vertex.tools.urlContext({}) as ProviderDefinedTool
break
case 'google':
tools.url_context = google.tools.urlContext({}) as ProviderDefinedTool
break
case 'anthropic':
case 'google-vertex-anthropic':
tools.web_fetch = (
aiSdkProviderId === 'anthropic'
? anthropic.tools.webFetch_20250910({
maxUses: webSearchConfig.maxResults,
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
})
: vertexAnthropic.tools.webFetch_20250910({
maxUses: webSearchConfig.maxResults,
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
})
) as ProviderDefinedTool
break
}
} }
// 构建基础参数 // 构建基础参数
@@ -32,7 +32,8 @@ const AIHUBMIX_RULES: RuleSet = {
match: (model) => match: (model) =>
(startsWith('gemini')(model) || startsWith('imagen')(model)) && (startsWith('gemini')(model) || startsWith('imagen')(model)) &&
!model.id.endsWith('-nothink') && !model.id.endsWith('-nothink') &&
!model.id.endsWith('-search'), !model.id.endsWith('-search') &&
!model.id.includes('embedding'),
provider: (provider: Provider) => { provider: (provider: Provider) => {
return extraProviderConfig({ return extraProviderConfig({
...provider, ...provider,
@@ -7,24 +7,27 @@ import {
} from '@cherrystudio/ai-core/provider' } from '@cherrystudio/ai-core/provider'
import { cacheService } from '@data/CacheService' import { cacheService } from '@data/CacheService'
import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models' import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
import { isNewApiProvider } from '@renderer/config/providers' import {
isAnthropicProvider,
isAzureOpenAIProvider,
isGeminiProvider,
isNewApiProvider
} from '@renderer/config/providers'
import { import {
getAwsBedrockAccessKeyId, getAwsBedrockAccessKeyId,
getAwsBedrockRegion, getAwsBedrockRegion,
getAwsBedrockSecretAccessKey getAwsBedrockSecretAccessKey
} from '@renderer/hooks/useAwsBedrock' } from '@renderer/hooks/useAwsBedrock'
import { createVertexProvider, isVertexAIConfigured } from '@renderer/hooks/useVertexAI' import { createVertexProvider, isVertexAIConfigured, isVertexProvider } from '@renderer/hooks/useVertexAI'
import { getProviderByModel } from '@renderer/services/AssistantService' import { getProviderByModel } from '@renderer/services/AssistantService'
import { loggerService } from '@renderer/services/LoggerService'
import store from '@renderer/store' import store from '@renderer/store'
import { isSystemProvider, type Model, type Provider } from '@renderer/types' import { isSystemProvider, type Model, type Provider, SystemProviderIds } from '@renderer/types'
import { formatApiHost } from '@renderer/utils/api' import { formatApiHost, formatAzureOpenAIApiHost, formatVertexApiHost, routeToEndpoint } from '@renderer/utils/api'
import { cloneDeep, trim } from 'lodash' import { cloneDeep } from 'lodash'
import { aihubmixProviderCreator, newApiResolverCreator, vertexAnthropicProviderCreator } from './config' import { aihubmixProviderCreator, newApiResolverCreator, vertexAnthropicProviderCreator } from './config'
import { COPILOT_DEFAULT_HEADERS } from './constants' import { COPILOT_DEFAULT_HEADERS } from './constants'
import { getAiSdkProviderId } from './factory' import { getAiSdkProviderId } from './factory'
const logger = loggerService.withContext('ProviderConfigProcessor')
/** /**
* API key * API key
@@ -56,13 +59,6 @@ function getRotatedApiKey(provider: Provider): string {
* provider的转换逻辑 * provider的转换逻辑
*/ */
function handleSpecialProviders(model: Model, provider: Provider): Provider { function handleSpecialProviders(model: Model, provider: Provider): Provider {
// if (provider.type === 'vertexai' && !isVertexProvider(provider)) {
// if (!isVertexAIConfigured()) {
// throw new Error('VertexAI is not configured. Please configure project, location and service account credentials.')
// }
// return createVertexProvider(provider)
// }
if (isNewApiProvider(provider)) { if (isNewApiProvider(provider)) {
return newApiResolverCreator(model, provider) return newApiResolverCreator(model, provider)
} }
@@ -79,43 +75,30 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
} }
/** /**
* provider的API Host * AISdk的BaseURL格式
* @param provider
* @returns
*/ */
function formatAnthropicApiHost(host: string): string {
const trimmedHost = host?.trim()
if (!trimmedHost) {
return ''
}
if (trimmedHost.endsWith('/')) {
return trimmedHost
}
if (trimmedHost.endsWith('/v1')) {
return `${trimmedHost}/`
}
return formatApiHost(trimmedHost)
}
function formatProviderApiHost(provider: Provider): Provider { function formatProviderApiHost(provider: Provider): Provider {
const formatted = { ...provider } const formatted = { ...provider }
if (formatted.anthropicApiHost) { if (formatted.anthropicApiHost) {
formatted.anthropicApiHost = formatAnthropicApiHost(formatted.anthropicApiHost) formatted.anthropicApiHost = formatApiHost(formatted.anthropicApiHost)
} }
if (formatted.type === 'anthropic') { if (isAnthropicProvider(provider)) {
const baseHost = formatted.anthropicApiHost || formatted.apiHost const baseHost = formatted.anthropicApiHost || formatted.apiHost
formatted.apiHost = formatAnthropicApiHost(baseHost) formatted.apiHost = formatApiHost(baseHost)
if (!formatted.anthropicApiHost) { if (!formatted.anthropicApiHost) {
formatted.anthropicApiHost = formatted.apiHost formatted.anthropicApiHost = formatted.apiHost
} }
} else if (formatted.id === 'copilot') { } else if (formatted.id === SystemProviderIds.copilot || formatted.id === SystemProviderIds.github) {
const trimmed = trim(formatted.apiHost) formatted.apiHost = formatApiHost(formatted.apiHost, false)
formatted.apiHost = trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed } else if (isGeminiProvider(formatted)) {
} else if (formatted.type === 'gemini') { formatted.apiHost = formatApiHost(formatted.apiHost, true, 'v1beta')
formatted.apiHost = formatApiHost(formatted.apiHost, 'v1beta') } else if (isAzureOpenAIProvider(formatted)) {
formatted.apiHost = formatAzureOpenAIApiHost(formatted.apiHost)
} else if (isVertexProvider(formatted)) {
formatted.apiHost = formatVertexApiHost(formatted)
} else { } else {
formatted.apiHost = formatApiHost(formatted.apiHost) formatted.apiHost = formatApiHost(formatted.apiHost)
} }
@@ -149,15 +132,15 @@ export function providerToAiSdkConfig(
options: ProviderSettingsMap[keyof ProviderSettingsMap] options: ProviderSettingsMap[keyof ProviderSettingsMap]
} { } {
const aiSdkProviderId = getAiSdkProviderId(actualProvider) const aiSdkProviderId = getAiSdkProviderId(actualProvider)
logger.debug('providerToAiSdkConfig', { aiSdkProviderId })
// 构建基础配置 // 构建基础配置
const { baseURL, endpoint } = routeToEndpoint(actualProvider.apiHost)
const baseConfig = { const baseConfig = {
baseURL: trim(actualProvider.apiHost), baseURL: baseURL,
apiKey: getRotatedApiKey(actualProvider) apiKey: getRotatedApiKey(actualProvider)
} }
const isCopilotProvider = actualProvider.id === 'copilot' const isCopilotProvider = actualProvider.id === SystemProviderIds.copilot
if (isCopilotProvider) { if (isCopilotProvider) {
const storedHeaders = store.getState().copilot.defaultHeaders ?? {} const storedHeaders = store.getState().copilot.defaultHeaders ?? {}
const options = ProviderConfigFactory.fromProvider('github-copilot-openai-compatible', baseConfig, { const options = ProviderConfigFactory.fromProvider('github-copilot-openai-compatible', baseConfig, {
@@ -178,6 +161,7 @@ export function providerToAiSdkConfig(
// 处理OpenAI模式 // 处理OpenAI模式
const extraOptions: any = {} const extraOptions: any = {}
extraOptions.endpoint = endpoint
if (actualProvider.type === 'openai-response' && !isOpenAIChatCompletionOnlyModel(model)) { if (actualProvider.type === 'openai-response' && !isOpenAIChatCompletionOnlyModel(model)) {
extraOptions.mode = 'responses' extraOptions.mode = 'responses'
} else if (aiSdkProviderId === 'openai') { } else if (aiSdkProviderId === 'openai') {
@@ -199,13 +183,11 @@ export function providerToAiSdkConfig(
} }
// azure // azure
if (aiSdkProviderId === 'azure' || actualProvider.type === 'azure-openai') { if (aiSdkProviderId === 'azure' || actualProvider.type === 'azure-openai') {
extraOptions.apiVersion = actualProvider.apiVersion // extraOptions.apiVersion = actualProvider.apiVersion 默认使用v1,不使用azure endpoint
baseConfig.baseURL += '/openai'
if (actualProvider.apiVersion === 'preview') { if (actualProvider.apiVersion === 'preview') {
extraOptions.mode = 'responses' extraOptions.mode = 'responses'
} else { } else {
extraOptions.mode = 'chat' extraOptions.mode = 'chat'
extraOptions.useDeploymentBasedUrls = true
} }
} }
@@ -227,22 +209,7 @@ export function providerToAiSdkConfig(
...googleCredentials, ...googleCredentials,
privateKey: formatPrivateKey(googleCredentials.privateKey) privateKey: formatPrivateKey(googleCredentials.privateKey)
} }
// extraOptions.headers = window.api.vertexAI.getAuthHeaders({ baseConfig.baseURL += aiSdkProviderId === 'google-vertex' ? '/publishers/google' : '/publishers/anthropic/models'
// projectId: project,
// serviceAccount: {
// privateKey: googleCredentials.privateKey,
// clientEmail: googleCredentials.clientEmail
// }
// })
if (baseConfig.baseURL.endsWith('/v1/')) {
baseConfig.baseURL = baseConfig.baseURL.slice(0, -4)
} else if (baseConfig.baseURL.endsWith('/v1')) {
baseConfig.baseURL = baseConfig.baseURL.slice(0, -3)
}
if (baseConfig.baseURL && !baseConfig.baseURL.includes('publishers/google')) {
baseConfig.baseURL = `${baseConfig.baseURL}/v1/projects/${project}/locations/${location}/publishers/google`
}
} }
if (hasProviderConfig(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') { if (hasProviderConfig(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') {
@@ -5,6 +5,16 @@ import { describe, expect, it, vi } from 'vitest'
import { DraggableList } from '../' import { DraggableList } from '../'
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
// mock @hello-pangea/dnd 组件 // mock @hello-pangea/dnd 组件
vi.mock('@hello-pangea/dnd', () => { vi.mock('@hello-pangea/dnd', () => {
return { return {
@@ -3,6 +3,16 @@ import { describe, expect, it, vi } from 'vitest'
import { DraggableVirtualList } from '../' import { DraggableVirtualList } from '../'
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
// Mock 依赖项 // Mock 依赖项
vi.mock('@hello-pangea/dnd', () => ({ vi.mock('@hello-pangea/dnd', () => ({
__esModule: true, __esModule: true,
@@ -16,8 +16,8 @@ import {
import { loggerService } from '@logger' import { loggerService } from '@logger'
import type { Selection } from '@react-types/shared' import type { Selection } from '@react-types/shared'
import ClaudeIcon from '@renderer/assets/images/models/claude.png' import ClaudeIcon from '@renderer/assets/images/models/claude.png'
import { permissionModeCards } from '@renderer/config/agent'
import { agentModelFilter, getModelLogoById } from '@renderer/config/models' import { agentModelFilter, getModelLogoById } from '@renderer/config/models'
import { permissionModeCards } from '@renderer/constants/permissionModes'
import { useAgents } from '@renderer/hooks/agents/useAgents' import { useAgents } from '@renderer/hooks/agents/useAgents'
import { useApiModels } from '@renderer/hooks/agents/useModels' import { useApiModels } from '@renderer/hooks/agents/useModels'
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent' import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
+1 -1
View File
@@ -1,4 +1,4 @@
import { makeSvgSizeAdaptive } from '@renderer/utils' import { makeSvgSizeAdaptive } from '@renderer/utils/image'
import DOMPurify from 'dompurify' import DOMPurify from 'dompurify'
/** /**
+3 -1
View File
@@ -18,6 +18,7 @@ interface BaseSelectorProps<V = string | number> {
options: SelectorOption<V>[] options: SelectorOption<V>[]
placeholder?: string placeholder?: string
placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom' placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom'
style?: React.CSSProperties
/** 字体大小 */ /** 字体大小 */
size?: number size?: number
/** 是否禁用 */ /** 是否禁用 */
@@ -45,6 +46,7 @@ const Selector = <V extends string | number>({
placement = 'bottomRight', placement = 'bottomRight',
size = 13, size = 13,
placeholder, placeholder,
style,
disabled = false, disabled = false,
multiple = false multiple = false
}: SelectorProps<V>) => { }: SelectorProps<V>) => {
@@ -137,7 +139,7 @@ const Selector = <V extends string | number>({
placement={placement} placement={placement}
open={open && !disabled} open={open && !disabled}
onOpenChange={handleOpenChange}> onOpenChange={handleOpenChange}>
<Label $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}> <Label style={style} $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
{label} {label}
<LabelIcon size={size + 3} /> <LabelIcon size={size + 3} />
</Label> </Label>
@@ -3,7 +3,7 @@ import { Box } from '@cherrystudio/ui'
import { getToastUtilities } from '@cherrystudio/ui' import { getToastUtilities } from '@cherrystudio/ui'
import TopViewMinappContainer from '@renderer/components/MinApp/TopViewMinappContainer' import TopViewMinappContainer from '@renderer/components/MinApp/TopViewMinappContainer'
import { useAppInit } from '@renderer/hooks/useAppInit' import { useAppInit } from '@renderer/hooks/useAppInit'
import { useShortcuts } from '@renderer/hooks/useShortcuts' import { useAllShortcuts } from '@renderer/hooks/useShortcuts'
import { Modal } from 'antd' import { Modal } from 'antd'
import type { PropsWithChildren } from 'react' import type { PropsWithChildren } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
@@ -35,8 +35,9 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
elementsRef.current = elements elementsRef.current = elements
const [modal, modalContextHolder] = Modal.useModal() const [modal, modalContextHolder] = Modal.useModal()
const { shortcuts } = useShortcuts() const shortcuts = useAllShortcuts()
const enableQuitFullScreen = shortcuts.find((item) => item.key === 'exit_fullscreen')?.enabled const enableQuitFullScreen = shortcuts.find((item) => item.definition.key === 'shortcut.app.exit_fullscreen')
?.preference.enabled
useAppInit() useAppInit()
@@ -23,6 +23,16 @@ const mocks = vi.hoisted(() => ({
} }
})) }))
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
// Mock antd components to prevent flaky snapshot tests // Mock antd components to prevent flaky snapshot tests
vi.mock('antd', () => { vi.mock('antd', () => {
const MockSpaceCompact: React.FC<React.PropsWithChildren<{ style?: React.CSSProperties }>> = ({ const MockSpaceCompact: React.FC<React.PropsWithChildren<{ style?: React.CSSProperties }>> = ({
@@ -18,6 +18,15 @@ describe('Qwen Model Detection', () => {
vi.mock('@renderer/services/AssistantService', () => ({ vi.mock('@renderer/services/AssistantService', () => ({
getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryai' }) getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryai' })
})) }))
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
}) })
test('isQwenReasoningModel', () => { test('isQwenReasoningModel', () => {
expect(isQwenReasoningModel({ id: 'qwen3-thinking' } as Model)).toBe(true) expect(isQwenReasoningModel({ id: 'qwen3-thinking' } as Model)).toBe(true)
@@ -2,6 +2,16 @@ import { describe, expect, it, vi } from 'vitest'
import { isDoubaoSeedAfter251015, isDoubaoThinkingAutoModel, isLingReasoningModel } from '../models/reasoning' import { isDoubaoSeedAfter251015, isDoubaoThinkingAutoModel, isLingReasoningModel } from '../models/reasoning'
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
// FIXME: Idk why it's imported. Maybe circular dependency somewhere // FIXME: Idk why it's imported. Maybe circular dependency somewhere
vi.mock('@renderer/services/AssistantService.ts', () => ({ vi.mock('@renderer/services/AssistantService.ts', () => ({
getDefaultAssistant: () => { getDefaultAssistant: () => {
+45
View File
@@ -1,5 +1,6 @@
import ClaudeAvatar from '@renderer/assets/images/models/claude.png' import ClaudeAvatar from '@renderer/assets/images/models/claude.png'
import type { AgentBase, AgentType } from '@renderer/types' import type { AgentBase, AgentType } from '@renderer/types'
import type { PermissionModeCard } from '@renderer/types/agent'
// base agent config. no default config for now. // base agent config. no default config for now.
const DEFAULT_AGENT_CONFIG: Omit<AgentBase, 'model'> = { const DEFAULT_AGENT_CONFIG: Omit<AgentBase, 'model'> = {
@@ -19,3 +20,47 @@ export const getAgentTypeAvatar = (type: AgentType): string => {
return '' return ''
} }
} }
export const permissionModeCards: PermissionModeCard[] = [
{
mode: 'default',
// t('agent.settings.tooling.permissionMode.default.title')
titleKey: 'agent.settings.tooling.permissionMode.default.title',
titleFallback: 'Default (ask before continuing)',
descriptionKey: 'agent.settings.tooling.permissionMode.default.description',
descriptionFallback: 'Read-only tools are pre-approved; everything else still needs permission.',
behaviorKey: 'agent.settings.tooling.permissionMode.default.behavior',
behaviorFallback: 'Read-only tools are pre-approved automatically.'
},
{
mode: 'plan',
// t('agent.settings.tooling.permissionMode.plan.title')
titleKey: 'agent.settings.tooling.permissionMode.plan.title',
titleFallback: 'Planning mode',
descriptionKey: 'agent.settings.tooling.permissionMode.plan.description',
descriptionFallback: 'Shares the default read-only tool set but presents a plan before execution.',
behaviorKey: 'agent.settings.tooling.permissionMode.plan.behavior',
behaviorFallback: 'Read-only defaults are pre-approved while execution remains disabled.'
},
{
mode: 'acceptEdits',
// t('agent.settings.tooling.permissionMode.acceptEdits.title')
titleKey: 'agent.settings.tooling.permissionMode.acceptEdits.title',
titleFallback: 'Auto-accept file edits',
descriptionKey: 'agent.settings.tooling.permissionMode.acceptEdits.description',
descriptionFallback: 'File edits and filesystem operations are automatically approved.',
behaviorKey: 'agent.settings.tooling.permissionMode.acceptEdits.behavior',
behaviorFallback: 'Pre-approves trusted filesystem tools so edits run immediately.'
},
{
mode: 'bypassPermissions',
// t('agent.settings.tooling.permissionMode.bypassPermissions.title')
titleKey: 'agent.settings.tooling.permissionMode.bypassPermissions.title',
titleFallback: 'Bypass permission checks',
descriptionKey: 'agent.settings.tooling.permissionMode.bypassPermissions.description',
descriptionFallback: 'All permission prompts are skipped — use with caution.',
behaviorKey: 'agent.settings.tooling.permissionMode.bypassPermissions.behavior',
behaviorFallback: 'Every tool is pre-approved automatically.',
caution: true
}
]
@@ -1741,6 +1741,7 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
id: 'DeepSeek-R1', id: 'DeepSeek-R1',
provider: 'cephalon', provider: 'cephalon',
name: 'DeepSeek-R1满血版', name: 'DeepSeek-R1满血版',
capabilities: [{ type: 'reasoning' }],
group: 'DeepSeek' group: 'DeepSeek'
} }
], ],
+13 -7
View File
@@ -1,7 +1,9 @@
import { getProviderByModel } from '@renderer/services/AssistantService' import { getProviderByModel } from '@renderer/services/AssistantService'
import type { Model } from '@renderer/types' import type { Model } from '@renderer/types'
import { SystemProviderIds } from '@renderer/types'
import { getLowerBaseModelName, isUserSelectedModelType } from '@renderer/utils' import { getLowerBaseModelName, isUserSelectedModelType } from '@renderer/utils'
import { isGeminiProvider, isNewApiProvider, isOpenAICompatibleProvider, isOpenAIProvider } from '../providers'
import { isEmbeddingModel, isRerankModel } from './embedding' import { isEmbeddingModel, isRerankModel } from './embedding'
import { isAnthropicModel } from './utils' import { isAnthropicModel } from './utils'
import { isPureGenerateImageModel, isTextToImageModel } from './vision' import { isPureGenerateImageModel, isTextToImageModel } from './vision'
@@ -65,12 +67,16 @@ export function isWebSearchModel(model: Model): boolean {
const modelId = getLowerBaseModelName(model.id, '/') const modelId = getLowerBaseModelName(model.id, '/')
// 不管哪个供应商都判断了 // bedrock和vertex不支持
if (isAnthropicModel(model)) { if (
isAnthropicModel(model) &&
(provider.id === SystemProviderIds['aws-bedrock'] || provider.id === SystemProviderIds.vertexai)
) {
return CLAUDE_SUPPORTED_WEBSEARCH_REGEX.test(modelId) return CLAUDE_SUPPORTED_WEBSEARCH_REGEX.test(modelId)
} }
if (provider.type === 'openai-response') { // TODO: 当其他供应商采用Response端点时,这个地方逻辑需要改进
if (isOpenAIProvider(provider)) {
if (isOpenAIWebSearchModel(model)) { if (isOpenAIWebSearchModel(model)) {
return true return true
} }
@@ -78,11 +84,11 @@ export function isWebSearchModel(model: Model): boolean {
return false return false
} }
if (provider.id === 'perplexity') { if (provider.id === SystemProviderIds.perplexity) {
return PERPLEXITY_SEARCH_MODELS.includes(modelId) return PERPLEXITY_SEARCH_MODELS.includes(modelId)
} }
if (provider.id === 'aihubmix') { if (provider.id === SystemProviderIds.aihubmix) {
// modelId 不以-search结尾 // modelId 不以-search结尾
if (!modelId.endsWith('-search') && GEMINI_SEARCH_REGEX.test(modelId)) { if (!modelId.endsWith('-search') && GEMINI_SEARCH_REGEX.test(modelId)) {
return true return true
@@ -95,13 +101,13 @@ export function isWebSearchModel(model: Model): boolean {
return false return false
} }
if (provider?.type === 'openai') { if (isOpenAICompatibleProvider(provider) || isNewApiProvider(provider)) {
if (GEMINI_SEARCH_REGEX.test(modelId) || isOpenAIWebSearchModel(model)) { if (GEMINI_SEARCH_REGEX.test(modelId) || isOpenAIWebSearchModel(model)) {
return true return true
} }
} }
if (provider.id === 'gemini' || provider?.type === 'gemini' || provider.type === 'vertexai') { if (isGeminiProvider(provider) || provider.id === SystemProviderIds.vertexai) {
return GEMINI_SEARCH_REGEX.test(modelId) return GEMINI_SEARCH_REGEX.test(modelId)
} }
+53 -6
View File
@@ -56,7 +56,14 @@ import VoyageAIProviderLogo from '@renderer/assets/images/providers/voyageai.png
import XirangProviderLogo from '@renderer/assets/images/providers/xirang.png' import XirangProviderLogo from '@renderer/assets/images/providers/xirang.png'
import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png' import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png' import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
import type { AtLeast, Provider, ProviderType, SystemProvider, SystemProviderId } from '@renderer/types' import type {
AtLeast,
AzureOpenAIProvider,
Provider,
ProviderType,
SystemProvider,
SystemProviderId
} from '@renderer/types'
import { isSystemProvider, OpenAIServiceTiers } from '@renderer/types' import { isSystemProvider, OpenAIServiceTiers } from '@renderer/types'
import { TOKENFLUX_HOST } from './constant' import { TOKENFLUX_HOST } from './constant'
@@ -348,7 +355,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
name: 'VertexAI', name: 'VertexAI',
type: 'vertexai', type: 'vertexai',
apiKey: '', apiKey: '',
apiHost: 'https://aiplatform.googleapis.com', apiHost: '',
models: SYSTEM_MODELS.vertexai, models: SYSTEM_MODELS.vertexai,
isSystem: true, isSystem: true,
enabled: false, enabled: false,
@@ -1288,7 +1295,7 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
}, },
vertexai: { vertexai: {
api: { api: {
url: 'https://console.cloud.google.com/apis/api/aiplatform.googleapis.com/overview' url: ''
}, },
websites: { websites: {
official: 'https://cloud.google.com/vertex-ai', official: 'https://cloud.google.com/vertex-ai',
@@ -1368,7 +1375,8 @@ const NOT_SUPPORT_ARRAY_CONTENT_PROVIDERS = [
'baichuan', 'baichuan',
'minimax', 'minimax',
'xirang', 'xirang',
'poe' 'poe',
'cephalon'
] as const satisfies SystemProviderId[] ] as const satisfies SystemProviderId[]
/** /**
@@ -1433,10 +1441,15 @@ export const isSupportServiceTierProvider = (provider: Provider) => {
) )
} }
const SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES = ['gemini', 'vertexai'] as const satisfies ProviderType[] const SUPPORT_URL_CONTEXT_PROVIDER_TYPES = [
'gemini',
'vertexai',
'anthropic',
'new-api'
] as const satisfies ProviderType[]
export const isSupportUrlContextProvider = (provider: Provider) => { export const isSupportUrlContextProvider = (provider: Provider) => {
return SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type) return SUPPORT_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type)
} }
const SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS = ['gemini', 'vertexai'] as const satisfies SystemProviderId[] const SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS = ['gemini', 'vertexai'] as const satisfies SystemProviderId[]
@@ -1449,3 +1462,37 @@ export const isGeminiWebSearchProvider = (provider: Provider) => {
export const isNewApiProvider = (provider: Provider) => { export const isNewApiProvider = (provider: Provider) => {
return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api' return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api'
} }
/**
* OpenAI
* @param {Provider} provider
* @returns {boolean} OpenAI
*/
export function isOpenAICompatibleProvider(provider: Provider): boolean {
return ['openai', 'new-api', 'mistral'].includes(provider.type)
}
export function isAzureOpenAIProvider(provider: Provider): provider is AzureOpenAIProvider {
return provider.type === 'azure-openai'
}
export function isOpenAIProvider(provider: Provider): boolean {
return provider.type === 'openai-response'
}
export function isAnthropicProvider(provider: Provider): boolean {
return provider.type === 'anthropic'
}
export function isGeminiProvider(provider: Provider): boolean {
return provider.type === 'gemini'
}
const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot'] as const satisfies SystemProviderId[]
export const isSupportAPIVersionProvider = (provider: Provider) => {
if (isSystemProvider(provider)) {
return !NOT_SUPPORT_API_VERSION_PROVIDERS.some((pid) => pid === provider.id)
}
return provider.apiOptions?.isNotSupportAPIVersion !== false
}
@@ -1,53 +0,0 @@
import type { PermissionMode } from '@renderer/types'
export type PermissionModeCard = {
mode: PermissionMode
titleKey: string
titleFallback: string
descriptionKey: string
descriptionFallback: string
behaviorKey: string
behaviorFallback: string
caution?: boolean
unsupported?: boolean
}
export const permissionModeCards: PermissionModeCard[] = [
{
mode: 'default',
titleKey: 'agent.settings.tooling.permissionMode.default.title',
titleFallback: 'Default (ask before continuing)',
descriptionKey: 'agent.settings.tooling.permissionMode.default.description',
descriptionFallback: 'Read-only tools are pre-approved; everything else still needs permission.',
behaviorKey: 'agent.settings.tooling.permissionMode.default.behavior',
behaviorFallback: 'Read-only tools are pre-approved automatically.'
},
{
mode: 'plan',
titleKey: 'agent.settings.tooling.permissionMode.plan.title',
titleFallback: 'Planning mode',
descriptionKey: 'agent.settings.tooling.permissionMode.plan.description',
descriptionFallback: 'Shares the default read-only tool set but presents a plan before execution.',
behaviorKey: 'agent.settings.tooling.permissionMode.plan.behavior',
behaviorFallback: 'Read-only defaults are pre-approved while execution remains disabled.'
},
{
mode: 'acceptEdits',
titleKey: 'agent.settings.tooling.permissionMode.acceptEdits.title',
titleFallback: 'Auto-accept file edits',
descriptionKey: 'agent.settings.tooling.permissionMode.acceptEdits.description',
descriptionFallback: 'File edits and filesystem operations are automatically approved.',
behaviorKey: 'agent.settings.tooling.permissionMode.acceptEdits.behavior',
behaviorFallback: 'Pre-approves trusted filesystem tools so edits run immediately.'
},
{
mode: 'bypassPermissions',
titleKey: 'agent.settings.tooling.permissionMode.bypassPermissions.title',
titleFallback: 'Bypass permission checks',
descriptionKey: 'agent.settings.tooling.permissionMode.bypassPermissions.description',
descriptionFallback: 'All permission prompts are skipped — use with caution.',
behaviorKey: 'agent.settings.tooling.permissionMode.bypassPermissions.behavior',
behaviorFallback: 'Every tool is pre-approved automatically.',
caution: true
}
]
+5 -11
View File
@@ -1,29 +1,23 @@
import { useAppSelector } from '@renderer/store' import { useShortcut } from '@renderer/hooks/useShortcuts'
import { IpcChannel } from '@shared/IpcChannel' import { IpcChannel } from '@shared/IpcChannel'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
const NavigationHandler: React.FC = () => { const NavigationHandler: React.FC = () => {
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const showSettingsShortcutEnabled = useAppSelector(
(state) => state.shortcuts.shortcuts.find((s) => s.key === 'show_settings')?.enabled
)
useHotkeys( useShortcut(
'meta+, ! ctrl+,', 'shortcut.app.show_settings',
function () { () => {
if (location.pathname.startsWith('/settings')) { if (location.pathname.startsWith('/settings')) {
return return
} }
navigate('/settings/provider') navigate('/settings/provider')
}, },
{ {
splitKey: '!',
enableOnContentEditable: true,
enableOnFormTags: true, enableOnFormTags: true,
enabled: showSettingsShortcutEnabled enableOnContentEditable: true
} }
) )
+157 -59
View File
@@ -1,7 +1,15 @@
import { isMac, isWin } from '@renderer/config/constant' import { useMultiplePreferences, usePreference } from '@data/hooks/usePreference'
import { useAppSelector } from '@renderer/store' import { isMac } from '@renderer/config/constant'
import { orderBy } from 'lodash' import type { PreferenceShortcutType } from '@shared/data/preference/preferenceTypes'
import { useCallback } from 'react' import { findShortcutDefinition, SHORTCUT_DEFINITIONS } from '@shared/shortcuts/definitions'
import type { ShortcutDefinition, ShortcutPreferenceKey, ShortcutPreferenceValue } from '@shared/shortcuts/types'
import {
coerceShortcutPreference,
convertAcceleratorToHotkey,
formatShortcutDisplay,
getDefaultShortcutPreference
} from '@shared/shortcuts/utils'
import { useCallback, useMemo } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
interface UseShortcutOptions { interface UseShortcutOptions {
@@ -9,85 +17,175 @@ interface UseShortcutOptions {
enableOnFormTags?: boolean enableOnFormTags?: boolean
enabled?: boolean enabled?: boolean
description?: string description?: string
enableOnContentEditable?: boolean
} }
const defaultOptions: UseShortcutOptions = { const defaultOptions: UseShortcutOptions = {
preventDefault: true, preventDefault: true,
enableOnFormTags: true, enableOnFormTags: true,
enabled: true enabled: true,
enableOnContentEditable: false
}
const resolvePreferenceValue = (
definition: ShortcutDefinition | undefined,
preference: PreferenceShortcutType | Record<string, unknown> | undefined
): ShortcutPreferenceValue | null => {
if (!definition) {
return null
}
return coerceShortcutPreference(definition, preference as PreferenceShortcutType | undefined)
} }
export const useShortcut = ( export const useShortcut = (
shortcutKey: string, shortcutKey: ShortcutPreferenceKey,
callback: (e: KeyboardEvent) => void, callback: (event: KeyboardEvent) => void,
options: UseShortcutOptions = defaultOptions options: UseShortcutOptions = defaultOptions
) => { ) => {
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts) const definition = useMemo(() => findShortcutDefinition(shortcutKey), [shortcutKey])
const [preference] = usePreference(shortcutKey)
const preferenceState = useMemo(() => resolvePreferenceValue(definition, preference), [definition, preference])
const formatShortcut = useCallback((shortcut: string[]) => { const hotkey = useMemo(() => {
return shortcut if (!definition || !preferenceState) {
.map((key) => { return 'none'
switch (key.toLowerCase()) { }
case 'command':
return 'meta'
case 'commandorcontrol':
return isMac ? 'meta' : 'ctrl'
default:
return key.toLowerCase()
}
})
.join('+')
}, [])
const shortcutConfig = shortcuts.find((s) => s.key === shortcutKey) if (definition.scope === 'main') {
return 'none'
}
if (!preferenceState.enabled) {
return 'none'
}
const effectiveBinding = preferenceState.binding.length > 0 ? preferenceState.binding : definition.defaultKey
if (!effectiveBinding.length) {
return 'none'
}
return convertAcceleratorToHotkey(effectiveBinding)
}, [definition, preferenceState])
useHotkeys( useHotkeys(
shortcutConfig?.enabled ? formatShortcut(shortcutConfig.shortcut) : 'none', hotkey,
(e) => { (event) => {
if (options.preventDefault) { if (options.preventDefault) {
e.preventDefault() event.preventDefault()
} }
if (options.enabled !== false) { if (options.enabled !== false) {
callback(e) callback(event)
} }
}, },
{ {
enableOnFormTags: options.enableOnFormTags, enableOnFormTags: options.enableOnFormTags,
description: options.description || shortcutConfig?.key, description: options.description ?? shortcutKey,
enabled: !!shortcutConfig?.enabled enabled: hotkey !== 'none',
} enableOnContentEditable: options.enableOnContentEditable
},
[hotkey, callback, options]
) )
} }
export function useShortcuts() { export const useShortcutDisplay = (shortcutKey: ShortcutPreferenceKey): string => {
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts) const definition = useMemo(() => findShortcutDefinition(shortcutKey), [shortcutKey])
return { shortcuts: orderBy(shortcuts, 'system', 'desc') } const [preference] = usePreference(shortcutKey)
const preferenceState = useMemo(() => resolvePreferenceValue(definition, preference), [definition, preference])
if (!definition || !preferenceState || !preferenceState.enabled) {
return ''
}
const displayBinding = preferenceState.binding.length > 0 ? preferenceState.binding : definition.defaultKey
if (!displayBinding.length) {
return ''
}
return formatShortcutDisplay(displayBinding, isMac)
} }
export function useShortcutDisplay(key: string) { export interface ShortcutListItem {
const formatShortcut = useCallback((shortcut: string[]) => { definition: ShortcutDefinition
return shortcut preference: ShortcutPreferenceValue
.map((key) => { defaultPreference: ShortcutPreferenceValue
switch (key.toLowerCase()) { updatePreference: (patch: Partial<PreferenceShortcutType>) => Promise<void>
case 'control': }
return isMac ? '⌃' : 'Ctrl'
case 'ctrl': export const useAllShortcuts = (): ShortcutListItem[] => {
return isMac ? '⌃' : 'Ctrl' const keyMap = useMemo(
case 'command': () =>
return isMac ? '⌘' : isWin ? 'Win' : 'Super' SHORTCUT_DEFINITIONS.reduce<Record<string, ShortcutPreferenceKey>>((acc, definition) => {
case 'alt': acc[definition.key] = definition.key
return isMac ? '⌥' : 'Alt' return acc
case 'shift': }, {}),
return isMac ? '⇧' : 'Shift' []
case 'commandorcontrol': )
return isMac ? '⌘' : 'Ctrl'
default: const [values, setValues] = useMultiplePreferences(keyMap)
return key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()
} const buildNextPreference = useCallback(
}) (
.join('+') state: ShortcutPreferenceValue,
}, []) currentValue: PreferenceShortcutType | undefined,
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts) patch: Partial<PreferenceShortcutType>
const shortcutConfig = shortcuts.find((s) => s.key === key) ): PreferenceShortcutType => {
return shortcutConfig?.enabled ? formatShortcut(shortcutConfig.shortcut) : '' const current = (currentValue ?? {}) as PreferenceShortcutType
const nextKey = Array.isArray(patch.key) ? patch.key : Array.isArray(current.key) ? current.key : state.rawBinding
const nextEnabled =
typeof patch.enabled === 'boolean'
? patch.enabled
: typeof current.enabled === 'boolean'
? current.enabled
: state.enabled
const nextEditable =
typeof patch.editable === 'boolean'
? patch.editable
: typeof current.editable === 'boolean'
? current.editable
: state.editable
const nextSystem =
typeof patch.system === 'boolean'
? patch.system
: typeof current.system === 'boolean'
? current.system
: state.system
return {
key: nextKey,
enabled: nextEnabled,
editable: nextEditable,
system: nextSystem
}
},
[]
)
return useMemo(
() =>
SHORTCUT_DEFINITIONS.map((definition) => {
const rawValue = values[definition.key] as PreferenceShortcutType | undefined
const preference = coerceShortcutPreference(definition, rawValue)
const defaultPreference = getDefaultShortcutPreference(definition)
const updatePreference = async (patch: Partial<PreferenceShortcutType>) => {
const currentValue = values[definition.key] as PreferenceShortcutType | undefined
const nextValue = buildNextPreference(preference, currentValue, patch)
await setValues({ [definition.key]: nextValue } as Partial<Record<string, PreferenceShortcutType>>)
}
return {
definition,
preference,
defaultPreference,
updatePreference
}
}),
[buildNextPreference, setValues, values]
)
} }
+1 -1
View File
@@ -42,7 +42,7 @@ export function getVertexAIServiceAccount() {
* Provider VertexProvider * Provider VertexProvider
*/ */
export function isVertexProvider(provider: Provider): provider is VertexProvider { export function isVertexProvider(provider: Provider): provider is VertexProvider {
return provider.type === 'vertexai' && 'googleCredentials' in provider return provider.type === 'vertexai'
} }
/** /**
+2 -2
View File
@@ -4148,7 +4148,6 @@
}, },
"anthropic_api_host": "Anthropic API Host", "anthropic_api_host": "Anthropic API Host",
"anthropic_api_host_preview": "Anthropic preview: {{url}}", "anthropic_api_host_preview": "Anthropic preview: {{url}}",
"anthropic_api_host_tip": "Only configure this when your provider exposes an Anthropic-compatible endpoint. Ending with / ignores v1, ending with # forces use of input address.",
"anthropic_api_host_tooltip": "Use only when the provider offers a Claude-compatible base URL.", "anthropic_api_host_tooltip": "Use only when the provider offers a Claude-compatible base URL.",
"api": { "api": {
"key": { "key": {
@@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "Preview: {{url}}", "preview": "Preview: {{url}}",
"reset": "Reset", "reset": "Reset",
"tip": "Ending with / ignores v1, ending with # forces use of input address" "tip": "ending with # forces use of input address"
} }
}, },
"api_host": "API Host", "api_host": "API Host",
"api_host_no_valid": "API address is invalid",
"api_host_preview": "Preview: {{url}}", "api_host_preview": "Preview: {{url}}",
"api_host_tooltip": "Override only when your provider requires a custom OpenAI-compatible endpoint.", "api_host_tooltip": "Override only when your provider requires a custom OpenAI-compatible endpoint.",
"api_key": { "api_key": {
+2 -2
View File
@@ -4148,7 +4148,6 @@
}, },
"anthropic_api_host": "Anthropic API 地址", "anthropic_api_host": "Anthropic API 地址",
"anthropic_api_host_preview": "Anthropic 预览:{{url}}", "anthropic_api_host_preview": "Anthropic 预览:{{url}}",
"anthropic_api_host_tip": "仅在服务商提供兼容 Anthropic 的地址时填写。以 / 结尾会忽略自动追加的 v1,以 # 结尾则强制使用原始地址。",
"anthropic_api_host_tooltip": "仅当服务商提供 Claude 兼容的基础地址时填写。", "anthropic_api_host_tooltip": "仅当服务商提供 Claude 兼容的基础地址时填写。",
"api": { "api": {
"key": { "key": {
@@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "预览: {{url}}", "preview": "预览: {{url}}",
"reset": "重置", "reset": "重置",
"tip": "/ 结尾忽略 v1 版本,# 结尾强制使用输入地址" "tip": "# 结尾强制使用输入地址"
} }
}, },
"api_host": "API 地址", "api_host": "API 地址",
"api_host_no_valid": "API 地址不合法",
"api_host_preview": "预览:{{url}}", "api_host_preview": "预览:{{url}}",
"api_host_tooltip": "仅在服务商需要自定义的 OpenAI 兼容地址时覆盖。", "api_host_tooltip": "仅在服务商需要自定义的 OpenAI 兼容地址时覆盖。",
"api_key": { "api_key": {
+2 -2
View File
@@ -4148,7 +4148,6 @@
}, },
"anthropic_api_host": "Anthropic API 主機地址", "anthropic_api_host": "Anthropic API 主機地址",
"anthropic_api_host_preview": "Anthropic 預覽:{{url}}", "anthropic_api_host_preview": "Anthropic 預覽:{{url}}",
"anthropic_api_host_tip": "僅在服務商提供與 Anthropic 相容的網址時設定。以 / 結尾會忽略自動附加的 v1,以 # 結尾則強制使用原始地址。",
"anthropic_api_host_tooltip": "僅在服務商提供 Claude 相容的基礎網址時設定。", "anthropic_api_host_tooltip": "僅在服務商提供 Claude 相容的基礎網址時設定。",
"api": { "api": {
"key": { "key": {
@@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "預覽:{{url}}", "preview": "預覽:{{url}}",
"reset": "重設", "reset": "重設",
"tip": "/ 結尾忽略 v1 版本,# 結尾強制使用輸入位址" "tip": "# 結尾強制使用輸入位址"
} }
}, },
"api_host": "API 主機地址", "api_host": "API 主機地址",
"api_host_no_valid": "API 位址不合法",
"api_host_preview": "預覽:{{url}}", "api_host_preview": "預覽:{{url}}",
"api_host_tooltip": "僅在服務商需要自訂的 OpenAI 相容端點時才覆蓋。", "api_host_tooltip": "僅在服務商需要自訂的 OpenAI 相容端點時才覆蓋。",
"api_key": { "api_key": {
+2 -2
View File
@@ -4148,7 +4148,6 @@
}, },
"anthropic_api_host": "Anthropic API-Adresse", "anthropic_api_host": "Anthropic API-Adresse",
"anthropic_api_host_preview": "Anthropic-Vorschau: {{url}}", "anthropic_api_host_preview": "Anthropic-Vorschau: {{url}}",
"anthropic_api_host_tip": "Nur bei Anbietern, die ein Anthropic-kompatibles Endpunkt anbieten. Eine / am Ende ignoriert automatisch hinzugefügtes v1, ein # am Ende erzwingt die Verwendung der ursprünglichen Adresse.",
"anthropic_api_host_tooltip": "Nur bei Anbietern, die ein Claude-kompatibles Basis-Endpunkt anbieten.", "anthropic_api_host_tooltip": "Nur bei Anbietern, die ein Claude-kompatibles Basis-Endpunkt anbieten.",
"api": { "api": {
"key": { "key": {
@@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "Vorschau: {{url}}", "preview": "Vorschau: {{url}}",
"reset": "Zurücksetzen", "reset": "Zurücksetzen",
"tip": "/ am Ende ignorieren v1-Version, # am Ende erzwingt die Verwendung der Eingabe-Adresse" "tip": "# am Ende erzwingt die Verwendung der Eingabe-Adresse"
} }
}, },
"api_host": "API-Adresse", "api_host": "API-Adresse",
"api_host_no_valid": "API-Adresse ist ungültig",
"api_host_preview": "Vorschau: {{url}}", "api_host_preview": "Vorschau: {{url}}",
"api_host_tooltip": "Nur bei Anbietern, die ein OpenAI-kompatibles Endpunkt anbieten. Eine / am Ende ignoriert automatisch hinzugefügtes v1, ein # am Ende erzwingt die Verwendung der ursprünglichen Adresse.", "api_host_tooltip": "Nur bei Anbietern, die ein OpenAI-kompatibles Endpunkt anbieten. Eine / am Ende ignoriert automatisch hinzugefügtes v1, ein # am Ende erzwingt die Verwendung der ursprünglichen Adresse.",
"api_key": { "api_key": {
+2 -2
View File
@@ -4148,7 +4148,6 @@
}, },
"anthropic_api_host": "Διεύθυνση API Anthropic", "anthropic_api_host": "Διεύθυνση API Anthropic",
"anthropic_api_host_preview": "Προεπισκόπηση Anthropic: {{url}}", "anthropic_api_host_preview": "Προεπισκόπηση Anthropic: {{url}}",
"anthropic_api_host_tip": "Συμπληρώστε μόνο εάν ο πάροχος προσφέρει συμβατή με Anthropic διεύθυνση. Η λήξη με / αγνοεί το v1 που προστίθεται αυτόματα, η λήξη με # επιβάλλει τη χρήση της αρχικής διεύθυνσης.",
"anthropic_api_host_tooltip": "Συμπληρώστε μόνο όταν ο πάροχος παρέχει βασική διεύθυνση συμβατή με Claude.", "anthropic_api_host_tooltip": "Συμπληρώστε μόνο όταν ο πάροχος παρέχει βασική διεύθυνση συμβατή με Claude.",
"api": { "api": {
"key": { "key": {
@@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "Προεπισκόπηση: {{url}}", "preview": "Προεπισκόπηση: {{url}}",
"reset": "Επαναφορά", "reset": "Επαναφορά",
"tip": "/τέλος αγνόηση v1 έκδοσης, #τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως" "tip": "#τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως"
} }
}, },
"api_host": "Διεύθυνση API", "api_host": "Διεύθυνση API",
"api_host_no_valid": "Η διεύθυνση API δεν είναι έγκυρη",
"api_host_preview": "Προεπισκόπηση: {{url}}", "api_host_preview": "Προεπισκόπηση: {{url}}",
"api_host_tooltip": "Αντικατάσταση μόνο όταν ο πάροχος απαιτεί προσαρμοσμένη διεύθυνση συμβατή με OpenAI.", "api_host_tooltip": "Αντικατάσταση μόνο όταν ο πάροχος απαιτεί προσαρμοσμένη διεύθυνση συμβατή με OpenAI.",
"api_key": { "api_key": {
+2 -2
View File
@@ -4148,7 +4148,6 @@
}, },
"anthropic_api_host": "Dirección API de Anthropic", "anthropic_api_host": "Dirección API de Anthropic",
"anthropic_api_host_preview": "Vista previa de Anthropic: {{url}}", "anthropic_api_host_preview": "Vista previa de Anthropic: {{url}}",
"anthropic_api_host_tip": "Rellenar solo si el proveedor ofrece una dirección compatible con Anthropic. Terminar con / ignora el v1 añadido automáticamente, terminar con # fuerza el uso de la dirección original.",
"anthropic_api_host_tooltip": "Rellenar solo cuando el proveedor proporcione una dirección base compatible con Claude.", "anthropic_api_host_tooltip": "Rellenar solo cuando el proveedor proporcione una dirección base compatible con Claude.",
"api": { "api": {
"key": { "key": {
@@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "Vista previa: {{url}}", "preview": "Vista previa: {{url}}",
"reset": "Restablecer", "reset": "Restablecer",
"tip": "Ignorar v1 al final con /, forzar uso de dirección de entrada con # al final" "tip": "forzar uso de dirección de entrada con # al final"
} }
}, },
"api_host": "Dirección API", "api_host": "Dirección API",
"api_host_no_valid": "La dirección de la API no es válida",
"api_host_preview": "Vista previa: {{url}}", "api_host_preview": "Vista previa: {{url}}",
"api_host_tooltip": "Sobrescribir solo cuando el proveedor necesite una dirección compatible con OpenAI personalizada.", "api_host_tooltip": "Sobrescribir solo cuando el proveedor necesite una dirección compatible con OpenAI personalizada.",
"api_key": { "api_key": {
+2 -2
View File
@@ -4148,7 +4148,6 @@
}, },
"anthropic_api_host": "Adresse API Anthropic", "anthropic_api_host": "Adresse API Anthropic",
"anthropic_api_host_preview": "Aperçu Anthropic : {{url}}", "anthropic_api_host_preview": "Aperçu Anthropic : {{url}}",
"anthropic_api_host_tip": "Remplir seulement si le fournisseur propose une adresse compatible Anthropic. Se terminant par / ignore le v1 ajouté automatiquement, se terminant par # force l'utilisation de l'adresse originale.",
"anthropic_api_host_tooltip": "Remplir seulement lorsque le fournisseur propose une adresse de base compatible Claude.", "anthropic_api_host_tooltip": "Remplir seulement lorsque le fournisseur propose une adresse de base compatible Claude.",
"api": { "api": {
"key": { "key": {
@@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "Aperçu : {{url}}", "preview": "Aperçu : {{url}}",
"reset": "Réinitialiser", "reset": "Réinitialiser",
"tip": "Ignorer la version v1 si terminé par /, forcer l'utilisation de l'adresse d'entrée si terminé par #" "tip": "forcer l'utilisation de l'adresse d'entrée si terminé par #"
} }
}, },
"api_host": "Adresse API", "api_host": "Adresse API",
"api_host_no_valid": "Adresse API invalide",
"api_host_preview": "Aperçu : {{url}}", "api_host_preview": "Aperçu : {{url}}",
"api_host_tooltip": "Remplacer seulement lorsque le fournisseur nécessite une adresse compatible OpenAI personnalisée.", "api_host_tooltip": "Remplacer seulement lorsque le fournisseur nécessite une adresse compatible OpenAI personnalisée.",
"api_key": { "api_key": {
+2 -2
View File
@@ -4148,7 +4148,6 @@
}, },
"anthropic_api_host": "Anthropic APIアドレス", "anthropic_api_host": "Anthropic APIアドレス",
"anthropic_api_host_preview": "Anthropic プレビュー:{{url}}", "anthropic_api_host_preview": "Anthropic プレビュー:{{url}}",
"anthropic_api_host_tip": "サービスプロバイダーがAnthropic互換のアドレスを提供する場合のみ入力してください。/で終わる場合は自動追加されるv1を無視し、#で終わる場合は元のアドレスを強制的に使用します。",
"anthropic_api_host_tooltip": "サービスプロバイダーがClaude互換のベースアドレスを提供する場合のみ入力してください。", "anthropic_api_host_tooltip": "サービスプロバイダーがClaude互換のベースアドレスを提供する場合のみ入力してください。",
"api": { "api": {
"key": { "key": {
@@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "プレビュー: {{url}}", "preview": "プレビュー: {{url}}",
"reset": "リセット", "reset": "リセット",
"tip": "/で終わる場合、v1を無視します。#で終わる場合、入力されたアドレスを強制的に使用します" "tip": "#で終わる場合、入力されたアドレスを強制的に使用します"
} }
}, },
"api_host": "APIホスト", "api_host": "APIホスト",
"api_host_no_valid": "APIアドレスが無効です",
"api_host_preview": "プレビュー:{{url}}", "api_host_preview": "プレビュー:{{url}}",
"api_host_tooltip": "サービスプロバイダーがカスタムOpenAI互換アドレスを必要とする場合のみ上書きしてください。", "api_host_tooltip": "サービスプロバイダーがカスタムOpenAI互換アドレスを必要とする場合のみ上書きしてください。",
"api_key": { "api_key": {
+2 -2
View File
@@ -4148,7 +4148,6 @@
}, },
"anthropic_api_host": "Endereço da API Anthropic", "anthropic_api_host": "Endereço da API Anthropic",
"anthropic_api_host_preview": "Pré-visualização Anthropic: {{url}}", "anthropic_api_host_preview": "Pré-visualização Anthropic: {{url}}",
"anthropic_api_host_tip": "Preencher apenas se o fornecedor oferecer um endereço compatível com Anthropic. Terminar com / ignora o v1 adicionado automaticamente, terminar com # força o uso do endereço original.",
"anthropic_api_host_tooltip": "Preencher apenas quando o fornecedor fornece um endereço base compatível com Claude.", "anthropic_api_host_tooltip": "Preencher apenas quando o fornecedor fornece um endereço base compatível com Claude.",
"api": { "api": {
"key": { "key": {
@@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "Pré-visualização: {{url}}", "preview": "Pré-visualização: {{url}}",
"reset": "Redefinir", "reset": "Redefinir",
"tip": "Ignorar v1 na versão finalizada com /, usar endereço de entrada forçado se terminar com #" "tip": "e forçar o uso do endereço original quando terminar com '#'"
} }
}, },
"api_host": "Endereço API", "api_host": "Endereço API",
"api_host_no_valid": "O endereço da API é inválido",
"api_host_preview": "Pré-visualização: {{url}}", "api_host_preview": "Pré-visualização: {{url}}",
"api_host_tooltip": "Substituir apenas quando o fornecedor necessita de um endereço compatível com OpenAI personalizado.", "api_host_tooltip": "Substituir apenas quando o fornecedor necessita de um endereço compatível com OpenAI personalizado.",
"api_key": { "api_key": {
+2 -2
View File
@@ -4148,7 +4148,6 @@
}, },
"anthropic_api_host": "Адрес API Anthropic", "anthropic_api_host": "Адрес API Anthropic",
"anthropic_api_host_preview": "Предпросмотр Anthropic: {{url}}", "anthropic_api_host_preview": "Предпросмотр Anthropic: {{url}}",
"anthropic_api_host_tip": "Заполняйте только если провайдер предоставляет совместимый с Anthropic адрес. Окончание на / игнорирует автоматически добавляемое v1, окончание на # принудительно использует оригинальный адрес.",
"anthropic_api_host_tooltip": "Заполняйте только когда провайдер предоставляет базовый адрес, совместимый с Claude.", "anthropic_api_host_tooltip": "Заполняйте только когда провайдер предоставляет базовый адрес, совместимый с Claude.",
"api": { "api": {
"key": { "key": {
@@ -4193,10 +4192,11 @@
"url": { "url": {
"preview": "Предпросмотр: {{url}}", "preview": "Предпросмотр: {{url}}",
"reset": "Сброс", "reset": "Сброс",
"tip": "Заканчивая на / игнорирует v1, заканчивая на # принудительно использует введенный адрес" "tip": "заканчивая на # принудительно использует введенный адрес"
} }
}, },
"api_host": "Хост API", "api_host": "Хост API",
"api_host_no_valid": "Недопустимый адрес API",
"api_host_preview": "Предпросмотр: {{url}}", "api_host_preview": "Предпросмотр: {{url}}",
"api_host_tooltip": "Переопределяйте только когда провайдер требует пользовательский адрес, совместимый с OpenAI.", "api_host_tooltip": "Переопределяйте только когда провайдер требует пользовательский адрес, совместимый с OpenAI.",
"api_key": { "api_key": {
+3 -3
View File
@@ -70,7 +70,7 @@ const Chat: FC<Props> = (props) => {
contentSearchRef.current?.disable() contentSearchRef.current?.disable()
}) })
useShortcut('search_message_in_chat', () => { useShortcut('shortcut.chat.search_message', () => {
try { try {
const selectedText = window.getSelection()?.toString().trim() const selectedText = window.getSelection()?.toString().trim()
contentSearchRef.current?.enable(selectedText) contentSearchRef.current?.enable(selectedText)
@@ -79,7 +79,7 @@ const Chat: FC<Props> = (props) => {
} }
}) })
useShortcut('rename_topic', async () => { useShortcut('shortcut.topic.rename', async () => {
const topic = props.activeTopic const topic = props.activeTopic
if (!topic) return if (!topic) return
@@ -98,7 +98,7 @@ const Chat: FC<Props> = (props) => {
}) })
useShortcut( useShortcut(
'new_topic', 'shortcut.topic.new',
() => { () => {
if (activeTopicOrSession !== 'session' || !activeAgentId) { if (activeTopicOrSession !== 'session' || !activeAgentId) {
return return
+3 -3
View File
@@ -37,9 +37,9 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
const { showTopics, toggleShowTopics } = useShowTopics() const { showTopics, toggleShowTopics } = useShowTopics()
const { isTopNavbar } = useNavbarPosition() const { isTopNavbar } = useNavbarPosition()
useShortcut('toggle_show_assistants', toggleShowAssistants) useShortcut('shortcut.app.toggle_show_assistants', toggleShowAssistants)
useShortcut('toggle_show_topics', () => { useShortcut('shortcut.topic.toggle_show_topics', () => {
if (topicPosition === 'right') { if (topicPosition === 'right') {
toggleShowTopics() toggleShowTopics()
} else { } else {
@@ -47,7 +47,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
} }
}) })
useShortcut('search_message', () => { useShortcut('shortcut.app.search_message', () => {
SearchPopup.show() SearchPopup.show()
}) })
@@ -2,7 +2,6 @@ import { Tooltip } from '@heroui/react'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { ActionIconButton } from '@renderer/components/Buttons' import { ActionIconButton } from '@renderer/components/Buttons'
import { QuickPanelView } from '@renderer/components/QuickPanel' import { QuickPanelView } from '@renderer/components/QuickPanel'
import { useAgent } from '@renderer/hooks/agents/useAgent'
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession' import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
import { useSession } from '@renderer/hooks/agents/useSession' import { useSession } from '@renderer/hooks/agents/useSession'
import { selectNewTopicLoading } from '@renderer/hooks/useMessageOperations' import { selectNewTopicLoading } from '@renderer/hooks/useMessageOperations'
@@ -47,10 +46,9 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
const [text, setText] = useState(_text) const [text, setText] = useState(_text)
const [inputFocus, setInputFocus] = useState(false) const [inputFocus, setInputFocus] = useState(false)
const { session } = useSession(agentId, sessionId) const { session } = useSession(agentId, sessionId)
const { agent } = useAgent(agentId)
const { apiServer } = useSettings() const { apiServer } = useSettings()
const { createDefaultSession, creatingSession } = useCreateDefaultSession(agentId) const { createDefaultSession, creatingSession } = useCreateDefaultSession(agentId)
const newTopicShortcut = useShortcutDisplay('new_topic') const newTopicShortcut = useShortcutDisplay('shortcut.topic.new')
const { sendMessageShortcut, fontSize, enableSpellCheck } = useSettings() const { sendMessageShortcut, fontSize, enableSpellCheck } = useSettings()
const textareaRef = useRef<TextAreaRef>(null) const textareaRef = useRef<TextAreaRef>(null)
@@ -185,7 +183,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
const userMessageBlocks: MessageBlock[] = [mainBlock] const userMessageBlocks: MessageBlock[] = [mainBlock]
// Extract the actual model ID from session.model (format: "provider:modelId") // Extract the actual model ID from session.model (format: "provider:modelId")
const [providerId, actualModelId] = agent?.model.split(':') ?? [undefined, undefined] const [providerId, actualModelId] = session?.model?.split(':') ?? [undefined, undefined]
// Try to find the actual model from providers // Try to find the actual model from providers
const actualModel = actualModelId ? getModel(actualModelId, providerId) : undefined const actualModel = actualModelId ? getModel(actualModelId, providerId) : undefined
@@ -231,7 +229,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
logger.warn('Failed to send message:', error as Error) logger.warn('Failed to send message:', error as Error)
} }
}, [ }, [
agent?.model, session?.model,
agentId, agentId,
dispatch, dispatch,
sendDisabled, sendDisabled,
@@ -723,13 +723,13 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
} }
}, [onPaste]) }, [onPaste])
useShortcut('new_topic', () => { useShortcut('shortcut.topic.new', () => {
addNewTopic() addNewTopic()
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR) EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
focusTextarea() focusTextarea()
}) })
useShortcut('clear_topic', clearTopic) useShortcut('shortcut.chat.clear', clearTopic)
useEffect(() => { useEffect(() => {
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true }) const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
@@ -5,6 +5,7 @@ import { loggerService } from '@logger'
import { ActionIconButton } from '@renderer/components/Buttons' import { ActionIconButton } from '@renderer/components/Buttons'
import type { QuickPanelListItem } from '@renderer/components/QuickPanel' import type { QuickPanelListItem } from '@renderer/components/QuickPanel'
import { import {
isAnthropicModel,
isGeminiModel, isGeminiModel,
isGenerateImageModel, isGenerateImageModel,
isMandatoryWebSearchModel, isMandatoryWebSearchModel,
@@ -193,8 +194,8 @@ const InputbarTools = ({
updateAssistant({ enableGenerateImage: !assistant.enableGenerateImage }) updateAssistant({ enableGenerateImage: !assistant.enableGenerateImage })
}, [assistant.enableGenerateImage, updateAssistant]) }, [assistant.enableGenerateImage, updateAssistant])
const newTopicShortcut = useShortcutDisplay('new_topic') const newTopicShortcut = useShortcutDisplay('shortcut.topic.new')
const clearTopicShortcut = useShortcutDisplay('clear_topic') const clearTopicShortcut = useShortcutDisplay('shortcut.chat.clear')
const toggleToolVisibility = useCallback( const toggleToolVisibility = useCallback(
(toolKey: InputBarToolType, isVisible: boolean | undefined) => { (toolKey: InputBarToolType, isVisible: boolean | undefined) => {
@@ -391,7 +392,7 @@ const InputbarTools = ({
label: t('chat.input.url_context'), label: t('chat.input.url_context'),
component: <UrlContextButton ref={urlContextButtonRef} assistantId={assistant.id} />, component: <UrlContextButton ref={urlContextButtonRef} assistantId={assistant.id} />,
condition: condition:
isGeminiModel(model) && (isGeminiModel(model) || isAnthropicModel(model)) &&
(isSupportUrlContextProvider(getProviderByModel(model)) || model.endpoint_type === 'gemini') (isSupportUrlContextProvider(getProviderByModel(model)) || model.endpoint_type === 'gemini')
}, },
{ {
@@ -9,10 +9,10 @@ interface Props {
} }
const NewContextButton: FC<Props> = ({ onNewContext }) => { const NewContextButton: FC<Props> = ({ onNewContext }) => {
const newContextShortcut = useShortcutDisplay('toggle_new_context') const newContextShortcut = useShortcutDisplay('shortcut.chat.toggle_new_context')
const { t } = useTranslation() const { t } = useTranslation()
useShortcut('toggle_new_context', onNewContext) useShortcut('shortcut.chat.toggle_new_context', onNewContext)
return ( return (
<Tooltip content={t('chat.input.new.context', { Command: newContextShortcut })} closeDelay={0}> <Tooltip content={t('chat.input.new.context', { Command: newContextShortcut })} closeDelay={0}>
@@ -268,7 +268,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
) )
}, [displayMessages.length, hasMore, isLoadingMore, messages, setTimeoutTimer]) }, [displayMessages.length, hasMore, isLoadingMore, messages, setTimeoutTimer])
useShortcut('copy_last_message', () => { useShortcut('shortcut.chat.copy_last_message', () => {
const lastMessage = last(messages) const lastMessage = last(messages)
if (lastMessage) { if (lastMessage) {
navigator.clipboard.writeText(getMainTextContent(lastMessage)) navigator.clipboard.writeText(getMainTextContent(lastMessage))
@@ -276,7 +276,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
} }
}) })
useShortcut('edit_last_user_message', () => { useShortcut('shortcut.chat.edit_last_user_message', () => {
const lastUserMessage = messagesRef.current.findLast((m) => m.role === 'user' && m.type !== 'clear') const lastUserMessage = messagesRef.current.findLast((m) => m.role === 'user' && m.type !== 'clear')
if (lastUserMessage) { if (lastUserMessage) {
EventEmitter.emit(EVENT_NAMES.EDIT_MESSAGE, lastUserMessage.id) EventEmitter.emit(EVENT_NAMES.EDIT_MESSAGE, lastUserMessage.id)
+3 -3
View File
@@ -38,9 +38,9 @@ const HeaderNavbar: FC<Props> = ({
const { showAssistants, toggleShowAssistants } = useShowAssistants() const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { showTopics, toggleShowTopics } = useShowTopics() const { showTopics, toggleShowTopics } = useShowTopics()
useShortcut('toggle_show_assistants', toggleShowAssistants) useShortcut('shortcut.app.toggle_show_assistants', toggleShowAssistants)
useShortcut('toggle_show_topics', () => { useShortcut('shortcut.topic.toggle_show_topics', () => {
if (topicPosition === 'right') { if (topicPosition === 'right') {
toggleShowTopics() toggleShowTopics()
} else { } else {
@@ -48,7 +48,7 @@ const HeaderNavbar: FC<Props> = ({
} }
}) })
useShortcut('search_message', () => { useShortcut('shortcut.app.search_message', () => {
SearchPopup.show() SearchPopup.show()
}) })
@@ -93,7 +93,7 @@ const KnowledgePage: FC = () => {
[deleteKnowledgeBase, handleEditKnowledgeBase, renameKnowledgeBase, t] [deleteKnowledgeBase, handleEditKnowledgeBase, renameKnowledgeBase, t]
) )
useShortcut('search_message', () => { useShortcut('shortcut.app.search_message', () => {
if (selectedBase) { if (selectedBase) {
KnowledgeSearchPopup.show({ base: selectedBase }).then() KnowledgeSearchPopup.show({ base: selectedBase }).then()
} }
@@ -1,6 +1,5 @@
import { Alert, Card, CardBody, CardHeader, Chip, Input, Switch } from '@heroui/react' import { Alert, Card, CardBody, CardHeader, Chip, Input, Switch } from '@heroui/react'
import { permissionModeCards } from '@renderer/constants/permissionModes' import { permissionModeCards } from '@renderer/config/agent'
import { useAgentClient } from '@renderer/hooks/agents/useAgentClient'
import { useMCPServers } from '@renderer/hooks/useMCPServers' import { useMCPServers } from '@renderer/hooks/useMCPServers'
import useScrollPosition from '@renderer/hooks/useScrollPosition' import useScrollPosition from '@renderer/hooks/useScrollPosition'
import type { import type {
@@ -17,9 +16,8 @@ import { AgentConfigurationSchema } from '@renderer/types'
import { Modal } from 'antd' import { Modal } from 'antd'
import { ShieldAlert, ShieldCheck, Wrench } from 'lucide-react' import { ShieldAlert, ShieldCheck, Wrench } from 'lucide-react'
import type { FC } from 'react' import type { FC } from 'react'
import { startTransition, useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { mutate } from 'swr'
import { SettingsContainer, SettingsItem, SettingsTitle } from './shared' import { SettingsContainer, SettingsItem, SettingsTitle } from './shared'
@@ -37,6 +35,13 @@ type AgentConfigurationState = AgentConfiguration & Record<string, unknown>
const defaultConfiguration: AgentConfigurationState = AgentConfigurationSchema.parse({}) const defaultConfiguration: AgentConfigurationState = AgentConfigurationSchema.parse({})
/**
* Computes the list of tool IDs that should be automatically approved for a given permission mode.
*
* @param mode - The permission mode to compute defaults for.
* @param tools - The full list of available tools.
* @returns An array of tool IDs that are approved by default for the specified mode.
*/
const computeModeDefaults = (mode: PermissionMode, tools: Tool[]): string[] => { const computeModeDefaults = (mode: PermissionMode, tools: Tool[]): string[] => {
const defaultToolIds = tools.filter((tool) => !tool.requirePermissions).map((tool) => tool.id) const defaultToolIds = tools.filter((tool) => !tool.requirePermissions).map((tool) => tool.id)
switch (mode) { switch (mode) {
@@ -66,52 +71,34 @@ const unique = (values: string[]) => Array.from(new Set(values))
export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, update }) => { export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, update }) => {
const { containerRef, handleScroll } = useScrollPosition('AgentToolingSettings', 100) const { containerRef, handleScroll } = useScrollPosition('AgentToolingSettings', 100)
const { t } = useTranslation() const { t } = useTranslation()
const client = useAgentClient()
const { mcpServers: allServers } = useMCPServers() const { mcpServers: allServers } = useMCPServers()
const [modal, contextHolder] = Modal.useModal() const [modal, contextHolder] = Modal.useModal()
const [configuration, setConfiguration] = useState<AgentConfigurationState>(defaultConfiguration) const configuration: AgentConfigurationState = useMemo(
const [selectedMode, setSelectedMode] = useState<PermissionMode>(defaultConfiguration.permission_mode) () => agentBase?.configuration ?? defaultConfiguration,
const [autoToolIds, setAutoToolIds] = useState<string[]>([]) [agentBase?.configuration]
const [approvedToolIds, setApprovedToolIds] = useState<string[]>([]) )
const selectedMode = useMemo(
() => agentBase?.configuration?.permission_mode ?? defaultConfiguration.permission_mode,
[agentBase?.configuration?.permission_mode]
)
const availableTools = useMemo(() => agentBase?.tools ?? [], [agentBase?.tools])
const autoToolIds = useMemo(() => computeModeDefaults(selectedMode, availableTools), [availableTools, selectedMode])
const approvedToolIds = useMemo(() => {
const allowed = agentBase?.allowed_tools ?? []
const sanitized = allowed.filter((id) => availableTools.some((tool) => tool.id === id))
// Ensure defaults are included even if backend omitted them
const merged = unique([...sanitized, ...autoToolIds])
return merged
}, [agentBase?.allowed_tools, autoToolIds, availableTools])
const selectedMcpIds = useMemo(() => agentBase?.mcps ?? [], [agentBase?.mcps])
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
const [isUpdatingMode, setIsUpdatingMode] = useState(false) const [isUpdatingMode, setIsUpdatingMode] = useState(false)
const [isUpdatingTools, setIsUpdatingTools] = useState(false) const [isUpdatingTools, setIsUpdatingTools] = useState(false)
const [selectedMcpIds, setSelectedMcpIds] = useState<string[]>([])
const [isUpdatingMcp, setIsUpdatingMcp] = useState(false) const [isUpdatingMcp, setIsUpdatingMcp] = useState(false)
const availableTools = useMemo(() => agentBase?.tools ?? [], [agentBase?.tools])
const availableServers = useMemo(() => allServers ?? [], [allServers]) const availableServers = useMemo(() => allServers ?? [], [allServers])
useEffect(() => {
if (!agentBase) {
setConfiguration(defaultConfiguration)
setSelectedMode(defaultConfiguration.permission_mode)
setApprovedToolIds([])
setAutoToolIds([])
setSelectedMcpIds([])
return
}
const parsed: AgentConfigurationState = AgentConfigurationSchema.parse(agentBase.configuration ?? {})
setConfiguration(parsed)
setSelectedMode(parsed.permission_mode)
const defaults = computeModeDefaults(parsed.permission_mode, availableTools)
setAutoToolIds(defaults)
const allowed = agentBase.allowed_tools ?? []
setApprovedToolIds((prev) => {
const sanitized = allowed.filter((id) => availableTools.some((tool) => tool.id === id))
const isSame = sanitized.length === prev.length && sanitized.every((id) => prev.includes(id))
if (isSame) {
return prev
}
// Ensure defaults are included even if backend omitted them
const merged = unique([...sanitized, ...defaults])
return merged
})
setSelectedMcpIds(agentBase.mcps ?? [])
}, [agentBase, availableTools])
const filteredTools = useMemo(() => { const filteredTools = useMemo(() => {
if (!searchTerm.trim()) { if (!searchTerm.trim()) {
return availableTools return availableTools
@@ -147,10 +134,6 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
configuration: nextConfiguration, configuration: nextConfiguration,
allowed_tools: merged allowed_tools: merged
} satisfies UpdateAgentBaseForm) } satisfies UpdateAgentBaseForm)
setConfiguration(nextConfiguration)
setSelectedMode(nextMode)
setAutoToolIds(defaults)
setApprovedToolIds(merged)
} finally { } finally {
setIsUpdatingMode(false) setIsUpdatingMode(false)
} }
@@ -212,33 +195,25 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
) )
const handleToggleTool = useCallback( const handleToggleTool = useCallback(
(toolId: string, isApproved: boolean) => { async (toolId: string, isApproved: boolean) => {
if (!agentBase || isUpdatingTools) { if (!agentBase || isUpdatingTools) {
return return
} }
startTransition(() => {
setApprovedToolIds((prev) => { const exists = approvedToolIds.includes(toolId)
const exists = prev.includes(toolId) if (isApproved === exists) {
if (isApproved === exists) { return
return prev }
} setIsUpdatingTools(true)
const next = isApproved ? [...prev, toolId] : prev.filter((id) => id !== toolId) const next = isApproved ? [...approvedToolIds, toolId] : approvedToolIds.filter((id) => id !== toolId)
const sanitized = unique( const sanitized = unique(next.filter((id) => availableTools.some((tool) => tool.id === id)).concat(autoToolIds))
next.filter((id) => availableTools.some((tool) => tool.id === id)).concat(autoToolIds) try {
) await update({ id: agentBase.id, allowed_tools: sanitized } satisfies UpdateAgentBaseForm)
setIsUpdatingTools(true) } finally {
void (async () => { setIsUpdatingTools(false)
try { }
await update({ id: agentBase.id, allowed_tools: sanitized } satisfies UpdateAgentBaseForm)
} finally {
setIsUpdatingTools(false)
}
})()
return sanitized
})
})
}, },
[agentBase, isUpdatingTools, availableTools, autoToolIds, update] [agentBase, isUpdatingTools, approvedToolIds, autoToolIds, availableTools, update]
) )
const { agentSummary, autoCount, customCount } = useMemo(() => { const { agentSummary, autoCount, customCount } = useMemo(() => {
@@ -258,31 +233,24 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
}, [selectedMode, autoToolIds, userAddedIds, availableTools.length, selectedMcpIds.length]) }, [selectedMode, autoToolIds, userAddedIds, availableTools.length, selectedMcpIds.length])
const handleToggleMcp = useCallback( const handleToggleMcp = useCallback(
(serverId: string, enabled: boolean) => { async (serverId: string, enabled: boolean) => {
if (!agentBase || isUpdatingMcp) { if (!agentBase || isUpdatingMcp) {
return return
} }
setSelectedMcpIds((prev) => { const exists = selectedMcpIds.includes(serverId)
const exists = prev.includes(serverId) if (enabled === exists) {
if (enabled === exists) { return
return prev }
} const next = enabled ? [...selectedMcpIds, serverId] : selectedMcpIds.filter((id) => id !== serverId)
const next = enabled ? [...prev, serverId] : prev.filter((id) => id !== serverId)
setIsUpdatingMcp(true) setIsUpdatingMcp(true)
void (async () => { try {
try { await update({ id: agentBase.id, mcps: next } satisfies UpdateAgentBaseForm)
await update({ id: agentBase.id, mcps: next } satisfies UpdateAgentBaseForm) } finally {
const refreshed = await client.getAgent(agentBase.id) setIsUpdatingMcp(false)
const key = client.agentPaths.withId(agentBase.id) }
mutate(key, refreshed, false)
} finally {
setIsUpdatingMcp(false)
}
})()
return next
})
}, },
[agentBase, isUpdatingMcp, client, update] [agentBase, isUpdatingMcp, selectedMcpIds, update]
) )
if (!agentBase) { if (!agentBase) {
@@ -2,11 +2,22 @@ import { Button, Flex, RowFlex, Switch, Tooltip, WarnTooltip } from '@cherrystud
import OpenAIAlert from '@renderer/components/Alert/OpenAIAlert' import OpenAIAlert from '@renderer/components/Alert/OpenAIAlert'
import { LoadingIcon } from '@renderer/components/Icons' import { LoadingIcon } from '@renderer/components/Icons'
import { ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup' import { ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup'
import Selector from '@renderer/components/Selector'
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models' import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
import { PROVIDER_URLS } from '@renderer/config/providers' import {
isAnthropicProvider,
isAzureOpenAIProvider,
isGeminiProvider,
isNewApiProvider,
isOpenAICompatibleProvider,
isOpenAIProvider,
isSupportAPIVersionProvider,
PROVIDER_URLS
} from '@renderer/config/providers'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider' import { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import { isVertexProvider } from '@renderer/hooks/useVertexAI'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import AnthropicSettings from '@renderer/pages/settings/ProviderSettings/AnthropicSettings' import AnthropicSettings from '@renderer/pages/settings/ProviderSettings/AnthropicSettings'
import { ModelList } from '@renderer/pages/settings/ProviderSettings/ModelList' import { ModelList } from '@renderer/pages/settings/ProviderSettings/ModelList'
@@ -14,15 +25,17 @@ import { checkApi } from '@renderer/services/ApiService'
import { isProviderSupportAuth } from '@renderer/services/ProviderService' import { isProviderSupportAuth } from '@renderer/services/ProviderService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { updateWebSearchProvider } from '@renderer/store/websearch' import { updateWebSearchProvider } from '@renderer/store/websearch'
import type { SystemProviderId } from '@renderer/types'
import { isSystemProvider, isSystemProviderId, SystemProviderIds } from '@renderer/types' import { isSystemProvider, isSystemProviderId, SystemProviderIds } from '@renderer/types'
import type { ApiKeyConnectivity } from '@renderer/types/healthCheck' import type { ApiKeyConnectivity } from '@renderer/types/healthCheck'
import { HealthStatus } from '@renderer/types/healthCheck' import { HealthStatus } from '@renderer/types/healthCheck'
import { import {
formatApiHost, formatApiHost,
formatApiKeys, formatApiKeys,
formatAzureOpenAIApiHost,
formatVertexApiHost,
getFancyProviderName, getFancyProviderName,
isAnthropicProvider, validateApiHost
isOpenAIProvider
} from '@renderer/utils' } from '@renderer/utils'
import { formatErrorMessage } from '@renderer/utils/error' import { formatErrorMessage } from '@renderer/utils/error'
import { Divider, Input, Select, Space } from 'antd' import { Divider, Input, Select, Space } from 'antd'
@@ -65,7 +78,9 @@ const ANTHROPIC_COMPATIBLE_PROVIDER_IDS = [
SystemProviderIds.dashscope, SystemProviderIds.dashscope,
SystemProviderIds.modelscope, SystemProviderIds.modelscope,
SystemProviderIds.aihubmix, SystemProviderIds.aihubmix,
SystemProviderIds.grok SystemProviderIds.grok,
SystemProviderIds.cherryin,
SystemProviderIds.longcat
] as const ] as const
type AnthropicCompatibleProviderId = (typeof ANTHROPIC_COMPATIBLE_PROVIDER_IDS)[number] type AnthropicCompatibleProviderId = (typeof ANTHROPIC_COMPATIBLE_PROVIDER_IDS)[number]
@@ -74,6 +89,8 @@ const isAnthropicCompatibleProviderId = (id: string): id is AnthropicCompatibleP
return ANTHROPIC_COMPATIBLE_PROVIDER_ID_SET.has(id) return ANTHROPIC_COMPATIBLE_PROVIDER_ID_SET.has(id)
} }
type HostField = 'apiHost' | 'anthropicApiHost'
const ProviderSetting: FC<Props> = ({ providerId }) => { const ProviderSetting: FC<Props> = ({ providerId }) => {
const { provider, updateProvider, models } = useProvider(providerId) const { provider, updateProvider, models } = useProvider(providerId)
const allProviders = useAllProviders() const allProviders = useAllProviders()
@@ -81,19 +98,23 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
const [apiHost, setApiHost] = useState(provider.apiHost) const [apiHost, setApiHost] = useState(provider.apiHost)
const [anthropicApiHost, setAnthropicHost] = useState<string | undefined>(provider.anthropicApiHost) const [anthropicApiHost, setAnthropicHost] = useState<string | undefined>(provider.anthropicApiHost)
const [apiVersion, setApiVersion] = useState(provider.apiVersion) const [apiVersion, setApiVersion] = useState(provider.apiVersion)
const [activeHostField, setActiveHostField] = useState<HostField>('apiHost')
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const { setTimeoutTimer } = useTimer() const { setTimeoutTimer } = useTimer()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const isAzureOpenAI = provider.id === 'azure-openai' || provider.type === 'azure-openai' const isAzureOpenAI = isAzureOpenAIProvider(provider)
const isDmxapi = provider.id === 'dmxapi' const isDmxapi = provider.id === 'dmxapi'
const hideApiInput = ['vertexai', 'aws-bedrock'].includes(provider.id) const noAPIInputProviders = ['aws-bedrock'] as const satisfies SystemProviderId[]
const hideApiInput = noAPIInputProviders.some((id) => id === provider.id)
const noAPIKeyInputProviders = ['copilot', 'vertexai'] as const satisfies SystemProviderId[]
const hideApiKeyInput = noAPIKeyInputProviders.some((id) => id === provider.id)
const providerConfig = PROVIDER_URLS[provider.id] const providerConfig = PROVIDER_URLS[provider.id]
const officialWebsite = providerConfig?.websites?.official const officialWebsite = providerConfig?.websites?.official
const apiKeyWebsite = providerConfig?.websites?.apiKey const apiKeyWebsite = providerConfig?.websites?.apiKey
const configedApiHost = providerConfig?.api?.url const configuredApiHost = providerConfig?.api?.url
const fancyProviderName = getFancyProviderName(provider) const fancyProviderName = getFancyProviderName(provider)
@@ -153,7 +174,12 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
) )
const onUpdateApiHost = () => { const onUpdateApiHost = () => {
if (apiHost.trim()) { if (!validateApiHost(apiHost)) {
setApiHost(provider.apiHost)
window.toast.error(t('settings.provider.api_host_no_valid'))
return
}
if (isVertexProvider(provider) || apiHost.trim()) {
updateProvider({ apiHost }) updateProvider({ apiHost })
} else { } else {
setApiHost(provider.apiHost) setApiHost(provider.apiHost)
@@ -240,27 +266,46 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
} }
} }
const onReset = () => { const onReset = useCallback(() => {
setApiHost(configedApiHost) setApiHost(configuredApiHost)
updateProvider({ apiHost: configedApiHost }) updateProvider({ apiHost: configuredApiHost })
} }, [configuredApiHost, updateProvider])
const isApiHostResettable = useMemo(() => {
return !isEmpty(configuredApiHost) && apiHost !== configuredApiHost
}, [configuredApiHost, apiHost])
const hostPreview = () => { const hostPreview = () => {
if (apiHost.endsWith('#')) { if (apiHost.endsWith('#')) {
return apiHost.replace('#', '') return apiHost.replace('#', '')
} }
if (provider.type === 'openai') {
return formatApiHost(apiHost) + 'chat/completions' if (isOpenAICompatibleProvider(provider)) {
return formatApiHost(apiHost, isSupportAPIVersionProvider(provider)) + '/chat/completions'
} }
if (provider.type === 'azure-openai') { if (isAzureOpenAIProvider(provider)) {
return formatApiHost(apiHost) + 'openai/v1' const apiVersion = provider.apiVersion
const path = !['preview', 'v1'].includes(apiVersion)
? `/v1/chat/completion?apiVersion=v1`
: `/v1/responses?apiVersion=v1`
return formatAzureOpenAIApiHost(apiHost) + path
} }
if (provider.type === 'anthropic') { if (isAnthropicProvider(provider)) {
return formatApiHost(apiHost) + 'messages' return formatApiHost(apiHost) + '/messages'
} }
return formatApiHost(apiHost) + 'responses'
if (isGeminiProvider(provider)) {
return formatApiHost(apiHost, true, 'v1beta') + '/models'
}
if (isOpenAIProvider(provider)) {
return formatApiHost(apiHost) + '/responses'
}
if (isVertexProvider(provider)) {
return formatVertexApiHost(provider) + '/publishers/google'
}
return formatApiHost(apiHost)
} }
// API key 连通性检查状态指示器,目前仅在失败时显示 // API key 连通性检查状态指示器,目前仅在失败时显示
@@ -289,31 +334,44 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
}, [provider.anthropicApiHost]) }, [provider.anthropicApiHost])
const canConfigureAnthropicHost = useMemo(() => { const canConfigureAnthropicHost = useMemo(() => {
if (isNewApiProvider(provider)) {
return true
}
return ( return (
provider.type !== 'anthropic' && isSystemProviderId(provider.id) && isAnthropicCompatibleProviderId(provider.id) provider.type !== 'anthropic' && isSystemProviderId(provider.id) && isAnthropicCompatibleProviderId(provider.id)
) )
}, [provider]) }, [provider])
const anthropicHostPreview = useMemo(() => { const anthropicHostPreview = useMemo(() => {
const rawHost = (anthropicApiHost ?? provider.anthropicApiHost)?.trim() const rawHost = anthropicApiHost ?? provider.anthropicApiHost
if (!rawHost) { const normalizedHost = formatApiHost(rawHost)
return ''
}
if (/\/messages\/?$/.test(rawHost)) {
return rawHost.replace(/\/$/, '')
}
let normalizedHost = rawHost
if (/\/v\d+(?:\/)?$/i.test(normalizedHost)) {
normalizedHost = normalizedHost.replace(/\/$/, '')
} else {
normalizedHost = formatApiHost(normalizedHost).replace(/\/$/, '')
}
return `${normalizedHost}/messages` return `${normalizedHost}/messages`
}, [anthropicApiHost, provider.anthropicApiHost]) }, [anthropicApiHost, provider.anthropicApiHost])
const hostSelectorOptions = useMemo(() => {
const options: { value: HostField; label: string }[] = [
{ value: 'apiHost', label: t('settings.provider.api_host') }
]
if (canConfigureAnthropicHost) {
options.push({ value: 'anthropicApiHost', label: t('settings.provider.anthropic_api_host') })
}
return options
}, [canConfigureAnthropicHost, t])
useEffect(() => {
if (!canConfigureAnthropicHost && activeHostField === 'anthropicApiHost') {
setActiveHostField('apiHost')
}
}, [canConfigureAnthropicHost, activeHostField])
const hostSelectorTooltip =
activeHostField === 'anthropicApiHost'
? t('settings.provider.anthropic_api_host_tooltip')
: t('settings.provider.api_host_tooltip')
const isAnthropicOAuth = () => provider.id === 'anthropic' && provider.authType === 'oauth' const isAnthropicOAuth = () => provider.id === 'anthropic' && provider.authType === 'oauth'
return ( return (
@@ -372,104 +430,119 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
)} )}
{!hideApiInput && !isAnthropicOAuth() && ( {!hideApiInput && !isAnthropicOAuth() && (
<> <>
<SettingSubtitle {!hideApiKeyInput && (
style={{ <>
marginTop: 5, <SettingSubtitle
display: 'flex', style={{
alignItems: 'center', marginTop: 5,
justifyContent: 'space-between' display: 'flex',
}}> alignItems: 'center',
{t('settings.provider.api_key.label')} justifyContent: 'space-between'
{provider.id !== 'copilot' && ( }}>
<Tooltip content={t('settings.provider.api.key.list.open')} delay={500}> {t('settings.provider.api_key.label')}
<Button variant="ghost" onClick={openApiKeyList} size="icon"> {provider.id !== 'copilot' && (
<Settings2 size={16} /> <Tooltip title={t('settings.provider.api.key.list.open')} delay={500}>
<Button variant="ghost" onClick={openApiKeyList} size="icon">
<Settings2 size={16} />
</Button>
</Tooltip>
)}
</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input.Password
value={localApiKey}
placeholder={t('settings.provider.api_key.label')}
onChange={(e) => setLocalApiKey(e.target.value)}
spellCheck={false}
autoFocus={provider.enabled && provider.apiKey === '' && !isProviderSupportAuth(provider)}
disabled={provider.id === 'copilot'}
suffix={renderStatusIndicator()}
/>
<Button
variant={isApiKeyConnectable ? 'ghost' : undefined}
onClick={onCheckApi}
disabled={!apiHost || apiKeyConnectivity.checking}>
{apiKeyConnectivity.checking ? (
<LoadingIcon />
) : apiKeyConnectivity.status === HealthStatus.SUCCESS ? (
<Check size={16} className="lucide-custom" />
) : (
t('settings.provider.check')
)}
</Button> </Button>
</Tooltip> </Space.Compact>
)} <SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
</SettingSubtitle> <RowFlex>
<Space.Compact style={{ width: '100%', marginTop: 5 }}> {apiKeyWebsite && !isDmxapi && (
<Input.Password <SettingHelpLink target="_blank" href={apiKeyWebsite}>
value={localApiKey} {t('settings.provider.get_api_key')}
placeholder={t('settings.provider.api_key.label')} </SettingHelpLink>
onChange={(e) => setLocalApiKey(e.target.value)} )}
spellCheck={false} </RowFlex>
autoFocus={provider.enabled && provider.apiKey === '' && !isProviderSupportAuth(provider)} <SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
disabled={provider.id === 'copilot'} </SettingHelpTextRow>
suffix={renderStatusIndicator()} </>
/> )}
<Button {!isDmxapi && (
variant={isApiKeyConnectable ? 'ghost' : undefined}
onClick={onCheckApi}
disabled={!apiHost || apiKeyConnectivity.checking}>
{apiKeyConnectivity.checking ? (
<LoadingIcon />
) : apiKeyConnectivity.status === HealthStatus.SUCCESS ? (
<Check size={16} className="lucide-custom" />
) : (
t('settings.provider.check')
)}
</Button>
</Space.Compact>
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<RowFlex>
{apiKeyWebsite && !isDmxapi && (
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
{t('settings.provider.get_api_key')}
</SettingHelpLink>
)}
</RowFlex>
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
</SettingHelpTextRow>
{!isDmxapi && !isAnthropicOAuth() && (
<> <>
<SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Tooltip content={t('settings.provider.api_host_tooltip')} delay={300}> <Tooltip title={hostSelectorTooltip} delay={300}>
<SubtitleLabel>{t('settings.provider.api_host')}</SubtitleLabel> <Selector
size={14}
value={activeHostField}
onChange={(value) => setActiveHostField(value as HostField)}
options={hostSelectorOptions}
style={{ paddingLeft: 1, fontWeight: 'bold' }}
placement="bottomLeft"
/>
</Tooltip> </Tooltip>
<Button variant="ghost" onClick={() => CustomHeaderPopup.show({ provider })} size="icon"> <Button variant="ghost" onClick={() => CustomHeaderPopup.show({ provider })} size="icon">
<Settings2 size={16} /> <Settings2 size={16} />
</Button> </Button>
</SettingSubtitle> </SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}> {activeHostField === 'apiHost' && (
<Input <>
value={apiHost} <Space.Compact style={{ width: '100%', marginTop: 5 }}>
placeholder={t('settings.provider.api_host')} <Input
onChange={(e) => setApiHost(e.target.value)} value={apiHost}
onBlur={onUpdateApiHost} placeholder={t('settings.provider.api_host')}
/> onChange={(e) => setApiHost(e.target.value)}
{!isEmpty(configedApiHost) && apiHost !== configedApiHost && ( onBlur={onUpdateApiHost}
<Button variant="destructive" onClick={onReset}> />
{t('settings.provider.api.url.reset')} {isApiHostResettable && (
</Button> <Button variant="destructive" onClick={onReset}>
)} {t('settings.provider.api.url.reset')}
</Space.Compact> </Button>
)}
{(isOpenAIProvider(provider) || isAnthropicProvider(provider)) && ( </Space.Compact>
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}> {isVertexProvider(provider) && (
<SettingHelpText <SettingHelpTextRow>
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}> <SettingHelpText>{t('settings.provider.vertex_ai.api_host_help')}</SettingHelpText>
{t('settings.provider.api_host_preview', { url: hostPreview() })} </SettingHelpTextRow>
</SettingHelpText> )}
<SettingHelpText style={{ minWidth: 'fit-content' }}> {(isOpenAICompatibleProvider(provider) ||
{t('settings.provider.api.url.tip')} isAzureOpenAIProvider(provider) ||
</SettingHelpText> isAnthropicProvider(provider) ||
</SettingHelpTextRow> isGeminiProvider(provider) ||
isVertexProvider(provider) ||
isOpenAIProvider(provider)) && (
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<SettingHelpText
style={{
marginLeft: 6,
marginRight: '1em',
whiteSpace: 'break-spaces',
wordBreak: 'break-all'
}}>
{t('settings.provider.api_host_preview', { url: hostPreview() })}
</SettingHelpText>
</SettingHelpTextRow>
)}
</>
)} )}
{canConfigureAnthropicHost && ( {activeHostField === 'anthropicApiHost' && canConfigureAnthropicHost && (
<> <>
<SettingSubtitle
style={{
marginTop: 5,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<Tooltip content={t('settings.provider.anthropic_api_host_tooltip')} delay={300}>
<SubtitleLabel>{t('settings.provider.anthropic_api_host')}</SubtitleLabel>
</Tooltip>
</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}> <Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input <Input
value={anthropicApiHost ?? ''} value={anthropicApiHost ?? ''}
@@ -484,9 +557,6 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
url: anthropicHostPreview || '—' url: anthropicHostPreview || '—'
})} })}
</SettingHelpText> </SettingHelpText>
<SettingHelpText style={{ marginLeft: 6 }}>
{t('settings.provider.anthropic_api_host_tip')}
</SettingHelpText>
</SettingHelpTextRow> </SettingHelpTextRow>
</> </>
)} )}
@@ -516,21 +586,12 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
{provider.id === 'gpustack' && <GPUStackSettings />} {provider.id === 'gpustack' && <GPUStackSettings />}
{provider.id === 'copilot' && <GithubCopilotSettings providerId={provider.id} />} {provider.id === 'copilot' && <GithubCopilotSettings providerId={provider.id} />}
{provider.id === 'aws-bedrock' && <AwsBedrockSettings />} {provider.id === 'aws-bedrock' && <AwsBedrockSettings />}
{provider.id === 'vertexai' && <VertexAISettings providerId={provider.id} />} {provider.id === 'vertexai' && <VertexAISettings />}
<ModelList providerId={provider.id} /> <ModelList providerId={provider.id} />
</SettingContainer> </SettingContainer>
) )
} }
const SubtitleLabel = styled.span`
display: inline-flex;
align-items: center;
line-height: inherit;
font-size: inherit;
font-weight: inherit;
color: inherit;
`
const ProviderName = styled.span` const ProviderName = styled.span`
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
@@ -1,19 +1,13 @@
import { RowFlex } from '@cherrystudio/ui' import { RowFlex } from '@cherrystudio/ui'
import { PROVIDER_URLS } from '@renderer/config/providers' import { PROVIDER_URLS } from '@renderer/config/providers'
import { useProvider } from '@renderer/hooks/useProvider'
import { useVertexAISettings } from '@renderer/hooks/useVertexAI' import { useVertexAISettings } from '@renderer/hooks/useVertexAI'
import { Alert, Input, Space } from 'antd' import { Alert, Input } from 'antd'
import type { FC } from 'react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '..' import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '..'
interface Props { const VertexAISettings = () => {
providerId: string
}
const VertexAISettings: FC<Props> = ({ providerId }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
projectId, projectId,
@@ -28,16 +22,9 @@ const VertexAISettings: FC<Props> = ({ providerId }) => {
const [localProjectId, setLocalProjectId] = useState(projectId) const [localProjectId, setLocalProjectId] = useState(projectId)
const [localLocation, setLocalLocation] = useState(location) const [localLocation, setLocalLocation] = useState(location)
const { provider, updateProvider } = useProvider(providerId)
const [apiHost, setApiHost] = useState(provider.apiHost)
const providerConfig = PROVIDER_URLS['vertexai'] const providerConfig = PROVIDER_URLS['vertexai']
const apiKeyWebsite = providerConfig?.websites?.apiKey const apiKeyWebsite = providerConfig?.websites?.apiKey
const onUpdateApiHost = () => {
updateProvider({ apiHost })
}
const handleProjectIdChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleProjectIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setLocalProjectId(e.target.value) setLocalProjectId(e.target.value)
} }
@@ -73,18 +60,6 @@ const VertexAISettings: FC<Props> = ({ providerId }) => {
return ( return (
<> <>
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input
value={apiHost}
placeholder={t('settings.provider.api_host')}
onChange={(e) => setApiHost(e.target.value)}
onBlur={onUpdateApiHost}
/>
</Space.Compact>
<SettingHelpTextRow>
<SettingHelpText>{t('settings.provider.vertex_ai.api_host_help')}</SettingHelpText>
</SettingHelpTextRow>
<SettingSubtitle style={{ marginTop: 5 }}> <SettingSubtitle style={{ marginTop: 5 }}>
{t('settings.provider.vertex_ai.service_account.title')} {t('settings.provider.vertex_ai.service_account.title')}
</SettingSubtitle> </SettingSubtitle>
@@ -1,173 +1,150 @@
import { ClearOutlined, UndoOutlined } from '@ant-design/icons' import { ClearOutlined, UndoOutlined } from '@ant-design/icons'
import { Button, RowFlex, Switch, Tooltip } from '@cherrystudio/ui' import { Button, RowFlex, Switch, Tooltip } from '@cherrystudio/ui'
import { preferenceService } from '@data/PreferenceService'
import { isMac, isWin } from '@renderer/config/constant' import { isMac, isWin } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useShortcuts } from '@renderer/hooks/useShortcuts' import { useAllShortcuts } from '@renderer/hooks/useShortcuts'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import { getShortcutLabel } from '@renderer/i18n/label' import { getShortcutLabel } from '@renderer/i18n/label'
import { useAppDispatch } from '@renderer/store' import type { PreferenceShortcutType } from '@shared/data/preference/preferenceTypes'
import { initialState, resetShortcuts, toggleShortcut, updateShortcut } from '@renderer/store/shortcuts' import type { ShortcutPreferenceKey } from '@shared/shortcuts/types'
import type { Shortcut } from '@renderer/types' import { convertKeyToAccelerator, formatShortcutDisplay, isValidShortcut } from '@shared/shortcuts/utils'
import type { InputRef } from 'antd' import type { InputRef } from 'antd'
import { Input, Table as AntTable } from 'antd' import { Input, Table as AntTable } from 'antd'
import type { ColumnsType } from 'antd/es/table' import type { ColumnsType } from 'antd/es/table'
import type { FC } from 'react' import type { FC, KeyboardEvent as ReactKeyboardEvent } from 'react'
import React, { useRef, useState } from 'react' import { useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '.' import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '.'
const labelKeyMap: Record<string, string> = {
'shortcut.app.show_main_window': 'show_app',
'shortcut.app.show_mini_window': 'mini_window',
'shortcut.app.show_settings': 'show_settings',
'shortcut.app.toggle_show_assistants': 'toggle_show_assistants',
'shortcut.app.exit_fullscreen': 'exit_fullscreen',
'shortcut.app.zoom_in': 'zoom_in',
'shortcut.app.zoom_out': 'zoom_out',
'shortcut.app.zoom_reset': 'zoom_reset',
'shortcut.app.search_message': 'search_message',
'shortcut.chat.clear': 'clear_topic',
'shortcut.chat.search_message': 'search_message_in_chat',
'shortcut.chat.toggle_new_context': 'toggle_new_context',
'shortcut.chat.copy_last_message': 'copy_last_message',
'shortcut.chat.edit_last_user_message': 'edit_last_user_message',
'shortcut.topic.new': 'new_topic',
'shortcut.topic.rename': 'rename_topic',
'shortcut.topic.toggle_show_topics': 'toggle_show_topics',
'shortcut.selection.toggle_enabled': 'selection_assistant_toggle',
'shortcut.selection.get_text': 'selection_assistant_select_text'
}
type ShortcutRecord = {
id: string
label: string
key: ShortcutPreferenceKey
enabled: boolean
editable: boolean
displayKeys: string[]
rawKeys: string[]
hasCustomBinding: boolean
system: boolean
updatePreference: (patch: Partial<PreferenceShortcutType>) => Promise<void>
defaultPreference: {
binding: string[]
enabled: boolean
}
}
const ShortcutSettings: FC = () => { const ShortcutSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const dispatch = useAppDispatch() const shortcuts = useAllShortcuts()
const { shortcuts: originalShortcuts } = useShortcuts()
const inputRefs = useRef<Record<string, InputRef>>({}) const inputRefs = useRef<Record<string, InputRef>>({})
const [editingKey, setEditingKey] = useState<string | null>(null) const [editingKey, setEditingKey] = useState<string | null>(null)
const { setTimeoutTimer } = useTimer() const { setTimeoutTimer } = useTimer()
//if shortcut is not available on all the platforms, block the shortcut here const displayedShortcuts = useMemo<ShortcutRecord[]>(() => {
let shortcuts = originalShortcuts const filtered = !isWin && !isMac ? shortcuts.filter((item) => item.definition.category !== 'selection') : shortcuts
if (!isWin && !isMac) {
//Selection Assistant only available on Windows now return filtered.map((item) => {
const excludedShortcuts = ['selection_assistant_toggle', 'selection_assistant_select_text'] const labelKey = labelKeyMap[item.definition.key] ?? item.definition.key
shortcuts = shortcuts.filter((s) => !excludedShortcuts.includes(s.key)) const label = getShortcutLabel(labelKey)
const displayKeys = item.preference.hasCustomBinding
? item.preference.rawBinding
: item.preference.binding.length > 0
? item.preference.binding
: item.definition.defaultKey
return {
id: item.definition.key,
label,
key: item.definition.key,
enabled: item.preference.enabled,
editable: item.preference.editable,
displayKeys,
rawKeys: item.preference.rawBinding,
hasCustomBinding: item.preference.hasCustomBinding,
system: item.preference.system,
updatePreference: item.updatePreference,
defaultPreference: {
binding: item.defaultPreference.binding,
enabled: item.defaultPreference.enabled
}
}
})
}, [shortcuts])
const handleClear = (record: ShortcutRecord) => {
void record.updatePreference({ key: [] })
} }
const handleClear = (record: Shortcut) => { const handleAddShortcut = (record: ShortcutRecord) => {
dispatch( setEditingKey(record.id)
updateShortcut({
...record,
shortcut: []
})
)
}
const handleAddShortcut = (record: Shortcut) => {
setEditingKey(record.key)
setTimeoutTimer( setTimeoutTimer(
'handleAddShortcut', `focus-${record.id}`,
() => { () => {
inputRefs.current[record.key]?.focus() inputRefs.current[record.id]?.focus()
}, },
0 0
) )
} }
const isShortcutModified = (record: Shortcut) => { const isShortcutModified = (record: ShortcutRecord) => {
const defaultShortcut = initialState.shortcuts.find((s) => s.key === record.key) const bindingChanged = record.hasCustomBinding
return defaultShortcut?.shortcut.join('+') !== record.shortcut.join('+') ? record.rawKeys.length !== record.defaultPreference.binding.length ||
record.rawKeys.some((key, index) => key !== record.defaultPreference.binding[index])
: false
const enabledChanged = record.enabled !== record.defaultPreference.enabled
return bindingChanged || enabledChanged
} }
const handleResetShortcut = (record: Shortcut) => { const handleResetShortcut = (record: ShortcutRecord) => {
const defaultShortcut = initialState.shortcuts.find((s) => s.key === record.key) void record.updatePreference({
if (defaultShortcut) { key: record.defaultPreference.binding,
dispatch( enabled: record.defaultPreference.enabled
updateShortcut({ })
...record, setEditingKey(null)
shortcut: defaultShortcut.shortcut
})
)
}
} }
const isValidShortcut = (keys: string[]): boolean => { const isDuplicateShortcut = (keys: string[], currentKey: ShortcutPreferenceKey) => {
// OLD WAY FOR MODIFIER KEYS, KEEP THEM HERE FOR REFERENCE const normalized = keys.map((key) => key.toLowerCase()).join('+')
// const hasModifier = keys.some((key) => ['Control', 'Ctrl', 'Command', 'Alt', 'Shift'].includes(key))
// const hasNonModifier = keys.some((key) => !['Control', 'Ctrl', 'Command', 'Alt', 'Shift'].includes(key))
// NEW WAY FOR MODIFIER KEYS return displayedShortcuts.some((record) => {
const hasModifier = keys.some((key) => ['CommandOrControl', 'Ctrl', 'Alt', 'Meta', 'Shift'].includes(key)) if (record.key === currentKey) return false
const hasNonModifier = keys.some((key) => !['CommandOrControl', 'Ctrl', 'Alt', 'Meta', 'Shift'].includes(key)) const binding = record.displayKeys
if (!binding.length) return false
const hasFnKey = keys.some((key) => /^F\d+$/.test(key)) return binding.map((key) => key.toLowerCase()).join('+') === normalized
})
return (hasModifier && hasNonModifier && keys.length >= 2) || hasFnKey
} }
const isDuplicateShortcut = (newShortcut: string[], currentKey: string): boolean => { const usableEndKeys = (event: ReactKeyboardEvent): string | null => {
return shortcuts.some(
(s) => s.key !== currentKey && s.shortcut.length > 0 && s.shortcut.join('+') === newShortcut.join('+')
)
}
// how the shortcut is displayed in the UI
const formatShortcut = (shortcut: string[]): string => {
return shortcut
.map((key) => {
switch (key) {
// OLD WAY FOR MODIFIER KEYS, KEEP THEM HERE FOR REFERENCE
// case 'Control':
// return isMac ? '⌃' : 'Ctrl'
// case 'Ctrl':
// return isMac ? '⌃' : 'Ctrl'
// case 'Command':
// return isMac ? '⌘' : isWin ? 'Win' : 'Super'
// case 'Alt':
// return isMac ? '⌥' : 'Alt'
// case 'Shift':
// return isMac ? '⇧' : 'Shift'
// case 'CommandOrControl':
// return isMac ? '⌘' : 'Ctrl'
// new way for modifier keys
case 'CommandOrControl':
return isMac ? '⌘' : 'Ctrl'
case 'Ctrl':
return isMac ? '⌃' : 'Ctrl'
case 'Alt':
return isMac ? '⌥' : 'Alt'
case 'Meta':
return isMac ? '⌘' : isWin ? 'Win' : 'Super'
case 'Shift':
return isMac ? '⇧' : 'Shift'
// for backward compatibility with old data
case 'Command':
case 'Cmd':
return isMac ? '⌘' : 'Ctrl'
case 'Control':
return isMac ? '⌃' : 'Ctrl'
case 'ArrowUp':
return '↑'
case 'ArrowDown':
return '↓'
case 'ArrowLeft':
return '←'
case 'ArrowRight':
return '→'
case 'Slash':
return '/'
case 'Semicolon':
return ';'
case 'BracketLeft':
return '['
case 'BracketRight':
return ']'
case 'Backslash':
return '\\'
case 'Quote':
return "'"
case 'Comma':
return ','
case 'Minus':
return '-'
case 'Equal':
return '='
default:
return key.charAt(0).toUpperCase() + key.slice(1)
}
})
.join(' + ')
}
const usableEndKeys = (event: React.KeyboardEvent): string | null => {
const { code } = event const { code } = event
// No lock keys
// Among the commonly used keys, not including: Escape, NumpadMultiply, NumpadDivide, NumpadSubtract, NumpadAdd, NumpadDecimal
// The react-hotkeys-hook library does not differentiate between `Digit` and `Numpad`
switch (code) { switch (code) {
case 'KeyA': case 'KeyA':
case 'KeyB': case 'KeyB':
@@ -217,10 +194,15 @@ const ShortcutSettings: FC = () => {
case 'Numpad9': case 'Numpad9':
return code.slice(-1) return code.slice(-1)
case 'Space': case 'Space':
return 'Space'
case 'Enter': case 'Enter':
return 'Enter'
case 'Backspace': case 'Backspace':
return 'Backspace'
case 'Tab': case 'Tab':
return 'Tab'
case 'Delete': case 'Delete':
return 'Delete'
case 'PageUp': case 'PageUp':
case 'PageDown': case 'PageDown':
case 'Insert': case 'Insert':
@@ -256,7 +238,6 @@ const ShortcutSettings: FC = () => {
return '.' return '.'
case 'NumpadEnter': case 'NumpadEnter':
return 'Enter' return 'Enter'
// The react-hotkeys-hook library does not handle the symbol strings for the following keys
case 'Slash': case 'Slash':
case 'Semicolon': case 'Semicolon':
case 'BracketLeft': case 'BracketLeft':
@@ -272,28 +253,19 @@ const ShortcutSettings: FC = () => {
} }
} }
const handleKeyDown = (e: React.KeyboardEvent, record: Shortcut) => { const handleKeyDown = (event: ReactKeyboardEvent, record: ShortcutRecord) => {
e.preventDefault() event.preventDefault()
const keys: string[] = [] const keys: string[] = []
// OLD WAY FOR MODIFIER KEYS, KEEP THEM HERE FOR REFERENCE if (event.ctrlKey) keys.push(isMac ? 'Ctrl' : 'CommandOrControl')
// if (e.ctrlKey) keys.push(isMac ? 'Control' : 'Ctrl') if (event.altKey) keys.push('Alt')
// if (e.metaKey) keys.push('Command') if (event.metaKey) keys.push(isMac ? 'CommandOrControl' : 'Meta')
// if (e.altKey) keys.push('Alt') if (event.shiftKey) keys.push('Shift')
// if (e.shiftKey) keys.push('Shift')
// NEW WAY FOR MODIFIER KEYS const endKey = usableEndKeys(event)
// for capability across platforms, we transform the modifier keys to the really meaning keys
// mainly consider the habit of users on different platforms
if (e.ctrlKey) keys.push(isMac ? 'Ctrl' : 'CommandOrControl') // for win&linux, ctrl key is almost the same as command key in macOS
if (e.altKey) keys.push('Alt')
if (e.metaKey) keys.push(isMac ? 'CommandOrControl' : 'Meta') // for macOS, meta(Command) key is almost the same as Ctrl key in win&linux
if (e.shiftKey) keys.push('Shift')
const endKey = usableEndKeys(e)
if (endKey) { if (endKey) {
keys.push(endKey) keys.push(convertKeyToAccelerator(endKey))
} }
if (!isValidShortcut(keys)) { if (!isValidShortcut(keys)) {
@@ -304,7 +276,7 @@ const ShortcutSettings: FC = () => {
return return
} }
dispatch(updateShortcut({ ...record, shortcut: keys })) void record.updatePreference({ key: keys })
setEditingKey(null) setEditingKey(null)
} }
@@ -312,50 +284,60 @@ const ShortcutSettings: FC = () => {
window.modal.confirm({ window.modal.confirm({
title: t('settings.shortcuts.reset_defaults_confirm'), title: t('settings.shortcuts.reset_defaults_confirm'),
centered: true, centered: true,
onOk: () => dispatch(resetShortcuts()) onOk: async () => {
const updates: Record<string, PreferenceShortcutType> = {}
shortcuts.forEach((item) => {
updates[item.definition.key] = {
key: item.defaultPreference.binding,
enabled: item.defaultPreference.enabled,
editable: item.defaultPreference.editable,
system: item.defaultPreference.system
}
})
await preferenceService.setMultiple(updates)
}
}) })
} }
// 由于启用了showHeader = false,不再需要title字段 const columns: ColumnsType<ShortcutRecord> = [
const columns: ColumnsType<Shortcut> = [
{ {
// title: t('settings.shortcuts.action'), dataIndex: 'label',
dataIndex: 'name', key: 'label'
key: 'name'
}, },
{ {
// title: t('settings.shortcuts.label'), dataIndex: 'displayKeys',
dataIndex: 'shortcut',
key: 'shortcut', key: 'shortcut',
align: 'right', align: 'right',
render: (shortcut: string[], record: Shortcut) => { render: (_value, record) => {
const isEditing = editingKey === record.key const isEditing = editingKey === record.id
const shortcutConfig = shortcuts.find((s) => s.key === record.key) const displayShortcut = record.displayKeys.length > 0 ? formatShortcutDisplay(record.displayKeys, isMac) : ''
const isEditable = shortcutConfig?.editable !== false const editingShortcut = record.rawKeys.length > 0 ? formatShortcutDisplay(record.rawKeys, isMac) : ''
return ( return (
<RowFlex className="items-center justify-end gap-2"> <RowFlex className="items-center justify-end gap-2">
<RowFlex className="relative items-center"> <RowFlex className="relative items-center">
{isEditing ? ( {isEditing ? (
<ShortcutInput <ShortcutInput
ref={(el) => { ref={(element) => {
if (el) { if (element) {
inputRefs.current[record.key] = el inputRefs.current[record.id] = element
} }
}} }}
value={formatShortcut(shortcut)} value={editingShortcut}
placeholder={t('settings.shortcuts.press_shortcut')} placeholder={t('settings.shortcuts.press_shortcut')}
onKeyDown={(e) => handleKeyDown(e, record)} onKeyDown={(event) => handleKeyDown(event, record)}
onBlur={(e) => { onBlur={(event) => {
const isUndoClick = e.relatedTarget?.closest('.shortcut-undo-icon') const isUndoClick = event.relatedTarget?.closest('.shortcut-undo-icon')
if (!isUndoClick) { if (!isUndoClick) {
setEditingKey(null) setEditingKey(null)
} }
}} }}
/> />
) : ( ) : (
<ShortcutText isEditable={isEditable} onClick={() => isEditable && handleAddShortcut(record)}> <ShortcutText isEditable={record.editable} onClick={() => record.editable && handleAddShortcut(record)}>
{shortcut.length > 0 ? formatShortcut(shortcut) : t('settings.shortcuts.press_shortcut')} {displayShortcut || t('settings.shortcuts.press_shortcut')}
</ShortcutText> </ShortcutText>
)} )}
</RowFlex> </RowFlex>
@@ -364,11 +346,10 @@ const ShortcutSettings: FC = () => {
} }
}, },
{ {
// title: t('settings.shortcuts.actions'),
key: 'actions', key: 'actions',
align: 'right', align: 'right',
width: '70px', width: 70,
render: (record: Shortcut) => ( render: (record) => (
<RowFlex className="items-center justify-end gap-2"> <RowFlex className="items-center justify-end gap-2">
<Tooltip content={t('settings.shortcuts.reset_to_default')}> <Tooltip content={t('settings.shortcuts.reset_to_default')}>
<Button size="icon-sm" onClick={() => handleResetShortcut(record)} disabled={!isShortcutModified(record)}> <Button size="icon-sm" onClick={() => handleResetShortcut(record)} disabled={!isShortcutModified(record)}>
@@ -379,7 +360,7 @@ const ShortcutSettings: FC = () => {
<Button <Button
size="icon-sm" size="icon-sm"
onClick={() => handleClear(record)} onClick={() => handleClear(record)}
disabled={record.shortcut.length === 0 || !record.editable}> disabled={record.rawKeys.length === 0 || !record.editable}>
<ClearOutlined /> <ClearOutlined />
</Button> </Button>
</Tooltip> </Tooltip>
@@ -387,12 +368,15 @@ const ShortcutSettings: FC = () => {
) )
}, },
{ {
// title: t('settings.shortcuts.enabled'),
key: 'enabled', key: 'enabled',
align: 'right', align: 'right',
width: '50px', width: 50,
render: (record: Shortcut) => ( render: (record) => (
<Switch size="sm" isSelected={record.enabled} onValueChange={() => dispatch(toggleShortcut(record.key))} /> <Switch
size="sm"
isSelected={record.enabled}
onValueChange={() => void record.updatePreference({ enabled: !record.enabled })}
/>
) )
} }
] ]
@@ -404,10 +388,11 @@ const ShortcutSettings: FC = () => {
<SettingDivider style={{ marginBottom: 0 }} /> <SettingDivider style={{ marginBottom: 0 }} />
<Table <Table
columns={columns as ColumnsType<unknown>} columns={columns as ColumnsType<unknown>}
dataSource={shortcuts.map((s) => ({ ...s, name: getShortcutLabel(s.key) }))} dataSource={displayedShortcuts}
pagination={false} pagination={false}
size="middle" size="middle"
showHeader={false} showHeader={false}
rowKey="id"
/> />
<SettingDivider style={{ marginBottom: 0 }} /> <SettingDivider style={{ marginBottom: 0 }} />
<RowFlex className="justify-end p-4"> <RowFlex className="justify-end p-4">
@@ -3,6 +3,7 @@ import type { Span } from '@opentelemetry/api'
import AiProvider from '@renderer/aiCore' import AiProvider from '@renderer/aiCore'
import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT, DEFAULT_KNOWLEDGE_THRESHOLD } from '@renderer/config/constant' import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT, DEFAULT_KNOWLEDGE_THRESHOLD } from '@renderer/config/constant'
import { getEmbeddingMaxContext } from '@renderer/config/embedings' import { getEmbeddingMaxContext } from '@renderer/config/embedings'
import { isGeminiProvider } from '@renderer/config/providers'
import { addSpan, endSpan } from '@renderer/services/SpanManagerService' import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
import store from '@renderer/store' import store from '@renderer/store'
import type { import type {
@@ -41,7 +42,7 @@ export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams
let host = aiProvider.getBaseURL() let host = aiProvider.getBaseURL()
const rerankHost = rerankAiProvider.getBaseURL() const rerankHost = rerankAiProvider.getBaseURL()
if (provider.type === 'gemini') { if (isGeminiProvider(provider)) {
host = host + '/v1beta/openai/' host = host + '/v1beta/openai/'
} }
+12 -1
View File
@@ -2720,7 +2720,6 @@ const migrateConfig = {
} }
}, },
'166': (state: RootState) => { '166': (state: RootState) => {
// added after 1.6.5 and 1.7.0-beta.2
try { try {
if (state.assistants.presets === undefined) { if (state.assistants.presets === undefined) {
state.assistants.presets = [] state.assistants.presets = []
@@ -2737,6 +2736,18 @@ const migrateConfig = {
if (dashscopeProvider) { if (dashscopeProvider) {
dashscopeProvider.anthropicApiHost = 'https://dashscope.aliyuncs.com/apps/anthropic' dashscopeProvider.anthropicApiHost = 'https://dashscope.aliyuncs.com/apps/anthropic'
} }
state.llm.providers.forEach((provider) => {
if (provider.id === SystemProviderIds['new-api'] && provider.type !== 'new-api') {
provider.type = 'new-api'
}
if (provider.id === SystemProviderIds.longcat) {
// https://longcat.chat/platform/docs/zh/#anthropic-api-%E6%A0%BC%E5%BC%8F
if (!provider.anthropicApiHost) {
provider.anthropicApiHost = 'https://api.longcat.chat/anthropic'
}
}
})
return state return state
} catch (error) { } catch (error) {
logger.error('migrate 166 error', error as Error) logger.error('migrate 166 error', error as Error)
+12
View File
@@ -381,3 +381,15 @@ export type ReplaceSessionRequest = z.infer<typeof ReplaceSessionRequestSchema>
export const CreateSessionMessageRequestSchema = z.object({ export const CreateSessionMessageRequestSchema = z.object({
content: z.string().min(1, 'Content must be a valid string') content: z.string().min(1, 'Content must be a valid string')
}) })
export type PermissionModeCard = {
mode: PermissionMode
titleKey: string
titleFallback: string
descriptionKey: string
descriptionFallback: string
behaviorKey: string
behaviorFallback: string
caution?: boolean
unsupported?: boolean
}
+1 -1
View File
@@ -41,7 +41,7 @@ export type Assistant = {
/** enableWebSearch 代表使用模型内置网络搜索功能 */ /** enableWebSearch 代表使用模型内置网络搜索功能 */
enableWebSearch?: boolean enableWebSearch?: boolean
webSearchProviderId?: WebSearchProvider['id'] webSearchProviderId?: WebSearchProvider['id']
// enableUrlContext 是 Gemini 的特有功能 // enableUrlContext 是 Gemini/Anthropic 的特有功能
enableUrlContext?: boolean enableUrlContext?: boolean
enableGenerateImage?: boolean enableGenerateImage?: boolean
mcpServers?: MCPServer[] mcpServers?: MCPServer[]
+7 -1
View File
@@ -6,7 +6,6 @@ export const ProviderTypeSchema = z.enum([
'openai-response', 'openai-response',
'anthropic', 'anthropic',
'gemini', 'gemini',
'qwenlm',
'azure-openai', 'azure-openai',
'vertexai', 'vertexai',
'mistral', 'mistral',
@@ -37,6 +36,8 @@ export type ProviderApiOptions = {
isSupportServiceTier?: boolean isSupportServiceTier?: boolean
/** 是否不支持 enable_thinking 参数 */ /** 是否不支持 enable_thinking 参数 */
isNotSupportEnableThinking?: boolean isNotSupportEnableThinking?: boolean
/** 是否不支持 APIVersion */
isNotSupportAPIVersion?: boolean
} }
export const OpenAIServiceTiers = { export const OpenAIServiceTiers = {
@@ -187,6 +188,11 @@ export type VertexProvider = Provider & {
location: string location: string
} }
export type AzureOpenAIProvider = Provider & {
type: 'azure-openai'
apiVersion: string
}
/** /**
* 使`provider.isSystem` * 使`provider.isSystem`
* @param provider - Provider对象 * @param provider - Provider对象
+216 -23
View File
@@ -1,31 +1,106 @@
import { describe, expect, it } from 'vitest' import store from '@renderer/store'
import type { VertexProvider } from '@renderer/types'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { formatApiHost, maskApiKey, splitApiKeyString } from '../api' import {
formatApiHost,
formatApiKeys,
formatAzureOpenAIApiHost,
formatVertexApiHost,
hasAPIVersion,
maskApiKey,
routeToEndpoint,
splitApiKeyString,
validateApiHost
} from '../api'
vi.mock('@renderer/store', () => {
const getState = vi.fn()
return {
default: {
getState
}
}
})
const getStateMock = store.getState as unknown as ReturnType<typeof vi.fn>
const createVertexProvider = (apiHost: string): VertexProvider => ({
id: 'vertex-provider',
type: 'vertexai',
name: 'Vertex AI',
apiKey: '',
apiHost,
models: [],
googleCredentials: {
privateKey: '',
clientEmail: ''
},
project: '',
location: ''
})
beforeEach(() => {
getStateMock.mockReset()
getStateMock.mockReturnValue({
llm: {
settings: {
vertexai: {
projectId: 'test-project',
location: 'us-central1'
}
}
}
})
})
describe('api', () => { describe('api', () => {
describe('formatApiHost', () => { describe('formatApiHost', () => {
it('should return original host when it ends with a slash', () => { it('returns empty string for falsy host', () => {
expect(formatApiHost('https://api.example.com/')).toBe('https://api.example.com/')
expect(formatApiHost('http://localhost:5173/')).toBe('http://localhost:5173/')
})
it('should return original host when it ends with volces.com/api/v3', () => {
expect(formatApiHost('https://api.volces.com/api/v3')).toBe('https://api.volces.com/api/v3')
expect(formatApiHost('http://volces.com/api/v3')).toBe('http://volces.com/api/v3')
})
it('should append /v1/ to hosts that do not match special conditions', () => {
expect(formatApiHost('https://api.example.com')).toBe('https://api.example.com/v1/')
expect(formatApiHost('http://localhost:5173')).toBe('http://localhost:5173/v1/')
expect(formatApiHost('https://api.openai.com')).toBe('https://api.openai.com/v1/')
})
it('should not modify hosts that already have a path but do not end with a slash', () => {
expect(formatApiHost('https://api.example.com/custom')).toBe('https://api.example.com/custom/v1/')
})
it('should handle empty string gracefully', () => {
expect(formatApiHost('')).toBe('') expect(formatApiHost('')).toBe('')
expect(formatApiHost(undefined)).toBe('')
})
it('appends api version when missing', () => {
expect(formatApiHost('https://api.example.com')).toBe('https://api.example.com/v1')
expect(formatApiHost('http://localhost:5173/')).toBe('http://localhost:5173/v1')
expect(formatApiHost(' https://api.openai.com ')).toBe('https://api.openai.com/v1')
})
it('keeps original host when api version already present', () => {
expect(formatApiHost('https://api.volces.com/api/v3')).toBe('https://api.volces.com/api/v3')
expect(formatApiHost('http://localhost:5173/v2beta')).toBe('http://localhost:5173/v2beta')
})
it('supports custom api version parameter', () => {
expect(formatApiHost('https://api.example.com', true, 'v2')).toBe('https://api.example.com/v2')
})
it('keeps host untouched when api version unsupported', () => {
expect(formatApiHost('https://api.example.com', false)).toBe('https://api.example.com')
})
})
describe('hasAPIVersion', () => {
it('detects numeric version suffix', () => {
expect(hasAPIVersion('https://api.example.com/v1')).toBe(true)
expect(hasAPIVersion('http://localhost:3000/v2beta')).toBe(true)
expect(hasAPIVersion('/v3alpha/resources')).toBe(true)
})
it('returns false when no version found', () => {
expect(hasAPIVersion('https://api.example.com')).toBe(false)
expect(hasAPIVersion('')).toBe(false)
expect(hasAPIVersion(undefined)).toBe(false)
})
it('return flase when starting without v character', () => {
expect(hasAPIVersion('https://api.example.com/a1v')).toBe(false)
expect(hasAPIVersion('/av1/users')).toBe(false)
})
it('return flase when starting with v- word', () => {
expect(hasAPIVersion('https://api.example.com/vendor')).toBe(false)
}) })
}) })
@@ -123,4 +198,122 @@ describe('api', () => {
expect(result).toEqual(['key1', 'key2,withcomma', 'key3']) expect(result).toEqual(['key1', 'key2,withcomma', 'key3'])
}) })
}) })
describe('validateApiHost', () => {
it('accepts empty or whitespace-only host', () => {
expect(validateApiHost('')).toBe(true)
expect(validateApiHost(' ')).toBe(true)
})
it('rejects unsupported protocols', () => {
expect(validateApiHost('ftp://api.example.com')).toBe(false)
})
it('validates supported endpoint fragments when using hash suffix', () => {
expect(validateApiHost('https://api.example.com/v1/chat/completions#')).toBe(true)
expect(validateApiHost('https://api.example.com/v1/unknown#')).toBe(true)
})
})
describe('routeToEndpoint', () => {
it('returns host without endpoint when not using hash suffix', () => {
expect(routeToEndpoint(' https://api.example.com/v1 ')).toEqual({
baseURL: 'https://api.example.com/v1',
endpoint: ''
})
})
it('extracts known endpoint and base url when using hash suffix', () => {
expect(routeToEndpoint('https://api.example.com/v1/chat/completions#')).toEqual({
baseURL: 'https://api.example.com/v1',
endpoint: 'chat/completions'
})
})
it('returns empty endpoint when unsupported endpoint fragment is provided', () => {
expect(routeToEndpoint('https://api.example.com/v1/custom#')).toEqual({
baseURL: 'https://api.example.com/v1/custom',
endpoint: ''
})
})
it('prefers the most specific endpoint match when multiple matches exist', () => {
expect(routeToEndpoint('https://api.example.com/v1/streamGenerateContent#')).toEqual({
baseURL: 'https://api.example.com/v1',
endpoint: 'streamGenerateContent'
})
})
it('extract OpenAI images generations endpoint', () => {
expect(routeToEndpoint('https://open.cherryin.net/v1/images/generations#')).toEqual({
baseURL: 'https://open.cherryin.net/v1',
endpoint: 'images/generations'
})
})
it('extract Gemini images generation endpoint', () => {
expect(routeToEndpoint('https://open.cherryin.net/v1beta/models/imagen-4.0-generate-001:predict#')).toEqual({
baseURL: 'https://open.cherryin.net/v1beta/models/imagen-4.0-generate-001',
endpoint: 'predict'
})
})
})
describe('formatApiKeys', () => {
it('normalizes chinese commas and new lines', () => {
expect(formatApiKeys('key1key2\nkey3')).toBe('key1,key2,key3')
})
it('returns empty string unchanged', () => {
expect(formatApiKeys('')).toBe('')
})
})
describe('formatAzureOpenAIApiHost', () => {
it('normalizes trailing segments and disables auto version append', () => {
expect(formatAzureOpenAIApiHost('https://example.openai.azure.com/')).toBe(
'https://example.openai.azure.com/openai'
)
expect(formatAzureOpenAIApiHost('https://example.openai.azure.com/openai/')).toBe(
'https://example.openai.azure.com/openai'
)
})
})
describe('formatVertexApiHost', () => {
it('builds default google endpoint when host absent', () => {
expect(formatVertexApiHost(createVertexProvider(''))).toBe(
'https://us-central1-aiplatform.googleapis.com/v1/projects/test-project/locations/us-central1'
)
})
it('prefers default endpoint when host ends with google domain', () => {
expect(formatVertexApiHost(createVertexProvider('https://aiplatform.googleapis.com'))).toBe(
'https://us-central1-aiplatform.googleapis.com/v1/projects/test-project/locations/us-central1'
)
})
it('appends api version to custom host', () => {
expect(formatVertexApiHost(createVertexProvider('https://custom.googleapis.com/vertex'))).toBe(
'https://custom.googleapis.com/vertex/v1'
)
})
it('uses global endpoint when location equals global', () => {
getStateMock.mockReturnValueOnce({
llm: {
settings: {
vertexai: {
projectId: 'global-project',
location: 'global'
}
}
}
})
expect(formatVertexApiHost(createVertexProvider(''))).toBe(
'https://aiplatform.googleapis.com/v1/projects/global-project/locations/global'
)
})
})
}) })
+11 -1
View File
@@ -1,8 +1,18 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import { runAsyncFunction } from '../index' import { runAsyncFunction } from '../index'
import { hasPath, isValidProxyUrl, removeQuotes, removeSpecialCharacters } from '../index' import { hasPath, isValidProxyUrl, removeQuotes, removeSpecialCharacters } from '../index'
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
describe('Unclassified Utils', () => { describe('Unclassified Utils', () => {
describe('runAsyncFunction', () => { describe('runAsyncFunction', () => {
it('should execute async function', async () => { it('should execute async function', async () => {
+11 -1
View File
@@ -1,7 +1,17 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import { isJSON, parseJSON } from '../index' import { isJSON, parseJSON } from '../index'
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
describe('json', () => { describe('json', () => {
describe('isJSON', () => { describe('isJSON', () => {
it('should return true for valid JSON string', () => { it('should return true for valid JSON string', () => {
+157 -17
View File
@@ -1,3 +1,7 @@
import store from '@renderer/store'
import type { VertexProvider } from '@renderer/types'
import { trim } from 'lodash'
/** /**
* API key * API key
* *
@@ -9,30 +13,166 @@ export function formatApiKeys(value: string): string {
} }
/** /**
* API * host path /v1/v2beta
* *
* host `/v1/` * @param host - host path
* - host `/` `volces.com/api/v3` * @returns path true false
* -
*
* @param {string} host - API
* @param {string} apiVersion - API
* @returns {string} API
*/ */
export function formatApiHost(host: string, apiVersion: string = 'v1'): string { export function hasAPIVersion(host?: string): boolean {
if (!host) { if (!host) return false
// 匹配路径中以 `/v<number>` 开头并可选跟随 `alpha` 或 `beta` 的版本段,
// 该段后面可以跟 `/` 或字符串结束(用于匹配诸如 `/v3alpha/resources` 的情况)。
const versionRegex = /\/v\d+(?:alpha|beta)?(?=\/|$)/i
try {
const url = new URL(host)
return versionRegex.test(url.pathname)
} catch {
// 若无法作为完整 URL 解析,则当作路径直接检测
return versionRegex.test(host)
}
}
/**
* Removes the trailing slash from a URL string if it exists.
*
* @template T - The string type to preserve type safety
* @param {T} url - The URL string to process
* @returns {T} The URL string without a trailing slash
*
* @example
* ```ts
* withoutTrailingSlash('https://example.com/') // 'https://example.com'
* withoutTrailingSlash('https://example.com') // 'https://example.com'
* ```
*/
export function withoutTrailingSlash<T extends string>(url: T): T {
return url.replace(/\/$/, '') as T
}
/**
* Formats an API host URL by normalizing it and optionally appending an API version.
*
* @param host - The API host URL to format. Leading/trailing whitespace will be trimmed and trailing slashes removed.
* @param isSupportedAPIVerion - Whether the API version is supported. Defaults to `true`.
* @param apiVersion - The API version to append if needed. Defaults to `'v1'`.
*
* @returns The formatted API host URL. If the host is empty after normalization, returns an empty string.
* If the host ends with '#', API version is not supported, or the host already contains a version, returns the normalized host as-is.
* Otherwise, returns the host with the API version appended.
*
* @example
* formatApiHost('https://api.example.com/') // Returns 'https://api.example.com/v1'
* formatApiHost('https://api.example.com#') // Returns 'https://api.example.com#'
* formatApiHost('https://api.example.com/v2', true, 'v1') // Returns 'https://api.example.com/v2'
*/
export function formatApiHost(host?: string, isSupportedAPIVerion: boolean = true, apiVersion: string = 'v1'): string {
const normalizedHost = withoutTrailingSlash(trim(host))
if (!normalizedHost) {
return '' return ''
} }
const forceUseOriginalHost = () => { if (normalizedHost.endsWith('#') || !isSupportedAPIVerion || hasAPIVersion(normalizedHost)) {
if (host.endsWith('/')) { return normalizedHost
return true
}
return host.endsWith('volces.com/api/v3')
} }
return `${normalizedHost}/${apiVersion}`
}
return forceUseOriginalHost() ? host : `${host}/${apiVersion}/` /**
* Azure OpenAI API
*/
export function formatAzureOpenAIApiHost(host: string): string {
const normalizedHost = withoutTrailingSlash(host)
?.replace(/\/v1$/, '')
.replace(/\/openai$/, '')
// NOTE: AISDK会添加上`v1`
return formatApiHost(normalizedHost + '/openai', false)
}
export function formatVertexApiHost(provider: VertexProvider): string {
const { apiHost } = provider
const { projectId: project, location } = store.getState().llm.settings.vertexai
const trimmedHost = withoutTrailingSlash(trim(apiHost))
if (!trimmedHost || trimmedHost.endsWith('aiplatform.googleapis.com')) {
const host =
location == 'global' ? 'https://aiplatform.googleapis.com' : `https://${location}-aiplatform.googleapis.com`
return `${formatApiHost(host)}/projects/${project}/locations/${location}`
}
return formatApiHost(trimmedHost)
}
// 目前对话界面只支持这些端点
export const SUPPORTED_IMAGE_ENDPOINT_LIST = ['images/generations', 'images/edits', 'predict'] as const
export const SUPPORTED_ENDPOINT_LIST = [
'chat/completions',
'responses',
'messages',
'generateContent',
'streamGenerateContent',
...SUPPORTED_IMAGE_ENDPOINT_LIST
] as const
/**
* Converts an API host URL into separate base URL and endpoint components.
*
* @param apiHost - The API host string to parse. Expected to be a trimmed URL that may end with '#' followed by an endpoint identifier.
* @returns An object containing:
* - `baseURL`: The base URL without the endpoint suffix
* - `endpoint`: The matched endpoint identifier, or empty string if no match found
*
* @description
* This function extracts endpoint information from a composite API host string.
* If the host ends with '#', it attempts to match the preceding part against the supported endpoint list.
* The '#' delimiter is removed before processing.
*
* @example
* routeToEndpoint('https://api.example.com/openai/chat/completions#')
* // Returns: { baseURL: 'https://api.example.com/v1', endpoint: 'chat/completions' }
*
* @example
* routeToEndpoint('https://api.example.com/v1')
* // Returns: { baseURL: 'https://api.example.com/v1', endpoint: '' }
*/
export function routeToEndpoint(apiHost: string): { baseURL: string; endpoint: string } {
const trimmedHost = trim(apiHost)
// 前面已经确保apiHost合法
if (!trimmedHost.endsWith('#')) {
return { baseURL: trimmedHost, endpoint: '' }
}
// 去掉结尾的 #
const host = trimmedHost.slice(0, -1)
const endpointMatch = SUPPORTED_ENDPOINT_LIST.find((endpoint) => host.endsWith(endpoint))
if (!endpointMatch) {
const baseURL = withoutTrailingSlash(host)
return { baseURL, endpoint: '' }
}
const baseSegment = host.slice(0, host.length - endpointMatch.length)
const baseURL = withoutTrailingSlash(baseSegment).replace(/:$/, '') // 去掉结尾可能存在的冒号(gemini的特殊情况)
return { baseURL, endpoint: endpointMatch }
}
/**
* API
*
* @param {string} apiHost - API
* @returns {boolean} URL true false
*/
export function validateApiHost(apiHost: string): boolean {
// 允许apiHost为空
if (!apiHost || !trim(apiHost)) {
return true
}
try {
const url = new URL(trim(apiHost))
// 验证协议是否为 http 或 https
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
return false
}
return true
} catch {
return false
}
} }
/** /**
+8 -15
View File
@@ -1,5 +1,5 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import type { Model, ModelType, Provider } from '@renderer/types' import type { Model, ModelType } from '@renderer/types'
import type { ModalFuncProps } from 'antd' import type { ModalFuncProps } from 'antd'
import { isEqual } from 'lodash' import { isEqual } from 'lodash'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
@@ -55,7 +55,13 @@ export const waitAsyncFunction = (
})() })()
} }
export const uuid = () => uuidv4() /**
* Generate a UUID v4 string.
* @returns {string} A UUID v4 string
*/
export function uuid(): string {
return uuidv4()
}
/** /**
* *
@@ -196,19 +202,6 @@ export function getMcpConfigSampleFromReadme(readme: string): Record<string, any
return null return null
} }
/**
* OpenAI
* @param {Provider} provider
* @returns {boolean} OpenAI
*/
export function isOpenAIProvider(provider: Provider): boolean {
return !['anthropic', 'gemini', 'vertexai'].includes(provider.type)
}
export function isAnthropicProvider(provider: Provider): boolean {
return provider.type === 'anthropic'
}
/** /**
* *
* @param {Model} model * @param {Model} model
+68 -69
View File
@@ -74,11 +74,11 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@ai-sdk/amazon-bedrock@npm:^3.0.35": "@ai-sdk/amazon-bedrock@npm:^3.0.42":
version: 3.0.35 version: 3.0.42
resolution: "@ai-sdk/amazon-bedrock@npm:3.0.35" resolution: "@ai-sdk/amazon-bedrock@npm:3.0.42"
dependencies: dependencies:
"@ai-sdk/anthropic": "npm:2.0.27" "@ai-sdk/anthropic": "npm:2.0.32"
"@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12" "@ai-sdk/provider-utils": "npm:3.0.12"
"@smithy/eventstream-codec": "npm:^4.0.1" "@smithy/eventstream-codec": "npm:^4.0.1"
@@ -86,32 +86,32 @@ __metadata:
aws4fetch: "npm:^1.0.20" aws4fetch: "npm:^1.0.20"
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
checksum: 10c0/0e3e0ed1730fa6a14d8d7ca14b7823ec0b80c9d666435d97a505e7fb0c1818378343cdb647e3cc08d7f15d201cbeb04272c5128065f6cc6858b4404961eca761 checksum: 10c0/659de3d62f1907489bb14cd7fe049274c0a5f754222eda41b500d66573422ddaad3380cf8fc6eaae8a39ab25445e81aca7664ca2068b4a93c49bcb605889b2ba
languageName: node languageName: node
linkType: hard linkType: hard
"@ai-sdk/anthropic@npm:2.0.27, @ai-sdk/anthropic@npm:^2.0.27": "@ai-sdk/anthropic@npm:2.0.32, @ai-sdk/anthropic@npm:^2.0.32":
version: 2.0.27 version: 2.0.32
resolution: "@ai-sdk/anthropic@npm:2.0.27" resolution: "@ai-sdk/anthropic@npm:2.0.32"
dependencies: dependencies:
"@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12" "@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
checksum: 10c0/b568b3b8639af8ec7ea9b766061a4f18bcdef16f2bb12da3a4c4124c751bd6aab1b96dbe1a0eb8e38831d305871ce0787a6536d1a4d8a8ab8aaf03aca3e48e3f checksum: 10c0/f83ec81fe150dacd9207b67a173f7e150b44a0b2b57e6361c061e35b663bbb95240ea18066bd2bce73df722b85772ca174c4f1546b29eb6e6d1fcf4f349e756b
languageName: node languageName: node
linkType: hard linkType: hard
"@ai-sdk/azure@npm:^2.0.49": "@ai-sdk/azure@npm:^2.0.53":
version: 2.0.49 version: 2.0.53
resolution: "@ai-sdk/azure@npm:2.0.49" resolution: "@ai-sdk/azure@npm:2.0.53"
dependencies: dependencies:
"@ai-sdk/openai": "npm:2.0.48" "@ai-sdk/openai": "npm:2.0.52"
"@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12" "@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
checksum: 10c0/d4dc5a8e0cbe0cefc8db987c4a7b784a9898d40cc55ef38618c71eba7f40dbef77b754aec1d507559f643fed49e538ffe2b677b327f001a2efc0474f6b544ba9 checksum: 10c0/39346f50434c3568b40bb57aa64010261ae767d9aa49b4477999ca78431326275b111879b9c5431ce35ca4ca376c47455618c8bf528c54402b0dad1b03e10487
languageName: node languageName: node
linkType: hard linkType: hard
@@ -128,55 +128,55 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@ai-sdk/gateway@npm:1.0.39": "@ai-sdk/gateway@npm:2.0.0":
version: 1.0.39 version: 2.0.0
resolution: "@ai-sdk/gateway@npm:1.0.39" resolution: "@ai-sdk/gateway@npm:2.0.0"
dependencies: dependencies:
"@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12" "@ai-sdk/provider-utils": "npm:3.0.12"
"@vercel/oidc": "npm:3.0.2" "@vercel/oidc": "npm:3.0.3"
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
checksum: 10c0/1b6eedf12ac641c96a1eb75e48e43474694b60eb7dca273f76a636a4e2bfc89efda1d9855d5abf9cc464e23cdbf5a3119fed65c3d22cec726e29a2bad3c3318b checksum: 10c0/720cfb827bc64f3eb6bb86d17e7e7947c54bdc7d74db7f6e9e162be0973a45368c05829e4b257704182ca9c4886e7f3c74f6b64841e88359930f48f288aa958a
languageName: node languageName: node
linkType: hard linkType: hard
"@ai-sdk/google-vertex@npm:^3.0.40": "@ai-sdk/google-vertex@npm:^3.0.48":
version: 3.0.40 version: 3.0.48
resolution: "@ai-sdk/google-vertex@npm:3.0.40" resolution: "@ai-sdk/google-vertex@npm:3.0.48"
dependencies: dependencies:
"@ai-sdk/anthropic": "npm:2.0.27" "@ai-sdk/anthropic": "npm:2.0.32"
"@ai-sdk/google": "npm:2.0.20" "@ai-sdk/google": "npm:2.0.23"
"@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12" "@ai-sdk/provider-utils": "npm:3.0.12"
google-auth-library: "npm:^9.15.0" google-auth-library: "npm:^9.15.0"
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
checksum: 10c0/680a06e1b80bc036744e2f13e1a55b57661c3674000ab82b863d6536730edfc3696b1b0b2235f6354de11fa323c4ef817d8edbd2dbf94dc4037ea882e560c9ea checksum: 10c0/79f0ccb78c4930ea57a41e81f31a1935531d8f02b738d0aae13fa865272f4dac6b1c31b2e1c8b8ca65671a96b90cd4f14fabaa9d60ab0252c6c0e6a1828e7f09
languageName: node languageName: node
linkType: hard linkType: hard
"@ai-sdk/google@npm:2.0.20": "@ai-sdk/google@npm:2.0.23":
version: 2.0.20 version: 2.0.23
resolution: "@ai-sdk/google@npm:2.0.20" resolution: "@ai-sdk/google@npm:2.0.23"
dependencies: dependencies:
"@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12" "@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
checksum: 10c0/9c73bb67061673b16f0996c85bf4e79ab9968c8a203c4f9731bf569e45960db88950dfc227aca69661ea805d381b285697ba1763faa03a38c01b86e6d2e90629 checksum: 10c0/402b78f392196c3e23c75cc35fc1d701f9521b57aace2fb1bbae6a0d57bbb3894a778b0485305bd6674998403e44c3883dca2416f2d48377722351debead9f11
languageName: node languageName: node
linkType: hard linkType: hard
"@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.20#~/.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch": "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.23#~/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch":
version: 2.0.20 version: 2.0.23
resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.20#~/.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch::version=2.0.20&hash=1f2ccb" resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.23#~/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch::version=2.0.23&hash=df67ed"
dependencies: dependencies:
"@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12" "@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
checksum: 10c0/2d567361d533a4e2be83aa135cb5f01f09ea54c255d7751171855ef4244cfaeff73fe7b3c7b044b384a9c170e89d053160a26933176ad68dcaf03bd3c69c0be3 checksum: 10c0/e7fda169f04190b3ef37937e61219dcf8dade735cf76a9af8f1a1def83a43846659a361835814f0b68a2c392bc840a457a693cb69fed42af375771dd210ebdbe
languageName: node languageName: node
linkType: hard linkType: hard
@@ -242,27 +242,39 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@ai-sdk/openai@npm:2.0.48, @ai-sdk/openai@npm:^2.0.48": "@ai-sdk/openai@npm:2.0.52":
version: 2.0.48 version: 2.0.52
resolution: "@ai-sdk/openai@npm:2.0.48" resolution: "@ai-sdk/openai@npm:2.0.52"
dependencies: dependencies:
"@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12" "@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
checksum: 10c0/6c584d7ffb80025da6b7253106a83f8c7a023e8ca322fd32e6858453782d6a0a6d268d7afa7145e3ea743a9c6cbc882932bb59eb1a659750f5205639c414fb49 checksum: 10c0/253125303235dc677e272eaffbcd5c788373e12f897e42da7cce827bcc952f31e4bb11b72ba06931f37d49a2588f6cba8526127d539025bbd58d78d7bcfc691d
languageName: node languageName: node
linkType: hard linkType: hard
"@ai-sdk/openai@npm:^2.0.42": "@ai-sdk/openai@npm:^2.0.42":
version: 2.0.47 version: 2.0.53
resolution: "@ai-sdk/openai@npm:2.0.47" resolution: "@ai-sdk/openai@npm:2.0.53"
dependencies: dependencies:
"@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.11" "@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
checksum: 10c0/7fabcdda707134971bcc2b285705d4595f8bf419285dbdd9266b3b0858ea11b6ac200e63dd2eeb1822f99571910093d64d4a76154a365331cf184f56452933c6 checksum: 10c0/acb014c7e4d99be0502fe2190c3b91c76ee86ade25e80dad939ffd113a5f013f29a81f06e13fa0e6a76b49fcb8cc524aab180fc1a622ceb8d3dac58fd655de1c
languageName: node
linkType: hard
"@ai-sdk/openai@patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch":
version: 2.0.52
resolution: "@ai-sdk/openai@patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch::version=2.0.52&hash=c7ceb9"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/a3ac267a645ffd50952c312318d0ea6190e1ca961f910f9e3067211df731ac4ba0eb89face21b5cc195770b643326b295a6fece91f07b60db8aef32f45d4664e
languageName: node languageName: node
linkType: hard linkType: hard
@@ -291,19 +303,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@ai-sdk/provider-utils@npm:3.0.11":
version: 3.0.11
resolution: "@ai-sdk/provider-utils@npm:3.0.11"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@standard-schema/spec": "npm:^1.0.0"
eventsource-parser: "npm:^3.0.5"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/31081b127b48f3eefb448eaca59574b4631da9577aa0778622d28669c71bbde0361c9b37962c5edbb1d0c163ed1479755fc889da9251a03e906b1e27d0d2eb24
languageName: node
linkType: hard
"@ai-sdk/provider-utils@npm:3.0.12, @ai-sdk/provider-utils@npm:^3.0.12": "@ai-sdk/provider-utils@npm:3.0.12, @ai-sdk/provider-utils@npm:^3.0.12":
version: 3.0.12 version: 3.0.12
resolution: "@ai-sdk/provider-utils@npm:3.0.12" resolution: "@ai-sdk/provider-utils@npm:3.0.12"
@@ -2564,10 +2563,10 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@cherrystudio/ai-core@workspace:packages/aiCore" resolution: "@cherrystudio/ai-core@workspace:packages/aiCore"
dependencies: dependencies:
"@ai-sdk/anthropic": "npm:^2.0.27" "@ai-sdk/anthropic": "npm:^2.0.32"
"@ai-sdk/azure": "npm:^2.0.49" "@ai-sdk/azure": "npm:^2.0.53"
"@ai-sdk/deepseek": "npm:^1.0.23" "@ai-sdk/deepseek": "npm:^1.0.23"
"@ai-sdk/openai": "npm:^2.0.48" "@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch"
"@ai-sdk/openai-compatible": "npm:^1.0.22" "@ai-sdk/openai-compatible": "npm:^1.0.22"
"@ai-sdk/provider": "npm:^2.0.0" "@ai-sdk/provider": "npm:^2.0.0"
"@ai-sdk/provider-utils": "npm:^3.0.12" "@ai-sdk/provider-utils": "npm:^3.0.12"
@@ -16866,10 +16865,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@vercel/oidc@npm:3.0.2": "@vercel/oidc@npm:3.0.3":
version: 3.0.2 version: 3.0.3
resolution: "@vercel/oidc@npm:3.0.2" resolution: "@vercel/oidc@npm:3.0.3"
checksum: 10c0/8d4c8553baa5aed339ab7614d775139bc124a6d443b76877ab17e98c156daa4dbeb3cf2f3bf21fabfae2ac0dd3ff462ab43b9398708e02483e5923d302a1c4c8 checksum: 10c0/c8eecb1324559435f4ab8a955f5ef44f74f546d11c2ddcf28151cb636d989bd4b34e0673fd8716cb21bb21afb34b3de663bacc30c9506036eeecbcbf2fd86241
languageName: node languageName: node
linkType: hard linkType: hard
@@ -17176,8 +17175,8 @@ __metadata:
"@agentic/exa": "npm:^7.3.3" "@agentic/exa": "npm:^7.3.3"
"@agentic/searxng": "npm:^7.3.3" "@agentic/searxng": "npm:^7.3.3"
"@agentic/tavily": "npm:^7.3.3" "@agentic/tavily": "npm:^7.3.3"
"@ai-sdk/amazon-bedrock": "npm:^3.0.35" "@ai-sdk/amazon-bedrock": "npm:^3.0.42"
"@ai-sdk/google-vertex": "npm:^3.0.40" "@ai-sdk/google-vertex": "npm:^3.0.48"
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch" "@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch"
"@ai-sdk/mistral": "npm:^2.0.19" "@ai-sdk/mistral": "npm:^2.0.19"
"@ai-sdk/perplexity": "npm:^2.0.13" "@ai-sdk/perplexity": "npm:^2.0.13"
@@ -17305,7 +17304,7 @@ __metadata:
"@viz-js/lang-dot": "npm:^1.0.5" "@viz-js/lang-dot": "npm:^1.0.5"
"@viz-js/viz": "npm:^3.14.0" "@viz-js/viz": "npm:^3.14.0"
"@xyflow/react": "npm:^12.4.4" "@xyflow/react": "npm:^12.4.4"
ai: "npm:^5.0.68" ai: "npm:^5.0.76"
antd: "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch" antd: "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch"
archiver: "npm:^7.0.1" archiver: "npm:^7.0.1"
async-mutex: "npm:^0.5.0" async-mutex: "npm:^0.5.0"
@@ -17572,17 +17571,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ai@npm:^5.0.68": "ai@npm:^5.0.76":
version: 5.0.68 version: 5.0.76
resolution: "ai@npm:5.0.68" resolution: "ai@npm:5.0.76"
dependencies: dependencies:
"@ai-sdk/gateway": "npm:1.0.39" "@ai-sdk/gateway": "npm:2.0.0"
"@ai-sdk/provider": "npm:2.0.0" "@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12" "@ai-sdk/provider-utils": "npm:3.0.12"
"@opentelemetry/api": "npm:1.9.0" "@opentelemetry/api": "npm:1.9.0"
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
checksum: 10c0/0c042cd58c7193a47b06b3074a9e62790c4d5a8134e8e12bbb750714151e9aa217c641ee60c8cbe59d9869bade52ccbb283f9fcbf6d79711ebf1f774fa3feee3 checksum: 10c0/167a191354b72106b1af6cfc8b53975637ca43919b8f48db81c0cf542ef0172f55958ed9331adcd08d017a608a98cb0b4a253c62732038322c78091e32595771
languageName: node languageName: node
linkType: hard linkType: hard