Compare commits

..

1 Commits

Author SHA1 Message Date
beyondkmp
441fb1de53 refactor by codex 2025-10-29 17:48:35 +08:00
166 changed files with 4113 additions and 9181 deletions

View File

@@ -1,4 +1,4 @@
name: 🐛 Bug Report
name: 🐛 Bug Report (English)
description: Create a report to help us improve
title: '[Bug]: '
labels: ['BUG']

View File

@@ -1,4 +1,4 @@
name: 💡 Feature Request
name: 💡 Feature Request (English)
description: Suggest an idea for this project
title: '[Feature]: '
labels: ['feature']

View File

@@ -1,4 +1,4 @@
name: 🤔 Other Questions
name: 🤔 Other Questions (English)
description: Submit questions that don't fit into bug reports or feature requests
title: '[Other]: '
body:

View File

@@ -0,0 +1,13 @@
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

View File

@@ -1,26 +0,0 @@
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

View File

@@ -1,76 +0,0 @@
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" });
}

View File

@@ -0,0 +1,71 @@
diff --git a/dist/utils/tiktoken.cjs b/dist/utils/tiktoken.cjs
index 973b0d0e75aeaf8de579419af31b879b32975413..f23c7caa8b9dc8bd404132725346a4786f6b278b 100644
--- a/dist/utils/tiktoken.cjs
+++ b/dist/utils/tiktoken.cjs
@@ -1,25 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.encodingForModel = exports.getEncoding = void 0;
-const lite_1 = require("js-tiktoken/lite");
const async_caller_js_1 = require("./async_caller.cjs");
const cache = {};
const caller = /* #__PURE__ */ new async_caller_js_1.AsyncCaller({});
async function getEncoding(encoding) {
- if (!(encoding in cache)) {
- cache[encoding] = caller
- .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`)
- .then((res) => res.json())
- .then((data) => new lite_1.Tiktoken(data))
- .catch((e) => {
- delete cache[encoding];
- throw e;
- });
- }
- return await cache[encoding];
+ throw new Error("TikToken Not implemented");
}
exports.getEncoding = getEncoding;
async function encodingForModel(model) {
- return getEncoding((0, lite_1.getEncodingNameForModel)(model));
+ throw new Error("TikToken Not implemented");
}
exports.encodingForModel = encodingForModel;
diff --git a/dist/utils/tiktoken.js b/dist/utils/tiktoken.js
index 8e41ee6f00f2f9c7fa2c59fa2b2f4297634b97aa..aa5f314a6349ad0d1c5aea8631a56aad099176e0 100644
--- a/dist/utils/tiktoken.js
+++ b/dist/utils/tiktoken.js
@@ -1,20 +1,9 @@
-import { Tiktoken, getEncodingNameForModel, } from "js-tiktoken/lite";
import { AsyncCaller } from "./async_caller.js";
const cache = {};
const caller = /* #__PURE__ */ new AsyncCaller({});
export async function getEncoding(encoding) {
- if (!(encoding in cache)) {
- cache[encoding] = caller
- .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`)
- .then((res) => res.json())
- .then((data) => new Tiktoken(data))
- .catch((e) => {
- delete cache[encoding];
- throw e;
- });
- }
- return await cache[encoding];
+ throw new Error("TikToken Not implemented");
}
export async function encodingForModel(model) {
- return getEncoding(getEncodingNameForModel(model));
+ throw new Error("TikToken Not implemented");
}
diff --git a/package.json b/package.json
index 36072aecf700fca1bc49832a19be832eca726103..90b8922fba1c3d1b26f78477c891b07816d6238a 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,6 @@
"ansi-styles": "^5.0.0",
"camelcase": "6",
"decamelize": "1.2.0",
- "js-tiktoken": "^1.0.12",
"langsmith": ">=0.2.8 <0.4.0",
"mustache": "^4.2.0",
"p-queue": "^6.6.2",

View File

@@ -1,68 +0,0 @@
diff --git a/dist/utils/tiktoken.cjs b/dist/utils/tiktoken.cjs
index c5b41f121d2e3d24c3a4969e31fa1acffdcad3b9..ec724489dcae79ee6c61acf2d4d84bd19daef036 100644
--- a/dist/utils/tiktoken.cjs
+++ b/dist/utils/tiktoken.cjs
@@ -1,6 +1,5 @@
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
const require_utils_async_caller = require('./async_caller.cjs');
-const js_tiktoken_lite = require_rolldown_runtime.__toESM(require("js-tiktoken/lite"));
//#region src/utils/tiktoken.ts
var tiktoken_exports = {};
@@ -11,14 +10,10 @@ require_rolldown_runtime.__export(tiktoken_exports, {
const cache = {};
const caller = /* @__PURE__ */ new require_utils_async_caller.AsyncCaller({});
async function getEncoding(encoding) {
- if (!(encoding in cache)) cache[encoding] = caller.fetch(`https://tiktoken.pages.dev/js/${encoding}.json`).then((res) => res.json()).then((data) => new js_tiktoken_lite.Tiktoken(data)).catch((e) => {
- delete cache[encoding];
- throw e;
- });
- return await cache[encoding];
+ throw new Error("TikToken Not implemented");
}
async function encodingForModel(model) {
- return getEncoding((0, js_tiktoken_lite.getEncodingNameForModel)(model));
+ throw new Error("TikToken Not implemented");
}
//#endregion
diff --git a/dist/utils/tiktoken.js b/dist/utils/tiktoken.js
index 641acca03cb92f04a6fa5c9c31f1880ce635572e..707389970ad957aa0ff20ef37fa8dd2875be737c 100644
--- a/dist/utils/tiktoken.js
+++ b/dist/utils/tiktoken.js
@@ -1,6 +1,5 @@
import { __export } from "../_virtual/rolldown_runtime.js";
import { AsyncCaller } from "./async_caller.js";
-import { Tiktoken, getEncodingNameForModel } from "js-tiktoken/lite";
//#region src/utils/tiktoken.ts
var tiktoken_exports = {};
@@ -11,14 +10,10 @@ __export(tiktoken_exports, {
const cache = {};
const caller = /* @__PURE__ */ new AsyncCaller({});
async function getEncoding(encoding) {
- if (!(encoding in cache)) cache[encoding] = caller.fetch(`https://tiktoken.pages.dev/js/${encoding}.json`).then((res) => res.json()).then((data) => new Tiktoken(data)).catch((e) => {
- delete cache[encoding];
- throw e;
- });
- return await cache[encoding];
+ throw new Error("TikToken Not implemented");
}
async function encodingForModel(model) {
- return getEncoding(getEncodingNameForModel(model));
+ throw new Error("TikToken Not implemented");
}
//#endregion
diff --git a/package.json b/package.json
index a24f8fc61de58526051999260f2ebee5f136354b..e885359e8966e7730c51772533ce37e01edb3046 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,6 @@
"ansi-styles": "^5.0.0",
"camelcase": "6",
"decamelize": "1.2.0",
- "js-tiktoken": "^1.0.12",
"langsmith": "^0.3.64",
"mustache": "^4.2.0",
"p-queue": "^6.6.2",

View File

@@ -0,0 +1,19 @@
diff --git a/dist/embeddings.js b/dist/embeddings.js
index 1f8154be3e9c22442a915eb4b85fa6d2a21b0d0c..dc13ef4a30e6c282824a5357bcee9bd0ae222aab 100644
--- a/dist/embeddings.js
+++ b/dist/embeddings.js
@@ -214,10 +214,12 @@ export class OpenAIEmbeddings extends Embeddings {
* @returns Promise that resolves to an embedding for the document.
*/
async embedQuery(text) {
+ const isBaiduCloud = this.clientConfig.baseURL.includes('baidubce.com')
+ const input = this.stripNewLines ? text.replace(/\n/g, ' ') : text
const params = {
model: this.model,
- input: this.stripNewLines ? text.replace(/\n/g, " ") : text,
- };
+ input: isBaiduCloud ? [input] : input
+ }
if (this.dimensions) {
params.dimensions = this.dimensions;
}

View File

@@ -1,17 +0,0 @@
diff --git a/dist/embeddings.js b/dist/embeddings.js
index 6f4b928d3e4717309382e1b5c2e31ab5bc6c5af0..bc79429c88a6d27d4997a2740c4d8ae0707f5991 100644
--- a/dist/embeddings.js
+++ b/dist/embeddings.js
@@ -94,9 +94,11 @@ var OpenAIEmbeddings = class extends Embeddings {
* @returns Promise that resolves to an embedding for the document.
*/
async embedQuery(text) {
+ const isBaiduCloud = this.clientConfig.baseURL.includes('baidubce.com');
+ const input = this.stripNewLines ? text.replace(/\n/g, " ") : text
const params = {
model: this.model,
- input: this.stripNewLines ? text.replace(/\n/g, " ") : text
+ input: isBaiduCloud ? [input] : input
};
if (this.dimensions) params.dimensions = this.dimensions;
if (this.encodingFormat) params.encoding_format = this.encodingFormat;

View File

@@ -77,7 +77,7 @@ Please review the following critical information before submitting your Pull Req
Our core team is currently focused on significant architectural updates that involve these data structures. To ensure stability and focus during this period, contributions of this nature will be temporarily managed internally.
* **PRs that require changes to Redux state shape or IndexedDB schemas will be closed.**
* **This restriction is temporary and will be lifted with the release of `v2.0.0`.** You can track the progress of `v2.0.0` and its related discussions on issue [#10162](https://github.com/CherryHQ/cherry-studio/pull/10162).
* **This restriction is temporary and will be lifted with the release of `v2.0.0`.** You can track the progress of `v2.0.0` and its related discussions on issue [#10162](https://github.com/YOUR_ORG/YOUR_REPO/issues/10162) (please replace with your actual repo link).
We highly encourage contributions for:
* Bug fixes 🐞

View File

@@ -23,7 +23,7 @@
},
"files": {
"ignoreUnknown": false,
"includes": ["**", "!**/.claude/**"],
"includes": ["**"],
"maxSize": 2097152
},
"formatter": {

View File

@@ -81,7 +81,7 @@ git commit --signoff -m "Your commit message"
我们的核心团队目前正专注于涉及这些数据结构的关键架构更新和基础工作。为确保在此期间的稳定性与专注,此类贡献将暂时由内部进行管理。
* **需要更改 Redux 状态结构或 IndexedDB schema 的 PR 将会被关闭。**
* **此限制是临时性的,并将在 `v2.0.0` 版本发布后解除。** 您可以通过 Issue [#10162](https://github.com/CherryHQ/cherry-studio/pull/10162) 跟踪 `v2.0.0` 的进展及相关讨论。
* **此限制是临时性的,并将在 `v2.0.0` 版本发布后解除。** 您可以通过 Issue [#10162](https://github.com/YOUR_ORG/YOUR_REPO/issues/10162) (请替换为您的实际仓库链接) 跟踪 `v2.0.0` 的进展及相关讨论。
我们非常鼓励以下类型的贡献:
* 错误修复 🐞

View File

@@ -134,116 +134,60 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
<!--LANG:en-->
What's New in v1.7.0-beta.3
What's New in v1.7.0-beta.2
New Features:
- Enhanced Tool Permission System: Real-time tool approval interface with improved UX
- Plugin Management System: Support for Claude Agent plugins (agents, commands, skills)
- Skill Tool: Add skill execution capabilities for agents
- Mobile App Data Restore: Support restoring data to mobile applications
- OpenMinerU Preprocessor: Knowledge base now supports open-source MinerU for document processing
- HuggingFace Provider: Added HuggingFace as AI provider
- Claude Haiku 4.5: Support for the latest Claude Haiku 4.5 model
- Ling Series Models: Added support for Ling-1T and related models
- Intel OVMS Painting: New painting provider using Intel OpenVINO Model Server
- Automatic Update Checks: Implement periodic update checking with configurable intervals
- HuggingChat Mini App: New mini app for HuggingChat integration
- Session Settings: Manage session-specific settings and model configurations independently
- Notes Full-Text Search: Search across all notes with match highlighting
- Built-in DiDi MCP Server: Integration with DiDi ride-hailing services (China only)
- Intel OV OCR: Hardware-accelerated OCR using Intel NPU
- Auto-start API Server: Automatically starts when agents exist
Improvements:
- Agent Creation: New agents are now automatically activated upon creation
- Lazy Loading: Optimize page load performance with route lazy loading
- UI Enhancements: Improved agent item styling and layout consistency
- Navigation: Better navbar layout for fullscreen mode on macOS
- Settings Tab: Enhanced context slider consistency
- Backup Manager: Unified footer layout for local and S3 backup managers
- Menu System: Enhanced application menu with improved help section
- Proxy Rules: Comprehensive proxy bypass rule matching
- German Language: Added German language support
- MCP Confirmation: Added confirmation modal when activating protocol-installed MCP servers
- Translation: Enhanced translation script with concurrency and validation
- Electron & Vite: Updated to Electron 38 and Vite 4.0.1
Claude Code Tool Improvements:
- GlobTool: Now counts lines instead of files in output for better clarity
- ReadTool: Automatically removes system reminder tags from output
- TodoWriteTool: Improved rendering behavior
- Environment Variables: Updated model-related environment variable names
- Agent model selection now requires explicit user choice
- Added Mistral AI provider support
- Added NewAPI generic provider support
- Improved navbar layout consistency across different modes
- Enhanced chat component responsiveness
- Better code block display on small screens
- Updated OVMS to 2025.3 official release
- Added Greek language support
Bug Fixes:
- Fixed session model not being used when sending messages
- Fixed tool approval UI and shared workspace plugin inconsistencies
- Fixed API server readiness notification to renderer
- Fixed grouped items not respecting saved tag order
- Fixed assistant/agent activation when creating new ones
- Fixed Dashscope Anthropic API host and migrated old configs
- Fixed Qwen3 thinking mode control for Ollama
- Fixed disappeared MCP button
- Fixed create assistant causing blank screen
- Fixed up-down button visibility in some cases
- Fixed hooks preventing save on composing enter key
- Fixed Azure GPT-image-1 and OpenRouter Gemini-image
- Fixed Silicon reasoning issues
- Fixed topic branch incomplete copy with two-pass ID mapping
- Fixed deep research model search context restrictions
- Fixed model capability checking logic
- Fixed reranker API error response capture
- Fixed right-click paste file content into inputbar
- Fixed minimax-m2 support in aiCore
- Fixed GitHub Copilot gpt-5-codex streaming issues
- Fixed assistant creation failures
- Fixed translate auto-copy functionality
- Fixed miniapps external link opening
- Fixed message layout and overflow issues
- Fixed API key parsing to preserve spaces
- Fixed agent display in different navbar layouts
<!--LANG:zh-CN-->
v1.7.0-beta.3 新特性
v1.7.0-beta.2 新特性
新功能:
- 增强工具权限系统:实时工具审批界面,改进用户体验
- 插件管理系统:支持 Claude Agent 插件agents、commands、skills
- 技能工具:为 Agent 添加技能执行能力
- 移动应用数据恢复:支持将数据恢复到移动应用程序
- OpenMinerU 预处理器:知识库现支持使用开源 MinerU 处理文档
- HuggingFace 提供商:添加 HuggingFace 作为 AI 提供商
- Claude Haiku 4.5:支持最新的 Claude Haiku 4.5 模型
- Ling 系列模型:添加 Ling-1T 及相关模型支持
- Intel OVMS 绘图:使用 Intel OpenVINO 模型服务器的新绘图提供商
- 自动更新检查:实现可配置间隔的定期更新检查
- HuggingChat 小程序:新增 HuggingChat 集成小程序
- 会话设置:独立管理会话特定的设置和模型配置
- 笔记全文搜索:跨所有笔记搜索并高亮匹配内容
- 内置滴滴 MCP 服务器:集成滴滴打车服务(仅限中国地区)
- Intel OV OCR使用 Intel NPU 的硬件加速 OCR
- 自动启动 API 服务器:当存在 Agent 时自动启动
改进:
- Agent 创建:新创建的 Agent 现在会自动激活
- 懒加载:通过路由懒加载优化页面加载性能
- UI 增强:改进 Agent 项目样式和布局一致性
- 导航:改进 macOS 全屏模式下的导航栏布局
- 设置选项卡:增强上下文滑块一致性
- 备份管理器:统一本地和 S3 备份管理器的页脚布局
- 菜单系统:增强应用菜单,改进帮助部分
- 代理规则:全面的代理绕过规则匹配
- 德语支持:添加德语语言支持
- MCP 确认:添加激活协议安装的 MCP 服务器时的确认模态框
- 翻译:增强翻译脚本的并发和验证功能
- Electron & Vite更新至 Electron 38 和 Vite 4.0.1
Claude Code 工具改进:
- GlobTool现在计算行数而不是文件数提供更清晰的输出
- ReadTool自动从输出中移除系统提醒标签
- TodoWriteTool改进渲染行为
- 环境变量:更新模型相关的环境变量名称
- Agent 模型选择现在需要用户显式选择
- 添加 Mistral AI 提供商支持
- 添加 NewAPI 通用提供商支持
- 改进不同模式下的导航栏布局一致性
- 增强聊天组件响应式设计
- 优化小屏幕代码块显示
- 更新 OVMS 至 2025.3 正式版
- 添加希腊语支持
问题修复:
- 修复发送消息时未使用会话模型
- 修复工具审批 UI 和共享工作区插件不一致
- 修复 API 服务器就绪通知到渲染器
- 修复分组项目不遵守已保存标签顺序
- 修复创建新的助手/Agent 时的激活问题
- 修复 Dashscope Anthropic API 主机并迁移旧配置
- 修复 Ollama 的 Qwen3 思考模式控制
- 修复 MCP 按钮消失
- 修复创建助手导致空白屏幕
- 修复某些情况下上下按钮可见性
- 修复钩子在输入法输入时阻止保存
- 修复 Azure GPT-image-1 和 OpenRouter Gemini-image
- 修复 Silicon 推理问题
- 修复主题分支不完整复制,采用两阶段 ID 映射
- 修复深度研究模型搜索上下文限制
- 修复模型能力检查逻辑
- 修复 reranker API 错误响应捕获
- 修复右键粘贴文件内容到输入栏
- 修复 aiCore 中的 minimax-m2 支持
- 修复 GitHub Copilot gpt-5-codex 流式传输问题
- 修复助手创建失败
- 修复翻译自动复制功能
- 修复小程序外部链接打开
- 修复消息布局和溢出问题
- 修复 API 密钥解析以保留空格
- 修复不同导航栏布局中的 Agent 显示
<!--LANG:END-->

View File

@@ -95,10 +95,8 @@
"node-stream-zip": "^1.15.0",
"officeparser": "^4.2.0",
"os-proxy-config": "^1.1.2",
"qrcode.react": "^4.2.0",
"selection-hook": "^1.0.12",
"sharp": "^0.34.3",
"socket.io": "^4.8.1",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"tesseract.js": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
@@ -108,8 +106,8 @@
"@agentic/exa": "^7.3.3",
"@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3",
"@ai-sdk/amazon-bedrock": "^3.0.42",
"@ai-sdk/google-vertex": "^3.0.48",
"@ai-sdk/amazon-bedrock": "^3.0.35",
"@ai-sdk/google-vertex": "^3.0.40",
"@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/perplexity": "^2.0.13",
@@ -151,9 +149,7 @@
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
"@hello-pangea/dnd": "^18.0.1",
"@heroui/react": "^2.8.3",
"@langchain/community": "^1.0.0",
"@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
"@langchain/openai": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
"@langchain/community": "^0.3.50",
"@mistralai/mistralai": "^1.7.5",
"@modelcontextprotocol/sdk": "^1.17.5",
"@mozilla/readability": "^0.6.0",
@@ -234,7 +230,7 @@
"@viz-js/lang-dot": "^1.0.5",
"@viz-js/viz": "^3.14.0",
"@xyflow/react": "^12.4.4",
"ai": "^5.0.76",
"ai": "^5.0.68",
"antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
"archiver": "^7.0.1",
"async-mutex": "^0.5.0",
@@ -379,7 +375,9 @@
"@codemirror/language": "6.11.3",
"@codemirror/lint": "6.8.5",
"@codemirror/view": "6.38.1",
"@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
"@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A0.3.44#~/.yarn/patches/@langchain-core-npm-0.3.44-41d5c3cb0a.patch",
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
"app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch",
"app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch",
"atomically@npm:^1.7.0": "patch:atomically@npm%3A1.7.0#~/.yarn/patches/atomically-npm-1.7.0-e742e5293b.patch",
@@ -395,18 +393,14 @@
"undici": "6.21.2",
"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",
"@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",
"@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",
"@img/sharp-darwin-arm64": "0.34.3",
"@img/sharp-darwin-x64": "0.34.3",
"@img/sharp-linux-arm": "0.34.3",
"@img/sharp-linux-arm64": "0.34.3",
"@img/sharp-linux-x64": "0.34.3",
"@img/sharp-win32-x64": "0.34.3",
"openai@npm:5.12.2": "npm:@cherrystudio/openai@6.5.0",
"@langchain/openai@npm:>=0.1.0 <0.6.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
"@langchain/openai@npm:>=0.2.0 <0.7.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch"
"openai@npm:5.12.2": "npm:@cherrystudio/openai@6.5.0"
},
"packageManager": "yarn@4.9.1",
"lint-staged": {

View File

@@ -36,10 +36,10 @@
"ai": "^5.0.26"
},
"dependencies": {
"@ai-sdk/anthropic": "^2.0.32",
"@ai-sdk/azure": "^2.0.53",
"@ai-sdk/anthropic": "^2.0.27",
"@ai-sdk/azure": "^2.0.49",
"@ai-sdk/deepseek": "^1.0.23",
"@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": "^2.0.48",
"@ai-sdk/openai-compatible": "^1.0.22",
"@ai-sdk/provider": "^2.0.0",
"@ai-sdk/provider-utils": "^3.0.12",

View File

@@ -204,7 +204,9 @@ export enum IpcChannel {
Export_Word = 'export:word',
Shortcuts_GetAll = 'shortcuts:getAll',
Shortcuts_Update = 'shortcuts:update',
Shortcuts_Updated = 'shortcuts:updated',
// backup
Backup_Backup = 'backup:backup',
@@ -354,7 +356,6 @@ export enum IpcChannel {
ApiServer_Stop = 'api-server:stop',
ApiServer_Restart = 'api-server:restart',
ApiServer_GetStatus = 'api-server:get-status',
ApiServer_Ready = 'api-server:ready',
// NOTE: This api is not be used.
ApiServer_GetConfig = 'api-server:get-config',
@@ -396,12 +397,5 @@ export enum IpcChannel {
ClaudeCodePlugin_ListInstalled = 'claudeCodePlugin:list-installed',
ClaudeCodePlugin_InvalidateCache = 'claudeCodePlugin:invalidate-cache',
ClaudeCodePlugin_ReadContent = 'claudeCodePlugin:read-content',
ClaudeCodePlugin_WriteContent = 'claudeCodePlugin:write-content',
// WebSocket
WebSocket_Start = 'webSocket:start',
WebSocket_Stop = 'webSocket:stop',
WebSocket_Status = 'webSocket:status',
WebSocket_SendFile = 'webSocket:send-file',
WebSocket_GetAllCandidates = 'webSocket:get-all-candidates'
ClaudeCodePlugin_WriteContent = 'claudeCodePlugin:write-content'
}

View File

@@ -11,12 +11,23 @@
import { TRANSLATE_PROMPT } from '@shared/config/prompts'
import * as PreferenceTypes from '@shared/data/preference/preferenceTypes'
import { shortcutDefinitions } from '@shared/shortcuts/definitions'
/* eslint @typescript-eslint/member-ordering: ["error", {
"interfaces": { "order": "alphabetically" },
"typeLiterals": { "order": "alphabetically" }
}] */
const defaultShortcutPreferences: PreferenceTypes.ShortcutPreferencesValue = Object.fromEntries(
shortcutDefinitions.map((definition) => [
definition.name,
{
enabled: definition.defaultEnabled,
key: [...definition.defaultKey]
}
])
)
export interface PreferenceSchemas {
default: {
// redux/settings/enableDeveloperMode
@@ -377,6 +388,8 @@ export interface PreferenceSchemas {
'shortcut.chat.search_message': Record<string, unknown>
// redux/shortcuts/shortcuts.toggle_new_context
'shortcut.chat.toggle_new_context': Record<string, unknown>
// unified shortcut overrides
'shortcut.preferences': PreferenceTypes.ShortcutPreferencesValue
// redux/shortcuts/shortcuts.selection_assistant_select_text
'shortcut.selection.get_text': Record<string, unknown>
// redux/shortcuts/shortcuts.selection_assistant_toggle
@@ -645,6 +658,7 @@ export const DefaultPreferences: PreferenceSchemas = {
key: ['CommandOrControl', 'K'],
system: false
},
'shortcut.preferences': defaultShortcutPreferences,
'shortcut.selection.get_text': { 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 },

View File

@@ -1,3 +1,5 @@
import type { ShortcutPreferenceMap } from '@shared/shortcuts/types'
import type { PreferenceSchemas } from './preferenceSchemas'
export type PreferenceDefaultScopeType = PreferenceSchemas['default']
@@ -14,6 +16,8 @@ export type PreferenceShortcutType = {
system: boolean
}
export type ShortcutPreferencesValue = ShortcutPreferenceMap
export enum SelectionTriggerMode {
Selected = 'selected',
Ctrlkey = 'ctrlkey',

View File

@@ -0,0 +1,175 @@
import type { ShortcutDefinition } from './types'
export const shortcutDefinitions: ShortcutDefinition[] = [
{
name: 'show_app',
defaultKey: [],
defaultEnabled: true,
description: 'Show or hide the main window',
scope: 'main',
editable: true,
system: true
},
{
name: 'show_mini_window',
defaultKey: ['CommandOrControl', 'E'],
defaultEnabled: false,
description: 'Show or hide the mini window',
scope: 'main',
editable: true,
system: true
},
{
name: 'selection_assistant_toggle',
defaultKey: [],
defaultEnabled: false,
description: 'Enable or disable the selection assistant',
scope: 'main',
editable: true,
system: true
},
{
name: 'selection_assistant_select_text',
defaultKey: [],
defaultEnabled: false,
description: 'Trigger selection assistant text capture',
scope: 'main',
editable: true,
system: true
},
{
name: 'zoom_in',
defaultKey: ['CommandOrControl', '='],
defaultEnabled: true,
description: 'Zoom in',
scope: 'main',
editable: false,
system: true
},
{
name: 'zoom_out',
defaultKey: ['CommandOrControl', '-'],
defaultEnabled: true,
description: 'Zoom out',
scope: 'main',
editable: false,
system: true
},
{
name: 'zoom_reset',
defaultKey: ['CommandOrControl', '0'],
defaultEnabled: true,
description: 'Reset zoom',
scope: 'main',
editable: false,
system: true
},
{
name: 'show_settings',
defaultKey: ['CommandOrControl', ','],
defaultEnabled: true,
description: 'Open settings',
scope: 'renderer',
editable: false,
system: true
},
{
name: 'new_topic',
defaultKey: ['CommandOrControl', 'N'],
defaultEnabled: true,
description: 'Start a new chat topic',
scope: 'renderer',
editable: true,
system: false
},
{
name: 'rename_topic',
defaultKey: ['CommandOrControl', 'T'],
defaultEnabled: false,
description: 'Rename current topic',
scope: 'renderer',
editable: true,
system: false
},
{
name: 'toggle_show_assistants',
defaultKey: ['CommandOrControl', '['],
defaultEnabled: true,
description: 'Toggle assistant sidebar',
scope: 'renderer',
editable: true,
system: false
},
{
name: 'toggle_show_topics',
defaultKey: ['CommandOrControl', ']'],
defaultEnabled: true,
description: 'Toggle topic sidebar',
scope: 'renderer',
editable: true,
system: false
},
{
name: 'copy_last_message',
defaultKey: ['CommandOrControl', 'Shift', 'C'],
defaultEnabled: false,
description: 'Copy the last assistant reply',
scope: 'renderer',
editable: true,
system: false
},
{
name: 'edit_last_user_message',
defaultKey: ['CommandOrControl', 'Shift', 'E'],
defaultEnabled: false,
description: 'Edit the last user message',
scope: 'renderer',
editable: true,
system: false
},
{
name: 'search_message_in_chat',
defaultKey: ['CommandOrControl', 'F'],
defaultEnabled: true,
description: 'Search messages in current chat',
scope: 'renderer',
editable: true,
system: false
},
{
name: 'search_message',
defaultKey: ['CommandOrControl', 'Shift', 'F'],
defaultEnabled: true,
description: 'Search messages globally',
scope: 'renderer',
editable: true,
system: false
},
{
name: 'clear_topic',
defaultKey: ['CommandOrControl', 'L'],
defaultEnabled: true,
description: 'Clear current topic',
scope: 'renderer',
editable: true,
system: false
},
{
name: 'toggle_new_context',
defaultKey: ['CommandOrControl', 'K'],
defaultEnabled: true,
description: 'Toggle new context mode',
scope: 'renderer',
editable: true,
system: false
},
{
name: 'exit_fullscreen',
defaultKey: ['Escape'],
defaultEnabled: true,
description: 'Exit fullscreen mode',
scope: 'renderer',
editable: false,
system: true
}
]

View File

@@ -0,0 +1,25 @@
export type ShortcutScope = 'main' | 'renderer'
export interface ShortcutDefinition {
name: string
defaultKey: string[]
defaultEnabled: boolean
description: string
scope: ShortcutScope
editable: boolean
system: boolean
}
export interface ShortcutPreferenceEntry {
key?: string[]
enabled?: boolean
}
export type ShortcutPreferenceMap = Record<string, ShortcutPreferenceEntry>
export type HydratedShortcut = ShortcutDefinition & {
key: string[]
enabled: boolean
}
export type HydratedShortcutMap = Record<string, HydratedShortcut>

View File

@@ -1,26 +1,4 @@
# 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 v4 (replacing styled-components)
- **Design System**: Custom CSS variable system (`--cs-*` namespace)
- **Theme System**: CSS variables + Tailwind CSS 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 [README.md](./README.md))
5. **Performance Priority**: Optimize bundle size and rendering performance
6. **Designer Collaboration**: Work with UI designers for gradual component encapsulation and UI optimization
# UI Component Library Migration Status
## Usage Example
@@ -46,68 +24,115 @@ function MyComponent() {
@packages/ui/
├── src/
│ ├── components/ # Main components directory
│ │ ├── primitives/ # Basic/primitive components (Avatar, ErrorBoundary, Selector, etc.)
│ │ │ └── shadcn-io/ # shadcn/ui components (dropzone, etc.)
│ │ ├── icons/ # Icon components (Icon, FileIcons, etc.)
│ │ ── composites/ # Composite components (CodeEditor, ListItem, etc.)
│ │ ├── base/ # Basic components (buttons, inputs, labels, etc.)
│ │ ├── display/ # Display components (cards, lists, tables, etc.)
│ │ ├── layout/ # Layout components (containers, grids, spacing, etc.)
│ │ ── icons/ # Icon components
│ │ ├── interactive/ # Interactive components (modals, tooltips, dropdowns, etc.)
│ │ └── composite/ # Composite components (made from multiple base components)
│ ├── hooks/ # Custom React Hooks
── styles/ # Global styles and CSS variables
│ ├── types/ # TypeScript type definitions
│ ├── utils/ # Utility functions
│ └── index.ts # Main export file
── types/ # TypeScript type definitions
```
### Component Classification Guide
When submitting PRs, please place components in the correct directory based on their function:
- **primitives**: Basic and primitive UI elements, shadcn/ui components
- `Avatar`: Avatar components
- `ErrorBoundary`: Error boundary components
- `Selector`: Selection components
- `shadcn-io/`: Direct shadcn/ui components or adaptations
- **base**: Most basic UI elements like buttons, inputs, switches, labels, etc.
- **display**: Components for displaying content like cards, lists, tables, tabs, etc.
- **layout**: Components for page layout like containers, grid systems, dividers, etc.
- **icons**: All icon-related components
- `Icon`: Icon factory and basic icons
- `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.)
- **interactive**: Components requiring user interaction like modals, drawers, tooltips, dropdowns, etc.
- **composite**: Composite components made from multiple base components
## Component Extraction Criteria
## Migration Overview
### Extraction Standards
- **Total Components**: 236
- **Migrated**: 34
- **Refactored**: 18
- **Pending Migration**: 184
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
## Component Status Table
### Extraction Principles
- **Single Responsibility**: Each component should only handle one clear function
- **Highly Configurable**: Provide flexible configuration options through props
- **Backward Compatible**: New versions maintain API backward compatibility
- **Complete Documentation**: Provide clear API documentation and usage examples
- **Type Safety**: Use TypeScript to ensure type safety
### Cases Not Recommended for Extraction
- Simple display components used only on a single page
- Overly customized business logic components
- Components tightly coupled to specific data sources
| Category | Component Name | Migration Status | Refactoring Status | Description |
| ----------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **base** | | | | Base components |
| | CopyButton | ✅ | ✅ | Copy button |
| | CustomTag | ✅ | ✅ | Custom tag |
| | DividerWithText | ✅ | ✅ | Divider with text |
| | EmojiIcon | ✅ | ✅ | Emoji icon |
| | ErrorBoundary | ✅ | ✅ | Error boundary (decoupled via props) |
| | StatusTag | ✅ | ✅ | Unified status tag (merged ErrorTag, SuccessTag, WarnTag, InfoTag) |
| | IndicatorLight | ✅ | ✅ | Indicator light |
| | Spinner | ✅ | ✅ | Loading spinner |
| | TextBadge | ✅ | ✅ | Text badge |
| | CustomCollapse | ✅ | ✅ | Custom collapse panel |
| **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
| Phase | Status | Main Tasks | Description |
| --- | --- | --- | --- |
| **Phase 1** | ✅ **Completed** | **Design System Integration** | • Converted design tokens from todocss.css to tokens.css with `--cs-*` namespace<br>• Created theme.css mapping all design tokens to standard Tailwind classes<br>• Extended Tailwind with semantic spacing (5xs~8xl) and radius (4xs~3xl) systems<br>• Established two usage modes: full override and selective override<br>• Cleaned up main package's conflicting Shadcn theme definitions |
| **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 |
| **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 1: Copy Migration (Current Phase)
- Copy components as-is to @packages/ui
- Retain original dependencies (antd, styled-components, etc.)
- Add original path comment at file top
### 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
@@ -118,33 +143,9 @@ When submitting PRs, please place components in the correct directory based on t
2. **Can migrate** but need decoupling later:
- Components using i18n (change i18n to props)
- Components using antd (replace with shadcn/ui later)
- Components using HeroUI (replace with shadcn/ui later)
- Components using antd (replace with HeroUI later)
3. **Submission Guidelines**:
- Each PR should focus on one category of components
- Ensure all migrated components are exported
- Follow component extraction criteria, only migrate qualified components
## Design System Integration
### CSS Variable System
- All design tokens use `--cs-*` namespace (e.g., `--cs-primary`, `--cs-red-500`)
- Complete color palette: 17 colors × 11 shades each
- Semantic spacing system: `5xs` through `8xl` (16 levels)
- Semantic radius system: `4xs` through `3xl` plus `round` (11 levels)
- Full light/dark mode support
- See [README.md](./README.md) for usage documentation
### 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
- Update migration status in this document

View File

@@ -2,120 +2,54 @@
Cherry Studio UI 组件库 - 为 Cherry Studio 设计的 React 组件集合
## 特性
## 特性
- 🎨 **设计系统**: 完整的 CherryStudio 设计令牌17种颜色 × 11个色阶 + 语义化主题)
- 🌓 **Dark Mode**: 开箱即用的深色模式支持
- 🚀 **Tailwind v4**: 基于最新 Tailwind CSS v4 构建
- 📦 **灵活导入**: 2种样式导入方式满足不同使用场景
- 🔷 **TypeScript**: 完整的类型定义和智能提示
- 🎯 **零冲突**: CSS 变量隔离,不覆盖用户主题
- 🎨 基于 Tailwind CSS 的现代化设计
- 📦 支持 ESM 和 CJS 格式
- 🔷 完整的 TypeScript 支持
- 🚀 可以作为 npm 包发布
- 🔧 开箱即用的常用 hooks 和工具函数
---
## 🚀 快速开始
### 安装
## 安装
```bash
# 安装组件库
npm install @cherrystudio/ui
# peer dependencies
# 安装必需的 peer dependencies
npm install @heroui/react framer-motion react react-dom tailwindcss
```
### 两种使用方式
## 配置
#### 方式 1完整覆盖 ✨
### 1. Tailwind CSS v4 配置
使用完整的 CherryStudio 设计系统,所有 Tailwind 类名映射到设计系统。
本组件库使用 Tailwind CSS v4配置方式已改变。在你的主 CSS 文件(如 `src/styles/tailwind.css`)中:
```css
/* app.css */
@import '@cherrystudio/ui/styles/theme.css';
```
**特点**
- ✅ 直接使用标准 Tailwind 类名(`bg-primary``bg-red-500``p-md``rounded-lg`
- ✅ 所有颜色使用设计师定义的值
- ✅ 扩展的 Spacing 系统(`p-5xs` ~ `p-8xl`,共 16 个语义化尺寸)
- ✅ 扩展的 Radius 系统(`rounded-4xs` ~ `rounded-3xl`,共 11 个圆角)
- ⚠️ 会完全覆盖 Tailwind 默认主题
**示例**
```tsx
<Button className="bg-primary text-red-500 p-md rounded-lg">
{/* bg-primary → 品牌色lime-500 */}
{/* text-red-500 → 设计师定义的红色 */}
{/* p-md → 2.5remspacing-md */}
{/* rounded-lg → 2.5remradius-lg */}
</Button>
{/* 扩展的工具类 */}
<div className="p-5xs"> (0.5rem)</div>
<div className="p-xs"> (1rem)</div>
<div className="p-sm"> (1.5rem)</div>
<div className="p-md"> (2.5rem)</div>
<div className="p-lg"> (3.5rem)</div>
<div className="p-xl"> (5rem)</div>
<div className="p-8xl"> (15rem)</div>
<div className="rounded-4xs"> (0.25rem)</div>
<div className="rounded-xs"> (1rem)</div>
<div className="rounded-md"> (2rem)</div>
<div className="rounded-xl"> (3rem)</div>
<div className="rounded-round"> (999px)</div>
```
#### 方式 2选择性覆盖 🎯
只导入设计令牌CSS 变量),手动选择要覆盖的部分。
```css
/* app.css */
@import 'tailwindcss';
@import '@cherrystudio/ui/styles/tokens.css';
/* 只使用部分设计系统 */
/* 必须扫描组件库文件以提取类名 */
@source '../node_modules/@cherrystudio/ui/dist/**/*.{js,mjs}';
/* 你的应用源文件 */
@source './src/**/*.{js,ts,jsx,tsx}';
/*
* 如果你的应用直接使用 HeroUI 组件,需要添加:
* @source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
* @plugin '@heroui/react/plugin';
*/
/* 自定义主题配置(可选) */
@theme {
--color-primary: var(--cs-primary); /* 使用 CS 的主色 */
--color-red-500: oklch(...); /* 使用自己的红色 */
--spacing-md: var(--cs-size-md); /* 使用 CS 的间距 */
--radius-lg: 1rem; /* 使用自己的圆角 */
/* 你的主题扩展 */
}
```
**特点**
注意Tailwind CSS v4 不再使用 `tailwind.config.js` 文件,所有配置都在 CSS 中完成。
- ✅ 不覆盖任何 Tailwind 默认主题
- ✅ 通过 CSS 变量访问所有设计令牌(`var(--cs-primary)``var(--cs-red-500)`
- ✅ 精细控制哪些使用 CS、哪些保持原样
- ✅ 适合有自己设计系统但想借用部分 CS 设计令牌的场景
**示例**
```tsx
{/* 通过 CSS 变量使用 CS 设计令牌 */}
<button style={{ backgroundColor: 'var(--cs-primary)' }}>
使 CherryStudio
</button>
{/* 保持原有的 Tailwind 类名不受影响 */}
<div className="bg-red-500">
使 Tailwind
</div>
{/* 可用的 CSS 变量 */}
<div style={{
color: 'var(--cs-primary)', // 品牌色
backgroundColor: 'var(--cs-red-500)', // 红色-500
padding: 'var(--cs-size-md)', // 间距
borderRadius: 'var(--cs-radius-lg)' // 圆角
}} />
```
### Provider 配置
### 2. Provider 配置
在你的 App 根组件中添加 HeroUI Provider
@@ -160,6 +94,9 @@ function App() {
// 只导入组件
import { Button } from '@cherrystudio/ui/components'
// 只导入 hooks
import { useDebounce, useLocalStorage } from '@cherrystudio/ui/hooks'
// 只导入工具函数
import { cn, formatFileSize } from '@cherrystudio/ui/utils'
```

View File

@@ -13,7 +13,7 @@
"tailwind": {
"baseColor": "zinc",
"config": "",
"css": "src/styles/theme.css",
"css": "src/styles/globals.css",
"cssVariables": true,
"prefix": ""
},

View File

@@ -1,214 +0,0 @@
# todocss.css → design-tokens.css 转换日志
## ✅ 已转换的变量
### 基础颜色 (Primitive Colors)
- ✅ Neutral (50-950)
- ✅ Zinc (50-950)
- ✅ Red (50-950)
- ✅ Orange (50-950)
- ✅ Amber (50-950)
- ✅ Yellow (50-950)
- ✅ Lime (50-950) - 品牌主色
- ✅ Green (50-950)
- ✅ Emerald (50-950)
- ✅ Purple (50-950)
- ✅ Blue (50-950)
- ✅ Black & White
### 语义化颜色 (Semantic Colors)
-`--cs-primary` (Lime 500)
-`--cs-destructive` (Red 500)
-`--cs-success` (Green 500)
-`--cs-warning` (Amber 500)
-`--cs-background` (Zinc 50/900)
-`--cs-foreground` 系列 (main, secondary, muted)
-`--cs-border` 系列 (default, hover, active)
-`--cs-ring` (Focus)
### 容器颜色
-`--cs-card` (White/Black)
-`--cs-popover` (White/Black)
-`--cs-sidebar` (White/Black)
### UI 元素细分颜色 (新增补充)
-**Modal / Overlay**
- `--cs-modal-backdrop`
- `--cs-modal-thumb`
- `--cs-modal-thumb-hover`
-**Icon**
- `--cs-icon-default`
- `--cs-icon-hover`
-**Input / Select**
- `--cs-input-background`
- `--cs-input-border`
- `--cs-input-border-hover`
- `--cs-input-border-focus`
-**Primary Button**
- `--cs-primary-button-background`
- `--cs-primary-button-text`
- `--cs-primary-button-background-hover`
- `--cs-primary-button-background-active`
- `--cs-primary-button-background-2nd`
- `--cs-primary-button-background-3rd`
-**Secondary Button**
- `--cs-secondary-button-background`
- `--cs-secondary-button-text`
- `--cs-secondary-button-background-hover`
- `--cs-secondary-button-background-active`
- `--cs-secondary-button-border`
-**Ghost Button**
- `--cs-ghost-button-background`
- `--cs-ghost-button-text`
- `--cs-ghost-button-background-hover`
- `--cs-ghost-button-background-active`
### 尺寸系统
- ✅ Spacing/Sizing 合并为 `--cs-size-*` (5xs ~ 8xl)
- ✅ Border Radius (4xs ~ 3xl, round)
- ✅ Border Width (sm, md, lg)
### 字体排版
- ✅ Font Families (Heading, Body)
- ✅ Font Weights (修正单位错误: 400px → 400)
- ✅ Font Sizes (Body & Heading)
- ✅ Line Heights (Body & Heading)
- ✅ Paragraph Spacing
---
## ❌ 已废弃的变量
### Opacity 变量 (全部废弃)
使用 Tailwind 的 `/modifier` 语法替代:
| todocss.css | 替代方案 |
|-------------|---------|
| `--Opacity--Red--Red-80` | `bg-cs-destructive/80` |
| `--Opacity--Green--Green-60` | `bg-cs-success/60` |
| `--Opacity--White--White-10` | `bg-white/10` |
**原因**: Tailwind v4 原生支持透明度修饰符,无需单独定义变量。
---
## 🔧 关键修正
### 1. 单位错误
```css
/* ❌ todocss.css */
--Font_weight--Regular: 400px;
/* ✅ design-tokens.css */
--cs-font-weight-regular: 400;
```
### 2. px → rem 转换
```css
/* ❌ todocss.css */
--Spacing--md: 40px;
/* ✅ design-tokens.css */
--cs-size-md: 2.5rem; /* 40px / 16 = 2.5rem */
```
### 3. 变量合并
```css
/* ❌ todocss.css (冗余) */
--Spacing--md: 40px;
--Sizing--md: 40px;
/* ✅ design-tokens.css (合并) */
--cs-size-md: 2.5rem;
```
### 4. Dark Mode 分离
```css
/* ❌ todocss.css (Light 和 Dark 都在 :root) */
:root {
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--50);
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--900); /* 后面覆盖 */
}
/* ✅ design-tokens.css (正确分离) */
:root {
--cs-background: var(--cs-zinc-50);
}
.dark {
--cs-background: var(--cs-zinc-900);
}
```
---
## 📊 变量统计
| 分类 | todocss.css | design-tokens.css | 说明 |
|------|-------------|-------------------|------|
| Primitive Colors | ~250 | ~250 | 完整保留 |
| Semantic Colors | ~20 | ~20 | 完整转换 |
| UI Element Colors | ~30 | ~30 | ✅ 已补充完整 |
| Opacity Variables | ~50 | 0 | 废弃,用 `/modifier` |
| Spacing/Sizing | 32 | 16 | 合并去重 |
| Typography | ~50 | ~50 | 修正单位 |
| **总计** | ~430 | ~390 | 优化 40 个变量 |
---
## 🎨 Dark Mode 变量对比
| Light Mode | Dark Mode | 变量名 |
|-----------|-----------|-------|
| Zinc 50 | Zinc 900 | `--cs-background` |
| Black 90% | White 90% | `--cs-foreground` |
| Black 60% | White 60% | `--cs-foreground-secondary` |
| Black 10% | White 10% | `--cs-border` |
| White | Black | `--cs-card` |
| White | Black | `--cs-popover` |
| White | Black | `--cs-sidebar` |
| White | Black | `--cs-input-background` |
| Black 40% | Black 6% | `--cs-modal-backdrop` |
| Black 20% | White 20% | `--cs-modal-thumb` |
| Black 5% | White 10% | `--cs-secondary` |
| Black 0% | White 0% | `--cs-ghost-button-background` |
---
## ✅ 验证清单
- [x] 所有 Primitive 颜色已转换
- [x] 所有语义化颜色已转换
- [x] 所有 UI 元素颜色已转换
- [x] Dark Mode 变量完整
- [x] 尺寸单位统一为 rem
- [x] Font Weight 单位已修正
- [x] Opacity 变量已废弃
- [x] Spacing/Sizing 已合并
---
## 📝 使用指南
### 如果设计师更新 todocss.css
1. 对比此文档,找出新增/修改的变量
2. 按照转换规则更新 `design-tokens.css`
3. 验证 Light/Dark Mode 是否完整
4. 更新此日志
### 验证转换正确性
```bash
# 检查 Light Mode 变量数量
grep -c "^ --cs-" packages/ui/src/styles/design-tokens.css
# 检查 Dark Mode 覆盖数量
grep -c "^ --cs-" packages/ui/src/styles/design-tokens.css | grep -A 100 ".dark"
```

View File

@@ -1,26 +0,0 @@
# Design Reference
本文件夹包含设计师提供的原始设计令牌文件,仅作为参考使用。
## 文件说明
### todocss.css
- **来源**:设计师提供的原始设计令牌
- **状态**:已转换为 `src/styles/design-tokens.css`
- **用途**
- 追溯设计决策
- 验证转换正确性
- 设计师更新时作为对比基准
## 转换规则
原始文件 → 生产文件的转换规则参见:
- [DESIGN_SYSTEM.md](../DESIGN_SYSTEM.md)
- [USAGE_GUIDE.md](../USAGE_GUIDE.md)
## 注意事项
⚠️ **请勿直接使用此文件夹中的文件**
- 这些文件仅供参考
- 实际使用请导入 `src/styles/` 中的文件

View File

@@ -1,870 +0,0 @@
: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;
}

View File

@@ -124,11 +124,7 @@
"import": "./dist/utils/index.mjs",
"require": "./dist/utils/index.js",
"default": "./dist/utils/index.js"
},
"./styles": "./src/styles/index.css",
"./styles/tokens.css": "./src/styles/tokens.css",
"./styles/theme.css": "./src/styles/theme.css",
"./styles/index.css": "./src/styles/index.css"
}
},
"packageManager": "yarn@4.9.1"
}

View File

@@ -0,0 +1,123 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -1,24 +0,0 @@
/**
* CherryStudio Styles - npm 包入口
*
* 用途:仅导出 CSS 变量,不生成工具类,不覆盖用户主题
* 使用场景:
* - npm 用户想使用设计系统的变量,但不想覆盖 Tailwind 默认主题
* - 可以通过 var(--cs-primary) 等方式使用
* - 不会生成 bg-primary 等工具类(用户自己的 bg-primary 不受影响)
*
* 示例:
* ```css
* @import '@cherrystudio/ui/styles/index.css';
*
* .my-button {
* background: var(--cs-primary); // 使用 CS 的品牌色
* padding: var(--cs-size-md); // 使用 CS 的间距
* }
* ```
*
* 如果想要完整的主题覆盖(生成 bg-primary 等类),请导入:
* @import '@cherrystudio/ui/styles/theme.css';
*/
@import './tokens.css';

View File

@@ -1,433 +0,0 @@
/**
* CherryStudio Theme - Tailwind CSS 主题配置
*
* 用途:映射设计师的设计系统(--cs-*)到 Tailwind 标准命名
* 使用场景:
* - 主包:完全使用设计系统
* - npm 用户:可选择导入,会覆盖 Tailwind 默认主题
*
* 生成的工具类bg-primary, bg-red-500, p-md 等(无前缀)
*/
@import 'tailwindcss';
@import './tokens.css';
@custom-variant dark (&:is(.dark *));
@theme {
/* ==================== */
/* 语义化颜色(覆盖 Tailwind 标准命名) */
/* ==================== */
--color-primary: var(--cs-primary);
--color-primary-foreground: var(--cs-white);
--color-destructive: var(--cs-destructive);
--color-destructive-foreground: var(--cs-white);
--color-success: var(--cs-success);
--color-success-foreground: var(--cs-white);
--color-warning: var(--cs-warning);
--color-warning-foreground: var(--cs-black);
--color-background: var(--cs-background);
--color-foreground: var(--cs-foreground);
--color-card: var(--cs-card);
--color-card-foreground: var(--cs-foreground);
--color-popover: var(--cs-popover);
--color-popover-foreground: var(--cs-foreground);
--color-secondary: var(--cs-secondary);
--color-secondary-foreground: var(--cs-foreground);
--color-muted: var(--cs-muted);
--color-muted-foreground: var(--cs-foreground-muted);
--color-accent: var(--cs-accent);
--color-accent-foreground: var(--cs-foreground);
--color-border: var(--cs-border);
--color-input: var(--cs-border);
--color-ring: var(--cs-ring);
/* 侧边栏 */
--color-sidebar: var(--cs-sidebar);
--color-sidebar-foreground: var(--cs-foreground);
--color-sidebar-primary: var(--cs-primary);
--color-sidebar-primary-foreground: var(--cs-white);
--color-sidebar-accent: var(--cs-sidebar-accent);
--color-sidebar-accent-foreground: var(--cs-foreground);
--color-sidebar-border: var(--cs-border);
--color-sidebar-ring: var(--cs-ring);
/* 图表颜色 */
--color-chart-1: var(--cs-blue-500);
--color-chart-2: var(--cs-emerald-500);
--color-chart-3: var(--cs-purple-500);
--color-chart-4: var(--cs-amber-500);
--color-chart-5: var(--cs-orange-500);
/* ==================== */
/* Primitive 颜色(覆盖 Tailwind 默认色板) */
/* ==================== */
/* Neutral */
--color-neutral-50: var(--cs-neutral-50);
--color-neutral-100: var(--cs-neutral-100);
--color-neutral-200: var(--cs-neutral-200);
--color-neutral-300: var(--cs-neutral-300);
--color-neutral-400: var(--cs-neutral-400);
--color-neutral-500: var(--cs-neutral-500);
--color-neutral-600: var(--cs-neutral-600);
--color-neutral-700: var(--cs-neutral-700);
--color-neutral-800: var(--cs-neutral-800);
--color-neutral-900: var(--cs-neutral-900);
--color-neutral-950: var(--cs-neutral-950);
/* Stone */
--color-stone-50: var(--cs-stone-50);
--color-stone-100: var(--cs-stone-100);
--color-stone-200: var(--cs-stone-200);
--color-stone-300: var(--cs-stone-300);
--color-stone-400: var(--cs-stone-400);
--color-stone-500: var(--cs-stone-500);
--color-stone-600: var(--cs-stone-600);
--color-stone-700: var(--cs-stone-700);
--color-stone-800: var(--cs-stone-800);
--color-stone-900: var(--cs-stone-900);
--color-stone-950: var(--cs-stone-950);
/* Zinc */
--color-zinc-50: var(--cs-zinc-50);
--color-zinc-100: var(--cs-zinc-100);
--color-zinc-200: var(--cs-zinc-200);
--color-zinc-300: var(--cs-zinc-300);
--color-zinc-400: var(--cs-zinc-400);
--color-zinc-500: var(--cs-zinc-500);
--color-zinc-600: var(--cs-zinc-600);
--color-zinc-700: var(--cs-zinc-700);
--color-zinc-800: var(--cs-zinc-800);
--color-zinc-900: var(--cs-zinc-900);
--color-zinc-950: var(--cs-zinc-950);
/* Slate */
--color-slate-50: var(--cs-slate-50);
--color-slate-100: var(--cs-slate-100);
--color-slate-200: var(--cs-slate-200);
--color-slate-300: var(--cs-slate-300);
--color-slate-400: var(--cs-slate-400);
--color-slate-500: var(--cs-slate-500);
--color-slate-600: var(--cs-slate-600);
--color-slate-700: var(--cs-slate-700);
--color-slate-800: var(--cs-slate-800);
--color-slate-900: var(--cs-slate-900);
--color-slate-950: var(--cs-slate-950);
/* Gray */
--color-gray-50: var(--cs-gray-50);
--color-gray-100: var(--cs-gray-100);
--color-gray-200: var(--cs-gray-200);
--color-gray-300: var(--cs-gray-300);
--color-gray-400: var(--cs-gray-400);
--color-gray-500: var(--cs-gray-500);
--color-gray-600: var(--cs-gray-600);
--color-gray-700: var(--cs-gray-700);
--color-gray-800: var(--cs-gray-800);
--color-gray-900: var(--cs-gray-900);
--color-gray-950: var(--cs-gray-950);
/* Red */
--color-red-50: var(--cs-red-50);
--color-red-100: var(--cs-red-100);
--color-red-200: var(--cs-red-200);
--color-red-300: var(--cs-red-300);
--color-red-400: var(--cs-red-400);
--color-red-500: var(--cs-red-500);
--color-red-600: var(--cs-red-600);
--color-red-700: var(--cs-red-700);
--color-red-800: var(--cs-red-800);
--color-red-900: var(--cs-red-900);
--color-red-950: var(--cs-red-950);
/* Orange */
--color-orange-50: var(--cs-orange-50);
--color-orange-100: var(--cs-orange-100);
--color-orange-200: var(--cs-orange-200);
--color-orange-300: var(--cs-orange-300);
--color-orange-400: var(--cs-orange-400);
--color-orange-500: var(--cs-orange-500);
--color-orange-600: var(--cs-orange-600);
--color-orange-700: var(--cs-orange-700);
--color-orange-800: var(--cs-orange-800);
--color-orange-900: var(--cs-orange-900);
--color-orange-950: var(--cs-orange-950);
/* Amber */
--color-amber-50: var(--cs-amber-50);
--color-amber-100: var(--cs-amber-100);
--color-amber-200: var(--cs-amber-200);
--color-amber-300: var(--cs-amber-300);
--color-amber-400: var(--cs-amber-400);
--color-amber-500: var(--cs-amber-500);
--color-amber-600: var(--cs-amber-600);
--color-amber-700: var(--cs-amber-700);
--color-amber-800: var(--cs-amber-800);
--color-amber-900: var(--cs-amber-900);
--color-amber-950: var(--cs-amber-950);
/* Yellow */
--color-yellow-50: var(--cs-yellow-50);
--color-yellow-100: var(--cs-yellow-100);
--color-yellow-200: var(--cs-yellow-200);
--color-yellow-300: var(--cs-yellow-300);
--color-yellow-400: var(--cs-yellow-400);
--color-yellow-500: var(--cs-yellow-500);
--color-yellow-600: var(--cs-yellow-600);
--color-yellow-700: var(--cs-yellow-700);
--color-yellow-800: var(--cs-yellow-800);
--color-yellow-900: var(--cs-yellow-900);
--color-yellow-950: var(--cs-yellow-950);
/* Lime (品牌主色) */
--color-lime-50: var(--cs-lime-50);
--color-lime-100: var(--cs-lime-100);
--color-lime-200: var(--cs-lime-200);
--color-lime-300: var(--cs-lime-300);
--color-lime-400: var(--cs-lime-400);
--color-lime-500: var(--cs-lime-500);
--color-lime-600: var(--cs-lime-600);
--color-lime-700: var(--cs-lime-700);
--color-lime-800: var(--cs-lime-800);
--color-lime-900: var(--cs-lime-900);
--color-lime-950: var(--cs-lime-950);
/* Green */
--color-green-50: var(--cs-green-50);
--color-green-100: var(--cs-green-100);
--color-green-200: var(--cs-green-200);
--color-green-300: var(--cs-green-300);
--color-green-400: var(--cs-green-400);
--color-green-500: var(--cs-green-500);
--color-green-600: var(--cs-green-600);
--color-green-700: var(--cs-green-700);
--color-green-800: var(--cs-green-800);
--color-green-900: var(--cs-green-900);
--color-green-950: var(--cs-green-950);
/* Emerald */
--color-emerald-50: var(--cs-emerald-50);
--color-emerald-100: var(--cs-emerald-100);
--color-emerald-200: var(--cs-emerald-200);
--color-emerald-300: var(--cs-emerald-300);
--color-emerald-400: var(--cs-emerald-400);
--color-emerald-500: var(--cs-emerald-500);
--color-emerald-600: var(--cs-emerald-600);
--color-emerald-700: var(--cs-emerald-700);
--color-emerald-800: var(--cs-emerald-800);
--color-emerald-900: var(--cs-emerald-900);
--color-emerald-950: var(--cs-emerald-950);
/* Teal */
--color-teal-50: var(--cs-teal-50);
--color-teal-100: var(--cs-teal-100);
--color-teal-200: var(--cs-teal-200);
--color-teal-300: var(--cs-teal-300);
--color-teal-400: var(--cs-teal-400);
--color-teal-500: var(--cs-teal-500);
--color-teal-600: var(--cs-teal-600);
--color-teal-700: var(--cs-teal-700);
--color-teal-800: var(--cs-teal-800);
--color-teal-900: var(--cs-teal-900);
--color-teal-950: var(--cs-teal-950);
/* Cyan */
--color-cyan-50: var(--cs-cyan-50);
--color-cyan-100: var(--cs-cyan-100);
--color-cyan-200: var(--cs-cyan-200);
--color-cyan-300: var(--cs-cyan-300);
--color-cyan-400: var(--cs-cyan-400);
--color-cyan-500: var(--cs-cyan-500);
--color-cyan-600: var(--cs-cyan-600);
--color-cyan-700: var(--cs-cyan-700);
--color-cyan-800: var(--cs-cyan-800);
--color-cyan-900: var(--cs-cyan-900);
--color-cyan-950: var(--cs-cyan-950);
/* Sky */
--color-sky-50: var(--cs-sky-50);
--color-sky-100: var(--cs-sky-100);
--color-sky-200: var(--cs-sky-200);
--color-sky-300: var(--cs-sky-300);
--color-sky-400: var(--cs-sky-400);
--color-sky-500: var(--cs-sky-500);
--color-sky-600: var(--cs-sky-600);
--color-sky-700: var(--cs-sky-700);
--color-sky-800: var(--cs-sky-800);
--color-sky-900: var(--cs-sky-900);
--color-sky-950: var(--cs-sky-950);
/* Blue */
--color-blue-50: var(--cs-blue-50);
--color-blue-100: var(--cs-blue-100);
--color-blue-200: var(--cs-blue-200);
--color-blue-300: var(--cs-blue-300);
--color-blue-400: var(--cs-blue-400);
--color-blue-500: var(--cs-blue-500);
--color-blue-600: var(--cs-blue-600);
--color-blue-700: var(--cs-blue-700);
--color-blue-800: var(--cs-blue-800);
--color-blue-900: var(--cs-blue-900);
--color-blue-950: var(--cs-blue-950);
/* Indigo */
--color-indigo-50: var(--cs-indigo-50);
--color-indigo-100: var(--cs-indigo-100);
--color-indigo-200: var(--cs-indigo-200);
--color-indigo-300: var(--cs-indigo-300);
--color-indigo-400: var(--cs-indigo-400);
--color-indigo-500: var(--cs-indigo-500);
--color-indigo-600: var(--cs-indigo-600);
--color-indigo-700: var(--cs-indigo-700);
--color-indigo-800: var(--cs-indigo-800);
--color-indigo-900: var(--cs-indigo-900);
--color-indigo-950: var(--cs-indigo-950);
/* Violet */
--color-violet-50: var(--cs-violet-50);
--color-violet-100: var(--cs-violet-100);
--color-violet-200: var(--cs-violet-200);
--color-violet-300: var(--cs-violet-300);
--color-violet-400: var(--cs-violet-400);
--color-violet-500: var(--cs-violet-500);
--color-violet-600: var(--cs-violet-600);
--color-violet-700: var(--cs-violet-700);
--color-violet-800: var(--cs-violet-800);
--color-violet-900: var(--cs-violet-900);
--color-violet-950: var(--cs-violet-950);
/* Purple */
--color-purple-50: var(--cs-purple-50);
--color-purple-100: var(--cs-purple-100);
--color-purple-200: var(--cs-purple-200);
--color-purple-300: var(--cs-purple-300);
--color-purple-400: var(--cs-purple-400);
--color-purple-500: var(--cs-purple-500);
--color-purple-600: var(--cs-purple-600);
--color-purple-700: var(--cs-purple-700);
--color-purple-800: var(--cs-purple-800);
--color-purple-900: var(--cs-purple-900);
--color-purple-950: var(--cs-purple-950);
/* Fuchsia */
--color-fuchsia-50: var(--cs-fuchsia-50);
--color-fuchsia-100: var(--cs-fuchsia-100);
--color-fuchsia-200: var(--cs-fuchsia-200);
--color-fuchsia-300: var(--cs-fuchsia-300);
--color-fuchsia-400: var(--cs-fuchsia-400);
--color-fuchsia-500: var(--cs-fuchsia-500);
--color-fuchsia-600: var(--cs-fuchsia-600);
--color-fuchsia-700: var(--cs-fuchsia-700);
--color-fuchsia-800: var(--cs-fuchsia-800);
--color-fuchsia-900: var(--cs-fuchsia-900);
--color-fuchsia-950: var(--cs-fuchsia-950);
/* Pink */
--color-pink-50: var(--cs-pink-50);
--color-pink-100: var(--cs-pink-100);
--color-pink-200: var(--cs-pink-200);
--color-pink-300: var(--cs-pink-300);
--color-pink-400: var(--cs-pink-400);
--color-pink-500: var(--cs-pink-500);
--color-pink-600: var(--cs-pink-600);
--color-pink-700: var(--cs-pink-700);
--color-pink-800: var(--cs-pink-800);
--color-pink-900: var(--cs-pink-900);
--color-pink-950: var(--cs-pink-950);
/* Rose */
--color-rose-50: var(--cs-rose-50);
--color-rose-100: var(--cs-rose-100);
--color-rose-200: var(--cs-rose-200);
--color-rose-300: var(--cs-rose-300);
--color-rose-400: var(--cs-rose-400);
--color-rose-500: var(--cs-rose-500);
--color-rose-600: var(--cs-rose-600);
--color-rose-700: var(--cs-rose-700);
--color-rose-800: var(--cs-rose-800);
--color-rose-900: var(--cs-rose-900);
--color-rose-950: var(--cs-rose-950);
/* Black & White */
--color-black: var(--cs-black);
--color-white: var(--cs-white);
/* ==================== */
/* Spacing & Sizing自定义尺寸额外扩展 */
/* ==================== */
--spacing-5xs: var(--cs-size-5xs);
--spacing-4xs: var(--cs-size-4xs);
--spacing-3xs: var(--cs-size-3xs);
--spacing-2xs: var(--cs-size-2xs);
--spacing-xs: var(--cs-size-xs);
--spacing-sm: var(--cs-size-sm);
--spacing-md: var(--cs-size-md);
--spacing-lg: var(--cs-size-lg);
--spacing-xl: var(--cs-size-xl);
--spacing-2xl: var(--cs-size-2xl);
--spacing-3xl: var(--cs-size-3xl);
--spacing-4xl: var(--cs-size-4xl);
--spacing-5xl: var(--cs-size-5xl);
--spacing-6xl: var(--cs-size-6xl);
--spacing-7xl: var(--cs-size-7xl);
--spacing-8xl: var(--cs-size-8xl);
/* ==================== */
/* Border Radius自定义圆角额外扩展 */
/* ==================== */
--radius-4xs: var(--cs-radius-4xs);
--radius-3xs: var(--cs-radius-3xs);
--radius-2xs: var(--cs-radius-2xs);
--radius-xs: var(--cs-radius-xs);
--radius-sm: var(--cs-radius-sm);
--radius-md: var(--cs-radius-md);
--radius-lg: var(--cs-radius-lg);
--radius-xl: var(--cs-radius-xl);
--radius-2xl: var(--cs-radius-2xl);
--radius-3xl: var(--cs-radius-3xl);
--radius-round: var(--cs-radius-round);
}
/* ==================== */
/* Dark Mode 覆盖 */
/* ==================== */
@theme dark {
--color-background: var(--cs-background);
--color-foreground: var(--cs-foreground);
--color-card: var(--cs-card);
--color-popover: var(--cs-popover);
--color-border: var(--cs-border);
--color-input: var(--cs-border);
--color-secondary: var(--cs-secondary);
--color-muted: var(--cs-muted);
--color-accent: var(--cs-accent);
--color-sidebar: var(--cs-sidebar);
--color-sidebar-accent: var(--cs-sidebar-accent);
}
/* ==================== */
/* Base Styles可选 */
/* ==================== */
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -1,560 +0,0 @@
/**
* CherryStudio Design Tokens
*
* 基于 todocss.css 转换的设计令牌
* - 所有变量使用 --cs-* 前缀CherryStudio
* - 合并重复的 Spacing/Sizing
* - 修正错误(如 font-weight 单位)
* - 删除 Opacity 变量(使用 Tailwind 修饰符)
* - 正确分离 Light/Dark Mode
*/
:root {
/* ==================== */
/* Primitive Colors - Light Mode */
/* ==================== */
/* Neutral */
--cs-neutral-50: hsla(0, 0%, 98%, 1);
--cs-neutral-100: hsla(0, 0%, 96%, 1);
--cs-neutral-200: hsla(0, 0%, 90%, 1);
--cs-neutral-300: hsla(0, 0%, 83%, 1);
--cs-neutral-400: hsla(0, 0%, 64%, 1);
--cs-neutral-500: hsla(0, 0%, 45%, 1);
--cs-neutral-600: hsla(215, 14%, 34%, 1);
--cs-neutral-700: hsla(0, 0%, 25%, 1);
--cs-neutral-800: hsla(0, 0%, 15%, 1);
--cs-neutral-900: hsla(0, 0%, 9%, 1);
--cs-neutral-950: hsla(0, 0%, 4%, 1);
/* Stone */
--cs-stone-50: hsla(60, 9%, 98%, 1);
--cs-stone-100: hsla(60, 5%, 96%, 1);
--cs-stone-200: hsla(20, 6%, 90%, 1);
--cs-stone-300: hsla(24, 6%, 83%, 1);
--cs-stone-400: hsla(24, 5%, 64%, 1);
--cs-stone-500: hsla(25, 5%, 45%, 1);
--cs-stone-600: hsla(33, 5%, 32%, 1);
--cs-stone-700: hsla(30, 6%, 25%, 1);
--cs-stone-800: hsla(12, 6%, 15%, 1);
--cs-stone-900: hsla(24, 10%, 10%, 1);
--cs-stone-950: hsla(20, 14%, 4%, 1);
/* Zinc */
--cs-zinc-50: hsla(0, 0%, 98%, 1);
--cs-zinc-100: hsla(240, 5%, 96%, 1);
--cs-zinc-200: hsla(240, 6%, 90%, 1);
--cs-zinc-300: hsla(240, 5%, 84%, 1);
--cs-zinc-400: hsla(240, 5%, 65%, 1);
--cs-zinc-500: hsla(240, 4%, 46%, 1);
--cs-zinc-600: hsla(240, 5%, 34%, 1);
--cs-zinc-700: hsla(240, 5%, 26%, 1);
--cs-zinc-800: hsla(240, 4%, 16%, 1);
--cs-zinc-900: hsla(240, 6%, 10%, 1);
--cs-zinc-950: hsla(240, 10%, 4%, 1);
/* Slate */
--cs-slate-50: hsla(210, 40%, 98%, 1);
--cs-slate-100: hsla(210, 40%, 96%, 1);
--cs-slate-200: hsla(214, 32%, 91%, 1);
--cs-slate-300: hsla(213, 27%, 84%, 1);
--cs-slate-400: hsla(215, 20%, 65%, 1);
--cs-slate-500: hsla(215, 16%, 47%, 1);
--cs-slate-600: hsla(215, 19%, 35%, 1);
--cs-slate-700: hsla(215, 25%, 27%, 1);
--cs-slate-800: hsla(217, 33%, 17%, 1);
--cs-slate-900: hsla(222, 47%, 11%, 1);
--cs-slate-950: hsla(229, 84%, 5%, 1);
/* Gray */
--cs-gray-50: hsla(210, 20%, 98%, 1);
--cs-gray-100: hsla(220, 14%, 96%, 1);
--cs-gray-200: hsla(220, 13%, 91%, 1);
--cs-gray-300: hsla(216, 12%, 84%, 1);
--cs-gray-400: hsla(218, 11%, 65%, 1);
--cs-gray-500: hsla(220, 9%, 46%, 1);
--cs-gray-600: hsla(0, 0%, 32%, 1);
--cs-gray-700: hsla(217, 19%, 27%, 1);
--cs-gray-800: hsla(215, 28%, 17%, 1);
--cs-gray-900: hsla(221, 39%, 11%, 1);
--cs-gray-950: hsla(224, 71%, 4%, 1);
/* Red */
--cs-red-50: hsla(0, 86%, 97%, 1);
--cs-red-100: hsla(0, 93%, 94%, 1);
--cs-red-200: hsla(0, 96%, 89%, 1);
--cs-red-300: hsla(0, 94%, 82%, 1);
--cs-red-400: hsla(0, 91%, 71%, 1);
--cs-red-500: hsla(0, 84%, 60%, 1);
--cs-red-600: hsla(0, 72%, 51%, 1);
--cs-red-700: hsla(0, 74%, 42%, 1);
--cs-red-800: hsla(0, 70%, 35%, 1);
--cs-red-900: hsla(0, 63%, 31%, 1);
--cs-red-950: hsla(0, 75%, 15%, 1);
/* Orange */
--cs-orange-50: hsla(33, 100%, 96%, 1);
--cs-orange-100: hsla(34, 100%, 92%, 1);
--cs-orange-200: hsla(32, 98%, 83%, 1);
--cs-orange-300: hsla(31, 97%, 72%, 1);
--cs-orange-400: hsla(27, 96%, 61%, 1);
--cs-orange-500: hsla(25, 95%, 53%, 1);
--cs-orange-600: hsla(21, 90%, 48%, 1);
--cs-orange-700: hsla(17, 88%, 40%, 1);
--cs-orange-800: hsla(15, 79%, 34%, 1);
--cs-orange-900: hsla(15, 75%, 28%, 1);
--cs-orange-950: hsla(13, 81%, 15%, 1);
/* Amber */
--cs-amber-50: hsla(48, 100%, 96%, 1);
--cs-amber-100: hsla(48, 96%, 89%, 1);
--cs-amber-200: hsla(48, 97%, 77%, 1);
--cs-amber-300: hsla(46, 97%, 65%, 1);
--cs-amber-400: hsla(43, 96%, 56%, 1);
--cs-amber-500: hsla(38, 92%, 50%, 1);
--cs-amber-600: hsla(32, 95%, 44%, 1);
--cs-amber-700: hsla(26, 90%, 37%, 1);
--cs-amber-800: hsla(23, 83%, 31%, 1);
--cs-amber-900: hsla(22, 78%, 26%, 1);
--cs-amber-950: hsla(21, 92%, 14%, 1);
/* Yellow */
--cs-yellow-50: hsla(55, 92%, 95%, 1);
--cs-yellow-100: hsla(55, 97%, 88%, 1);
--cs-yellow-200: hsla(53, 98%, 77%, 1);
--cs-yellow-300: hsla(50, 98%, 64%, 1);
--cs-yellow-400: hsla(48, 96%, 53%, 1);
--cs-yellow-500: hsla(45, 93%, 47%, 1);
--cs-yellow-600: hsla(41, 96%, 40%, 1);
--cs-yellow-700: hsla(35, 92%, 33%, 1);
--cs-yellow-800: hsla(32, 81%, 29%, 1);
--cs-yellow-900: hsla(28, 73%, 26%, 1);
--cs-yellow-950: hsla(26, 83%, 14%, 1);
/* Lime (品牌主色) */
--cs-lime-50: hsla(78, 92%, 95%, 1);
--cs-lime-100: hsla(80, 89%, 89%, 1);
--cs-lime-200: hsla(81, 88%, 80%, 1);
--cs-lime-300: hsla(82, 85%, 67%, 1);
--cs-lime-400: hsla(83, 78%, 55%, 1);
--cs-lime-500: hsla(84, 81%, 44%, 1);
--cs-lime-600: hsla(85, 85%, 35%, 1);
--cs-lime-700: hsla(86, 78%, 27%, 1);
--cs-lime-800: hsla(86, 69%, 23%, 1);
--cs-lime-900: hsla(88, 61%, 20%, 1);
--cs-lime-950: hsla(89, 80%, 10%, 1);
/* Green */
--cs-green-50: hsla(138, 76%, 97%, 1);
--cs-green-100: hsla(141, 84%, 93%, 1);
--cs-green-200: hsla(141, 79%, 85%, 1);
--cs-green-300: hsla(142, 77%, 73%, 1);
--cs-green-400: hsla(142, 69%, 58%, 1);
--cs-green-500: hsla(142, 71%, 45%, 1);
--cs-green-600: hsla(142, 76%, 36%, 1);
--cs-green-700: hsla(142, 72%, 29%, 1);
--cs-green-800: hsla(143, 64%, 24%, 1);
--cs-green-900: hsla(144, 61%, 20%, 1);
--cs-green-950: hsla(145, 80%, 10%, 1);
/* Emerald */
--cs-emerald-50: hsla(152, 81%, 96%, 1);
--cs-emerald-100: hsla(149, 80%, 90%, 1);
--cs-emerald-200: hsla(152, 76%, 80%, 1);
--cs-emerald-300: hsla(156, 72%, 67%, 1);
--cs-emerald-400: hsla(158, 64%, 52%, 1);
--cs-emerald-500: hsla(160, 84%, 39%, 1);
--cs-emerald-600: hsla(161, 94%, 30%, 1);
--cs-emerald-700: hsla(163, 94%, 24%, 1);
--cs-emerald-800: hsla(163, 88%, 20%, 1);
--cs-emerald-900: hsla(164, 86%, 16%, 1);
--cs-emerald-950: hsla(166, 91%, 9%, 1);
/* Teal */
--cs-teal-50: hsla(166, 76%, 97%, 1);
--cs-teal-100: hsla(167, 85%, 89%, 1);
--cs-teal-200: hsla(168, 84%, 78%, 1);
--cs-teal-300: hsla(171, 77%, 64%, 1);
--cs-teal-400: hsla(172, 66%, 50%, 1);
--cs-teal-500: hsla(173, 80%, 40%, 1);
--cs-teal-600: hsla(175, 84%, 32%, 1);
--cs-teal-700: hsla(175, 77%, 26%, 1);
--cs-teal-800: hsla(176, 69%, 22%, 1);
--cs-teal-900: hsla(176, 61%, 19%, 1);
--cs-teal-950: hsla(179, 84%, 10%, 1);
/* Cyan */
--cs-cyan-50: hsla(183, 100%, 96%, 1);
--cs-cyan-100: hsla(185, 96%, 90%, 1);
--cs-cyan-200: hsla(186, 94%, 82%, 1);
--cs-cyan-300: hsla(187, 92%, 69%, 1);
--cs-cyan-400: hsla(188, 86%, 53%, 1);
--cs-cyan-500: hsla(189, 94%, 43%, 1);
--cs-cyan-600: hsla(192, 91%, 36%, 1);
--cs-cyan-700: hsla(193, 82%, 31%, 1);
--cs-cyan-800: hsla(194, 70%, 27%, 1);
--cs-cyan-900: hsla(196, 64%, 24%, 1);
--cs-cyan-950: hsla(197, 79%, 15%, 1);
/* Sky */
--cs-sky-50: hsla(204, 100%, 97%, 1);
--cs-sky-100: hsla(204, 94%, 94%, 1);
--cs-sky-200: hsla(201, 94%, 86%, 1);
--cs-sky-300: hsla(199, 95%, 74%, 1);
--cs-sky-400: hsla(198, 93%, 60%, 1);
--cs-sky-500: hsla(199, 89%, 48%, 1);
--cs-sky-600: hsla(200, 98%, 39%, 1);
--cs-sky-700: hsla(201, 96%, 32%, 1);
--cs-sky-800: hsla(201, 90%, 27%, 1);
--cs-sky-900: hsla(202, 80%, 24%, 1);
--cs-sky-950: hsla(204, 80%, 16%, 1);
/* Blue */
--cs-blue-50: hsla(214, 100%, 97%, 1);
--cs-blue-100: hsla(214, 95%, 93%, 1);
--cs-blue-200: hsla(213, 97%, 87%, 1);
--cs-blue-300: hsla(212, 96%, 78%, 1);
--cs-blue-400: hsla(213, 94%, 68%, 1);
--cs-blue-500: hsla(217, 91%, 60%, 1);
--cs-blue-600: hsla(221, 83%, 53%, 1);
--cs-blue-700: hsla(224, 76%, 48%, 1);
--cs-blue-800: hsla(226, 71%, 40%, 1);
--cs-blue-900: hsla(224, 64%, 33%, 1);
--cs-blue-950: hsla(226, 57%, 21%, 1);
/* Indigo */
--cs-indigo-50: hsla(226, 100%, 97%, 1);
--cs-indigo-100: hsla(226, 100%, 94%, 1);
--cs-indigo-200: hsla(228, 96%, 89%, 1);
--cs-indigo-300: hsla(230, 94%, 82%, 1);
--cs-indigo-400: hsla(234, 89%, 74%, 1);
--cs-indigo-500: hsla(239, 84%, 67%, 1);
--cs-indigo-600: hsla(243, 75%, 59%, 1);
--cs-indigo-700: hsla(245, 58%, 51%, 1);
--cs-indigo-800: hsla(244, 55%, 41%, 1);
--cs-indigo-900: hsla(242, 47%, 34%, 1);
--cs-indigo-950: hsla(244, 47%, 20%, 1);
/* Violet */
--cs-violet-50: hsla(250, 100%, 98%, 1);
--cs-violet-100: hsla(251, 91%, 95%, 1);
--cs-violet-200: hsla(251, 95%, 92%, 1);
--cs-violet-300: hsla(253, 95%, 85%, 1);
--cs-violet-400: hsla(255, 92%, 76%, 1);
--cs-violet-500: hsla(258, 90%, 66%, 1);
--cs-violet-600: hsla(262, 83%, 58%, 1);
--cs-violet-700: hsla(263, 70%, 50%, 1);
--cs-violet-800: hsla(263, 69%, 42%, 1);
--cs-violet-900: hsla(264, 67%, 35%, 1);
--cs-violet-950: hsla(262, 78%, 23%, 1);
/* Purple */
--cs-purple-50: hsla(270, 100%, 98%, 1);
--cs-purple-100: hsla(269, 100%, 95%, 1);
--cs-purple-200: hsla(269, 100%, 92%, 1);
--cs-purple-300: hsla(269, 97%, 85%, 1);
--cs-purple-400: hsla(270, 95%, 75%, 1);
--cs-purple-500: hsla(271, 91%, 65%, 1);
--cs-purple-600: hsla(271, 81%, 56%, 1);
--cs-purple-700: hsla(272, 72%, 47%, 1);
--cs-purple-800: hsla(273, 67%, 39%, 1);
--cs-purple-900: hsla(274, 66%, 32%, 1);
--cs-purple-950: hsla(274, 87%, 21%, 1);
/* Fuchsia */
--cs-fuchsia-50: hsla(289, 100%, 98%, 1);
--cs-fuchsia-100: hsla(287, 100%, 95%, 1);
--cs-fuchsia-200: hsla(288, 96%, 91%, 1);
--cs-fuchsia-300: hsla(291, 93%, 83%, 1);
--cs-fuchsia-400: hsla(292, 91%, 73%, 1);
--cs-fuchsia-500: hsla(292, 84%, 61%, 1);
--cs-fuchsia-600: hsla(293, 69%, 49%, 1);
--cs-fuchsia-700: hsla(295, 72%, 40%, 1);
--cs-fuchsia-800: hsla(295, 70%, 33%, 1);
--cs-fuchsia-900: hsla(297, 64%, 28%, 1);
--cs-fuchsia-950: hsla(297, 90%, 16%, 1);
/* Pink */
--cs-pink-50: hsla(327, 73%, 97%, 1);
--cs-pink-100: hsla(326, 78%, 95%, 1);
--cs-pink-200: hsla(326, 85%, 90%, 1);
--cs-pink-300: hsla(327, 87%, 82%, 1);
--cs-pink-400: hsla(329, 86%, 70%, 1);
--cs-pink-500: hsla(330, 81%, 60%, 1);
--cs-pink-600: hsla(333, 71%, 51%, 1);
--cs-pink-700: hsla(335, 78%, 42%, 1);
--cs-pink-800: hsla(336, 74%, 35%, 1);
--cs-pink-900: hsla(336, 69%, 30%, 1);
--cs-pink-950: hsla(336, 84%, 17%, 1);
/* Rose */
--cs-rose-50: hsla(356, 100%, 97%, 1);
--cs-rose-100: hsla(356, 100%, 95%, 1);
--cs-rose-200: hsla(353, 96%, 90%, 1);
--cs-rose-300: hsla(353, 96%, 82%, 1);
--cs-rose-400: hsla(351, 95%, 71%, 1);
--cs-rose-500: hsla(350, 89%, 60%, 1);
--cs-rose-600: hsla(347, 77%, 50%, 1);
--cs-rose-700: hsla(345, 83%, 41%, 1);
--cs-rose-800: hsla(343, 80%, 35%, 1);
--cs-rose-900: hsla(342, 75%, 30%, 1);
--cs-rose-950: hsla(343, 88%, 16%, 1);
/* Black & White */
--cs-black: hsla(0, 0%, 0%, 1);
--cs-white: hsla(0, 0%, 100%, 1);
/* ==================== */
/* Semantic Tokens - Light Mode */
/* ==================== */
/* Brand Colors */
--cs-primary: var(--cs-lime-500);
--cs-destructive: var(--cs-red-500);
--cs-success: var(--cs-green-500);
--cs-warning: var(--cs-amber-500);
/* Background & Foreground */
--cs-background: var(--cs-zinc-50);
--cs-background-subtle: hsla(0, 0%, 0%, 0.02);
--cs-foreground: hsla(0, 0%, 0%, 0.9);
--cs-foreground-secondary: hsla(0, 0%, 0%, 0.6);
--cs-foreground-muted: hsla(0, 0%, 0%, 0.4);
/* Card & Popover */
--cs-card: var(--cs-white);
--cs-popover: var(--cs-white);
/* Border */
--cs-border: hsla(0, 0%, 0%, 0.1);
--cs-border-hover: hsla(0, 0%, 0%, 0.2);
--cs-border-active: hsla(0, 0%, 0%, 0.3);
/* Ring (Focus) */
--cs-ring: hsla(84, 81%, 44%, 0.4);
/* UI Element Colors */
--cs-secondary: hsla(0, 0%, 0%, 0.05); /* Secondary Button Background */
--cs-secondary-hover: hsla(0, 0%, 0%, 0.85);
--cs-secondary-active: hsla(0, 0%, 0%, 0.7);
--cs-muted: hsla(0, 0%, 0%, 0.05); /* Muted/Subtle Background */
--cs-accent: hsla(0, 0%, 0%, 0.05); /* Accent Background */
--cs-ghost-hover: hsla(0, 0%, 0%, 0.05); /* Ghost Button Hover */
--cs-ghost-active: hsla(0, 0%, 0%, 0.1); /* Ghost Button Active */
/* Sidebar */
--cs-sidebar: var(--cs-white);
--cs-sidebar-accent: hsla(0, 0%, 0%, 0.05);
/* ==================== */
/* UI 元素细分颜色 */
/* ==================== */
/* Modal / Overlay */
--cs-modal-backdrop: hsla(0, 0%, 0%, 0.4);
--cs-modal-thumb: hsla(0, 0%, 0%, 0.2);
--cs-modal-thumb-hover: hsla(0, 0%, 0%, 0.3);
/* Icon */
--cs-icon-default: var(--cs-foreground-secondary);
--cs-icon-hover: var(--cs-foreground);
/* Input / Select */
--cs-input-background: var(--cs-white);
--cs-input-border: var(--cs-border);
--cs-input-border-hover: var(--cs-border-hover);
--cs-input-border-focus: var(--cs-primary);
/* Primary Button */
--cs-primary-button-background: var(--cs-primary);
--cs-primary-button-text: var(--cs-white);
--cs-primary-button-background-hover: hsla(84, 81%, 44%, 0.85);
--cs-primary-button-background-active: hsla(84, 81%, 44%, 0.7);
--cs-primary-button-background-2nd: hsla(84, 81%, 44%, 0.1);
--cs-primary-button-background-3rd: hsla(84, 81%, 44%, 0.05);
/* Secondary Button */
--cs-secondary-button-background: var(--cs-secondary);
--cs-secondary-button-text: var(--cs-foreground);
--cs-secondary-button-background-hover: hsla(0, 0%, 0%, 0.85);
--cs-secondary-button-background-active: hsla(0, 0%, 0%, 0.7);
--cs-secondary-button-border: var(--cs-border);
/* Ghost Button */
--cs-ghost-button-background: hsla(0, 0%, 0%, 0);
--cs-ghost-button-text: var(--cs-foreground);
--cs-ghost-button-background-hover: var(--cs-ghost-hover);
--cs-ghost-button-background-active: var(--cs-ghost-active);
/* ==================== */
/* Spacing & Sizing (合并) */
/* ==================== */
--cs-size-5xs: 0.25rem; /* 4px */
--cs-size-4xs: 0.5rem; /* 8px */
--cs-size-3xs: 0.75rem; /* 12px */
--cs-size-2xs: 1rem; /* 16px */
--cs-size-xs: 1.5rem; /* 24px */
--cs-size-sm: 2rem; /* 32px */
--cs-size-md: 2.5rem; /* 40px */
--cs-size-lg: 3rem; /* 48px */
--cs-size-xl: 3.5rem; /* 56px */
--cs-size-2xl: 4rem; /* 64px */
--cs-size-3xl: 4.5rem; /* 72px */
--cs-size-4xl: 5rem; /* 80px */
--cs-size-5xl: 5.5rem; /* 88px */
--cs-size-6xl: 6rem; /* 96px */
--cs-size-7xl: 6.5rem; /* 104px */
--cs-size-8xl: 7rem; /* 112px */
/* ==================== */
/* Border Radius */
/* ==================== */
--cs-radius-4xs: 4px; /* 4px */
--cs-radius-3xs: 8px; /* 8px */
--cs-radius-2xs: 12px; /* 12px */
--cs-radius-xs: 16px; /* 16px */
--cs-radius-sm: 24px; /* 24px */
--cs-radius-md: 32px; /* 32px */
--cs-radius-lg: 2.5rem; /* 40px */
--cs-radius-xl: 48px; /* 48px */
--cs-radius-2xl: 56px; /* 56px */
--cs-radius-3xl: 64px; /* 64px */
--cs-radius-round: 999px; /* 保持 px因为是特殊值 */
/* Border Width */
--cs-border-width-sm: 1px; /* 1px */
--cs-border-width-md: 2px; /* 2px */
--cs-border-width-lg: 3px; /* 3px */
/* ==================== */
/* Typography */
/* ==================== */
/* Font Families */
--cs-font-family-heading: Inter;
--cs-font-family-body: Inter;
/* Font Weights (修正单位错误) */
--cs-font-weight-regular: 400;
--cs-font-weight-medium: 500;
--cs-font-weight-bold: 700;
/* Font Sizes - Body */
--cs-font-size-body-xs: 0.75rem; /* 12px */
--cs-font-size-body-sm: 0.875rem; /* 14px */
--cs-font-size-body-md: 1rem; /* 16px */
--cs-font-size-body-lg: 1.125rem; /* 18px */
/* Font Sizes - Heading */
--cs-font-size-heading-xs: 1.25rem; /* 20px */
--cs-font-size-heading-sm: 1.5rem; /* 24px */
--cs-font-size-heading-md: 2rem; /* 32px */
--cs-font-size-heading-lg: 2.5rem; /* 40px */
--cs-font-size-heading-xl: 3rem; /* 48px */
--cs-font-size-heading-2xl: 3.75rem; /* 60px */
/* Line Heights - Body */
--cs-line-height-body-xs: 1.25rem; /* 20px */
--cs-line-height-body-sm: 1.5rem; /* 24px */
--cs-line-height-body-md: 1.5rem; /* 24px */
--cs-line-height-body-lg: 1.75rem; /* 28px */
/* Line Heights - Heading */
--cs-line-height-heading-xs: 2rem; /* 32px */
--cs-line-height-heading-sm: 2.5rem; /* 40px */
--cs-line-height-heading-md: 3rem; /* 48px */
--cs-line-height-heading-lg: 3.75rem; /* 60px */
--cs-line-height-heading-xl: 5rem; /* 80px */
/* Paragraph Spacing */
--cs-paragraph-spacing-body-xs: 0.75rem; /* 12px */
--cs-paragraph-spacing-body-sm: 0.875rem; /* 14px */
--cs-paragraph-spacing-body-md: 1rem; /* 16px */
--cs-paragraph-spacing-body-lg: 1.125rem; /* 18px */
--cs-paragraph-spacing-heading-xs: 1.25rem; /* 20px */
--cs-paragraph-spacing-heading-sm: 1.5rem; /* 24px */
--cs-paragraph-spacing-heading-md: 2rem; /* 32px */
--cs-paragraph-spacing-heading-lg: 2.5rem; /* 40px */
--cs-paragraph-spacing-heading-xl: 3rem; /* 48px */
--cs-paragraph-spacing-heading-2xl: 3.75rem; /* 60px */
}
/* ==================== */
/* Dark Mode */
/* ==================== */
.dark {
/* Background & Foreground */
--cs-background: var(--cs-zinc-900);
--cs-background-subtle: hsla(0, 0%, 100%, 0.02);
--cs-foreground: hsla(0, 0%, 100%, 0.9);
--cs-foreground-secondary: hsla(0, 0%, 100%, 0.6);
--cs-foreground-muted: hsla(0, 0%, 100%, 0.4);
/* Card & Popover */
--cs-card: var(--cs-black);
--cs-popover: var(--cs-black);
/* Border */
--cs-border: hsla(0, 0%, 100%, 0.1);
--cs-border-hover: hsla(0, 0%, 100%, 0.2);
--cs-border-active: hsla(0, 0%, 100%, 0.3);
/* Ring (Focus) - 保持不变 */
--cs-ring: hsla(84, 81%, 44%, 0.4);
/* UI Element Colors - Dark Mode */
--cs-secondary: hsla(0, 0%, 100%, 0.1); /* Secondary Button Background */
--cs-secondary-hover: hsla(0, 0%, 100%, 0.2);
--cs-secondary-active: hsla(0, 0%, 100%, 0.25);
--cs-muted: hsla(0, 0%, 100%, 0.1); /* Muted/Subtle Background */
--cs-accent: hsla(0, 0%, 100%, 0.1); /* Accent Background */
--cs-ghost-hover: hsla(0, 0%, 100%, 0.1); /* Ghost Button Hover */
--cs-ghost-active: hsla(0, 0%, 100%, 0.15); /* Ghost Button Active */
/* Sidebar */
--cs-sidebar: var(--cs-black);
--cs-sidebar-accent: hsla(0, 0%, 100%, 0.1);
/* ==================== */
/* UI 元素细分颜色 (Dark Mode) */
/* ==================== */
/* Modal / Overlay */
--cs-modal-backdrop: hsla(0, 0%, 0%, 0.06);
--cs-modal-thumb: hsla(0, 0%, 100%, 0.2);
--cs-modal-thumb-hover: hsla(0, 0%, 100%, 0.3);
/* Icon */
--cs-icon-default: var(--cs-foreground-secondary);
--cs-icon-hover: var(--cs-foreground);
/* Input / Select */
--cs-input-background: var(--cs-black);
--cs-input-border: var(--cs-border);
--cs-input-border-hover: var(--cs-border-hover);
--cs-input-border-focus: var(--cs-primary);
/* Primary Button - 保持不变 */
--cs-primary-button-background: var(--cs-primary);
--cs-primary-button-text: var(--cs-white);
--cs-primary-button-background-hover: hsla(84, 81%, 44%, 0.85);
--cs-primary-button-background-active: hsla(84, 81%, 44%, 0.7);
--cs-primary-button-background-2nd: hsla(84, 81%, 44%, 0.1);
--cs-primary-button-background-3rd: hsla(84, 81%, 44%, 0.05);
/* Secondary Button */
--cs-secondary-button-background: var(--cs-secondary);
--cs-secondary-button-text: var(--cs-foreground);
--cs-secondary-button-background-hover: hsla(0, 0%, 100%, 0.2);
--cs-secondary-button-background-active: hsla(0, 0%, 100%, 0.25);
--cs-secondary-button-border: var(--cs-border);
/* Ghost Button */
--cs-ghost-button-background: hsla(0, 0%, 100%, 0);
--cs-ghost-button-text: var(--cs-foreground);
--cs-ghost-button-background-hover: var(--cs-ghost-hover);
--cs-ghost-button-background-active: var(--cs-ghost-active);
}

View File

@@ -1,870 +0,0 @@
: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;
}

View File

@@ -1,10 +1,8 @@
import { createServer } from 'node:http'
import { loggerService } from '@logger'
import { IpcChannel } from '@shared/IpcChannel'
import { agentService } from '../services/agents'
import { windowService } from '../services/WindowService'
import { app } from './app'
import { config } from './config'
@@ -45,13 +43,6 @@ export class ApiServer {
return new Promise((resolve, reject) => {
this.server!.listen(port, host, () => {
logger.info('API server started', { host, port })
// Notify renderer that API server is ready
const mainWindow = windowService.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(IpcChannel.ApiServer_Ready)
}
resolve()
})

View File

@@ -14,6 +14,7 @@ import type { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH } from '@shared/config/constant'
import type { UpgradeChannel } from '@shared/data/preference/preferenceTypes'
import { IpcChannel } from '@shared/IpcChannel'
import type { ShortcutPreferenceMap } from '@shared/shortcuts/types'
import type {
AgentPersistedMessage,
FileMetadata,
@@ -30,13 +31,11 @@ import { BrowserWindow, dialog, ipcMain, session, shell, systemPreferences, webC
import fontList from 'font-list'
import { agentMessageRepository } from './services/agents/database'
import { PluginService } from './services/agents/plugins/PluginService'
import { apiServerService } from './services/ApiServerService'
import appService from './services/AppService'
import AppUpdater from './services/AppUpdater'
import BackupManager from './services/BackupManager'
import { codeToolsService } from './services/CodeToolsService'
import { configManager } from './services/ConfigManager'
import CopilotService from './services/CopilotService'
import DxtService from './services/DxtService'
import { ExportService } from './services/ExportService'
@@ -51,12 +50,12 @@ import * as NutstoreService from './services/NutstoreService'
import ObsidianVaultService from './services/ObsidianVaultService'
import { ocrService } from './services/ocr/OcrService'
import OvmsManager from './services/OvmsManager'
import { PluginService } from './services/PluginService'
import { proxyManager } from './services/ProxyManager'
import { pythonService } from './services/PythonService'
import { FileServiceManager } from './services/remotefile/FileServiceManager'
import { searchService } from './services/SearchService'
import { SelectionService } from './services/SelectionService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import {
addEndMessage,
addStreamMessage,
@@ -72,7 +71,6 @@ import {
} from './services/SpanCacheService'
import storeSyncService from './services/StoreSyncService'
import VertexAIService from './services/VertexAIService'
import WebSocketService from './services/WebSocketService'
import { setOpenLinkExternal } from './services/WebviewService'
import { windowService } from './services/WindowService'
import { calculateDirectorySize, getResourcePath } from './utils'
@@ -583,13 +581,19 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
})
// shortcuts
ipcMain.handle(IpcChannel.Shortcuts_Update, (_, shortcuts: Shortcut[]) => {
configManager.setShortcuts(shortcuts)
// Refresh shortcuts registration
if (mainWindow) {
unregisterAllShortcuts()
registerShortcuts(mainWindow)
ipcMain.handle(IpcChannel.Shortcuts_Update, async (_, shortcuts: Shortcut[]) => {
const existingPreferences = preferenceService.get('shortcut.preferences') ?? {}
const nextPreferences: ShortcutPreferenceMap = { ...existingPreferences }
for (const shortcut of shortcuts) {
const name = shortcut.key === 'mini_window' ? 'show_mini_window' : shortcut.key
nextPreferences[name] = {
key: [...shortcut.shortcut],
enabled: shortcut.enabled
}
}
await preferenceService.set('shortcut.preferences', nextPreferences)
})
ipcMain.handle(IpcChannel.KnowledgeBase_Create, KnowledgeService.create.bind(KnowledgeService))
@@ -1022,13 +1026,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}
})
// WebSocket
ipcMain.handle(IpcChannel.WebSocket_Start, WebSocketService.start)
ipcMain.handle(IpcChannel.WebSocket_Stop, WebSocketService.stop)
ipcMain.handle(IpcChannel.WebSocket_Status, WebSocketService.getStatus)
ipcMain.handle(IpcChannel.WebSocket_SendFile, WebSocketService.sendFile)
ipcMain.handle(IpcChannel.WebSocket_GetAllCandidates, WebSocketService.getAllCandidates)
// Preference handlers
PreferenceService.registerIpcHandler()
}

View File

@@ -1,15 +1,15 @@
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { OllamaEmbeddings } from '@cherrystudio/embedjs-ollama'
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings'
import type { ApiClient } from '@types'
import { net } from 'electron'
import { VoyageEmbeddings } from './VoyageEmbeddings'
export default class EmbeddingsFactory {
static create({ embedApiClient, dimensions }: { embedApiClient: ApiClient; dimensions?: number }): BaseEmbeddings {
const batchSize = 10
const { model, provider, apiKey, baseURL } = embedApiClient
const { model, provider, apiKey, apiVersion, baseURL } = embedApiClient
if (provider === 'voyageai') {
return new VoyageEmbeddings({
modelName: model,
@@ -38,13 +38,22 @@ export default class EmbeddingsFactory {
}
})
}
// NOTE: Azure OpenAI 也走 OpenAIEmbeddings, baseURL是https://xxxx.openai.azure.com/openai/v1
if (apiVersion !== undefined) {
return new AzureOpenAiEmbeddings({
azureOpenAIApiKey: apiKey,
azureOpenAIApiVersion: apiVersion,
azureOpenAIApiDeploymentName: model,
azureOpenAIEndpoint: baseURL,
dimensions,
batchSize
})
}
return new OpenAiEmbeddings({
model,
apiKey,
dimensions,
batchSize,
configuration: { baseURL, fetch: net.fetch as typeof fetch }
configuration: { baseURL }
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,298 +1,349 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger'
import { handleZoomFactor } from '@main/utils/zoom'
import type { Shortcut } from '@types'
import { IpcChannel } from '@shared/IpcChannel'
import { shortcutDefinitions } from '@shared/shortcuts/definitions'
import type { HydratedShortcut, ShortcutDefinition, ShortcutPreferenceMap } from '@shared/shortcuts/types'
import type { BrowserWindow } from 'electron'
import { globalShortcut } from 'electron'
import { BrowserWindow as ElectronBrowserWindow, globalShortcut, ipcMain } from 'electron'
import { configManager } from './ConfigManager'
import selectionService from './SelectionService'
import { windowService } from './WindowService'
const logger = loggerService.withContext('ShortcutService')
let showAppAccelerator: string | null = null
let showMiniWindowAccelerator: string | null = null
let selectionAssistantToggleAccelerator: string | null = null
let selectionAssistantSelectTextAccelerator: string | null = null
type ShortcutHandler = (window: BrowserWindow | undefined) => void
//indicate if the shortcuts are registered on app boot time
let isRegisterOnBoot = true
class ShortcutService {
private handlers = new Map<string, ShortcutHandler>()
private hydratedShortcuts = new Map<string, HydratedShortcut>()
private registeredAccelerators = new Map<string, string[]>()
private readonly definitionMap = new Map<string, ShortcutDefinition>()
private ipcRegistered = false
// store the focus and blur handlers for each window to unregister them later
const windowOnHandlers = new Map<BrowserWindow, { onFocusHandler: () => void; onBlurHandler: () => void }>()
constructor() {
this.definitionMap = new Map(shortcutDefinitions.map((definition) => [definition.name, definition]))
function getShortcutHandler(shortcut: Shortcut) {
switch (shortcut.key) {
case 'zoom_in':
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()
this.setupIpcHandlers()
this.registerDefaultHandlers()
this.hydrateShortcuts()
this.registerPreferenceListeners()
}
public registerHandler(name: string, handler: ShortcutHandler) {
if (this.handlers.has(name)) {
logger.warn(`Handler for shortcut '${name}' is being overwritten.`)
}
this.handlers.set(name, handler)
}
public registerMainProcessShortcuts(window?: BrowserWindow) {
const targetWindow = this.getTargetWindow(window)
this.unregisterTrackedAccelerators()
for (const config of this.hydratedShortcuts.values()) {
if (config.scope !== 'main') {
continue
}
case 'mini_window':
return () => {
windowService.toggleMiniWindow()
if (!config.enabled || config.key.length === 0) {
continue
}
case 'selection_assistant_toggle':
return () => {
if (selectionService) {
selectionService.toggleEnabled()
const handler = this.handlers.get(config.name)
if (!handler) {
logger.warn(`No handler registered for shortcut '${config.name}'.`)
continue
}
const accelerators = this.buildAccelerators(config)
if (accelerators.length === 0) {
continue
}
for (const accelerator of accelerators) {
try {
const registered = globalShortcut.register(accelerator, () => {
try {
handler(this.getTargetWindow(targetWindow))
} catch (error) {
logger.error(`Error while executing handler for shortcut '${config.name}':`, error as Error)
}
})
if (!registered) {
logger.warn(`Electron rejected shortcut accelerator '${accelerator}' for '${config.name}'.`)
continue
}
this.trackAccelerator(config.name, accelerator)
} catch (error) {
logger.warn(`Failed to register shortcut '${config.name}' with accelerator '${accelerator}':`, error as Error)
}
}
case 'selection_assistant_select_text':
return () => {
if (selectionService) {
selectionService.processSelectTextByShortcut()
}
this.broadcastShortcuts()
}
public unregisterAllShortcuts() {
this.unregisterTrackedAccelerators()
}
public getHydratedShortcuts(): Record<string, HydratedShortcut> {
return Object.fromEntries(
[...this.hydratedShortcuts.entries()].map(([name, config]) => [
name,
{
...config,
key: [...config.key]
}
])
)
}
private setupIpcHandlers() {
if (this.ipcRegistered) {
return
}
ipcMain.handle(IpcChannel.Shortcuts_GetAll, () => {
return this.getHydratedShortcuts()
})
this.ipcRegistered = true
}
private registerPreferenceListeners() {
preferenceService.subscribeChange('shortcut.preferences', (newPreferences) => {
this.hydrateAndRegister(newPreferences)
})
}
private hydrateAndRegister(preferences?: ShortcutPreferenceMap) {
this.hydrateShortcuts(preferences)
this.registerMainProcessShortcuts()
}
private hydrateShortcuts(preferences?: ShortcutPreferenceMap) {
const preferenceSnapshot = preferences ?? preferenceService.get('shortcut.preferences')
this.hydratedShortcuts.clear()
for (const definition of shortcutDefinitions) {
const userPreference = preferenceSnapshot?.[definition.name]
const key =
userPreference?.key && userPreference.key.length > 0 ? [...userPreference.key] : [...definition.defaultKey]
const enabled = typeof userPreference?.enabled === 'boolean' ? userPreference.enabled : definition.defaultEnabled
this.hydratedShortcuts.set(definition.name, {
...definition,
key,
enabled
})
}
}
private broadcastShortcuts() {
const payload = this.getHydratedShortcuts()
for (const window of ElectronBrowserWindow.getAllWindows()) {
if (window.isDestroyed()) {
continue
}
try {
window.webContents.send(IpcChannel.Shortcuts_Updated, payload)
} catch (error) {
logger.warn('Failed to broadcast shortcut update to renderer window:', error as Error)
}
}
}
private unregisterTrackedAccelerators() {
for (const accelerators of this.registeredAccelerators.values()) {
for (const accelerator of accelerators) {
try {
globalShortcut.unregister(accelerator)
} catch (error) {
logger.warn(`Failed to unregister accelerator '${accelerator}':`, error as Error)
}
}
default:
}
this.registeredAccelerators.clear()
}
private trackAccelerator(name: string, accelerator: string) {
if (!this.registeredAccelerators.has(name)) {
this.registeredAccelerators.set(name, [])
}
this.registeredAccelerators.get(name)!.push(accelerator)
}
private buildAccelerators(config: HydratedShortcut): string[] {
if (config.key.length === 0) {
return []
}
const baseAccelerator = this.normalizeAccelerator(config.key)
if (!baseAccelerator) {
logger.warn(`Invalid shortcut configuration for '${config.name}', skipping registration.`)
return []
}
if (config.name === 'zoom_in' && this.isUsingDefaultKey(config)) {
return [baseAccelerator, 'CommandOrControl+numadd']
}
if (config.name === 'zoom_out' && this.isUsingDefaultKey(config)) {
return [baseAccelerator, 'CommandOrControl+numsub']
}
if (config.name === 'zoom_reset' && this.isUsingDefaultKey(config)) {
return [baseAccelerator, 'CommandOrControl+num0']
}
return [baseAccelerator]
}
private isUsingDefaultKey(config: HydratedShortcut): boolean {
const definition = this.definitionMap.get(config.name)
if (!definition) {
return false
}
if (definition.defaultKey.length !== config.key.length) {
return false
}
return definition.defaultKey.every((key, index) => key === config.key[index])
}
private normalizeAccelerator(keys: string[]): string | null {
const normalizedKeys = keys.map((key) => this.normalizeKeyForElectron(key)).filter((key): key is string => !!key)
if (normalizedKeys.length !== keys.length) {
return null
}
return normalizedKeys.join('+')
}
private normalizeKeyForElectron(key: string): string | null {
switch (key) {
case 'CommandOrControl':
case 'Ctrl':
case 'Alt':
case 'Meta':
case 'Shift':
return key
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 '='
case 'Space':
return 'Space'
default:
return key
}
}
private registerDefaultHandlers() {
this.registerHandler('zoom_in', (window) => {
const target = this.getTargetWindow(window)
if (!target) {
return
}
handleZoomFactor([target], 0.1)
})
this.registerHandler('zoom_out', (window) => {
const target = this.getTargetWindow(window)
if (!target) {
return
}
handleZoomFactor([target], -0.1)
})
this.registerHandler('zoom_reset', (window) => {
const target = this.getTargetWindow(window)
if (!target) {
return
}
handleZoomFactor([target], 0, true)
})
this.registerHandler('show_app', () => {
windowService.toggleMainWindow()
})
this.registerHandler('show_mini_window', () => {
if (!preferenceService.get('feature.quick_assistant.enabled')) {
return
}
windowService.toggleMiniWindow()
})
this.registerHandler('selection_assistant_toggle', () => {
selectionService?.toggleEnabled()
})
this.registerHandler('selection_assistant_select_text', () => {
selectionService?.processSelectTextByShortcut()
})
}
private getTargetWindow(window?: BrowserWindow): BrowserWindow | undefined {
if (window && !window.isDestroyed()) {
return window
}
const mainWindow = windowService.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) {
return mainWindow
}
return undefined
}
}
function formatShortcutKey(shortcut: string[]): string {
return shortcut.join('+')
}
// 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())
}
})()
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 const shortcutService = new ShortcutService()
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
const registerOnlyUniversalShortcuts = () => {
register(true)
}
//onlyUniversalShortcuts is used to register shortcuts that are not window specific, like show_app & mini_window
//onlyUniversalShortcuts is needed when we launch to tray
const register = (onlyUniversalShortcuts: boolean = false) => {
if (window.isDestroyed()) return
const shortcuts = configManager.getShortcuts()
if (!shortcuts) return
shortcuts.forEach((shortcut) => {
try {
if (shortcut.shortcut.length === 0) {
return
}
//if not enabled, exit early from the process.
if (!shortcut.enabled) {
return
}
// only register universal shortcuts when needed
if (
onlyUniversalShortcuts &&
!['show_app', 'mini_window', 'selection_assistant_toggle', 'selection_assistant_select_text'].includes(
shortcut.key
)
) {
return
}
const handler = getShortcutHandler(shortcut)
if (!handler) {
return
}
switch (shortcut.key) {
case 'show_app':
showAppAccelerator = formatShortcutKey(shortcut.shortcut)
break
case 'mini_window':
//available only when QuickAssistant enabled
if (!preferenceService.get('feature.quick_assistant.enabled')) {
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 = () => {
if (window.isDestroyed()) return
try {
globalShortcut.unregisterAll()
if (showAppAccelerator) {
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
const accelerator = convertShortcutFormat(showAppAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
}
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) {
logger.warn('Failed to unregister shortcuts')
}
}
// only register the event handlers once
if (undefined === windowOnHandlers.get(window)) {
// pass register() directly to listener, the func will receive Event as argument, it's not expected
const registerHandler = () => {
register()
}
window.on('focus', registerHandler)
window.on('blur', unregister)
windowOnHandlers.set(window, { onFocusHandler: registerHandler, onBlurHandler: unregister })
}
if (!window.isDestroyed() && window.isFocused()) {
register()
}
shortcutService.registerMainProcessShortcuts(window)
}
export function unregisterAllShortcuts() {
try {
showAppAccelerator = null
showMiniWindowAccelerator = null
selectionAssistantToggleAccelerator = null
selectionAssistantSelectTextAccelerator = null
windowOnHandlers.forEach((handlers, window) => {
window.off('focus', handlers.onFocusHandler)
window.off('blur', handlers.onBlurHandler)
})
windowOnHandlers.clear()
globalShortcut.unregisterAll()
} catch (error) {
logger.warn('Failed to unregister all shortcuts')
}
shortcutService.unregisterAllShortcuts()
}

View File

@@ -1,369 +0,0 @@
import { loggerService } from '@logger'
import * as fs from 'fs'
import { networkInterfaces } from 'os'
import * as path from 'path'
import type { Socket } from 'socket.io'
import { Server } from 'socket.io'
import { windowService } from './WindowService'
const logger = loggerService.withContext('WebSocketService')
class WebSocketService {
private io: Server | null = null
private isStarted = false
private port = 7017
private connectedClients = new Set<string>()
private getLocalIpAddress(): string | undefined {
const interfaces = networkInterfaces()
// 按优先级排序的网络接口名称模式
const interfacePriority = [
// macOS: 以太网/Wi-Fi 优先
/^en[0-9]+$/, // en0, en1 (以太网/Wi-Fi)
/^(en|eth)[0-9]+$/, // 以太网接口
/^wlan[0-9]+$/, // 无线接口
// Windows: 以太网/Wi-Fi 优先
/^(Ethernet|Wi-Fi|Local Area Connection)/,
/^(Wi-Fi|无线网络连接)/,
// Linux: 以太网/Wi-Fi 优先
/^(eth|enp|wlp|wlan)[0-9]+/,
// 虚拟化接口(低优先级)
/^bridge[0-9]+$/, // Docker bridge
/^veth[0-9]+$/, // Docker veth
/^docker[0-9]+/, // Docker interfaces
/^br-[0-9a-f]+/, // Docker bridge
/^vmnet[0-9]+$/, // VMware
/^vboxnet[0-9]+$/, // VirtualBox
// VPN 隧道接口(低优先级)
/^utun[0-9]+$/, // macOS VPN
/^tun[0-9]+$/, // Linux/Unix VPN
/^tap[0-9]+$/, // TAP interfaces
/^tailscale[0-9]*$/, // Tailscale VPN
/^wg[0-9]+$/ // WireGuard VPN
]
const candidates: Array<{ interface: string; address: string; priority: number }> = []
for (const [name, ifaces] of Object.entries(interfaces)) {
for (const iface of ifaces || []) {
if (iface.family === 'IPv4' && !iface.internal) {
// 计算接口优先级
let priority = 999 // 默认最低优先级
for (let i = 0; i < interfacePriority.length; i++) {
if (interfacePriority[i].test(name)) {
priority = i
break
}
}
candidates.push({
interface: name,
address: iface.address,
priority
})
}
}
}
if (candidates.length === 0) {
logger.warn('无法获取局域网 IP使用默认 IP: 127.0.0.1')
return '127.0.0.1'
}
// 按优先级排序,选择优先级最高的
candidates.sort((a, b) => a.priority - b.priority)
const best = candidates[0]
logger.info(`获取局域网 IP: ${best.address} (interface: ${best.interface})`)
return best.address
}
public start = async (): Promise<{ success: boolean; port?: number; error?: string }> => {
if (this.isStarted && this.io) {
return { success: true, port: this.port }
}
try {
this.io = new Server(this.port, {
cors: {
origin: '*',
methods: ['GET', 'POST']
},
transports: ['websocket', 'polling'],
allowEIO3: true,
pingTimeout: 60000,
pingInterval: 25000
})
this.io.on('connection', (socket: Socket) => {
this.connectedClients.add(socket.id)
const mainWindow = windowService.getMainWindow()
if (!mainWindow) {
logger.error('Main window is null, cannot send connection event')
} else {
mainWindow.webContents.send('websocket-client-connected', {
connected: true,
clientId: socket.id
})
logger.info(`Connection event sent to renderer, total clients: ${this.connectedClients.size}`)
}
socket.on('message', (data) => {
logger.info('Received message from mobile:', data)
mainWindow?.webContents.send('websocket-message-received', data)
socket.emit('message_received', { success: true })
})
socket.on('disconnect', () => {
logger.info(`Client disconnected: ${socket.id}`)
this.connectedClients.delete(socket.id)
if (this.connectedClients.size === 0) {
mainWindow?.webContents.send('websocket-client-connected', {
connected: false,
clientId: socket.id
})
}
})
})
// Engine 层面的事件监听
this.io.engine.on('connection_error', (err) => {
logger.error('Engine connection error:', err)
})
this.io.engine.on('connection', (rawSocket) => {
const remoteAddr = rawSocket.request.connection.remoteAddress
logger.info(`[Engine] Raw connection from: ${remoteAddr}`)
logger.info(`[Engine] Transport: ${rawSocket.transport.name}`)
rawSocket.on('packet', (packet: { type: string; data?: any }) => {
logger.info(
`[Engine] ← Packet from ${remoteAddr}: type="${packet.type}"`,
packet.data ? { data: packet.data } : {}
)
})
rawSocket.on('packetCreate', (packet: { type: string; data?: any }) => {
logger.info(`[Engine] → Packet to ${remoteAddr}: type="${packet.type}"`)
})
rawSocket.on('close', (reason: string) => {
logger.warn(`[Engine] Connection closed from ${remoteAddr}, reason: ${reason}`)
})
rawSocket.on('error', (error: Error) => {
logger.error(`[Engine] Connection error from ${remoteAddr}:`, error)
})
})
// Socket.IO 握手失败监听
this.io.on('connection_error', (err) => {
logger.error('[Socket.IO] Connection error during handshake:', err)
})
this.isStarted = true
logger.info(`WebSocket server started on port ${this.port}`)
return { success: true, port: this.port }
} catch (error) {
logger.error('Failed to start WebSocket server:', error as Error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}
}
}
public stop = async (): Promise<{ success: boolean }> => {
if (!this.isStarted || !this.io) {
return { success: true }
}
try {
await new Promise<void>((resolve) => {
this.io!.close(() => {
resolve()
})
})
this.io = null
this.isStarted = false
this.connectedClients.clear()
logger.info('WebSocket server stopped')
return { success: true }
} catch (error) {
logger.error('Failed to stop WebSocket server:', error as Error)
return { success: false }
}
}
public getStatus = async (): Promise<{
isRunning: boolean
port?: number
ip?: string
clientConnected: boolean
}> => {
return {
isRunning: this.isStarted,
port: this.isStarted ? this.port : undefined,
ip: this.isStarted ? this.getLocalIpAddress() : undefined,
clientConnected: this.connectedClients.size > 0
}
}
public getAllCandidates = async (): Promise<
Array<{
host: string
interface: string
priority: number
}>
> => {
const interfaces = networkInterfaces()
// 按优先级排序的网络接口名称模式
const interfacePriority = [
// macOS: 以太网/Wi-Fi 优先
/^en[0-9]+$/, // en0, en1 (以太网/Wi-Fi)
/^(en|eth)[0-9]+$/, // 以太网接口
/^wlan[0-9]+$/, // 无线接口
// Windows: 以太网/Wi-Fi 优先
/^(Ethernet|Wi-Fi|Local Area Connection)/,
/^(Wi-Fi|无线网络连接)/,
// Linux: 以太网/Wi-Fi 优先
/^(eth|enp|wlp|wlan)[0-9]+/,
// 虚拟化接口(低优先级)
/^bridge[0-9]+$/, // Docker bridge
/^veth[0-9]+$/, // Docker veth
/^docker[0-9]+/, // Docker interfaces
/^br-[0-9a-f]+/, // Docker bridge
/^vmnet[0-9]+$/, // VMware
/^vboxnet[0-9]+$/, // VirtualBox
// VPN 隧道接口(低优先级)
/^utun[0-9]+$/, // macOS VPN
/^tun[0-9]+$/, // Linux/Unix VPN
/^tap[0-9]+$/, // TAP interfaces
/^tailscale[0-9]*$/, // Tailscale VPN
/^wg[0-9]+$/ // WireGuard VPN
]
const candidates: Array<{ host: string; interface: string; priority: number }> = []
for (const [name, ifaces] of Object.entries(interfaces)) {
for (const iface of ifaces || []) {
if (iface.family === 'IPv4' && !iface.internal) {
// 计算接口优先级
let priority = 999 // 默认最低优先级
for (let i = 0; i < interfacePriority.length; i++) {
if (interfacePriority[i].test(name)) {
priority = i
break
}
}
candidates.push({
host: iface.address,
interface: name,
priority
})
logger.debug(`Found interface: ${name} -> ${iface.address} (priority: ${priority})`)
}
}
}
// 按优先级排序返回
candidates.sort((a, b) => a.priority - b.priority)
logger.info(
`Found ${candidates.length} IP candidates: ${candidates.map((c) => `${c.host}(${c.interface})`).join(', ')}`
)
return candidates
}
public sendFile = async (
_: Electron.IpcMainInvokeEvent,
filePath: string
): Promise<{ success: boolean; error?: string }> => {
if (!this.isStarted || !this.io) {
const errorMsg = 'WebSocket server is not running.'
logger.error(errorMsg)
return { success: false, error: errorMsg }
}
if (this.connectedClients.size === 0) {
const errorMsg = 'No client connected.'
logger.error(errorMsg)
return { success: false, error: errorMsg }
}
const mainWindow = windowService.getMainWindow()
return new Promise((resolve, reject) => {
const stats = fs.statSync(filePath)
const totalSize = stats.size
const filename = path.basename(filePath)
const stream = fs.createReadStream(filePath)
let bytesSent = 0
const startTime = Date.now()
logger.info(`Starting file transfer: ${filename} (${this.formatFileSize(totalSize)})`)
// 向客户端发送文件开始的信号,包含文件名和总大小
this.io!.emit('zip-file-start', { filename, totalSize })
stream.on('data', (chunk) => {
bytesSent += chunk.length
const progress = (bytesSent / totalSize) * 100
// 向客户端发送文件块
this.io!.emit('zip-file-chunk', chunk)
// 向渲染进程发送进度更新
mainWindow?.webContents.send('file-send-progress', { progress })
// 每10%记录一次进度
if (Math.floor(progress) % 10 === 0) {
const elapsed = (Date.now() - startTime) / 1000
const speed = elapsed > 0 ? bytesSent / elapsed : 0
logger.info(`Transfer progress: ${Math.floor(progress)}% (${this.formatFileSize(speed)}/s)`)
}
})
stream.on('end', () => {
const totalTime = (Date.now() - startTime) / 1000
const avgSpeed = totalTime > 0 ? totalSize / totalTime : 0
logger.info(
`File transfer completed: ${filename} in ${totalTime.toFixed(1)}s (${this.formatFileSize(avgSpeed)}/s)`
)
// 确保发送100%的进度
mainWindow?.webContents.send('file-send-progress', { progress: 100 })
// 向客户端发送文件结束的信号
this.io!.emit('zip-file-end')
resolve({ success: true })
})
stream.on('error', (error) => {
logger.error(`File transfer failed: ${filename}`, error)
reject({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
})
})
})
}
private formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
}
export default new WebSocketService()

View File

@@ -1,426 +0,0 @@
import { loggerService } from '@logger'
import { findAllSkillDirectories, parsePluginMetadata, parseSkillMetadata } from '@main/utils/markdownParser'
import type { CachedPluginsData, InstalledPlugin, PluginError, PluginMetadata, PluginType } from '@types'
import { CachedPluginsDataSchema } from '@types'
import * as fs from 'fs'
import * as path from 'path'
const logger = loggerService.withContext('PluginCacheStore')
interface PluginCacheStoreDeps {
allowedExtensions: string[]
getPluginDirectoryName: (type: PluginType) => 'agents' | 'commands' | 'skills'
getClaudeBasePath: (workdir: string) => string
getClaudePluginDirectory: (workdir: string, type: PluginType) => string
getPluginsBasePath: () => string
}
export class PluginCacheStore {
constructor(private readonly deps: PluginCacheStoreDeps) {}
async listAvailableFilePlugins(type: 'agent' | 'command'): Promise<PluginMetadata[]> {
const basePath = this.deps.getPluginsBasePath()
const directory = path.join(basePath, this.deps.getPluginDirectoryName(type))
try {
await fs.promises.access(directory, fs.constants.R_OK)
} catch (error) {
logger.warn(`Plugin directory not accessible: ${directory}`, {
error: error instanceof Error ? error.message : String(error)
})
return []
}
const plugins: PluginMetadata[] = []
const categories = await fs.promises.readdir(directory, { withFileTypes: true })
for (const categoryEntry of categories) {
if (!categoryEntry.isDirectory()) {
continue
}
const category = categoryEntry.name
const categoryPath = path.join(directory, category)
const files = await fs.promises.readdir(categoryPath, { withFileTypes: true })
for (const file of files) {
if (!file.isFile()) {
continue
}
const ext = path.extname(file.name).toLowerCase()
if (!this.deps.allowedExtensions.includes(ext)) {
continue
}
try {
const filePath = path.join(categoryPath, file.name)
const sourcePath = path.join(this.deps.getPluginDirectoryName(type), category, file.name)
const metadata = await parsePluginMetadata(filePath, sourcePath, category, type)
plugins.push(metadata)
} catch (error) {
logger.warn(`Failed to parse plugin: ${file.name}`, {
category,
error: error instanceof Error ? error.message : String(error)
})
}
}
}
return plugins
}
async listAvailableSkills(): Promise<PluginMetadata[]> {
const basePath = this.deps.getPluginsBasePath()
const skillsPath = path.join(basePath, this.deps.getPluginDirectoryName('skill'))
const skills: PluginMetadata[] = []
try {
await fs.promises.access(skillsPath)
} catch {
logger.warn('Skills directory not found', { skillsPath })
return []
}
try {
const skillDirectories = await findAllSkillDirectories(skillsPath, basePath)
logger.info(`Found ${skillDirectories.length} skill directories`, { skillsPath })
for (const { folderPath, sourcePath } of skillDirectories) {
try {
const metadata = await parseSkillMetadata(folderPath, sourcePath, 'skills')
skills.push(metadata)
} catch (error) {
logger.warn(`Failed to parse skill folder: ${sourcePath}`, {
folderPath,
error: error instanceof Error ? error.message : String(error)
})
}
}
} catch (error) {
logger.error('Failed to scan skill directory', {
skillsPath,
error: error instanceof Error ? error.message : String(error)
})
}
return skills
}
async readSourceContent(sourcePath: string): Promise<string> {
const absolutePath = this.resolveSourcePath(sourcePath)
try {
await fs.promises.access(absolutePath, fs.constants.R_OK)
} catch {
throw {
type: 'FILE_NOT_FOUND',
path: sourcePath
} as PluginError
}
try {
return await fs.promises.readFile(absolutePath, 'utf-8')
} catch (error) {
throw {
type: 'READ_FAILED',
path: sourcePath,
reason: error instanceof Error ? error.message : String(error)
} as PluginError
}
}
resolveSourcePath(sourcePath: string): string {
const normalized = path.normalize(sourcePath)
if (normalized.includes('..')) {
throw {
type: 'PATH_TRAVERSAL',
message: 'Path traversal detected',
path: sourcePath
} as PluginError
}
const basePath = this.deps.getPluginsBasePath()
const absolutePath = path.join(basePath, normalized)
const resolvedPath = path.resolve(absolutePath)
if (!resolvedPath.startsWith(path.resolve(basePath))) {
throw {
type: 'PATH_TRAVERSAL',
message: 'Path outside plugins directory',
path: sourcePath
} as PluginError
}
return resolvedPath
}
async ensureSkillSourceDirectory(sourceAbsolutePath: string, sourcePath: string): Promise<void> {
let stats: fs.Stats
try {
stats = await fs.promises.stat(sourceAbsolutePath)
} catch {
throw {
type: 'FILE_NOT_FOUND',
path: sourceAbsolutePath
} as PluginError
}
if (!stats.isDirectory()) {
throw {
type: 'INVALID_METADATA',
reason: 'Skill source is not a directory',
path: sourcePath
} as PluginError
}
}
async validatePluginFile(filePath: string, maxFileSize: number): Promise<void> {
let stats: fs.Stats
try {
stats = await fs.promises.stat(filePath)
} catch {
throw {
type: 'FILE_NOT_FOUND',
path: filePath
} as PluginError
}
if (stats.size > maxFileSize) {
throw {
type: 'FILE_TOO_LARGE',
size: stats.size,
max: maxFileSize
} as PluginError
}
const ext = path.extname(filePath).toLowerCase()
if (!this.deps.allowedExtensions.includes(ext)) {
throw {
type: 'INVALID_FILE_TYPE',
extension: ext
} as PluginError
}
try {
const basePath = this.deps.getPluginsBasePath()
const relativeSourcePath = path.relative(basePath, filePath)
const segments = relativeSourcePath.split(path.sep)
const rootDir = segments[0]
const agentDir = this.deps.getPluginDirectoryName('agent')
const type: 'agent' | 'command' = rootDir === agentDir ? 'agent' : 'command'
const category = path.basename(path.dirname(filePath))
await parsePluginMetadata(filePath, relativeSourcePath, category, type)
} catch (error) {
throw {
type: 'INVALID_METADATA',
reason: 'Failed to parse frontmatter',
path: filePath
} as PluginError
}
}
async listInstalled(workdir: string): Promise<InstalledPlugin[]> {
const claudePath = this.deps.getClaudeBasePath(workdir)
const cacheData = await this.readCacheFile(claudePath)
if (cacheData) {
logger.debug(`Loaded ${cacheData.plugins.length} plugins from cache`, { workdir })
return cacheData.plugins
}
logger.info('Cache read failed, rebuilding from filesystem', { workdir })
return await this.rebuild(workdir)
}
async upsert(workdir: string, plugin: InstalledPlugin): Promise<void> {
const claudePath = this.deps.getClaudeBasePath(workdir)
let cacheData = await this.readCacheFile(claudePath)
let plugins = cacheData?.plugins
if (!plugins) {
plugins = await this.rebuild(workdir)
cacheData = {
version: 1,
lastUpdated: Date.now(),
plugins
}
}
const updatedPlugin: InstalledPlugin = {
...plugin,
metadata: {
...plugin.metadata,
installedAt: plugin.metadata.installedAt ?? Date.now()
}
}
const index = plugins.findIndex((p) => p.filename === updatedPlugin.filename && p.type === updatedPlugin.type)
if (index >= 0) {
plugins[index] = updatedPlugin
} else {
plugins.push(updatedPlugin)
}
const data: CachedPluginsData = {
version: cacheData?.version ?? 1,
lastUpdated: Date.now(),
plugins
}
await fs.promises.mkdir(claudePath, { recursive: true })
await this.writeCacheFile(claudePath, data)
}
async remove(workdir: string, filename: string, type: PluginType): Promise<void> {
const claudePath = this.deps.getClaudeBasePath(workdir)
let cacheData = await this.readCacheFile(claudePath)
let plugins = cacheData?.plugins
if (!plugins) {
plugins = await this.rebuild(workdir)
cacheData = {
version: 1,
lastUpdated: Date.now(),
plugins
}
}
const filtered = plugins.filter((p) => !(p.filename === filename && p.type === type))
const data: CachedPluginsData = {
version: cacheData?.version ?? 1,
lastUpdated: Date.now(),
plugins: filtered
}
await fs.promises.mkdir(claudePath, { recursive: true })
await this.writeCacheFile(claudePath, data)
}
async rebuild(workdir: string): Promise<InstalledPlugin[]> {
logger.info('Rebuilding plugin cache from filesystem', { workdir })
const claudePath = this.deps.getClaudeBasePath(workdir)
try {
await fs.promises.access(claudePath, fs.constants.R_OK)
} catch {
logger.warn('.claude directory not found, returning empty plugin list', { claudePath })
return []
}
const plugins: InstalledPlugin[] = []
await Promise.all([
this.collectFilePlugins(workdir, 'agent', plugins),
this.collectFilePlugins(workdir, 'command', plugins),
this.collectSkillPlugins(workdir, plugins)
])
try {
const cacheData: CachedPluginsData = {
version: 1,
lastUpdated: Date.now(),
plugins
}
await this.writeCacheFile(claudePath, cacheData)
logger.info(`Rebuilt cache with ${plugins.length} plugins`, { workdir })
} catch (error) {
logger.error('Failed to write cache file after rebuild', {
error: error instanceof Error ? error.message : String(error)
})
}
return plugins
}
private async collectFilePlugins(
workdir: string,
type: Exclude<PluginType, 'skill'>,
plugins: InstalledPlugin[]
): Promise<void> {
const directory = this.deps.getClaudePluginDirectory(workdir, type)
try {
await fs.promises.access(directory, fs.constants.R_OK)
} catch {
logger.debug(`${type} directory not found or not accessible`, { directory })
return
}
const files = await fs.promises.readdir(directory, { withFileTypes: true })
for (const file of files) {
if (!file.isFile()) {
continue
}
const ext = path.extname(file.name).toLowerCase()
if (!this.deps.allowedExtensions.includes(ext)) {
continue
}
try {
const filePath = path.join(directory, file.name)
const sourcePath = path.join(this.deps.getPluginDirectoryName(type), file.name)
const metadata = await parsePluginMetadata(filePath, sourcePath, this.deps.getPluginDirectoryName(type), type)
plugins.push({ filename: file.name, type, metadata })
} catch (error) {
logger.warn(`Failed to parse ${type} plugin: ${file.name}`, {
error: error instanceof Error ? error.message : String(error)
})
}
}
}
private async collectSkillPlugins(workdir: string, plugins: InstalledPlugin[]): Promise<void> {
const skillsPath = this.deps.getClaudePluginDirectory(workdir, 'skill')
const claudePath = this.deps.getClaudeBasePath(workdir)
try {
await fs.promises.access(skillsPath, fs.constants.R_OK)
} catch {
logger.debug('Skills directory not found or not accessible', { skillsPath })
return
}
const skillDirectories = await findAllSkillDirectories(skillsPath, claudePath)
for (const { folderPath, sourcePath } of skillDirectories) {
try {
const metadata = await parseSkillMetadata(folderPath, sourcePath, 'skills')
plugins.push({ filename: metadata.filename, type: 'skill', metadata })
} catch (error) {
logger.warn(`Failed to parse skill plugin: ${sourcePath}`, {
error: error instanceof Error ? error.message : String(error)
})
}
}
}
private async readCacheFile(claudePath: string): Promise<CachedPluginsData | null> {
const cachePath = path.join(claudePath, 'plugins.json')
try {
const content = await fs.promises.readFile(cachePath, 'utf-8')
const data = JSON.parse(content)
return CachedPluginsDataSchema.parse(data)
} catch (err) {
logger.warn(`Failed to read cache file at ${cachePath}`, {
error: err instanceof Error ? err.message : String(err)
})
return null
}
}
private async writeCacheFile(claudePath: string, data: CachedPluginsData): Promise<void> {
const cachePath = path.join(claudePath, 'plugins.json')
const tempPath = `${cachePath}.tmp`
const content = JSON.stringify(data, null, 2)
await fs.promises.writeFile(tempPath, content, 'utf-8')
await fs.promises.rename(tempPath, cachePath)
}
}

View File

@@ -1,149 +0,0 @@
import { loggerService } from '@logger'
import { copyDirectoryRecursive, deleteDirectoryRecursive } from '@main/utils/fileOperations'
import type { PluginError } from '@types'
import * as crypto from 'crypto'
import * as fs from 'fs'
const logger = loggerService.withContext('PluginInstaller')
export class PluginInstaller {
async installFilePlugin(agentId: string, sourceAbsolutePath: string, destPath: string): Promise<void> {
const tempPath = `${destPath}.tmp`
let fileCopied = false
try {
await fs.promises.copyFile(sourceAbsolutePath, tempPath)
fileCopied = true
logger.debug('File copied to temp location', { agentId, tempPath })
await fs.promises.rename(tempPath, destPath)
logger.debug('File moved to final location', { agentId, destPath })
} catch (error) {
if (fileCopied) {
await this.safeUnlink(tempPath, 'temp file')
}
throw this.toPluginError('install', error)
}
}
async uninstallFilePlugin(
agentId: string,
filename: string,
type: 'agent' | 'command',
filePath: string
): Promise<void> {
try {
await fs.promises.unlink(filePath)
logger.debug('Plugin file deleted', { agentId, filename, type, filePath })
} catch (error) {
const nodeError = error as NodeJS.ErrnoException
if (nodeError.code !== 'ENOENT') {
throw this.toPluginError('uninstall', error)
}
logger.warn('Plugin file already deleted', { agentId, filename, type, filePath })
}
}
async updateFilePluginContent(agentId: string, filePath: string, content: string): Promise<string> {
try {
await fs.promises.access(filePath, fs.constants.W_OK)
} catch {
throw {
type: 'FILE_NOT_FOUND',
path: filePath
} as PluginError
}
try {
await fs.promises.writeFile(filePath, content, 'utf8')
logger.debug('Plugin content written successfully', {
agentId,
filePath,
size: Buffer.byteLength(content, 'utf8')
})
} catch (error) {
throw {
type: 'WRITE_FAILED',
path: filePath,
reason: error instanceof Error ? error.message : String(error)
} as PluginError
}
return crypto.createHash('sha256').update(content).digest('hex')
}
async installSkill(agentId: string, sourceAbsolutePath: string, destPath: string): Promise<void> {
const logContext = logger.withContext('installSkill')
let folderCopied = false
const tempPath = `${destPath}.tmp`
try {
try {
await fs.promises.access(destPath)
await deleteDirectoryRecursive(destPath)
logContext.info('Removed existing skill folder', { agentId, destPath })
} catch {
// No existing folder
}
await copyDirectoryRecursive(sourceAbsolutePath, tempPath)
folderCopied = true
logContext.info('Skill folder copied to temp location', { agentId, tempPath })
await fs.promises.rename(tempPath, destPath)
logContext.info('Skill folder moved to final location', { agentId, destPath })
} catch (error) {
if (folderCopied) {
await this.safeRemoveDirectory(tempPath, 'temp folder')
}
throw this.toPluginError('install-skill', error)
}
}
async uninstallSkill(agentId: string, folderName: string, skillPath: string): Promise<void> {
const logContext = logger.withContext('uninstallSkill')
try {
await deleteDirectoryRecursive(skillPath)
logContext.info('Skill folder deleted', { agentId, folderName, skillPath })
} catch (error) {
const nodeError = error as NodeJS.ErrnoException
if (nodeError.code !== 'ENOENT') {
throw this.toPluginError('uninstall-skill', error)
}
logContext.warn('Skill folder already deleted', { agentId, folderName, skillPath })
}
}
private toPluginError(operation: string, error: unknown): PluginError {
return {
type: 'TRANSACTION_FAILED',
operation,
reason: error instanceof Error ? error.message : String(error)
}
}
private async safeUnlink(targetPath: string, label: string): Promise<void> {
try {
await fs.promises.unlink(targetPath)
logger.debug(`Rolled back ${label}`, { targetPath })
} catch (unlinkError) {
logger.error(`Failed to rollback ${label}`, {
targetPath,
error: unlinkError instanceof Error ? unlinkError.message : String(unlinkError)
})
}
}
private async safeRemoveDirectory(targetPath: string, label: string): Promise<void> {
try {
await deleteDirectoryRecursive(targetPath)
logger.info(`Rolled back ${label}`, { targetPath })
} catch (unlinkError) {
logger.error(`Failed to rollback ${label}`, {
targetPath,
error: unlinkError instanceof Error ? unlinkError.message : String(unlinkError)
})
}
}
}

View File

@@ -1,614 +0,0 @@
import { loggerService } from '@logger'
import { parsePluginMetadata, parseSkillMetadata } from '@main/utils/markdownParser'
import type {
GetAgentResponse,
InstalledPlugin,
InstallPluginOptions,
ListAvailablePluginsResult,
PluginError,
PluginMetadata,
PluginType,
UninstallPluginOptions
} from '@types'
import { app } from 'electron'
import * as fs from 'fs'
import * as path from 'path'
import { AgentService } from '../services/AgentService'
import { PluginCacheStore } from './PluginCacheStore'
import { PluginInstaller } from './PluginInstaller'
const logger = loggerService.withContext('PluginService')
interface PluginServiceConfig {
maxFileSize: number // bytes
cacheTimeout: number // milliseconds
}
/**
* PluginService manages agent and command plugins from resources directory.
*
* Features:
* - Singleton pattern for consistent state management
* - Caching of available plugins for performance
* - Security validation (path traversal, file size, extensions)
* - Transactional install/uninstall operations
* - Integration with AgentService for metadata persistence
*/
export class PluginService {
private static instance: PluginService | null = null
private availablePluginsCache: ListAvailablePluginsResult | null = null
private cacheTimestamp = 0
private config: PluginServiceConfig
private readonly cacheStore: PluginCacheStore
private readonly installer: PluginInstaller
private readonly agentService: AgentService
private readonly ALLOWED_EXTENSIONS = ['.md', '.markdown']
private constructor(config?: Partial<PluginServiceConfig>) {
this.config = {
maxFileSize: config?.maxFileSize ?? 1024 * 1024, // 1MB default
cacheTimeout: config?.cacheTimeout ?? 5 * 60 * 1000 // 5 minutes default
}
this.agentService = AgentService.getInstance()
this.cacheStore = new PluginCacheStore({
allowedExtensions: this.ALLOWED_EXTENSIONS,
getPluginDirectoryName: this.getPluginDirectoryName.bind(this),
getClaudeBasePath: this.getClaudeBasePath.bind(this),
getClaudePluginDirectory: this.getClaudePluginDirectory.bind(this),
getPluginsBasePath: this.getPluginsBasePath.bind(this)
})
this.installer = new PluginInstaller()
logger.info('PluginService initialized', {
maxFileSize: this.config.maxFileSize,
cacheTimeout: this.config.cacheTimeout
})
}
/**
* Get singleton instance
*/
static getInstance(config?: Partial<PluginServiceConfig>): PluginService {
if (!PluginService.instance) {
PluginService.instance = new PluginService(config)
}
return PluginService.instance
}
/**
* List all available plugins from resources directory (with caching)
*/
async listAvailable(): Promise<ListAvailablePluginsResult> {
const now = Date.now()
// Return cached data if still valid
if (this.availablePluginsCache && now - this.cacheTimestamp < this.config.cacheTimeout) {
logger.debug('Returning cached plugin list', {
cacheAge: now - this.cacheTimestamp
})
return this.availablePluginsCache
}
logger.info('Scanning available plugins')
// Scan all plugin types
const [agents, commands, skills] = await Promise.all([
this.cacheStore.listAvailableFilePlugins('agent'),
this.cacheStore.listAvailableFilePlugins('command'),
this.cacheStore.listAvailableSkills()
])
const result: ListAvailablePluginsResult = {
agents,
commands,
skills, // NEW: include skills
total: agents.length + commands.length + skills.length
}
// Update cache
this.availablePluginsCache = result
this.cacheTimestamp = now
logger.info('Available plugins scanned', {
agentsCount: agents.length,
commandsCount: commands.length,
skillsCount: skills.length,
total: result.total
})
return result
}
/**
* Install plugin with validation and transactional safety
*/
async install(options: InstallPluginOptions): Promise<PluginMetadata> {
logger.info('Installing plugin', options)
const context = await this.prepareInstallContext(options)
if (options.type === 'skill') {
return await this.installSkillPlugin(options, context)
}
return await this.installFilePlugin(options, context)
}
private async prepareInstallContext(options: InstallPluginOptions): Promise<{
agent: GetAgentResponse
workdir: string
sourceAbsolutePath: string
}> {
const agent = await this.getAgentOrThrow(options.agentId)
const workdir = this.getWorkdirOrThrow(agent, options.agentId)
await this.validateWorkdir(agent, workdir)
const sourceAbsolutePath = this.cacheStore.resolveSourcePath(options.sourcePath)
return { agent, workdir, sourceAbsolutePath }
}
private async installSkillPlugin(
options: InstallPluginOptions,
context: {
agent: GetAgentResponse
workdir: string
sourceAbsolutePath: string
}
): Promise<PluginMetadata> {
const { agent, workdir, sourceAbsolutePath } = context
await this.cacheStore.ensureSkillSourceDirectory(sourceAbsolutePath, options.sourcePath)
const metadata = await parseSkillMetadata(sourceAbsolutePath, options.sourcePath, 'skills')
const sanitizedFolderName = this.sanitizeFolderName(metadata.filename)
await this.ensureClaudeDirectory(workdir, 'skill')
const destPath = this.getClaudePluginPath(workdir, 'skill', sanitizedFolderName)
metadata.filename = sanitizedFolderName
await this.installer.installSkill(agent.id, sourceAbsolutePath, destPath)
const installedAt = Date.now()
const metadataWithInstall: PluginMetadata = {
...metadata,
filename: sanitizedFolderName,
installedAt,
updatedAt: metadata.updatedAt ?? installedAt,
type: 'skill'
}
const installedPlugin: InstalledPlugin = {
filename: sanitizedFolderName,
type: 'skill',
metadata: metadataWithInstall
}
await this.cacheStore.upsert(workdir, installedPlugin)
this.upsertAgentPlugin(agent, installedPlugin)
logger.info('Skill installed successfully', {
agentId: options.agentId,
sourcePath: options.sourcePath,
folderName: sanitizedFolderName
})
return metadataWithInstall
}
private async installFilePlugin(
options: InstallPluginOptions,
context: {
agent: GetAgentResponse
workdir: string
sourceAbsolutePath: string
}
): Promise<PluginMetadata> {
const { agent, workdir, sourceAbsolutePath } = context
if (options.type === 'skill') {
throw {
type: 'INVALID_FILE_TYPE',
extension: options.type
} as PluginError
}
const filePluginType: 'agent' | 'command' = options.type
await this.cacheStore.validatePluginFile(sourceAbsolutePath, this.config.maxFileSize)
const category = path.basename(path.dirname(options.sourcePath))
const metadata = await parsePluginMetadata(sourceAbsolutePath, options.sourcePath, category, filePluginType)
const sanitizedFilename = this.sanitizeFilename(metadata.filename)
metadata.filename = sanitizedFilename
await this.ensureClaudeDirectory(workdir, filePluginType)
const destPath = this.getClaudePluginPath(workdir, filePluginType, sanitizedFilename)
await this.installer.installFilePlugin(agent.id, sourceAbsolutePath, destPath)
const installedAt = Date.now()
const metadataWithInstall: PluginMetadata = {
...metadata,
filename: sanitizedFilename,
installedAt,
updatedAt: metadata.updatedAt ?? installedAt,
type: filePluginType
}
const installedPlugin: InstalledPlugin = {
filename: sanitizedFilename,
type: filePluginType,
metadata: metadataWithInstall
}
await this.cacheStore.upsert(workdir, installedPlugin)
this.upsertAgentPlugin(agent, installedPlugin)
logger.info('Plugin installed successfully', {
agentId: options.agentId,
filename: sanitizedFilename,
type: filePluginType
})
return metadataWithInstall
}
/**
* Uninstall plugin with cleanup
*/
async uninstall(options: UninstallPluginOptions): Promise<void> {
logger.info('Uninstalling plugin', options)
const agent = await this.getAgentOrThrow(options.agentId)
const workdir = this.getWorkdirOrThrow(agent, options.agentId)
await this.validateWorkdir(agent, workdir)
if (options.type === 'skill') {
const sanitizedFolderName = this.sanitizeFolderName(options.filename)
const skillPath = this.getClaudePluginPath(workdir, 'skill', sanitizedFolderName)
await this.installer.uninstallSkill(agent.id, sanitizedFolderName, skillPath)
await this.cacheStore.remove(workdir, sanitizedFolderName, 'skill')
this.removeAgentPlugin(agent, sanitizedFolderName, 'skill')
logger.info('Skill uninstalled successfully', {
agentId: options.agentId,
folderName: sanitizedFolderName
})
return
}
const sanitizedFilename = this.sanitizeFilename(options.filename)
const filePath = this.getClaudePluginPath(workdir, options.type, sanitizedFilename)
await this.installer.uninstallFilePlugin(agent.id, sanitizedFilename, options.type, filePath)
await this.cacheStore.remove(workdir, sanitizedFilename, options.type)
this.removeAgentPlugin(agent, sanitizedFilename, options.type)
logger.info('Plugin uninstalled successfully', {
agentId: options.agentId,
filename: sanitizedFilename,
type: options.type
})
}
/**
* List installed plugins for an agent (from database + filesystem validation)
*/
async listInstalled(agentId: string): Promise<InstalledPlugin[]> {
logger.debug('Listing installed plugins', { agentId })
const agent = await this.getAgentOrThrow(agentId)
const workdir = agent.accessible_paths?.[0]
if (!workdir) {
logger.warn('Agent has no accessible paths', { agentId })
return []
}
const plugins = await this.listInstalledFromCache(workdir)
logger.debug('Listed installed plugins from cache', {
agentId,
count: plugins.length
})
return plugins
}
/**
* Invalidate plugin cache (for development/testing)
*/
invalidateCache(): void {
this.availablePluginsCache = null
this.cacheTimestamp = 0
logger.info('Plugin cache invalidated')
}
// ============================================================================
// Cache File Management (for installed plugins)
// ============================================================================
/**
* Read cache file from .claude/plugins.json
* Returns null if cache doesn't exist or is invalid
*/
/**
* List installed plugins from cache file
* Falls back to filesystem scan if cache is missing or corrupt
*/
async listInstalledFromCache(workdir: string): Promise<InstalledPlugin[]> {
logger.debug('Listing installed plugins from cache', { workdir })
return await this.cacheStore.listInstalled(workdir)
}
/**
* Read plugin content from source (resources directory)
*/
async readContent(sourcePath: string): Promise<string> {
logger.info('Reading plugin content', { sourcePath })
const content = await this.cacheStore.readSourceContent(sourcePath)
logger.debug('Plugin content read successfully', {
sourcePath,
size: content.length
})
return content
}
/**
* Write plugin content to installed plugin (in agent's .claude directory)
* Note: Only works for file-based plugins (agents/commands), not skills
*/
async writeContent(agentId: string, filename: string, type: PluginType, content: string): Promise<void> {
logger.info('Writing plugin content', { agentId, filename, type })
const agent = await this.getAgentOrThrow(agentId)
const workdir = this.getWorkdirOrThrow(agent, agentId)
await this.validateWorkdir(agent, workdir)
// Check if plugin is installed
let installedPlugins = agent.installed_plugins ?? []
if (installedPlugins.length === 0) {
installedPlugins = await this.cacheStore.listInstalled(workdir)
agent.installed_plugins = installedPlugins
}
const installedPlugin = installedPlugins.find((p) => p.filename === filename && p.type === type)
if (!installedPlugin) {
throw {
type: 'PLUGIN_NOT_INSTALLED',
filename,
agentId
} as PluginError
}
if (type === 'skill') {
throw {
type: 'INVALID_FILE_TYPE',
extension: type
} as PluginError
}
const filePluginType = type as 'agent' | 'command'
const filePath = this.getClaudePluginPath(workdir, filePluginType, filename)
const newContentHash = await this.installer.updateFilePluginContent(agent.id, filePath, content)
const updatedMetadata: PluginMetadata = {
...installedPlugin.metadata,
contentHash: newContentHash,
size: Buffer.byteLength(content, 'utf8'),
updatedAt: Date.now(),
filename,
type: filePluginType
}
const updatedPlugin: InstalledPlugin = {
filename,
type: filePluginType,
metadata: updatedMetadata
}
await this.cacheStore.upsert(workdir, updatedPlugin)
this.upsertAgentPlugin(agent, updatedPlugin)
logger.info('Plugin content updated successfully', {
agentId,
filename,
type: filePluginType,
newContentHash
})
}
// ============================================================================
// Private Helper Methods
// ============================================================================
/**
* Resolve plugin type to directory name under .claude
*/
private getPluginDirectoryName(type: PluginType): 'agents' | 'commands' | 'skills' {
if (type === 'agent') {
return 'agents'
}
if (type === 'command') {
return 'commands'
}
return 'skills'
}
/**
* Get the base .claude directory for a workdir
*/
private getClaudeBasePath(workdir: string): string {
return path.join(workdir, '.claude')
}
/**
* Get the directory for a specific plugin type inside .claude
*/
private getClaudePluginDirectory(workdir: string, type: PluginType): string {
return path.join(this.getClaudeBasePath(workdir), this.getPluginDirectoryName(type))
}
/**
* Get the absolute path for a plugin file/folder inside .claude
*/
private getClaudePluginPath(workdir: string, type: PluginType, filename: string): string {
return path.join(this.getClaudePluginDirectory(workdir, type), filename)
}
/**
* Get absolute path to plugins directory (handles packaged vs dev)
*/
private getPluginsBasePath(): string {
// Use the utility function which handles both dev and production correctly
if (app.isPackaged) {
return path.join(process.resourcesPath, 'claude-code-plugins')
}
return path.join(__dirname, '../../node_modules/claude-code-plugins/plugins')
}
/**
* Validate source path to prevent path traversal attacks
*/
private async getAgentOrThrow(agentId: string): Promise<GetAgentResponse> {
const agent = await this.agentService.getAgent(agentId)
if (!agent) {
throw {
type: 'INVALID_WORKDIR',
agentId,
workdir: '',
message: 'Agent not found'
} as PluginError
}
return agent
}
private getWorkdirOrThrow(agent: GetAgentResponse, agentId: string): string {
const workdir = agent.accessible_paths?.[0]
if (!workdir) {
throw {
type: 'INVALID_WORKDIR',
agentId,
workdir: '',
message: 'Agent has no accessible paths'
} as PluginError
}
return workdir
}
/**
* Validate workdir against agent's accessible paths
*/
private async validateWorkdir(agent: GetAgentResponse, workdir: string): Promise<void> {
// Verify workdir is in agent's accessible_paths
if (!agent.accessible_paths?.includes(workdir)) {
throw {
type: 'INVALID_WORKDIR',
workdir,
agentId: agent.id,
message: 'Workdir not in agent accessible paths'
} as PluginError
}
// Verify workdir exists and is accessible
try {
await fs.promises.access(workdir, fs.constants.R_OK | fs.constants.W_OK)
} catch (error) {
throw {
type: 'WORKDIR_NOT_FOUND',
workdir,
message: 'Workdir does not exist or is not accessible'
} as PluginError
}
}
private upsertAgentPlugin(agent: GetAgentResponse, plugin: InstalledPlugin): void {
const existing = agent.installed_plugins ?? []
const filtered = existing.filter((p) => !(p.filename === plugin.filename && p.type === plugin.type))
agent.installed_plugins = [...filtered, plugin]
}
private removeAgentPlugin(agent: GetAgentResponse, filename: string, type: PluginType): void {
if (!agent.installed_plugins) {
agent.installed_plugins = []
return
}
agent.installed_plugins = agent.installed_plugins.filter((p) => !(p.filename === filename && p.type === type))
}
/**
* Sanitize filename to remove unsafe characters (for agents/commands)
*/
private sanitizeFilename(filename: string): string {
// Remove path separators
let sanitized = filename.replace(/[/\\]/g, '_')
// Remove null bytes using String method to avoid control-regex lint error
sanitized = sanitized.replace(new RegExp(String.fromCharCode(0), 'g'), '')
// Limit to safe characters (alphanumeric, dash, underscore, dot)
sanitized = sanitized.replace(/[^a-zA-Z0-9._-]/g, '_')
// Ensure .md extension
if (!sanitized.endsWith('.md') && !sanitized.endsWith('.markdown')) {
sanitized += '.md'
}
return sanitized
}
/**
* Sanitize folder name for skills (different rules than file names)
* NO dots allowed to avoid confusion with file extensions
*/
private sanitizeFolderName(folderName: string): string {
// Remove path separators
let sanitized = folderName.replace(/[/\\]/g, '_')
// Remove null bytes using String method to avoid control-regex lint error
sanitized = sanitized.replace(new RegExp(String.fromCharCode(0), 'g'), '')
// Limit to safe characters (alphanumeric, dash, underscore)
// NOTE: No dots allowed to avoid confusion with file extensions
sanitized = sanitized.replace(/[^a-zA-Z0-9_-]/g, '_')
// Validate no extension was provided
if (folderName.includes('.')) {
logger.warn('Skill folder name contained dots, sanitized', {
original: folderName,
sanitized
})
}
return sanitized
}
/**
* Ensure .claude subdirectory exists for the given plugin type
*/
private async ensureClaudeDirectory(workdir: string, type: PluginType): Promise<void> {
const typeDir = this.getClaudePluginDirectory(workdir, type)
try {
await fs.promises.mkdir(typeDir, { recursive: true })
logger.debug('Ensured directory exists', { typeDir })
} catch (error) {
logger.error('Failed to create directory', {
typeDir,
error: error instanceof Error ? error.message : String(error)
})
throw {
type: 'PERMISSION_DENIED',
path: typeDir
} as PluginError
}
}
}
export const pluginService = PluginService.getInstance()

View File

@@ -1,7 +1,5 @@
import path from 'node:path'
import { loggerService } from '@logger'
import { pluginService } from '@main/services/agents/plugins/PluginService'
import { getDataPath } from '@main/utils'
import type {
AgentEntity,
@@ -19,8 +17,6 @@ import { BaseService } from '../BaseService'
import { type AgentRow, agentsTable, type InsertAgentRow } from '../database/schema'
import type { AgentModelField } from '../errors'
const logger = loggerService.withContext('AgentService')
export class AgentService extends BaseService {
private static instance: AgentService | null = null
private readonly modelFields: AgentModelField[] = ['model', 'plan_model', 'small_model']
@@ -96,24 +92,6 @@ export class AgentService extends BaseService {
const agent = this.deserializeJsonFields(result[0]) as GetAgentResponse
agent.tools = await this.listMcpTools(agent.type, agent.mcps)
// Load installed_plugins from cache file instead of database
const workdir = agent.accessible_paths?.[0]
if (workdir) {
try {
agent.installed_plugins = await pluginService.listInstalledFromCache(workdir)
} catch (error) {
// Log error but don't fail the request
logger.warn(`Failed to load installed plugins for agent ${id}`, {
workdir,
error: error instanceof Error ? error.message : String(error)
})
agent.installed_plugins = []
}
} else {
agent.installed_plugins = []
}
return agent
}

View File

@@ -106,10 +106,7 @@ class ClaudeCodeService implements AgentServiceInterface {
ANTHROPIC_AUTH_TOKEN: modelInfo.provider.apiKey,
ANTHROPIC_BASE_URL: modelInfo.provider.anthropicApiHost?.trim() || modelInfo.provider.apiHost,
ANTHROPIC_MODEL: modelInfo.modelId,
ANTHROPIC_DEFAULT_OPUS_MODEL: modelInfo.modelId,
ANTHROPIC_DEFAULT_SONNET_MODEL: modelInfo.modelId,
// TODO: support set small model in UI
ANTHROPIC_DEFAULT_HAIKU_MODEL: modelInfo.modelId,
ANTHROPIC_SMALL_FAST_MODEL: modelInfo.modelId,
ELECTRON_RUN_AS_NODE: '1',
ELECTRON_NO_ATTACH_CONSOLE: '1'
}

View File

@@ -73,15 +73,6 @@ const emptyUsage: LanguageModelUsage = {
*/
const generateMessageId = (): string => `msg_${uuidv4().replace(/-/g, '')}`
/**
* Filters out command-* tags from text content to prevent internal command
* messages from appearing in the user-facing UI.
* Removes tags like <command-message>...</command-message> and <command-name>...</command-name>
*/
const filterCommandTags = (text: string): string => {
return text.replace(/<command-[^>]+>.*?<\/command-[^>]+>/gs, '').trim()
}
/**
* Extracts provider metadata from the raw Claude message so we can surface it
* on every emitted stream part for observability and debugging purposes.
@@ -279,17 +270,12 @@ function handleUserMessage(
const chunks: AgentStreamPart[] = []
const providerMetadata = sdkMessageToProviderMetadata(message)
const content = message.message.content
const isSynthetic = message.isSynthetic ?? false
if (typeof content === 'string') {
if (!content) {
return chunks
}
const filteredContent = filterCommandTags(content)
if (!filteredContent) {
return chunks
}
const id = message.uuid?.toString() || generateMessageId()
chunks.push({
type: 'text-start',
@@ -299,7 +285,7 @@ function handleUserMessage(
chunks.push({
type: 'text-delta',
id,
text: filteredContent,
text: content,
providerMetadata
})
chunks.push({
@@ -337,30 +323,24 @@ function handleUserMessage(
providerExecuted: true
})
}
} else if (block.type === 'text' && !isSynthetic) {
const rawText = (block as { text: string }).text
const filteredText = filterCommandTags(rawText)
// Only push text chunks if there's content after filtering
if (filteredText) {
const id = message.uuid?.toString() || generateMessageId()
chunks.push({
type: 'text-start',
id,
providerMetadata
})
chunks.push({
type: 'text-delta',
id,
text: filteredText,
providerMetadata
})
chunks.push({
type: 'text-end',
id,
providerMetadata
})
}
} else if (block.type === 'text') {
const id = message.uuid?.toString() || generateMessageId()
chunks.push({
type: 'text-start',
id,
providerMetadata
})
chunks.push({
type: 'text-delta',
id,
text: (block as { text: string }).text,
providerMetadata
})
chunks.push({
type: 'text-end',
id,
providerMetadata
})
} else {
logger.warn('Unhandled user content block', { type: (block as any).type })
}

View File

@@ -9,20 +9,13 @@ const logger = loggerService.withContext('URLSchema:handleMcpProtocolUrl')
function installMCPServer(server: MCPServer) {
const mainWindow = windowService.getMainWindow()
const now = Date.now()
const payload: MCPServer = {
...server,
id: server.id ?? nanoid(),
installSource: 'protocol',
isTrusted: false,
isActive: false,
trustedAt: undefined,
installedAt: server.installedAt ?? now
if (!server.id) {
server.id = nanoid()
}
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(IpcChannel.Mcp_AddServer, payload)
mainWindow.webContents.send(IpcChannel.Mcp_AddServer, server)
}
}

View File

@@ -237,6 +237,7 @@ const api = {
},
openPath: (path: string) => ipcRenderer.invoke(IpcChannel.Open_Path, path),
shortcuts: {
getAll: () => ipcRenderer.invoke(IpcChannel.Shortcuts_GetAll),
update: (shortcuts: Shortcut[]) => ipcRenderer.invoke(IpcChannel.Shortcuts_Update, shortcuts)
},
knowledgeBase: {
@@ -567,16 +568,7 @@ const api = {
getStatus: (): Promise<GetApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_GetStatus),
start: (): Promise<StartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Start),
restart: (): Promise<RestartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Restart),
stop: (): Promise<StopApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Stop),
onReady: (callback: () => void): (() => void) => {
const listener = () => {
callback()
}
ipcRenderer.on(IpcChannel.ApiServer_Ready, listener)
return () => {
ipcRenderer.removeListener(IpcChannel.ApiServer_Ready, listener)
}
}
stop: (): Promise<StopApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Stop)
},
claudeCodePlugin: {
listAvailable: (): Promise<PluginResult<ListAvailablePluginsResult>> =>
@@ -592,13 +584,6 @@ const api = {
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_ReadContent, sourcePath),
writeContent: (options: WritePluginContentOptions): Promise<PluginResult<void>> =>
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_WriteContent, options)
},
webSocket: {
start: () => ipcRenderer.invoke(IpcChannel.WebSocket_Start),
stop: () => ipcRenderer.invoke(IpcChannel.WebSocket_Stop),
status: () => ipcRenderer.invoke(IpcChannel.WebSocket_Status),
sendFile: (filePath: string) => ipcRenderer.invoke(IpcChannel.WebSocket_SendFile, filePath),
getAllCandidates: () => ipcRenderer.invoke(IpcChannel.WebSocket_GetAllCandidates)
}
}

View File

@@ -6,14 +6,7 @@
import { loggerService } from '@logger'
import { processKnowledgeReferences } from '@renderer/services/KnowledgeService'
import type {
BaseTool,
MCPCallToolResponse,
MCPTool,
MCPToolResponse,
MCPToolResultContent,
NormalToolResponse
} from '@renderer/types'
import type { BaseTool, MCPTool, MCPToolResponse, NormalToolResponse } from '@renderer/types'
import type { Chunk } from '@renderer/types/chunk'
import { ChunkType } from '@renderer/types/chunk'
import type { ToolSet, TypedToolCall, TypedToolError, TypedToolResult } from 'ai'
@@ -262,7 +255,6 @@ export class ToolCallChunkHandler {
type: 'tool-result'
} & TypedToolResult<ToolSet>
): void {
// TODO: 基于AI SDK为供应商内置工具做更好的展示和类型安全处理
const { toolCallId, output, input } = chunk
if (!toolCallId) {
@@ -308,7 +300,12 @@ export class ToolCallChunkHandler {
responses: [toolResponse]
})
const images = extractImagesFromToolOutput(toolResponse.response)
const images: string[] = []
for (const content of toolResponse.response?.content || []) {
if (content.type === 'image' && content.data) {
images.push(`data:${content.mimeType};base64,${content.data}`)
}
}
if (images.length) {
this.onChunk({
@@ -355,41 +352,3 @@ export class 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'
}

View File

@@ -14,7 +14,6 @@ import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
import type { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types'
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
import { SUPPORTED_IMAGE_ENDPOINT_LIST } from '@renderer/utils'
import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic'
import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai'
@@ -79,7 +78,7 @@ export default class ModernAiProvider {
return this.actualProvider
}
public async completions(modelId: string, params: StreamTextParams, providerConfig: ModernAiProviderConfig) {
public async completions(modelId: string, params: StreamTextParams, config: ModernAiProviderConfig) {
// 检查model是否存在
if (!this.model) {
throw new Error('Model is required for completions. Please use constructor with model parameter.')
@@ -87,10 +86,7 @@ export default class ModernAiProvider {
// 每次请求时重新生成配置以确保API key轮换生效
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)
@@ -101,13 +97,13 @@ export default class ModernAiProvider {
// 提前构建中间件
const middlewares = buildAiSdkMiddlewares({
...providerConfig,
...config,
provider: this.actualProvider,
assistant: providerConfig.assistant
assistant: config.assistant
})
logger.debug('Built middlewares in completions', {
middlewareCount: middlewares.length,
isImageGeneration: providerConfig.isImageGenerationEndpoint
isImageGeneration: config.isImageGenerationEndpoint
})
if (!this.localProvider) {
throw new Error('Local provider not created')
@@ -115,7 +111,7 @@ export default class ModernAiProvider {
// 根据endpoint类型创建对应的模型
let model: AiSdkModel | undefined
if (providerConfig.isImageGenerationEndpoint) {
if (config.isImageGenerationEndpoint) {
model = this.localProvider.imageModel(modelId)
} else {
model = this.localProvider.languageModel(modelId)
@@ -131,15 +127,15 @@ export default class ModernAiProvider {
params.messages = [...claudeCodeSystemMessage, ...(params.messages || [])]
}
if (providerConfig.topicId && (await preferenceService.get('app.developer_mode.enabled'))) {
if (config.topicId && (await preferenceService.get('app.developer_mode.enabled'))) {
// TypeScript类型窄化确保topicId是string类型
const traceConfig = {
...providerConfig,
topicId: providerConfig.topicId
...config,
topicId: config.topicId
}
return await this._completionsForTrace(model, params, traceConfig)
} else {
return await this._completionsOrImageGeneration(model, params, providerConfig)
return await this._completionsOrImageGeneration(model, params, config)
}
}

View File

@@ -1,4 +1,5 @@
import type { Provider } from '@renderer/types'
import { isOpenAIProvider } from '@renderer/utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AihubmixAPIClient } from '../aihubmix/AihubmixAPIClient'
@@ -201,4 +202,36 @@ describe('ApiClientFactory', () => {
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)
})
})
})

View File

@@ -10,12 +10,12 @@ import type { EndpointType, Model, Provider } from '@renderer/types'
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@renderer/config/models', () => ({
DEFAULT_MODEL_MAP: {
assistant: { id: 'gpt-4', name: 'GPT-4' },
quick: { id: 'gpt-4', name: 'GPT-4' },
translate: { id: 'gpt-4', name: 'GPT-4' }
},
SYSTEM_MODELS: {
defaultModel: [
{ id: 'gpt-4', name: 'GPT-4' },
{ id: 'gpt-4', name: 'GPT-4' },
{ id: 'gpt-4', name: 'GPT-4' }
],
zhipu: [],
silicon: [],
openai: [],

View File

@@ -1,5 +1,5 @@
import type { AiPlugin } from '@cherrystudio/ai-core'
import { createPromptToolUsePlugin, webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins'
import { createPromptToolUsePlugin, googleToolsPlugin, webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins'
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger'
import type { Assistant } from '@renderer/types'
@@ -68,9 +68,9 @@ export async function buildPlugins(
)
}
// if (middlewareConfig.enableUrlContext && middlewareConfig.) {
// plugins.push(googleToolsPlugin({ urlContext: true }))
// }
if (middlewareConfig.enableUrlContext) {
plugins.push(googleToolsPlugin({ urlContext: true }))
}
logger.debug(
'Final plugin list:',

View File

@@ -114,7 +114,7 @@ export async function handleGeminiFileUpload(file: FileMetadata, model: Model):
}
/**
* 处理OpenAI兼容大文件上传
* 处理OpenAI大文件上传
*/
export async function handleOpenAILargeFileUpload(
file: FileMetadata,

View File

@@ -3,8 +3,6 @@
* 构建AI SDK的流式和非流式参数
*/
import { anthropic } from '@ai-sdk/anthropic'
import { google } from '@ai-sdk/google'
import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic/edge'
import { vertex } from '@ai-sdk/google-vertex/edge'
import type { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
@@ -99,6 +97,10 @@ export async function buildStreamTextParams(
let tools = setupToolsConfig(mcpTools)
// if (webSearchProviderId) {
// tools['builtin_web_search'] = webSearchTool(webSearchProviderId)
// }
// 构建真正的 providerOptions
const webSearchConfig: CherryWebSearchConfig = {
maxResults: store.getState().websearch.maxResults,
@@ -141,34 +143,12 @@ export async function buildStreamTextParams(
}
}
if (enableUrlContext) {
// google-vertex
if (enableUrlContext && aiSdkProviderId === 'google-vertex') {
if (!tools) {
tools = {}
}
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
}
tools.url_context = vertex.tools.urlContext({}) as ProviderDefinedTool
}
// 构建基础参数

View File

@@ -32,8 +32,7 @@ const AIHUBMIX_RULES: RuleSet = {
match: (model) =>
(startsWith('gemini')(model) || startsWith('imagen')(model)) &&
!model.id.endsWith('-nothink') &&
!model.id.endsWith('-search') &&
!model.id.includes('embedding'),
!model.id.endsWith('-search'),
provider: (provider: Provider) => {
return extraProviderConfig({
...provider,

View File

@@ -7,27 +7,24 @@ import {
} from '@cherrystudio/ai-core/provider'
import { cacheService } from '@data/CacheService'
import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
import {
isAnthropicProvider,
isAzureOpenAIProvider,
isGeminiProvider,
isNewApiProvider
} from '@renderer/config/providers'
import { isNewApiProvider } from '@renderer/config/providers'
import {
getAwsBedrockAccessKeyId,
getAwsBedrockRegion,
getAwsBedrockSecretAccessKey
} from '@renderer/hooks/useAwsBedrock'
import { createVertexProvider, isVertexAIConfigured, isVertexProvider } from '@renderer/hooks/useVertexAI'
import { createVertexProvider, isVertexAIConfigured } from '@renderer/hooks/useVertexAI'
import { getProviderByModel } from '@renderer/services/AssistantService'
import { loggerService } from '@renderer/services/LoggerService'
import store from '@renderer/store'
import { isSystemProvider, type Model, type Provider, SystemProviderIds } from '@renderer/types'
import { formatApiHost, formatAzureOpenAIApiHost, formatVertexApiHost, routeToEndpoint } from '@renderer/utils/api'
import { cloneDeep } from 'lodash'
import { isSystemProvider, type Model, type Provider } from '@renderer/types'
import { formatApiHost } from '@renderer/utils/api'
import { cloneDeep, trim } from 'lodash'
import { aihubmixProviderCreator, newApiResolverCreator, vertexAnthropicProviderCreator } from './config'
import { COPILOT_DEFAULT_HEADERS } from './constants'
import { getAiSdkProviderId } from './factory'
const logger = loggerService.withContext('ProviderConfigProcessor')
/**
* 获取轮询的API key
@@ -59,6 +56,13 @@ function getRotatedApiKey(provider: Provider): string {
* 处理特殊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)) {
return newApiResolverCreator(model, provider)
}
@@ -75,30 +79,43 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
}
/**
* 主要用来对齐AISdk的BaseURL格式
* @param provider
* @returns
* 格式化provider的API Host
*/
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 {
const formatted = { ...provider }
if (formatted.anthropicApiHost) {
formatted.anthropicApiHost = formatApiHost(formatted.anthropicApiHost)
formatted.anthropicApiHost = formatAnthropicApiHost(formatted.anthropicApiHost)
}
if (isAnthropicProvider(provider)) {
if (formatted.type === 'anthropic') {
const baseHost = formatted.anthropicApiHost || formatted.apiHost
formatted.apiHost = formatApiHost(baseHost)
formatted.apiHost = formatAnthropicApiHost(baseHost)
if (!formatted.anthropicApiHost) {
formatted.anthropicApiHost = formatted.apiHost
}
} else if (formatted.id === SystemProviderIds.copilot || formatted.id === SystemProviderIds.github) {
formatted.apiHost = formatApiHost(formatted.apiHost, false)
} else if (isGeminiProvider(formatted)) {
formatted.apiHost = formatApiHost(formatted.apiHost, true, 'v1beta')
} else if (isAzureOpenAIProvider(formatted)) {
formatted.apiHost = formatAzureOpenAIApiHost(formatted.apiHost)
} else if (isVertexProvider(formatted)) {
formatted.apiHost = formatVertexApiHost(formatted)
} else if (formatted.id === 'copilot') {
const trimmed = trim(formatted.apiHost)
formatted.apiHost = trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed
} else if (formatted.type === 'gemini') {
formatted.apiHost = formatApiHost(formatted.apiHost, 'v1beta')
} else {
formatted.apiHost = formatApiHost(formatted.apiHost)
}
@@ -132,15 +149,15 @@ export function providerToAiSdkConfig(
options: ProviderSettingsMap[keyof ProviderSettingsMap]
} {
const aiSdkProviderId = getAiSdkProviderId(actualProvider)
logger.debug('providerToAiSdkConfig', { aiSdkProviderId })
// 构建基础配置
const { baseURL, endpoint } = routeToEndpoint(actualProvider.apiHost)
const baseConfig = {
baseURL: baseURL,
baseURL: trim(actualProvider.apiHost),
apiKey: getRotatedApiKey(actualProvider)
}
const isCopilotProvider = actualProvider.id === SystemProviderIds.copilot
const isCopilotProvider = actualProvider.id === 'copilot'
if (isCopilotProvider) {
const storedHeaders = store.getState().copilot.defaultHeaders ?? {}
const options = ProviderConfigFactory.fromProvider('github-copilot-openai-compatible', baseConfig, {
@@ -161,7 +178,6 @@ export function providerToAiSdkConfig(
// 处理OpenAI模式
const extraOptions: any = {}
extraOptions.endpoint = endpoint
if (actualProvider.type === 'openai-response' && !isOpenAIChatCompletionOnlyModel(model)) {
extraOptions.mode = 'responses'
} else if (aiSdkProviderId === 'openai') {
@@ -183,11 +199,13 @@ export function providerToAiSdkConfig(
}
// azure
if (aiSdkProviderId === 'azure' || actualProvider.type === 'azure-openai') {
// extraOptions.apiVersion = actualProvider.apiVersion 默认使用v1不使用azure endpoint
extraOptions.apiVersion = actualProvider.apiVersion
baseConfig.baseURL += '/openai'
if (actualProvider.apiVersion === 'preview') {
extraOptions.mode = 'responses'
} else {
extraOptions.mode = 'chat'
extraOptions.useDeploymentBasedUrls = true
}
}
@@ -209,7 +227,22 @@ export function providerToAiSdkConfig(
...googleCredentials,
privateKey: formatPrivateKey(googleCredentials.privateKey)
}
baseConfig.baseURL += aiSdkProviderId === 'google-vertex' ? '/publishers/google' : '/publishers/anthropic/models'
// extraOptions.headers = window.api.vertexAI.getAuthHeaders({
// 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') {

View File

@@ -1,6 +1,7 @@
@import './color.css';
@import './font.css';
@import './markdown.css';
@import './ant.css';
@import './scrollbar.css';
@import './container.css';
@import './animation.css';

View File

@@ -1,12 +1,10 @@
@import 'tailwindcss';
@import 'tailwindcss' source('../../../../renderer');
@import 'tw-animate-css';
@import '../../../../../packages/ui/src/styles/theme.css';
/* TODO heroui 迁移完成后即可删除 */
/* heroui */
/* @plugin '../../hero.ts'; */
@source '../../../../../packages/ui/src/components/**/*.{js,ts,jsx,tsx}';
@plugin '../../hero.ts';
@source '../../../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
@source '../../../../../packages/ui/src/**/*.{js,ts,jsx,tsx}';
@custom-variant dark (&:is(.dark *));
@@ -23,24 +21,117 @@
4. Put the new custom utility class into the utilities layer.
*/
/* 应用特定的原始变量 */
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
--icon: #00000099;
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
--icon: #ffffff99;
}
/* 应用特定的变量和动画(不与 UI 库冲突) */
@theme inline {
/* Icon 颜色 - 应用特定变量 */
--color-icon: var(--icon);
/* 跑马灯动画 - 应用特定 */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--animate-marquee: marquee var(--duration) infinite linear;
--animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;
--color-icon: var(--icon);
@keyframes marquee {
from {
transform: translateX(0);

View File

@@ -5,16 +5,6 @@ import { describe, expect, it, vi } from 'vitest'
import { DraggableList } from '../'
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
// mock @hello-pangea/dnd 组件
vi.mock('@hello-pangea/dnd', () => {
return {

View File

@@ -3,16 +3,6 @@ import { describe, expect, it, vi } from 'vitest'
import { DraggableVirtualList } from '../'
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
// Mock 依赖项
vi.mock('@hello-pangea/dnd', () => ({
__esModule: true,

View File

@@ -20,7 +20,6 @@ export interface EditableNumberProps {
suffix?: string
prefix?: string
align?: 'start' | 'center' | 'end'
formatter?: (value: number | null) => string | number
}
const EditableNumber: FC<EditableNumberProps> = ({
@@ -37,8 +36,7 @@ const EditableNumber: FC<EditableNumberProps> = ({
style,
className,
size = 'middle',
align = 'end',
formatter
align = 'end'
}) => {
const [isEditing, setIsEditing] = useState(false)
const [inputValue, setInputValue] = useState(value)
@@ -92,7 +90,7 @@ const EditableNumber: FC<EditableNumberProps> = ({
changeOnBlur={changeOnBlur}
/>
<DisplayText style={style} className={className} $align={align} $isEditing={isEditing}>
{formatter ? formatter(value ?? null) : (value ?? placeholder)}
{value ?? placeholder}
</DisplayText>
</Container>
)

View File

@@ -1,4 +1,4 @@
import { Button } from '@cherrystudio/ui'
import { Button } from '@heroui/button'
import { formatErrorMessage } from '@renderer/utils/error'
import { Alert, Space } from 'antd'
import type { ComponentType, ReactNode } from 'react'

View File

@@ -1,119 +0,0 @@
import { cn } from '@heroui/react'
import { TopView } from '@renderer/components/TopView'
import { Modal } from 'antd'
import { Bot, MessageSquare } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
type OptionType = 'assistant' | 'agent'
interface ShowParams {
onSelect: (type: OptionType) => void
}
interface Props extends ShowParams {
resolve: (data: { type?: OptionType }) => void
}
const PopupContainer: React.FC<Props> = ({ onSelect, resolve }) => {
const { t } = useTranslation()
const [open, setOpen] = useState(true)
const [hoveredOption, setHoveredOption] = useState<OptionType | null>(null)
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
const handleSelect = (type: OptionType) => {
setOpen(false)
onSelect(type)
resolve({ type })
}
AddAssistantOrAgentPopup.hide = onCancel
return (
<Modal
title={t('chat.add.option.title')}
open={open}
onCancel={onCancel}
afterClose={onClose}
transitionName="animation-move-down"
centered
footer={null}
width={560}>
<div className="grid grid-cols-2 gap-4 py-4">
{/* Assistant Option */}
<button
type="button"
onClick={() => handleSelect('assistant')}
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
onMouseEnter={() => setHoveredOption('assistant')}
onMouseLeave={() => setHoveredOption(null)}>
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
<MessageSquare
size={24}
className={cn(
'transition-colors',
hoveredOption === 'assistant' ? 'text-[var(--color-primary)]' : 'text-[var(--color-icon-white)]'
)}
/>
</div>
<div className="text-center">
<h3 className="mb-1 font-semibold text-[var(--color-text-1)] text-base">{t('chat.add.assistant.title')}</h3>
<p className="text-[var(--color-text-2)] text-sm">{t('chat.add.assistant.description')}</p>
</div>
</button>
{/* Agent Option */}
<button
onClick={() => handleSelect('agent')}
type="button"
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
onMouseEnter={() => setHoveredOption('agent')}
onMouseLeave={() => setHoveredOption(null)}>
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
<Bot
size={24}
className={cn(
'transition-colors',
hoveredOption === 'agent' ? 'text-[var(--color-primary)]' : 'text-[var(--color-icon-white)]'
)}
/>
</div>
<div className="text-center">
<h3 className="mb-1 font-semibold text-[var(--color-text-1)] text-base">{t('agent.add.title')}</h3>
<p className="text-[var(--color-text-2)] text-sm">{t('agent.add.description')}</p>
</div>
</button>
</div>
</Modal>
)
}
const TopViewKey = 'AddAssistantOrAgentPopup'
export default class AddAssistantOrAgentPopup {
static topviewId = 0
static hide() {
TopView.hide(TopViewKey)
}
static show(props: ShowParams) {
return new Promise<{ type?: OptionType }>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
})
}
}

View File

@@ -1,591 +0,0 @@
import { Button } from '@heroui/button'
import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@heroui/modal'
import { Progress } from '@heroui/progress'
import { Spinner } from '@heroui/spinner'
import { loggerService } from '@logger'
import { SettingHelpText, SettingRow } from '@renderer/pages/settings'
import { QRCodeSVG } from 'qrcode.react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { TopView } from '../TopView'
const logger = loggerService.withContext('ExportToPhoneLanPopup')
interface Props {
resolve: (data: any) => void
}
type ConnectionPhase = 'initializing' | 'waiting_qr_scan' | 'connecting' | 'connected' | 'disconnected' | 'error'
type TransferPhase = 'idle' | 'preparing' | 'sending' | 'completed' | 'error'
const LoadingQRCode: React.FC = () => {
const { t } = useTranslation()
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
<Spinner />
<span style={{ fontSize: '14px', color: 'var(--color-text-2)' }}>
{t('settings.data.export_to_phone.lan.generating_qr')}
</span>
</div>
)
}
const ScanQRCode: React.FC<{ qrCodeValue: string }> = ({ qrCodeValue }) => {
const { t } = useTranslation()
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
<QRCodeSVG
marginSize={2}
value={qrCodeValue}
level="Q"
size={160}
imageSettings={{
src: '/src/assets/images/logo.png',
width: 40,
height: 40,
excavate: true
}}
/>
<span style={{ fontSize: '12px', color: 'var(--color-text-2)' }}>
{t('settings.data.export_to_phone.lan.scan_qr')}
</span>
</div>
)
}
const ConnectingAnimation: React.FC = () => {
const { t } = useTranslation()
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
<div
style={{
width: '160px',
height: '160px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
border: '2px dashed var(--color-status-warning)',
borderRadius: '12px',
backgroundColor: 'var(--color-status-warning)'
}}>
<Spinner size="lg" color="warning" />
<span style={{ fontSize: '14px', color: 'var(--color-text)', marginTop: '12px' }}>
{t('settings.data.export_to_phone.lan.status.connecting')}
</span>
</div>
</div>
)
}
const ConnectedDisplay: React.FC = () => {
const { t } = useTranslation()
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
<div
style={{
width: '160px',
height: '160px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
border: '2px dashed var(--color-status-success)',
borderRadius: '12px',
backgroundColor: 'var(--color-status-success)'
}}>
<span style={{ fontSize: '48px' }}>📱</span>
<span style={{ fontSize: '14px', color: 'var(--color-text)', marginTop: '8px' }}>
{t('settings.data.export_to_phone.lan.connected')}
</span>
</div>
</div>
)
}
const ErrorQRCode: React.FC<{ error: string | null }> = ({ error }) => {
const { t } = useTranslation()
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '12px',
padding: '20px',
border: `1px solid var(--color-error)`,
borderRadius: '8px',
backgroundColor: 'var(--color-error)'
}}>
<span style={{ fontSize: '48px' }}></span>
<span style={{ fontSize: '14px', color: 'var(--color-text)' }}>
{t('settings.data.export_to_phone.lan.connection_failed')}
</span>
{error && <span style={{ fontSize: '12px', color: 'var(--color-text-2)' }}>{error}</span>}
</div>
)
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [isOpen, setIsOpen] = useState(true)
const [connectionPhase, setConnectionPhase] = useState<ConnectionPhase>('initializing')
const [transferPhase, setTransferPhase] = useState<TransferPhase>('idle')
const [qrCodeValue, setQrCodeValue] = useState('')
const [selectedFolderPath, setSelectedFolderPath] = useState<string | null>(null)
const [sendProgress, setSendProgress] = useState(0)
const [error, setError] = useState<string | null>(null)
const [showCloseConfirm, setShowCloseConfirm] = useState(false)
const [autoCloseCountdown, setAutoCloseCountdown] = useState<number | null>(null)
const { t } = useTranslation()
// 派生状态
const isConnected = connectionPhase === 'connected'
const canSend = isConnected && selectedFolderPath && transferPhase === 'idle'
const isSending = transferPhase === 'preparing' || transferPhase === 'sending'
// 状态文本映射
const connectionStatusText = useMemo(() => {
const statusMap = {
initializing: t('settings.data.export_to_phone.lan.status.initializing'),
waiting_qr_scan: t('settings.data.export_to_phone.lan.status.waiting_qr_scan'),
connecting: t('settings.data.export_to_phone.lan.status.connecting'),
connected: t('settings.data.export_to_phone.lan.status.connected'),
disconnected: t('settings.data.export_to_phone.lan.status.disconnected'),
error: t('settings.data.export_to_phone.lan.status.error')
}
return statusMap[connectionPhase]
}, [connectionPhase, t])
const transferStatusText = useMemo(() => {
const statusMap = {
idle: '',
preparing: t('settings.data.export_to_phone.lan.status.preparing'),
sending: t('settings.data.export_to_phone.lan.status.sending'),
completed: t('settings.data.export_to_phone.lan.status.completed'),
error: t('settings.data.export_to_phone.lan.status.error')
}
return statusMap[transferPhase]
}, [transferPhase, t])
// 状态样式映射
const connectionStatusStyles = useMemo(() => {
const styleMap = {
initializing: {
bg: 'var(--color-background-mute)',
border: 'var(--color-border-mute)'
},
waiting_qr_scan: {
bg: 'var(--color-primary-mute)',
border: 'var(--color-primary-soft)'
},
connecting: { bg: 'var(--color-status-warning)', border: 'var(--color-status-warning)' },
connected: {
bg: 'var(--color-status-success)',
border: 'var(--color-status-success)'
},
disconnected: { bg: 'var(--color-error)', border: 'var(--color-error)' },
error: { bg: 'var(--color-error)', border: 'var(--color-error)' }
}
return styleMap[connectionPhase]
}, [connectionPhase])
const initWebSocket = useCallback(async () => {
try {
setConnectionPhase('initializing')
await window.api.webSocket.start()
const { port, ip } = await window.api.webSocket.status()
if (ip && port) {
const candidates = await window.api.webSocket.getAllCandidates()
const connectionInfo = {
type: 'cherry-studio-app',
candidates,
selectedHost: ip,
port,
timestamp: Date.now()
}
setQrCodeValue(JSON.stringify(connectionInfo))
setConnectionPhase('waiting_qr_scan')
logger.info(`QR code generated: ${ip}:${port} with ${candidates.length} IP candidates`)
} else {
setError(t('settings.data.export_to_phone.lan.error.no_ip'))
setConnectionPhase('error')
}
} catch (error) {
setError(
`${t('settings.data.export_to_phone.lan.error.init_failed')}: ${error instanceof Error ? error.message : ''}`
)
setConnectionPhase('error')
logger.error('Failed to initialize WebSocket:', error as Error)
}
}, [t])
const handleClientConnected = useCallback((_event: any, data: { connected: boolean }) => {
logger.info(`Client connection status: ${data.connected ? 'connected' : 'disconnected'}`)
if (data.connected) {
setConnectionPhase('connected')
setError(null)
} else {
setConnectionPhase('disconnected')
}
}, [])
const handleMessageReceived = useCallback((_event: any, data: any) => {
logger.info(`Received message from mobile: ${JSON.stringify(data)}`)
}, [])
const handleSendProgress = useCallback(
(_event: any, data: { progress: number }) => {
const progress = data.progress
setSendProgress(progress)
if (transferPhase === 'preparing' && progress > 0) {
setTransferPhase('sending')
}
if (progress >= 100) {
setTransferPhase('completed')
// 启动 3 秒倒计时自动关闭
setAutoCloseCountdown(3)
}
},
[transferPhase]
)
const handleSelectZip = useCallback(async () => {
const result = await window.api.file.select()
if (result) {
setSelectedFolderPath(result[0].path)
}
}, [])
const handleSendZip = useCallback(async () => {
if (!selectedFolderPath) {
setError(t('settings.data.export_to_phone.lan.error.no_file'))
return
}
setTransferPhase('preparing')
setError(null)
setSendProgress(0)
try {
logger.info(`Starting file transfer: ${selectedFolderPath}`)
await window.api.webSocket.sendFile(selectedFolderPath)
} catch (error) {
setError(
`${t('settings.data.export_to_phone.lan.error.send_failed')}: ${error instanceof Error ? error.message : ''}`
)
setTransferPhase('error')
logger.error('Failed to send file:', error as Error)
}
}, [selectedFolderPath, t])
// 尝试关闭弹窗 - 如果正在传输则显示确认
const handleCancel = useCallback(() => {
if (isSending) {
setShowCloseConfirm(true)
} else {
setIsOpen(false)
}
}, [isSending])
// 确认强制关闭
const handleForceClose = useCallback(() => {
logger.info('Force closing popup during transfer')
setIsOpen(false)
}, [])
// 取消关闭确认
const handleCancelClose = useCallback(() => {
setShowCloseConfirm(false)
}, [])
// 清理并关闭
const handleClose = useCallback(async () => {
try {
// 主动断开 WebSocket 连接
if (isConnected || connectionPhase !== 'disconnected') {
logger.info('Closing popup, stopping WebSocket')
await window.api.webSocket.stop()
}
} catch (error) {
logger.error('Failed to stop WebSocket on close:', error as Error)
}
resolve({})
}, [resolve, isConnected, connectionPhase])
useEffect(() => {
initWebSocket()
const removeClientConnectedListener = window.electron.ipcRenderer.on(
'websocket-client-connected',
handleClientConnected
)
const removeMessageReceivedListener = window.electron.ipcRenderer.on(
'websocket-message-received',
handleMessageReceived
)
const removeSendProgressListener = window.electron.ipcRenderer.on('file-send-progress', handleSendProgress)
return () => {
removeClientConnectedListener()
removeMessageReceivedListener()
removeSendProgressListener()
window.api.webSocket.stop()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// 自动关闭倒计时
useEffect(() => {
if (autoCloseCountdown === null) return
if (autoCloseCountdown <= 0) {
logger.debug('Auto-closing popup after transfer completion')
setIsOpen(false)
return
}
const timer = setTimeout(() => {
setAutoCloseCountdown(autoCloseCountdown - 1)
}, 1000)
return () => clearTimeout(timer)
}, [autoCloseCountdown])
// 状态指示器组件
const StatusIndicator = useCallback(
() => (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 12px',
borderRadius: '8px',
backgroundColor: connectionStatusStyles.bg,
border: `1px solid ${connectionStatusStyles.border}`
}}>
<span style={{ fontSize: '14px', fontWeight: '500', color: 'var(--color-text)' }}>{connectionStatusText}</span>
</div>
),
[connectionStatusStyles, connectionStatusText]
)
// 二维码显示组件 - 使用显式条件渲染以避免类型不匹配
const QRCodeDisplay = useCallback(() => {
switch (connectionPhase) {
case 'waiting_qr_scan':
case 'disconnected':
return <ScanQRCode qrCodeValue={qrCodeValue} />
case 'initializing':
return <LoadingQRCode />
case 'connecting':
return <ConnectingAnimation />
case 'connected':
return <ConnectedDisplay />
case 'error':
return <ErrorQRCode error={error} />
default:
return null
}
}, [connectionPhase, qrCodeValue, error])
// 传输进度组件
const TransferProgress = useCallback(() => {
if (!isSending && transferPhase !== 'completed') return null
return (
<div style={{ paddingTop: '8px' }}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
padding: '12px',
border: `1px solid var(--color-border)`,
borderRadius: '8px',
backgroundColor: 'var(--color-background-mute)'
}}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
fontSize: '14px',
fontWeight: '500'
}}>
<span style={{ color: 'var(--color-text)' }}>
{t('settings.data.export_to_phone.lan.transfer_progress')}
</span>
<span
style={{ color: transferPhase === 'completed' ? 'var(--color-status-success)' : 'var(--color-primary)' }}>
{transferPhase === 'completed' ? '✅ ' + t('common.completed') : `${Math.round(sendProgress)}%`}
</span>
</div>
<Progress
value={Math.round(sendProgress)}
size="md"
color={transferPhase === 'completed' ? 'success' : 'primary'}
showValueLabel={false}
aria-label="Send progress"
/>
</div>
</div>
)
}, [isSending, transferPhase, sendProgress, t])
const AutoCloseCountdown = useCallback(() => {
if (transferPhase !== 'completed' || autoCloseCountdown === null || autoCloseCountdown <= 0) return null
return (
<div
style={{
fontSize: '12px',
color: 'var(--color-text-2)',
textAlign: 'center',
paddingTop: '4px'
}}>
{t('settings.data.export_to_phone.lan.auto_close_tip', { seconds: autoCloseCountdown })}
</div>
)
}, [transferPhase, autoCloseCountdown, t])
// 错误显示组件
const ErrorDisplay = useCallback(() => {
if (!error || transferPhase !== 'error') return null
return (
<div
style={{
padding: '12px',
border: `1px solid var(--color-error)`,
borderRadius: '8px',
backgroundColor: 'var(--color-error)',
textAlign: 'center'
}}>
<span style={{ fontSize: '14px', color: 'var(--color-text)' }}> {error}</span>
</div>
)
}, [error, transferPhase])
return (
<Modal
isOpen={isOpen}
onOpenChange={(open) => {
if (!open) {
handleCancel()
}
}}
isDismissable={false}
isKeyboardDismissDisabled={false}
placement="center"
onClose={handleClose}>
<ModalContent>
{() => (
<>
<ModalHeader>{t('settings.data.export_to_phone.lan.title')}</ModalHeader>
<ModalBody>
<SettingRow>
<StatusIndicator />
</SettingRow>
<SettingRow>
<div>{t('settings.data.export_to_phone.lan.content')}</div>
</SettingRow>
<SettingRow style={{ display: 'flex', justifyContent: 'center', minHeight: '180px' }}>
<QRCodeDisplay />
</SettingRow>
<SettingRow style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: 10, justifyContent: 'center', width: '100%' }}>
<Button color="default" variant="flat" onPress={handleSelectZip} isDisabled={isSending}>
{t('settings.data.export_to_phone.lan.selectZip')}
</Button>
<Button color="primary" onPress={handleSendZip} isDisabled={!canSend} isLoading={isSending}>
{transferStatusText || t('settings.data.export_to_phone.lan.sendZip')}
</Button>
</div>
</SettingRow>
<SettingHelpText
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
textAlign: 'center'
}}>
{selectedFolderPath || t('settings.data.export_to_phone.lan.noZipSelected')}
</SettingHelpText>
<TransferProgress />
<AutoCloseCountdown />
<ErrorDisplay />
</ModalBody>
{showCloseConfirm && (
<ModalFooter>
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
gap: '12px',
padding: '8px',
borderRadius: '8px',
backgroundColor: 'var(--color-status-warning)',
border: '1px solid var(--color-status-warning)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '20px' }}></span>
<span style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: '500' }}>
{t('settings.data.export_to_phone.lan.confirm_close_title')}
</span>
</div>
<span style={{ fontSize: '13px', color: 'var(--color-text-2)', marginLeft: '28px' }}>
{t('settings.data.export_to_phone.lan.confirm_close_message')}
</span>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '4px' }}>
<Button size="sm" color="default" variant="flat" onPress={handleCancelClose}>
{t('common.cancel')}
</Button>
<Button size="sm" color="danger" onPress={handleForceClose}>
{t('settings.data.export_to_phone.lan.force_close')}
</Button>
</div>
</div>
</ModalFooter>
)}
</>
)}
</ModalContent>
</Modal>
)
}
const TopViewKey = 'ExportToPhoneLanPopup'
export default class ExportToPhoneLanPopup {
static topviewId = 0
static hide() {
TopView.hide(TopViewKey)
}
static show() {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
})
}
}

View File

@@ -16,8 +16,8 @@ import {
import { loggerService } from '@logger'
import type { Selection } from '@react-types/shared'
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
import { permissionModeCards } from '@renderer/config/agent'
import { agentModelFilter, getModelLogoById } from '@renderer/config/models'
import { permissionModeCards } from '@renderer/constants/permissionModes'
import { useAgents } from '@renderer/hooks/agents/useAgents'
import { useApiModels } from '@renderer/hooks/agents/useModels'
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
@@ -68,7 +68,6 @@ type Props = {
agent?: AgentWithTools
isOpen: boolean
onClose: () => void
afterSubmit?: (a: AgentEntity) => void
}
/**
@@ -80,7 +79,7 @@ type Props = {
* @param onClose - Optional callback when modal closes. From useDisclosure.
* @returns Modal component for agent creation/editing
*/
export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _onClose, afterSubmit }) => {
export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _onClose }) => {
const { isOpen, onClose } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
const { t } = useTranslation()
const loadingRef = useRef(false)
@@ -303,13 +302,8 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
configuration: form.configuration ? { ...form.configuration } : undefined
} satisfies UpdateAgentForm
const result = await updateAgent(updatePayload)
if (result) {
logger.debug('Updated agent', result)
afterSubmit?.(result)
} else {
logger.error('Update failed.')
}
updateAgent(updatePayload)
logger.debug('Updated agent', updatePayload)
} else {
const newAgent = {
type: form.type,
@@ -322,13 +316,12 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
configuration: form.configuration ? { ...form.configuration } : undefined
} satisfies AddAgentForm
const result = await addAgent(newAgent)
if (!result.success) {
loadingRef.current = false
throw result.error
}
afterSubmit?.(result.data)
}
loadingRef.current = false
// setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
@@ -337,17 +330,16 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
[
form.type,
form.model,
form.accessible_paths,
form.name,
form.description,
form.instructions,
form.accessible_paths,
form.allowed_tools,
form.configuration,
agent,
onClose,
t,
updateAgent,
afterSubmit,
addAgent
]
)

View File

@@ -1,4 +1,4 @@
import { makeSvgSizeAdaptive } from '@renderer/utils/image'
import { makeSvgSizeAdaptive } from '@renderer/utils'
import DOMPurify from 'dompurify'
/**

View File

@@ -18,7 +18,6 @@ interface BaseSelectorProps<V = string | number> {
options: SelectorOption<V>[]
placeholder?: string
placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom'
style?: React.CSSProperties
/** 字体大小 */
size?: number
/** 是否禁用 */
@@ -46,7 +45,6 @@ const Selector = <V extends string | number>({
placement = 'bottomRight',
size = 13,
placeholder,
style,
disabled = false,
multiple = false
}: SelectorProps<V>) => {
@@ -139,7 +137,7 @@ const Selector = <V extends string | number>({
placement={placement}
open={open && !disabled}
onOpenChange={handleOpenChange}>
<Label style={style} $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
<Label $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
{label}
<LabelIcon size={size + 3} />
</Label>

View File

@@ -23,16 +23,6 @@ const mocks = vi.hoisted(() => ({
}
}))
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
// Mock antd components to prevent flaky snapshot tests
vi.mock('antd', () => {
const MockSpaceCompact: React.FC<React.PropsWithChildren<{ style?: React.CSSProperties }>> = ({

View File

@@ -18,15 +18,6 @@ describe('Qwen Model Detection', () => {
vi.mock('@renderer/services/AssistantService', () => ({
getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryai' })
}))
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({
llm: {
settings: {}
}
})
}
}))
})
test('isQwenReasoningModel', () => {
expect(isQwenReasoningModel({ id: 'qwen3-thinking' } as Model)).toBe(true)

View File

@@ -2,16 +2,6 @@ import { describe, expect, it, vi } from 'vitest'
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
vi.mock('@renderer/services/AssistantService.ts', () => ({
getDefaultAssistant: () => {

View File

@@ -1,6 +1,5 @@
import ClaudeAvatar from '@renderer/assets/images/models/claude.png'
import type { AgentBase, AgentType } from '@renderer/types'
import type { PermissionModeCard } from '@renderer/types/agent'
// base agent config. no default config for now.
const DEFAULT_AGENT_CONFIG: Omit<AgentBase, 'model'> = {
@@ -20,47 +19,3 @@ export const getAgentTypeAvatar = (type: AgentType): string => {
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
}
]

View File

@@ -14,13 +14,17 @@ export const qwen38bModel: Model = {
group: 'Qwen'
}
export const DEFAULT_MODEL_MAP = {
assistant: glm45FlashModel,
quick: qwen38bModel,
translate: glm45FlashModel
} as const
export const SYSTEM_MODELS: Record<SystemProviderId, Model[]> = {
export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> = {
defaultModel: [
// Default assistant model
glm45FlashModel,
// Default topic naming model
qwen38bModel,
// Default translation model
glm45FlashModel,
// Default quick assistant model
glm45FlashModel
],
cherryin: [],
vertexai: [],
'302ai': [
@@ -1737,7 +1741,6 @@ export const SYSTEM_MODELS: Record<SystemProviderId, Model[]> = {
id: 'DeepSeek-R1',
provider: 'cephalon',
name: 'DeepSeek-R1满血版',
capabilities: [{ type: 'reasoning' }],
group: 'DeepSeek'
}
],

View File

@@ -1,9 +1,7 @@
import { getProviderByModel } from '@renderer/services/AssistantService'
import type { Model } from '@renderer/types'
import { SystemProviderIds } from '@renderer/types'
import { getLowerBaseModelName, isUserSelectedModelType } from '@renderer/utils'
import { isGeminiProvider, isNewApiProvider, isOpenAICompatibleProvider, isOpenAIProvider } from '../providers'
import { isEmbeddingModel, isRerankModel } from './embedding'
import { isAnthropicModel } from './utils'
import { isPureGenerateImageModel, isTextToImageModel } from './vision'
@@ -67,16 +65,12 @@ export function isWebSearchModel(model: Model): boolean {
const modelId = getLowerBaseModelName(model.id, '/')
// bedrock和vertex不支持
if (
isAnthropicModel(model) &&
(provider.id === SystemProviderIds['aws-bedrock'] || provider.id === SystemProviderIds.vertexai)
) {
// 不管哪个供应商都判断了
if (isAnthropicModel(model)) {
return CLAUDE_SUPPORTED_WEBSEARCH_REGEX.test(modelId)
}
// TODO: 当其他供应商采用Response端点时这个地方逻辑需要改进
if (isOpenAIProvider(provider)) {
if (provider.type === 'openai-response') {
if (isOpenAIWebSearchModel(model)) {
return true
}
@@ -84,11 +78,11 @@ export function isWebSearchModel(model: Model): boolean {
return false
}
if (provider.id === SystemProviderIds.perplexity) {
if (provider.id === 'perplexity') {
return PERPLEXITY_SEARCH_MODELS.includes(modelId)
}
if (provider.id === SystemProviderIds.aihubmix) {
if (provider.id === 'aihubmix') {
// modelId 不以-search结尾
if (!modelId.endsWith('-search') && GEMINI_SEARCH_REGEX.test(modelId)) {
return true
@@ -101,13 +95,13 @@ export function isWebSearchModel(model: Model): boolean {
return false
}
if (isOpenAICompatibleProvider(provider) || isNewApiProvider(provider)) {
if (provider?.type === 'openai') {
if (GEMINI_SEARCH_REGEX.test(modelId) || isOpenAIWebSearchModel(model)) {
return true
}
}
if (isGeminiProvider(provider) || provider.id === SystemProviderIds.vertexai) {
if (provider.id === 'gemini' || provider?.type === 'gemini' || provider.type === 'vertexai') {
return GEMINI_SEARCH_REGEX.test(modelId)
}

View File

@@ -56,14 +56,7 @@ import VoyageAIProviderLogo from '@renderer/assets/images/providers/voyageai.png
import XirangProviderLogo from '@renderer/assets/images/providers/xirang.png'
import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
import type {
AtLeast,
AzureOpenAIProvider,
Provider,
ProviderType,
SystemProvider,
SystemProviderId
} from '@renderer/types'
import type { AtLeast, Provider, ProviderType, SystemProvider, SystemProviderId } from '@renderer/types'
import { isSystemProvider, OpenAIServiceTiers } from '@renderer/types'
import { TOKENFLUX_HOST } from './constant'
@@ -355,7 +348,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
name: 'VertexAI',
type: 'vertexai',
apiKey: '',
apiHost: '',
apiHost: 'https://aiplatform.googleapis.com',
models: SYSTEM_MODELS.vertexai,
isSystem: true,
enabled: false,
@@ -1295,7 +1288,7 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
},
vertexai: {
api: {
url: ''
url: 'https://console.cloud.google.com/apis/api/aiplatform.googleapis.com/overview'
},
websites: {
official: 'https://cloud.google.com/vertex-ai',
@@ -1375,8 +1368,7 @@ const NOT_SUPPORT_ARRAY_CONTENT_PROVIDERS = [
'baichuan',
'minimax',
'xirang',
'poe',
'cephalon'
'poe'
] as const satisfies SystemProviderId[]
/**
@@ -1441,15 +1433,10 @@ export const isSupportServiceTierProvider = (provider: Provider) => {
)
}
const SUPPORT_URL_CONTEXT_PROVIDER_TYPES = [
'gemini',
'vertexai',
'anthropic',
'new-api'
] as const satisfies ProviderType[]
const SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES = ['gemini', 'vertexai'] as const satisfies ProviderType[]
export const isSupportUrlContextProvider = (provider: Provider) => {
return SUPPORT_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type)
return SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type)
}
const SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS = ['gemini', 'vertexai'] as const satisfies SystemProviderId[]
@@ -1462,37 +1449,3 @@ export const isGeminiWebSearchProvider = (provider: Provider) => {
export const isNewApiProvider = (provider: Provider) => {
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
}

View File

@@ -0,0 +1,53 @@
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
}
]

View File

@@ -1,4 +1,4 @@
import { useAppSelector } from '@renderer/store'
import { useShortcutConfig } from '@renderer/hooks/useShortcuts'
import { IpcChannel } from '@shared/IpcChannel'
import { useEffect } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
@@ -7,9 +7,8 @@ import { useLocation, useNavigate } from 'react-router-dom'
const NavigationHandler: React.FC = () => {
const location = useLocation()
const navigate = useNavigate()
const showSettingsShortcutEnabled = useAppSelector(
(state) => state.shortcuts.shortcuts.find((s) => s.key === 'show_settings')?.enabled
)
const showSettingsShortcut = useShortcutConfig('show_settings')
const showSettingsShortcutEnabled = showSettingsShortcut?.enabled ?? false
useHotkeys(
'meta+, ! ctrl+,',

View File

@@ -0,0 +1,4 @@
export type UpdateAgentBaseOptions = {
/** Whether to show success toast after updating. Defaults to true. */
showSuccessToast?: boolean
}

View File

@@ -10,10 +10,6 @@ export const useAgent = (id: string | null) => {
const client = useAgentClient()
const key = id ? client.agentPaths.withId(id) : null
const { apiServerConfig, apiServerRunning } = useApiServer()
// Disable SWR fetching when server is not running by setting key to null
const swrKey = apiServerRunning && id ? key : null
const fetcher = useCallback(async () => {
if (!id) {
throw new Error(t('agent.get.error.null_id'))
@@ -21,10 +17,13 @@ export const useAgent = (id: string | null) => {
if (!apiServerConfig.enabled) {
throw new Error(t('apiServer.messages.notEnabled'))
}
if (!apiServerRunning) {
throw new Error(t('agent.server.error.not_running'))
}
const result = await client.getAgent(id)
return result
}, [apiServerConfig.enabled, client, id, t])
const { data, error, isLoading } = useSWR(swrKey, fetcher)
}, [apiServerConfig.enabled, apiServerRunning, client, id, t])
const { data, error, isLoading } = useSWR(key, id ? fetcher : null)
return {
agent: data,

View File

@@ -25,10 +25,6 @@ export const useAgents = () => {
const client = useAgentClient()
const key = client.agentPaths.base
const { apiServerConfig, apiServerRunning } = useApiServer()
// Disable SWR fetching when server is not running by setting key to null
const swrKey = apiServerRunning ? key : null
const fetcher = useCallback(async () => {
// API server will start on startup if enabled OR there are agents
if (!apiServerConfig.enabled && !apiServerRunning) {
@@ -41,7 +37,7 @@ export const useAgents = () => {
// NOTE: We only use the array for now. useUpdateAgent depends on this behavior.
return result.data
}, [apiServerConfig.enabled, apiServerRunning, client, t])
const { data, error, isLoading, mutate } = useSWR(swrKey, fetcher)
const { data, error, isLoading, mutate } = useSWR(key, fetcher)
const { chat } = useRuntime()
const { activeAgentId } = chat
const dispatch = useAppDispatch()

View File

@@ -1,10 +1,10 @@
import type { AgentEntity, ListAgentsResponse, UpdateAgentForm } from '@renderer/types'
import type { UpdateAgentBaseOptions, UpdateAgentFunction } from '@renderer/types/agent'
import type { ListAgentsResponse, UpdateAgentForm } from '@renderer/types'
import { formatErrorMessageWithPrefix } from '@renderer/utils/error'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { mutate } from 'swr'
import type { UpdateAgentBaseOptions } from './types'
import { useAgentClient } from './useAgentClient'
export const useUpdateAgent = () => {
@@ -12,8 +12,8 @@ export const useUpdateAgent = () => {
const client = useAgentClient()
const listKey = client.agentPaths.base
const updateAgent: UpdateAgentFunction = useCallback(
async (form: UpdateAgentForm, options?: UpdateAgentBaseOptions): Promise<AgentEntity | undefined> => {
const updateAgent = useCallback(
async (form: UpdateAgentForm, options?: UpdateAgentBaseOptions) => {
try {
const itemKey = client.agentPaths.withId(form.id)
// may change to optimistic update
@@ -23,10 +23,8 @@ export const useUpdateAgent = () => {
if (options?.showSuccessToast ?? true) {
window.toast.success(t('common.update_success'))
}
return result
} catch (error) {
window.toast.error(formatErrorMessageWithPrefix(error, t('agent.update.error.failed')))
return undefined
}
},
[client, listKey, t]

View File

@@ -1,18 +1,18 @@
import type { AgentSessionEntity, ListAgentSessionsResponse, UpdateSessionForm } from '@renderer/types'
import type { UpdateAgentBaseOptions, UpdateAgentSessionFunction } from '@renderer/types/agent'
import type { ListAgentSessionsResponse, UpdateSessionForm } from '@renderer/types'
import { getErrorMessage } from '@renderer/utils/error'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { mutate } from 'swr'
import type { UpdateAgentBaseOptions } from './types'
import { useAgentClient } from './useAgentClient'
export const useUpdateSession = (agentId: string | null) => {
const { t } = useTranslation()
const client = useAgentClient()
const updateSession: UpdateAgentSessionFunction = useCallback(
async (form: UpdateSessionForm, options?: UpdateAgentBaseOptions): Promise<AgentSessionEntity | undefined> => {
const updateSession = useCallback(
async (form: UpdateSessionForm, options?: UpdateAgentBaseOptions) => {
if (!agentId) return
const paths = client.getSessionPaths(agentId)
const listKey = paths.base
@@ -29,10 +29,8 @@ export const useUpdateSession = (agentId: string | null) => {
if (options?.showSuccessToast ?? true) {
window.toast.success(t('common.update_success'))
}
return result
} catch (error) {
window.toast.error({ title: t('agent.session.update.error.failed'), description: getErrorMessage(error) })
return undefined
}
},
[agentId, client, t]

View File

@@ -14,8 +14,8 @@ export const useApiServer = () => {
const apiServerConfig = useAppSelector((state) => state.settings.apiServer)
const dispatch = useAppDispatch()
// Initial state - no longer optimistic, wait for actual status
const [apiServerRunning, setApiServerRunning] = useState(false)
// Optimistic initial state.
const [apiServerRunning, setApiServerRunning] = useState(apiServerConfig.enabled)
const [apiServerLoading, setApiServerLoading] = useState(true)
const setApiServerEnabled = useCallback(
@@ -99,16 +99,6 @@ export const useApiServer = () => {
checkApiServerStatus()
}, [checkApiServerStatus])
// Listen for API server ready event
useEffect(() => {
const cleanup = window.api.apiServer.onReady(() => {
logger.info('API server ready event received, checking status')
checkApiServerStatus()
})
return cleanup
}, [checkApiServerStatus])
return {
apiServerConfig,
apiServerRunning,

View File

@@ -80,34 +80,17 @@ export function useAppInit() {
useEffect(() => {
savedAvatar?.value && cacheService.set('avatar', savedAvatar.value)
}, [savedAvatar])
}, [savedAvatar, dispatch])
useEffect(() => {
const checkForUpdates = async () => {
const { isPackaged } = await window.api.getAppInfo()
if (!isPackaged || !autoCheckUpdate) {
return
}
const { updateInfo } = await window.api.checkForUpdate()
updateAppUpdateState({ info: updateInfo })
}
// Initial check with delay
runAsyncFunction(async () => {
const { isPackaged } = await window.api.getAppInfo()
if (isPackaged && autoCheckUpdate) {
await delay(2)
await checkForUpdates()
const { updateInfo } = await window.api.checkForUpdate()
updateAppUpdateState({ info: updateInfo })
}
})
// Set up 4-hour interval check
const FOUR_HOURS = 4 * 60 * 60 * 1000
const intervalId = setInterval(checkForUpdates, FOUR_HOURS)
return () => clearInterval(intervalId)
}, [autoCheckUpdate, updateAppUpdateState])
useEffect(() => {
@@ -152,7 +135,7 @@ export function useAppInit() {
cacheService.set('filesPath', info.filesPath)
cacheService.set('resourcesPath', info.resourcesPath)
})
}, [])
}, [dispatch])
useEffect(() => {
KnowledgeQueue.checkAllBases()

View File

@@ -1,6 +1,5 @@
import { loggerService } from '@logger'
import {
DEFAULT_MODEL_MAP,
getThinkModelType,
isSupportedReasoningEffortModel,
isSupportedThinkingTokenModel,
@@ -25,11 +24,7 @@ import {
updateTopic,
updateTopics
} from '@renderer/store/assistants'
import {
setDefaultModel as setDefaultModelAction,
setQuickModel as setQuickModelAction,
setTranslateModel as setTranslateModelAction
} from '@renderer/store/llm'
import { setDefaultModel, setQuickModel, setTranslateModel } from '@renderer/store/llm'
import type { Assistant, AssistantSettings, Model, ThinkingOption, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { useCallback, useEffect, useMemo, useRef } from 'react'
@@ -203,31 +198,12 @@ export function useDefaultModel() {
const { defaultModel, quickModel, translateModel } = useAppSelector((state) => state.llm)
const dispatch = useAppDispatch()
const setDefaultModel = useCallback((model: Model) => dispatch(setDefaultModelAction({ model })), [dispatch])
const setQuickModel = useCallback((model: Model) => dispatch(setQuickModelAction({ model })), [dispatch])
const setTranslateModel = useCallback((model: Model) => dispatch(setTranslateModelAction({ model })), [dispatch])
const resetDefaultAssistantModel = useCallback(() => {
setDefaultModel(DEFAULT_MODEL_MAP.assistant)
}, [setDefaultModel])
const resetTranslateModel = useCallback(() => {
setTranslateModel(DEFAULT_MODEL_MAP.translate)
}, [setTranslateModel])
const resetQuickModel = useCallback(() => {
setQuickModel(DEFAULT_MODEL_MAP.quick)
}, [setQuickModel])
return {
defaultModel,
setDefaultModel,
resetDefaultAssistantModel,
quickModel,
resetQuickModel,
setQuickModel,
translateModel,
setTranslateModel,
resetTranslateModel
setDefaultModel: (model: Model) => dispatch(setDefaultModel({ model })),
setQuickModel: (model: Model) => dispatch(setQuickModel({ model })),
setTranslateModel: (model: Model) => dispatch(setTranslateModel({ model }))
}
}

View File

@@ -1,57 +0,0 @@
import ProtocolInstallWarningContent from '@renderer/pages/settings/MCPSettings/ProtocolInstallWarning'
import {
ensureServerTrusted as ensureServerTrustedCore,
getCommandPreview
} from '@renderer/pages/settings/MCPSettings/utils'
import type { MCPServer } from '@renderer/types'
import { modalConfirm } from '@renderer/utils'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useMCPServers } from './useMCPServers'
/**
* Hook for handling MCP server trust verification
* Binds UI (modal dialog) to the core trust verification logic
*/
export const useMCPServerTrust = () => {
const { updateMCPServer } = useMCPServers()
const { t } = useTranslation()
/**
* Request user confirmation to trust a server
* Shows a warning modal with server command preview
*/
const requestConfirm = useCallback(
async (server: MCPServer): Promise<boolean> => {
const commandPreview = getCommandPreview(server)
return modalConfirm({
title: t('settings.mcp.protocolInstallWarning.title'),
content: (
<ProtocolInstallWarningContent
message={t('settings.mcp.protocolInstallWarning.message')}
commandLabel={t('settings.mcp.protocolInstallWarning.command')}
commandPreview={commandPreview}
/>
),
okText: t('settings.mcp.protocolInstallWarning.run'),
cancelText: t('common.cancel'),
okButtonProps: { danger: true }
})
},
[t]
)
/**
* Ensures a server is trusted before proceeding
* Combines core logic with UI confirmation
*/
const ensureServerTrusted = useCallback(
async (server: MCPServer): Promise<MCPServer | null> => {
return ensureServerTrustedCore(server, requestConfirm, updateMCPServer)
},
[requestConfirm, updateMCPServer]
)
return { ensureServerTrusted }
}

View File

@@ -1,10 +1,13 @@
import { preferenceService } from '@data/PreferenceService'
import { isMac, isWin } from '@renderer/config/constant'
import { useAppSelector } from '@renderer/store'
import { shortcutRendererStore } from '@renderer/services/ShortcutService'
import { shortcutDefinitions } from '@shared/shortcuts/definitions'
import type { HydratedShortcut, ShortcutPreferenceEntry, ShortcutPreferenceMap } from '@shared/shortcuts/types'
import { orderBy } from 'lodash'
import { useCallback } from 'react'
import { useMemo, useSyncExternalStore } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
interface UseShortcutOptions {
export interface UseShortcutOptions {
preventDefault?: boolean
enableOnFormTags?: boolean
enabled?: boolean
@@ -17,77 +20,232 @@ const defaultOptions: UseShortcutOptions = {
enabled: true
}
const definitionMap = new Map(shortcutDefinitions.map((definition) => [definition.name, definition]))
const toHotkeysFormat = (keys: string[]): string => {
return keys
.map((key) => {
switch (key.toLowerCase()) {
case 'commandorcontrol':
case 'command':
case 'cmd':
return 'mod'
case 'control':
case 'ctrl':
return 'ctrl'
case 'alt':
case 'altgraph':
return 'alt'
case 'shift':
return 'shift'
case 'meta':
return 'meta'
case 'arrowup':
return 'up'
case 'arrowdown':
return 'down'
case 'arrowleft':
return 'left'
case 'arrowright':
return 'right'
case 'escape':
return 'escape'
case 'space':
return 'space'
default:
return key.toLowerCase()
}
})
.join('+')
}
const toDisplayFormat = (keys: string[]): string => {
return keys
.map((key) => {
switch (key.toLowerCase()) {
case 'control':
case 'ctrl':
return isMac ? '⌃' : 'Ctrl'
case 'command':
case 'cmd':
case 'commandorcontrol':
return isMac ? '⌘' : 'Ctrl'
case 'meta':
return isMac ? '⌘' : isWin ? 'Win' : 'Super'
case 'alt':
case 'altgraph':
return isMac ? '⌥' : 'Alt'
case 'shift':
return isMac ? '⇧' : 'Shift'
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 '='
case 'escape':
return isMac ? '⎋' : 'Esc'
default:
return key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()
}
})
.join(isMac ? '' : ' + ')
}
const useShortcutMap = () =>
useSyncExternalStore(
shortcutRendererStore.subscribe,
shortcutRendererStore.getSnapshot,
shortcutRendererStore.getServerSnapshot
)
export const useShortcut = (
shortcutKey: string,
callback: (e: KeyboardEvent) => void,
name: string,
callback: (event: KeyboardEvent) => void,
options: UseShortcutOptions = defaultOptions
) => {
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
const shortcuts = useShortcutMap()
const shortcutConfig = shortcuts[name]
const formatShortcut = useCallback((shortcut: string[]) => {
return shortcut
.map((key) => {
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)
const hotkey = useMemo(() => {
if (
!shortcutConfig ||
shortcutConfig.scope !== 'renderer' ||
!shortcutConfig.enabled ||
shortcutConfig.key.length === 0
) {
return null
}
return toHotkeysFormat(shortcutConfig.key)
}, [shortcutConfig])
useHotkeys(
shortcutConfig?.enabled ? formatShortcut(shortcutConfig.shortcut) : 'none',
(e) => {
hotkey ?? 'none',
(event) => {
if (options.preventDefault) {
e.preventDefault()
event.preventDefault()
}
if (options.enabled !== false) {
callback(e)
callback(event)
}
},
{
enableOnFormTags: options.enableOnFormTags,
description: options.description || shortcutConfig?.key,
enabled: !!shortcutConfig?.enabled
}
description: options.description ?? shortcutConfig?.description,
enabled: Boolean(hotkey && shortcutConfig?.enabled)
},
[
callback,
hotkey,
shortcutConfig,
options.preventDefault,
options.enableOnFormTags,
options.enabled,
options.description
]
)
}
export function useShortcuts() {
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
return { shortcuts: orderBy(shortcuts, 'system', 'desc') }
const shortcuts = useShortcutMap()
const list = useMemo(() => {
return orderBy(
Object.values(shortcuts).map((shortcut) => ({
...shortcut,
key: [...shortcut.key]
})),
['system', 'name'],
['desc', 'asc']
)
}, [shortcuts])
return { shortcuts: list }
}
export function useShortcutDisplay(key: string) {
const formatShortcut = useCallback((shortcut: string[]) => {
return shortcut
.map((key) => {
switch (key.toLowerCase()) {
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'
default:
return key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()
}
})
.join('+')
}, [])
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
const shortcutConfig = shortcuts.find((s) => s.key === key)
return shortcutConfig?.enabled ? formatShortcut(shortcutConfig.shortcut) : ''
export function useShortcutConfig(name: string): HydratedShortcut | undefined {
const shortcuts = useShortcutMap()
return shortcuts[name]
}
export function useShortcutDisplay(name: string) {
const shortcut = useShortcutConfig(name)
return useMemo(() => {
if (!shortcut || !shortcut.enabled || shortcut.key.length === 0) {
return ''
}
return toDisplayFormat(shortcut.key)
}, [shortcut])
}
async function writeShortcutPreferences(updater: (current: ShortcutPreferenceMap) => ShortcutPreferenceMap) {
const current = await preferenceService.get('shortcut.preferences')
const next = updater({ ...current })
await preferenceService.set('shortcut.preferences', next)
}
export async function setShortcutBinding(name: string, keys: string[]) {
await writeShortcutPreferences((current) => {
const entry: ShortcutPreferenceEntry = { ...current[name] }
entry.key = [...keys]
current[name] = entry
return current
})
}
export async function setShortcutEnabled(name: string, enabled: boolean) {
await writeShortcutPreferences((current) => {
const entry: ShortcutPreferenceEntry = { ...current[name] }
entry.enabled = enabled
current[name] = entry
return current
})
}
export async function resetShortcut(name: string) {
const definition = definitionMap.get(name)
if (!definition) {
return
}
await writeShortcutPreferences((current) => {
current[name] = {
key: [...definition.defaultKey],
enabled: definition.defaultEnabled
}
return current
})
}
export async function resetAllShortcuts() {
await writeShortcutPreferences(() => {
return Object.fromEntries(
shortcutDefinitions.map((definition) => [
definition.name,
{
key: [...definition.defaultKey],
enabled: definition.defaultEnabled
}
])
)
})
}

View File

@@ -11,12 +11,12 @@ import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import type { Assistant, Topic } from '@renderer/types'
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
import { find, isEmpty } from 'lodash'
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import { useAssistant } from './useAssistant'
let _activeTopic: Topic
let _setActiveTopic: Dispatch<SetStateAction<Topic>>
let _setActiveTopic: (topic: Topic) => void
// const logger = loggerService.withContext('useTopic')

View File

@@ -14,7 +14,6 @@ export default function useUserTheme() {
const colorPrimary = Color(theme.colorPrimary)
document.body.style.setProperty('--color-primary', colorPrimary.toString())
document.body.style.setProperty('--color-primary-foreground', getForegroundColor(colorPrimary.hex()))
// overwrite hero UI primary color.
document.body.style.setProperty('--primary', colorPrimary.toString())
document.body.style.setProperty('--primary-foreground', getForegroundColor(colorPrimary.hex()))

View File

@@ -42,7 +42,7 @@ export function getVertexAIServiceAccount() {
* 类型守卫:检查 Provider 是否为 VertexProvider
*/
export function isVertexProvider(provider: Provider): provider is VertexProvider {
return provider.type === 'vertexai'
return provider.type === 'vertexai' && 'googleCredentials' in provider
}
/**

View File

@@ -200,6 +200,7 @@ const shortcutKeyMap = {
exit_fullscreen: 'settings.shortcuts.exit_fullscreen',
label: 'settings.shortcuts.label',
mini_window: 'settings.shortcuts.mini_window',
show_mini_window: 'settings.shortcuts.show_mini_window',
new_topic: 'settings.shortcuts.new_topic',
press_shortcut: 'settings.shortcuts.press_shortcut',
reset_defaults: 'settings.shortcuts.reset_defaults',

View File

@@ -1,7 +1,6 @@
{
"agent": {
"add": {
"description": "Handle complex tasks with various tools",
"error": {
"failed": "Failed to add a agent",
"invalid_agent": "Invalid Agent"
@@ -548,12 +547,8 @@
"chat": {
"add": {
"assistant": {
"description": "Daily conversations and quick Q&A",
"title": "Add Assistant"
},
"option": {
"title": "Select Type"
},
"topic": {
"title": "New Topic"
}
@@ -843,7 +838,7 @@
"label": "Context",
"tip": "The number of previous messages to keep in the context."
},
"max": "Unlimited",
"max": "Max",
"max_tokens": {
"confirm": "Set max tokens",
"confirm_content": "Set the maximum number of tokens the model can generate. Need to consider the context limit of the model, otherwise an error will be reported",
@@ -1052,12 +1047,10 @@
"clear": "Clear",
"close": "Close",
"collapse": "Collapse",
"completed": "Completed",
"confirm": "Confirm",
"copied": "Copied",
"copy": "Copy",
"copy_failed": "Copy failed",
"current": "Current",
"cut": "Cut",
"default": "Default",
"delete": "Delete",
@@ -2928,14 +2921,15 @@
},
"description": "A powerful AI assistant for producer",
"downloading": "Downloading...",
"enterprise": {
"title": "Enterprise"
},
"feedback": {
"button": "Feedback",
"title": "Feedback"
},
"label": "About & Feedback",
"license": {
"button": "License",
"title": "License"
},
"releases": {
"button": "Releases",
"title": "Release Notes"
@@ -3043,46 +3037,6 @@
"title": "Export Menu Settings",
"yuque": "Export to Yuque"
},
"export_to_phone": {
"confirm": {
"button": "Select backup file"
},
"content": "Export some data, including chat logs and settings. Please note that the backup process may take some time. Thank you for your patience.",
"lan": {
"auto_close_tip": "Auto-closing in {{seconds}} seconds...",
"confirm_close_message": "File transfer is in progress. Closing will interrupt the transfer. Are you sure you want to force close?",
"confirm_close_title": "Confirm Close",
"connected": "Connected",
"connection_failed": "Connection failed",
"content": "Please ensure your computer and phone are on the same network for LAN transfer. Open the Cherry Studio App to scan this QR code.",
"error": {
"init_failed": "Initialization failed",
"no_file": "No file selected",
"no_ip": "Unable to get IP address",
"send_failed": "Failed to send file"
},
"force_close": "Force Close",
"generating_qr": "Generating QR code...",
"noZipSelected": "No compressed file selected",
"scan_qr": "Please scan QR code with your phone",
"selectZip": "Select a compressed file",
"sendZip": "Begin data recovery",
"status": {
"completed": "Transfer completed",
"connected": "Connected",
"connecting": "Connecting...",
"disconnected": "Disconnected",
"error": "Connection error",
"initializing": "Initializing connection...",
"preparing": "Preparing transfer...",
"sending": "Transferring {{progress}}%",
"waiting_qr_scan": "Please scan QR code to connect"
},
"title": "LAN transmission",
"transfer_progress": "Transfer progress"
},
"title": "Export to phone"
},
"hour_interval_one": "{{count}} hour",
"hour_interval_other": "{{count}} hours",
"joplin": {
@@ -3829,12 +3783,6 @@
"noPromptsAvailable": "No prompts available",
"requiredField": "Required Field"
},
"protocolInstallWarning": {
"command": "Startup command",
"message": "This MCP was installed from an external source via protocol. Running unknown tools may harm your computer.",
"run": "Run",
"title": "Run external MCP?"
},
"provider": "Provider",
"providerPlaceholder": "Provider name",
"providerUrl": "Provider URL",
@@ -4200,6 +4148,7 @@
},
"anthropic_api_host": "Anthropic API Host",
"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.",
"api": {
"key": {
@@ -4244,11 +4193,10 @@
"url": {
"preview": "Preview: {{url}}",
"reset": "Reset",
"tip": "ending with # forces use of input address"
"tip": "Ending with / ignores v1, ending with # forces use of input address"
}
},
"api_host": "API Host",
"api_host_no_valid": "API address is invalid",
"api_host_preview": "Preview: {{url}}",
"api_host_tooltip": "Override only when your provider requires a custom OpenAI-compatible endpoint.",
"api_key": {

View File

@@ -1,7 +1,6 @@
{
"agent": {
"add": {
"description": "调用各种工具处理复杂任务",
"error": {
"failed": "添加 Agent 失败",
"invalid_agent": "无效的 Agent"
@@ -548,12 +547,8 @@
"chat": {
"add": {
"assistant": {
"description": "日常对话和快速问答",
"title": "添加助手"
},
"option": {
"title": "选择添加类型"
},
"topic": {
"title": "新建话题"
}
@@ -1052,12 +1047,10 @@
"clear": "清除",
"close": "关闭",
"collapse": "折叠",
"completed": "完成",
"confirm": "确认",
"copied": "已复制",
"copy": "复制",
"copy_failed": "复制失败",
"current": "当前",
"cut": "剪切",
"default": "默认",
"delete": "删除",
@@ -2928,14 +2921,15 @@
},
"description": "一款为创造者而生的 AI 助手",
"downloading": "正在下载更新...",
"enterprise": {
"title": "企业版"
},
"feedback": {
"button": "反馈",
"title": "意见反馈"
},
"label": "关于我们",
"license": {
"button": "查看",
"title": "许可证"
},
"releases": {
"button": "查看",
"title": "更新日志"
@@ -3043,46 +3037,6 @@
"title": "导出菜单设置",
"yuque": "导出到语雀"
},
"export_to_phone": {
"confirm": {
"button": "选择备份文件"
},
"content": "导出部分数据,包括聊天记录、设置。请注意,备份过程可能需要一些时间,感谢您的耐心等待。",
"lan": {
"auto_close_tip": "{{seconds}} 秒后自动关闭...",
"confirm_close_message": "文件正在传输中,关闭将中断传输。确定要强制关闭吗?",
"confirm_close_title": "确认关闭",
"connected": "连接成功",
"connection_failed": "连接失败",
"content": "请确保电脑和手机处于同一网络以使用局域网传输。请打开 Cherry Studio App 扫描此二维码。",
"error": {
"init_failed": "初始化失败",
"no_file": "未选择文件",
"no_ip": "无法获取 IP 地址",
"send_failed": "发送文件失败"
},
"force_close": "强制关闭",
"generating_qr": "正在生成二维码...",
"noZipSelected": "未选择压缩文件",
"scan_qr": "请使用手机扫码连接",
"selectZip": "选择压缩文件",
"sendZip": "开始恢复数据",
"status": {
"completed": "传输完成",
"connected": "连接成功",
"connecting": "正在连接中...",
"disconnected": "连接已断开",
"error": "连接出错",
"initializing": "正在初始化连接...",
"preparing": "准备传输中...",
"sending": "传输中 {{progress}}%",
"waiting_qr_scan": "请扫描二维码连接"
},
"title": "局域网传输",
"transfer_progress": "传输进度"
},
"title": "导出至手机"
},
"hour_interval_one": "{{count}} 小时",
"hour_interval_other": "{{count}} 小时",
"joplin": {
@@ -3829,12 +3783,6 @@
"noPromptsAvailable": "无可用提示",
"requiredField": "必填字段"
},
"protocolInstallWarning": {
"command": "启动命令",
"message": "该 MCP 是通过协议从外部来源安装的,运行来历不明的工具可能对您的计算机造成危害。",
"run": "运行",
"title": "运行外部 MCP"
},
"provider": "提供者",
"providerPlaceholder": "提供者名称",
"providerUrl": "提供者网址",
@@ -4200,6 +4148,7 @@
},
"anthropic_api_host": "Anthropic API 地址",
"anthropic_api_host_preview": "Anthropic 预览:{{url}}",
"anthropic_api_host_tip": "仅在服务商提供兼容 Anthropic 的地址时填写。以 / 结尾会忽略自动追加的 v1以 # 结尾则强制使用原始地址。",
"anthropic_api_host_tooltip": "仅当服务商提供 Claude 兼容的基础地址时填写。",
"api": {
"key": {
@@ -4244,11 +4193,10 @@
"url": {
"preview": "预览: {{url}}",
"reset": "重置",
"tip": "# 结尾强制使用输入地址"
"tip": "/ 结尾忽略 v1 版本,# 结尾强制使用输入地址"
}
},
"api_host": "API 地址",
"api_host_no_valid": "API 地址不合法",
"api_host_preview": "预览:{{url}}",
"api_host_tooltip": "仅在服务商需要自定义的 OpenAI 兼容地址时覆盖。",
"api_key": {

View File

@@ -1,7 +1,6 @@
{
"agent": {
"add": {
"description": "調用各種工具處理複雜任務",
"error": {
"failed": "無法新增代理人",
"invalid_agent": "無效的 Agent"
@@ -548,12 +547,8 @@
"chat": {
"add": {
"assistant": {
"description": "日常對話和快速問答",
"title": "新增助手"
},
"option": {
"title": "選擇新增類型"
},
"topic": {
"title": "新增話題"
}
@@ -843,7 +838,7 @@
"label": "上下文",
"tip": "在上下文中保留的前幾則訊息"
},
"max": "不限",
"max": "最大",
"max_tokens": {
"confirm": "設置最大 Token 數",
"confirm_content": "設置單次交互所用的最大 Token 數,會影響返回結果的長度。要根據模型上下文限制來設定,否則會發生錯誤",
@@ -1052,12 +1047,10 @@
"clear": "清除",
"close": "關閉",
"collapse": "折疊",
"completed": "已完成",
"confirm": "確認",
"copied": "已複製",
"copy": "複製",
"copy_failed": "複製失敗",
"current": "当前",
"cut": "剪下",
"default": "預設",
"delete": "刪除",
@@ -2928,14 +2921,15 @@
},
"description": "一款為創作者而生的強大 AI 助手",
"downloading": "正在下載...",
"enterprise": {
"title": "企業版"
},
"feedback": {
"button": "回饋",
"title": "回饋"
},
"label": "關於與回饋",
"license": {
"button": "檢視",
"title": "授權"
},
"releases": {
"button": "檢視",
"title": "更新日誌"
@@ -3043,46 +3037,6 @@
"title": "匯出選單設定",
"yuque": "匯出到語雀"
},
"export_to_phone": {
"confirm": {
"button": "選擇備份檔案"
},
"content": "匯出部分數據,包括聊天記錄、設定。請注意,備份過程可能需要一些時間,感謝您的耐心等候。",
"lan": {
"auto_close_tip": "將於 {{seconds}} 秒後自動關閉...",
"confirm_close_message": "檔案傳輸正在進行中。關閉將會中斷傳輸。您確定要強制關閉嗎?",
"confirm_close_title": "確認關閉",
"connected": "已連線",
"connection_failed": "連線失敗",
"content": "請確保電腦和手機處於同一網路以使用區域網路傳輸。請打開 Cherry Studio App 掃描此 QR 碼。",
"error": {
"init_failed": "初始化失敗",
"no_file": "未選擇檔案",
"no_ip": "無法取得 IP 位址",
"send_failed": "無法傳送檔案"
},
"force_close": "強制關閉",
"generating_qr": "正在生成 QR 碼...",
"noZipSelected": "未選取壓縮檔案",
"scan_qr": "請使用手機掃描QR碼",
"selectZip": "選擇壓縮檔案",
"sendZip": "開始恢復資料",
"status": {
"completed": "轉帳完成",
"connected": "已連線",
"connecting": "連線中...",
"disconnected": "已斷線",
"error": "連線錯誤",
"initializing": "正在初始化連線...",
"preparing": "正在準備傳輸...",
"sending": "傳輸中 {{progress}}%",
"waiting_qr_scan": "請掃描QR碼以連接"
},
"title": "區域網路傳輸",
"transfer_progress": "傳輸進度"
},
"title": "匯出手機"
},
"hour_interval_one": "{{count}} 小時",
"hour_interval_other": "{{count}} 小時",
"joplin": {
@@ -3829,12 +3783,6 @@
"noPromptsAvailable": "無可用提示",
"requiredField": "必填欄位"
},
"protocolInstallWarning": {
"command": "啟動命令",
"message": "此 MCP 透過協議從外部來源安裝,執行來源不明的工具可能會對您的電腦造成危害。",
"run": "執行",
"title": "執行外部 MCP"
},
"provider": "提供者",
"providerPlaceholder": "提供者名稱",
"providerUrl": "提供者網址",
@@ -4200,6 +4148,7 @@
},
"anthropic_api_host": "Anthropic API 主機地址",
"anthropic_api_host_preview": "Anthropic 預覽:{{url}}",
"anthropic_api_host_tip": "僅在服務商提供與 Anthropic 相容的網址時設定。以 / 結尾會忽略自動附加的 v1以 # 結尾則強制使用原始地址。",
"anthropic_api_host_tooltip": "僅在服務商提供 Claude 相容的基礎網址時設定。",
"api": {
"key": {
@@ -4244,11 +4193,10 @@
"url": {
"preview": "預覽:{{url}}",
"reset": "重設",
"tip": "# 結尾強制使用輸入位址"
"tip": "/ 結尾忽略 v1 版本,# 結尾強制使用輸入位址"
}
},
"api_host": "API 主機地址",
"api_host_no_valid": "API 位址不合法",
"api_host_preview": "預覽:{{url}}",
"api_host_tooltip": "僅在服務商需要自訂的 OpenAI 相容端點時才覆蓋。",
"api_key": {

View File

@@ -1,7 +1,6 @@
{
"agent": {
"add": {
"description": "Bearbeiten Sie komplexe Aufgaben mit verschiedenen Werkzeugen",
"error": {
"failed": "Agent hinzufügen fehlgeschlagen",
"invalid_agent": "Ungültiger Agent"
@@ -548,12 +547,8 @@
"chat": {
"add": {
"assistant": {
"description": "Tägliche Gespräche und kurze Fragen & Antworten",
"title": "Assistent hinzufügen"
},
"option": {
"title": "Typ auswählen"
},
"topic": {
"title": "Neues Thema erstellen"
}
@@ -1052,12 +1047,10 @@
"clear": "Löschen",
"close": "Schließen",
"collapse": "Einklappen",
"completed": "Abgeschlossen",
"confirm": "Bestätigen",
"copied": "Kopiert",
"copy": "Kopieren",
"copy_failed": "Kopieren fehlgeschlagen",
"current": "Aktuell",
"cut": "Ausschneiden",
"default": "Standard",
"delete": "Löschen",
@@ -2928,14 +2921,15 @@
},
"description": "Ein KI-Assistent für Kreative",
"downloading": "Update wird heruntergeladen...",
"enterprise": {
"title": "Unternehmen"
},
"feedback": {
"button": "Feedback",
"title": "Feedback"
},
"label": "Über uns",
"license": {
"button": "Anzeigen",
"title": "Lizenz"
},
"releases": {
"button": "Anzeigen",
"title": "Changelog"
@@ -3043,46 +3037,6 @@
"title": "Export-Menü-Einstellungen",
"yuque": "Nach Yuque exportieren"
},
"export_to_phone": {
"confirm": {
"button": "Sicherungsdatei auswählen"
},
"content": "Exportieren Sie einige Daten, einschließlich Chat-Protokollen und Einstellungen. Bitte beachten Sie, dass der Sicherungsvorgang einige Zeit in Anspruch nehmen kann. Vielen Dank für Ihre Geduld.",
"lan": {
"auto_close_tip": "Automatisches Schließen in {{seconds}} Sekunden...",
"confirm_close_message": "Dateiübertragung läuft. Beim Schließen wird die Übertragung unterbrochen. Möchten Sie wirklich das Schließen erzwingen?",
"confirm_close_title": "Schließen bestätigen",
"connected": "Verbunden",
"connection_failed": "Verbindung fehlgeschlagen",
"content": "Bitte stelle sicher, dass sich dein Computer und dein Telefon im selben Netzwerk befinden, um eine LAN-Übertragung durchzuführen. Öffne die Cherry Studio App, um diesen QR-Code zu scannen.",
"error": {
"init_failed": "Initialisierung fehlgeschlagen",
"no_file": "Keine Datei ausgewählt",
"no_ip": "IP-Adresse kann nicht abgerufen werden",
"send_failed": "Fehler beim Senden der Datei"
},
"force_close": "Erzwungenes Schließen",
"generating_qr": "QR-Code wird generiert...",
"noZipSelected": "Keine komprimierte Datei ausgewählt",
"scan_qr": "Bitte scannen Sie den QR-Code mit Ihrem Telefon.",
"selectZip": "Wählen Sie eine komprimierte Datei",
"sendZip": "Datenwiederherstellung beginnen",
"status": {
"completed": "Übertragung abgeschlossen",
"connected": "Verbunden",
"connecting": "Verbindung wird hergestellt...",
"disconnected": "Getrennt",
"error": "Verbindungsfehler",
"initializing": "Verbindung wird initialisiert...",
"preparing": "Übertragung wird vorbereitet...",
"sending": "Übertrage {{progress}}%",
"waiting_qr_scan": "Bitte QR-Code scannen, um zu verbinden"
},
"title": "LAN-Übertragung",
"transfer_progress": "Übertragungsfortschritt"
},
"title": "Auf Telefon exportieren"
},
"hour_interval_one": "{{count}} Stunde",
"hour_interval_other": "{{count}} Stunden",
"joplin": {
@@ -3829,12 +3783,6 @@
"noPromptsAvailable": "Keine Prompts verfügbar",
"requiredField": "Pflichtfeld"
},
"protocolInstallWarning": {
"command": "Startbefehl",
"message": "Dieses MCP wurde über ein Protokoll aus einer externen Quelle installiert. Das Ausführen unbekannter Tools kann Ihren Computer schädigen.",
"run": "Laufen",
"title": "Externes MCP ausführen?"
},
"provider": "Anbieter",
"providerPlaceholder": "Anbietername",
"providerUrl": "Anbieter-Website",
@@ -4200,6 +4148,7 @@
},
"anthropic_api_host": "Anthropic API-Adresse",
"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.",
"api": {
"key": {
@@ -4244,11 +4193,10 @@
"url": {
"preview": "Vorschau: {{url}}",
"reset": "Zurücksetzen",
"tip": "# am Ende erzwingt die Verwendung der Eingabe-Adresse"
"tip": "/ am Ende ignorieren v1-Version, # am Ende erzwingt die Verwendung der Eingabe-Adresse"
}
},
"api_host": "API-Adresse",
"api_host_no_valid": "API-Adresse ist ungültig",
"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_key": {

View File

@@ -1,7 +1,6 @@
{
"agent": {
"add": {
"description": "Χειριστείτε πολύπλοκες εργασίες με διάφορα εργαλεία",
"error": {
"failed": "Αποτυχία προσθήκης πράκτορα",
"invalid_agent": "Μη έγκυρος Agent"
@@ -548,12 +547,8 @@
"chat": {
"add": {
"assistant": {
"description": "Καθημερινές συνομιλίες και γρήγορες ερωταπαντήσεις",
"title": "Προσθήκη βοηθού"
},
"option": {
"title": "Επιλέξτε Τύπο"
},
"topic": {
"title": "Δημιουργία νέου θέματος"
}
@@ -843,7 +838,7 @@
"label": "Πλήθος ενδιάμεσων",
"tip": "Πλήθος των μηνυμάτων που θα παραμείνουν στα ενδιάμεσα, όσο μεγαλύτερο είναι το αριθμός, τόσο μεγαλύτερο είναι το μήκος του ενδιάμεσου και τόσο περισσότερα tokens χρησιμοποιούνται. Συνομιλία συνήθως συνιστάται μεταξύ 5-10"
},
"max": "άπειρος",
"max": "Όχι ορισμένο",
"max_tokens": {
"confirm": "Ενεργοποίηση περιορισμού μήκους μηνύματος",
"confirm_content": "Μετά την ενεργοποίηση του περιορισμού μήκους μηνύματος, ο μέγιστος αριθμός των tokens που χρησιμοποιούνται κάθε φορά, θα επηρεάζει το μήκος της απάντησης. Πρέπει να το ρυθμίζετε βάσει των περιορισμών του πλαισίου του μοντέλου, διαφορετικά θα σφάλλεται.",
@@ -1052,12 +1047,10 @@
"clear": "Καθαρισμός",
"close": "Κλείσιμο",
"collapse": "Σύμπτυξη",
"completed": "Ολοκληρώθηκε",
"confirm": "Επιβεβαίωση",
"copied": "Αντιγράφηκε",
"copy": "Αντιγραφή",
"copy_failed": "Αποτυχία αντιγραφής",
"current": "Τρέχων",
"cut": "Κοπή",
"default": "Προεπιλογή",
"delete": "Διαγραφή",
@@ -2928,14 +2921,15 @@
},
"description": "Ένα AI ασιστάντα που έχει σχεδιαστεί για δημιουργούς",
"downloading": "Λήψη ενημερώσεων...",
"enterprise": {
"title": "Επιχείρηση"
},
"feedback": {
"button": "Σχόλια και Παρατηρήσεις",
"title": "Αποστολή σχολίων"
},
"label": "Περί μας",
"license": {
"button": "Προβολή",
"title": "Licenses"
},
"releases": {
"button": "Προβολή",
"title": "Ημερολόγιο Ενημερώσεων"
@@ -3043,46 +3037,6 @@
"title": "Εξαγωγή ρυθμίσεων μενού",
"yuque": "Εξαγωγή στο Yuque"
},
"export_to_phone": {
"confirm": {
"button": "Επιλέξτε αρχείο αντιγράφων ασφαλείας"
},
"content": "Εξαγωγή μέρους των δεδομένων, συμπεριλαμβανομένων των ιστορικών συνομιλιών και των ρυθμίσεων. Σημειώστε ότι η διαδικασία δημιουργίας αντιγράφων ασφαλείας ενδέχεται να διαρκέσει κάποιο χρονικό διάστημα, ευχαριστούμε για την υπομονή σας.",
"lan": {
"auto_close_tip": "Αυτόματο κλείσιμο σε {{seconds}} δευτερόλεπτα...",
"confirm_close_message": "Η μεταφορά αρχείων είναι σε εξέλιξη. Το κλείσιμο θα διακόψει τη μεταφορά. Είστε σίγουροι ότι θέλετε να κλείσετε βίαια;",
"confirm_close_title": "Επιβεβαίωση Κλεισίματος",
"connected": "Συνδεδεμένος",
"connection_failed": "Η σύνδεση απέτυχε",
"content": "Βεβαιωθείτε ότι ο υπολογιστής και το κινητό βρίσκονται στο ίδιο δίκτυο για να χρησιμοποιήσετε τη μεταφορά LAN. Ανοίξτε την εφαρμογή Cherry Studio και σαρώστε αυτόν τον κωδικό QR.",
"error": {
"init_failed": "Η αρχικοποίηση απέτυχε",
"no_file": "Κανένα αρχείο δεν επιλέχθηκε",
"no_ip": "Αδυναμία λήψης διεύθυνσης IP",
"send_failed": "Αποτυχία αποστολής αρχείου"
},
"force_close": "Κλείσιμο με βία",
"generating_qr": "Δημιουργία κώδικα QR...",
"noZipSelected": "Δεν επιλέχθηκε συμπιεσμένο αρχείο",
"scan_qr": "Παρακαλώ σαρώστε τον κωδικό QR με το τηλέφωνό σας",
"selectZip": "Επιλέξτε συμπιεσμένο αρχείο",
"sendZip": "Έναρξη ανάκτησης δεδομένων",
"status": {
"completed": "Η μεταφορά ολοκληρώθηκε",
"connected": "Συνδεδεμένος",
"connecting": "Σύνδεση...",
"disconnected": "Αποσυνδέθηκε",
"error": "Σφάλμα σύνδεσης",
"initializing": "Αρχικοποίηση σύνδεσης...",
"preparing": "Προετοιμασία μεταφοράς...",
"sending": "Μεταφορά {{progress}}%",
"waiting_qr_scan": "Παρακαλώ σαρώστε τον κωδικό QR για σύνδεση"
},
"title": "Μεταφορά τοπικού δικτύου",
"transfer_progress": "Πρόοδος μεταφοράς"
},
"title": "Εξαγωγή στο κινητό"
},
"hour_interval_one": "{{count}} ώρα",
"hour_interval_other": "{{count}} ώρες",
"joplin": {
@@ -3829,12 +3783,6 @@
"noPromptsAvailable": "Δεν υπάρχουν διαθέσιμες υποδείξεις",
"requiredField": "Υποχρεωτικό πεδίο"
},
"protocolInstallWarning": {
"command": "Εντολή εκκίνησης",
"message": "Αυτό το MCP εγκαταστάθηκε από εξωτερική πηγή μέσω πρωτοκόλλου. Η εκτέλεση άγνωστων εργαλείων ενδέχεται να βλάψει τον υπολογιστή σας.",
"run": "Τρέξε",
"title": "Εκτέλεση εξωτερικού MCP;"
},
"provider": "Πάροχος",
"providerPlaceholder": "Όνομα παρόχου",
"providerUrl": "URL Παρόχου",
@@ -4200,6 +4148,7 @@
},
"anthropic_api_host": "Διεύθυνση API Anthropic",
"anthropic_api_host_preview": "Προεπισκόπηση Anthropic: {{url}}",
"anthropic_api_host_tip": "Συμπληρώστε μόνο εάν ο πάροχος προσφέρει συμβατή με Anthropic διεύθυνση. Η λήξη με / αγνοεί το v1 που προστίθεται αυτόματα, η λήξη με # επιβάλλει τη χρήση της αρχικής διεύθυνσης.",
"anthropic_api_host_tooltip": "Συμπληρώστε μόνο όταν ο πάροχος παρέχει βασική διεύθυνση συμβατή με Claude.",
"api": {
"key": {
@@ -4244,11 +4193,10 @@
"url": {
"preview": "Προεπισκόπηση: {{url}}",
"reset": "Επαναφορά",
"tip": "#τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως"
"tip": "/τέλος αγνόηση v1 έκδοσης, #τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως"
}
},
"api_host": "Διεύθυνση API",
"api_host_no_valid": "Η διεύθυνση API δεν είναι έγκυρη",
"api_host_preview": "Προεπισκόπηση: {{url}}",
"api_host_tooltip": "Αντικατάσταση μόνο όταν ο πάροχος απαιτεί προσαρμοσμένη διεύθυνση συμβατή με OpenAI.",
"api_key": {

View File

@@ -1,7 +1,6 @@
{
"agent": {
"add": {
"description": "Maneja tareas complejas con varias herramientas",
"error": {
"failed": "Error al añadir agente",
"invalid_agent": "Agent inválido"
@@ -548,12 +547,8 @@
"chat": {
"add": {
"assistant": {
"description": "Conversaciones diarias y preguntas y respuestas rápidas",
"title": "Agregar asistente"
},
"option": {
"title": "Seleccionar Tipo"
},
"topic": {
"title": "Crear nuevo tema"
}
@@ -1052,12 +1047,10 @@
"clear": "Limpiar",
"close": "Cerrar",
"collapse": "Colapsar",
"completed": "Completado",
"confirm": "Confirmar",
"copied": "Copiado",
"copy": "Copiar",
"copy_failed": "Error al copiar",
"current": "Actual",
"cut": "Cortar",
"default": "Predeterminado",
"delete": "Eliminar",
@@ -2928,14 +2921,15 @@
},
"description": "Una asistente de IA creada para los creadores",
"downloading": "Descargando actualización...",
"enterprise": {
"title": "Empresa"
},
"feedback": {
"button": "Enviar feedback",
"title": "Enviar comentarios"
},
"label": "Acerca de nosotros",
"license": {
"button": "Ver",
"title": "Licencia"
},
"releases": {
"button": "Ver",
"title": "Registro de cambios"
@@ -3043,46 +3037,6 @@
"title": "Exportar configuración del menú",
"yuque": "Exportar a Yuque"
},
"export_to_phone": {
"confirm": {
"button": "Seleccionar archivo de copia de seguridad"
},
"content": "Exportar parte de los datos, incluidos los registros de chat y la configuración. Tenga en cuenta que el proceso de copia de seguridad puede tardar un tiempo; gracias por su paciencia.",
"lan": {
"auto_close_tip": "Cierre automático en {{seconds}} segundos...",
"confirm_close_message": "La transferencia de archivos está en progreso. Cerrar interrumpirá la transferencia. ¿Estás seguro de que quieres forzar el cierre?",
"confirm_close_title": "Confirmar Cierre",
"connected": "Conectado",
"connection_failed": "Conexión fallida",
"content": "Asegúrate de que el ordenador y el móvil estén en la misma red para usar la transferencia por LAN. Abre la aplicación Cherry Studio y escanea este código QR.",
"error": {
"init_failed": "Falló la inicialización",
"no_file": "Ningún archivo seleccionado",
"no_ip": "No se puede obtener la dirección IP",
"send_failed": "Error al enviar el archivo"
},
"force_close": "Cerrar forzosamente",
"generating_qr": "Generando código QR...",
"noZipSelected": "No se ha seleccionado ningún archivo comprimido",
"scan_qr": "Por favor, escanea el código QR con tu teléfono",
"selectZip": "Seleccionar archivo comprimido",
"sendZip": "Comenzar la recuperación de datos",
"status": {
"completed": "Transferencia completada",
"connected": "Conectado",
"connecting": "Conectando...",
"disconnected": "Desconectado",
"error": "Error de conexión",
"initializing": "Inicializando conexión...",
"preparing": "Preparando transferencia...",
"sending": "Transfiriendo {{progress}}%",
"waiting_qr_scan": "Por favor, escanea el código QR para conectarte"
},
"title": "Transferencia de red local",
"transfer_progress": "Progreso de transferencia"
},
"title": "Exportar al teléfono"
},
"hour_interval_one": "{{count}} hora",
"hour_interval_other": "{{count}} horas",
"joplin": {
@@ -3829,12 +3783,6 @@
"noPromptsAvailable": "No hay indicaciones disponibles",
"requiredField": "Campo obligatorio"
},
"protocolInstallWarning": {
"command": "Comando de inicio",
"message": "Este MCP fue instalado desde una fuente externa a través del protocolo. Ejecutar herramientas desconocidas puede dañar tu computadora.",
"run": "Correr",
"title": "¿Ejecutar MCP externo?"
},
"provider": "Proveedor",
"providerPlaceholder": "Nombre del proveedor",
"providerUrl": "URL del proveedor",
@@ -4200,6 +4148,7 @@
},
"anthropic_api_host": "Dirección API de Anthropic",
"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.",
"api": {
"key": {
@@ -4244,11 +4193,10 @@
"url": {
"preview": "Vista previa: {{url}}",
"reset": "Restablecer",
"tip": "forzar uso de dirección de entrada con # al final"
"tip": "Ignorar v1 al final con /, forzar uso de dirección de entrada con # al final"
}
},
"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_tooltip": "Sobrescribir solo cuando el proveedor necesite una dirección compatible con OpenAI personalizada.",
"api_key": {

View File

@@ -1,7 +1,6 @@
{
"agent": {
"add": {
"description": "Gérez des tâches complexes avec divers outils",
"error": {
"failed": "Échec de l'ajout de l'agent",
"invalid_agent": "Agent invalide"
@@ -548,12 +547,8 @@
"chat": {
"add": {
"assistant": {
"description": "Conversations quotidiennes et Q&R rapides",
"title": "Ajouter un assistant"
},
"option": {
"title": "Sélectionner le type"
},
"topic": {
"title": "Nouveau sujet"
}
@@ -1052,12 +1047,10 @@
"clear": "Effacer",
"close": "Fermer",
"collapse": "Réduire",
"completed": "Terminé",
"confirm": "Confirmer",
"copied": "Copié",
"copy": "Copier",
"copy_failed": "Échec de la copie",
"current": "Actuel",
"cut": "Couper",
"default": "Défaut",
"delete": "Supprimer",
@@ -2928,14 +2921,15 @@
},
"description": "Un assistant IA conçu pour les créateurs",
"downloading": "Téléchargement de la mise à jour en cours...",
"enterprise": {
"title": "Entreprise"
},
"feedback": {
"button": "Faire un retour",
"title": "Retour d'information"
},
"label": "À propos de nous",
"license": {
"button": "Afficher",
"title": "Licence"
},
"releases": {
"button": "Afficher",
"title": "Journal des mises à jour"
@@ -3043,46 +3037,6 @@
"title": "Exporter les paramètres du menu",
"yuque": "Exporter vers Yuque"
},
"export_to_phone": {
"confirm": {
"button": "Sélectionner le fichier de sauvegarde"
},
"content": "Exporter une partie des données, incluant les historiques de discussion et les paramètres. Veuillez noter que le processus de sauvegarde peut prendre un certain temps ; merci pour votre patience.",
"lan": {
"auto_close_tip": "Fermeture automatique dans {{seconds}} secondes...",
"confirm_close_message": "Le transfert de fichier est en cours. Fermer interrompra le transfert. Êtes-vous sûr de vouloir forcer la fermeture ?",
"confirm_close_title": "Confirmer la fermeture",
"connected": "Connecté",
"connection_failed": "Échec de la connexion",
"content": "Assurez-vous que l'ordinateur et le téléphone sont connectés au même réseau pour utiliser le transfert en réseau local. Ouvrez l'application Cherry Studio et scannez ce code QR.",
"error": {
"init_failed": "Échec de l'initialisation",
"no_file": "Aucun fichier sélectionné",
"no_ip": "Impossible d'obtenir l'adresse IP",
"send_failed": "Échec de l'envoi du fichier"
},
"force_close": "Fermer de force",
"generating_qr": "Génération du code QR...",
"noZipSelected": "Aucun fichier compressé sélectionné",
"scan_qr": "Veuillez scanner le code QR avec votre téléphone",
"selectZip": "Sélectionner le fichier compressé",
"sendZip": "Commencer la restauration des données",
"status": {
"completed": "Transfert terminé",
"connected": "Connecté",
"connecting": "Connexion...",
"disconnected": "Déconnecté",
"error": "Erreur de connexion",
"initializing": "Initialisation de la connexion...",
"preparing": "Préparation du transfert...",
"sending": "Transfert {{progress}} %",
"waiting_qr_scan": "Veuillez scanner le code QR pour vous connecter"
},
"title": "Transmission en réseau local",
"transfer_progress": "Progression du transfert"
},
"title": "Exporter vers le téléphone"
},
"hour_interval_one": "{{count}} heure",
"hour_interval_other": "{{count}} heures",
"joplin": {
@@ -3829,12 +3783,6 @@
"noPromptsAvailable": "Aucune invite disponible",
"requiredField": "Champ obligatoire"
},
"protocolInstallWarning": {
"command": "Commande de démarrage",
"message": "Ce MCP a été installé depuis une source externe via le protocole. L'exécution d'outils inconnus peut endommager votre ordinateur.",
"run": "Courir",
"title": "Exécuter un MCP externe ?"
},
"provider": "Поставщик",
"providerPlaceholder": "Название поставщика",
"providerUrl": "Адрес поставщика",
@@ -4200,6 +4148,7 @@
},
"anthropic_api_host": "Adresse API Anthropic",
"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.",
"api": {
"key": {
@@ -4244,11 +4193,10 @@
"url": {
"preview": "Aperçu : {{url}}",
"reset": "Réinitialiser",
"tip": "forcer l'utilisation de l'adresse d'entrée si terminé par #"
"tip": "Ignorer la version v1 si terminé par /, forcer l'utilisation de l'adresse d'entrée si terminé par #"
}
},
"api_host": "Adresse API",
"api_host_no_valid": "Adresse API invalide",
"api_host_preview": "Aperçu : {{url}}",
"api_host_tooltip": "Remplacer seulement lorsque le fournisseur nécessite une adresse compatible OpenAI personnalisée.",
"api_key": {

View File

@@ -1,7 +1,6 @@
{
"agent": {
"add": {
"description": "さまざまなツールを使って複雑なタスクを処理する",
"error": {
"failed": "エージェントの追加に失敗しました",
"invalid_agent": "無効なエージェント"
@@ -548,12 +547,8 @@
"chat": {
"add": {
"assistant": {
"description": "日常会話と簡単なQ&A",
"title": "アシスタントを追加"
},
"option": {
"title": "タイプを選択"
},
"topic": {
"title": "新しいトピック"
}
@@ -843,7 +838,7 @@
"label": "コンテキスト",
"tip": "コンテキストに保持する以前のメッセージの数"
},
"max": "制限なし",
"max": "最大",
"max_tokens": {
"confirm": "最大トークン数",
"confirm_content": "最大トークン数を設定すると、モデルが生成できる最大トークン数が制限されます。これにより、返される結果の長さに影響が出る可能性があります。モデルのコンテキスト制限に基づいて設定する必要があります。そうしないとエラーが発生します",
@@ -1052,12 +1047,10 @@
"clear": "クリア",
"close": "閉じる",
"collapse": "折りたたむ",
"completed": "完了",
"confirm": "確認",
"copied": "コピーされました",
"copy": "コピー",
"copy_failed": "コピーに失敗しました",
"current": "現在",
"cut": "切り取り",
"default": "デフォルト",
"delete": "削除",
@@ -2928,14 +2921,15 @@
},
"description": "クリエイターのための強力なAIアシスタント",
"downloading": "ダウンロード中...",
"enterprise": {
"title": "エンタープライズ"
},
"feedback": {
"button": "フィードバック",
"title": "フィードバック"
},
"label": "について",
"license": {
"button": "ライセンス",
"title": "ライセンス"
},
"releases": {
"button": "リリース",
"title": "リリースノート"
@@ -3043,46 +3037,6 @@
"title": "エクスポートメニュー設定",
"yuque": "語雀にエクスポート"
},
"export_to_phone": {
"confirm": {
"button": "バックアップファイルを選択"
},
"content": "一部のデータ、チャット履歴や設定をエクスポートします。バックアップには時間がかかる場合がありますので、しばらくお待ちください。",
"lan": {
"auto_close_tip": "{{seconds}}秒後に自動的に閉じます...",
"confirm_close_message": "ファイル転送が進行中です。閉じると転送が中断されます。強制終了してもよろしいですか?",
"confirm_close_title": "閉じることを確認",
"connected": "接続済み",
"connection_failed": "接続に失敗しました",
"content": "コンピューターとスマートフォンが同じネットワークに接続されていることを確認し、ローカルエリアネットワーク転送を使用してください。Cherry Studioアプリを開き、このQRコードをスキャンしてください。",
"error": {
"init_failed": "初期化に失敗しました",
"no_file": "ファイルが選択されていません",
"no_ip": "IPアドレスを取得できません",
"send_failed": "ファイルの送信に失敗しました"
},
"force_close": "強制終了",
"generating_qr": "QRコードを生成中...",
"noZipSelected": "圧縮ファイルが選択されていません",
"scan_qr": "携帯電話でQRコードをスキャンしてください",
"selectZip": "圧縮ファイルを選択",
"sendZip": "データの復元を開始します",
"status": {
"completed": "転送完了",
"connected": "接続済み",
"connecting": "接続中...",
"disconnected": "切断されました",
"error": "接続エラー",
"initializing": "接続を初期化中...",
"preparing": "転送準備中...",
"sending": "転送中 {{progress}}%",
"waiting_qr_scan": "QRコードをスキャンして接続してください"
},
"title": "LAN転送",
"transfer_progress": "転送進行"
},
"title": "スマートフォンにエクスポート"
},
"hour_interval_one": "{{count}} 時間",
"hour_interval_other": "{{count}} 時間",
"joplin": {
@@ -3829,12 +3783,6 @@
"noPromptsAvailable": "利用可能なプロンプトはありません",
"requiredField": "必須フィールド"
},
"protocolInstallWarning": {
"command": "起動コマンド",
"message": "このMCPは外部ソースからプロトコル経由でインストールされました。不明なツールを実行すると、コンピューターに危害を及ぼす可能性があります。",
"run": "走る",
"title": "外部のMCPを実行しますか"
},
"provider": "プロバイダー",
"providerPlaceholder": "プロバイダー名",
"providerUrl": "プロバイダーURL",
@@ -4200,6 +4148,7 @@
},
"anthropic_api_host": "Anthropic APIアドレス",
"anthropic_api_host_preview": "Anthropic プレビュー:{{url}}",
"anthropic_api_host_tip": "サービスプロバイダーがAnthropic互換のアドレスを提供する場合のみ入力してください。/で終わる場合は自動追加されるv1を無視し、#で終わる場合は元のアドレスを強制的に使用します。",
"anthropic_api_host_tooltip": "サービスプロバイダーがClaude互換のベースアドレスを提供する場合のみ入力してください。",
"api": {
"key": {
@@ -4244,11 +4193,10 @@
"url": {
"preview": "プレビュー: {{url}}",
"reset": "リセット",
"tip": "#で終わる場合、入力されたアドレスを強制的に使用します"
"tip": "/で終わる場合、v1を無視します。#で終わる場合、入力されたアドレスを強制的に使用します"
}
},
"api_host": "APIホスト",
"api_host_no_valid": "APIアドレスが無効です",
"api_host_preview": "プレビュー:{{url}}",
"api_host_tooltip": "サービスプロバイダーがカスタムOpenAI互換アドレスを必要とする場合のみ上書きしてください。",
"api_key": {

View File

@@ -1,7 +1,6 @@
{
"agent": {
"add": {
"description": "Lide com tarefas complexas usando várias ferramentas",
"error": {
"failed": "Falha ao adicionar agente",
"invalid_agent": "Agent inválido"
@@ -548,12 +547,8 @@
"chat": {
"add": {
"assistant": {
"description": "Conversas diárias e perguntas e respostas rápidas",
"title": "Adicionar assistente"
},
"option": {
"title": "Selecionar Tipo"
},
"topic": {
"title": "Novo Tópico"
}
@@ -1052,12 +1047,10 @@
"clear": "Limpar",
"close": "Fechar",
"collapse": "Recolher",
"completed": "Concluído",
"confirm": "Confirmar",
"copied": "Copiado",
"copy": "Copiar",
"copy_failed": "Falha ao copiar",
"current": "Atual",
"cut": "Cortar",
"default": "Padrão",
"delete": "Excluir",
@@ -2928,14 +2921,15 @@
},
"description": "Um assistente de IA criado para criadores",
"downloading": "Baixando atualizações...",
"enterprise": {
"title": "Empresa"
},
"feedback": {
"button": "Feedback",
"title": "Enviar feedback"
},
"label": "Sobre Nós",
"license": {
"button": "Ver",
"title": "Licença"
},
"releases": {
"button": "Ver",
"title": "Registro de alterações"
@@ -3043,46 +3037,6 @@
"title": "Exportar Configurações do Menu",
"yuque": "Exportar para Yuque"
},
"export_to_phone": {
"confirm": {
"button": "Selecionar arquivo de backup"
},
"content": "Exportar parte dos dados, incluindo registros de conversas e configurações. Observe que o processo de backup pode demorar um pouco; agradecemos sua paciência.",
"lan": {
"auto_close_tip": "Fechando automaticamente em {{seconds}} segundos...",
"confirm_close_message": "Transferência de arquivo em andamento. Fechar irá interromper a transferência. Tem certeza de que deseja forçar o fechamento?",
"confirm_close_title": "Confirmar Fechamento",
"connected": "Conectado",
"connection_failed": "Falha na conexão",
"content": "Certifique-se de que o computador e o telefone estejam na mesma rede para usar a transferência via LAN. Abra o aplicativo Cherry Studio e escaneie este código QR.",
"error": {
"init_failed": "Falha na inicialização",
"no_file": "Nenhum arquivo selecionado",
"no_ip": "Incapaz de obter endereço IP",
"send_failed": "Falha ao enviar arquivo"
},
"force_close": "Forçar Fechamento",
"generating_qr": "Gerando código QR...",
"noZipSelected": "Nenhum arquivo de compressão selecionado",
"scan_qr": "Por favor, escaneie o código QR com o seu telefone",
"selectZip": "Selecionar arquivo compactado",
"sendZip": "Iniciar recuperação de dados",
"status": {
"completed": "Transferência concluída",
"connected": "Conectado",
"connecting": "Conectando...",
"disconnected": "Desconectado",
"error": "Erro de conexão",
"initializing": "Inicializando conexão...",
"preparing": "Preparando transferência...",
"sending": "Transferindo {{progress}}%",
"waiting_qr_scan": "Por favor, escaneie o código QR para conectar"
},
"title": "transmissão de rede local",
"transfer_progress": "Progresso da transferência"
},
"title": "Exportar para o telemóvel"
},
"hour_interval_one": "{{count}} hora",
"hour_interval_other": "{{count}} horas",
"joplin": {
@@ -3829,12 +3783,6 @@
"noPromptsAvailable": "Nenhuma dica disponível",
"requiredField": "Campo obrigatório"
},
"protocolInstallWarning": {
"command": "Comando de inicialização",
"message": "Este MCP foi instalado a partir de uma fonte externa via protocolo. Executar ferramentas desconhecidas pode prejudicar seu computador.",
"run": "Correr",
"title": "Executar MCP externo?"
},
"provider": "Fornecedor",
"providerPlaceholder": "Nome do Fornecedor",
"providerUrl": "URL do Fornecedor",
@@ -4200,6 +4148,7 @@
},
"anthropic_api_host": "Endereço da API Anthropic",
"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.",
"api": {
"key": {
@@ -4244,11 +4193,10 @@
"url": {
"preview": "Pré-visualização: {{url}}",
"reset": "Redefinir",
"tip": "e forçar o uso do endereço original quando terminar com '#'"
"tip": "Ignorar v1 na versão finalizada com /, usar endereço de entrada forçado se terminar com #"
}
},
"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_tooltip": "Substituir apenas quando o fornecedor necessita de um endereço compatível com OpenAI personalizado.",
"api_key": {

View File

@@ -1,7 +1,6 @@
{
"agent": {
"add": {
"description": "Справляйтесь со сложными задачами с помощью различных инструментов",
"error": {
"failed": "Не удалось добавить агента",
"invalid_agent": "Недействительный агент"
@@ -548,12 +547,8 @@
"chat": {
"add": {
"assistant": {
"description": "Ежедневные разговоры и быстрые вопросы и ответы",
"title": "Добавить ассистента"
},
"option": {
"title": "Выберите тип"
},
"topic": {
"title": "Новый топик"
}
@@ -843,7 +838,7 @@
"label": "Контекст",
"tip": "Количество предыдущих сообщений, которые нужно сохранить в контексте."
},
"max": "без ограничений",
"max": "Максимум",
"max_tokens": {
"confirm": "Максимальное количество токенов",
"confirm_content": "Установить максимальное количество токенов, влияет на длину результата. Нужно учитывать контекст модели, иначе будет ошибка",
@@ -1052,12 +1047,10 @@
"clear": "Очистить",
"close": "Закрыть",
"collapse": "Свернуть",
"completed": "Завершено",
"confirm": "Подтверждение",
"copied": "Скопировано",
"copy": "Копировать",
"copy_failed": "Не удалось скопировать",
"current": "Текущий",
"cut": "Вырезать",
"default": "По умолчанию",
"delete": "Удалить",
@@ -2928,14 +2921,15 @@
},
"description": "Мощный AI-ассистент для созидания",
"downloading": "Загрузка...",
"enterprise": {
"title": "Предприятие"
},
"feedback": {
"button": "Обратная связь",
"title": "Обратная связь"
},
"label": "О программе и обратная связь",
"license": {
"button": "Лицензия",
"title": "Лицензия"
},
"releases": {
"button": "Релизы",
"title": "Заметки о релизах"
@@ -3043,46 +3037,6 @@
"title": "Настройки меню экспорта",
"yuque": "Экспорт в Yuque"
},
"export_to_phone": {
"confirm": {
"button": "Выберите файл резервной копии"
},
"content": "Экспорт части данных, включая чат и настройки. Пожалуйста, обратите внимание, что процесс резервного копирования может занять некоторое время. Благодарим за ваше терпение.",
"lan": {
"auto_close_tip": "Автоматическое закрытие через {{seconds}} секунд...",
"confirm_close_message": "Передача файла в процессе. Закрытие прервет передачу. Вы уверены, что хотите принудительно закрыть?",
"confirm_close_title": "Подтвердить закрытие",
"connected": "Подключено",
"connection_failed": "Соединение не удалось",
"content": "Убедитесь, что компьютер и телефон подключены к одной сети, чтобы использовать локальную передачу. Откройте приложение Cherry Studio и отсканируйте этот QR-код.",
"error": {
"init_failed": "Инициализация не удалась",
"no_file": "Файл не выбран",
"no_ip": "Не удалось получить IP-адрес",
"send_failed": "Не удалось отправить файл"
},
"force_close": "Принудительное закрытие",
"generating_qr": "Генерация QR-кода...",
"noZipSelected": "Архив не выбран",
"scan_qr": "Пожалуйста, отсканируйте QR-код с помощью вашего телефона",
"selectZip": "Выберите архив",
"sendZip": "Начать восстановление данных",
"status": {
"completed": "Перевод завершён",
"connected": "Подключено",
"connecting": "Подключение...",
"disconnected": "Отключено",
"error": "Ошибка подключения",
"initializing": "Инициализация соединения...",
"preparing": "Подготовка передачи...",
"sending": "Передача {{progress}}%",
"waiting_qr_scan": "Пожалуйста, отсканируйте QR-код для подключения"
},
"title": "Передача по локальной сети",
"transfer_progress": "Прогресс передачи"
},
"title": "Экспорт на телефон"
},
"hour_interval_one": "{{count}} час",
"hour_interval_other": "{{count}} часов",
"joplin": {
@@ -3829,12 +3783,6 @@
"noPromptsAvailable": "Нет доступных подсказок",
"requiredField": "Обязательное поле"
},
"protocolInstallWarning": {
"command": "Команда запуска",
"message": "Этот MCP был установлен из внешнего источника через протокол. Запуск неизвестных инструментов может повредить ваш компьютер.",
"run": "Беги",
"title": "Запускать внешний MCP?"
},
"provider": "Провайдер",
"providerPlaceholder": "Имя провайдера",
"providerUrl": "URL провайдера",
@@ -4200,6 +4148,7 @@
},
"anthropic_api_host": "Адрес API Anthropic",
"anthropic_api_host_preview": "Предпросмотр Anthropic: {{url}}",
"anthropic_api_host_tip": "Заполняйте только если провайдер предоставляет совместимый с Anthropic адрес. Окончание на / игнорирует автоматически добавляемое v1, окончание на # принудительно использует оригинальный адрес.",
"anthropic_api_host_tooltip": "Заполняйте только когда провайдер предоставляет базовый адрес, совместимый с Claude.",
"api": {
"key": {
@@ -4244,11 +4193,10 @@
"url": {
"preview": "Предпросмотр: {{url}}",
"reset": "Сброс",
"tip": "заканчивая на # принудительно использует введенный адрес"
"tip": "Заканчивая на / игнорирует v1, заканчивая на # принудительно использует введенный адрес"
}
},
"api_host": "Хост API",
"api_host_no_valid": "Недопустимый адрес API",
"api_host_preview": "Предпросмотр: {{url}}",
"api_host_tooltip": "Переопределяйте только когда провайдер требует пользовательский адрес, совместимый с OpenAI.",
"api_key": {

Some files were not shown because too many files have changed in this diff Show More