Compare commits
28 Commits
refactor/o
...
feat/termi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c758d1400 | ||
|
|
a4b19f7650 | ||
|
|
97b3608786 | ||
|
|
793625d009 | ||
|
|
00ee90a857 | ||
|
|
1200f9973c | ||
|
|
4f107e9305 | ||
|
|
eaf4bd18a3 | ||
|
|
cf008ca22e | ||
|
|
851ff8992f | ||
|
|
91f9088436 | ||
|
|
c971daf23c | ||
|
|
0c7cee2700 | ||
|
|
dfbfc2869c | ||
|
|
1575e97168 | ||
|
|
e0a2ed0481 | ||
|
|
3e9d9f16d6 | ||
|
|
f3a279d8de | ||
|
|
5790c12011 | ||
|
|
352ecbc506 | ||
|
|
fc4f30feab | ||
|
|
888a183328 | ||
|
|
9a01e092f6 | ||
|
|
5986800c9d | ||
|
|
56d68276e1 | ||
|
|
29c1173365 | ||
|
|
c7ceb3035d | ||
|
|
7bcae6fba2 |
6
.github/workflows/issue-management.yml
vendored
6
.github/workflows/issue-management.yml
vendored
@@ -29,8 +29,10 @@ jobs:
|
||||
days-before-close: 0 # Close immediately after stale
|
||||
stale-issue-label: 'inactive'
|
||||
close-issue-label: 'closed:no-response'
|
||||
exempt-all-milestones: true
|
||||
exempt-all-assignees: true
|
||||
stale-issue-message: |
|
||||
This issue has been labeled as needing more information and has been inactive for ${{ env.daysBeforeStale }} days.
|
||||
This issue has been labeled as needing more information and has been inactive for ${{ env.daysBeforeStale }} days.
|
||||
It will be closed now due to lack of additional information.
|
||||
|
||||
该问题被标记为"需要更多信息"且已经 ${{ env.daysBeforeStale }} 天没有任何活动,将立即关闭。
|
||||
@@ -46,6 +48,8 @@ jobs:
|
||||
days-before-stale: ${{ env.daysBeforeStale }}
|
||||
days-before-close: ${{ env.daysBeforeClose }}
|
||||
stale-issue-label: 'inactive'
|
||||
exempt-all-milestones: true
|
||||
exempt-all-assignees: true
|
||||
stale-issue-message: |
|
||||
This issue has been inactive for a prolonged period and will be closed automatically in ${{ env.daysBeforeClose }} days.
|
||||
该问题已长时间处于闲置状态,${{ env.daysBeforeClose }} 天后将自动关闭。
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index 69ab1599c76801dc1167551b6fa283dded123466..f0af43bba7ad1196fe05338817e65b4ebda40955 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||
|
||||
// src/get-model-path.ts
|
||||
function getModelPath(modelId) {
|
||||
- return modelId.includes("/") ? modelId : `models/${modelId}`;
|
||||
+ return modelId?.includes("models/") ? modelId : `models/${modelId}`;
|
||||
}
|
||||
|
||||
// src/google-generative-ai-options.ts
|
||||
26
.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch
vendored
Normal file
26
.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
diff --git a/dist/index.js b/dist/index.js
|
||||
index 4cc66d83af1cef39f6447dc62e680251e05ddf9f..eb9819cb674c1808845ceb29936196c4bb355172 100644
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -471,7 +471,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||
|
||||
// src/get-model-path.ts
|
||||
function getModelPath(modelId) {
|
||||
- return modelId.includes("/") ? modelId : `models/${modelId}`;
|
||||
+ return modelId.includes("models/") ? modelId : `models/${modelId}`;
|
||||
}
|
||||
|
||||
// src/google-generative-ai-options.ts
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index a032505ec54e132dc386dde001dc51f710f84c83..5efada51b9a8b56e3f01b35e734908ebe3c37043 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||
|
||||
// src/get-model-path.ts
|
||||
function getModelPath(modelId) {
|
||||
- return modelId.includes("/") ? modelId : `models/${modelId}`;
|
||||
+ return modelId.includes("models/") ? modelId : `models/${modelId}`;
|
||||
}
|
||||
|
||||
// src/google-generative-ai-options.ts
|
||||
76
.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch
vendored
Normal file
76
.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
diff --git a/dist/index.js b/dist/index.js
|
||||
index cc6652c4e7f32878a64a2614115bf7eeb3b7c890..76e989017549c89b45d633525efb1f318026d9b2 100644
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -274,6 +274,7 @@ var openaiChatResponseSchema = (0, import_provider_utils3.lazyValidator)(
|
||||
message: import_v42.z.object({
|
||||
role: import_v42.z.literal("assistant").nullish(),
|
||||
content: import_v42.z.string().nullish(),
|
||||
+ reasoning_content: import_v42.z.string().nullish(),
|
||||
tool_calls: import_v42.z.array(
|
||||
import_v42.z.object({
|
||||
id: import_v42.z.string().nullish(),
|
||||
@@ -340,6 +341,7 @@ var openaiChatChunkSchema = (0, import_provider_utils3.lazyValidator)(
|
||||
delta: import_v42.z.object({
|
||||
role: import_v42.z.enum(["assistant"]).nullish(),
|
||||
content: import_v42.z.string().nullish(),
|
||||
+ reasoning_content: import_v42.z.string().nullish(),
|
||||
tool_calls: import_v42.z.array(
|
||||
import_v42.z.object({
|
||||
index: import_v42.z.number(),
|
||||
@@ -785,6 +787,14 @@ var OpenAIChatLanguageModel = class {
|
||||
if (text != null && text.length > 0) {
|
||||
content.push({ type: "text", text });
|
||||
}
|
||||
+ const reasoning =
|
||||
+ choice.message.reasoning_content;
|
||||
+ if (reasoning != null && reasoning.length > 0) {
|
||||
+ content.push({
|
||||
+ type: 'reasoning',
|
||||
+ text: reasoning,
|
||||
+ });
|
||||
+ }
|
||||
for (const toolCall of (_a = choice.message.tool_calls) != null ? _a : []) {
|
||||
content.push({
|
||||
type: "tool-call",
|
||||
@@ -866,6 +876,7 @@ var OpenAIChatLanguageModel = class {
|
||||
};
|
||||
let isFirstChunk = true;
|
||||
let isActiveText = false;
|
||||
+ let isActiveReasoning = false;
|
||||
const providerMetadata = { openai: {} };
|
||||
return {
|
||||
stream: response.pipeThrough(
|
||||
@@ -920,6 +931,22 @@ var OpenAIChatLanguageModel = class {
|
||||
return;
|
||||
}
|
||||
const delta = choice.delta;
|
||||
+ const reasoningContent = delta.reasoning_content;
|
||||
+ if (reasoningContent) {
|
||||
+ if (!isActiveReasoning) {
|
||||
+ controller.enqueue({
|
||||
+ type: 'reasoning-start',
|
||||
+ id: 'reasoning-0',
|
||||
+ });
|
||||
+ isActiveReasoning = true;
|
||||
+ }
|
||||
+
|
||||
+ controller.enqueue({
|
||||
+ type: 'reasoning-delta',
|
||||
+ id: 'reasoning-0',
|
||||
+ delta: reasoningContent,
|
||||
+ });
|
||||
+ }
|
||||
if (delta.content != null) {
|
||||
if (!isActiveText) {
|
||||
controller.enqueue({ type: "text-start", id: "0" });
|
||||
@@ -1032,6 +1059,9 @@ var OpenAIChatLanguageModel = class {
|
||||
}
|
||||
},
|
||||
flush(controller) {
|
||||
+ if (isActiveReasoning) {
|
||||
+ controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' });
|
||||
+ }
|
||||
if (isActiveText) {
|
||||
controller.enqueue({ type: "text-end", id: "0" });
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
diff --git a/sdk.mjs b/sdk.mjs
|
||||
index 461e9a2ba246778261108a682762ffcf26f7224e..44bd667d9f591969d36a105ba5eb8b478c738dd8 100644
|
||||
index 10162e5b1624f8ce667768943347a6e41089ad2f..32568ae08946590e382270c88d85fba81187568e 100755
|
||||
--- a/sdk.mjs
|
||||
+++ b/sdk.mjs
|
||||
@@ -6215,7 +6215,7 @@ function createAbortController(maxListeners = DEFAULT_MAX_LISTENERS) {
|
||||
@@ -6213,7 +6213,7 @@ function createAbortController(maxListeners = DEFAULT_MAX_LISTENERS) {
|
||||
}
|
||||
|
||||
|
||||
// ../src/transport/ProcessTransport.ts
|
||||
-import { spawn } from "child_process";
|
||||
+import { fork } from "child_process";
|
||||
import { createInterface } from "readline";
|
||||
|
||||
|
||||
// ../src/utils/fsOperations.ts
|
||||
@@ -6473,14 +6473,11 @@ class ProcessTransport {
|
||||
@@ -6487,14 +6487,11 @@ class ProcessTransport {
|
||||
const errorMessage = isNativeBinary(pathToClaudeCodeExecutable) ? `Claude Code native binary not found at ${pathToClaudeCodeExecutable}. Please ensure Claude Code is installed via native installer or specify a valid path with options.pathToClaudeCodeExecutable.` : `Claude Code executable not found at ${pathToClaudeCodeExecutable}. Is options.pathToClaudeCodeExecutable set?`;
|
||||
throw new ReferenceError(errorMessage);
|
||||
}
|
||||
- const isNative = isNativeBinary(pathToClaudeCodeExecutable);
|
||||
- const spawnCommand = isNative ? pathToClaudeCodeExecutable : executable;
|
||||
- const spawnArgs = isNative ? args : [...executableArgs, pathToClaudeCodeExecutable, ...args];
|
||||
- this.logForDebugging(isNative ? `Spawning Claude Code native binary: ${pathToClaudeCodeExecutable} ${args.join(" ")}` : `Spawning Claude Code process: ${executable} ${[...executableArgs, pathToClaudeCodeExecutable, ...args].join(" ")}`);
|
||||
- const spawnArgs = isNative ? [...executableArgs, ...args] : [...executableArgs, pathToClaudeCodeExecutable, ...args];
|
||||
- this.logForDebugging(isNative ? `Spawning Claude Code native binary: ${spawnCommand} ${spawnArgs.join(" ")}` : `Spawning Claude Code process: ${spawnCommand} ${spawnArgs.join(" ")}`);
|
||||
+ this.logForDebugging(`Forking Claude Code Node.js process: ${pathToClaudeCodeExecutable} ${args.join(" ")}`);
|
||||
const stderrMode = env.DEBUG || stderr ? "pipe" : "ignore";
|
||||
- this.child = spawn(spawnCommand, spawnArgs, {
|
||||
@@ -10,8 +10,9 @@ This file provides guidance to AI coding assistants when working with code in th
|
||||
- **Build with HeroUI**: Use HeroUI for every new UI component; never add `antd` or `styled-components`.
|
||||
- **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`.
|
||||
- **Research via subagent**: Lean on `subagent` for external docs, APIs, news, and references.
|
||||
- **Seek review**: Ask a human developer to review substantial changes before merging.
|
||||
- **Commit in rhythm**: Keep commits small, conventional, and emoji-tagged.
|
||||
- **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications.
|
||||
- **Write conventional commits with emoji**: Commit small, focused changes using emoji-prefixed Conventional Commit messages (e.g., `✨ feat:`, `🐛 fix:`, `♻️ refactor:`, `
|
||||
📝 docs:`).
|
||||
|
||||
## Development Commands
|
||||
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
"quoteStyle": "single"
|
||||
}
|
||||
},
|
||||
"files": { "ignoreUnknown": false },
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**"],
|
||||
"maxSize": 2097152
|
||||
},
|
||||
"formatter": {
|
||||
"attributePosition": "auto",
|
||||
"bracketSameLine": false,
|
||||
|
||||
1093
docs/terminal-implementation.md
Normal file
1093
docs/terminal-implementation.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,10 @@ asarUnpack:
|
||||
extraResources:
|
||||
- from: "migrations/sqlite-drizzle"
|
||||
to: "migrations/sqlite-drizzle"
|
||||
# copy from node_modules/claude-code-plugins/plugins to resources/data/claude-code-pluginso
|
||||
- from: "./node_modules/claude-code-plugins/plugins/"
|
||||
to: "claude-code-plugins"
|
||||
|
||||
win:
|
||||
executableName: Cherry Studio
|
||||
artifactName: ${productName}-${version}-${arch}-setup.${ext}
|
||||
|
||||
16
package.json
16
package.json
@@ -81,7 +81,7 @@
|
||||
"release:aicore": "yarn workspace @cherrystudio/ai-core version patch --immediate && yarn workspace @cherrystudio/ai-core npm publish --access public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.1#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.1-d937b73fed.patch",
|
||||
"@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.25#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.25-08bbabb5d3.patch",
|
||||
"@libsql/client": "0.14.0",
|
||||
"@libsql/win32-x64-msvc": "^0.4.7",
|
||||
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
|
||||
@@ -89,6 +89,8 @@
|
||||
"express": "^5.1.0",
|
||||
"font-list": "^2.0.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"gray-matter": "^4.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsdom": "26.1.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"officeparser": "^4.2.0",
|
||||
@@ -104,8 +106,8 @@
|
||||
"@agentic/exa": "^7.3.3",
|
||||
"@agentic/searxng": "^7.3.3",
|
||||
"@agentic/tavily": "^7.3.3",
|
||||
"@ai-sdk/amazon-bedrock": "^3.0.35",
|
||||
"@ai-sdk/google-vertex": "^3.0.40",
|
||||
"@ai-sdk/amazon-bedrock": "^3.0.42",
|
||||
"@ai-sdk/google-vertex": "^3.0.48",
|
||||
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch",
|
||||
"@ai-sdk/mistral": "^2.0.19",
|
||||
"@ai-sdk/perplexity": "^2.0.13",
|
||||
@@ -198,6 +200,7 @@
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/he": "^1",
|
||||
"@types/html-to-text": "^9",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/markdown-it": "^14",
|
||||
"@types/md5": "^2.3.5",
|
||||
@@ -226,8 +229,9 @@
|
||||
"@vitest/web-worker": "^3.2.4",
|
||||
"@viz-js/lang-dot": "^1.0.5",
|
||||
"@viz-js/viz": "^3.14.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"ai": "^5.0.68",
|
||||
"ai": "^5.0.76",
|
||||
"antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
|
||||
"archiver": "^7.0.1",
|
||||
"async-mutex": "^0.5.0",
|
||||
@@ -237,6 +241,7 @@
|
||||
"check-disk-space": "3.4.0",
|
||||
"cheerio": "^1.1.2",
|
||||
"chokidar": "^4.0.3",
|
||||
"claude-code-plugins": "1.0.1",
|
||||
"cli-progress": "^3.12.0",
|
||||
"clsx": "^2.1.1",
|
||||
"code-inspector-plugin": "^0.20.14",
|
||||
@@ -389,7 +394,8 @@
|
||||
"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.20": "patch:@ai-sdk/google@npm%3A2.0.20#~/.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch",
|
||||
"@ai-sdk/google@npm:2.0.23": "patch:@ai-sdk/google@npm%3A2.0.23#~/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch",
|
||||
"@ai-sdk/openai@npm:^2.0.52": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch",
|
||||
"@img/sharp-darwin-arm64": "0.34.3",
|
||||
"@img/sharp-darwin-x64": "0.34.3",
|
||||
"@img/sharp-linux-arm": "0.34.3",
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
"ai": "^5.0.26"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^2.0.27",
|
||||
"@ai-sdk/azure": "^2.0.49",
|
||||
"@ai-sdk/anthropic": "^2.0.32",
|
||||
"@ai-sdk/azure": "^2.0.53",
|
||||
"@ai-sdk/deepseek": "^1.0.23",
|
||||
"@ai-sdk/openai": "^2.0.48",
|
||||
"@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch",
|
||||
"@ai-sdk/openai-compatible": "^1.0.22",
|
||||
"@ai-sdk/provider": "^2.0.0",
|
||||
"@ai-sdk/provider-utils": "^3.0.12",
|
||||
|
||||
@@ -96,6 +96,10 @@ export enum IpcChannel {
|
||||
AgentMessage_PersistExchange = 'agent-message:persist-exchange',
|
||||
AgentMessage_GetHistory = 'agent-message:get-history',
|
||||
|
||||
AgentToolPermission_Request = 'agent-tool-permission:request',
|
||||
AgentToolPermission_Response = 'agent-tool-permission:response',
|
||||
AgentToolPermission_Result = 'agent-tool-permission:result',
|
||||
|
||||
//copilot
|
||||
Copilot_GetAuthMessage = 'copilot:get-auth-message',
|
||||
Copilot_GetCopilotToken = 'copilot:get-copilot-token',
|
||||
@@ -382,5 +386,14 @@ export enum IpcChannel {
|
||||
Ovms_StopOVMS = 'ovms:stop-ovms',
|
||||
|
||||
// CherryAI
|
||||
Cherryai_GetSignature = 'cherryai:get-signature'
|
||||
Cherryai_GetSignature = 'cherryai:get-signature',
|
||||
|
||||
// Claude Code Plugins
|
||||
ClaudeCodePlugin_ListAvailable = 'claudeCodePlugin:list-available',
|
||||
ClaudeCodePlugin_Install = 'claudeCodePlugin:install',
|
||||
ClaudeCodePlugin_Uninstall = 'claudeCodePlugin:uninstall',
|
||||
ClaudeCodePlugin_ListInstalled = 'claudeCodePlugin:list-installed',
|
||||
ClaudeCodePlugin_InvalidateCache = 'claudeCodePlugin:invalidate-cache',
|
||||
ClaudeCodePlugin_ReadContent = 'claudeCodePlugin:read-content',
|
||||
ClaudeCodePlugin_WriteContent = 'claudeCodePlugin:write-content'
|
||||
}
|
||||
|
||||
368
packages/ui/DESIGN_SYSTEM.md
Normal file
368
packages/ui/DESIGN_SYSTEM.md
Normal file
@@ -0,0 +1,368 @@
|
||||
# Cherry Studio Design System 集成方案
|
||||
|
||||
本文档聚焦三个核心问题:
|
||||
|
||||
1. **如何将 todocss.css 集成到 Tailwind CSS v4**
|
||||
2. **如何在项目中使用集成后的设计系统**
|
||||
3. **如何平衡 UI 库和主包的需求**
|
||||
|
||||
---
|
||||
|
||||
## 一、集成策略
|
||||
|
||||
### 1.1 文件架构
|
||||
|
||||
```
|
||||
todocss.css (设计师提供)
|
||||
↓ 转换 & 优化
|
||||
design-tokens.css (--ds-* 变量)
|
||||
↓ @theme inline 映射
|
||||
globals.css (cs-* 工具类)
|
||||
↓ 开发者使用
|
||||
React Components
|
||||
```
|
||||
|
||||
### 1.2 核心转换规则
|
||||
|
||||
#### 变量简化
|
||||
|
||||
```css
|
||||
/* todocss.css */
|
||||
--Brand--Base_Colors--Primary: hsla(84, 81%, 44%, 1);
|
||||
|
||||
/* ↓ 转换为 design-tokens.css */
|
||||
--ds-primary: hsla(84, 81%, 44%, 1);
|
||||
|
||||
/* ↓ 映射到 globals.css */
|
||||
@theme inline {
|
||||
--color-cs-primary: var(--ds-primary);
|
||||
}
|
||||
|
||||
/* ↓ 生成工具类 */
|
||||
bg-cs-primary, text-cs-primary, border-cs-primary
|
||||
```
|
||||
|
||||
#### 去除冗余
|
||||
|
||||
- **间距/尺寸合并**: `--Spacing--md` 和 `--Sizing--md` 值相同 → 统一为 `--ds-size-md`
|
||||
- **透明度废弃**: `--Opacity--Red--Red-80` → 使用 `bg-cs-destructive/80`
|
||||
- **错误修正**: `--Font_weight--Regular: 400px` → `--ds-font-weight-regular: 400`
|
||||
|
||||
### 1.3 命名规范
|
||||
|
||||
| 层级 | 前缀 | 示例 | 用途 |
|
||||
|------|------|------|------|
|
||||
| 设计令牌 | `--ds-*` | `--ds-primary` | 定义值 |
|
||||
| Tailwind 映射 | `--color-cs-*` | `--color-cs-primary` | 生成工具类 |
|
||||
| 工具类 | `cs-*` | `bg-cs-primary` | 开发者使用 |
|
||||
|
||||
#### Tailwind v4 映射规则
|
||||
|
||||
| 变量前缀 | 生成的工具类 |
|
||||
|----------|-------------|
|
||||
| `--color-cs-*` | `bg-*`, `text-*`, `border-*`, `fill-*` |
|
||||
| `--spacing-cs-*` | `p-*`, `m-*`, `gap-*` |
|
||||
| `--size-cs-*` | `w-*`, `h-*`, `size-*` |
|
||||
| `--radius-cs-*` | `rounded-*` |
|
||||
| `--font-size-cs-*` | `text-*` |
|
||||
|
||||
### 1.4 为什么使用 @theme inline
|
||||
|
||||
```css
|
||||
/* ❌ @theme - 静态编译,不支持运行时主题切换 */
|
||||
@theme {
|
||||
--color-primary: var(--ds-primary);
|
||||
}
|
||||
|
||||
/* ✅ @theme inline - 保留变量引用,支持运行时切换 */
|
||||
@theme inline {
|
||||
--color-cs-primary: var(--ds-primary);
|
||||
}
|
||||
```
|
||||
|
||||
**关键差异**:`@theme inline` 使 CSS 变量在运行时动态解析,实现明暗主题切换。
|
||||
|
||||
---
|
||||
|
||||
## 二、项目使用指南
|
||||
|
||||
### 2.1 在 UI 库中使用
|
||||
|
||||
#### 文件结构
|
||||
|
||||
```
|
||||
packages/ui/
|
||||
├── src/styles/
|
||||
│ ├── design-tokens.css # 核心变量定义
|
||||
│ └── globals.css # Tailwind 集成
|
||||
└── package.json # 导出配置
|
||||
```
|
||||
|
||||
#### globals.css 示例
|
||||
|
||||
```css
|
||||
@import 'tailwindcss';
|
||||
@import './design-tokens.css';
|
||||
|
||||
@theme inline {
|
||||
/* 颜色 */
|
||||
--color-cs-primary: var(--ds-primary);
|
||||
--color-cs-bg: var(--ds-background);
|
||||
--color-cs-fg: var(--ds-foreground);
|
||||
|
||||
/* 间距 */
|
||||
--spacing-cs-xs: var(--ds-size-xs);
|
||||
--spacing-cs-sm: var(--ds-size-sm);
|
||||
--spacing-cs-md: var(--ds-size-md);
|
||||
|
||||
/* 尺寸 */
|
||||
--size-cs-xs: var(--ds-size-xs);
|
||||
--size-cs-sm: var(--ds-size-sm);
|
||||
|
||||
/* 圆角 */
|
||||
--radius-cs-sm: var(--ds-radius-sm);
|
||||
--radius-cs-md: var(--ds-radius-md);
|
||||
}
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
```
|
||||
|
||||
#### 组件中使用
|
||||
|
||||
```tsx
|
||||
// packages/ui/src/components/Button.tsx
|
||||
export const Button = ({ children }) => (
|
||||
<button className="
|
||||
bg-cs-primary
|
||||
text-white
|
||||
px-cs-sm
|
||||
py-cs-xs
|
||||
rounded-cs-md
|
||||
hover:bg-cs-primary/90
|
||||
transition-colors
|
||||
">
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
```
|
||||
|
||||
### 2.2 在主项目中使用
|
||||
|
||||
#### 导入 UI 库样式
|
||||
|
||||
```css
|
||||
/* src/renderer/src/assets/styles/tailwind.css */
|
||||
@import 'tailwindcss' source('../../../../renderer');
|
||||
@import '@cherrystudio/ui/styles/globals.css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
```
|
||||
|
||||
#### 覆盖或扩展变量
|
||||
|
||||
```css
|
||||
/* src/renderer/src/assets/styles/tailwind.css */
|
||||
@import '@cherrystudio/ui/styles/globals.css';
|
||||
|
||||
/* 主项目特定覆盖 */
|
||||
:root {
|
||||
--ds-primary: #custom-color; /* 覆盖 UI 库的主题色 */
|
||||
}
|
||||
```
|
||||
|
||||
#### 在主项目组件中使用
|
||||
|
||||
```tsx
|
||||
// src/renderer/src/pages/Home.tsx
|
||||
export const Home = () => (
|
||||
<div className="
|
||||
bg-cs-bg
|
||||
p-cs-md
|
||||
rounded-cs-lg
|
||||
">
|
||||
<Button>主项目按钮</Button>
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
### 2.3 主题切换实现
|
||||
|
||||
```tsx
|
||||
// App.tsx
|
||||
import { useState } from 'react'
|
||||
|
||||
export function App() {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light')
|
||||
|
||||
return (
|
||||
<div className={theme}>
|
||||
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
||||
切换主题
|
||||
</button>
|
||||
{/* 所有子组件自动响应主题 */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 透明度修饰符
|
||||
|
||||
```tsx
|
||||
<div className="
|
||||
bg-cs-primary/10 /* 10% 透明度 */
|
||||
bg-cs-primary/50 /* 50% 透明度 */
|
||||
bg-cs-primary/[0.15] /* 自定义透明度 */
|
||||
">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、UI 库与主包平衡策略
|
||||
|
||||
### 3.1 UI 库职责
|
||||
|
||||
**目标**:提供可复用、可定制的基础设计系统
|
||||
|
||||
```json
|
||||
// packages/ui/package.json
|
||||
{
|
||||
"exports": {
|
||||
"./styles/design-tokens.css": "./src/styles/design-tokens.css",
|
||||
"./styles/globals.css": "./src/styles/globals.css"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**原则**:
|
||||
|
||||
- ✅ 定义通用的设计令牌(`--ds-*`)
|
||||
- ✅ 提供默认的 Tailwind 映射(`--color-cs-*`)
|
||||
- ✅ 保持变量语义化,不包含业务逻辑
|
||||
- ❌ 不包含主项目特定的颜色或尺寸
|
||||
|
||||
### 3.2 主包职责
|
||||
|
||||
**目标**:导入 UI 库,根据业务需求扩展或覆盖
|
||||
|
||||
```css
|
||||
/* src/renderer/src/assets/styles/tailwind.css */
|
||||
@import '@cherrystudio/ui/styles/globals.css';
|
||||
|
||||
/* 主项目扩展 */
|
||||
@theme inline {
|
||||
--color-cs-brand-accent: #ff6b6b; /* 新增颜色 */
|
||||
}
|
||||
|
||||
/* 主项目覆盖 */
|
||||
:root {
|
||||
--ds-primary: #custom-primary; /* 覆盖 UI 库的主题色 */
|
||||
}
|
||||
```
|
||||
|
||||
**原则**:
|
||||
|
||||
- ✅ 导入 UI 库的 `globals.css`
|
||||
- ✅ 通过覆盖 `--ds-*` 变量定制主题
|
||||
- ✅ 添加项目特定的 `--color-cs-*` 映射
|
||||
- ✅ 保留向后兼容的旧变量(如 `color.css`)
|
||||
|
||||
### 3.3 向后兼容方案
|
||||
|
||||
#### 保留旧变量
|
||||
|
||||
```css
|
||||
/* src/renderer/src/assets/styles/color.css */
|
||||
:root {
|
||||
--color-primary: #00b96b; /* 旧变量 */
|
||||
--color-background: #181818; /* 旧变量 */
|
||||
}
|
||||
|
||||
/* 映射到新系统 */
|
||||
:root {
|
||||
--ds-primary: var(--color-primary);
|
||||
--ds-background: var(--color-background);
|
||||
}
|
||||
```
|
||||
|
||||
#### 渐进式迁移
|
||||
|
||||
```tsx
|
||||
// 阶段 1:旧代码继续工作
|
||||
<div style={{ color: 'var(--color-primary)' }}>旧代码</div>
|
||||
|
||||
// 阶段 2:新代码使用工具类
|
||||
<div className="text-cs-primary">新代码</div>
|
||||
|
||||
// 阶段 3:逐步替换旧代码
|
||||
```
|
||||
|
||||
### 3.4 冲突处理
|
||||
|
||||
| 场景 | 策略 |
|
||||
|------|------|
|
||||
| UI 库与 Tailwind 默认类冲突 | 使用 `cs-` 前缀隔离 |
|
||||
| 主包需要覆盖 UI 库颜色 | 覆盖 `--ds-*` 变量 |
|
||||
| 主包需要新增颜色 | 添加新的 `--color-cs-*` 映射 |
|
||||
| 旧变量与新系统共存 | 通过 `var()` 映射到 `--ds-*` |
|
||||
|
||||
### 3.5 独立发布 UI 库
|
||||
|
||||
```json
|
||||
// packages/ui/package.json
|
||||
{
|
||||
"name": "@cherrystudio/ui",
|
||||
"exports": {
|
||||
"./styles/design-tokens.css": "./src/styles/design-tokens.css",
|
||||
"./styles/globals.css": "./src/styles/globals.css"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": "^4.1.13"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**外部项目使用**:
|
||||
```css
|
||||
/* 其他项目的 tailwind.css */
|
||||
@import 'tailwindcss';
|
||||
@import '@cherrystudio/ui/styles/globals.css';
|
||||
|
||||
/* 覆盖主题色 */
|
||||
:root {
|
||||
--ds-primary: #your-brand-color;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、完整映射示例
|
||||
|
||||
### todocss.css → design-tokens.css
|
||||
|
||||
| todocss.css | design-tokens.css | 说明 |
|
||||
|-------------|-------------------|------|
|
||||
| `--Brand--Base_Colors--Primary` | `--ds-primary` | 简化命名 |
|
||||
| `--Spacing--md` + `--Sizing--md` | `--ds-size-md` | 合并重复 |
|
||||
| `--Opacity--Red--Red-80` | *(删除)* | 使用 `/80` 修饰符 |
|
||||
| `--Font_weight--Regular: 400px` | `--ds-font-weight-regular: 400` | 修正错误 |
|
||||
| `--Brand--UI_Element_Colors--Primary_Button--Background` | `--ds-btn-primary` | 简化语义 |
|
||||
|
||||
### design-tokens.css → globals.css → 工具类
|
||||
|
||||
| design-tokens.css | globals.css | 工具类 |
|
||||
|-------------------|-------------|--------|
|
||||
| `--ds-primary` | `--color-cs-primary` | `bg-cs-primary` |
|
||||
| `--ds-size-md` | `--spacing-cs-md` | `p-cs-md` |
|
||||
| `--ds-size-md` | `--size-cs-md` | `w-cs-md` |
|
||||
| `--ds-radius-lg` | `--radius-cs-lg` | `rounded-cs-lg` |
|
||||
|
||||
---
|
||||
|
||||
## 五、关键决策记录
|
||||
|
||||
1. **使用 `@theme inline`** - 支持运行时主题切换
|
||||
2. **`cs-` 前缀** - 命名空间隔离,避免冲突
|
||||
3. **合并 Spacing/Sizing** - 消除冗余
|
||||
4. **废弃 Opacity 变量** - 使用 Tailwind 的 `/modifier` 语法
|
||||
5. **双层变量系统** - `--ds-*` (定义) → `--color-cs-*` (映射)
|
||||
6. **共存策略** - Tailwind 默认类 + `cs-` 品牌类
|
||||
@@ -1,4 +1,26 @@
|
||||
# UI Component Library Migration Status
|
||||
# Cherry Studio UI Migration Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the detailed plan for migrating Cherry Studio from antd + styled-components to shadcn/ui + Tailwind CSS. We will adopt a progressive migration strategy to ensure system stability and development efficiency, while gradually implementing UI refactoring in collaboration with UI designers.
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Target Tech Stack
|
||||
|
||||
- **UI Component Library**: shadcn/ui (replacing antd and previously migrated HeroUI)
|
||||
- **Styling Solution**: Tailwind CSS (replacing styled-components)
|
||||
- **Design System**: Custom CSS variable system (see [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md))
|
||||
- **Theme System**: CSS variables + shadcn/ui theme
|
||||
|
||||
### Migration Principles
|
||||
|
||||
1. **Backward Compatibility**: Old components continue working until new components are fully available
|
||||
2. **Progressive Migration**: Migrate components one by one to avoid large-scale rewrites
|
||||
3. **Feature Parity**: Ensure new components have all the functionality of old components
|
||||
4. **Design Consistency**: Follow new design system specifications (see DESIGN_SYSTEM.md)
|
||||
5. **Performance Priority**: Optimize bundle size and rendering performance
|
||||
6. **Designer Collaboration**: Work with UI designers for gradual component encapsulation and UI optimization
|
||||
|
||||
## Usage Example
|
||||
|
||||
@@ -24,115 +46,68 @@ function MyComponent() {
|
||||
@packages/ui/
|
||||
├── src/
|
||||
│ ├── components/ # Main components directory
|
||||
│ │ ├── 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)
|
||||
│ │ ├── 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.)
|
||||
│ ├── hooks/ # Custom React Hooks
|
||||
│ └── types/ # TypeScript type definitions
|
||||
│ ├── styles/ # Global styles and CSS variables
|
||||
│ ├── types/ # TypeScript type definitions
|
||||
│ ├── utils/ # Utility functions
|
||||
│ └── index.ts # Main export file
|
||||
```
|
||||
|
||||
### Component Classification Guide
|
||||
|
||||
When submitting PRs, please place components in the correct directory based on their function:
|
||||
|
||||
- **base**: Most basic UI elements like buttons, inputs, switches, labels, etc.
|
||||
- **display**: Components for displaying content like cards, lists, tables, tabs, etc.
|
||||
- **layout**: Components for page layout like containers, grid systems, dividers, etc.
|
||||
- **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
|
||||
- **icons**: All icon-related components
|
||||
- **interactive**: Components requiring user interaction like modals, drawers, tooltips, dropdowns, etc.
|
||||
- **composite**: Composite components made from multiple base 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.)
|
||||
|
||||
## Migration Overview
|
||||
## Component Extraction Criteria
|
||||
|
||||
- **Total Components**: 236
|
||||
- **Migrated**: 34
|
||||
- **Refactored**: 18
|
||||
- **Pending Migration**: 184
|
||||
### Extraction Standards
|
||||
|
||||
## Component Status Table
|
||||
1. **Usage Frequency**: Component is used in ≥ 3 places in the codebase
|
||||
2. **Future Reusability**: Expected to be used in multiple scenarios in the future
|
||||
3. **Business Complexity**: Component contains complex interaction logic or state management
|
||||
4. **Maintenance Cost**: Centralized management can reduce maintenance overhead
|
||||
5. **Design Consistency**: Components that require unified visual and interaction experience
|
||||
6. **Test Coverage**: As common components, they facilitate unit test writing and maintenance
|
||||
|
||||
| Category | Component Name | Migration Status | Refactoring Status | Description |
|
||||
| ----------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **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) |
|
||||
### 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
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### 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
|
||||
| Phase | Status | Main Tasks | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| **Phase 1** | 🚧 **In Progress** | **Design System Integration** | • Integrate design system CSS variables (todocss.css → design-tokens.css → globals.css)<br>• Configure Tailwind CSS to use custom design tokens<br>• Establish basic style guidelines and theme system |
|
||||
| **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 |
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -143,9 +118,27 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
|
||||
2. **Can migrate** but need decoupling later:
|
||||
- Components using i18n (change i18n to props)
|
||||
- Components using antd (replace with HeroUI later)
|
||||
- Components using antd (replace with shadcn/ui later)
|
||||
- Components using HeroUI (replace with shadcn/ui later)
|
||||
|
||||
3. **Submission Guidelines**:
|
||||
- Each PR should focus on one category of components
|
||||
- Ensure all migrated components are exported
|
||||
- Update migration status in this document
|
||||
- Follow component extraction criteria, only migrate qualified components
|
||||
|
||||
## Design System Integration
|
||||
|
||||
### CSS Variable System
|
||||
- Refer to [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md) for complete design system planning
|
||||
- Design variables will be managed through CSS variable system, naming conventions TBD
|
||||
- Support theme switching and responsive design
|
||||
|
||||
### Migration Priority Adjustment
|
||||
1. **High Priority**: Basic components (buttons, inputs, tags, etc.)
|
||||
2. **Medium Priority**: Display components (cards, lists, tables, etc.)
|
||||
3. **Low Priority**: Composite components and business-coupled components
|
||||
|
||||
### UI Designer Collaboration
|
||||
- All component designs need confirmation from UI designers
|
||||
- Gradually implement UI refactoring to maintain visual consistency
|
||||
- New components must comply with design system specifications
|
||||
870
packages/ui/todocss.css
Normal file
870
packages/ui/todocss.css
Normal file
@@ -0,0 +1,870 @@
|
||||
:root {
|
||||
/* Typography: Desktop mode */
|
||||
--Font_family--Heading: Inter;
|
||||
--Font_weight--Regular: 400px;
|
||||
--Font_size--Heading--2xl: 60px;
|
||||
--Font_size--Heading--xl: 48px;
|
||||
--Font_size--Heading--lg: 40px;
|
||||
--Font_size--Heading--md: 32px;
|
||||
--Font_size--Heading--sm: 24px;
|
||||
--Font_size--Heading--xs: 20px;
|
||||
--Line_height--Heading--xl: 80px;
|
||||
--Line_height--Body--lg: 28px;
|
||||
--Line_height--Body--md: 24px;
|
||||
--Line_height--Body--sm: 24px;
|
||||
--Line_height--Body--xs: 20px;
|
||||
--Paragraph_spacing--Body--lg: 18px;
|
||||
--Paragraph_spacing--Body--md: 16px;
|
||||
--Paragraph_spacing--Body--sm: 14px;
|
||||
--Paragraph_spacing--Body--xs: 12px;
|
||||
--Line_height--Heading--lg: 60px;
|
||||
--Line_height--Heading--md: 48px;
|
||||
--Line_height--Heading--sm: 40px;
|
||||
--Line_height--Heading--xs: 32px;
|
||||
--Font_size--Body--lg: 18px;
|
||||
--Font_size--Body--md: 16px;
|
||||
--Font_size--Body--sm: 14px;
|
||||
--Font_size--Body--xs: 12px;
|
||||
--Font_weight--Italic: 400px;
|
||||
--Font_weight--Medium: 500px;
|
||||
--Font_weight--Bold: 700px;
|
||||
--Font_family--Body: Inter;
|
||||
--Paragraph_spacing--Heading--2xl: 60px;
|
||||
--Paragraph_spacing--Heading--xl: 48px;
|
||||
--Paragraph_spacing--Heading--lg: 40px;
|
||||
--Paragraph_spacing--Heading--md: 32px;
|
||||
--Paragraph_spacing--Heading--sm: 24px;
|
||||
--Paragraph_spacing--Heading--xs: 20px;
|
||||
--typography_components--h1--font-family: Inter;
|
||||
--typography_components--h2--font-family: Inter;
|
||||
--typography_components--h2--font-size: 30px;
|
||||
--typography_components--h2--line-height: 36px;
|
||||
--typography_components--h2--font-weight: 600;
|
||||
--typography_components--h2--letter-spacing: -0.4000000059604645px;
|
||||
--typography_components--h1--font-size: 36px;
|
||||
--typography_components--h1--font-size-lg: 48px;
|
||||
--typography_components--h1--line-height: 40px;
|
||||
--typography_components--h1--font-weight: 800;
|
||||
--typography_components--h1--letter-spacing: -0.4000000059604645px;
|
||||
--typography_components--h3--font-family: Inter;
|
||||
--typography_components--h3--font-size: 24px;
|
||||
--typography_components--h3--line-height: 32px;
|
||||
--typography_components--h3--font-weight: 600;
|
||||
--typography_components--h3--letter-spacing: -0.4000000059604645px;
|
||||
--typography_components--h4--font-family: Inter;
|
||||
--typography_components--h4--font-size: 20px;
|
||||
--typography_components--h4--line-height: 28px;
|
||||
--typography_components--h4--font-weight: 600;
|
||||
--typography_components--h4--letter-spacing: -0.4000000059604645px;
|
||||
--typography_components--p--font-family: Inter;
|
||||
--typography_components--p--font-size: 16px;
|
||||
--typography_components--p--line-height: 28px;
|
||||
--typography_components--p--font-weight: 400;
|
||||
--typography_components--p--letter-spacing: 0px;
|
||||
--typography_components--blockquote--font-family: Inter;
|
||||
--typography_components--blockquote--font-size: 16px;
|
||||
--typography_components--blockquote--line-height: 24px;
|
||||
--typography_components--blockquote--letter-spacing: 0px;
|
||||
--typography_components--blockquote--font-style: italic;
|
||||
--typography_components--list--font-family: Inter;
|
||||
--typography_components--list--font-size: 16px;
|
||||
--typography_components--list--line-height: 28px;
|
||||
--typography_components--list--letter-spacing: 0px;
|
||||
--typography_components--inline_code--font-family: Menlo;
|
||||
--typography_components--inline_code--font-size: 14px;
|
||||
--typography_components--inline_code--line-height: 20px;
|
||||
--typography_components--inline_code--font-weight: 600;
|
||||
--typography_components--inline_code--letter-spacing: 0px;
|
||||
--typography_components--lead--font-family: Inter;
|
||||
--typography_components--lead--font-size: 20px;
|
||||
--typography_components--lead--line-height: 28px;
|
||||
--typography_components--lead--font-weight: 400;
|
||||
--typography_components--lead--letter-spacing: 0px;
|
||||
--typography_components--large--font-family: Inter;
|
||||
--typography_components--large--font-size: 18px;
|
||||
--typography_components--large--line-height: 28px;
|
||||
--typography_components--large--font-weight: 600;
|
||||
--typography_components--large--letter-spacing: 0px;
|
||||
--typography_components--small--font-family: Inter;
|
||||
--typography_components--small--font-size: 14px;
|
||||
--typography_components--small--line-height: 14px;
|
||||
--typography_components--small--font-weight: 500;
|
||||
--typography_components--table--font-family: Inter;
|
||||
--typography_components--table--font-size: 16px;
|
||||
--typography_components--table--font-weight: 400;
|
||||
--typography_components--table--font-weight-bold: 700;
|
||||
--typography_components--table--letter-spacing: 0px;
|
||||
|
||||
/* Spacing and sizing: Desktop */
|
||||
--Border_width--sm: 1px;
|
||||
--Border_width--md: 2px;
|
||||
--Border_width--lg: 3px;
|
||||
--Radius--4xs: 4px;
|
||||
--Radius--3xs: 8px;
|
||||
--Radius--2xs: 12px;
|
||||
--Radius--xs: 16px;
|
||||
--Radius--sm: 24px;
|
||||
--Radius--md: 32px;
|
||||
--Radius--lg: 40px;
|
||||
--Radius--xl: 48px;
|
||||
--Radius--2xl: 56px;
|
||||
--Radius--3xl: 64px;
|
||||
--Radius--round: 999px;
|
||||
--Spacing--5xs: 4px;
|
||||
--Spacing--4xs: 8px;
|
||||
--Spacing--3xs: 12px;
|
||||
--Spacing--2xs: 16px;
|
||||
--Spacing--xs: 24px;
|
||||
--Spacing--sm: 32px;
|
||||
--Spacing--md: 40px;
|
||||
--Spacing--lg: 48px;
|
||||
--Spacing--xl: 56px;
|
||||
--Spacing--2xl: 64px;
|
||||
--Spacing--3xl: 72px;
|
||||
--Spacing--4xl: 80px;
|
||||
--Spacing--5xl: 88px;
|
||||
--Spacing--6xl: 96px;
|
||||
--Spacing--7xl: 104px;
|
||||
--Spacing--8xl: 112px;
|
||||
--Sizing--5xs: 4px;
|
||||
--Sizing--4xs: 8px;
|
||||
--Sizing--3xs: 12px;
|
||||
--Sizing--2xs: 16px;
|
||||
--Sizing--xs: 24px;
|
||||
--Sizing--sm: 32px;
|
||||
--Sizing--md: 40px;
|
||||
--Sizing--lg: 48px;
|
||||
--Sizing--xl: 56px;
|
||||
--Sizing--2xl: 64px;
|
||||
--Sizing--3xl: 72px;
|
||||
--Sizing--4xl: 80px;
|
||||
--Sizing--5xl: 88px;
|
||||
|
||||
/* Color: Light mode */
|
||||
--Opacity--Red--Red-100: var(--Primitive--Red--600);
|
||||
--Opacity--Red--Red-80: hsla(0, 72%, 51%, 0.8);
|
||||
--Opacity--Red--Red-60: hsla(0, 72%, 51%, 0.6);
|
||||
--Opacity--Red--Red-40: hsla(0, 72%, 51%, 0.4);
|
||||
--Opacity--Red--Red-20: hsla(0, 72%, 51%, 0.2);
|
||||
--Opacity--Red--Red-10: hsla(0, 72%, 51%, 0.1);
|
||||
--Opacity--Green--Green-100: var(--Primitive--Green--600);
|
||||
--Opacity--Green--Green-80: hsla(142, 76%, 36%, 0.8);
|
||||
--Opacity--Green--Green-60: hsla(142, 76%, 36%, 0.6);
|
||||
--Opacity--Green--Green-40: hsla(142, 76%, 36%, 0.4);
|
||||
--Opacity--Green--Green-20: hsla(142, 76%, 36%, 0.2);
|
||||
--Opacity--Green--Green-10: hsla(142, 76%, 36%, 0.1);
|
||||
--Opacity--Yellow--Yellow-100: var(--Primitive--Amber--400);
|
||||
--Opacity--Yellow--Yellow-80: hsla(48, 96%, 53%, 0.8);
|
||||
--Opacity--Yellow--Yellow-60: hsla(48, 96%, 53%, 0.6);
|
||||
--Opacity--Yellow--Yellow-40: hsla(48, 96%, 53%, 0.4);
|
||||
--Opacity--Yellow--Yellow-20: hsla(48, 96%, 53%, 0.2);
|
||||
--Opacity--Yellow--Yellow-10: hsla(48, 96%, 53%, 0.1);
|
||||
--Opacity--Violet--Violet-100: var(--Primitive--Violet--500);
|
||||
--Opacity--Violet--Violet-80: hsla(258, 90%, 66%, 0.8);
|
||||
--Opacity--Violet--Violet-60: hsla(258, 90%, 66%, 0.6);
|
||||
--Opacity--Violet--Violet-40: hsla(258, 90%, 66%, 0.4);
|
||||
--Opacity--Violet--Violet-20: hsla(258, 90%, 66%, 0.2);
|
||||
--Opacity--Violet--Violet-10: hsla(258, 90%, 66%, 0.1);
|
||||
--Opacity--Indigo--Indigo-100: var(--Primitive--Indigo--500);
|
||||
--Opacity--Indigo--Indigo-80: hsla(239, 84%, 67%, 0.8);
|
||||
--Opacity--Indigo--Indigo-60: hsla(239, 84%, 67%, 0.6);
|
||||
--Opacity--Indigo--Indigo-40: hsla(239, 84%, 67%, 0.4);
|
||||
--Opacity--Indigo--Indigo-20: hsla(239, 84%, 67%, 0.2);
|
||||
--Opacity--Indigo--Indigo-10: hsla(239, 84%, 67%, 0.1);
|
||||
--Opacity--Blue--Blue-100: var(--Primitive--Blue--500);
|
||||
--Opacity--Blue--Blue-80: hsla(217, 91%, 60%, 0.8);
|
||||
--Opacity--Blue--Blue-60: hsla(217, 91%, 60%, 0.6);
|
||||
--Opacity--Blue--Blue-40: hsla(217, 91%, 60%, 0.4);
|
||||
--Opacity--Blue--Blue-20: hsla(217, 91%, 60%, 0.2);
|
||||
--Opacity--Blue--Blue-10: hsla(217, 91%, 60%, 0.1);
|
||||
--Opacity--Grey--Grey-100: var(--Primitive--Gray--500);
|
||||
--Opacity--Grey--Grey-80: hsla(220, 9%, 46%, 0.8);
|
||||
--Opacity--Grey--Grey-60: hsla(220, 9%, 46%, 0.6);
|
||||
--Opacity--Grey--Grey-40: hsla(220, 9%, 46%, 0.4);
|
||||
--Opacity--Grey--Grey-20: hsla(220, 9%, 46%, 0.2);
|
||||
--Opacity--Grey--Grey-10: hsla(220, 9%, 46%, 0.1);
|
||||
--Opacity--White--White-100: var(--Primitive--White);
|
||||
--Opacity--White--White-80: hsla(0, 0%, 100%, 0.8);
|
||||
--Opacity--White--White-60: hsla(0, 0%, 100%, 0.6);
|
||||
--Opacity--White--White-40: hsla(0, 0%, 100%, 0.4);
|
||||
--Opacity--White--White-20: hsla(0, 0%, 100%, 0.2);
|
||||
--Opacity--White--White-10: hsla(0, 0%, 100%, 0.1);
|
||||
--Opacity--White--White-0: hsla(0, 0%, 100%, 0);
|
||||
--Status--Error--colorErrorBg: var(--color--Red--50);
|
||||
--Status--Error--colorErrorBgHover: var(--color--Red--100);
|
||||
--Status--Error--colorErrorBorder: var(--color--Red--200);
|
||||
--Status--Error--colorErrorBorderHover: var(--color--Red--300);
|
||||
--Status--Error--colorErrorBase: var(--color--Red--500);
|
||||
--Status--Error--colorErrorActive: var(--color--Red--600);
|
||||
--Status--Error--colorErrorTextHover: var(--color--Red--700);
|
||||
--Status--Error--colorErrorText: var(--color--Red--800);
|
||||
--Status--Success--colorSuccessBg: var(--color--Green--50);
|
||||
--Status--Success--colorSuccessBgHover: var(--color--Green--100);
|
||||
--Status--Success--colorSuccessBase: var(--color--Green--500);
|
||||
--Status--Success--colorSuccessTextHover: var(--color--Green--700);
|
||||
--Status--Warning--colorWarningBg: var(--color--Yellow--50);
|
||||
--Status--Warning--colorWarningBgHover: var(--color--Yellow--100);
|
||||
--Status--Warning--colorWarningBase: var(--color--Yellow--500);
|
||||
--Status--Warning--colorWarningActive: var(--color--Yellow--600);
|
||||
--Status--Warning--colorWarningTextHover: var(--color--Yellow--700);
|
||||
--Primitive--Black: hsla(0, 0%, 0%, 1);
|
||||
--Primitive--White: hsla(0, 0%, 100%, 1);
|
||||
--Brand--Base_Colors--Primary: var(--Primitive--Lime--500);
|
||||
--Primitive--Neutral--50: hsla(0, 0%, 98%, 1);
|
||||
--Primitive--Neutral--100: hsla(0, 0%, 96%, 1);
|
||||
--Primitive--Neutral--200: hsla(0, 0%, 90%, 1);
|
||||
--Primitive--Neutral--300: hsla(0, 0%, 83%, 1);
|
||||
--Primitive--Neutral--400: hsla(0, 0%, 64%, 1);
|
||||
--Primitive--Neutral--500: hsla(0, 0%, 45%, 1);
|
||||
--Primitive--Neutral--600: hsla(215, 14%, 34%, 1);
|
||||
--Primitive--Neutral--700: hsla(0, 0%, 25%, 1);
|
||||
--Primitive--Neutral--800: hsla(0, 0%, 15%, 1);
|
||||
--Primitive--Neutral--900: hsla(0, 0%, 9%, 1);
|
||||
--Primitive--Neutral--950: hsla(0, 0%, 4%, 1);
|
||||
--Primitive--Stone--50: hsla(60, 9%, 98%, 1);
|
||||
--Primitive--Stone--100: hsla(60, 5%, 96%, 1);
|
||||
--Primitive--Stone--200: hsla(20, 6%, 90%, 1);
|
||||
--Primitive--Stone--300: hsla(24, 6%, 83%, 1);
|
||||
--Primitive--Stone--400: hsla(24, 5%, 64%, 1);
|
||||
--Primitive--Stone--500: hsla(25, 5%, 45%, 1);
|
||||
--Primitive--Stone--600: hsla(33, 5%, 32%, 1);
|
||||
--Primitive--Stone--700: hsla(30, 6%, 25%, 1);
|
||||
--Primitive--Stone--800: hsla(12, 6%, 15%, 1);
|
||||
--Primitive--Stone--900: hsla(24, 10%, 10%, 1);
|
||||
--Primitive--Stone--950: hsla(20, 14%, 4%, 1);
|
||||
--Primitive--Zinc--50: hsla(0, 0%, 98%, 1);
|
||||
--Primitive--Zinc--100: hsla(240, 5%, 96%, 1);
|
||||
--Primitive--Zinc--200: hsla(240, 6%, 90%, 1);
|
||||
--Primitive--Zinc--300: hsla(240, 5%, 84%, 1);
|
||||
--Primitive--Zinc--400: hsla(240, 5%, 65%, 1);
|
||||
--Primitive--Zinc--500: hsla(240, 4%, 46%, 1);
|
||||
--Primitive--Zinc--600: hsla(240, 5%, 34%, 1);
|
||||
--Primitive--Zinc--700: hsla(240, 5%, 26%, 1);
|
||||
--Primitive--Zinc--800: hsla(240, 4%, 16%, 1);
|
||||
--Primitive--Zinc--900: hsla(240, 6%, 10%, 1);
|
||||
--Primitive--Zinc--950: hsla(240, 10%, 4%, 1);
|
||||
--Primitive--Slate--50: hsla(210, 40%, 98%, 1);
|
||||
--Primitive--Slate--100: hsla(210, 40%, 96%, 1);
|
||||
--Primitive--Slate--200: hsla(214, 32%, 91%, 1);
|
||||
--Primitive--Slate--300: hsla(213, 27%, 84%, 1);
|
||||
--Primitive--Slate--400: hsla(215, 20%, 65%, 1);
|
||||
--Primitive--Slate--500: hsla(215, 16%, 47%, 1);
|
||||
--Primitive--Slate--600: hsla(215, 19%, 35%, 1);
|
||||
--Primitive--Slate--700: hsla(215, 25%, 27%, 1);
|
||||
--Primitive--Slate--800: hsla(217, 33%, 17%, 1);
|
||||
--Primitive--Slate--900: hsla(222, 47%, 11%, 1);
|
||||
--Primitive--Slate--950: hsla(229, 84%, 5%, 1);
|
||||
--Primitive--Gray--50: hsla(210, 20%, 98%, 1);
|
||||
--Primitive--Gray--100: hsla(220, 14%, 96%, 1);
|
||||
--Primitive--Gray--200: hsla(220, 13%, 91%, 1);
|
||||
--Primitive--Gray--300: hsla(216, 12%, 84%, 1);
|
||||
--Primitive--Gray--400: hsla(218, 11%, 65%, 1);
|
||||
--Primitive--Gray--500: hsla(220, 9%, 46%, 1);
|
||||
--Primitive--Gray--600: hsla(0, 0%, 32%, 1);
|
||||
--Primitive--Gray--700: hsla(217, 19%, 27%, 1);
|
||||
--Primitive--Gray--800: hsla(215, 28%, 17%, 1);
|
||||
--Primitive--Gray--900: hsla(221, 39%, 11%, 1);
|
||||
--Primitive--Gray--950: hsla(224, 71%, 4%, 1);
|
||||
--Primitive--Red--50: hsla(0, 86%, 97%, 1);
|
||||
--Primitive--Red--100: hsla(0, 93%, 94%, 1);
|
||||
--Primitive--Red--200: hsla(0, 96%, 89%, 1);
|
||||
--Primitive--Red--300: hsla(0, 94%, 82%, 1);
|
||||
--Primitive--Red--400: hsla(0, 91%, 71%, 1);
|
||||
--Primitive--Red--500: hsla(0, 84%, 60%, 1);
|
||||
--Primitive--Red--600: hsla(0, 72%, 51%, 1);
|
||||
--Primitive--Red--700: hsla(0, 74%, 42%, 1);
|
||||
--Primitive--Red--800: hsla(0, 70%, 35%, 1);
|
||||
--Primitive--Red--900: hsla(0, 63%, 31%, 1);
|
||||
--Primitive--Red--950: hsla(0, 75%, 15%, 1);
|
||||
--Primitive--Orange--50: hsla(33, 100%, 96%, 1);
|
||||
--Primitive--Orange--100: hsla(34, 100%, 92%, 1);
|
||||
--Primitive--Orange--200: hsla(32, 98%, 83%, 1);
|
||||
--Primitive--Orange--300: hsla(31, 97%, 72%, 1);
|
||||
--Primitive--Orange--400: hsla(27, 96%, 61%, 1);
|
||||
--Primitive--Orange--500: hsla(25, 95%, 53%, 1);
|
||||
--Primitive--Orange--600: hsla(21, 90%, 48%, 1);
|
||||
--Primitive--Orange--700: hsla(17, 88%, 40%, 1);
|
||||
--Primitive--Orange--800: hsla(15, 79%, 34%, 1);
|
||||
--Primitive--Orange--900: hsla(15, 75%, 28%, 1);
|
||||
--Primitive--Orange--950: hsla(13, 81%, 15%, 1);
|
||||
--Primitive--Amber--50: hsla(48, 100%, 96%, 1);
|
||||
--Primitive--Amber--100: hsla(48, 96%, 89%, 1);
|
||||
--Primitive--Amber--200: hsla(48, 97%, 77%, 1);
|
||||
--Primitive--Amber--300: hsla(46, 97%, 65%, 1);
|
||||
--Primitive--Amber--400: hsla(43, 96%, 56%, 1);
|
||||
--Primitive--Amber--500: hsla(38, 92%, 50%, 1);
|
||||
--Primitive--Amber--600: hsla(32, 95%, 44%, 1);
|
||||
--Primitive--Amber--700: hsla(26, 90%, 37%, 1);
|
||||
--Primitive--Amber--800: hsla(23, 83%, 31%, 1);
|
||||
--Primitive--Amber--900: hsla(22, 78%, 26%, 1);
|
||||
--Primitive--Amber--950: hsla(21, 92%, 14%, 1);
|
||||
--Primitive--Yellow--50: hsla(55, 92%, 95%, 1);
|
||||
--Primitive--Yellow--100: hsla(55, 97%, 88%, 1);
|
||||
--Primitive--Yellow--200: hsla(53, 98%, 77%, 1);
|
||||
--Primitive--Yellow--300: hsla(50, 98%, 64%, 1);
|
||||
--Primitive--Yellow--400: hsla(48, 96%, 53%, 1);
|
||||
--Primitive--Yellow--500: hsla(45, 93%, 47%, 1);
|
||||
--Primitive--Yellow--600: hsla(41, 96%, 40%, 1);
|
||||
--Primitive--Yellow--700: hsla(35, 92%, 33%, 1);
|
||||
--Primitive--Yellow--800: hsla(32, 81%, 29%, 1);
|
||||
--Primitive--Yellow--900: hsla(28, 73%, 26%, 1);
|
||||
--Primitive--Yellow--950: hsla(26, 83%, 14%, 1);
|
||||
--Primitive--Lime--50: hsla(78, 92%, 95%, 1);
|
||||
--Primitive--Lime--100: hsla(80, 89%, 89%, 1);
|
||||
--Primitive--Lime--200: hsla(81, 88%, 80%, 1);
|
||||
--Primitive--Lime--300: hsla(82, 85%, 67%, 1);
|
||||
--Primitive--Lime--400: hsla(83, 78%, 55%, 1);
|
||||
--Primitive--Lime--500: hsla(84, 81%, 44%, 1);
|
||||
--Primitive--Lime--600: hsla(85, 85%, 35%, 1);
|
||||
--Primitive--Lime--700: hsla(86, 78%, 27%, 1);
|
||||
--Primitive--Lime--800: hsla(86, 69%, 23%, 1);
|
||||
--Primitive--Lime--900: hsla(88, 61%, 20%, 1);
|
||||
--Primitive--Lime--950: hsla(89, 80%, 10%, 1);
|
||||
--Primitive--Green--50: hsla(138, 76%, 97%, 1);
|
||||
--Primitive--Green--100: hsla(141, 84%, 93%, 1);
|
||||
--Primitive--Green--200: hsla(141, 79%, 85%, 1);
|
||||
--Primitive--Green--300: hsla(142, 77%, 73%, 1);
|
||||
--Primitive--Green--400: hsla(142, 69%, 58%, 1);
|
||||
--Primitive--Green--500: hsla(142, 71%, 45%, 1);
|
||||
--Primitive--Green--600: hsla(142, 76%, 36%, 1);
|
||||
--Primitive--Green--700: hsla(142, 72%, 29%, 1);
|
||||
--Primitive--Green--800: hsla(143, 64%, 24%, 1);
|
||||
--Primitive--Green--900: hsla(144, 61%, 20%, 1);
|
||||
--Primitive--Green--950: hsla(145, 80%, 10%, 1);
|
||||
--Primitive--Emerald--50: hsla(152, 81%, 96%, 1);
|
||||
--Primitive--Emerald--100: hsla(149, 80%, 90%, 1);
|
||||
--Primitive--Emerald--200: hsla(152, 76%, 80%, 1);
|
||||
--Primitive--Emerald--300: hsla(156, 72%, 67%, 1);
|
||||
--Primitive--Emerald--400: hsla(158, 64%, 52%, 1);
|
||||
--Primitive--Emerald--500: hsla(160, 84%, 39%, 1);
|
||||
--Primitive--Emerald--600: hsla(161, 94%, 30%, 1);
|
||||
--Primitive--Emerald--700: hsla(163, 94%, 24%, 1);
|
||||
--Primitive--Emerald--800: hsla(163, 88%, 20%, 1);
|
||||
--Primitive--Emerald--900: hsla(164, 86%, 16%, 1);
|
||||
--Primitive--Emerald--950: hsla(166, 91%, 9%, 1);
|
||||
--Primitive--Teal--50: hsla(166, 76%, 97%, 1);
|
||||
--Primitive--Teal--100: hsla(167, 85%, 89%, 1);
|
||||
--Primitive--Teal--200: hsla(168, 84%, 78%, 1);
|
||||
--Primitive--Teal--300: hsla(171, 77%, 64%, 1);
|
||||
--Primitive--Teal--400: hsla(172, 66%, 50%, 1);
|
||||
--Primitive--Teal--500: hsla(173, 80%, 40%, 1);
|
||||
--Primitive--Teal--600: hsla(175, 84%, 32%, 1);
|
||||
--Primitive--Teal--700: hsla(175, 77%, 26%, 1);
|
||||
--Primitive--Teal--800: hsla(176, 69%, 22%, 1);
|
||||
--Primitive--Teal--900: hsla(176, 61%, 19%, 1);
|
||||
--Primitive--Teal--950: hsla(179, 84%, 10%, 1);
|
||||
--Primitive--Cyan--50: hsla(183, 100%, 96%, 1);
|
||||
--Primitive--Cyan--100: hsla(185, 96%, 90%, 1);
|
||||
--Primitive--Cyan--200: hsla(186, 94%, 82%, 1);
|
||||
--Primitive--Cyan--300: hsla(187, 92%, 69%, 1);
|
||||
--Primitive--Cyan--400: hsla(188, 86%, 53%, 1);
|
||||
--Primitive--Cyan--500: hsla(189, 94%, 43%, 1);
|
||||
--Primitive--Cyan--600: hsla(192, 91%, 36%, 1);
|
||||
--Primitive--Cyan--700: hsla(193, 82%, 31%, 1);
|
||||
--Primitive--Cyan--800: hsla(194, 70%, 27%, 1);
|
||||
--Primitive--Cyan--900: hsla(196, 64%, 24%, 1);
|
||||
--Primitive--Cyan--950: hsla(197, 79%, 15%, 1);
|
||||
--Primitive--Sky--50: hsla(204, 100%, 97%, 1);
|
||||
--Primitive--Sky--100: hsla(204, 94%, 94%, 1);
|
||||
--Primitive--Sky--200: hsla(201, 94%, 86%, 1);
|
||||
--Primitive--Sky--300: hsla(199, 95%, 74%, 1);
|
||||
--Primitive--Sky--400: hsla(198, 93%, 60%, 1);
|
||||
--Primitive--Sky--500: hsla(199, 89%, 48%, 1);
|
||||
--Primitive--Sky--600: hsla(200, 98%, 39%, 1);
|
||||
--Primitive--Sky--700: hsla(201, 96%, 32%, 1);
|
||||
--Primitive--Sky--800: hsla(201, 90%, 27%, 1);
|
||||
--Primitive--Sky--900: hsla(202, 80%, 24%, 1);
|
||||
--Primitive--Sky--950: hsla(204, 80%, 16%, 1);
|
||||
--Primitive--Blue--50: hsla(214, 100%, 97%, 1);
|
||||
--Primitive--Blue--100: hsla(214, 95%, 93%, 1);
|
||||
--Primitive--Blue--200: hsla(213, 97%, 87%, 1);
|
||||
--Primitive--Blue--300: hsla(212, 96%, 78%, 1);
|
||||
--Primitive--Blue--400: hsla(213, 94%, 68%, 1);
|
||||
--Primitive--Blue--500: hsla(217, 91%, 60%, 1);
|
||||
--Primitive--Blue--600: hsla(221, 83%, 53%, 1);
|
||||
--Primitive--Blue--700: hsla(224, 76%, 48%, 1);
|
||||
--Primitive--Blue--800: hsla(226, 71%, 40%, 1);
|
||||
--Primitive--Blue--900: hsla(224, 64%, 33%, 1);
|
||||
--Primitive--Blue--950: hsla(226, 57%, 21%, 1);
|
||||
--Primitive--Indigo--50: hsla(226, 100%, 97%, 1);
|
||||
--Primitive--Indigo--100: hsla(226, 100%, 94%, 1);
|
||||
--Primitive--Indigo--200: hsla(228, 96%, 89%, 1);
|
||||
--Primitive--Indigo--300: hsla(230, 94%, 82%, 1);
|
||||
--Primitive--Indigo--400: hsla(234, 89%, 74%, 1);
|
||||
--Primitive--Indigo--500: hsla(239, 84%, 67%, 1);
|
||||
--Primitive--Indigo--600: hsla(243, 75%, 59%, 1);
|
||||
--Primitive--Indigo--700: hsla(245, 58%, 51%, 1);
|
||||
--Primitive--Indigo--800: hsla(244, 55%, 41%, 1);
|
||||
--Primitive--Indigo--900: hsla(242, 47%, 34%, 1);
|
||||
--Primitive--Indigo--950: hsla(244, 47%, 20%, 1);
|
||||
--Primitive--Violet--50: hsla(250, 100%, 98%, 1);
|
||||
--Primitive--Violet--100: hsla(251, 91%, 95%, 1);
|
||||
--Primitive--Violet--200: hsla(251, 95%, 92%, 1);
|
||||
--Primitive--Violet--300: hsla(253, 95%, 85%, 1);
|
||||
--Primitive--Violet--400: hsla(255, 92%, 76%, 1);
|
||||
--Primitive--Violet--500: hsla(258, 90%, 66%, 1);
|
||||
--Primitive--Violet--600: hsla(262, 83%, 58%, 1);
|
||||
--Primitive--Violet--700: hsla(263, 70%, 50%, 1);
|
||||
--Primitive--Violet--800: hsla(263, 69%, 42%, 1);
|
||||
--Primitive--Violet--900: hsla(264, 67%, 35%, 1);
|
||||
--Primitive--Violet--950: hsla(262, 78%, 23%, 1);
|
||||
--Primitive--Purple--50: hsla(270, 100%, 98%, 1);
|
||||
--Primitive--Purple--100: hsla(269, 100%, 95%, 1);
|
||||
--Primitive--Purple--200: hsla(269, 100%, 92%, 1);
|
||||
--Primitive--Purple--300: hsla(269, 97%, 85%, 1);
|
||||
--Primitive--Purple--400: hsla(270, 95%, 75%, 1);
|
||||
--Primitive--Purple--500: hsla(271, 91%, 65%, 1);
|
||||
--Primitive--Purple--600: hsla(271, 81%, 56%, 1);
|
||||
--Primitive--Purple--700: hsla(272, 72%, 47%, 1);
|
||||
--Primitive--Purple--800: hsla(273, 67%, 39%, 1);
|
||||
--Primitive--Purple--900: hsla(274, 66%, 32%, 1);
|
||||
--Primitive--Purple--950: hsla(274, 87%, 21%, 1);
|
||||
--Primitive--Fuchsia--50: hsla(289, 100%, 98%, 1);
|
||||
--Primitive--Fuchsia--100: hsla(287, 100%, 95%, 1);
|
||||
--Primitive--Fuchsia--200: hsla(288, 96%, 91%, 1);
|
||||
--Primitive--Fuchsia--300: hsla(291, 93%, 83%, 1);
|
||||
--Primitive--Fuchsia--400: hsla(292, 91%, 73%, 1);
|
||||
--Primitive--Fuchsia--500: hsla(292, 84%, 61%, 1);
|
||||
--Primitive--Fuchsia--600: hsla(293, 69%, 49%, 1);
|
||||
--Primitive--Fuchsia--700: hsla(295, 72%, 40%, 1);
|
||||
--Primitive--Fuchsia--800: hsla(295, 70%, 33%, 1);
|
||||
--Primitive--Fuchsia--900: hsla(297, 64%, 28%, 1);
|
||||
--Primitive--Fuchsia--950: hsla(297, 90%, 16%, 1);
|
||||
--Primitive--Pink--50: hsla(327, 73%, 97%, 1);
|
||||
--Primitive--Pink--100: hsla(326, 78%, 95%, 1);
|
||||
--Primitive--Pink--200: hsla(326, 85%, 90%, 1);
|
||||
--Primitive--Pink--300: hsla(327, 87%, 82%, 1);
|
||||
--Primitive--Pink--400: hsla(329, 86%, 70%, 1);
|
||||
--Primitive--Pink--500: hsla(330, 81%, 60%, 1);
|
||||
--Primitive--Pink--600: hsla(333, 71%, 51%, 1);
|
||||
--Primitive--Pink--700: hsla(335, 78%, 42%, 1);
|
||||
--Primitive--Pink--800: hsla(336, 74%, 35%, 1);
|
||||
--Primitive--Pink--900: hsla(336, 69%, 30%, 1);
|
||||
--Primitive--Pink--950: hsla(336, 84%, 17%, 1);
|
||||
--Primitive--Rose--50: hsla(356, 100%, 97%, 1);
|
||||
--Primitive--Rose--100: hsla(356, 100%, 95%, 1);
|
||||
--Primitive--Rose--200: hsla(353, 96%, 90%, 1);
|
||||
--Primitive--Rose--300: hsla(353, 96%, 82%, 1);
|
||||
--Primitive--Rose--400: hsla(351, 95%, 71%, 1);
|
||||
--Primitive--Rose--500: hsla(350, 89%, 60%, 1);
|
||||
--Primitive--Rose--600: hsla(347, 77%, 50%, 1);
|
||||
--Primitive--Rose--700: hsla(345, 83%, 41%, 1);
|
||||
--Primitive--Rose--800: hsla(343, 80%, 35%, 1);
|
||||
--Primitive--Rose--900: hsla(342, 75%, 30%, 1);
|
||||
--Primitive--Rose--950: hsla(343, 88%, 16%, 1);
|
||||
--Brand--Base_Colors--Destructive: var(--Primitive--Red--500);
|
||||
--Brand--Base_Colors--Success: var(--Primitive--Green--500);
|
||||
--Brand--Base_Colors--Warning: var(--Primitive--Amber--500);
|
||||
--Brand--Base_Colors--White: var(--Primitive--White);
|
||||
--Brand--Base_Colors--Black: var(--Primitive--Black);
|
||||
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--50); /*页面背景色:应用在整个页面的最底层。*/
|
||||
--Brand--Semantic_Colors--Background-subtle: hsla(
|
||||
0,
|
||||
0%,
|
||||
0%,
|
||||
0.02
|
||||
); /*细微背景色:用于需要与主背景有微弱区分的区域,如代码块背景。*/
|
||||
--Brand--Semantic_Colors--Foreground: hsla(0, 0%, 0%, 0.9); /*主要前景/文字色:用于正文、标题等。*/
|
||||
--Brand--Semantic_Colors--Foreground-secondary: hsla(0, 0%, 0%, 0.6); /*次要前景/文字色:用于辅助性文本、描述。*/
|
||||
--Brand--Semantic_Colors--Foreground-muted: hsla(0, 0%, 0%, 0.4); /*静默前景/文字色:用于禁用状态的文字、占位符。*/
|
||||
--Brand--Semantic_Colors--Border: hsla(0, 0%, 0%, 0.1); /*默认边框色:用于卡片、输入框、分隔线。*/
|
||||
--Brand--Semantic_Colors--Border-hover: hsla(0, 0%, 0%, 0.2); /*激活边框色:用于元素被按下或激活时的边框。*/
|
||||
--Brand--Semantic_Colors--Border-active: hsla(0, 0%, 0%, 0.3); /*激活边框色:用于元素被按下或激活时的边框。*/
|
||||
--Brand--Semantic_Colors--Ring: hsla(
|
||||
84,
|
||||
81%,
|
||||
44%,
|
||||
0.4
|
||||
); /*聚焦环颜色:用于输入框等元素在聚焦 (Focus) 状态下的外发光。*/
|
||||
--Brand--UI_Element_Colors--Modal--Backdrop: hsla(0, 0%, 0%, 0.4);
|
||||
--Brand--UI_Element_Colors--Modal--Thumb: hsla(0, 0%, 0%, 0.2);
|
||||
--Brand--UI_Element_Colors--Modal--Thumb_Hover: hsla(0, 0%, 0%, 0.3);
|
||||
--Brand--UI_Element_Colors--Icon--Default: var(--Brand--Semantic_Colors--Foreground-secondary);
|
||||
--Brand--UI_Element_Colors--Icon--Hover: var(--Brand--Semantic_Colors--Foreground);
|
||||
--Brand--UI_Element_Colors--Input_Select--Background: var(--Brand--Base_Colors--White);
|
||||
--Brand--UI_Element_Colors--Input_Select--Border: var(--Brand--Semantic_Colors--Border);
|
||||
--Brand--UI_Element_Colors--Input_Select--Border_Hover: var(--Brand--Semantic_Colors--Border-hover);
|
||||
--Brand--UI_Element_Colors--Input_Select--Border_Focus: var(--Brand--Base_Colors--Primary);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Background: var(--Brand--Base_Colors--Primary);
|
||||
--Brand--UI_Element_Colors--Card_Container--Background: var(--Brand--Base_Colors--White);
|
||||
--Brand--UI_Element_Colors--Card_Container--Border: var(--Brand--Semantic_Colors--Border);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Background: hsla(0, 0%, 0%, 0);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Text: var(--Brand--Semantic_Colors--Foreground);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Background_Hover: hsla(0, 0%, 0%, 0.05);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Background_Active: hsla(0, 0%, 0%, 0.1);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Background: hsla(0, 0%, 0%, 0.05);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Text: var(--Brand--Semantic_Colors--Foreground);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Background_Hover: hsla(0, 0%, 0%, 0.85);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Background_Active: hsla(0, 0%, 0%, 0.7);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Border: var(--Brand--Semantic_Colors--Border);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Text: var(--Brand--Base_Colors--White);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Background_Hover: hsla(84, 81%, 44%, 0.85);
|
||||
--Brand--UI_Element_Colors--Primary_Button--2nd_Background: hsla(84, 81%, 44%, 0.1);
|
||||
--Brand--UI_Element_Colors--Primary_Button--3rd_Background: hsla(84, 81%, 44%, 0.05);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Background_Active: hsla(84, 81%, 44%, 0.7);
|
||||
--Boolean: false;
|
||||
|
||||
/* Color: Dark mode */
|
||||
--Opacity--Red--Red-100: var(--Primitive--Red--600);
|
||||
--Opacity--Red--Red-80: hsla(0, 72%, 51%, 0.8);
|
||||
--Opacity--Red--Red-60: hsla(0, 72%, 51%, 0.6);
|
||||
--Opacity--Red--Red-40: hsla(0, 72%, 51%, 0.4);
|
||||
--Opacity--Red--Red-20: hsla(0, 72%, 51%, 0.2);
|
||||
--Opacity--Red--Red-10: hsla(0, 72%, 51%, 0.1);
|
||||
--Opacity--Green--Green-100: var(--Primitive--Green--600);
|
||||
--Opacity--Green--Green-80: hsla(142, 76%, 36%, 0.8);
|
||||
--Opacity--Green--Green-60: hsla(142, 76%, 36%, 0.6);
|
||||
--Opacity--Green--Green-40: hsla(142, 76%, 36%, 0.4);
|
||||
--Opacity--Green--Green-20: hsla(142, 76%, 36%, 0.2);
|
||||
--Opacity--Green--Green-10: hsla(142, 76%, 36%, 0.1);
|
||||
--Opacity--Yellow--Yellow-100: var(--Primitive--Yellow--400);
|
||||
--Opacity--Yellow--Yellow-80: hsla(48, 96%, 53%, 0.8);
|
||||
--Opacity--Yellow--Yellow-60: hsla(48, 96%, 53%, 0.6);
|
||||
--Opacity--Yellow--Yellow-40: hsla(48, 96%, 53%, 0.4);
|
||||
--Opacity--Yellow--Yellow-20: hsla(48, 96%, 53%, 0.2);
|
||||
--Opacity--Yellow--Yellow-10: hsla(48, 96%, 53%, 0.1);
|
||||
--Opacity--Violet--Violet-100: var(--Primitive--Violet--500);
|
||||
--Opacity--Violet--Violet-80: hsla(258, 90%, 66%, 0.8);
|
||||
--Opacity--Violet--Violet-60: hsla(258, 90%, 66%, 0.6);
|
||||
--Opacity--Violet--Violet-40: hsla(258, 90%, 66%, 0.4);
|
||||
--Opacity--Violet--Violet-20: hsla(258, 90%, 66%, 0.2);
|
||||
--Opacity--Violet--Violet-10: hsla(258, 90%, 66%, 0.1);
|
||||
--Opacity--Indigo--Indigo-100: var(--Primitive--Indigo--500);
|
||||
--Opacity--Indigo--Indigo-80: hsla(239, 84%, 67%, 0.8);
|
||||
--Opacity--Indigo--Indigo-60: hsla(239, 84%, 67%, 0.6);
|
||||
--Opacity--Indigo--Indigo-40: hsla(239, 84%, 67%, 0.4);
|
||||
--Opacity--Indigo--Indigo-20: hsla(239, 84%, 67%, 0.2);
|
||||
--Opacity--Indigo--Indigo-10: hsla(239, 84%, 67%, 0.1);
|
||||
--Opacity--Blue--Blue-100: var(--Primitive--Blue--500);
|
||||
--Opacity--Blue--Blue-80: hsla(217, 91%, 60%, 0.8);
|
||||
--Opacity--Blue--Blue-60: hsla(217, 91%, 60%, 0.6);
|
||||
--Opacity--Blue--Blue-40: hsla(217, 91%, 60%, 0.4);
|
||||
--Opacity--Blue--Blue-20: hsla(217, 91%, 60%, 0.2);
|
||||
--Opacity--Blue--Blue-10: hsla(217, 91%, 60%, 0.1);
|
||||
--Opacity--Grey--Grey-100: var(--Primitive--Gray--500);
|
||||
--Opacity--Grey--Grey-80: hsla(220, 9%, 46%, 0.8);
|
||||
--Opacity--Grey--Grey-60: hsla(220, 9%, 46%, 0.6);
|
||||
--Opacity--Grey--Grey-40: hsla(220, 9%, 46%, 0.4);
|
||||
--Opacity--Grey--Grey-20: hsla(220, 9%, 46%, 0.2);
|
||||
--Opacity--Grey--Grey-10: hsla(220, 9%, 46%, 0.1);
|
||||
--Opacity--White--White-100: var(--Primitive--White);
|
||||
--Opacity--White--White-80: hsla(0, 0%, 100%, 0.8);
|
||||
--Opacity--White--White-60: hsla(0, 0%, 100%, 0.6);
|
||||
--Opacity--White--White-40: hsla(0, 0%, 100%, 0.4);
|
||||
--Opacity--White--White-20: hsla(0, 0%, 100%, 0.2);
|
||||
--Opacity--White--White-10: hsla(0, 0%, 100%, 0.1);
|
||||
--Opacity--White--White-0: hsla(0, 0%, 100%, 0);
|
||||
--Status--Error--colorErrorBg: var(--color--Red--900);
|
||||
--Status--Error--colorErrorBgHover: var(--color--Red--800);
|
||||
--Status--Error--colorErrorBorder: var(--color--Red--700);
|
||||
--Status--Error--colorErrorBorderHover: var(--color--Red--600);
|
||||
--Status--Error--colorErrorBase: var(--color--Red--400);
|
||||
--Status--Error--colorErrorActive: var(--color--Red--300);
|
||||
--Status--Error--colorErrorTextHover: var(--color--Red--200);
|
||||
--Status--Error--colorErrorText: var(--color--Red--100);
|
||||
--Status--Success--colorSuccessBg: var(--color--Green--900);
|
||||
--Status--Success--colorSuccessBgHover: var(--color--Green--800);
|
||||
--Status--Success--colorSuccessBase: var(--color--Green--400);
|
||||
--Status--Success--colorSuccessTextHover: var(--color--Green--200);
|
||||
--Status--Warning--colorWarningBg: var(--color--Yellow--900);
|
||||
--Status--Warning--colorWarningBgHover: var(--color--Yellow--800);
|
||||
--Status--Warning--colorWarningBase: var(--color--Yellow--400);
|
||||
--Status--Warning--colorWarningActive: var(--color--Yellow--300);
|
||||
--Status--Warning--colorWarningTextHover: var(--color--Yellow--200);
|
||||
--Primitive--Black: hsla(0, 0%, 0%, 1);
|
||||
--Primitive--White: hsla(0, 0%, 100%, 1);
|
||||
--Brand--Base_Colors--Primary: var(--Primitive--Lime--500);
|
||||
--Primitive--Neutral--50: hsla(0, 0%, 98%, 1);
|
||||
--Primitive--Neutral--100: hsla(0, 0%, 96%, 1);
|
||||
--Primitive--Neutral--200: hsla(0, 0%, 90%, 1);
|
||||
--Primitive--Neutral--300: hsla(0, 0%, 83%, 1);
|
||||
--Primitive--Neutral--400: hsla(0, 0%, 64%, 1);
|
||||
--Primitive--Neutral--500: hsla(0, 0%, 45%, 1);
|
||||
--Primitive--Neutral--600: hsla(215, 14%, 34%, 1);
|
||||
--Primitive--Neutral--700: hsla(0, 0%, 25%, 1);
|
||||
--Primitive--Neutral--800: hsla(0, 0%, 15%, 1);
|
||||
--Primitive--Neutral--900: hsla(0, 0%, 9%, 1);
|
||||
--Primitive--Neutral--950: hsla(0, 0%, 4%, 1);
|
||||
--Primitive--Stone--50: hsla(60, 9%, 98%, 1);
|
||||
--Primitive--Stone--100: hsla(60, 5%, 96%, 1);
|
||||
--Primitive--Stone--200: hsla(20, 6%, 90%, 1);
|
||||
--Primitive--Stone--300: hsla(24, 6%, 83%, 1);
|
||||
--Primitive--Stone--400: hsla(24, 5%, 64%, 1);
|
||||
--Primitive--Stone--500: hsla(25, 5%, 45%, 1);
|
||||
--Primitive--Stone--600: hsla(33, 5%, 32%, 1);
|
||||
--Primitive--Stone--700: hsla(30, 6%, 25%, 1);
|
||||
--Primitive--Stone--800: hsla(12, 6%, 15%, 1);
|
||||
--Primitive--Stone--900: hsla(24, 10%, 10%, 1);
|
||||
--Primitive--Stone--950: hsla(20, 14%, 4%, 1);
|
||||
--Primitive--Zinc--50: hsla(0, 0%, 98%, 1);
|
||||
--Primitive--Zinc--100: hsla(240, 5%, 96%, 1);
|
||||
--Primitive--Zinc--200: hsla(240, 6%, 90%, 1);
|
||||
--Primitive--Zinc--300: hsla(240, 5%, 84%, 1);
|
||||
--Primitive--Zinc--400: hsla(240, 5%, 65%, 1);
|
||||
--Primitive--Zinc--500: hsla(240, 4%, 46%, 1);
|
||||
--Primitive--Zinc--600: hsla(240, 5%, 34%, 1);
|
||||
--Primitive--Zinc--700: hsla(240, 5%, 26%, 1);
|
||||
--Primitive--Zinc--800: hsla(240, 4%, 16%, 1);
|
||||
--Primitive--Zinc--900: hsla(240, 6%, 10%, 1);
|
||||
--Primitive--Zinc--950: hsla(240, 10%, 4%, 1);
|
||||
--Primitive--Slate--50: hsla(210, 40%, 98%, 1);
|
||||
--Primitive--Slate--100: hsla(210, 40%, 96%, 1);
|
||||
--Primitive--Slate--200: hsla(214, 32%, 91%, 1);
|
||||
--Primitive--Slate--300: hsla(213, 27%, 84%, 1);
|
||||
--Primitive--Slate--400: hsla(215, 20%, 65%, 1);
|
||||
--Primitive--Slate--500: hsla(215, 16%, 47%, 1);
|
||||
--Primitive--Slate--600: hsla(215, 19%, 35%, 1);
|
||||
--Primitive--Slate--700: hsla(215, 25%, 27%, 1);
|
||||
--Primitive--Slate--800: hsla(217, 33%, 17%, 1);
|
||||
--Primitive--Slate--900: hsla(222, 47%, 11%, 1);
|
||||
--Primitive--Slate--950: hsla(229, 84%, 5%, 1);
|
||||
--Primitive--Gray--50: hsla(210, 20%, 98%, 1);
|
||||
--Primitive--Gray--100: hsla(220, 14%, 96%, 1);
|
||||
--Primitive--Gray--200: hsla(220, 13%, 91%, 1);
|
||||
--Primitive--Gray--300: hsla(216, 12%, 84%, 1);
|
||||
--Primitive--Gray--400: hsla(218, 11%, 65%, 1);
|
||||
--Primitive--Gray--500: hsla(220, 9%, 46%, 1);
|
||||
--Primitive--Gray--600: hsla(0, 0%, 32%, 1);
|
||||
--Primitive--Gray--700: hsla(217, 19%, 27%, 1);
|
||||
--Primitive--Gray--800: hsla(215, 28%, 17%, 1);
|
||||
--Primitive--Gray--900: hsla(221, 39%, 11%, 1);
|
||||
--Primitive--Gray--950: hsla(224, 71%, 4%, 1);
|
||||
--Primitive--Red--50: hsla(0, 86%, 97%, 1);
|
||||
--Primitive--Red--100: hsla(0, 93%, 94%, 1);
|
||||
--Primitive--Red--200: hsla(0, 96%, 89%, 1);
|
||||
--Primitive--Red--300: hsla(0, 94%, 82%, 1);
|
||||
--Primitive--Red--400: hsla(0, 91%, 71%, 1);
|
||||
--Primitive--Red--500: hsla(0, 84%, 60%, 1);
|
||||
--Primitive--Red--600: hsla(0, 72%, 51%, 1);
|
||||
--Primitive--Red--700: hsla(0, 74%, 42%, 1);
|
||||
--Primitive--Red--800: hsla(0, 70%, 35%, 1);
|
||||
--Primitive--Red--900: hsla(0, 63%, 31%, 1);
|
||||
--Primitive--Red--950: hsla(0, 75%, 15%, 1);
|
||||
--Primitive--Orange--50: hsla(33, 100%, 96%, 1);
|
||||
--Primitive--Orange--100: hsla(34, 100%, 92%, 1);
|
||||
--Primitive--Orange--200: hsla(32, 98%, 83%, 1);
|
||||
--Primitive--Orange--300: hsla(31, 97%, 72%, 1);
|
||||
--Primitive--Orange--400: hsla(27, 96%, 61%, 1);
|
||||
--Primitive--Orange--500: hsla(25, 95%, 53%, 1);
|
||||
--Primitive--Orange--600: hsla(21, 90%, 48%, 1);
|
||||
--Primitive--Orange--700: hsla(17, 88%, 40%, 1);
|
||||
--Primitive--Orange--800: hsla(15, 79%, 34%, 1);
|
||||
--Primitive--Orange--900: hsla(15, 75%, 28%, 1);
|
||||
--Primitive--Orange--950: hsla(13, 81%, 15%, 1);
|
||||
--Primitive--Amber--50: hsla(48, 100%, 96%, 1);
|
||||
--Primitive--Amber--100: hsla(48, 96%, 89%, 1);
|
||||
--Primitive--Amber--200: hsla(48, 97%, 77%, 1);
|
||||
--Primitive--Amber--300: hsla(46, 97%, 65%, 1);
|
||||
--Primitive--Amber--400: hsla(43, 96%, 56%, 1);
|
||||
--Primitive--Amber--500: hsla(38, 92%, 50%, 1);
|
||||
--Primitive--Amber--600: hsla(32, 95%, 44%, 1);
|
||||
--Primitive--Amber--700: hsla(26, 90%, 37%, 1);
|
||||
--Primitive--Amber--800: hsla(23, 83%, 31%, 1);
|
||||
--Primitive--Amber--900: hsla(22, 78%, 26%, 1);
|
||||
--Primitive--Amber--950: hsla(21, 92%, 14%, 1);
|
||||
--Primitive--Yellow--50: hsla(55, 92%, 95%, 1);
|
||||
--Primitive--Yellow--100: hsla(55, 97%, 88%, 1);
|
||||
--Primitive--Yellow--200: hsla(53, 98%, 77%, 1);
|
||||
--Primitive--Yellow--300: hsla(50, 98%, 64%, 1);
|
||||
--Primitive--Yellow--400: hsla(48, 96%, 53%, 1);
|
||||
--Primitive--Yellow--500: hsla(45, 93%, 47%, 1);
|
||||
--Primitive--Yellow--600: hsla(41, 96%, 40%, 1);
|
||||
--Primitive--Yellow--700: hsla(35, 92%, 33%, 1);
|
||||
--Primitive--Yellow--800: hsla(32, 81%, 29%, 1);
|
||||
--Primitive--Yellow--900: hsla(28, 73%, 26%, 1);
|
||||
--Primitive--Yellow--950: hsla(26, 83%, 14%, 1);
|
||||
--Primitive--Lime--50: hsla(78, 92%, 95%, 1);
|
||||
--Primitive--Lime--100: hsla(80, 89%, 89%, 1);
|
||||
--Primitive--Lime--200: hsla(81, 88%, 80%, 1);
|
||||
--Primitive--Lime--300: hsla(82, 85%, 67%, 1);
|
||||
--Primitive--Lime--400: hsla(83, 78%, 55%, 1);
|
||||
--Primitive--Lime--500: hsla(84, 81%, 44%, 1);
|
||||
--Primitive--Lime--600: hsla(85, 85%, 35%, 1);
|
||||
--Primitive--Lime--700: hsla(86, 78%, 27%, 1);
|
||||
--Primitive--Lime--800: hsla(86, 69%, 23%, 1);
|
||||
--Primitive--Lime--900: hsla(88, 61%, 20%, 1);
|
||||
--Primitive--Lime--950: hsla(89, 80%, 10%, 1);
|
||||
--Primitive--Green--50: hsla(138, 76%, 97%, 1);
|
||||
--Primitive--Green--100: hsla(141, 84%, 93%, 1);
|
||||
--Primitive--Green--200: hsla(141, 79%, 85%, 1);
|
||||
--Primitive--Green--300: hsla(142, 77%, 73%, 1);
|
||||
--Primitive--Green--400: hsla(142, 69%, 58%, 1);
|
||||
--Primitive--Green--500: hsla(142, 71%, 45%, 1);
|
||||
--Primitive--Green--600: hsla(142, 76%, 36%, 1);
|
||||
--Primitive--Green--700: hsla(142, 72%, 29%, 1);
|
||||
--Primitive--Green--800: hsla(143, 64%, 24%, 1);
|
||||
--Primitive--Green--900: hsla(144, 61%, 20%, 1);
|
||||
--Primitive--Green--950: hsla(145, 80%, 10%, 1);
|
||||
--Primitive--Emerald--50: hsla(152, 81%, 96%, 1);
|
||||
--Primitive--Emerald--100: hsla(149, 80%, 90%, 1);
|
||||
--Primitive--Emerald--200: hsla(152, 76%, 80%, 1);
|
||||
--Primitive--Emerald--300: hsla(156, 72%, 67%, 1);
|
||||
--Primitive--Emerald--400: hsla(158, 64%, 52%, 1);
|
||||
--Primitive--Emerald--500: hsla(160, 84%, 39%, 1);
|
||||
--Primitive--Emerald--600: hsla(161, 94%, 30%, 1);
|
||||
--Primitive--Emerald--700: hsla(163, 94%, 24%, 1);
|
||||
--Primitive--Emerald--800: hsla(163, 88%, 20%, 1);
|
||||
--Primitive--Emerald--900: hsla(164, 86%, 16%, 1);
|
||||
--Primitive--Emerald--950: hsla(166, 91%, 9%, 1);
|
||||
--Primitive--Teal--50: hsla(166, 76%, 97%, 1);
|
||||
--Primitive--Teal--100: hsla(167, 85%, 89%, 1);
|
||||
--Primitive--Teal--200: hsla(168, 84%, 78%, 1);
|
||||
--Primitive--Teal--300: hsla(171, 77%, 64%, 1);
|
||||
--Primitive--Teal--400: hsla(172, 66%, 50%, 1);
|
||||
--Primitive--Teal--500: hsla(173, 80%, 40%, 1);
|
||||
--Primitive--Teal--600: hsla(175, 84%, 32%, 1);
|
||||
--Primitive--Teal--700: hsla(175, 77%, 26%, 1);
|
||||
--Primitive--Teal--800: hsla(176, 69%, 22%, 1);
|
||||
--Primitive--Teal--900: hsla(176, 61%, 19%, 1);
|
||||
--Primitive--Teal--950: hsla(179, 84%, 10%, 1);
|
||||
--Primitive--Cyan--50: hsla(183, 100%, 96%, 1);
|
||||
--Primitive--Cyan--100: hsla(185, 96%, 90%, 1);
|
||||
--Primitive--Cyan--200: hsla(186, 94%, 82%, 1);
|
||||
--Primitive--Cyan--300: hsla(187, 92%, 69%, 1);
|
||||
--Primitive--Cyan--400: hsla(188, 86%, 53%, 1);
|
||||
--Primitive--Cyan--500: hsla(189, 94%, 43%, 1);
|
||||
--Primitive--Cyan--600: hsla(192, 91%, 36%, 1);
|
||||
--Primitive--Cyan--700: hsla(193, 82%, 31%, 1);
|
||||
--Primitive--Cyan--800: hsla(194, 70%, 27%, 1);
|
||||
--Primitive--Cyan--900: hsla(196, 64%, 24%, 1);
|
||||
--Primitive--Cyan--950: hsla(197, 79%, 15%, 1);
|
||||
--Primitive--Sky--50: hsla(204, 100%, 97%, 1);
|
||||
--Primitive--Sky--100: hsla(204, 94%, 94%, 1);
|
||||
--Primitive--Sky--200: hsla(201, 94%, 86%, 1);
|
||||
--Primitive--Sky--300: hsla(199, 95%, 74%, 1);
|
||||
--Primitive--Sky--400: hsla(198, 93%, 60%, 1);
|
||||
--Primitive--Sky--500: hsla(199, 89%, 48%, 1);
|
||||
--Primitive--Sky--600: hsla(200, 98%, 39%, 1);
|
||||
--Primitive--Sky--700: hsla(201, 96%, 32%, 1);
|
||||
--Primitive--Sky--800: hsla(201, 90%, 27%, 1);
|
||||
--Primitive--Sky--900: hsla(202, 80%, 24%, 1);
|
||||
--Primitive--Sky--950: hsla(204, 80%, 16%, 1);
|
||||
--Primitive--Blue--50: hsla(214, 100%, 97%, 1);
|
||||
--Primitive--Blue--100: hsla(214, 95%, 93%, 1);
|
||||
--Primitive--Blue--200: hsla(213, 97%, 87%, 1);
|
||||
--Primitive--Blue--300: hsla(212, 96%, 78%, 1);
|
||||
--Primitive--Blue--400: hsla(213, 94%, 68%, 1);
|
||||
--Primitive--Blue--500: hsla(217, 91%, 60%, 1);
|
||||
--Primitive--Blue--600: hsla(221, 83%, 53%, 1);
|
||||
--Primitive--Blue--700: hsla(224, 76%, 48%, 1);
|
||||
--Primitive--Blue--800: hsla(226, 71%, 40%, 1);
|
||||
--Primitive--Blue--900: hsla(224, 64%, 33%, 1);
|
||||
--Primitive--Blue--950: hsla(226, 57%, 21%, 1);
|
||||
--Primitive--Indigo--50: hsla(226, 100%, 97%, 1);
|
||||
--Primitive--Indigo--100: hsla(226, 100%, 94%, 1);
|
||||
--Primitive--Indigo--200: hsla(228, 96%, 89%, 1);
|
||||
--Primitive--Indigo--300: hsla(230, 94%, 82%, 1);
|
||||
--Primitive--Indigo--400: hsla(234, 89%, 74%, 1);
|
||||
--Primitive--Indigo--500: hsla(239, 84%, 67%, 1);
|
||||
--Primitive--Indigo--600: hsla(243, 75%, 59%, 1);
|
||||
--Primitive--Indigo--700: hsla(245, 58%, 51%, 1);
|
||||
--Primitive--Indigo--800: hsla(244, 55%, 41%, 1);
|
||||
--Primitive--Indigo--900: hsla(242, 47%, 34%, 1);
|
||||
--Primitive--Indigo--950: hsla(244, 47%, 20%, 1);
|
||||
--Primitive--Violet--50: hsla(250, 100%, 98%, 1);
|
||||
--Primitive--Violet--100: hsla(251, 91%, 95%, 1);
|
||||
--Primitive--Violet--200: hsla(251, 95%, 92%, 1);
|
||||
--Primitive--Violet--300: hsla(253, 95%, 85%, 1);
|
||||
--Primitive--Violet--400: hsla(255, 92%, 76%, 1);
|
||||
--Primitive--Violet--500: hsla(258, 90%, 66%, 1);
|
||||
--Primitive--Violet--600: hsla(262, 83%, 58%, 1);
|
||||
--Primitive--Violet--700: hsla(263, 70%, 50%, 1);
|
||||
--Primitive--Violet--800: hsla(263, 69%, 42%, 1);
|
||||
--Primitive--Violet--900: hsla(264, 67%, 35%, 1);
|
||||
--Primitive--Violet--950: hsla(262, 78%, 23%, 1);
|
||||
--Primitive--Purple--50: hsla(270, 100%, 98%, 1);
|
||||
--Primitive--Purple--100: hsla(269, 100%, 95%, 1);
|
||||
--Primitive--Purple--200: hsla(269, 100%, 92%, 1);
|
||||
--Primitive--Purple--300: hsla(269, 97%, 85%, 1);
|
||||
--Primitive--Purple--400: hsla(270, 95%, 75%, 1);
|
||||
--Primitive--Purple--500: hsla(271, 91%, 65%, 1);
|
||||
--Primitive--Purple--600: hsla(271, 81%, 56%, 1);
|
||||
--Primitive--Purple--700: hsla(272, 72%, 47%, 1);
|
||||
--Primitive--Purple--800: hsla(273, 67%, 39%, 1);
|
||||
--Primitive--Purple--900: hsla(274, 66%, 32%, 1);
|
||||
--Primitive--Purple--950: hsla(274, 87%, 21%, 1);
|
||||
--Primitive--Fuchsia--50: hsla(289, 100%, 98%, 1);
|
||||
--Primitive--Fuchsia--100: hsla(287, 100%, 95%, 1);
|
||||
--Primitive--Fuchsia--200: hsla(288, 96%, 91%, 1);
|
||||
--Primitive--Fuchsia--300: hsla(291, 93%, 83%, 1);
|
||||
--Primitive--Fuchsia--400: hsla(292, 91%, 73%, 1);
|
||||
--Primitive--Fuchsia--500: hsla(292, 84%, 61%, 1);
|
||||
--Primitive--Fuchsia--600: hsla(293, 69%, 49%, 1);
|
||||
--Primitive--Fuchsia--700: hsla(295, 72%, 40%, 1);
|
||||
--Primitive--Fuchsia--800: hsla(295, 70%, 33%, 1);
|
||||
--Primitive--Fuchsia--900: hsla(297, 64%, 28%, 1);
|
||||
--Primitive--Fuchsia--950: hsla(297, 90%, 16%, 1);
|
||||
--Primitive--Pink--50: hsla(327, 73%, 97%, 1);
|
||||
--Primitive--Pink--100: hsla(326, 78%, 95%, 1);
|
||||
--Primitive--Pink--200: hsla(326, 85%, 90%, 1);
|
||||
--Primitive--Pink--300: hsla(327, 87%, 82%, 1);
|
||||
--Primitive--Pink--400: hsla(329, 86%, 70%, 1);
|
||||
--Primitive--Pink--500: hsla(330, 81%, 60%, 1);
|
||||
--Primitive--Pink--600: hsla(333, 71%, 51%, 1);
|
||||
--Primitive--Pink--700: hsla(335, 78%, 42%, 1);
|
||||
--Primitive--Pink--800: hsla(336, 74%, 35%, 1);
|
||||
--Primitive--Pink--900: hsla(336, 69%, 30%, 1);
|
||||
--Primitive--Pink--950: hsla(336, 84%, 17%, 1);
|
||||
--Primitive--Rose--50: hsla(356, 100%, 97%, 1);
|
||||
--Primitive--Rose--100: hsla(356, 100%, 95%, 1);
|
||||
--Primitive--Rose--200: hsla(353, 96%, 90%, 1);
|
||||
--Primitive--Rose--300: hsla(353, 96%, 82%, 1);
|
||||
--Primitive--Rose--400: hsla(351, 95%, 71%, 1);
|
||||
--Primitive--Rose--500: hsla(350, 89%, 60%, 1);
|
||||
--Primitive--Rose--600: hsla(347, 77%, 50%, 1);
|
||||
--Primitive--Rose--700: hsla(345, 83%, 41%, 1);
|
||||
--Primitive--Rose--800: hsla(343, 80%, 35%, 1);
|
||||
--Primitive--Rose--900: hsla(342, 75%, 30%, 1);
|
||||
--Primitive--Rose--950: hsla(343, 88%, 16%, 1);
|
||||
--Brand--Base_Colors--Destructive: var(--Primitive--Red--500);
|
||||
--Brand--Base_Colors--Success: var(--Primitive--Green--500);
|
||||
--Brand--Base_Colors--Warning: var(--Primitive--Amber--500);
|
||||
--Brand--Base_Colors--White: var(--Primitive--White);
|
||||
--Brand--Base_Colors--Black: var(--Primitive--Black);
|
||||
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--900); /*页面背景色:应用在整个页面的最底层。*/
|
||||
--Brand--Semantic_Colors--Background-subtle: hsla(
|
||||
0,
|
||||
0%,
|
||||
100%,
|
||||
0.02
|
||||
); /*细微背景色:用于需要与主背景有微弱区分的区域,如代码块背景。*/
|
||||
--Brand--Semantic_Colors--Foreground: hsla(0, 0%, 100%, 0.9); /*主要前景/文字色:用于正文、标题等。*/
|
||||
--Brand--Semantic_Colors--Foreground-secondary: hsla(0, 0%, 100%, 0.6); /*次要前景/文字色:用于辅助性文本、描述。*/
|
||||
--Brand--Semantic_Colors--Foreground-muted: hsla(0, 0%, 100%, 0.4); /*静默前景/文字色:用于禁用状态的文字、占位符。*/
|
||||
--Brand--Semantic_Colors--Border: hsla(0, 0%, 100%, 0.1); /*默认边框色:用于卡片、输入框、分隔线。*/
|
||||
--Brand--Semantic_Colors--Border-hover: hsla(0, 0%, 100%, 0.2); /*激活边框色:用于元素被按下或激活时的边框。*/
|
||||
--Brand--Semantic_Colors--Border-active: hsla(0, 0%, 100%, 0.3); /*激活边框色:用于元素被按下或激活时的边框。*/
|
||||
--Brand--Semantic_Colors--Ring: hsla(
|
||||
84,
|
||||
81%,
|
||||
44%,
|
||||
0.4
|
||||
); /*聚焦环颜色:用于输入框等元素在聚焦 (Focus) 状态下的外发光。*/
|
||||
--Brand--UI_Element_Colors--Modal--Backdrop: hsla(0, 0%, 0%, 0.06);
|
||||
--Brand--UI_Element_Colors--Modal--Thumb: hsla(0, 0%, 100%, 0.2);
|
||||
--Brand--UI_Element_Colors--Modal--Thumb_Hover: hsla(0, 0%, 100%, 0.3);
|
||||
--Brand--UI_Element_Colors--Icon--Default: var(--Brand--Semantic_Colors--Foreground-secondary);
|
||||
--Brand--UI_Element_Colors--Icon--Hover: var(--Brand--Semantic_Colors--Foreground);
|
||||
--Brand--UI_Element_Colors--Input_Select--Background: var(--Brand--Base_Colors--Black);
|
||||
--Brand--UI_Element_Colors--Input_Select--Border: var(--Brand--Semantic_Colors--Border);
|
||||
--Brand--UI_Element_Colors--Input_Select--Border_Hover: var(--Brand--Semantic_Colors--Border-hover);
|
||||
--Brand--UI_Element_Colors--Input_Select--Border_Focus: var(--Brand--Base_Colors--Primary);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Background: var(--Brand--Base_Colors--Primary);
|
||||
--Brand--UI_Element_Colors--Card_Container--Background: var(--Brand--Base_Colors--Black);
|
||||
--Brand--UI_Element_Colors--Card_Container--Border: var(--Brand--Semantic_Colors--Border);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Background: hsla(0, 0%, 100%, 0);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Text: var(--Brand--Semantic_Colors--Foreground);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Background_Hover: var(--Opacity--White--White-10);
|
||||
--Brand--UI_Element_Colors--Ghost_Button--Background_Active: hsla(0, 0%, 100%, 0.15);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Background: var(--Opacity--White--White-10);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Text: var(--Brand--Semantic_Colors--Foreground);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Background_Hover: var(--Opacity--White--White-20);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Background_Active: hsla(0, 0%, 100%, 0.25);
|
||||
--Brand--UI_Element_Colors--Secondary_Button--Border: var(--Brand--Semantic_Colors--Border);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Text: var(--Brand--Base_Colors--White);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Background_Hover: hsla(84, 81%, 44%, 0.85);
|
||||
--Brand--UI_Element_Colors--Primary_Button--2nd_Background: hsla(84, 81%, 44%, 0.1);
|
||||
--Brand--UI_Element_Colors--Primary_Button--3rd_Background: hsla(84, 81%, 44%, 0.05);
|
||||
--Brand--UI_Element_Colors--Primary_Button--Background_Active: hsla(84, 81%, 44%, 0.7);
|
||||
--Boolean: false;
|
||||
}
|
||||
127
src/main/ipc.ts
127
src/main/ipc.ts
@@ -19,6 +19,7 @@ import type {
|
||||
FileMetadata,
|
||||
Notification,
|
||||
OcrProvider,
|
||||
PluginError,
|
||||
Provider,
|
||||
Shortcut,
|
||||
SupportedOcrFile
|
||||
@@ -49,6 +50,7 @@ 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'
|
||||
@@ -95,6 +97,18 @@ const vertexAIService = VertexAIService.getInstance()
|
||||
const memoryService = MemoryService.getInstance()
|
||||
const dxtService = new DxtService()
|
||||
const ovmsManager = new OvmsManager()
|
||||
const pluginService = PluginService.getInstance()
|
||||
|
||||
function normalizeError(error: unknown): Error {
|
||||
return error instanceof Error ? error : new Error(String(error))
|
||||
}
|
||||
|
||||
function extractPluginError(error: unknown): PluginError | null {
|
||||
if (error && typeof error === 'object' && 'type' in error && typeof (error as { type: unknown }).type === 'string') {
|
||||
return error as PluginError
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
const appUpdater = new AppUpdater()
|
||||
@@ -894,6 +908,119 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
// CherryAI
|
||||
ipcMain.handle(IpcChannel.Cherryai_GetSignature, (_, params) => generateSignature(params))
|
||||
|
||||
// Claude Code Plugins
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_ListAvailable, async () => {
|
||||
try {
|
||||
const data = await pluginService.listAvailable()
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
const pluginError = extractPluginError(error)
|
||||
if (pluginError) {
|
||||
logger.error('Failed to list available plugins', pluginError)
|
||||
return { success: false, error: pluginError }
|
||||
}
|
||||
|
||||
const err = normalizeError(error)
|
||||
logger.error('Failed to list available plugins', err)
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type: 'TRANSACTION_FAILED',
|
||||
operation: 'list-available',
|
||||
reason: err.message
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_Install, async (_, options) => {
|
||||
try {
|
||||
const data = await pluginService.install(options)
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
logger.error('Failed to install plugin', { options, error })
|
||||
return { success: false, error }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_Uninstall, async (_, options) => {
|
||||
try {
|
||||
await pluginService.uninstall(options)
|
||||
return { success: true, data: undefined }
|
||||
} catch (error) {
|
||||
logger.error('Failed to uninstall plugin', { options, error })
|
||||
return { success: false, error }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_ListInstalled, async (_, agentId: string) => {
|
||||
try {
|
||||
const data = await pluginService.listInstalled(agentId)
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
const pluginError = extractPluginError(error)
|
||||
if (pluginError) {
|
||||
logger.error('Failed to list installed plugins', { agentId, error: pluginError })
|
||||
return { success: false, error: pluginError }
|
||||
}
|
||||
|
||||
const err = normalizeError(error)
|
||||
logger.error('Failed to list installed plugins', { agentId, error: err })
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type: 'TRANSACTION_FAILED',
|
||||
operation: 'list-installed',
|
||||
reason: err.message
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_InvalidateCache, async () => {
|
||||
try {
|
||||
pluginService.invalidateCache()
|
||||
return { success: true, data: undefined }
|
||||
} catch (error) {
|
||||
const pluginError = extractPluginError(error)
|
||||
if (pluginError) {
|
||||
logger.error('Failed to invalidate plugin cache', pluginError)
|
||||
return { success: false, error: pluginError }
|
||||
}
|
||||
|
||||
const err = normalizeError(error)
|
||||
logger.error('Failed to invalidate plugin cache', err)
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type: 'TRANSACTION_FAILED',
|
||||
operation: 'invalidate-cache',
|
||||
reason: err.message
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_ReadContent, async (_, sourcePath: string) => {
|
||||
try {
|
||||
const data = await pluginService.readContent(sourcePath)
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
logger.error('Failed to read plugin content', { sourcePath, error })
|
||||
return { success: false, error }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_WriteContent, async (_, options) => {
|
||||
try {
|
||||
await pluginService.writeContent(options.agentId, options.filename, options.type, options.content)
|
||||
return { success: true, data: undefined }
|
||||
} catch (error) {
|
||||
logger.error('Failed to write plugin content', { options, error })
|
||||
return { success: false, error }
|
||||
}
|
||||
})
|
||||
|
||||
// Preference handlers
|
||||
PreferenceService.registerIpcHandler()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 { VoyageEmbeddings } from './VoyageEmbeddings'
|
||||
@@ -9,7 +8,7 @@ import { VoyageEmbeddings } from './VoyageEmbeddings'
|
||||
export default class EmbeddingsFactory {
|
||||
static create({ embedApiClient, dimensions }: { embedApiClient: ApiClient; dimensions?: number }): BaseEmbeddings {
|
||||
const batchSize = 10
|
||||
const { model, provider, apiKey, apiVersion, baseURL } = embedApiClient
|
||||
const { model, provider, apiKey, baseURL } = embedApiClient
|
||||
if (provider === 'voyageai') {
|
||||
return new VoyageEmbeddings({
|
||||
modelName: model,
|
||||
@@ -38,16 +37,7 @@ export default class EmbeddingsFactory {
|
||||
}
|
||||
})
|
||||
}
|
||||
if (apiVersion !== undefined) {
|
||||
return new AzureOpenAiEmbeddings({
|
||||
azureOpenAIApiKey: apiKey,
|
||||
azureOpenAIApiVersion: apiVersion,
|
||||
azureOpenAIApiDeploymentName: model,
|
||||
azureOpenAIEndpoint: baseURL,
|
||||
dimensions,
|
||||
batchSize
|
||||
})
|
||||
}
|
||||
// NOTE: Azure OpenAI 也走 OpenAIEmbeddings, baseURL是https://xxxx.openai.azure.com/openai/v1
|
||||
return new OpenAiEmbeddings({
|
||||
model,
|
||||
apiKey,
|
||||
|
||||
199
src/main/knowledge/preprocess/OpenMineruPreprocessProvider.ts
Normal file
199
src/main/knowledge/preprocess/OpenMineruPreprocessProvider.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { fileStorage } from '@main/services/FileStorage'
|
||||
import type { FileMetadata, PreprocessProvider } from '@types'
|
||||
import AdmZip from 'adm-zip'
|
||||
import { net } from 'electron'
|
||||
import FormData from 'form-data'
|
||||
|
||||
import BasePreprocessProvider from './BasePreprocessProvider'
|
||||
|
||||
const logger = loggerService.withContext('MineruPreprocessProvider')
|
||||
|
||||
export default class OpenMineruPreprocessProvider extends BasePreprocessProvider {
|
||||
constructor(provider: PreprocessProvider, userId?: string) {
|
||||
super(provider, userId)
|
||||
}
|
||||
|
||||
public async parseFile(
|
||||
sourceId: string,
|
||||
file: FileMetadata
|
||||
): Promise<{ processedFile: FileMetadata; quota: number }> {
|
||||
try {
|
||||
const filePath = fileStorage.getFilePathById(file)
|
||||
logger.info(`Open MinerU preprocess processing started: ${filePath}`)
|
||||
await this.validateFile(filePath)
|
||||
|
||||
// 1. Update progress
|
||||
await this.sendPreprocessProgress(sourceId, 50)
|
||||
logger.info(`File ${file.name} is starting processing...`)
|
||||
|
||||
// 2. Upload file and extract
|
||||
const { path: outputPath } = await this.uploadFileAndExtract(file)
|
||||
|
||||
// 3. Check quota
|
||||
const quota = await this.checkQuota()
|
||||
|
||||
// 4. Create processed file info
|
||||
return {
|
||||
processedFile: this.createProcessedFileInfo(file, outputPath),
|
||||
quota
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Open MinerU preprocess processing failed for:`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public async checkQuota() {
|
||||
// self-hosted version always has enough quota
|
||||
return Infinity
|
||||
}
|
||||
|
||||
private async validateFile(filePath: string): Promise<void> {
|
||||
const pdfBuffer = await fs.promises.readFile(filePath)
|
||||
|
||||
const doc = await this.readPdf(pdfBuffer)
|
||||
|
||||
// File page count must be less than 600 pages
|
||||
if (doc.numPages >= 600) {
|
||||
throw new Error(`PDF page count (${doc.numPages}) exceeds the limit of 600 pages`)
|
||||
}
|
||||
// File size must be less than 200MB
|
||||
if (pdfBuffer.length >= 200 * 1024 * 1024) {
|
||||
const fileSizeMB = Math.round(pdfBuffer.length / (1024 * 1024))
|
||||
throw new Error(`PDF file size (${fileSizeMB}MB) exceeds the limit of 200MB`)
|
||||
}
|
||||
}
|
||||
|
||||
private createProcessedFileInfo(file: FileMetadata, outputPath: string): FileMetadata {
|
||||
// Find the main file after extraction
|
||||
let finalPath = ''
|
||||
let finalName = file.origin_name.replace('.pdf', '.md')
|
||||
// Find the corresponding folder by file name
|
||||
outputPath = path.join(outputPath, `${file.origin_name.replace('.pdf', '')}`)
|
||||
try {
|
||||
const files = fs.readdirSync(outputPath)
|
||||
|
||||
const mdFile = files.find((f) => f.endsWith('.md'))
|
||||
if (mdFile) {
|
||||
const originalMdPath = path.join(outputPath, mdFile)
|
||||
const newMdPath = path.join(outputPath, finalName)
|
||||
|
||||
// Rename file to original file name
|
||||
try {
|
||||
fs.renameSync(originalMdPath, newMdPath)
|
||||
finalPath = newMdPath
|
||||
logger.info(`Renamed markdown file from ${mdFile} to ${finalName}`)
|
||||
} catch (renameError) {
|
||||
logger.warn(`Failed to rename file ${mdFile} to ${finalName}: ${renameError}`)
|
||||
// If rename fails, use the original file
|
||||
finalPath = originalMdPath
|
||||
finalName = mdFile
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to read output directory ${outputPath}:`, error as Error)
|
||||
finalPath = path.join(outputPath, `${file.id}.md`)
|
||||
}
|
||||
|
||||
return {
|
||||
...file,
|
||||
name: finalName,
|
||||
path: finalPath,
|
||||
ext: '.md',
|
||||
size: fs.existsSync(finalPath) ? fs.statSync(finalPath).size : 0
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadFileAndExtract(
|
||||
file: FileMetadata,
|
||||
maxRetries: number = 5,
|
||||
intervalMs: number = 5000
|
||||
): Promise<{ path: string }> {
|
||||
let retries = 0
|
||||
|
||||
const endpoint = `${this.provider.apiHost}/file_parse`
|
||||
|
||||
// Get file stream
|
||||
const filePath = fileStorage.getFilePathById(file)
|
||||
const fileBuffer = await fs.promises.readFile(filePath)
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('return_md', 'true')
|
||||
formData.append('response_format_zip', 'true')
|
||||
formData.append('files', fileBuffer, {
|
||||
filename: file.origin_name
|
||||
})
|
||||
|
||||
while (retries < maxRetries) {
|
||||
let zipPath: string | undefined
|
||||
|
||||
try {
|
||||
const response = await net.fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
token: this.userId ?? '',
|
||||
...(this.provider.apiKey ? { Authorization: `Bearer ${this.provider.apiKey}` } : {}),
|
||||
...formData.getHeaders()
|
||||
},
|
||||
body: formData.getBuffer()
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
// Check if response header is application/zip
|
||||
if (response.headers.get('content-type') !== 'application/zip') {
|
||||
throw new Error(`Downloaded ZIP file has unexpected content-type: ${response.headers.get('content-type')}`)
|
||||
}
|
||||
|
||||
const dirPath = this.storageDir
|
||||
|
||||
zipPath = path.join(dirPath, `${file.id}.zip`)
|
||||
const extractPath = path.join(dirPath, `${file.id}`)
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
fs.writeFileSync(zipPath, Buffer.from(arrayBuffer))
|
||||
logger.info(`Downloaded ZIP file: ${zipPath}`)
|
||||
|
||||
// Ensure extraction directory exists
|
||||
if (!fs.existsSync(extractPath)) {
|
||||
fs.mkdirSync(extractPath, { recursive: true })
|
||||
}
|
||||
|
||||
// Extract files
|
||||
const zip = new AdmZip(zipPath)
|
||||
zip.extractAllTo(extractPath, true)
|
||||
logger.info(`Extracted files to: ${extractPath}`)
|
||||
|
||||
return { path: extractPath }
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Failed to upload and extract file: ${(error as Error).message}, retry ${retries + 1}/${maxRetries}`
|
||||
)
|
||||
if (retries === maxRetries - 1) {
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
// Delete temporary ZIP file
|
||||
if (zipPath && fs.existsSync(zipPath)) {
|
||||
try {
|
||||
fs.unlinkSync(zipPath)
|
||||
logger.info(`Deleted temporary ZIP file: ${zipPath}`)
|
||||
} catch (deleteError) {
|
||||
logger.warn(`Failed to delete temporary ZIP file ${zipPath}:`, deleteError as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
retries++
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs))
|
||||
}
|
||||
|
||||
throw new Error(`Processing timeout for file: ${file.id}`)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import DefaultPreprocessProvider from './DefaultPreprocessProvider'
|
||||
import Doc2xPreprocessProvider from './Doc2xPreprocessProvider'
|
||||
import MineruPreprocessProvider from './MineruPreprocessProvider'
|
||||
import MistralPreprocessProvider from './MistralPreprocessProvider'
|
||||
import OpenMineruPreprocessProvider from './OpenMineruPreprocessProvider'
|
||||
export default class PreprocessProviderFactory {
|
||||
static create(provider: PreprocessProvider, userId?: string): BasePreprocessProvider {
|
||||
switch (provider.id) {
|
||||
@@ -14,6 +15,8 @@ export default class PreprocessProviderFactory {
|
||||
return new MistralPreprocessProvider(provider)
|
||||
case 'mineru':
|
||||
return new MineruPreprocessProvider(provider, userId)
|
||||
case 'open-mineru':
|
||||
return new OpenMineruPreprocessProvider(provider, userId)
|
||||
default:
|
||||
return new DefaultPreprocessProvider(provider)
|
||||
}
|
||||
|
||||
1171
src/main/services/PluginService.ts
Normal file
1171
src/main/services/PluginService.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
import type { McpHttpServerConfig, Options, SDKMessage } from '@anthropic-ai/claude-agent-sdk'
|
||||
import type { CanUseTool, McpHttpServerConfig, Options, SDKMessage } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { loggerService } from '@logger'
|
||||
import { config as apiConfigService } from '@main/apiServer/config'
|
||||
@@ -12,10 +12,23 @@ import { app } from 'electron'
|
||||
|
||||
import type { GetAgentSessionResponse } from '../..'
|
||||
import type { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface'
|
||||
import { promptForToolApproval } from './tool-permissions'
|
||||
import { ClaudeStreamState, transformSDKMessageToStreamParts } from './transform'
|
||||
|
||||
const require_ = createRequire(import.meta.url)
|
||||
const logger = loggerService.withContext('ClaudeCodeService')
|
||||
const DEFAULT_AUTO_ALLOW_TOOLS = new Set(['Read', 'Glob', 'Grep'])
|
||||
const shouldAutoApproveTools = process.env.CHERRY_AUTO_ALLOW_TOOLS === '1'
|
||||
|
||||
type UserInputMessage = {
|
||||
type: 'user'
|
||||
parent_tool_use_id: string | null
|
||||
session_id: string
|
||||
message: {
|
||||
role: 'user'
|
||||
content: string
|
||||
}
|
||||
}
|
||||
|
||||
class ClaudeCodeStream extends EventEmitter implements AgentStream {
|
||||
declare emit: (event: 'data', data: AgentStreamEvent) => boolean
|
||||
@@ -100,6 +113,41 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
|
||||
const errorChunks: string[] = []
|
||||
|
||||
const sessionAllowedTools = new Set<string>(session.allowed_tools ?? [])
|
||||
const autoAllowTools = new Set<string>([...DEFAULT_AUTO_ALLOW_TOOLS, ...sessionAllowedTools])
|
||||
const normalizeToolName = (name: string) => (name.startsWith('builtin_') ? name.slice('builtin_'.length) : name)
|
||||
|
||||
const canUseTool: CanUseTool = async (toolName, input, options) => {
|
||||
logger.info('Handling tool permission check', {
|
||||
toolName,
|
||||
suggestionCount: options.suggestions?.length ?? 0
|
||||
})
|
||||
|
||||
if (shouldAutoApproveTools) {
|
||||
logger.debug('Auto-approving tool due to CHERRY_AUTO_ALLOW_TOOLS flag', { toolName })
|
||||
return { behavior: 'allow', updatedInput: input }
|
||||
}
|
||||
|
||||
if (options.signal.aborted) {
|
||||
logger.debug('Permission request signal already aborted; denying tool', { toolName })
|
||||
return {
|
||||
behavior: 'deny',
|
||||
message: 'Tool request was cancelled before prompting the user'
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedToolName = normalizeToolName(toolName)
|
||||
if (autoAllowTools.has(toolName) || autoAllowTools.has(normalizedToolName)) {
|
||||
logger.debug('Auto-allowing tool from allowed list', {
|
||||
toolName,
|
||||
normalizedToolName
|
||||
})
|
||||
return { behavior: 'allow', updatedInput: input }
|
||||
}
|
||||
|
||||
return promptForToolApproval(toolName, input, options)
|
||||
}
|
||||
|
||||
// Build SDK options from parameters
|
||||
const options: Options = {
|
||||
abortController,
|
||||
@@ -122,7 +170,8 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
includePartialMessages: true,
|
||||
permissionMode: session.configuration?.permission_mode,
|
||||
maxTurns: session.configuration?.max_turns,
|
||||
allowedTools: session.allowed_tools
|
||||
allowedTools: session.allowed_tools,
|
||||
canUseTool
|
||||
}
|
||||
|
||||
if (session.accessible_paths.length > 1) {
|
||||
@@ -161,9 +210,14 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
resume: options.resume
|
||||
})
|
||||
|
||||
const { stream: userInputStream, close: closeUserStream } = this.createUserMessageStream(
|
||||
prompt,
|
||||
abortController.signal
|
||||
)
|
||||
|
||||
// Start async processing on the next tick so listeners can subscribe first
|
||||
setImmediate(() => {
|
||||
this.processSDKQuery(prompt, options, aiStream, errorChunks).catch((error) => {
|
||||
this.processSDKQuery(userInputStream, closeUserStream, options, aiStream, errorChunks).catch((error) => {
|
||||
logger.error('Unhandled Claude Code stream error', {
|
||||
error: error instanceof Error ? { name: error.name, message: error.message } : String(error)
|
||||
})
|
||||
@@ -177,17 +231,90 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
return aiStream
|
||||
}
|
||||
|
||||
private async *userMessages(prompt: string) {
|
||||
{
|
||||
yield {
|
||||
type: 'user' as const,
|
||||
parent_tool_use_id: null,
|
||||
session_id: '',
|
||||
message: {
|
||||
role: 'user' as const,
|
||||
content: prompt
|
||||
private createUserMessageStream(initialPrompt: string, abortSignal: AbortSignal) {
|
||||
const queue: Array<UserInputMessage | null> = []
|
||||
const waiters: Array<(value: UserInputMessage | null) => void> = []
|
||||
let closed = false
|
||||
|
||||
const flushWaiters = (value: UserInputMessage | null) => {
|
||||
const resolve = waiters.shift()
|
||||
if (resolve) {
|
||||
resolve(value)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const enqueue = (value: UserInputMessage | null) => {
|
||||
if (closed) return
|
||||
if (value === null) {
|
||||
closed = true
|
||||
}
|
||||
if (!flushWaiters(value)) {
|
||||
queue.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
if (closed) return
|
||||
enqueue(null)
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
close()
|
||||
}
|
||||
|
||||
if (abortSignal.aborted) {
|
||||
close()
|
||||
} else {
|
||||
abortSignal.addEventListener('abort', onAbort, { once: true })
|
||||
}
|
||||
|
||||
const iterator = (async function* () {
|
||||
try {
|
||||
while (true) {
|
||||
let value: UserInputMessage | null
|
||||
if (queue.length > 0) {
|
||||
value = queue.shift() ?? null
|
||||
} else if (closed) {
|
||||
break
|
||||
} else {
|
||||
// Wait for next message or close signal
|
||||
value = await new Promise<UserInputMessage | null>((resolve) => {
|
||||
waiters.push(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
break
|
||||
}
|
||||
|
||||
yield value
|
||||
}
|
||||
} finally {
|
||||
closed = true
|
||||
abortSignal.removeEventListener('abort', onAbort)
|
||||
while (waiters.length > 0) {
|
||||
const resolve = waiters.shift()
|
||||
resolve?.(null)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
enqueue({
|
||||
type: 'user',
|
||||
parent_tool_use_id: null,
|
||||
session_id: '',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: initialPrompt
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
stream: iterator,
|
||||
enqueue,
|
||||
close
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +322,8 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
* Process SDK query and emit stream events
|
||||
*/
|
||||
private async processSDKQuery(
|
||||
prompt: string,
|
||||
promptStream: AsyncIterable<UserInputMessage>,
|
||||
closePromptStream: () => void,
|
||||
options: Options,
|
||||
stream: ClaudeCodeStream,
|
||||
errorChunks: string[]
|
||||
@@ -203,14 +331,10 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
const jsonOutput: SDKMessage[] = []
|
||||
let hasCompleted = false
|
||||
const startTime = Date.now()
|
||||
|
||||
const streamState = new ClaudeStreamState()
|
||||
|
||||
try {
|
||||
// Process streaming responses using SDK query
|
||||
for await (const message of query({
|
||||
prompt: this.userMessages(prompt),
|
||||
options
|
||||
})) {
|
||||
for await (const message of query({ prompt: promptStream, options })) {
|
||||
if (hasCompleted) break
|
||||
|
||||
jsonOutput.push(message)
|
||||
@@ -221,10 +345,10 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
content: JSON.stringify(message.message.content)
|
||||
})
|
||||
} else if (message.type === 'stream_event') {
|
||||
logger.silly('Claude stream event', {
|
||||
message,
|
||||
event: JSON.stringify(message.event)
|
||||
})
|
||||
// logger.silly('Claude stream event', {
|
||||
// message,
|
||||
// event: JSON.stringify(message.event)
|
||||
// })
|
||||
} else {
|
||||
logger.silly('Claude response', {
|
||||
message,
|
||||
@@ -232,7 +356,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
})
|
||||
}
|
||||
|
||||
// Transform SDKMessage to UIMessageChunks
|
||||
const chunks = transformSDKMessageToStreamParts(message, streamState)
|
||||
for (const chunk of chunks) {
|
||||
stream.emit('data', {
|
||||
@@ -242,7 +365,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
// Successfully completed
|
||||
hasCompleted = true
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
@@ -251,7 +373,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
messageCount: jsonOutput.length
|
||||
})
|
||||
|
||||
// Emit completion event
|
||||
stream.emit('data', {
|
||||
type: 'complete'
|
||||
})
|
||||
@@ -260,8 +381,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
hasCompleted = true
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
// Check if this is an abort error
|
||||
const errorObj = error as any
|
||||
const isAborted =
|
||||
errorObj?.name === 'AbortError' ||
|
||||
@@ -270,7 +389,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
|
||||
if (isAborted) {
|
||||
logger.info('SDK query aborted by client disconnect', { duration })
|
||||
// Simply cleanup and return - don't emit error events
|
||||
stream.emit('data', {
|
||||
type: 'cancelled',
|
||||
error: new Error('Request aborted by client')
|
||||
@@ -285,11 +403,13 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
error: errorObj instanceof Error ? { name: errorObj.name, message: errorObj.message } : String(errorObj),
|
||||
stderr: errorChunks
|
||||
})
|
||||
// Emit error event
|
||||
|
||||
stream.emit('data', {
|
||||
type: 'error',
|
||||
error: new Error(errorMessage)
|
||||
})
|
||||
} finally {
|
||||
closePromptStream()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
323
src/main/services/agents/services/claudecode/tool-permissions.ts
Normal file
323
src/main/services/agents/services/claudecode/tool-permissions.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { loggerService } from '@logger'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
import { windowService } from '../../../WindowService'
|
||||
import { builtinTools } from './tools'
|
||||
|
||||
const logger = loggerService.withContext('ClaudeCodeService')
|
||||
|
||||
const TOOL_APPROVAL_TIMEOUT_MS = 30_000
|
||||
const MAX_PREVIEW_LENGTH = 2_000
|
||||
const shouldAutoApproveTools = process.env.CHERRY_AUTO_ALLOW_TOOLS === '1'
|
||||
|
||||
type ToolPermissionBehavior = 'allow' | 'deny'
|
||||
|
||||
type ToolPermissionResponsePayload = {
|
||||
requestId: string
|
||||
behavior: ToolPermissionBehavior
|
||||
updatedInput?: unknown
|
||||
message?: string
|
||||
updatedPermissions?: PermissionUpdate[]
|
||||
}
|
||||
|
||||
type PendingPermissionRequest = {
|
||||
fulfill: (update: PermissionResult) => void
|
||||
timeout: NodeJS.Timeout
|
||||
signal?: AbortSignal
|
||||
abortListener?: () => void
|
||||
originalInput: Record<string, unknown>
|
||||
toolName: string
|
||||
}
|
||||
|
||||
type RendererPermissionRequestPayload = {
|
||||
requestId: string
|
||||
toolName: string
|
||||
toolId: string
|
||||
description?: string
|
||||
requiresPermissions: boolean
|
||||
input: Record<string, unknown>
|
||||
inputPreview: string
|
||||
createdAt: number
|
||||
expiresAt: number
|
||||
suggestions: PermissionUpdate[]
|
||||
}
|
||||
|
||||
type RendererPermissionResultPayload = {
|
||||
requestId: string
|
||||
behavior: ToolPermissionBehavior
|
||||
message?: string
|
||||
reason: 'response' | 'timeout' | 'aborted' | 'no-window'
|
||||
}
|
||||
|
||||
const pendingRequests = new Map<string, PendingPermissionRequest>()
|
||||
let ipcHandlersInitialized = false
|
||||
|
||||
const jsonReplacer = (_key: string, value: unknown) => {
|
||||
if (typeof value === 'bigint') return value.toString()
|
||||
if (value instanceof Map) return Object.fromEntries(value.entries())
|
||||
if (value instanceof Set) return Array.from(value.values())
|
||||
if (value instanceof Date) return value.toISOString()
|
||||
if (typeof value === 'function') return undefined
|
||||
if (value === undefined) return undefined
|
||||
return value
|
||||
}
|
||||
|
||||
const sanitizeStructuredData = <T>(value: T): T => {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value, jsonReplacer)) as T
|
||||
} catch (error) {
|
||||
logger.warn('Failed to sanitize structured data for tool permission payload', {
|
||||
error: error instanceof Error ? { name: error.name, message: error.message } : String(error)
|
||||
})
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const buildInputPreview = (value: unknown): string => {
|
||||
let preview: string
|
||||
|
||||
try {
|
||||
preview = JSON.stringify(value, null, 2)
|
||||
} catch (error) {
|
||||
preview = typeof value === 'string' ? value : String(value)
|
||||
}
|
||||
|
||||
if (preview.length > MAX_PREVIEW_LENGTH) {
|
||||
preview = `${preview.slice(0, MAX_PREVIEW_LENGTH)}...`
|
||||
}
|
||||
|
||||
return preview
|
||||
}
|
||||
|
||||
const broadcastToRenderer = (
|
||||
channel: IpcChannel,
|
||||
payload: RendererPermissionRequestPayload | RendererPermissionResultPayload
|
||||
): boolean => {
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
|
||||
if (!mainWindow) {
|
||||
logger.warn('Unable to send agent tool permission payload – main window unavailable', {
|
||||
channel,
|
||||
requestId: 'requestId' in payload ? payload.requestId : undefined
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
mainWindow.webContents.send(channel, payload)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const finalizeRequest = (
|
||||
requestId: string,
|
||||
update: PermissionResult,
|
||||
reason: RendererPermissionResultPayload['reason']
|
||||
) => {
|
||||
const pending = pendingRequests.get(requestId)
|
||||
|
||||
if (!pending) {
|
||||
logger.debug('Attempted to finalize unknown tool permission request', { requestId, reason })
|
||||
return false
|
||||
}
|
||||
|
||||
logger.debug('Finalizing tool permission request', {
|
||||
requestId,
|
||||
toolName: pending.toolName,
|
||||
behavior: update.behavior,
|
||||
reason
|
||||
})
|
||||
|
||||
pendingRequests.delete(requestId)
|
||||
clearTimeout(pending.timeout)
|
||||
|
||||
if (pending.signal && pending.abortListener) {
|
||||
pending.signal.removeEventListener('abort', pending.abortListener)
|
||||
}
|
||||
|
||||
pending.fulfill(update)
|
||||
|
||||
const resultPayload: RendererPermissionResultPayload = {
|
||||
requestId,
|
||||
behavior: update.behavior,
|
||||
message: update.behavior === 'deny' ? update.message : undefined,
|
||||
reason
|
||||
}
|
||||
|
||||
const dispatched = broadcastToRenderer(IpcChannel.AgentToolPermission_Result, resultPayload)
|
||||
|
||||
logger.debug('Sent tool permission result to renderer', {
|
||||
requestId,
|
||||
dispatched
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const ensureIpcHandlersRegistered = () => {
|
||||
if (ipcHandlersInitialized) return
|
||||
|
||||
ipcHandlersInitialized = true
|
||||
|
||||
ipcMain.handle(IpcChannel.AgentToolPermission_Response, async (_event, payload: ToolPermissionResponsePayload) => {
|
||||
logger.debug('main received AgentToolPermission_Response', payload)
|
||||
const { requestId, behavior, updatedInput, message } = payload
|
||||
const pending = pendingRequests.get(requestId)
|
||||
|
||||
if (!pending) {
|
||||
logger.warn('Received renderer tool permission response for unknown request', { requestId })
|
||||
return { success: false, error: 'unknown-request' }
|
||||
}
|
||||
|
||||
logger.debug('Received renderer response for tool permission', {
|
||||
requestId,
|
||||
toolName: pending.toolName,
|
||||
behavior,
|
||||
hasUpdatedPermissions: Array.isArray(payload.updatedPermissions) && payload.updatedPermissions.length > 0
|
||||
})
|
||||
|
||||
const maybeUpdatedInput =
|
||||
updatedInput && typeof updatedInput === 'object' && !Array.isArray(updatedInput)
|
||||
? (updatedInput as Record<string, unknown>)
|
||||
: pending.originalInput
|
||||
|
||||
const sanitizedUpdatedPermissions = Array.isArray(payload.updatedPermissions)
|
||||
? payload.updatedPermissions.map((perm) => sanitizeStructuredData(perm))
|
||||
: undefined
|
||||
|
||||
const finalUpdate: PermissionResult =
|
||||
behavior === 'allow'
|
||||
? {
|
||||
behavior: 'allow',
|
||||
updatedInput: sanitizeStructuredData(maybeUpdatedInput),
|
||||
updatedPermissions: sanitizedUpdatedPermissions
|
||||
}
|
||||
: {
|
||||
behavior: 'deny',
|
||||
message: message ?? 'User denied permission for this tool'
|
||||
}
|
||||
|
||||
finalizeRequest(requestId, finalUpdate, 'response')
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
}
|
||||
|
||||
export async function promptForToolApproval(
|
||||
toolName: string,
|
||||
input: Record<string, unknown>,
|
||||
options?: { signal: AbortSignal; suggestions?: PermissionUpdate[] }
|
||||
): Promise<PermissionResult> {
|
||||
if (shouldAutoApproveTools) {
|
||||
logger.debug('promptForToolApproval auto-approving tool for test', {
|
||||
toolName
|
||||
})
|
||||
|
||||
return { behavior: 'allow', updatedInput: input }
|
||||
}
|
||||
|
||||
ensureIpcHandlersRegistered()
|
||||
|
||||
if (options?.signal?.aborted) {
|
||||
logger.info('Skipping tool approval prompt because request signal is already aborted', { toolName })
|
||||
return { behavior: 'deny', message: 'Tool request was cancelled before prompting the user' }
|
||||
}
|
||||
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
|
||||
if (!mainWindow) {
|
||||
logger.warn('Denying tool usage because no renderer window is available to obtain approval', { toolName })
|
||||
return { behavior: 'deny', message: 'Unable to request approval – renderer not ready' }
|
||||
}
|
||||
|
||||
const toolMetadata = builtinTools.find((tool) => tool.name === toolName || tool.id === toolName)
|
||||
const sanitizedInput = sanitizeStructuredData(input)
|
||||
const inputPreview = buildInputPreview(sanitizedInput)
|
||||
const sanitizedSuggestions = (options?.suggestions ?? []).map((suggestion) => sanitizeStructuredData(suggestion))
|
||||
|
||||
const requestId = randomUUID()
|
||||
const createdAt = Date.now()
|
||||
const expiresAt = createdAt + TOOL_APPROVAL_TIMEOUT_MS
|
||||
|
||||
logger.info('Requesting user approval for tool usage', {
|
||||
requestId,
|
||||
toolName,
|
||||
description: toolMetadata?.description
|
||||
})
|
||||
|
||||
const requestPayload: RendererPermissionRequestPayload = {
|
||||
requestId,
|
||||
toolName,
|
||||
toolId: toolMetadata?.id ?? toolName,
|
||||
description: toolMetadata?.description,
|
||||
requiresPermissions: toolMetadata?.requirePermissions ?? false,
|
||||
input: sanitizedInput,
|
||||
inputPreview,
|
||||
createdAt,
|
||||
expiresAt,
|
||||
suggestions: sanitizedSuggestions
|
||||
}
|
||||
|
||||
const defaultDenyUpdate: PermissionResult = { behavior: 'deny', message: 'Tool request aborted before user decision' }
|
||||
|
||||
logger.debug('Registering tool permission request', {
|
||||
requestId,
|
||||
toolName,
|
||||
requiresPermissions: requestPayload.requiresPermissions,
|
||||
timeoutMs: TOOL_APPROVAL_TIMEOUT_MS,
|
||||
suggestionCount: sanitizedSuggestions.length
|
||||
})
|
||||
|
||||
return new Promise<PermissionResult>((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
logger.info('User tool permission request timed out', { requestId, toolName })
|
||||
finalizeRequest(requestId, { behavior: 'deny', message: 'Timed out waiting for approval' }, 'timeout')
|
||||
}, TOOL_APPROVAL_TIMEOUT_MS)
|
||||
|
||||
const pending: PendingPermissionRequest = {
|
||||
fulfill: resolve,
|
||||
timeout,
|
||||
originalInput: sanitizedInput,
|
||||
toolName,
|
||||
signal: options?.signal
|
||||
}
|
||||
|
||||
if (options?.signal) {
|
||||
const abortListener = () => {
|
||||
logger.info('Tool permission request aborted before user responded', { requestId, toolName })
|
||||
finalizeRequest(requestId, defaultDenyUpdate, 'aborted')
|
||||
}
|
||||
|
||||
pending.abortListener = abortListener
|
||||
options.signal.addEventListener('abort', abortListener, { once: true })
|
||||
}
|
||||
|
||||
pendingRequests.set(requestId, pending)
|
||||
|
||||
logger.debug('Pending tool permission request count', {
|
||||
count: pendingRequests.size
|
||||
})
|
||||
|
||||
const sent = broadcastToRenderer(IpcChannel.AgentToolPermission_Request, requestPayload)
|
||||
|
||||
logger.debug('Broadcasted tool permission request to renderer', {
|
||||
requestId,
|
||||
toolName,
|
||||
sent
|
||||
})
|
||||
|
||||
if (!sent) {
|
||||
finalizeRequest(
|
||||
requestId,
|
||||
{
|
||||
behavior: 'deny',
|
||||
message: 'Unable to request approval because the renderer window is unavailable'
|
||||
},
|
||||
'no-window'
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
223
src/main/utils/fileOperations.ts
Normal file
223
src/main/utils/fileOperations.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import * as fs from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
|
||||
import { isPathInside } from './file'
|
||||
|
||||
const logger = loggerService.withContext('Utils:FileOperations')
|
||||
|
||||
const MAX_RECURSION_DEPTH = 1000
|
||||
|
||||
/**
|
||||
* Recursively copy a directory and all its contents
|
||||
* @param source - Source directory path (must be absolute)
|
||||
* @param destination - Destination directory path (must be absolute)
|
||||
* @param options - Copy options
|
||||
* @param depth - Current recursion depth (internal use)
|
||||
* @throws If copy operation fails or paths are invalid
|
||||
*/
|
||||
export async function copyDirectoryRecursive(
|
||||
source: string,
|
||||
destination: string,
|
||||
options?: { allowedBasePath?: string },
|
||||
depth = 0
|
||||
): Promise<void> {
|
||||
// Input validation
|
||||
if (!source || !destination) {
|
||||
throw new TypeError('Source and destination paths are required')
|
||||
}
|
||||
|
||||
if (!path.isAbsolute(source) || !path.isAbsolute(destination)) {
|
||||
throw new Error('Source and destination paths must be absolute')
|
||||
}
|
||||
|
||||
// Depth limit to prevent stack overflow
|
||||
if (depth > MAX_RECURSION_DEPTH) {
|
||||
throw new Error(`Maximum recursion depth exceeded: ${MAX_RECURSION_DEPTH}`)
|
||||
}
|
||||
|
||||
// Path validation - ensure operations stay within allowed boundaries
|
||||
if (options?.allowedBasePath) {
|
||||
if (!isPathInside(source, options.allowedBasePath)) {
|
||||
throw new Error(`Source path is outside allowed directory: ${source}`)
|
||||
}
|
||||
if (!isPathInside(destination, options.allowedBasePath)) {
|
||||
throw new Error(`Destination path is outside allowed directory: ${destination}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify source exists and is a directory
|
||||
const sourceStats = await fs.promises.lstat(source)
|
||||
if (!sourceStats.isDirectory()) {
|
||||
throw new Error(`Source is not a directory: ${source}`)
|
||||
}
|
||||
|
||||
// Create destination directory
|
||||
await fs.promises.mkdir(destination, { recursive: true })
|
||||
logger.debug('Created destination directory', { destination })
|
||||
|
||||
// Read source directory
|
||||
const entries = await fs.promises.readdir(source, { withFileTypes: true })
|
||||
|
||||
// Copy each entry
|
||||
for (const entry of entries) {
|
||||
const sourcePath = path.join(source, entry.name)
|
||||
const destPath = path.join(destination, entry.name)
|
||||
|
||||
// Use lstat to detect symlinks and prevent following them
|
||||
const entryStats = await fs.promises.lstat(sourcePath)
|
||||
|
||||
if (entryStats.isSymbolicLink()) {
|
||||
logger.warn('Skipping symlink for security', { path: sourcePath })
|
||||
continue
|
||||
}
|
||||
|
||||
if (entryStats.isDirectory()) {
|
||||
// Recursively copy subdirectory
|
||||
await copyDirectoryRecursive(sourcePath, destPath, options, depth + 1)
|
||||
} else if (entryStats.isFile()) {
|
||||
// Copy file with error handling for race conditions
|
||||
try {
|
||||
await fs.promises.copyFile(sourcePath, destPath)
|
||||
// Preserve file permissions
|
||||
await fs.promises.chmod(destPath, entryStats.mode)
|
||||
logger.debug('Copied file', { from: sourcePath, to: destPath })
|
||||
} catch (error) {
|
||||
// Handle race condition where file was deleted during copy
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
logger.warn('File disappeared during copy', { sourcePath })
|
||||
continue
|
||||
}
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
// Skip special files (pipes, sockets, devices, etc.)
|
||||
logger.debug('Skipping special file', { path: sourcePath })
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Directory copied successfully', { from: source, to: destination, depth })
|
||||
} catch (error) {
|
||||
logger.error('Failed to copy directory', { source, destination, depth, error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively delete a directory and all its contents
|
||||
* @param dirPath - Directory path to delete (must be absolute)
|
||||
* @param options - Delete options
|
||||
* @throws If deletion fails or path is invalid
|
||||
*/
|
||||
export async function deleteDirectoryRecursive(dirPath: string, options?: { allowedBasePath?: string }): Promise<void> {
|
||||
// Input validation
|
||||
if (!dirPath) {
|
||||
throw new TypeError('Directory path is required')
|
||||
}
|
||||
|
||||
if (!path.isAbsolute(dirPath)) {
|
||||
throw new Error('Directory path must be absolute')
|
||||
}
|
||||
|
||||
// Path validation - ensure operations stay within allowed boundaries
|
||||
if (options?.allowedBasePath) {
|
||||
if (!isPathInside(dirPath, options.allowedBasePath)) {
|
||||
throw new Error(`Path is outside allowed directory: ${dirPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify path exists before attempting deletion
|
||||
try {
|
||||
const stats = await fs.promises.lstat(dirPath)
|
||||
if (!stats.isDirectory()) {
|
||||
throw new Error(`Path is not a directory: ${dirPath}`)
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
logger.warn('Directory already deleted', { dirPath })
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Node.js 14.14+ has fs.rm with recursive option
|
||||
await fs.promises.rm(dirPath, { recursive: true, force: true })
|
||||
logger.info('Directory deleted successfully', { dirPath })
|
||||
} catch (error) {
|
||||
logger.error('Failed to delete directory', { dirPath, error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total size of a directory (in bytes)
|
||||
* @param dirPath - Directory path (must be absolute)
|
||||
* @param options - Size calculation options
|
||||
* @param depth - Current recursion depth (internal use)
|
||||
* @returns Total size in bytes
|
||||
* @throws If size calculation fails or path is invalid
|
||||
*/
|
||||
export async function getDirectorySize(
|
||||
dirPath: string,
|
||||
options?: { allowedBasePath?: string },
|
||||
depth = 0
|
||||
): Promise<number> {
|
||||
// Input validation
|
||||
if (!dirPath) {
|
||||
throw new TypeError('Directory path is required')
|
||||
}
|
||||
|
||||
if (!path.isAbsolute(dirPath)) {
|
||||
throw new Error('Directory path must be absolute')
|
||||
}
|
||||
|
||||
// Depth limit to prevent stack overflow
|
||||
if (depth > MAX_RECURSION_DEPTH) {
|
||||
throw new Error(`Maximum recursion depth exceeded: ${MAX_RECURSION_DEPTH}`)
|
||||
}
|
||||
|
||||
// Path validation - ensure operations stay within allowed boundaries
|
||||
if (options?.allowedBasePath) {
|
||||
if (!isPathInside(dirPath, options.allowedBasePath)) {
|
||||
throw new Error(`Path is outside allowed directory: ${dirPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
let totalSize = 0
|
||||
|
||||
try {
|
||||
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(dirPath, entry.name)
|
||||
|
||||
// Use lstat to detect symlinks and prevent following them
|
||||
const entryStats = await fs.promises.lstat(entryPath)
|
||||
|
||||
if (entryStats.isSymbolicLink()) {
|
||||
logger.debug('Skipping symlink in size calculation', { path: entryPath })
|
||||
continue
|
||||
}
|
||||
|
||||
if (entryStats.isDirectory()) {
|
||||
// Recursively get size of subdirectory
|
||||
totalSize += await getDirectorySize(entryPath, options, depth + 1)
|
||||
} else if (entryStats.isFile()) {
|
||||
// Get file size from lstat (already have it)
|
||||
totalSize += entryStats.size
|
||||
} else {
|
||||
// Skip special files
|
||||
logger.debug('Skipping special file in size calculation', { path: entryPath })
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('Calculated directory size', { dirPath, size: totalSize, depth })
|
||||
return totalSize
|
||||
} catch (error) {
|
||||
logger.error('Failed to calculate directory size', { dirPath, depth, error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
309
src/main/utils/markdownParser.ts
Normal file
309
src/main/utils/markdownParser.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import { loggerService } from '@logger'
|
||||
import type { PluginError, PluginMetadata } from '@types'
|
||||
import * as crypto from 'crypto'
|
||||
import * as fs from 'fs'
|
||||
import matter from 'gray-matter'
|
||||
import * as yaml from 'js-yaml'
|
||||
import * as path from 'path'
|
||||
|
||||
import { getDirectorySize } from './fileOperations'
|
||||
|
||||
const logger = loggerService.withContext('Utils:MarkdownParser')
|
||||
|
||||
/**
|
||||
* Parse plugin metadata from a markdown file with frontmatter
|
||||
* @param filePath Absolute path to the markdown file
|
||||
* @param sourcePath Relative source path from plugins directory
|
||||
* @param category Category name derived from parent folder
|
||||
* @param type Plugin type (agent or command)
|
||||
* @returns PluginMetadata object with parsed frontmatter and file info
|
||||
*/
|
||||
export async function parsePluginMetadata(
|
||||
filePath: string,
|
||||
sourcePath: string,
|
||||
category: string,
|
||||
type: 'agent' | 'command'
|
||||
): Promise<PluginMetadata> {
|
||||
const content = await fs.promises.readFile(filePath, 'utf8')
|
||||
const stats = await fs.promises.stat(filePath)
|
||||
|
||||
// Parse frontmatter safely with FAILSAFE_SCHEMA to prevent deserialization attacks
|
||||
const { data } = matter(content, {
|
||||
engines: {
|
||||
yaml: (s) => yaml.load(s, { schema: yaml.FAILSAFE_SCHEMA }) as object
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate content hash for integrity checking
|
||||
const contentHash = crypto.createHash('sha256').update(content).digest('hex')
|
||||
|
||||
// Extract filename
|
||||
const filename = path.basename(filePath)
|
||||
|
||||
// Parse allowed_tools - handle both array and comma-separated string
|
||||
let allowedTools: string[] | undefined
|
||||
if (data['allowed-tools'] || data.allowed_tools) {
|
||||
const toolsData = data['allowed-tools'] || data.allowed_tools
|
||||
if (Array.isArray(toolsData)) {
|
||||
allowedTools = toolsData
|
||||
} else if (typeof toolsData === 'string') {
|
||||
allowedTools = toolsData
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse tools - similar handling
|
||||
let tools: string[] | undefined
|
||||
if (data.tools) {
|
||||
if (Array.isArray(data.tools)) {
|
||||
tools = data.tools
|
||||
} else if (typeof data.tools === 'string') {
|
||||
tools = data.tools
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse tags
|
||||
let tags: string[] | undefined
|
||||
if (data.tags) {
|
||||
if (Array.isArray(data.tags)) {
|
||||
tags = data.tags
|
||||
} else if (typeof data.tags === 'string') {
|
||||
tags = data.tags
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sourcePath,
|
||||
filename,
|
||||
name: data.name || filename.replace(/\.md$/, ''),
|
||||
description: data.description,
|
||||
allowed_tools: allowedTools,
|
||||
tools,
|
||||
category,
|
||||
type,
|
||||
tags,
|
||||
version: data.version,
|
||||
author: data.author,
|
||||
size: stats.size,
|
||||
contentHash
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all directories containing SKILL.md
|
||||
*
|
||||
* @param dirPath - Directory to search in
|
||||
* @param basePath - Base path for calculating relative source paths
|
||||
* @param maxDepth - Maximum depth to search (default: 10 to prevent infinite loops)
|
||||
* @param currentDepth - Current search depth (used internally)
|
||||
* @returns Array of objects with absolute folder path and relative source path
|
||||
*/
|
||||
export async function findAllSkillDirectories(
|
||||
dirPath: string,
|
||||
basePath: string,
|
||||
maxDepth = 10,
|
||||
currentDepth = 0
|
||||
): Promise<Array<{ folderPath: string; sourcePath: string }>> {
|
||||
const results: Array<{ folderPath: string; sourcePath: string }> = []
|
||||
|
||||
// Prevent excessive recursion
|
||||
if (currentDepth > maxDepth) {
|
||||
return results
|
||||
}
|
||||
|
||||
// Check if current directory contains SKILL.md
|
||||
const skillMdPath = path.join(dirPath, 'SKILL.md')
|
||||
|
||||
try {
|
||||
await fs.promises.stat(skillMdPath)
|
||||
// Found SKILL.md in this directory
|
||||
const relativePath = path.relative(basePath, dirPath)
|
||||
results.push({
|
||||
folderPath: dirPath,
|
||||
sourcePath: relativePath
|
||||
})
|
||||
return results
|
||||
} catch {
|
||||
// SKILL.md not in current directory
|
||||
}
|
||||
|
||||
// Only search subdirectories if current directory doesn't have SKILL.md
|
||||
try {
|
||||
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const subDirPath = path.join(dirPath, entry.name)
|
||||
const subResults = await findAllSkillDirectories(subDirPath, basePath, maxDepth, currentDepth + 1)
|
||||
results.push(...subResults)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Ignore errors when reading subdirectories (e.g., permission denied)
|
||||
logger.debug('Failed to read subdirectory during skill search', {
|
||||
dirPath,
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse metadata from SKILL.md within a skill folder
|
||||
*
|
||||
* @param skillFolderPath - Absolute path to skill folder (must be absolute and contain SKILL.md)
|
||||
* @param sourcePath - Relative path from plugins base (e.g., "skills/my-skill")
|
||||
* @param category - Category name (typically "skills" for flat structure)
|
||||
* @returns PluginMetadata with folder name as filename (no extension)
|
||||
* @throws PluginError if SKILL.md not found or parsing fails
|
||||
*/
|
||||
export async function parseSkillMetadata(
|
||||
skillFolderPath: string,
|
||||
sourcePath: string,
|
||||
category: string
|
||||
): Promise<PluginMetadata> {
|
||||
// Input validation
|
||||
if (!skillFolderPath || !path.isAbsolute(skillFolderPath)) {
|
||||
throw {
|
||||
type: 'INVALID_METADATA',
|
||||
reason: 'Skill folder path must be absolute',
|
||||
path: skillFolderPath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
// Look for SKILL.md directly in this folder (no recursion)
|
||||
const skillMdPath = path.join(skillFolderPath, 'SKILL.md')
|
||||
|
||||
// Check if SKILL.md exists
|
||||
try {
|
||||
await fs.promises.stat(skillMdPath)
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
logger.error('SKILL.md not found in skill folder', { skillMdPath })
|
||||
throw {
|
||||
type: 'FILE_NOT_FOUND',
|
||||
path: skillMdPath,
|
||||
message: 'SKILL.md not found in skill folder'
|
||||
} as PluginError
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Read SKILL.md content
|
||||
let content: string
|
||||
try {
|
||||
content = await fs.promises.readFile(skillMdPath, 'utf8')
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to read SKILL.md', { skillMdPath, error })
|
||||
throw {
|
||||
type: 'READ_FAILED',
|
||||
path: skillMdPath,
|
||||
reason: error.message || 'Unknown error'
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
// Parse frontmatter safely with FAILSAFE_SCHEMA to prevent deserialization attacks
|
||||
let data: any
|
||||
try {
|
||||
const parsed = matter(content, {
|
||||
engines: {
|
||||
yaml: (s) => yaml.load(s, { schema: yaml.FAILSAFE_SCHEMA }) as object
|
||||
}
|
||||
})
|
||||
data = parsed.data
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to parse SKILL.md frontmatter', { skillMdPath, error })
|
||||
throw {
|
||||
type: 'INVALID_METADATA',
|
||||
reason: `Failed to parse frontmatter: ${error.message}`,
|
||||
path: skillMdPath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
// Calculate hash of SKILL.md only (not entire folder)
|
||||
// Note: This means changes to other files in the skill won't trigger cache invalidation
|
||||
// This is intentional - only SKILL.md metadata changes should trigger updates
|
||||
const contentHash = crypto.createHash('sha256').update(content).digest('hex')
|
||||
|
||||
// Get folder name as identifier (NO EXTENSION)
|
||||
const folderName = path.basename(skillFolderPath)
|
||||
|
||||
// Get total folder size
|
||||
let folderSize: number
|
||||
try {
|
||||
folderSize = await getDirectorySize(skillFolderPath)
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to calculate skill folder size', { skillFolderPath, error })
|
||||
// Use 0 as fallback instead of failing completely
|
||||
folderSize = 0
|
||||
}
|
||||
|
||||
// Parse tools (skills use 'tools', not 'allowed_tools')
|
||||
let tools: string[] | undefined
|
||||
if (data.tools) {
|
||||
if (Array.isArray(data.tools)) {
|
||||
// Validate all elements are strings
|
||||
tools = data.tools.filter((t) => typeof t === 'string')
|
||||
} else if (typeof data.tools === 'string') {
|
||||
tools = data.tools
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse tags
|
||||
let tags: string[] | undefined
|
||||
if (data.tags) {
|
||||
if (Array.isArray(data.tags)) {
|
||||
// Validate all elements are strings
|
||||
tags = data.tags.filter((t) => typeof t === 'string')
|
||||
} else if (typeof data.tags === 'string') {
|
||||
tags = data.tags
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate and sanitize name
|
||||
const name = typeof data.name === 'string' && data.name.trim() ? data.name.trim() : folderName
|
||||
|
||||
// Validate and sanitize description
|
||||
const description =
|
||||
typeof data.description === 'string' && data.description.trim() ? data.description.trim() : undefined
|
||||
|
||||
// Validate version and author
|
||||
const version = typeof data.version === 'string' ? data.version : undefined
|
||||
const author = typeof data.author === 'string' ? data.author : undefined
|
||||
|
||||
logger.debug('Successfully parsed skill metadata', {
|
||||
skillFolderPath,
|
||||
folderName,
|
||||
size: folderSize
|
||||
})
|
||||
|
||||
return {
|
||||
sourcePath, // e.g., "skills/my-skill"
|
||||
filename: folderName, // e.g., "my-skill" (folder name, NO .md extension)
|
||||
name,
|
||||
description,
|
||||
tools,
|
||||
category, // "skills" for flat structure
|
||||
type: 'skill',
|
||||
tags,
|
||||
version,
|
||||
author,
|
||||
size: folderSize,
|
||||
contentHash // Hash of SKILL.md content only
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import type { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
||||
import type { SpanContext } from '@opentelemetry/api'
|
||||
@@ -42,6 +43,16 @@ import type { OpenDialogOptions } from 'electron'
|
||||
import { contextBridge, ipcRenderer, shell, webUtils } from 'electron'
|
||||
import type { CreateDirectoryOptions } from 'webdav'
|
||||
|
||||
import type {
|
||||
InstalledPlugin,
|
||||
InstallPluginOptions,
|
||||
ListAvailablePluginsResult,
|
||||
PluginMetadata,
|
||||
PluginResult,
|
||||
UninstallPluginOptions,
|
||||
WritePluginContentOptions
|
||||
} from '../renderer/src/types/plugin'
|
||||
|
||||
export function tracedInvoke(channel: string, spanContext: SpanContext | undefined, ...args: any[]) {
|
||||
if (spanContext) {
|
||||
const data = { type: 'trace', context: spanContext }
|
||||
@@ -426,6 +437,15 @@ const api = {
|
||||
minimizeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowMinimize),
|
||||
pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned)
|
||||
},
|
||||
agentTools: {
|
||||
respondToPermission: (payload: {
|
||||
requestId: string
|
||||
behavior: 'allow' | 'deny'
|
||||
updatedInput?: Record<string, unknown>
|
||||
message?: string
|
||||
updatedPermissions?: PermissionUpdate[]
|
||||
}) => ipcRenderer.invoke(IpcChannel.AgentToolPermission_Response, payload)
|
||||
},
|
||||
quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text),
|
||||
// setDisableHardwareAcceleration: (isDisable: boolean) =>
|
||||
// ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable),
|
||||
@@ -548,6 +568,21 @@ const api = {
|
||||
start: (): Promise<StartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Start),
|
||||
restart: (): Promise<RestartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Restart),
|
||||
stop: (): Promise<StopApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Stop)
|
||||
},
|
||||
claudeCodePlugin: {
|
||||
listAvailable: (): Promise<PluginResult<ListAvailablePluginsResult>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_ListAvailable),
|
||||
install: (options: InstallPluginOptions): Promise<PluginResult<PluginMetadata>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_Install, options),
|
||||
uninstall: (options: UninstallPluginOptions): Promise<PluginResult<void>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_Uninstall, options),
|
||||
listInstalled: (agentId: string): Promise<PluginResult<InstalledPlugin[]>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_ListInstalled, agentId),
|
||||
invalidateCache: (): Promise<PluginResult<void>> => ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_InvalidateCache),
|
||||
readContent: (sourcePath: string): Promise<PluginResult<string>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_ReadContent, sourcePath),
|
||||
writeContent: (options: WritePluginContentOptions): Promise<PluginResult<void>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_WriteContent, options)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import NotesPage from './pages/notes/NotesPage'
|
||||
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import AssistantPresetsPage from './pages/store/assistants/presets/AssistantPresetsPage'
|
||||
import { TerminalPage } from './pages/terminal/TerminalPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
|
||||
const Router: FC = () => {
|
||||
@@ -40,6 +41,7 @@ const Router: FC = () => {
|
||||
<Route path="/apps" element={<MinAppsPage />} />
|
||||
<Route path="/code" element={<CodeToolsPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
<Route path="/terminal" element={<TerminalPage />} />
|
||||
<Route path="/launchpad" element={<LaunchpadPage />} />
|
||||
</Routes>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -6,7 +6,14 @@
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { processKnowledgeReferences } from '@renderer/services/KnowledgeService'
|
||||
import type { BaseTool, MCPTool, MCPToolResponse, NormalToolResponse } from '@renderer/types'
|
||||
import type {
|
||||
BaseTool,
|
||||
MCPCallToolResponse,
|
||||
MCPTool,
|
||||
MCPToolResponse,
|
||||
MCPToolResultContent,
|
||||
NormalToolResponse
|
||||
} from '@renderer/types'
|
||||
import type { Chunk } from '@renderer/types/chunk'
|
||||
import { ChunkType } from '@renderer/types/chunk'
|
||||
import type { ToolSet, TypedToolCall, TypedToolError, TypedToolResult } from 'ai'
|
||||
@@ -255,6 +262,7 @@ export class ToolCallChunkHandler {
|
||||
type: 'tool-result'
|
||||
} & TypedToolResult<ToolSet>
|
||||
): void {
|
||||
// TODO: 基于AI SDK为供应商内置工具做更好的展示和类型安全处理
|
||||
const { toolCallId, output, input } = chunk
|
||||
|
||||
if (!toolCallId) {
|
||||
@@ -300,12 +308,7 @@ export class ToolCallChunkHandler {
|
||||
responses: [toolResponse]
|
||||
})
|
||||
|
||||
const images: string[] = []
|
||||
for (const content of toolResponse.response?.content || []) {
|
||||
if (content.type === 'image' && content.data) {
|
||||
images.push(`data:${content.mimeType};base64,${content.data}`)
|
||||
}
|
||||
}
|
||||
const images = extractImagesFromToolOutput(toolResponse.response)
|
||||
|
||||
if (images.length) {
|
||||
this.onChunk({
|
||||
@@ -352,3 +355,41 @@ 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'
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ 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'
|
||||
|
||||
@@ -78,7 +79,7 @@ export default class ModernAiProvider {
|
||||
return this.actualProvider
|
||||
}
|
||||
|
||||
public async completions(modelId: string, params: StreamTextParams, config: ModernAiProviderConfig) {
|
||||
public async completions(modelId: string, params: StreamTextParams, providerConfig: ModernAiProviderConfig) {
|
||||
// 检查model是否存在
|
||||
if (!this.model) {
|
||||
throw new Error('Model is required for completions. Please use constructor with model parameter.')
|
||||
@@ -86,7 +87,10 @@ 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)
|
||||
|
||||
@@ -97,12 +101,13 @@ export default class ModernAiProvider {
|
||||
|
||||
// 提前构建中间件
|
||||
const middlewares = buildAiSdkMiddlewares({
|
||||
...config,
|
||||
provider: this.actualProvider
|
||||
...providerConfig,
|
||||
provider: this.actualProvider,
|
||||
assistant: providerConfig.assistant
|
||||
})
|
||||
logger.debug('Built middlewares in completions', {
|
||||
middlewareCount: middlewares.length,
|
||||
isImageGeneration: config.isImageGenerationEndpoint
|
||||
isImageGeneration: providerConfig.isImageGenerationEndpoint
|
||||
})
|
||||
if (!this.localProvider) {
|
||||
throw new Error('Local provider not created')
|
||||
@@ -110,7 +115,7 @@ export default class ModernAiProvider {
|
||||
|
||||
// 根据endpoint类型创建对应的模型
|
||||
let model: AiSdkModel | undefined
|
||||
if (config.isImageGenerationEndpoint) {
|
||||
if (providerConfig.isImageGenerationEndpoint) {
|
||||
model = this.localProvider.imageModel(modelId)
|
||||
} else {
|
||||
model = this.localProvider.languageModel(modelId)
|
||||
@@ -126,15 +131,15 @@ export default class ModernAiProvider {
|
||||
params.messages = [...claudeCodeSystemMessage, ...(params.messages || [])]
|
||||
}
|
||||
|
||||
if (config.topicId && (await preferenceService.get('app.developer_mode.enabled'))) {
|
||||
if (providerConfig.topicId && (await preferenceService.get('app.developer_mode.enabled'))) {
|
||||
// TypeScript类型窄化:确保topicId是string类型
|
||||
const traceConfig = {
|
||||
...config,
|
||||
topicId: config.topicId
|
||||
...providerConfig,
|
||||
topicId: providerConfig.topicId
|
||||
}
|
||||
return await this._completionsForTrace(model, params, traceConfig)
|
||||
} else {
|
||||
return await this._completionsOrImageGeneration(model, params, config)
|
||||
return await this._completionsOrImageGeneration(model, params, providerConfig)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Provider } from '@renderer/types'
|
||||
import { isOpenAIProvider } from '@renderer/utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { AihubmixAPIClient } from '../aihubmix/AihubmixAPIClient'
|
||||
@@ -202,36 +201,4 @@ 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import type { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
import { loggerService } from '@logger'
|
||||
import { type MCPTool, type Message, type Model, type Provider } from '@renderer/types'
|
||||
import { isSupportedThinkingTokenQwenModel } from '@renderer/config/models'
|
||||
import { isSupportEnableThinkingProvider } from '@renderer/config/providers'
|
||||
import type { MCPTool } from '@renderer/types'
|
||||
import { type Assistant, type Message, type Model, type Provider } from '@renderer/types'
|
||||
import type { Chunk } from '@renderer/types/chunk'
|
||||
import type { LanguageModelMiddleware } from 'ai'
|
||||
import { extractReasoningMiddleware, simulateStreamingMiddleware } from 'ai'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import { isOpenRouterGeminiGenerateImageModel } from '../utils/image'
|
||||
import { noThinkMiddleware } from './noThinkMiddleware'
|
||||
import { openrouterGenerateImageMiddleware } from './openrouterGenerateImageMiddleware'
|
||||
import { qwenThinkingMiddleware } from './qwenThinkingMiddleware'
|
||||
import { toolChoiceMiddleware } from './toolChoiceMiddleware'
|
||||
|
||||
const logger = loggerService.withContext('AiSdkMiddlewareBuilder')
|
||||
@@ -20,6 +25,7 @@ export interface AiSdkMiddlewareConfig {
|
||||
onChunk?: (chunk: Chunk) => void
|
||||
model?: Model
|
||||
provider?: Provider
|
||||
assistant?: Assistant
|
||||
enableReasoning: boolean
|
||||
// 是否开启提示词工具调用
|
||||
isPromptToolUse: boolean
|
||||
@@ -128,7 +134,7 @@ export function buildAiSdkMiddlewares(config: AiSdkMiddlewareConfig): LanguageMo
|
||||
const builder = new AiSdkMiddlewareBuilder()
|
||||
|
||||
// 0. 知识库强制调用中间件(必须在最前面,确保第一轮强制调用知识库)
|
||||
if (config.knowledgeRecognition === 'off') {
|
||||
if (!isEmpty(config.assistant?.knowledge_bases?.map((base) => base.id)) && config.knowledgeRecognition !== 'on') {
|
||||
builder.add({
|
||||
name: 'force-knowledge-first',
|
||||
middleware: toolChoiceMiddleware('builtin_knowledge_search')
|
||||
@@ -219,6 +225,21 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config:
|
||||
function addModelSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config: AiSdkMiddlewareConfig): void {
|
||||
if (!config.model || !config.provider) return
|
||||
|
||||
// Qwen models on providers that don't support enable_thinking parameter (like Ollama, LM Studio, NVIDIA)
|
||||
// Use /think or /no_think suffix to control thinking mode
|
||||
if (
|
||||
config.provider &&
|
||||
isSupportedThinkingTokenQwenModel(config.model) &&
|
||||
!isSupportEnableThinkingProvider(config.provider)
|
||||
) {
|
||||
const enableThinking = config.assistant?.settings?.reasoning_effort !== undefined
|
||||
builder.add({
|
||||
name: 'qwen-thinking-control',
|
||||
middleware: qwenThinkingMiddleware(enableThinking)
|
||||
})
|
||||
logger.debug(`Added Qwen thinking middleware with thinking ${enableThinking ? 'enabled' : 'disabled'}`)
|
||||
}
|
||||
|
||||
// 可以根据模型ID或特性添加特定中间件
|
||||
// 例如:图像生成模型、多模态模型等
|
||||
if (isOpenRouterGeminiGenerateImageModel(config.model, config.provider)) {
|
||||
|
||||
39
src/renderer/src/aiCore/middleware/qwenThinkingMiddleware.ts
Normal file
39
src/renderer/src/aiCore/middleware/qwenThinkingMiddleware.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { LanguageModelMiddleware } from 'ai'
|
||||
|
||||
/**
|
||||
* Qwen Thinking Middleware
|
||||
* Controls thinking mode for Qwen models on providers that don't support enable_thinking parameter (like Ollama)
|
||||
* Appends '/think' or '/no_think' suffix to user messages based on reasoning_effort setting
|
||||
* @param enableThinking - Whether thinking mode is enabled (based on reasoning_effort !== undefined)
|
||||
* @returns LanguageModelMiddleware
|
||||
*/
|
||||
export function qwenThinkingMiddleware(enableThinking: boolean): LanguageModelMiddleware {
|
||||
const suffix = enableThinking ? ' /think' : ' /no_think'
|
||||
|
||||
return {
|
||||
middlewareVersion: 'v2',
|
||||
|
||||
transformParams: async ({ params }) => {
|
||||
const transformedParams = { ...params }
|
||||
// Process messages in prompt
|
||||
if (transformedParams.prompt && Array.isArray(transformedParams.prompt)) {
|
||||
transformedParams.prompt = transformedParams.prompt.map((message) => {
|
||||
// Only process user messages
|
||||
if (message.role === 'user') {
|
||||
// Process content array
|
||||
if (Array.isArray(message.content)) {
|
||||
for (const part of message.content) {
|
||||
if (part.type === 'text' && !part.text.endsWith('/think') && !part.text.endsWith('/no_think')) {
|
||||
part.text += suffix
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return message
|
||||
})
|
||||
}
|
||||
|
||||
return transformedParams
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AiPlugin } from '@cherrystudio/ai-core'
|
||||
import { createPromptToolUsePlugin, googleToolsPlugin, webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
import { createPromptToolUsePlugin, webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
import { preferenceService } from '@data/PreferenceService'
|
||||
import { loggerService } from '@logger'
|
||||
import type { Assistant } from '@renderer/types'
|
||||
@@ -68,9 +68,9 @@ export async function buildPlugins(
|
||||
)
|
||||
}
|
||||
|
||||
if (middlewareConfig.enableUrlContext) {
|
||||
plugins.push(googleToolsPlugin({ urlContext: true }))
|
||||
}
|
||||
// if (middlewareConfig.enableUrlContext && middlewareConfig.) {
|
||||
// plugins.push(googleToolsPlugin({ urlContext: true }))
|
||||
// }
|
||||
|
||||
logger.debug(
|
||||
'Final plugin list:',
|
||||
|
||||
@@ -114,7 +114,7 @@ export async function handleGeminiFileUpload(file: FileMetadata, model: Model):
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理OpenAI大文件上传
|
||||
* 处理OpenAI兼容大文件上传
|
||||
*/
|
||||
export async function handleOpenAILargeFileUpload(
|
||||
file: FileMetadata,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* 构建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'
|
||||
@@ -97,10 +99,6 @@ 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,
|
||||
@@ -143,12 +141,34 @@ export async function buildStreamTextParams(
|
||||
}
|
||||
}
|
||||
|
||||
// google-vertex
|
||||
if (enableUrlContext && aiSdkProviderId === 'google-vertex') {
|
||||
if (enableUrlContext) {
|
||||
if (!tools) {
|
||||
tools = {}
|
||||
}
|
||||
tools.url_context = vertex.tools.urlContext({}) as ProviderDefinedTool
|
||||
const blockedDomains = mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
|
||||
switch (aiSdkProviderId) {
|
||||
case 'google-vertex':
|
||||
tools.url_context = vertex.tools.urlContext({}) as ProviderDefinedTool
|
||||
break
|
||||
case 'google':
|
||||
tools.url_context = google.tools.urlContext({}) as ProviderDefinedTool
|
||||
break
|
||||
case 'anthropic':
|
||||
case 'google-vertex-anthropic':
|
||||
tools.web_fetch = (
|
||||
aiSdkProviderId === 'anthropic'
|
||||
? anthropic.tools.webFetch_20250910({
|
||||
maxUses: webSearchConfig.maxResults,
|
||||
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
|
||||
})
|
||||
: vertexAnthropic.tools.webFetch_20250910({
|
||||
maxUses: webSearchConfig.maxResults,
|
||||
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
|
||||
})
|
||||
) as ProviderDefinedTool
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 构建基础参数
|
||||
|
||||
@@ -32,7 +32,8 @@ const AIHUBMIX_RULES: RuleSet = {
|
||||
match: (model) =>
|
||||
(startsWith('gemini')(model) || startsWith('imagen')(model)) &&
|
||||
!model.id.endsWith('-nothink') &&
|
||||
!model.id.endsWith('-search'),
|
||||
!model.id.endsWith('-search') &&
|
||||
!model.id.includes('embedding'),
|
||||
provider: (provider: Provider) => {
|
||||
return extraProviderConfig({
|
||||
...provider,
|
||||
|
||||
@@ -7,24 +7,27 @@ import {
|
||||
} from '@cherrystudio/ai-core/provider'
|
||||
import { cacheService } from '@data/CacheService'
|
||||
import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
|
||||
import { isNewApiProvider } from '@renderer/config/providers'
|
||||
import {
|
||||
isAnthropicProvider,
|
||||
isAzureOpenAIProvider,
|
||||
isGeminiProvider,
|
||||
isNewApiProvider
|
||||
} from '@renderer/config/providers'
|
||||
import {
|
||||
getAwsBedrockAccessKeyId,
|
||||
getAwsBedrockRegion,
|
||||
getAwsBedrockSecretAccessKey
|
||||
} from '@renderer/hooks/useAwsBedrock'
|
||||
import { createVertexProvider, isVertexAIConfigured } from '@renderer/hooks/useVertexAI'
|
||||
import { createVertexProvider, isVertexAIConfigured, isVertexProvider } from '@renderer/hooks/useVertexAI'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import store from '@renderer/store'
|
||||
import { isSystemProvider, type Model, type Provider } from '@renderer/types'
|
||||
import { formatApiHost } from '@renderer/utils/api'
|
||||
import { cloneDeep, trim } from 'lodash'
|
||||
import { isSystemProvider, type Model, type Provider, SystemProviderIds } from '@renderer/types'
|
||||
import { formatApiHost, formatAzureOpenAIApiHost, formatVertexApiHost, routeToEndpoint } from '@renderer/utils/api'
|
||||
import { cloneDeep } 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
|
||||
@@ -56,13 +59,6 @@ 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)
|
||||
}
|
||||
@@ -79,43 +75,30 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化provider的API Host
|
||||
* 主要用来对齐AISdk的BaseURL格式
|
||||
* @param provider
|
||||
* @returns
|
||||
*/
|
||||
function formatAnthropicApiHost(host: string): string {
|
||||
const trimmedHost = host?.trim()
|
||||
|
||||
if (!trimmedHost) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (trimmedHost.endsWith('/')) {
|
||||
return trimmedHost
|
||||
}
|
||||
|
||||
if (trimmedHost.endsWith('/v1')) {
|
||||
return `${trimmedHost}/`
|
||||
}
|
||||
|
||||
return formatApiHost(trimmedHost)
|
||||
}
|
||||
|
||||
function formatProviderApiHost(provider: Provider): Provider {
|
||||
const formatted = { ...provider }
|
||||
if (formatted.anthropicApiHost) {
|
||||
formatted.anthropicApiHost = formatAnthropicApiHost(formatted.anthropicApiHost)
|
||||
formatted.anthropicApiHost = formatApiHost(formatted.anthropicApiHost)
|
||||
}
|
||||
|
||||
if (formatted.type === 'anthropic') {
|
||||
if (isAnthropicProvider(provider)) {
|
||||
const baseHost = formatted.anthropicApiHost || formatted.apiHost
|
||||
formatted.apiHost = formatAnthropicApiHost(baseHost)
|
||||
formatted.apiHost = formatApiHost(baseHost)
|
||||
if (!formatted.anthropicApiHost) {
|
||||
formatted.anthropicApiHost = formatted.apiHost
|
||||
}
|
||||
} 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 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 {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost)
|
||||
}
|
||||
@@ -149,15 +132,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: trim(actualProvider.apiHost),
|
||||
baseURL: baseURL,
|
||||
apiKey: getRotatedApiKey(actualProvider)
|
||||
}
|
||||
|
||||
const isCopilotProvider = actualProvider.id === 'copilot'
|
||||
const isCopilotProvider = actualProvider.id === SystemProviderIds.copilot
|
||||
if (isCopilotProvider) {
|
||||
const storedHeaders = store.getState().copilot.defaultHeaders ?? {}
|
||||
const options = ProviderConfigFactory.fromProvider('github-copilot-openai-compatible', baseConfig, {
|
||||
@@ -178,6 +161,7 @@ export function providerToAiSdkConfig(
|
||||
|
||||
// 处理OpenAI模式
|
||||
const extraOptions: any = {}
|
||||
extraOptions.endpoint = endpoint
|
||||
if (actualProvider.type === 'openai-response' && !isOpenAIChatCompletionOnlyModel(model)) {
|
||||
extraOptions.mode = 'responses'
|
||||
} else if (aiSdkProviderId === 'openai') {
|
||||
@@ -199,13 +183,11 @@ export function providerToAiSdkConfig(
|
||||
}
|
||||
// azure
|
||||
if (aiSdkProviderId === 'azure' || actualProvider.type === 'azure-openai') {
|
||||
extraOptions.apiVersion = actualProvider.apiVersion
|
||||
baseConfig.baseURL += '/openai'
|
||||
// extraOptions.apiVersion = actualProvider.apiVersion 默认使用v1,不使用azure endpoint
|
||||
if (actualProvider.apiVersion === 'preview') {
|
||||
extraOptions.mode = 'responses'
|
||||
} else {
|
||||
extraOptions.mode = 'chat'
|
||||
extraOptions.useDeploymentBasedUrls = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,22 +209,7 @@ export function providerToAiSdkConfig(
|
||||
...googleCredentials,
|
||||
privateKey: formatPrivateKey(googleCredentials.privateKey)
|
||||
}
|
||||
// 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`
|
||||
}
|
||||
baseConfig.baseURL += aiSdkProviderId === 'google-vertex' ? '/publishers/google' : '/publishers/anthropic/models'
|
||||
}
|
||||
|
||||
if (hasProviderConfig(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') {
|
||||
|
||||
@@ -5,6 +5,16 @@ 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 {
|
||||
|
||||
@@ -3,6 +3,16 @@ 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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@cherrystudio/ui'
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -20,18 +20,12 @@ const ExpandableText = ({
|
||||
setIsExpanded((prev) => !prev)
|
||||
}, [])
|
||||
|
||||
const button = useMemo(() => {
|
||||
return (
|
||||
<Button variant="ghost" onClick={toggleExpand} className="self-end">
|
||||
{isExpanded ? t('common.collapse') : t('common.expand')}
|
||||
</Button>
|
||||
)
|
||||
}, [isExpanded, t, toggleExpand])
|
||||
|
||||
return (
|
||||
<Container ref={ref} style={style} $expanded={isExpanded}>
|
||||
<TextContainer $expanded={isExpanded}>{text}</TextContainer>
|
||||
{button}
|
||||
<Button variant="ghost" onClick={toggleExpand} className="self-end">
|
||||
{isExpanded ? t('common.collapse') : t('common.expand')}
|
||||
</Button>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -48,4 +42,4 @@ const TextContainer = styled.div<{ $expanded?: boolean }>`
|
||||
line-height: ${(props) => (props.$expanded ? 'unset' : '30px')};
|
||||
`
|
||||
|
||||
export default memo(ExpandableText)
|
||||
export default ExpandableText
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DeleteOutlined, ExclamationCircleOutlined, ReloadOutlined } from '@ant-
|
||||
import { Button, Flex, Tooltip } from '@cherrystudio/ui'
|
||||
import { restoreFromLocal } from '@renderer/services/BackupService'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import { Modal, Table } from 'antd'
|
||||
import { Modal, Space, Table } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -221,6 +221,26 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe
|
||||
}
|
||||
}
|
||||
|
||||
const footerContent = (
|
||||
<Space align="center">
|
||||
<Button key="refresh" onClick={fetchBackupFiles} disabled={loading}>
|
||||
<ReloadOutlined />
|
||||
{t('settings.data.local.backup.manager.refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
key="delete"
|
||||
variant="destructive"
|
||||
onClick={handleDeleteSelected}
|
||||
disabled={selectedRowKeys.length === 0 || deleting}>
|
||||
<DeleteOutlined />
|
||||
{t('settings.data.local.backup.manager.delete.selected')} ({selectedRowKeys.length})
|
||||
</Button>
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('settings.data.local.backup.manager.title')}
|
||||
@@ -229,24 +249,7 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe
|
||||
width={800}
|
||||
centered
|
||||
transitionName="animation-move-down"
|
||||
classNames={{ footer: 'flex justify-end gap-1' }}
|
||||
footer={[
|
||||
<Button key="refresh" onClick={fetchBackupFiles} disabled={loading}>
|
||||
<ReloadOutlined />
|
||||
{t('settings.data.local.backup.manager.refresh')}
|
||||
</Button>,
|
||||
<Button
|
||||
key="delete"
|
||||
variant="destructive"
|
||||
onClick={handleDeleteSelected}
|
||||
disabled={selectedRowKeys.length === 0 || deleting}>
|
||||
<DeleteOutlined />
|
||||
{t('settings.data.local.backup.manager.delete.selected')} ({selectedRowKeys.length})
|
||||
</Button>,
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
]}>
|
||||
footer={footerContent}>
|
||||
<Table
|
||||
rowKey="fileName"
|
||||
columns={columns}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -12,8 +12,8 @@ vi.mock('react-i18next', () => ({
|
||||
|
||||
// Mock ImageToolButton
|
||||
vi.mock('../ImageToolButton', () => ({
|
||||
default: vi.fn(({ tooltip, onPress, icon }) => (
|
||||
<button type="button" onClick={onPress} role="button" aria-label={tooltip}>
|
||||
default: vi.fn(({ tooltip, onClick, icon }) => (
|
||||
<button type="button" onClick={onClick} role="button" aria-label={tooltip}>
|
||||
{icon}
|
||||
</button>
|
||||
))
|
||||
|
||||
@@ -4,8 +4,8 @@ exports[`ImageToolButton > should match snapshot 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Test tooltip"
|
||||
class="rounded-full"
|
||||
data-testid="button"
|
||||
radius="full"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { makeSvgSizeAdaptive } from '@renderer/utils'
|
||||
import { makeSvgSizeAdaptive } from '@renderer/utils/image'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button, Tooltip } from '@cherrystudio/ui'
|
||||
import { restoreFromS3 } from '@renderer/services/BackupService'
|
||||
import type { S3Config } from '@renderer/types'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import { Modal, Space, Table } from 'antd'
|
||||
import { Modal, Table } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -18,6 +18,7 @@ interface BaseSelectorProps<V = string | number> {
|
||||
options: SelectorOption<V>[]
|
||||
placeholder?: string
|
||||
placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom'
|
||||
style?: React.CSSProperties
|
||||
/** 字体大小 */
|
||||
size?: number
|
||||
/** 是否禁用 */
|
||||
@@ -45,6 +46,7 @@ const Selector = <V extends string | number>({
|
||||
placement = 'bottomRight',
|
||||
size = 13,
|
||||
placeholder,
|
||||
style,
|
||||
disabled = false,
|
||||
multiple = false
|
||||
}: SelectorProps<V>) => {
|
||||
@@ -137,7 +139,7 @@ const Selector = <V extends string | number>({
|
||||
placement={placement}
|
||||
open={open && !disabled}
|
||||
onOpenChange={handleOpenChange}>
|
||||
<Label $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
|
||||
<Label style={style} $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
|
||||
{label}
|
||||
<LabelIcon size={size + 3} />
|
||||
</Label>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { classNames } from '@renderer/utils'
|
||||
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import type { LRUCache } from 'lru-cache'
|
||||
import {
|
||||
Code,
|
||||
FileSearch,
|
||||
Folder,
|
||||
Hammer,
|
||||
@@ -106,6 +107,8 @@ const getTabIcon = (
|
||||
case 'settings':
|
||||
return <Settings size={14} />
|
||||
case 'code':
|
||||
return <Code size={14} />
|
||||
case 'terminal':
|
||||
return <Terminal size={14} />
|
||||
default:
|
||||
return null
|
||||
|
||||
@@ -9,6 +9,15 @@ vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: (k: string) => k })
|
||||
}))
|
||||
|
||||
// mock @cherrystudio/ui Button component
|
||||
vi.mock('@cherrystudio/ui', () => ({
|
||||
Button: ({ children, onPress, ...props }: any) => (
|
||||
<button type="button" onClick={onPress} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}))
|
||||
|
||||
describe('ExpandableText', () => {
|
||||
const TEXT = 'This is a long text for testing.'
|
||||
|
||||
|
||||
@@ -23,6 +23,16 @@ 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 }>> = ({
|
||||
|
||||
@@ -65,7 +65,7 @@ const NavbarContainer = styled.div<{ $isFullScreen: boolean }>`
|
||||
min-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: ${isMac ? 'env(titlebar-area-height)' : 'var(--navbar-height)'};
|
||||
min-height: ${({ $isFullScreen }) => (!$isFullScreen && isMac ? 'env(titlebar-area-height)' : 'var(--navbar-height)')};
|
||||
max-height: var(--navbar-height);
|
||||
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1 + 2px)' : 0};
|
||||
padding-left: ${({ $isFullScreen }) =>
|
||||
|
||||
@@ -18,6 +18,15 @@ 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)
|
||||
|
||||
@@ -2,6 +2,16 @@ 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: () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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'> = {
|
||||
@@ -19,3 +20,47 @@ 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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1741,6 +1741,7 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
||||
id: 'DeepSeek-R1',
|
||||
provider: 'cephalon',
|
||||
name: 'DeepSeek-R1满血版',
|
||||
capabilities: [{ type: 'reasoning' }],
|
||||
group: 'DeepSeek'
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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'
|
||||
@@ -65,12 +67,16 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
|
||||
const modelId = getLowerBaseModelName(model.id, '/')
|
||||
|
||||
// 不管哪个供应商都判断了
|
||||
if (isAnthropicModel(model)) {
|
||||
// bedrock和vertex不支持
|
||||
if (
|
||||
isAnthropicModel(model) &&
|
||||
(provider.id === SystemProviderIds['aws-bedrock'] || provider.id === SystemProviderIds.vertexai)
|
||||
) {
|
||||
return CLAUDE_SUPPORTED_WEBSEARCH_REGEX.test(modelId)
|
||||
}
|
||||
|
||||
if (provider.type === 'openai-response') {
|
||||
// TODO: 当其他供应商采用Response端点时,这个地方逻辑需要改进
|
||||
if (isOpenAIProvider(provider)) {
|
||||
if (isOpenAIWebSearchModel(model)) {
|
||||
return true
|
||||
}
|
||||
@@ -78,11 +84,11 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
if (provider.id === 'perplexity') {
|
||||
if (provider.id === SystemProviderIds.perplexity) {
|
||||
return PERPLEXITY_SEARCH_MODELS.includes(modelId)
|
||||
}
|
||||
|
||||
if (provider.id === 'aihubmix') {
|
||||
if (provider.id === SystemProviderIds.aihubmix) {
|
||||
// modelId 不以-search结尾
|
||||
if (!modelId.endsWith('-search') && GEMINI_SEARCH_REGEX.test(modelId)) {
|
||||
return true
|
||||
@@ -95,13 +101,13 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
if (provider?.type === 'openai') {
|
||||
if (isOpenAICompatibleProvider(provider) || isNewApiProvider(provider)) {
|
||||
if (GEMINI_SEARCH_REGEX.test(modelId) || isOpenAIWebSearchModel(model)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (provider.id === 'gemini' || provider?.type === 'gemini' || provider.type === 'vertexai') {
|
||||
if (isGeminiProvider(provider) || provider.id === SystemProviderIds.vertexai) {
|
||||
return GEMINI_SEARCH_REGEX.test(modelId)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ export function getPreprocessProviderLogo(providerId: PreprocessProviderId) {
|
||||
return MistralLogo
|
||||
case 'mineru':
|
||||
return MinerULogo
|
||||
case 'open-mineru':
|
||||
return MinerULogo
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
@@ -36,5 +38,11 @@ export const PREPROCESS_PROVIDER_CONFIG: Record<PreprocessProviderId, Preprocess
|
||||
official: 'https://mineru.net/',
|
||||
apiKey: 'https://mineru.net/apiManage'
|
||||
}
|
||||
},
|
||||
'open-mineru': {
|
||||
websites: {
|
||||
official: 'https://github.com/opendatalab/MinerU/',
|
||||
apiKey: 'https://github.com/opendatalab/MinerU/'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,14 @@ 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, Provider, ProviderType, SystemProvider, SystemProviderId } from '@renderer/types'
|
||||
import type {
|
||||
AtLeast,
|
||||
AzureOpenAIProvider,
|
||||
Provider,
|
||||
ProviderType,
|
||||
SystemProvider,
|
||||
SystemProviderId
|
||||
} from '@renderer/types'
|
||||
import { isSystemProvider, OpenAIServiceTiers } from '@renderer/types'
|
||||
|
||||
import { TOKENFLUX_HOST } from './constant'
|
||||
@@ -348,7 +355,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
name: 'VertexAI',
|
||||
type: 'vertexai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://aiplatform.googleapis.com',
|
||||
apiHost: '',
|
||||
models: SYSTEM_MODELS.vertexai,
|
||||
isSystem: true,
|
||||
enabled: false,
|
||||
@@ -412,7 +419,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
|
||||
anthropicApiHost: 'https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy',
|
||||
anthropicApiHost: 'https://dashscope.aliyuncs.com/apps/anthropic',
|
||||
models: SYSTEM_MODELS.dashscope,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
@@ -1288,7 +1295,7 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
||||
},
|
||||
vertexai: {
|
||||
api: {
|
||||
url: 'https://console.cloud.google.com/apis/api/aiplatform.googleapis.com/overview'
|
||||
url: ''
|
||||
},
|
||||
websites: {
|
||||
official: 'https://cloud.google.com/vertex-ai',
|
||||
@@ -1368,7 +1375,8 @@ const NOT_SUPPORT_ARRAY_CONTENT_PROVIDERS = [
|
||||
'baichuan',
|
||||
'minimax',
|
||||
'xirang',
|
||||
'poe'
|
||||
'poe',
|
||||
'cephalon'
|
||||
] as const satisfies SystemProviderId[]
|
||||
|
||||
/**
|
||||
@@ -1433,10 +1441,15 @@ export const isSupportServiceTierProvider = (provider: Provider) => {
|
||||
)
|
||||
}
|
||||
|
||||
const SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES = ['gemini', 'vertexai'] as const satisfies ProviderType[]
|
||||
const SUPPORT_URL_CONTEXT_PROVIDER_TYPES = [
|
||||
'gemini',
|
||||
'vertexai',
|
||||
'anthropic',
|
||||
'new-api'
|
||||
] as const satisfies ProviderType[]
|
||||
|
||||
export const isSupportUrlContextProvider = (provider: Provider) => {
|
||||
return SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type)
|
||||
return SUPPORT_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type)
|
||||
}
|
||||
|
||||
const SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS = ['gemini', 'vertexai'] as const satisfies SystemProviderId[]
|
||||
@@ -1449,3 +1462,37 @@ 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
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { PermissionMode } from '@renderer/types'
|
||||
|
||||
export type PermissionModeCard = {
|
||||
mode: PermissionMode
|
||||
titleKey: string
|
||||
titleFallback: string
|
||||
descriptionKey: string
|
||||
descriptionFallback: string
|
||||
behaviorKey: string
|
||||
behaviorFallback: string
|
||||
caution?: boolean
|
||||
unsupported?: boolean
|
||||
}
|
||||
|
||||
export const permissionModeCards: PermissionModeCard[] = [
|
||||
{
|
||||
mode: 'default',
|
||||
titleKey: 'agent.settings.tooling.permissionMode.default.title',
|
||||
titleFallback: 'Default (ask before continuing)',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.default.description',
|
||||
descriptionFallback: 'Read-only tools are pre-approved; everything else still needs permission.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.default.behavior',
|
||||
behaviorFallback: 'Read-only tools are pre-approved automatically.'
|
||||
},
|
||||
{
|
||||
mode: 'plan',
|
||||
titleKey: 'agent.settings.tooling.permissionMode.plan.title',
|
||||
titleFallback: 'Planning mode',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.plan.description',
|
||||
descriptionFallback: 'Shares the default read-only tool set but presents a plan before execution.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.plan.behavior',
|
||||
behaviorFallback: 'Read-only defaults are pre-approved while execution remains disabled.'
|
||||
},
|
||||
{
|
||||
mode: 'acceptEdits',
|
||||
titleKey: 'agent.settings.tooling.permissionMode.acceptEdits.title',
|
||||
titleFallback: 'Auto-accept file edits',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.acceptEdits.description',
|
||||
descriptionFallback: 'File edits and filesystem operations are automatically approved.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.acceptEdits.behavior',
|
||||
behaviorFallback: 'Pre-approves trusted filesystem tools so edits run immediately.'
|
||||
},
|
||||
{
|
||||
mode: 'bypassPermissions',
|
||||
titleKey: 'agent.settings.tooling.permissionMode.bypassPermissions.title',
|
||||
titleFallback: 'Bypass permission checks',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.bypassPermissions.description',
|
||||
descriptionFallback: 'All permission prompts are skipped — use with caution.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.bypassPermissions.behavior',
|
||||
behaviorFallback: 'Every tool is pre-approved automatically.',
|
||||
caution: true
|
||||
}
|
||||
]
|
||||
10
src/renderer/src/env.d.ts
vendored
10
src/renderer/src/env.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import type { ToastUtilities } from '@cherrystudio/ui'
|
||||
import type { HookAPI } from 'antd/es/modal/useModal'
|
||||
import type { NavigateFunction } from 'react-router-dom'
|
||||
@@ -19,5 +20,14 @@ declare global {
|
||||
store: any
|
||||
navigate: NavigateFunction
|
||||
toast: ToastUtilities
|
||||
agentTools: {
|
||||
respondToPermission: (payload: {
|
||||
requestId: string
|
||||
behavior: 'allow' | 'deny'
|
||||
updatedInput?: Record<string, unknown>
|
||||
message?: string
|
||||
updatedPermissions?: PermissionUpdate[]
|
||||
}) => Promise<{ success: boolean }>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
src/renderer/src/hooks/agents/useCreateDefaultSession.ts
Normal file
49
src/renderer/src/hooks/agents/useCreateDefaultSession.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveSessionIdAction, setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import type { CreateSessionForm } from '@renderer/types'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Returns a stable callback that creates a default agent session and updates UI state.
|
||||
*/
|
||||
export const useCreateDefaultSession = (agentId: string | null) => {
|
||||
const { agent } = useAgent(agentId)
|
||||
const { createSession } = useSessions(agentId)
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
const [creatingSession, setCreatingSession] = useState(false)
|
||||
|
||||
const createDefaultSession = useCallback(async () => {
|
||||
if (!agentId || !agent || creatingSession) {
|
||||
return null
|
||||
}
|
||||
|
||||
setCreatingSession(true)
|
||||
try {
|
||||
const session = {
|
||||
...agent,
|
||||
id: undefined,
|
||||
name: t('common.unnamed')
|
||||
} satisfies CreateSessionForm
|
||||
|
||||
const created = await createSession(session)
|
||||
|
||||
if (created) {
|
||||
dispatch(setActiveSessionIdAction({ agentId, sessionId: created.id }))
|
||||
dispatch(setActiveTopicOrSessionAction('session'))
|
||||
}
|
||||
|
||||
return created
|
||||
} finally {
|
||||
setCreatingSession(false)
|
||||
}
|
||||
}, [agentId, agent, createSession, creatingSession, dispatch, t])
|
||||
|
||||
return {
|
||||
createDefaultSession,
|
||||
creatingSession
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,18 @@ import { useAppDispatch } from '@renderer/store'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { handleSaveData } from '@renderer/store'
|
||||
import { selectMemoryConfig } from '@renderer/store/memory'
|
||||
import {
|
||||
type ToolPermissionRequestPayload,
|
||||
type ToolPermissionResultPayload,
|
||||
toolPermissionsActions
|
||||
} from '@renderer/store/toolPermissions'
|
||||
import { delay, runAsyncFunction } from '@renderer/utils'
|
||||
import { checkDataLimit } from '@renderer/utils'
|
||||
import { defaultLanguage } from '@shared/config/constant'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useDefaultModel } from './useAssistant'
|
||||
import useFullScreenNotice from './useFullScreenNotice'
|
||||
@@ -27,6 +33,7 @@ import { useNavbarPosition } from './useNavbar'
|
||||
const logger = loggerService.withContext('useAppInit')
|
||||
|
||||
export function useAppInit() {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const [language] = usePreference('app.language')
|
||||
const [windowStyle] = usePreference('ui.window_style')
|
||||
@@ -148,6 +155,64 @@ export function useAppInit() {
|
||||
}
|
||||
}, [customCss])
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.electron?.ipcRenderer) return
|
||||
|
||||
const requestListener = (_event: Electron.IpcRendererEvent, payload: ToolPermissionRequestPayload) => {
|
||||
logger.debug('Renderer received tool permission request', {
|
||||
requestId: payload.requestId,
|
||||
toolName: payload.toolName,
|
||||
expiresAt: payload.expiresAt,
|
||||
suggestionCount: payload.suggestions.length
|
||||
})
|
||||
dispatch(toolPermissionsActions.requestReceived(payload))
|
||||
}
|
||||
|
||||
const resultListener = (_event: Electron.IpcRendererEvent, payload: ToolPermissionResultPayload) => {
|
||||
logger.debug('Renderer received tool permission result', {
|
||||
requestId: payload.requestId,
|
||||
behavior: payload.behavior,
|
||||
reason: payload.reason
|
||||
})
|
||||
dispatch(toolPermissionsActions.requestResolved(payload))
|
||||
|
||||
if (payload.behavior === 'deny') {
|
||||
const message =
|
||||
payload.reason === 'timeout'
|
||||
? (payload.message ?? t('agent.toolPermission.toast.timeout'))
|
||||
: (payload.message ?? t('agent.toolPermission.toast.denied'))
|
||||
|
||||
if (payload.reason === 'no-window') {
|
||||
logger.debug('Displaying deny toast for tool permission', {
|
||||
requestId: payload.requestId,
|
||||
behavior: payload.behavior,
|
||||
reason: payload.reason
|
||||
})
|
||||
window.toast?.error?.(message)
|
||||
} else if (payload.reason === 'timeout') {
|
||||
logger.debug('Displaying timeout toast for tool permission', {
|
||||
requestId: payload.requestId
|
||||
})
|
||||
window.toast?.warning?.(message)
|
||||
} else {
|
||||
logger.debug('Displaying info toast for tool permission deny', {
|
||||
requestId: payload.requestId,
|
||||
reason: payload.reason
|
||||
})
|
||||
window.toast?.info?.(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Request, requestListener)
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
|
||||
return () => {
|
||||
window.electron?.ipcRenderer.removeListener(IpcChannel.AgentToolPermission_Request, requestListener)
|
||||
window.electron?.ipcRenderer.removeListener(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
}
|
||||
}, [dispatch, t])
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: init data collection
|
||||
}, [enableDataCollection])
|
||||
|
||||
@@ -57,7 +57,7 @@ export const useKnowledgeBaseForm = (base?: KnowledgeBase) => {
|
||||
label: t('settings.tool.preprocess.provider'),
|
||||
title: t('settings.tool.preprocess.provider'),
|
||||
options: preprocessProviders
|
||||
.filter((p) => p.apiKey !== '' || p.id === 'mineru')
|
||||
.filter((p) => p.apiKey !== '' || ['mineru', 'open-mineru'].includes(p.id))
|
||||
.map((p) => ({ value: p.id, label: p.name }))
|
||||
}
|
||||
return [preprocessOptions]
|
||||
|
||||
163
src/renderer/src/hooks/usePlugins.ts
Normal file
163
src/renderer/src/hooks/usePlugins.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import type { InstalledPlugin, PluginError, PluginMetadata } from '@renderer/types/plugin'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Helper to extract error message from PluginError union type
|
||||
*/
|
||||
function getPluginErrorMessage(error: PluginError, defaultMessage: string): string {
|
||||
if ('message' in error && error.message) return error.message
|
||||
if ('reason' in error) return error.reason
|
||||
if ('path' in error) return `Error with file: ${error.path}`
|
||||
return defaultMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch and cache available plugins from the resources directory
|
||||
* @returns Object containing available agents, commands, skills, loading state, and error
|
||||
*/
|
||||
export function useAvailablePlugins() {
|
||||
const [agents, setAgents] = useState<PluginMetadata[]>([])
|
||||
const [commands, setCommands] = useState<PluginMetadata[]>([])
|
||||
const [skills, setSkills] = useState<PluginMetadata[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAvailablePlugins = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await window.api.claudeCodePlugin.listAvailable()
|
||||
|
||||
if (result.success) {
|
||||
setAgents(result.data.agents)
|
||||
setCommands(result.data.commands)
|
||||
setSkills(result.data.skills)
|
||||
} else {
|
||||
setError(getPluginErrorMessage(result.error, 'Failed to load available plugins'))
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error occurred')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchAvailablePlugins()
|
||||
}, [])
|
||||
|
||||
return { agents, commands, skills, loading, error }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch installed plugins for a specific agent
|
||||
* @param agentId - The ID of the agent to fetch plugins for
|
||||
* @returns Object containing installed plugins, loading state, error, and refresh function
|
||||
*/
|
||||
export function useInstalledPlugins(agentId: string | undefined) {
|
||||
const [plugins, setPlugins] = useState<InstalledPlugin[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!agentId) {
|
||||
setPlugins([])
|
||||
setLoading(false)
|
||||
setError(null)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await window.api.claudeCodePlugin.listInstalled(agentId)
|
||||
|
||||
if (result.success) {
|
||||
setPlugins(result.data)
|
||||
} else {
|
||||
setError(getPluginErrorMessage(result.error, 'Failed to load installed plugins'))
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error occurred')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [agentId])
|
||||
|
||||
useEffect(() => {
|
||||
refresh()
|
||||
}, [refresh])
|
||||
|
||||
return { plugins, loading, error, refresh }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to provide install and uninstall actions for plugins
|
||||
* @param agentId - The ID of the agent to perform actions for
|
||||
* @param onSuccess - Optional callback to be called on successful operations
|
||||
* @returns Object containing install, uninstall functions and their loading states
|
||||
*/
|
||||
export function usePluginActions(agentId: string, onSuccess?: () => void) {
|
||||
const [installing, setInstalling] = useState<boolean>(false)
|
||||
const [uninstalling, setUninstalling] = useState<boolean>(false)
|
||||
|
||||
const install = useCallback(
|
||||
async (sourcePath: string, type: 'agent' | 'command' | 'skill') => {
|
||||
setInstalling(true)
|
||||
|
||||
try {
|
||||
const result = await window.api.claudeCodePlugin.install({
|
||||
agentId,
|
||||
sourcePath,
|
||||
type
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
onSuccess?.()
|
||||
return { success: true as const, data: result.data }
|
||||
} else {
|
||||
const errorMessage = getPluginErrorMessage(result.error, 'Failed to install plugin')
|
||||
return { success: false as const, error: errorMessage }
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
|
||||
return { success: false as const, error: errorMessage }
|
||||
} finally {
|
||||
setInstalling(false)
|
||||
}
|
||||
},
|
||||
[agentId, onSuccess]
|
||||
)
|
||||
|
||||
const uninstall = useCallback(
|
||||
async (filename: string, type: 'agent' | 'command' | 'skill') => {
|
||||
setUninstalling(true)
|
||||
|
||||
try {
|
||||
const result = await window.api.claudeCodePlugin.uninstall({
|
||||
agentId,
|
||||
filename,
|
||||
type
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
onSuccess?.()
|
||||
return { success: true as const }
|
||||
} else {
|
||||
const errorMessage = getPluginErrorMessage(result.error, 'Failed to uninstall plugin')
|
||||
return { success: false as const, error: errorMessage }
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
|
||||
return { success: false as const, error: errorMessage }
|
||||
} finally {
|
||||
setUninstalling(false)
|
||||
}
|
||||
},
|
||||
[agentId, onSuccess]
|
||||
)
|
||||
|
||||
return { install, uninstall, installing, uninstalling }
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export function getVertexAIServiceAccount() {
|
||||
* 类型守卫:检查 Provider 是否为 VertexProvider
|
||||
*/
|
||||
export function isVertexProvider(provider: Provider): provider is VertexProvider {
|
||||
return provider.type === 'vertexai' && 'googleCredentials' in provider
|
||||
return provider.type === 'vertexai'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -143,7 +143,8 @@ const titleKeyMap = {
|
||||
notes: 'title.notes',
|
||||
paintings: 'title.paintings',
|
||||
settings: 'title.settings',
|
||||
translate: 'title.translate'
|
||||
translate: 'title.translate',
|
||||
terminal: 'title.terminal'
|
||||
} as const
|
||||
|
||||
export const getTitleLabel = (key: string): string => {
|
||||
|
||||
@@ -107,6 +107,50 @@
|
||||
"title": "Advanced Settings"
|
||||
},
|
||||
"essential": "Essential Settings",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Available Plugins"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "Are you sure you want to uninstall this plugin?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "No plugins found matching your filters. Try adjusting your search or category filters."
|
||||
},
|
||||
"error": {
|
||||
"install": "Failed to install plugin",
|
||||
"load": "Failed to load plugins",
|
||||
"uninstall": "Failed to uninstall plugin"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All Categories"
|
||||
},
|
||||
"install": "Install",
|
||||
"installed": {
|
||||
"empty": "No plugins installed yet. Browse available plugins to get started.",
|
||||
"title": "Installed Plugins"
|
||||
},
|
||||
"installing": "Installing...",
|
||||
"results": "{{count}} plugin(s) found",
|
||||
"search": {
|
||||
"placeholder": "Search plugins..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Plugin installed successfully",
|
||||
"uninstall": "Plugin uninstalled successfully"
|
||||
},
|
||||
"tab": "Plugins",
|
||||
"type": {
|
||||
"agent": "Agent",
|
||||
"agents": "Agents",
|
||||
"all": "All",
|
||||
"command": "Command",
|
||||
"commands": "Commands",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"uninstall": "Uninstall",
|
||||
"uninstalling": "Uninstalling..."
|
||||
},
|
||||
"prompt": "Prompt Settings",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -191,6 +235,39 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Allow tool request",
|
||||
"denyRequest": "Deny tool request",
|
||||
"hideDetails": "Hide tool details",
|
||||
"runWithOptions": "Run with additional options",
|
||||
"showDetails": "Show tool details"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Cancel",
|
||||
"run": "Run"
|
||||
},
|
||||
"confirmation": "Are you sure you want to run this Claude tool?",
|
||||
"defaultDenyMessage": "User denied permission for this tool.",
|
||||
"defaultDescription": "Executes code or system actions in your environment. Make sure the command looks safe before running it.",
|
||||
"error": {
|
||||
"sendFailed": "Failed to send your decision. Please try again."
|
||||
},
|
||||
"expired": "Expired",
|
||||
"inputPreview": "Tool input preview",
|
||||
"pending": "Pending ({{seconds}}s)",
|
||||
"permissionExpired": "Permission request expired. Waiting for new instructions...",
|
||||
"requiresElevatedPermissions": "This tool requires elevated permissions.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Approving may update multiple session permissions if you chose to always allow this tool.",
|
||||
"permissionUpdateSingle": "Approving may update your session permissions if you chose to always allow this tool."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "Tool request was denied.",
|
||||
"timeout": "Tool request timed out before receiving approval."
|
||||
},
|
||||
"waiting": "Waiting for tool permission decision..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Agent Type",
|
||||
"unknown": "Unknown Type"
|
||||
@@ -2299,6 +2376,32 @@
|
||||
"seed_tip": "Controls upscaling randomness"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Actions",
|
||||
"agents": "Agents",
|
||||
"all_categories": "All Categories",
|
||||
"all_types": "All",
|
||||
"category": "Category",
|
||||
"commands": "Commands",
|
||||
"confirm_uninstall": "Are you sure you want to uninstall {{name}}?",
|
||||
"install": "Install",
|
||||
"install_plugins_from_browser": "Browse available plugins to get started",
|
||||
"installing": "Installing...",
|
||||
"name": "Name",
|
||||
"no_description": "No description available",
|
||||
"no_installed_plugins": "No plugins installed yet",
|
||||
"no_results": "No plugins found",
|
||||
"search_placeholder": "Search plugins...",
|
||||
"showing_results": "Showing {{count}} plugin",
|
||||
"showing_results_one": "Showing {{count}} plugin",
|
||||
"showing_results_other": "Showing {{count}} plugins",
|
||||
"showing_results_plural": "Showing {{count}} plugins",
|
||||
"skills": "Skills",
|
||||
"try_different_search": "Try adjusting your search or category filters",
|
||||
"type": "Type",
|
||||
"uninstall": "Uninstall",
|
||||
"uninstalling": "Uninstalling..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Copy as image"
|
||||
@@ -4045,7 +4148,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -4090,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Preview: {{url}}",
|
||||
"reset": "Reset",
|
||||
"tip": "Ending with / ignores v1, ending with # forces use of input address"
|
||||
"tip": "ending with # forces use of input address"
|
||||
}
|
||||
},
|
||||
"api_host": "API Host",
|
||||
"api_host_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": {
|
||||
@@ -4512,6 +4615,7 @@
|
||||
"paintings": "Paintings",
|
||||
"settings": "Settings",
|
||||
"store": "Assistant Library",
|
||||
"terminal": "Terminal",
|
||||
"translate": "Translate"
|
||||
},
|
||||
"trace": {
|
||||
|
||||
@@ -107,6 +107,50 @@
|
||||
"title": "高级设置"
|
||||
},
|
||||
"essential": "基础设置",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "可用插件"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "确定要卸载此插件吗?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "未找到匹配的插件。请尝试调整搜索或类别筛选。"
|
||||
},
|
||||
"error": {
|
||||
"install": "安装插件失败",
|
||||
"load": "加载插件失败",
|
||||
"uninstall": "卸载插件失败"
|
||||
},
|
||||
"filter": {
|
||||
"all": "所有类别"
|
||||
},
|
||||
"install": "安装",
|
||||
"installed": {
|
||||
"empty": "尚未安装任何插件。浏览可用插件以开始使用。",
|
||||
"title": "已安装插件"
|
||||
},
|
||||
"installing": "安装中...",
|
||||
"results": "找到 {{count}} 个插件",
|
||||
"search": {
|
||||
"placeholder": "搜索插件..."
|
||||
},
|
||||
"success": {
|
||||
"install": "插件安装成功",
|
||||
"uninstall": "插件卸载成功"
|
||||
},
|
||||
"tab": "插件",
|
||||
"type": {
|
||||
"agent": "代理",
|
||||
"agents": "代理",
|
||||
"all": "全部",
|
||||
"command": "命令",
|
||||
"commands": "命令",
|
||||
"skills": "技能"
|
||||
},
|
||||
"uninstall": "卸载",
|
||||
"uninstalling": "卸载中..."
|
||||
},
|
||||
"prompt": "提示词设置",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -191,6 +235,39 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "允许工具请求",
|
||||
"denyRequest": "拒绝工具请求",
|
||||
"hideDetails": "隐藏工具详情",
|
||||
"runWithOptions": "带选项运行",
|
||||
"showDetails": "显示工具详情"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "取消",
|
||||
"run": "运行"
|
||||
},
|
||||
"confirmation": "确定要运行此 Claude 工具吗?",
|
||||
"defaultDenyMessage": "用户拒绝了该工具的权限。",
|
||||
"defaultDescription": "在您的环境中执行代码或系统操作。运行前请确保命令安全。",
|
||||
"error": {
|
||||
"sendFailed": "发送您的决定失败,请重试。"
|
||||
},
|
||||
"expired": "已过期",
|
||||
"inputPreview": "工具输入预览",
|
||||
"pending": "等待中 ({{seconds}}秒)",
|
||||
"permissionExpired": "权限请求已过期。等待新指令...",
|
||||
"requiresElevatedPermissions": "此工具需要更高权限。",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "如果您选择总是允许此工具,批准可能会更新多个会话权限。",
|
||||
"permissionUpdateSingle": "如果您选择总是允许此工具,批准可能会更新您的会话权限。"
|
||||
},
|
||||
"toast": {
|
||||
"denied": "工具请求已被拒绝。",
|
||||
"timeout": "工具请求在收到批准前超时。"
|
||||
},
|
||||
"waiting": "等待工具权限决定..."
|
||||
},
|
||||
"type": {
|
||||
"label": "智能体类型",
|
||||
"unknown": "未知类型"
|
||||
@@ -2299,6 +2376,32 @@
|
||||
"seed_tip": "控制放大结果的随机性"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "操作",
|
||||
"agents": "代理",
|
||||
"all_categories": "所有类别",
|
||||
"all_types": "全部",
|
||||
"category": "类别",
|
||||
"commands": "命令",
|
||||
"confirm_uninstall": "确定要卸载 {{name}} 吗?",
|
||||
"install": "安装",
|
||||
"install_plugins_from_browser": "浏览可用插件以开始使用",
|
||||
"installing": "安装中...",
|
||||
"name": "名称",
|
||||
"no_description": "无描述",
|
||||
"no_installed_plugins": "尚未安装任何插件",
|
||||
"no_results": "未找到插件",
|
||||
"search_placeholder": "搜索插件...",
|
||||
"showing_results": "显示 {{count}} 个插件",
|
||||
"showing_results_one": "显示 {{count}} 个插件",
|
||||
"showing_results_other": "显示 {{count}} 个插件",
|
||||
"showing_results_plural": "显示 {{count}} 个插件",
|
||||
"skills": "技能",
|
||||
"try_different_search": "请尝试调整搜索或类别筛选",
|
||||
"type": "类型",
|
||||
"uninstall": "卸载",
|
||||
"uninstalling": "卸载中..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "复制为图片"
|
||||
@@ -4045,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic API 地址",
|
||||
"anthropic_api_host_preview": "Anthropic 预览:{{url}}",
|
||||
"anthropic_api_host_tip": "仅在服务商提供兼容 Anthropic 的地址时填写。以 / 结尾会忽略自动追加的 v1,以 # 结尾则强制使用原始地址。",
|
||||
"anthropic_api_host_tooltip": "仅当服务商提供 Claude 兼容的基础地址时填写。",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4090,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "预览: {{url}}",
|
||||
"reset": "重置",
|
||||
"tip": "/ 结尾忽略 v1 版本,# 结尾强制使用输入地址"
|
||||
"tip": "# 结尾强制使用输入地址"
|
||||
}
|
||||
},
|
||||
"api_host": "API 地址",
|
||||
"api_host_no_valid": "API 地址不合法",
|
||||
"api_host_preview": "预览:{{url}}",
|
||||
"api_host_tooltip": "仅在服务商需要自定义的 OpenAI 兼容地址时覆盖。",
|
||||
"api_key": {
|
||||
@@ -4512,6 +4615,7 @@
|
||||
"paintings": "绘画",
|
||||
"settings": "设置",
|
||||
"store": "助手库",
|
||||
"terminal": "终端",
|
||||
"translate": "翻译"
|
||||
},
|
||||
"trace": {
|
||||
|
||||
@@ -107,6 +107,50 @@
|
||||
"title": "進階設定"
|
||||
},
|
||||
"essential": "必要設定",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "可用外掛"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "確定要解除安裝此外掛嗎?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "未找到符合的外掛。請嘗試調整搜尋或類別篩選。"
|
||||
},
|
||||
"error": {
|
||||
"install": "安裝外掛失敗",
|
||||
"load": "載入外掛失敗",
|
||||
"uninstall": "解除安裝外掛失敗"
|
||||
},
|
||||
"filter": {
|
||||
"all": "所有類別"
|
||||
},
|
||||
"install": "安裝",
|
||||
"installed": {
|
||||
"empty": "尚未安裝任何外掛。瀏覽可用外掛以開始使用。",
|
||||
"title": "已安裝外掛"
|
||||
},
|
||||
"installing": "安裝中...",
|
||||
"results": "找到 {{count}} 個外掛",
|
||||
"search": {
|
||||
"placeholder": "搜尋外掛..."
|
||||
},
|
||||
"success": {
|
||||
"install": "外掛安裝成功",
|
||||
"uninstall": "外掛解除安裝成功"
|
||||
},
|
||||
"tab": "外掛",
|
||||
"type": {
|
||||
"agent": "代理",
|
||||
"agents": "代理",
|
||||
"all": "全部",
|
||||
"command": "指令",
|
||||
"commands": "指令",
|
||||
"skills": "技能"
|
||||
},
|
||||
"uninstall": "解除安裝",
|
||||
"uninstalling": "解除安裝中..."
|
||||
},
|
||||
"prompt": "提示設定",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -191,6 +235,39 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "允許工具請求",
|
||||
"denyRequest": "拒絕工具請求",
|
||||
"hideDetails": "隱藏工具詳情",
|
||||
"runWithOptions": "帶選項執行",
|
||||
"showDetails": "顯示工具詳情"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "取消",
|
||||
"run": "執行"
|
||||
},
|
||||
"confirmation": "確定要執行此 Claude 工具嗎?",
|
||||
"defaultDenyMessage": "使用者拒絕了該工具的權限。",
|
||||
"defaultDescription": "在您的環境中執行程式碼或系統操作。執行前請確保指令安全。",
|
||||
"error": {
|
||||
"sendFailed": "傳送您的決定失敗,請重試。"
|
||||
},
|
||||
"expired": "已過期",
|
||||
"inputPreview": "工具輸入預覽",
|
||||
"pending": "等待中 ({{seconds}}秒)",
|
||||
"permissionExpired": "權限請求已過期。等待新指令...",
|
||||
"requiresElevatedPermissions": "此工具需要提升的權限。",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "如果您選擇總是允許此工具,核准可能會更新多個工作階段權限。",
|
||||
"permissionUpdateSingle": "如果您選擇總是允許此工具,核准可能會更新您的工作階段權限。"
|
||||
},
|
||||
"toast": {
|
||||
"denied": "工具請求已被拒絕。",
|
||||
"timeout": "工具請求在收到核准前逾時。"
|
||||
},
|
||||
"waiting": "等待工具權限決定..."
|
||||
},
|
||||
"type": {
|
||||
"label": "代理類型",
|
||||
"unknown": "未知類型"
|
||||
@@ -2299,6 +2376,32 @@
|
||||
"seed_tip": "控制放大結果的隨機性"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "操作",
|
||||
"agents": "代理",
|
||||
"all_categories": "所有類別",
|
||||
"all_types": "全部",
|
||||
"category": "類別",
|
||||
"commands": "指令",
|
||||
"confirm_uninstall": "確定要解除安裝 {{name}} 嗎?",
|
||||
"install": "安裝",
|
||||
"install_plugins_from_browser": "瀏覽可用外掛以開始使用",
|
||||
"installing": "安裝中...",
|
||||
"name": "名稱",
|
||||
"no_description": "無描述",
|
||||
"no_installed_plugins": "尚未安裝任何外掛",
|
||||
"no_results": "未找到外掛",
|
||||
"search_placeholder": "搜尋外掛...",
|
||||
"showing_results": "顯示 {{count}} 個外掛",
|
||||
"showing_results_one": "顯示 {{count}} 個外掛",
|
||||
"showing_results_other": "顯示 {{count}} 個外掛",
|
||||
"showing_results_plural": "顯示 {{count}} 個外掛",
|
||||
"skills": "技能",
|
||||
"try_different_search": "請嘗試調整搜尋或類別篩選",
|
||||
"type": "類型",
|
||||
"uninstall": "解除安裝",
|
||||
"uninstalling": "解除安裝中..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "複製為圖片"
|
||||
@@ -4045,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic API 主機地址",
|
||||
"anthropic_api_host_preview": "Anthropic 預覽:{{url}}",
|
||||
"anthropic_api_host_tip": "僅在服務商提供與 Anthropic 相容的網址時設定。以 / 結尾會忽略自動附加的 v1,以 # 結尾則強制使用原始地址。",
|
||||
"anthropic_api_host_tooltip": "僅在服務商提供 Claude 相容的基礎網址時設定。",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4090,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "預覽:{{url}}",
|
||||
"reset": "重設",
|
||||
"tip": "/ 結尾忽略 v1 版本,# 結尾強制使用輸入位址"
|
||||
"tip": "# 結尾強制使用輸入位址"
|
||||
}
|
||||
},
|
||||
"api_host": "API 主機地址",
|
||||
"api_host_no_valid": "API 位址不合法",
|
||||
"api_host_preview": "預覽:{{url}}",
|
||||
"api_host_tooltip": "僅在服務商需要自訂的 OpenAI 相容端點時才覆蓋。",
|
||||
"api_key": {
|
||||
@@ -4512,6 +4615,7 @@
|
||||
"paintings": "繪畫",
|
||||
"settings": "設定",
|
||||
"store": "助手庫",
|
||||
"terminal": "終端機",
|
||||
"translate": "翻譯"
|
||||
},
|
||||
"trace": {
|
||||
|
||||
@@ -107,6 +107,50 @@
|
||||
"title": "Erweiterte Einstellungen"
|
||||
},
|
||||
"essential": "Grundeinstellungen",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Verfügbare Plugins"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "Sind Sie sicher, dass Sie dieses Plugin deinstallieren möchten?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "Keine Plugins gefunden, die deinen Filtern entsprechen. Versuche, deine Such- oder Kategoriefilter anzupassen."
|
||||
},
|
||||
"error": {
|
||||
"install": "Fehler beim Installieren des Plugins",
|
||||
"load": "Fehler beim Laden der Plugins",
|
||||
"uninstall": "Fehler beim Deinstallieren des Plugins"
|
||||
},
|
||||
"filter": {
|
||||
"all": "Alle Kategorien"
|
||||
},
|
||||
"install": "Installieren",
|
||||
"installed": {
|
||||
"empty": "Noch keine Plugins installiert. Durchsuche verfügbare Plugins, um loszulegen.",
|
||||
"title": "Installierte Plugins"
|
||||
},
|
||||
"installing": "Wird installiert...",
|
||||
"results": "{{count}} Plugin(s) gefunden",
|
||||
"search": {
|
||||
"placeholder": "Such-Plugins..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Plugin erfolgreich installiert",
|
||||
"uninstall": "Plugin erfolgreich deinstalliert"
|
||||
},
|
||||
"tab": "Plugins",
|
||||
"type": {
|
||||
"agent": "Agent",
|
||||
"agents": "Agenten",
|
||||
"all": "Alle",
|
||||
"command": "Befehl",
|
||||
"commands": "Befehle",
|
||||
"skills": "Fähigkeiten"
|
||||
},
|
||||
"uninstall": "Deinstallieren",
|
||||
"uninstalling": "Deinstallation läuft..."
|
||||
},
|
||||
"prompt": "Prompt-Einstellungen",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -191,6 +235,39 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Werkzeuganfrage zulassen",
|
||||
"denyRequest": "Werkzeuganfrage ablehnen",
|
||||
"hideDetails": "Werkzeugdetails ausblenden",
|
||||
"runWithOptions": "Mit zusätzlichen Optionen ausführen",
|
||||
"showDetails": "Zeige Werkzeugdetails"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Abbrechen",
|
||||
"run": "Laufen"
|
||||
},
|
||||
"confirmation": "Bist du sicher, dass du dieses Claude-Tool ausführen möchtest?",
|
||||
"defaultDenyMessage": "Der Benutzer hat die Berechtigung für dieses Tool verweigert.",
|
||||
"defaultDescription": "Führt Code oder Systemaktionen in Ihrer Umgebung aus. Vergewissern Sie sich, dass der Befehl sicher aussieht, bevor Sie ihn ausführen.",
|
||||
"error": {
|
||||
"sendFailed": "Ihre Entscheidung konnte nicht gesendet werden. Bitte versuchen Sie es erneut."
|
||||
},
|
||||
"expired": "Abgelaufen",
|
||||
"inputPreview": "Vorschau der Werkzeugeingabe",
|
||||
"pending": "Ausstehend ({{seconds}}s)",
|
||||
"permissionExpired": "Berechtigungsanfrage abgelaufen. Warte auf neue Anweisungen...",
|
||||
"requiresElevatedPermissions": "Dieses Tool erfordert erhöhte Berechtigungen.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Das Genehmigen kann mehrere Sitzungsberechtigungen aktualisieren, wenn Sie sich entschieden haben, dieses Tool immer zuzulassen.",
|
||||
"permissionUpdateSingle": "Das Genehmigen kann Ihre Sitzungsberechtigungen aktualisieren, wenn Sie sich entschieden haben, dieses Tool immer zuzulassen."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "Tool-Anfrage wurde abgelehnt.",
|
||||
"timeout": "Tool-Anfrage ist abgelaufen, bevor eine Genehmigung eingegangen ist."
|
||||
},
|
||||
"waiting": "Warten auf Entscheidung über Tool-Berechtigung..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Agent-Typ",
|
||||
"unknown": "Unbekannter Typ"
|
||||
@@ -2299,6 +2376,32 @@
|
||||
"seed_tip": "Kontrolle der Zufälligkeit des Upscale-Ergebnisses"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Aktionen",
|
||||
"agents": "Agenten",
|
||||
"all_categories": "Alle Kategorien",
|
||||
"all_types": "Alle",
|
||||
"category": "Kategorie",
|
||||
"commands": "Befehle",
|
||||
"confirm_uninstall": "Sind Sie sicher, dass Sie {{name}} deinstallieren möchten?",
|
||||
"install": "Installieren",
|
||||
"install_plugins_from_browser": "Durchsuche verfügbare Plugins, um loszulegen",
|
||||
"installing": "Installiere…",
|
||||
"name": "Name",
|
||||
"no_description": "Keine Beschreibung verfügbar",
|
||||
"no_installed_plugins": "Noch keine Plugins installiert",
|
||||
"no_results": "Keine Plugins gefunden",
|
||||
"search_placeholder": "Such-Plugins...",
|
||||
"showing_results": "{{count}} Plugin anzeigen",
|
||||
"showing_results_one": "{{count}} Plugin anzeigen",
|
||||
"showing_results_other": "Zeige {{count}} Plugins",
|
||||
"showing_results_plural": "{{count}} Plugins anzeigen",
|
||||
"skills": "Fähigkeiten",
|
||||
"try_different_search": "Versuchen Sie, Ihre Suche oder die Kategoriefilter anzupassen.",
|
||||
"type": "Typ",
|
||||
"uninstall": "Deinstallieren",
|
||||
"uninstalling": "Deinstallation läuft..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Als Bild kopieren"
|
||||
@@ -4045,7 +4148,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -4090,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Vorschau: {{url}}",
|
||||
"reset": "Zurücksetzen",
|
||||
"tip": "/ am Ende ignorieren v1-Version, # am Ende erzwingt die Verwendung der Eingabe-Adresse"
|
||||
"tip": "# am Ende erzwingt die Verwendung der Eingabe-Adresse"
|
||||
}
|
||||
},
|
||||
"api_host": "API-Adresse",
|
||||
"api_host_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": {
|
||||
@@ -4512,6 +4615,7 @@
|
||||
"paintings": "Zeichnen",
|
||||
"settings": "Einstellungen",
|
||||
"store": "Assistenten-Bibliothek",
|
||||
"terminal": "Terminal",
|
||||
"translate": "Übersetzen"
|
||||
},
|
||||
"trace": {
|
||||
|
||||
@@ -107,6 +107,50 @@
|
||||
"title": "Ρυθμίσεις για προχωρημένους"
|
||||
},
|
||||
"essential": "Βασικές Ρυθμίσεις",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Διαθέσιμα πρόσθετα"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "Είστε βέβαιοι ότι θέλετε να απεγκαταστήσετε αυτό το πρόσθετο;"
|
||||
},
|
||||
"empty": {
|
||||
"available": "Δεν βρέθηκε συμβατό πρόσθετο. Δοκιμάστε να προσαρμόσετε την αναζήτηση ή το φίλτρο κατηγοριών."
|
||||
},
|
||||
"error": {
|
||||
"install": "Η εγκατάσταση του πρόσθετου απέτυχε",
|
||||
"load": "Η φόρτωση του πρόσθετου απέτυχε",
|
||||
"uninstall": "Η απεγκατάσταση του πρόσθετου απέτυχε"
|
||||
},
|
||||
"filter": {
|
||||
"all": "Όλες οι κατηγορίες"
|
||||
},
|
||||
"install": "εγκατάσταση",
|
||||
"installed": {
|
||||
"empty": "Δεν έχει εγκατασταθεί κανένα πρόσθετο. Περιηγηθείτε στα διαθέσιμα πρόσθετα για να ξεκινήσετε.",
|
||||
"title": "Έχει εγκατασταθεί το πρόσθετο"
|
||||
},
|
||||
"installing": "Εγκατάσταση...",
|
||||
"results": "Βρέθηκαν {{count}} πρόσθετα",
|
||||
"search": {
|
||||
"placeholder": "Αναζήτηση πρόσθετου..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Η εγκατάσταση του πρόσθετου ολοκληρώθηκε με επιτυχία",
|
||||
"uninstall": "Η απεγκατάσταση του πρόσθετου ολοκληρώθηκε με επιτυχία"
|
||||
},
|
||||
"tab": "Πρόσθετο",
|
||||
"type": {
|
||||
"agent": "αντιπρόσωπος",
|
||||
"agents": "αντιπρόσωπος",
|
||||
"all": "όλα",
|
||||
"command": "εντολή",
|
||||
"commands": "εντολή",
|
||||
"skills": "δεξιότητα"
|
||||
},
|
||||
"uninstall": "απεγκατάσταση",
|
||||
"uninstalling": "Απεγκατάσταση..."
|
||||
},
|
||||
"prompt": "Ρυθμίσεις Προτροπής",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -191,6 +235,39 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Επίτρεψη αίτησης εργαλείου",
|
||||
"denyRequest": "Απόρριψη αιτήματος εργαλείου",
|
||||
"hideDetails": "Απόκρυψη λεπτομερειών εργαλείου",
|
||||
"runWithOptions": "Εκτέλεση με επιπλέον επιλογές",
|
||||
"showDetails": "Εμφάνιση λεπτομερειών εργαλείου"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Ακύρωση",
|
||||
"run": "Τρέξε"
|
||||
},
|
||||
"confirmation": "Είσαι σίγουρος ότι θέλεις να εκτελέσεις αυτό το εργαλείο Claude;",
|
||||
"defaultDenyMessage": "Ο χρήστης αρνήθηκε την άδεια για αυτό το εργαλείο.",
|
||||
"defaultDescription": "Εκτελεί κώδικα ή ενέργειες συστήματος στο περιβάλλον σας. Βεβαιωθείτε ότι η εντολή φαίνεται ασφαλής πριν την εκτελέσετε.",
|
||||
"error": {
|
||||
"sendFailed": "Αποτυχία αποστολής της απόφασής σας. Προσπαθήστε ξανά."
|
||||
},
|
||||
"expired": "Ληγμένο",
|
||||
"inputPreview": "Προεπισκόπηση εισόδου εργαλείου",
|
||||
"pending": "Εκκρεμεί ({{seconds}}δ)",
|
||||
"permissionExpired": "Το αίτημα άδειας έληξε. Αναμονή για νέες οδηγίες...",
|
||||
"requiresElevatedPermissions": "Αυτό το εργαλείο απαιτεί αυξημένα δικαιώματα.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Η έγκριση μπορεί να ενημερώσει πολλές άδειες συνεδρίας αν επιλέξατε να επιτρέπετε πάντα αυτό το εργαλείο.",
|
||||
"permissionUpdateSingle": "Η έγκριση ενδέχεται να ενημερώσει τα δικαιώματα περιόδου σύνδεσής σας, εάν επιλέξατε να επιτρέπετε πάντα αυτό το εργαλείο."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "Το αίτημα για εργαλείο απορρίφθηκε.",
|
||||
"timeout": "Το αίτημα για το εργαλείο έληξε πριν λάβει έγκριση."
|
||||
},
|
||||
"waiting": "Αναμονή για απόφαση άδειας εργαλείου..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Τύπος Πράκτορα",
|
||||
"unknown": "Άγνωστος Τύπος"
|
||||
@@ -2299,6 +2376,32 @@
|
||||
"seed_tip": "Ελέγχει την τυχαιότητα του αποτελέσματος μεγέθυνσης"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Λειτουργία",
|
||||
"agents": "αντιπρόσωπος",
|
||||
"all_categories": "Όλες οι κατηγορίες",
|
||||
"all_types": "ολόκληρο",
|
||||
"category": "Κατηγορία",
|
||||
"commands": "εντολή",
|
||||
"confirm_uninstall": "Είστε σίγουροι ότι θέλετε να απεγκαταστήσετε το {{name}};",
|
||||
"install": "εγκατάσταση",
|
||||
"install_plugins_from_browser": "Περιηγηθείτε στα διαθέσιμα πρόσθετα για να ξεκινήσετε",
|
||||
"installing": "Εγκατάσταση...",
|
||||
"name": "Όνομα",
|
||||
"no_description": "Χωρίς περιγραφή",
|
||||
"no_installed_plugins": "Δεν έχει εγκατασταθεί κανένα πρόσθετο",
|
||||
"no_results": "Δεν βρέθηκε πρόσθετο",
|
||||
"search_placeholder": "Πρόσθετο αναζήτησης...",
|
||||
"showing_results": "Εμφάνιση {{count}} προσθέτων",
|
||||
"showing_results_one": "Εμφάνιση {{count}} προσθέτων",
|
||||
"showing_results_other": "Εμφάνιση {{count}} προσθέτων",
|
||||
"showing_results_plural": "Εμφάνιση {{count}} πρόσθετων",
|
||||
"skills": "δεξιότητα",
|
||||
"try_different_search": "Προσπαθήστε να προσαρμόσετε την αναζήτηση ή το φίλτρο κατηγοριών",
|
||||
"type": "τύπος",
|
||||
"uninstall": "κατάργηση εγκατάστασης",
|
||||
"uninstalling": "Απεγκατάσταση..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Αντιγραφή ως εικόνα"
|
||||
@@ -4045,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Διεύθυνση API Anthropic",
|
||||
"anthropic_api_host_preview": "Προεπισκόπηση Anthropic: {{url}}",
|
||||
"anthropic_api_host_tip": "Συμπληρώστε μόνο εάν ο πάροχος προσφέρει συμβατή με Anthropic διεύθυνση. Η λήξη με / αγνοεί το v1 που προστίθεται αυτόματα, η λήξη με # επιβάλλει τη χρήση της αρχικής διεύθυνσης.",
|
||||
"anthropic_api_host_tooltip": "Συμπληρώστε μόνο όταν ο πάροχος παρέχει βασική διεύθυνση συμβατή με Claude.",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4090,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Προεπισκόπηση: {{url}}",
|
||||
"reset": "Επαναφορά",
|
||||
"tip": "/τέλος αγνόηση v1 έκδοσης, #τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως"
|
||||
"tip": "#τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως"
|
||||
}
|
||||
},
|
||||
"api_host": "Διεύθυνση API",
|
||||
"api_host_no_valid": "Η διεύθυνση API δεν είναι έγκυρη",
|
||||
"api_host_preview": "Προεπισκόπηση: {{url}}",
|
||||
"api_host_tooltip": "Αντικατάσταση μόνο όταν ο πάροχος απαιτεί προσαρμοσμένη διεύθυνση συμβατή με OpenAI.",
|
||||
"api_key": {
|
||||
@@ -4512,6 +4615,7 @@
|
||||
"paintings": "Ζωγραφική",
|
||||
"settings": "Ρυθμίσεις",
|
||||
"store": "Βιβλιοθήκη βοηθών",
|
||||
"terminal": "Τερματικό",
|
||||
"translate": "Μετάφραση"
|
||||
},
|
||||
"trace": {
|
||||
|
||||
@@ -107,6 +107,50 @@
|
||||
"title": "Configuración avanzada"
|
||||
},
|
||||
"essential": "Configuraciones esenciales",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Complementos disponibles"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "¿Estás seguro de que quieres desinstalar este complemento?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "No se encontró ningún complemento que coincida. Intenta ajustar la búsqueda o los filtros de categoría."
|
||||
},
|
||||
"error": {
|
||||
"install": "Error al instalar el complemento",
|
||||
"load": "Error al cargar el complemento",
|
||||
"uninstall": "Error al desinstalar el complemento"
|
||||
},
|
||||
"filter": {
|
||||
"all": "Todas las categorías"
|
||||
},
|
||||
"install": "instalación",
|
||||
"installed": {
|
||||
"empty": "Aún no se ha instalado ningún complemento. Explora los complementos disponibles para comenzar.",
|
||||
"title": "Complemento instalado"
|
||||
},
|
||||
"installing": "Instalando...",
|
||||
"results": "Encontrados {{count}} complementos",
|
||||
"search": {
|
||||
"placeholder": "Buscar complemento..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Complemento instalado con éxito",
|
||||
"uninstall": "Complemento desinstalado correctamente"
|
||||
},
|
||||
"tab": "complemento",
|
||||
"type": {
|
||||
"agent": "agente",
|
||||
"agents": "Agente",
|
||||
"all": "todo",
|
||||
"command": "comando",
|
||||
"commands": "comando",
|
||||
"skills": "habilidad"
|
||||
},
|
||||
"uninstall": "Desinstalar",
|
||||
"uninstalling": "Desinstalando..."
|
||||
},
|
||||
"prompt": "Configuración de indicaciones",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -191,6 +235,39 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Permitir solicitud de herramienta",
|
||||
"denyRequest": "Denegar solicitud de herramienta",
|
||||
"hideDetails": "Ocultar detalles de la herramienta",
|
||||
"runWithOptions": "Ejecutar con opciones adicionales",
|
||||
"showDetails": "Mostrar detalles de la herramienta"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Cancelar",
|
||||
"run": "Correr"
|
||||
},
|
||||
"confirmation": "¿Estás seguro de que quieres ejecutar esta herramienta de Claude?",
|
||||
"defaultDenyMessage": "El usuario denegó el permiso para esta herramienta.",
|
||||
"defaultDescription": "Ejecuta código o acciones del sistema en tu entorno. Asegúrate de que el comando parezca seguro antes de ejecutarlo.",
|
||||
"error": {
|
||||
"sendFailed": "No se pudo enviar tu decisión. Por favor, inténtalo de nuevo."
|
||||
},
|
||||
"expired": "Caducado",
|
||||
"inputPreview": "Vista previa de entrada de herramienta",
|
||||
"pending": "Pendiente ({{seconds}}s)",
|
||||
"permissionExpired": "Solicitud de permiso expirada. Esperando nuevas instrucciones...",
|
||||
"requiresElevatedPermissions": "Esta herramienta requiere permisos elevados.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Aprobar puede actualizar varios permisos de sesión si elegiste permitir siempre esta herramienta.",
|
||||
"permissionUpdateSingle": "Aprobar puede actualizar los permisos de tu sesión si elegiste permitir siempre esta herramienta."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "La solicitud de herramienta fue denegada.",
|
||||
"timeout": "La solicitud de herramienta expiró antes de recibir la aprobación."
|
||||
},
|
||||
"waiting": "Esperando la decisión de permiso de la herramienta..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipo de Agente",
|
||||
"unknown": "Tipo desconocido"
|
||||
@@ -2299,6 +2376,32 @@
|
||||
"seed_tip": "Controla la aleatoriedad del resultado de la ampliación"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Operación",
|
||||
"agents": "Agente",
|
||||
"all_categories": "Todas las categorías",
|
||||
"all_types": "todo",
|
||||
"category": "Categoría",
|
||||
"commands": "comando",
|
||||
"confirm_uninstall": "¿Estás seguro de que quieres desinstalar {{name}}?",
|
||||
"install": "instalación",
|
||||
"install_plugins_from_browser": "Explora los complementos disponibles para empezar a usar",
|
||||
"installing": "Instalando...",
|
||||
"name": "Nombre",
|
||||
"no_description": "Sin descripción",
|
||||
"no_installed_plugins": "Aún no se ha instalado ningún complemento",
|
||||
"no_results": "No se encontró el complemento",
|
||||
"search_placeholder": "Buscar complemento...",
|
||||
"showing_results": "Mostrar {{count}} complementos",
|
||||
"showing_results_one": "Mostrar {{count}} complementos",
|
||||
"showing_results_other": "Mostrar {{count}} complementos",
|
||||
"showing_results_plural": "Mostrar {{count}} complementos",
|
||||
"skills": "habilidad",
|
||||
"try_different_search": "Por favor, intenta ajustar la búsqueda o los filtros de categoría.",
|
||||
"type": "tipo",
|
||||
"uninstall": "Desinstalar",
|
||||
"uninstalling": "Desinstalando..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Copiar como imagen"
|
||||
@@ -4045,7 +4148,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -4090,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Vista previa: {{url}}",
|
||||
"reset": "Restablecer",
|
||||
"tip": "Ignorar v1 al final con /, forzar uso de dirección de entrada con # al final"
|
||||
"tip": "forzar uso de dirección de entrada con # al final"
|
||||
}
|
||||
},
|
||||
"api_host": "Dirección API",
|
||||
"api_host_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": {
|
||||
@@ -4512,6 +4615,7 @@
|
||||
"paintings": "Pinturas",
|
||||
"settings": "Configuración",
|
||||
"store": "Biblioteca de asistentes",
|
||||
"terminal": "Terminal",
|
||||
"translate": "Traducir"
|
||||
},
|
||||
"trace": {
|
||||
|
||||
@@ -107,6 +107,50 @@
|
||||
"title": "Paramètres avancés"
|
||||
},
|
||||
"essential": "Paramètres essentiels",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Plugins disponibles"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "Êtes-vous sûr de vouloir désinstaller ce plugin ?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "Aucun plugin correspondant trouvé. Veuillez essayer d’ajuster la recherche ou les filtres de catégorie."
|
||||
},
|
||||
"error": {
|
||||
"install": "Échec de l'installation du plugin",
|
||||
"load": "Échec du chargement du plugin",
|
||||
"uninstall": "Échec de la désinstallation du plugin"
|
||||
},
|
||||
"filter": {
|
||||
"all": "Toutes les catégories"
|
||||
},
|
||||
"install": "Installation",
|
||||
"installed": {
|
||||
"empty": "Aucun plugin n'est encore installé. Parcourez les plugins disponibles pour commencer.",
|
||||
"title": "Extension installée"
|
||||
},
|
||||
"installing": "Installation en cours...",
|
||||
"results": "{{count}} modules complémentaires trouvés",
|
||||
"search": {
|
||||
"placeholder": "Recherche de plug-ins..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Installation du plugin réussie",
|
||||
"uninstall": "Désinstallation du plugin réussie"
|
||||
},
|
||||
"tab": "Module d'extension",
|
||||
"type": {
|
||||
"agent": "mandataire",
|
||||
"agents": "mandataire",
|
||||
"all": "Tout",
|
||||
"command": "commande",
|
||||
"commands": "commande",
|
||||
"skills": "compétence"
|
||||
},
|
||||
"uninstall": "Désinstaller",
|
||||
"uninstalling": "Désinstallation en cours..."
|
||||
},
|
||||
"prompt": "Paramètres de l'invite",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -191,6 +235,39 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Autoriser la demande d'outil",
|
||||
"denyRequest": "Refuser la demande d'outil",
|
||||
"hideDetails": "Masquer les détails de l'outil",
|
||||
"runWithOptions": "Exécuter avec des options supplémentaires",
|
||||
"showDetails": "Afficher les détails de l'outil"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Annuler",
|
||||
"run": "Courir"
|
||||
},
|
||||
"confirmation": "Êtes-vous sûr de vouloir exécuter cet outil Claude ?",
|
||||
"defaultDenyMessage": "L'utilisateur a refusé l'autorisation pour cet outil.",
|
||||
"defaultDescription": "Exécute du code ou des actions système dans votre environnement. Assurez-vous que la commande semble sûre avant de l’exécuter.",
|
||||
"error": {
|
||||
"sendFailed": "Échec de l'envoi de votre décision. Veuillez réessayer."
|
||||
},
|
||||
"expired": "Expiré",
|
||||
"inputPreview": "Aperçu de l'entrée de l'outil",
|
||||
"pending": "En attente ({{seconds}}s)",
|
||||
"permissionExpired": "Demande de permission expirée. En attente de nouvelles instructions...",
|
||||
"requiresElevatedPermissions": "Cet outil nécessite des autorisations élevées.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Approuver peut mettre à jour plusieurs autorisations de session si vous avez choisi de toujours autoriser cet outil.",
|
||||
"permissionUpdateSingle": "Approuver peut mettre à jour vos permissions de session si vous avez choisi de toujours autoriser cet outil."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "La demande d'outil a été refusée.",
|
||||
"timeout": "La demande d'outil a expiré avant d'obtenir l'approbation."
|
||||
},
|
||||
"waiting": "En attente de la décision d'autorisation de l'outil..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Type d'agent",
|
||||
"unknown": "Type inconnu"
|
||||
@@ -2299,6 +2376,32 @@
|
||||
"seed_tip": "Contrôle la randomisation du résultat d'agrandissement"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Opération",
|
||||
"agents": "mandataire",
|
||||
"all_categories": "Toutes les catégories",
|
||||
"all_types": "Tout",
|
||||
"category": "Catégorie",
|
||||
"commands": "commande",
|
||||
"confirm_uninstall": "Êtes-vous sûr de vouloir désinstaller {{name}} ?",
|
||||
"install": "Installation",
|
||||
"install_plugins_from_browser": "Parcourir les plugins disponibles pour commencer",
|
||||
"installing": "Installation en cours...",
|
||||
"name": "Nom",
|
||||
"no_description": "Sans description",
|
||||
"no_installed_plugins": "Aucun plugin n’est encore installé",
|
||||
"no_results": "Aucun plugin trouvé",
|
||||
"search_placeholder": "Rechercher des modules d'extension...",
|
||||
"showing_results": "Afficher {{count}} extensions",
|
||||
"showing_results_one": "Afficher {{count}} modules d’extension",
|
||||
"showing_results_other": "Afficher {{count}} modules d'extension",
|
||||
"showing_results_plural": "Afficher {{count}} modules d'extension",
|
||||
"skills": "compétence",
|
||||
"try_different_search": "Veuillez essayer d’ajuster la recherche ou le filtre de catégorie.",
|
||||
"type": "type",
|
||||
"uninstall": "Désinstaller",
|
||||
"uninstalling": "Désinstallation en cours..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Copier en tant qu'image"
|
||||
@@ -4045,7 +4148,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -4090,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Aperçu : {{url}}",
|
||||
"reset": "Réinitialiser",
|
||||
"tip": "Ignorer la version v1 si terminé par /, forcer l'utilisation de l'adresse d'entrée si terminé par #"
|
||||
"tip": "forcer l'utilisation de l'adresse d'entrée si terminé par #"
|
||||
}
|
||||
},
|
||||
"api_host": "Adresse API",
|
||||
"api_host_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": {
|
||||
@@ -4512,6 +4615,7 @@
|
||||
"paintings": "Peintures",
|
||||
"settings": "Paramètres",
|
||||
"store": "Bibliothèque d'assistants",
|
||||
"terminal": "Terminal",
|
||||
"translate": "Traduire"
|
||||
},
|
||||
"trace": {
|
||||
|
||||
@@ -107,6 +107,50 @@
|
||||
"title": "高級設定"
|
||||
},
|
||||
"essential": "必須設定",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "利用可能なプラグイン"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "このプラグインをアンインストールしてもよろしいですか?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "一致するプラグインが見つかりませんでした。検索キーワードやカテゴリフィルターを調整してみてください。"
|
||||
},
|
||||
"error": {
|
||||
"install": "プラグインのインストールに失敗しました",
|
||||
"load": "プラグインの読み込みに失敗しました",
|
||||
"uninstall": "プラグインのアンインストールに失敗しました"
|
||||
},
|
||||
"filter": {
|
||||
"all": "すべてのカテゴリー"
|
||||
},
|
||||
"install": "インストール",
|
||||
"installed": {
|
||||
"empty": "まだプラグインがインストールされていません。利用可能なプラグインを見てみましょう。",
|
||||
"title": "インストール済みプラグイン"
|
||||
},
|
||||
"installing": "インストール中...",
|
||||
"results": "{{count}} 個のプラグインが見つかりました",
|
||||
"search": {
|
||||
"placeholder": "検索プラグイン..."
|
||||
},
|
||||
"success": {
|
||||
"install": "プラグインのインストールが成功しました",
|
||||
"uninstall": "プラグインのアンインストールが成功しました"
|
||||
},
|
||||
"tab": "プラグイン",
|
||||
"type": {
|
||||
"agent": "代理",
|
||||
"agents": "代理",
|
||||
"all": "全部",
|
||||
"command": "命令",
|
||||
"commands": "命令",
|
||||
"skills": "技能"
|
||||
},
|
||||
"uninstall": "アンインストール",
|
||||
"uninstalling": "アンインストール中..."
|
||||
},
|
||||
"prompt": "プロンプト設定",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -191,6 +235,39 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "ツールリクエストを許可",
|
||||
"denyRequest": "ツールリクエストを拒否",
|
||||
"hideDetails": "ツールの詳細を非表示",
|
||||
"runWithOptions": "追加オプションで実行",
|
||||
"showDetails": "ツールの詳細を表示"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "キャンセル",
|
||||
"run": "走る"
|
||||
},
|
||||
"confirmation": "このClaudeツールを実行してもよろしいですか?",
|
||||
"defaultDenyMessage": "ユーザーはこのツールの使用を拒否しました。",
|
||||
"defaultDescription": "環境内でコードまたはシステムアクションを実行します。実行前にコマンドが安全であることを確認してください。",
|
||||
"error": {
|
||||
"sendFailed": "決定の送信に失敗しました。もう一度お試しください。"
|
||||
},
|
||||
"expired": "期限切れ",
|
||||
"inputPreview": "ツール入力プレビュー",
|
||||
"pending": "保留中({{seconds}}秒)",
|
||||
"permissionExpired": "許可リクエストの期限が切れました。新しい指示を待っています...",
|
||||
"requiresElevatedPermissions": "このツールは昇格した権限が必要です。",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "承認すると、このツールを常に許可することを選択した場合、複数のセッション権限が更新されることがあります。",
|
||||
"permissionUpdateSingle": "承認すると、このツールを常に許可することを選択した場合、セッションの権限が更新されることがあります。"
|
||||
},
|
||||
"toast": {
|
||||
"denied": "ツールリクエストは拒否されました。",
|
||||
"timeout": "ツールリクエストは承認を受ける前にタイムアウトしました。"
|
||||
},
|
||||
"waiting": "ツールの許可決定を待っています..."
|
||||
},
|
||||
"type": {
|
||||
"label": "エージェントタイプ",
|
||||
"unknown": "不明なタイプ"
|
||||
@@ -2299,6 +2376,32 @@
|
||||
"seed_tip": "拡大結果のランダム性を制御します"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "操作",
|
||||
"agents": "代理",
|
||||
"all_categories": "すべてのカテゴリー",
|
||||
"all_types": "全部",
|
||||
"category": "カテゴリー",
|
||||
"commands": "命令",
|
||||
"confirm_uninstall": "{{name}}をアンインストールしてもよろしいですか?",
|
||||
"install": "インストール",
|
||||
"install_plugins_from_browser": "利用可能なプラグインを閲覧して、使用を開始してください",
|
||||
"installing": "インストール中...",
|
||||
"name": "名称",
|
||||
"no_description": "説明なし",
|
||||
"no_installed_plugins": "まだプラグインがインストールされていません",
|
||||
"no_results": "プラグインが見つかりません",
|
||||
"search_placeholder": "検索プラグイン...",
|
||||
"showing_results": "{{count}} 個のプラグインを表示",
|
||||
"showing_results_one": "{{count}} 個のプラグインを表示",
|
||||
"showing_results_other": "{{count}} 個のプラグインを表示",
|
||||
"showing_results_plural": "{{count}} 個のプラグインを表示",
|
||||
"skills": "スキル",
|
||||
"try_different_search": "検索またはカテゴリフィルターを調整してみてください",
|
||||
"type": "タイプ",
|
||||
"uninstall": "アンインストール",
|
||||
"uninstalling": "アンインストール中..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "画像としてコピー"
|
||||
@@ -4045,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic APIアドレス",
|
||||
"anthropic_api_host_preview": "Anthropic プレビュー:{{url}}",
|
||||
"anthropic_api_host_tip": "サービスプロバイダーがAnthropic互換のアドレスを提供する場合のみ入力してください。/で終わる場合は自動追加されるv1を無視し、#で終わる場合は元のアドレスを強制的に使用します。",
|
||||
"anthropic_api_host_tooltip": "サービスプロバイダーがClaude互換のベースアドレスを提供する場合のみ入力してください。",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4090,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "プレビュー: {{url}}",
|
||||
"reset": "リセット",
|
||||
"tip": "/で終わる場合、v1を無視します。#で終わる場合、入力されたアドレスを強制的に使用します"
|
||||
"tip": "#で終わる場合、入力されたアドレスを強制的に使用します"
|
||||
}
|
||||
},
|
||||
"api_host": "APIホスト",
|
||||
"api_host_no_valid": "APIアドレスが無効です",
|
||||
"api_host_preview": "プレビュー:{{url}}",
|
||||
"api_host_tooltip": "サービスプロバイダーがカスタムOpenAI互換アドレスを必要とする場合のみ上書きしてください。",
|
||||
"api_key": {
|
||||
@@ -4512,6 +4615,7 @@
|
||||
"paintings": "ペインティング",
|
||||
"settings": "設定",
|
||||
"store": "アシスタントライブラリ",
|
||||
"terminal": "ターミナル",
|
||||
"translate": "翻訳"
|
||||
},
|
||||
"trace": {
|
||||
|
||||
@@ -107,6 +107,50 @@
|
||||
"title": "Configurações avançadas"
|
||||
},
|
||||
"essential": "Configurações Essenciais",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Plugins disponíveis"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "Tem certeza de que deseja desinstalar este plugin?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "Nenhum plugin correspondente encontrado. Tente ajustar a pesquisa ou os filtros de categoria."
|
||||
},
|
||||
"error": {
|
||||
"install": "Falha na instalação do plugin",
|
||||
"load": "Falha ao carregar o plugin",
|
||||
"uninstall": "Falha ao desinstalar o plug-in"
|
||||
},
|
||||
"filter": {
|
||||
"all": "Todas as categorias"
|
||||
},
|
||||
"install": "Instalação",
|
||||
"installed": {
|
||||
"empty": "Nenhum plugin foi instalado ainda. Explore os plugins disponíveis para começar.",
|
||||
"title": "Plugin instalado"
|
||||
},
|
||||
"installing": "Instalando...",
|
||||
"results": "Encontrados {{count}} plugins",
|
||||
"search": {
|
||||
"placeholder": "Pesquisar extensão..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Plugin instalado com sucesso",
|
||||
"uninstall": "插件 desinstalado com sucesso"
|
||||
},
|
||||
"tab": "plug-in",
|
||||
"type": {
|
||||
"agent": "agente",
|
||||
"agents": "agente",
|
||||
"all": "tudo",
|
||||
"command": "comando",
|
||||
"commands": "comando",
|
||||
"skills": "habilidade"
|
||||
},
|
||||
"uninstall": "desinstalar",
|
||||
"uninstalling": "Desinstalando..."
|
||||
},
|
||||
"prompt": "Configurações de Prompt",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -191,6 +235,39 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Permitir solicitação de ferramenta",
|
||||
"denyRequest": "Negar solicitação de ferramenta",
|
||||
"hideDetails": "Ocultar detalhes da ferramenta",
|
||||
"runWithOptions": "Executar com opções adicionais",
|
||||
"showDetails": "Mostrar detalhes da ferramenta"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Cancelar",
|
||||
"run": "Correr"
|
||||
},
|
||||
"confirmation": "Tem certeza de que quer executar esta ferramenta Claude?",
|
||||
"defaultDenyMessage": "Usuário negou permissão para esta ferramenta.",
|
||||
"defaultDescription": "Executa código ou ações do sistema no seu ambiente. Certifique-se de que o comando parece seguro antes de executá-lo.",
|
||||
"error": {
|
||||
"sendFailed": "Falha ao enviar sua decisão. Por favor, tente novamente."
|
||||
},
|
||||
"expired": "Expirado",
|
||||
"inputPreview": "Pré-visualização da entrada da ferramenta",
|
||||
"pending": "Pendente ({{seconds}}s)",
|
||||
"permissionExpired": "Solicitação de permissão expirou. Aguardando novas instruções...",
|
||||
"requiresElevatedPermissions": "Esta ferramenta requer permissões elevadas.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Aprovar pode atualizar várias permissões de sessão se você escolheu sempre permitir esta ferramenta.",
|
||||
"permissionUpdateSingle": "Aprovar pode atualizar as permissões da sua sessão se você escolheu sempre permitir esta ferramenta."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "Solicitação de ferramenta foi negada.",
|
||||
"timeout": "A solicitação da ferramenta expirou antes de receber aprovação."
|
||||
},
|
||||
"waiting": "Aguardando decisão de permissão da ferramenta..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipo de Agente",
|
||||
"unknown": "Tipo Desconhecido"
|
||||
@@ -2299,6 +2376,32 @@
|
||||
"seed_tip": "Controla a aleatoriedade do resultado de ampliação"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Operação",
|
||||
"agents": "agente",
|
||||
"all_categories": "Todas as categorias",
|
||||
"all_types": "Tudo",
|
||||
"category": "categoria",
|
||||
"commands": "comando",
|
||||
"confirm_uninstall": "Tem certeza de que deseja desinstalar {{name}}?",
|
||||
"install": "Instalação",
|
||||
"install_plugins_from_browser": "Navegue pelos plugins disponíveis para começar a usar",
|
||||
"installing": "Instalando...",
|
||||
"name": "Nome",
|
||||
"no_description": "Sem descrição",
|
||||
"no_installed_plugins": "Nenhum plugin foi instalado ainda",
|
||||
"no_results": "Plugin não encontrado",
|
||||
"search_placeholder": "Pesquisar plugin...",
|
||||
"showing_results": "Exibir {{count}} extensões",
|
||||
"showing_results_one": "Mostrar {{count}} extensões",
|
||||
"showing_results_other": "Exibir {{count}} extensões",
|
||||
"showing_results_plural": "Exibir {{count}} extensões",
|
||||
"skills": "habilidade",
|
||||
"try_different_search": "Por favor, tente ajustar a pesquisa ou os filtros de categoria.",
|
||||
"type": "tipo",
|
||||
"uninstall": "Desinstalar",
|
||||
"uninstalling": "Desinstalando..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Copiar como imagem"
|
||||
@@ -4045,7 +4148,6 @@
|
||||
},
|
||||
"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": {
|
||||
@@ -4090,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Pré-visualização: {{url}}",
|
||||
"reset": "Redefinir",
|
||||
"tip": "Ignorar v1 na versão finalizada com /, usar endereço de entrada forçado se terminar com #"
|
||||
"tip": "e forçar o uso do endereço original quando terminar com '#'"
|
||||
}
|
||||
},
|
||||
"api_host": "Endereço API",
|
||||
"api_host_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": {
|
||||
@@ -4512,6 +4615,7 @@
|
||||
"paintings": "Pinturas",
|
||||
"settings": "Configurações",
|
||||
"store": "Biblioteca de assistentes",
|
||||
"terminal": "Terminal",
|
||||
"translate": "Traduzir"
|
||||
},
|
||||
"trace": {
|
||||
|
||||
@@ -107,6 +107,50 @@
|
||||
"title": "Расширенные настройки"
|
||||
},
|
||||
"essential": "Основные настройки",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Доступные плагины"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "Вы уверены, что хотите удалить этот плагин?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "Совпадающие плагины не найдены. Попробуйте изменить поиск или фильтр категорий."
|
||||
},
|
||||
"error": {
|
||||
"install": "Ошибка установки плагина",
|
||||
"load": "Ошибка загрузки плагина",
|
||||
"uninstall": "Не удалось удалить плагин"
|
||||
},
|
||||
"filter": {
|
||||
"all": "Все категории"
|
||||
},
|
||||
"install": "установка",
|
||||
"installed": {
|
||||
"empty": "Плагины ещё не установлены. Просмотрите доступные плагины, чтобы начать.",
|
||||
"title": "Установленный плагин"
|
||||
},
|
||||
"installing": "Установка...",
|
||||
"results": "Найдено {{count}} плагинов",
|
||||
"search": {
|
||||
"placeholder": "Поиск плагинов..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Плагин успешно установлен",
|
||||
"uninstall": "Плагин успешно удалён"
|
||||
},
|
||||
"tab": "плагин",
|
||||
"type": {
|
||||
"agent": "агент",
|
||||
"agents": "Прокси",
|
||||
"all": "всё",
|
||||
"command": "команда",
|
||||
"commands": "команда",
|
||||
"skills": "навык"
|
||||
},
|
||||
"uninstall": "Удаление",
|
||||
"uninstalling": "Удаление..."
|
||||
},
|
||||
"prompt": "Настройки подсказки",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -191,6 +235,39 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Разрешить запрос инструмента",
|
||||
"denyRequest": "Отклонить запрос на инструмент",
|
||||
"hideDetails": "Скрыть сведения об инструменте",
|
||||
"runWithOptions": "Запустить с дополнительными параметрами",
|
||||
"showDetails": "Показать сведения об инструменте"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Отмена",
|
||||
"run": "Беги"
|
||||
},
|
||||
"confirmation": "Вы уверены, что хотите запустить этот инструмент Claude?",
|
||||
"defaultDenyMessage": "Пользователь отказал в разрешении на использование этого инструмента.",
|
||||
"defaultDescription": "Выполняет код или системные действия в вашей среде. Убедитесь, что команда выглядит безопасно, прежде чем запускать её.",
|
||||
"error": {
|
||||
"sendFailed": "Не удалось отправить ваше решение. Попробуйте ещё раз."
|
||||
},
|
||||
"expired": "Истёк",
|
||||
"inputPreview": "Предварительный просмотр ввода инструмента",
|
||||
"pending": "Ожидание ({{seconds}}с)",
|
||||
"permissionExpired": "Срок действия запроса на разрешение истёк. Ожидание новых инструкций...",
|
||||
"requiresElevatedPermissions": "Этому инструменту требуются повышенные разрешения.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Одобрение может обновить разрешения для нескольких сеансов, если вы выбрали всегда разрешать использование этого инструмента.",
|
||||
"permissionUpdateSingle": "Одобрение может обновить разрешения вашей сессии, если вы выбрали всегда разрешать использование этого инструмента."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "Запрос на инструмент был отклонён.",
|
||||
"timeout": "Запрос на инструмент превысил время ожидания до получения подтверждения."
|
||||
},
|
||||
"waiting": "Ожидание решения о разрешении на использование инструмента..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Тип агента",
|
||||
"unknown": "Неизвестный тип"
|
||||
@@ -2299,6 +2376,32 @@
|
||||
"seed_tip": "Контролирует случайный характер увеличения изображений для воспроизводимых результатов"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Операция",
|
||||
"agents": "агент",
|
||||
"all_categories": "Все категории",
|
||||
"all_types": "всё",
|
||||
"category": "категория",
|
||||
"commands": "команда",
|
||||
"confirm_uninstall": "Вы уверены, что хотите удалить {{name}}?",
|
||||
"install": "установка",
|
||||
"install_plugins_from_browser": "Просмотрите доступные плагины, чтобы начать работу",
|
||||
"installing": "Установка...",
|
||||
"name": "название",
|
||||
"no_description": "Без описания",
|
||||
"no_installed_plugins": "Плагины ещё не установлены",
|
||||
"no_results": "Плагин не найден",
|
||||
"search_placeholder": "Поиск плагинов...",
|
||||
"showing_results": "Отображено {{count}} плагинов",
|
||||
"showing_results_one": "Отображено {{count}} плагинов",
|
||||
"showing_results_other": "Отображено {{count}} плагинов",
|
||||
"showing_results_plural": "Отображение {{count}} плагинов",
|
||||
"skills": "навык",
|
||||
"try_different_search": "Пожалуйста, попробуйте изменить поиск или фильтры категорий",
|
||||
"type": "тип",
|
||||
"uninstall": "Удаление",
|
||||
"uninstalling": "Удаление..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Скопировать как изображение"
|
||||
@@ -4045,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Адрес API Anthropic",
|
||||
"anthropic_api_host_preview": "Предпросмотр Anthropic: {{url}}",
|
||||
"anthropic_api_host_tip": "Заполняйте только если провайдер предоставляет совместимый с Anthropic адрес. Окончание на / игнорирует автоматически добавляемое v1, окончание на # принудительно использует оригинальный адрес.",
|
||||
"anthropic_api_host_tooltip": "Заполняйте только когда провайдер предоставляет базовый адрес, совместимый с Claude.",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4090,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Предпросмотр: {{url}}",
|
||||
"reset": "Сброс",
|
||||
"tip": "Заканчивая на / игнорирует v1, заканчивая на # принудительно использует введенный адрес"
|
||||
"tip": "заканчивая на # принудительно использует введенный адрес"
|
||||
}
|
||||
},
|
||||
"api_host": "Хост API",
|
||||
"api_host_no_valid": "Недопустимый адрес API",
|
||||
"api_host_preview": "Предпросмотр: {{url}}",
|
||||
"api_host_tooltip": "Переопределяйте только когда провайдер требует пользовательский адрес, совместимый с OpenAI.",
|
||||
"api_key": {
|
||||
@@ -4512,6 +4615,7 @@
|
||||
"paintings": "Рисунки",
|
||||
"settings": "Настройки",
|
||||
"store": "Библиотека помощников",
|
||||
"terminal": "Терминал",
|
||||
"translate": "Перевод"
|
||||
},
|
||||
"trace": {
|
||||
|
||||
@@ -72,7 +72,7 @@ export const getCodeToolsApiBaseUrl = (model: Model, type: EndpointType) => {
|
||||
},
|
||||
dashscope: {
|
||||
anthropic: {
|
||||
api_base_url: 'https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy'
|
||||
api_base_url: 'https://dashscope.aliyuncs.com/apps/anthropic'
|
||||
}
|
||||
},
|
||||
modelscope: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ContentSearch } from '@renderer/components/ContentSearch'
|
||||
import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { QuickPanelProvider } from '@renderer/components/QuickPanel'
|
||||
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { useNavbarPosition } from '@renderer/hooks/useNavbar'
|
||||
@@ -56,6 +57,8 @@ const Chat: FC<Props> = (props) => {
|
||||
const { chat } = useRuntime()
|
||||
const { activeTopicOrSession, activeAgentId, activeSessionIdMap } = chat
|
||||
const activeSessionId = activeAgentId ? activeSessionIdMap[activeAgentId] : null
|
||||
const sessionAgentId = activeTopicOrSession === 'session' ? activeAgentId : null
|
||||
const { createDefaultSession } = useCreateDefaultSession(sessionAgentId)
|
||||
|
||||
const mainRef = React.useRef<HTMLDivElement>(null)
|
||||
const contentSearchRef = React.useRef<ContentSearchRef>(null)
|
||||
@@ -94,6 +97,21 @@ const Chat: FC<Props> = (props) => {
|
||||
}
|
||||
})
|
||||
|
||||
useShortcut(
|
||||
'new_topic',
|
||||
() => {
|
||||
if (activeTopicOrSession !== 'session' || !activeAgentId) {
|
||||
return
|
||||
}
|
||||
void createDefaultSession()
|
||||
},
|
||||
{
|
||||
enabled: activeTopicOrSession === 'session',
|
||||
preventDefault: true,
|
||||
enableOnFormTags: true
|
||||
}
|
||||
)
|
||||
|
||||
const contentSearchFilter: NodeFilter = {
|
||||
acceptNode(node) {
|
||||
const container = node.parentElement?.closest('.message-content-container')
|
||||
|
||||
@@ -2,11 +2,12 @@ import { Tooltip } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { ActionIconButton } from '@renderer/components/Buttons'
|
||||
import { QuickPanelView } from '@renderer/components/QuickPanel'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
|
||||
import { useSession } from '@renderer/hooks/agents/useSession'
|
||||
import { selectNewTopicLoading } from '@renderer/hooks/useMessageOperations'
|
||||
import { getModel } from '@renderer/hooks/useModel'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import PasteService from '@renderer/services/PasteService'
|
||||
import { pauseTrace } from '@renderer/services/SpanManagerService'
|
||||
@@ -22,7 +23,7 @@ import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/
|
||||
import { createMainTextBlock, createMessage } from '@renderer/utils/messageUtils/create'
|
||||
import TextArea, { type TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { CirclePause } from 'lucide-react'
|
||||
import { CirclePause, MessageSquareDiff } from 'lucide-react'
|
||||
import type { CSSProperties, FC } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -45,8 +46,9 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
const [text, setText] = useState(_text)
|
||||
const [inputFocus, setInputFocus] = useState(false)
|
||||
const { session } = useSession(agentId, sessionId)
|
||||
const { agent } = useAgent(agentId)
|
||||
const { apiServer } = useSettings()
|
||||
const { createDefaultSession, creatingSession } = useCreateDefaultSession(agentId)
|
||||
const newTopicShortcut = useShortcutDisplay('new_topic')
|
||||
|
||||
const { sendMessageShortcut, fontSize, enableSpellCheck } = useSettings()
|
||||
const textareaRef = useRef<TextAreaRef>(null)
|
||||
@@ -88,6 +90,22 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
}, [topicMessages])
|
||||
|
||||
const canAbort = loading && streamingAskIds.length > 0
|
||||
const createSessionDisabled = creatingSession || !apiServer.enabled
|
||||
|
||||
const handleCreateSession = useCallback(async () => {
|
||||
if (createSessionDisabled) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const created = await createDefaultSession()
|
||||
if (created) {
|
||||
focusTextarea()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Failed to create agent session via toolbar:', error as Error)
|
||||
}
|
||||
}, [createDefaultSession, createSessionDisabled, focusTextarea])
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
//to check if the SendMessage key is pressed
|
||||
@@ -165,7 +183,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
const userMessageBlocks: MessageBlock[] = [mainBlock]
|
||||
|
||||
// Extract the actual model ID from session.model (format: "provider:modelId")
|
||||
const [providerId, actualModelId] = agent?.model.split(':') ?? [undefined, undefined]
|
||||
const [providerId, actualModelId] = session?.model?.split(':') ?? [undefined, undefined]
|
||||
|
||||
// Try to find the actual model from providers
|
||||
const actualModel = actualModelId ? getModel(actualModelId, providerId) : undefined
|
||||
@@ -211,7 +229,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
logger.warn('Failed to send message:', error as Error)
|
||||
}
|
||||
}, [
|
||||
agent?.model,
|
||||
session?.model,
|
||||
agentId,
|
||||
dispatch,
|
||||
sendDisabled,
|
||||
@@ -287,8 +305,16 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
}}
|
||||
onBlur={() => setInputFocus(false)}
|
||||
/>
|
||||
<div className="flex justify-end px-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<Toolbar>
|
||||
<ToolbarGroup>
|
||||
<Tooltip placement="top" content={t('chat.input.new_topic', { Command: newTopicShortcut })} delay={0}>
|
||||
<ActionIconButton
|
||||
onClick={handleCreateSession}
|
||||
disabled={createSessionDisabled}
|
||||
icon={<MessageSquareDiff size={19} />}></ActionIconButton>
|
||||
</Tooltip>
|
||||
</ToolbarGroup>
|
||||
<ToolbarGroup>
|
||||
<SendMessageButton sendMessage={sendMessage} disabled={sendDisabled} />
|
||||
{canAbort && (
|
||||
<Tooltip placement="top" content={t('chat.input.pause')}>
|
||||
@@ -299,8 +325,8 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ToolbarGroup>
|
||||
</Toolbar>
|
||||
</InputBarContainer>
|
||||
</Container>
|
||||
</NarrowLayout>
|
||||
@@ -346,6 +372,25 @@ const InputBarContainer = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const Toolbar = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 5px 8px;
|
||||
height: 40px;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const ToolbarGroup = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
`
|
||||
|
||||
const TextareaStyle: CSSProperties = {
|
||||
paddingLeft: 0,
|
||||
padding: '6px 15px 0px' // 减小顶部padding
|
||||
|
||||
@@ -5,6 +5,7 @@ import { loggerService } from '@logger'
|
||||
import { ActionIconButton } from '@renderer/components/Buttons'
|
||||
import type { QuickPanelListItem } from '@renderer/components/QuickPanel'
|
||||
import {
|
||||
isAnthropicModel,
|
||||
isGeminiModel,
|
||||
isGenerateImageModel,
|
||||
isMandatoryWebSearchModel,
|
||||
@@ -391,7 +392,7 @@ const InputbarTools = ({
|
||||
label: t('chat.input.url_context'),
|
||||
component: <UrlContextButton ref={urlContextButtonRef} assistantId={assistant.id} />,
|
||||
condition:
|
||||
isGeminiModel(model) &&
|
||||
(isGeminiModel(model) || isAnthropicModel(model)) &&
|
||||
(isSupportUrlContextProvider(getProviderByModel(model)) || model.endpoint_type === 'gemini')
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { NormalToolResponse } from '@renderer/types'
|
||||
export * from './types'
|
||||
|
||||
// 导入所有渲染器
|
||||
import ToolPermissionRequestCard from '../ToolPermissionRequestCard'
|
||||
import { BashOutputTool } from './BashOutputTool'
|
||||
import { BashTool } from './BashTool'
|
||||
import { EditTool } from './EditTool'
|
||||
@@ -78,12 +79,16 @@ function renderToolContent(toolName: AgentToolsType, input: ToolInput, output?:
|
||||
|
||||
// 统一的组件渲染入口
|
||||
export function MessageAgentTools({ toolResponse }: { toolResponse: NormalToolResponse }) {
|
||||
const { arguments: args, response, tool } = toolResponse
|
||||
const { arguments: args, response, tool, status } = toolResponse
|
||||
logger.info('Rendering agent tool response', {
|
||||
tool: tool,
|
||||
arguments: args,
|
||||
response
|
||||
})
|
||||
|
||||
if (status === 'pending') {
|
||||
return <ToolPermissionRequestCard toolResponse={toolResponse} />
|
||||
}
|
||||
|
||||
return renderToolContent(tool.name as AgentToolsType, args as ToolInput, response as ToolOutput)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { Button, ButtonGroup, Chip, ScrollShadow } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { selectPendingPermissionByToolName, toolPermissionsActions } from '@renderer/store/toolPermissions'
|
||||
import type { NormalToolResponse } from '@renderer/types'
|
||||
import { ChevronDown, CirclePlay, CircleX } from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const logger = loggerService.withContext('ToolPermissionRequestCard')
|
||||
|
||||
interface Props {
|
||||
toolResponse: NormalToolResponse
|
||||
}
|
||||
|
||||
export function ToolPermissionRequestCard({ toolResponse }: Props) {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const request = useAppSelector((state) =>
|
||||
selectPendingPermissionByToolName(state.toolPermissions, toolResponse.tool.name)
|
||||
)
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!request) return
|
||||
|
||||
logger.debug('Rendering inline tool permission card', {
|
||||
requestId: request.requestId,
|
||||
toolName: request.toolName,
|
||||
expiresAt: request.expiresAt
|
||||
})
|
||||
|
||||
setNow(Date.now())
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
setNow(Date.now())
|
||||
}, 500)
|
||||
|
||||
return () => {
|
||||
window.clearInterval(interval)
|
||||
}
|
||||
}, [request])
|
||||
|
||||
const remainingMs = useMemo(() => {
|
||||
if (!request) return 0
|
||||
return Math.max(0, request.expiresAt - now)
|
||||
}, [request, now])
|
||||
|
||||
const remainingSeconds = useMemo(() => Math.ceil(remainingMs / 1000), [remainingMs])
|
||||
const isExpired = remainingMs <= 0
|
||||
|
||||
const isSubmittingAllow = request?.status === 'submitting-allow'
|
||||
const isSubmittingDeny = request?.status === 'submitting-deny'
|
||||
const isSubmitting = isSubmittingAllow || isSubmittingDeny
|
||||
const hasSuggestions = (request?.suggestions?.length ?? 0) > 0
|
||||
|
||||
const handleDecision = useCallback(
|
||||
async (
|
||||
behavior: 'allow' | 'deny',
|
||||
extra?: {
|
||||
updatedInput?: Record<string, unknown>
|
||||
updatedPermissions?: PermissionUpdate[]
|
||||
message?: string
|
||||
}
|
||||
) => {
|
||||
if (!request) return
|
||||
|
||||
logger.debug('Submitting inline tool permission decision', {
|
||||
requestId: request.requestId,
|
||||
toolName: request.toolName,
|
||||
behavior
|
||||
})
|
||||
|
||||
dispatch(toolPermissionsActions.submissionSent({ requestId: request.requestId, behavior }))
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
requestId: request.requestId,
|
||||
behavior,
|
||||
...(behavior === 'allow'
|
||||
? {
|
||||
updatedInput: extra?.updatedInput ?? request.input,
|
||||
updatedPermissions: extra?.updatedPermissions
|
||||
}
|
||||
: {
|
||||
message: extra?.message ?? t('agent.toolPermission.defaultDenyMessage')
|
||||
})
|
||||
}
|
||||
|
||||
const response = await window.api.agentTools.respondToPermission(payload)
|
||||
|
||||
if (!response?.success) {
|
||||
throw new Error('Renderer response rejected by main process')
|
||||
}
|
||||
|
||||
logger.debug('Tool permission decision acknowledged by main process', {
|
||||
requestId: request.requestId,
|
||||
behavior
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Failed to send tool permission response', error as Error)
|
||||
window.toast?.error?.(t('agent.toolPermission.error.sendFailed'))
|
||||
dispatch(toolPermissionsActions.submissionFailed({ requestId: request.requestId }))
|
||||
}
|
||||
},
|
||||
[dispatch, request, t]
|
||||
)
|
||||
|
||||
if (!request) {
|
||||
return (
|
||||
<div className="rounded-xl border border-default-200 bg-default-100 px-4 py-3 text-default-500 text-sm">
|
||||
{t('agent.toolPermission.waiting')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-xl rounded-xl border border-default-200 bg-default-100 px-4 py-3 shadow-sm">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="font-semibold text-default-700 text-sm">{request.toolName}</div>
|
||||
<div className="text-default-500 text-xs">
|
||||
{request.description?.trim() || t('agent.toolPermission.defaultDescription')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<Chip color={isExpired ? 'danger' : 'warning'} size="sm" variant="flat">
|
||||
{isExpired
|
||||
? t('agent.toolPermission.expired')
|
||||
: t('agent.toolPermission.pending', { seconds: remainingSeconds })}
|
||||
</Chip>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
aria-label={t('agent.toolPermission.aria.denyRequest')}
|
||||
className="h-8"
|
||||
color="danger"
|
||||
isDisabled={isSubmitting || isExpired}
|
||||
isLoading={isSubmittingDeny}
|
||||
onPress={() => handleDecision('deny')}
|
||||
startContent={<CircleX size={16} />}
|
||||
variant="bordered">
|
||||
{t('agent.toolPermission.button.cancel')}
|
||||
</Button>
|
||||
|
||||
{hasSuggestions ? (
|
||||
<ButtonGroup className="h-8">
|
||||
<Button
|
||||
className="h-8 px-3"
|
||||
color="success"
|
||||
isDisabled={isSubmitting || isExpired}
|
||||
isLoading={isSubmittingAllow}
|
||||
onPress={() => handleDecision('allow')}
|
||||
startContent={<CirclePlay size={16} />}>
|
||||
{t('agent.toolPermission.button.run')}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('agent.toolPermission.aria.runWithOptions')}
|
||||
className="h-8 rounded-l-none"
|
||||
color="success"
|
||||
isDisabled={isSubmitting || isExpired}
|
||||
isIconOnly
|
||||
variant="solid"></Button>
|
||||
</ButtonGroup>
|
||||
) : (
|
||||
<Button
|
||||
aria-label={t('agent.toolPermission.aria.allowRequest')}
|
||||
className="h-8 px-3"
|
||||
color="success"
|
||||
isDisabled={isSubmitting || isExpired}
|
||||
isLoading={isSubmittingAllow}
|
||||
onPress={() => handleDecision('allow')}
|
||||
startContent={<CirclePlay size={16} />}>
|
||||
{t('agent.toolPermission.button.run')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
aria-label={
|
||||
showDetails ? t('agent.toolPermission.aria.hideDetails') : t('agent.toolPermission.aria.showDetails')
|
||||
}
|
||||
className="h-8"
|
||||
isIconOnly
|
||||
onPress={() => setShowDetails((value) => !value)}
|
||||
variant="light">
|
||||
<ChevronDown className={`transition-transform ${showDetails ? 'rotate-180' : ''}`} size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showDetails && (
|
||||
<div className="flex flex-col gap-3 border-default-200 border-t pt-3">
|
||||
<div className="rounded-lg bg-default-200/60 px-3 py-2 text-default-600 text-sm">
|
||||
{t('agent.toolPermission.confirmation')}
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-default-200 bg-default-100 p-3">
|
||||
<p className="mb-2 font-medium text-default-400 text-xs uppercase tracking-wide">
|
||||
{t('agent.toolPermission.inputPreview')}
|
||||
</p>
|
||||
<ScrollShadow className="max-h-48 font-mono text-xs" hideScrollBar>
|
||||
<pre className="whitespace-pre-wrap break-all text-left">{request.inputPreview}</pre>
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
|
||||
{request.requiresPermissions && (
|
||||
<div className="rounded-md border border-warning-300 bg-warning-50 p-3 text-warning-700 text-xs">
|
||||
{t('agent.toolPermission.requiresElevatedPermissions')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{request.suggestions.length > 0 && (
|
||||
<div className="rounded-md border border-default-200 bg-default-50 p-3 text-default-500 text-xs">
|
||||
{request.suggestions.length === 1
|
||||
? t('agent.toolPermission.suggestion.permissionUpdateSingle')
|
||||
: t('agent.toolPermission.suggestion.permissionUpdateMultiple')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpired && !isSubmitting && (
|
||||
<div className="text-center text-danger-500 text-xs">{t('agent.toolPermission.permissionExpired')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToolPermissionRequestCard
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Alert, Spinner } from '@heroui/react'
|
||||
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
setActiveTopicOrSessionAction,
|
||||
setSessionWaitingAction
|
||||
} from '@renderer/store/runtime'
|
||||
import type { CreateSessionForm } from '@renderer/types'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { motion } from 'framer-motion'
|
||||
import { memo, useCallback, useEffect } from 'react'
|
||||
@@ -27,11 +26,11 @@ interface SessionsProps {
|
||||
|
||||
const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
const { t } = useTranslation()
|
||||
const { agent } = useAgent(agentId)
|
||||
const { sessions, isLoading, error, deleteSession, createSession } = useSessions(agentId)
|
||||
const { sessions, isLoading, error, deleteSession } = useSessions(agentId)
|
||||
const { chat } = useRuntime()
|
||||
const { activeSessionIdMap } = chat
|
||||
const dispatch = useAppDispatch()
|
||||
const { createDefaultSession, creatingSession } = useCreateDefaultSession(agentId)
|
||||
|
||||
const setActiveSessionId = useCallback(
|
||||
(agentId: string, sessionId: string | null) => {
|
||||
@@ -41,19 +40,6 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const handleCreateSession = useCallback(async () => {
|
||||
if (!agent) return
|
||||
const session = {
|
||||
...agent,
|
||||
id: undefined,
|
||||
name: t('common.unnamed')
|
||||
} satisfies CreateSessionForm
|
||||
const created = await createSession(session)
|
||||
if (created) {
|
||||
dispatch(setActiveSessionIdAction({ agentId, sessionId: created.id }))
|
||||
}
|
||||
}, [agent, agentId, createSession, dispatch, t])
|
||||
|
||||
const handleDeleteSession = useCallback(
|
||||
async (id: string) => {
|
||||
if (sessions.length === 1) {
|
||||
@@ -110,7 +96,7 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
|
||||
return (
|
||||
<div className="sessions-tab flex h-full w-full flex-col p-2">
|
||||
<AddButton onClick={handleCreateSession} className="mb-2">
|
||||
<AddButton onClick={createDefaultSession} className="mb-2" disabled={creatingSession}>
|
||||
{t('agent.session.add.title')}
|
||||
</AddButton>
|
||||
{/* h-9 */}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import App from '@renderer/components/MinApp/MinApp'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { Code, FileSearch, Folder, Languages, LayoutGrid, NotepadText, Palette, Sparkle } from 'lucide-react'
|
||||
import { Code, FileSearch, Folder, Languages, LayoutGrid, NotepadText, Palette, Sparkle, Terminal } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -57,6 +57,12 @@ const LaunchpadPage: FC = () => {
|
||||
path: '/code',
|
||||
bgColor: 'linear-gradient(135deg, #1F2937, #374151)' // Code CLI:高级暗黑色,代表专业和技术
|
||||
},
|
||||
{
|
||||
icon: <Terminal size={32} className="icon" />,
|
||||
text: t('title.terminal'),
|
||||
path: '/terminal',
|
||||
bgColor: 'linear-gradient(135deg, #000000, #1F2937)' // Terminal:纯黑渐变,代表命令行界面
|
||||
},
|
||||
{
|
||||
icon: <NotepadText size={32} className="icon" />,
|
||||
text: t('title.notes'),
|
||||
|
||||
@@ -120,7 +120,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
||||
return
|
||||
}
|
||||
try {
|
||||
target.findInPage(text, options)
|
||||
target.findInPage(text, options || {})
|
||||
} catch (error) {
|
||||
logger.error('findInPage failed', { error })
|
||||
window.toast?.error(t('common.error'))
|
||||
|
||||
@@ -19,6 +19,15 @@ vi.mock('react-i18next', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
// mock @cherrystudio/ui Button component to handle onClick
|
||||
vi.mock('@cherrystudio/ui', () => ({
|
||||
Button: ({ children, onClick, disabled, ...props }: any) => (
|
||||
<button type="button" onClick={onClick} disabled={disabled} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}))
|
||||
|
||||
const createWebviewMock = () => {
|
||||
const listeners = new Map<string, Set<(event: Event & { result?: Electron.FoundInPageResult }) => void>>()
|
||||
const findInPageMock = vi.fn()
|
||||
@@ -255,7 +264,7 @@ describe('WebviewSearch', () => {
|
||||
await user.type(input, 'Cherry')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(findInPageMock).toHaveBeenCalledWith('Cherry', undefined)
|
||||
expect(findInPageMock).toHaveBeenCalledWith('Cherry', {})
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
@@ -307,7 +316,7 @@ describe('WebviewSearch', () => {
|
||||
await user.type(input, 'Cherry')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(findInPageMock).toHaveBeenCalledWith('Cherry', undefined)
|
||||
expect(findInPageMock).toHaveBeenCalledWith('Cherry', {})
|
||||
})
|
||||
findInPageMock.mockClear()
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import AdvancedSettings from './AdvancedSettings'
|
||||
import EssentialSettings from './EssentialSettings'
|
||||
import PluginSettings from './PluginSettings'
|
||||
import PromptSettings from './PromptSettings'
|
||||
import { AgentLabel, LeftMenu, Settings, StyledMenu, StyledModal } from './shared'
|
||||
import ToolingSettings from './ToolingSettings'
|
||||
@@ -20,7 +21,7 @@ interface AgentSettingPopupParams extends AgentSettingPopupShowParams {
|
||||
resolve: () => void
|
||||
}
|
||||
|
||||
type AgentSettingPopupTab = 'essential' | 'prompt' | 'tooling' | 'advanced' | 'session-mcps'
|
||||
type AgentSettingPopupTab = 'essential' | 'prompt' | 'tooling' | 'advanced' | 'plugins' | 'session-mcps'
|
||||
|
||||
const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, agentId, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
@@ -56,6 +57,10 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
||||
key: 'tooling',
|
||||
label: t('agent.settings.tooling.tab', 'Tooling & permissions')
|
||||
},
|
||||
{
|
||||
key: 'plugins',
|
||||
label: t('agent.settings.plugins.tab', 'Plugins')
|
||||
},
|
||||
{
|
||||
key: 'advanced',
|
||||
label: t('agent.settings.advance.title', 'Advanced Settings')
|
||||
@@ -75,6 +80,9 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (!agent) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div className="flex w-full flex-1">
|
||||
<LeftMenu>
|
||||
@@ -90,6 +98,7 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
||||
{menu === 'essential' && <EssentialSettings agentBase={agent} update={updateAgent} />}
|
||||
{menu === 'prompt' && <PromptSettings agentBase={agent} update={updateAgent} />}
|
||||
{menu === 'tooling' && <ToolingSettings agentBase={agent} update={updateAgent} />}
|
||||
{menu === 'plugins' && <PluginSettings agentBase={agent} update={updateAgent} />}
|
||||
{menu === 'advanced' && <AdvancedSettings agentBase={agent} update={updateAgent} />}
|
||||
</Settings>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EmojiAvatarWithPicker } from '@renderer/components/Avatar/EmojiAvatarWithPicker'
|
||||
import type { AgentEntity, UpdateAgentForm } from '@renderer/types'
|
||||
import { isAgentType } from '@renderer/types'
|
||||
import { AgentConfigurationSchema, isAgentType } from '@renderer/types'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -20,13 +20,11 @@ export const AvatarSetting: React.FC<AvatarSettingsProps> = ({ agent, update })
|
||||
|
||||
const updateAvatar = useCallback(
|
||||
(avatar: string) => {
|
||||
const parsedConfiguration = AgentConfigurationSchema.parse(agent.configuration ?? {})
|
||||
const payload = {
|
||||
id: agent.id,
|
||||
// hard-encoded default values. better to implement incremental update for configuration
|
||||
configuration: {
|
||||
...agent.configuration,
|
||||
permission_mode: agent.configuration?.permission_mode ?? 'default',
|
||||
max_turns: agent.configuration?.max_turns ?? 100,
|
||||
...parsedConfiguration,
|
||||
avatar
|
||||
}
|
||||
} satisfies UpdateAgentForm
|
||||
|
||||
115
src/renderer/src/pages/settings/AgentSettings/PluginSettings.tsx
Normal file
115
src/renderer/src/pages/settings/AgentSettings/PluginSettings.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Card, CardBody, Tab, Tabs } from '@heroui/react'
|
||||
import { useAvailablePlugins, useInstalledPlugins, usePluginActions } from '@renderer/hooks/usePlugins'
|
||||
import type { GetAgentResponse, GetAgentSessionResponse, UpdateAgentBaseForm } from '@renderer/types/agent'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { InstalledPluginsList } from './components/InstalledPluginsList'
|
||||
import { PluginBrowser } from './components/PluginBrowser'
|
||||
import { SettingsContainer } from './shared'
|
||||
|
||||
interface PluginSettingsProps {
|
||||
agentBase: GetAgentResponse | GetAgentSessionResponse
|
||||
update: (partial: UpdateAgentBaseForm) => Promise<void>
|
||||
}
|
||||
|
||||
const PluginSettings: FC<PluginSettingsProps> = ({ agentBase }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Fetch available plugins
|
||||
const { agents, commands, skills, loading: loadingAvailable, error: errorAvailable } = useAvailablePlugins()
|
||||
|
||||
// Fetch installed plugins
|
||||
const { plugins, loading: loadingInstalled, error: errorInstalled, refresh } = useInstalledPlugins(agentBase.id)
|
||||
|
||||
// Plugin actions
|
||||
const { install, uninstall, installing, uninstalling } = usePluginActions(agentBase.id, refresh)
|
||||
|
||||
// Handle install action
|
||||
const handleInstall = useCallback(
|
||||
async (sourcePath: string, type: 'agent' | 'command' | 'skill') => {
|
||||
const result = await install(sourcePath, type)
|
||||
|
||||
if (result.success) {
|
||||
window.toast.success(t('agent.settings.plugins.success.install'))
|
||||
} else {
|
||||
window.toast.error(t('agent.settings.plugins.error.install') + (result.error ? ': ' + result.error : ''))
|
||||
}
|
||||
},
|
||||
[install, t]
|
||||
)
|
||||
|
||||
// Handle uninstall action
|
||||
const handleUninstall = useCallback(
|
||||
async (filename: string, type: 'agent' | 'command' | 'skill') => {
|
||||
const result = await uninstall(filename, type)
|
||||
|
||||
if (result.success) {
|
||||
window.toast.success(t('agent.settings.plugins.success.uninstall'))
|
||||
} else {
|
||||
window.toast.error(t('agent.settings.plugins.error.uninstall') + (result.error ? ': ' + result.error : ''))
|
||||
}
|
||||
},
|
||||
[uninstall, t]
|
||||
)
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<Tabs
|
||||
aria-label="Plugin settings tabs"
|
||||
classNames={{
|
||||
base: 'w-full',
|
||||
tabList: 'w-full',
|
||||
panel: 'w-full flex-1 overflow-hidden'
|
||||
}}>
|
||||
<Tab key="available" title={t('agent.settings.plugins.available.title')}>
|
||||
<div className="flex h-full flex-col overflow-y-auto pt-4">
|
||||
{errorAvailable ? (
|
||||
<Card className="bg-danger-50 dark:bg-danger-900/20">
|
||||
<CardBody>
|
||||
<p className="text-danger">
|
||||
{t('agent.settings.plugins.error.load')}: {errorAvailable}
|
||||
</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
) : (
|
||||
<PluginBrowser
|
||||
agentId={agentBase.id}
|
||||
agents={agents}
|
||||
commands={commands}
|
||||
skills={skills}
|
||||
installedPlugins={plugins}
|
||||
onInstall={handleInstall}
|
||||
onUninstall={handleUninstall}
|
||||
loading={loadingAvailable || installing || uninstalling}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab key="installed" title={t('agent.settings.plugins.installed.title')}>
|
||||
<div className="flex h-full flex-col overflow-y-auto pt-4">
|
||||
{errorInstalled ? (
|
||||
<Card className="bg-danger-50 dark:bg-danger-900/20">
|
||||
<CardBody>
|
||||
<p className="text-danger">
|
||||
{t('agent.settings.plugins.error.load')}: {errorInstalled}
|
||||
</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
) : (
|
||||
<InstalledPluginsList
|
||||
plugins={plugins}
|
||||
onUninstall={handleUninstall}
|
||||
loading={loadingInstalled || uninstalling}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</SettingsContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default PluginSettings
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Alert, Card, CardBody, CardHeader, Chip, Input, Switch } from '@heroui/react'
|
||||
import { permissionModeCards } from '@renderer/constants/permissionModes'
|
||||
import { useAgentClient } from '@renderer/hooks/agents/useAgentClient'
|
||||
import { permissionModeCards } from '@renderer/config/agent'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||
import type {
|
||||
@@ -17,9 +16,8 @@ import { AgentConfigurationSchema } from '@renderer/types'
|
||||
import { Modal } from 'antd'
|
||||
import { ShieldAlert, ShieldCheck, Wrench } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { startTransition, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { mutate } from 'swr'
|
||||
|
||||
import { SettingsContainer, SettingsItem, SettingsTitle } from './shared'
|
||||
|
||||
@@ -37,6 +35,13 @@ type AgentConfigurationState = AgentConfiguration & Record<string, unknown>
|
||||
|
||||
const defaultConfiguration: AgentConfigurationState = AgentConfigurationSchema.parse({})
|
||||
|
||||
/**
|
||||
* Computes the list of tool IDs that should be automatically approved for a given permission mode.
|
||||
*
|
||||
* @param mode - The permission mode to compute defaults for.
|
||||
* @param tools - The full list of available tools.
|
||||
* @returns An array of tool IDs that are approved by default for the specified mode.
|
||||
*/
|
||||
const computeModeDefaults = (mode: PermissionMode, tools: Tool[]): string[] => {
|
||||
const defaultToolIds = tools.filter((tool) => !tool.requirePermissions).map((tool) => tool.id)
|
||||
switch (mode) {
|
||||
@@ -66,52 +71,34 @@ const unique = (values: string[]) => Array.from(new Set(values))
|
||||
export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, update }) => {
|
||||
const { containerRef, handleScroll } = useScrollPosition('AgentToolingSettings', 100)
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
const { mcpServers: allServers } = useMCPServers()
|
||||
const [modal, contextHolder] = Modal.useModal()
|
||||
|
||||
const [configuration, setConfiguration] = useState<AgentConfigurationState>(defaultConfiguration)
|
||||
const [selectedMode, setSelectedMode] = useState<PermissionMode>(defaultConfiguration.permission_mode)
|
||||
const [autoToolIds, setAutoToolIds] = useState<string[]>([])
|
||||
const [approvedToolIds, setApprovedToolIds] = useState<string[]>([])
|
||||
const configuration: AgentConfigurationState = useMemo(
|
||||
() => agentBase?.configuration ?? defaultConfiguration,
|
||||
[agentBase?.configuration]
|
||||
)
|
||||
const selectedMode = useMemo(
|
||||
() => agentBase?.configuration?.permission_mode ?? defaultConfiguration.permission_mode,
|
||||
[agentBase?.configuration?.permission_mode]
|
||||
)
|
||||
const availableTools = useMemo(() => agentBase?.tools ?? [], [agentBase?.tools])
|
||||
const autoToolIds = useMemo(() => computeModeDefaults(selectedMode, availableTools), [availableTools, selectedMode])
|
||||
const approvedToolIds = useMemo(() => {
|
||||
const allowed = agentBase?.allowed_tools ?? []
|
||||
const sanitized = allowed.filter((id) => availableTools.some((tool) => tool.id === id))
|
||||
// Ensure defaults are included even if backend omitted them
|
||||
const merged = unique([...sanitized, ...autoToolIds])
|
||||
return merged
|
||||
}, [agentBase?.allowed_tools, autoToolIds, availableTools])
|
||||
const selectedMcpIds = useMemo(() => agentBase?.mcps ?? [], [agentBase?.mcps])
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [isUpdatingMode, setIsUpdatingMode] = useState(false)
|
||||
const [isUpdatingTools, setIsUpdatingTools] = useState(false)
|
||||
const [selectedMcpIds, setSelectedMcpIds] = useState<string[]>([])
|
||||
const [isUpdatingMcp, setIsUpdatingMcp] = useState(false)
|
||||
|
||||
const availableTools = useMemo(() => agentBase?.tools ?? [], [agentBase?.tools])
|
||||
const availableServers = useMemo(() => allServers ?? [], [allServers])
|
||||
|
||||
useEffect(() => {
|
||||
if (!agentBase) {
|
||||
setConfiguration(defaultConfiguration)
|
||||
setSelectedMode(defaultConfiguration.permission_mode)
|
||||
setApprovedToolIds([])
|
||||
setAutoToolIds([])
|
||||
setSelectedMcpIds([])
|
||||
return
|
||||
}
|
||||
const parsed: AgentConfigurationState = AgentConfigurationSchema.parse(agentBase.configuration ?? {})
|
||||
setConfiguration(parsed)
|
||||
setSelectedMode(parsed.permission_mode)
|
||||
|
||||
const defaults = computeModeDefaults(parsed.permission_mode, availableTools)
|
||||
setAutoToolIds(defaults)
|
||||
const allowed = agentBase.allowed_tools ?? []
|
||||
setApprovedToolIds((prev) => {
|
||||
const sanitized = allowed.filter((id) => availableTools.some((tool) => tool.id === id))
|
||||
const isSame = sanitized.length === prev.length && sanitized.every((id) => prev.includes(id))
|
||||
if (isSame) {
|
||||
return prev
|
||||
}
|
||||
// Ensure defaults are included even if backend omitted them
|
||||
const merged = unique([...sanitized, ...defaults])
|
||||
return merged
|
||||
})
|
||||
setSelectedMcpIds(agentBase.mcps ?? [])
|
||||
}, [agentBase, availableTools])
|
||||
|
||||
const filteredTools = useMemo(() => {
|
||||
if (!searchTerm.trim()) {
|
||||
return availableTools
|
||||
@@ -147,10 +134,6 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
|
||||
configuration: nextConfiguration,
|
||||
allowed_tools: merged
|
||||
} satisfies UpdateAgentBaseForm)
|
||||
setConfiguration(nextConfiguration)
|
||||
setSelectedMode(nextMode)
|
||||
setAutoToolIds(defaults)
|
||||
setApprovedToolIds(merged)
|
||||
} finally {
|
||||
setIsUpdatingMode(false)
|
||||
}
|
||||
@@ -212,33 +195,25 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
|
||||
)
|
||||
|
||||
const handleToggleTool = useCallback(
|
||||
(toolId: string, isApproved: boolean) => {
|
||||
async (toolId: string, isApproved: boolean) => {
|
||||
if (!agentBase || isUpdatingTools) {
|
||||
return
|
||||
}
|
||||
startTransition(() => {
|
||||
setApprovedToolIds((prev) => {
|
||||
const exists = prev.includes(toolId)
|
||||
if (isApproved === exists) {
|
||||
return prev
|
||||
}
|
||||
const next = isApproved ? [...prev, toolId] : prev.filter((id) => id !== toolId)
|
||||
const sanitized = unique(
|
||||
next.filter((id) => availableTools.some((tool) => tool.id === id)).concat(autoToolIds)
|
||||
)
|
||||
setIsUpdatingTools(true)
|
||||
void (async () => {
|
||||
try {
|
||||
await update({ id: agentBase.id, allowed_tools: sanitized } satisfies UpdateAgentBaseForm)
|
||||
} finally {
|
||||
setIsUpdatingTools(false)
|
||||
}
|
||||
})()
|
||||
return sanitized
|
||||
})
|
||||
})
|
||||
|
||||
const exists = approvedToolIds.includes(toolId)
|
||||
if (isApproved === exists) {
|
||||
return
|
||||
}
|
||||
setIsUpdatingTools(true)
|
||||
const next = isApproved ? [...approvedToolIds, toolId] : approvedToolIds.filter((id) => id !== toolId)
|
||||
const sanitized = unique(next.filter((id) => availableTools.some((tool) => tool.id === id)).concat(autoToolIds))
|
||||
try {
|
||||
await update({ id: agentBase.id, allowed_tools: sanitized } satisfies UpdateAgentBaseForm)
|
||||
} finally {
|
||||
setIsUpdatingTools(false)
|
||||
}
|
||||
},
|
||||
[agentBase, isUpdatingTools, availableTools, autoToolIds, update]
|
||||
[agentBase, isUpdatingTools, approvedToolIds, autoToolIds, availableTools, update]
|
||||
)
|
||||
|
||||
const { agentSummary, autoCount, customCount } = useMemo(() => {
|
||||
@@ -258,31 +233,24 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
|
||||
}, [selectedMode, autoToolIds, userAddedIds, availableTools.length, selectedMcpIds.length])
|
||||
|
||||
const handleToggleMcp = useCallback(
|
||||
(serverId: string, enabled: boolean) => {
|
||||
async (serverId: string, enabled: boolean) => {
|
||||
if (!agentBase || isUpdatingMcp) {
|
||||
return
|
||||
}
|
||||
setSelectedMcpIds((prev) => {
|
||||
const exists = prev.includes(serverId)
|
||||
if (enabled === exists) {
|
||||
return prev
|
||||
}
|
||||
const next = enabled ? [...prev, serverId] : prev.filter((id) => id !== serverId)
|
||||
setIsUpdatingMcp(true)
|
||||
void (async () => {
|
||||
try {
|
||||
await update({ id: agentBase.id, mcps: next } satisfies UpdateAgentBaseForm)
|
||||
const refreshed = await client.getAgent(agentBase.id)
|
||||
const key = client.agentPaths.withId(agentBase.id)
|
||||
mutate(key, refreshed, false)
|
||||
} finally {
|
||||
setIsUpdatingMcp(false)
|
||||
}
|
||||
})()
|
||||
return next
|
||||
})
|
||||
const exists = selectedMcpIds.includes(serverId)
|
||||
if (enabled === exists) {
|
||||
return
|
||||
}
|
||||
const next = enabled ? [...selectedMcpIds, serverId] : selectedMcpIds.filter((id) => id !== serverId)
|
||||
|
||||
setIsUpdatingMcp(true)
|
||||
try {
|
||||
await update({ id: agentBase.id, mcps: next } satisfies UpdateAgentBaseForm)
|
||||
} finally {
|
||||
setIsUpdatingMcp(false)
|
||||
}
|
||||
},
|
||||
[agentBase, isUpdatingMcp, client, update]
|
||||
[agentBase, isUpdatingMcp, selectedMcpIds, update]
|
||||
)
|
||||
|
||||
if (!agentBase) {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Chip } from '@heroui/react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface CategoryFilterProps {
|
||||
categories: string[]
|
||||
selectedCategories: string[]
|
||||
onChange: (categories: string[]) => void
|
||||
}
|
||||
|
||||
export const CategoryFilter: FC<CategoryFilterProps> = ({ categories, selectedCategories, onChange }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isAllSelected = selectedCategories.length === 0
|
||||
|
||||
const handleCategoryClick = (category: string) => {
|
||||
if (selectedCategories.includes(category)) {
|
||||
onChange(selectedCategories.filter((c) => c !== category))
|
||||
} else {
|
||||
onChange([...selectedCategories, category])
|
||||
}
|
||||
}
|
||||
|
||||
const handleAllClick = () => {
|
||||
onChange([])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex max-h-24 flex-wrap gap-2 overflow-y-auto">
|
||||
<Chip
|
||||
variant={isAllSelected ? 'solid' : 'bordered'}
|
||||
color={isAllSelected ? 'primary' : 'default'}
|
||||
onClick={handleAllClick}
|
||||
className="cursor-pointer">
|
||||
{t('plugins.all_categories')}
|
||||
</Chip>
|
||||
|
||||
{categories.map((category) => {
|
||||
const isSelected = selectedCategories.includes(category)
|
||||
return (
|
||||
<Chip
|
||||
key={category}
|
||||
variant={isSelected ? 'solid' : 'bordered'}
|
||||
color={isSelected ? 'primary' : 'default'}
|
||||
onClick={() => handleCategoryClick(category)}
|
||||
className="cursor-pointer">
|
||||
{category}
|
||||
</Chip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Button, Chip, Skeleton, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@heroui/react'
|
||||
import type { InstalledPlugin } from '@renderer/types/plugin'
|
||||
import { Trash2 } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface InstalledPluginsListProps {
|
||||
plugins: InstalledPlugin[]
|
||||
onUninstall: (filename: string, type: 'agent' | 'command' | 'skill') => void
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export const InstalledPluginsList: FC<InstalledPluginsListProps> = ({ plugins, onUninstall, loading }) => {
|
||||
const { t } = useTranslation()
|
||||
const [uninstallingPlugin, setUninstallingPlugin] = useState<string | null>(null)
|
||||
|
||||
const handleUninstall = useCallback(
|
||||
(plugin: InstalledPlugin) => {
|
||||
const confirmed = window.confirm(
|
||||
t('plugins.confirm_uninstall', { name: plugin.metadata.name || plugin.filename })
|
||||
)
|
||||
|
||||
if (confirmed) {
|
||||
setUninstallingPlugin(plugin.filename)
|
||||
onUninstall(plugin.filename, plugin.type)
|
||||
// Reset after a delay to allow the operation to complete
|
||||
setTimeout(() => setUninstallingPlugin(null), 2000)
|
||||
}
|
||||
},
|
||||
[onUninstall, t]
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-12 w-full rounded-lg" />
|
||||
<Skeleton className="h-12 w-full rounded-lg" />
|
||||
<Skeleton className="h-12 w-full rounded-lg" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (plugins.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<p className="text-default-400">{t('plugins.no_installed_plugins')}</p>
|
||||
<p className="text-default-300 text-small">{t('plugins.install_plugins_from_browser')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Table aria-label="Installed plugins table" removeWrapper>
|
||||
<TableHeader>
|
||||
<TableColumn>{t('plugins.name')}</TableColumn>
|
||||
<TableColumn>{t('plugins.type')}</TableColumn>
|
||||
<TableColumn>{t('plugins.category')}</TableColumn>
|
||||
<TableColumn width={100}>{t('plugins.actions')}</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{plugins.map((plugin) => (
|
||||
<TableRow key={plugin.filename}>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-small">{plugin.metadata.name}</span>
|
||||
{plugin.metadata.description && (
|
||||
<span className="line-clamp-1 text-default-400 text-tiny">{plugin.metadata.description}</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip size="sm" variant="flat" color={plugin.type === 'agent' ? 'primary' : 'secondary'}>
|
||||
{plugin.type}
|
||||
</Chip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip size="sm" variant="dot">
|
||||
{plugin.metadata.category}
|
||||
</Chip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
variant="light"
|
||||
isIconOnly
|
||||
onPress={() => handleUninstall(plugin)}
|
||||
isLoading={uninstallingPlugin === plugin.filename}
|
||||
isDisabled={loading}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import { Input, Pagination, Tab, Tabs } from '@heroui/react'
|
||||
import type { InstalledPlugin, PluginMetadata } from '@renderer/types/plugin'
|
||||
import { Search } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { CategoryFilter } from './CategoryFilter'
|
||||
import { PluginCard } from './PluginCard'
|
||||
import { PluginDetailModal } from './PluginDetailModal'
|
||||
|
||||
export interface PluginBrowserProps {
|
||||
agentId: string
|
||||
agents: PluginMetadata[]
|
||||
commands: PluginMetadata[]
|
||||
skills: PluginMetadata[]
|
||||
installedPlugins: InstalledPlugin[]
|
||||
onInstall: (sourcePath: string, type: 'agent' | 'command' | 'skill') => void
|
||||
onUninstall: (filename: string, type: 'agent' | 'command' | 'skill') => void
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
type PluginType = 'all' | 'agent' | 'command' | 'skill'
|
||||
|
||||
const ITEMS_PER_PAGE = 12
|
||||
|
||||
export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
agentId,
|
||||
agents,
|
||||
commands,
|
||||
skills,
|
||||
installedPlugins,
|
||||
onInstall,
|
||||
onUninstall,
|
||||
loading
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
|
||||
const [activeType, setActiveType] = useState<PluginType>('all')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [actioningPlugin, setActioningPlugin] = useState<string | null>(null)
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<PluginMetadata | null>(null)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
// Combine all plugins based on active type
|
||||
const allPlugins = useMemo(() => {
|
||||
switch (activeType) {
|
||||
case 'agent':
|
||||
return agents
|
||||
case 'command':
|
||||
return commands
|
||||
case 'skill':
|
||||
return skills
|
||||
case 'all':
|
||||
default:
|
||||
return [...agents, ...commands, ...skills]
|
||||
}
|
||||
}, [agents, commands, skills, activeType])
|
||||
|
||||
// Extract all unique categories
|
||||
const allCategories = useMemo(() => {
|
||||
const categories = new Set<string>()
|
||||
allPlugins.forEach((plugin) => {
|
||||
if (plugin.category) {
|
||||
categories.add(plugin.category)
|
||||
}
|
||||
})
|
||||
return Array.from(categories).sort()
|
||||
}, [allPlugins])
|
||||
|
||||
// Filter plugins based on search query and selected categories
|
||||
const filteredPlugins = useMemo(() => {
|
||||
return allPlugins.filter((plugin) => {
|
||||
// Filter by search query
|
||||
const searchLower = searchQuery.toLowerCase()
|
||||
const matchesSearch =
|
||||
!searchQuery ||
|
||||
plugin.name.toLowerCase().includes(searchLower) ||
|
||||
plugin.description?.toLowerCase().includes(searchLower) ||
|
||||
plugin.tags?.some((tag) => tag.toLowerCase().includes(searchLower))
|
||||
|
||||
// Filter by selected categories
|
||||
const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(plugin.category)
|
||||
|
||||
return matchesSearch && matchesCategory
|
||||
})
|
||||
}, [allPlugins, searchQuery, selectedCategories])
|
||||
|
||||
// Paginate filtered plugins
|
||||
const paginatedPlugins = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE
|
||||
const endIndex = startIndex + ITEMS_PER_PAGE
|
||||
return filteredPlugins.slice(startIndex, endIndex)
|
||||
}, [filteredPlugins, currentPage])
|
||||
|
||||
const totalPages = Math.ceil(filteredPlugins.length / ITEMS_PER_PAGE)
|
||||
|
||||
// Check if a plugin is installed
|
||||
const isPluginInstalled = (plugin: PluginMetadata): boolean => {
|
||||
return installedPlugins.some(
|
||||
(installed) => installed.filename === plugin.filename && installed.type === plugin.type
|
||||
)
|
||||
}
|
||||
|
||||
// Handle install with loading state
|
||||
const handleInstall = async (plugin: PluginMetadata) => {
|
||||
setActioningPlugin(plugin.sourcePath)
|
||||
await onInstall(plugin.sourcePath, plugin.type)
|
||||
setActioningPlugin(null)
|
||||
}
|
||||
|
||||
// Handle uninstall with loading state
|
||||
const handleUninstall = async (plugin: PluginMetadata) => {
|
||||
setActioningPlugin(plugin.sourcePath)
|
||||
await onUninstall(plugin.filename, plugin.type)
|
||||
setActioningPlugin(null)
|
||||
}
|
||||
|
||||
// Reset to first page when filters change
|
||||
const handleSearchChange = (value: string) => {
|
||||
setSearchQuery(value)
|
||||
setCurrentPage(1)
|
||||
}
|
||||
|
||||
const handleCategoryChange = (categories: string[]) => {
|
||||
setSelectedCategories(categories)
|
||||
setCurrentPage(1)
|
||||
}
|
||||
|
||||
const handleTypeChange = (type: string | number) => {
|
||||
setActiveType(type as PluginType)
|
||||
setCurrentPage(1)
|
||||
}
|
||||
|
||||
const handlePluginClick = (plugin: PluginMetadata) => {
|
||||
setSelectedPlugin(plugin)
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
|
||||
const handleModalClose = () => {
|
||||
setIsModalOpen(false)
|
||||
setSelectedPlugin(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Search Input */}
|
||||
<Input
|
||||
placeholder={t('plugins.search_placeholder')}
|
||||
value={searchQuery}
|
||||
onValueChange={handleSearchChange}
|
||||
startContent={<Search className="h-4 w-4 text-default-400" />}
|
||||
isClearable
|
||||
classNames={{
|
||||
input: 'text-small',
|
||||
inputWrapper: 'h-10'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Category Filter */}
|
||||
<CategoryFilter
|
||||
categories={allCategories}
|
||||
selectedCategories={selectedCategories}
|
||||
onChange={handleCategoryChange}
|
||||
/>
|
||||
|
||||
{/* Type Tabs */}
|
||||
<Tabs selectedKey={activeType} onSelectionChange={handleTypeChange} variant="underlined">
|
||||
<Tab key="all" title={t('plugins.all_types')} />
|
||||
<Tab key="agent" title={t('plugins.agents')} />
|
||||
<Tab key="command" title={t('plugins.commands')} />
|
||||
<Tab key="skill" title={t('plugins.skills')} />
|
||||
</Tabs>
|
||||
|
||||
{/* Result Count */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-default-500 text-small">{t('plugins.showing_results', { count: filteredPlugins.length })}</p>
|
||||
</div>
|
||||
|
||||
{/* Plugin Grid */}
|
||||
{paginatedPlugins.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<p className="text-default-400">{t('plugins.no_results')}</p>
|
||||
<p className="text-default-300 text-small">{t('plugins.try_different_search')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{paginatedPlugins.map((plugin) => {
|
||||
const installed = isPluginInstalled(plugin)
|
||||
const isActioning = actioningPlugin === plugin.sourcePath
|
||||
|
||||
return (
|
||||
<PluginCard
|
||||
key={`${plugin.type}-${plugin.sourcePath}`}
|
||||
plugin={plugin}
|
||||
installed={installed}
|
||||
onInstall={() => handleInstall(plugin)}
|
||||
onUninstall={() => handleUninstall(plugin)}
|
||||
loading={loading || isActioning}
|
||||
onClick={() => handlePluginClick(plugin)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center">
|
||||
<Pagination total={totalPages} page={currentPage} onChange={setCurrentPage} showControls />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Plugin Detail Modal */}
|
||||
<PluginDetailModal
|
||||
agentId={agentId}
|
||||
plugin={selectedPlugin}
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleModalClose}
|
||||
installed={selectedPlugin ? isPluginInstalled(selectedPlugin) : false}
|
||||
onInstall={() => selectedPlugin && handleInstall(selectedPlugin)}
|
||||
onUninstall={() => selectedPlugin && handleUninstall(selectedPlugin)}
|
||||
loading={selectedPlugin ? actioningPlugin === selectedPlugin.sourcePath : false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Button, Card, CardBody, CardFooter, CardHeader, Chip, Spinner } from '@heroui/react'
|
||||
import type { PluginMetadata } from '@renderer/types/plugin'
|
||||
import { Download, Trash2 } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface PluginCardProps {
|
||||
plugin: PluginMetadata
|
||||
installed: boolean
|
||||
onInstall: () => void
|
||||
onUninstall: () => void
|
||||
loading: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export const PluginCard: FC<PluginCardProps> = ({ plugin, installed, onInstall, onUninstall, loading, onClick }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Card className="w-full cursor-pointer transition-shadow hover:shadow-md" isPressable onPress={onClick}>
|
||||
<CardHeader className="flex flex-col items-start gap-2 pb-2">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="font-semibold text-medium">{plugin.name}</h3>
|
||||
<Chip
|
||||
size="sm"
|
||||
variant="solid"
|
||||
color={plugin.type === 'agent' ? 'primary' : plugin.type === 'skill' ? 'success' : 'secondary'}>
|
||||
{plugin.type}
|
||||
</Chip>
|
||||
</div>
|
||||
<Chip size="sm" variant="dot" color="default">
|
||||
{plugin.category}
|
||||
</Chip>
|
||||
</CardHeader>
|
||||
|
||||
<CardBody className="py-2">
|
||||
<p className="line-clamp-3 text-default-500 text-small">{plugin.description || t('plugins.no_description')}</p>
|
||||
|
||||
{plugin.tags && plugin.tags.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{plugin.tags.map((tag) => (
|
||||
<Chip key={tag} size="sm" variant="bordered" className="text-tiny">
|
||||
{tag}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardBody>
|
||||
|
||||
<CardFooter className="pt-2">
|
||||
{installed ? (
|
||||
<Button
|
||||
color="danger"
|
||||
variant="flat"
|
||||
size="sm"
|
||||
startContent={loading ? <Spinner size="sm" color="current" /> : <Trash2 className="h-4 w-4" />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onUninstall()
|
||||
}}
|
||||
isDisabled={loading}
|
||||
fullWidth>
|
||||
{loading ? t('plugins.uninstalling') : t('plugins.uninstall')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
color="primary"
|
||||
variant="flat"
|
||||
size="sm"
|
||||
startContent={loading ? <Spinner size="sm" color="current" /> : <Download className="h-4 w-4" />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onInstall()
|
||||
}}
|
||||
isDisabled={loading}
|
||||
fullWidth>
|
||||
{loading ? t('plugins.installing') : t('plugins.install')}
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
import {
|
||||
Button,
|
||||
Chip,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Spinner,
|
||||
Textarea
|
||||
} from '@heroui/react'
|
||||
import type { PluginMetadata } from '@renderer/types/plugin'
|
||||
import { Download, Edit, Save, Trash2, X } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface PluginDetailModalProps {
|
||||
agentId: string
|
||||
plugin: PluginMetadata | null
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
installed: boolean
|
||||
onInstall: () => void
|
||||
onUninstall: () => void
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export const PluginDetailModal: FC<PluginDetailModalProps> = ({
|
||||
agentId,
|
||||
plugin,
|
||||
isOpen,
|
||||
onClose,
|
||||
installed,
|
||||
onInstall,
|
||||
onUninstall,
|
||||
loading
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [content, setContent] = useState<string>('')
|
||||
const [contentLoading, setContentLoading] = useState(false)
|
||||
const [contentError, setContentError] = useState<string | null>(null)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editedContent, setEditedContent] = useState<string>('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
// Fetch plugin content when modal opens or plugin changes
|
||||
useEffect(() => {
|
||||
if (!isOpen || !plugin) {
|
||||
setContent('')
|
||||
setContentError(null)
|
||||
setIsEditing(false)
|
||||
setEditedContent('')
|
||||
return
|
||||
}
|
||||
|
||||
const fetchContent = async () => {
|
||||
setContentLoading(true)
|
||||
setContentError(null)
|
||||
setIsEditing(false)
|
||||
setEditedContent('')
|
||||
try {
|
||||
let sourcePath = plugin.sourcePath
|
||||
if (plugin.type === 'skill') {
|
||||
sourcePath = sourcePath + '/' + 'SKILL.md'
|
||||
}
|
||||
|
||||
const result = await window.api.claudeCodePlugin.readContent(sourcePath)
|
||||
if (result.success) {
|
||||
setContent(result.data)
|
||||
} else {
|
||||
setContentError(`Failed to load content: ${result.error.type}`)
|
||||
}
|
||||
} catch (error) {
|
||||
setContentError(`Error loading content: ${error instanceof Error ? error.message : String(error)}`)
|
||||
} finally {
|
||||
setContentLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchContent()
|
||||
}, [isOpen, plugin])
|
||||
|
||||
const handleEdit = () => {
|
||||
setEditedContent(content)
|
||||
setIsEditing(true)
|
||||
}
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setIsEditing(false)
|
||||
setEditedContent('')
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!plugin) return
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
const result = await window.api.claudeCodePlugin.writeContent({
|
||||
agentId,
|
||||
filename: plugin.filename,
|
||||
type: plugin.type,
|
||||
content: editedContent
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
setContent(editedContent)
|
||||
setIsEditing(false)
|
||||
window.toast?.success('Plugin content saved successfully')
|
||||
} else {
|
||||
window.toast?.error(`Failed to save: ${result.error.type}`)
|
||||
}
|
||||
} catch (error) {
|
||||
window.toast?.error(`Error saving: ${error instanceof Error ? error.message : String(error)}`)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!plugin) return null
|
||||
|
||||
const modalContent = (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size="2xl"
|
||||
scrollBehavior="inside"
|
||||
classNames={{
|
||||
wrapper: 'z-[9999]'
|
||||
}}>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="font-bold text-xl">{plugin.name}</h2>
|
||||
<Chip size="sm" variant="solid" color={plugin.type === 'agent' ? 'primary' : 'secondary'}>
|
||||
{plugin.type}
|
||||
</Chip>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Chip size="sm" variant="dot" color="default">
|
||||
{plugin.category}
|
||||
</Chip>
|
||||
{plugin.version && (
|
||||
<Chip size="sm" variant="bordered">
|
||||
v{plugin.version}
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{/* Description */}
|
||||
{plugin.description && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Description</h3>
|
||||
<p className="text-default-600 text-small">{plugin.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Author */}
|
||||
{plugin.author && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Author</h3>
|
||||
<p className="text-default-600 text-small">{plugin.author}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tools (for agents) */}
|
||||
{plugin.tools && plugin.tools.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Tools</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.tools.map((tool) => (
|
||||
<Chip key={tool} size="sm" variant="flat">
|
||||
{tool}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Allowed Tools (for commands) */}
|
||||
{plugin.allowed_tools && plugin.allowed_tools.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Allowed Tools</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.allowed_tools.map((tool) => (
|
||||
<Chip key={tool} size="sm" variant="flat">
|
||||
{tool}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
{plugin.tags && plugin.tags.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Tags</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{plugin.tags.map((tag) => (
|
||||
<Chip key={tag} size="sm" variant="bordered">
|
||||
{tag}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 font-semibold text-small">Metadata</h3>
|
||||
<div className="space-y-1 text-small">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">File:</span>
|
||||
<span className="font-mono text-default-600 text-tiny">{plugin.filename}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Size:</span>
|
||||
<span className="text-default-600">{(plugin.size / 1024).toFixed(2)} KB</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Source:</span>
|
||||
<span className="font-mono text-default-600 text-tiny">{plugin.sourcePath}</span>
|
||||
</div>
|
||||
{plugin.installedAt && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-default-500">Installed:</span>
|
||||
<span className="text-default-600">{new Date(plugin.installedAt).toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-small">Content</h3>
|
||||
{installed && !contentLoading && !contentError && (
|
||||
<div className="flex gap-2">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="danger"
|
||||
startContent={<X className="h-3 w-3" />}
|
||||
onPress={handleCancelEdit}
|
||||
isDisabled={saving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
startContent={saving ? <Spinner size="sm" color="current" /> : <Save className="h-3 w-3" />}
|
||||
onPress={handleSave}
|
||||
isDisabled={saving}>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button size="sm" variant="flat" startContent={<Edit className="h-3 w-3" />} onPress={handleEdit}>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{contentLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Spinner size="sm" />
|
||||
</div>
|
||||
) : contentError ? (
|
||||
<div className="rounded-md bg-danger-50 p-3 text-danger text-small">{contentError}</div>
|
||||
) : isEditing ? (
|
||||
<Textarea
|
||||
value={editedContent}
|
||||
onValueChange={setEditedContent}
|
||||
minRows={20}
|
||||
classNames={{
|
||||
input: 'font-mono text-tiny'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<pre className="max-h-96 overflow-auto whitespace-pre-wrap rounded-md bg-default-100 p-3 font-mono text-tiny">
|
||||
{content}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
{installed ? (
|
||||
<Button
|
||||
color="danger"
|
||||
variant="flat"
|
||||
startContent={loading ? <Spinner size="sm" color="current" /> : <Trash2 className="h-4 w-4" />}
|
||||
onPress={onUninstall}
|
||||
isDisabled={loading}>
|
||||
{loading ? t('plugins.uninstalling') : t('plugins.uninstall')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
color="primary"
|
||||
startContent={loading ? <Spinner size="sm" color="current" /> : <Download className="h-4 w-4" />}
|
||||
onPress={onInstall}
|
||||
isDisabled={loading}>
|
||||
{loading ? t('plugins.installing') : t('plugins.install')}
|
||||
</Button>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
return createPortal(modalContent, document.body)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export type { CategoryFilterProps } from './CategoryFilter'
|
||||
export { CategoryFilter } from './CategoryFilter'
|
||||
export type { InstalledPluginsListProps } from './InstalledPluginsList'
|
||||
export { InstalledPluginsList } from './InstalledPluginsList'
|
||||
export type { PluginBrowserProps } from './PluginBrowser'
|
||||
export { PluginBrowser } from './PluginBrowser'
|
||||
export type { PluginCardProps } from './PluginCard'
|
||||
export { PluginCard } from './PluginCard'
|
||||
export type { PluginDetailModalProps } from './PluginDetailModal'
|
||||
export { PluginDetailModal } from './PluginDetailModal'
|
||||
@@ -77,7 +77,6 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
|
||||
src={getPreprocessProviderLogo(preprocessProvider.id)}
|
||||
className="h-4 w-4 border-[0.5px] border-[var(--color-border)]"
|
||||
/>
|
||||
|
||||
<ProviderName> {preprocessProvider.name}</ProviderName>
|
||||
{officialWebsite && preprocessProviderConfig?.websites && (
|
||||
<Link target="_blank" href={preprocessProviderConfig.websites.official}>
|
||||
|
||||
@@ -2,11 +2,22 @@ import { Button, Flex, RowFlex, Switch, Tooltip, WarnTooltip } from '@cherrystud
|
||||
import OpenAIAlert from '@renderer/components/Alert/OpenAIAlert'
|
||||
import { LoadingIcon } from '@renderer/components/Icons'
|
||||
import { ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup'
|
||||
import Selector from '@renderer/components/Selector'
|
||||
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
|
||||
import { PROVIDER_URLS } from '@renderer/config/providers'
|
||||
import {
|
||||
isAnthropicProvider,
|
||||
isAzureOpenAIProvider,
|
||||
isGeminiProvider,
|
||||
isNewApiProvider,
|
||||
isOpenAICompatibleProvider,
|
||||
isOpenAIProvider,
|
||||
isSupportAPIVersionProvider,
|
||||
PROVIDER_URLS
|
||||
} from '@renderer/config/providers'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAllProviders, useProvider, useProviders } from '@renderer/hooks/useProvider'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { isVertexProvider } from '@renderer/hooks/useVertexAI'
|
||||
import i18n from '@renderer/i18n'
|
||||
import AnthropicSettings from '@renderer/pages/settings/ProviderSettings/AnthropicSettings'
|
||||
import { ModelList } from '@renderer/pages/settings/ProviderSettings/ModelList'
|
||||
@@ -14,15 +25,17 @@ import { checkApi } from '@renderer/services/ApiService'
|
||||
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { updateWebSearchProvider } from '@renderer/store/websearch'
|
||||
import type { SystemProviderId } from '@renderer/types'
|
||||
import { isSystemProvider, isSystemProviderId, SystemProviderIds } from '@renderer/types'
|
||||
import type { ApiKeyConnectivity } from '@renderer/types/healthCheck'
|
||||
import { HealthStatus } from '@renderer/types/healthCheck'
|
||||
import {
|
||||
formatApiHost,
|
||||
formatApiKeys,
|
||||
formatAzureOpenAIApiHost,
|
||||
formatVertexApiHost,
|
||||
getFancyProviderName,
|
||||
isAnthropicProvider,
|
||||
isOpenAIProvider
|
||||
validateApiHost
|
||||
} from '@renderer/utils'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { Divider, Input, Select, Space } from 'antd'
|
||||
@@ -65,7 +78,9 @@ const ANTHROPIC_COMPATIBLE_PROVIDER_IDS = [
|
||||
SystemProviderIds.dashscope,
|
||||
SystemProviderIds.modelscope,
|
||||
SystemProviderIds.aihubmix,
|
||||
SystemProviderIds.grok
|
||||
SystemProviderIds.grok,
|
||||
SystemProviderIds.cherryin,
|
||||
SystemProviderIds.longcat
|
||||
] as const
|
||||
type AnthropicCompatibleProviderId = (typeof ANTHROPIC_COMPATIBLE_PROVIDER_IDS)[number]
|
||||
|
||||
@@ -74,6 +89,8 @@ const isAnthropicCompatibleProviderId = (id: string): id is AnthropicCompatibleP
|
||||
return ANTHROPIC_COMPATIBLE_PROVIDER_ID_SET.has(id)
|
||||
}
|
||||
|
||||
type HostField = 'apiHost' | 'anthropicApiHost'
|
||||
|
||||
const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
const { provider, updateProvider, models } = useProvider(providerId)
|
||||
const allProviders = useAllProviders()
|
||||
@@ -81,19 +98,23 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
const [apiHost, setApiHost] = useState(provider.apiHost)
|
||||
const [anthropicApiHost, setAnthropicHost] = useState<string | undefined>(provider.anthropicApiHost)
|
||||
const [apiVersion, setApiVersion] = useState(provider.apiVersion)
|
||||
const [activeHostField, setActiveHostField] = useState<HostField>('apiHost')
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const isAzureOpenAI = provider.id === 'azure-openai' || provider.type === 'azure-openai'
|
||||
const isAzureOpenAI = isAzureOpenAIProvider(provider)
|
||||
const isDmxapi = provider.id === 'dmxapi'
|
||||
const hideApiInput = ['vertexai', 'aws-bedrock'].includes(provider.id)
|
||||
const noAPIInputProviders = ['aws-bedrock'] as const satisfies SystemProviderId[]
|
||||
const hideApiInput = noAPIInputProviders.some((id) => id === provider.id)
|
||||
const noAPIKeyInputProviders = ['copilot', 'vertexai'] as const satisfies SystemProviderId[]
|
||||
const hideApiKeyInput = noAPIKeyInputProviders.some((id) => id === provider.id)
|
||||
|
||||
const providerConfig = PROVIDER_URLS[provider.id]
|
||||
const officialWebsite = providerConfig?.websites?.official
|
||||
const apiKeyWebsite = providerConfig?.websites?.apiKey
|
||||
const configedApiHost = providerConfig?.api?.url
|
||||
const configuredApiHost = providerConfig?.api?.url
|
||||
|
||||
const fancyProviderName = getFancyProviderName(provider)
|
||||
|
||||
@@ -153,7 +174,12 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
)
|
||||
|
||||
const onUpdateApiHost = () => {
|
||||
if (apiHost.trim()) {
|
||||
if (!validateApiHost(apiHost)) {
|
||||
setApiHost(provider.apiHost)
|
||||
window.toast.error(t('settings.provider.api_host_no_valid'))
|
||||
return
|
||||
}
|
||||
if (isVertexProvider(provider) || apiHost.trim()) {
|
||||
updateProvider({ apiHost })
|
||||
} else {
|
||||
setApiHost(provider.apiHost)
|
||||
@@ -240,27 +266,46 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
setApiHost(configedApiHost)
|
||||
updateProvider({ apiHost: configedApiHost })
|
||||
}
|
||||
const onReset = useCallback(() => {
|
||||
setApiHost(configuredApiHost)
|
||||
updateProvider({ apiHost: configuredApiHost })
|
||||
}, [configuredApiHost, updateProvider])
|
||||
|
||||
const isApiHostResettable = useMemo(() => {
|
||||
return !isEmpty(configuredApiHost) && apiHost !== configuredApiHost
|
||||
}, [configuredApiHost, apiHost])
|
||||
|
||||
const hostPreview = () => {
|
||||
if (apiHost.endsWith('#')) {
|
||||
return apiHost.replace('#', '')
|
||||
}
|
||||
if (provider.type === 'openai') {
|
||||
return formatApiHost(apiHost) + 'chat/completions'
|
||||
|
||||
if (isOpenAICompatibleProvider(provider)) {
|
||||
return formatApiHost(apiHost, isSupportAPIVersionProvider(provider)) + '/chat/completions'
|
||||
}
|
||||
|
||||
if (provider.type === 'azure-openai') {
|
||||
return formatApiHost(apiHost) + 'openai/v1'
|
||||
if (isAzureOpenAIProvider(provider)) {
|
||||
const apiVersion = provider.apiVersion
|
||||
const path = !['preview', 'v1'].includes(apiVersion)
|
||||
? `/v1/chat/completion?apiVersion=v1`
|
||||
: `/v1/responses?apiVersion=v1`
|
||||
return formatAzureOpenAIApiHost(apiHost) + path
|
||||
}
|
||||
|
||||
if (provider.type === 'anthropic') {
|
||||
return formatApiHost(apiHost) + 'messages'
|
||||
if (isAnthropicProvider(provider)) {
|
||||
return formatApiHost(apiHost) + '/messages'
|
||||
}
|
||||
return formatApiHost(apiHost) + 'responses'
|
||||
|
||||
if (isGeminiProvider(provider)) {
|
||||
return formatApiHost(apiHost, true, 'v1beta') + '/models'
|
||||
}
|
||||
if (isOpenAIProvider(provider)) {
|
||||
return formatApiHost(apiHost) + '/responses'
|
||||
}
|
||||
if (isVertexProvider(provider)) {
|
||||
return formatVertexApiHost(provider) + '/publishers/google'
|
||||
}
|
||||
return formatApiHost(apiHost)
|
||||
}
|
||||
|
||||
// API key 连通性检查状态指示器,目前仅在失败时显示
|
||||
@@ -289,31 +334,44 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
}, [provider.anthropicApiHost])
|
||||
|
||||
const canConfigureAnthropicHost = useMemo(() => {
|
||||
if (isNewApiProvider(provider)) {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
provider.type !== 'anthropic' && isSystemProviderId(provider.id) && isAnthropicCompatibleProviderId(provider.id)
|
||||
)
|
||||
}, [provider])
|
||||
|
||||
const anthropicHostPreview = useMemo(() => {
|
||||
const rawHost = (anthropicApiHost ?? provider.anthropicApiHost)?.trim()
|
||||
if (!rawHost) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (/\/messages\/?$/.test(rawHost)) {
|
||||
return rawHost.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
let normalizedHost = rawHost
|
||||
if (/\/v\d+(?:\/)?$/i.test(normalizedHost)) {
|
||||
normalizedHost = normalizedHost.replace(/\/$/, '')
|
||||
} else {
|
||||
normalizedHost = formatApiHost(normalizedHost).replace(/\/$/, '')
|
||||
}
|
||||
const rawHost = anthropicApiHost ?? provider.anthropicApiHost
|
||||
const normalizedHost = formatApiHost(rawHost)
|
||||
|
||||
return `${normalizedHost}/messages`
|
||||
}, [anthropicApiHost, provider.anthropicApiHost])
|
||||
|
||||
const hostSelectorOptions = useMemo(() => {
|
||||
const options: { value: HostField; label: string }[] = [
|
||||
{ value: 'apiHost', label: t('settings.provider.api_host') }
|
||||
]
|
||||
|
||||
if (canConfigureAnthropicHost) {
|
||||
options.push({ value: 'anthropicApiHost', label: t('settings.provider.anthropic_api_host') })
|
||||
}
|
||||
|
||||
return options
|
||||
}, [canConfigureAnthropicHost, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (!canConfigureAnthropicHost && activeHostField === 'anthropicApiHost') {
|
||||
setActiveHostField('apiHost')
|
||||
}
|
||||
}, [canConfigureAnthropicHost, activeHostField])
|
||||
|
||||
const hostSelectorTooltip =
|
||||
activeHostField === 'anthropicApiHost'
|
||||
? t('settings.provider.anthropic_api_host_tooltip')
|
||||
: t('settings.provider.api_host_tooltip')
|
||||
|
||||
const isAnthropicOAuth = () => provider.id === 'anthropic' && provider.authType === 'oauth'
|
||||
|
||||
return (
|
||||
@@ -372,104 +430,119 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
)}
|
||||
{!hideApiInput && !isAnthropicOAuth() && (
|
||||
<>
|
||||
<SettingSubtitle
|
||||
style={{
|
||||
marginTop: 5,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
{t('settings.provider.api_key.label')}
|
||||
{provider.id !== 'copilot' && (
|
||||
<Tooltip content={t('settings.provider.api.key.list.open')} delay={500}>
|
||||
<Button variant="ghost" onClick={openApiKeyList} size="icon">
|
||||
<Settings2 size={16} />
|
||||
{!hideApiKeyInput && (
|
||||
<>
|
||||
<SettingSubtitle
|
||||
style={{
|
||||
marginTop: 5,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
{t('settings.provider.api_key.label')}
|
||||
{provider.id !== 'copilot' && (
|
||||
<Tooltip title={t('settings.provider.api.key.list.open')} delay={500}>
|
||||
<Button variant="ghost" onClick={openApiKeyList} size="icon">
|
||||
<Settings2 size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input.Password
|
||||
value={localApiKey}
|
||||
placeholder={t('settings.provider.api_key.label')}
|
||||
onChange={(e) => setLocalApiKey(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoFocus={provider.enabled && provider.apiKey === '' && !isProviderSupportAuth(provider)}
|
||||
disabled={provider.id === 'copilot'}
|
||||
suffix={renderStatusIndicator()}
|
||||
/>
|
||||
<Button
|
||||
variant={isApiKeyConnectable ? 'ghost' : undefined}
|
||||
onClick={onCheckApi}
|
||||
disabled={!apiHost || apiKeyConnectivity.checking}>
|
||||
{apiKeyConnectivity.checking ? (
|
||||
<LoadingIcon />
|
||||
) : apiKeyConnectivity.status === HealthStatus.SUCCESS ? (
|
||||
<Check size={16} className="lucide-custom" />
|
||||
) : (
|
||||
t('settings.provider.check')
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input.Password
|
||||
value={localApiKey}
|
||||
placeholder={t('settings.provider.api_key.label')}
|
||||
onChange={(e) => setLocalApiKey(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoFocus={provider.enabled && provider.apiKey === '' && !isProviderSupportAuth(provider)}
|
||||
disabled={provider.id === 'copilot'}
|
||||
suffix={renderStatusIndicator()}
|
||||
/>
|
||||
<Button
|
||||
variant={isApiKeyConnectable ? 'ghost' : undefined}
|
||||
onClick={onCheckApi}
|
||||
disabled={!apiHost || apiKeyConnectivity.checking}>
|
||||
{apiKeyConnectivity.checking ? (
|
||||
<LoadingIcon />
|
||||
) : apiKeyConnectivity.status === HealthStatus.SUCCESS ? (
|
||||
<Check size={16} className="lucide-custom" />
|
||||
) : (
|
||||
t('settings.provider.check')
|
||||
)}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<RowFlex>
|
||||
{apiKeyWebsite && !isDmxapi && (
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
)}
|
||||
</RowFlex>
|
||||
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
{!isDmxapi && !isAnthropicOAuth() && (
|
||||
</Space.Compact>
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<RowFlex>
|
||||
{apiKeyWebsite && !isDmxapi && (
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
)}
|
||||
</RowFlex>
|
||||
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
</>
|
||||
)}
|
||||
{!isDmxapi && (
|
||||
<>
|
||||
<SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Tooltip content={t('settings.provider.api_host_tooltip')} delay={300}>
|
||||
<SubtitleLabel>{t('settings.provider.api_host')}</SubtitleLabel>
|
||||
<Tooltip title={hostSelectorTooltip} delay={300}>
|
||||
<Selector
|
||||
size={14}
|
||||
value={activeHostField}
|
||||
onChange={(value) => setActiveHostField(value as HostField)}
|
||||
options={hostSelectorOptions}
|
||||
style={{ paddingLeft: 1, fontWeight: 'bold' }}
|
||||
placement="bottomLeft"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Button variant="ghost" onClick={() => CustomHeaderPopup.show({ provider })} size="icon">
|
||||
<Settings2 size={16} />
|
||||
</Button>
|
||||
</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
/>
|
||||
{!isEmpty(configedApiHost) && apiHost !== configedApiHost && (
|
||||
<Button variant="destructive" onClick={onReset}>
|
||||
{t('settings.provider.api.url.reset')}
|
||||
</Button>
|
||||
)}
|
||||
</Space.Compact>
|
||||
|
||||
{(isOpenAIProvider(provider) || isAnthropicProvider(provider)) && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<SettingHelpText
|
||||
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
|
||||
{t('settings.provider.api_host_preview', { url: hostPreview() })}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText style={{ minWidth: 'fit-content' }}>
|
||||
{t('settings.provider.api.url.tip')}
|
||||
</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
{activeHostField === 'apiHost' && (
|
||||
<>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
/>
|
||||
{isApiHostResettable && (
|
||||
<Button variant="destructive" onClick={onReset}>
|
||||
{t('settings.provider.api.url.reset')}
|
||||
</Button>
|
||||
)}
|
||||
</Space.Compact>
|
||||
{isVertexProvider(provider) && (
|
||||
<SettingHelpTextRow>
|
||||
<SettingHelpText>{t('settings.provider.vertex_ai.api_host_help')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
{(isOpenAICompatibleProvider(provider) ||
|
||||
isAzureOpenAIProvider(provider) ||
|
||||
isAnthropicProvider(provider) ||
|
||||
isGeminiProvider(provider) ||
|
||||
isVertexProvider(provider) ||
|
||||
isOpenAIProvider(provider)) && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<SettingHelpText
|
||||
style={{
|
||||
marginLeft: 6,
|
||||
marginRight: '1em',
|
||||
whiteSpace: 'break-spaces',
|
||||
wordBreak: 'break-all'
|
||||
}}>
|
||||
{t('settings.provider.api_host_preview', { url: hostPreview() })}
|
||||
</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{canConfigureAnthropicHost && (
|
||||
{activeHostField === 'anthropicApiHost' && canConfigureAnthropicHost && (
|
||||
<>
|
||||
<SettingSubtitle
|
||||
style={{
|
||||
marginTop: 5,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<Tooltip content={t('settings.provider.anthropic_api_host_tooltip')} delay={300}>
|
||||
<SubtitleLabel>{t('settings.provider.anthropic_api_host')}</SubtitleLabel>
|
||||
</Tooltip>
|
||||
</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={anthropicApiHost ?? ''}
|
||||
@@ -484,9 +557,6 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
url: anthropicHostPreview || '—'
|
||||
})}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText style={{ marginLeft: 6 }}>
|
||||
{t('settings.provider.anthropic_api_host_tip')}
|
||||
</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
</>
|
||||
)}
|
||||
@@ -516,21 +586,12 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
{provider.id === 'gpustack' && <GPUStackSettings />}
|
||||
{provider.id === 'copilot' && <GithubCopilotSettings providerId={provider.id} />}
|
||||
{provider.id === 'aws-bedrock' && <AwsBedrockSettings />}
|
||||
{provider.id === 'vertexai' && <VertexAISettings providerId={provider.id} />}
|
||||
{provider.id === 'vertexai' && <VertexAISettings />}
|
||||
<ModelList providerId={provider.id} />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const SubtitleLabel = styled.span`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
`
|
||||
|
||||
const ProviderName = styled.span`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import { RowFlex } from '@cherrystudio/ui'
|
||||
import { PROVIDER_URLS } from '@renderer/config/providers'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { useVertexAISettings } from '@renderer/hooks/useVertexAI'
|
||||
import { Alert, Input, Space } from 'antd'
|
||||
import type { FC } from 'react'
|
||||
import { Alert, Input } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '..'
|
||||
|
||||
interface Props {
|
||||
providerId: string
|
||||
}
|
||||
|
||||
const VertexAISettings: FC<Props> = ({ providerId }) => {
|
||||
const VertexAISettings = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
projectId,
|
||||
@@ -28,16 +22,9 @@ const VertexAISettings: FC<Props> = ({ providerId }) => {
|
||||
const [localProjectId, setLocalProjectId] = useState(projectId)
|
||||
const [localLocation, setLocalLocation] = useState(location)
|
||||
|
||||
const { provider, updateProvider } = useProvider(providerId)
|
||||
const [apiHost, setApiHost] = useState(provider.apiHost)
|
||||
|
||||
const providerConfig = PROVIDER_URLS['vertexai']
|
||||
const apiKeyWebsite = providerConfig?.websites?.apiKey
|
||||
|
||||
const onUpdateApiHost = () => {
|
||||
updateProvider({ apiHost })
|
||||
}
|
||||
|
||||
const handleProjectIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setLocalProjectId(e.target.value)
|
||||
}
|
||||
@@ -73,18 +60,6 @@ const VertexAISettings: FC<Props> = ({ providerId }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
/>
|
||||
</Space.Compact>
|
||||
<SettingHelpTextRow>
|
||||
<SettingHelpText>{t('settings.provider.vertex_ai.api_host_help')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>
|
||||
{t('settings.provider.vertex_ai.service_account.title')}
|
||||
</SettingSubtitle>
|
||||
|
||||
19
src/renderer/src/pages/terminal/TerminalPage.tsx
Normal file
19
src/renderer/src/pages/terminal/TerminalPage.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const TerminalPage: FC = () => {
|
||||
// const { pathname } = useLocation()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// const isRoute = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('title.terminal')}</NavbarCenter>
|
||||
</Navbar>
|
||||
<div>🥲 Not Implemented</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { Span } from '@opentelemetry/api'
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT, DEFAULT_KNOWLEDGE_THRESHOLD } from '@renderer/config/constant'
|
||||
import { getEmbeddingMaxContext } from '@renderer/config/embedings'
|
||||
import { isGeminiProvider } from '@renderer/config/providers'
|
||||
import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
|
||||
import store from '@renderer/store'
|
||||
import type {
|
||||
@@ -41,7 +42,7 @@ export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams
|
||||
|
||||
let host = aiProvider.getBaseURL()
|
||||
const rerankHost = rerankAiProvider.getBaseURL()
|
||||
if (provider.type === 'gemini') {
|
||||
if (isGeminiProvider(provider)) {
|
||||
host = host + '/v1beta/openai/'
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user