Compare commits
28 Commits
refactor/o
...
feat/termi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c758d1400 | ||
|
|
a4b19f7650 | ||
|
|
97b3608786 | ||
|
|
793625d009 | ||
|
|
00ee90a857 | ||
|
|
1200f9973c | ||
|
|
4f107e9305 | ||
|
|
eaf4bd18a3 | ||
|
|
cf008ca22e | ||
|
|
851ff8992f | ||
|
|
91f9088436 | ||
|
|
c971daf23c | ||
|
|
0c7cee2700 | ||
|
|
dfbfc2869c | ||
|
|
1575e97168 | ||
|
|
e0a2ed0481 | ||
|
|
3e9d9f16d6 | ||
|
|
f3a279d8de | ||
|
|
5790c12011 | ||
|
|
352ecbc506 | ||
|
|
fc4f30feab | ||
|
|
888a183328 | ||
|
|
9a01e092f6 | ||
|
|
5986800c9d | ||
|
|
56d68276e1 | ||
|
|
29c1173365 | ||
|
|
c7ceb3035d | ||
|
|
7bcae6fba2 |
6
.github/workflows/issue-management.yml
vendored
6
.github/workflows/issue-management.yml
vendored
@@ -29,8 +29,10 @@ jobs:
|
||||
days-before-close: 0 # Close immediately after stale
|
||||
stale-issue-label: 'inactive'
|
||||
close-issue-label: 'closed:no-response'
|
||||
exempt-all-milestones: true
|
||||
exempt-all-assignees: true
|
||||
stale-issue-message: |
|
||||
This issue has been labeled as needing more information and has been inactive for ${{ env.daysBeforeStale }} days.
|
||||
This issue has been labeled as needing more information and has been inactive for ${{ env.daysBeforeStale }} days.
|
||||
It will be closed now due to lack of additional information.
|
||||
|
||||
该问题被标记为"需要更多信息"且已经 ${{ env.daysBeforeStale }} 天没有任何活动,将立即关闭。
|
||||
@@ -46,6 +48,8 @@ jobs:
|
||||
days-before-stale: ${{ env.daysBeforeStale }}
|
||||
days-before-close: ${{ env.daysBeforeClose }}
|
||||
stale-issue-label: 'inactive'
|
||||
exempt-all-milestones: true
|
||||
exempt-all-assignees: true
|
||||
stale-issue-message: |
|
||||
This issue has been inactive for a prolonged period and will be closed automatically in ${{ env.daysBeforeClose }} days.
|
||||
该问题已长时间处于闲置状态,${{ env.daysBeforeClose }} 天后将自动关闭。
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index 69ab1599c76801dc1167551b6fa283dded123466..f0af43bba7ad1196fe05338817e65b4ebda40955 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||
|
||||
// src/get-model-path.ts
|
||||
function getModelPath(modelId) {
|
||||
- return modelId.includes("/") ? modelId : `models/${modelId}`;
|
||||
+ return modelId?.includes("models/") ? modelId : `models/${modelId}`;
|
||||
}
|
||||
|
||||
// src/google-generative-ai-options.ts
|
||||
26
.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch
vendored
Normal file
26
.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
diff --git a/dist/index.js b/dist/index.js
|
||||
index 4cc66d83af1cef39f6447dc62e680251e05ddf9f..eb9819cb674c1808845ceb29936196c4bb355172 100644
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -471,7 +471,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||
|
||||
// src/get-model-path.ts
|
||||
function getModelPath(modelId) {
|
||||
- return modelId.includes("/") ? modelId : `models/${modelId}`;
|
||||
+ return modelId.includes("models/") ? modelId : `models/${modelId}`;
|
||||
}
|
||||
|
||||
// src/google-generative-ai-options.ts
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index a032505ec54e132dc386dde001dc51f710f84c83..5efada51b9a8b56e3f01b35e734908ebe3c37043 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||
|
||||
// src/get-model-path.ts
|
||||
function getModelPath(modelId) {
|
||||
- return modelId.includes("/") ? modelId : `models/${modelId}`;
|
||||
+ return modelId.includes("models/") ? modelId : `models/${modelId}`;
|
||||
}
|
||||
|
||||
// src/google-generative-ai-options.ts
|
||||
76
.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch
vendored
Normal file
76
.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
diff --git a/dist/index.js b/dist/index.js
|
||||
index cc6652c4e7f32878a64a2614115bf7eeb3b7c890..76e989017549c89b45d633525efb1f318026d9b2 100644
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -274,6 +274,7 @@ var openaiChatResponseSchema = (0, import_provider_utils3.lazyValidator)(
|
||||
message: import_v42.z.object({
|
||||
role: import_v42.z.literal("assistant").nullish(),
|
||||
content: import_v42.z.string().nullish(),
|
||||
+ reasoning_content: import_v42.z.string().nullish(),
|
||||
tool_calls: import_v42.z.array(
|
||||
import_v42.z.object({
|
||||
id: import_v42.z.string().nullish(),
|
||||
@@ -340,6 +341,7 @@ var openaiChatChunkSchema = (0, import_provider_utils3.lazyValidator)(
|
||||
delta: import_v42.z.object({
|
||||
role: import_v42.z.enum(["assistant"]).nullish(),
|
||||
content: import_v42.z.string().nullish(),
|
||||
+ reasoning_content: import_v42.z.string().nullish(),
|
||||
tool_calls: import_v42.z.array(
|
||||
import_v42.z.object({
|
||||
index: import_v42.z.number(),
|
||||
@@ -785,6 +787,14 @@ var OpenAIChatLanguageModel = class {
|
||||
if (text != null && text.length > 0) {
|
||||
content.push({ type: "text", text });
|
||||
}
|
||||
+ const reasoning =
|
||||
+ choice.message.reasoning_content;
|
||||
+ if (reasoning != null && reasoning.length > 0) {
|
||||
+ content.push({
|
||||
+ type: 'reasoning',
|
||||
+ text: reasoning,
|
||||
+ });
|
||||
+ }
|
||||
for (const toolCall of (_a = choice.message.tool_calls) != null ? _a : []) {
|
||||
content.push({
|
||||
type: "tool-call",
|
||||
@@ -866,6 +876,7 @@ var OpenAIChatLanguageModel = class {
|
||||
};
|
||||
let isFirstChunk = true;
|
||||
let isActiveText = false;
|
||||
+ let isActiveReasoning = false;
|
||||
const providerMetadata = { openai: {} };
|
||||
return {
|
||||
stream: response.pipeThrough(
|
||||
@@ -920,6 +931,22 @@ var OpenAIChatLanguageModel = class {
|
||||
return;
|
||||
}
|
||||
const delta = choice.delta;
|
||||
+ const reasoningContent = delta.reasoning_content;
|
||||
+ if (reasoningContent) {
|
||||
+ if (!isActiveReasoning) {
|
||||
+ controller.enqueue({
|
||||
+ type: 'reasoning-start',
|
||||
+ id: 'reasoning-0',
|
||||
+ });
|
||||
+ isActiveReasoning = true;
|
||||
+ }
|
||||
+
|
||||
+ controller.enqueue({
|
||||
+ type: 'reasoning-delta',
|
||||
+ id: 'reasoning-0',
|
||||
+ delta: reasoningContent,
|
||||
+ });
|
||||
+ }
|
||||
if (delta.content != null) {
|
||||
if (!isActiveText) {
|
||||
controller.enqueue({ type: "text-start", id: "0" });
|
||||
@@ -1032,6 +1059,9 @@ var OpenAIChatLanguageModel = class {
|
||||
}
|
||||
},
|
||||
flush(controller) {
|
||||
+ if (isActiveReasoning) {
|
||||
+ controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' });
|
||||
+ }
|
||||
if (isActiveText) {
|
||||
controller.enqueue({ type: "text-end", id: "0" });
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
diff --git a/sdk.mjs b/sdk.mjs
|
||||
index 461e9a2ba246778261108a682762ffcf26f7224e..44bd667d9f591969d36a105ba5eb8b478c738dd8 100644
|
||||
index 10162e5b1624f8ce667768943347a6e41089ad2f..32568ae08946590e382270c88d85fba81187568e 100755
|
||||
--- a/sdk.mjs
|
||||
+++ b/sdk.mjs
|
||||
@@ -6215,7 +6215,7 @@ function createAbortController(maxListeners = DEFAULT_MAX_LISTENERS) {
|
||||
@@ -6213,7 +6213,7 @@ function createAbortController(maxListeners = DEFAULT_MAX_LISTENERS) {
|
||||
}
|
||||
|
||||
|
||||
// ../src/transport/ProcessTransport.ts
|
||||
-import { spawn } from "child_process";
|
||||
+import { fork } from "child_process";
|
||||
import { createInterface } from "readline";
|
||||
|
||||
|
||||
// ../src/utils/fsOperations.ts
|
||||
@@ -6473,14 +6473,11 @@ class ProcessTransport {
|
||||
@@ -6487,14 +6487,11 @@ class ProcessTransport {
|
||||
const errorMessage = isNativeBinary(pathToClaudeCodeExecutable) ? `Claude Code native binary not found at ${pathToClaudeCodeExecutable}. Please ensure Claude Code is installed via native installer or specify a valid path with options.pathToClaudeCodeExecutable.` : `Claude Code executable not found at ${pathToClaudeCodeExecutable}. Is options.pathToClaudeCodeExecutable set?`;
|
||||
throw new ReferenceError(errorMessage);
|
||||
}
|
||||
- const isNative = isNativeBinary(pathToClaudeCodeExecutable);
|
||||
- const spawnCommand = isNative ? pathToClaudeCodeExecutable : executable;
|
||||
- const spawnArgs = isNative ? args : [...executableArgs, pathToClaudeCodeExecutable, ...args];
|
||||
- this.logForDebugging(isNative ? `Spawning Claude Code native binary: ${pathToClaudeCodeExecutable} ${args.join(" ")}` : `Spawning Claude Code process: ${executable} ${[...executableArgs, pathToClaudeCodeExecutable, ...args].join(" ")}`);
|
||||
- const spawnArgs = isNative ? [...executableArgs, ...args] : [...executableArgs, pathToClaudeCodeExecutable, ...args];
|
||||
- this.logForDebugging(isNative ? `Spawning Claude Code native binary: ${spawnCommand} ${spawnArgs.join(" ")}` : `Spawning Claude Code process: ${spawnCommand} ${spawnArgs.join(" ")}`);
|
||||
+ this.logForDebugging(`Forking Claude Code Node.js process: ${pathToClaudeCodeExecutable} ${args.join(" ")}`);
|
||||
const stderrMode = env.DEBUG || stderr ? "pipe" : "ignore";
|
||||
- this.child = spawn(spawnCommand, spawnArgs, {
|
||||
@@ -10,8 +10,9 @@ This file provides guidance to AI coding assistants when working with code in th
|
||||
- **Build with HeroUI**: Use HeroUI for every new UI component; never add `antd` or `styled-components`.
|
||||
- **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`.
|
||||
- **Research via subagent**: Lean on `subagent` for external docs, APIs, news, and references.
|
||||
- **Seek review**: Ask a human developer to review substantial changes before merging.
|
||||
- **Commit in rhythm**: Keep commits small, conventional, and emoji-tagged.
|
||||
- **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications.
|
||||
- **Write conventional commits with emoji**: Commit small, focused changes using emoji-prefixed Conventional Commit messages (e.g., `✨ feat:`, `🐛 fix:`, `♻️ refactor:`, `
|
||||
📝 docs:`).
|
||||
|
||||
## Development Commands
|
||||
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
"quoteStyle": "single"
|
||||
}
|
||||
},
|
||||
"files": { "ignoreUnknown": false },
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**"],
|
||||
"maxSize": 2097152
|
||||
},
|
||||
"formatter": {
|
||||
"attributePosition": "auto",
|
||||
"bracketSameLine": false,
|
||||
|
||||
@@ -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 工作流
|
||||
- **性能测试**: 资源使用和时间
|
||||
- **错误场景**: 全面的错误处理测试
|
||||
1093
docs/terminal-implementation.md
Normal file
1093
docs/terminal-implementation.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,10 @@ asarUnpack:
|
||||
extraResources:
|
||||
- from: "migrations/sqlite-drizzle"
|
||||
to: "migrations/sqlite-drizzle"
|
||||
# copy from node_modules/claude-code-plugins/plugins to resources/data/claude-code-pluginso
|
||||
- from: "./node_modules/claude-code-plugins/plugins/"
|
||||
to: "claude-code-plugins"
|
||||
|
||||
win:
|
||||
executableName: Cherry Studio
|
||||
artifactName: ${productName}-${version}-${arch}-setup.${ext}
|
||||
|
||||
@@ -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"
|
||||
|
||||
31
package.json
31
package.json
@@ -81,21 +81,22 @@
|
||||
"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",
|
||||
"selection-hook": "^1.0.12",
|
||||
"sharp": "0.34.4",
|
||||
"sharp": "^0.34.3",
|
||||
"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 +106,8 @@
|
||||
"@agentic/exa": "^7.3.3",
|
||||
"@agentic/searxng": "^7.3.3",
|
||||
"@agentic/tavily": "^7.3.3",
|
||||
"@ai-sdk/amazon-bedrock": "^3.0.35",
|
||||
"@ai-sdk/google-vertex": "^3.0.40",
|
||||
"@ai-sdk/amazon-bedrock": "^3.0.42",
|
||||
"@ai-sdk/google-vertex": "^3.0.48",
|
||||
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch",
|
||||
"@ai-sdk/mistral": "^2.0.19",
|
||||
"@ai-sdk/perplexity": "^2.0.13",
|
||||
@@ -199,6 +200,7 @@
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/he": "^1",
|
||||
"@types/html-to-text": "^9",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/markdown-it": "^14",
|
||||
"@types/md5": "^2.3.5",
|
||||
@@ -227,8 +229,9 @@
|
||||
"@vitest/web-worker": "^3.2.4",
|
||||
"@viz-js/lang-dot": "^1.0.5",
|
||||
"@viz-js/viz": "^3.14.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"ai": "^5.0.68",
|
||||
"ai": "^5.0.76",
|
||||
"antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
|
||||
"archiver": "^7.0.1",
|
||||
"async-mutex": "^0.5.0",
|
||||
@@ -238,6 +241,7 @@
|
||||
"check-disk-space": "3.4.0",
|
||||
"cheerio": "^1.1.2",
|
||||
"chokidar": "^4.0.3",
|
||||
"claude-code-plugins": "1.0.1",
|
||||
"cli-progress": "^3.12.0",
|
||||
"clsx": "^2.1.1",
|
||||
"code-inspector-plugin": "^0.20.14",
|
||||
@@ -390,13 +394,14 @@
|
||||
"undici": "6.21.2",
|
||||
"vite": "npm:rolldown-vite@7.1.5",
|
||||
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||
"@ai-sdk/google@npm:2.0.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",
|
||||
"@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"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
|
||||
@@ -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',
|
||||
@@ -369,7 +373,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 +386,14 @@ export enum IpcChannel {
|
||||
Ovms_StopOVMS = 'ovms:stop-ovms',
|
||||
|
||||
// CherryAI
|
||||
Cherryai_GetSignature = 'cherryai:get-signature'
|
||||
Cherryai_GetSignature = 'cherryai:get-signature',
|
||||
|
||||
// Claude Code Plugins
|
||||
ClaudeCodePlugin_ListAvailable = 'claudeCodePlugin:list-available',
|
||||
ClaudeCodePlugin_Install = 'claudeCodePlugin:install',
|
||||
ClaudeCodePlugin_Uninstall = 'claudeCodePlugin:uninstall',
|
||||
ClaudeCodePlugin_ListInstalled = 'claudeCodePlugin:list-installed',
|
||||
ClaudeCodePlugin_InvalidateCache = 'claudeCodePlugin:invalidate-cache',
|
||||
ClaudeCodePlugin_ReadContent = 'claudeCodePlugin:read-content',
|
||||
ClaudeCodePlugin_WriteContent = 'claudeCodePlugin:write-content'
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
368
packages/ui/DESIGN_SYSTEM.md
Normal file
368
packages/ui/DESIGN_SYSTEM.md
Normal file
@@ -0,0 +1,368 @@
|
||||
# Cherry Studio Design System 集成方案
|
||||
|
||||
本文档聚焦三个核心问题:
|
||||
|
||||
1. **如何将 todocss.css 集成到 Tailwind CSS v4**
|
||||
2. **如何在项目中使用集成后的设计系统**
|
||||
3. **如何平衡 UI 库和主包的需求**
|
||||
|
||||
---
|
||||
|
||||
## 一、集成策略
|
||||
|
||||
### 1.1 文件架构
|
||||
|
||||
```
|
||||
todocss.css (设计师提供)
|
||||
↓ 转换 & 优化
|
||||
design-tokens.css (--ds-* 变量)
|
||||
↓ @theme inline 映射
|
||||
globals.css (cs-* 工具类)
|
||||
↓ 开发者使用
|
||||
React Components
|
||||
```
|
||||
|
||||
### 1.2 核心转换规则
|
||||
|
||||
#### 变量简化
|
||||
|
||||
```css
|
||||
/* todocss.css */
|
||||
--Brand--Base_Colors--Primary: hsla(84, 81%, 44%, 1);
|
||||
|
||||
/* ↓ 转换为 design-tokens.css */
|
||||
--ds-primary: hsla(84, 81%, 44%, 1);
|
||||
|
||||
/* ↓ 映射到 globals.css */
|
||||
@theme inline {
|
||||
--color-cs-primary: var(--ds-primary);
|
||||
}
|
||||
|
||||
/* ↓ 生成工具类 */
|
||||
bg-cs-primary, text-cs-primary, border-cs-primary
|
||||
```
|
||||
|
||||
#### 去除冗余
|
||||
|
||||
- **间距/尺寸合并**: `--Spacing--md` 和 `--Sizing--md` 值相同 → 统一为 `--ds-size-md`
|
||||
- **透明度废弃**: `--Opacity--Red--Red-80` → 使用 `bg-cs-destructive/80`
|
||||
- **错误修正**: `--Font_weight--Regular: 400px` → `--ds-font-weight-regular: 400`
|
||||
|
||||
### 1.3 命名规范
|
||||
|
||||
| 层级 | 前缀 | 示例 | 用途 |
|
||||
|------|------|------|------|
|
||||
| 设计令牌 | `--ds-*` | `--ds-primary` | 定义值 |
|
||||
| Tailwind 映射 | `--color-cs-*` | `--color-cs-primary` | 生成工具类 |
|
||||
| 工具类 | `cs-*` | `bg-cs-primary` | 开发者使用 |
|
||||
|
||||
#### Tailwind v4 映射规则
|
||||
|
||||
| 变量前缀 | 生成的工具类 |
|
||||
|----------|-------------|
|
||||
| `--color-cs-*` | `bg-*`, `text-*`, `border-*`, `fill-*` |
|
||||
| `--spacing-cs-*` | `p-*`, `m-*`, `gap-*` |
|
||||
| `--size-cs-*` | `w-*`, `h-*`, `size-*` |
|
||||
| `--radius-cs-*` | `rounded-*` |
|
||||
| `--font-size-cs-*` | `text-*` |
|
||||
|
||||
### 1.4 为什么使用 @theme inline
|
||||
|
||||
```css
|
||||
/* ❌ @theme - 静态编译,不支持运行时主题切换 */
|
||||
@theme {
|
||||
--color-primary: var(--ds-primary);
|
||||
}
|
||||
|
||||
/* ✅ @theme inline - 保留变量引用,支持运行时切换 */
|
||||
@theme inline {
|
||||
--color-cs-primary: var(--ds-primary);
|
||||
}
|
||||
```
|
||||
|
||||
**关键差异**:`@theme inline` 使 CSS 变量在运行时动态解析,实现明暗主题切换。
|
||||
|
||||
---
|
||||
|
||||
## 二、项目使用指南
|
||||
|
||||
### 2.1 在 UI 库中使用
|
||||
|
||||
#### 文件结构
|
||||
|
||||
```
|
||||
packages/ui/
|
||||
├── src/styles/
|
||||
│ ├── design-tokens.css # 核心变量定义
|
||||
│ └── globals.css # Tailwind 集成
|
||||
└── package.json # 导出配置
|
||||
```
|
||||
|
||||
#### globals.css 示例
|
||||
|
||||
```css
|
||||
@import 'tailwindcss';
|
||||
@import './design-tokens.css';
|
||||
|
||||
@theme inline {
|
||||
/* 颜色 */
|
||||
--color-cs-primary: var(--ds-primary);
|
||||
--color-cs-bg: var(--ds-background);
|
||||
--color-cs-fg: var(--ds-foreground);
|
||||
|
||||
/* 间距 */
|
||||
--spacing-cs-xs: var(--ds-size-xs);
|
||||
--spacing-cs-sm: var(--ds-size-sm);
|
||||
--spacing-cs-md: var(--ds-size-md);
|
||||
|
||||
/* 尺寸 */
|
||||
--size-cs-xs: var(--ds-size-xs);
|
||||
--size-cs-sm: var(--ds-size-sm);
|
||||
|
||||
/* 圆角 */
|
||||
--radius-cs-sm: var(--ds-radius-sm);
|
||||
--radius-cs-md: var(--ds-radius-md);
|
||||
}
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
```
|
||||
|
||||
#### 组件中使用
|
||||
|
||||
```tsx
|
||||
// packages/ui/src/components/Button.tsx
|
||||
export const Button = ({ children }) => (
|
||||
<button className="
|
||||
bg-cs-primary
|
||||
text-white
|
||||
px-cs-sm
|
||||
py-cs-xs
|
||||
rounded-cs-md
|
||||
hover:bg-cs-primary/90
|
||||
transition-colors
|
||||
">
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
```
|
||||
|
||||
### 2.2 在主项目中使用
|
||||
|
||||
#### 导入 UI 库样式
|
||||
|
||||
```css
|
||||
/* src/renderer/src/assets/styles/tailwind.css */
|
||||
@import 'tailwindcss' source('../../../../renderer');
|
||||
@import '@cherrystudio/ui/styles/globals.css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
```
|
||||
|
||||
#### 覆盖或扩展变量
|
||||
|
||||
```css
|
||||
/* src/renderer/src/assets/styles/tailwind.css */
|
||||
@import '@cherrystudio/ui/styles/globals.css';
|
||||
|
||||
/* 主项目特定覆盖 */
|
||||
:root {
|
||||
--ds-primary: #custom-color; /* 覆盖 UI 库的主题色 */
|
||||
}
|
||||
```
|
||||
|
||||
#### 在主项目组件中使用
|
||||
|
||||
```tsx
|
||||
// src/renderer/src/pages/Home.tsx
|
||||
export const Home = () => (
|
||||
<div className="
|
||||
bg-cs-bg
|
||||
p-cs-md
|
||||
rounded-cs-lg
|
||||
">
|
||||
<Button>主项目按钮</Button>
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
### 2.3 主题切换实现
|
||||
|
||||
```tsx
|
||||
// App.tsx
|
||||
import { useState } from 'react'
|
||||
|
||||
export function App() {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light')
|
||||
|
||||
return (
|
||||
<div className={theme}>
|
||||
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
||||
切换主题
|
||||
</button>
|
||||
{/* 所有子组件自动响应主题 */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 透明度修饰符
|
||||
|
||||
```tsx
|
||||
<div className="
|
||||
bg-cs-primary/10 /* 10% 透明度 */
|
||||
bg-cs-primary/50 /* 50% 透明度 */
|
||||
bg-cs-primary/[0.15] /* 自定义透明度 */
|
||||
">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、UI 库与主包平衡策略
|
||||
|
||||
### 3.1 UI 库职责
|
||||
|
||||
**目标**:提供可复用、可定制的基础设计系统
|
||||
|
||||
```json
|
||||
// packages/ui/package.json
|
||||
{
|
||||
"exports": {
|
||||
"./styles/design-tokens.css": "./src/styles/design-tokens.css",
|
||||
"./styles/globals.css": "./src/styles/globals.css"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**原则**:
|
||||
|
||||
- ✅ 定义通用的设计令牌(`--ds-*`)
|
||||
- ✅ 提供默认的 Tailwind 映射(`--color-cs-*`)
|
||||
- ✅ 保持变量语义化,不包含业务逻辑
|
||||
- ❌ 不包含主项目特定的颜色或尺寸
|
||||
|
||||
### 3.2 主包职责
|
||||
|
||||
**目标**:导入 UI 库,根据业务需求扩展或覆盖
|
||||
|
||||
```css
|
||||
/* src/renderer/src/assets/styles/tailwind.css */
|
||||
@import '@cherrystudio/ui/styles/globals.css';
|
||||
|
||||
/* 主项目扩展 */
|
||||
@theme inline {
|
||||
--color-cs-brand-accent: #ff6b6b; /* 新增颜色 */
|
||||
}
|
||||
|
||||
/* 主项目覆盖 */
|
||||
:root {
|
||||
--ds-primary: #custom-primary; /* 覆盖 UI 库的主题色 */
|
||||
}
|
||||
```
|
||||
|
||||
**原则**:
|
||||
|
||||
- ✅ 导入 UI 库的 `globals.css`
|
||||
- ✅ 通过覆盖 `--ds-*` 变量定制主题
|
||||
- ✅ 添加项目特定的 `--color-cs-*` 映射
|
||||
- ✅ 保留向后兼容的旧变量(如 `color.css`)
|
||||
|
||||
### 3.3 向后兼容方案
|
||||
|
||||
#### 保留旧变量
|
||||
|
||||
```css
|
||||
/* src/renderer/src/assets/styles/color.css */
|
||||
:root {
|
||||
--color-primary: #00b96b; /* 旧变量 */
|
||||
--color-background: #181818; /* 旧变量 */
|
||||
}
|
||||
|
||||
/* 映射到新系统 */
|
||||
:root {
|
||||
--ds-primary: var(--color-primary);
|
||||
--ds-background: var(--color-background);
|
||||
}
|
||||
```
|
||||
|
||||
#### 渐进式迁移
|
||||
|
||||
```tsx
|
||||
// 阶段 1:旧代码继续工作
|
||||
<div style={{ color: 'var(--color-primary)' }}>旧代码</div>
|
||||
|
||||
// 阶段 2:新代码使用工具类
|
||||
<div className="text-cs-primary">新代码</div>
|
||||
|
||||
// 阶段 3:逐步替换旧代码
|
||||
```
|
||||
|
||||
### 3.4 冲突处理
|
||||
|
||||
| 场景 | 策略 |
|
||||
|------|------|
|
||||
| UI 库与 Tailwind 默认类冲突 | 使用 `cs-` 前缀隔离 |
|
||||
| 主包需要覆盖 UI 库颜色 | 覆盖 `--ds-*` 变量 |
|
||||
| 主包需要新增颜色 | 添加新的 `--color-cs-*` 映射 |
|
||||
| 旧变量与新系统共存 | 通过 `var()` 映射到 `--ds-*` |
|
||||
|
||||
### 3.5 独立发布 UI 库
|
||||
|
||||
```json
|
||||
// packages/ui/package.json
|
||||
{
|
||||
"name": "@cherrystudio/ui",
|
||||
"exports": {
|
||||
"./styles/design-tokens.css": "./src/styles/design-tokens.css",
|
||||
"./styles/globals.css": "./src/styles/globals.css"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": "^4.1.13"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**外部项目使用**:
|
||||
```css
|
||||
/* 其他项目的 tailwind.css */
|
||||
@import 'tailwindcss';
|
||||
@import '@cherrystudio/ui/styles/globals.css';
|
||||
|
||||
/* 覆盖主题色 */
|
||||
:root {
|
||||
--ds-primary: #your-brand-color;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、完整映射示例
|
||||
|
||||
### todocss.css → design-tokens.css
|
||||
|
||||
| todocss.css | design-tokens.css | 说明 |
|
||||
|-------------|-------------------|------|
|
||||
| `--Brand--Base_Colors--Primary` | `--ds-primary` | 简化命名 |
|
||||
| `--Spacing--md` + `--Sizing--md` | `--ds-size-md` | 合并重复 |
|
||||
| `--Opacity--Red--Red-80` | *(删除)* | 使用 `/80` 修饰符 |
|
||||
| `--Font_weight--Regular: 400px` | `--ds-font-weight-regular: 400` | 修正错误 |
|
||||
| `--Brand--UI_Element_Colors--Primary_Button--Background` | `--ds-btn-primary` | 简化语义 |
|
||||
|
||||
### design-tokens.css → globals.css → 工具类
|
||||
|
||||
| design-tokens.css | globals.css | 工具类 |
|
||||
|-------------------|-------------|--------|
|
||||
| `--ds-primary` | `--color-cs-primary` | `bg-cs-primary` |
|
||||
| `--ds-size-md` | `--spacing-cs-md` | `p-cs-md` |
|
||||
| `--ds-size-md` | `--size-cs-md` | `w-cs-md` |
|
||||
| `--ds-radius-lg` | `--radius-cs-lg` | `rounded-cs-lg` |
|
||||
|
||||
---
|
||||
|
||||
## 五、关键决策记录
|
||||
|
||||
1. **使用 `@theme inline`** - 支持运行时主题切换
|
||||
2. **`cs-` 前缀** - 命名空间隔离,避免冲突
|
||||
3. **合并 Spacing/Sizing** - 消除冗余
|
||||
4. **废弃 Opacity 变量** - 使用 Tailwind 的 `/modifier` 语法
|
||||
5. **双层变量系统** - `--ds-*` (定义) → `--color-cs-*` (映射)
|
||||
6. **共存策略** - Tailwind 默认类 + `cs-` 品牌类
|
||||
@@ -1,4 +1,26 @@
|
||||
# UI Component Library Migration Status
|
||||
# Cherry Studio UI Migration Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the detailed plan for migrating Cherry Studio from antd + styled-components to shadcn/ui + Tailwind CSS. We will adopt a progressive migration strategy to ensure system stability and development efficiency, while gradually implementing UI refactoring in collaboration with UI designers.
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Target Tech Stack
|
||||
|
||||
- **UI Component Library**: shadcn/ui (replacing antd and previously migrated HeroUI)
|
||||
- **Styling Solution**: Tailwind CSS (replacing styled-components)
|
||||
- **Design System**: Custom CSS variable system (see [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md))
|
||||
- **Theme System**: CSS variables + shadcn/ui theme
|
||||
|
||||
### Migration Principles
|
||||
|
||||
1. **Backward Compatibility**: Old components continue working until new components are fully available
|
||||
2. **Progressive Migration**: Migrate components one by one to avoid large-scale rewrites
|
||||
3. **Feature Parity**: Ensure new components have all the functionality of old components
|
||||
4. **Design Consistency**: Follow new design system specifications (see DESIGN_SYSTEM.md)
|
||||
5. **Performance Priority**: Optimize bundle size and rendering performance
|
||||
6. **Designer Collaboration**: Work with UI designers for gradual component encapsulation and UI optimization
|
||||
|
||||
## Usage Example
|
||||
|
||||
@@ -24,115 +46,68 @@ function MyComponent() {
|
||||
@packages/ui/
|
||||
├── src/
|
||||
│ ├── components/ # Main components directory
|
||||
│ │ ├── base/ # Basic components (buttons, inputs, labels, etc.)
|
||||
│ │ ├── display/ # Display components (cards, lists, tables, etc.)
|
||||
│ │ ├── layout/ # Layout components (containers, grids, spacing, etc.)
|
||||
│ │ ├── icons/ # Icon components
|
||||
│ │ ├── interactive/ # Interactive components (modals, tooltips, dropdowns, etc.)
|
||||
│ │ └── composite/ # Composite components (made from multiple base components)
|
||||
│ │ ├── primitives/ # Basic/primitive components (Avatar, ErrorBoundary, Selector, etc.)
|
||||
│ │ │ └── shadcn-io/ # shadcn/ui components (dropzone, etc.)
|
||||
│ │ ├── icons/ # Icon components (Icon, FileIcons, etc.)
|
||||
│ │ └── composites/ # Composite components (CodeEditor, ListItem, etc.)
|
||||
│ ├── hooks/ # Custom React Hooks
|
||||
│ └── types/ # TypeScript type definitions
|
||||
│ ├── styles/ # Global styles and CSS variables
|
||||
│ ├── types/ # TypeScript type definitions
|
||||
│ ├── utils/ # Utility functions
|
||||
│ └── index.ts # Main export file
|
||||
```
|
||||
|
||||
### Component Classification Guide
|
||||
|
||||
When submitting PRs, please place components in the correct directory based on their function:
|
||||
|
||||
- **base**: Most basic UI elements like buttons, inputs, switches, labels, etc.
|
||||
- **display**: Components for displaying content like cards, lists, tables, tabs, etc.
|
||||
- **layout**: Components for page layout like containers, grid systems, dividers, etc.
|
||||
- **primitives**: Basic and primitive UI elements, shadcn/ui components
|
||||
- `Avatar`: Avatar components
|
||||
- `ErrorBoundary`: Error boundary components
|
||||
- `Selector`: Selection components
|
||||
- `shadcn-io/`: Direct shadcn/ui components or adaptations
|
||||
- **icons**: All icon-related components
|
||||
- **interactive**: Components requiring user interaction like modals, drawers, tooltips, dropdowns, etc.
|
||||
- **composite**: Composite components made from multiple base components
|
||||
- `Icon`: Icon factory and basic icons
|
||||
- `FileIcons`: File-specific icons
|
||||
- Loading/spinner icons (SvgSpinners180Ring, ToolsCallingIcon, etc.)
|
||||
- **composites**: Complex components made from multiple primitives
|
||||
- `CodeEditor`: Code editing components
|
||||
- `ListItem`: List item components
|
||||
- `ThinkingEffect`: Animation components
|
||||
- Form and interaction components (DraggableList, EditableNumber, etc.)
|
||||
|
||||
## Migration Overview
|
||||
## Component Extraction Criteria
|
||||
|
||||
- **Total Components**: 236
|
||||
- **Migrated**: 34
|
||||
- **Refactored**: 18
|
||||
- **Pending Migration**: 184
|
||||
### Extraction Standards
|
||||
|
||||
## Component Status Table
|
||||
1. **Usage Frequency**: Component is used in ≥ 3 places in the codebase
|
||||
2. **Future Reusability**: Expected to be used in multiple scenarios in the future
|
||||
3. **Business Complexity**: Component contains complex interaction logic or state management
|
||||
4. **Maintenance Cost**: Centralized management can reduce maintenance overhead
|
||||
5. **Design Consistency**: Components that require unified visual and interaction experience
|
||||
6. **Test Coverage**: As common components, they facilitate unit test writing and maintenance
|
||||
|
||||
| Category | Component Name | Migration Status | Refactoring Status | Description |
|
||||
| ----------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **base** | | | | Base components |
|
||||
| | CopyButton | ✅ | ✅ | Copy button |
|
||||
| | CustomTag | ✅ | ✅ | Custom tag |
|
||||
| | DividerWithText | ✅ | ✅ | Divider with text |
|
||||
| | EmojiIcon | ✅ | ✅ | Emoji icon |
|
||||
| | ErrorBoundary | ✅ | ✅ | Error boundary (decoupled via props) |
|
||||
| | StatusTag | ✅ | ✅ | Unified status tag (merged ErrorTag, SuccessTag, WarnTag, InfoTag) |
|
||||
| | IndicatorLight | ✅ | ✅ | Indicator light |
|
||||
| | Spinner | ✅ | ✅ | Loading spinner |
|
||||
| | TextBadge | ✅ | ✅ | Text badge |
|
||||
| | CustomCollapse | ✅ | ✅ | Custom collapse panel |
|
||||
| **display** | | | | Display components |
|
||||
| | Ellipsis | ✅ | ✅ | Text ellipsis |
|
||||
| | ExpandableText | ✅ | ✅ | Expandable text |
|
||||
| | ThinkingEffect | ✅ | ✅ | Thinking effect animation |
|
||||
| | EmojiAvatar | ✅ | ✅ | Emoji avatar |
|
||||
| | ListItem | ✅ | ✅ | List item |
|
||||
| | MaxContextCount | ✅ | ✅ | Max context count display |
|
||||
| | ProviderAvatar | ✅ | ✅ | Provider avatar |
|
||||
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
|
||||
| | OGCard | ❌ | ❌ | OG card |
|
||||
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
|
||||
| | Preview/* | ❌ | ❌ | Preview components |
|
||||
| **layout** | | | | Layout components |
|
||||
| | HorizontalScrollContainer | ✅ | ❌ | Horizontal scroll container |
|
||||
| | Scrollbar | ✅ | ❌ | Scrollbar |
|
||||
| | Layout/* | ✅ | ✅ | Layout components |
|
||||
| | Tab/* | ❌ | ❌ | Tab (Redux dependency) |
|
||||
| | TopView | ❌ | ❌ | Top view (window.api dependency) |
|
||||
| **icons** | | | | Icon components |
|
||||
| | Icon | ✅ | ✅ | Icon factory function and predefined icons (merged CopyIcon, DeleteIcon, EditIcon, RefreshIcon, ResetIcon, ToolIcon, VisionIcon, WebSearchIcon, WrapIcon, UnWrapIcon, OcrIcon) |
|
||||
| | FileIcons | ✅ | ❌ | File icons (FileSvgIcon, FilePngIcon) |
|
||||
| | ReasoningIcon | ✅ | ❌ | Reasoning icon |
|
||||
| | SvgSpinners180Ring | ✅ | ❌ | Spinner loading icon |
|
||||
| | ToolsCallingIcon | ✅ | ❌ | Tools calling icon |
|
||||
| **interactive** | | | | Interactive components |
|
||||
| | InfoTooltip | ✅ | ❌ | Info tooltip |
|
||||
| | HelpTooltip | ✅ | ❌ | Help tooltip |
|
||||
| | WarnTooltip | ✅ | ❌ | Warning tooltip |
|
||||
| | EditableNumber | ✅ | ❌ | Editable number |
|
||||
| | InfoPopover | ✅ | ❌ | Info popover |
|
||||
| | CollapsibleSearchBar | ✅ | ❌ | Collapsible search bar |
|
||||
| | ImageToolButton | ✅ | ❌ | Image tool button |
|
||||
| | DraggableList | ✅ | ❌ | Draggable list |
|
||||
| | CodeEditor | ✅ | ❌ | Code editor |
|
||||
| | EmojiPicker | ❌ | ❌ | Emoji picker (useTheme dependency) |
|
||||
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
|
||||
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
|
||||
| | LanguageSelect | ❌ | ❌ | Language select |
|
||||
| | TranslateButton | ❌ | ❌ | Translate button (window.api dependency) |
|
||||
| **composite** | | | | Composite components |
|
||||
| | - | - | - | No composite components yet |
|
||||
| **Uncategorized** | | | | Components needing categorization |
|
||||
| | Popups/* (16+ files) | ❌ | ❌ | Popup components (business coupled) |
|
||||
| | RichEditor/* (30+ files) | ❌ | ❌ | Rich text editor |
|
||||
| | MarkdownEditor/* | ❌ | ❌ | Markdown editor |
|
||||
| | MinApp/* | ❌ | ❌ | Mini app (Redux dependency) |
|
||||
| | Avatar/* | ❌ | ❌ | Avatar components |
|
||||
| | ActionTools/* | ❌ | ❌ | Action tools |
|
||||
| | CodeBlockView/* | ❌ | ❌ | Code block view (window.api dependency) |
|
||||
| | ContextMenu | ❌ | ❌ | Context menu (Electron API) |
|
||||
| | WindowControls | ❌ | ❌ | Window controls (Electron API) |
|
||||
| | ErrorBoundary | ❌ | ❌ | Error boundary (window.api dependency) |
|
||||
### Extraction Principles
|
||||
|
||||
- **Single Responsibility**: Each component should only handle one clear function
|
||||
- **Highly Configurable**: Provide flexible configuration options through props
|
||||
- **Backward Compatible**: New versions maintain API backward compatibility
|
||||
- **Complete Documentation**: Provide clear API documentation and usage examples
|
||||
- **Type Safety**: Use TypeScript to ensure type safety
|
||||
|
||||
### Cases Not Recommended for Extraction
|
||||
|
||||
- Simple display components used only on a single page
|
||||
- Overly customized business logic components
|
||||
- Components tightly coupled to specific data sources
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Phase 1: Copy Migration (Current Phase)
|
||||
|
||||
- Copy components as-is to @packages/ui
|
||||
- Retain original dependencies (antd, styled-components, etc.)
|
||||
- Add original path comment at file top
|
||||
|
||||
### Phase 2: Refactor and Optimize
|
||||
|
||||
- Remove antd dependencies, replace with HeroUI
|
||||
- Remove styled-components, replace with Tailwind CSS
|
||||
- Optimize component APIs and type definitions
|
||||
| Phase | Status | Main Tasks | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| **Phase 1** | 🚧 **In Progress** | **Design System Integration** | • Integrate design system CSS variables (todocss.css → design-tokens.css → globals.css)<br>• Configure Tailwind CSS to use custom design tokens<br>• Establish basic style guidelines and theme system |
|
||||
| **Phase 2** | ⏳ **To Start** | **Component Migration and Optimization** | • Filter components for migration based on extraction criteria<br>• Remove antd dependencies, replace with shadcn/ui<br>• Remove HeroUI dependencies, replace with shadcn/ui<br>• Remove styled-components, replace with Tailwind CSS + design system variables<br>• Optimize component APIs and type definitions |
|
||||
| **Phase 3** | ⏳ **To Start** | **UI Refactoring and Optimization** | • Gradually implement UI refactoring with UI designers<br>• Ensure visual consistency and user experience<br>• Performance optimization and code quality improvement |
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -143,9 +118,27 @@ When submitting PRs, please place components in the correct directory based on t
|
||||
|
||||
2. **Can migrate** but need decoupling later:
|
||||
- Components using i18n (change i18n to props)
|
||||
- Components using antd (replace with HeroUI later)
|
||||
- Components using antd (replace with shadcn/ui later)
|
||||
- Components using HeroUI (replace with shadcn/ui later)
|
||||
|
||||
3. **Submission Guidelines**:
|
||||
- Each PR should focus on one category of components
|
||||
- Ensure all migrated components are exported
|
||||
- Update migration status in this document
|
||||
- Follow component extraction criteria, only migrate qualified components
|
||||
|
||||
## Design System Integration
|
||||
|
||||
### CSS Variable System
|
||||
- Refer to [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md) for complete design system planning
|
||||
- Design variables will be managed through CSS variable system, naming conventions TBD
|
||||
- Support theme switching and responsive design
|
||||
|
||||
### Migration Priority Adjustment
|
||||
1. **High Priority**: Basic components (buttons, inputs, tags, etc.)
|
||||
2. **Medium Priority**: Display components (cards, lists, tables, etc.)
|
||||
3. **Low Priority**: Composite components and business-coupled components
|
||||
|
||||
### UI Designer Collaboration
|
||||
- All component designs need confirmation from UI designers
|
||||
- Gradually implement UI refactoring to maintain visual consistency
|
||||
- New components must comply with design system specifications
|
||||
@@ -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 }
|
||||
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',
|
||||
|
||||
@@ -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()
|
||||
134
src/main/ipc.ts
134
src/main/ipc.ts
@@ -18,7 +18,8 @@ import type {
|
||||
AgentPersistedMessage,
|
||||
FileMetadata,
|
||||
Notification,
|
||||
OcrParams,
|
||||
OcrProvider,
|
||||
PluginError,
|
||||
Provider,
|
||||
Shortcut,
|
||||
SupportedOcrFile
|
||||
@@ -49,6 +50,7 @@ import * as NutstoreService from './services/NutstoreService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import { ocrService } from './services/ocr/OcrService'
|
||||
import OvmsManager from './services/OvmsManager'
|
||||
import { PluginService } from './services/PluginService'
|
||||
import { proxyManager } from './services/ProxyManager'
|
||||
import { pythonService } from './services/PythonService'
|
||||
import { FileServiceManager } from './services/remotefile/FileServiceManager'
|
||||
@@ -95,6 +97,18 @@ const vertexAIService = VertexAIService.getInstance()
|
||||
const memoryService = MemoryService.getInstance()
|
||||
const dxtService = new DxtService()
|
||||
const ovmsManager = new OvmsManager()
|
||||
const pluginService = PluginService.getInstance()
|
||||
|
||||
function normalizeError(error: unknown): Error {
|
||||
return error instanceof Error ? error : new Error(String(error))
|
||||
}
|
||||
|
||||
function extractPluginError(error: unknown): PluginError | null {
|
||||
if (error && typeof error === 'object' && 'type' in error && typeof (error as { type: unknown }).type === 'string') {
|
||||
return error as PluginError
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
const appUpdater = new AppUpdater()
|
||||
@@ -875,7 +889,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 +908,119 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
// CherryAI
|
||||
ipcMain.handle(IpcChannel.Cherryai_GetSignature, (_, params) => generateSignature(params))
|
||||
|
||||
// Claude Code Plugins
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_ListAvailable, async () => {
|
||||
try {
|
||||
const data = await pluginService.listAvailable()
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
const pluginError = extractPluginError(error)
|
||||
if (pluginError) {
|
||||
logger.error('Failed to list available plugins', pluginError)
|
||||
return { success: false, error: pluginError }
|
||||
}
|
||||
|
||||
const err = normalizeError(error)
|
||||
logger.error('Failed to list available plugins', err)
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type: 'TRANSACTION_FAILED',
|
||||
operation: 'list-available',
|
||||
reason: err.message
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_Install, async (_, options) => {
|
||||
try {
|
||||
const data = await pluginService.install(options)
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
logger.error('Failed to install plugin', { options, error })
|
||||
return { success: false, error }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_Uninstall, async (_, options) => {
|
||||
try {
|
||||
await pluginService.uninstall(options)
|
||||
return { success: true, data: undefined }
|
||||
} catch (error) {
|
||||
logger.error('Failed to uninstall plugin', { options, error })
|
||||
return { success: false, error }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_ListInstalled, async (_, agentId: string) => {
|
||||
try {
|
||||
const data = await pluginService.listInstalled(agentId)
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
const pluginError = extractPluginError(error)
|
||||
if (pluginError) {
|
||||
logger.error('Failed to list installed plugins', { agentId, error: pluginError })
|
||||
return { success: false, error: pluginError }
|
||||
}
|
||||
|
||||
const err = normalizeError(error)
|
||||
logger.error('Failed to list installed plugins', { agentId, error: err })
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type: 'TRANSACTION_FAILED',
|
||||
operation: 'list-installed',
|
||||
reason: err.message
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_InvalidateCache, async () => {
|
||||
try {
|
||||
pluginService.invalidateCache()
|
||||
return { success: true, data: undefined }
|
||||
} catch (error) {
|
||||
const pluginError = extractPluginError(error)
|
||||
if (pluginError) {
|
||||
logger.error('Failed to invalidate plugin cache', pluginError)
|
||||
return { success: false, error: pluginError }
|
||||
}
|
||||
|
||||
const err = normalizeError(error)
|
||||
logger.error('Failed to invalidate plugin cache', err)
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type: 'TRANSACTION_FAILED',
|
||||
operation: 'invalidate-cache',
|
||||
reason: err.message
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_ReadContent, async (_, sourcePath: string) => {
|
||||
try {
|
||||
const data = await pluginService.readContent(sourcePath)
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
logger.error('Failed to read plugin content', { sourcePath, error })
|
||||
return { success: false, error }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_WriteContent, async (_, options) => {
|
||||
try {
|
||||
await pluginService.writeContent(options.agentId, options.filename, options.type, options.content)
|
||||
return { success: true, data: undefined }
|
||||
} catch (error) {
|
||||
logger.error('Failed to write plugin content', { options, error })
|
||||
return { success: false, error }
|
||||
}
|
||||
})
|
||||
|
||||
// Preference handlers
|
||||
PreferenceService.registerIpcHandler()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
|
||||
import { OllamaEmbeddings } from '@cherrystudio/embedjs-ollama'
|
||||
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
|
||||
import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings'
|
||||
import type { ApiClient } from '@types'
|
||||
|
||||
import { VoyageEmbeddings } from './VoyageEmbeddings'
|
||||
@@ -9,7 +8,7 @@ import { VoyageEmbeddings } from './VoyageEmbeddings'
|
||||
export default class EmbeddingsFactory {
|
||||
static create({ embedApiClient, dimensions }: { embedApiClient: ApiClient; dimensions?: number }): BaseEmbeddings {
|
||||
const batchSize = 10
|
||||
const { model, provider, apiKey, apiVersion, baseURL } = embedApiClient
|
||||
const { model, provider, apiKey, baseURL } = embedApiClient
|
||||
if (provider === 'voyageai') {
|
||||
return new VoyageEmbeddings({
|
||||
modelName: model,
|
||||
@@ -38,16 +37,7 @@ export default class EmbeddingsFactory {
|
||||
}
|
||||
})
|
||||
}
|
||||
if (apiVersion !== undefined) {
|
||||
return new AzureOpenAiEmbeddings({
|
||||
azureOpenAIApiKey: apiKey,
|
||||
azureOpenAIApiVersion: apiVersion,
|
||||
azureOpenAIApiDeploymentName: model,
|
||||
azureOpenAIEndpoint: baseURL,
|
||||
dimensions,
|
||||
batchSize
|
||||
})
|
||||
}
|
||||
// NOTE: Azure OpenAI 也走 OpenAIEmbeddings, baseURL是https://xxxx.openai.azure.com/openai/v1
|
||||
return new OpenAiEmbeddings({
|
||||
model,
|
||||
apiKey,
|
||||
|
||||
199
src/main/knowledge/preprocess/OpenMineruPreprocessProvider.ts
Normal file
199
src/main/knowledge/preprocess/OpenMineruPreprocessProvider.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { fileStorage } from '@main/services/FileStorage'
|
||||
import type { FileMetadata, PreprocessProvider } from '@types'
|
||||
import AdmZip from 'adm-zip'
|
||||
import { net } from 'electron'
|
||||
import FormData from 'form-data'
|
||||
|
||||
import BasePreprocessProvider from './BasePreprocessProvider'
|
||||
|
||||
const logger = loggerService.withContext('MineruPreprocessProvider')
|
||||
|
||||
export default class OpenMineruPreprocessProvider extends BasePreprocessProvider {
|
||||
constructor(provider: PreprocessProvider, userId?: string) {
|
||||
super(provider, userId)
|
||||
}
|
||||
|
||||
public async parseFile(
|
||||
sourceId: string,
|
||||
file: FileMetadata
|
||||
): Promise<{ processedFile: FileMetadata; quota: number }> {
|
||||
try {
|
||||
const filePath = fileStorage.getFilePathById(file)
|
||||
logger.info(`Open MinerU preprocess processing started: ${filePath}`)
|
||||
await this.validateFile(filePath)
|
||||
|
||||
// 1. Update progress
|
||||
await this.sendPreprocessProgress(sourceId, 50)
|
||||
logger.info(`File ${file.name} is starting processing...`)
|
||||
|
||||
// 2. Upload file and extract
|
||||
const { path: outputPath } = await this.uploadFileAndExtract(file)
|
||||
|
||||
// 3. Check quota
|
||||
const quota = await this.checkQuota()
|
||||
|
||||
// 4. Create processed file info
|
||||
return {
|
||||
processedFile: this.createProcessedFileInfo(file, outputPath),
|
||||
quota
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Open MinerU preprocess processing failed for:`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public async checkQuota() {
|
||||
// self-hosted version always has enough quota
|
||||
return Infinity
|
||||
}
|
||||
|
||||
private async validateFile(filePath: string): Promise<void> {
|
||||
const pdfBuffer = await fs.promises.readFile(filePath)
|
||||
|
||||
const doc = await this.readPdf(pdfBuffer)
|
||||
|
||||
// File page count must be less than 600 pages
|
||||
if (doc.numPages >= 600) {
|
||||
throw new Error(`PDF page count (${doc.numPages}) exceeds the limit of 600 pages`)
|
||||
}
|
||||
// File size must be less than 200MB
|
||||
if (pdfBuffer.length >= 200 * 1024 * 1024) {
|
||||
const fileSizeMB = Math.round(pdfBuffer.length / (1024 * 1024))
|
||||
throw new Error(`PDF file size (${fileSizeMB}MB) exceeds the limit of 200MB`)
|
||||
}
|
||||
}
|
||||
|
||||
private createProcessedFileInfo(file: FileMetadata, outputPath: string): FileMetadata {
|
||||
// Find the main file after extraction
|
||||
let finalPath = ''
|
||||
let finalName = file.origin_name.replace('.pdf', '.md')
|
||||
// Find the corresponding folder by file name
|
||||
outputPath = path.join(outputPath, `${file.origin_name.replace('.pdf', '')}`)
|
||||
try {
|
||||
const files = fs.readdirSync(outputPath)
|
||||
|
||||
const mdFile = files.find((f) => f.endsWith('.md'))
|
||||
if (mdFile) {
|
||||
const originalMdPath = path.join(outputPath, mdFile)
|
||||
const newMdPath = path.join(outputPath, finalName)
|
||||
|
||||
// Rename file to original file name
|
||||
try {
|
||||
fs.renameSync(originalMdPath, newMdPath)
|
||||
finalPath = newMdPath
|
||||
logger.info(`Renamed markdown file from ${mdFile} to ${finalName}`)
|
||||
} catch (renameError) {
|
||||
logger.warn(`Failed to rename file ${mdFile} to ${finalName}: ${renameError}`)
|
||||
// If rename fails, use the original file
|
||||
finalPath = originalMdPath
|
||||
finalName = mdFile
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to read output directory ${outputPath}:`, error as Error)
|
||||
finalPath = path.join(outputPath, `${file.id}.md`)
|
||||
}
|
||||
|
||||
return {
|
||||
...file,
|
||||
name: finalName,
|
||||
path: finalPath,
|
||||
ext: '.md',
|
||||
size: fs.existsSync(finalPath) ? fs.statSync(finalPath).size : 0
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadFileAndExtract(
|
||||
file: FileMetadata,
|
||||
maxRetries: number = 5,
|
||||
intervalMs: number = 5000
|
||||
): Promise<{ path: string }> {
|
||||
let retries = 0
|
||||
|
||||
const endpoint = `${this.provider.apiHost}/file_parse`
|
||||
|
||||
// Get file stream
|
||||
const filePath = fileStorage.getFilePathById(file)
|
||||
const fileBuffer = await fs.promises.readFile(filePath)
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('return_md', 'true')
|
||||
formData.append('response_format_zip', 'true')
|
||||
formData.append('files', fileBuffer, {
|
||||
filename: file.origin_name
|
||||
})
|
||||
|
||||
while (retries < maxRetries) {
|
||||
let zipPath: string | undefined
|
||||
|
||||
try {
|
||||
const response = await net.fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
token: this.userId ?? '',
|
||||
...(this.provider.apiKey ? { Authorization: `Bearer ${this.provider.apiKey}` } : {}),
|
||||
...formData.getHeaders()
|
||||
},
|
||||
body: formData.getBuffer()
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
// Check if response header is application/zip
|
||||
if (response.headers.get('content-type') !== 'application/zip') {
|
||||
throw new Error(`Downloaded ZIP file has unexpected content-type: ${response.headers.get('content-type')}`)
|
||||
}
|
||||
|
||||
const dirPath = this.storageDir
|
||||
|
||||
zipPath = path.join(dirPath, `${file.id}.zip`)
|
||||
const extractPath = path.join(dirPath, `${file.id}`)
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
fs.writeFileSync(zipPath, Buffer.from(arrayBuffer))
|
||||
logger.info(`Downloaded ZIP file: ${zipPath}`)
|
||||
|
||||
// Ensure extraction directory exists
|
||||
if (!fs.existsSync(extractPath)) {
|
||||
fs.mkdirSync(extractPath, { recursive: true })
|
||||
}
|
||||
|
||||
// Extract files
|
||||
const zip = new AdmZip(zipPath)
|
||||
zip.extractAllTo(extractPath, true)
|
||||
logger.info(`Extracted files to: ${extractPath}`)
|
||||
|
||||
return { path: extractPath }
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Failed to upload and extract file: ${(error as Error).message}, retry ${retries + 1}/${maxRetries}`
|
||||
)
|
||||
if (retries === maxRetries - 1) {
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
// Delete temporary ZIP file
|
||||
if (zipPath && fs.existsSync(zipPath)) {
|
||||
try {
|
||||
fs.unlinkSync(zipPath)
|
||||
logger.info(`Deleted temporary ZIP file: ${zipPath}`)
|
||||
} catch (deleteError) {
|
||||
logger.warn(`Failed to delete temporary ZIP file ${zipPath}:`, deleteError as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
retries++
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs))
|
||||
}
|
||||
|
||||
throw new Error(`Processing timeout for file: ${file.id}`)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import DefaultPreprocessProvider from './DefaultPreprocessProvider'
|
||||
import Doc2xPreprocessProvider from './Doc2xPreprocessProvider'
|
||||
import MineruPreprocessProvider from './MineruPreprocessProvider'
|
||||
import MistralPreprocessProvider from './MistralPreprocessProvider'
|
||||
import OpenMineruPreprocessProvider from './OpenMineruPreprocessProvider'
|
||||
export default class PreprocessProviderFactory {
|
||||
static create(provider: PreprocessProvider, userId?: string): BasePreprocessProvider {
|
||||
switch (provider.id) {
|
||||
@@ -14,6 +15,8 @@ export default class PreprocessProviderFactory {
|
||||
return new MistralPreprocessProvider(provider)
|
||||
case 'mineru':
|
||||
return new MineruPreprocessProvider(provider, userId)
|
||||
case 'open-mineru':
|
||||
return new OpenMineruPreprocessProvider(provider, userId)
|
||||
default:
|
||||
return new DefaultPreprocessProvider(provider)
|
||||
}
|
||||
|
||||
1171
src/main/services/PluginService.ts
Normal file
1171
src/main/services/PluginService.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
import type { McpHttpServerConfig, Options, SDKMessage } from '@anthropic-ai/claude-agent-sdk'
|
||||
import type { CanUseTool, McpHttpServerConfig, Options, SDKMessage } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { loggerService } from '@logger'
|
||||
import { config as apiConfigService } from '@main/apiServer/config'
|
||||
@@ -12,10 +12,23 @@ import { app } from 'electron'
|
||||
|
||||
import type { GetAgentSessionResponse } from '../..'
|
||||
import type { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface'
|
||||
import { promptForToolApproval } from './tool-permissions'
|
||||
import { ClaudeStreamState, transformSDKMessageToStreamParts } from './transform'
|
||||
|
||||
const require_ = createRequire(import.meta.url)
|
||||
const logger = loggerService.withContext('ClaudeCodeService')
|
||||
const DEFAULT_AUTO_ALLOW_TOOLS = new Set(['Read', 'Glob', 'Grep'])
|
||||
const shouldAutoApproveTools = process.env.CHERRY_AUTO_ALLOW_TOOLS === '1'
|
||||
|
||||
type UserInputMessage = {
|
||||
type: 'user'
|
||||
parent_tool_use_id: string | null
|
||||
session_id: string
|
||||
message: {
|
||||
role: 'user'
|
||||
content: string
|
||||
}
|
||||
}
|
||||
|
||||
class ClaudeCodeStream extends EventEmitter implements AgentStream {
|
||||
declare emit: (event: 'data', data: AgentStreamEvent) => boolean
|
||||
@@ -100,6 +113,41 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
|
||||
const errorChunks: string[] = []
|
||||
|
||||
const sessionAllowedTools = new Set<string>(session.allowed_tools ?? [])
|
||||
const autoAllowTools = new Set<string>([...DEFAULT_AUTO_ALLOW_TOOLS, ...sessionAllowedTools])
|
||||
const normalizeToolName = (name: string) => (name.startsWith('builtin_') ? name.slice('builtin_'.length) : name)
|
||||
|
||||
const canUseTool: CanUseTool = async (toolName, input, options) => {
|
||||
logger.info('Handling tool permission check', {
|
||||
toolName,
|
||||
suggestionCount: options.suggestions?.length ?? 0
|
||||
})
|
||||
|
||||
if (shouldAutoApproveTools) {
|
||||
logger.debug('Auto-approving tool due to CHERRY_AUTO_ALLOW_TOOLS flag', { toolName })
|
||||
return { behavior: 'allow', updatedInput: input }
|
||||
}
|
||||
|
||||
if (options.signal.aborted) {
|
||||
logger.debug('Permission request signal already aborted; denying tool', { toolName })
|
||||
return {
|
||||
behavior: 'deny',
|
||||
message: 'Tool request was cancelled before prompting the user'
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedToolName = normalizeToolName(toolName)
|
||||
if (autoAllowTools.has(toolName) || autoAllowTools.has(normalizedToolName)) {
|
||||
logger.debug('Auto-allowing tool from allowed list', {
|
||||
toolName,
|
||||
normalizedToolName
|
||||
})
|
||||
return { behavior: 'allow', updatedInput: input }
|
||||
}
|
||||
|
||||
return promptForToolApproval(toolName, input, options)
|
||||
}
|
||||
|
||||
// Build SDK options from parameters
|
||||
const options: Options = {
|
||||
abortController,
|
||||
@@ -122,7 +170,8 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
includePartialMessages: true,
|
||||
permissionMode: session.configuration?.permission_mode,
|
||||
maxTurns: session.configuration?.max_turns,
|
||||
allowedTools: session.allowed_tools
|
||||
allowedTools: session.allowed_tools,
|
||||
canUseTool
|
||||
}
|
||||
|
||||
if (session.accessible_paths.length > 1) {
|
||||
@@ -161,9 +210,14 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
resume: options.resume
|
||||
})
|
||||
|
||||
const { stream: userInputStream, close: closeUserStream } = this.createUserMessageStream(
|
||||
prompt,
|
||||
abortController.signal
|
||||
)
|
||||
|
||||
// Start async processing on the next tick so listeners can subscribe first
|
||||
setImmediate(() => {
|
||||
this.processSDKQuery(prompt, options, aiStream, errorChunks).catch((error) => {
|
||||
this.processSDKQuery(userInputStream, closeUserStream, options, aiStream, errorChunks).catch((error) => {
|
||||
logger.error('Unhandled Claude Code stream error', {
|
||||
error: error instanceof Error ? { name: error.name, message: error.message } : String(error)
|
||||
})
|
||||
@@ -177,17 +231,90 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
return aiStream
|
||||
}
|
||||
|
||||
private async *userMessages(prompt: string) {
|
||||
{
|
||||
yield {
|
||||
type: 'user' as const,
|
||||
parent_tool_use_id: null,
|
||||
session_id: '',
|
||||
message: {
|
||||
role: 'user' as const,
|
||||
content: prompt
|
||||
private createUserMessageStream(initialPrompt: string, abortSignal: AbortSignal) {
|
||||
const queue: Array<UserInputMessage | null> = []
|
||||
const waiters: Array<(value: UserInputMessage | null) => void> = []
|
||||
let closed = false
|
||||
|
||||
const flushWaiters = (value: UserInputMessage | null) => {
|
||||
const resolve = waiters.shift()
|
||||
if (resolve) {
|
||||
resolve(value)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const enqueue = (value: UserInputMessage | null) => {
|
||||
if (closed) return
|
||||
if (value === null) {
|
||||
closed = true
|
||||
}
|
||||
if (!flushWaiters(value)) {
|
||||
queue.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
if (closed) return
|
||||
enqueue(null)
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
close()
|
||||
}
|
||||
|
||||
if (abortSignal.aborted) {
|
||||
close()
|
||||
} else {
|
||||
abortSignal.addEventListener('abort', onAbort, { once: true })
|
||||
}
|
||||
|
||||
const iterator = (async function* () {
|
||||
try {
|
||||
while (true) {
|
||||
let value: UserInputMessage | null
|
||||
if (queue.length > 0) {
|
||||
value = queue.shift() ?? null
|
||||
} else if (closed) {
|
||||
break
|
||||
} else {
|
||||
// Wait for next message or close signal
|
||||
value = await new Promise<UserInputMessage | null>((resolve) => {
|
||||
waiters.push(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
break
|
||||
}
|
||||
|
||||
yield value
|
||||
}
|
||||
} finally {
|
||||
closed = true
|
||||
abortSignal.removeEventListener('abort', onAbort)
|
||||
while (waiters.length > 0) {
|
||||
const resolve = waiters.shift()
|
||||
resolve?.(null)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
enqueue({
|
||||
type: 'user',
|
||||
parent_tool_use_id: null,
|
||||
session_id: '',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: initialPrompt
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
stream: iterator,
|
||||
enqueue,
|
||||
close
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +322,8 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
* Process SDK query and emit stream events
|
||||
*/
|
||||
private async processSDKQuery(
|
||||
prompt: string,
|
||||
promptStream: AsyncIterable<UserInputMessage>,
|
||||
closePromptStream: () => void,
|
||||
options: Options,
|
||||
stream: ClaudeCodeStream,
|
||||
errorChunks: string[]
|
||||
@@ -203,14 +331,10 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
const jsonOutput: SDKMessage[] = []
|
||||
let hasCompleted = false
|
||||
const startTime = Date.now()
|
||||
|
||||
const streamState = new ClaudeStreamState()
|
||||
|
||||
try {
|
||||
// Process streaming responses using SDK query
|
||||
for await (const message of query({
|
||||
prompt: this.userMessages(prompt),
|
||||
options
|
||||
})) {
|
||||
for await (const message of query({ prompt: promptStream, options })) {
|
||||
if (hasCompleted) break
|
||||
|
||||
jsonOutput.push(message)
|
||||
@@ -221,10 +345,10 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
content: JSON.stringify(message.message.content)
|
||||
})
|
||||
} else if (message.type === 'stream_event') {
|
||||
logger.silly('Claude stream event', {
|
||||
message,
|
||||
event: JSON.stringify(message.event)
|
||||
})
|
||||
// logger.silly('Claude stream event', {
|
||||
// message,
|
||||
// event: JSON.stringify(message.event)
|
||||
// })
|
||||
} else {
|
||||
logger.silly('Claude response', {
|
||||
message,
|
||||
@@ -232,7 +356,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
})
|
||||
}
|
||||
|
||||
// Transform SDKMessage to UIMessageChunks
|
||||
const chunks = transformSDKMessageToStreamParts(message, streamState)
|
||||
for (const chunk of chunks) {
|
||||
stream.emit('data', {
|
||||
@@ -242,7 +365,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
// Successfully completed
|
||||
hasCompleted = true
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
@@ -251,7 +373,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
messageCount: jsonOutput.length
|
||||
})
|
||||
|
||||
// Emit completion event
|
||||
stream.emit('data', {
|
||||
type: 'complete'
|
||||
})
|
||||
@@ -260,8 +381,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
hasCompleted = true
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
// Check if this is an abort error
|
||||
const errorObj = error as any
|
||||
const isAborted =
|
||||
errorObj?.name === 'AbortError' ||
|
||||
@@ -270,7 +389,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
|
||||
if (isAborted) {
|
||||
logger.info('SDK query aborted by client disconnect', { duration })
|
||||
// Simply cleanup and return - don't emit error events
|
||||
stream.emit('data', {
|
||||
type: 'cancelled',
|
||||
error: new Error('Request aborted by client')
|
||||
@@ -285,11 +403,13 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
error: errorObj instanceof Error ? { name: errorObj.name, message: errorObj.message } : String(errorObj),
|
||||
stderr: errorChunks
|
||||
})
|
||||
// Emit error event
|
||||
|
||||
stream.emit('data', {
|
||||
type: 'error',
|
||||
error: new Error(errorMessage)
|
||||
})
|
||||
} finally {
|
||||
closePromptStream()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
323
src/main/services/agents/services/claudecode/tool-permissions.ts
Normal file
323
src/main/services/agents/services/claudecode/tool-permissions.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import type { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { loggerService } from '@logger'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
import { windowService } from '../../../WindowService'
|
||||
import { builtinTools } from './tools'
|
||||
|
||||
const logger = loggerService.withContext('ClaudeCodeService')
|
||||
|
||||
const TOOL_APPROVAL_TIMEOUT_MS = 30_000
|
||||
const MAX_PREVIEW_LENGTH = 2_000
|
||||
const shouldAutoApproveTools = process.env.CHERRY_AUTO_ALLOW_TOOLS === '1'
|
||||
|
||||
type ToolPermissionBehavior = 'allow' | 'deny'
|
||||
|
||||
type ToolPermissionResponsePayload = {
|
||||
requestId: string
|
||||
behavior: ToolPermissionBehavior
|
||||
updatedInput?: unknown
|
||||
message?: string
|
||||
updatedPermissions?: PermissionUpdate[]
|
||||
}
|
||||
|
||||
type PendingPermissionRequest = {
|
||||
fulfill: (update: PermissionResult) => void
|
||||
timeout: NodeJS.Timeout
|
||||
signal?: AbortSignal
|
||||
abortListener?: () => void
|
||||
originalInput: Record<string, unknown>
|
||||
toolName: string
|
||||
}
|
||||
|
||||
type RendererPermissionRequestPayload = {
|
||||
requestId: string
|
||||
toolName: string
|
||||
toolId: string
|
||||
description?: string
|
||||
requiresPermissions: boolean
|
||||
input: Record<string, unknown>
|
||||
inputPreview: string
|
||||
createdAt: number
|
||||
expiresAt: number
|
||||
suggestions: PermissionUpdate[]
|
||||
}
|
||||
|
||||
type RendererPermissionResultPayload = {
|
||||
requestId: string
|
||||
behavior: ToolPermissionBehavior
|
||||
message?: string
|
||||
reason: 'response' | 'timeout' | 'aborted' | 'no-window'
|
||||
}
|
||||
|
||||
const pendingRequests = new Map<string, PendingPermissionRequest>()
|
||||
let ipcHandlersInitialized = false
|
||||
|
||||
const jsonReplacer = (_key: string, value: unknown) => {
|
||||
if (typeof value === 'bigint') return value.toString()
|
||||
if (value instanceof Map) return Object.fromEntries(value.entries())
|
||||
if (value instanceof Set) return Array.from(value.values())
|
||||
if (value instanceof Date) return value.toISOString()
|
||||
if (typeof value === 'function') return undefined
|
||||
if (value === undefined) return undefined
|
||||
return value
|
||||
}
|
||||
|
||||
const sanitizeStructuredData = <T>(value: T): T => {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value, jsonReplacer)) as T
|
||||
} catch (error) {
|
||||
logger.warn('Failed to sanitize structured data for tool permission payload', {
|
||||
error: error instanceof Error ? { name: error.name, message: error.message } : String(error)
|
||||
})
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const buildInputPreview = (value: unknown): string => {
|
||||
let preview: string
|
||||
|
||||
try {
|
||||
preview = JSON.stringify(value, null, 2)
|
||||
} catch (error) {
|
||||
preview = typeof value === 'string' ? value : String(value)
|
||||
}
|
||||
|
||||
if (preview.length > MAX_PREVIEW_LENGTH) {
|
||||
preview = `${preview.slice(0, MAX_PREVIEW_LENGTH)}...`
|
||||
}
|
||||
|
||||
return preview
|
||||
}
|
||||
|
||||
const broadcastToRenderer = (
|
||||
channel: IpcChannel,
|
||||
payload: RendererPermissionRequestPayload | RendererPermissionResultPayload
|
||||
): boolean => {
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
|
||||
if (!mainWindow) {
|
||||
logger.warn('Unable to send agent tool permission payload – main window unavailable', {
|
||||
channel,
|
||||
requestId: 'requestId' in payload ? payload.requestId : undefined
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
mainWindow.webContents.send(channel, payload)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const finalizeRequest = (
|
||||
requestId: string,
|
||||
update: PermissionResult,
|
||||
reason: RendererPermissionResultPayload['reason']
|
||||
) => {
|
||||
const pending = pendingRequests.get(requestId)
|
||||
|
||||
if (!pending) {
|
||||
logger.debug('Attempted to finalize unknown tool permission request', { requestId, reason })
|
||||
return false
|
||||
}
|
||||
|
||||
logger.debug('Finalizing tool permission request', {
|
||||
requestId,
|
||||
toolName: pending.toolName,
|
||||
behavior: update.behavior,
|
||||
reason
|
||||
})
|
||||
|
||||
pendingRequests.delete(requestId)
|
||||
clearTimeout(pending.timeout)
|
||||
|
||||
if (pending.signal && pending.abortListener) {
|
||||
pending.signal.removeEventListener('abort', pending.abortListener)
|
||||
}
|
||||
|
||||
pending.fulfill(update)
|
||||
|
||||
const resultPayload: RendererPermissionResultPayload = {
|
||||
requestId,
|
||||
behavior: update.behavior,
|
||||
message: update.behavior === 'deny' ? update.message : undefined,
|
||||
reason
|
||||
}
|
||||
|
||||
const dispatched = broadcastToRenderer(IpcChannel.AgentToolPermission_Result, resultPayload)
|
||||
|
||||
logger.debug('Sent tool permission result to renderer', {
|
||||
requestId,
|
||||
dispatched
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const ensureIpcHandlersRegistered = () => {
|
||||
if (ipcHandlersInitialized) return
|
||||
|
||||
ipcHandlersInitialized = true
|
||||
|
||||
ipcMain.handle(IpcChannel.AgentToolPermission_Response, async (_event, payload: ToolPermissionResponsePayload) => {
|
||||
logger.debug('main received AgentToolPermission_Response', payload)
|
||||
const { requestId, behavior, updatedInput, message } = payload
|
||||
const pending = pendingRequests.get(requestId)
|
||||
|
||||
if (!pending) {
|
||||
logger.warn('Received renderer tool permission response for unknown request', { requestId })
|
||||
return { success: false, error: 'unknown-request' }
|
||||
}
|
||||
|
||||
logger.debug('Received renderer response for tool permission', {
|
||||
requestId,
|
||||
toolName: pending.toolName,
|
||||
behavior,
|
||||
hasUpdatedPermissions: Array.isArray(payload.updatedPermissions) && payload.updatedPermissions.length > 0
|
||||
})
|
||||
|
||||
const maybeUpdatedInput =
|
||||
updatedInput && typeof updatedInput === 'object' && !Array.isArray(updatedInput)
|
||||
? (updatedInput as Record<string, unknown>)
|
||||
: pending.originalInput
|
||||
|
||||
const sanitizedUpdatedPermissions = Array.isArray(payload.updatedPermissions)
|
||||
? payload.updatedPermissions.map((perm) => sanitizeStructuredData(perm))
|
||||
: undefined
|
||||
|
||||
const finalUpdate: PermissionResult =
|
||||
behavior === 'allow'
|
||||
? {
|
||||
behavior: 'allow',
|
||||
updatedInput: sanitizeStructuredData(maybeUpdatedInput),
|
||||
updatedPermissions: sanitizedUpdatedPermissions
|
||||
}
|
||||
: {
|
||||
behavior: 'deny',
|
||||
message: message ?? 'User denied permission for this tool'
|
||||
}
|
||||
|
||||
finalizeRequest(requestId, finalUpdate, 'response')
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
}
|
||||
|
||||
export async function promptForToolApproval(
|
||||
toolName: string,
|
||||
input: Record<string, unknown>,
|
||||
options?: { signal: AbortSignal; suggestions?: PermissionUpdate[] }
|
||||
): Promise<PermissionResult> {
|
||||
if (shouldAutoApproveTools) {
|
||||
logger.debug('promptForToolApproval auto-approving tool for test', {
|
||||
toolName
|
||||
})
|
||||
|
||||
return { behavior: 'allow', updatedInput: input }
|
||||
}
|
||||
|
||||
ensureIpcHandlersRegistered()
|
||||
|
||||
if (options?.signal?.aborted) {
|
||||
logger.info('Skipping tool approval prompt because request signal is already aborted', { toolName })
|
||||
return { behavior: 'deny', message: 'Tool request was cancelled before prompting the user' }
|
||||
}
|
||||
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
|
||||
if (!mainWindow) {
|
||||
logger.warn('Denying tool usage because no renderer window is available to obtain approval', { toolName })
|
||||
return { behavior: 'deny', message: 'Unable to request approval – renderer not ready' }
|
||||
}
|
||||
|
||||
const toolMetadata = builtinTools.find((tool) => tool.name === toolName || tool.id === toolName)
|
||||
const sanitizedInput = sanitizeStructuredData(input)
|
||||
const inputPreview = buildInputPreview(sanitizedInput)
|
||||
const sanitizedSuggestions = (options?.suggestions ?? []).map((suggestion) => sanitizeStructuredData(suggestion))
|
||||
|
||||
const requestId = randomUUID()
|
||||
const createdAt = Date.now()
|
||||
const expiresAt = createdAt + TOOL_APPROVAL_TIMEOUT_MS
|
||||
|
||||
logger.info('Requesting user approval for tool usage', {
|
||||
requestId,
|
||||
toolName,
|
||||
description: toolMetadata?.description
|
||||
})
|
||||
|
||||
const requestPayload: RendererPermissionRequestPayload = {
|
||||
requestId,
|
||||
toolName,
|
||||
toolId: toolMetadata?.id ?? toolName,
|
||||
description: toolMetadata?.description,
|
||||
requiresPermissions: toolMetadata?.requirePermissions ?? false,
|
||||
input: sanitizedInput,
|
||||
inputPreview,
|
||||
createdAt,
|
||||
expiresAt,
|
||||
suggestions: sanitizedSuggestions
|
||||
}
|
||||
|
||||
const defaultDenyUpdate: PermissionResult = { behavior: 'deny', message: 'Tool request aborted before user decision' }
|
||||
|
||||
logger.debug('Registering tool permission request', {
|
||||
requestId,
|
||||
toolName,
|
||||
requiresPermissions: requestPayload.requiresPermissions,
|
||||
timeoutMs: TOOL_APPROVAL_TIMEOUT_MS,
|
||||
suggestionCount: sanitizedSuggestions.length
|
||||
})
|
||||
|
||||
return new Promise<PermissionResult>((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
logger.info('User tool permission request timed out', { requestId, toolName })
|
||||
finalizeRequest(requestId, { behavior: 'deny', message: 'Timed out waiting for approval' }, 'timeout')
|
||||
}, TOOL_APPROVAL_TIMEOUT_MS)
|
||||
|
||||
const pending: PendingPermissionRequest = {
|
||||
fulfill: resolve,
|
||||
timeout,
|
||||
originalInput: sanitizedInput,
|
||||
toolName,
|
||||
signal: options?.signal
|
||||
}
|
||||
|
||||
if (options?.signal) {
|
||||
const abortListener = () => {
|
||||
logger.info('Tool permission request aborted before user responded', { requestId, toolName })
|
||||
finalizeRequest(requestId, defaultDenyUpdate, 'aborted')
|
||||
}
|
||||
|
||||
pending.abortListener = abortListener
|
||||
options.signal.addEventListener('abort', abortListener, { once: true })
|
||||
}
|
||||
|
||||
pendingRequests.set(requestId, pending)
|
||||
|
||||
logger.debug('Pending tool permission request count', {
|
||||
count: pendingRequests.size
|
||||
})
|
||||
|
||||
const sent = broadcastToRenderer(IpcChannel.AgentToolPermission_Request, requestPayload)
|
||||
|
||||
logger.debug('Broadcasted tool permission request to renderer', {
|
||||
requestId,
|
||||
toolName,
|
||||
sent
|
||||
})
|
||||
|
||||
if (!sent) {
|
||||
finalizeRequest(
|
||||
requestId,
|
||||
{
|
||||
behavior: 'deny',
|
||||
message: 'Unable to request approval because the renderer window is unavailable'
|
||||
},
|
||||
'no-window'
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
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> }) =>
|
||||
@@ -546,6 +568,21 @@ const api = {
|
||||
start: (): Promise<StartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Start),
|
||||
restart: (): Promise<RestartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Restart),
|
||||
stop: (): Promise<StopApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Stop)
|
||||
},
|
||||
claudeCodePlugin: {
|
||||
listAvailable: (): Promise<PluginResult<ListAvailablePluginsResult>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_ListAvailable),
|
||||
install: (options: InstallPluginOptions): Promise<PluginResult<PluginMetadata>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_Install, options),
|
||||
uninstall: (options: UninstallPluginOptions): Promise<PluginResult<void>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_Uninstall, options),
|
||||
listInstalled: (agentId: string): Promise<PluginResult<InstalledPlugin[]>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_ListInstalled, agentId),
|
||||
invalidateCache: (): Promise<PluginResult<void>> => ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_InvalidateCache),
|
||||
readContent: (sourcePath: string): Promise<PluginResult<string>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_ReadContent, sourcePath),
|
||||
writeContent: (options: WritePluginContentOptions): Promise<PluginResult<void>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_WriteContent, options)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import NotesPage from './pages/notes/NotesPage'
|
||||
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import AssistantPresetsPage from './pages/store/assistants/presets/AssistantPresetsPage'
|
||||
import { TerminalPage } from './pages/terminal/TerminalPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
|
||||
const Router: FC = () => {
|
||||
@@ -40,6 +41,7 @@ const Router: FC = () => {
|
||||
<Route path="/apps" element={<MinAppsPage />} />
|
||||
<Route path="/code" element={<CodeToolsPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
<Route path="/terminal" element={<TerminalPage />} />
|
||||
<Route path="/launchpad" element={<LaunchpadPage />} />
|
||||
</Routes>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -6,7 +6,14 @@
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { processKnowledgeReferences } from '@renderer/services/KnowledgeService'
|
||||
import type { BaseTool, MCPTool, MCPToolResponse, NormalToolResponse } from '@renderer/types'
|
||||
import type {
|
||||
BaseTool,
|
||||
MCPCallToolResponse,
|
||||
MCPTool,
|
||||
MCPToolResponse,
|
||||
MCPToolResultContent,
|
||||
NormalToolResponse
|
||||
} from '@renderer/types'
|
||||
import type { Chunk } from '@renderer/types/chunk'
|
||||
import { ChunkType } from '@renderer/types/chunk'
|
||||
import type { ToolSet, TypedToolCall, TypedToolError, TypedToolResult } from 'ai'
|
||||
@@ -255,6 +262,7 @@ export class ToolCallChunkHandler {
|
||||
type: 'tool-result'
|
||||
} & TypedToolResult<ToolSet>
|
||||
): void {
|
||||
// TODO: 基于AI SDK为供应商内置工具做更好的展示和类型安全处理
|
||||
const { toolCallId, output, input } = chunk
|
||||
|
||||
if (!toolCallId) {
|
||||
@@ -300,12 +308,7 @@ export class ToolCallChunkHandler {
|
||||
responses: [toolResponse]
|
||||
})
|
||||
|
||||
const images: string[] = []
|
||||
for (const content of toolResponse.response?.content || []) {
|
||||
if (content.type === 'image' && content.data) {
|
||||
images.push(`data:${content.mimeType};base64,${content.data}`)
|
||||
}
|
||||
}
|
||||
const images = extractImagesFromToolOutput(toolResponse.response)
|
||||
|
||||
if (images.length) {
|
||||
this.onChunk({
|
||||
@@ -352,3 +355,41 @@ export class ToolCallChunkHandler {
|
||||
}
|
||||
|
||||
export const addActiveToolCall = ToolCallChunkHandler.addActiveToolCall.bind(ToolCallChunkHandler)
|
||||
|
||||
function extractImagesFromToolOutput(output: unknown): string[] {
|
||||
if (!output) {
|
||||
return []
|
||||
}
|
||||
|
||||
const contents: unknown[] = []
|
||||
|
||||
if (isMcpCallToolResponse(output)) {
|
||||
contents.push(...output.content)
|
||||
} else if (Array.isArray(output)) {
|
||||
contents.push(...output)
|
||||
} else if (hasContentArray(output)) {
|
||||
contents.push(...output.content)
|
||||
}
|
||||
|
||||
return contents
|
||||
.filter(isMcpImageContent)
|
||||
.map((content) => `data:${content.mimeType ?? 'image/png'};base64,${content.data}`)
|
||||
}
|
||||
|
||||
function isMcpCallToolResponse(value: unknown): value is MCPCallToolResponse {
|
||||
return typeof value === 'object' && value !== null && Array.isArray((value as MCPCallToolResponse).content)
|
||||
}
|
||||
|
||||
function hasContentArray(value: unknown): value is { content: unknown[] } {
|
||||
return typeof value === 'object' && value !== null && Array.isArray((value as { content?: unknown }).content)
|
||||
}
|
||||
|
||||
function isMcpImageContent(content: unknown): content is MCPToolResultContent & { data: string } {
|
||||
if (typeof content !== 'object' || content === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const resultContent = content as MCPToolResultContent
|
||||
|
||||
return resultContent.type === 'image' && typeof resultContent.data === 'string'
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
|
||||
import type { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
|
||||
import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types'
|
||||
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
|
||||
import { SUPPORTED_IMAGE_ENDPOINT_LIST } from '@renderer/utils'
|
||||
import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic'
|
||||
import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai'
|
||||
|
||||
@@ -78,7 +79,7 @@ export default class ModernAiProvider {
|
||||
return this.actualProvider
|
||||
}
|
||||
|
||||
public async completions(modelId: string, params: StreamTextParams, config: ModernAiProviderConfig) {
|
||||
public async completions(modelId: string, params: StreamTextParams, providerConfig: ModernAiProviderConfig) {
|
||||
// 检查model是否存在
|
||||
if (!this.model) {
|
||||
throw new Error('Model is required for completions. Please use constructor with model parameter.')
|
||||
@@ -86,7 +87,10 @@ export default class ModernAiProvider {
|
||||
|
||||
// 每次请求时重新生成配置以确保API key轮换生效
|
||||
this.config = providerToAiSdkConfig(this.actualProvider, this.model)
|
||||
|
||||
logger.debug('Generated provider config for completions', this.config)
|
||||
if (SUPPORTED_IMAGE_ENDPOINT_LIST.includes(this.config.options.endpoint)) {
|
||||
providerConfig.isImageGenerationEndpoint = true
|
||||
}
|
||||
// 准备特殊配置
|
||||
await prepareSpecialProviderConfig(this.actualProvider, this.config)
|
||||
|
||||
@@ -97,12 +101,13 @@ export default class ModernAiProvider {
|
||||
|
||||
// 提前构建中间件
|
||||
const middlewares = buildAiSdkMiddlewares({
|
||||
...config,
|
||||
provider: this.actualProvider
|
||||
...providerConfig,
|
||||
provider: this.actualProvider,
|
||||
assistant: providerConfig.assistant
|
||||
})
|
||||
logger.debug('Built middlewares in completions', {
|
||||
middlewareCount: middlewares.length,
|
||||
isImageGeneration: config.isImageGenerationEndpoint
|
||||
isImageGeneration: providerConfig.isImageGenerationEndpoint
|
||||
})
|
||||
if (!this.localProvider) {
|
||||
throw new Error('Local provider not created')
|
||||
@@ -110,7 +115,7 @@ export default class ModernAiProvider {
|
||||
|
||||
// 根据endpoint类型创建对应的模型
|
||||
let model: AiSdkModel | undefined
|
||||
if (config.isImageGenerationEndpoint) {
|
||||
if (providerConfig.isImageGenerationEndpoint) {
|
||||
model = this.localProvider.imageModel(modelId)
|
||||
} else {
|
||||
model = this.localProvider.languageModel(modelId)
|
||||
@@ -126,15 +131,15 @@ export default class ModernAiProvider {
|
||||
params.messages = [...claudeCodeSystemMessage, ...(params.messages || [])]
|
||||
}
|
||||
|
||||
if (config.topicId && (await preferenceService.get('app.developer_mode.enabled'))) {
|
||||
if (providerConfig.topicId && (await preferenceService.get('app.developer_mode.enabled'))) {
|
||||
// TypeScript类型窄化:确保topicId是string类型
|
||||
const traceConfig = {
|
||||
...config,
|
||||
topicId: config.topicId
|
||||
...providerConfig,
|
||||
topicId: providerConfig.topicId
|
||||
}
|
||||
return await this._completionsForTrace(model, params, traceConfig)
|
||||
} else {
|
||||
return await this._completionsOrImageGeneration(model, params, config)
|
||||
return await this._completionsOrImageGeneration(model, params, providerConfig)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Provider } from '@renderer/types'
|
||||
import { isOpenAIProvider } from '@renderer/utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { AihubmixAPIClient } from '../aihubmix/AihubmixAPIClient'
|
||||
@@ -202,36 +201,4 @@ describe('ApiClientFactory', () => {
|
||||
expect(client).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isOpenAIProvider', () => {
|
||||
it('should return true for openai type', () => {
|
||||
const provider = createTestProvider('openai', 'openai')
|
||||
expect(isOpenAIProvider(provider)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for azure-openai type', () => {
|
||||
const provider = createTestProvider('azure-openai', 'azure-openai')
|
||||
expect(isOpenAIProvider(provider)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for unknown type (fallback to OpenAI)', () => {
|
||||
const provider = createTestProvider('unknown', 'unknown')
|
||||
expect(isOpenAIProvider(provider)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for vertexai type', () => {
|
||||
const provider = createTestProvider('vertex', 'vertexai')
|
||||
expect(isOpenAIProvider(provider)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for anthropic type', () => {
|
||||
const provider = createTestProvider('anthropic', 'anthropic')
|
||||
expect(isOpenAIProvider(provider)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for gemini type', () => {
|
||||
const provider = createTestProvider('gemini', 'gemini')
|
||||
expect(isOpenAIProvider(provider)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import type { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
import { loggerService } from '@logger'
|
||||
import { type MCPTool, type Message, type Model, type Provider } from '@renderer/types'
|
||||
import { isSupportedThinkingTokenQwenModel } from '@renderer/config/models'
|
||||
import { isSupportEnableThinkingProvider } from '@renderer/config/providers'
|
||||
import type { MCPTool } from '@renderer/types'
|
||||
import { type Assistant, type Message, type Model, type Provider } from '@renderer/types'
|
||||
import type { Chunk } from '@renderer/types/chunk'
|
||||
import type { LanguageModelMiddleware } from 'ai'
|
||||
import { extractReasoningMiddleware, simulateStreamingMiddleware } from 'ai'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import { isOpenRouterGeminiGenerateImageModel } from '../utils/image'
|
||||
import { noThinkMiddleware } from './noThinkMiddleware'
|
||||
import { openrouterGenerateImageMiddleware } from './openrouterGenerateImageMiddleware'
|
||||
import { qwenThinkingMiddleware } from './qwenThinkingMiddleware'
|
||||
import { toolChoiceMiddleware } from './toolChoiceMiddleware'
|
||||
|
||||
const logger = loggerService.withContext('AiSdkMiddlewareBuilder')
|
||||
@@ -20,6 +25,7 @@ export interface AiSdkMiddlewareConfig {
|
||||
onChunk?: (chunk: Chunk) => void
|
||||
model?: Model
|
||||
provider?: Provider
|
||||
assistant?: Assistant
|
||||
enableReasoning: boolean
|
||||
// 是否开启提示词工具调用
|
||||
isPromptToolUse: boolean
|
||||
@@ -128,7 +134,7 @@ export function buildAiSdkMiddlewares(config: AiSdkMiddlewareConfig): LanguageMo
|
||||
const builder = new AiSdkMiddlewareBuilder()
|
||||
|
||||
// 0. 知识库强制调用中间件(必须在最前面,确保第一轮强制调用知识库)
|
||||
if (config.knowledgeRecognition === 'off') {
|
||||
if (!isEmpty(config.assistant?.knowledge_bases?.map((base) => base.id)) && config.knowledgeRecognition !== 'on') {
|
||||
builder.add({
|
||||
name: 'force-knowledge-first',
|
||||
middleware: toolChoiceMiddleware('builtin_knowledge_search')
|
||||
@@ -219,6 +225,21 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config:
|
||||
function addModelSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config: AiSdkMiddlewareConfig): void {
|
||||
if (!config.model || !config.provider) return
|
||||
|
||||
// Qwen models on providers that don't support enable_thinking parameter (like Ollama, LM Studio, NVIDIA)
|
||||
// Use /think or /no_think suffix to control thinking mode
|
||||
if (
|
||||
config.provider &&
|
||||
isSupportedThinkingTokenQwenModel(config.model) &&
|
||||
!isSupportEnableThinkingProvider(config.provider)
|
||||
) {
|
||||
const enableThinking = config.assistant?.settings?.reasoning_effort !== undefined
|
||||
builder.add({
|
||||
name: 'qwen-thinking-control',
|
||||
middleware: qwenThinkingMiddleware(enableThinking)
|
||||
})
|
||||
logger.debug(`Added Qwen thinking middleware with thinking ${enableThinking ? 'enabled' : 'disabled'}`)
|
||||
}
|
||||
|
||||
// 可以根据模型ID或特性添加特定中间件
|
||||
// 例如:图像生成模型、多模态模型等
|
||||
if (isOpenRouterGeminiGenerateImageModel(config.model, config.provider)) {
|
||||
|
||||
39
src/renderer/src/aiCore/middleware/qwenThinkingMiddleware.ts
Normal file
39
src/renderer/src/aiCore/middleware/qwenThinkingMiddleware.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { LanguageModelMiddleware } from 'ai'
|
||||
|
||||
/**
|
||||
* Qwen Thinking Middleware
|
||||
* Controls thinking mode for Qwen models on providers that don't support enable_thinking parameter (like Ollama)
|
||||
* Appends '/think' or '/no_think' suffix to user messages based on reasoning_effort setting
|
||||
* @param enableThinking - Whether thinking mode is enabled (based on reasoning_effort !== undefined)
|
||||
* @returns LanguageModelMiddleware
|
||||
*/
|
||||
export function qwenThinkingMiddleware(enableThinking: boolean): LanguageModelMiddleware {
|
||||
const suffix = enableThinking ? ' /think' : ' /no_think'
|
||||
|
||||
return {
|
||||
middlewareVersion: 'v2',
|
||||
|
||||
transformParams: async ({ params }) => {
|
||||
const transformedParams = { ...params }
|
||||
// Process messages in prompt
|
||||
if (transformedParams.prompt && Array.isArray(transformedParams.prompt)) {
|
||||
transformedParams.prompt = transformedParams.prompt.map((message) => {
|
||||
// Only process user messages
|
||||
if (message.role === 'user') {
|
||||
// Process content array
|
||||
if (Array.isArray(message.content)) {
|
||||
for (const part of message.content) {
|
||||
if (part.type === 'text' && !part.text.endsWith('/think') && !part.text.endsWith('/no_think')) {
|
||||
part.text += suffix
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return message
|
||||
})
|
||||
}
|
||||
|
||||
return transformedParams
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AiPlugin } from '@cherrystudio/ai-core'
|
||||
import { createPromptToolUsePlugin, googleToolsPlugin, webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
import { createPromptToolUsePlugin, webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
import { preferenceService } from '@data/PreferenceService'
|
||||
import { loggerService } from '@logger'
|
||||
import type { Assistant } from '@renderer/types'
|
||||
@@ -68,9 +68,9 @@ export async function buildPlugins(
|
||||
)
|
||||
}
|
||||
|
||||
if (middlewareConfig.enableUrlContext) {
|
||||
plugins.push(googleToolsPlugin({ urlContext: true }))
|
||||
}
|
||||
// if (middlewareConfig.enableUrlContext && middlewareConfig.) {
|
||||
// plugins.push(googleToolsPlugin({ urlContext: true }))
|
||||
// }
|
||||
|
||||
logger.debug(
|
||||
'Final plugin list:',
|
||||
|
||||
@@ -114,7 +114,7 @@ export async function handleGeminiFileUpload(file: FileMetadata, model: Model):
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理OpenAI大文件上传
|
||||
* 处理OpenAI兼容大文件上传
|
||||
*/
|
||||
export async function handleOpenAILargeFileUpload(
|
||||
file: FileMetadata,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* 构建AI SDK的流式和非流式参数
|
||||
*/
|
||||
|
||||
import { anthropic } from '@ai-sdk/anthropic'
|
||||
import { google } from '@ai-sdk/google'
|
||||
import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic/edge'
|
||||
import { vertex } from '@ai-sdk/google-vertex/edge'
|
||||
import type { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
@@ -97,10 +99,6 @@ export async function buildStreamTextParams(
|
||||
|
||||
let tools = setupToolsConfig(mcpTools)
|
||||
|
||||
// if (webSearchProviderId) {
|
||||
// tools['builtin_web_search'] = webSearchTool(webSearchProviderId)
|
||||
// }
|
||||
|
||||
// 构建真正的 providerOptions
|
||||
const webSearchConfig: CherryWebSearchConfig = {
|
||||
maxResults: store.getState().websearch.maxResults,
|
||||
@@ -143,12 +141,34 @@ export async function buildStreamTextParams(
|
||||
}
|
||||
}
|
||||
|
||||
// google-vertex
|
||||
if (enableUrlContext && aiSdkProviderId === 'google-vertex') {
|
||||
if (enableUrlContext) {
|
||||
if (!tools) {
|
||||
tools = {}
|
||||
}
|
||||
tools.url_context = vertex.tools.urlContext({}) as ProviderDefinedTool
|
||||
const blockedDomains = mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
|
||||
switch (aiSdkProviderId) {
|
||||
case 'google-vertex':
|
||||
tools.url_context = vertex.tools.urlContext({}) as ProviderDefinedTool
|
||||
break
|
||||
case 'google':
|
||||
tools.url_context = google.tools.urlContext({}) as ProviderDefinedTool
|
||||
break
|
||||
case 'anthropic':
|
||||
case 'google-vertex-anthropic':
|
||||
tools.web_fetch = (
|
||||
aiSdkProviderId === 'anthropic'
|
||||
? anthropic.tools.webFetch_20250910({
|
||||
maxUses: webSearchConfig.maxResults,
|
||||
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
|
||||
})
|
||||
: vertexAnthropic.tools.webFetch_20250910({
|
||||
maxUses: webSearchConfig.maxResults,
|
||||
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
|
||||
})
|
||||
) as ProviderDefinedTool
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 构建基础参数
|
||||
|
||||
@@ -32,7 +32,8 @@ const AIHUBMIX_RULES: RuleSet = {
|
||||
match: (model) =>
|
||||
(startsWith('gemini')(model) || startsWith('imagen')(model)) &&
|
||||
!model.id.endsWith('-nothink') &&
|
||||
!model.id.endsWith('-search'),
|
||||
!model.id.endsWith('-search') &&
|
||||
!model.id.includes('embedding'),
|
||||
provider: (provider: Provider) => {
|
||||
return extraProviderConfig({
|
||||
...provider,
|
||||
|
||||
@@ -7,24 +7,27 @@ import {
|
||||
} from '@cherrystudio/ai-core/provider'
|
||||
import { cacheService } from '@data/CacheService'
|
||||
import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
|
||||
import { isNewApiProvider } from '@renderer/config/providers'
|
||||
import {
|
||||
isAnthropicProvider,
|
||||
isAzureOpenAIProvider,
|
||||
isGeminiProvider,
|
||||
isNewApiProvider
|
||||
} from '@renderer/config/providers'
|
||||
import {
|
||||
getAwsBedrockAccessKeyId,
|
||||
getAwsBedrockRegion,
|
||||
getAwsBedrockSecretAccessKey
|
||||
} from '@renderer/hooks/useAwsBedrock'
|
||||
import { createVertexProvider, isVertexAIConfigured } from '@renderer/hooks/useVertexAI'
|
||||
import { createVertexProvider, isVertexAIConfigured, isVertexProvider } from '@renderer/hooks/useVertexAI'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import store from '@renderer/store'
|
||||
import { isSystemProvider, type Model, type Provider } from '@renderer/types'
|
||||
import { formatApiHost } from '@renderer/utils/api'
|
||||
import { cloneDeep, trim } from 'lodash'
|
||||
import { isSystemProvider, type Model, type Provider, SystemProviderIds } from '@renderer/types'
|
||||
import { formatApiHost, formatAzureOpenAIApiHost, formatVertexApiHost, routeToEndpoint } from '@renderer/utils/api'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
import { aihubmixProviderCreator, newApiResolverCreator, vertexAnthropicProviderCreator } from './config'
|
||||
import { COPILOT_DEFAULT_HEADERS } from './constants'
|
||||
import { getAiSdkProviderId } from './factory'
|
||||
const logger = loggerService.withContext('ProviderConfigProcessor')
|
||||
|
||||
/**
|
||||
* 获取轮询的API key
|
||||
@@ -56,13 +59,6 @@ function getRotatedApiKey(provider: Provider): string {
|
||||
* 处理特殊provider的转换逻辑
|
||||
*/
|
||||
function handleSpecialProviders(model: Model, provider: Provider): Provider {
|
||||
// if (provider.type === 'vertexai' && !isVertexProvider(provider)) {
|
||||
// if (!isVertexAIConfigured()) {
|
||||
// throw new Error('VertexAI is not configured. Please configure project, location and service account credentials.')
|
||||
// }
|
||||
// return createVertexProvider(provider)
|
||||
// }
|
||||
|
||||
if (isNewApiProvider(provider)) {
|
||||
return newApiResolverCreator(model, provider)
|
||||
}
|
||||
@@ -79,43 +75,30 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化provider的API Host
|
||||
* 主要用来对齐AISdk的BaseURL格式
|
||||
* @param provider
|
||||
* @returns
|
||||
*/
|
||||
function formatAnthropicApiHost(host: string): string {
|
||||
const trimmedHost = host?.trim()
|
||||
|
||||
if (!trimmedHost) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (trimmedHost.endsWith('/')) {
|
||||
return trimmedHost
|
||||
}
|
||||
|
||||
if (trimmedHost.endsWith('/v1')) {
|
||||
return `${trimmedHost}/`
|
||||
}
|
||||
|
||||
return formatApiHost(trimmedHost)
|
||||
}
|
||||
|
||||
function formatProviderApiHost(provider: Provider): Provider {
|
||||
const formatted = { ...provider }
|
||||
if (formatted.anthropicApiHost) {
|
||||
formatted.anthropicApiHost = formatAnthropicApiHost(formatted.anthropicApiHost)
|
||||
formatted.anthropicApiHost = formatApiHost(formatted.anthropicApiHost)
|
||||
}
|
||||
|
||||
if (formatted.type === 'anthropic') {
|
||||
if (isAnthropicProvider(provider)) {
|
||||
const baseHost = formatted.anthropicApiHost || formatted.apiHost
|
||||
formatted.apiHost = formatAnthropicApiHost(baseHost)
|
||||
formatted.apiHost = formatApiHost(baseHost)
|
||||
if (!formatted.anthropicApiHost) {
|
||||
formatted.anthropicApiHost = formatted.apiHost
|
||||
}
|
||||
} else if (formatted.id === 'copilot') {
|
||||
const trimmed = trim(formatted.apiHost)
|
||||
formatted.apiHost = trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed
|
||||
} else if (formatted.type === 'gemini') {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, 'v1beta')
|
||||
} else if (formatted.id === SystemProviderIds.copilot || formatted.id === SystemProviderIds.github) {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, false)
|
||||
} else if (isGeminiProvider(formatted)) {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, true, 'v1beta')
|
||||
} else if (isAzureOpenAIProvider(formatted)) {
|
||||
formatted.apiHost = formatAzureOpenAIApiHost(formatted.apiHost)
|
||||
} else if (isVertexProvider(formatted)) {
|
||||
formatted.apiHost = formatVertexApiHost(formatted)
|
||||
} else {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost)
|
||||
}
|
||||
@@ -149,15 +132,15 @@ export function providerToAiSdkConfig(
|
||||
options: ProviderSettingsMap[keyof ProviderSettingsMap]
|
||||
} {
|
||||
const aiSdkProviderId = getAiSdkProviderId(actualProvider)
|
||||
logger.debug('providerToAiSdkConfig', { aiSdkProviderId })
|
||||
|
||||
// 构建基础配置
|
||||
const { baseURL, endpoint } = routeToEndpoint(actualProvider.apiHost)
|
||||
const baseConfig = {
|
||||
baseURL: trim(actualProvider.apiHost),
|
||||
baseURL: baseURL,
|
||||
apiKey: getRotatedApiKey(actualProvider)
|
||||
}
|
||||
|
||||
const isCopilotProvider = actualProvider.id === 'copilot'
|
||||
const isCopilotProvider = actualProvider.id === SystemProviderIds.copilot
|
||||
if (isCopilotProvider) {
|
||||
const storedHeaders = store.getState().copilot.defaultHeaders ?? {}
|
||||
const options = ProviderConfigFactory.fromProvider('github-copilot-openai-compatible', baseConfig, {
|
||||
@@ -178,6 +161,7 @@ export function providerToAiSdkConfig(
|
||||
|
||||
// 处理OpenAI模式
|
||||
const extraOptions: any = {}
|
||||
extraOptions.endpoint = endpoint
|
||||
if (actualProvider.type === 'openai-response' && !isOpenAIChatCompletionOnlyModel(model)) {
|
||||
extraOptions.mode = 'responses'
|
||||
} else if (aiSdkProviderId === 'openai') {
|
||||
@@ -199,13 +183,11 @@ export function providerToAiSdkConfig(
|
||||
}
|
||||
// azure
|
||||
if (aiSdkProviderId === 'azure' || actualProvider.type === 'azure-openai') {
|
||||
extraOptions.apiVersion = actualProvider.apiVersion
|
||||
baseConfig.baseURL += '/openai'
|
||||
// extraOptions.apiVersion = actualProvider.apiVersion 默认使用v1,不使用azure endpoint
|
||||
if (actualProvider.apiVersion === 'preview') {
|
||||
extraOptions.mode = 'responses'
|
||||
} else {
|
||||
extraOptions.mode = 'chat'
|
||||
extraOptions.useDeploymentBasedUrls = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,22 +209,7 @@ export function providerToAiSdkConfig(
|
||||
...googleCredentials,
|
||||
privateKey: formatPrivateKey(googleCredentials.privateKey)
|
||||
}
|
||||
// extraOptions.headers = window.api.vertexAI.getAuthHeaders({
|
||||
// projectId: project,
|
||||
// serviceAccount: {
|
||||
// privateKey: googleCredentials.privateKey,
|
||||
// clientEmail: googleCredentials.clientEmail
|
||||
// }
|
||||
// })
|
||||
if (baseConfig.baseURL.endsWith('/v1/')) {
|
||||
baseConfig.baseURL = baseConfig.baseURL.slice(0, -4)
|
||||
} else if (baseConfig.baseURL.endsWith('/v1')) {
|
||||
baseConfig.baseURL = baseConfig.baseURL.slice(0, -3)
|
||||
}
|
||||
|
||||
if (baseConfig.baseURL && !baseConfig.baseURL.includes('publishers/google')) {
|
||||
baseConfig.baseURL = `${baseConfig.baseURL}/v1/projects/${project}/locations/${location}/publishers/google`
|
||||
}
|
||||
baseConfig.baseURL += aiSdkProviderId === 'google-vertex' ? '/publishers/google' : '/publishers/anthropic/models'
|
||||
}
|
||||
|
||||
if (hasProviderConfig(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') {
|
||||
|
||||
@@ -5,6 +5,16 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { DraggableList } from '../'
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// mock @hello-pangea/dnd 组件
|
||||
vi.mock('@hello-pangea/dnd', () => {
|
||||
return {
|
||||
|
||||
@@ -3,6 +3,16 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { DraggableVirtualList } from '../'
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock 依赖项
|
||||
vi.mock('@hello-pangea/dnd', () => ({
|
||||
__esModule: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@cherrystudio/ui'
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -20,18 +20,12 @@ const ExpandableText = ({
|
||||
setIsExpanded((prev) => !prev)
|
||||
}, [])
|
||||
|
||||
const button = useMemo(() => {
|
||||
return (
|
||||
<Button variant="ghost" onClick={toggleExpand} className="self-end">
|
||||
{isExpanded ? t('common.collapse') : t('common.expand')}
|
||||
</Button>
|
||||
)
|
||||
}, [isExpanded, t, toggleExpand])
|
||||
|
||||
return (
|
||||
<Container ref={ref} style={style} $expanded={isExpanded}>
|
||||
<TextContainer $expanded={isExpanded}>{text}</TextContainer>
|
||||
{button}
|
||||
<Button variant="ghost" onClick={toggleExpand} className="self-end">
|
||||
{isExpanded ? t('common.collapse') : t('common.expand')}
|
||||
</Button>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -48,4 +42,4 @@ const TextContainer = styled.div<{ $expanded?: boolean }>`
|
||||
line-height: ${(props) => (props.$expanded ? 'unset' : '30px')};
|
||||
`
|
||||
|
||||
export default memo(ExpandableText)
|
||||
export default ExpandableText
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DeleteOutlined, ExclamationCircleOutlined, ReloadOutlined } from '@ant-
|
||||
import { Button, Flex, Tooltip } from '@cherrystudio/ui'
|
||||
import { restoreFromLocal } from '@renderer/services/BackupService'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import { Modal, Table } from 'antd'
|
||||
import { Modal, Space, Table } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -221,6 +221,26 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe
|
||||
}
|
||||
}
|
||||
|
||||
const footerContent = (
|
||||
<Space align="center">
|
||||
<Button key="refresh" onClick={fetchBackupFiles} disabled={loading}>
|
||||
<ReloadOutlined />
|
||||
{t('settings.data.local.backup.manager.refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
key="delete"
|
||||
variant="destructive"
|
||||
onClick={handleDeleteSelected}
|
||||
disabled={selectedRowKeys.length === 0 || deleting}>
|
||||
<DeleteOutlined />
|
||||
{t('settings.data.local.backup.manager.delete.selected')} ({selectedRowKeys.length})
|
||||
</Button>
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('settings.data.local.backup.manager.title')}
|
||||
@@ -229,24 +249,7 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe
|
||||
width={800}
|
||||
centered
|
||||
transitionName="animation-move-down"
|
||||
classNames={{ footer: 'flex justify-end gap-1' }}
|
||||
footer={[
|
||||
<Button key="refresh" onClick={fetchBackupFiles} disabled={loading}>
|
||||
<ReloadOutlined />
|
||||
{t('settings.data.local.backup.manager.refresh')}
|
||||
</Button>,
|
||||
<Button
|
||||
key="delete"
|
||||
variant="destructive"
|
||||
onClick={handleDeleteSelected}
|
||||
disabled={selectedRowKeys.length === 0 || deleting}>
|
||||
<DeleteOutlined />
|
||||
{t('settings.data.local.backup.manager.delete.selected')} ({selectedRowKeys.length})
|
||||
</Button>,
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
]}>
|
||||
footer={footerContent}>
|
||||
<Table
|
||||
rowKey="fileName"
|
||||
columns={columns}
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
import { loggerService } from '@logger'
|
||||
import type { Selection } from '@react-types/shared'
|
||||
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
|
||||
import { permissionModeCards } from '@renderer/config/agent'
|
||||
import { agentModelFilter, getModelLogoById } from '@renderer/config/models'
|
||||
import { permissionModeCards } from '@renderer/constants/permissionModes'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
|
||||
@@ -12,8 +12,8 @@ vi.mock('react-i18next', () => ({
|
||||
|
||||
// Mock ImageToolButton
|
||||
vi.mock('../ImageToolButton', () => ({
|
||||
default: vi.fn(({ tooltip, onPress, icon }) => (
|
||||
<button type="button" onClick={onPress} role="button" aria-label={tooltip}>
|
||||
default: vi.fn(({ tooltip, onClick, icon }) => (
|
||||
<button type="button" onClick={onClick} role="button" aria-label={tooltip}>
|
||||
{icon}
|
||||
</button>
|
||||
))
|
||||
|
||||
@@ -4,8 +4,8 @@ exports[`ImageToolButton > should match snapshot 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
aria-label="Test tooltip"
|
||||
class="rounded-full"
|
||||
data-testid="button"
|
||||
radius="full"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { makeSvgSizeAdaptive } from '@renderer/utils'
|
||||
import { makeSvgSizeAdaptive } from '@renderer/utils/image'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ interface BaseSelectorProps<V = string | number> {
|
||||
options: SelectorOption<V>[]
|
||||
placeholder?: string
|
||||
placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom'
|
||||
style?: React.CSSProperties
|
||||
/** 字体大小 */
|
||||
size?: number
|
||||
/** 是否禁用 */
|
||||
@@ -45,6 +46,7 @@ const Selector = <V extends string | number>({
|
||||
placement = 'bottomRight',
|
||||
size = 13,
|
||||
placeholder,
|
||||
style,
|
||||
disabled = false,
|
||||
multiple = false
|
||||
}: SelectorProps<V>) => {
|
||||
@@ -137,7 +139,7 @@ const Selector = <V extends string | number>({
|
||||
placement={placement}
|
||||
open={open && !disabled}
|
||||
onOpenChange={handleOpenChange}>
|
||||
<Label $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
|
||||
<Label style={style} $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
|
||||
{label}
|
||||
<LabelIcon size={size + 3} />
|
||||
</Label>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { classNames } from '@renderer/utils'
|
||||
import { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import type { LRUCache } from 'lru-cache'
|
||||
import {
|
||||
Code,
|
||||
FileSearch,
|
||||
Folder,
|
||||
Hammer,
|
||||
@@ -106,6 +107,8 @@ const getTabIcon = (
|
||||
case 'settings':
|
||||
return <Settings size={14} />
|
||||
case 'code':
|
||||
return <Code size={14} />
|
||||
case 'terminal':
|
||||
return <Terminal size={14} />
|
||||
default:
|
||||
return null
|
||||
|
||||
@@ -9,6 +9,15 @@ vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: (k: string) => k })
|
||||
}))
|
||||
|
||||
// mock @cherrystudio/ui Button component
|
||||
vi.mock('@cherrystudio/ui', () => ({
|
||||
Button: ({ children, onPress, ...props }: any) => (
|
||||
<button type="button" onClick={onPress} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}))
|
||||
|
||||
describe('ExpandableText', () => {
|
||||
const TEXT = 'This is a long text for testing.'
|
||||
|
||||
|
||||
@@ -23,6 +23,16 @@ const mocks = vi.hoisted(() => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock antd components to prevent flaky snapshot tests
|
||||
vi.mock('antd', () => {
|
||||
const MockSpaceCompact: React.FC<React.PropsWithChildren<{ style?: React.CSSProperties }>> = ({
|
||||
|
||||
@@ -65,7 +65,7 @@ const NavbarContainer = styled.div<{ $isFullScreen: boolean }>`
|
||||
min-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: ${isMac ? 'env(titlebar-area-height)' : 'var(--navbar-height)'};
|
||||
min-height: ${({ $isFullScreen }) => (!$isFullScreen && isMac ? 'env(titlebar-area-height)' : 'var(--navbar-height)')};
|
||||
max-height: var(--navbar-height);
|
||||
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1 + 2px)' : 0};
|
||||
padding-left: ${({ $isFullScreen }) =>
|
||||
|
||||
@@ -18,6 +18,15 @@ describe('Qwen Model Detection', () => {
|
||||
vi.mock('@renderer/services/AssistantService', () => ({
|
||||
getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryai' })
|
||||
}))
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
})
|
||||
test('isQwenReasoningModel', () => {
|
||||
expect(isQwenReasoningModel({ id: 'qwen3-thinking' } as Model)).toBe(true)
|
||||
|
||||
@@ -2,6 +2,16 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { isDoubaoSeedAfter251015, isDoubaoThinkingAutoModel, isLingReasoningModel } from '../models/reasoning'
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// FIXME: Idk why it's imported. Maybe circular dependency somewhere
|
||||
vi.mock('@renderer/services/AssistantService.ts', () => ({
|
||||
getDefaultAssistant: () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ClaudeAvatar from '@renderer/assets/images/models/claude.png'
|
||||
import type { AgentBase, AgentType } from '@renderer/types'
|
||||
import type { PermissionModeCard } from '@renderer/types/agent'
|
||||
|
||||
// base agent config. no default config for now.
|
||||
const DEFAULT_AGENT_CONFIG: Omit<AgentBase, 'model'> = {
|
||||
@@ -19,3 +20,47 @@ export const getAgentTypeAvatar = (type: AgentType): string => {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export const permissionModeCards: PermissionModeCard[] = [
|
||||
{
|
||||
mode: 'default',
|
||||
// t('agent.settings.tooling.permissionMode.default.title')
|
||||
titleKey: 'agent.settings.tooling.permissionMode.default.title',
|
||||
titleFallback: 'Default (ask before continuing)',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.default.description',
|
||||
descriptionFallback: 'Read-only tools are pre-approved; everything else still needs permission.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.default.behavior',
|
||||
behaviorFallback: 'Read-only tools are pre-approved automatically.'
|
||||
},
|
||||
{
|
||||
mode: 'plan',
|
||||
// t('agent.settings.tooling.permissionMode.plan.title')
|
||||
titleKey: 'agent.settings.tooling.permissionMode.plan.title',
|
||||
titleFallback: 'Planning mode',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.plan.description',
|
||||
descriptionFallback: 'Shares the default read-only tool set but presents a plan before execution.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.plan.behavior',
|
||||
behaviorFallback: 'Read-only defaults are pre-approved while execution remains disabled.'
|
||||
},
|
||||
{
|
||||
mode: 'acceptEdits',
|
||||
// t('agent.settings.tooling.permissionMode.acceptEdits.title')
|
||||
titleKey: 'agent.settings.tooling.permissionMode.acceptEdits.title',
|
||||
titleFallback: 'Auto-accept file edits',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.acceptEdits.description',
|
||||
descriptionFallback: 'File edits and filesystem operations are automatically approved.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.acceptEdits.behavior',
|
||||
behaviorFallback: 'Pre-approves trusted filesystem tools so edits run immediately.'
|
||||
},
|
||||
{
|
||||
mode: 'bypassPermissions',
|
||||
// t('agent.settings.tooling.permissionMode.bypassPermissions.title')
|
||||
titleKey: 'agent.settings.tooling.permissionMode.bypassPermissions.title',
|
||||
titleFallback: 'Bypass permission checks',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.bypassPermissions.description',
|
||||
descriptionFallback: 'All permission prompts are skipped — use with caution.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.bypassPermissions.behavior',
|
||||
behaviorFallback: 'Every tool is pre-approved automatically.',
|
||||
caution: true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1741,6 +1741,7 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
||||
id: 'DeepSeek-R1',
|
||||
provider: 'cephalon',
|
||||
name: 'DeepSeek-R1满血版',
|
||||
capabilities: [{ type: 'reasoning' }],
|
||||
group: 'DeepSeek'
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import type { Model } from '@renderer/types'
|
||||
import { SystemProviderIds } from '@renderer/types'
|
||||
import { getLowerBaseModelName, isUserSelectedModelType } from '@renderer/utils'
|
||||
|
||||
import { isGeminiProvider, isNewApiProvider, isOpenAICompatibleProvider, isOpenAIProvider } from '../providers'
|
||||
import { isEmbeddingModel, isRerankModel } from './embedding'
|
||||
import { isAnthropicModel } from './utils'
|
||||
import { isPureGenerateImageModel, isTextToImageModel } from './vision'
|
||||
@@ -65,12 +67,16 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
|
||||
const modelId = getLowerBaseModelName(model.id, '/')
|
||||
|
||||
// 不管哪个供应商都判断了
|
||||
if (isAnthropicModel(model)) {
|
||||
// bedrock和vertex不支持
|
||||
if (
|
||||
isAnthropicModel(model) &&
|
||||
(provider.id === SystemProviderIds['aws-bedrock'] || provider.id === SystemProviderIds.vertexai)
|
||||
) {
|
||||
return CLAUDE_SUPPORTED_WEBSEARCH_REGEX.test(modelId)
|
||||
}
|
||||
|
||||
if (provider.type === 'openai-response') {
|
||||
// TODO: 当其他供应商采用Response端点时,这个地方逻辑需要改进
|
||||
if (isOpenAIProvider(provider)) {
|
||||
if (isOpenAIWebSearchModel(model)) {
|
||||
return true
|
||||
}
|
||||
@@ -78,11 +84,11 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
if (provider.id === 'perplexity') {
|
||||
if (provider.id === SystemProviderIds.perplexity) {
|
||||
return PERPLEXITY_SEARCH_MODELS.includes(modelId)
|
||||
}
|
||||
|
||||
if (provider.id === 'aihubmix') {
|
||||
if (provider.id === SystemProviderIds.aihubmix) {
|
||||
// modelId 不以-search结尾
|
||||
if (!modelId.endsWith('-search') && GEMINI_SEARCH_REGEX.test(modelId)) {
|
||||
return true
|
||||
@@ -95,13 +101,13 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
if (provider?.type === 'openai') {
|
||||
if (isOpenAICompatibleProvider(provider) || isNewApiProvider(provider)) {
|
||||
if (GEMINI_SEARCH_REGEX.test(modelId) || isOpenAIWebSearchModel(model)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (provider.id === 'gemini' || provider?.type === 'gemini' || provider.type === 'vertexai') {
|
||||
if (isGeminiProvider(provider) || provider.id === SystemProviderIds.vertexai) {
|
||||
return GEMINI_SEARCH_REGEX.test(modelId)
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,182 @@
|
||||
// All config are migrated to @shared/config/ocr
|
||||
import type {
|
||||
BuiltinOcrProvider,
|
||||
BuiltinOcrProviderId,
|
||||
OcrOvProvider,
|
||||
OcrPpocrProvider,
|
||||
OcrProviderCapability,
|
||||
OcrSystemProvider,
|
||||
OcrTesseractProvider,
|
||||
TesseractLangCode,
|
||||
TranslateLanguageCode
|
||||
} from '@renderer/types'
|
||||
|
||||
import { isMac, isWin } from './constant'
|
||||
|
||||
const tesseract: OcrTesseractProvider = {
|
||||
id: 'tesseract',
|
||||
name: 'Tesseract',
|
||||
capabilities: {
|
||||
image: true
|
||||
},
|
||||
config: {
|
||||
langs: {
|
||||
chi_sim: true,
|
||||
chi_tra: true,
|
||||
eng: true
|
||||
}
|
||||
}
|
||||
} as const
|
||||
|
||||
const systemOcr: OcrSystemProvider = {
|
||||
id: 'system',
|
||||
name: 'System',
|
||||
config: {
|
||||
langs: isWin ? ['en-us'] : undefined
|
||||
},
|
||||
capabilities: {
|
||||
image: true
|
||||
// pdf: true
|
||||
}
|
||||
} as const satisfies OcrSystemProvider
|
||||
|
||||
const ppocrOcr: OcrPpocrProvider = {
|
||||
id: 'paddleocr',
|
||||
name: 'PaddleOCR',
|
||||
config: {
|
||||
apiUrl: ''
|
||||
},
|
||||
capabilities: {
|
||||
image: true
|
||||
// pdf: true
|
||||
}
|
||||
} as const
|
||||
|
||||
const ovOcr: OcrOvProvider = {
|
||||
id: 'ovocr',
|
||||
name: 'Intel OV(NPU) OCR',
|
||||
config: {
|
||||
langs: isWin ? ['en-us', 'zh-cn'] : undefined
|
||||
},
|
||||
capabilities: {
|
||||
image: true
|
||||
// pdf: true
|
||||
}
|
||||
} as const satisfies OcrOvProvider
|
||||
|
||||
export const BUILTIN_OCR_PROVIDERS_MAP = {
|
||||
tesseract,
|
||||
system: systemOcr,
|
||||
paddleocr: ppocrOcr,
|
||||
ovocr: ovOcr
|
||||
} as const satisfies Record<BuiltinOcrProviderId, BuiltinOcrProvider>
|
||||
|
||||
export const BUILTIN_OCR_PROVIDERS: BuiltinOcrProvider[] = Object.values(BUILTIN_OCR_PROVIDERS_MAP)
|
||||
|
||||
export const DEFAULT_OCR_PROVIDER = {
|
||||
image: isWin || isMac ? systemOcr : tesseract
|
||||
} as const satisfies Record<OcrProviderCapability, BuiltinOcrProvider>
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ export function getPreprocessProviderLogo(providerId: PreprocessProviderId) {
|
||||
return MistralLogo
|
||||
case 'mineru':
|
||||
return MinerULogo
|
||||
case 'open-mineru':
|
||||
return MinerULogo
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
@@ -36,5 +38,11 @@ export const PREPROCESS_PROVIDER_CONFIG: Record<PreprocessProviderId, Preprocess
|
||||
official: 'https://mineru.net/',
|
||||
apiKey: 'https://mineru.net/apiManage'
|
||||
}
|
||||
},
|
||||
'open-mineru': {
|
||||
websites: {
|
||||
official: 'https://github.com/opendatalab/MinerU/',
|
||||
apiKey: 'https://github.com/opendatalab/MinerU/'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,14 @@ import VoyageAIProviderLogo from '@renderer/assets/images/providers/voyageai.png
|
||||
import XirangProviderLogo from '@renderer/assets/images/providers/xirang.png'
|
||||
import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
||||
import type { AtLeast, Provider, ProviderType, SystemProvider, SystemProviderId } from '@renderer/types'
|
||||
import type {
|
||||
AtLeast,
|
||||
AzureOpenAIProvider,
|
||||
Provider,
|
||||
ProviderType,
|
||||
SystemProvider,
|
||||
SystemProviderId
|
||||
} from '@renderer/types'
|
||||
import { isSystemProvider, OpenAIServiceTiers } from '@renderer/types'
|
||||
|
||||
import { TOKENFLUX_HOST } from './constant'
|
||||
@@ -348,7 +355,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
name: 'VertexAI',
|
||||
type: 'vertexai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://aiplatform.googleapis.com',
|
||||
apiHost: '',
|
||||
models: SYSTEM_MODELS.vertexai,
|
||||
isSystem: true,
|
||||
enabled: false,
|
||||
@@ -412,7 +419,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
|
||||
anthropicApiHost: 'https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy',
|
||||
anthropicApiHost: 'https://dashscope.aliyuncs.com/apps/anthropic',
|
||||
models: SYSTEM_MODELS.dashscope,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
@@ -1288,7 +1295,7 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
||||
},
|
||||
vertexai: {
|
||||
api: {
|
||||
url: 'https://console.cloud.google.com/apis/api/aiplatform.googleapis.com/overview'
|
||||
url: ''
|
||||
},
|
||||
websites: {
|
||||
official: 'https://cloud.google.com/vertex-ai',
|
||||
@@ -1368,7 +1375,8 @@ const NOT_SUPPORT_ARRAY_CONTENT_PROVIDERS = [
|
||||
'baichuan',
|
||||
'minimax',
|
||||
'xirang',
|
||||
'poe'
|
||||
'poe',
|
||||
'cephalon'
|
||||
] as const satisfies SystemProviderId[]
|
||||
|
||||
/**
|
||||
@@ -1433,10 +1441,15 @@ export const isSupportServiceTierProvider = (provider: Provider) => {
|
||||
)
|
||||
}
|
||||
|
||||
const SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES = ['gemini', 'vertexai'] as const satisfies ProviderType[]
|
||||
const SUPPORT_URL_CONTEXT_PROVIDER_TYPES = [
|
||||
'gemini',
|
||||
'vertexai',
|
||||
'anthropic',
|
||||
'new-api'
|
||||
] as const satisfies ProviderType[]
|
||||
|
||||
export const isSupportUrlContextProvider = (provider: Provider) => {
|
||||
return SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type)
|
||||
return SUPPORT_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type)
|
||||
}
|
||||
|
||||
const SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS = ['gemini', 'vertexai'] as const satisfies SystemProviderId[]
|
||||
@@ -1449,3 +1462,37 @@ export const isGeminiWebSearchProvider = (provider: Provider) => {
|
||||
export const isNewApiProvider = (provider: Provider) => {
|
||||
return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api'
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 OpenAI 兼容的提供商
|
||||
* @param {Provider} provider 提供商对象
|
||||
* @returns {boolean} 是否为 OpenAI 兼容提供商
|
||||
*/
|
||||
export function isOpenAICompatibleProvider(provider: Provider): boolean {
|
||||
return ['openai', 'new-api', 'mistral'].includes(provider.type)
|
||||
}
|
||||
|
||||
export function isAzureOpenAIProvider(provider: Provider): provider is AzureOpenAIProvider {
|
||||
return provider.type === 'azure-openai'
|
||||
}
|
||||
|
||||
export function isOpenAIProvider(provider: Provider): boolean {
|
||||
return provider.type === 'openai-response'
|
||||
}
|
||||
|
||||
export function isAnthropicProvider(provider: Provider): boolean {
|
||||
return provider.type === 'anthropic'
|
||||
}
|
||||
|
||||
export function isGeminiProvider(provider: Provider): boolean {
|
||||
return provider.type === 'gemini'
|
||||
}
|
||||
|
||||
const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot'] as const satisfies SystemProviderId[]
|
||||
|
||||
export const isSupportAPIVersionProvider = (provider: Provider) => {
|
||||
if (isSystemProvider(provider)) {
|
||||
return !NOT_SUPPORT_API_VERSION_PROVIDERS.some((pid) => pid === provider.id)
|
||||
}
|
||||
return provider.apiOptions?.isNotSupportAPIVersion !== false
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { PermissionMode } from '@renderer/types'
|
||||
|
||||
export type PermissionModeCard = {
|
||||
mode: PermissionMode
|
||||
titleKey: string
|
||||
titleFallback: string
|
||||
descriptionKey: string
|
||||
descriptionFallback: string
|
||||
behaviorKey: string
|
||||
behaviorFallback: string
|
||||
caution?: boolean
|
||||
unsupported?: boolean
|
||||
}
|
||||
|
||||
export const permissionModeCards: PermissionModeCard[] = [
|
||||
{
|
||||
mode: 'default',
|
||||
titleKey: 'agent.settings.tooling.permissionMode.default.title',
|
||||
titleFallback: 'Default (ask before continuing)',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.default.description',
|
||||
descriptionFallback: 'Read-only tools are pre-approved; everything else still needs permission.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.default.behavior',
|
||||
behaviorFallback: 'Read-only tools are pre-approved automatically.'
|
||||
},
|
||||
{
|
||||
mode: 'plan',
|
||||
titleKey: 'agent.settings.tooling.permissionMode.plan.title',
|
||||
titleFallback: 'Planning mode',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.plan.description',
|
||||
descriptionFallback: 'Shares the default read-only tool set but presents a plan before execution.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.plan.behavior',
|
||||
behaviorFallback: 'Read-only defaults are pre-approved while execution remains disabled.'
|
||||
},
|
||||
{
|
||||
mode: 'acceptEdits',
|
||||
titleKey: 'agent.settings.tooling.permissionMode.acceptEdits.title',
|
||||
titleFallback: 'Auto-accept file edits',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.acceptEdits.description',
|
||||
descriptionFallback: 'File edits and filesystem operations are automatically approved.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.acceptEdits.behavior',
|
||||
behaviorFallback: 'Pre-approves trusted filesystem tools so edits run immediately.'
|
||||
},
|
||||
{
|
||||
mode: 'bypassPermissions',
|
||||
titleKey: 'agent.settings.tooling.permissionMode.bypassPermissions.title',
|
||||
titleFallback: 'Bypass permission checks',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.bypassPermissions.description',
|
||||
descriptionFallback: 'All permission prompts are skipped — use with caution.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.bypassPermissions.behavior',
|
||||
behaviorFallback: 'Every tool is pre-approved automatically.',
|
||||
caution: true
|
||||
}
|
||||
]
|
||||
10
src/renderer/src/env.d.ts
vendored
10
src/renderer/src/env.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import type { ToastUtilities } from '@cherrystudio/ui'
|
||||
import type { HookAPI } from 'antd/es/modal/useModal'
|
||||
import type { NavigateFunction } from 'react-router-dom'
|
||||
@@ -19,5 +20,14 @@ declare global {
|
||||
store: any
|
||||
navigate: NavigateFunction
|
||||
toast: ToastUtilities
|
||||
agentTools: {
|
||||
respondToPermission: (payload: {
|
||||
requestId: string
|
||||
behavior: 'allow' | 'deny'
|
||||
updatedInput?: Record<string, unknown>
|
||||
message?: string
|
||||
updatedPermissions?: PermissionUpdate[]
|
||||
}) => Promise<{ success: boolean }>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
src/renderer/src/hooks/agents/useCreateDefaultSession.ts
Normal file
49
src/renderer/src/hooks/agents/useCreateDefaultSession.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveSessionIdAction, setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import type { CreateSessionForm } from '@renderer/types'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Returns a stable callback that creates a default agent session and updates UI state.
|
||||
*/
|
||||
export const useCreateDefaultSession = (agentId: string | null) => {
|
||||
const { agent } = useAgent(agentId)
|
||||
const { createSession } = useSessions(agentId)
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
const [creatingSession, setCreatingSession] = useState(false)
|
||||
|
||||
const createDefaultSession = useCallback(async () => {
|
||||
if (!agentId || !agent || creatingSession) {
|
||||
return null
|
||||
}
|
||||
|
||||
setCreatingSession(true)
|
||||
try {
|
||||
const session = {
|
||||
...agent,
|
||||
id: undefined,
|
||||
name: t('common.unnamed')
|
||||
} satisfies CreateSessionForm
|
||||
|
||||
const created = await createSession(session)
|
||||
|
||||
if (created) {
|
||||
dispatch(setActiveSessionIdAction({ agentId, sessionId: created.id }))
|
||||
dispatch(setActiveTopicOrSessionAction('session'))
|
||||
}
|
||||
|
||||
return created
|
||||
} finally {
|
||||
setCreatingSession(false)
|
||||
}
|
||||
}, [agentId, agent, createSession, creatingSession, dispatch, t])
|
||||
|
||||
return {
|
||||
createDefaultSession,
|
||||
creatingSession
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { usePreference } from '@data/hooks/usePreference'
|
||||
|
||||
import { useOcrProvider } from './useOcrProvider'
|
||||
|
||||
export const useOcrImageProvider = () => {
|
||||
const [imageProviderId, setImageProviderId] = usePreference('ocr.settings.image_provider_id')
|
||||
const { provider: imageProvider, mutating, loading, error, updateConfig } = useOcrProvider(imageProviderId)
|
||||
return { imageProvider, loading, mutating, error, updateConfig, imageProviderId, setImageProviderId }
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useMutation, useQuery } from '@data/hooks/useDataApi'
|
||||
import type { OcrProviderConfig } from '@renderer/types'
|
||||
import { getErrorMessage } from '@renderer/utils'
|
||||
import type { ConcreteApiPaths } from '@shared/data/api'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
// const logger = loggerService.withContext('useOcrProvider')
|
||||
|
||||
export const useOcrProvider = (id: string | null) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const path: ConcreteApiPaths = `/ocr/providers/${id}`
|
||||
const { data, loading, error } = useQuery(path)
|
||||
const { mutate, loading: mutating } = useMutation('PATCH', path)
|
||||
|
||||
const updateConfig = useCallback(
|
||||
async (update: Partial<OcrProviderConfig>) => {
|
||||
if (!id) return
|
||||
try {
|
||||
await mutate({ body: { id, config: update } })
|
||||
} catch (e) {
|
||||
window.toast.error({ title: t('ocr.provider.config.patch.error.failed'), description: getErrorMessage(e) })
|
||||
}
|
||||
},
|
||||
[id, mutate, t]
|
||||
)
|
||||
|
||||
return {
|
||||
/** undefined: loading; null: invalid, id is null */
|
||||
provider: id ? data?.data : null,
|
||||
loading,
|
||||
mutating,
|
||||
error,
|
||||
updateConfig
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useQuery } from '@data/hooks/useDataApi'
|
||||
import { getBuiltinOcrProviderLabel } from '@renderer/i18n/label'
|
||||
import type { ListOcrProvidersQuery, OcrProvider } from '@renderer/types'
|
||||
import { isBuiltinOcrProvider } from '@renderer/types'
|
||||
|
||||
export const useOcrProviders = (query?: ListOcrProvidersQuery) => {
|
||||
const { data, loading, error } = useQuery('/ocr/providers', { query })
|
||||
|
||||
const getOcrProviderName = (p: OcrProvider) => {
|
||||
return isBuiltinOcrProvider(p) ? getBuiltinOcrProviderLabel(p.id) : p.name
|
||||
}
|
||||
|
||||
return {
|
||||
providers: data?.data,
|
||||
loading,
|
||||
error,
|
||||
getOcrProviderName
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,18 @@ import { useAppDispatch } from '@renderer/store'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { handleSaveData } from '@renderer/store'
|
||||
import { selectMemoryConfig } from '@renderer/store/memory'
|
||||
import {
|
||||
type ToolPermissionRequestPayload,
|
||||
type ToolPermissionResultPayload,
|
||||
toolPermissionsActions
|
||||
} from '@renderer/store/toolPermissions'
|
||||
import { delay, runAsyncFunction } from '@renderer/utils'
|
||||
import { checkDataLimit } from '@renderer/utils'
|
||||
import { defaultLanguage } from '@shared/config/constant'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useDefaultModel } from './useAssistant'
|
||||
import useFullScreenNotice from './useFullScreenNotice'
|
||||
@@ -27,6 +33,7 @@ import { useNavbarPosition } from './useNavbar'
|
||||
const logger = loggerService.withContext('useAppInit')
|
||||
|
||||
export function useAppInit() {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const [language] = usePreference('app.language')
|
||||
const [windowStyle] = usePreference('ui.window_style')
|
||||
@@ -148,6 +155,64 @@ export function useAppInit() {
|
||||
}
|
||||
}, [customCss])
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.electron?.ipcRenderer) return
|
||||
|
||||
const requestListener = (_event: Electron.IpcRendererEvent, payload: ToolPermissionRequestPayload) => {
|
||||
logger.debug('Renderer received tool permission request', {
|
||||
requestId: payload.requestId,
|
||||
toolName: payload.toolName,
|
||||
expiresAt: payload.expiresAt,
|
||||
suggestionCount: payload.suggestions.length
|
||||
})
|
||||
dispatch(toolPermissionsActions.requestReceived(payload))
|
||||
}
|
||||
|
||||
const resultListener = (_event: Electron.IpcRendererEvent, payload: ToolPermissionResultPayload) => {
|
||||
logger.debug('Renderer received tool permission result', {
|
||||
requestId: payload.requestId,
|
||||
behavior: payload.behavior,
|
||||
reason: payload.reason
|
||||
})
|
||||
dispatch(toolPermissionsActions.requestResolved(payload))
|
||||
|
||||
if (payload.behavior === 'deny') {
|
||||
const message =
|
||||
payload.reason === 'timeout'
|
||||
? (payload.message ?? t('agent.toolPermission.toast.timeout'))
|
||||
: (payload.message ?? t('agent.toolPermission.toast.denied'))
|
||||
|
||||
if (payload.reason === 'no-window') {
|
||||
logger.debug('Displaying deny toast for tool permission', {
|
||||
requestId: payload.requestId,
|
||||
behavior: payload.behavior,
|
||||
reason: payload.reason
|
||||
})
|
||||
window.toast?.error?.(message)
|
||||
} else if (payload.reason === 'timeout') {
|
||||
logger.debug('Displaying timeout toast for tool permission', {
|
||||
requestId: payload.requestId
|
||||
})
|
||||
window.toast?.warning?.(message)
|
||||
} else {
|
||||
logger.debug('Displaying info toast for tool permission deny', {
|
||||
requestId: payload.requestId,
|
||||
reason: payload.reason
|
||||
})
|
||||
window.toast?.info?.(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Request, requestListener)
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
|
||||
return () => {
|
||||
window.electron?.ipcRenderer.removeListener(IpcChannel.AgentToolPermission_Request, requestListener)
|
||||
window.electron?.ipcRenderer.removeListener(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
}
|
||||
}, [dispatch, t])
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: init data collection
|
||||
}, [enableDataCollection])
|
||||
|
||||
@@ -57,7 +57,7 @@ export const useKnowledgeBaseForm = (base?: KnowledgeBase) => {
|
||||
label: t('settings.tool.preprocess.provider'),
|
||||
title: t('settings.tool.preprocess.provider'),
|
||||
options: preprocessProviders
|
||||
.filter((p) => p.apiKey !== '' || p.id === 'mineru')
|
||||
.filter((p) => p.apiKey !== '' || ['mineru', 'open-mineru'].includes(p.id))
|
||||
.map((p) => ({ value: p.id, label: p.name }))
|
||||
}
|
||||
return [preprocessOptions]
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { loggerService } from '@logger'
|
||||
import * as OcrService from '@renderer/services/ocr/OcrService'
|
||||
import type { ImageFileMetadata, OcrProvider, SupportedOcrFile } from '@renderer/types'
|
||||
import type { ImageFileMetadata, SupportedOcrFile } from '@renderer/types'
|
||||
import { isImageFileMetadata } from '@renderer/types'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useOcrImageProvider } from './useOcrImageProvider'
|
||||
import { useOcrProviders } from './useOcrProvider'
|
||||
|
||||
const logger = loggerService.withContext('useOcr')
|
||||
|
||||
const isProviderAvailable = (provider: OcrProvider | undefined | null): provider is OcrProvider =>
|
||||
provider !== undefined
|
||||
|
||||
export const useOcr = () => {
|
||||
const { t } = useTranslation()
|
||||
const { imageProvider, imageProviderId } = useOcrImageProvider()
|
||||
const { imageProvider } = useOcrProviders()
|
||||
|
||||
/**
|
||||
* 对图片文件进行OCR识别
|
||||
@@ -25,16 +22,10 @@ export const useOcr = () => {
|
||||
*/
|
||||
const ocrImage = useCallback(
|
||||
async (image: ImageFileMetadata) => {
|
||||
if (isProviderAvailable(imageProvider)) {
|
||||
logger.debug('ocrImage', { provider: imageProvider })
|
||||
return OcrService.ocr(image, {
|
||||
providerId: imageProvider.id
|
||||
})
|
||||
} else {
|
||||
throw new Error(t('ocr.error.provider.not_availabel', { provider: imageProviderId }))
|
||||
}
|
||||
logger.debug('ocrImage', { config: imageProvider.config })
|
||||
return OcrService.ocr(image, imageProvider)
|
||||
},
|
||||
[imageProvider, imageProviderId, t]
|
||||
[imageProvider]
|
||||
)
|
||||
|
||||
/**
|
||||
148
src/renderer/src/hooks/useOcrProvider.tsx
Normal file
148
src/renderer/src/hooks/useOcrProvider.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Avatar } from '@cherrystudio/ui'
|
||||
import { loggerService } from '@logger'
|
||||
import IntelLogo from '@renderer/assets/images/providers/intel.png'
|
||||
import PaddleocrLogo from '@renderer/assets/images/providers/paddleocr.png'
|
||||
import TesseractLogo from '@renderer/assets/images/providers/Tesseract.js.png'
|
||||
import { BUILTIN_OCR_PROVIDERS_MAP, DEFAULT_OCR_PROVIDER } from '@renderer/config/ocr'
|
||||
import { getBuiltinOcrProviderLabel } from '@renderer/i18n/label'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { addOcrProvider, removeOcrProvider, setImageOcrProviderId, updateOcrProviderConfig } from '@renderer/store/ocr'
|
||||
import type { ImageOcrProvider, OcrProvider, OcrProviderConfig } from '@renderer/types'
|
||||
import { isBuiltinOcrProvider, isBuiltinOcrProviderId, isImageOcrProvider } from '@renderer/types'
|
||||
import { FileQuestionMarkIcon, MonitorIcon } from 'lucide-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
const logger = loggerService.withContext('useOcrProvider')
|
||||
|
||||
export const useOcrProviders = () => {
|
||||
const providers = useAppSelector((state) => state.ocr.providers)
|
||||
const imageProviders = providers.filter(isImageOcrProvider)
|
||||
const imageProviderId = useAppSelector((state) => state.ocr.imageProviderId)
|
||||
const [imageProvider, setImageProvider] = useState<ImageOcrProvider>(DEFAULT_OCR_PROVIDER.image)
|
||||
const dispatch = useDispatch()
|
||||
const { t } = useTranslation()
|
||||
|
||||
/**
|
||||
* 添加一个新的OCR服务提供者
|
||||
* @param provider - OCR提供者对象,包含id和其他配置信息
|
||||
* @throws {Error} 当尝试添加一个已存在ID的提供者时抛出错误
|
||||
*/
|
||||
const addProvider = useCallback(
|
||||
(provider: OcrProvider) => {
|
||||
if (providers.some((p) => p.id === provider.id)) {
|
||||
const msg = `Provider with id ${provider.id} already exists`
|
||||
logger.error(msg)
|
||||
window.toast.error(t('ocr.error.provider.existing'))
|
||||
throw new Error(msg)
|
||||
}
|
||||
dispatch(addOcrProvider(provider))
|
||||
},
|
||||
[dispatch, providers, t]
|
||||
)
|
||||
|
||||
/**
|
||||
* 移除一个OCR服务提供者
|
||||
* @param id - 要移除的OCR提供者ID
|
||||
* @throws {Error} 当尝试移除一个内置提供商时抛出错误
|
||||
*/
|
||||
const removeProvider = (id: string) => {
|
||||
if (isBuiltinOcrProviderId(id)) {
|
||||
const msg = `Cannot remove builtin provider ${id}`
|
||||
logger.error(msg)
|
||||
window.toast.error(t('ocr.error.provider.cannot_remove_builtin'))
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
dispatch(removeOcrProvider(id))
|
||||
}
|
||||
|
||||
const setImageProviderId = useCallback(
|
||||
(id: string) => {
|
||||
dispatch(setImageOcrProviderId(id))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const getOcrProviderName = (p: OcrProvider) => {
|
||||
return isBuiltinOcrProvider(p) ? getBuiltinOcrProviderLabel(p.id) : p.name
|
||||
}
|
||||
|
||||
const OcrProviderLogo = ({ provider: p, size = 14 }: { provider: OcrProvider; size?: number }) => {
|
||||
if (isBuiltinOcrProvider(p)) {
|
||||
switch (p.id) {
|
||||
case 'tesseract':
|
||||
return <Avatar src={TesseractLogo} style={{ width: size, height: size }} />
|
||||
case 'system':
|
||||
return <MonitorIcon size={size} />
|
||||
case 'paddleocr':
|
||||
return <Avatar src={PaddleocrLogo} style={{ width: size, height: size }} />
|
||||
case 'ovocr':
|
||||
return <Avatar src={IntelLogo} style={{ width: size, height: size }} />
|
||||
}
|
||||
}
|
||||
return <FileQuestionMarkIcon size={size} />
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const actualImageProvider = imageProviders.find((p) => p.id === imageProviderId)
|
||||
if (!actualImageProvider) {
|
||||
if (isBuiltinOcrProviderId(imageProviderId)) {
|
||||
logger.warn(`Builtin ocr provider ${imageProviderId} not exist. Will add it to providers.`)
|
||||
addProvider(BUILTIN_OCR_PROVIDERS_MAP[imageProviderId])
|
||||
}
|
||||
setImageProviderId(DEFAULT_OCR_PROVIDER.image.id)
|
||||
setImageProvider(DEFAULT_OCR_PROVIDER.image)
|
||||
} else {
|
||||
setImageProviderId(actualImageProvider.id)
|
||||
setImageProvider(actualImageProvider)
|
||||
}
|
||||
}, [addProvider, imageProviderId, imageProviders, setImageProviderId])
|
||||
|
||||
return {
|
||||
providers,
|
||||
imageProvider,
|
||||
addProvider,
|
||||
removeProvider,
|
||||
setImageProviderId,
|
||||
getOcrProviderName,
|
||||
OcrProviderLogo
|
||||
}
|
||||
}
|
||||
|
||||
export const useOcrProvider = (id: string) => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useDispatch()
|
||||
const { providers, addProvider } = useOcrProviders()
|
||||
let provider = providers.find((p) => p.id === id)
|
||||
|
||||
// safely fallback
|
||||
if (!provider) {
|
||||
logger.error(`Ocr Provider ${id} not found`)
|
||||
window.toast.error(t('ocr.error.provider.not_found'))
|
||||
if (isBuiltinOcrProviderId(id)) {
|
||||
try {
|
||||
addProvider(BUILTIN_OCR_PROVIDERS_MAP[id])
|
||||
} catch (e) {
|
||||
logger.warn(`Add ${BUILTIN_OCR_PROVIDERS_MAP[id].name} failed. Just use temp provider from config.`)
|
||||
window.toast.warning(t('ocr.warning.provider.fallback', { name: BUILTIN_OCR_PROVIDERS_MAP[id].name }))
|
||||
} finally {
|
||||
provider = BUILTIN_OCR_PROVIDERS_MAP[id]
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Fallback to tesseract`)
|
||||
window.toast.warning(t('ocr.warning.provider.fallback', { name: 'Tesseract' }))
|
||||
provider = BUILTIN_OCR_PROVIDERS_MAP.tesseract
|
||||
}
|
||||
}
|
||||
|
||||
const updateConfig = (update: Partial<OcrProviderConfig>) => {
|
||||
dispatch(updateOcrProviderConfig({ id: provider.id, update }))
|
||||
}
|
||||
|
||||
return {
|
||||
provider,
|
||||
updateConfig
|
||||
}
|
||||
}
|
||||
163
src/renderer/src/hooks/usePlugins.ts
Normal file
163
src/renderer/src/hooks/usePlugins.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import type { InstalledPlugin, PluginError, PluginMetadata } from '@renderer/types/plugin'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Helper to extract error message from PluginError union type
|
||||
*/
|
||||
function getPluginErrorMessage(error: PluginError, defaultMessage: string): string {
|
||||
if ('message' in error && error.message) return error.message
|
||||
if ('reason' in error) return error.reason
|
||||
if ('path' in error) return `Error with file: ${error.path}`
|
||||
return defaultMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch and cache available plugins from the resources directory
|
||||
* @returns Object containing available agents, commands, skills, loading state, and error
|
||||
*/
|
||||
export function useAvailablePlugins() {
|
||||
const [agents, setAgents] = useState<PluginMetadata[]>([])
|
||||
const [commands, setCommands] = useState<PluginMetadata[]>([])
|
||||
const [skills, setSkills] = useState<PluginMetadata[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAvailablePlugins = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await window.api.claudeCodePlugin.listAvailable()
|
||||
|
||||
if (result.success) {
|
||||
setAgents(result.data.agents)
|
||||
setCommands(result.data.commands)
|
||||
setSkills(result.data.skills)
|
||||
} else {
|
||||
setError(getPluginErrorMessage(result.error, 'Failed to load available plugins'))
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error occurred')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchAvailablePlugins()
|
||||
}, [])
|
||||
|
||||
return { agents, commands, skills, loading, error }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch installed plugins for a specific agent
|
||||
* @param agentId - The ID of the agent to fetch plugins for
|
||||
* @returns Object containing installed plugins, loading state, error, and refresh function
|
||||
*/
|
||||
export function useInstalledPlugins(agentId: string | undefined) {
|
||||
const [plugins, setPlugins] = useState<InstalledPlugin[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!agentId) {
|
||||
setPlugins([])
|
||||
setLoading(false)
|
||||
setError(null)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await window.api.claudeCodePlugin.listInstalled(agentId)
|
||||
|
||||
if (result.success) {
|
||||
setPlugins(result.data)
|
||||
} else {
|
||||
setError(getPluginErrorMessage(result.error, 'Failed to load installed plugins'))
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error occurred')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [agentId])
|
||||
|
||||
useEffect(() => {
|
||||
refresh()
|
||||
}, [refresh])
|
||||
|
||||
return { plugins, loading, error, refresh }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to provide install and uninstall actions for plugins
|
||||
* @param agentId - The ID of the agent to perform actions for
|
||||
* @param onSuccess - Optional callback to be called on successful operations
|
||||
* @returns Object containing install, uninstall functions and their loading states
|
||||
*/
|
||||
export function usePluginActions(agentId: string, onSuccess?: () => void) {
|
||||
const [installing, setInstalling] = useState<boolean>(false)
|
||||
const [uninstalling, setUninstalling] = useState<boolean>(false)
|
||||
|
||||
const install = useCallback(
|
||||
async (sourcePath: string, type: 'agent' | 'command' | 'skill') => {
|
||||
setInstalling(true)
|
||||
|
||||
try {
|
||||
const result = await window.api.claudeCodePlugin.install({
|
||||
agentId,
|
||||
sourcePath,
|
||||
type
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
onSuccess?.()
|
||||
return { success: true as const, data: result.data }
|
||||
} else {
|
||||
const errorMessage = getPluginErrorMessage(result.error, 'Failed to install plugin')
|
||||
return { success: false as const, error: errorMessage }
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
|
||||
return { success: false as const, error: errorMessage }
|
||||
} finally {
|
||||
setInstalling(false)
|
||||
}
|
||||
},
|
||||
[agentId, onSuccess]
|
||||
)
|
||||
|
||||
const uninstall = useCallback(
|
||||
async (filename: string, type: 'agent' | 'command' | 'skill') => {
|
||||
setUninstalling(true)
|
||||
|
||||
try {
|
||||
const result = await window.api.claudeCodePlugin.uninstall({
|
||||
agentId,
|
||||
filename,
|
||||
type
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
onSuccess?.()
|
||||
return { success: true as const }
|
||||
} else {
|
||||
const errorMessage = getPluginErrorMessage(result.error, 'Failed to uninstall plugin')
|
||||
return { success: false as const, error: errorMessage }
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
|
||||
return { success: false as const, error: errorMessage }
|
||||
} finally {
|
||||
setUninstalling(false)
|
||||
}
|
||||
},
|
||||
[agentId, onSuccess]
|
||||
)
|
||||
|
||||
return { install, uninstall, installing, uninstalling }
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export function getVertexAIServiceAccount() {
|
||||
* 类型守卫:检查 Provider 是否为 VertexProvider
|
||||
*/
|
||||
export function isVertexProvider(provider: Provider): provider is VertexProvider {
|
||||
return provider.type === 'vertexai' && 'googleCredentials' in provider
|
||||
return provider.type === 'vertexai'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -143,7 +143,8 @@ const titleKeyMap = {
|
||||
notes: 'title.notes',
|
||||
paintings: 'title.paintings',
|
||||
settings: 'title.settings',
|
||||
translate: 'title.translate'
|
||||
translate: 'title.translate',
|
||||
terminal: 'title.terminal'
|
||||
} as const
|
||||
|
||||
export const getTitleLabel = (key: string): string => {
|
||||
|
||||
@@ -107,6 +107,50 @@
|
||||
"title": "Advanced Settings"
|
||||
},
|
||||
"essential": "Essential Settings",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Available Plugins"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "Are you sure you want to uninstall this plugin?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "No plugins found matching your filters. Try adjusting your search or category filters."
|
||||
},
|
||||
"error": {
|
||||
"install": "Failed to install plugin",
|
||||
"load": "Failed to load plugins",
|
||||
"uninstall": "Failed to uninstall plugin"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All Categories"
|
||||
},
|
||||
"install": "Install",
|
||||
"installed": {
|
||||
"empty": "No plugins installed yet. Browse available plugins to get started.",
|
||||
"title": "Installed Plugins"
|
||||
},
|
||||
"installing": "Installing...",
|
||||
"results": "{{count}} plugin(s) found",
|
||||
"search": {
|
||||
"placeholder": "Search plugins..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Plugin installed successfully",
|
||||
"uninstall": "Plugin uninstalled successfully"
|
||||
},
|
||||
"tab": "Plugins",
|
||||
"type": {
|
||||
"agent": "Agent",
|
||||
"agents": "Agents",
|
||||
"all": "All",
|
||||
"command": "Command",
|
||||
"commands": "Commands",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"uninstall": "Uninstall",
|
||||
"uninstalling": "Uninstalling..."
|
||||
},
|
||||
"prompt": "Prompt Settings",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -191,6 +235,39 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Allow tool request",
|
||||
"denyRequest": "Deny tool request",
|
||||
"hideDetails": "Hide tool details",
|
||||
"runWithOptions": "Run with additional options",
|
||||
"showDetails": "Show tool details"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Cancel",
|
||||
"run": "Run"
|
||||
},
|
||||
"confirmation": "Are you sure you want to run this Claude tool?",
|
||||
"defaultDenyMessage": "User denied permission for this tool.",
|
||||
"defaultDescription": "Executes code or system actions in your environment. Make sure the command looks safe before running it.",
|
||||
"error": {
|
||||
"sendFailed": "Failed to send your decision. Please try again."
|
||||
},
|
||||
"expired": "Expired",
|
||||
"inputPreview": "Tool input preview",
|
||||
"pending": "Pending ({{seconds}}s)",
|
||||
"permissionExpired": "Permission request expired. Waiting for new instructions...",
|
||||
"requiresElevatedPermissions": "This tool requires elevated permissions.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Approving may update multiple session permissions if you chose to always allow this tool.",
|
||||
"permissionUpdateSingle": "Approving may update your session permissions if you chose to always allow this tool."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "Tool request was denied.",
|
||||
"timeout": "Tool request timed out before receiving approval."
|
||||
},
|
||||
"waiting": "Waiting for tool permission decision..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Agent Type",
|
||||
"unknown": "Unknown Type"
|
||||
@@ -2055,7 +2132,6 @@
|
||||
"cannot_remove_builtin": "Cannot delete built-in provider",
|
||||
"existing": "The provider already exists",
|
||||
"get_providers": "Failed to get available providers",
|
||||
"not_availabel": "Provide {{provider}} is not available",
|
||||
"not_found": "OCR provider does not exist",
|
||||
"update_failed": "Failed to update configuration"
|
||||
},
|
||||
@@ -2065,40 +2141,6 @@
|
||||
"not_supported": "Unsupported file type {{type}}"
|
||||
},
|
||||
"processing": "OCR processing...",
|
||||
"provider": {
|
||||
"config": {
|
||||
"patch": {
|
||||
"error": {
|
||||
"failed": "Failed to update config"
|
||||
}
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"error": {
|
||||
"failed": "Failed to create provider"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"error": {
|
||||
"failed": "Failed to delete provider {{provider}}"
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Failed to get provider {{provider}}"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"error": {
|
||||
"failed": "Failed to list providers"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"error": {
|
||||
"failed": "Failed to update the provider"
|
||||
}
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"provider": {
|
||||
"fallback": "Reverted to {{name}}, which may cause issues"
|
||||
@@ -2334,6 +2376,32 @@
|
||||
"seed_tip": "Controls upscaling randomness"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Actions",
|
||||
"agents": "Agents",
|
||||
"all_categories": "All Categories",
|
||||
"all_types": "All",
|
||||
"category": "Category",
|
||||
"commands": "Commands",
|
||||
"confirm_uninstall": "Are you sure you want to uninstall {{name}}?",
|
||||
"install": "Install",
|
||||
"install_plugins_from_browser": "Browse available plugins to get started",
|
||||
"installing": "Installing...",
|
||||
"name": "Name",
|
||||
"no_description": "No description available",
|
||||
"no_installed_plugins": "No plugins installed yet",
|
||||
"no_results": "No plugins found",
|
||||
"search_placeholder": "Search plugins...",
|
||||
"showing_results": "Showing {{count}} plugin",
|
||||
"showing_results_one": "Showing {{count}} plugin",
|
||||
"showing_results_other": "Showing {{count}} plugins",
|
||||
"showing_results_plural": "Showing {{count}} plugins",
|
||||
"skills": "Skills",
|
||||
"try_different_search": "Try adjusting your search or category filters",
|
||||
"type": "Type",
|
||||
"uninstall": "Uninstall",
|
||||
"uninstalling": "Uninstalling..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Copy as image"
|
||||
@@ -4080,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic API Host",
|
||||
"anthropic_api_host_preview": "Anthropic preview: {{url}}",
|
||||
"anthropic_api_host_tip": "Only configure this when your provider exposes an Anthropic-compatible endpoint. Ending with / ignores v1, ending with # forces use of input address.",
|
||||
"anthropic_api_host_tooltip": "Use only when the provider offers a Claude-compatible base URL.",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4125,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Preview: {{url}}",
|
||||
"reset": "Reset",
|
||||
"tip": "Ending with / ignores v1, ending with # forces use of input address"
|
||||
"tip": "ending with # forces use of input address"
|
||||
}
|
||||
},
|
||||
"api_host": "API Host",
|
||||
"api_host_no_valid": "API address is invalid",
|
||||
"api_host_preview": "Preview: {{url}}",
|
||||
"api_host_tooltip": "Override only when your provider requires a custom OpenAI-compatible endpoint.",
|
||||
"api_key": {
|
||||
@@ -4547,6 +4615,7 @@
|
||||
"paintings": "Paintings",
|
||||
"settings": "Settings",
|
||||
"store": "Assistant Library",
|
||||
"terminal": "Terminal",
|
||||
"translate": "Translate"
|
||||
},
|
||||
"trace": {
|
||||
|
||||
@@ -107,6 +107,50 @@
|
||||
"title": "高级设置"
|
||||
},
|
||||
"essential": "基础设置",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "可用插件"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "确定要卸载此插件吗?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "未找到匹配的插件。请尝试调整搜索或类别筛选。"
|
||||
},
|
||||
"error": {
|
||||
"install": "安装插件失败",
|
||||
"load": "加载插件失败",
|
||||
"uninstall": "卸载插件失败"
|
||||
},
|
||||
"filter": {
|
||||
"all": "所有类别"
|
||||
},
|
||||
"install": "安装",
|
||||
"installed": {
|
||||
"empty": "尚未安装任何插件。浏览可用插件以开始使用。",
|
||||
"title": "已安装插件"
|
||||
},
|
||||
"installing": "安装中...",
|
||||
"results": "找到 {{count}} 个插件",
|
||||
"search": {
|
||||
"placeholder": "搜索插件..."
|
||||
},
|
||||
"success": {
|
||||
"install": "插件安装成功",
|
||||
"uninstall": "插件卸载成功"
|
||||
},
|
||||
"tab": "插件",
|
||||
"type": {
|
||||
"agent": "代理",
|
||||
"agents": "代理",
|
||||
"all": "全部",
|
||||
"command": "命令",
|
||||
"commands": "命令",
|
||||
"skills": "技能"
|
||||
},
|
||||
"uninstall": "卸载",
|
||||
"uninstalling": "卸载中..."
|
||||
},
|
||||
"prompt": "提示词设置",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -191,6 +235,39 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "允许工具请求",
|
||||
"denyRequest": "拒绝工具请求",
|
||||
"hideDetails": "隐藏工具详情",
|
||||
"runWithOptions": "带选项运行",
|
||||
"showDetails": "显示工具详情"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "取消",
|
||||
"run": "运行"
|
||||
},
|
||||
"confirmation": "确定要运行此 Claude 工具吗?",
|
||||
"defaultDenyMessage": "用户拒绝了该工具的权限。",
|
||||
"defaultDescription": "在您的环境中执行代码或系统操作。运行前请确保命令安全。",
|
||||
"error": {
|
||||
"sendFailed": "发送您的决定失败,请重试。"
|
||||
},
|
||||
"expired": "已过期",
|
||||
"inputPreview": "工具输入预览",
|
||||
"pending": "等待中 ({{seconds}}秒)",
|
||||
"permissionExpired": "权限请求已过期。等待新指令...",
|
||||
"requiresElevatedPermissions": "此工具需要更高权限。",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "如果您选择总是允许此工具,批准可能会更新多个会话权限。",
|
||||
"permissionUpdateSingle": "如果您选择总是允许此工具,批准可能会更新您的会话权限。"
|
||||
},
|
||||
"toast": {
|
||||
"denied": "工具请求已被拒绝。",
|
||||
"timeout": "工具请求在收到批准前超时。"
|
||||
},
|
||||
"waiting": "等待工具权限决定..."
|
||||
},
|
||||
"type": {
|
||||
"label": "智能体类型",
|
||||
"unknown": "未知类型"
|
||||
@@ -2055,7 +2132,6 @@
|
||||
"cannot_remove_builtin": "不能删除内置提供商",
|
||||
"existing": "提供商已存在",
|
||||
"get_providers": "获取可用提供商失败",
|
||||
"not_availabel": "{{provider}} 暂不可用",
|
||||
"not_found": "OCR 提供商不存在",
|
||||
"update_failed": "更新配置失败"
|
||||
},
|
||||
@@ -2065,40 +2141,6 @@
|
||||
"not_supported": "不支持的文件类型 {{type}}"
|
||||
},
|
||||
"processing": "OCR 处理中...",
|
||||
"provider": {
|
||||
"config": {
|
||||
"patch": {
|
||||
"error": {
|
||||
"failed": "更新配置失败"
|
||||
}
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"error": {
|
||||
"failed": "创建提供商失败"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"error": {
|
||||
"failed": "删除提供商 {{provider}} 失败"
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "获取提供商 {{provider}} 失败"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"error": {
|
||||
"failed": "获取提供商列表失败"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"error": {
|
||||
"failed": "更新提供商失败"
|
||||
}
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"provider": {
|
||||
"fallback": "已回退到 {{name}},这可能导致问题"
|
||||
@@ -2334,6 +2376,32 @@
|
||||
"seed_tip": "控制放大结果的随机性"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "操作",
|
||||
"agents": "代理",
|
||||
"all_categories": "所有类别",
|
||||
"all_types": "全部",
|
||||
"category": "类别",
|
||||
"commands": "命令",
|
||||
"confirm_uninstall": "确定要卸载 {{name}} 吗?",
|
||||
"install": "安装",
|
||||
"install_plugins_from_browser": "浏览可用插件以开始使用",
|
||||
"installing": "安装中...",
|
||||
"name": "名称",
|
||||
"no_description": "无描述",
|
||||
"no_installed_plugins": "尚未安装任何插件",
|
||||
"no_results": "未找到插件",
|
||||
"search_placeholder": "搜索插件...",
|
||||
"showing_results": "显示 {{count}} 个插件",
|
||||
"showing_results_one": "显示 {{count}} 个插件",
|
||||
"showing_results_other": "显示 {{count}} 个插件",
|
||||
"showing_results_plural": "显示 {{count}} 个插件",
|
||||
"skills": "技能",
|
||||
"try_different_search": "请尝试调整搜索或类别筛选",
|
||||
"type": "类型",
|
||||
"uninstall": "卸载",
|
||||
"uninstalling": "卸载中..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "复制为图片"
|
||||
@@ -4080,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic API 地址",
|
||||
"anthropic_api_host_preview": "Anthropic 预览:{{url}}",
|
||||
"anthropic_api_host_tip": "仅在服务商提供兼容 Anthropic 的地址时填写。以 / 结尾会忽略自动追加的 v1,以 # 结尾则强制使用原始地址。",
|
||||
"anthropic_api_host_tooltip": "仅当服务商提供 Claude 兼容的基础地址时填写。",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4125,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "预览: {{url}}",
|
||||
"reset": "重置",
|
||||
"tip": "/ 结尾忽略 v1 版本,# 结尾强制使用输入地址"
|
||||
"tip": "# 结尾强制使用输入地址"
|
||||
}
|
||||
},
|
||||
"api_host": "API 地址",
|
||||
"api_host_no_valid": "API 地址不合法",
|
||||
"api_host_preview": "预览:{{url}}",
|
||||
"api_host_tooltip": "仅在服务商需要自定义的 OpenAI 兼容地址时覆盖。",
|
||||
"api_key": {
|
||||
@@ -4547,6 +4615,7 @@
|
||||
"paintings": "绘画",
|
||||
"settings": "设置",
|
||||
"store": "助手库",
|
||||
"terminal": "终端",
|
||||
"translate": "翻译"
|
||||
},
|
||||
"trace": {
|
||||
|
||||
@@ -107,6 +107,50 @@
|
||||
"title": "進階設定"
|
||||
},
|
||||
"essential": "必要設定",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "可用外掛"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "確定要解除安裝此外掛嗎?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "未找到符合的外掛。請嘗試調整搜尋或類別篩選。"
|
||||
},
|
||||
"error": {
|
||||
"install": "安裝外掛失敗",
|
||||
"load": "載入外掛失敗",
|
||||
"uninstall": "解除安裝外掛失敗"
|
||||
},
|
||||
"filter": {
|
||||
"all": "所有類別"
|
||||
},
|
||||
"install": "安裝",
|
||||
"installed": {
|
||||
"empty": "尚未安裝任何外掛。瀏覽可用外掛以開始使用。",
|
||||
"title": "已安裝外掛"
|
||||
},
|
||||
"installing": "安裝中...",
|
||||
"results": "找到 {{count}} 個外掛",
|
||||
"search": {
|
||||
"placeholder": "搜尋外掛..."
|
||||
},
|
||||
"success": {
|
||||
"install": "外掛安裝成功",
|
||||
"uninstall": "外掛解除安裝成功"
|
||||
},
|
||||
"tab": "外掛",
|
||||
"type": {
|
||||
"agent": "代理",
|
||||
"agents": "代理",
|
||||
"all": "全部",
|
||||
"command": "指令",
|
||||
"commands": "指令",
|
||||
"skills": "技能"
|
||||
},
|
||||
"uninstall": "解除安裝",
|
||||
"uninstalling": "解除安裝中..."
|
||||
},
|
||||
"prompt": "提示設定",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -191,6 +235,39 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "允許工具請求",
|
||||
"denyRequest": "拒絕工具請求",
|
||||
"hideDetails": "隱藏工具詳情",
|
||||
"runWithOptions": "帶選項執行",
|
||||
"showDetails": "顯示工具詳情"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "取消",
|
||||
"run": "執行"
|
||||
},
|
||||
"confirmation": "確定要執行此 Claude 工具嗎?",
|
||||
"defaultDenyMessage": "使用者拒絕了該工具的權限。",
|
||||
"defaultDescription": "在您的環境中執行程式碼或系統操作。執行前請確保指令安全。",
|
||||
"error": {
|
||||
"sendFailed": "傳送您的決定失敗,請重試。"
|
||||
},
|
||||
"expired": "已過期",
|
||||
"inputPreview": "工具輸入預覽",
|
||||
"pending": "等待中 ({{seconds}}秒)",
|
||||
"permissionExpired": "權限請求已過期。等待新指令...",
|
||||
"requiresElevatedPermissions": "此工具需要提升的權限。",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "如果您選擇總是允許此工具,核准可能會更新多個工作階段權限。",
|
||||
"permissionUpdateSingle": "如果您選擇總是允許此工具,核准可能會更新您的工作階段權限。"
|
||||
},
|
||||
"toast": {
|
||||
"denied": "工具請求已被拒絕。",
|
||||
"timeout": "工具請求在收到核准前逾時。"
|
||||
},
|
||||
"waiting": "等待工具權限決定..."
|
||||
},
|
||||
"type": {
|
||||
"label": "代理類型",
|
||||
"unknown": "未知類型"
|
||||
@@ -2055,7 +2132,6 @@
|
||||
"cannot_remove_builtin": "不能刪除內建提供者",
|
||||
"existing": "提供者已存在",
|
||||
"get_providers": "取得可用提供者失敗",
|
||||
"not_availabel": "提供 {{provider}} 不可用",
|
||||
"not_found": "OCR 提供者不存在",
|
||||
"update_failed": "更新配置失敗"
|
||||
},
|
||||
@@ -2065,40 +2141,6 @@
|
||||
"not_supported": "不支持的文件類型 {{type}}"
|
||||
},
|
||||
"processing": "OCR 處理中...",
|
||||
"provider": {
|
||||
"config": {
|
||||
"patch": {
|
||||
"error": {
|
||||
"failed": "更新設定失敗"
|
||||
}
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"error": {
|
||||
"failed": "無法建立提供者"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"error": {
|
||||
"failed": "刪除提供者 {{provider}} 失敗"
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "無法取得提供者 {{provider}}"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"error": {
|
||||
"failed": "無法列出提供者"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"error": {
|
||||
"failed": "無法更新提供者"
|
||||
}
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"provider": {
|
||||
"fallback": "已回退到 {{name}},這可能導致問題"
|
||||
@@ -2334,6 +2376,32 @@
|
||||
"seed_tip": "控制放大結果的隨機性"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "操作",
|
||||
"agents": "代理",
|
||||
"all_categories": "所有類別",
|
||||
"all_types": "全部",
|
||||
"category": "類別",
|
||||
"commands": "指令",
|
||||
"confirm_uninstall": "確定要解除安裝 {{name}} 嗎?",
|
||||
"install": "安裝",
|
||||
"install_plugins_from_browser": "瀏覽可用外掛以開始使用",
|
||||
"installing": "安裝中...",
|
||||
"name": "名稱",
|
||||
"no_description": "無描述",
|
||||
"no_installed_plugins": "尚未安裝任何外掛",
|
||||
"no_results": "未找到外掛",
|
||||
"search_placeholder": "搜尋外掛...",
|
||||
"showing_results": "顯示 {{count}} 個外掛",
|
||||
"showing_results_one": "顯示 {{count}} 個外掛",
|
||||
"showing_results_other": "顯示 {{count}} 個外掛",
|
||||
"showing_results_plural": "顯示 {{count}} 個外掛",
|
||||
"skills": "技能",
|
||||
"try_different_search": "請嘗試調整搜尋或類別篩選",
|
||||
"type": "類型",
|
||||
"uninstall": "解除安裝",
|
||||
"uninstalling": "解除安裝中..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "複製為圖片"
|
||||
@@ -4080,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic API 主機地址",
|
||||
"anthropic_api_host_preview": "Anthropic 預覽:{{url}}",
|
||||
"anthropic_api_host_tip": "僅在服務商提供與 Anthropic 相容的網址時設定。以 / 結尾會忽略自動附加的 v1,以 # 結尾則強制使用原始地址。",
|
||||
"anthropic_api_host_tooltip": "僅在服務商提供 Claude 相容的基礎網址時設定。",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4125,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "預覽:{{url}}",
|
||||
"reset": "重設",
|
||||
"tip": "/ 結尾忽略 v1 版本,# 結尾強制使用輸入位址"
|
||||
"tip": "# 結尾強制使用輸入位址"
|
||||
}
|
||||
},
|
||||
"api_host": "API 主機地址",
|
||||
"api_host_no_valid": "API 位址不合法",
|
||||
"api_host_preview": "預覽:{{url}}",
|
||||
"api_host_tooltip": "僅在服務商需要自訂的 OpenAI 相容端點時才覆蓋。",
|
||||
"api_key": {
|
||||
@@ -4547,6 +4615,7 @@
|
||||
"paintings": "繪畫",
|
||||
"settings": "設定",
|
||||
"store": "助手庫",
|
||||
"terminal": "終端機",
|
||||
"translate": "翻譯"
|
||||
},
|
||||
"trace": {
|
||||
|
||||
@@ -107,6 +107,50 @@
|
||||
"title": "Erweiterte Einstellungen"
|
||||
},
|
||||
"essential": "Grundeinstellungen",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Verfügbare Plugins"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "Sind Sie sicher, dass Sie dieses Plugin deinstallieren möchten?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "Keine Plugins gefunden, die deinen Filtern entsprechen. Versuche, deine Such- oder Kategoriefilter anzupassen."
|
||||
},
|
||||
"error": {
|
||||
"install": "Fehler beim Installieren des Plugins",
|
||||
"load": "Fehler beim Laden der Plugins",
|
||||
"uninstall": "Fehler beim Deinstallieren des Plugins"
|
||||
},
|
||||
"filter": {
|
||||
"all": "Alle Kategorien"
|
||||
},
|
||||
"install": "Installieren",
|
||||
"installed": {
|
||||
"empty": "Noch keine Plugins installiert. Durchsuche verfügbare Plugins, um loszulegen.",
|
||||
"title": "Installierte Plugins"
|
||||
},
|
||||
"installing": "Wird installiert...",
|
||||
"results": "{{count}} Plugin(s) gefunden",
|
||||
"search": {
|
||||
"placeholder": "Such-Plugins..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Plugin erfolgreich installiert",
|
||||
"uninstall": "Plugin erfolgreich deinstalliert"
|
||||
},
|
||||
"tab": "Plugins",
|
||||
"type": {
|
||||
"agent": "Agent",
|
||||
"agents": "Agenten",
|
||||
"all": "Alle",
|
||||
"command": "Befehl",
|
||||
"commands": "Befehle",
|
||||
"skills": "Fähigkeiten"
|
||||
},
|
||||
"uninstall": "Deinstallieren",
|
||||
"uninstalling": "Deinstallation läuft..."
|
||||
},
|
||||
"prompt": "Prompt-Einstellungen",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -191,6 +235,39 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Werkzeuganfrage zulassen",
|
||||
"denyRequest": "Werkzeuganfrage ablehnen",
|
||||
"hideDetails": "Werkzeugdetails ausblenden",
|
||||
"runWithOptions": "Mit zusätzlichen Optionen ausführen",
|
||||
"showDetails": "Zeige Werkzeugdetails"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Abbrechen",
|
||||
"run": "Laufen"
|
||||
},
|
||||
"confirmation": "Bist du sicher, dass du dieses Claude-Tool ausführen möchtest?",
|
||||
"defaultDenyMessage": "Der Benutzer hat die Berechtigung für dieses Tool verweigert.",
|
||||
"defaultDescription": "Führt Code oder Systemaktionen in Ihrer Umgebung aus. Vergewissern Sie sich, dass der Befehl sicher aussieht, bevor Sie ihn ausführen.",
|
||||
"error": {
|
||||
"sendFailed": "Ihre Entscheidung konnte nicht gesendet werden. Bitte versuchen Sie es erneut."
|
||||
},
|
||||
"expired": "Abgelaufen",
|
||||
"inputPreview": "Vorschau der Werkzeugeingabe",
|
||||
"pending": "Ausstehend ({{seconds}}s)",
|
||||
"permissionExpired": "Berechtigungsanfrage abgelaufen. Warte auf neue Anweisungen...",
|
||||
"requiresElevatedPermissions": "Dieses Tool erfordert erhöhte Berechtigungen.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Das Genehmigen kann mehrere Sitzungsberechtigungen aktualisieren, wenn Sie sich entschieden haben, dieses Tool immer zuzulassen.",
|
||||
"permissionUpdateSingle": "Das Genehmigen kann Ihre Sitzungsberechtigungen aktualisieren, wenn Sie sich entschieden haben, dieses Tool immer zuzulassen."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "Tool-Anfrage wurde abgelehnt.",
|
||||
"timeout": "Tool-Anfrage ist abgelaufen, bevor eine Genehmigung eingegangen ist."
|
||||
},
|
||||
"waiting": "Warten auf Entscheidung über Tool-Berechtigung..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Agent-Typ",
|
||||
"unknown": "Unbekannter Typ"
|
||||
@@ -2055,7 +2132,6 @@
|
||||
"cannot_remove_builtin": "Eingebauter Anbieter kann nicht entfernt werden",
|
||||
"existing": "Anbieter existiert bereits",
|
||||
"get_providers": "Failed to obtain available providers",
|
||||
"not_availabel": "{{provider}} ist nicht verfügbar",
|
||||
"not_found": "OCR-Anbieter nicht gefunden",
|
||||
"update_failed": "Konfiguration aktualisieren fehlgeschlagen"
|
||||
},
|
||||
@@ -2065,40 +2141,6 @@
|
||||
"not_supported": "Nicht unterstützter Dateityp {{type}}"
|
||||
},
|
||||
"processing": "OCR wird verarbeitet...",
|
||||
"provider": {
|
||||
"config": {
|
||||
"patch": {
|
||||
"error": {
|
||||
"failed": "Fehler beim Aktualisieren der Konfiguration"
|
||||
}
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"error": {
|
||||
"failed": "Fehler beim Erstellen des Anbieters"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"error": {
|
||||
"failed": "Fehler beim Löschen des Anbieters {{provider}}"
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Fehler beim Abrufen des Anbieters {{provider}}"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"error": {
|
||||
"failed": "Anbieter konnten nicht aufgelistet werden"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"error": {
|
||||
"failed": "Fehler beim Aktualisieren des Anbieters"
|
||||
}
|
||||
}
|
||||
},
|
||||
"warning": {
|
||||
"provider": {
|
||||
"fallback": "Auf {{name}} zurückgefallen, dies kann zu Problemen führen"
|
||||
@@ -2334,6 +2376,32 @@
|
||||
"seed_tip": "Kontrolle der Zufälligkeit des Upscale-Ergebnisses"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Aktionen",
|
||||
"agents": "Agenten",
|
||||
"all_categories": "Alle Kategorien",
|
||||
"all_types": "Alle",
|
||||
"category": "Kategorie",
|
||||
"commands": "Befehle",
|
||||
"confirm_uninstall": "Sind Sie sicher, dass Sie {{name}} deinstallieren möchten?",
|
||||
"install": "Installieren",
|
||||
"install_plugins_from_browser": "Durchsuche verfügbare Plugins, um loszulegen",
|
||||
"installing": "Installiere…",
|
||||
"name": "Name",
|
||||
"no_description": "Keine Beschreibung verfügbar",
|
||||
"no_installed_plugins": "Noch keine Plugins installiert",
|
||||
"no_results": "Keine Plugins gefunden",
|
||||
"search_placeholder": "Such-Plugins...",
|
||||
"showing_results": "{{count}} Plugin anzeigen",
|
||||
"showing_results_one": "{{count}} Plugin anzeigen",
|
||||
"showing_results_other": "Zeige {{count}} Plugins",
|
||||
"showing_results_plural": "{{count}} Plugins anzeigen",
|
||||
"skills": "Fähigkeiten",
|
||||
"try_different_search": "Versuchen Sie, Ihre Suche oder die Kategoriefilter anzupassen.",
|
||||
"type": "Typ",
|
||||
"uninstall": "Deinstallieren",
|
||||
"uninstalling": "Deinstallation läuft..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Als Bild kopieren"
|
||||
@@ -4080,7 +4148,6 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic API-Adresse",
|
||||
"anthropic_api_host_preview": "Anthropic-Vorschau: {{url}}",
|
||||
"anthropic_api_host_tip": "Nur bei Anbietern, die ein Anthropic-kompatibles Endpunkt anbieten. Eine / am Ende ignoriert automatisch hinzugefügtes v1, ein # am Ende erzwingt die Verwendung der ursprünglichen Adresse.",
|
||||
"anthropic_api_host_tooltip": "Nur bei Anbietern, die ein Claude-kompatibles Basis-Endpunkt anbieten.",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4125,10 +4192,11 @@
|
||||
"url": {
|
||||
"preview": "Vorschau: {{url}}",
|
||||
"reset": "Zurücksetzen",
|
||||
"tip": "/ am Ende ignorieren v1-Version, # am Ende erzwingt die Verwendung der Eingabe-Adresse"
|
||||
"tip": "# am Ende erzwingt die Verwendung der Eingabe-Adresse"
|
||||
}
|
||||
},
|
||||
"api_host": "API-Adresse",
|
||||
"api_host_no_valid": "API-Adresse ist ungültig",
|
||||
"api_host_preview": "Vorschau: {{url}}",
|
||||
"api_host_tooltip": "Nur bei Anbietern, die ein OpenAI-kompatibles Endpunkt anbieten. Eine / am Ende ignoriert automatisch hinzugefügtes v1, ein # am Ende erzwingt die Verwendung der ursprünglichen Adresse.",
|
||||
"api_key": {
|
||||
@@ -4547,6 +4615,7 @@
|
||||
"paintings": "Zeichnen",
|
||||
"settings": "Einstellungen",
|
||||
"store": "Assistenten-Bibliothek",
|
||||
"terminal": "Terminal",
|
||||
"translate": "Übersetzen"
|
||||
},
|
||||
"trace": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user