Compare commits
66 Commits
refactor/o
...
feat/reset
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e0c96d118 | ||
|
|
60b7795d16 | ||
|
|
9df850b226 | ||
|
|
44378cc879 | ||
|
|
50edf016f6 | ||
|
|
c49c592175 | ||
|
|
4437871644 | ||
|
|
e59dfdd27d | ||
|
|
ac917e12f2 | ||
|
|
7ecfbce5a5 | ||
|
|
e6e20d2d72 | ||
|
|
5101488d65 | ||
|
|
dc06c103e0 | ||
|
|
7c0b03dbdc | ||
|
|
1f0381aebe | ||
|
|
fb02a61a48 | ||
|
|
562fbb3ff7 | ||
|
|
1018ad87b8 | ||
|
|
82ca35fc29 | ||
|
|
fe53b0914a | ||
|
|
67a379641f | ||
|
|
b3dc2d0422 | ||
|
|
1828ef8997 | ||
|
|
9dbc6fbf67 | ||
|
|
b3f88a7fc2 | ||
|
|
8da43ab794 | ||
|
|
2a06c606e1 | ||
|
|
b6dcf2f5fa | ||
|
|
2c07ea0dd8 | ||
|
|
68e0d8b0f1 | ||
|
|
6042ee8ca8 | ||
|
|
d164d7c8bf | ||
|
|
7f1c234ac1 | ||
|
|
c1fd23742f | ||
|
|
d792bf7fe0 | ||
|
|
f8a599322f | ||
|
|
aa810a7ead | ||
|
|
b586e1796e | ||
|
|
23f7b39753 | ||
|
|
fa2ec69fa9 | ||
|
|
fe188ba8fb | ||
|
|
dd8690b592 | ||
|
|
09e6b9741e | ||
|
|
0767952a6f | ||
|
|
72299f833a | ||
|
|
7badaf02b9 | ||
|
|
cf008ca22e | ||
|
|
851ff8992f | ||
|
|
91f9088436 | ||
|
|
c971daf23c | ||
|
|
0c7cee2700 | ||
|
|
dfbfc2869c | ||
|
|
1575e97168 | ||
|
|
e0a2ed0481 | ||
|
|
3e9d9f16d6 | ||
|
|
f3a279d8de | ||
|
|
5790c12011 | ||
|
|
352ecbc506 | ||
|
|
fc4f30feab | ||
|
|
888a183328 | ||
|
|
9a01e092f6 | ||
|
|
5986800c9d | ||
|
|
56d68276e1 | ||
|
|
29c1173365 | ||
|
|
c7ceb3035d | ||
|
|
7bcae6fba2 |
2
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: 🐛 Bug Report (English)
|
||||
name: 🐛 Bug Report
|
||||
description: Create a report to help us improve
|
||||
title: '[Bug]: '
|
||||
labels: ['BUG']
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: 💡 Feature Request (English)
|
||||
name: 💡 Feature Request
|
||||
description: Suggest an idea for this project
|
||||
title: '[Feature]: '
|
||||
labels: ['feature']
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/3_others.yml
vendored
2
.github/ISSUE_TEMPLATE/3_others.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: 🤔 Other Questions (English)
|
||||
name: 🤔 Other Questions
|
||||
description: Submit questions that don't fit into bug reports or feature requests
|
||||
title: '[Other]: '
|
||||
body:
|
||||
|
||||
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, {
|
||||
@@ -1,71 +0,0 @@
|
||||
diff --git a/dist/utils/tiktoken.cjs b/dist/utils/tiktoken.cjs
|
||||
index 973b0d0e75aeaf8de579419af31b879b32975413..f23c7caa8b9dc8bd404132725346a4786f6b278b 100644
|
||||
--- a/dist/utils/tiktoken.cjs
|
||||
+++ b/dist/utils/tiktoken.cjs
|
||||
@@ -1,25 +1,14 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.encodingForModel = exports.getEncoding = void 0;
|
||||
-const lite_1 = require("js-tiktoken/lite");
|
||||
const async_caller_js_1 = require("./async_caller.cjs");
|
||||
const cache = {};
|
||||
const caller = /* #__PURE__ */ new async_caller_js_1.AsyncCaller({});
|
||||
async function getEncoding(encoding) {
|
||||
- if (!(encoding in cache)) {
|
||||
- cache[encoding] = caller
|
||||
- .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`)
|
||||
- .then((res) => res.json())
|
||||
- .then((data) => new lite_1.Tiktoken(data))
|
||||
- .catch((e) => {
|
||||
- delete cache[encoding];
|
||||
- throw e;
|
||||
- });
|
||||
- }
|
||||
- return await cache[encoding];
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
exports.getEncoding = getEncoding;
|
||||
async function encodingForModel(model) {
|
||||
- return getEncoding((0, lite_1.getEncodingNameForModel)(model));
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
exports.encodingForModel = encodingForModel;
|
||||
diff --git a/dist/utils/tiktoken.js b/dist/utils/tiktoken.js
|
||||
index 8e41ee6f00f2f9c7fa2c59fa2b2f4297634b97aa..aa5f314a6349ad0d1c5aea8631a56aad099176e0 100644
|
||||
--- a/dist/utils/tiktoken.js
|
||||
+++ b/dist/utils/tiktoken.js
|
||||
@@ -1,20 +1,9 @@
|
||||
-import { Tiktoken, getEncodingNameForModel, } from "js-tiktoken/lite";
|
||||
import { AsyncCaller } from "./async_caller.js";
|
||||
const cache = {};
|
||||
const caller = /* #__PURE__ */ new AsyncCaller({});
|
||||
export async function getEncoding(encoding) {
|
||||
- if (!(encoding in cache)) {
|
||||
- cache[encoding] = caller
|
||||
- .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`)
|
||||
- .then((res) => res.json())
|
||||
- .then((data) => new Tiktoken(data))
|
||||
- .catch((e) => {
|
||||
- delete cache[encoding];
|
||||
- throw e;
|
||||
- });
|
||||
- }
|
||||
- return await cache[encoding];
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
export async function encodingForModel(model) {
|
||||
- return getEncoding(getEncodingNameForModel(model));
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
diff --git a/package.json b/package.json
|
||||
index 36072aecf700fca1bc49832a19be832eca726103..90b8922fba1c3d1b26f78477c891b07816d6238a 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -37,7 +37,6 @@
|
||||
"ansi-styles": "^5.0.0",
|
||||
"camelcase": "6",
|
||||
"decamelize": "1.2.0",
|
||||
- "js-tiktoken": "^1.0.12",
|
||||
"langsmith": ">=0.2.8 <0.4.0",
|
||||
"mustache": "^4.2.0",
|
||||
"p-queue": "^6.6.2",
|
||||
68
.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch
vendored
Normal file
68
.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
diff --git a/dist/utils/tiktoken.cjs b/dist/utils/tiktoken.cjs
|
||||
index c5b41f121d2e3d24c3a4969e31fa1acffdcad3b9..ec724489dcae79ee6c61acf2d4d84bd19daef036 100644
|
||||
--- a/dist/utils/tiktoken.cjs
|
||||
+++ b/dist/utils/tiktoken.cjs
|
||||
@@ -1,6 +1,5 @@
|
||||
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
||||
const require_utils_async_caller = require('./async_caller.cjs');
|
||||
-const js_tiktoken_lite = require_rolldown_runtime.__toESM(require("js-tiktoken/lite"));
|
||||
|
||||
//#region src/utils/tiktoken.ts
|
||||
var tiktoken_exports = {};
|
||||
@@ -11,14 +10,10 @@ require_rolldown_runtime.__export(tiktoken_exports, {
|
||||
const cache = {};
|
||||
const caller = /* @__PURE__ */ new require_utils_async_caller.AsyncCaller({});
|
||||
async function getEncoding(encoding) {
|
||||
- if (!(encoding in cache)) cache[encoding] = caller.fetch(`https://tiktoken.pages.dev/js/${encoding}.json`).then((res) => res.json()).then((data) => new js_tiktoken_lite.Tiktoken(data)).catch((e) => {
|
||||
- delete cache[encoding];
|
||||
- throw e;
|
||||
- });
|
||||
- return await cache[encoding];
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
async function encodingForModel(model) {
|
||||
- return getEncoding((0, js_tiktoken_lite.getEncodingNameForModel)(model));
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
|
||||
//#endregion
|
||||
diff --git a/dist/utils/tiktoken.js b/dist/utils/tiktoken.js
|
||||
index 641acca03cb92f04a6fa5c9c31f1880ce635572e..707389970ad957aa0ff20ef37fa8dd2875be737c 100644
|
||||
--- a/dist/utils/tiktoken.js
|
||||
+++ b/dist/utils/tiktoken.js
|
||||
@@ -1,6 +1,5 @@
|
||||
import { __export } from "../_virtual/rolldown_runtime.js";
|
||||
import { AsyncCaller } from "./async_caller.js";
|
||||
-import { Tiktoken, getEncodingNameForModel } from "js-tiktoken/lite";
|
||||
|
||||
//#region src/utils/tiktoken.ts
|
||||
var tiktoken_exports = {};
|
||||
@@ -11,14 +10,10 @@ __export(tiktoken_exports, {
|
||||
const cache = {};
|
||||
const caller = /* @__PURE__ */ new AsyncCaller({});
|
||||
async function getEncoding(encoding) {
|
||||
- if (!(encoding in cache)) cache[encoding] = caller.fetch(`https://tiktoken.pages.dev/js/${encoding}.json`).then((res) => res.json()).then((data) => new Tiktoken(data)).catch((e) => {
|
||||
- delete cache[encoding];
|
||||
- throw e;
|
||||
- });
|
||||
- return await cache[encoding];
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
async function encodingForModel(model) {
|
||||
- return getEncoding(getEncodingNameForModel(model));
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
|
||||
//#endregion
|
||||
diff --git a/package.json b/package.json
|
||||
index a24f8fc61de58526051999260f2ebee5f136354b..e885359e8966e7730c51772533ce37e01edb3046 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -20,7 +20,6 @@
|
||||
"ansi-styles": "^5.0.0",
|
||||
"camelcase": "6",
|
||||
"decamelize": "1.2.0",
|
||||
- "js-tiktoken": "^1.0.12",
|
||||
"langsmith": "^0.3.64",
|
||||
"mustache": "^4.2.0",
|
||||
"p-queue": "^6.6.2",
|
||||
@@ -1,19 +0,0 @@
|
||||
diff --git a/dist/embeddings.js b/dist/embeddings.js
|
||||
index 1f8154be3e9c22442a915eb4b85fa6d2a21b0d0c..dc13ef4a30e6c282824a5357bcee9bd0ae222aab 100644
|
||||
--- a/dist/embeddings.js
|
||||
+++ b/dist/embeddings.js
|
||||
@@ -214,10 +214,12 @@ export class OpenAIEmbeddings extends Embeddings {
|
||||
* @returns Promise that resolves to an embedding for the document.
|
||||
*/
|
||||
async embedQuery(text) {
|
||||
+ const isBaiduCloud = this.clientConfig.baseURL.includes('baidubce.com')
|
||||
+ const input = this.stripNewLines ? text.replace(/\n/g, ' ') : text
|
||||
const params = {
|
||||
model: this.model,
|
||||
- input: this.stripNewLines ? text.replace(/\n/g, " ") : text,
|
||||
- };
|
||||
+ input: isBaiduCloud ? [input] : input
|
||||
+ }
|
||||
if (this.dimensions) {
|
||||
params.dimensions = this.dimensions;
|
||||
}
|
||||
17
.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch
vendored
Normal file
17
.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
diff --git a/dist/embeddings.js b/dist/embeddings.js
|
||||
index 6f4b928d3e4717309382e1b5c2e31ab5bc6c5af0..bc79429c88a6d27d4997a2740c4d8ae0707f5991 100644
|
||||
--- a/dist/embeddings.js
|
||||
+++ b/dist/embeddings.js
|
||||
@@ -94,9 +94,11 @@ var OpenAIEmbeddings = class extends Embeddings {
|
||||
* @returns Promise that resolves to an embedding for the document.
|
||||
*/
|
||||
async embedQuery(text) {
|
||||
+ const isBaiduCloud = this.clientConfig.baseURL.includes('baidubce.com');
|
||||
+ const input = this.stripNewLines ? text.replace(/\n/g, " ") : text
|
||||
const params = {
|
||||
model: this.model,
|
||||
- input: this.stripNewLines ? text.replace(/\n/g, " ") : text
|
||||
+ input: isBaiduCloud ? [input] : input
|
||||
};
|
||||
if (this.dimensions) params.dimensions = this.dimensions;
|
||||
if (this.encodingFormat) params.encoding_format = this.encodingFormat;
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ Please review the following critical information before submitting your Pull Req
|
||||
Our core team is currently focused on significant architectural updates that involve these data structures. To ensure stability and focus during this period, contributions of this nature will be temporarily managed internally.
|
||||
|
||||
* **PRs that require changes to Redux state shape or IndexedDB schemas will be closed.**
|
||||
* **This restriction is temporary and will be lifted with the release of `v2.0.0`.** You can track the progress of `v2.0.0` and its related discussions on issue [#10162](https://github.com/YOUR_ORG/YOUR_REPO/issues/10162) (please replace with your actual repo link).
|
||||
* **This restriction is temporary and will be lifted with the release of `v2.0.0`.** You can track the progress of `v2.0.0` and its related discussions on issue [#10162](https://github.com/CherryHQ/cherry-studio/pull/10162).
|
||||
|
||||
We highly encourage contributions for:
|
||||
* Bug fixes 🐞
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
"quoteStyle": "single"
|
||||
}
|
||||
},
|
||||
"files": { "ignoreUnknown": false },
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**", "!**/.claude/**"],
|
||||
"maxSize": 2097152
|
||||
},
|
||||
"formatter": {
|
||||
"attributePosition": "auto",
|
||||
"bracketSameLine": false,
|
||||
|
||||
@@ -81,7 +81,7 @@ git commit --signoff -m "Your commit message"
|
||||
我们的核心团队目前正专注于涉及这些数据结构的关键架构更新和基础工作。为确保在此期间的稳定性与专注,此类贡献将暂时由内部进行管理。
|
||||
|
||||
* **需要更改 Redux 状态结构或 IndexedDB schema 的 PR 将会被关闭。**
|
||||
* **此限制是临时性的,并将在 `v2.0.0` 版本发布后解除。** 您可以通过 Issue [#10162](https://github.com/YOUR_ORG/YOUR_REPO/issues/10162) (请替换为您的实际仓库链接) 跟踪 `v2.0.0` 的进展及相关讨论。
|
||||
* **此限制是临时性的,并将在 `v2.0.0` 版本发布后解除。** 您可以通过 Issue [#10162](https://github.com/CherryHQ/cherry-studio/pull/10162) 跟踪 `v2.0.0` 的进展及相关讨论。
|
||||
|
||||
我们非常鼓励以下类型的贡献:
|
||||
* 错误修复 🐞
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
> [!NOTE]
|
||||
> This technical documentation was automatically generated by Claude Code based on analysis of the current OCR implementation in the codebase. The content reflects the architecture as of the current branch state.
|
||||
|
||||
# OCR Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Cherry Studio's OCR (Optical Character Recognition) system is a modular, extensible architecture designed to support multiple OCR providers and file types. The architecture follows a layered approach with clear separation of concerns between data access, business logic, and provider implementations.
|
||||
|
||||
## Architecture Layers
|
||||
|
||||
The OCR architecture follows a layered approach where data interactions occur through RESTful APIs, while IPC serves as part of the API layer, allowing the renderer to interact directly with the business layer:
|
||||
|
||||
### 1. API Layer
|
||||
**Location**: `src/main/data/api/handlers/`, `src/main/ipc.ts`, `src/preload/index.ts`
|
||||
|
||||
- **IPC Bridge**: Serves as API layer connecting renderer to main process
|
||||
- **Request Routing**: Routes IPC calls to appropriate service methods
|
||||
- **Type Safety**: Zod schemas for request/response validation
|
||||
- **Error Handling**: Centralized error propagation across process boundaries
|
||||
- **Security**: Secure communication sandbox between renderer and main processes
|
||||
|
||||
### 2. OCR Service Layer (Business Layer)
|
||||
**Location**: `src/main/services/ocr/`
|
||||
|
||||
- **OcrService**: Main business logic orchestrator and central coordinator
|
||||
- **Provider Registry**: Manages registered OCR providers
|
||||
- **Data Integration**: Direct interaction with data layer for provider management
|
||||
- **Lifecycle Management**: Handles provider initialization and disposal
|
||||
- **Validation**: Ensures provider availability and data integrity
|
||||
- **Orchestration**: Coordinates between providers and data services
|
||||
- **Direct IPC Access**: Renderer can directly invoke business layer methods via IPC
|
||||
|
||||
### 3. Provider Services Layer
|
||||
**Location**: `src/main/services/ocr/builtin/`
|
||||
|
||||
- **Base Service**: Abstract `OcrBaseService` defines common interface
|
||||
- **Data Independence**: No direct database interactions, relies on injected data
|
||||
- **Built-in Providers**:
|
||||
- `TesseractService`: Local Tesseract.js implementation
|
||||
- `SystemOcrService`: Platform-specific system OCR
|
||||
- `PpocrService`: PaddleOCR integration
|
||||
- `OvOcrService`: Intel OpenVINO (NPU) OCR
|
||||
- **Pure OCR Logic**: Focus solely on OCR processing capabilities
|
||||
|
||||
### 4. Data Layer
|
||||
**Location**: `src/main/data/db/schemas/ocr/`, `src/main/data/repositories/`
|
||||
|
||||
- **Database Schema**: Uses Drizzle ORM with SQLite database
|
||||
- **Repository Pattern**: `OcrProviderRepository` handles all database operations
|
||||
- **Provider Storage**: Stores provider configurations in `ocr_provider` table
|
||||
- **JSON Configuration**: Polymorphic `config` field stores provider-specific settings
|
||||
- **Data Access**: Exclusively accessed by OCR Service layer
|
||||
|
||||
### 5. Frontend Layer
|
||||
**Location**: `src/renderer/src/services/ocr/`, `src/renderer/src/hooks/ocr/`
|
||||
|
||||
- **Direct IPC Communication**: Direct interaction with business layer via IPC
|
||||
- **React Hooks**: Custom hooks for OCR operations and state management
|
||||
- **Configuration UI**: Settings pages for provider configuration
|
||||
- **State Management**: Frontend state synchronization with backend data
|
||||
|
||||
## Data Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Frontend UI] --> B[Frontend OCR Service]
|
||||
B --> C[API Layer - IPC Bridge]
|
||||
C --> D[OCR Service Layer - Business Logic]
|
||||
D --> E[Data Layer - Provider Repository]
|
||||
D --> F[Provider Services Layer]
|
||||
F --> G[OCR Processing]
|
||||
G --> H[Result]
|
||||
H --> F
|
||||
F --> D
|
||||
D --> C
|
||||
C --> B
|
||||
B --> A
|
||||
|
||||
style D fill:#e1f5fe
|
||||
style F fill:#f3e5f5
|
||||
style E fill:#e8f5e8
|
||||
style C fill:#fff3e0
|
||||
```
|
||||
|
||||
**Key Flow Characteristics:**
|
||||
- **Direct Business Access**: Frontend communicates directly with OCR Service layer via IPC
|
||||
- **IPC as API Gateway**: IPC bridge functions as the API layer, handling routing and validation
|
||||
- **Data Isolation**: Only business layer interacts with data persistence
|
||||
- **Provider Independence**: OCR providers remain isolated from data concerns
|
||||
|
||||
## Provider System
|
||||
|
||||
### Provider Registration
|
||||
- **Built-in Providers**: Automatically registered on service initialization
|
||||
- **Custom Providers**: Support for extensible provider system
|
||||
- **Configuration**: Each provider has its own configuration schema
|
||||
|
||||
### Provider Capabilities
|
||||
```typescript
|
||||
interface OcrProviderCapabilityRecord {
|
||||
image?: boolean // Image file OCR support
|
||||
pdf?: boolean // PDF file OCR support (future)
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Architecture
|
||||
- **Polymorphic Config**: JSON-based configuration adapts to provider needs
|
||||
- **Type Safety**: Zod schemas validate provider-specific configurations
|
||||
- **Runtime Validation**: Configuration validation before OCR operations
|
||||
|
||||
## Type System
|
||||
|
||||
### Core Types
|
||||
- **`OcrProvider`**: Base provider interface
|
||||
- **`OcrParams`**: OCR operation parameters
|
||||
- **`OcrResult`**: Standardized OCR result format
|
||||
- **`SupportedOcrFile`**: File types supported for OCR
|
||||
|
||||
### Business Types
|
||||
- **`OcrProviderBusiness`**: Domain-level provider representation
|
||||
- **Operations**: Create, Update, Replace, Delete operations
|
||||
- **Queries**: List providers with filtering options
|
||||
|
||||
### Provider-Specific Types
|
||||
- **TesseractConfig**: Language selection, model paths
|
||||
- **SystemOcrConfig**: Language preferences
|
||||
- **PaddleOCRConfig**: API endpoints, authentication
|
||||
- **OpenVINOConfig**: Device selection, model paths
|
||||
|
||||
## Built-in Providers
|
||||
|
||||
### Tesseract OCR
|
||||
- **Engine**: Tesseract.js
|
||||
- **Languages**: Multi-language support with automatic download
|
||||
- **Configuration**: Language selection, cache management
|
||||
- **Performance**: Worker pooling for concurrent processing
|
||||
|
||||
### System OCR
|
||||
- **Windows**: Windows Media Foundation OCR
|
||||
- **macOS**: Vision framework OCR
|
||||
- **Linux**: Platform-specific implementations
|
||||
- **Features**: Native performance, system integration
|
||||
|
||||
### PaddleOCR
|
||||
- **Deployment**: Remote API integration
|
||||
- **Languages**: Chinese, English, and mixed language support
|
||||
- **Configuration**: API endpoints and authentication
|
||||
|
||||
### Intel OpenVINO OCR
|
||||
- **Hardware**: NPU acceleration support
|
||||
- **Performance**: Optimized for Intel hardware
|
||||
- **Use Case**: High-performance OCR scenarios
|
||||
|
||||
## Configuration Management
|
||||
|
||||
### Database Schema
|
||||
```sql
|
||||
CREATE TABLE ocr_provider (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
capabilities TEXT NOT NULL, -- JSON
|
||||
config TEXT NOT NULL, -- JSON
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### Provider Defaults
|
||||
- **Initial Configuration**: Defined in `packages/shared/config/ocr.ts`
|
||||
- **Migration System**: Automatic provider initialization on startup
|
||||
- **User Customization**: Runtime configuration updates
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Categories
|
||||
- **Provider Errors**: OCR engine failures, missing dependencies
|
||||
- **Configuration Errors**: Invalid settings, missing parameters
|
||||
- **File Errors**: Unsupported formats, corrupted files
|
||||
- **System Errors**: Resource exhaustion, permissions
|
||||
|
||||
### Error Propagation
|
||||
- **Logging**: Centralized logging with context
|
||||
- **User Feedback**: Translated error messages
|
||||
- **Recovery**: Graceful fallback options
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Resource Management
|
||||
- **Worker Disposal**: Proper cleanup of OCR workers
|
||||
- **Memory Management**: Limits on file sizes and concurrent operations
|
||||
- **Caching**: Model and result caching where applicable
|
||||
|
||||
### Optimization
|
||||
- **Lazy Loading**: Providers initialized on demand
|
||||
- **Concurrent Processing**: Multiple workers for parallel operations
|
||||
- **Hardware Acceleration**: NPU and GPU support where available
|
||||
|
||||
## Security
|
||||
|
||||
### Input Validation
|
||||
- **File Type Checking**: Strict validation of supported formats
|
||||
- **Size Limits**: Protection against resource exhaustion
|
||||
- **Path Validation**: Prevention of path traversal attacks
|
||||
|
||||
### Configuration Security
|
||||
- **API Key Storage**: Secure storage of sensitive configuration
|
||||
- **Validation**: Runtime validation of configuration parameters
|
||||
- **Sandboxing**: Isolated execution of OCR operations
|
||||
|
||||
## Extension Points
|
||||
|
||||
### Custom Providers
|
||||
- **Interface**: Implement `OcrBaseService` for new providers
|
||||
- **Registration**: Dynamic provider registration system
|
||||
- **Configuration**: Extensible configuration schemas
|
||||
|
||||
### File Type Support
|
||||
- **Handlers**: Modular file type processors
|
||||
- **Capabilities**: Declarative provider capabilities
|
||||
- **Future Support**: PDF, document formats planned
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Legacy System
|
||||
- **Data Migration**: Automatic migration from old configuration formats
|
||||
- **Compatibility**: Backward compatibility during transition
|
||||
- **Testing**: Comprehensive test coverage for migration paths
|
||||
|
||||
### Future Enhancements
|
||||
- **PDF Support**: Planned extension to document OCR
|
||||
- **Cloud Providers**: API-based OCR services integration
|
||||
- **AI Enhancement**: Post-processing and accuracy improvements
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Adding New Providers
|
||||
1. Create provider service extending `OcrBaseService`
|
||||
2. Define provider-specific configuration schema
|
||||
3. Register provider in `OcrService`
|
||||
4. Add configuration UI components
|
||||
5. Include comprehensive tests
|
||||
|
||||
> [!WARNING]
|
||||
> Provider services should never directly access the data layer. All data operations must go through the OCR Service layer to maintain proper separation of concerns.
|
||||
|
||||
### Configuration Changes
|
||||
1. Update provider configuration schema
|
||||
2. Add migration logic for existing configurations
|
||||
3. Update UI validation and error handling
|
||||
4. Test with various configuration scenarios
|
||||
|
||||
> [!WARNING]
|
||||
> Always validate configuration changes before saving to the database. Use Zod schemas for runtime validation to prevent corrupted provider configurations.
|
||||
|
||||
### Testing
|
||||
- **Unit Tests**: Provider implementation testing
|
||||
- **Integration Tests**: End-to-end OCR workflows
|
||||
- **Performance Tests**: Resource usage and timing
|
||||
- **Error Scenarios**: Comprehensive error handling testing
|
||||
@@ -1,260 +0,0 @@
|
||||
> [!NOTE]
|
||||
> 本技术文档由 Claude Code 基于对当前代码库中 OCR 实现的分析自动生成。内容反映了当前分支状态的架构设计。
|
||||
|
||||
# OCR 架构文档
|
||||
|
||||
## 概述
|
||||
|
||||
Cherry Studio 的 OCR(光学字符识别)系统是一个模块化、可扩展的架构,旨在支持多个 OCR 提供商和文件类型。该架构采用分层设计,在数据访问、业务逻辑和提供商实现之间有明确的关注点分离。
|
||||
|
||||
## 架构分层
|
||||
|
||||
OCR 架构采用分层方法,其中数据交互通过 RESTful API 进行,而 IPC 作为 API 层的一部分,允许 Renderer 直接与业务层交互:
|
||||
|
||||
### 1. API 层
|
||||
**位置**: `src/main/data/api/handlers/`, `src/main/ipc.ts`, `src/preload/index.ts`
|
||||
|
||||
- **IPC 桥接**: 作为 API 层连接 Renderer 到主进程
|
||||
- **请求路由**: 将 IPC 调用路由到相应的服务方法
|
||||
- **类型安全**: 使用 Zod 模式进行请求/响应验证
|
||||
- **错误处理**: 跨进程边界的集中式错误传播
|
||||
- **安全**: Renderer 和主进程之间的安全通信沙盒
|
||||
|
||||
### 2. OCR 服务层(业务层)
|
||||
**位置**: `src/main/services/ocr/`
|
||||
|
||||
- **OcrService**: 主要业务逻辑协调器和中央协调器
|
||||
- **提供商注册表**: 管理已注册的 OCR 提供商
|
||||
- **数据集成**: 与数据层直接交互进行提供商管理
|
||||
- **生命周期管理**: 处理提供商初始化和销毁
|
||||
- **验证**: 确保提供商可用性和数据完整性
|
||||
- **协调**: 协调提供商和数据服务之间的交互
|
||||
- **直接 IPC 访问**: Renderer 可通过 IPC 直接调用业务层方法
|
||||
|
||||
### 3. 提供商服务层
|
||||
**位置**: `src/main/services/ocr/builtin/`
|
||||
|
||||
- **基础服务**: 抽象的 `OcrBaseService` 定义通用接口
|
||||
- **数据独立性**: 无直接数据库交互,依赖外部传入的数据
|
||||
- **内置提供商**:
|
||||
- `TesseractService`: 本地 Tesseract.js 实现
|
||||
- `SystemOcrService`: 平台特定的系统 OCR
|
||||
- `PpocrService`: PaddleOCR 集成
|
||||
- `OvOcrService`: Intel OpenVINO (NPU) OCR
|
||||
- **纯 OCR 逻辑**: 专注于 OCR 处理能力
|
||||
|
||||
### 4. 数据层
|
||||
**位置**: `src/main/data/db/schemas/ocr/`, `src/main/data/repositories/`
|
||||
|
||||
- **数据库架构**: 使用 Drizzle ORM 和 SQLite 数据库
|
||||
- **仓储模式**: `OcrProviderRepository` 处理所有数据库操作
|
||||
- **提供商存储**: 在 `ocr_provider` 表中存储提供商配置
|
||||
- **JSON 配置**: 多态的 `config` 字段存储提供商特定的设置
|
||||
- **数据访问**: 仅由 OCR 服务层访问
|
||||
|
||||
### 5. Renderer 层
|
||||
**位置**: `src/renderer/src/services/ocr/`, `src/renderer/src/hooks/ocr/`
|
||||
|
||||
- **直接 IPC 通信**: 通过 IPC 与业务层直接交互
|
||||
- **React Hooks**: 用于 OCR 操作和状态管理的自定义钩子
|
||||
- **配置 UI**: 提供商配置的设置页面
|
||||
- **状态管理**: Renderer 状态与后端数据同步
|
||||
|
||||
## 数据流
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Renderer UI] --> B[Renderer OCR 服务]
|
||||
B --> C[API 层 - IPC 桥接]
|
||||
C --> D[OCR 服务层 - 业务逻辑]
|
||||
D --> E[数据层 - 提供商仓储]
|
||||
D --> F[提供商服务层]
|
||||
F --> G[OCR 处理]
|
||||
G --> H[结果]
|
||||
H --> F
|
||||
F --> D
|
||||
D --> C
|
||||
C --> B
|
||||
B --> A
|
||||
|
||||
style D fill:#e1f5fe
|
||||
style F fill:#f3e5f5
|
||||
style E fill:#e8f5e8
|
||||
style C fill:#fff3e0
|
||||
```
|
||||
|
||||
**关键流程特征**:
|
||||
- **直接业务访问**: Renderer 通过 IPC 与 OCR 服务层直接通信
|
||||
- **IPC 作为 API 网关**: IPC 桥接作为 API 层,处理路由和验证
|
||||
- **数据隔离**: 只有业务层与数据持久化交互
|
||||
- **提供商独立性**: OCR 提供商保持与数据关注点的隔离
|
||||
|
||||
## 提供商系统
|
||||
|
||||
### 提供商注册
|
||||
- **内置提供商**: 在服务初始化时自动注册
|
||||
- **自定义提供商**: 支持可扩展的提供商系统
|
||||
- **配置**: 每个提供商都有自己的配置模式
|
||||
|
||||
### 提供商能力
|
||||
```typescript
|
||||
interface OcrProviderCapabilityRecord {
|
||||
image?: boolean // 图像文件 OCR 支持
|
||||
pdf?: boolean // PDF 文件 OCR 支持(未来)
|
||||
}
|
||||
```
|
||||
|
||||
### 配置架构
|
||||
- **多态配置**: 基于 JSON 的配置适应提供商需求
|
||||
- **类型安全**: Zod 模式验证提供商特定的配置
|
||||
- **运行时验证**: OCR 操作前的配置验证
|
||||
|
||||
## 类型系统
|
||||
|
||||
### 核心类型
|
||||
- **`OcrProvider`**: 基础提供商接口
|
||||
- **`OcrParams`**: OCR 操作参数
|
||||
- **`OcrResult`**: 标准化的 OCR 结果格式
|
||||
- **`SupportedOcrFile`**: 支持 OCR 的文件类型
|
||||
|
||||
### 业务类型
|
||||
- **`OcrProviderBusiness`**: 域级别的提供商表示
|
||||
- **操作**: 创建、更新、替换、删除操作
|
||||
- **查询**: 带过滤选项的提供商列表
|
||||
|
||||
### 提供商特定类型
|
||||
- **TesseractConfig**: 语言选择、模型路径
|
||||
- **SystemOcrConfig**: 语言偏好
|
||||
- **PaddleOCRConfig**: API 端点、认证
|
||||
- **OpenVINOConfig**: 设备选择、模型路径
|
||||
|
||||
## 内置提供商
|
||||
|
||||
### Tesseract OCR
|
||||
- **引擎**: Tesseract.js
|
||||
- **语言**: 支持多语言,自动下载
|
||||
- **配置**: 语言选择、缓存管理
|
||||
- **性能**: 工作池用于并发处理
|
||||
|
||||
### 系统 OCR
|
||||
- **Windows**: Windows Media Foundation OCR
|
||||
- **macOS**: Vision 框架 OCR
|
||||
- **Linux**: 平台特定实现
|
||||
- **特性**: 原生性能、系统集成
|
||||
|
||||
### PaddleOCR
|
||||
- **部署**: 远程 API 集成
|
||||
- **语言**: 中文、英文和混合语言支持
|
||||
- **配置**: API 端点和认证
|
||||
|
||||
### Intel OpenVINO OCR
|
||||
- **硬件**: NPU 加速支持
|
||||
- **性能**: 为 Intel 硬件优化
|
||||
- **用例**: 高性能 OCR 场景
|
||||
|
||||
## 配置管理
|
||||
|
||||
### 数据库架构
|
||||
```sql
|
||||
CREATE TABLE ocr_provider (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
capabilities TEXT NOT NULL, -- JSON
|
||||
config TEXT NOT NULL, -- JSON
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### 提供商默认值
|
||||
- **初始配置**: 在 `packages/shared/config/ocr.ts` 中定义
|
||||
- **迁移系统**: 启动时自动提供商初始化
|
||||
- **用户自定义**: 运行时配置更新
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 错误类别
|
||||
- **提供商错误**: OCR 引擎故障、缺少依赖
|
||||
- **配置错误**: 无效设置、缺少参数
|
||||
- **文件错误**: 不支持的格式、损坏的文件
|
||||
- **系统错误**: 资源耗尽、权限问题
|
||||
|
||||
### 错误传播
|
||||
- **日志**: 带上下文的集中日志记录
|
||||
- **用户反馈**: 翻译的错误消息
|
||||
- **恢复**: 优雅的回退选项
|
||||
|
||||
## 性能考虑
|
||||
|
||||
### 资源管理
|
||||
- **工作器销毁**: OCR 工作器的适当清理
|
||||
- **内存管理**: 文件大小和并发操作限制
|
||||
- **缓存**: 模型和结果缓存(如适用)
|
||||
|
||||
### 优化
|
||||
- **延迟加载**: 按需初始化提供商
|
||||
- **并发处理**: 多工作器用于并行操作
|
||||
- **硬件加速**: NPU 和 GPU 支持(如可用)
|
||||
|
||||
## 安全
|
||||
|
||||
### 输入验证
|
||||
- **文件类型检查**: 严格验证支持的格式
|
||||
- **大小限制**: 防止资源耗尽
|
||||
- **路径验证**: 防止路径遍历攻击
|
||||
|
||||
### 配置安全
|
||||
- **API 密钥存储**: 敏感配置的安全存储
|
||||
- **验证**: 配置参数的运行时验证
|
||||
- **沙盒**: OCR 操作的隔离执行
|
||||
|
||||
## 扩展点
|
||||
|
||||
### 自定义提供商
|
||||
- **接口**: 为新提供商实现 `OcrBaseService`
|
||||
- **注册**: 动态提供商注册系统
|
||||
- **配置**: 可扩展的配置模式
|
||||
|
||||
### 文件类型支持
|
||||
- **处理器**: 模块化文件类型处理器
|
||||
- **能力**: 声明式提供商能力
|
||||
- **未来支持**: PDF、文档格式计划中
|
||||
|
||||
## 迁移策略
|
||||
|
||||
### 遗留系统
|
||||
- **数据迁移**: 从旧配置格式自动迁移
|
||||
- **兼容性**: 过渡期间的向后兼容性
|
||||
- **测试**: 迁移路径的全面测试覆盖
|
||||
|
||||
### 未来增强
|
||||
- **PDF 支持**: 计划扩展到文档 OCR
|
||||
- **云提供商**: 基于 API 的 OCR 服务集成
|
||||
- **AI 增强**: 后处理和准确性改进
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 添加新提供商
|
||||
1. 创建扩展 `OcrBaseService` 的提供商服务
|
||||
2. 定义提供商特定的配置模式
|
||||
3. 在 `OcrService` 中注册提供商
|
||||
4. 添加配置 UI 组件
|
||||
5. 包含全面的测试
|
||||
|
||||
> [!WARNING]
|
||||
> 提供商服务绝不应直接访问数据层。所有数据操作必须通过 OCR 服务层进行,以保持适当的关注点分离。
|
||||
|
||||
### 配置更改
|
||||
1. 更新提供商配置模式
|
||||
2. 为现有配置添加迁移逻辑
|
||||
3. 更新 UI 验证和错误处理
|
||||
4. 测试各种配置场景
|
||||
|
||||
> [!WARNING]
|
||||
> 在保存到数据库之前,务必验证配置更改。使用 Zod 模式进行运行时验证,防止提供商配置损坏。
|
||||
|
||||
### 测试
|
||||
- **单元测试**: 提供商实现测试
|
||||
- **集成测试**: 端到端 OCR 工作流
|
||||
- **性能测试**: 资源使用和时间
|
||||
- **错误场景**: 全面的错误处理测试
|
||||
@@ -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}
|
||||
@@ -130,60 +134,116 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
<!--LANG:en-->
|
||||
What's New in v1.7.0-beta.2
|
||||
What's New in v1.7.0-beta.3
|
||||
|
||||
New Features:
|
||||
- Session Settings: Manage session-specific settings and model configurations independently
|
||||
- Notes Full-Text Search: Search across all notes with match highlighting
|
||||
- Built-in DiDi MCP Server: Integration with DiDi ride-hailing services (China only)
|
||||
- Intel OV OCR: Hardware-accelerated OCR using Intel NPU
|
||||
- Auto-start API Server: Automatically starts when agents exist
|
||||
- Enhanced Tool Permission System: Real-time tool approval interface with improved UX
|
||||
- Plugin Management System: Support for Claude Agent plugins (agents, commands, skills)
|
||||
- Skill Tool: Add skill execution capabilities for agents
|
||||
- Mobile App Data Restore: Support restoring data to mobile applications
|
||||
- OpenMinerU Preprocessor: Knowledge base now supports open-source MinerU for document processing
|
||||
- HuggingFace Provider: Added HuggingFace as AI provider
|
||||
- Claude Haiku 4.5: Support for the latest Claude Haiku 4.5 model
|
||||
- Ling Series Models: Added support for Ling-1T and related models
|
||||
- Intel OVMS Painting: New painting provider using Intel OpenVINO Model Server
|
||||
- Automatic Update Checks: Implement periodic update checking with configurable intervals
|
||||
- HuggingChat Mini App: New mini app for HuggingChat integration
|
||||
|
||||
Improvements:
|
||||
- Agent model selection now requires explicit user choice
|
||||
- Added Mistral AI provider support
|
||||
- Added NewAPI generic provider support
|
||||
- Improved navbar layout consistency across different modes
|
||||
- Enhanced chat component responsiveness
|
||||
- Better code block display on small screens
|
||||
- Updated OVMS to 2025.3 official release
|
||||
- Added Greek language support
|
||||
- Agent Creation: New agents are now automatically activated upon creation
|
||||
- Lazy Loading: Optimize page load performance with route lazy loading
|
||||
- UI Enhancements: Improved agent item styling and layout consistency
|
||||
- Navigation: Better navbar layout for fullscreen mode on macOS
|
||||
- Settings Tab: Enhanced context slider consistency
|
||||
- Backup Manager: Unified footer layout for local and S3 backup managers
|
||||
- Menu System: Enhanced application menu with improved help section
|
||||
- Proxy Rules: Comprehensive proxy bypass rule matching
|
||||
- German Language: Added German language support
|
||||
- MCP Confirmation: Added confirmation modal when activating protocol-installed MCP servers
|
||||
- Translation: Enhanced translation script with concurrency and validation
|
||||
- Electron & Vite: Updated to Electron 38 and Vite 4.0.1
|
||||
|
||||
Claude Code Tool Improvements:
|
||||
- GlobTool: Now counts lines instead of files in output for better clarity
|
||||
- ReadTool: Automatically removes system reminder tags from output
|
||||
- TodoWriteTool: Improved rendering behavior
|
||||
- Environment Variables: Updated model-related environment variable names
|
||||
|
||||
Bug Fixes:
|
||||
- Fixed GitHub Copilot gpt-5-codex streaming issues
|
||||
- Fixed assistant creation failures
|
||||
- Fixed translate auto-copy functionality
|
||||
- Fixed miniapps external link opening
|
||||
- Fixed message layout and overflow issues
|
||||
- Fixed API key parsing to preserve spaces
|
||||
- Fixed agent display in different navbar layouts
|
||||
- Fixed session model not being used when sending messages
|
||||
- Fixed tool approval UI and shared workspace plugin inconsistencies
|
||||
- Fixed API server readiness notification to renderer
|
||||
- Fixed grouped items not respecting saved tag order
|
||||
- Fixed assistant/agent activation when creating new ones
|
||||
- Fixed Dashscope Anthropic API host and migrated old configs
|
||||
- Fixed Qwen3 thinking mode control for Ollama
|
||||
- Fixed disappeared MCP button
|
||||
- Fixed create assistant causing blank screen
|
||||
- Fixed up-down button visibility in some cases
|
||||
- Fixed hooks preventing save on composing enter key
|
||||
- Fixed Azure GPT-image-1 and OpenRouter Gemini-image
|
||||
- Fixed Silicon reasoning issues
|
||||
- Fixed topic branch incomplete copy with two-pass ID mapping
|
||||
- Fixed deep research model search context restrictions
|
||||
- Fixed model capability checking logic
|
||||
- Fixed reranker API error response capture
|
||||
- Fixed right-click paste file content into inputbar
|
||||
- Fixed minimax-m2 support in aiCore
|
||||
|
||||
<!--LANG:zh-CN-->
|
||||
v1.7.0-beta.2 新特性
|
||||
v1.7.0-beta.3 新特性
|
||||
|
||||
新功能:
|
||||
- 会话设置:独立管理会话特定的设置和模型配置
|
||||
- 笔记全文搜索:跨所有笔记搜索并高亮匹配内容
|
||||
- 内置滴滴 MCP 服务器:集成滴滴打车服务(仅限中国地区)
|
||||
- Intel OV OCR:使用 Intel NPU 的硬件加速 OCR
|
||||
- 自动启动 API 服务器:当存在 Agent 时自动启动
|
||||
- 增强工具权限系统:实时工具审批界面,改进用户体验
|
||||
- 插件管理系统:支持 Claude Agent 插件(agents、commands、skills)
|
||||
- 技能工具:为 Agent 添加技能执行能力
|
||||
- 移动应用数据恢复:支持将数据恢复到移动应用程序
|
||||
- OpenMinerU 预处理器:知识库现支持使用开源 MinerU 处理文档
|
||||
- HuggingFace 提供商:添加 HuggingFace 作为 AI 提供商
|
||||
- Claude Haiku 4.5:支持最新的 Claude Haiku 4.5 模型
|
||||
- Ling 系列模型:添加 Ling-1T 及相关模型支持
|
||||
- Intel OVMS 绘图:使用 Intel OpenVINO 模型服务器的新绘图提供商
|
||||
- 自动更新检查:实现可配置间隔的定期更新检查
|
||||
- HuggingChat 小程序:新增 HuggingChat 集成小程序
|
||||
|
||||
改进:
|
||||
- Agent 模型选择现在需要用户显式选择
|
||||
- 添加 Mistral AI 提供商支持
|
||||
- 添加 NewAPI 通用提供商支持
|
||||
- 改进不同模式下的导航栏布局一致性
|
||||
- 增强聊天组件响应式设计
|
||||
- 优化小屏幕代码块显示
|
||||
- 更新 OVMS 至 2025.3 正式版
|
||||
- 添加希腊语支持
|
||||
- Agent 创建:新创建的 Agent 现在会自动激活
|
||||
- 懒加载:通过路由懒加载优化页面加载性能
|
||||
- UI 增强:改进 Agent 项目样式和布局一致性
|
||||
- 导航:改进 macOS 全屏模式下的导航栏布局
|
||||
- 设置选项卡:增强上下文滑块一致性
|
||||
- 备份管理器:统一本地和 S3 备份管理器的页脚布局
|
||||
- 菜单系统:增强应用菜单,改进帮助部分
|
||||
- 代理规则:全面的代理绕过规则匹配
|
||||
- 德语支持:添加德语语言支持
|
||||
- MCP 确认:添加激活协议安装的 MCP 服务器时的确认模态框
|
||||
- 翻译:增强翻译脚本的并发和验证功能
|
||||
- Electron & Vite:更新至 Electron 38 和 Vite 4.0.1
|
||||
|
||||
Claude Code 工具改进:
|
||||
- GlobTool:现在计算行数而不是文件数,提供更清晰的输出
|
||||
- ReadTool:自动从输出中移除系统提醒标签
|
||||
- TodoWriteTool:改进渲染行为
|
||||
- 环境变量:更新模型相关的环境变量名称
|
||||
|
||||
问题修复:
|
||||
- 修复 GitHub Copilot gpt-5-codex 流式传输问题
|
||||
- 修复助手创建失败
|
||||
- 修复翻译自动复制功能
|
||||
- 修复小程序外部链接打开
|
||||
- 修复消息布局和溢出问题
|
||||
- 修复 API 密钥解析以保留空格
|
||||
- 修复不同导航栏布局中的 Agent 显示
|
||||
- 修复发送消息时未使用会话模型
|
||||
- 修复工具审批 UI 和共享工作区插件不一致
|
||||
- 修复 API 服务器就绪通知到渲染器
|
||||
- 修复分组项目不遵守已保存标签顺序
|
||||
- 修复创建新的助手/Agent 时的激活问题
|
||||
- 修复 Dashscope Anthropic API 主机并迁移旧配置
|
||||
- 修复 Ollama 的 Qwen3 思考模式控制
|
||||
- 修复 MCP 按钮消失
|
||||
- 修复创建助手导致空白屏幕
|
||||
- 修复某些情况下上下按钮可见性
|
||||
- 修复钩子在输入法输入时阻止保存
|
||||
- 修复 Azure GPT-image-1 和 OpenRouter Gemini-image
|
||||
- 修复 Silicon 推理问题
|
||||
- 修复主题分支不完整复制,采用两阶段 ID 映射
|
||||
- 修复深度研究模型搜索上下文限制
|
||||
- 修复模型能力检查逻辑
|
||||
- 修复 reranker API 错误响应捕获
|
||||
- 修复右键粘贴文件内容到输入栏
|
||||
- 修复 aiCore 中的 minimax-m2 支持
|
||||
<!--LANG:END-->
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
CREATE TABLE `ocr_provider` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`capabilities` text NOT NULL,
|
||||
`config` text NOT NULL,
|
||||
`created_at` integer,
|
||||
`updated_at` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `name` ON `ocr_provider` (`name`);
|
||||
@@ -1,172 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "64f7ad88-7111-4574-988c-d7ef429e375d",
|
||||
"prevId": "de8009d7-95b9-4f99-99fa-4b8795708f21",
|
||||
"tables": {
|
||||
"app_state": {
|
||||
"name": "app_state",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"ocr_provider": {
|
||||
"name": "ocr_provider",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"capabilities": {
|
||||
"name": "capabilities",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"name": {
|
||||
"name": "name",
|
||||
"columns": ["name"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"preference": {
|
||||
"name": "preference",
|
||||
"columns": {
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"scope_name_idx": {
|
||||
"name": "scope_name_idx",
|
||||
"columns": ["scope", "key"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,6 @@
|
||||
"tag": "0000_solid_lord_hawal",
|
||||
"version": "6",
|
||||
"when": 1754745234572
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1760969721294,
|
||||
"tag": "0001_previous_sir_ram",
|
||||
"breakpoints": true
|
||||
}
|
||||
],
|
||||
"version": "7"
|
||||
|
||||
45
package.json
45
package.json
@@ -81,21 +81,24 @@
|
||||
"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",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"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",
|
||||
"os-proxy-config": "^1.1.2",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"selection-hook": "^1.0.12",
|
||||
"sharp": "0.34.4",
|
||||
"sharp": "^0.34.3",
|
||||
"socket.io": "^4.8.1",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"tesseract.js": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||
@@ -105,8 +108,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",
|
||||
@@ -148,7 +151,9 @@
|
||||
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@heroui/react": "^2.8.3",
|
||||
"@langchain/community": "^0.3.50",
|
||||
"@langchain/community": "^1.0.0",
|
||||
"@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
|
||||
"@langchain/openai": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
|
||||
"@mistralai/mistralai": "^1.7.5",
|
||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
@@ -199,6 +204,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",
|
||||
@@ -228,7 +234,7 @@
|
||||
"@viz-js/lang-dot": "^1.0.5",
|
||||
"@viz-js/viz": "^3.14.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",
|
||||
@@ -238,6 +244,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",
|
||||
@@ -372,9 +379,7 @@
|
||||
"@codemirror/language": "6.11.3",
|
||||
"@codemirror/lint": "6.8.5",
|
||||
"@codemirror/view": "6.38.1",
|
||||
"@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A0.3.44#~/.yarn/patches/@langchain-core-npm-0.3.44-41d5c3cb0a.patch",
|
||||
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
|
||||
"app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch",
|
||||
"app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch",
|
||||
"atomically@npm:^1.7.0": "patch:atomically@npm%3A1.7.0#~/.yarn/patches/atomically-npm-1.7.0-e742e5293b.patch",
|
||||
@@ -390,14 +395,18 @@
|
||||
"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",
|
||||
"@img/sharp-darwin-arm64": "0.34.4",
|
||||
"@img/sharp-darwin-x64": "0.34.4",
|
||||
"@img/sharp-linux-arm": "0.34.4",
|
||||
"@img/sharp-linux-arm64": "0.34.4",
|
||||
"@img/sharp-linux-x64": "0.34.4",
|
||||
"@img/sharp-win32-x64": "0.34.4",
|
||||
"openai@npm:5.12.2": "npm:@cherrystudio/openai@6.5.0"
|
||||
"@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",
|
||||
"@img/sharp-linux-arm64": "0.34.3",
|
||||
"@img/sharp-linux-x64": "0.34.3",
|
||||
"@img/sharp-win32-x64": "0.34.3",
|
||||
"openai@npm:5.12.2": "npm:@cherrystudio/openai@6.5.0",
|
||||
"@langchain/openai@npm:>=0.1.0 <0.6.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
|
||||
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
|
||||
"@langchain/openai@npm:>=0.2.0 <0.7.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"lint-staged": {
|
||||
|
||||
@@ -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',
|
||||
@@ -350,6 +354,7 @@ export enum IpcChannel {
|
||||
ApiServer_Stop = 'api-server:stop',
|
||||
ApiServer_Restart = 'api-server:restart',
|
||||
ApiServer_GetStatus = 'api-server:get-status',
|
||||
ApiServer_Ready = 'api-server:ready',
|
||||
// NOTE: This api is not be used.
|
||||
ApiServer_GetConfig = 'api-server:get-config',
|
||||
|
||||
@@ -369,7 +374,8 @@ export enum IpcChannel {
|
||||
CodeTools_RemoveCustomTerminalPath = 'code-tools:remove-custom-terminal-path',
|
||||
|
||||
// OCR
|
||||
OCR_Ocr = 'ocr:ocr',
|
||||
OCR_ocr = 'ocr:ocr',
|
||||
OCR_ListProviders = 'ocr:list-providers',
|
||||
|
||||
// OVMS
|
||||
Ovms_AddModel = 'ovms:add-model',
|
||||
@@ -381,5 +387,21 @@ 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',
|
||||
|
||||
// WebSocket
|
||||
WebSocket_Start = 'webSocket:start',
|
||||
WebSocket_Stop = 'webSocket:stop',
|
||||
WebSocket_Status = 'webSocket:status',
|
||||
WebSocket_SendFile = 'webSocket:send-file',
|
||||
WebSocket_GetAllCandidates = 'webSocket:get-all-candidates'
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
import type {
|
||||
BuiltinOcrProvider,
|
||||
BuiltinOcrProviderId,
|
||||
OcrOvProvider,
|
||||
OcrPpocrProvider,
|
||||
OcrSystemProvider,
|
||||
OcrTesseractProvider,
|
||||
TesseractLangCode
|
||||
} from '@types'
|
||||
|
||||
import type { TranslateLanguageCode } from '../../../src/renderer/src/types/translate'
|
||||
|
||||
export const tesseract: OcrTesseractProvider = {
|
||||
id: 'tesseract',
|
||||
name: 'Tesseract',
|
||||
capabilities: {
|
||||
image: true
|
||||
},
|
||||
config: {
|
||||
langs: {
|
||||
chi_sim: true,
|
||||
chi_tra: true,
|
||||
eng: true
|
||||
},
|
||||
enabled: false
|
||||
}
|
||||
} as const
|
||||
|
||||
export const systemOcr: OcrSystemProvider = {
|
||||
id: 'system',
|
||||
name: 'System',
|
||||
capabilities: {
|
||||
image: true
|
||||
// pdf: true
|
||||
},
|
||||
config: {
|
||||
langs: ['en-us'],
|
||||
enabled: false
|
||||
}
|
||||
} as const satisfies OcrSystemProvider
|
||||
|
||||
export const ppocrOcr: OcrPpocrProvider = {
|
||||
id: 'paddleocr',
|
||||
name: 'PaddleOCR',
|
||||
capabilities: {
|
||||
image: true
|
||||
// pdf: true
|
||||
},
|
||||
config: { apiUrl: '', enabled: false }
|
||||
} as const
|
||||
|
||||
export const ovOcr: OcrOvProvider = {
|
||||
id: 'ovocr',
|
||||
name: 'Intel OV(NPU) OCR',
|
||||
capabilities: {
|
||||
image: true
|
||||
// pdf: true
|
||||
},
|
||||
config: {
|
||||
enabled: false
|
||||
}
|
||||
} as const satisfies OcrOvProvider
|
||||
|
||||
export const INITIAL_BUILTIN_OCR_PROVIDER_MAP = {
|
||||
tesseract,
|
||||
system: systemOcr,
|
||||
paddleocr: ppocrOcr,
|
||||
ovocr: ovOcr
|
||||
} as const satisfies Record<BuiltinOcrProviderId, BuiltinOcrProvider>
|
||||
|
||||
export const BUILTIN_OCR_PROVIDERS: BuiltinOcrProvider[] = Object.values(INITIAL_BUILTIN_OCR_PROVIDER_MAP)
|
||||
|
||||
export const TESSERACT_LANG_MAP: Record<TranslateLanguageCode, TesseractLangCode> = {
|
||||
'af-za': 'afr',
|
||||
'am-et': 'amh',
|
||||
'ar-sa': 'ara',
|
||||
'as-in': 'asm',
|
||||
'az-az': 'aze',
|
||||
'az-cyrl-az': 'aze_cyrl',
|
||||
'be-by': 'bel',
|
||||
'bn-bd': 'ben',
|
||||
'bo-cn': 'bod',
|
||||
'bs-ba': 'bos',
|
||||
'bg-bg': 'bul',
|
||||
'ca-es': 'cat',
|
||||
'ceb-ph': 'ceb',
|
||||
'cs-cz': 'ces',
|
||||
'zh-cn': 'chi_sim',
|
||||
'zh-tw': 'chi_tra',
|
||||
'chr-us': 'chr',
|
||||
'cy-gb': 'cym',
|
||||
'da-dk': 'dan',
|
||||
'de-de': 'deu',
|
||||
'dz-bt': 'dzo',
|
||||
'el-gr': 'ell',
|
||||
'en-us': 'eng',
|
||||
'enm-gb': 'enm',
|
||||
'eo-world': 'epo',
|
||||
'et-ee': 'est',
|
||||
'eu-es': 'eus',
|
||||
'fa-ir': 'fas',
|
||||
'fi-fi': 'fin',
|
||||
'fr-fr': 'fra',
|
||||
'frk-de': 'frk',
|
||||
'frm-fr': 'frm',
|
||||
'ga-ie': 'gle',
|
||||
'gl-es': 'glg',
|
||||
'grc-gr': 'grc',
|
||||
'gu-in': 'guj',
|
||||
'ht-ht': 'hat',
|
||||
'he-il': 'heb',
|
||||
'hi-in': 'hin',
|
||||
'hr-hr': 'hrv',
|
||||
'hu-hu': 'hun',
|
||||
'iu-ca': 'iku',
|
||||
'id-id': 'ind',
|
||||
'is-is': 'isl',
|
||||
'it-it': 'ita',
|
||||
'ita-it': 'ita_old',
|
||||
'jv-id': 'jav',
|
||||
'ja-jp': 'jpn',
|
||||
'kn-in': 'kan',
|
||||
'ka-ge': 'kat',
|
||||
'kat-ge': 'kat_old',
|
||||
'kk-kz': 'kaz',
|
||||
'km-kh': 'khm',
|
||||
'ky-kg': 'kir',
|
||||
'ko-kr': 'kor',
|
||||
'ku-tr': 'kur',
|
||||
'la-la': 'lao',
|
||||
'la-va': 'lat',
|
||||
'lv-lv': 'lav',
|
||||
'lt-lt': 'lit',
|
||||
'ml-in': 'mal',
|
||||
'mr-in': 'mar',
|
||||
'mk-mk': 'mkd',
|
||||
'mt-mt': 'mlt',
|
||||
'ms-my': 'msa',
|
||||
'my-mm': 'mya',
|
||||
'ne-np': 'nep',
|
||||
'nl-nl': 'nld',
|
||||
'no-no': 'nor',
|
||||
'or-in': 'ori',
|
||||
'pa-in': 'pan',
|
||||
'pl-pl': 'pol',
|
||||
'pt-pt': 'por',
|
||||
'ps-af': 'pus',
|
||||
'ro-ro': 'ron',
|
||||
'ru-ru': 'rus',
|
||||
'sa-in': 'san',
|
||||
'si-lk': 'sin',
|
||||
'sk-sk': 'slk',
|
||||
'sl-si': 'slv',
|
||||
'es-es': 'spa',
|
||||
'spa-es': 'spa_old',
|
||||
'sq-al': 'sqi',
|
||||
'sr-rs': 'srp',
|
||||
'sr-latn-rs': 'srp_latn',
|
||||
'sw-tz': 'swa',
|
||||
'sv-se': 'swe',
|
||||
'syr-sy': 'syr',
|
||||
'ta-in': 'tam',
|
||||
'te-in': 'tel',
|
||||
'tg-tj': 'tgk',
|
||||
'tl-ph': 'tgl',
|
||||
'th-th': 'tha',
|
||||
'ti-er': 'tir',
|
||||
'tr-tr': 'tur',
|
||||
'ug-cn': 'uig',
|
||||
'uk-ua': 'ukr',
|
||||
'ur-pk': 'urd',
|
||||
'uz-uz': 'uzb',
|
||||
'uz-cyrl-uz': 'uzb_cyrl',
|
||||
'vi-vn': 'vie',
|
||||
'yi-us': 'yid'
|
||||
}
|
||||
@@ -1,18 +1,5 @@
|
||||
// NOTE: Types are defined inline in the schema for simplicity
|
||||
// If needed, specific types can be imported from './apiModels'
|
||||
import type {
|
||||
CreateOcrProviderRequest,
|
||||
CreateOcrProviderResponse,
|
||||
GetOcrProviderResponse,
|
||||
ListOcrProvidersQuery,
|
||||
ListOcrProvidersResponse,
|
||||
OcrProviderId,
|
||||
ReplaceOcrProviderRequest,
|
||||
ReplaceOcrProviderResponse,
|
||||
UpdateOcrProviderRequest,
|
||||
UpdateOcrProviderResponse
|
||||
} from '@types'
|
||||
|
||||
import type { BodyForPath, ConcreteApiPaths, QueryParamsForPath, ResponseForPath } from './apiPaths'
|
||||
import type { HttpMethod, PaginatedResponse, PaginationParams } from './apiTypes'
|
||||
|
||||
@@ -358,38 +345,6 @@ export interface ApiSchemas {
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
'/ocr/providers': {
|
||||
GET: {
|
||||
query: ListOcrProvidersQuery
|
||||
response: ListOcrProvidersResponse
|
||||
}
|
||||
POST: {
|
||||
body: CreateOcrProviderRequest
|
||||
response: CreateOcrProviderResponse
|
||||
}
|
||||
}
|
||||
|
||||
'/ocr/providers/:id': {
|
||||
GET: {
|
||||
params: { id: OcrProviderId }
|
||||
response: GetOcrProviderResponse
|
||||
}
|
||||
PATCH: {
|
||||
params: { id: OcrProviderId }
|
||||
body: UpdateOcrProviderRequest
|
||||
response: UpdateOcrProviderResponse
|
||||
}
|
||||
PUT: {
|
||||
params: { id: OcrProviderId }
|
||||
body: ReplaceOcrProviderRequest
|
||||
response: ReplaceOcrProviderResponse
|
||||
}
|
||||
DELETE: {
|
||||
params: { id: OcrProviderId }
|
||||
response: void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -351,8 +351,6 @@ export interface PreferenceSchemas {
|
||||
'feature.translate.model_prompt': string
|
||||
// redux/settings/targetLanguage
|
||||
'feature.translate.target_language': string
|
||||
// redux/ocr/imageProviderId
|
||||
'ocr.settings.image_provider_id': string | null
|
||||
// redux/shortcuts/shortcuts.exit_fullscreen
|
||||
'shortcut.app.exit_fullscreen': Record<string, unknown>
|
||||
// redux/shortcuts/shortcuts.search_message
|
||||
@@ -614,7 +612,6 @@ export const DefaultPreferences: PreferenceSchemas = {
|
||||
'feature.selection.trigger_mode': PreferenceTypes.SelectionTriggerMode.Selected,
|
||||
'feature.translate.model_prompt': TRANSLATE_PROMPT,
|
||||
'feature.translate.target_language': 'en-us',
|
||||
'ocr.settings.image_provider_id': null,
|
||||
'shortcut.app.exit_fullscreen': { editable: false, enabled: true, key: ['Escape'], system: true },
|
||||
'shortcut.app.search_message': {
|
||||
editable: true,
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './json'
|
||||
export * from './net'
|
||||
@@ -1,7 +0,0 @@
|
||||
export function safeParseJson(text: string): unknown | null {
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -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 v4 (replacing styled-components)
|
||||
- **Design System**: Custom CSS variable system (`--cs-*` namespace)
|
||||
- **Theme System**: CSS variables + Tailwind CSS theme
|
||||
|
||||
### Migration Principles
|
||||
|
||||
1. **Backward Compatibility**: Old components continue working until new components are fully available
|
||||
2. **Progressive Migration**: Migrate components one by one to avoid large-scale rewrites
|
||||
3. **Feature Parity**: Ensure new components have all the functionality of old components
|
||||
4. **Design Consistency**: Follow new design system specifications (see [README.md](./README.md))
|
||||
5. **Performance Priority**: Optimize bundle size and rendering performance
|
||||
6. **Designer Collaboration**: Work with UI designers for gradual component encapsulation and UI optimization
|
||||
|
||||
## 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** | ✅ **Completed** | **Design System Integration** | • Converted design tokens from todocss.css to tokens.css with `--cs-*` namespace<br>• Created theme.css mapping all design tokens to standard Tailwind classes<br>• Extended Tailwind with semantic spacing (5xs~8xl) and radius (4xs~3xl) systems<br>• Established two usage modes: full override and selective override<br>• Cleaned up main package's conflicting Shadcn theme definitions |
|
||||
| **Phase 2** | ⏳ **To Start** | **Component Migration and Optimization** | • Filter components for migration based on extraction criteria<br>• Remove antd dependencies, replace with shadcn/ui<br>• Remove HeroUI dependencies, replace with shadcn/ui<br>• Remove styled-components, replace with Tailwind CSS + design system variables<br>• Optimize component APIs and type definitions |
|
||||
| **Phase 3** | ⏳ **To Start** | **UI Refactoring and Optimization** | • Gradually implement UI refactoring with UI designers<br>• Ensure visual consistency and user experience<br>• Performance optimization and code quality improvement |
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -143,9 +118,33 @@ 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
|
||||
|
||||
- All design tokens use `--cs-*` namespace (e.g., `--cs-primary`, `--cs-red-500`)
|
||||
- Complete color palette: 17 colors × 11 shades each
|
||||
- Semantic spacing system: `5xs` through `8xl` (16 levels)
|
||||
- Semantic radius system: `4xs` through `3xl` plus `round` (11 levels)
|
||||
- Full light/dark mode support
|
||||
- See [README.md](./README.md) for usage documentation
|
||||
|
||||
### Migration Priority Adjustment
|
||||
|
||||
1. **High Priority**: Basic components (buttons, inputs, tags, etc.)
|
||||
2. **Medium Priority**: Display components (cards, lists, tables, etc.)
|
||||
3. **Low Priority**: Composite components and business-coupled components
|
||||
|
||||
### UI Designer Collaboration
|
||||
|
||||
- All component designs need confirmation from UI designers
|
||||
- Gradually implement UI refactoring to maintain visual consistency
|
||||
- New components must comply with design system specifications
|
||||
|
||||
@@ -2,54 +2,120 @@
|
||||
|
||||
Cherry Studio UI 组件库 - 为 Cherry Studio 设计的 React 组件集合
|
||||
|
||||
## 特性
|
||||
## ✨ 特性
|
||||
|
||||
- 🎨 基于 Tailwind CSS 的现代化设计
|
||||
- 📦 支持 ESM 和 CJS 格式
|
||||
- 🔷 完整的 TypeScript 支持
|
||||
- 🚀 可以作为 npm 包发布
|
||||
- 🔧 开箱即用的常用 hooks 和工具函数
|
||||
- 🎨 **设计系统**: 完整的 CherryStudio 设计令牌(17种颜色 × 11个色阶 + 语义化主题)
|
||||
- 🌓 **Dark Mode**: 开箱即用的深色模式支持
|
||||
- 🚀 **Tailwind v4**: 基于最新 Tailwind CSS v4 构建
|
||||
- 📦 **灵活导入**: 2种样式导入方式,满足不同使用场景
|
||||
- 🔷 **TypeScript**: 完整的类型定义和智能提示
|
||||
- 🎯 **零冲突**: CSS 变量隔离,不覆盖用户主题
|
||||
|
||||
## 安装
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
# 安装组件库
|
||||
npm install @cherrystudio/ui
|
||||
|
||||
# 安装必需的 peer dependencies
|
||||
# peer dependencies
|
||||
npm install @heroui/react framer-motion react react-dom tailwindcss
|
||||
```
|
||||
|
||||
## 配置
|
||||
### 两种使用方式
|
||||
|
||||
### 1. Tailwind CSS v4 配置
|
||||
#### 方式 1:完整覆盖 ✨
|
||||
|
||||
本组件库使用 Tailwind CSS v4,配置方式已改变。在你的主 CSS 文件(如 `src/styles/tailwind.css`)中:
|
||||
使用完整的 CherryStudio 设计系统,所有 Tailwind 类名映射到设计系统。
|
||||
|
||||
```css
|
||||
/* app.css */
|
||||
@import '@cherrystudio/ui/styles/theme.css';
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
- ✅ 直接使用标准 Tailwind 类名(`bg-primary`、`bg-red-500`、`p-md`、`rounded-lg`)
|
||||
- ✅ 所有颜色使用设计师定义的值
|
||||
- ✅ 扩展的 Spacing 系统(`p-5xs` ~ `p-8xl`,共 16 个语义化尺寸)
|
||||
- ✅ 扩展的 Radius 系统(`rounded-4xs` ~ `rounded-3xl`,共 11 个圆角)
|
||||
- ⚠️ 会完全覆盖 Tailwind 默认主题
|
||||
|
||||
**示例**:
|
||||
|
||||
```tsx
|
||||
<Button className="bg-primary text-red-500 p-md rounded-lg">
|
||||
{/* bg-primary → 品牌色(lime-500) */}
|
||||
{/* text-red-500 → 设计师定义的红色 */}
|
||||
{/* p-md → 2.5rem(spacing-md) */}
|
||||
{/* rounded-lg → 2.5rem(radius-lg) */}
|
||||
</Button>
|
||||
|
||||
{/* 扩展的工具类 */}
|
||||
<div className="p-5xs">最小间距 (0.5rem)</div>
|
||||
<div className="p-xs">超小间距 (1rem)</div>
|
||||
<div className="p-sm">小间距 (1.5rem)</div>
|
||||
<div className="p-md">中等间距 (2.5rem)</div>
|
||||
<div className="p-lg">大间距 (3.5rem)</div>
|
||||
<div className="p-xl">超大间距 (5rem)</div>
|
||||
<div className="p-8xl">最大间距 (15rem)</div>
|
||||
|
||||
<div className="rounded-4xs">最小圆角 (0.25rem)</div>
|
||||
<div className="rounded-xs">小圆角 (1rem)</div>
|
||||
<div className="rounded-md">中等圆角 (2rem)</div>
|
||||
<div className="rounded-xl">大圆角 (3rem)</div>
|
||||
<div className="rounded-round">完全圆角 (999px)</div>
|
||||
```
|
||||
|
||||
#### 方式 2:选择性覆盖 🎯
|
||||
|
||||
只导入设计令牌(CSS 变量),手动选择要覆盖的部分。
|
||||
|
||||
```css
|
||||
/* app.css */
|
||||
@import 'tailwindcss';
|
||||
@import '@cherrystudio/ui/styles/tokens.css';
|
||||
|
||||
/* 必须扫描组件库文件以提取类名 */
|
||||
@source '../node_modules/@cherrystudio/ui/dist/**/*.{js,mjs}';
|
||||
|
||||
/* 你的应用源文件 */
|
||||
@source './src/**/*.{js,ts,jsx,tsx}';
|
||||
|
||||
/*
|
||||
* 如果你的应用直接使用 HeroUI 组件,需要添加:
|
||||
* @source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||
* @plugin '@heroui/react/plugin';
|
||||
*/
|
||||
|
||||
/* 自定义主题配置(可选) */
|
||||
/* 只使用部分设计系统 */
|
||||
@theme {
|
||||
/* 你的主题扩展 */
|
||||
--color-primary: var(--cs-primary); /* 使用 CS 的主色 */
|
||||
--color-red-500: oklch(...); /* 使用自己的红色 */
|
||||
--spacing-md: var(--cs-size-md); /* 使用 CS 的间距 */
|
||||
--radius-lg: 1rem; /* 使用自己的圆角 */
|
||||
}
|
||||
```
|
||||
|
||||
注意:Tailwind CSS v4 不再使用 `tailwind.config.js` 文件,所有配置都在 CSS 中完成。
|
||||
**特点**:
|
||||
|
||||
### 2. Provider 配置
|
||||
- ✅ 不覆盖任何 Tailwind 默认主题
|
||||
- ✅ 通过 CSS 变量访问所有设计令牌(`var(--cs-primary)`、`var(--cs-red-500)`)
|
||||
- ✅ 精细控制哪些使用 CS、哪些保持原样
|
||||
- ✅ 适合有自己设计系统但想借用部分 CS 设计令牌的场景
|
||||
|
||||
**示例**:
|
||||
|
||||
```tsx
|
||||
{/* 通过 CSS 变量使用 CS 设计令牌 */}
|
||||
<button style={{ backgroundColor: 'var(--cs-primary)' }}>
|
||||
使用 CherryStudio 品牌色
|
||||
</button>
|
||||
|
||||
{/* 保持原有的 Tailwind 类名不受影响 */}
|
||||
<div className="bg-red-500">
|
||||
使用 Tailwind 默认的红色
|
||||
</div>
|
||||
|
||||
{/* 可用的 CSS 变量 */}
|
||||
<div style={{
|
||||
color: 'var(--cs-primary)', // 品牌色
|
||||
backgroundColor: 'var(--cs-red-500)', // 红色-500
|
||||
padding: 'var(--cs-size-md)', // 间距
|
||||
borderRadius: 'var(--cs-radius-lg)' // 圆角
|
||||
}} />
|
||||
```
|
||||
|
||||
### Provider 配置
|
||||
|
||||
在你的 App 根组件中添加 HeroUI Provider:
|
||||
|
||||
@@ -94,9 +160,6 @@ function App() {
|
||||
// 只导入组件
|
||||
import { Button } from '@cherrystudio/ui/components'
|
||||
|
||||
// 只导入 hooks
|
||||
import { useDebounce, useLocalStorage } from '@cherrystudio/ui/hooks'
|
||||
|
||||
// 只导入工具函数
|
||||
import { cn, formatFileSize } from '@cherrystudio/ui/utils'
|
||||
```
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"tailwind": {
|
||||
"baseColor": "zinc",
|
||||
"config": "",
|
||||
"css": "src/styles/globals.css",
|
||||
"css": "src/styles/theme.css",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
|
||||
214
packages/ui/design-reference/CONVERSION_LOG.md
Normal file
214
packages/ui/design-reference/CONVERSION_LOG.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# todocss.css → design-tokens.css 转换日志
|
||||
|
||||
## ✅ 已转换的变量
|
||||
|
||||
### 基础颜色 (Primitive Colors)
|
||||
- ✅ Neutral (50-950)
|
||||
- ✅ Zinc (50-950)
|
||||
- ✅ Red (50-950)
|
||||
- ✅ Orange (50-950)
|
||||
- ✅ Amber (50-950)
|
||||
- ✅ Yellow (50-950)
|
||||
- ✅ Lime (50-950) - 品牌主色
|
||||
- ✅ Green (50-950)
|
||||
- ✅ Emerald (50-950)
|
||||
- ✅ Purple (50-950)
|
||||
- ✅ Blue (50-950)
|
||||
- ✅ Black & White
|
||||
|
||||
### 语义化颜色 (Semantic Colors)
|
||||
- ✅ `--cs-primary` (Lime 500)
|
||||
- ✅ `--cs-destructive` (Red 500)
|
||||
- ✅ `--cs-success` (Green 500)
|
||||
- ✅ `--cs-warning` (Amber 500)
|
||||
- ✅ `--cs-background` (Zinc 50/900)
|
||||
- ✅ `--cs-foreground` 系列 (main, secondary, muted)
|
||||
- ✅ `--cs-border` 系列 (default, hover, active)
|
||||
- ✅ `--cs-ring` (Focus)
|
||||
|
||||
### 容器颜色
|
||||
- ✅ `--cs-card` (White/Black)
|
||||
- ✅ `--cs-popover` (White/Black)
|
||||
- ✅ `--cs-sidebar` (White/Black)
|
||||
|
||||
### UI 元素细分颜色 (新增补充)
|
||||
- ✅ **Modal / Overlay**
|
||||
- `--cs-modal-backdrop`
|
||||
- `--cs-modal-thumb`
|
||||
- `--cs-modal-thumb-hover`
|
||||
|
||||
- ✅ **Icon**
|
||||
- `--cs-icon-default`
|
||||
- `--cs-icon-hover`
|
||||
|
||||
- ✅ **Input / Select**
|
||||
- `--cs-input-background`
|
||||
- `--cs-input-border`
|
||||
- `--cs-input-border-hover`
|
||||
- `--cs-input-border-focus`
|
||||
|
||||
- ✅ **Primary Button**
|
||||
- `--cs-primary-button-background`
|
||||
- `--cs-primary-button-text`
|
||||
- `--cs-primary-button-background-hover`
|
||||
- `--cs-primary-button-background-active`
|
||||
- `--cs-primary-button-background-2nd`
|
||||
- `--cs-primary-button-background-3rd`
|
||||
|
||||
- ✅ **Secondary Button**
|
||||
- `--cs-secondary-button-background`
|
||||
- `--cs-secondary-button-text`
|
||||
- `--cs-secondary-button-background-hover`
|
||||
- `--cs-secondary-button-background-active`
|
||||
- `--cs-secondary-button-border`
|
||||
|
||||
- ✅ **Ghost Button**
|
||||
- `--cs-ghost-button-background`
|
||||
- `--cs-ghost-button-text`
|
||||
- `--cs-ghost-button-background-hover`
|
||||
- `--cs-ghost-button-background-active`
|
||||
|
||||
### 尺寸系统
|
||||
- ✅ Spacing/Sizing 合并为 `--cs-size-*` (5xs ~ 8xl)
|
||||
- ✅ Border Radius (4xs ~ 3xl, round)
|
||||
- ✅ Border Width (sm, md, lg)
|
||||
|
||||
### 字体排版
|
||||
- ✅ Font Families (Heading, Body)
|
||||
- ✅ Font Weights (修正单位错误: 400px → 400)
|
||||
- ✅ Font Sizes (Body & Heading)
|
||||
- ✅ Line Heights (Body & Heading)
|
||||
- ✅ Paragraph Spacing
|
||||
|
||||
---
|
||||
|
||||
## ❌ 已废弃的变量
|
||||
|
||||
### Opacity 变量 (全部废弃)
|
||||
使用 Tailwind 的 `/modifier` 语法替代:
|
||||
|
||||
| todocss.css | 替代方案 |
|
||||
|-------------|---------|
|
||||
| `--Opacity--Red--Red-80` | `bg-cs-destructive/80` |
|
||||
| `--Opacity--Green--Green-60` | `bg-cs-success/60` |
|
||||
| `--Opacity--White--White-10` | `bg-white/10` |
|
||||
|
||||
**原因**: Tailwind v4 原生支持透明度修饰符,无需单独定义变量。
|
||||
|
||||
---
|
||||
|
||||
## 🔧 关键修正
|
||||
|
||||
### 1. 单位错误
|
||||
```css
|
||||
/* ❌ todocss.css */
|
||||
--Font_weight--Regular: 400px;
|
||||
|
||||
/* ✅ design-tokens.css */
|
||||
--cs-font-weight-regular: 400;
|
||||
```
|
||||
|
||||
### 2. px → rem 转换
|
||||
```css
|
||||
/* ❌ todocss.css */
|
||||
--Spacing--md: 40px;
|
||||
|
||||
/* ✅ design-tokens.css */
|
||||
--cs-size-md: 2.5rem; /* 40px / 16 = 2.5rem */
|
||||
```
|
||||
|
||||
### 3. 变量合并
|
||||
```css
|
||||
/* ❌ todocss.css (冗余) */
|
||||
--Spacing--md: 40px;
|
||||
--Sizing--md: 40px;
|
||||
|
||||
/* ✅ design-tokens.css (合并) */
|
||||
--cs-size-md: 2.5rem;
|
||||
```
|
||||
|
||||
### 4. Dark Mode 分离
|
||||
```css
|
||||
/* ❌ todocss.css (Light 和 Dark 都在 :root) */
|
||||
:root {
|
||||
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--50);
|
||||
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--900); /* 后面覆盖 */
|
||||
}
|
||||
|
||||
/* ✅ design-tokens.css (正确分离) */
|
||||
:root {
|
||||
--cs-background: var(--cs-zinc-50);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--cs-background: var(--cs-zinc-900);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 变量统计
|
||||
|
||||
| 分类 | todocss.css | design-tokens.css | 说明 |
|
||||
|------|-------------|-------------------|------|
|
||||
| Primitive Colors | ~250 | ~250 | 完整保留 |
|
||||
| Semantic Colors | ~20 | ~20 | 完整转换 |
|
||||
| UI Element Colors | ~30 | ~30 | ✅ 已补充完整 |
|
||||
| Opacity Variables | ~50 | 0 | 废弃,用 `/modifier` |
|
||||
| Spacing/Sizing | 32 | 16 | 合并去重 |
|
||||
| Typography | ~50 | ~50 | 修正单位 |
|
||||
| **总计** | ~430 | ~390 | 优化 40 个变量 |
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Dark Mode 变量对比
|
||||
|
||||
| Light Mode | Dark Mode | 变量名 |
|
||||
|-----------|-----------|-------|
|
||||
| Zinc 50 | Zinc 900 | `--cs-background` |
|
||||
| Black 90% | White 90% | `--cs-foreground` |
|
||||
| Black 60% | White 60% | `--cs-foreground-secondary` |
|
||||
| Black 10% | White 10% | `--cs-border` |
|
||||
| White | Black | `--cs-card` |
|
||||
| White | Black | `--cs-popover` |
|
||||
| White | Black | `--cs-sidebar` |
|
||||
| White | Black | `--cs-input-background` |
|
||||
| Black 40% | Black 6% | `--cs-modal-backdrop` |
|
||||
| Black 20% | White 20% | `--cs-modal-thumb` |
|
||||
| Black 5% | White 10% | `--cs-secondary` |
|
||||
| Black 0% | White 0% | `--cs-ghost-button-background` |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 验证清单
|
||||
|
||||
- [x] 所有 Primitive 颜色已转换
|
||||
- [x] 所有语义化颜色已转换
|
||||
- [x] 所有 UI 元素颜色已转换
|
||||
- [x] Dark Mode 变量完整
|
||||
- [x] 尺寸单位统一为 rem
|
||||
- [x] Font Weight 单位已修正
|
||||
- [x] Opacity 变量已废弃
|
||||
- [x] Spacing/Sizing 已合并
|
||||
|
||||
---
|
||||
|
||||
## 📝 使用指南
|
||||
|
||||
### 如果设计师更新 todocss.css
|
||||
|
||||
1. 对比此文档,找出新增/修改的变量
|
||||
2. 按照转换规则更新 `design-tokens.css`
|
||||
3. 验证 Light/Dark Mode 是否完整
|
||||
4. 更新此日志
|
||||
|
||||
### 验证转换正确性
|
||||
|
||||
```bash
|
||||
# 检查 Light Mode 变量数量
|
||||
grep -c "^ --cs-" packages/ui/src/styles/design-tokens.css
|
||||
|
||||
# 检查 Dark Mode 覆盖数量
|
||||
grep -c "^ --cs-" packages/ui/src/styles/design-tokens.css | grep -A 100 ".dark"
|
||||
```
|
||||
|
||||
26
packages/ui/design-reference/README.md
Normal file
26
packages/ui/design-reference/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Design Reference
|
||||
|
||||
本文件夹包含设计师提供的原始设计令牌文件,仅作为参考使用。
|
||||
|
||||
## 文件说明
|
||||
|
||||
### todocss.css
|
||||
- **来源**:设计师提供的原始设计令牌
|
||||
- **状态**:已转换为 `src/styles/design-tokens.css`
|
||||
- **用途**:
|
||||
- 追溯设计决策
|
||||
- 验证转换正确性
|
||||
- 设计师更新时作为对比基准
|
||||
|
||||
## 转换规则
|
||||
|
||||
原始文件 → 生产文件的转换规则参见:
|
||||
- [DESIGN_SYSTEM.md](../DESIGN_SYSTEM.md)
|
||||
- [USAGE_GUIDE.md](../USAGE_GUIDE.md)
|
||||
|
||||
## 注意事项
|
||||
|
||||
⚠️ **请勿直接使用此文件夹中的文件**
|
||||
- 这些文件仅供参考
|
||||
- 实际使用请导入 `src/styles/` 中的文件
|
||||
|
||||
870
packages/ui/design-reference/todocss.css
Normal file
870
packages/ui/design-reference/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;
|
||||
}
|
||||
@@ -124,7 +124,11 @@
|
||||
"import": "./dist/utils/index.mjs",
|
||||
"require": "./dist/utils/index.js",
|
||||
"default": "./dist/utils/index.js"
|
||||
}
|
||||
},
|
||||
"./styles": "./src/styles/index.css",
|
||||
"./styles/tokens.css": "./src/styles/tokens.css",
|
||||
"./styles/theme.css": "./src/styles/theme.css",
|
||||
"./styles/index.css": "./src/styles/index.css"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1"
|
||||
}
|
||||
|
||||
@@ -87,5 +87,3 @@ export * from './primitives/dialog'
|
||||
export * from './primitives/popover'
|
||||
export * from './primitives/radioGroup'
|
||||
export * from './primitives/shadcn-io/dropzone'
|
||||
export * from './primitives/shadcn-io/skeleton'
|
||||
export * from './primitives/shadcn-io/tabs'
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { cn } from '@cherrystudio/ui/utils'
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="skeleton" className={cn('bg-accent animate-pulse rounded-md', className)} {...props} />
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -1,39 +0,0 @@
|
||||
import { cn } from '@cherrystudio/ui/utils'
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
||||
import * as React from 'react'
|
||||
|
||||
function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return <TabsPrimitive.Root data-slot="tabs" className={cn('flex flex-col gap-2', className)} {...props} />
|
||||
}
|
||||
|
||||
function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return <TabsPrimitive.Content data-slot="tabs-content" className={cn('flex-1 outline-none', className)} {...props} />
|
||||
}
|
||||
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger }
|
||||
@@ -1,123 +0,0 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.145 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.145 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 0 0);
|
||||
--ring: oklch(0.439 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.269 0 0);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
24
packages/ui/src/styles/index.css
Normal file
24
packages/ui/src/styles/index.css
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* CherryStudio Styles - npm 包入口
|
||||
*
|
||||
* 用途:仅导出 CSS 变量,不生成工具类,不覆盖用户主题
|
||||
* 使用场景:
|
||||
* - npm 用户想使用设计系统的变量,但不想覆盖 Tailwind 默认主题
|
||||
* - 可以通过 var(--cs-primary) 等方式使用
|
||||
* - 不会生成 bg-primary 等工具类(用户自己的 bg-primary 不受影响)
|
||||
*
|
||||
* 示例:
|
||||
* ```css
|
||||
* @import '@cherrystudio/ui/styles/index.css';
|
||||
*
|
||||
* .my-button {
|
||||
* background: var(--cs-primary); // 使用 CS 的品牌色
|
||||
* padding: var(--cs-size-md); // 使用 CS 的间距
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 如果想要完整的主题覆盖(生成 bg-primary 等类),请导入:
|
||||
* @import '@cherrystudio/ui/styles/theme.css';
|
||||
*/
|
||||
|
||||
@import './tokens.css';
|
||||
433
packages/ui/src/styles/theme.css
Normal file
433
packages/ui/src/styles/theme.css
Normal file
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* CherryStudio Theme - Tailwind CSS 主题配置
|
||||
*
|
||||
* 用途:映射设计师的设计系统(--cs-*)到 Tailwind 标准命名
|
||||
* 使用场景:
|
||||
* - 主包:完全使用设计系统
|
||||
* - npm 用户:可选择导入,会覆盖 Tailwind 默认主题
|
||||
*
|
||||
* 生成的工具类:bg-primary, bg-red-500, p-md 等(无前缀)
|
||||
*/
|
||||
|
||||
@import 'tailwindcss';
|
||||
@import './tokens.css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
/* ==================== */
|
||||
/* 语义化颜色(覆盖 Tailwind 标准命名) */
|
||||
/* ==================== */
|
||||
--color-primary: var(--cs-primary);
|
||||
--color-primary-foreground: var(--cs-white);
|
||||
|
||||
--color-destructive: var(--cs-destructive);
|
||||
--color-destructive-foreground: var(--cs-white);
|
||||
|
||||
--color-success: var(--cs-success);
|
||||
--color-success-foreground: var(--cs-white);
|
||||
|
||||
--color-warning: var(--cs-warning);
|
||||
--color-warning-foreground: var(--cs-black);
|
||||
|
||||
--color-background: var(--cs-background);
|
||||
--color-foreground: var(--cs-foreground);
|
||||
|
||||
--color-card: var(--cs-card);
|
||||
--color-card-foreground: var(--cs-foreground);
|
||||
|
||||
--color-popover: var(--cs-popover);
|
||||
--color-popover-foreground: var(--cs-foreground);
|
||||
|
||||
--color-secondary: var(--cs-secondary);
|
||||
--color-secondary-foreground: var(--cs-foreground);
|
||||
|
||||
--color-muted: var(--cs-muted);
|
||||
--color-muted-foreground: var(--cs-foreground-muted);
|
||||
|
||||
--color-accent: var(--cs-accent);
|
||||
--color-accent-foreground: var(--cs-foreground);
|
||||
|
||||
--color-border: var(--cs-border);
|
||||
--color-input: var(--cs-border);
|
||||
--color-ring: var(--cs-ring);
|
||||
|
||||
/* 侧边栏 */
|
||||
--color-sidebar: var(--cs-sidebar);
|
||||
--color-sidebar-foreground: var(--cs-foreground);
|
||||
--color-sidebar-primary: var(--cs-primary);
|
||||
--color-sidebar-primary-foreground: var(--cs-white);
|
||||
--color-sidebar-accent: var(--cs-sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--cs-foreground);
|
||||
--color-sidebar-border: var(--cs-border);
|
||||
--color-sidebar-ring: var(--cs-ring);
|
||||
|
||||
/* 图表颜色 */
|
||||
--color-chart-1: var(--cs-blue-500);
|
||||
--color-chart-2: var(--cs-emerald-500);
|
||||
--color-chart-3: var(--cs-purple-500);
|
||||
--color-chart-4: var(--cs-amber-500);
|
||||
--color-chart-5: var(--cs-orange-500);
|
||||
|
||||
/* ==================== */
|
||||
/* Primitive 颜色(覆盖 Tailwind 默认色板) */
|
||||
/* ==================== */
|
||||
|
||||
/* Neutral */
|
||||
--color-neutral-50: var(--cs-neutral-50);
|
||||
--color-neutral-100: var(--cs-neutral-100);
|
||||
--color-neutral-200: var(--cs-neutral-200);
|
||||
--color-neutral-300: var(--cs-neutral-300);
|
||||
--color-neutral-400: var(--cs-neutral-400);
|
||||
--color-neutral-500: var(--cs-neutral-500);
|
||||
--color-neutral-600: var(--cs-neutral-600);
|
||||
--color-neutral-700: var(--cs-neutral-700);
|
||||
--color-neutral-800: var(--cs-neutral-800);
|
||||
--color-neutral-900: var(--cs-neutral-900);
|
||||
--color-neutral-950: var(--cs-neutral-950);
|
||||
|
||||
/* Stone */
|
||||
--color-stone-50: var(--cs-stone-50);
|
||||
--color-stone-100: var(--cs-stone-100);
|
||||
--color-stone-200: var(--cs-stone-200);
|
||||
--color-stone-300: var(--cs-stone-300);
|
||||
--color-stone-400: var(--cs-stone-400);
|
||||
--color-stone-500: var(--cs-stone-500);
|
||||
--color-stone-600: var(--cs-stone-600);
|
||||
--color-stone-700: var(--cs-stone-700);
|
||||
--color-stone-800: var(--cs-stone-800);
|
||||
--color-stone-900: var(--cs-stone-900);
|
||||
--color-stone-950: var(--cs-stone-950);
|
||||
|
||||
/* Zinc */
|
||||
--color-zinc-50: var(--cs-zinc-50);
|
||||
--color-zinc-100: var(--cs-zinc-100);
|
||||
--color-zinc-200: var(--cs-zinc-200);
|
||||
--color-zinc-300: var(--cs-zinc-300);
|
||||
--color-zinc-400: var(--cs-zinc-400);
|
||||
--color-zinc-500: var(--cs-zinc-500);
|
||||
--color-zinc-600: var(--cs-zinc-600);
|
||||
--color-zinc-700: var(--cs-zinc-700);
|
||||
--color-zinc-800: var(--cs-zinc-800);
|
||||
--color-zinc-900: var(--cs-zinc-900);
|
||||
--color-zinc-950: var(--cs-zinc-950);
|
||||
|
||||
/* Slate */
|
||||
--color-slate-50: var(--cs-slate-50);
|
||||
--color-slate-100: var(--cs-slate-100);
|
||||
--color-slate-200: var(--cs-slate-200);
|
||||
--color-slate-300: var(--cs-slate-300);
|
||||
--color-slate-400: var(--cs-slate-400);
|
||||
--color-slate-500: var(--cs-slate-500);
|
||||
--color-slate-600: var(--cs-slate-600);
|
||||
--color-slate-700: var(--cs-slate-700);
|
||||
--color-slate-800: var(--cs-slate-800);
|
||||
--color-slate-900: var(--cs-slate-900);
|
||||
--color-slate-950: var(--cs-slate-950);
|
||||
|
||||
/* Gray */
|
||||
--color-gray-50: var(--cs-gray-50);
|
||||
--color-gray-100: var(--cs-gray-100);
|
||||
--color-gray-200: var(--cs-gray-200);
|
||||
--color-gray-300: var(--cs-gray-300);
|
||||
--color-gray-400: var(--cs-gray-400);
|
||||
--color-gray-500: var(--cs-gray-500);
|
||||
--color-gray-600: var(--cs-gray-600);
|
||||
--color-gray-700: var(--cs-gray-700);
|
||||
--color-gray-800: var(--cs-gray-800);
|
||||
--color-gray-900: var(--cs-gray-900);
|
||||
--color-gray-950: var(--cs-gray-950);
|
||||
|
||||
/* Red */
|
||||
--color-red-50: var(--cs-red-50);
|
||||
--color-red-100: var(--cs-red-100);
|
||||
--color-red-200: var(--cs-red-200);
|
||||
--color-red-300: var(--cs-red-300);
|
||||
--color-red-400: var(--cs-red-400);
|
||||
--color-red-500: var(--cs-red-500);
|
||||
--color-red-600: var(--cs-red-600);
|
||||
--color-red-700: var(--cs-red-700);
|
||||
--color-red-800: var(--cs-red-800);
|
||||
--color-red-900: var(--cs-red-900);
|
||||
--color-red-950: var(--cs-red-950);
|
||||
|
||||
/* Orange */
|
||||
--color-orange-50: var(--cs-orange-50);
|
||||
--color-orange-100: var(--cs-orange-100);
|
||||
--color-orange-200: var(--cs-orange-200);
|
||||
--color-orange-300: var(--cs-orange-300);
|
||||
--color-orange-400: var(--cs-orange-400);
|
||||
--color-orange-500: var(--cs-orange-500);
|
||||
--color-orange-600: var(--cs-orange-600);
|
||||
--color-orange-700: var(--cs-orange-700);
|
||||
--color-orange-800: var(--cs-orange-800);
|
||||
--color-orange-900: var(--cs-orange-900);
|
||||
--color-orange-950: var(--cs-orange-950);
|
||||
|
||||
/* Amber */
|
||||
--color-amber-50: var(--cs-amber-50);
|
||||
--color-amber-100: var(--cs-amber-100);
|
||||
--color-amber-200: var(--cs-amber-200);
|
||||
--color-amber-300: var(--cs-amber-300);
|
||||
--color-amber-400: var(--cs-amber-400);
|
||||
--color-amber-500: var(--cs-amber-500);
|
||||
--color-amber-600: var(--cs-amber-600);
|
||||
--color-amber-700: var(--cs-amber-700);
|
||||
--color-amber-800: var(--cs-amber-800);
|
||||
--color-amber-900: var(--cs-amber-900);
|
||||
--color-amber-950: var(--cs-amber-950);
|
||||
|
||||
/* Yellow */
|
||||
--color-yellow-50: var(--cs-yellow-50);
|
||||
--color-yellow-100: var(--cs-yellow-100);
|
||||
--color-yellow-200: var(--cs-yellow-200);
|
||||
--color-yellow-300: var(--cs-yellow-300);
|
||||
--color-yellow-400: var(--cs-yellow-400);
|
||||
--color-yellow-500: var(--cs-yellow-500);
|
||||
--color-yellow-600: var(--cs-yellow-600);
|
||||
--color-yellow-700: var(--cs-yellow-700);
|
||||
--color-yellow-800: var(--cs-yellow-800);
|
||||
--color-yellow-900: var(--cs-yellow-900);
|
||||
--color-yellow-950: var(--cs-yellow-950);
|
||||
|
||||
/* Lime (品牌主色) */
|
||||
--color-lime-50: var(--cs-lime-50);
|
||||
--color-lime-100: var(--cs-lime-100);
|
||||
--color-lime-200: var(--cs-lime-200);
|
||||
--color-lime-300: var(--cs-lime-300);
|
||||
--color-lime-400: var(--cs-lime-400);
|
||||
--color-lime-500: var(--cs-lime-500);
|
||||
--color-lime-600: var(--cs-lime-600);
|
||||
--color-lime-700: var(--cs-lime-700);
|
||||
--color-lime-800: var(--cs-lime-800);
|
||||
--color-lime-900: var(--cs-lime-900);
|
||||
--color-lime-950: var(--cs-lime-950);
|
||||
|
||||
/* Green */
|
||||
--color-green-50: var(--cs-green-50);
|
||||
--color-green-100: var(--cs-green-100);
|
||||
--color-green-200: var(--cs-green-200);
|
||||
--color-green-300: var(--cs-green-300);
|
||||
--color-green-400: var(--cs-green-400);
|
||||
--color-green-500: var(--cs-green-500);
|
||||
--color-green-600: var(--cs-green-600);
|
||||
--color-green-700: var(--cs-green-700);
|
||||
--color-green-800: var(--cs-green-800);
|
||||
--color-green-900: var(--cs-green-900);
|
||||
--color-green-950: var(--cs-green-950);
|
||||
|
||||
/* Emerald */
|
||||
--color-emerald-50: var(--cs-emerald-50);
|
||||
--color-emerald-100: var(--cs-emerald-100);
|
||||
--color-emerald-200: var(--cs-emerald-200);
|
||||
--color-emerald-300: var(--cs-emerald-300);
|
||||
--color-emerald-400: var(--cs-emerald-400);
|
||||
--color-emerald-500: var(--cs-emerald-500);
|
||||
--color-emerald-600: var(--cs-emerald-600);
|
||||
--color-emerald-700: var(--cs-emerald-700);
|
||||
--color-emerald-800: var(--cs-emerald-800);
|
||||
--color-emerald-900: var(--cs-emerald-900);
|
||||
--color-emerald-950: var(--cs-emerald-950);
|
||||
|
||||
/* Teal */
|
||||
--color-teal-50: var(--cs-teal-50);
|
||||
--color-teal-100: var(--cs-teal-100);
|
||||
--color-teal-200: var(--cs-teal-200);
|
||||
--color-teal-300: var(--cs-teal-300);
|
||||
--color-teal-400: var(--cs-teal-400);
|
||||
--color-teal-500: var(--cs-teal-500);
|
||||
--color-teal-600: var(--cs-teal-600);
|
||||
--color-teal-700: var(--cs-teal-700);
|
||||
--color-teal-800: var(--cs-teal-800);
|
||||
--color-teal-900: var(--cs-teal-900);
|
||||
--color-teal-950: var(--cs-teal-950);
|
||||
|
||||
/* Cyan */
|
||||
--color-cyan-50: var(--cs-cyan-50);
|
||||
--color-cyan-100: var(--cs-cyan-100);
|
||||
--color-cyan-200: var(--cs-cyan-200);
|
||||
--color-cyan-300: var(--cs-cyan-300);
|
||||
--color-cyan-400: var(--cs-cyan-400);
|
||||
--color-cyan-500: var(--cs-cyan-500);
|
||||
--color-cyan-600: var(--cs-cyan-600);
|
||||
--color-cyan-700: var(--cs-cyan-700);
|
||||
--color-cyan-800: var(--cs-cyan-800);
|
||||
--color-cyan-900: var(--cs-cyan-900);
|
||||
--color-cyan-950: var(--cs-cyan-950);
|
||||
|
||||
/* Sky */
|
||||
--color-sky-50: var(--cs-sky-50);
|
||||
--color-sky-100: var(--cs-sky-100);
|
||||
--color-sky-200: var(--cs-sky-200);
|
||||
--color-sky-300: var(--cs-sky-300);
|
||||
--color-sky-400: var(--cs-sky-400);
|
||||
--color-sky-500: var(--cs-sky-500);
|
||||
--color-sky-600: var(--cs-sky-600);
|
||||
--color-sky-700: var(--cs-sky-700);
|
||||
--color-sky-800: var(--cs-sky-800);
|
||||
--color-sky-900: var(--cs-sky-900);
|
||||
--color-sky-950: var(--cs-sky-950);
|
||||
|
||||
/* Blue */
|
||||
--color-blue-50: var(--cs-blue-50);
|
||||
--color-blue-100: var(--cs-blue-100);
|
||||
--color-blue-200: var(--cs-blue-200);
|
||||
--color-blue-300: var(--cs-blue-300);
|
||||
--color-blue-400: var(--cs-blue-400);
|
||||
--color-blue-500: var(--cs-blue-500);
|
||||
--color-blue-600: var(--cs-blue-600);
|
||||
--color-blue-700: var(--cs-blue-700);
|
||||
--color-blue-800: var(--cs-blue-800);
|
||||
--color-blue-900: var(--cs-blue-900);
|
||||
--color-blue-950: var(--cs-blue-950);
|
||||
|
||||
/* Indigo */
|
||||
--color-indigo-50: var(--cs-indigo-50);
|
||||
--color-indigo-100: var(--cs-indigo-100);
|
||||
--color-indigo-200: var(--cs-indigo-200);
|
||||
--color-indigo-300: var(--cs-indigo-300);
|
||||
--color-indigo-400: var(--cs-indigo-400);
|
||||
--color-indigo-500: var(--cs-indigo-500);
|
||||
--color-indigo-600: var(--cs-indigo-600);
|
||||
--color-indigo-700: var(--cs-indigo-700);
|
||||
--color-indigo-800: var(--cs-indigo-800);
|
||||
--color-indigo-900: var(--cs-indigo-900);
|
||||
--color-indigo-950: var(--cs-indigo-950);
|
||||
|
||||
/* Violet */
|
||||
--color-violet-50: var(--cs-violet-50);
|
||||
--color-violet-100: var(--cs-violet-100);
|
||||
--color-violet-200: var(--cs-violet-200);
|
||||
--color-violet-300: var(--cs-violet-300);
|
||||
--color-violet-400: var(--cs-violet-400);
|
||||
--color-violet-500: var(--cs-violet-500);
|
||||
--color-violet-600: var(--cs-violet-600);
|
||||
--color-violet-700: var(--cs-violet-700);
|
||||
--color-violet-800: var(--cs-violet-800);
|
||||
--color-violet-900: var(--cs-violet-900);
|
||||
--color-violet-950: var(--cs-violet-950);
|
||||
|
||||
/* Purple */
|
||||
--color-purple-50: var(--cs-purple-50);
|
||||
--color-purple-100: var(--cs-purple-100);
|
||||
--color-purple-200: var(--cs-purple-200);
|
||||
--color-purple-300: var(--cs-purple-300);
|
||||
--color-purple-400: var(--cs-purple-400);
|
||||
--color-purple-500: var(--cs-purple-500);
|
||||
--color-purple-600: var(--cs-purple-600);
|
||||
--color-purple-700: var(--cs-purple-700);
|
||||
--color-purple-800: var(--cs-purple-800);
|
||||
--color-purple-900: var(--cs-purple-900);
|
||||
--color-purple-950: var(--cs-purple-950);
|
||||
|
||||
/* Fuchsia */
|
||||
--color-fuchsia-50: var(--cs-fuchsia-50);
|
||||
--color-fuchsia-100: var(--cs-fuchsia-100);
|
||||
--color-fuchsia-200: var(--cs-fuchsia-200);
|
||||
--color-fuchsia-300: var(--cs-fuchsia-300);
|
||||
--color-fuchsia-400: var(--cs-fuchsia-400);
|
||||
--color-fuchsia-500: var(--cs-fuchsia-500);
|
||||
--color-fuchsia-600: var(--cs-fuchsia-600);
|
||||
--color-fuchsia-700: var(--cs-fuchsia-700);
|
||||
--color-fuchsia-800: var(--cs-fuchsia-800);
|
||||
--color-fuchsia-900: var(--cs-fuchsia-900);
|
||||
--color-fuchsia-950: var(--cs-fuchsia-950);
|
||||
|
||||
/* Pink */
|
||||
--color-pink-50: var(--cs-pink-50);
|
||||
--color-pink-100: var(--cs-pink-100);
|
||||
--color-pink-200: var(--cs-pink-200);
|
||||
--color-pink-300: var(--cs-pink-300);
|
||||
--color-pink-400: var(--cs-pink-400);
|
||||
--color-pink-500: var(--cs-pink-500);
|
||||
--color-pink-600: var(--cs-pink-600);
|
||||
--color-pink-700: var(--cs-pink-700);
|
||||
--color-pink-800: var(--cs-pink-800);
|
||||
--color-pink-900: var(--cs-pink-900);
|
||||
--color-pink-950: var(--cs-pink-950);
|
||||
|
||||
/* Rose */
|
||||
--color-rose-50: var(--cs-rose-50);
|
||||
--color-rose-100: var(--cs-rose-100);
|
||||
--color-rose-200: var(--cs-rose-200);
|
||||
--color-rose-300: var(--cs-rose-300);
|
||||
--color-rose-400: var(--cs-rose-400);
|
||||
--color-rose-500: var(--cs-rose-500);
|
||||
--color-rose-600: var(--cs-rose-600);
|
||||
--color-rose-700: var(--cs-rose-700);
|
||||
--color-rose-800: var(--cs-rose-800);
|
||||
--color-rose-900: var(--cs-rose-900);
|
||||
--color-rose-950: var(--cs-rose-950);
|
||||
|
||||
/* Black & White */
|
||||
--color-black: var(--cs-black);
|
||||
--color-white: var(--cs-white);
|
||||
|
||||
/* ==================== */
|
||||
/* Spacing & Sizing(自定义尺寸,额外扩展) */
|
||||
/* ==================== */
|
||||
|
||||
--spacing-5xs: var(--cs-size-5xs);
|
||||
--spacing-4xs: var(--cs-size-4xs);
|
||||
--spacing-3xs: var(--cs-size-3xs);
|
||||
--spacing-2xs: var(--cs-size-2xs);
|
||||
--spacing-xs: var(--cs-size-xs);
|
||||
--spacing-sm: var(--cs-size-sm);
|
||||
--spacing-md: var(--cs-size-md);
|
||||
--spacing-lg: var(--cs-size-lg);
|
||||
--spacing-xl: var(--cs-size-xl);
|
||||
--spacing-2xl: var(--cs-size-2xl);
|
||||
--spacing-3xl: var(--cs-size-3xl);
|
||||
--spacing-4xl: var(--cs-size-4xl);
|
||||
--spacing-5xl: var(--cs-size-5xl);
|
||||
--spacing-6xl: var(--cs-size-6xl);
|
||||
--spacing-7xl: var(--cs-size-7xl);
|
||||
--spacing-8xl: var(--cs-size-8xl);
|
||||
|
||||
/* ==================== */
|
||||
/* Border Radius(自定义圆角,额外扩展) */
|
||||
/* ==================== */
|
||||
|
||||
--radius-4xs: var(--cs-radius-4xs);
|
||||
--radius-3xs: var(--cs-radius-3xs);
|
||||
--radius-2xs: var(--cs-radius-2xs);
|
||||
--radius-xs: var(--cs-radius-xs);
|
||||
--radius-sm: var(--cs-radius-sm);
|
||||
--radius-md: var(--cs-radius-md);
|
||||
--radius-lg: var(--cs-radius-lg);
|
||||
--radius-xl: var(--cs-radius-xl);
|
||||
--radius-2xl: var(--cs-radius-2xl);
|
||||
--radius-3xl: var(--cs-radius-3xl);
|
||||
--radius-round: var(--cs-radius-round);
|
||||
}
|
||||
|
||||
/* ==================== */
|
||||
/* Dark Mode 覆盖 */
|
||||
/* ==================== */
|
||||
|
||||
@theme dark {
|
||||
--color-background: var(--cs-background);
|
||||
--color-foreground: var(--cs-foreground);
|
||||
--color-card: var(--cs-card);
|
||||
--color-popover: var(--cs-popover);
|
||||
--color-border: var(--cs-border);
|
||||
--color-input: var(--cs-border);
|
||||
--color-secondary: var(--cs-secondary);
|
||||
--color-muted: var(--cs-muted);
|
||||
--color-accent: var(--cs-accent);
|
||||
--color-sidebar: var(--cs-sidebar);
|
||||
--color-sidebar-accent: var(--cs-sidebar-accent);
|
||||
}
|
||||
|
||||
/* ==================== */
|
||||
/* Base Styles(可选) */
|
||||
/* ==================== */
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
560
packages/ui/src/styles/tokens.css
Normal file
560
packages/ui/src/styles/tokens.css
Normal file
@@ -0,0 +1,560 @@
|
||||
/**
|
||||
* CherryStudio Design Tokens
|
||||
*
|
||||
* 基于 todocss.css 转换的设计令牌
|
||||
* - 所有变量使用 --cs-* 前缀(CherryStudio)
|
||||
* - 合并重复的 Spacing/Sizing
|
||||
* - 修正错误(如 font-weight 单位)
|
||||
* - 删除 Opacity 变量(使用 Tailwind 修饰符)
|
||||
* - 正确分离 Light/Dark Mode
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ==================== */
|
||||
/* Primitive Colors - Light Mode */
|
||||
/* ==================== */
|
||||
|
||||
/* Neutral */
|
||||
--cs-neutral-50: hsla(0, 0%, 98%, 1);
|
||||
--cs-neutral-100: hsla(0, 0%, 96%, 1);
|
||||
--cs-neutral-200: hsla(0, 0%, 90%, 1);
|
||||
--cs-neutral-300: hsla(0, 0%, 83%, 1);
|
||||
--cs-neutral-400: hsla(0, 0%, 64%, 1);
|
||||
--cs-neutral-500: hsla(0, 0%, 45%, 1);
|
||||
--cs-neutral-600: hsla(215, 14%, 34%, 1);
|
||||
--cs-neutral-700: hsla(0, 0%, 25%, 1);
|
||||
--cs-neutral-800: hsla(0, 0%, 15%, 1);
|
||||
--cs-neutral-900: hsla(0, 0%, 9%, 1);
|
||||
--cs-neutral-950: hsla(0, 0%, 4%, 1);
|
||||
|
||||
/* Stone */
|
||||
--cs-stone-50: hsla(60, 9%, 98%, 1);
|
||||
--cs-stone-100: hsla(60, 5%, 96%, 1);
|
||||
--cs-stone-200: hsla(20, 6%, 90%, 1);
|
||||
--cs-stone-300: hsla(24, 6%, 83%, 1);
|
||||
--cs-stone-400: hsla(24, 5%, 64%, 1);
|
||||
--cs-stone-500: hsla(25, 5%, 45%, 1);
|
||||
--cs-stone-600: hsla(33, 5%, 32%, 1);
|
||||
--cs-stone-700: hsla(30, 6%, 25%, 1);
|
||||
--cs-stone-800: hsla(12, 6%, 15%, 1);
|
||||
--cs-stone-900: hsla(24, 10%, 10%, 1);
|
||||
--cs-stone-950: hsla(20, 14%, 4%, 1);
|
||||
|
||||
/* Zinc */
|
||||
--cs-zinc-50: hsla(0, 0%, 98%, 1);
|
||||
--cs-zinc-100: hsla(240, 5%, 96%, 1);
|
||||
--cs-zinc-200: hsla(240, 6%, 90%, 1);
|
||||
--cs-zinc-300: hsla(240, 5%, 84%, 1);
|
||||
--cs-zinc-400: hsla(240, 5%, 65%, 1);
|
||||
--cs-zinc-500: hsla(240, 4%, 46%, 1);
|
||||
--cs-zinc-600: hsla(240, 5%, 34%, 1);
|
||||
--cs-zinc-700: hsla(240, 5%, 26%, 1);
|
||||
--cs-zinc-800: hsla(240, 4%, 16%, 1);
|
||||
--cs-zinc-900: hsla(240, 6%, 10%, 1);
|
||||
--cs-zinc-950: hsla(240, 10%, 4%, 1);
|
||||
|
||||
/* Slate */
|
||||
--cs-slate-50: hsla(210, 40%, 98%, 1);
|
||||
--cs-slate-100: hsla(210, 40%, 96%, 1);
|
||||
--cs-slate-200: hsla(214, 32%, 91%, 1);
|
||||
--cs-slate-300: hsla(213, 27%, 84%, 1);
|
||||
--cs-slate-400: hsla(215, 20%, 65%, 1);
|
||||
--cs-slate-500: hsla(215, 16%, 47%, 1);
|
||||
--cs-slate-600: hsla(215, 19%, 35%, 1);
|
||||
--cs-slate-700: hsla(215, 25%, 27%, 1);
|
||||
--cs-slate-800: hsla(217, 33%, 17%, 1);
|
||||
--cs-slate-900: hsla(222, 47%, 11%, 1);
|
||||
--cs-slate-950: hsla(229, 84%, 5%, 1);
|
||||
|
||||
/* Gray */
|
||||
--cs-gray-50: hsla(210, 20%, 98%, 1);
|
||||
--cs-gray-100: hsla(220, 14%, 96%, 1);
|
||||
--cs-gray-200: hsla(220, 13%, 91%, 1);
|
||||
--cs-gray-300: hsla(216, 12%, 84%, 1);
|
||||
--cs-gray-400: hsla(218, 11%, 65%, 1);
|
||||
--cs-gray-500: hsla(220, 9%, 46%, 1);
|
||||
--cs-gray-600: hsla(0, 0%, 32%, 1);
|
||||
--cs-gray-700: hsla(217, 19%, 27%, 1);
|
||||
--cs-gray-800: hsla(215, 28%, 17%, 1);
|
||||
--cs-gray-900: hsla(221, 39%, 11%, 1);
|
||||
--cs-gray-950: hsla(224, 71%, 4%, 1);
|
||||
|
||||
/* Red */
|
||||
--cs-red-50: hsla(0, 86%, 97%, 1);
|
||||
--cs-red-100: hsla(0, 93%, 94%, 1);
|
||||
--cs-red-200: hsla(0, 96%, 89%, 1);
|
||||
--cs-red-300: hsla(0, 94%, 82%, 1);
|
||||
--cs-red-400: hsla(0, 91%, 71%, 1);
|
||||
--cs-red-500: hsla(0, 84%, 60%, 1);
|
||||
--cs-red-600: hsla(0, 72%, 51%, 1);
|
||||
--cs-red-700: hsla(0, 74%, 42%, 1);
|
||||
--cs-red-800: hsla(0, 70%, 35%, 1);
|
||||
--cs-red-900: hsla(0, 63%, 31%, 1);
|
||||
--cs-red-950: hsla(0, 75%, 15%, 1);
|
||||
|
||||
/* Orange */
|
||||
--cs-orange-50: hsla(33, 100%, 96%, 1);
|
||||
--cs-orange-100: hsla(34, 100%, 92%, 1);
|
||||
--cs-orange-200: hsla(32, 98%, 83%, 1);
|
||||
--cs-orange-300: hsla(31, 97%, 72%, 1);
|
||||
--cs-orange-400: hsla(27, 96%, 61%, 1);
|
||||
--cs-orange-500: hsla(25, 95%, 53%, 1);
|
||||
--cs-orange-600: hsla(21, 90%, 48%, 1);
|
||||
--cs-orange-700: hsla(17, 88%, 40%, 1);
|
||||
--cs-orange-800: hsla(15, 79%, 34%, 1);
|
||||
--cs-orange-900: hsla(15, 75%, 28%, 1);
|
||||
--cs-orange-950: hsla(13, 81%, 15%, 1);
|
||||
|
||||
/* Amber */
|
||||
--cs-amber-50: hsla(48, 100%, 96%, 1);
|
||||
--cs-amber-100: hsla(48, 96%, 89%, 1);
|
||||
--cs-amber-200: hsla(48, 97%, 77%, 1);
|
||||
--cs-amber-300: hsla(46, 97%, 65%, 1);
|
||||
--cs-amber-400: hsla(43, 96%, 56%, 1);
|
||||
--cs-amber-500: hsla(38, 92%, 50%, 1);
|
||||
--cs-amber-600: hsla(32, 95%, 44%, 1);
|
||||
--cs-amber-700: hsla(26, 90%, 37%, 1);
|
||||
--cs-amber-800: hsla(23, 83%, 31%, 1);
|
||||
--cs-amber-900: hsla(22, 78%, 26%, 1);
|
||||
--cs-amber-950: hsla(21, 92%, 14%, 1);
|
||||
|
||||
/* Yellow */
|
||||
--cs-yellow-50: hsla(55, 92%, 95%, 1);
|
||||
--cs-yellow-100: hsla(55, 97%, 88%, 1);
|
||||
--cs-yellow-200: hsla(53, 98%, 77%, 1);
|
||||
--cs-yellow-300: hsla(50, 98%, 64%, 1);
|
||||
--cs-yellow-400: hsla(48, 96%, 53%, 1);
|
||||
--cs-yellow-500: hsla(45, 93%, 47%, 1);
|
||||
--cs-yellow-600: hsla(41, 96%, 40%, 1);
|
||||
--cs-yellow-700: hsla(35, 92%, 33%, 1);
|
||||
--cs-yellow-800: hsla(32, 81%, 29%, 1);
|
||||
--cs-yellow-900: hsla(28, 73%, 26%, 1);
|
||||
--cs-yellow-950: hsla(26, 83%, 14%, 1);
|
||||
|
||||
/* Lime (品牌主色) */
|
||||
--cs-lime-50: hsla(78, 92%, 95%, 1);
|
||||
--cs-lime-100: hsla(80, 89%, 89%, 1);
|
||||
--cs-lime-200: hsla(81, 88%, 80%, 1);
|
||||
--cs-lime-300: hsla(82, 85%, 67%, 1);
|
||||
--cs-lime-400: hsla(83, 78%, 55%, 1);
|
||||
--cs-lime-500: hsla(84, 81%, 44%, 1);
|
||||
--cs-lime-600: hsla(85, 85%, 35%, 1);
|
||||
--cs-lime-700: hsla(86, 78%, 27%, 1);
|
||||
--cs-lime-800: hsla(86, 69%, 23%, 1);
|
||||
--cs-lime-900: hsla(88, 61%, 20%, 1);
|
||||
--cs-lime-950: hsla(89, 80%, 10%, 1);
|
||||
|
||||
/* Green */
|
||||
--cs-green-50: hsla(138, 76%, 97%, 1);
|
||||
--cs-green-100: hsla(141, 84%, 93%, 1);
|
||||
--cs-green-200: hsla(141, 79%, 85%, 1);
|
||||
--cs-green-300: hsla(142, 77%, 73%, 1);
|
||||
--cs-green-400: hsla(142, 69%, 58%, 1);
|
||||
--cs-green-500: hsla(142, 71%, 45%, 1);
|
||||
--cs-green-600: hsla(142, 76%, 36%, 1);
|
||||
--cs-green-700: hsla(142, 72%, 29%, 1);
|
||||
--cs-green-800: hsla(143, 64%, 24%, 1);
|
||||
--cs-green-900: hsla(144, 61%, 20%, 1);
|
||||
--cs-green-950: hsla(145, 80%, 10%, 1);
|
||||
|
||||
/* Emerald */
|
||||
--cs-emerald-50: hsla(152, 81%, 96%, 1);
|
||||
--cs-emerald-100: hsla(149, 80%, 90%, 1);
|
||||
--cs-emerald-200: hsla(152, 76%, 80%, 1);
|
||||
--cs-emerald-300: hsla(156, 72%, 67%, 1);
|
||||
--cs-emerald-400: hsla(158, 64%, 52%, 1);
|
||||
--cs-emerald-500: hsla(160, 84%, 39%, 1);
|
||||
--cs-emerald-600: hsla(161, 94%, 30%, 1);
|
||||
--cs-emerald-700: hsla(163, 94%, 24%, 1);
|
||||
--cs-emerald-800: hsla(163, 88%, 20%, 1);
|
||||
--cs-emerald-900: hsla(164, 86%, 16%, 1);
|
||||
--cs-emerald-950: hsla(166, 91%, 9%, 1);
|
||||
|
||||
/* Teal */
|
||||
--cs-teal-50: hsla(166, 76%, 97%, 1);
|
||||
--cs-teal-100: hsla(167, 85%, 89%, 1);
|
||||
--cs-teal-200: hsla(168, 84%, 78%, 1);
|
||||
--cs-teal-300: hsla(171, 77%, 64%, 1);
|
||||
--cs-teal-400: hsla(172, 66%, 50%, 1);
|
||||
--cs-teal-500: hsla(173, 80%, 40%, 1);
|
||||
--cs-teal-600: hsla(175, 84%, 32%, 1);
|
||||
--cs-teal-700: hsla(175, 77%, 26%, 1);
|
||||
--cs-teal-800: hsla(176, 69%, 22%, 1);
|
||||
--cs-teal-900: hsla(176, 61%, 19%, 1);
|
||||
--cs-teal-950: hsla(179, 84%, 10%, 1);
|
||||
|
||||
/* Cyan */
|
||||
--cs-cyan-50: hsla(183, 100%, 96%, 1);
|
||||
--cs-cyan-100: hsla(185, 96%, 90%, 1);
|
||||
--cs-cyan-200: hsla(186, 94%, 82%, 1);
|
||||
--cs-cyan-300: hsla(187, 92%, 69%, 1);
|
||||
--cs-cyan-400: hsla(188, 86%, 53%, 1);
|
||||
--cs-cyan-500: hsla(189, 94%, 43%, 1);
|
||||
--cs-cyan-600: hsla(192, 91%, 36%, 1);
|
||||
--cs-cyan-700: hsla(193, 82%, 31%, 1);
|
||||
--cs-cyan-800: hsla(194, 70%, 27%, 1);
|
||||
--cs-cyan-900: hsla(196, 64%, 24%, 1);
|
||||
--cs-cyan-950: hsla(197, 79%, 15%, 1);
|
||||
|
||||
/* Sky */
|
||||
--cs-sky-50: hsla(204, 100%, 97%, 1);
|
||||
--cs-sky-100: hsla(204, 94%, 94%, 1);
|
||||
--cs-sky-200: hsla(201, 94%, 86%, 1);
|
||||
--cs-sky-300: hsla(199, 95%, 74%, 1);
|
||||
--cs-sky-400: hsla(198, 93%, 60%, 1);
|
||||
--cs-sky-500: hsla(199, 89%, 48%, 1);
|
||||
--cs-sky-600: hsla(200, 98%, 39%, 1);
|
||||
--cs-sky-700: hsla(201, 96%, 32%, 1);
|
||||
--cs-sky-800: hsla(201, 90%, 27%, 1);
|
||||
--cs-sky-900: hsla(202, 80%, 24%, 1);
|
||||
--cs-sky-950: hsla(204, 80%, 16%, 1);
|
||||
|
||||
/* Blue */
|
||||
--cs-blue-50: hsla(214, 100%, 97%, 1);
|
||||
--cs-blue-100: hsla(214, 95%, 93%, 1);
|
||||
--cs-blue-200: hsla(213, 97%, 87%, 1);
|
||||
--cs-blue-300: hsla(212, 96%, 78%, 1);
|
||||
--cs-blue-400: hsla(213, 94%, 68%, 1);
|
||||
--cs-blue-500: hsla(217, 91%, 60%, 1);
|
||||
--cs-blue-600: hsla(221, 83%, 53%, 1);
|
||||
--cs-blue-700: hsla(224, 76%, 48%, 1);
|
||||
--cs-blue-800: hsla(226, 71%, 40%, 1);
|
||||
--cs-blue-900: hsla(224, 64%, 33%, 1);
|
||||
--cs-blue-950: hsla(226, 57%, 21%, 1);
|
||||
|
||||
/* Indigo */
|
||||
--cs-indigo-50: hsla(226, 100%, 97%, 1);
|
||||
--cs-indigo-100: hsla(226, 100%, 94%, 1);
|
||||
--cs-indigo-200: hsla(228, 96%, 89%, 1);
|
||||
--cs-indigo-300: hsla(230, 94%, 82%, 1);
|
||||
--cs-indigo-400: hsla(234, 89%, 74%, 1);
|
||||
--cs-indigo-500: hsla(239, 84%, 67%, 1);
|
||||
--cs-indigo-600: hsla(243, 75%, 59%, 1);
|
||||
--cs-indigo-700: hsla(245, 58%, 51%, 1);
|
||||
--cs-indigo-800: hsla(244, 55%, 41%, 1);
|
||||
--cs-indigo-900: hsla(242, 47%, 34%, 1);
|
||||
--cs-indigo-950: hsla(244, 47%, 20%, 1);
|
||||
|
||||
/* Violet */
|
||||
--cs-violet-50: hsla(250, 100%, 98%, 1);
|
||||
--cs-violet-100: hsla(251, 91%, 95%, 1);
|
||||
--cs-violet-200: hsla(251, 95%, 92%, 1);
|
||||
--cs-violet-300: hsla(253, 95%, 85%, 1);
|
||||
--cs-violet-400: hsla(255, 92%, 76%, 1);
|
||||
--cs-violet-500: hsla(258, 90%, 66%, 1);
|
||||
--cs-violet-600: hsla(262, 83%, 58%, 1);
|
||||
--cs-violet-700: hsla(263, 70%, 50%, 1);
|
||||
--cs-violet-800: hsla(263, 69%, 42%, 1);
|
||||
--cs-violet-900: hsla(264, 67%, 35%, 1);
|
||||
--cs-violet-950: hsla(262, 78%, 23%, 1);
|
||||
|
||||
/* Purple */
|
||||
--cs-purple-50: hsla(270, 100%, 98%, 1);
|
||||
--cs-purple-100: hsla(269, 100%, 95%, 1);
|
||||
--cs-purple-200: hsla(269, 100%, 92%, 1);
|
||||
--cs-purple-300: hsla(269, 97%, 85%, 1);
|
||||
--cs-purple-400: hsla(270, 95%, 75%, 1);
|
||||
--cs-purple-500: hsla(271, 91%, 65%, 1);
|
||||
--cs-purple-600: hsla(271, 81%, 56%, 1);
|
||||
--cs-purple-700: hsla(272, 72%, 47%, 1);
|
||||
--cs-purple-800: hsla(273, 67%, 39%, 1);
|
||||
--cs-purple-900: hsla(274, 66%, 32%, 1);
|
||||
--cs-purple-950: hsla(274, 87%, 21%, 1);
|
||||
|
||||
/* Fuchsia */
|
||||
--cs-fuchsia-50: hsla(289, 100%, 98%, 1);
|
||||
--cs-fuchsia-100: hsla(287, 100%, 95%, 1);
|
||||
--cs-fuchsia-200: hsla(288, 96%, 91%, 1);
|
||||
--cs-fuchsia-300: hsla(291, 93%, 83%, 1);
|
||||
--cs-fuchsia-400: hsla(292, 91%, 73%, 1);
|
||||
--cs-fuchsia-500: hsla(292, 84%, 61%, 1);
|
||||
--cs-fuchsia-600: hsla(293, 69%, 49%, 1);
|
||||
--cs-fuchsia-700: hsla(295, 72%, 40%, 1);
|
||||
--cs-fuchsia-800: hsla(295, 70%, 33%, 1);
|
||||
--cs-fuchsia-900: hsla(297, 64%, 28%, 1);
|
||||
--cs-fuchsia-950: hsla(297, 90%, 16%, 1);
|
||||
|
||||
/* Pink */
|
||||
--cs-pink-50: hsla(327, 73%, 97%, 1);
|
||||
--cs-pink-100: hsla(326, 78%, 95%, 1);
|
||||
--cs-pink-200: hsla(326, 85%, 90%, 1);
|
||||
--cs-pink-300: hsla(327, 87%, 82%, 1);
|
||||
--cs-pink-400: hsla(329, 86%, 70%, 1);
|
||||
--cs-pink-500: hsla(330, 81%, 60%, 1);
|
||||
--cs-pink-600: hsla(333, 71%, 51%, 1);
|
||||
--cs-pink-700: hsla(335, 78%, 42%, 1);
|
||||
--cs-pink-800: hsla(336, 74%, 35%, 1);
|
||||
--cs-pink-900: hsla(336, 69%, 30%, 1);
|
||||
--cs-pink-950: hsla(336, 84%, 17%, 1);
|
||||
|
||||
/* Rose */
|
||||
--cs-rose-50: hsla(356, 100%, 97%, 1);
|
||||
--cs-rose-100: hsla(356, 100%, 95%, 1);
|
||||
--cs-rose-200: hsla(353, 96%, 90%, 1);
|
||||
--cs-rose-300: hsla(353, 96%, 82%, 1);
|
||||
--cs-rose-400: hsla(351, 95%, 71%, 1);
|
||||
--cs-rose-500: hsla(350, 89%, 60%, 1);
|
||||
--cs-rose-600: hsla(347, 77%, 50%, 1);
|
||||
--cs-rose-700: hsla(345, 83%, 41%, 1);
|
||||
--cs-rose-800: hsla(343, 80%, 35%, 1);
|
||||
--cs-rose-900: hsla(342, 75%, 30%, 1);
|
||||
--cs-rose-950: hsla(343, 88%, 16%, 1);
|
||||
|
||||
/* Black & White */
|
||||
--cs-black: hsla(0, 0%, 0%, 1);
|
||||
--cs-white: hsla(0, 0%, 100%, 1);
|
||||
|
||||
/* ==================== */
|
||||
/* Semantic Tokens - Light Mode */
|
||||
/* ==================== */
|
||||
|
||||
/* Brand Colors */
|
||||
--cs-primary: var(--cs-lime-500);
|
||||
--cs-destructive: var(--cs-red-500);
|
||||
--cs-success: var(--cs-green-500);
|
||||
--cs-warning: var(--cs-amber-500);
|
||||
|
||||
/* Background & Foreground */
|
||||
--cs-background: var(--cs-zinc-50);
|
||||
--cs-background-subtle: hsla(0, 0%, 0%, 0.02);
|
||||
--cs-foreground: hsla(0, 0%, 0%, 0.9);
|
||||
--cs-foreground-secondary: hsla(0, 0%, 0%, 0.6);
|
||||
--cs-foreground-muted: hsla(0, 0%, 0%, 0.4);
|
||||
|
||||
/* Card & Popover */
|
||||
--cs-card: var(--cs-white);
|
||||
--cs-popover: var(--cs-white);
|
||||
|
||||
/* Border */
|
||||
--cs-border: hsla(0, 0%, 0%, 0.1);
|
||||
--cs-border-hover: hsla(0, 0%, 0%, 0.2);
|
||||
--cs-border-active: hsla(0, 0%, 0%, 0.3);
|
||||
|
||||
/* Ring (Focus) */
|
||||
--cs-ring: hsla(84, 81%, 44%, 0.4);
|
||||
|
||||
/* UI Element Colors */
|
||||
--cs-secondary: hsla(0, 0%, 0%, 0.05); /* Secondary Button Background */
|
||||
--cs-secondary-hover: hsla(0, 0%, 0%, 0.85);
|
||||
--cs-secondary-active: hsla(0, 0%, 0%, 0.7);
|
||||
--cs-muted: hsla(0, 0%, 0%, 0.05); /* Muted/Subtle Background */
|
||||
--cs-accent: hsla(0, 0%, 0%, 0.05); /* Accent Background */
|
||||
--cs-ghost-hover: hsla(0, 0%, 0%, 0.05); /* Ghost Button Hover */
|
||||
--cs-ghost-active: hsla(0, 0%, 0%, 0.1); /* Ghost Button Active */
|
||||
|
||||
/* Sidebar */
|
||||
--cs-sidebar: var(--cs-white);
|
||||
--cs-sidebar-accent: hsla(0, 0%, 0%, 0.05);
|
||||
|
||||
/* ==================== */
|
||||
/* UI 元素细分颜色 */
|
||||
/* ==================== */
|
||||
|
||||
/* Modal / Overlay */
|
||||
--cs-modal-backdrop: hsla(0, 0%, 0%, 0.4);
|
||||
--cs-modal-thumb: hsla(0, 0%, 0%, 0.2);
|
||||
--cs-modal-thumb-hover: hsla(0, 0%, 0%, 0.3);
|
||||
|
||||
/* Icon */
|
||||
--cs-icon-default: var(--cs-foreground-secondary);
|
||||
--cs-icon-hover: var(--cs-foreground);
|
||||
|
||||
/* Input / Select */
|
||||
--cs-input-background: var(--cs-white);
|
||||
--cs-input-border: var(--cs-border);
|
||||
--cs-input-border-hover: var(--cs-border-hover);
|
||||
--cs-input-border-focus: var(--cs-primary);
|
||||
|
||||
/* Primary Button */
|
||||
--cs-primary-button-background: var(--cs-primary);
|
||||
--cs-primary-button-text: var(--cs-white);
|
||||
--cs-primary-button-background-hover: hsla(84, 81%, 44%, 0.85);
|
||||
--cs-primary-button-background-active: hsla(84, 81%, 44%, 0.7);
|
||||
--cs-primary-button-background-2nd: hsla(84, 81%, 44%, 0.1);
|
||||
--cs-primary-button-background-3rd: hsla(84, 81%, 44%, 0.05);
|
||||
|
||||
/* Secondary Button */
|
||||
--cs-secondary-button-background: var(--cs-secondary);
|
||||
--cs-secondary-button-text: var(--cs-foreground);
|
||||
--cs-secondary-button-background-hover: hsla(0, 0%, 0%, 0.85);
|
||||
--cs-secondary-button-background-active: hsla(0, 0%, 0%, 0.7);
|
||||
--cs-secondary-button-border: var(--cs-border);
|
||||
|
||||
/* Ghost Button */
|
||||
--cs-ghost-button-background: hsla(0, 0%, 0%, 0);
|
||||
--cs-ghost-button-text: var(--cs-foreground);
|
||||
--cs-ghost-button-background-hover: var(--cs-ghost-hover);
|
||||
--cs-ghost-button-background-active: var(--cs-ghost-active);
|
||||
|
||||
/* ==================== */
|
||||
/* Spacing & Sizing (合并) */
|
||||
/* ==================== */
|
||||
|
||||
--cs-size-5xs: 0.25rem; /* 4px */
|
||||
--cs-size-4xs: 0.5rem; /* 8px */
|
||||
--cs-size-3xs: 0.75rem; /* 12px */
|
||||
--cs-size-2xs: 1rem; /* 16px */
|
||||
--cs-size-xs: 1.5rem; /* 24px */
|
||||
--cs-size-sm: 2rem; /* 32px */
|
||||
--cs-size-md: 2.5rem; /* 40px */
|
||||
--cs-size-lg: 3rem; /* 48px */
|
||||
--cs-size-xl: 3.5rem; /* 56px */
|
||||
--cs-size-2xl: 4rem; /* 64px */
|
||||
--cs-size-3xl: 4.5rem; /* 72px */
|
||||
--cs-size-4xl: 5rem; /* 80px */
|
||||
--cs-size-5xl: 5.5rem; /* 88px */
|
||||
--cs-size-6xl: 6rem; /* 96px */
|
||||
--cs-size-7xl: 6.5rem; /* 104px */
|
||||
--cs-size-8xl: 7rem; /* 112px */
|
||||
|
||||
/* ==================== */
|
||||
/* Border Radius */
|
||||
/* ==================== */
|
||||
|
||||
--cs-radius-4xs: 4px; /* 4px */
|
||||
--cs-radius-3xs: 8px; /* 8px */
|
||||
--cs-radius-2xs: 12px; /* 12px */
|
||||
--cs-radius-xs: 16px; /* 16px */
|
||||
--cs-radius-sm: 24px; /* 24px */
|
||||
--cs-radius-md: 32px; /* 32px */
|
||||
--cs-radius-lg: 2.5rem; /* 40px */
|
||||
--cs-radius-xl: 48px; /* 48px */
|
||||
--cs-radius-2xl: 56px; /* 56px */
|
||||
--cs-radius-3xl: 64px; /* 64px */
|
||||
--cs-radius-round: 999px; /* 保持 px,因为是特殊值 */
|
||||
|
||||
/* Border Width */
|
||||
--cs-border-width-sm: 1px; /* 1px */
|
||||
--cs-border-width-md: 2px; /* 2px */
|
||||
--cs-border-width-lg: 3px; /* 3px */
|
||||
|
||||
/* ==================== */
|
||||
/* Typography */
|
||||
/* ==================== */
|
||||
|
||||
/* Font Families */
|
||||
--cs-font-family-heading: Inter;
|
||||
--cs-font-family-body: Inter;
|
||||
|
||||
/* Font Weights (修正单位错误) */
|
||||
--cs-font-weight-regular: 400;
|
||||
--cs-font-weight-medium: 500;
|
||||
--cs-font-weight-bold: 700;
|
||||
|
||||
/* Font Sizes - Body */
|
||||
--cs-font-size-body-xs: 0.75rem; /* 12px */
|
||||
--cs-font-size-body-sm: 0.875rem; /* 14px */
|
||||
--cs-font-size-body-md: 1rem; /* 16px */
|
||||
--cs-font-size-body-lg: 1.125rem; /* 18px */
|
||||
|
||||
/* Font Sizes - Heading */
|
||||
--cs-font-size-heading-xs: 1.25rem; /* 20px */
|
||||
--cs-font-size-heading-sm: 1.5rem; /* 24px */
|
||||
--cs-font-size-heading-md: 2rem; /* 32px */
|
||||
--cs-font-size-heading-lg: 2.5rem; /* 40px */
|
||||
--cs-font-size-heading-xl: 3rem; /* 48px */
|
||||
--cs-font-size-heading-2xl: 3.75rem; /* 60px */
|
||||
|
||||
/* Line Heights - Body */
|
||||
--cs-line-height-body-xs: 1.25rem; /* 20px */
|
||||
--cs-line-height-body-sm: 1.5rem; /* 24px */
|
||||
--cs-line-height-body-md: 1.5rem; /* 24px */
|
||||
--cs-line-height-body-lg: 1.75rem; /* 28px */
|
||||
|
||||
/* Line Heights - Heading */
|
||||
--cs-line-height-heading-xs: 2rem; /* 32px */
|
||||
--cs-line-height-heading-sm: 2.5rem; /* 40px */
|
||||
--cs-line-height-heading-md: 3rem; /* 48px */
|
||||
--cs-line-height-heading-lg: 3.75rem; /* 60px */
|
||||
--cs-line-height-heading-xl: 5rem; /* 80px */
|
||||
|
||||
/* Paragraph Spacing */
|
||||
--cs-paragraph-spacing-body-xs: 0.75rem; /* 12px */
|
||||
--cs-paragraph-spacing-body-sm: 0.875rem; /* 14px */
|
||||
--cs-paragraph-spacing-body-md: 1rem; /* 16px */
|
||||
--cs-paragraph-spacing-body-lg: 1.125rem; /* 18px */
|
||||
--cs-paragraph-spacing-heading-xs: 1.25rem; /* 20px */
|
||||
--cs-paragraph-spacing-heading-sm: 1.5rem; /* 24px */
|
||||
--cs-paragraph-spacing-heading-md: 2rem; /* 32px */
|
||||
--cs-paragraph-spacing-heading-lg: 2.5rem; /* 40px */
|
||||
--cs-paragraph-spacing-heading-xl: 3rem; /* 48px */
|
||||
--cs-paragraph-spacing-heading-2xl: 3.75rem; /* 60px */
|
||||
}
|
||||
|
||||
/* ==================== */
|
||||
/* Dark Mode */
|
||||
/* ==================== */
|
||||
|
||||
.dark {
|
||||
/* Background & Foreground */
|
||||
--cs-background: var(--cs-zinc-900);
|
||||
--cs-background-subtle: hsla(0, 0%, 100%, 0.02);
|
||||
--cs-foreground: hsla(0, 0%, 100%, 0.9);
|
||||
--cs-foreground-secondary: hsla(0, 0%, 100%, 0.6);
|
||||
--cs-foreground-muted: hsla(0, 0%, 100%, 0.4);
|
||||
|
||||
/* Card & Popover */
|
||||
--cs-card: var(--cs-black);
|
||||
--cs-popover: var(--cs-black);
|
||||
|
||||
/* Border */
|
||||
--cs-border: hsla(0, 0%, 100%, 0.1);
|
||||
--cs-border-hover: hsla(0, 0%, 100%, 0.2);
|
||||
--cs-border-active: hsla(0, 0%, 100%, 0.3);
|
||||
|
||||
/* Ring (Focus) - 保持不变 */
|
||||
--cs-ring: hsla(84, 81%, 44%, 0.4);
|
||||
|
||||
/* UI Element Colors - Dark Mode */
|
||||
--cs-secondary: hsla(0, 0%, 100%, 0.1); /* Secondary Button Background */
|
||||
--cs-secondary-hover: hsla(0, 0%, 100%, 0.2);
|
||||
--cs-secondary-active: hsla(0, 0%, 100%, 0.25);
|
||||
--cs-muted: hsla(0, 0%, 100%, 0.1); /* Muted/Subtle Background */
|
||||
--cs-accent: hsla(0, 0%, 100%, 0.1); /* Accent Background */
|
||||
--cs-ghost-hover: hsla(0, 0%, 100%, 0.1); /* Ghost Button Hover */
|
||||
--cs-ghost-active: hsla(0, 0%, 100%, 0.15); /* Ghost Button Active */
|
||||
|
||||
/* Sidebar */
|
||||
--cs-sidebar: var(--cs-black);
|
||||
--cs-sidebar-accent: hsla(0, 0%, 100%, 0.1);
|
||||
|
||||
/* ==================== */
|
||||
/* UI 元素细分颜色 (Dark Mode) */
|
||||
/* ==================== */
|
||||
|
||||
/* Modal / Overlay */
|
||||
--cs-modal-backdrop: hsla(0, 0%, 0%, 0.06);
|
||||
--cs-modal-thumb: hsla(0, 0%, 100%, 0.2);
|
||||
--cs-modal-thumb-hover: hsla(0, 0%, 100%, 0.3);
|
||||
|
||||
/* Icon */
|
||||
--cs-icon-default: var(--cs-foreground-secondary);
|
||||
--cs-icon-hover: var(--cs-foreground);
|
||||
|
||||
/* Input / Select */
|
||||
--cs-input-background: var(--cs-black);
|
||||
--cs-input-border: var(--cs-border);
|
||||
--cs-input-border-hover: var(--cs-border-hover);
|
||||
--cs-input-border-focus: var(--cs-primary);
|
||||
|
||||
/* Primary Button - 保持不变 */
|
||||
--cs-primary-button-background: var(--cs-primary);
|
||||
--cs-primary-button-text: var(--cs-white);
|
||||
--cs-primary-button-background-hover: hsla(84, 81%, 44%, 0.85);
|
||||
--cs-primary-button-background-active: hsla(84, 81%, 44%, 0.7);
|
||||
--cs-primary-button-background-2nd: hsla(84, 81%, 44%, 0.1);
|
||||
--cs-primary-button-background-3rd: hsla(84, 81%, 44%, 0.05);
|
||||
|
||||
/* Secondary Button */
|
||||
--cs-secondary-button-background: var(--cs-secondary);
|
||||
--cs-secondary-button-text: var(--cs-foreground);
|
||||
--cs-secondary-button-background-hover: hsla(0, 0%, 100%, 0.2);
|
||||
--cs-secondary-button-background-active: hsla(0, 0%, 100%, 0.25);
|
||||
--cs-secondary-button-border: var(--cs-border);
|
||||
|
||||
/* Ghost Button */
|
||||
--cs-ghost-button-background: hsla(0, 0%, 100%, 0);
|
||||
--cs-ghost-button-text: var(--cs-foreground);
|
||||
--cs-ghost-button-background-hover: var(--cs-ghost-hover);
|
||||
--cs-ghost-button-background-active: var(--cs-ghost-active);
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -4,9 +4,9 @@ const { downloadNpmPackage } = require('./utils')
|
||||
// if you want to add new prebuild binaries packages with different architectures, you can add them here
|
||||
// please add to allX64 and allArm64 from yarn.lock
|
||||
const allArm64 = {
|
||||
'@img/sharp-darwin-arm64': '0.34.4',
|
||||
'@img/sharp-win32-arm64': '0.34.4',
|
||||
'@img/sharp-linux-arm64': '0.34.4',
|
||||
'@img/sharp-darwin-arm64': '0.34.3',
|
||||
'@img/sharp-win32-arm64': '0.34.3',
|
||||
'@img/sharp-linux-arm64': '0.34.3',
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64': '1.2.0',
|
||||
'@img/sharp-libvips-linux-arm64': '1.2.0',
|
||||
@@ -20,9 +20,9 @@ const allArm64 = {
|
||||
}
|
||||
|
||||
const allX64 = {
|
||||
'@img/sharp-darwin-x64': '0.34.4',
|
||||
'@img/sharp-linux-x64': '0.34.4',
|
||||
'@img/sharp-win32-x64': '0.34.4',
|
||||
'@img/sharp-darwin-x64': '0.34.3',
|
||||
'@img/sharp-linux-x64': '0.34.3',
|
||||
'@img/sharp-win32-x64': '0.34.3',
|
||||
|
||||
'@img/sharp-libvips-darwin-x64': '1.2.0',
|
||||
'@img/sharp-libvips-linux-x64': '1.2.0',
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createServer } from 'node:http'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
|
||||
import { agentService } from '../services/agents'
|
||||
import { windowService } from '../services/WindowService'
|
||||
import { app } from './app'
|
||||
import { config } from './config'
|
||||
|
||||
@@ -43,6 +45,13 @@ export class ApiServer {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server!.listen(port, host, () => {
|
||||
logger.info('API server started', { host, port })
|
||||
|
||||
// Notify renderer that API server is ready
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send(IpcChannel.ApiServer_Ready)
|
||||
}
|
||||
|
||||
resolve()
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
* TypeScript will error if any endpoint is missing.
|
||||
*/
|
||||
|
||||
import { ocrService } from '@main/services/ocr/OcrService'
|
||||
import type { ApiImplementation } from '@shared/data/api/apiSchemas'
|
||||
|
||||
import { TestService } from '../services/TestService'
|
||||
@@ -13,7 +12,6 @@ import { TestService } from '../services/TestService'
|
||||
// Service instances
|
||||
const testService = TestService.getInstance()
|
||||
|
||||
// Defining all handlers here feels a bit bloated; perhaps we should modularize things?
|
||||
/**
|
||||
* Complete API handlers implementation
|
||||
* Must implement every path+method combination from ApiSchemas
|
||||
@@ -209,40 +207,5 @@ export const apiHandlers: ApiImplementation = {
|
||||
data: { executed: true, timestamp: new Date().toISOString() }
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
||||
'/ocr/providers': {
|
||||
GET: async ({ query }) => {
|
||||
const result = await ocrService.listProviders(query)
|
||||
return { data: result }
|
||||
},
|
||||
POST: async ({ body }) => {
|
||||
const result = await ocrService.createProvider(body)
|
||||
return { data: result }
|
||||
}
|
||||
},
|
||||
|
||||
'/ocr/providers/:id': {
|
||||
GET: async ({ params }) => {
|
||||
const result = await ocrService.getProvider(params.id)
|
||||
return { data: result }
|
||||
},
|
||||
PATCH: async ({ params, body }) => {
|
||||
if (params.id !== body.id) {
|
||||
throw new Error('Provider ID in path does not match ID in body')
|
||||
}
|
||||
const result = await ocrService.updateProvider(params.id, body)
|
||||
return { data: result }
|
||||
},
|
||||
PUT: async ({ params, body }) => {
|
||||
if (params.id !== body.id) {
|
||||
throw new Error('Provider ID in path does not match ID in body')
|
||||
}
|
||||
const result = await ocrService.replaceProvider(body)
|
||||
return { data: result }
|
||||
},
|
||||
DELETE: async ({ params }) => {
|
||||
return ocrService.deleteProvider(params.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ import type { PaginationParams, ServiceOptions } from '@shared/data/api/apiTypes
|
||||
/**
|
||||
* Standard service interface for data operations
|
||||
* Defines the contract that all services should implement
|
||||
* @template T - Type of the entity returned by service methods
|
||||
* @template TCreate - Type of the data required to create a new entity
|
||||
* @template TUpdate - Type of the data required to update an existing entity
|
||||
*/
|
||||
export interface IBaseService<T = any, TCreate = any, TUpdate = any> {
|
||||
/**
|
||||
|
||||
@@ -1,299 +0,0 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { dbService } from '@main/data/db/DbService'
|
||||
import { ocrProviderTable } from '@main/data/db/schemas/ocrProvider'
|
||||
import type { PaginationParams, ServiceOptions } from '@shared/data/api/apiTypes'
|
||||
import type { DbOcrProvider, DbOcrProviderCreate, DbOcrProviderReplace, DbOcrProviderUpdate } from '@types'
|
||||
import { BuiltinOcrProviderIds, isDbOcrProvider } from '@types'
|
||||
import dayjs from 'dayjs'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { merge } from 'lodash'
|
||||
|
||||
import type { IBaseService } from './IBaseService'
|
||||
|
||||
const logger = loggerService.withContext('OcrProviderService')
|
||||
|
||||
/**
|
||||
* Service layer for OCR providers
|
||||
* Implements the standard service interface and handles all OCR provider operations
|
||||
* NOTE: Not completely finished since data architecture is not completely designed and implemented.
|
||||
* It's a early version.
|
||||
*/
|
||||
export class OcrProviderService implements IBaseService<DbOcrProvider, DbOcrProviderCreate, DbOcrProviderUpdate> {
|
||||
/**
|
||||
* Find OCR provider by ID
|
||||
*/
|
||||
async findById(id: string, _options?: ServiceOptions): Promise<DbOcrProvider | null> {
|
||||
try {
|
||||
const providers = await dbService
|
||||
.getDb()
|
||||
.select()
|
||||
.from(ocrProviderTable)
|
||||
.where(eq(ocrProviderTable.id, id))
|
||||
.limit(1)
|
||||
|
||||
if (providers.length === 0) {
|
||||
logger.warn(`OCR provider ${id} not found`)
|
||||
return null
|
||||
}
|
||||
|
||||
logger.debug(`Retrieved OCR provider: ${id}`)
|
||||
return providers[0]
|
||||
} catch (error) {
|
||||
logger.error(`Failed to find OCR provider ${id}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find multiple OCR providers with pagination
|
||||
*/
|
||||
async findMany(
|
||||
params: PaginationParams & Record<string, any>,
|
||||
_options?: ServiceOptions
|
||||
): Promise<{
|
||||
items: DbOcrProvider[]
|
||||
total: number
|
||||
hasNext?: boolean
|
||||
nextCursor?: string
|
||||
}> {
|
||||
try {
|
||||
const { page = 1, limit = 20, cursor } = params
|
||||
|
||||
let providers = await dbService.getDb().select().from(ocrProviderTable)
|
||||
|
||||
// Apply filters if provided
|
||||
if (params.registered) {
|
||||
// This filter would need access to the OCR service registry
|
||||
// For now, we'll return all providers and let the service layer filter
|
||||
logger.debug('Registered filter requested - returning all providers for service layer filtering')
|
||||
}
|
||||
|
||||
const total = providers.length
|
||||
|
||||
// Apply pagination
|
||||
if (cursor) {
|
||||
// Cursor-based pagination
|
||||
const index = providers.findIndex((p) => p.id === cursor)
|
||||
if (index !== -1) {
|
||||
providers = providers.slice(index + 1, index + 1 + limit)
|
||||
}
|
||||
} else {
|
||||
// Offset-based pagination
|
||||
const startIndex = (page - 1) * limit
|
||||
providers = providers.slice(startIndex, startIndex + limit)
|
||||
}
|
||||
|
||||
const hasNext =
|
||||
providers.length === limit && (cursor ? providers[providers.length - 1] !== undefined : page * limit < total)
|
||||
|
||||
logger.debug(`Retrieved ${providers.length} OCR providers`, { total, page, limit })
|
||||
|
||||
return {
|
||||
items: providers,
|
||||
total,
|
||||
hasNext,
|
||||
nextCursor: hasNext && providers.length > 0 ? providers[providers.length - 1].id : undefined
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to find OCR providers', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new OCR provider
|
||||
*/
|
||||
async create(data: DbOcrProviderCreate, _options?: ServiceOptions): Promise<DbOcrProvider> {
|
||||
try {
|
||||
// Check if provider already exists
|
||||
const existing = await this.findById(data.id)
|
||||
if (existing) {
|
||||
throw new Error(`OCR provider ${data.id} already exists`)
|
||||
}
|
||||
|
||||
const timestamp = dayjs().valueOf()
|
||||
const newProvider = {
|
||||
...data,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp
|
||||
} satisfies DbOcrProvider
|
||||
|
||||
// Validate data structure
|
||||
if (!isDbOcrProvider(newProvider)) {
|
||||
throw new Error('Invalid OCR provider data')
|
||||
}
|
||||
|
||||
const [created] = await dbService.getDb().insert(ocrProviderTable).values(newProvider).returning()
|
||||
|
||||
logger.info(`Created OCR provider: ${data.id}`)
|
||||
return created
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create OCR provider ${data.id}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing OCR provider
|
||||
*/
|
||||
async update(id: string, data: DbOcrProviderUpdate, _options?: ServiceOptions): Promise<DbOcrProvider> {
|
||||
try {
|
||||
const existing = await this.findById(id)
|
||||
if (!existing) {
|
||||
throw new Error(`OCR provider ${id} not found`)
|
||||
}
|
||||
|
||||
const newProvider = {
|
||||
...merge({}, existing, data),
|
||||
updatedAt: dayjs().valueOf()
|
||||
} satisfies DbOcrProvider
|
||||
|
||||
// Validate data structure
|
||||
if (!isDbOcrProvider(newProvider)) {
|
||||
throw new Error('Invalid OCR provider data')
|
||||
}
|
||||
|
||||
const [updated] = await dbService
|
||||
.getDb()
|
||||
.update(ocrProviderTable)
|
||||
.set(newProvider)
|
||||
.where(eq(ocrProviderTable.id, id))
|
||||
.returning()
|
||||
|
||||
logger.info(`Updated OCR provider: ${id}`)
|
||||
return updated
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update OCR provider ${id}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete OCR provider
|
||||
*/
|
||||
async delete(id: string, _options?: ServiceOptions): Promise<void> {
|
||||
try {
|
||||
// Check if it's a built-in provider
|
||||
if (BuiltinOcrProviderIds.some((pid) => pid === id)) {
|
||||
throw new Error('Built-in OCR providers cannot be deleted.')
|
||||
}
|
||||
|
||||
// Check if provider exists
|
||||
const existing = await this.findById(id)
|
||||
if (!existing) {
|
||||
throw new Error(`OCR provider ${id} not found`)
|
||||
}
|
||||
|
||||
await dbService.getDb().delete(ocrProviderTable).where(eq(ocrProviderTable.id, id))
|
||||
|
||||
logger.info(`Deleted OCR provider: ${id}`)
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete OCR provider ${id}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OCR provider exists
|
||||
*/
|
||||
async exists(id: string, _options?: ServiceOptions): Promise<boolean> {
|
||||
try {
|
||||
const provider = await this.findById(id)
|
||||
return provider !== null
|
||||
} catch (error) {
|
||||
logger.error(`Failed to check if OCR provider ${id} exists`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace OCR provider (full update)
|
||||
* This method is specific to OCR providers and not part of IBaseService
|
||||
*/
|
||||
async replace(data: DbOcrProviderReplace): Promise<DbOcrProvider> {
|
||||
try {
|
||||
// Check if it's a built-in provider
|
||||
if (BuiltinOcrProviderIds.some((pid) => pid === data.id)) {
|
||||
throw new Error('Built-in OCR providers cannot be modified with PUT method.')
|
||||
}
|
||||
|
||||
const timestamp = dayjs().valueOf()
|
||||
const existing = await this.exists(data.id)
|
||||
|
||||
let newProvider: DbOcrProvider
|
||||
|
||||
if (existing) {
|
||||
// Update existing
|
||||
const current = await this.findById(data.id)
|
||||
if (!current) {
|
||||
throw new Error(`OCR provider ${data.id} not found during replace operation`)
|
||||
}
|
||||
newProvider = {
|
||||
...data,
|
||||
updatedAt: timestamp,
|
||||
createdAt: current.createdAt
|
||||
}
|
||||
} else {
|
||||
// Create new
|
||||
newProvider = {
|
||||
...data,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp
|
||||
}
|
||||
}
|
||||
|
||||
// Validate data structure
|
||||
if (!isDbOcrProvider(newProvider)) {
|
||||
throw new Error('Invalid OCR provider data')
|
||||
}
|
||||
|
||||
const [saved] = await dbService
|
||||
.getDb()
|
||||
.insert(ocrProviderTable)
|
||||
.values(newProvider)
|
||||
.onConflictDoUpdate({
|
||||
target: ocrProviderTable.id,
|
||||
set: newProvider
|
||||
})
|
||||
.returning()
|
||||
|
||||
logger.info(`Replaced OCR provider: ${data.id}`)
|
||||
return saved
|
||||
} catch (error) {
|
||||
logger.error(`Failed to replace OCR provider ${data.id}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize built-in providers in database
|
||||
* This method is specific to OCR providers and not part of IBaseService
|
||||
*/
|
||||
async initializeBuiltInProviders(): Promise<void> {
|
||||
try {
|
||||
// Import built-in provider configurations
|
||||
const { BUILTIN_OCR_PROVIDERS } = await import('@shared/config/ocr')
|
||||
|
||||
logger.info('Initializing built-in OCR providers')
|
||||
|
||||
// Check and create each built-in provider if it doesn't exist
|
||||
for (const provider of BUILTIN_OCR_PROVIDERS) {
|
||||
const exists = await this.exists(provider.id)
|
||||
if (!exists) {
|
||||
logger.info(`Creating built-in OCR provider: ${provider.id}`)
|
||||
await this.create(provider)
|
||||
} else {
|
||||
logger.debug(`Built-in OCR provider already exists: ${provider.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Initialized ${BUILTIN_OCR_PROVIDERS.length} built-in OCR providers`)
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize built-in OCR providers', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const ocrProviderService = new OcrProviderService()
|
||||
@@ -1,49 +0,0 @@
|
||||
import type { OcrProviderCapabilityRecord, OcrProviderConfig } from '@types'
|
||||
import { index, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
import { createUpdateTimestamps } from './columnHelpers'
|
||||
|
||||
export const ocrProviderTable = sqliteTable(
|
||||
'ocr_provider',
|
||||
{
|
||||
/**
|
||||
* Unique identifier for the provider.
|
||||
* For built-in providers, it's 'tesseract', 'system', etc.
|
||||
* For custom providers, it can be any unique string (we typically use UUID v4).
|
||||
* As the primary key, it ensures the uniqueness of each provider.
|
||||
*/
|
||||
id: text('id').primaryKey(),
|
||||
|
||||
/**
|
||||
* Display name of the provider, e.g., "Tesseract OCR".
|
||||
* For built-in providers, this value is used internally and is not exposed to users; the display name shown in the UI is locale-based by i18n.
|
||||
* Cannot be null.
|
||||
*/
|
||||
name: text('name').notNull(),
|
||||
|
||||
/**
|
||||
* Object describing the provider's capabilities, e.g., { image: true }.
|
||||
* Stored as JSON in a text column. Drizzle's `mode: 'json'` handles
|
||||
* serialization and deserialization automatically. `$type` provides strong typing.
|
||||
* Cannot be null; should store an empty object `{}` even if no specific capabilities.
|
||||
*/
|
||||
capabilities: text('capabilities', { mode: 'json' }).$type<OcrProviderCapabilityRecord>().notNull(),
|
||||
|
||||
/**
|
||||
* Provider-specific configuration. This is a polymorphic field, its structure varies by provider type.
|
||||
* For example, Tesseract's configuration is entirely different from PaddleOCR's.
|
||||
* Storing it as JSON is the most flexible approach to accommodate any configuration structure.
|
||||
* Since this is a polymorphic field, both frontend and backend must validate
|
||||
* that the structure matches the expected schema for the corresponding provider type
|
||||
* before saving.
|
||||
*/
|
||||
config: text('config', { mode: 'json' }).$type<OcrProviderConfig>().notNull(),
|
||||
|
||||
/** Unix timestamp (milliseconds since epoch) for creation and last update. */
|
||||
...createUpdateTimestamps
|
||||
},
|
||||
(t) => [index('name').on(t.name)]
|
||||
)
|
||||
|
||||
export type OcrProviderInsert = typeof ocrProviderTable.$inferInsert
|
||||
export type OcrProviderSelect = typeof ocrProviderTable.$inferSelect
|
||||
@@ -8,8 +8,6 @@
|
||||
* === AUTO-GENERATED CONTENT START ===
|
||||
*/
|
||||
|
||||
import type { PreferenceSchemas } from '@shared/data/preference/preferenceSchemas'
|
||||
|
||||
/**
|
||||
* ElectronStore映射关系 - 简单一层结构
|
||||
*
|
||||
@@ -254,8 +252,6 @@ export const REDUX_STORE_MAPPINGS = {
|
||||
},
|
||||
{
|
||||
originalKey: 'mathEngine',
|
||||
// TODO
|
||||
// @ts-expect-error check how to fix it later
|
||||
targetKey: 'chat.message.math_engine'
|
||||
},
|
||||
{
|
||||
@@ -340,8 +336,6 @@ export const REDUX_STORE_MAPPINGS = {
|
||||
},
|
||||
{
|
||||
originalKey: 'topicNamingPrompt',
|
||||
// TODO
|
||||
// @ts-expect-error check how to fix it later
|
||||
targetKey: 'topic.naming.prompt'
|
||||
},
|
||||
{
|
||||
@@ -670,8 +664,6 @@ export const REDUX_STORE_MAPPINGS = {
|
||||
},
|
||||
{
|
||||
originalKey: 'nutstoreSyncState',
|
||||
// TODO
|
||||
// @ts-expect-error check how to fix it later
|
||||
targetKey: 'data.backup.nutstore.sync_state'
|
||||
},
|
||||
{
|
||||
@@ -744,17 +736,8 @@ export const REDUX_STORE_MAPPINGS = {
|
||||
originalKey: 'shortcuts.exit_fullscreen',
|
||||
targetKey: 'shortcut.app.exit_fullscreen'
|
||||
}
|
||||
],
|
||||
ocr: [
|
||||
{
|
||||
originalKey: 'ocr.imageProviderId',
|
||||
targetKey: 'ocr.settings.image_provider_id'
|
||||
}
|
||||
]
|
||||
} as const satisfies Record<
|
||||
string,
|
||||
Array<{ originalKey: string; targetKey: keyof PreferenceSchemas[keyof PreferenceSchemas] }>
|
||||
>
|
||||
} as const
|
||||
|
||||
// === AUTO-GENERATED CONTENT END ===
|
||||
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
import { dbService } from '@data/db/DbService'
|
||||
import { ocrProviderTable } from '@data/db/schemas/ocrProvider'
|
||||
import { loggerService } from '@logger'
|
||||
import type {
|
||||
DbOcrProvider,
|
||||
DbOcrProviderCreate,
|
||||
DbOcrProviderReplace,
|
||||
DbOcrProviderUpdate,
|
||||
OcrProviderId
|
||||
} from '@types'
|
||||
import { BuiltinOcrProviderIds, isDbOcrProvider } from '@types'
|
||||
import dayjs from 'dayjs'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { merge } from 'lodash'
|
||||
|
||||
const logger = loggerService.withContext('OcrProviderRepository')
|
||||
|
||||
/**
|
||||
* Data access layer for OCR providers
|
||||
* Handles all database operations and data validation
|
||||
*
|
||||
* TODO: This class is already functional, but the data interaction service should be
|
||||
* migrated to src/main/data/api/services.
|
||||
*
|
||||
* The reason why the migration hasn't been completed yet is that the data
|
||||
* architecture is still under development, and we need to wait until the
|
||||
* architectural design is finalized before proceeding with the migration.
|
||||
*/
|
||||
export class OcrProviderRepository {
|
||||
/**
|
||||
* Get all OCR providers
|
||||
*/
|
||||
public async findAll(): Promise<DbOcrProvider[]> {
|
||||
try {
|
||||
const providers = await dbService.getDb().select().from(ocrProviderTable)
|
||||
|
||||
return providers
|
||||
} catch (error) {
|
||||
logger.error('Failed to find all OCR providers', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OCR provider by ID
|
||||
*/
|
||||
public async findById(id: OcrProviderId): Promise<DbOcrProvider> {
|
||||
try {
|
||||
const providers = await dbService
|
||||
.getDb()
|
||||
.select()
|
||||
.from(ocrProviderTable)
|
||||
.where(eq(ocrProviderTable.id, id))
|
||||
.limit(1)
|
||||
|
||||
if (providers.length === 0) {
|
||||
throw new Error(`OCR provider ${id} not found`)
|
||||
}
|
||||
|
||||
return providers[0]
|
||||
} catch (error) {
|
||||
logger.error(`Failed to find OCR provider ${id}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if provider exists
|
||||
*/
|
||||
public async exists(id: OcrProviderId): Promise<boolean> {
|
||||
try {
|
||||
const providers = await dbService
|
||||
.getDb()
|
||||
.select({ id: ocrProviderTable.id })
|
||||
.from(ocrProviderTable)
|
||||
.where(eq(ocrProviderTable.id, id))
|
||||
.limit(1)
|
||||
|
||||
return providers.length > 0
|
||||
} catch (error) {
|
||||
logger.error(`Failed to check if OCR provider ${id} exists`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new OCR provider
|
||||
*/
|
||||
public async create(param: DbOcrProviderCreate): Promise<DbOcrProvider> {
|
||||
try {
|
||||
// Check if provider already exists
|
||||
if (await this.exists(param.id)) {
|
||||
throw new Error(`OCR provider ${param.id} already exists`)
|
||||
}
|
||||
|
||||
const timestamp = dayjs().valueOf()
|
||||
const newProvider = {
|
||||
...param,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp
|
||||
} satisfies DbOcrProvider
|
||||
|
||||
// Validate data structure
|
||||
if (!isDbOcrProvider(newProvider)) {
|
||||
throw new Error('Invalid OCR provider data')
|
||||
}
|
||||
|
||||
const [created] = await dbService.getDb().insert(ocrProviderTable).values(newProvider).returning()
|
||||
|
||||
logger.info(`Created OCR provider: ${param.id}`)
|
||||
return created
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create OCR provider ${param.id}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update OCR provider (partial update)
|
||||
*/
|
||||
public async update(id: OcrProviderId, update: DbOcrProviderUpdate): Promise<DbOcrProvider> {
|
||||
try {
|
||||
const existing = await this.findById(id)
|
||||
|
||||
const newProvider = {
|
||||
...merge({}, existing, update),
|
||||
updatedAt: dayjs().valueOf()
|
||||
} satisfies DbOcrProvider
|
||||
|
||||
// Validate data structure
|
||||
if (!isDbOcrProvider(newProvider)) {
|
||||
throw new Error('Invalid OCR provider data')
|
||||
}
|
||||
|
||||
const [updated] = await dbService
|
||||
.getDb()
|
||||
.update(ocrProviderTable)
|
||||
.set(newProvider)
|
||||
.where(eq(ocrProviderTable.id, id))
|
||||
.returning()
|
||||
|
||||
logger.info(`Updated OCR provider: ${id}`)
|
||||
return updated
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update OCR provider ${id}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace OCR provider (full update)
|
||||
*/
|
||||
public async replace(data: DbOcrProviderReplace): Promise<DbOcrProvider> {
|
||||
try {
|
||||
// Check if it's a built-in provider
|
||||
if (BuiltinOcrProviderIds.some((pid) => pid === data.id)) {
|
||||
throw new Error('Built-in OCR providers cannot be modified with PUT method.')
|
||||
}
|
||||
|
||||
const timestamp = dayjs().valueOf()
|
||||
const existing = await this.exists(data.id)
|
||||
|
||||
let newProvider: DbOcrProvider
|
||||
|
||||
if (existing) {
|
||||
// Update existing
|
||||
const current = await this.findById(data.id)
|
||||
newProvider = {
|
||||
...data,
|
||||
updatedAt: timestamp,
|
||||
createdAt: current.createdAt
|
||||
}
|
||||
} else {
|
||||
// Create new
|
||||
newProvider = {
|
||||
...data,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp
|
||||
}
|
||||
}
|
||||
|
||||
// Validate data structure
|
||||
if (!isDbOcrProvider(newProvider)) {
|
||||
throw new Error('Invalid OCR provider data')
|
||||
}
|
||||
|
||||
const [saved] = await dbService
|
||||
.getDb()
|
||||
.insert(ocrProviderTable)
|
||||
.values(newProvider)
|
||||
.onConflictDoUpdate({
|
||||
target: ocrProviderTable.id,
|
||||
set: newProvider
|
||||
})
|
||||
.returning()
|
||||
|
||||
logger.info(`Replaced OCR provider: ${data.id}`)
|
||||
return saved
|
||||
} catch (error) {
|
||||
logger.error(`Failed to replace OCR provider ${data.id}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete OCR provider
|
||||
*/
|
||||
public async delete(id: OcrProviderId): Promise<void> {
|
||||
try {
|
||||
// Check if it's a built-in provider
|
||||
if (BuiltinOcrProviderIds.some((pid) => pid === id)) {
|
||||
throw new Error('Built-in OCR providers cannot be deleted.')
|
||||
}
|
||||
|
||||
// Check if provider exists
|
||||
await this.findById(id)
|
||||
|
||||
await dbService.getDb().delete(ocrProviderTable).where(eq(ocrProviderTable.id, id))
|
||||
|
||||
logger.info(`Deleted OCR provider: ${id}`)
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete OCR provider ${id}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize built-in providers in database
|
||||
*/
|
||||
public async initializeBuiltInProviders(): Promise<void> {
|
||||
try {
|
||||
// Import built-in provider configurations
|
||||
const { BUILTIN_OCR_PROVIDERS } = await import('@shared/config/ocr')
|
||||
|
||||
logger.info('Initializing built-in OCR providers')
|
||||
|
||||
// Check and create each built-in provider if it doesn't exist
|
||||
for (const provider of BUILTIN_OCR_PROVIDERS) {
|
||||
const exists = await this.exists(provider.id)
|
||||
if (!exists) {
|
||||
logger.info(`Creating built-in OCR provider: ${provider.id}`)
|
||||
await this.create(provider)
|
||||
} else {
|
||||
logger.debug(`Built-in OCR provider already exists: ${provider.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Initialized ${BUILTIN_OCR_PROVIDERS.length} built-in OCR providers`)
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize built-in OCR providers', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ocrProviderRepository = new OcrProviderRepository()
|
||||
142
src/main/ipc.ts
142
src/main/ipc.ts
@@ -18,7 +18,8 @@ import type {
|
||||
AgentPersistedMessage,
|
||||
FileMetadata,
|
||||
Notification,
|
||||
OcrParams,
|
||||
OcrProvider,
|
||||
PluginError,
|
||||
Provider,
|
||||
Shortcut,
|
||||
SupportedOcrFile
|
||||
@@ -29,6 +30,7 @@ import { BrowserWindow, dialog, ipcMain, session, shell, systemPreferences, webC
|
||||
import fontList from 'font-list'
|
||||
|
||||
import { agentMessageRepository } from './services/agents/database'
|
||||
import { PluginService } from './services/agents/plugins/PluginService'
|
||||
import { apiServerService } from './services/ApiServerService'
|
||||
import appService from './services/AppService'
|
||||
import AppUpdater from './services/AppUpdater'
|
||||
@@ -70,6 +72,7 @@ import {
|
||||
} from './services/SpanCacheService'
|
||||
import storeSyncService from './services/StoreSyncService'
|
||||
import VertexAIService from './services/VertexAIService'
|
||||
import WebSocketService from './services/WebSocketService'
|
||||
import { setOpenLinkExternal } from './services/WebviewService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { calculateDirectorySize, getResourcePath } from './utils'
|
||||
@@ -95,6 +98,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()
|
||||
@@ -875,7 +890,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
)
|
||||
|
||||
// OCR
|
||||
ipcMain.handle(IpcChannel.OCR_Ocr, (_, file: SupportedOcrFile, params: OcrParams) => ocrService.ocr(file, params))
|
||||
ipcMain.handle(IpcChannel.OCR_ocr, (_, file: SupportedOcrFile, provider: OcrProvider) =>
|
||||
ocrService.ocr(file, provider)
|
||||
)
|
||||
ipcMain.handle(IpcChannel.OCR_ListProviders, () => ocrService.listProviderIds())
|
||||
|
||||
// OVMS
|
||||
ipcMain.handle(IpcChannel.Ovms_AddModel, (_, modelName: string, modelId: string, modelSource: string, task: string) =>
|
||||
@@ -891,6 +909,126 @@ 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 }
|
||||
}
|
||||
})
|
||||
|
||||
// WebSocket
|
||||
ipcMain.handle(IpcChannel.WebSocket_Start, WebSocketService.start)
|
||||
ipcMain.handle(IpcChannel.WebSocket_Stop, WebSocketService.stop)
|
||||
ipcMain.handle(IpcChannel.WebSocket_Status, WebSocketService.getStatus)
|
||||
ipcMain.handle(IpcChannel.WebSocket_SendFile, WebSocketService.sendFile)
|
||||
ipcMain.handle(IpcChannel.WebSocket_GetAllCandidates, WebSocketService.getAllCandidates)
|
||||
|
||||
// Preference handlers
|
||||
PreferenceService.registerIpcHandler()
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
|
||||
import { OllamaEmbeddings } from '@cherrystudio/embedjs-ollama'
|
||||
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
|
||||
import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings'
|
||||
import type { ApiClient } from '@types'
|
||||
import { net } from 'electron'
|
||||
|
||||
import { VoyageEmbeddings } from './VoyageEmbeddings'
|
||||
|
||||
export default class EmbeddingsFactory {
|
||||
static create({ embedApiClient, dimensions }: { embedApiClient: ApiClient; dimensions?: number }): BaseEmbeddings {
|
||||
const batchSize = 10
|
||||
const { model, provider, apiKey, apiVersion, baseURL } = embedApiClient
|
||||
const { model, provider, apiKey, baseURL } = embedApiClient
|
||||
if (provider === 'voyageai') {
|
||||
return new VoyageEmbeddings({
|
||||
modelName: model,
|
||||
@@ -38,22 +38,13 @@ 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,
|
||||
dimensions,
|
||||
batchSize,
|
||||
configuration: { baseURL }
|
||||
configuration: { baseURL, fetch: net.fetch as typeof fetch }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
369
src/main/services/WebSocketService.ts
Normal file
369
src/main/services/WebSocketService.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
import { loggerService } from '@logger'
|
||||
import * as fs from 'fs'
|
||||
import { networkInterfaces } from 'os'
|
||||
import * as path from 'path'
|
||||
import type { Socket } from 'socket.io'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
const logger = loggerService.withContext('WebSocketService')
|
||||
|
||||
class WebSocketService {
|
||||
private io: Server | null = null
|
||||
private isStarted = false
|
||||
private port = 7017
|
||||
private connectedClients = new Set<string>()
|
||||
|
||||
private getLocalIpAddress(): string | undefined {
|
||||
const interfaces = networkInterfaces()
|
||||
|
||||
// 按优先级排序的网络接口名称模式
|
||||
const interfacePriority = [
|
||||
// macOS: 以太网/Wi-Fi 优先
|
||||
/^en[0-9]+$/, // en0, en1 (以太网/Wi-Fi)
|
||||
/^(en|eth)[0-9]+$/, // 以太网接口
|
||||
/^wlan[0-9]+$/, // 无线接口
|
||||
// Windows: 以太网/Wi-Fi 优先
|
||||
/^(Ethernet|Wi-Fi|Local Area Connection)/,
|
||||
/^(Wi-Fi|无线网络连接)/,
|
||||
// Linux: 以太网/Wi-Fi 优先
|
||||
/^(eth|enp|wlp|wlan)[0-9]+/,
|
||||
// 虚拟化接口(低优先级)
|
||||
/^bridge[0-9]+$/, // Docker bridge
|
||||
/^veth[0-9]+$/, // Docker veth
|
||||
/^docker[0-9]+/, // Docker interfaces
|
||||
/^br-[0-9a-f]+/, // Docker bridge
|
||||
/^vmnet[0-9]+$/, // VMware
|
||||
/^vboxnet[0-9]+$/, // VirtualBox
|
||||
// VPN 隧道接口(低优先级)
|
||||
/^utun[0-9]+$/, // macOS VPN
|
||||
/^tun[0-9]+$/, // Linux/Unix VPN
|
||||
/^tap[0-9]+$/, // TAP interfaces
|
||||
/^tailscale[0-9]*$/, // Tailscale VPN
|
||||
/^wg[0-9]+$/ // WireGuard VPN
|
||||
]
|
||||
|
||||
const candidates: Array<{ interface: string; address: string; priority: number }> = []
|
||||
|
||||
for (const [name, ifaces] of Object.entries(interfaces)) {
|
||||
for (const iface of ifaces || []) {
|
||||
if (iface.family === 'IPv4' && !iface.internal) {
|
||||
// 计算接口优先级
|
||||
let priority = 999 // 默认最低优先级
|
||||
for (let i = 0; i < interfacePriority.length; i++) {
|
||||
if (interfacePriority[i].test(name)) {
|
||||
priority = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
candidates.push({
|
||||
interface: name,
|
||||
address: iface.address,
|
||||
priority
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.length === 0) {
|
||||
logger.warn('无法获取局域网 IP,使用默认 IP: 127.0.0.1')
|
||||
return '127.0.0.1'
|
||||
}
|
||||
|
||||
// 按优先级排序,选择优先级最高的
|
||||
candidates.sort((a, b) => a.priority - b.priority)
|
||||
const best = candidates[0]
|
||||
|
||||
logger.info(`获取局域网 IP: ${best.address} (interface: ${best.interface})`)
|
||||
return best.address
|
||||
}
|
||||
|
||||
public start = async (): Promise<{ success: boolean; port?: number; error?: string }> => {
|
||||
if (this.isStarted && this.io) {
|
||||
return { success: true, port: this.port }
|
||||
}
|
||||
|
||||
try {
|
||||
this.io = new Server(this.port, {
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST']
|
||||
},
|
||||
transports: ['websocket', 'polling'],
|
||||
allowEIO3: true,
|
||||
pingTimeout: 60000,
|
||||
pingInterval: 25000
|
||||
})
|
||||
|
||||
this.io.on('connection', (socket: Socket) => {
|
||||
this.connectedClients.add(socket.id)
|
||||
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
if (!mainWindow) {
|
||||
logger.error('Main window is null, cannot send connection event')
|
||||
} else {
|
||||
mainWindow.webContents.send('websocket-client-connected', {
|
||||
connected: true,
|
||||
clientId: socket.id
|
||||
})
|
||||
logger.info(`Connection event sent to renderer, total clients: ${this.connectedClients.size}`)
|
||||
}
|
||||
|
||||
socket.on('message', (data) => {
|
||||
logger.info('Received message from mobile:', data)
|
||||
mainWindow?.webContents.send('websocket-message-received', data)
|
||||
socket.emit('message_received', { success: true })
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
logger.info(`Client disconnected: ${socket.id}`)
|
||||
this.connectedClients.delete(socket.id)
|
||||
|
||||
if (this.connectedClients.size === 0) {
|
||||
mainWindow?.webContents.send('websocket-client-connected', {
|
||||
connected: false,
|
||||
clientId: socket.id
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Engine 层面的事件监听
|
||||
this.io.engine.on('connection_error', (err) => {
|
||||
logger.error('Engine connection error:', err)
|
||||
})
|
||||
|
||||
this.io.engine.on('connection', (rawSocket) => {
|
||||
const remoteAddr = rawSocket.request.connection.remoteAddress
|
||||
logger.info(`[Engine] Raw connection from: ${remoteAddr}`)
|
||||
logger.info(`[Engine] Transport: ${rawSocket.transport.name}`)
|
||||
|
||||
rawSocket.on('packet', (packet: { type: string; data?: any }) => {
|
||||
logger.info(
|
||||
`[Engine] ← Packet from ${remoteAddr}: type="${packet.type}"`,
|
||||
packet.data ? { data: packet.data } : {}
|
||||
)
|
||||
})
|
||||
|
||||
rawSocket.on('packetCreate', (packet: { type: string; data?: any }) => {
|
||||
logger.info(`[Engine] → Packet to ${remoteAddr}: type="${packet.type}"`)
|
||||
})
|
||||
|
||||
rawSocket.on('close', (reason: string) => {
|
||||
logger.warn(`[Engine] Connection closed from ${remoteAddr}, reason: ${reason}`)
|
||||
})
|
||||
|
||||
rawSocket.on('error', (error: Error) => {
|
||||
logger.error(`[Engine] Connection error from ${remoteAddr}:`, error)
|
||||
})
|
||||
})
|
||||
|
||||
// Socket.IO 握手失败监听
|
||||
this.io.on('connection_error', (err) => {
|
||||
logger.error('[Socket.IO] Connection error during handshake:', err)
|
||||
})
|
||||
|
||||
this.isStarted = true
|
||||
logger.info(`WebSocket server started on port ${this.port}`)
|
||||
|
||||
return { success: true, port: this.port }
|
||||
} catch (error) {
|
||||
logger.error('Failed to start WebSocket server:', error as Error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public stop = async (): Promise<{ success: boolean }> => {
|
||||
if (!this.isStarted || !this.io) {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.io!.close(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
this.io = null
|
||||
this.isStarted = false
|
||||
this.connectedClients.clear()
|
||||
logger.info('WebSocket server stopped')
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
logger.error('Failed to stop WebSocket server:', error as Error)
|
||||
return { success: false }
|
||||
}
|
||||
}
|
||||
|
||||
public getStatus = async (): Promise<{
|
||||
isRunning: boolean
|
||||
port?: number
|
||||
ip?: string
|
||||
clientConnected: boolean
|
||||
}> => {
|
||||
return {
|
||||
isRunning: this.isStarted,
|
||||
port: this.isStarted ? this.port : undefined,
|
||||
ip: this.isStarted ? this.getLocalIpAddress() : undefined,
|
||||
clientConnected: this.connectedClients.size > 0
|
||||
}
|
||||
}
|
||||
|
||||
public getAllCandidates = async (): Promise<
|
||||
Array<{
|
||||
host: string
|
||||
interface: string
|
||||
priority: number
|
||||
}>
|
||||
> => {
|
||||
const interfaces = networkInterfaces()
|
||||
|
||||
// 按优先级排序的网络接口名称模式
|
||||
const interfacePriority = [
|
||||
// macOS: 以太网/Wi-Fi 优先
|
||||
/^en[0-9]+$/, // en0, en1 (以太网/Wi-Fi)
|
||||
/^(en|eth)[0-9]+$/, // 以太网接口
|
||||
/^wlan[0-9]+$/, // 无线接口
|
||||
// Windows: 以太网/Wi-Fi 优先
|
||||
/^(Ethernet|Wi-Fi|Local Area Connection)/,
|
||||
/^(Wi-Fi|无线网络连接)/,
|
||||
// Linux: 以太网/Wi-Fi 优先
|
||||
/^(eth|enp|wlp|wlan)[0-9]+/,
|
||||
// 虚拟化接口(低优先级)
|
||||
/^bridge[0-9]+$/, // Docker bridge
|
||||
/^veth[0-9]+$/, // Docker veth
|
||||
/^docker[0-9]+/, // Docker interfaces
|
||||
/^br-[0-9a-f]+/, // Docker bridge
|
||||
/^vmnet[0-9]+$/, // VMware
|
||||
/^vboxnet[0-9]+$/, // VirtualBox
|
||||
// VPN 隧道接口(低优先级)
|
||||
/^utun[0-9]+$/, // macOS VPN
|
||||
/^tun[0-9]+$/, // Linux/Unix VPN
|
||||
/^tap[0-9]+$/, // TAP interfaces
|
||||
/^tailscale[0-9]*$/, // Tailscale VPN
|
||||
/^wg[0-9]+$/ // WireGuard VPN
|
||||
]
|
||||
|
||||
const candidates: Array<{ host: string; interface: string; priority: number }> = []
|
||||
|
||||
for (const [name, ifaces] of Object.entries(interfaces)) {
|
||||
for (const iface of ifaces || []) {
|
||||
if (iface.family === 'IPv4' && !iface.internal) {
|
||||
// 计算接口优先级
|
||||
let priority = 999 // 默认最低优先级
|
||||
for (let i = 0; i < interfacePriority.length; i++) {
|
||||
if (interfacePriority[i].test(name)) {
|
||||
priority = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
candidates.push({
|
||||
host: iface.address,
|
||||
interface: name,
|
||||
priority
|
||||
})
|
||||
|
||||
logger.debug(`Found interface: ${name} -> ${iface.address} (priority: ${priority})`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按优先级排序返回
|
||||
candidates.sort((a, b) => a.priority - b.priority)
|
||||
logger.info(
|
||||
`Found ${candidates.length} IP candidates: ${candidates.map((c) => `${c.host}(${c.interface})`).join(', ')}`
|
||||
)
|
||||
return candidates
|
||||
}
|
||||
|
||||
public sendFile = async (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
filePath: string
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
if (!this.isStarted || !this.io) {
|
||||
const errorMsg = 'WebSocket server is not running.'
|
||||
logger.error(errorMsg)
|
||||
return { success: false, error: errorMsg }
|
||||
}
|
||||
|
||||
if (this.connectedClients.size === 0) {
|
||||
const errorMsg = 'No client connected.'
|
||||
logger.error(errorMsg)
|
||||
return { success: false, error: errorMsg }
|
||||
}
|
||||
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const stats = fs.statSync(filePath)
|
||||
const totalSize = stats.size
|
||||
const filename = path.basename(filePath)
|
||||
const stream = fs.createReadStream(filePath)
|
||||
let bytesSent = 0
|
||||
const startTime = Date.now()
|
||||
|
||||
logger.info(`Starting file transfer: ${filename} (${this.formatFileSize(totalSize)})`)
|
||||
|
||||
// 向客户端发送文件开始的信号,包含文件名和总大小
|
||||
this.io!.emit('zip-file-start', { filename, totalSize })
|
||||
|
||||
stream.on('data', (chunk) => {
|
||||
bytesSent += chunk.length
|
||||
const progress = (bytesSent / totalSize) * 100
|
||||
|
||||
// 向客户端发送文件块
|
||||
this.io!.emit('zip-file-chunk', chunk)
|
||||
|
||||
// 向渲染进程发送进度更新
|
||||
mainWindow?.webContents.send('file-send-progress', { progress })
|
||||
|
||||
// 每10%记录一次进度
|
||||
if (Math.floor(progress) % 10 === 0) {
|
||||
const elapsed = (Date.now() - startTime) / 1000
|
||||
const speed = elapsed > 0 ? bytesSent / elapsed : 0
|
||||
logger.info(`Transfer progress: ${Math.floor(progress)}% (${this.formatFileSize(speed)}/s)`)
|
||||
}
|
||||
})
|
||||
|
||||
stream.on('end', () => {
|
||||
const totalTime = (Date.now() - startTime) / 1000
|
||||
const avgSpeed = totalTime > 0 ? totalSize / totalTime : 0
|
||||
logger.info(
|
||||
`File transfer completed: ${filename} in ${totalTime.toFixed(1)}s (${this.formatFileSize(avgSpeed)}/s)`
|
||||
)
|
||||
|
||||
// 确保发送100%的进度
|
||||
mainWindow?.webContents.send('file-send-progress', { progress: 100 })
|
||||
// 向客户端发送文件结束的信号
|
||||
this.io!.emit('zip-file-end')
|
||||
resolve({ success: true })
|
||||
})
|
||||
|
||||
stream.on('error', (error) => {
|
||||
logger.error(`File transfer failed: ${filename}`, error)
|
||||
reject({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebSocketService()
|
||||
426
src/main/services/agents/plugins/PluginCacheStore.ts
Normal file
426
src/main/services/agents/plugins/PluginCacheStore.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { findAllSkillDirectories, parsePluginMetadata, parseSkillMetadata } from '@main/utils/markdownParser'
|
||||
import type { CachedPluginsData, InstalledPlugin, PluginError, PluginMetadata, PluginType } from '@types'
|
||||
import { CachedPluginsDataSchema } from '@types'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
const logger = loggerService.withContext('PluginCacheStore')
|
||||
|
||||
interface PluginCacheStoreDeps {
|
||||
allowedExtensions: string[]
|
||||
getPluginDirectoryName: (type: PluginType) => 'agents' | 'commands' | 'skills'
|
||||
getClaudeBasePath: (workdir: string) => string
|
||||
getClaudePluginDirectory: (workdir: string, type: PluginType) => string
|
||||
getPluginsBasePath: () => string
|
||||
}
|
||||
|
||||
export class PluginCacheStore {
|
||||
constructor(private readonly deps: PluginCacheStoreDeps) {}
|
||||
|
||||
async listAvailableFilePlugins(type: 'agent' | 'command'): Promise<PluginMetadata[]> {
|
||||
const basePath = this.deps.getPluginsBasePath()
|
||||
const directory = path.join(basePath, this.deps.getPluginDirectoryName(type))
|
||||
|
||||
try {
|
||||
await fs.promises.access(directory, fs.constants.R_OK)
|
||||
} catch (error) {
|
||||
logger.warn(`Plugin directory not accessible: ${directory}`, {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
|
||||
const plugins: PluginMetadata[] = []
|
||||
const categories = await fs.promises.readdir(directory, { withFileTypes: true })
|
||||
|
||||
for (const categoryEntry of categories) {
|
||||
if (!categoryEntry.isDirectory()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const category = categoryEntry.name
|
||||
const categoryPath = path.join(directory, category)
|
||||
const files = await fs.promises.readdir(categoryPath, { withFileTypes: true })
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.isFile()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const ext = path.extname(file.name).toLowerCase()
|
||||
if (!this.deps.allowedExtensions.includes(ext)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const filePath = path.join(categoryPath, file.name)
|
||||
const sourcePath = path.join(this.deps.getPluginDirectoryName(type), category, file.name)
|
||||
const metadata = await parsePluginMetadata(filePath, sourcePath, category, type)
|
||||
plugins.push(metadata)
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse plugin: ${file.name}`, {
|
||||
category,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return plugins
|
||||
}
|
||||
|
||||
async listAvailableSkills(): Promise<PluginMetadata[]> {
|
||||
const basePath = this.deps.getPluginsBasePath()
|
||||
const skillsPath = path.join(basePath, this.deps.getPluginDirectoryName('skill'))
|
||||
const skills: PluginMetadata[] = []
|
||||
|
||||
try {
|
||||
await fs.promises.access(skillsPath)
|
||||
} catch {
|
||||
logger.warn('Skills directory not found', { skillsPath })
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const skillDirectories = await findAllSkillDirectories(skillsPath, basePath)
|
||||
logger.info(`Found ${skillDirectories.length} skill directories`, { skillsPath })
|
||||
|
||||
for (const { folderPath, sourcePath } of skillDirectories) {
|
||||
try {
|
||||
const metadata = await parseSkillMetadata(folderPath, sourcePath, 'skills')
|
||||
skills.push(metadata)
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse skill folder: ${sourcePath}`, {
|
||||
folderPath,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to scan skill directory', {
|
||||
skillsPath,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
|
||||
return skills
|
||||
}
|
||||
|
||||
async readSourceContent(sourcePath: string): Promise<string> {
|
||||
const absolutePath = this.resolveSourcePath(sourcePath)
|
||||
|
||||
try {
|
||||
await fs.promises.access(absolutePath, fs.constants.R_OK)
|
||||
} catch {
|
||||
throw {
|
||||
type: 'FILE_NOT_FOUND',
|
||||
path: sourcePath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
try {
|
||||
return await fs.promises.readFile(absolutePath, 'utf-8')
|
||||
} catch (error) {
|
||||
throw {
|
||||
type: 'READ_FAILED',
|
||||
path: sourcePath,
|
||||
reason: error instanceof Error ? error.message : String(error)
|
||||
} as PluginError
|
||||
}
|
||||
}
|
||||
|
||||
resolveSourcePath(sourcePath: string): string {
|
||||
const normalized = path.normalize(sourcePath)
|
||||
|
||||
if (normalized.includes('..')) {
|
||||
throw {
|
||||
type: 'PATH_TRAVERSAL',
|
||||
message: 'Path traversal detected',
|
||||
path: sourcePath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
const basePath = this.deps.getPluginsBasePath()
|
||||
const absolutePath = path.join(basePath, normalized)
|
||||
const resolvedPath = path.resolve(absolutePath)
|
||||
|
||||
if (!resolvedPath.startsWith(path.resolve(basePath))) {
|
||||
throw {
|
||||
type: 'PATH_TRAVERSAL',
|
||||
message: 'Path outside plugins directory',
|
||||
path: sourcePath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
return resolvedPath
|
||||
}
|
||||
|
||||
async ensureSkillSourceDirectory(sourceAbsolutePath: string, sourcePath: string): Promise<void> {
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = await fs.promises.stat(sourceAbsolutePath)
|
||||
} catch {
|
||||
throw {
|
||||
type: 'FILE_NOT_FOUND',
|
||||
path: sourceAbsolutePath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
throw {
|
||||
type: 'INVALID_METADATA',
|
||||
reason: 'Skill source is not a directory',
|
||||
path: sourcePath
|
||||
} as PluginError
|
||||
}
|
||||
}
|
||||
|
||||
async validatePluginFile(filePath: string, maxFileSize: number): Promise<void> {
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = await fs.promises.stat(filePath)
|
||||
} catch {
|
||||
throw {
|
||||
type: 'FILE_NOT_FOUND',
|
||||
path: filePath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
if (stats.size > maxFileSize) {
|
||||
throw {
|
||||
type: 'FILE_TOO_LARGE',
|
||||
size: stats.size,
|
||||
max: maxFileSize
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
if (!this.deps.allowedExtensions.includes(ext)) {
|
||||
throw {
|
||||
type: 'INVALID_FILE_TYPE',
|
||||
extension: ext
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
try {
|
||||
const basePath = this.deps.getPluginsBasePath()
|
||||
const relativeSourcePath = path.relative(basePath, filePath)
|
||||
const segments = relativeSourcePath.split(path.sep)
|
||||
const rootDir = segments[0]
|
||||
const agentDir = this.deps.getPluginDirectoryName('agent')
|
||||
const type: 'agent' | 'command' = rootDir === agentDir ? 'agent' : 'command'
|
||||
const category = path.basename(path.dirname(filePath))
|
||||
|
||||
await parsePluginMetadata(filePath, relativeSourcePath, category, type)
|
||||
} catch (error) {
|
||||
throw {
|
||||
type: 'INVALID_METADATA',
|
||||
reason: 'Failed to parse frontmatter',
|
||||
path: filePath
|
||||
} as PluginError
|
||||
}
|
||||
}
|
||||
|
||||
async listInstalled(workdir: string): Promise<InstalledPlugin[]> {
|
||||
const claudePath = this.deps.getClaudeBasePath(workdir)
|
||||
const cacheData = await this.readCacheFile(claudePath)
|
||||
|
||||
if (cacheData) {
|
||||
logger.debug(`Loaded ${cacheData.plugins.length} plugins from cache`, { workdir })
|
||||
return cacheData.plugins
|
||||
}
|
||||
|
||||
logger.info('Cache read failed, rebuilding from filesystem', { workdir })
|
||||
return await this.rebuild(workdir)
|
||||
}
|
||||
|
||||
async upsert(workdir: string, plugin: InstalledPlugin): Promise<void> {
|
||||
const claudePath = this.deps.getClaudeBasePath(workdir)
|
||||
let cacheData = await this.readCacheFile(claudePath)
|
||||
let plugins = cacheData?.plugins
|
||||
|
||||
if (!plugins) {
|
||||
plugins = await this.rebuild(workdir)
|
||||
cacheData = {
|
||||
version: 1,
|
||||
lastUpdated: Date.now(),
|
||||
plugins
|
||||
}
|
||||
}
|
||||
|
||||
const updatedPlugin: InstalledPlugin = {
|
||||
...plugin,
|
||||
metadata: {
|
||||
...plugin.metadata,
|
||||
installedAt: plugin.metadata.installedAt ?? Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
const index = plugins.findIndex((p) => p.filename === updatedPlugin.filename && p.type === updatedPlugin.type)
|
||||
if (index >= 0) {
|
||||
plugins[index] = updatedPlugin
|
||||
} else {
|
||||
plugins.push(updatedPlugin)
|
||||
}
|
||||
|
||||
const data: CachedPluginsData = {
|
||||
version: cacheData?.version ?? 1,
|
||||
lastUpdated: Date.now(),
|
||||
plugins
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(claudePath, { recursive: true })
|
||||
await this.writeCacheFile(claudePath, data)
|
||||
}
|
||||
|
||||
async remove(workdir: string, filename: string, type: PluginType): Promise<void> {
|
||||
const claudePath = this.deps.getClaudeBasePath(workdir)
|
||||
let cacheData = await this.readCacheFile(claudePath)
|
||||
let plugins = cacheData?.plugins
|
||||
|
||||
if (!plugins) {
|
||||
plugins = await this.rebuild(workdir)
|
||||
cacheData = {
|
||||
version: 1,
|
||||
lastUpdated: Date.now(),
|
||||
plugins
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = plugins.filter((p) => !(p.filename === filename && p.type === type))
|
||||
|
||||
const data: CachedPluginsData = {
|
||||
version: cacheData?.version ?? 1,
|
||||
lastUpdated: Date.now(),
|
||||
plugins: filtered
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(claudePath, { recursive: true })
|
||||
await this.writeCacheFile(claudePath, data)
|
||||
}
|
||||
|
||||
async rebuild(workdir: string): Promise<InstalledPlugin[]> {
|
||||
logger.info('Rebuilding plugin cache from filesystem', { workdir })
|
||||
|
||||
const claudePath = this.deps.getClaudeBasePath(workdir)
|
||||
|
||||
try {
|
||||
await fs.promises.access(claudePath, fs.constants.R_OK)
|
||||
} catch {
|
||||
logger.warn('.claude directory not found, returning empty plugin list', { claudePath })
|
||||
return []
|
||||
}
|
||||
|
||||
const plugins: InstalledPlugin[] = []
|
||||
|
||||
await Promise.all([
|
||||
this.collectFilePlugins(workdir, 'agent', plugins),
|
||||
this.collectFilePlugins(workdir, 'command', plugins),
|
||||
this.collectSkillPlugins(workdir, plugins)
|
||||
])
|
||||
|
||||
try {
|
||||
const cacheData: CachedPluginsData = {
|
||||
version: 1,
|
||||
lastUpdated: Date.now(),
|
||||
plugins
|
||||
}
|
||||
await this.writeCacheFile(claudePath, cacheData)
|
||||
logger.info(`Rebuilt cache with ${plugins.length} plugins`, { workdir })
|
||||
} catch (error) {
|
||||
logger.error('Failed to write cache file after rebuild', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
|
||||
return plugins
|
||||
}
|
||||
|
||||
private async collectFilePlugins(
|
||||
workdir: string,
|
||||
type: Exclude<PluginType, 'skill'>,
|
||||
plugins: InstalledPlugin[]
|
||||
): Promise<void> {
|
||||
const directory = this.deps.getClaudePluginDirectory(workdir, type)
|
||||
|
||||
try {
|
||||
await fs.promises.access(directory, fs.constants.R_OK)
|
||||
} catch {
|
||||
logger.debug(`${type} directory not found or not accessible`, { directory })
|
||||
return
|
||||
}
|
||||
|
||||
const files = await fs.promises.readdir(directory, { withFileTypes: true })
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.isFile()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const ext = path.extname(file.name).toLowerCase()
|
||||
if (!this.deps.allowedExtensions.includes(ext)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const filePath = path.join(directory, file.name)
|
||||
const sourcePath = path.join(this.deps.getPluginDirectoryName(type), file.name)
|
||||
const metadata = await parsePluginMetadata(filePath, sourcePath, this.deps.getPluginDirectoryName(type), type)
|
||||
plugins.push({ filename: file.name, type, metadata })
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse ${type} plugin: ${file.name}`, {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async collectSkillPlugins(workdir: string, plugins: InstalledPlugin[]): Promise<void> {
|
||||
const skillsPath = this.deps.getClaudePluginDirectory(workdir, 'skill')
|
||||
const claudePath = this.deps.getClaudeBasePath(workdir)
|
||||
|
||||
try {
|
||||
await fs.promises.access(skillsPath, fs.constants.R_OK)
|
||||
} catch {
|
||||
logger.debug('Skills directory not found or not accessible', { skillsPath })
|
||||
return
|
||||
}
|
||||
|
||||
const skillDirectories = await findAllSkillDirectories(skillsPath, claudePath)
|
||||
|
||||
for (const { folderPath, sourcePath } of skillDirectories) {
|
||||
try {
|
||||
const metadata = await parseSkillMetadata(folderPath, sourcePath, 'skills')
|
||||
plugins.push({ filename: metadata.filename, type: 'skill', metadata })
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to parse skill plugin: ${sourcePath}`, {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async readCacheFile(claudePath: string): Promise<CachedPluginsData | null> {
|
||||
const cachePath = path.join(claudePath, 'plugins.json')
|
||||
try {
|
||||
const content = await fs.promises.readFile(cachePath, 'utf-8')
|
||||
const data = JSON.parse(content)
|
||||
return CachedPluginsDataSchema.parse(data)
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to read cache file at ${cachePath}`, {
|
||||
error: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private async writeCacheFile(claudePath: string, data: CachedPluginsData): Promise<void> {
|
||||
const cachePath = path.join(claudePath, 'plugins.json')
|
||||
const tempPath = `${cachePath}.tmp`
|
||||
|
||||
const content = JSON.stringify(data, null, 2)
|
||||
await fs.promises.writeFile(tempPath, content, 'utf-8')
|
||||
await fs.promises.rename(tempPath, cachePath)
|
||||
}
|
||||
}
|
||||
149
src/main/services/agents/plugins/PluginInstaller.ts
Normal file
149
src/main/services/agents/plugins/PluginInstaller.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { copyDirectoryRecursive, deleteDirectoryRecursive } from '@main/utils/fileOperations'
|
||||
import type { PluginError } from '@types'
|
||||
import * as crypto from 'crypto'
|
||||
import * as fs from 'fs'
|
||||
|
||||
const logger = loggerService.withContext('PluginInstaller')
|
||||
|
||||
export class PluginInstaller {
|
||||
async installFilePlugin(agentId: string, sourceAbsolutePath: string, destPath: string): Promise<void> {
|
||||
const tempPath = `${destPath}.tmp`
|
||||
let fileCopied = false
|
||||
|
||||
try {
|
||||
await fs.promises.copyFile(sourceAbsolutePath, tempPath)
|
||||
fileCopied = true
|
||||
logger.debug('File copied to temp location', { agentId, tempPath })
|
||||
|
||||
await fs.promises.rename(tempPath, destPath)
|
||||
logger.debug('File moved to final location', { agentId, destPath })
|
||||
} catch (error) {
|
||||
if (fileCopied) {
|
||||
await this.safeUnlink(tempPath, 'temp file')
|
||||
}
|
||||
throw this.toPluginError('install', error)
|
||||
}
|
||||
}
|
||||
|
||||
async uninstallFilePlugin(
|
||||
agentId: string,
|
||||
filename: string,
|
||||
type: 'agent' | 'command',
|
||||
filePath: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
await fs.promises.unlink(filePath)
|
||||
logger.debug('Plugin file deleted', { agentId, filename, type, filePath })
|
||||
} catch (error) {
|
||||
const nodeError = error as NodeJS.ErrnoException
|
||||
if (nodeError.code !== 'ENOENT') {
|
||||
throw this.toPluginError('uninstall', error)
|
||||
}
|
||||
logger.warn('Plugin file already deleted', { agentId, filename, type, filePath })
|
||||
}
|
||||
}
|
||||
|
||||
async updateFilePluginContent(agentId: string, filePath: string, content: string): Promise<string> {
|
||||
try {
|
||||
await fs.promises.access(filePath, fs.constants.W_OK)
|
||||
} catch {
|
||||
throw {
|
||||
type: 'FILE_NOT_FOUND',
|
||||
path: filePath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.promises.writeFile(filePath, content, 'utf8')
|
||||
logger.debug('Plugin content written successfully', {
|
||||
agentId,
|
||||
filePath,
|
||||
size: Buffer.byteLength(content, 'utf8')
|
||||
})
|
||||
} catch (error) {
|
||||
throw {
|
||||
type: 'WRITE_FAILED',
|
||||
path: filePath,
|
||||
reason: error instanceof Error ? error.message : String(error)
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
return crypto.createHash('sha256').update(content).digest('hex')
|
||||
}
|
||||
|
||||
async installSkill(agentId: string, sourceAbsolutePath: string, destPath: string): Promise<void> {
|
||||
const logContext = logger.withContext('installSkill')
|
||||
let folderCopied = false
|
||||
const tempPath = `${destPath}.tmp`
|
||||
|
||||
try {
|
||||
try {
|
||||
await fs.promises.access(destPath)
|
||||
await deleteDirectoryRecursive(destPath)
|
||||
logContext.info('Removed existing skill folder', { agentId, destPath })
|
||||
} catch {
|
||||
// No existing folder
|
||||
}
|
||||
|
||||
await copyDirectoryRecursive(sourceAbsolutePath, tempPath)
|
||||
folderCopied = true
|
||||
logContext.info('Skill folder copied to temp location', { agentId, tempPath })
|
||||
|
||||
await fs.promises.rename(tempPath, destPath)
|
||||
logContext.info('Skill folder moved to final location', { agentId, destPath })
|
||||
} catch (error) {
|
||||
if (folderCopied) {
|
||||
await this.safeRemoveDirectory(tempPath, 'temp folder')
|
||||
}
|
||||
throw this.toPluginError('install-skill', error)
|
||||
}
|
||||
}
|
||||
|
||||
async uninstallSkill(agentId: string, folderName: string, skillPath: string): Promise<void> {
|
||||
const logContext = logger.withContext('uninstallSkill')
|
||||
|
||||
try {
|
||||
await deleteDirectoryRecursive(skillPath)
|
||||
logContext.info('Skill folder deleted', { agentId, folderName, skillPath })
|
||||
} catch (error) {
|
||||
const nodeError = error as NodeJS.ErrnoException
|
||||
if (nodeError.code !== 'ENOENT') {
|
||||
throw this.toPluginError('uninstall-skill', error)
|
||||
}
|
||||
logContext.warn('Skill folder already deleted', { agentId, folderName, skillPath })
|
||||
}
|
||||
}
|
||||
|
||||
private toPluginError(operation: string, error: unknown): PluginError {
|
||||
return {
|
||||
type: 'TRANSACTION_FAILED',
|
||||
operation,
|
||||
reason: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
||||
|
||||
private async safeUnlink(targetPath: string, label: string): Promise<void> {
|
||||
try {
|
||||
await fs.promises.unlink(targetPath)
|
||||
logger.debug(`Rolled back ${label}`, { targetPath })
|
||||
} catch (unlinkError) {
|
||||
logger.error(`Failed to rollback ${label}`, {
|
||||
targetPath,
|
||||
error: unlinkError instanceof Error ? unlinkError.message : String(unlinkError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async safeRemoveDirectory(targetPath: string, label: string): Promise<void> {
|
||||
try {
|
||||
await deleteDirectoryRecursive(targetPath)
|
||||
logger.info(`Rolled back ${label}`, { targetPath })
|
||||
} catch (unlinkError) {
|
||||
logger.error(`Failed to rollback ${label}`, {
|
||||
targetPath,
|
||||
error: unlinkError instanceof Error ? unlinkError.message : String(unlinkError)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
614
src/main/services/agents/plugins/PluginService.ts
Normal file
614
src/main/services/agents/plugins/PluginService.ts
Normal file
@@ -0,0 +1,614 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { parsePluginMetadata, parseSkillMetadata } from '@main/utils/markdownParser'
|
||||
import type {
|
||||
GetAgentResponse,
|
||||
InstalledPlugin,
|
||||
InstallPluginOptions,
|
||||
ListAvailablePluginsResult,
|
||||
PluginError,
|
||||
PluginMetadata,
|
||||
PluginType,
|
||||
UninstallPluginOptions
|
||||
} from '@types'
|
||||
import { app } from 'electron'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
import { AgentService } from '../services/AgentService'
|
||||
import { PluginCacheStore } from './PluginCacheStore'
|
||||
import { PluginInstaller } from './PluginInstaller'
|
||||
|
||||
const logger = loggerService.withContext('PluginService')
|
||||
|
||||
interface PluginServiceConfig {
|
||||
maxFileSize: number // bytes
|
||||
cacheTimeout: number // milliseconds
|
||||
}
|
||||
|
||||
/**
|
||||
* PluginService manages agent and command plugins from resources directory.
|
||||
*
|
||||
* Features:
|
||||
* - Singleton pattern for consistent state management
|
||||
* - Caching of available plugins for performance
|
||||
* - Security validation (path traversal, file size, extensions)
|
||||
* - Transactional install/uninstall operations
|
||||
* - Integration with AgentService for metadata persistence
|
||||
*/
|
||||
export class PluginService {
|
||||
private static instance: PluginService | null = null
|
||||
|
||||
private availablePluginsCache: ListAvailablePluginsResult | null = null
|
||||
private cacheTimestamp = 0
|
||||
private config: PluginServiceConfig
|
||||
private readonly cacheStore: PluginCacheStore
|
||||
private readonly installer: PluginInstaller
|
||||
private readonly agentService: AgentService
|
||||
|
||||
private readonly ALLOWED_EXTENSIONS = ['.md', '.markdown']
|
||||
|
||||
private constructor(config?: Partial<PluginServiceConfig>) {
|
||||
this.config = {
|
||||
maxFileSize: config?.maxFileSize ?? 1024 * 1024, // 1MB default
|
||||
cacheTimeout: config?.cacheTimeout ?? 5 * 60 * 1000 // 5 minutes default
|
||||
}
|
||||
this.agentService = AgentService.getInstance()
|
||||
this.cacheStore = new PluginCacheStore({
|
||||
allowedExtensions: this.ALLOWED_EXTENSIONS,
|
||||
getPluginDirectoryName: this.getPluginDirectoryName.bind(this),
|
||||
getClaudeBasePath: this.getClaudeBasePath.bind(this),
|
||||
getClaudePluginDirectory: this.getClaudePluginDirectory.bind(this),
|
||||
getPluginsBasePath: this.getPluginsBasePath.bind(this)
|
||||
})
|
||||
this.installer = new PluginInstaller()
|
||||
|
||||
logger.info('PluginService initialized', {
|
||||
maxFileSize: this.config.maxFileSize,
|
||||
cacheTimeout: this.config.cacheTimeout
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
*/
|
||||
static getInstance(config?: Partial<PluginServiceConfig>): PluginService {
|
||||
if (!PluginService.instance) {
|
||||
PluginService.instance = new PluginService(config)
|
||||
}
|
||||
return PluginService.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available plugins from resources directory (with caching)
|
||||
*/
|
||||
async listAvailable(): Promise<ListAvailablePluginsResult> {
|
||||
const now = Date.now()
|
||||
|
||||
// Return cached data if still valid
|
||||
if (this.availablePluginsCache && now - this.cacheTimestamp < this.config.cacheTimeout) {
|
||||
logger.debug('Returning cached plugin list', {
|
||||
cacheAge: now - this.cacheTimestamp
|
||||
})
|
||||
return this.availablePluginsCache
|
||||
}
|
||||
|
||||
logger.info('Scanning available plugins')
|
||||
|
||||
// Scan all plugin types
|
||||
const [agents, commands, skills] = await Promise.all([
|
||||
this.cacheStore.listAvailableFilePlugins('agent'),
|
||||
this.cacheStore.listAvailableFilePlugins('command'),
|
||||
this.cacheStore.listAvailableSkills()
|
||||
])
|
||||
|
||||
const result: ListAvailablePluginsResult = {
|
||||
agents,
|
||||
commands,
|
||||
skills, // NEW: include skills
|
||||
total: agents.length + commands.length + skills.length
|
||||
}
|
||||
|
||||
// Update cache
|
||||
this.availablePluginsCache = result
|
||||
this.cacheTimestamp = now
|
||||
|
||||
logger.info('Available plugins scanned', {
|
||||
agentsCount: agents.length,
|
||||
commandsCount: commands.length,
|
||||
skillsCount: skills.length,
|
||||
total: result.total
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Install plugin with validation and transactional safety
|
||||
*/
|
||||
async install(options: InstallPluginOptions): Promise<PluginMetadata> {
|
||||
logger.info('Installing plugin', options)
|
||||
|
||||
const context = await this.prepareInstallContext(options)
|
||||
|
||||
if (options.type === 'skill') {
|
||||
return await this.installSkillPlugin(options, context)
|
||||
}
|
||||
|
||||
return await this.installFilePlugin(options, context)
|
||||
}
|
||||
|
||||
private async prepareInstallContext(options: InstallPluginOptions): Promise<{
|
||||
agent: GetAgentResponse
|
||||
workdir: string
|
||||
sourceAbsolutePath: string
|
||||
}> {
|
||||
const agent = await this.getAgentOrThrow(options.agentId)
|
||||
const workdir = this.getWorkdirOrThrow(agent, options.agentId)
|
||||
|
||||
await this.validateWorkdir(agent, workdir)
|
||||
|
||||
const sourceAbsolutePath = this.cacheStore.resolveSourcePath(options.sourcePath)
|
||||
|
||||
return { agent, workdir, sourceAbsolutePath }
|
||||
}
|
||||
|
||||
private async installSkillPlugin(
|
||||
options: InstallPluginOptions,
|
||||
context: {
|
||||
agent: GetAgentResponse
|
||||
workdir: string
|
||||
sourceAbsolutePath: string
|
||||
}
|
||||
): Promise<PluginMetadata> {
|
||||
const { agent, workdir, sourceAbsolutePath } = context
|
||||
|
||||
await this.cacheStore.ensureSkillSourceDirectory(sourceAbsolutePath, options.sourcePath)
|
||||
|
||||
const metadata = await parseSkillMetadata(sourceAbsolutePath, options.sourcePath, 'skills')
|
||||
const sanitizedFolderName = this.sanitizeFolderName(metadata.filename)
|
||||
|
||||
await this.ensureClaudeDirectory(workdir, 'skill')
|
||||
const destPath = this.getClaudePluginPath(workdir, 'skill', sanitizedFolderName)
|
||||
|
||||
metadata.filename = sanitizedFolderName
|
||||
|
||||
await this.installer.installSkill(agent.id, sourceAbsolutePath, destPath)
|
||||
|
||||
const installedAt = Date.now()
|
||||
const metadataWithInstall: PluginMetadata = {
|
||||
...metadata,
|
||||
filename: sanitizedFolderName,
|
||||
installedAt,
|
||||
updatedAt: metadata.updatedAt ?? installedAt,
|
||||
type: 'skill'
|
||||
}
|
||||
const installedPlugin: InstalledPlugin = {
|
||||
filename: sanitizedFolderName,
|
||||
type: 'skill',
|
||||
metadata: metadataWithInstall
|
||||
}
|
||||
|
||||
await this.cacheStore.upsert(workdir, installedPlugin)
|
||||
this.upsertAgentPlugin(agent, installedPlugin)
|
||||
|
||||
logger.info('Skill installed successfully', {
|
||||
agentId: options.agentId,
|
||||
sourcePath: options.sourcePath,
|
||||
folderName: sanitizedFolderName
|
||||
})
|
||||
|
||||
return metadataWithInstall
|
||||
}
|
||||
|
||||
private async installFilePlugin(
|
||||
options: InstallPluginOptions,
|
||||
context: {
|
||||
agent: GetAgentResponse
|
||||
workdir: string
|
||||
sourceAbsolutePath: string
|
||||
}
|
||||
): Promise<PluginMetadata> {
|
||||
const { agent, workdir, sourceAbsolutePath } = context
|
||||
|
||||
if (options.type === 'skill') {
|
||||
throw {
|
||||
type: 'INVALID_FILE_TYPE',
|
||||
extension: options.type
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
const filePluginType: 'agent' | 'command' = options.type
|
||||
|
||||
await this.cacheStore.validatePluginFile(sourceAbsolutePath, this.config.maxFileSize)
|
||||
|
||||
const category = path.basename(path.dirname(options.sourcePath))
|
||||
const metadata = await parsePluginMetadata(sourceAbsolutePath, options.sourcePath, category, filePluginType)
|
||||
|
||||
const sanitizedFilename = this.sanitizeFilename(metadata.filename)
|
||||
metadata.filename = sanitizedFilename
|
||||
|
||||
await this.ensureClaudeDirectory(workdir, filePluginType)
|
||||
const destPath = this.getClaudePluginPath(workdir, filePluginType, sanitizedFilename)
|
||||
|
||||
await this.installer.installFilePlugin(agent.id, sourceAbsolutePath, destPath)
|
||||
|
||||
const installedAt = Date.now()
|
||||
const metadataWithInstall: PluginMetadata = {
|
||||
...metadata,
|
||||
filename: sanitizedFilename,
|
||||
installedAt,
|
||||
updatedAt: metadata.updatedAt ?? installedAt,
|
||||
type: filePluginType
|
||||
}
|
||||
const installedPlugin: InstalledPlugin = {
|
||||
filename: sanitizedFilename,
|
||||
type: filePluginType,
|
||||
metadata: metadataWithInstall
|
||||
}
|
||||
|
||||
await this.cacheStore.upsert(workdir, installedPlugin)
|
||||
this.upsertAgentPlugin(agent, installedPlugin)
|
||||
|
||||
logger.info('Plugin installed successfully', {
|
||||
agentId: options.agentId,
|
||||
filename: sanitizedFilename,
|
||||
type: filePluginType
|
||||
})
|
||||
|
||||
return metadataWithInstall
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall plugin with cleanup
|
||||
*/
|
||||
async uninstall(options: UninstallPluginOptions): Promise<void> {
|
||||
logger.info('Uninstalling plugin', options)
|
||||
|
||||
const agent = await this.getAgentOrThrow(options.agentId)
|
||||
const workdir = this.getWorkdirOrThrow(agent, options.agentId)
|
||||
|
||||
await this.validateWorkdir(agent, workdir)
|
||||
|
||||
if (options.type === 'skill') {
|
||||
const sanitizedFolderName = this.sanitizeFolderName(options.filename)
|
||||
const skillPath = this.getClaudePluginPath(workdir, 'skill', sanitizedFolderName)
|
||||
|
||||
await this.installer.uninstallSkill(agent.id, sanitizedFolderName, skillPath)
|
||||
await this.cacheStore.remove(workdir, sanitizedFolderName, 'skill')
|
||||
this.removeAgentPlugin(agent, sanitizedFolderName, 'skill')
|
||||
|
||||
logger.info('Skill uninstalled successfully', {
|
||||
agentId: options.agentId,
|
||||
folderName: sanitizedFolderName
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const sanitizedFilename = this.sanitizeFilename(options.filename)
|
||||
const filePath = this.getClaudePluginPath(workdir, options.type, sanitizedFilename)
|
||||
|
||||
await this.installer.uninstallFilePlugin(agent.id, sanitizedFilename, options.type, filePath)
|
||||
await this.cacheStore.remove(workdir, sanitizedFilename, options.type)
|
||||
this.removeAgentPlugin(agent, sanitizedFilename, options.type)
|
||||
|
||||
logger.info('Plugin uninstalled successfully', {
|
||||
agentId: options.agentId,
|
||||
filename: sanitizedFilename,
|
||||
type: options.type
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* List installed plugins for an agent (from database + filesystem validation)
|
||||
*/
|
||||
async listInstalled(agentId: string): Promise<InstalledPlugin[]> {
|
||||
logger.debug('Listing installed plugins', { agentId })
|
||||
|
||||
const agent = await this.getAgentOrThrow(agentId)
|
||||
|
||||
const workdir = agent.accessible_paths?.[0]
|
||||
|
||||
if (!workdir) {
|
||||
logger.warn('Agent has no accessible paths', { agentId })
|
||||
return []
|
||||
}
|
||||
|
||||
const plugins = await this.listInstalledFromCache(workdir)
|
||||
|
||||
logger.debug('Listed installed plugins from cache', {
|
||||
agentId,
|
||||
count: plugins.length
|
||||
})
|
||||
|
||||
return plugins
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate plugin cache (for development/testing)
|
||||
*/
|
||||
invalidateCache(): void {
|
||||
this.availablePluginsCache = null
|
||||
this.cacheTimestamp = 0
|
||||
logger.info('Plugin cache invalidated')
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cache File Management (for installed plugins)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Read cache file from .claude/plugins.json
|
||||
* Returns null if cache doesn't exist or is invalid
|
||||
*/
|
||||
|
||||
/**
|
||||
* List installed plugins from cache file
|
||||
* Falls back to filesystem scan if cache is missing or corrupt
|
||||
*/
|
||||
async listInstalledFromCache(workdir: string): Promise<InstalledPlugin[]> {
|
||||
logger.debug('Listing installed plugins from cache', { workdir })
|
||||
return await this.cacheStore.listInstalled(workdir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read plugin content from source (resources directory)
|
||||
*/
|
||||
async readContent(sourcePath: string): Promise<string> {
|
||||
logger.info('Reading plugin content', { sourcePath })
|
||||
const content = await this.cacheStore.readSourceContent(sourcePath)
|
||||
logger.debug('Plugin content read successfully', {
|
||||
sourcePath,
|
||||
size: content.length
|
||||
})
|
||||
return content
|
||||
}
|
||||
|
||||
/**
|
||||
* Write plugin content to installed plugin (in agent's .claude directory)
|
||||
* Note: Only works for file-based plugins (agents/commands), not skills
|
||||
*/
|
||||
async writeContent(agentId: string, filename: string, type: PluginType, content: string): Promise<void> {
|
||||
logger.info('Writing plugin content', { agentId, filename, type })
|
||||
|
||||
const agent = await this.getAgentOrThrow(agentId)
|
||||
const workdir = this.getWorkdirOrThrow(agent, agentId)
|
||||
|
||||
await this.validateWorkdir(agent, workdir)
|
||||
|
||||
// Check if plugin is installed
|
||||
let installedPlugins = agent.installed_plugins ?? []
|
||||
if (installedPlugins.length === 0) {
|
||||
installedPlugins = await this.cacheStore.listInstalled(workdir)
|
||||
agent.installed_plugins = installedPlugins
|
||||
}
|
||||
const installedPlugin = installedPlugins.find((p) => p.filename === filename && p.type === type)
|
||||
|
||||
if (!installedPlugin) {
|
||||
throw {
|
||||
type: 'PLUGIN_NOT_INSTALLED',
|
||||
filename,
|
||||
agentId
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
if (type === 'skill') {
|
||||
throw {
|
||||
type: 'INVALID_FILE_TYPE',
|
||||
extension: type
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
const filePluginType = type as 'agent' | 'command'
|
||||
const filePath = this.getClaudePluginPath(workdir, filePluginType, filename)
|
||||
const newContentHash = await this.installer.updateFilePluginContent(agent.id, filePath, content)
|
||||
|
||||
const updatedMetadata: PluginMetadata = {
|
||||
...installedPlugin.metadata,
|
||||
contentHash: newContentHash,
|
||||
size: Buffer.byteLength(content, 'utf8'),
|
||||
updatedAt: Date.now(),
|
||||
filename,
|
||||
type: filePluginType
|
||||
}
|
||||
const updatedPlugin: InstalledPlugin = {
|
||||
filename,
|
||||
type: filePluginType,
|
||||
metadata: updatedMetadata
|
||||
}
|
||||
|
||||
await this.cacheStore.upsert(workdir, updatedPlugin)
|
||||
this.upsertAgentPlugin(agent, updatedPlugin)
|
||||
|
||||
logger.info('Plugin content updated successfully', {
|
||||
agentId,
|
||||
filename,
|
||||
type: filePluginType,
|
||||
newContentHash
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Private Helper Methods
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Resolve plugin type to directory name under .claude
|
||||
*/
|
||||
private getPluginDirectoryName(type: PluginType): 'agents' | 'commands' | 'skills' {
|
||||
if (type === 'agent') {
|
||||
return 'agents'
|
||||
}
|
||||
if (type === 'command') {
|
||||
return 'commands'
|
||||
}
|
||||
return 'skills'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base .claude directory for a workdir
|
||||
*/
|
||||
private getClaudeBasePath(workdir: string): string {
|
||||
return path.join(workdir, '.claude')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the directory for a specific plugin type inside .claude
|
||||
*/
|
||||
private getClaudePluginDirectory(workdir: string, type: PluginType): string {
|
||||
return path.join(this.getClaudeBasePath(workdir), this.getPluginDirectoryName(type))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the absolute path for a plugin file/folder inside .claude
|
||||
*/
|
||||
private getClaudePluginPath(workdir: string, type: PluginType, filename: string): string {
|
||||
return path.join(this.getClaudePluginDirectory(workdir, type), filename)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute path to plugins directory (handles packaged vs dev)
|
||||
*/
|
||||
private getPluginsBasePath(): string {
|
||||
// Use the utility function which handles both dev and production correctly
|
||||
if (app.isPackaged) {
|
||||
return path.join(process.resourcesPath, 'claude-code-plugins')
|
||||
}
|
||||
return path.join(__dirname, '../../node_modules/claude-code-plugins/plugins')
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate source path to prevent path traversal attacks
|
||||
*/
|
||||
private async getAgentOrThrow(agentId: string): Promise<GetAgentResponse> {
|
||||
const agent = await this.agentService.getAgent(agentId)
|
||||
if (!agent) {
|
||||
throw {
|
||||
type: 'INVALID_WORKDIR',
|
||||
agentId,
|
||||
workdir: '',
|
||||
message: 'Agent not found'
|
||||
} as PluginError
|
||||
}
|
||||
return agent
|
||||
}
|
||||
|
||||
private getWorkdirOrThrow(agent: GetAgentResponse, agentId: string): string {
|
||||
const workdir = agent.accessible_paths?.[0]
|
||||
if (!workdir) {
|
||||
throw {
|
||||
type: 'INVALID_WORKDIR',
|
||||
agentId,
|
||||
workdir: '',
|
||||
message: 'Agent has no accessible paths'
|
||||
} as PluginError
|
||||
}
|
||||
return workdir
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate workdir against agent's accessible paths
|
||||
*/
|
||||
private async validateWorkdir(agent: GetAgentResponse, workdir: string): Promise<void> {
|
||||
// Verify workdir is in agent's accessible_paths
|
||||
if (!agent.accessible_paths?.includes(workdir)) {
|
||||
throw {
|
||||
type: 'INVALID_WORKDIR',
|
||||
workdir,
|
||||
agentId: agent.id,
|
||||
message: 'Workdir not in agent accessible paths'
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
// Verify workdir exists and is accessible
|
||||
try {
|
||||
await fs.promises.access(workdir, fs.constants.R_OK | fs.constants.W_OK)
|
||||
} catch (error) {
|
||||
throw {
|
||||
type: 'WORKDIR_NOT_FOUND',
|
||||
workdir,
|
||||
message: 'Workdir does not exist or is not accessible'
|
||||
} as PluginError
|
||||
}
|
||||
}
|
||||
|
||||
private upsertAgentPlugin(agent: GetAgentResponse, plugin: InstalledPlugin): void {
|
||||
const existing = agent.installed_plugins ?? []
|
||||
const filtered = existing.filter((p) => !(p.filename === plugin.filename && p.type === plugin.type))
|
||||
agent.installed_plugins = [...filtered, plugin]
|
||||
}
|
||||
|
||||
private removeAgentPlugin(agent: GetAgentResponse, filename: string, type: PluginType): void {
|
||||
if (!agent.installed_plugins) {
|
||||
agent.installed_plugins = []
|
||||
return
|
||||
}
|
||||
agent.installed_plugins = agent.installed_plugins.filter((p) => !(p.filename === filename && p.type === type))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize filename to remove unsafe characters (for agents/commands)
|
||||
*/
|
||||
private sanitizeFilename(filename: string): string {
|
||||
// Remove path separators
|
||||
let sanitized = filename.replace(/[/\\]/g, '_')
|
||||
// Remove null bytes using String method to avoid control-regex lint error
|
||||
sanitized = sanitized.replace(new RegExp(String.fromCharCode(0), 'g'), '')
|
||||
// Limit to safe characters (alphanumeric, dash, underscore, dot)
|
||||
sanitized = sanitized.replace(/[^a-zA-Z0-9._-]/g, '_')
|
||||
|
||||
// Ensure .md extension
|
||||
if (!sanitized.endsWith('.md') && !sanitized.endsWith('.markdown')) {
|
||||
sanitized += '.md'
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize folder name for skills (different rules than file names)
|
||||
* NO dots allowed to avoid confusion with file extensions
|
||||
*/
|
||||
private sanitizeFolderName(folderName: string): string {
|
||||
// Remove path separators
|
||||
let sanitized = folderName.replace(/[/\\]/g, '_')
|
||||
// Remove null bytes using String method to avoid control-regex lint error
|
||||
sanitized = sanitized.replace(new RegExp(String.fromCharCode(0), 'g'), '')
|
||||
// Limit to safe characters (alphanumeric, dash, underscore)
|
||||
// NOTE: No dots allowed to avoid confusion with file extensions
|
||||
sanitized = sanitized.replace(/[^a-zA-Z0-9_-]/g, '_')
|
||||
|
||||
// Validate no extension was provided
|
||||
if (folderName.includes('.')) {
|
||||
logger.warn('Skill folder name contained dots, sanitized', {
|
||||
original: folderName,
|
||||
sanitized
|
||||
})
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure .claude subdirectory exists for the given plugin type
|
||||
*/
|
||||
private async ensureClaudeDirectory(workdir: string, type: PluginType): Promise<void> {
|
||||
const typeDir = this.getClaudePluginDirectory(workdir, type)
|
||||
|
||||
try {
|
||||
await fs.promises.mkdir(typeDir, { recursive: true })
|
||||
logger.debug('Ensured directory exists', { typeDir })
|
||||
} catch (error) {
|
||||
logger.error('Failed to create directory', {
|
||||
typeDir,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
throw {
|
||||
type: 'PERMISSION_DENIED',
|
||||
path: typeDir
|
||||
} as PluginError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const pluginService = PluginService.getInstance()
|
||||
@@ -1,5 +1,7 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { pluginService } from '@main/services/agents/plugins/PluginService'
|
||||
import { getDataPath } from '@main/utils'
|
||||
import type {
|
||||
AgentEntity,
|
||||
@@ -17,6 +19,8 @@ import { BaseService } from '../BaseService'
|
||||
import { type AgentRow, agentsTable, type InsertAgentRow } from '../database/schema'
|
||||
import type { AgentModelField } from '../errors'
|
||||
|
||||
const logger = loggerService.withContext('AgentService')
|
||||
|
||||
export class AgentService extends BaseService {
|
||||
private static instance: AgentService | null = null
|
||||
private readonly modelFields: AgentModelField[] = ['model', 'plan_model', 'small_model']
|
||||
@@ -92,6 +96,24 @@ export class AgentService extends BaseService {
|
||||
|
||||
const agent = this.deserializeJsonFields(result[0]) as GetAgentResponse
|
||||
agent.tools = await this.listMcpTools(agent.type, agent.mcps)
|
||||
|
||||
// Load installed_plugins from cache file instead of database
|
||||
const workdir = agent.accessible_paths?.[0]
|
||||
if (workdir) {
|
||||
try {
|
||||
agent.installed_plugins = await pluginService.listInstalledFromCache(workdir)
|
||||
} catch (error) {
|
||||
// Log error but don't fail the request
|
||||
logger.warn(`Failed to load installed plugins for agent ${id}`, {
|
||||
workdir,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
agent.installed_plugins = []
|
||||
}
|
||||
} else {
|
||||
agent.installed_plugins = []
|
||||
}
|
||||
|
||||
return agent
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -93,13 +106,51 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
ANTHROPIC_AUTH_TOKEN: modelInfo.provider.apiKey,
|
||||
ANTHROPIC_BASE_URL: modelInfo.provider.anthropicApiHost?.trim() || modelInfo.provider.apiHost,
|
||||
ANTHROPIC_MODEL: modelInfo.modelId,
|
||||
ANTHROPIC_SMALL_FAST_MODEL: modelInfo.modelId,
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: modelInfo.modelId,
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: modelInfo.modelId,
|
||||
// TODO: support set small model in UI
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: modelInfo.modelId,
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
ELECTRON_NO_ATTACH_CONSOLE: '1'
|
||||
}
|
||||
|
||||
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 +173,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 +213,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 +234,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 +325,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 +334,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 +348,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 +359,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
})
|
||||
}
|
||||
|
||||
// Transform SDKMessage to UIMessageChunks
|
||||
const chunks = transformSDKMessageToStreamParts(message, streamState)
|
||||
for (const chunk of chunks) {
|
||||
stream.emit('data', {
|
||||
@@ -242,7 +368,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
// Successfully completed
|
||||
hasCompleted = true
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
@@ -251,7 +376,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
messageCount: jsonOutput.length
|
||||
})
|
||||
|
||||
// Emit completion event
|
||||
stream.emit('data', {
|
||||
type: 'complete'
|
||||
})
|
||||
@@ -260,8 +384,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 +392,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 +406,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'
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -73,6 +73,15 @@ const emptyUsage: LanguageModelUsage = {
|
||||
*/
|
||||
const generateMessageId = (): string => `msg_${uuidv4().replace(/-/g, '')}`
|
||||
|
||||
/**
|
||||
* Filters out command-* tags from text content to prevent internal command
|
||||
* messages from appearing in the user-facing UI.
|
||||
* Removes tags like <command-message>...</command-message> and <command-name>...</command-name>
|
||||
*/
|
||||
const filterCommandTags = (text: string): string => {
|
||||
return text.replace(/<command-[^>]+>.*?<\/command-[^>]+>/gs, '').trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts provider metadata from the raw Claude message so we can surface it
|
||||
* on every emitted stream part for observability and debugging purposes.
|
||||
@@ -270,12 +279,17 @@ function handleUserMessage(
|
||||
const chunks: AgentStreamPart[] = []
|
||||
const providerMetadata = sdkMessageToProviderMetadata(message)
|
||||
const content = message.message.content
|
||||
|
||||
const isSynthetic = message.isSynthetic ?? false
|
||||
if (typeof content === 'string') {
|
||||
if (!content) {
|
||||
return chunks
|
||||
}
|
||||
|
||||
const filteredContent = filterCommandTags(content)
|
||||
if (!filteredContent) {
|
||||
return chunks
|
||||
}
|
||||
|
||||
const id = message.uuid?.toString() || generateMessageId()
|
||||
chunks.push({
|
||||
type: 'text-start',
|
||||
@@ -285,7 +299,7 @@ function handleUserMessage(
|
||||
chunks.push({
|
||||
type: 'text-delta',
|
||||
id,
|
||||
text: content,
|
||||
text: filteredContent,
|
||||
providerMetadata
|
||||
})
|
||||
chunks.push({
|
||||
@@ -323,24 +337,30 @@ function handleUserMessage(
|
||||
providerExecuted: true
|
||||
})
|
||||
}
|
||||
} else if (block.type === 'text') {
|
||||
const id = message.uuid?.toString() || generateMessageId()
|
||||
chunks.push({
|
||||
type: 'text-start',
|
||||
id,
|
||||
providerMetadata
|
||||
})
|
||||
chunks.push({
|
||||
type: 'text-delta',
|
||||
id,
|
||||
text: (block as { text: string }).text,
|
||||
providerMetadata
|
||||
})
|
||||
chunks.push({
|
||||
type: 'text-end',
|
||||
id,
|
||||
providerMetadata
|
||||
})
|
||||
} else if (block.type === 'text' && !isSynthetic) {
|
||||
const rawText = (block as { text: string }).text
|
||||
const filteredText = filterCommandTags(rawText)
|
||||
|
||||
// Only push text chunks if there's content after filtering
|
||||
if (filteredText) {
|
||||
const id = message.uuid?.toString() || generateMessageId()
|
||||
chunks.push({
|
||||
type: 'text-start',
|
||||
id,
|
||||
providerMetadata
|
||||
})
|
||||
chunks.push({
|
||||
type: 'text-delta',
|
||||
id,
|
||||
text: filteredText,
|
||||
providerMetadata
|
||||
})
|
||||
chunks.push({
|
||||
type: 'text-end',
|
||||
id,
|
||||
providerMetadata
|
||||
})
|
||||
}
|
||||
} else {
|
||||
logger.warn('Unhandled user content block', { type: (block as any).type })
|
||||
}
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { ocrProviderRepository } from '@main/data/repositories/OcrProviderRepository'
|
||||
import type {
|
||||
DbOcrProvider,
|
||||
ListOcrProvidersQuery,
|
||||
OcrParams,
|
||||
OcrProvider,
|
||||
OcrProviderBusiness,
|
||||
OcrProviderCreateBusiness,
|
||||
OcrProviderKeyBusiness,
|
||||
OcrProviderReplaceBusiness,
|
||||
OcrProviderUpdateBusiness,
|
||||
OcrResult,
|
||||
SupportedOcrFile
|
||||
} from '@types'
|
||||
import { BuiltinOcrProviderIdMap } from '@types'
|
||||
import { isLinux } from '@main/constant'
|
||||
import type { OcrHandler, OcrProvider, OcrResult, SupportedOcrFile } from '@types'
|
||||
import { BuiltinOcrProviderIds } from '@types'
|
||||
|
||||
import type { OcrBaseService } from './builtin/OcrBaseService'
|
||||
import { ovOcrService } from './builtin/OvOcrService'
|
||||
import { ppocrService } from './builtin/PpocrService'
|
||||
import { systemOcrService } from './builtin/SystemOcrService'
|
||||
@@ -23,285 +10,40 @@ import { tesseractService } from './builtin/TesseractService'
|
||||
|
||||
const logger = loggerService.withContext('OcrService')
|
||||
|
||||
/**
|
||||
* Business logic layer for OCR operations
|
||||
* Handles OCR provider registration, orchestration, and core OCR functionality
|
||||
*/
|
||||
class OcrService {
|
||||
private registry: Map<OcrProviderKeyBusiness, OcrBaseService> = new Map()
|
||||
private initialized: boolean = false
|
||||
export class OcrService {
|
||||
private registry: Map<string, OcrHandler> = new Map()
|
||||
|
||||
constructor() {
|
||||
this.registerBuiltinProviders()
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the service is initialized
|
||||
*/
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (!this.initialized) {
|
||||
await this.initializeBuiltinProviders()
|
||||
this.initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize built-in OCR providers
|
||||
*/
|
||||
private async initializeBuiltinProviders(): Promise<void> {
|
||||
try {
|
||||
// Ensure built-in providers exist in database
|
||||
await ocrProviderRepository.initializeBuiltInProviders()
|
||||
|
||||
logger.info('OCR service initialized with built-in providers')
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize OCR service', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register built-in providers (sync)
|
||||
*/
|
||||
private registerBuiltinProviders(): void {
|
||||
this.register(BuiltinOcrProviderIdMap.tesseract, tesseractService)
|
||||
|
||||
if (systemOcrService) {
|
||||
this.register(BuiltinOcrProviderIdMap.system, systemOcrService)
|
||||
}
|
||||
|
||||
this.register(BuiltinOcrProviderIdMap.paddleocr, ppocrService)
|
||||
|
||||
if (ovOcrService) {
|
||||
this.register(BuiltinOcrProviderIdMap.ovocr, ovOcrService)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an OCR provider service
|
||||
*/
|
||||
private register(providerId: OcrProviderKeyBusiness, service: OcrBaseService): void {
|
||||
register(providerId: string, handler: OcrHandler): void {
|
||||
if (this.registry.has(providerId)) {
|
||||
logger.warn(`Provider ${providerId} already registered. Overwriting.`)
|
||||
logger.warn(`Provider ${providerId} has existing handler. Overwrited.`)
|
||||
}
|
||||
this.registry.set(providerId, service)
|
||||
logger.info(`Registered OCR provider: ${providerId}`)
|
||||
this.registry.set(providerId, handler)
|
||||
}
|
||||
|
||||
// Not sure when it will be needed.
|
||||
/**
|
||||
* Unregister an OCR provider service
|
||||
*/
|
||||
// private unregister(providerId: OcrProviderId): void {
|
||||
// if (this.registry.delete(providerId)) {
|
||||
// logger.info(`Unregistered OCR provider: ${providerId}`)
|
||||
// }
|
||||
// }
|
||||
unregister(providerId: string): void {
|
||||
this.registry.delete(providerId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered provider IDs
|
||||
*/
|
||||
public getRegisteredProviderIds(): OcrProviderKeyBusiness[] {
|
||||
public listProviderIds(): string[] {
|
||||
return Array.from(this.registry.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider is registered
|
||||
*/
|
||||
public isProviderRegistered(providerId: OcrProviderKeyBusiness): boolean {
|
||||
return this.registry.has(providerId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of OCR providers
|
||||
*/
|
||||
public async listProviders(query?: ListOcrProvidersQuery): Promise<OcrProviderBusiness[]> {
|
||||
try {
|
||||
await this.ensureInitialized()
|
||||
const providers = await ocrProviderRepository.findAll()
|
||||
|
||||
let result = providers
|
||||
if (query?.registered) {
|
||||
// Filter by registered providers
|
||||
const registeredIds = this.getRegisteredProviderIds()
|
||||
result = providers.filter((provider) => registeredIds.includes(provider.id))
|
||||
}
|
||||
|
||||
logger.debug(`Listed ${result.length} OCR providers`)
|
||||
return result
|
||||
} catch (error) {
|
||||
logger.error('Failed to list OCR providers', error as Error)
|
||||
throw error
|
||||
public async ocr(file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> {
|
||||
const handler = this.registry.get(provider.id)
|
||||
if (!handler) {
|
||||
throw new Error(`Provider ${provider.id} is not registered`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OCR provider by ID
|
||||
*/
|
||||
public async getProvider(providerId: OcrProviderKeyBusiness): Promise<OcrProviderBusiness> {
|
||||
try {
|
||||
await this.ensureInitialized()
|
||||
const provider = await ocrProviderRepository.findById(providerId)
|
||||
logger.debug(`Retrieved OCR provider: ${providerId}`)
|
||||
return provider
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get OCR provider ${providerId}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new OCR provider
|
||||
*/
|
||||
public async createProvider(data: OcrProviderCreateBusiness): Promise<OcrProviderBusiness> {
|
||||
try {
|
||||
await this.ensureInitialized()
|
||||
const result = await ocrProviderRepository.create(data)
|
||||
logger.info(`Created OCR provider: ${data.id}`)
|
||||
return result
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create OCR provider ${data.id}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update OCR provider (partial update)
|
||||
*/
|
||||
public async updateProvider(
|
||||
id: OcrProviderKeyBusiness,
|
||||
data: OcrProviderUpdateBusiness
|
||||
): Promise<OcrProviderBusiness> {
|
||||
try {
|
||||
await this.ensureInitialized()
|
||||
const result = await ocrProviderRepository.update(id, data)
|
||||
logger.info(`Updated OCR provider: ${id}`)
|
||||
return result
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update OCR provider ${id}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace OCR provider (full update)
|
||||
*/
|
||||
public async replaceProvider(data: OcrProviderReplaceBusiness): Promise<OcrProviderBusiness> {
|
||||
try {
|
||||
await this.ensureInitialized()
|
||||
const result = await ocrProviderRepository.replace(data)
|
||||
logger.info(`Replaced OCR provider: ${data.id}`)
|
||||
return result
|
||||
} catch (error) {
|
||||
logger.error(`Failed to replace OCR provider ${data.id}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete OCR provider
|
||||
*/
|
||||
public async deleteProvider(id: OcrProviderKeyBusiness): Promise<void> {
|
||||
try {
|
||||
await this.ensureInitialized()
|
||||
await ocrProviderRepository.delete(id)
|
||||
logger.info(`Deleted OCR provider: ${id}`)
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete OCR provider ${id}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform OCR on a file using the specified provider
|
||||
*/
|
||||
public async ocr(file: SupportedOcrFile, params: OcrParams): Promise<OcrResult> {
|
||||
try {
|
||||
await this.ensureInitialized()
|
||||
const service = this.registry.get(params.providerId)
|
||||
if (!service) {
|
||||
throw new Error(`Provider ${params.providerId} is not registered`)
|
||||
}
|
||||
|
||||
// Validate that the provider exists in database
|
||||
const provider = await this.getProvider(params.providerId)
|
||||
|
||||
logger.debug(`Performing OCR with provider: ${JSON.stringify(provider, undefined, 2)}`)
|
||||
const result = await service.ocr(file, provider.config)
|
||||
|
||||
logger.info(`OCR completed successfully with provider: ${params.providerId}`)
|
||||
return result
|
||||
} catch (error) {
|
||||
logger.error(`OCR failed with provider ${params.providerId}`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider is available and ready
|
||||
*/
|
||||
public async isProviderAvailable(providerId: OcrProviderKeyBusiness): Promise<boolean> {
|
||||
try {
|
||||
const service = this.registry.get(providerId)
|
||||
if (!service) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if provider exists in database
|
||||
await this.getProvider(providerId)
|
||||
|
||||
// Additional availability checks can be added here
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.debug(`Provider ${providerId} is not available`, error as Error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private async _isProviderAvailable(provider: OcrProvider): Promise<boolean> {
|
||||
try {
|
||||
return this.registry.get(provider.id) !== undefined
|
||||
} catch (error) {
|
||||
logger.debug(`Provider ${provider.id} is not available`, error as Error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available providers
|
||||
* It's only for image type. May re-designed for a specific file type in the future.
|
||||
*
|
||||
*/
|
||||
public async getAvailableProvidersForFile(): Promise<DbOcrProvider[]> {
|
||||
try {
|
||||
const providers = await this.listProviders()
|
||||
|
||||
// Filter providers that can handle the file type
|
||||
// This logic can be extended based on file type and provider capabilities
|
||||
const availableProviders: DbOcrProvider[] = []
|
||||
const capFilter = (provider: OcrProvider) => provider.capabilities.image
|
||||
|
||||
for (const provider of providers.filter(capFilter)) {
|
||||
if (await this._isProviderAvailable(provider)) {
|
||||
availableProviders.push(provider)
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`Found ${availableProviders.length} available providers for file`)
|
||||
return availableProviders
|
||||
} catch (error) {
|
||||
logger.error('Failed to get available providers for file', error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.registry.clear()
|
||||
logger.info('OCR service disposed')
|
||||
return handler(file, provider.config)
|
||||
}
|
||||
}
|
||||
|
||||
export const ocrService = new OcrService()
|
||||
|
||||
// Register built-in providers
|
||||
ocrService.register(BuiltinOcrProviderIds.tesseract, tesseractService.ocr.bind(tesseractService))
|
||||
|
||||
!isLinux && ocrService.register(BuiltinOcrProviderIds.system, systemOcrService.ocr.bind(systemOcrService))
|
||||
|
||||
ocrService.register(BuiltinOcrProviderIds.paddleocr, ppocrService.ocr.bind(ppocrService))
|
||||
|
||||
ovOcrService.isAvailable() && ocrService.register(BuiltinOcrProviderIds.ovocr, ovOcrService.ocr.bind(ovOcrService))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { isWin } from '@main/constant'
|
||||
import type { OcrOvConfig, OcrProviderConfig, OcrResult, SupportedOcrFile } from '@types'
|
||||
import { isImageFileMetadata, isOcrOvConfig } from '@types'
|
||||
import type { OcrOvConfig, OcrResult, SupportedOcrFile } from '@types'
|
||||
import { isImageFileMetadata } from '@types'
|
||||
import { exec } from 'child_process'
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
@@ -15,17 +15,20 @@ const execAsync = promisify(exec)
|
||||
|
||||
const PATH_BAT_FILE = path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr', 'run.npu.bat')
|
||||
|
||||
const isOvAvailable =
|
||||
isWin &&
|
||||
os.cpus()[0].model.toLowerCase().includes('intel') &&
|
||||
os.cpus()[0].model.toLowerCase().includes('ultra') &&
|
||||
fs.existsSync(PATH_BAT_FILE)
|
||||
|
||||
export class OvOcrService extends OcrBaseService {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
public isAvailable(): boolean {
|
||||
return (
|
||||
isWin &&
|
||||
os.cpus()[0].model.toLowerCase().includes('intel') &&
|
||||
os.cpus()[0].model.toLowerCase().includes('ultra') &&
|
||||
fs.existsSync(PATH_BAT_FILE)
|
||||
)
|
||||
}
|
||||
|
||||
private getOvOcrPath(): string {
|
||||
return path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr')
|
||||
}
|
||||
@@ -78,8 +81,8 @@ export class OvOcrService extends OcrBaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async ocrImage(filePath: string, config?: OcrOvConfig): Promise<OcrResult> {
|
||||
logger.info(`OV OCR called on ${filePath} with options ${JSON.stringify(config)}`)
|
||||
private async ocrImage(filePath: string, options?: OcrOvConfig): Promise<OcrResult> {
|
||||
logger.info(`OV OCR called on ${filePath} with options ${JSON.stringify(options)}`)
|
||||
|
||||
try {
|
||||
// 1. Clear img directory and output directory
|
||||
@@ -114,16 +117,13 @@ export class OvOcrService extends OcrBaseService {
|
||||
}
|
||||
}
|
||||
|
||||
public ocr = async (file: SupportedOcrFile, config?: OcrProviderConfig): Promise<OcrResult> => {
|
||||
if (!isOcrOvConfig(config)) {
|
||||
throw new Error('Invalid OCR OV config')
|
||||
}
|
||||
public ocr = async (file: SupportedOcrFile, options?: OcrOvConfig): Promise<OcrResult> => {
|
||||
if (isImageFileMetadata(file)) {
|
||||
return this.ocrImage(file.path, config)
|
||||
return this.ocrImage(file.path, options)
|
||||
} else {
|
||||
throw new Error('Unsupported file type, currently only image files are supported')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ovOcrService = isOvAvailable ? new OvOcrService() : undefined
|
||||
export const ovOcrService = new OvOcrService()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { loadOcrImage } from '@main/utils/ocr'
|
||||
import type { ImageFileMetadata, OcrPpocrConfig, OcrResult, SupportedOcrFile } from '@types'
|
||||
import { isImageFileMetadata, isOcrPpocrConfig } from '@types'
|
||||
import { isImageFileMetadata } from '@types'
|
||||
import { net } from 'electron'
|
||||
import * as z from 'zod'
|
||||
|
||||
@@ -40,17 +40,14 @@ const OcrResponseSchema = z.object({
|
||||
})
|
||||
|
||||
export class PpocrService extends OcrBaseService {
|
||||
public ocr = async (file: SupportedOcrFile, config?: OcrPpocrConfig): Promise<OcrResult> => {
|
||||
if (!isOcrPpocrConfig(config)) {
|
||||
throw new Error('Invalid OCR config')
|
||||
}
|
||||
public ocr = async (file: SupportedOcrFile, options?: OcrPpocrConfig): Promise<OcrResult> => {
|
||||
if (!isImageFileMetadata(file)) {
|
||||
throw new Error('Only image files are supported currently')
|
||||
}
|
||||
if (!config) {
|
||||
if (!options) {
|
||||
throw new Error('config is required')
|
||||
}
|
||||
return this.imageOcr(file, config)
|
||||
return this.imageOcr(file, options)
|
||||
}
|
||||
|
||||
private async imageOcr(file: ImageFileMetadata, options: OcrPpocrConfig): Promise<OcrResult> {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { isLinux, isWin } from '@main/constant'
|
||||
import { loadOcrImage } from '@main/utils/ocr'
|
||||
import { OcrAccuracy, recognize } from '@napi-rs/system-ocr'
|
||||
import type { ImageFileMetadata, OcrProviderConfig, OcrResult, OcrSystemConfig, SupportedOcrFile } from '@types'
|
||||
import { isImageFileMetadata, isOcrSystemConfig } from '@types'
|
||||
import type { ImageFileMetadata, OcrResult, OcrSystemConfig, SupportedOcrFile } from '@types'
|
||||
import { isImageFileMetadata } from '@types'
|
||||
|
||||
import { OcrBaseService } from './OcrBaseService'
|
||||
|
||||
@@ -12,26 +12,23 @@ export class SystemOcrService extends OcrBaseService {
|
||||
super()
|
||||
}
|
||||
|
||||
private async ocrImage(file: ImageFileMetadata, config?: OcrSystemConfig): Promise<OcrResult> {
|
||||
private async ocrImage(file: ImageFileMetadata, options?: OcrSystemConfig): Promise<OcrResult> {
|
||||
if (isLinux) {
|
||||
return { text: '' }
|
||||
}
|
||||
const buffer = await loadOcrImage(file)
|
||||
const langs = isWin ? config?.langs : undefined
|
||||
const langs = isWin ? options?.langs : undefined
|
||||
const result = await recognize(buffer, OcrAccuracy.Accurate, langs)
|
||||
return { text: result.text }
|
||||
}
|
||||
|
||||
public ocr = async (file: SupportedOcrFile, config?: OcrProviderConfig): Promise<OcrResult> => {
|
||||
if (!isOcrSystemConfig(config)) {
|
||||
throw new Error('Invalid OCR configuration')
|
||||
}
|
||||
public ocr = async (file: SupportedOcrFile, options?: OcrSystemConfig): Promise<OcrResult> => {
|
||||
if (isImageFileMetadata(file)) {
|
||||
return this.ocrImage(file, config)
|
||||
return this.ocrImage(file, options)
|
||||
} else {
|
||||
throw new Error('Unsupported file type, currently only image files are supported')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const systemOcrService = !isLinux ? new SystemOcrService() : undefined
|
||||
export const systemOcrService = new SystemOcrService()
|
||||
|
||||
@@ -2,8 +2,8 @@ import { loggerService } from '@logger'
|
||||
import { getIpCountry } from '@main/utils/ipService'
|
||||
import { loadOcrImage } from '@main/utils/ocr'
|
||||
import { MB } from '@shared/config/constant'
|
||||
import type { ImageFileMetadata, OcrProviderConfig, OcrResult, OcrTesseractConfig, SupportedOcrFile } from '@types'
|
||||
import { isImageFileMetadata, isOcrTesseractConfig } from '@types'
|
||||
import type { ImageFileMetadata, OcrResult, OcrTesseractConfig, SupportedOcrFile } from '@types'
|
||||
import { isImageFileMetadata } from '@types'
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
import { isEqual } from 'lodash'
|
||||
@@ -70,8 +70,8 @@ export class TesseractService extends OcrBaseService {
|
||||
return this.worker
|
||||
}
|
||||
|
||||
private async imageOcr(file: ImageFileMetadata, config?: OcrTesseractConfig): Promise<OcrResult> {
|
||||
const worker = await this.getWorker(config)
|
||||
private async imageOcr(file: ImageFileMetadata, options?: OcrTesseractConfig): Promise<OcrResult> {
|
||||
const worker = await this.getWorker(options)
|
||||
const stat = await fs.promises.stat(file.path)
|
||||
if (stat.size > MB_SIZE_THRESHOLD * MB) {
|
||||
throw new Error(`This image is too large (max ${MB_SIZE_THRESHOLD}MB)`)
|
||||
@@ -81,14 +81,11 @@ export class TesseractService extends OcrBaseService {
|
||||
return { text: result.data.text }
|
||||
}
|
||||
|
||||
public ocr = async (file: SupportedOcrFile, config?: OcrProviderConfig): Promise<OcrResult> => {
|
||||
if (!isOcrTesseractConfig(config)) {
|
||||
throw new Error('Invalid Tesseract config')
|
||||
}
|
||||
public ocr = async (file: SupportedOcrFile, options?: OcrTesseractConfig): Promise<OcrResult> => {
|
||||
if (!isImageFileMetadata(file)) {
|
||||
throw new Error('Only image files are supported currently')
|
||||
}
|
||||
return this.imageOcr(file, config)
|
||||
return this.imageOcr(file, options)
|
||||
}
|
||||
|
||||
private async _getLangPath(): Promise<string> {
|
||||
|
||||
@@ -9,13 +9,20 @@ const logger = loggerService.withContext('URLSchema:handleMcpProtocolUrl')
|
||||
|
||||
function installMCPServer(server: MCPServer) {
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
const now = Date.now()
|
||||
|
||||
if (!server.id) {
|
||||
server.id = nanoid()
|
||||
const payload: MCPServer = {
|
||||
...server,
|
||||
id: server.id ?? nanoid(),
|
||||
installSource: 'protocol',
|
||||
isTrusted: false,
|
||||
isActive: false,
|
||||
trustedAt: undefined,
|
||||
installedAt: server.installedAt ?? now
|
||||
}
|
||||
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send(IpcChannel.Mcp_AddServer, server)
|
||||
mainWindow.webContents.send(IpcChannel.Mcp_AddServer, payload)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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'
|
||||
@@ -12,7 +13,7 @@ import type {
|
||||
} from '@shared/data/preference/preferenceTypes'
|
||||
import type { UpgradeChannel } from '@shared/data/preference/preferenceTypes'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import type { Notification, OcrParams } from '@types'
|
||||
import type { Notification } from '@types'
|
||||
import type {
|
||||
AddMemoryOptions,
|
||||
AssistantMessage,
|
||||
@@ -27,6 +28,7 @@ import type {
|
||||
MemoryConfig,
|
||||
MemoryListOptions,
|
||||
MemorySearchOptions,
|
||||
OcrProvider,
|
||||
OcrResult,
|
||||
Provider,
|
||||
RestartApiServerStatusResult,
|
||||
@@ -41,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 }
|
||||
@@ -425,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),
|
||||
@@ -475,8 +496,9 @@ const api = {
|
||||
ipcRenderer.invoke(IpcChannel.CodeTools_RemoveCustomTerminalPath, terminalId)
|
||||
},
|
||||
ocr: {
|
||||
ocr: (file: SupportedOcrFile, params: OcrParams): Promise<OcrResult> =>
|
||||
ipcRenderer.invoke(IpcChannel.OCR_Ocr, file, params)
|
||||
ocr: (file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> =>
|
||||
ipcRenderer.invoke(IpcChannel.OCR_ocr, file, provider),
|
||||
listProviders: (): Promise<string[]> => ipcRenderer.invoke(IpcChannel.OCR_ListProviders)
|
||||
},
|
||||
cherryai: {
|
||||
generateSignature: (params: { method: string; path: string; query: string; body: Record<string, any> }) =>
|
||||
@@ -545,7 +567,38 @@ const api = {
|
||||
getStatus: (): Promise<GetApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_GetStatus),
|
||||
start: (): Promise<StartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Start),
|
||||
restart: (): Promise<RestartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Restart),
|
||||
stop: (): Promise<StopApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Stop)
|
||||
stop: (): Promise<StopApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Stop),
|
||||
onReady: (callback: () => void): (() => void) => {
|
||||
const listener = () => {
|
||||
callback()
|
||||
}
|
||||
ipcRenderer.on(IpcChannel.ApiServer_Ready, listener)
|
||||
return () => {
|
||||
ipcRenderer.removeListener(IpcChannel.ApiServer_Ready, listener)
|
||||
}
|
||||
}
|
||||
},
|
||||
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)
|
||||
},
|
||||
webSocket: {
|
||||
start: () => ipcRenderer.invoke(IpcChannel.WebSocket_Start),
|
||||
stop: () => ipcRenderer.invoke(IpcChannel.WebSocket_Stop),
|
||||
status: () => ipcRenderer.invoke(IpcChannel.WebSocket_Status),
|
||||
sendFile: (filePath: string) => ipcRenderer.invoke(IpcChannel.WebSocket_SendFile, filePath),
|
||||
getAllCandidates: () => ipcRenderer.invoke(IpcChannel.WebSocket_GetAllCandidates)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,12 +10,12 @@ import type { EndpointType, Model, Provider } from '@renderer/types'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@renderer/config/models', () => ({
|
||||
DEFAULT_MODEL_MAP: {
|
||||
assistant: { id: 'gpt-4', name: 'GPT-4' },
|
||||
quick: { id: 'gpt-4', name: 'GPT-4' },
|
||||
translate: { id: 'gpt-4', name: 'GPT-4' }
|
||||
},
|
||||
SYSTEM_MODELS: {
|
||||
defaultModel: [
|
||||
{ id: 'gpt-4', name: 'GPT-4' },
|
||||
{ id: 'gpt-4', name: 'GPT-4' },
|
||||
{ id: 'gpt-4', name: 'GPT-4' }
|
||||
],
|
||||
zhipu: [],
|
||||
silicon: [],
|
||||
openai: [],
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@import './color.css';
|
||||
@import './font.css';
|
||||
@import './markdown.css';
|
||||
@import './ant.css';
|
||||
@import './scrollbar.css';
|
||||
@import './container.css';
|
||||
@import './animation.css';
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
@import 'tailwindcss' source('../../../../renderer');
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@import '../../../../../packages/ui/src/styles/theme.css';
|
||||
|
||||
/* TODO heroui 迁移完成后即可删除 */
|
||||
/* heroui */
|
||||
@plugin '../../hero.ts';
|
||||
@source '../../../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||
@source '../../../../../packages/ui/src/**/*.{js,ts,jsx,tsx}';
|
||||
/* @plugin '../../hero.ts'; */
|
||||
@source '../../../../../packages/ui/src/components/**/*.{js,ts,jsx,tsx}';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@@ -21,117 +23,24 @@
|
||||
4. Put the new custom utility class into the utilities layer.
|
||||
*/
|
||||
|
||||
/* 应用特定的原始变量 */
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||
--icon: #00000099;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
--icon: #ffffff99;
|
||||
}
|
||||
|
||||
/* 应用特定的变量和动画(不与 UI 库冲突) */
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
/* Icon 颜色 - 应用特定变量 */
|
||||
--color-icon: var(--icon);
|
||||
|
||||
/* 跑马灯动画 - 应用特定 */
|
||||
--animate-marquee: marquee var(--duration) infinite linear;
|
||||
--animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;
|
||||
--color-icon: var(--icon);
|
||||
|
||||
@keyframes marquee {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface EditableNumberProps {
|
||||
suffix?: string
|
||||
prefix?: string
|
||||
align?: 'start' | 'center' | 'end'
|
||||
formatter?: (value: number | null) => string | number
|
||||
}
|
||||
|
||||
const EditableNumber: FC<EditableNumberProps> = ({
|
||||
@@ -36,7 +37,8 @@ const EditableNumber: FC<EditableNumberProps> = ({
|
||||
style,
|
||||
className,
|
||||
size = 'middle',
|
||||
align = 'end'
|
||||
align = 'end',
|
||||
formatter
|
||||
}) => {
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [inputValue, setInputValue] = useState(value)
|
||||
@@ -90,7 +92,7 @@ const EditableNumber: FC<EditableNumberProps> = ({
|
||||
changeOnBlur={changeOnBlur}
|
||||
/>
|
||||
<DisplayText style={style} className={className} $align={align} $isEditing={isEditing}>
|
||||
{value ?? placeholder}
|
||||
{formatter ? formatter(value ?? null) : (value ?? placeholder)}
|
||||
</DisplayText>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { Button } from '@cherrystudio/ui'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { Alert, Space } from 'antd'
|
||||
import type { ComponentType, ReactNode } from 'react'
|
||||
|
||||
@@ -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}
|
||||
|
||||
119
src/renderer/src/components/Popups/AddAssistantOrAgentPopup.tsx
Normal file
119
src/renderer/src/components/Popups/AddAssistantOrAgentPopup.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { Modal } from 'antd'
|
||||
import { Bot, MessageSquare } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type OptionType = 'assistant' | 'agent'
|
||||
|
||||
interface ShowParams {
|
||||
onSelect: (type: OptionType) => void
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: { type?: OptionType }) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ onSelect, resolve }) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(true)
|
||||
const [hoveredOption, setHoveredOption] = useState<OptionType | null>(null)
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
const handleSelect = (type: OptionType) => {
|
||||
setOpen(false)
|
||||
onSelect(type)
|
||||
resolve({ type })
|
||||
}
|
||||
|
||||
AddAssistantOrAgentPopup.hide = onCancel
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('chat.add.option.title')}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
transitionName="animation-move-down"
|
||||
centered
|
||||
footer={null}
|
||||
width={560}>
|
||||
<div className="grid grid-cols-2 gap-4 py-4">
|
||||
{/* Assistant Option */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect('assistant')}
|
||||
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
onMouseEnter={() => setHoveredOption('assistant')}
|
||||
onMouseLeave={() => setHoveredOption(null)}>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
|
||||
<MessageSquare
|
||||
size={24}
|
||||
className={cn(
|
||||
'transition-colors',
|
||||
hoveredOption === 'assistant' ? 'text-[var(--color-primary)]' : 'text-[var(--color-icon-white)]'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="mb-1 font-semibold text-[var(--color-text-1)] text-base">{t('chat.add.assistant.title')}</h3>
|
||||
<p className="text-[var(--color-text-2)] text-sm">{t('chat.add.assistant.description')}</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Agent Option */}
|
||||
<button
|
||||
onClick={() => handleSelect('agent')}
|
||||
type="button"
|
||||
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
onMouseEnter={() => setHoveredOption('agent')}
|
||||
onMouseLeave={() => setHoveredOption(null)}>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
|
||||
<Bot
|
||||
size={24}
|
||||
className={cn(
|
||||
'transition-colors',
|
||||
hoveredOption === 'agent' ? 'text-[var(--color-primary)]' : 'text-[var(--color-icon-white)]'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="mb-1 font-semibold text-[var(--color-text-1)] text-base">{t('agent.add.title')}</h3>
|
||||
<p className="text-[var(--color-text-2)] text-sm">{t('agent.add.description')}</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'AddAssistantOrAgentPopup'
|
||||
|
||||
export default class AddAssistantOrAgentPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<{ type?: OptionType }>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
591
src/renderer/src/components/Popups/ExportToPhoneLanPopup.tsx
Normal file
591
src/renderer/src/components/Popups/ExportToPhoneLanPopup.tsx
Normal file
@@ -0,0 +1,591 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@heroui/modal'
|
||||
import { Progress } from '@heroui/progress'
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { loggerService } from '@logger'
|
||||
import { SettingHelpText, SettingRow } from '@renderer/pages/settings'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { TopView } from '../TopView'
|
||||
|
||||
const logger = loggerService.withContext('ExportToPhoneLanPopup')
|
||||
|
||||
interface Props {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
type ConnectionPhase = 'initializing' | 'waiting_qr_scan' | 'connecting' | 'connected' | 'disconnected' | 'error'
|
||||
type TransferPhase = 'idle' | 'preparing' | 'sending' | 'completed' | 'error'
|
||||
|
||||
const LoadingQRCode: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||||
<Spinner />
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text-2)' }}>
|
||||
{t('settings.data.export_to_phone.lan.generating_qr')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ScanQRCode: React.FC<{ qrCodeValue: string }> = ({ qrCodeValue }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||||
<QRCodeSVG
|
||||
marginSize={2}
|
||||
value={qrCodeValue}
|
||||
level="Q"
|
||||
size={160}
|
||||
imageSettings={{
|
||||
src: '/src/assets/images/logo.png',
|
||||
width: 40,
|
||||
height: 40,
|
||||
excavate: true
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: '12px', color: 'var(--color-text-2)' }}>
|
||||
{t('settings.data.export_to_phone.lan.scan_qr')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ConnectingAnimation: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '160px',
|
||||
height: '160px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '2px dashed var(--color-status-warning)',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: 'var(--color-status-warning)'
|
||||
}}>
|
||||
<Spinner size="lg" color="warning" />
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)', marginTop: '12px' }}>
|
||||
{t('settings.data.export_to_phone.lan.status.connecting')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ConnectedDisplay: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '160px',
|
||||
height: '160px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '2px dashed var(--color-status-success)',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: 'var(--color-status-success)'
|
||||
}}>
|
||||
<span style={{ fontSize: '48px' }}>📱</span>
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)', marginTop: '8px' }}>
|
||||
{t('settings.data.export_to_phone.lan.connected')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ErrorQRCode: React.FC<{ error: string | null }> = ({ error }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
padding: '20px',
|
||||
border: `1px solid var(--color-error)`,
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--color-error)'
|
||||
}}>
|
||||
<span style={{ fontSize: '48px' }}>⚠️</span>
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)' }}>
|
||||
{t('settings.data.export_to_phone.lan.connection_failed')}
|
||||
</span>
|
||||
{error && <span style={{ fontSize: '12px', color: 'var(--color-text-2)' }}>{error}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
const [connectionPhase, setConnectionPhase] = useState<ConnectionPhase>('initializing')
|
||||
const [transferPhase, setTransferPhase] = useState<TransferPhase>('idle')
|
||||
const [qrCodeValue, setQrCodeValue] = useState('')
|
||||
const [selectedFolderPath, setSelectedFolderPath] = useState<string | null>(null)
|
||||
const [sendProgress, setSendProgress] = useState(0)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showCloseConfirm, setShowCloseConfirm] = useState(false)
|
||||
const [autoCloseCountdown, setAutoCloseCountdown] = useState<number | null>(null)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 派生状态
|
||||
const isConnected = connectionPhase === 'connected'
|
||||
const canSend = isConnected && selectedFolderPath && transferPhase === 'idle'
|
||||
const isSending = transferPhase === 'preparing' || transferPhase === 'sending'
|
||||
|
||||
// 状态文本映射
|
||||
const connectionStatusText = useMemo(() => {
|
||||
const statusMap = {
|
||||
initializing: t('settings.data.export_to_phone.lan.status.initializing'),
|
||||
waiting_qr_scan: t('settings.data.export_to_phone.lan.status.waiting_qr_scan'),
|
||||
connecting: t('settings.data.export_to_phone.lan.status.connecting'),
|
||||
connected: t('settings.data.export_to_phone.lan.status.connected'),
|
||||
disconnected: t('settings.data.export_to_phone.lan.status.disconnected'),
|
||||
error: t('settings.data.export_to_phone.lan.status.error')
|
||||
}
|
||||
return statusMap[connectionPhase]
|
||||
}, [connectionPhase, t])
|
||||
|
||||
const transferStatusText = useMemo(() => {
|
||||
const statusMap = {
|
||||
idle: '',
|
||||
preparing: t('settings.data.export_to_phone.lan.status.preparing'),
|
||||
sending: t('settings.data.export_to_phone.lan.status.sending'),
|
||||
completed: t('settings.data.export_to_phone.lan.status.completed'),
|
||||
error: t('settings.data.export_to_phone.lan.status.error')
|
||||
}
|
||||
return statusMap[transferPhase]
|
||||
}, [transferPhase, t])
|
||||
|
||||
// 状态样式映射
|
||||
const connectionStatusStyles = useMemo(() => {
|
||||
const styleMap = {
|
||||
initializing: {
|
||||
bg: 'var(--color-background-mute)',
|
||||
border: 'var(--color-border-mute)'
|
||||
},
|
||||
waiting_qr_scan: {
|
||||
bg: 'var(--color-primary-mute)',
|
||||
border: 'var(--color-primary-soft)'
|
||||
},
|
||||
connecting: { bg: 'var(--color-status-warning)', border: 'var(--color-status-warning)' },
|
||||
connected: {
|
||||
bg: 'var(--color-status-success)',
|
||||
border: 'var(--color-status-success)'
|
||||
},
|
||||
disconnected: { bg: 'var(--color-error)', border: 'var(--color-error)' },
|
||||
error: { bg: 'var(--color-error)', border: 'var(--color-error)' }
|
||||
}
|
||||
return styleMap[connectionPhase]
|
||||
}, [connectionPhase])
|
||||
|
||||
const initWebSocket = useCallback(async () => {
|
||||
try {
|
||||
setConnectionPhase('initializing')
|
||||
await window.api.webSocket.start()
|
||||
const { port, ip } = await window.api.webSocket.status()
|
||||
|
||||
if (ip && port) {
|
||||
const candidates = await window.api.webSocket.getAllCandidates()
|
||||
const connectionInfo = {
|
||||
type: 'cherry-studio-app',
|
||||
candidates,
|
||||
selectedHost: ip,
|
||||
port,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
setQrCodeValue(JSON.stringify(connectionInfo))
|
||||
setConnectionPhase('waiting_qr_scan')
|
||||
logger.info(`QR code generated: ${ip}:${port} with ${candidates.length} IP candidates`)
|
||||
} else {
|
||||
setError(t('settings.data.export_to_phone.lan.error.no_ip'))
|
||||
setConnectionPhase('error')
|
||||
}
|
||||
} catch (error) {
|
||||
setError(
|
||||
`${t('settings.data.export_to_phone.lan.error.init_failed')}: ${error instanceof Error ? error.message : ''}`
|
||||
)
|
||||
setConnectionPhase('error')
|
||||
logger.error('Failed to initialize WebSocket:', error as Error)
|
||||
}
|
||||
}, [t])
|
||||
|
||||
const handleClientConnected = useCallback((_event: any, data: { connected: boolean }) => {
|
||||
logger.info(`Client connection status: ${data.connected ? 'connected' : 'disconnected'}`)
|
||||
if (data.connected) {
|
||||
setConnectionPhase('connected')
|
||||
setError(null)
|
||||
} else {
|
||||
setConnectionPhase('disconnected')
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleMessageReceived = useCallback((_event: any, data: any) => {
|
||||
logger.info(`Received message from mobile: ${JSON.stringify(data)}`)
|
||||
}, [])
|
||||
|
||||
const handleSendProgress = useCallback(
|
||||
(_event: any, data: { progress: number }) => {
|
||||
const progress = data.progress
|
||||
setSendProgress(progress)
|
||||
|
||||
if (transferPhase === 'preparing' && progress > 0) {
|
||||
setTransferPhase('sending')
|
||||
}
|
||||
|
||||
if (progress >= 100) {
|
||||
setTransferPhase('completed')
|
||||
// 启动 3 秒倒计时自动关闭
|
||||
setAutoCloseCountdown(3)
|
||||
}
|
||||
},
|
||||
[transferPhase]
|
||||
)
|
||||
|
||||
const handleSelectZip = useCallback(async () => {
|
||||
const result = await window.api.file.select()
|
||||
if (result) {
|
||||
setSelectedFolderPath(result[0].path)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSendZip = useCallback(async () => {
|
||||
if (!selectedFolderPath) {
|
||||
setError(t('settings.data.export_to_phone.lan.error.no_file'))
|
||||
return
|
||||
}
|
||||
|
||||
setTransferPhase('preparing')
|
||||
setError(null)
|
||||
setSendProgress(0)
|
||||
|
||||
try {
|
||||
logger.info(`Starting file transfer: ${selectedFolderPath}`)
|
||||
await window.api.webSocket.sendFile(selectedFolderPath)
|
||||
} catch (error) {
|
||||
setError(
|
||||
`${t('settings.data.export_to_phone.lan.error.send_failed')}: ${error instanceof Error ? error.message : ''}`
|
||||
)
|
||||
setTransferPhase('error')
|
||||
logger.error('Failed to send file:', error as Error)
|
||||
}
|
||||
}, [selectedFolderPath, t])
|
||||
|
||||
// 尝试关闭弹窗 - 如果正在传输则显示确认
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isSending) {
|
||||
setShowCloseConfirm(true)
|
||||
} else {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}, [isSending])
|
||||
|
||||
// 确认强制关闭
|
||||
const handleForceClose = useCallback(() => {
|
||||
logger.info('Force closing popup during transfer')
|
||||
setIsOpen(false)
|
||||
}, [])
|
||||
|
||||
// 取消关闭确认
|
||||
const handleCancelClose = useCallback(() => {
|
||||
setShowCloseConfirm(false)
|
||||
}, [])
|
||||
|
||||
// 清理并关闭
|
||||
const handleClose = useCallback(async () => {
|
||||
try {
|
||||
// 主动断开 WebSocket 连接
|
||||
if (isConnected || connectionPhase !== 'disconnected') {
|
||||
logger.info('Closing popup, stopping WebSocket')
|
||||
await window.api.webSocket.stop()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to stop WebSocket on close:', error as Error)
|
||||
}
|
||||
resolve({})
|
||||
}, [resolve, isConnected, connectionPhase])
|
||||
|
||||
useEffect(() => {
|
||||
initWebSocket()
|
||||
|
||||
const removeClientConnectedListener = window.electron.ipcRenderer.on(
|
||||
'websocket-client-connected',
|
||||
handleClientConnected
|
||||
)
|
||||
const removeMessageReceivedListener = window.electron.ipcRenderer.on(
|
||||
'websocket-message-received',
|
||||
handleMessageReceived
|
||||
)
|
||||
const removeSendProgressListener = window.electron.ipcRenderer.on('file-send-progress', handleSendProgress)
|
||||
|
||||
return () => {
|
||||
removeClientConnectedListener()
|
||||
removeMessageReceivedListener()
|
||||
removeSendProgressListener()
|
||||
window.api.webSocket.stop()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// 自动关闭倒计时
|
||||
useEffect(() => {
|
||||
if (autoCloseCountdown === null) return
|
||||
|
||||
if (autoCloseCountdown <= 0) {
|
||||
logger.debug('Auto-closing popup after transfer completion')
|
||||
setIsOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setAutoCloseCountdown(autoCloseCountdown - 1)
|
||||
}, 1000)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [autoCloseCountdown])
|
||||
|
||||
// 状态指示器组件
|
||||
const StatusIndicator = useCallback(
|
||||
() => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: connectionStatusStyles.bg,
|
||||
border: `1px solid ${connectionStatusStyles.border}`
|
||||
}}>
|
||||
<span style={{ fontSize: '14px', fontWeight: '500', color: 'var(--color-text)' }}>{connectionStatusText}</span>
|
||||
</div>
|
||||
),
|
||||
[connectionStatusStyles, connectionStatusText]
|
||||
)
|
||||
|
||||
// 二维码显示组件 - 使用显式条件渲染以避免类型不匹配
|
||||
const QRCodeDisplay = useCallback(() => {
|
||||
switch (connectionPhase) {
|
||||
case 'waiting_qr_scan':
|
||||
case 'disconnected':
|
||||
return <ScanQRCode qrCodeValue={qrCodeValue} />
|
||||
case 'initializing':
|
||||
return <LoadingQRCode />
|
||||
case 'connecting':
|
||||
return <ConnectingAnimation />
|
||||
case 'connected':
|
||||
return <ConnectedDisplay />
|
||||
case 'error':
|
||||
return <ErrorQRCode error={error} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}, [connectionPhase, qrCodeValue, error])
|
||||
|
||||
// 传输进度组件
|
||||
const TransferProgress = useCallback(() => {
|
||||
if (!isSending && transferPhase !== 'completed') return null
|
||||
|
||||
return (
|
||||
<div style={{ paddingTop: '8px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
padding: '12px',
|
||||
border: `1px solid var(--color-border)`,
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--color-background-mute)'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
<span style={{ color: 'var(--color-text)' }}>
|
||||
{t('settings.data.export_to_phone.lan.transfer_progress')}
|
||||
</span>
|
||||
<span
|
||||
style={{ color: transferPhase === 'completed' ? 'var(--color-status-success)' : 'var(--color-primary)' }}>
|
||||
{transferPhase === 'completed' ? '✅ ' + t('common.completed') : `${Math.round(sendProgress)}%`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
value={Math.round(sendProgress)}
|
||||
size="md"
|
||||
color={transferPhase === 'completed' ? 'success' : 'primary'}
|
||||
showValueLabel={false}
|
||||
aria-label="Send progress"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, [isSending, transferPhase, sendProgress, t])
|
||||
|
||||
const AutoCloseCountdown = useCallback(() => {
|
||||
if (transferPhase !== 'completed' || autoCloseCountdown === null || autoCloseCountdown <= 0) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-text-2)',
|
||||
textAlign: 'center',
|
||||
paddingTop: '4px'
|
||||
}}>
|
||||
{t('settings.data.export_to_phone.lan.auto_close_tip', { seconds: autoCloseCountdown })}
|
||||
</div>
|
||||
)
|
||||
}, [transferPhase, autoCloseCountdown, t])
|
||||
|
||||
// 错误显示组件
|
||||
const ErrorDisplay = useCallback(() => {
|
||||
if (!error || transferPhase !== 'error') return null
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '12px',
|
||||
border: `1px solid var(--color-error)`,
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--color-error)',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)' }}>❌ {error}</span>
|
||||
</div>
|
||||
)
|
||||
}, [error, transferPhase])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
handleCancel()
|
||||
}
|
||||
}}
|
||||
isDismissable={false}
|
||||
isKeyboardDismissDisabled={false}
|
||||
placement="center"
|
||||
onClose={handleClose}>
|
||||
<ModalContent>
|
||||
{() => (
|
||||
<>
|
||||
<ModalHeader>{t('settings.data.export_to_phone.lan.title')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<SettingRow>
|
||||
<StatusIndicator />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow>
|
||||
<div>{t('settings.data.export_to_phone.lan.content')}</div>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow style={{ display: 'flex', justifyContent: 'center', minHeight: '180px' }}>
|
||||
<QRCodeDisplay />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'center', width: '100%' }}>
|
||||
<Button color="default" variant="flat" onPress={handleSelectZip} isDisabled={isSending}>
|
||||
{t('settings.data.export_to_phone.lan.selectZip')}
|
||||
</Button>
|
||||
<Button color="primary" onPress={handleSendZip} isDisabled={!canSend} isLoading={isSending}>
|
||||
{transferStatusText || t('settings.data.export_to_phone.lan.sendZip')}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
|
||||
<SettingHelpText
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{selectedFolderPath || t('settings.data.export_to_phone.lan.noZipSelected')}
|
||||
</SettingHelpText>
|
||||
|
||||
<TransferProgress />
|
||||
<AutoCloseCountdown />
|
||||
<ErrorDisplay />
|
||||
</ModalBody>
|
||||
|
||||
{showCloseConfirm && (
|
||||
<ModalFooter>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: '12px',
|
||||
padding: '8px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--color-status-warning)',
|
||||
border: '1px solid var(--color-status-warning)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '20px' }}>⚠️</span>
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: '500' }}>
|
||||
{t('settings.data.export_to_phone.lan.confirm_close_title')}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ fontSize: '13px', color: 'var(--color-text-2)', marginLeft: '28px' }}>
|
||||
{t('settings.data.export_to_phone.lan.confirm_close_message')}
|
||||
</span>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '4px' }}>
|
||||
<Button size="sm" color="default" variant="flat" onPress={handleCancelClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button size="sm" color="danger" onPress={handleForceClose}>
|
||||
{t('settings.data.export_to_phone.lan.force_close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'ExportToPhoneLanPopup'
|
||||
|
||||
export default class ExportToPhoneLanPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show() {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
import { loggerService } from '@logger'
|
||||
import type { Selection } from '@react-types/shared'
|
||||
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
|
||||
import { permissionModeCards } from '@renderer/config/agent'
|
||||
import { agentModelFilter, getModelLogoById } from '@renderer/config/models'
|
||||
import { permissionModeCards } from '@renderer/constants/permissionModes'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
@@ -68,6 +68,7 @@ type Props = {
|
||||
agent?: AgentWithTools
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
afterSubmit?: (a: AgentEntity) => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,7 +80,7 @@ type Props = {
|
||||
* @param onClose - Optional callback when modal closes. From useDisclosure.
|
||||
* @returns Modal component for agent creation/editing
|
||||
*/
|
||||
export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _onClose }) => {
|
||||
export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _onClose, afterSubmit }) => {
|
||||
const { isOpen, onClose } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
|
||||
const { t } = useTranslation()
|
||||
const loadingRef = useRef(false)
|
||||
@@ -302,8 +303,13 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
configuration: form.configuration ? { ...form.configuration } : undefined
|
||||
} satisfies UpdateAgentForm
|
||||
|
||||
updateAgent(updatePayload)
|
||||
logger.debug('Updated agent', updatePayload)
|
||||
const result = await updateAgent(updatePayload)
|
||||
if (result) {
|
||||
logger.debug('Updated agent', result)
|
||||
afterSubmit?.(result)
|
||||
} else {
|
||||
logger.error('Update failed.')
|
||||
}
|
||||
} else {
|
||||
const newAgent = {
|
||||
type: form.type,
|
||||
@@ -316,12 +322,13 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
configuration: form.configuration ? { ...form.configuration } : undefined
|
||||
} satisfies AddAgentForm
|
||||
const result = await addAgent(newAgent)
|
||||
|
||||
if (!result.success) {
|
||||
loadingRef.current = false
|
||||
throw result.error
|
||||
}
|
||||
afterSubmit?.(result.data)
|
||||
}
|
||||
|
||||
loadingRef.current = false
|
||||
|
||||
// setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
@@ -330,16 +337,17 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
[
|
||||
form.type,
|
||||
form.model,
|
||||
form.accessible_paths,
|
||||
form.name,
|
||||
form.description,
|
||||
form.instructions,
|
||||
form.accessible_paths,
|
||||
form.allowed_tools,
|
||||
form.configuration,
|
||||
agent,
|
||||
onClose,
|
||||
t,
|
||||
updateAgent,
|
||||
afterSubmit,
|
||||
addAgent
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user