Compare commits
159 Commits
libsql
...
refactor/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00fd20d828 | ||
|
|
d94f73b5ca | ||
|
|
7f34d084cc | ||
|
|
821f233728 | ||
|
|
0cb60fb2d6 | ||
|
|
19d1ce4b2a | ||
|
|
c2ee3fff33 | ||
|
|
9b7094ea4a | ||
|
|
6cda7f891d | ||
|
|
dbfece3590 | ||
|
|
9a67ac9018 | ||
|
|
2bd5f39740 | ||
|
|
9ebe4801f4 | ||
|
|
c258035f6a | ||
|
|
569572bfdc | ||
|
|
b821ac5390 | ||
|
|
534c2ce485 | ||
|
|
bab1a5445c | ||
|
|
742f901052 | ||
|
|
cb12bb5137 | ||
|
|
06b6f2b9d8 | ||
|
|
2c102ed3b4 | ||
|
|
767e22c58d | ||
|
|
dee397f6ac | ||
|
|
a00aba23bd | ||
|
|
de5fb03efb | ||
|
|
a6e58776d2 | ||
|
|
bebe745e69 | ||
|
|
ec8c24a1c2 | ||
|
|
db4fcac768 | ||
|
|
6c71b92d1d | ||
|
|
d470fd8b88 | ||
|
|
99962b740c | ||
|
|
ef4bede062 | ||
|
|
e6e1fb0404 | ||
|
|
e6696def10 | ||
|
|
e5a3363021 | ||
|
|
f6ff436294 | ||
|
|
8a9b633af2 | ||
|
|
0a37146ba8 | ||
|
|
ac3dfcbfbe | ||
|
|
5ac09d5311 | ||
|
|
d4fd8ffdcc | ||
|
|
84274d9d85 | ||
|
|
a72feebead | ||
|
|
e930d3de43 | ||
|
|
ecc9923050 | ||
|
|
e469016775 | ||
|
|
15569387c7 | ||
|
|
4f746842a5 | ||
|
|
aab941d89c | ||
|
|
1b04fd065d | ||
|
|
76b3ba5d7e | ||
|
|
355e5b269d | ||
|
|
d4b0272fe7 | ||
|
|
59bf94b118 | ||
|
|
bd7cd22220 | ||
|
|
f48674b2c7 | ||
|
|
56af6f43c0 | ||
|
|
f83c3e171e | ||
|
|
d397a43806 | ||
|
|
8353f331f1 | ||
|
|
8cc6b08831 | ||
|
|
ffe897d58c | ||
|
|
182ac3bc98 | ||
|
|
c0cca4ae44 | ||
|
|
8981d0a09d | ||
|
|
de44938d9b | ||
|
|
75d5dcf275 | ||
|
|
d8f4825e5e | ||
|
|
c242abd81a | ||
|
|
79c9ed963f | ||
|
|
6079961f44 | ||
|
|
04ef5edea2 | ||
|
|
046ed3edef | ||
|
|
6eb9ab30b0 | ||
|
|
1c27481813 | ||
|
|
a6e19f7757 | ||
|
|
6d89f94335 | ||
|
|
2e07b4ea58 | ||
|
|
bf2f6ddd7f | ||
|
|
c936bddfe7 | ||
|
|
d3028f1dd1 | ||
|
|
0038280fba | ||
|
|
0a94609f78 | ||
|
|
f9f8390540 | ||
|
|
91dd6482ce | ||
|
|
016bbff79f | ||
|
|
32f41391c4 | ||
|
|
78a8ebc777 | ||
|
|
57fd73e51a | ||
|
|
bd448b5108 | ||
|
|
a7d12abd1f | ||
|
|
9e3618bc17 | ||
|
|
8cb270ca86 | ||
|
|
d321cd23ef | ||
|
|
9da3e82c47 | ||
|
|
2931e558b3 | ||
|
|
9a847dc5a3 | ||
|
|
c2a1178dff | ||
|
|
7f114ade4d | ||
|
|
7b633641d1 | ||
|
|
1dacdc3178 | ||
|
|
566dd14fed | ||
|
|
68cd87e069 | ||
|
|
1b57ffeb56 | ||
|
|
5d789ef394 | ||
|
|
820d6a6e96 | ||
|
|
0a67ab4103 | ||
|
|
5cc7390bb6 | ||
|
|
2ce4fabc7d | ||
|
|
7b2570974e | ||
|
|
0ef3852029 | ||
|
|
0dce1c57fc | ||
|
|
190ee76cf1 | ||
|
|
83fea49ed2 | ||
|
|
ccc50dbf2b | ||
|
|
6b503c4080 | ||
|
|
40fe381aa5 | ||
|
|
65c24a2f4b | ||
|
|
b15778b16b | ||
|
|
087e825086 | ||
|
|
3dd2bc1a40 | ||
|
|
9bde833419 | ||
|
|
e15005d1cf | ||
|
|
30e6883333 | ||
|
|
99be38c325 | ||
|
|
df876651b9 | ||
|
|
85bdcdc206 | ||
|
|
2860935e5b | ||
|
|
b219e96544 | ||
|
|
c02f93e6b9 | ||
|
|
72f32e4b8f | ||
|
|
a81f13848c | ||
|
|
81538d5709 | ||
|
|
54449e7130 | ||
|
|
c217a0bf02 | ||
|
|
39257f64b1 | ||
|
|
06dab978f7 | ||
|
|
c3f61533f7 | ||
|
|
8715eb1f41 | ||
|
|
92eb5aed7f | ||
|
|
ff965402cd | ||
|
|
973f26f9dd | ||
|
|
4e3f8a8f76 | ||
|
|
21e40db086 | ||
|
|
92cd012037 | ||
|
|
7cd937888e | ||
|
|
ec491f5f24 | ||
|
|
c0efb46c2b | ||
|
|
aa47fc3ed7 | ||
|
|
c3c9f9b3f2 | ||
|
|
d486b56595 | ||
|
|
4bb5ff8086 | ||
|
|
a748162e67 | ||
|
|
610e7481b3 | ||
|
|
9105e0f5c1 | ||
|
|
a248517520 | ||
|
|
d31f35b16d |
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -2,3 +2,10 @@
|
|||||||
/src/main/services/ConfigManager.ts @0xfullex
|
/src/main/services/ConfigManager.ts @0xfullex
|
||||||
/packages/shared/IpcChannel.ts @0xfullex
|
/packages/shared/IpcChannel.ts @0xfullex
|
||||||
/src/main/ipc.ts @0xfullex
|
/src/main/ipc.ts @0xfullex
|
||||||
|
|
||||||
|
/migrations/ @0xfullex
|
||||||
|
/packages/shared/data/ @0xfullex
|
||||||
|
/src/main/data/ @0xfullex
|
||||||
|
/src/renderer/src/data/ @0xfullex
|
||||||
|
|
||||||
|
/packages/ui/ @MyPrototypeWhat
|
||||||
|
|||||||
2
.github/workflows/pr-ci.yml
vendored
2
.github/workflows/pr-ci.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
PRCI: true
|
PRCI: true
|
||||||
if: github.event.pull_request.draft == false
|
if: github.event.pull_request.draft == false || github.head_ref == 'v2'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
"env": {
|
"env": {
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"files": ["src/main/**", "resources/scripts/**", "scripts/**", "playwright.config.ts", "electron.vite.config.ts"]
|
"files": ["src/main/**", "resources/scripts/**", "scripts/**", "playwright.config.ts", "electron.vite.config.ts", "packages/ui/scripts/**"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"env": {
|
"env": {
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
"src/renderer/**/*.{ts,tsx}",
|
"src/renderer/**/*.{ts,tsx}",
|
||||||
"packages/aiCore/**",
|
"packages/aiCore/**",
|
||||||
"packages/extension-table-plus/**",
|
"packages/extension-table-plus/**",
|
||||||
|
"packages/ui/**",
|
||||||
"resources/js/**"
|
"resources/js/**"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -140,7 +141,7 @@
|
|||||||
"typescript/await-thenable": "warn",
|
"typescript/await-thenable": "warn",
|
||||||
// "typescript/ban-ts-comment": "error",
|
// "typescript/ban-ts-comment": "error",
|
||||||
"typescript/no-array-constructor": "error",
|
"typescript/no-array-constructor": "error",
|
||||||
// "typescript/consistent-type-imports": "error",
|
"typescript/consistent-type-imports": "error",
|
||||||
"typescript/no-array-delete": "warn",
|
"typescript/no-array-delete": "warn",
|
||||||
"typescript/no-base-to-string": "warn",
|
"typescript/no-base-to-string": "warn",
|
||||||
"typescript/no-duplicate-enum-values": "error",
|
"typescript/no-duplicate-enum-values": "error",
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -31,7 +31,8 @@
|
|||||||
},
|
},
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.css": "tailwindcss"
|
"*.css": "tailwindcss",
|
||||||
|
".oxlintrc.json": "jsonc"
|
||||||
},
|
},
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
// "i18n-ally.displayLanguage": "zh-cn", // 界面显示语言
|
// "i18n-ally.displayLanguage": "zh-cn", // 界面显示语言
|
||||||
|
|||||||
113
CLAUDE.md
113
CLAUDE.md
@@ -35,14 +35,113 @@ This file provides guidance to AI coding assistants when working with code in th
|
|||||||
- **Renderer Process** (`src/renderer/`): React UI with Redux state management
|
- **Renderer Process** (`src/renderer/`): React UI with Redux state management
|
||||||
- **Preload Scripts** (`src/preload/`): Secure IPC bridge
|
- **Preload Scripts** (`src/preload/`): Secure IPC bridge
|
||||||
|
|
||||||
### Key Components
|
### Key Architectural Components
|
||||||
- **AI Core** (`src/renderer/src/aiCore/`): Middleware pipeline for multiple AI providers.
|
|
||||||
- **Services** (`src/main/services/`): MCPService, KnowledgeService, WindowService, etc.
|
#### Main Process Services (`src/main/services/`)
|
||||||
- **Build System**: Electron-Vite with experimental rolldown-vite, yarn workspaces.
|
|
||||||
- **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state.
|
- **MCPService**: Model Context Protocol server management
|
||||||
- **UI Components**: HeroUI (`@heroui/*`) for all new UI elements.
|
- **KnowledgeService**: Document processing and knowledge base management
|
||||||
|
- **FileStorage/S3Storage/WebDav**: Multiple storage backends
|
||||||
|
- **WindowService**: Multi-window management (main, mini, selection windows)
|
||||||
|
- **ProxyManager**: Network proxy handling
|
||||||
|
- **SearchService**: Full-text search capabilities
|
||||||
|
|
||||||
|
#### AI Core (`src/renderer/src/aiCore/`)
|
||||||
|
|
||||||
|
- **Middleware System**: Composable pipeline for AI request processing
|
||||||
|
- **Client Factory**: Supports multiple AI providers (OpenAI, Anthropic, Gemini, etc.)
|
||||||
|
- **Stream Processing**: Real-time response handling
|
||||||
|
|
||||||
|
#### Data Management
|
||||||
|
|
||||||
|
- **Cache System**: Three-layer caching (memory/shared/persist) with React hooks integration
|
||||||
|
- **Preferences**: Type-safe configuration management with multi-window synchronization
|
||||||
|
- **User Data**: SQLite-based storage with Drizzle ORM for business data
|
||||||
|
|
||||||
|
#### Knowledge Management
|
||||||
|
|
||||||
|
- **Embeddings**: Vector search with multiple providers (OpenAI, Voyage, etc.)
|
||||||
|
- **OCR**: Document text extraction (system OCR, Doc2x, Mineru)
|
||||||
|
- **Preprocessing**: Document preparation pipeline
|
||||||
|
- **Loaders**: Support for various file formats (PDF, DOCX, EPUB, etc.)
|
||||||
|
|
||||||
|
### Build System
|
||||||
|
|
||||||
|
- **Electron-Vite**: Development and build tooling (v4.0.0)
|
||||||
|
- **Rolldown-Vite**: Using experimental rolldown-vite instead of standard vite
|
||||||
|
- **Workspaces**: Monorepo structure with `packages/` directory
|
||||||
|
- **Multiple Entry Points**: Main app, mini window, selection toolbar
|
||||||
|
- **Styled Components**: CSS-in-JS styling with SWC optimization
|
||||||
|
|
||||||
|
### Testing Strategy
|
||||||
|
|
||||||
|
- **Vitest**: Unit and integration testing
|
||||||
|
- **Playwright**: End-to-end testing
|
||||||
|
- **Component Testing**: React Testing Library
|
||||||
|
- **Coverage**: Available via `yarn test:coverage`
|
||||||
|
|
||||||
|
### Key Patterns
|
||||||
|
|
||||||
|
- **IPC Communication**: Secure main-renderer communication via preload scripts
|
||||||
|
- **Service Layer**: Clear separation between UI and business logic
|
||||||
|
- **Plugin Architecture**: Extensible via MCP servers and middleware
|
||||||
|
- **Multi-language Support**: i18n with dynamic loading
|
||||||
|
- **Theme System**: Light/dark themes with custom CSS variables
|
||||||
|
|
||||||
|
### UI Design
|
||||||
|
|
||||||
|
The project is in the process of migrating from antd & styled-components to HeroUI. Please use HeroUI to build UI components. The use of antd and styled-components is prohibited.
|
||||||
|
|
||||||
|
HeroUI Docs: https://www.heroui.com/docs/guide/introduction
|
||||||
|
|
||||||
|
### Database Architecture
|
||||||
|
|
||||||
|
- **Database**: SQLite (`cherrystudio.sqlite`) + libsql driver
|
||||||
|
- **ORM**: Drizzle ORM with comprehensive migration system
|
||||||
|
- **Schemas**: Located in `src/main/data/db/schemas/` directory
|
||||||
|
|
||||||
|
#### Database Standards
|
||||||
|
|
||||||
|
- **Table Naming**: Use singular form with snake_case (e.g., `topic`, `message`, `app_state`)
|
||||||
|
- **Schema Exports**: Export using `xxxTable` pattern (e.g., `topicTable`, `appStateTable`)
|
||||||
|
- **Field Definition**: Drizzle auto-infers field names, no need to add default field names
|
||||||
|
- **JSON Fields**: For JSON support, add `{ mode: 'json' }`, refer to `preference.ts` table definition
|
||||||
|
- **JSON Serialization**: For JSON fields, no need to manually serialize/deserialize when reading/writing to database, Drizzle handles this automatically
|
||||||
|
- **Timestamps**: Use existing `crudTimestamps` utility
|
||||||
|
- **Migrations**: Generate via `yarn run migrations:generate`
|
||||||
|
|
||||||
|
## Data Access Patterns
|
||||||
|
|
||||||
|
The application uses three distinct data management systems. Choose the appropriate system based on data characteristics:
|
||||||
|
|
||||||
|
### Cache System
|
||||||
|
- **Purpose**: Temporary data that can be regenerated
|
||||||
|
- **Lifecycle**: Component-level (memory), window-level (shared), or persistent (survives restart)
|
||||||
|
- **Use Cases**: API response caching, computed results, temporary UI state
|
||||||
|
- **APIs**: `useCache`, `useSharedCache`, `usePersistCache` hooks, or `cacheService`
|
||||||
|
|
||||||
|
### Preference System
|
||||||
|
- **Purpose**: User configuration and application settings
|
||||||
|
- **Lifecycle**: Permanent until user changes
|
||||||
|
- **Use Cases**: Theme, language, editor settings, user preferences
|
||||||
|
- **APIs**: `usePreference`, `usePreferences` hooks, or `preferenceService`
|
||||||
|
|
||||||
|
### User Data API
|
||||||
|
- **Purpose**: Core business data (conversations, files, notes, etc.)
|
||||||
|
- **Lifecycle**: Permanent business records
|
||||||
|
- **Use Cases**: Topics, messages, files, knowledge base, user-generated content
|
||||||
|
- **APIs**: `useDataApi` hook or `dataApiService` for direct calls
|
||||||
|
|
||||||
|
### Selection Guidelines
|
||||||
|
|
||||||
|
- **Use Cache** for data that can be lost without impact (computed values, API responses)
|
||||||
|
- **Use Preferences** for user settings that affect app behavior (UI configuration, feature flags)
|
||||||
|
- **Use User Data API** for irreplaceable business data (conversations, documents, user content)
|
||||||
|
|
||||||
|
## Logging Standards
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
### Logging
|
|
||||||
```typescript
|
```typescript
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
const logger = loggerService.withContext('moduleName')
|
const logger = loggerService.withContext('moduleName')
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"!.github/**",
|
"!.github/**",
|
||||||
"!.husky/**",
|
"!.husky/**",
|
||||||
"!.vscode/**",
|
"!.vscode/**",
|
||||||
|
"!.claude/**",
|
||||||
"!*.yaml",
|
"!*.yaml",
|
||||||
"!*.yml",
|
"!*.yml",
|
||||||
"!*.mjs",
|
"!*.mjs",
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ asarUnpack:
|
|||||||
- resources/**
|
- resources/**
|
||||||
- "**/*.{metal,exp,lib}"
|
- "**/*.{metal,exp,lib}"
|
||||||
- "node_modules/@img/sharp-libvips-*/**"
|
- "node_modules/@img/sharp-libvips-*/**"
|
||||||
|
extraResources:
|
||||||
|
- from: "migrations/sqlite-drizzle"
|
||||||
|
to: "migrations/sqlite-drizzle"
|
||||||
win:
|
win:
|
||||||
executableName: Cherry Studio
|
executableName: Cherry Studio
|
||||||
artifactName: ${productName}-${version}-${arch}-setup.${ext}
|
artifactName: ${productName}-${version}-${arch}-setup.${ext}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
'@main': resolve('src/main'),
|
'@main': resolve('src/main'),
|
||||||
'@types': resolve('src/renderer/src/types'),
|
'@types': resolve('src/renderer/src/types'),
|
||||||
|
'@data': resolve('src/main/data'),
|
||||||
'@shared': resolve('packages/shared'),
|
'@shared': resolve('packages/shared'),
|
||||||
'@logger': resolve('src/main/services/LoggerService'),
|
'@logger': resolve('src/main/services/LoggerService'),
|
||||||
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
|
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
|
||||||
@@ -61,7 +62,20 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
sourcemap: isDev
|
sourcemap: isDev,
|
||||||
|
rollupOptions: {
|
||||||
|
// Unlike renderer which auto-discovers entries from HTML files,
|
||||||
|
// preload requires explicit entry point configuration for multiple scripts
|
||||||
|
input: {
|
||||||
|
index: resolve(__dirname, 'src/preload/index.ts'),
|
||||||
|
simplest: resolve(__dirname, 'src/preload/simplest.ts') // Minimal preload
|
||||||
|
},
|
||||||
|
external: ['electron'],
|
||||||
|
output: {
|
||||||
|
entryFileNames: '[name].js',
|
||||||
|
format: 'cjs'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderer: {
|
renderer: {
|
||||||
@@ -90,12 +104,14 @@ export default defineConfig({
|
|||||||
'@shared': resolve('packages/shared'),
|
'@shared': resolve('packages/shared'),
|
||||||
'@types': resolve('src/renderer/src/types'),
|
'@types': resolve('src/renderer/src/types'),
|
||||||
'@logger': resolve('src/renderer/src/services/LoggerService'),
|
'@logger': resolve('src/renderer/src/services/LoggerService'),
|
||||||
|
'@data': resolve('src/renderer/src/data'),
|
||||||
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
|
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
|
||||||
'@mcp-trace/trace-web': resolve('packages/mcp-trace/trace-web'),
|
'@mcp-trace/trace-web': resolve('packages/mcp-trace/trace-web'),
|
||||||
'@cherrystudio/ai-core/provider': resolve('packages/aiCore/src/core/providers'),
|
'@cherrystudio/ai-core/provider': resolve('packages/aiCore/src/core/providers'),
|
||||||
'@cherrystudio/ai-core/built-in/plugins': resolve('packages/aiCore/src/core/plugins/built-in'),
|
'@cherrystudio/ai-core/built-in/plugins': resolve('packages/aiCore/src/core/plugins/built-in'),
|
||||||
'@cherrystudio/ai-core': resolve('packages/aiCore/src'),
|
'@cherrystudio/ai-core': resolve('packages/aiCore/src'),
|
||||||
'@cherrystudio/extension-table-plus': resolve('packages/extension-table-plus/src')
|
'@cherrystudio/extension-table-plus': resolve('packages/extension-table-plus/src'),
|
||||||
|
'@cherrystudio/ui': resolve('packages/ui/src')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
@@ -115,7 +131,8 @@ export default defineConfig({
|
|||||||
miniWindow: resolve(__dirname, 'src/renderer/miniWindow.html'),
|
miniWindow: resolve(__dirname, 'src/renderer/miniWindow.html'),
|
||||||
selectionToolbar: resolve(__dirname, 'src/renderer/selectionToolbar.html'),
|
selectionToolbar: resolve(__dirname, 'src/renderer/selectionToolbar.html'),
|
||||||
selectionAction: resolve(__dirname, 'src/renderer/selectionAction.html'),
|
selectionAction: resolve(__dirname, 'src/renderer/selectionAction.html'),
|
||||||
traceWindow: resolve(__dirname, 'src/renderer/traceWindow.html')
|
traceWindow: resolve(__dirname, 'src/renderer/traceWindow.html'),
|
||||||
|
dataRefactorMigrate: resolve(__dirname, 'src/renderer/dataRefactorMigrate.html')
|
||||||
},
|
},
|
||||||
onwarn(warning, warn) {
|
onwarn(warning, warn) {
|
||||||
if (warning.code === 'COMMONJS_VARIABLE_IN_ESM') return
|
if (warning.code === 'COMMONJS_VARIABLE_IN_ESM') return
|
||||||
|
|||||||
@@ -72,8 +72,9 @@ export default defineConfig([
|
|||||||
...oxlint.configs['flat/eslint'],
|
...oxlint.configs['flat/eslint'],
|
||||||
...oxlint.configs['flat/typescript'],
|
...oxlint.configs['flat/typescript'],
|
||||||
...oxlint.configs['flat/unicorn'],
|
...oxlint.configs['flat/unicorn'],
|
||||||
|
// Custom rules should be after oxlint to overwrite
|
||||||
|
// LoggerService Custom Rules - only apply to src directory
|
||||||
{
|
{
|
||||||
// LoggerService Custom Rules - only apply to src directory
|
|
||||||
files: ['src/**/*.{ts,tsx,js,jsx}'],
|
files: ['src/**/*.{ts,tsx,js,jsx}'],
|
||||||
ignores: ['src/**/__tests__/**', 'src/**/__mocks__/**', 'src/**/*.test.*', 'src/preload/**'],
|
ignores: ['src/**/__tests__/**', 'src/**/__mocks__/**', 'src/**/*.test.*', 'src/preload/**'],
|
||||||
rules: {
|
rules: {
|
||||||
@@ -87,6 +88,7 @@ export default defineConfig([
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// i18n
|
||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx,js,jsx}'],
|
files: ['**/*.{ts,tsx,js,jsx}'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
@@ -134,4 +136,30 @@ export default defineConfig([
|
|||||||
'i18n/no-template-in-t': 'warn'
|
'i18n/no-template-in-t': 'warn'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// ui migration
|
||||||
|
{
|
||||||
|
// Component Rules - prevent importing antd components when migration completed
|
||||||
|
files: ['**/*.{ts,tsx,js,jsx}'],
|
||||||
|
ignores: ['src/renderer/src/windows/dataRefactorTest/**/*.{ts,tsx}'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
name: 'antd',
|
||||||
|
importNames: ['Flex', 'Switch', 'message', 'Button', 'Tooltip'],
|
||||||
|
message:
|
||||||
|
'❌ Do not import this component from antd. Use our custom components instead: import { ... } from "@cherrystudio/ui"'
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// name: '@heroui/react',
|
||||||
|
// message:
|
||||||
|
// '❌ Do not import components from heroui directly. Use our wrapped components instead: import { ... } from "@cherrystudio/ui"'
|
||||||
|
// }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
6
migrations/README.md
Normal file
6
migrations/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
**THIS DIRECTORY IS NOT FOR RUNTIME USE**
|
||||||
|
|
||||||
|
- Using `libsql` as the `sqlite3` driver, and `drizzle` as the ORM and database migration tool
|
||||||
|
- `migrations/sqlite-drizzle` contains auto-generated migration data. Please **DO NOT** modify it.
|
||||||
|
- If table structure changes, we should run migrations.
|
||||||
|
- To generate migrations, use the command `yarn run migrations:generate`
|
||||||
7
migrations/sqlite-drizzle.config.ts
Normal file
7
migrations/sqlite-drizzle.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'drizzle-kit'
|
||||||
|
export default defineConfig({
|
||||||
|
out: './migrations/sqlite-drizzle',
|
||||||
|
schema: './src/main/data/db/schemas/*',
|
||||||
|
dialect: 'sqlite',
|
||||||
|
casing: 'snake_case'
|
||||||
|
})
|
||||||
17
migrations/sqlite-drizzle/0000_solid_lord_hawal.sql
Normal file
17
migrations/sqlite-drizzle/0000_solid_lord_hawal.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
CREATE TABLE `app_state` (
|
||||||
|
`key` text PRIMARY KEY NOT NULL,
|
||||||
|
`value` text NOT NULL,
|
||||||
|
`description` text,
|
||||||
|
`created_at` integer,
|
||||||
|
`updated_at` integer
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `preference` (
|
||||||
|
`scope` text NOT NULL,
|
||||||
|
`key` text NOT NULL,
|
||||||
|
`value` text,
|
||||||
|
`created_at` integer,
|
||||||
|
`updated_at` integer
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX `scope_name_idx` ON `preference` (`scope`,`key`);
|
||||||
114
migrations/sqlite-drizzle/meta/0000_snapshot.json
Normal file
114
migrations/sqlite-drizzle/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
},
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"enums": {},
|
||||||
|
"id": "de8009d7-95b9-4f99-99fa-4b8795708f21",
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
},
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"tables": {
|
||||||
|
"app_state": {
|
||||||
|
"checkConstraints": {},
|
||||||
|
"columns": {
|
||||||
|
"created_at": {
|
||||||
|
"autoincrement": false,
|
||||||
|
"name": "created_at",
|
||||||
|
"notNull": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"autoincrement": false,
|
||||||
|
"name": "description",
|
||||||
|
"notNull": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"autoincrement": false,
|
||||||
|
"name": "key",
|
||||||
|
"notNull": true,
|
||||||
|
"primaryKey": true,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"autoincrement": false,
|
||||||
|
"name": "updated_at",
|
||||||
|
"notNull": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"autoincrement": false,
|
||||||
|
"name": "value",
|
||||||
|
"notNull": true,
|
||||||
|
"primaryKey": false,
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"indexes": {},
|
||||||
|
"name": "app_state",
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"preference": {
|
||||||
|
"checkConstraints": {},
|
||||||
|
"columns": {
|
||||||
|
"created_at": {
|
||||||
|
"autoincrement": false,
|
||||||
|
"name": "created_at",
|
||||||
|
"notNull": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"autoincrement": false,
|
||||||
|
"name": "key",
|
||||||
|
"notNull": true,
|
||||||
|
"primaryKey": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"autoincrement": false,
|
||||||
|
"name": "scope",
|
||||||
|
"notNull": true,
|
||||||
|
"primaryKey": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"autoincrement": false,
|
||||||
|
"name": "updated_at",
|
||||||
|
"notNull": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"autoincrement": false,
|
||||||
|
"name": "value",
|
||||||
|
"notNull": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"indexes": {
|
||||||
|
"scope_name_idx": {
|
||||||
|
"columns": ["scope", "key"],
|
||||||
|
"isUnique": false,
|
||||||
|
"name": "scope_name_idx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "preference",
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": "6",
|
||||||
|
"views": {}
|
||||||
|
}
|
||||||
13
migrations/sqlite-drizzle/meta/_journal.json
Normal file
13
migrations/sqlite-drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"breakpoints": true,
|
||||||
|
"idx": 0,
|
||||||
|
"tag": "0000_solid_lord_hawal",
|
||||||
|
"version": "6",
|
||||||
|
"when": 1754745234572
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": "7"
|
||||||
|
}
|
||||||
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "CherryStudio",
|
"name": "CherryStudio",
|
||||||
"version": "1.7.0-alpha.5",
|
"version": "2.0.0-alpha",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A powerful AI assistant for producer.",
|
"description": "A powerful AI assistant for producer.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
@@ -50,9 +50,10 @@
|
|||||||
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
|
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
|
||||||
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
|
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
|
||||||
"analyze:main": "VISUALIZER_MAIN=true yarn build",
|
"analyze:main": "VISUALIZER_MAIN=true yarn build",
|
||||||
"typecheck": "concurrently -n \"node,web\" -c \"cyan,magenta\" \"npm run typecheck:node\" \"npm run typecheck:web\"",
|
"typecheck": "concurrently -n \"node,web,ui\" -c \"cyan,magenta,green\" \"npm run typecheck:node\" \"npm run typecheck:web\" \"npm run typecheck:ui\"",
|
||||||
"typecheck:node": "tsgo --noEmit -p tsconfig.node.json --composite false",
|
"typecheck:node": "tsgo --noEmit -p tsconfig.node.json --composite false",
|
||||||
"typecheck:web": "tsgo --noEmit -p tsconfig.web.json --composite false",
|
"typecheck:web": "tsgo --noEmit -p tsconfig.web.json --composite false",
|
||||||
|
"typecheck:ui": "cd packages/ui && npm run type-check",
|
||||||
"check:i18n": "dotenv -e .env -- tsx scripts/check-i18n.ts",
|
"check:i18n": "dotenv -e .env -- tsx scripts/check-i18n.ts",
|
||||||
"sync:i18n": "dotenv -e .env -- tsx scripts/sync-i18n.ts",
|
"sync:i18n": "dotenv -e .env -- tsx scripts/sync-i18n.ts",
|
||||||
"update:i18n": "dotenv -e .env -- tsx scripts/update-i18n.ts",
|
"update:i18n": "dotenv -e .env -- tsx scripts/update-i18n.ts",
|
||||||
@@ -68,11 +69,13 @@
|
|||||||
"test:e2e": "yarn playwright test",
|
"test:e2e": "yarn playwright test",
|
||||||
"test:lint": "oxlint --deny-warnings && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --cache",
|
"test:lint": "oxlint --deny-warnings && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --cache",
|
||||||
"test:scripts": "vitest scripts",
|
"test:scripts": "vitest scripts",
|
||||||
"lint": "oxlint --fix && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --cache && yarn typecheck && yarn check:i18n && yarn format:check",
|
"lint": "oxlint --fix && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --cache && biome lint --write && biome format --write && yarn typecheck && yarn check:i18n && yarn format:check",
|
||||||
|
"lint:ox": "oxlint --fix && biome lint --write && biome format --write",
|
||||||
"format": "biome format --write && biome lint --write",
|
"format": "biome format --write && biome lint --write",
|
||||||
"format:check": "biome format && biome lint",
|
"format:check": "biome format && biome lint",
|
||||||
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky",
|
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky",
|
||||||
"claude": "dotenv -e .env -- claude",
|
"claude": "dotenv -e .env -- claude",
|
||||||
|
"migrations:generate": "drizzle-kit generate --config ./migrations/sqlite-drizzle.config.ts",
|
||||||
"release:aicore:alpha": "yarn workspace @cherrystudio/ai-core version prerelease --immediate && yarn workspace @cherrystudio/ai-core npm publish --tag alpha --access public",
|
"release:aicore:alpha": "yarn workspace @cherrystudio/ai-core version prerelease --immediate && yarn workspace @cherrystudio/ai-core npm publish --tag alpha --access public",
|
||||||
"release:aicore:beta": "yarn workspace @cherrystudio/ai-core version prerelease --immediate && yarn workspace @cherrystudio/ai-core npm publish --tag beta --access public",
|
"release:aicore:beta": "yarn workspace @cherrystudio/ai-core version prerelease --immediate && yarn workspace @cherrystudio/ai-core npm publish --tag beta --access public",
|
||||||
"release:aicore": "yarn workspace @cherrystudio/ai-core version patch --immediate && yarn workspace @cherrystudio/ai-core npm publish --access public"
|
"release:aicore": "yarn workspace @cherrystudio/ai-core version patch --immediate && yarn workspace @cherrystudio/ai-core npm publish --access public"
|
||||||
@@ -124,6 +127,7 @@
|
|||||||
"@cherrystudio/embedjs-ollama": "^0.1.31",
|
"@cherrystudio/embedjs-ollama": "^0.1.31",
|
||||||
"@cherrystudio/embedjs-openai": "^0.1.31",
|
"@cherrystudio/embedjs-openai": "^0.1.31",
|
||||||
"@cherrystudio/extension-table-plus": "workspace:^",
|
"@cherrystudio/extension-table-plus": "workspace:^",
|
||||||
|
"@cherrystudio/ui": "workspace:*",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
@@ -139,7 +143,6 @@
|
|||||||
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
|
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@heroui/react": "^2.8.3",
|
"@heroui/react": "^2.8.3",
|
||||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
|
||||||
"@langchain/community": "^0.3.50",
|
"@langchain/community": "^0.3.50",
|
||||||
"@mistralai/mistralai": "^1.7.5",
|
"@mistralai/mistralai": "^1.7.5",
|
||||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* 中间件管理器
|
* 中间件管理器
|
||||||
* 专注于 AI SDK 中间件的管理,与插件系统分离
|
* 专注于 AI SDK 中间件的管理,与插件系统分离
|
||||||
*/
|
*/
|
||||||
import { LanguageModelV2Middleware } from '@ai-sdk/provider'
|
import type { LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建中间件列表
|
* 创建中间件列表
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* 中间件系统类型定义
|
* 中间件系统类型定义
|
||||||
*/
|
*/
|
||||||
import { LanguageModelV2Middleware } from '@ai-sdk/provider'
|
import type { LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 具名中间件接口
|
* 具名中间件接口
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* 模型包装工具函数
|
* 模型包装工具函数
|
||||||
* 用于将中间件应用到LanguageModel上
|
* 用于将中间件应用到LanguageModel上
|
||||||
*/
|
*/
|
||||||
import { LanguageModelV2, LanguageModelV2Middleware } from '@ai-sdk/provider'
|
import type { LanguageModelV2, LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||||
import { wrapLanguageModel } from 'ai'
|
import { wrapLanguageModel } from 'ai'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* 集成了来自 ModelCreator 的特殊处理逻辑
|
* 集成了来自 ModelCreator 的特殊处理逻辑
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EmbeddingModelV2, ImageModelV2, LanguageModelV2, LanguageModelV2Middleware } from '@ai-sdk/provider'
|
import type { EmbeddingModelV2, ImageModelV2, LanguageModelV2, LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||||
|
|
||||||
import { wrapModelWithMiddlewares } from '../middleware/wrapper'
|
import { wrapModelWithMiddlewares } from '../middleware/wrapper'
|
||||||
import { DEFAULT_SEPARATOR, globalRegistryManagement } from '../providers/RegistryManagement'
|
import { DEFAULT_SEPARATOR, globalRegistryManagement } from '../providers/RegistryManagement'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Creation 模块类型定义
|
* Creation 模块类型定义
|
||||||
*/
|
*/
|
||||||
import { LanguageModelV2Middleware } from '@ai-sdk/provider'
|
import type { LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||||
|
|
||||||
import type { ProviderId, ProviderSettingsMap } from '../providers/types'
|
import type { ProviderId, ProviderSettingsMap } from '../providers/types'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ExtractProviderOptions, ProviderOptionsMap, TypedProviderOptions } from './types'
|
import type { ExtractProviderOptions, ProviderOptionsMap, TypedProviderOptions } from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建特定供应商的选项
|
* 创建特定供应商的选项
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { AiRequestContext } from '../../types'
|
|||||||
import { StreamEventManager } from './StreamEventManager'
|
import { StreamEventManager } from './StreamEventManager'
|
||||||
import { type TagConfig, TagExtractor } from './tagExtraction'
|
import { type TagConfig, TagExtractor } from './tagExtraction'
|
||||||
import { ToolExecutor } from './ToolExecutor'
|
import { ToolExecutor } from './ToolExecutor'
|
||||||
import { PromptToolUseConfig, ToolUseResult } from './type'
|
import type { PromptToolUseConfig, ToolUseResult } from './type'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具使用标签配置
|
* 工具使用标签配置
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ToolSet } from 'ai'
|
import type { ToolSet } from 'ai'
|
||||||
|
|
||||||
import { AiRequestContext } from '../..'
|
import type { AiRequestContext } from '../..'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析结果类型
|
* 解析结果类型
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { anthropic } from '@ai-sdk/anthropic'
|
import type { anthropic } from '@ai-sdk/anthropic'
|
||||||
import { google } from '@ai-sdk/google'
|
import type { google } from '@ai-sdk/google'
|
||||||
import { openai } from '@ai-sdk/openai'
|
import type { openai } from '@ai-sdk/openai'
|
||||||
import { InferToolInput, InferToolOutput, type Tool } from 'ai'
|
import type { InferToolInput, InferToolOutput, Tool } from 'ai'
|
||||||
|
|
||||||
import { ProviderOptionsMap } from '../../../options/types'
|
import type { ProviderOptionsMap } from '../../../options/types'
|
||||||
import { OpenRouterSearchConfig } from './openrouter'
|
import type { OpenRouterSearchConfig } from './openrouter'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从 AI SDK 的工具函数中提取参数类型,以确保类型安全。
|
* 从 AI SDK 的工具函数中提取参数类型,以确保类型安全。
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { openai } from '@ai-sdk/openai'
|
|||||||
import { createOpenRouterOptions, createXaiOptions, mergeProviderOptions } from '../../../options'
|
import { createOpenRouterOptions, createXaiOptions, mergeProviderOptions } from '../../../options'
|
||||||
import { definePlugin } from '../../'
|
import { definePlugin } from '../../'
|
||||||
import type { AiRequestContext } from '../../types'
|
import type { AiRequestContext } from '../../types'
|
||||||
import { DEFAULT_WEB_SEARCH_CONFIG, WebSearchPluginConfig } from './helper'
|
import type { WebSearchPluginConfig } from './helper'
|
||||||
|
import { DEFAULT_WEB_SEARCH_CONFIG } from './helper'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 网络搜索插件
|
* 网络搜索插件
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AiPlugin, AiRequestContext } from './types'
|
import type { AiPlugin, AiRequestContext } from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插件管理器
|
* 插件管理器
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* 例如: aihubmix:anthropic:claude-3.5-sonnet
|
* 例如: aihubmix:anthropic:claude-3.5-sonnet
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ProviderV2 } from '@ai-sdk/provider'
|
import type { ProviderV2 } from '@ai-sdk/provider'
|
||||||
import { customProvider } from 'ai'
|
import { customProvider } from 'ai'
|
||||||
|
|
||||||
import { globalRegistryManagement } from './RegistryManagement'
|
import { globalRegistryManagement } from './RegistryManagement'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 基于 AI SDK 原生的 createProviderRegistry
|
* 基于 AI SDK 原生的 createProviderRegistry
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EmbeddingModelV2, ImageModelV2, LanguageModelV2, ProviderV2 } from '@ai-sdk/provider'
|
import type { EmbeddingModelV2, ImageModelV2, LanguageModelV2, ProviderV2 } from '@ai-sdk/provider'
|
||||||
import { createProviderRegistry, type ProviderRegistryProvider } from 'ai'
|
import { createProviderRegistry, type ProviderRegistryProvider } from 'ai'
|
||||||
|
|
||||||
type PROVIDERS = Record<string, ProviderV2>
|
type PROVIDERS = Record<string, ProviderV2>
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import { createDeepSeek } from '@ai-sdk/deepseek'
|
|||||||
import { createGoogleGenerativeAI } from '@ai-sdk/google'
|
import { createGoogleGenerativeAI } from '@ai-sdk/google'
|
||||||
import { createOpenAI, type OpenAIProviderSettings } from '@ai-sdk/openai'
|
import { createOpenAI, type OpenAIProviderSettings } from '@ai-sdk/openai'
|
||||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
|
||||||
import { LanguageModelV2 } from '@ai-sdk/provider'
|
import type { LanguageModelV2 } from '@ai-sdk/provider'
|
||||||
import { createXai } from '@ai-sdk/xai'
|
import { createXai } from '@ai-sdk/xai'
|
||||||
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
||||||
import { customProvider, Provider } from 'ai'
|
import type { Provider } from 'ai'
|
||||||
|
import { customProvider } from 'ai'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { type DeepSeekProviderSettings } from '@ai-sdk/deepseek'
|
|||||||
import { type GoogleGenerativeAIProviderSettings } from '@ai-sdk/google'
|
import { type GoogleGenerativeAIProviderSettings } from '@ai-sdk/google'
|
||||||
import { type OpenAIProviderSettings } from '@ai-sdk/openai'
|
import { type OpenAIProviderSettings } from '@ai-sdk/openai'
|
||||||
import { type OpenAICompatibleProviderSettings } from '@ai-sdk/openai-compatible'
|
import { type OpenAICompatibleProviderSettings } from '@ai-sdk/openai-compatible'
|
||||||
import {
|
import type {
|
||||||
EmbeddingModelV2 as EmbeddingModel,
|
EmbeddingModelV2 as EmbeddingModel,
|
||||||
ImageModelV2 as ImageModel,
|
ImageModelV2 as ImageModel,
|
||||||
LanguageModelV2 as LanguageModel,
|
LanguageModelV2 as LanguageModel,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ImageModelV2 } from '@ai-sdk/provider'
|
import type { ImageModelV2 } from '@ai-sdk/provider'
|
||||||
import { experimental_generateImage as aiGenerateImage, NoImageGeneratedError } from 'ai'
|
import { experimental_generateImage as aiGenerateImage, NoImageGeneratedError } from 'ai'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
* 运行时执行器
|
* 运行时执行器
|
||||||
* 专注于插件化的AI调用处理
|
* 专注于插件化的AI调用处理
|
||||||
*/
|
*/
|
||||||
import { ImageModelV2, LanguageModelV2, LanguageModelV2Middleware } from '@ai-sdk/provider'
|
import type { ImageModelV2, LanguageModelV2, LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||||
|
import type { LanguageModel } from 'ai'
|
||||||
import {
|
import {
|
||||||
experimental_generateImage as _generateImage,
|
experimental_generateImage as _generateImage,
|
||||||
generateObject as _generateObject,
|
generateObject as _generateObject,
|
||||||
generateText as _generateText,
|
generateText as _generateText,
|
||||||
LanguageModel,
|
|
||||||
streamObject as _streamObject,
|
streamObject as _streamObject,
|
||||||
streamText as _streamText
|
streamText as _streamText
|
||||||
} from 'ai'
|
} from 'ai'
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export type { RuntimeConfig } from './types'
|
|||||||
|
|
||||||
// === 便捷工厂函数 ===
|
// === 便捷工厂函数 ===
|
||||||
|
|
||||||
import { LanguageModelV2Middleware } from '@ai-sdk/provider'
|
import type { LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||||
|
|
||||||
import { type AiPlugin } from '../plugins'
|
import { type AiPlugin } from '../plugins'
|
||||||
import { type ProviderId, type ProviderSettingsMap } from '../providers/types'
|
import { type ProviderId, type ProviderSettingsMap } from '../providers/types'
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
/* eslint-disable @eslint-react/naming-convention/context-name */
|
/* eslint-disable @eslint-react/naming-convention/context-name */
|
||||||
import { ImageModelV2 } from '@ai-sdk/provider'
|
import type { ImageModelV2 } from '@ai-sdk/provider'
|
||||||
import { experimental_generateImage, generateObject, generateText, LanguageModel, streamObject, streamText } from 'ai'
|
import type {
|
||||||
|
experimental_generateImage,
|
||||||
|
generateObject,
|
||||||
|
generateText,
|
||||||
|
LanguageModel,
|
||||||
|
streamObject,
|
||||||
|
streamText
|
||||||
|
} from 'ai'
|
||||||
|
|
||||||
import { type AiPlugin, createContext, PluginManager } from '../plugins'
|
import { type AiPlugin, createContext, PluginManager } from '../plugins'
|
||||||
import { type ProviderId } from '../providers/types'
|
import { type ProviderId } from '../providers/types'
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Runtime 层类型定义
|
* Runtime 层类型定义
|
||||||
*/
|
*/
|
||||||
import { ImageModelV2 } from '@ai-sdk/provider'
|
import type { ImageModelV2 } from '@ai-sdk/provider'
|
||||||
import { experimental_generateImage, generateObject, generateText, streamObject, streamText } from 'ai'
|
import type { experimental_generateImage, generateObject, generateText, streamObject, streamText } from 'ai'
|
||||||
|
|
||||||
import { type ModelConfig } from '../models/types'
|
import { type ModelConfig } from '../models/types'
|
||||||
import { type AiPlugin } from '../plugins'
|
import { type AiPlugin } from '../plugins'
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Extension, Node } from '@tiptap/core'
|
import type { Node } from '@tiptap/core'
|
||||||
|
import { Extension } from '@tiptap/core'
|
||||||
|
|
||||||
import type { TableCellOptions } from '../cell/index.js'
|
import type { TableCellOptions } from '../cell/index.js'
|
||||||
import { TableCell } from '../cell/index.js'
|
import { TableCell } from '../cell/index.js'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SpanKind, SpanStatusCode } from '@opentelemetry/api'
|
import { SpanKind, SpanStatusCode } from '@opentelemetry/api'
|
||||||
import { ReadableSpan } from '@opentelemetry/sdk-trace-base'
|
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'
|
||||||
|
|
||||||
import { SpanEntity } from '../types/config'
|
import type { SpanEntity } from '../types/config'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* convert ReadableSpan to SpanEntity
|
* convert ReadableSpan to SpanEntity
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReadableSpan } from '@opentelemetry/sdk-trace-base'
|
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'
|
||||||
|
|
||||||
export interface TraceCache {
|
export interface TraceCache {
|
||||||
createSpan: (span: ReadableSpan) => void
|
createSpan: (span: ReadableSpan) => void
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ExportResult, ExportResultCode } from '@opentelemetry/core'
|
import type { ExportResult } from '@opentelemetry/core'
|
||||||
import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'
|
import { ExportResultCode } from '@opentelemetry/core'
|
||||||
|
import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'
|
||||||
|
|
||||||
export type SaveFunction = (spans: ReadableSpan[]) => Promise<void>
|
export type SaveFunction = (spans: ReadableSpan[]) => Promise<void>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Context, trace } from '@opentelemetry/api'
|
import type { Context } from '@opentelemetry/api'
|
||||||
import { BatchSpanProcessor, BufferConfig, ReadableSpan, Span, SpanExporter } from '@opentelemetry/sdk-trace-base'
|
import { trace } from '@opentelemetry/api'
|
||||||
|
import type { BufferConfig, ReadableSpan, Span, SpanExporter } from '@opentelemetry/sdk-trace-base'
|
||||||
|
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
||||||
|
|
||||||
import { TraceCache } from '../core/traceCache'
|
import type { TraceCache } from '../core/traceCache'
|
||||||
|
|
||||||
export class CacheBatchSpanProcessor extends BatchSpanProcessor {
|
export class CacheBatchSpanProcessor extends BatchSpanProcessor {
|
||||||
private cache: TraceCache
|
private cache: TraceCache
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Context } from '@opentelemetry/api'
|
import type { Context } from '@opentelemetry/api'
|
||||||
import { BatchSpanProcessor, BufferConfig, ReadableSpan, Span, SpanExporter } from '@opentelemetry/sdk-trace-base'
|
import type { BufferConfig, ReadableSpan, Span, SpanExporter } from '@opentelemetry/sdk-trace-base'
|
||||||
import { EventEmitter } from 'stream'
|
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
||||||
|
import type { EventEmitter } from 'stream'
|
||||||
|
|
||||||
import { convertSpanToSpanEntity } from '../core/spanConvert'
|
import { convertSpanToSpanEntity } from '../core/spanConvert'
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Context, trace } from '@opentelemetry/api'
|
import type { Context } from '@opentelemetry/api'
|
||||||
import { BatchSpanProcessor, BufferConfig, ReadableSpan, Span, SpanExporter } from '@opentelemetry/sdk-trace-base'
|
import { trace } from '@opentelemetry/api'
|
||||||
|
import type { BufferConfig, ReadableSpan, Span, SpanExporter } from '@opentelemetry/sdk-trace-base'
|
||||||
|
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
||||||
|
|
||||||
export type SpanFunction = (span: ReadableSpan) => void
|
export type SpanFunction = (span: ReadableSpan) => void
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Link } from '@opentelemetry/api'
|
import type { Link } from '@opentelemetry/api'
|
||||||
import { TimedEvent } from '@opentelemetry/sdk-trace-base'
|
import type { TimedEvent } from '@opentelemetry/sdk-trace-base'
|
||||||
|
|
||||||
export type AttributeValue =
|
export type AttributeValue =
|
||||||
| string
|
| string
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { trace, Tracer } from '@opentelemetry/api'
|
import type { Tracer } from '@opentelemetry/api'
|
||||||
|
import { trace } from '@opentelemetry/api'
|
||||||
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
|
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'
|
||||||
import { W3CTraceContextPropagator } from '@opentelemetry/core'
|
import { W3CTraceContextPropagator } from '@opentelemetry/core'
|
||||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
|
||||||
import { BatchSpanProcessor, ConsoleSpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-base'
|
import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'
|
||||||
|
import { BatchSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base'
|
||||||
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
|
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
|
||||||
|
|
||||||
import { defaultConfig, TraceConfig } from '../trace-core/types/config'
|
import type { TraceConfig } from '../trace-core/types/config'
|
||||||
|
import { defaultConfig } from '../trace-core/types/config'
|
||||||
|
|
||||||
export class NodeTracer {
|
export class NodeTracer {
|
||||||
private static provider: NodeTracerProvider
|
private static provider: NodeTracerProvider
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Context, ContextManager, ROOT_CONTEXT } from '@opentelemetry/api'
|
import type { Context, ContextManager } from '@opentelemetry/api'
|
||||||
|
import { ROOT_CONTEXT } from '@opentelemetry/api'
|
||||||
|
|
||||||
export class TopicContextManager implements ContextManager {
|
export class TopicContextManager implements ContextManager {
|
||||||
private topicContextStack: Map<string, Context[]>
|
private topicContextStack: Map<string, Context[]>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Context, context } from '@opentelemetry/api'
|
import type { Context } from '@opentelemetry/api'
|
||||||
|
import { context } from '@opentelemetry/api'
|
||||||
|
|
||||||
const originalPromise = globalThis.Promise
|
const originalPromise = globalThis.Promise
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { W3CTraceContextPropagator } from '@opentelemetry/core'
|
import { W3CTraceContextPropagator } from '@opentelemetry/core'
|
||||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
|
||||||
import { BatchSpanProcessor, ConsoleSpanExporter, SpanProcessor } from '@opentelemetry/sdk-trace-base'
|
import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'
|
||||||
|
import { BatchSpanProcessor, ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base'
|
||||||
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'
|
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'
|
||||||
|
|
||||||
import { defaultConfig, TraceConfig } from '../trace-core/types/config'
|
import type { TraceConfig } from '../trace-core/types/config'
|
||||||
|
import { defaultConfig } from '../trace-core/types/config'
|
||||||
import { TopicContextManager } from './TopicContextManager'
|
import { TopicContextManager } from './TopicContextManager'
|
||||||
|
|
||||||
export const contextManager = new TopicContextManager()
|
export const contextManager = new TopicContextManager()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export enum IpcChannel {
|
|||||||
App_GetCacheSize = 'app:get-cache-size',
|
App_GetCacheSize = 'app:get-cache-size',
|
||||||
App_ClearCache = 'app:clear-cache',
|
App_ClearCache = 'app:clear-cache',
|
||||||
App_SetLaunchOnBoot = 'app:set-launch-on-boot',
|
App_SetLaunchOnBoot = 'app:set-launch-on-boot',
|
||||||
App_SetLanguage = 'app:set-language',
|
// App_SetLanguage = 'app:set-language',
|
||||||
App_SetEnableSpellCheck = 'app:set-enable-spell-check',
|
App_SetEnableSpellCheck = 'app:set-enable-spell-check',
|
||||||
App_SetSpellCheckLanguages = 'app:set-spell-check-languages',
|
App_SetSpellCheckLanguages = 'app:set-spell-check-languages',
|
||||||
App_CheckForUpdate = 'app:check-for-update',
|
App_CheckForUpdate = 'app:check-for-update',
|
||||||
@@ -14,7 +14,7 @@ export enum IpcChannel {
|
|||||||
App_SetLaunchToTray = 'app:set-launch-to-tray',
|
App_SetLaunchToTray = 'app:set-launch-to-tray',
|
||||||
App_SetTray = 'app:set-tray',
|
App_SetTray = 'app:set-tray',
|
||||||
App_SetTrayOnClose = 'app:set-tray-on-close',
|
App_SetTrayOnClose = 'app:set-tray-on-close',
|
||||||
App_SetTheme = 'app:set-theme',
|
// App_SetTheme = 'app:set-theme',
|
||||||
App_SetAutoUpdate = 'app:set-auto-update',
|
App_SetAutoUpdate = 'app:set-auto-update',
|
||||||
App_SetTestPlan = 'app:set-test-plan',
|
App_SetTestPlan = 'app:set-test-plan',
|
||||||
App_SetTestChannel = 'app:set-test-channel',
|
App_SetTestChannel = 'app:set-test-channel',
|
||||||
@@ -46,7 +46,7 @@ export enum IpcChannel {
|
|||||||
App_MacRequestProcessTrust = 'app:mac-request-process-trust',
|
App_MacRequestProcessTrust = 'app:mac-request-process-trust',
|
||||||
|
|
||||||
App_QuoteToMain = 'app:quote-to-main',
|
App_QuoteToMain = 'app:quote-to-main',
|
||||||
App_SetDisableHardwareAcceleration = 'app:set-disable-hardware-acceleration',
|
// App_SetDisableHardwareAcceleration = 'app:set-disable-hardware-acceleration',
|
||||||
|
|
||||||
Notification_Send = 'notification:send',
|
Notification_Send = 'notification:send',
|
||||||
Notification_OnClick = 'notification:on-click',
|
Notification_OnClick = 'notification:on-click',
|
||||||
@@ -220,6 +220,22 @@ export enum IpcChannel {
|
|||||||
Backup_DeleteS3File = 'backup:deleteS3File',
|
Backup_DeleteS3File = 'backup:deleteS3File',
|
||||||
Backup_CheckS3Connection = 'backup:checkS3Connection',
|
Backup_CheckS3Connection = 'backup:checkS3Connection',
|
||||||
|
|
||||||
|
// data migration
|
||||||
|
DataMigrate_CheckNeeded = 'data-migrate:check-needed',
|
||||||
|
DataMigrate_GetProgress = 'data-migrate:get-progress',
|
||||||
|
DataMigrate_Cancel = 'data-migrate:cancel',
|
||||||
|
DataMigrate_RequireBackup = 'data-migrate:require-backup',
|
||||||
|
DataMigrate_BackupCompleted = 'data-migrate:backup-completed',
|
||||||
|
DataMigrate_ShowBackupDialog = 'data-migrate:show-backup-dialog',
|
||||||
|
DataMigrate_StartFlow = 'data-migrate:start-flow',
|
||||||
|
DataMigrate_ProceedToBackup = 'data-migrate:proceed-to-backup',
|
||||||
|
DataMigrate_StartMigration = 'data-migrate:start-migration',
|
||||||
|
DataMigrate_RetryMigration = 'data-migrate:retry-migration',
|
||||||
|
DataMigrate_RestartApp = 'data-migrate:restart-app',
|
||||||
|
DataMigrate_CloseWindow = 'data-migrate:close-window',
|
||||||
|
DataMigrate_SendReduxData = 'data-migrate:send-redux-data',
|
||||||
|
DataMigrate_GetReduxData = 'data-migrate:get-redux-data',
|
||||||
|
|
||||||
// zip
|
// zip
|
||||||
Zip_Compress = 'zip:compress',
|
Zip_Compress = 'zip:compress',
|
||||||
Zip_Decompress = 'zip:decompress',
|
Zip_Decompress = 'zip:decompress',
|
||||||
@@ -234,7 +250,8 @@ export enum IpcChannel {
|
|||||||
|
|
||||||
// events
|
// events
|
||||||
BackupProgress = 'backup-progress',
|
BackupProgress = 'backup-progress',
|
||||||
ThemeUpdated = 'theme:updated',
|
DataMigrateProgress = 'data-migrate-progress',
|
||||||
|
NativeThemeUpdated = 'native-theme:updated',
|
||||||
RestoreProgress = 'restore-progress',
|
RestoreProgress = 'restore-progress',
|
||||||
UpdateError = 'update-error',
|
UpdateError = 'update-error',
|
||||||
UpdateAvailable = 'update-available',
|
UpdateAvailable = 'update-available',
|
||||||
@@ -273,12 +290,6 @@ export enum IpcChannel {
|
|||||||
Selection_ToolbarVisibilityChange = 'selection:toolbar-visibility-change',
|
Selection_ToolbarVisibilityChange = 'selection:toolbar-visibility-change',
|
||||||
Selection_ToolbarDetermineSize = 'selection:toolbar-determine-size',
|
Selection_ToolbarDetermineSize = 'selection:toolbar-determine-size',
|
||||||
Selection_WriteToClipboard = 'selection:write-to-clipboard',
|
Selection_WriteToClipboard = 'selection:write-to-clipboard',
|
||||||
Selection_SetEnabled = 'selection:set-enabled',
|
|
||||||
Selection_SetTriggerMode = 'selection:set-trigger-mode',
|
|
||||||
Selection_SetFilterMode = 'selection:set-filter-mode',
|
|
||||||
Selection_SetFilterList = 'selection:set-filter-list',
|
|
||||||
Selection_SetFollowToolbar = 'selection:set-follow-toolbar',
|
|
||||||
Selection_SetRemeberWinSize = 'selection:set-remeber-win-size',
|
|
||||||
Selection_ActionWindowClose = 'selection:action-window-close',
|
Selection_ActionWindowClose = 'selection:action-window-close',
|
||||||
Selection_ActionWindowMinimize = 'selection:action-window-minimize',
|
Selection_ActionWindowMinimize = 'selection:action-window-minimize',
|
||||||
Selection_ActionWindowPin = 'selection:action-window-pin',
|
Selection_ActionWindowPin = 'selection:action-window-pin',
|
||||||
@@ -297,6 +308,27 @@ export enum IpcChannel {
|
|||||||
Memory_DeleteAllMemoriesForUser = 'memory:delete-all-memories-for-user',
|
Memory_DeleteAllMemoriesForUser = 'memory:delete-all-memories-for-user',
|
||||||
Memory_GetUsersList = 'memory:get-users-list',
|
Memory_GetUsersList = 'memory:get-users-list',
|
||||||
|
|
||||||
|
// Data: Preference
|
||||||
|
Preference_Get = 'preference:get',
|
||||||
|
Preference_Set = 'preference:set',
|
||||||
|
Preference_GetMultiple = 'preference:get-multiple',
|
||||||
|
Preference_SetMultiple = 'preference:set-multiple',
|
||||||
|
Preference_GetAll = 'preference:get-all',
|
||||||
|
Preference_Subscribe = 'preference:subscribe',
|
||||||
|
Preference_Changed = 'preference:changed',
|
||||||
|
|
||||||
|
// Data: Cache
|
||||||
|
Cache_Sync = 'cache:sync',
|
||||||
|
Cache_SyncBatch = 'cache:sync-batch',
|
||||||
|
|
||||||
|
// Data: API Channels
|
||||||
|
DataApi_Request = 'data-api:request',
|
||||||
|
DataApi_Batch = 'data-api:batch',
|
||||||
|
DataApi_Transaction = 'data-api:transaction',
|
||||||
|
DataApi_Subscribe = 'data-api:subscribe',
|
||||||
|
DataApi_Unsubscribe = 'data-api:unsubscribe',
|
||||||
|
DataApi_Stream = 'data-api:stream',
|
||||||
|
|
||||||
// TRACE
|
// TRACE
|
||||||
TRACE_SAVE_DATA = 'trace:saveData',
|
TRACE_SAVE_DATA = 'trace:saveData',
|
||||||
TRACE_GET_DATA = 'trace:getData',
|
TRACE_GET_DATA = 'trace:getData',
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Anthropic from '@anthropic-ai/sdk'
|
import Anthropic from '@anthropic-ai/sdk'
|
||||||
import { TextBlockParam } from '@anthropic-ai/sdk/resources'
|
import type { TextBlockParam } from '@anthropic-ai/sdk/resources'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { Provider } from '@types'
|
import type { Provider } from '@types'
|
||||||
import type { ModelMessage } from 'ai'
|
import type { ModelMessage } from 'ai'
|
||||||
|
|
||||||
const logger = loggerService.withContext('anthropic-sdk')
|
const logger = loggerService.withContext('anthropic-sdk')
|
||||||
|
|||||||
@@ -197,11 +197,11 @@ export enum FeedUrl {
|
|||||||
GITHUB_LATEST = 'https://github.com/CherryHQ/cherry-studio/releases/latest/download'
|
GITHUB_LATEST = 'https://github.com/CherryHQ/cherry-studio/releases/latest/download'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum UpgradeChannel {
|
// export enum UpgradeChannel {
|
||||||
LATEST = 'latest', // 最新稳定版本
|
// LATEST = 'latest', // 最新稳定版本
|
||||||
RC = 'rc', // 公测版本
|
// RC = 'rc', // 公测版本
|
||||||
BETA = 'beta' // 预览版本
|
// BETA = 'beta' // 预览版本
|
||||||
}
|
// }
|
||||||
|
|
||||||
export const defaultTimeout = 10 * 1000 * 60
|
export const defaultTimeout = 10 * 1000 * 60
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ProcessingStatus } from '@types'
|
import type { ProcessingStatus } from '@types'
|
||||||
|
|
||||||
export type LoaderReturn = {
|
export type LoaderReturn = {
|
||||||
entriesAdded: number
|
entriesAdded: number
|
||||||
|
|||||||
106
packages/shared/data/README.md
Normal file
106
packages/shared/data/README.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Cherry Studio Shared Data
|
||||||
|
|
||||||
|
This directory contains shared type definitions and schemas for the Cherry Studio data management systems. These files provide type safety and consistency across the entire application.
|
||||||
|
|
||||||
|
## 📁 Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/shared/data/
|
||||||
|
├── api/ # Data API type system
|
||||||
|
│ ├── index.ts # Barrel exports for clean imports
|
||||||
|
│ ├── apiSchemas.ts # API endpoint definitions and mappings
|
||||||
|
│ ├── apiTypes.ts # Core request/response infrastructure types
|
||||||
|
│ ├── apiModels.ts # Business entity types and DTOs
|
||||||
|
│ ├── apiPaths.ts # API path definitions and utilities
|
||||||
|
│ └── errorCodes.ts # Standardized error handling
|
||||||
|
├── cache/ # Cache system type definitions
|
||||||
|
│ ├── cacheTypes.ts # Core cache infrastructure types
|
||||||
|
│ ├── cacheSchemas.ts # Cache key schemas and type mappings
|
||||||
|
│ └── cacheValueTypes.ts # Cache value type definitions
|
||||||
|
├── preference/ # Preference system type definitions
|
||||||
|
│ ├── preferenceTypes.ts # Core preference system types
|
||||||
|
│ └── preferenceSchemas.ts # Preference schemas and default values
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ System Overview
|
||||||
|
|
||||||
|
This directory provides type definitions for three main data management systems:
|
||||||
|
|
||||||
|
### API System (`api/`)
|
||||||
|
- **Purpose**: Type-safe IPC communication between Main and Renderer processes
|
||||||
|
- **Features**: RESTful patterns, error handling, business entity definitions
|
||||||
|
- **Usage**: Ensures type safety for all data API operations
|
||||||
|
|
||||||
|
### Cache System (`cache/`)
|
||||||
|
- **Purpose**: Type definitions for three-layer caching architecture
|
||||||
|
- **Features**: Memory/shared/persist cache schemas, TTL support, hook integration
|
||||||
|
- **Usage**: Type-safe caching operations across the application
|
||||||
|
|
||||||
|
### Preference System (`preference/`)
|
||||||
|
- **Purpose**: User configuration and settings management
|
||||||
|
- **Features**: 158 configuration items, default values, nested key support
|
||||||
|
- **Usage**: Type-safe preference access and synchronization
|
||||||
|
|
||||||
|
## 📋 File Categories
|
||||||
|
|
||||||
|
**Framework Infrastructure** - These are TypeScript type definitions that:
|
||||||
|
- ✅ Exist only at compile time
|
||||||
|
- ✅ Provide type safety and IntelliSense support
|
||||||
|
- ✅ Define contracts between application layers
|
||||||
|
- ✅ Enable static analysis and error detection
|
||||||
|
|
||||||
|
## 📖 Usage Examples
|
||||||
|
|
||||||
|
### API Types
|
||||||
|
```typescript
|
||||||
|
// Import API types
|
||||||
|
import type { DataRequest, DataResponse, ApiSchemas } from '@shared/data/api'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Types
|
||||||
|
```typescript
|
||||||
|
// Import cache types
|
||||||
|
import type { UseCacheKey, UseSharedCacheKey } from '@shared/data/cache'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Preference Types
|
||||||
|
```typescript
|
||||||
|
// Import preference types
|
||||||
|
import type { PreferenceKeyType, PreferenceDefaultScopeType } from '@shared/data/preference'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Development Guidelines
|
||||||
|
|
||||||
|
### Adding Cache Types
|
||||||
|
1. Add cache key to `cache/cacheSchemas.ts`
|
||||||
|
2. Define value type in `cache/cacheValueTypes.ts`
|
||||||
|
3. Update type mappings for type safety
|
||||||
|
|
||||||
|
### Adding Preference Types
|
||||||
|
1. Add preference key to `preference/preferenceSchemas.ts`
|
||||||
|
2. Define default value and type
|
||||||
|
3. Preference system automatically picks up new keys
|
||||||
|
|
||||||
|
### Adding API Types
|
||||||
|
1. Define business entities in `api/apiModels.ts`
|
||||||
|
2. Add endpoint definitions to `api/apiSchemas.ts`
|
||||||
|
3. Export types from `api/index.ts`
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
- Use `import type` for type-only imports
|
||||||
|
- Follow existing naming conventions
|
||||||
|
- Document complex types with JSDoc
|
||||||
|
- Maintain type safety across all imports
|
||||||
|
|
||||||
|
## 🔗 Related Implementation
|
||||||
|
|
||||||
|
### Main Process Services
|
||||||
|
- `src/main/data/CacheService.ts` - Main process cache management
|
||||||
|
- `src/main/data/PreferenceService.ts` - Preference management service
|
||||||
|
- `src/main/data/DataApiService.ts` - Data API coordination service
|
||||||
|
|
||||||
|
### Renderer Process Services
|
||||||
|
- `src/renderer/src/data/CacheService.ts` - Renderer cache service
|
||||||
|
- `src/renderer/src/data/PreferenceService.ts` - Renderer preference service
|
||||||
|
- `src/renderer/src/data/DataApiService.ts` - Renderer API client
|
||||||
107
packages/shared/data/api/apiModels.ts
Normal file
107
packages/shared/data/api/apiModels.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* Generic test model definitions
|
||||||
|
* Contains flexible types for comprehensive API testing
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic test item entity - flexible structure for testing various scenarios
|
||||||
|
*/
|
||||||
|
export interface TestItem {
|
||||||
|
/** Unique identifier */
|
||||||
|
id: string
|
||||||
|
/** Item title */
|
||||||
|
title: string
|
||||||
|
/** Optional description */
|
||||||
|
description?: string
|
||||||
|
/** Type category */
|
||||||
|
type: string
|
||||||
|
/** Current status */
|
||||||
|
status: string
|
||||||
|
/** Priority level */
|
||||||
|
priority: string
|
||||||
|
/** Associated tags */
|
||||||
|
tags: string[]
|
||||||
|
/** Creation timestamp */
|
||||||
|
createdAt: string
|
||||||
|
/** Last update timestamp */
|
||||||
|
updatedAt: string
|
||||||
|
/** Additional metadata */
|
||||||
|
metadata: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data Transfer Objects (DTOs) for test operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for creating a new test item
|
||||||
|
*/
|
||||||
|
export interface CreateTestItemDto {
|
||||||
|
/** Item title */
|
||||||
|
title: string
|
||||||
|
/** Optional description */
|
||||||
|
description?: string
|
||||||
|
/** Type category */
|
||||||
|
type?: string
|
||||||
|
/** Current status */
|
||||||
|
status?: string
|
||||||
|
/** Priority level */
|
||||||
|
priority?: string
|
||||||
|
/** Associated tags */
|
||||||
|
tags?: string[]
|
||||||
|
/** Additional metadata */
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for updating an existing test item
|
||||||
|
*/
|
||||||
|
export interface UpdateTestItemDto {
|
||||||
|
/** Updated title */
|
||||||
|
title?: string
|
||||||
|
/** Updated description */
|
||||||
|
description?: string
|
||||||
|
/** Updated type */
|
||||||
|
type?: string
|
||||||
|
/** Updated status */
|
||||||
|
status?: string
|
||||||
|
/** Updated priority */
|
||||||
|
priority?: string
|
||||||
|
/** Updated tags */
|
||||||
|
tags?: string[]
|
||||||
|
/** Updated metadata */
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk operation types for batch processing
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request for bulk operations on multiple items
|
||||||
|
*/
|
||||||
|
export interface BulkOperationRequest<TData = any> {
|
||||||
|
/** Type of bulk operation to perform */
|
||||||
|
operation: 'create' | 'update' | 'delete' | 'archive' | 'restore'
|
||||||
|
/** Array of data items to process */
|
||||||
|
data: TData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from a bulk operation
|
||||||
|
*/
|
||||||
|
export interface BulkOperationResponse {
|
||||||
|
/** Number of successfully processed items */
|
||||||
|
successful: number
|
||||||
|
/** Number of items that failed processing */
|
||||||
|
failed: number
|
||||||
|
/** Array of errors that occurred during processing */
|
||||||
|
errors: Array<{
|
||||||
|
/** Index of the item that failed */
|
||||||
|
index: number
|
||||||
|
/** Error message */
|
||||||
|
error: string
|
||||||
|
/** Optional additional error data */
|
||||||
|
data?: any
|
||||||
|
}>
|
||||||
|
}
|
||||||
60
packages/shared/data/api/apiPaths.ts
Normal file
60
packages/shared/data/api/apiPaths.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { ApiSchemas } from './apiSchemas'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template literal type utilities for converting parameterized paths to concrete paths
|
||||||
|
* This enables type-safe API calls with actual paths like '/test/items/123' instead of '/test/items/:id'
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert parameterized path templates to concrete path types
|
||||||
|
* @example '/test/items/:id' -> '/test/items/${string}'
|
||||||
|
* @example '/topics/:id/messages' -> '/topics/${string}/messages'
|
||||||
|
*/
|
||||||
|
export type ResolvedPath<T extends string> = T extends `${infer Prefix}/:${string}/${infer Suffix}`
|
||||||
|
? `${Prefix}/${string}/${ResolvedPath<Suffix>}`
|
||||||
|
: T extends `${infer Prefix}/:${string}`
|
||||||
|
? `${Prefix}/${string}`
|
||||||
|
: T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate all possible concrete paths from ApiSchemas
|
||||||
|
* This creates a union type of all valid API paths
|
||||||
|
*/
|
||||||
|
export type ConcreteApiPaths = {
|
||||||
|
[K in keyof ApiSchemas]: ResolvedPath<K & string>
|
||||||
|
}[keyof ApiSchemas]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse lookup: from concrete path back to original template path
|
||||||
|
* Used to determine which ApiSchema entry matches a concrete path
|
||||||
|
*/
|
||||||
|
export type MatchApiPath<Path extends string> = {
|
||||||
|
[K in keyof ApiSchemas]: Path extends ResolvedPath<K & string> ? K : never
|
||||||
|
}[keyof ApiSchemas]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract query parameters type for a given concrete path
|
||||||
|
*/
|
||||||
|
export type QueryParamsForPath<Path extends string> = MatchApiPath<Path> extends keyof ApiSchemas
|
||||||
|
? ApiSchemas[MatchApiPath<Path>] extends { GET: { query?: infer Q } }
|
||||||
|
? Q
|
||||||
|
: Record<string, any>
|
||||||
|
: Record<string, any>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract request body type for a given concrete path and HTTP method
|
||||||
|
*/
|
||||||
|
export type BodyForPath<Path extends string, Method extends string> = MatchApiPath<Path> extends keyof ApiSchemas
|
||||||
|
? ApiSchemas[MatchApiPath<Path>] extends { [M in Method]: { body: infer B } }
|
||||||
|
? B
|
||||||
|
: any
|
||||||
|
: any
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract response type for a given concrete path and HTTP method
|
||||||
|
*/
|
||||||
|
export type ResponseForPath<Path extends string, Method extends string> = MatchApiPath<Path> extends keyof ApiSchemas
|
||||||
|
? ApiSchemas[MatchApiPath<Path>] extends { [M in Method]: { response: infer R } }
|
||||||
|
? R
|
||||||
|
: any
|
||||||
|
: any
|
||||||
487
packages/shared/data/api/apiSchemas.ts
Normal file
487
packages/shared/data/api/apiSchemas.ts
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
// NOTE: Types are defined inline in the schema for simplicity
|
||||||
|
// If needed, specific types can be imported from './apiModels'
|
||||||
|
import type { BodyForPath, ConcreteApiPaths, QueryParamsForPath, ResponseForPath } from './apiPaths'
|
||||||
|
import type { HttpMethod, PaginatedResponse, PaginationParams } from './apiTypes'
|
||||||
|
|
||||||
|
// Re-export for external use
|
||||||
|
export type { ConcreteApiPaths } from './apiPaths'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete API Schema definitions for Test API
|
||||||
|
*
|
||||||
|
* Each path defines the supported HTTP methods with their:
|
||||||
|
* - Request parameters (params, query, body)
|
||||||
|
* - Response types
|
||||||
|
* - Type safety guarantees
|
||||||
|
*
|
||||||
|
* This schema serves as the contract between renderer and main processes,
|
||||||
|
* enabling full TypeScript type checking across IPC boundaries.
|
||||||
|
*/
|
||||||
|
export interface ApiSchemas {
|
||||||
|
/**
|
||||||
|
* Test items collection endpoint
|
||||||
|
* @example GET /test/items?page=1&limit=10&search=hello
|
||||||
|
* @example POST /test/items { "title": "New Test Item" }
|
||||||
|
*/
|
||||||
|
'/test/items': {
|
||||||
|
/** List all test items with optional filtering and pagination */
|
||||||
|
GET: {
|
||||||
|
query?: PaginationParams & {
|
||||||
|
/** Search items by title or description */
|
||||||
|
search?: string
|
||||||
|
/** Filter by item type */
|
||||||
|
type?: string
|
||||||
|
/** Filter by status */
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
response: PaginatedResponse<any>
|
||||||
|
}
|
||||||
|
/** Create a new test item */
|
||||||
|
POST: {
|
||||||
|
body: {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
type?: string
|
||||||
|
status?: string
|
||||||
|
priority?: string
|
||||||
|
tags?: string[]
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
}
|
||||||
|
response: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Individual test item endpoint
|
||||||
|
* @example GET /test/items/123
|
||||||
|
* @example PUT /test/items/123 { "title": "Updated Title" }
|
||||||
|
* @example DELETE /test/items/123
|
||||||
|
*/
|
||||||
|
'/test/items/:id': {
|
||||||
|
/** Get a specific test item by ID */
|
||||||
|
GET: {
|
||||||
|
params: { id: string }
|
||||||
|
response: any
|
||||||
|
}
|
||||||
|
/** Update a specific test item */
|
||||||
|
PUT: {
|
||||||
|
params: { id: string }
|
||||||
|
body: {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
type?: string
|
||||||
|
status?: string
|
||||||
|
priority?: string
|
||||||
|
tags?: string[]
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
}
|
||||||
|
response: any
|
||||||
|
}
|
||||||
|
/** Delete a specific test item */
|
||||||
|
DELETE: {
|
||||||
|
params: { id: string }
|
||||||
|
response: void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test search endpoint
|
||||||
|
* @example GET /test/search?query=hello&page=1&limit=20
|
||||||
|
*/
|
||||||
|
'/test/search': {
|
||||||
|
/** Search test items */
|
||||||
|
GET: {
|
||||||
|
query: {
|
||||||
|
/** Search query string */
|
||||||
|
query: string
|
||||||
|
/** Page number for pagination */
|
||||||
|
page?: number
|
||||||
|
/** Number of results per page */
|
||||||
|
limit?: number
|
||||||
|
/** Additional filters */
|
||||||
|
type?: string
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
response: PaginatedResponse<any>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test statistics endpoint
|
||||||
|
* @example GET /test/stats
|
||||||
|
*/
|
||||||
|
'/test/stats': {
|
||||||
|
/** Get comprehensive test statistics */
|
||||||
|
GET: {
|
||||||
|
response: {
|
||||||
|
/** Total number of items */
|
||||||
|
total: number
|
||||||
|
/** Item count grouped by type */
|
||||||
|
byType: Record<string, number>
|
||||||
|
/** Item count grouped by status */
|
||||||
|
byStatus: Record<string, number>
|
||||||
|
/** Item count grouped by priority */
|
||||||
|
byPriority: Record<string, number>
|
||||||
|
/** Recent activity timeline */
|
||||||
|
recentActivity: Array<{
|
||||||
|
/** Date of activity */
|
||||||
|
date: string
|
||||||
|
/** Number of items on that date */
|
||||||
|
count: number
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test bulk operations endpoint
|
||||||
|
* @example POST /test/bulk { "operation": "create", "data": [...] }
|
||||||
|
*/
|
||||||
|
'/test/bulk': {
|
||||||
|
/** Perform bulk operations on test items */
|
||||||
|
POST: {
|
||||||
|
body: {
|
||||||
|
/** Operation type */
|
||||||
|
operation: 'create' | 'update' | 'delete'
|
||||||
|
/** Array of data items to process */
|
||||||
|
data: any[]
|
||||||
|
}
|
||||||
|
response: {
|
||||||
|
successful: number
|
||||||
|
failed: number
|
||||||
|
errors: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test error simulation endpoint
|
||||||
|
* @example POST /test/error { "errorType": "timeout" }
|
||||||
|
*/
|
||||||
|
'/test/error': {
|
||||||
|
/** Simulate various error scenarios for testing */
|
||||||
|
POST: {
|
||||||
|
body: {
|
||||||
|
/** Type of error to simulate */
|
||||||
|
errorType:
|
||||||
|
| 'timeout'
|
||||||
|
| 'network'
|
||||||
|
| 'server'
|
||||||
|
| 'notfound'
|
||||||
|
| 'validation'
|
||||||
|
| 'unauthorized'
|
||||||
|
| 'ratelimit'
|
||||||
|
| 'generic'
|
||||||
|
}
|
||||||
|
response: never
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test slow response endpoint
|
||||||
|
* @example POST /test/slow { "delay": 2000 }
|
||||||
|
*/
|
||||||
|
'/test/slow': {
|
||||||
|
/** Test slow response for performance testing */
|
||||||
|
POST: {
|
||||||
|
body: {
|
||||||
|
/** Delay in milliseconds */
|
||||||
|
delay: number
|
||||||
|
}
|
||||||
|
response: {
|
||||||
|
message: string
|
||||||
|
delay: number
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test data reset endpoint
|
||||||
|
* @example POST /test/reset
|
||||||
|
*/
|
||||||
|
'/test/reset': {
|
||||||
|
/** Reset all test data to initial state */
|
||||||
|
POST: {
|
||||||
|
response: {
|
||||||
|
message: string
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test config endpoint
|
||||||
|
* @example GET /test/config
|
||||||
|
* @example PUT /test/config { "setting": "value" }
|
||||||
|
*/
|
||||||
|
'/test/config': {
|
||||||
|
/** Get test configuration */
|
||||||
|
GET: {
|
||||||
|
response: Record<string, any>
|
||||||
|
}
|
||||||
|
/** Update test configuration */
|
||||||
|
PUT: {
|
||||||
|
body: Record<string, any>
|
||||||
|
response: Record<string, any>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test status endpoint
|
||||||
|
* @example GET /test/status
|
||||||
|
*/
|
||||||
|
'/test/status': {
|
||||||
|
/** Get system test status */
|
||||||
|
GET: {
|
||||||
|
response: {
|
||||||
|
status: string
|
||||||
|
timestamp: string
|
||||||
|
version: string
|
||||||
|
uptime: number
|
||||||
|
environment: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test performance endpoint
|
||||||
|
* @example GET /test/performance
|
||||||
|
*/
|
||||||
|
'/test/performance': {
|
||||||
|
/** Get performance metrics */
|
||||||
|
GET: {
|
||||||
|
response: {
|
||||||
|
requestsPerSecond: number
|
||||||
|
averageLatency: number
|
||||||
|
memoryUsage: number
|
||||||
|
cpuUsage: number
|
||||||
|
uptime: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch execution of multiple requests
|
||||||
|
* @example POST /batch { "requests": [...], "parallel": true }
|
||||||
|
*/
|
||||||
|
'/batch': {
|
||||||
|
/** Execute multiple API requests in a single call */
|
||||||
|
POST: {
|
||||||
|
body: {
|
||||||
|
/** Array of requests to execute */
|
||||||
|
requests: Array<{
|
||||||
|
/** HTTP method for the request */
|
||||||
|
method: HttpMethod
|
||||||
|
/** API path for the request */
|
||||||
|
path: string
|
||||||
|
/** URL parameters */
|
||||||
|
params?: any
|
||||||
|
/** Request body */
|
||||||
|
body?: any
|
||||||
|
}>
|
||||||
|
/** Execute requests in parallel vs sequential */
|
||||||
|
parallel?: boolean
|
||||||
|
}
|
||||||
|
response: {
|
||||||
|
/** Results array matching input order */
|
||||||
|
results: Array<{
|
||||||
|
/** HTTP status code */
|
||||||
|
status: number
|
||||||
|
/** Response data if successful */
|
||||||
|
data?: any
|
||||||
|
/** Error information if failed */
|
||||||
|
error?: any
|
||||||
|
}>
|
||||||
|
/** Batch execution metadata */
|
||||||
|
metadata: {
|
||||||
|
/** Total execution duration in ms */
|
||||||
|
duration: number
|
||||||
|
/** Number of successful requests */
|
||||||
|
successCount: number
|
||||||
|
/** Number of failed requests */
|
||||||
|
errorCount: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomic transaction of multiple operations
|
||||||
|
* @example POST /transaction { "operations": [...], "options": { "rollbackOnError": true } }
|
||||||
|
*/
|
||||||
|
'/transaction': {
|
||||||
|
/** Execute multiple operations in a database transaction */
|
||||||
|
POST: {
|
||||||
|
body: {
|
||||||
|
/** Array of operations to execute atomically */
|
||||||
|
operations: Array<{
|
||||||
|
/** HTTP method for the operation */
|
||||||
|
method: HttpMethod
|
||||||
|
/** API path for the operation */
|
||||||
|
path: string
|
||||||
|
/** URL parameters */
|
||||||
|
params?: any
|
||||||
|
/** Request body */
|
||||||
|
body?: any
|
||||||
|
}>
|
||||||
|
/** Transaction configuration options */
|
||||||
|
options?: {
|
||||||
|
/** Database isolation level */
|
||||||
|
isolation?: 'read-uncommitted' | 'read-committed' | 'repeatable-read' | 'serializable'
|
||||||
|
/** Rollback all operations on any error */
|
||||||
|
rollbackOnError?: boolean
|
||||||
|
/** Transaction timeout in milliseconds */
|
||||||
|
timeout?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response: Array<{
|
||||||
|
/** HTTP status code */
|
||||||
|
status: number
|
||||||
|
/** Response data if successful */
|
||||||
|
data?: any
|
||||||
|
/** Error information if failed */
|
||||||
|
error?: any
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplified type extraction helpers
|
||||||
|
*/
|
||||||
|
export type ApiPaths = keyof ApiSchemas
|
||||||
|
export type ApiMethods<TPath extends ApiPaths> = keyof ApiSchemas[TPath] & HttpMethod
|
||||||
|
export type ApiResponse<TPath extends ApiPaths, TMethod extends string> = TPath extends keyof ApiSchemas
|
||||||
|
? TMethod extends keyof ApiSchemas[TPath]
|
||||||
|
? ApiSchemas[TPath][TMethod] extends { response: infer R }
|
||||||
|
? R
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type ApiParams<TPath extends ApiPaths, TMethod extends string> = TPath extends keyof ApiSchemas
|
||||||
|
? TMethod extends keyof ApiSchemas[TPath]
|
||||||
|
? ApiSchemas[TPath][TMethod] extends { params: infer P }
|
||||||
|
? P
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type ApiQuery<TPath extends ApiPaths, TMethod extends string> = TPath extends keyof ApiSchemas
|
||||||
|
? TMethod extends keyof ApiSchemas[TPath]
|
||||||
|
? ApiSchemas[TPath][TMethod] extends { query: infer Q }
|
||||||
|
? Q
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type ApiBody<TPath extends ApiPaths, TMethod extends string> = TPath extends keyof ApiSchemas
|
||||||
|
? TMethod extends keyof ApiSchemas[TPath]
|
||||||
|
? ApiSchemas[TPath][TMethod] extends { body: infer B }
|
||||||
|
? B
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type-safe API client interface using concrete paths
|
||||||
|
* Accepts actual paths like '/test/items/123' instead of '/test/items/:id'
|
||||||
|
* Automatically infers query, body, and response types from ApiSchemas
|
||||||
|
*/
|
||||||
|
export interface ApiClient {
|
||||||
|
get<TPath extends ConcreteApiPaths>(
|
||||||
|
path: TPath,
|
||||||
|
options?: {
|
||||||
|
query?: QueryParamsForPath<TPath>
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}
|
||||||
|
): Promise<ResponseForPath<TPath, 'GET'>>
|
||||||
|
|
||||||
|
post<TPath extends ConcreteApiPaths>(
|
||||||
|
path: TPath,
|
||||||
|
options: {
|
||||||
|
body?: BodyForPath<TPath, 'POST'>
|
||||||
|
query?: Record<string, any>
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}
|
||||||
|
): Promise<ResponseForPath<TPath, 'POST'>>
|
||||||
|
|
||||||
|
put<TPath extends ConcreteApiPaths>(
|
||||||
|
path: TPath,
|
||||||
|
options: {
|
||||||
|
body: BodyForPath<TPath, 'PUT'>
|
||||||
|
query?: Record<string, any>
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}
|
||||||
|
): Promise<ResponseForPath<TPath, 'PUT'>>
|
||||||
|
|
||||||
|
delete<TPath extends ConcreteApiPaths>(
|
||||||
|
path: TPath,
|
||||||
|
options?: {
|
||||||
|
query?: Record<string, any>
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}
|
||||||
|
): Promise<ResponseForPath<TPath, 'DELETE'>>
|
||||||
|
|
||||||
|
patch<TPath extends ConcreteApiPaths>(
|
||||||
|
path: TPath,
|
||||||
|
options: {
|
||||||
|
body?: BodyForPath<TPath, 'PATCH'>
|
||||||
|
query?: Record<string, any>
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}
|
||||||
|
): Promise<ResponseForPath<TPath, 'PATCH'>>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper types to determine if parameters are required based on schema
|
||||||
|
*/
|
||||||
|
type HasRequiredQuery<Path extends ApiPaths, Method extends ApiMethods<Path>> = Path extends keyof ApiSchemas
|
||||||
|
? Method extends keyof ApiSchemas[Path]
|
||||||
|
? ApiSchemas[Path][Method] extends { query: any }
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
: false
|
||||||
|
: false
|
||||||
|
|
||||||
|
type HasRequiredBody<Path extends ApiPaths, Method extends ApiMethods<Path>> = Path extends keyof ApiSchemas
|
||||||
|
? Method extends keyof ApiSchemas[Path]
|
||||||
|
? ApiSchemas[Path][Method] extends { body: any }
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
: false
|
||||||
|
: false
|
||||||
|
|
||||||
|
type HasRequiredParams<Path extends ApiPaths, Method extends ApiMethods<Path>> = Path extends keyof ApiSchemas
|
||||||
|
? Method extends keyof ApiSchemas[Path]
|
||||||
|
? ApiSchemas[Path][Method] extends { params: any }
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
: false
|
||||||
|
: false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler function for a specific API endpoint
|
||||||
|
* Provides type-safe parameter extraction based on ApiSchemas
|
||||||
|
* Parameters are required or optional based on the schema definition
|
||||||
|
*/
|
||||||
|
export type ApiHandler<Path extends ApiPaths, Method extends ApiMethods<Path>> = (
|
||||||
|
params: (HasRequiredParams<Path, Method> extends true
|
||||||
|
? { params: ApiParams<Path, Method> }
|
||||||
|
: { params?: ApiParams<Path, Method> }) &
|
||||||
|
(HasRequiredQuery<Path, Method> extends true
|
||||||
|
? { query: ApiQuery<Path, Method> }
|
||||||
|
: { query?: ApiQuery<Path, Method> }) &
|
||||||
|
(HasRequiredBody<Path, Method> extends true ? { body: ApiBody<Path, Method> } : { body?: ApiBody<Path, Method> })
|
||||||
|
) => Promise<ApiResponse<Path, Method>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete API implementation that must match ApiSchemas structure
|
||||||
|
* TypeScript will error if any endpoint is missing - this ensures exhaustive coverage
|
||||||
|
*/
|
||||||
|
export type ApiImplementation = {
|
||||||
|
[Path in ApiPaths]: {
|
||||||
|
[Method in ApiMethods<Path>]: ApiHandler<Path, Method>
|
||||||
|
}
|
||||||
|
}
|
||||||
289
packages/shared/data/api/apiTypes.ts
Normal file
289
packages/shared/data/api/apiTypes.ts
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
/**
|
||||||
|
* Core types for the Data API system
|
||||||
|
* Provides type definitions for request/response handling across renderer-main IPC communication
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard HTTP methods supported by the Data API
|
||||||
|
*/
|
||||||
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request object structure for Data API calls
|
||||||
|
*/
|
||||||
|
export interface DataRequest<T = any> {
|
||||||
|
/** Unique request identifier for tracking and correlation */
|
||||||
|
id: string
|
||||||
|
/** HTTP method for the request */
|
||||||
|
method: HttpMethod
|
||||||
|
/** API path (e.g., '/topics', '/topics/123') */
|
||||||
|
path: string
|
||||||
|
/** URL parameters for the request */
|
||||||
|
params?: Record<string, any>
|
||||||
|
/** Request body data */
|
||||||
|
body?: T
|
||||||
|
/** Request headers */
|
||||||
|
headers?: Record<string, string>
|
||||||
|
/** Additional metadata for request processing */
|
||||||
|
metadata?: {
|
||||||
|
/** Request timestamp */
|
||||||
|
timestamp: number
|
||||||
|
/** OpenTelemetry span context for tracing */
|
||||||
|
spanContext?: any
|
||||||
|
/** Cache options for this specific request */
|
||||||
|
cache?: CacheOptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response object structure for Data API calls
|
||||||
|
*/
|
||||||
|
export interface DataResponse<T = any> {
|
||||||
|
/** Request ID that this response corresponds to */
|
||||||
|
id: string
|
||||||
|
/** HTTP status code */
|
||||||
|
status: number
|
||||||
|
/** Response data if successful */
|
||||||
|
data?: T
|
||||||
|
/** Error information if request failed */
|
||||||
|
error?: DataApiError
|
||||||
|
/** Response metadata */
|
||||||
|
metadata?: {
|
||||||
|
/** Request processing duration in milliseconds */
|
||||||
|
duration: number
|
||||||
|
/** Whether response was served from cache */
|
||||||
|
cached?: boolean
|
||||||
|
/** Cache TTL if applicable */
|
||||||
|
cacheTtl?: number
|
||||||
|
/** Response timestamp */
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standardized error structure for Data API
|
||||||
|
*/
|
||||||
|
export interface DataApiError {
|
||||||
|
/** Error code for programmatic handling */
|
||||||
|
code: string
|
||||||
|
/** Human-readable error message */
|
||||||
|
message: string
|
||||||
|
/** HTTP status code */
|
||||||
|
status: number
|
||||||
|
/** Additional error details */
|
||||||
|
details?: any
|
||||||
|
/** Error stack trace (development mode only) */
|
||||||
|
stack?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard error codes for Data API
|
||||||
|
*/
|
||||||
|
export enum ErrorCode {
|
||||||
|
// Client errors (4xx)
|
||||||
|
BAD_REQUEST = 'BAD_REQUEST',
|
||||||
|
UNAUTHORIZED = 'UNAUTHORIZED',
|
||||||
|
FORBIDDEN = 'FORBIDDEN',
|
||||||
|
NOT_FOUND = 'NOT_FOUND',
|
||||||
|
METHOD_NOT_ALLOWED = 'METHOD_NOT_ALLOWED',
|
||||||
|
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
||||||
|
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
|
||||||
|
|
||||||
|
// Server errors (5xx)
|
||||||
|
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
|
||||||
|
DATABASE_ERROR = 'DATABASE_ERROR',
|
||||||
|
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
|
||||||
|
|
||||||
|
// Custom application errors
|
||||||
|
MIGRATION_ERROR = 'MIGRATION_ERROR',
|
||||||
|
PERMISSION_DENIED = 'PERMISSION_DENIED',
|
||||||
|
RESOURCE_LOCKED = 'RESOURCE_LOCKED',
|
||||||
|
CONCURRENT_MODIFICATION = 'CONCURRENT_MODIFICATION'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache configuration options
|
||||||
|
*/
|
||||||
|
export interface CacheOptions {
|
||||||
|
/** Cache TTL in seconds */
|
||||||
|
ttl?: number
|
||||||
|
/** Return stale data while revalidating in background */
|
||||||
|
staleWhileRevalidate?: boolean
|
||||||
|
/** Custom cache key override */
|
||||||
|
cacheKey?: string
|
||||||
|
/** Operations that should invalidate this cache entry */
|
||||||
|
invalidateOn?: string[]
|
||||||
|
/** Whether to bypass cache entirely */
|
||||||
|
noCache?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transaction request wrapper for atomic operations
|
||||||
|
*/
|
||||||
|
export interface TransactionRequest {
|
||||||
|
/** List of operations to execute in transaction */
|
||||||
|
operations: DataRequest[]
|
||||||
|
/** Transaction options */
|
||||||
|
options?: {
|
||||||
|
/** Database isolation level */
|
||||||
|
isolation?: 'read-uncommitted' | 'read-committed' | 'repeatable-read' | 'serializable'
|
||||||
|
/** Whether to rollback entire transaction on any error */
|
||||||
|
rollbackOnError?: boolean
|
||||||
|
/** Transaction timeout in milliseconds */
|
||||||
|
timeout?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch request for multiple operations
|
||||||
|
*/
|
||||||
|
export interface BatchRequest {
|
||||||
|
/** List of requests to execute */
|
||||||
|
requests: DataRequest[]
|
||||||
|
/** Whether to execute requests in parallel */
|
||||||
|
parallel?: boolean
|
||||||
|
/** Stop on first error */
|
||||||
|
stopOnError?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch response containing results for all requests
|
||||||
|
*/
|
||||||
|
export interface BatchResponse {
|
||||||
|
/** Individual response for each request */
|
||||||
|
results: DataResponse[]
|
||||||
|
/** Overall batch execution metadata */
|
||||||
|
metadata: {
|
||||||
|
/** Total execution time */
|
||||||
|
duration: number
|
||||||
|
/** Number of successful operations */
|
||||||
|
successCount: number
|
||||||
|
/** Number of failed operations */
|
||||||
|
errorCount: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pagination parameters for list operations
|
||||||
|
*/
|
||||||
|
export interface PaginationParams {
|
||||||
|
/** Page number (1-based) */
|
||||||
|
page?: number
|
||||||
|
/** Items per page */
|
||||||
|
limit?: number
|
||||||
|
/** Cursor for cursor-based pagination */
|
||||||
|
cursor?: string
|
||||||
|
/** Sort field and direction */
|
||||||
|
sort?: {
|
||||||
|
field: string
|
||||||
|
order: 'asc' | 'desc'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paginated response wrapper
|
||||||
|
*/
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
/** Items for current page */
|
||||||
|
items: T[]
|
||||||
|
/** Total number of items */
|
||||||
|
total: number
|
||||||
|
/** Current page number */
|
||||||
|
page: number
|
||||||
|
/** Total number of pages */
|
||||||
|
pageCount: number
|
||||||
|
/** Whether there are more pages */
|
||||||
|
hasNext: boolean
|
||||||
|
/** Whether there are previous pages */
|
||||||
|
hasPrev: boolean
|
||||||
|
/** Next cursor for cursor-based pagination */
|
||||||
|
nextCursor?: string
|
||||||
|
/** Previous cursor for cursor-based pagination */
|
||||||
|
prevCursor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription options for real-time data updates
|
||||||
|
*/
|
||||||
|
export interface SubscriptionOptions {
|
||||||
|
/** Path pattern to subscribe to */
|
||||||
|
path: string
|
||||||
|
/** Filters to apply to subscription */
|
||||||
|
filters?: Record<string, any>
|
||||||
|
/** Whether to receive initial data */
|
||||||
|
includeInitial?: boolean
|
||||||
|
/** Custom subscription metadata */
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription callback function
|
||||||
|
*/
|
||||||
|
export type SubscriptionCallback<T = any> = (data: T, event: SubscriptionEvent) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription event types
|
||||||
|
*/
|
||||||
|
export enum SubscriptionEvent {
|
||||||
|
CREATED = 'created',
|
||||||
|
UPDATED = 'updated',
|
||||||
|
DELETED = 'deleted',
|
||||||
|
INITIAL = 'initial',
|
||||||
|
ERROR = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware interface
|
||||||
|
*/
|
||||||
|
export interface Middleware {
|
||||||
|
/** Middleware name */
|
||||||
|
name: string
|
||||||
|
/** Execution priority (lower = earlier) */
|
||||||
|
priority?: number
|
||||||
|
/** Middleware execution function */
|
||||||
|
execute(req: DataRequest, res: DataResponse, next: () => Promise<void>): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request context passed through middleware chain
|
||||||
|
*/
|
||||||
|
export interface RequestContext {
|
||||||
|
/** Original request */
|
||||||
|
request: DataRequest
|
||||||
|
/** Response being built */
|
||||||
|
response: DataResponse
|
||||||
|
/** Path that matched this request */
|
||||||
|
path?: string
|
||||||
|
/** HTTP method */
|
||||||
|
method?: HttpMethod
|
||||||
|
/** Authenticated user (if any) */
|
||||||
|
user?: any
|
||||||
|
/** Additional context data */
|
||||||
|
data: Map<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base options for service operations
|
||||||
|
*/
|
||||||
|
export interface ServiceOptions {
|
||||||
|
/** Database transaction to use */
|
||||||
|
transaction?: any
|
||||||
|
/** User context for authorization */
|
||||||
|
user?: any
|
||||||
|
/** Additional service-specific options */
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard service response wrapper
|
||||||
|
*/
|
||||||
|
export interface ServiceResult<T = any> {
|
||||||
|
/** Whether operation was successful */
|
||||||
|
success: boolean
|
||||||
|
/** Result data if successful */
|
||||||
|
data?: T
|
||||||
|
/** Error information if failed */
|
||||||
|
error?: DataApiError
|
||||||
|
/** Additional metadata */
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
}
|
||||||
194
packages/shared/data/api/errorCodes.ts
Normal file
194
packages/shared/data/api/errorCodes.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* Centralized error code definitions for the Data API system
|
||||||
|
* Provides consistent error handling across renderer and main processes
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { DataApiError } from './apiTypes'
|
||||||
|
import { ErrorCode } from './apiTypes'
|
||||||
|
|
||||||
|
// Re-export ErrorCode for convenience
|
||||||
|
export { ErrorCode } from './apiTypes'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error code to HTTP status mapping
|
||||||
|
*/
|
||||||
|
export const ERROR_STATUS_MAP: Record<ErrorCode, number> = {
|
||||||
|
// Client errors (4xx)
|
||||||
|
[ErrorCode.BAD_REQUEST]: 400,
|
||||||
|
[ErrorCode.UNAUTHORIZED]: 401,
|
||||||
|
[ErrorCode.FORBIDDEN]: 403,
|
||||||
|
[ErrorCode.NOT_FOUND]: 404,
|
||||||
|
[ErrorCode.METHOD_NOT_ALLOWED]: 405,
|
||||||
|
[ErrorCode.VALIDATION_ERROR]: 422,
|
||||||
|
[ErrorCode.RATE_LIMIT_EXCEEDED]: 429,
|
||||||
|
|
||||||
|
// Server errors (5xx)
|
||||||
|
[ErrorCode.INTERNAL_SERVER_ERROR]: 500,
|
||||||
|
[ErrorCode.DATABASE_ERROR]: 500,
|
||||||
|
[ErrorCode.SERVICE_UNAVAILABLE]: 503,
|
||||||
|
|
||||||
|
// Custom application errors (5xx)
|
||||||
|
[ErrorCode.MIGRATION_ERROR]: 500,
|
||||||
|
[ErrorCode.PERMISSION_DENIED]: 403,
|
||||||
|
[ErrorCode.RESOURCE_LOCKED]: 423,
|
||||||
|
[ErrorCode.CONCURRENT_MODIFICATION]: 409
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default error messages for each error code
|
||||||
|
*/
|
||||||
|
export const ERROR_MESSAGES: Record<ErrorCode, string> = {
|
||||||
|
[ErrorCode.BAD_REQUEST]: 'Bad request: Invalid request format or parameters',
|
||||||
|
[ErrorCode.UNAUTHORIZED]: 'Unauthorized: Authentication required',
|
||||||
|
[ErrorCode.FORBIDDEN]: 'Forbidden: Insufficient permissions',
|
||||||
|
[ErrorCode.NOT_FOUND]: 'Not found: Requested resource does not exist',
|
||||||
|
[ErrorCode.METHOD_NOT_ALLOWED]: 'Method not allowed: HTTP method not supported for this endpoint',
|
||||||
|
[ErrorCode.VALIDATION_ERROR]: 'Validation error: Request data does not meet requirements',
|
||||||
|
[ErrorCode.RATE_LIMIT_EXCEEDED]: 'Rate limit exceeded: Too many requests',
|
||||||
|
|
||||||
|
[ErrorCode.INTERNAL_SERVER_ERROR]: 'Internal server error: An unexpected error occurred',
|
||||||
|
[ErrorCode.DATABASE_ERROR]: 'Database error: Failed to access or modify data',
|
||||||
|
[ErrorCode.SERVICE_UNAVAILABLE]: 'Service unavailable: The service is temporarily unavailable',
|
||||||
|
|
||||||
|
[ErrorCode.MIGRATION_ERROR]: 'Migration error: Failed to migrate data',
|
||||||
|
[ErrorCode.PERMISSION_DENIED]: 'Permission denied: Operation not allowed for current user',
|
||||||
|
[ErrorCode.RESOURCE_LOCKED]: 'Resource locked: Resource is currently locked by another operation',
|
||||||
|
[ErrorCode.CONCURRENT_MODIFICATION]: 'Concurrent modification: Resource was modified by another user'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for creating standardized Data API errors
|
||||||
|
*/
|
||||||
|
export class DataApiErrorFactory {
|
||||||
|
/**
|
||||||
|
* Create a DataApiError with standard properties
|
||||||
|
*/
|
||||||
|
static create(code: ErrorCode, customMessage?: string, details?: any, stack?: string): DataApiError {
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
message: customMessage || ERROR_MESSAGES[code],
|
||||||
|
status: ERROR_STATUS_MAP[code],
|
||||||
|
details,
|
||||||
|
stack: stack || undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a validation error with field-specific details
|
||||||
|
*/
|
||||||
|
static validation(fieldErrors: Record<string, string[]>, message?: string): DataApiError {
|
||||||
|
return this.create(ErrorCode.VALIDATION_ERROR, message || 'Request validation failed', { fieldErrors })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a not found error for specific resource
|
||||||
|
*/
|
||||||
|
static notFound(resource: string, id?: string): DataApiError {
|
||||||
|
const message = id ? `${resource} with id '${id}' not found` : `${resource} not found`
|
||||||
|
|
||||||
|
return this.create(ErrorCode.NOT_FOUND, message, { resource, id })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a database error with query details
|
||||||
|
*/
|
||||||
|
static database(originalError: Error, operation?: string): DataApiError {
|
||||||
|
return this.create(
|
||||||
|
ErrorCode.DATABASE_ERROR,
|
||||||
|
`Database operation failed${operation ? `: ${operation}` : ''}`,
|
||||||
|
{
|
||||||
|
originalError: originalError.message,
|
||||||
|
operation
|
||||||
|
},
|
||||||
|
originalError.stack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a permission denied error
|
||||||
|
*/
|
||||||
|
static permissionDenied(action: string, resource?: string): DataApiError {
|
||||||
|
const message = resource ? `Permission denied: Cannot ${action} ${resource}` : `Permission denied: Cannot ${action}`
|
||||||
|
|
||||||
|
return this.create(ErrorCode.PERMISSION_DENIED, message, { action, resource })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an internal server error from an unexpected error
|
||||||
|
*/
|
||||||
|
static internal(originalError: Error, context?: string): DataApiError {
|
||||||
|
const message = context
|
||||||
|
? `Internal error in ${context}: ${originalError.message}`
|
||||||
|
: `Internal error: ${originalError.message}`
|
||||||
|
|
||||||
|
return this.create(
|
||||||
|
ErrorCode.INTERNAL_SERVER_ERROR,
|
||||||
|
message,
|
||||||
|
{ originalError: originalError.message, context },
|
||||||
|
originalError.stack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a rate limit exceeded error
|
||||||
|
*/
|
||||||
|
static rateLimit(limit: number, windowMs: number): DataApiError {
|
||||||
|
return this.create(ErrorCode.RATE_LIMIT_EXCEEDED, `Rate limit exceeded: ${limit} requests per ${windowMs}ms`, {
|
||||||
|
limit,
|
||||||
|
windowMs
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a resource locked error
|
||||||
|
*/
|
||||||
|
static resourceLocked(resource: string, id: string, lockedBy?: string): DataApiError {
|
||||||
|
const message = lockedBy
|
||||||
|
? `${resource} '${id}' is locked by ${lockedBy}`
|
||||||
|
: `${resource} '${id}' is currently locked`
|
||||||
|
|
||||||
|
return this.create(ErrorCode.RESOURCE_LOCKED, message, { resource, id, lockedBy })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a concurrent modification error
|
||||||
|
*/
|
||||||
|
static concurrentModification(resource: string, id: string): DataApiError {
|
||||||
|
return this.create(ErrorCode.CONCURRENT_MODIFICATION, `${resource} '${id}' was modified by another user`, {
|
||||||
|
resource,
|
||||||
|
id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an error is a Data API error
|
||||||
|
*/
|
||||||
|
export function isDataApiError(error: any): error is DataApiError {
|
||||||
|
return (
|
||||||
|
error &&
|
||||||
|
typeof error === 'object' &&
|
||||||
|
typeof error.code === 'string' &&
|
||||||
|
typeof error.message === 'string' &&
|
||||||
|
typeof error.status === 'number'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a generic error to a DataApiError
|
||||||
|
*/
|
||||||
|
export function toDataApiError(error: unknown, context?: string): DataApiError {
|
||||||
|
if (isDataApiError(error)) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return DataApiErrorFactory.internal(error, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DataApiErrorFactory.create(
|
||||||
|
ErrorCode.INTERNAL_SERVER_ERROR,
|
||||||
|
`Unknown error${context ? ` in ${context}` : ''}: ${String(error)}`,
|
||||||
|
{ originalError: error, context }
|
||||||
|
)
|
||||||
|
}
|
||||||
121
packages/shared/data/api/index.ts
Normal file
121
packages/shared/data/api/index.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* Cherry Studio Data API - Barrel Exports
|
||||||
|
*
|
||||||
|
* This file provides a centralized entry point for all data API types,
|
||||||
|
* schemas, and utilities. Import everything you need from this single location.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { Topic, CreateTopicDto, ApiSchemas, DataRequest, ErrorCode } from '@/shared/data'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Core data API types and infrastructure
|
||||||
|
export type {
|
||||||
|
BatchRequest,
|
||||||
|
BatchResponse,
|
||||||
|
CacheOptions,
|
||||||
|
DataApiError,
|
||||||
|
DataRequest,
|
||||||
|
DataResponse,
|
||||||
|
HttpMethod,
|
||||||
|
Middleware,
|
||||||
|
PaginatedResponse,
|
||||||
|
PaginationParams,
|
||||||
|
RequestContext,
|
||||||
|
ServiceOptions,
|
||||||
|
ServiceResult,
|
||||||
|
SubscriptionCallback,
|
||||||
|
SubscriptionOptions,
|
||||||
|
TransactionRequest
|
||||||
|
} from './apiTypes'
|
||||||
|
export { ErrorCode, SubscriptionEvent } from './apiTypes'
|
||||||
|
|
||||||
|
// Domain models and DTOs
|
||||||
|
export type {
|
||||||
|
BulkOperationRequest,
|
||||||
|
BulkOperationResponse,
|
||||||
|
CreateTestItemDto,
|
||||||
|
TestItem,
|
||||||
|
UpdateTestItemDto
|
||||||
|
} from './apiModels'
|
||||||
|
|
||||||
|
// API schema definitions and type helpers
|
||||||
|
export type {
|
||||||
|
ApiBody,
|
||||||
|
ApiClient,
|
||||||
|
ApiMethods,
|
||||||
|
ApiParams,
|
||||||
|
ApiPaths,
|
||||||
|
ApiQuery,
|
||||||
|
ApiResponse,
|
||||||
|
ApiSchemas
|
||||||
|
} from './apiSchemas'
|
||||||
|
|
||||||
|
// Path type utilities for template literal types
|
||||||
|
export type {
|
||||||
|
BodyForPath,
|
||||||
|
ConcreteApiPaths,
|
||||||
|
MatchApiPath,
|
||||||
|
QueryParamsForPath,
|
||||||
|
ResolvedPath,
|
||||||
|
ResponseForPath
|
||||||
|
} from './apiPaths'
|
||||||
|
|
||||||
|
// Error handling utilities
|
||||||
|
export {
|
||||||
|
ErrorCode as DataApiErrorCode,
|
||||||
|
DataApiErrorFactory,
|
||||||
|
ERROR_MESSAGES,
|
||||||
|
ERROR_STATUS_MAP,
|
||||||
|
isDataApiError,
|
||||||
|
toDataApiError
|
||||||
|
} from './errorCodes'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-export commonly used type combinations for convenience
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Import types for re-export convenience types
|
||||||
|
import type { CreateTestItemDto, TestItem, UpdateTestItemDto } from './apiModels'
|
||||||
|
import type {
|
||||||
|
BatchRequest,
|
||||||
|
BatchResponse,
|
||||||
|
DataApiError,
|
||||||
|
DataRequest,
|
||||||
|
DataResponse,
|
||||||
|
ErrorCode,
|
||||||
|
PaginatedResponse,
|
||||||
|
PaginationParams,
|
||||||
|
TransactionRequest
|
||||||
|
} from './apiTypes'
|
||||||
|
import type { DataApiErrorFactory } from './errorCodes'
|
||||||
|
|
||||||
|
/** All test item-related types */
|
||||||
|
export type TestItemTypes = {
|
||||||
|
TestItem: TestItem
|
||||||
|
CreateTestItemDto: CreateTestItemDto
|
||||||
|
UpdateTestItemDto: UpdateTestItemDto
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All error-related types and utilities */
|
||||||
|
export type ErrorTypes = {
|
||||||
|
DataApiError: DataApiError
|
||||||
|
ErrorCode: ErrorCode
|
||||||
|
ErrorFactory: typeof DataApiErrorFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All request/response types */
|
||||||
|
export type RequestTypes = {
|
||||||
|
DataRequest: DataRequest
|
||||||
|
DataResponse: DataResponse
|
||||||
|
BatchRequest: BatchRequest
|
||||||
|
BatchResponse: BatchResponse
|
||||||
|
TransactionRequest: TransactionRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All pagination-related types */
|
||||||
|
export type PaginationTypes = {
|
||||||
|
PaginationParams: PaginationParams
|
||||||
|
PaginatedResponse: PaginatedResponse<any>
|
||||||
|
}
|
||||||
168
packages/shared/data/cache/cacheSchemas.ts
vendored
Normal file
168
packages/shared/data/cache/cacheSchemas.ts
vendored
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import type { TranslateLanguageCode } from '@types'
|
||||||
|
|
||||||
|
import type * as CacheValueTypes from './cacheValueTypes'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use cache schema for renderer hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type UseCacheSchema = {
|
||||||
|
// App state
|
||||||
|
'app.dist.update_state': CacheValueTypes.CacheAppUpdateState
|
||||||
|
'app.user.avatar': string
|
||||||
|
|
||||||
|
// Chat context
|
||||||
|
'chat.multi_select_mode': boolean
|
||||||
|
'chat.selected_message_ids': string[]
|
||||||
|
'chat.generating': boolean
|
||||||
|
'chat.websearch.searching': boolean
|
||||||
|
'chat.websearch.active_searches': CacheValueTypes.CacheActiveSearches
|
||||||
|
|
||||||
|
// Minapp management
|
||||||
|
'minapp.opened_keep_alive': CacheValueTypes.CacheMinAppType[]
|
||||||
|
'minapp.current_id': string
|
||||||
|
'minapp.show': boolean
|
||||||
|
'minapp.opened_oneoff': CacheValueTypes.CacheMinAppType | null
|
||||||
|
|
||||||
|
// Topic management
|
||||||
|
'topic.active': CacheValueTypes.CacheTopic | null
|
||||||
|
'topic.renaming': string[]
|
||||||
|
'topic.newly_renamed': string[]
|
||||||
|
|
||||||
|
// Translate state
|
||||||
|
'translate.lang.source': TranslateLanguageCode | 'auto'
|
||||||
|
'translate.lang.target': TranslateLanguageCode
|
||||||
|
'translate.input': string
|
||||||
|
'translate.output': string
|
||||||
|
'translate.detecting': boolean
|
||||||
|
'translate.translating': CacheValueTypes.CacheTranslating
|
||||||
|
'translate.bidirectional': CacheValueTypes.CacheTranslateBidirectional
|
||||||
|
|
||||||
|
// Test keys (for dataRefactorTest window)
|
||||||
|
// TODO: remove after testing
|
||||||
|
'test-hook-memory-1': string
|
||||||
|
'test-ttl-cache': string
|
||||||
|
'test-protected-cache': string
|
||||||
|
'test-deep-equal': { nested: { count: number }; tags: string[] }
|
||||||
|
'test-performance': number
|
||||||
|
'test-multi-hook': string
|
||||||
|
'concurrent-test-1': number
|
||||||
|
'concurrent-test-2': number
|
||||||
|
'large-data-test': Record<string, any>
|
||||||
|
'test-number-cache': number
|
||||||
|
'test-object-cache': { name: string; count: number; active: boolean }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DefaultUseCache: UseCacheSchema = {
|
||||||
|
// App state
|
||||||
|
'app.dist.update_state': {
|
||||||
|
info: null,
|
||||||
|
checking: false,
|
||||||
|
downloading: false,
|
||||||
|
downloaded: false,
|
||||||
|
downloadProgress: 0,
|
||||||
|
available: false
|
||||||
|
},
|
||||||
|
'app.user.avatar': '',
|
||||||
|
|
||||||
|
// Chat context
|
||||||
|
'chat.multi_select_mode': false,
|
||||||
|
'chat.selected_message_ids': [],
|
||||||
|
'chat.generating': false,
|
||||||
|
'chat.websearch.searching': false,
|
||||||
|
'chat.websearch.active_searches': {},
|
||||||
|
|
||||||
|
// Minapp management
|
||||||
|
'minapp.opened_keep_alive': [],
|
||||||
|
'minapp.current_id': '',
|
||||||
|
'minapp.show': false,
|
||||||
|
'minapp.opened_oneoff': null,
|
||||||
|
|
||||||
|
// Topic management
|
||||||
|
'topic.active': null,
|
||||||
|
'topic.renaming': [],
|
||||||
|
'topic.newly_renamed': [],
|
||||||
|
|
||||||
|
// Translate state
|
||||||
|
'translate.lang.source': 'auto',
|
||||||
|
'translate.lang.target': 'zh-cn',
|
||||||
|
'translate.input': '',
|
||||||
|
'translate.output': '',
|
||||||
|
'translate.detecting': false,
|
||||||
|
'translate.translating': { isTranslating: false, abortKey: null },
|
||||||
|
'translate.bidirectional': {
|
||||||
|
enabled: false,
|
||||||
|
origin: 'en-us',
|
||||||
|
target: 'zh-cn'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test keys (for dataRefactorTest window)
|
||||||
|
// TODO: remove after testing
|
||||||
|
'test-hook-memory-1': 'default-memory-value',
|
||||||
|
'test-ttl-cache': 'test-ttl-cache',
|
||||||
|
'test-protected-cache': 'protected-value',
|
||||||
|
'test-deep-equal': { nested: { count: 0 }, tags: ['initial'] },
|
||||||
|
'test-performance': 0,
|
||||||
|
'test-multi-hook': 'hook-1-default',
|
||||||
|
'concurrent-test-1': 0,
|
||||||
|
'concurrent-test-2': 0,
|
||||||
|
'large-data-test': {},
|
||||||
|
'test-number-cache': 42,
|
||||||
|
'test-object-cache': { name: 'test', count: 0, active: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use shared cache schema for renderer hook
|
||||||
|
*/
|
||||||
|
export type UseSharedCacheSchema = {
|
||||||
|
'example-key': string
|
||||||
|
|
||||||
|
// Test keys (for dataRefactorTest window)
|
||||||
|
// TODO: remove after testing
|
||||||
|
'test-hook-shared-1': string
|
||||||
|
'test-multi-hook': string
|
||||||
|
'concurrent-shared': number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DefaultUseSharedCache: UseSharedCacheSchema = {
|
||||||
|
'example-key': 'example default value',
|
||||||
|
|
||||||
|
// Test keys (for dataRefactorTest window)
|
||||||
|
// TODO: remove after testing
|
||||||
|
'concurrent-shared': 0,
|
||||||
|
'test-hook-shared-1': 'default-shared-value',
|
||||||
|
'test-multi-hook': 'hook-3-shared'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist cache schema defining allowed keys and their value types
|
||||||
|
* This ensures type safety and prevents key conflicts
|
||||||
|
*/
|
||||||
|
export type RendererPersistCacheSchema = {
|
||||||
|
'example-key': string
|
||||||
|
|
||||||
|
// Test keys (for dataRefactorTest window)
|
||||||
|
// TODO: remove after testing
|
||||||
|
'example-1': string
|
||||||
|
'example-2': string
|
||||||
|
'example-3': string
|
||||||
|
'example-4': string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DefaultRendererPersistCache: RendererPersistCacheSchema = {
|
||||||
|
'example-key': 'example default value',
|
||||||
|
|
||||||
|
// Test keys (for dataRefactorTest window)
|
||||||
|
// TODO: remove after testing
|
||||||
|
'example-1': 'example default value',
|
||||||
|
'example-2': 'example default value',
|
||||||
|
'example-3': 'example default value',
|
||||||
|
'example-4': 'example default value'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type-safe cache key
|
||||||
|
*/
|
||||||
|
export type RendererPersistCacheKey = keyof RendererPersistCacheSchema
|
||||||
|
export type UseCacheKey = keyof UseCacheSchema
|
||||||
|
export type UseSharedCacheKey = keyof UseSharedCacheSchema
|
||||||
43
packages/shared/data/cache/cacheTypes.ts
vendored
Normal file
43
packages/shared/data/cache/cacheTypes.ts
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Cache types and interfaces for CacheService
|
||||||
|
*
|
||||||
|
* Supports three-layer caching architecture:
|
||||||
|
* 1. Memory cache (cross-component within renderer)
|
||||||
|
* 2. Shared cache (cross-window via IPC)
|
||||||
|
* 3. Persist cache (cross-window with localStorage persistence)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache entry with optional TTL support
|
||||||
|
*/
|
||||||
|
export interface CacheEntry<T = any> {
|
||||||
|
value: T
|
||||||
|
expireAt?: number // Unix timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache synchronization message for IPC communication
|
||||||
|
*/
|
||||||
|
export interface CacheSyncMessage {
|
||||||
|
type: 'shared' | 'persist'
|
||||||
|
key: string
|
||||||
|
value: any
|
||||||
|
ttl?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch cache synchronization message
|
||||||
|
*/
|
||||||
|
export interface CacheSyncBatchMessage {
|
||||||
|
type: 'shared' | 'persist'
|
||||||
|
entries: Array<{
|
||||||
|
key: string
|
||||||
|
value: any
|
||||||
|
ttl?: number
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache subscription callback
|
||||||
|
*/
|
||||||
|
export type CacheSubscriber = () => void
|
||||||
32
packages/shared/data/cache/cacheValueTypes.ts
vendored
Normal file
32
packages/shared/data/cache/cacheValueTypes.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { MinAppType, Topic, TranslateLanguageCode, WebSearchStatus } from '@types'
|
||||||
|
import type { UpdateInfo } from 'builder-util-runtime'
|
||||||
|
|
||||||
|
export type CacheAppUpdateState = {
|
||||||
|
info: UpdateInfo | null
|
||||||
|
checking: boolean
|
||||||
|
downloading: boolean
|
||||||
|
downloaded: boolean
|
||||||
|
downloadProgress: number
|
||||||
|
available: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CacheActiveSearches = Record<string, WebSearchStatus>
|
||||||
|
|
||||||
|
// For cache schema, we use any for complex types to avoid circular dependencies
|
||||||
|
// The actual type checking will be done at runtime by the cache system
|
||||||
|
export type CacheMinAppType = MinAppType
|
||||||
|
export type CacheTopic = Topic
|
||||||
|
export type CacheTranslating =
|
||||||
|
| {
|
||||||
|
isTranslating: true
|
||||||
|
abortKey: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
isTranslating: false
|
||||||
|
abortKey: null
|
||||||
|
}
|
||||||
|
export type CacheTranslateBidirectional = {
|
||||||
|
enabled: boolean
|
||||||
|
origin: TranslateLanguageCode
|
||||||
|
target: TranslateLanguageCode
|
||||||
|
}
|
||||||
703
packages/shared/data/preference/preferenceSchemas.ts
Normal file
703
packages/shared/data/preference/preferenceSchemas.ts
Normal file
@@ -0,0 +1,703 @@
|
|||||||
|
/**
|
||||||
|
* Auto-generated preferences configuration
|
||||||
|
* Generated at: 2025-09-16T03:17:03.354Z
|
||||||
|
*
|
||||||
|
* This file is automatically generated from classification.json
|
||||||
|
* To update this file, modify classification.json and run:
|
||||||
|
* node .claude/data-classify/scripts/generate-preferences.js
|
||||||
|
*
|
||||||
|
* === AUTO-GENERATED CONTENT START ===
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TRANSLATE_PROMPT } from '@shared/config/prompts'
|
||||||
|
import * as PreferenceTypes from '@shared/data/preference/preferenceTypes'
|
||||||
|
|
||||||
|
/* eslint @typescript-eslint/member-ordering: ["error", {
|
||||||
|
"interfaces": { "order": "alphabetically" },
|
||||||
|
"typeLiterals": { "order": "alphabetically" }
|
||||||
|
}] */
|
||||||
|
|
||||||
|
export interface PreferenceSchemas {
|
||||||
|
default: {
|
||||||
|
// redux/settings/enableDeveloperMode
|
||||||
|
'app.developer_mode.enabled': boolean
|
||||||
|
// redux/settings/disableHardwareAcceleration
|
||||||
|
'app.disable_hardware_acceleration': boolean
|
||||||
|
// redux/settings/autoCheckUpdate
|
||||||
|
'app.dist.auto_update.enabled': boolean
|
||||||
|
// redux/settings/testChannel
|
||||||
|
'app.dist.test_plan.channel': PreferenceTypes.UpgradeChannel
|
||||||
|
// redux/settings/testPlan
|
||||||
|
'app.dist.test_plan.enabled': boolean
|
||||||
|
// redux/settings/language
|
||||||
|
'app.language': PreferenceTypes.LanguageVarious | null
|
||||||
|
// redux/settings/launchOnBoot
|
||||||
|
'app.launch_on_boot': boolean
|
||||||
|
// redux/settings/notification.assistant
|
||||||
|
'app.notification.assistant.enabled': boolean
|
||||||
|
// redux/settings/notification.backup
|
||||||
|
'app.notification.backup.enabled': boolean
|
||||||
|
// redux/settings/notification.knowledge
|
||||||
|
'app.notification.knowledge.enabled': boolean
|
||||||
|
// redux/settings/enableDataCollection
|
||||||
|
'app.privacy.data_collection.enabled': boolean
|
||||||
|
// redux/settings/proxyBypassRules
|
||||||
|
'app.proxy.bypass_rules': string
|
||||||
|
// redux/settings/proxyMode
|
||||||
|
'app.proxy.mode': PreferenceTypes.ProxyMode
|
||||||
|
// redux/settings/proxyUrl
|
||||||
|
'app.proxy.url': string
|
||||||
|
// redux/settings/enableSpellCheck
|
||||||
|
'app.spell_check.enabled': boolean
|
||||||
|
// redux/settings/spellCheckLanguages
|
||||||
|
'app.spell_check.languages': string[]
|
||||||
|
// redux/settings/tray
|
||||||
|
'app.tray.enabled': boolean
|
||||||
|
// redux/settings/trayOnClose
|
||||||
|
'app.tray.on_close': boolean
|
||||||
|
// redux/settings/launchToTray
|
||||||
|
'app.tray.on_launch': boolean
|
||||||
|
// redux/settings/userId
|
||||||
|
'app.user.id': string
|
||||||
|
// redux/settings/userName
|
||||||
|
'app.user.name': string
|
||||||
|
// electronStore/ZoomFactor/ZoomFactor
|
||||||
|
'app.zoom_factor': number
|
||||||
|
// redux/settings/clickAssistantToShowTopic
|
||||||
|
'assistant.click_to_show_topic': boolean
|
||||||
|
// redux/settings/assistantIconType
|
||||||
|
'assistant.icon_type': PreferenceTypes.AssistantIconType
|
||||||
|
// redux/settings/showAssistants
|
||||||
|
'assistant.tab.show': boolean
|
||||||
|
// redux/settings/assistantsTabSortType
|
||||||
|
'assistant.tab.sort_type': PreferenceTypes.AssistantTabSortType
|
||||||
|
// redux/settings/codeCollapsible
|
||||||
|
'chat.code.collapsible': boolean
|
||||||
|
// redux/settings/codeEditor.autocompletion
|
||||||
|
'chat.code.editor.autocompletion': boolean
|
||||||
|
// redux/settings/codeEditor.enabled
|
||||||
|
'chat.code.editor.enabled': boolean
|
||||||
|
// redux/settings/codeEditor.foldGutter
|
||||||
|
'chat.code.editor.fold_gutter': boolean
|
||||||
|
// redux/settings/codeEditor.highlightActiveLine
|
||||||
|
'chat.code.editor.highlight_active_line': boolean
|
||||||
|
// redux/settings/codeEditor.keymap
|
||||||
|
'chat.code.editor.keymap': boolean
|
||||||
|
// redux/settings/codeEditor.themeDark
|
||||||
|
'chat.code.editor.theme_dark': string
|
||||||
|
// redux/settings/codeEditor.themeLight
|
||||||
|
'chat.code.editor.theme_light': string
|
||||||
|
// redux/settings/codeExecution.enabled
|
||||||
|
'chat.code.execution.enabled': boolean
|
||||||
|
// redux/settings/codeExecution.timeoutMinutes
|
||||||
|
'chat.code.execution.timeout_minutes': number
|
||||||
|
// redux/settings/codeFancyBlock
|
||||||
|
'chat.code.fancy_block': boolean
|
||||||
|
// redux/settings/codeImageTools
|
||||||
|
'chat.code.image_tools': boolean
|
||||||
|
// redux/settings/codePreview.themeDark
|
||||||
|
'chat.code.preview.theme_dark': string
|
||||||
|
// redux/settings/codePreview.themeLight
|
||||||
|
'chat.code.preview.theme_light': string
|
||||||
|
// redux/settings/codeShowLineNumbers
|
||||||
|
'chat.code.show_line_numbers': boolean
|
||||||
|
// redux/settings/codeViewer.themeDark
|
||||||
|
'chat.code.viewer.theme_dark': string
|
||||||
|
// redux/settings/codeViewer.themeLight
|
||||||
|
'chat.code.viewer.theme_light': string
|
||||||
|
// redux/settings/codeWrappable
|
||||||
|
'chat.code.wrappable': boolean
|
||||||
|
// redux/settings/pasteLongTextAsFile
|
||||||
|
'chat.input.paste_long_text_as_file': boolean
|
||||||
|
// redux/settings/pasteLongTextThreshold
|
||||||
|
'chat.input.paste_long_text_threshold': number
|
||||||
|
// redux/settings/enableQuickPanelTriggers
|
||||||
|
'chat.input.quick_panel.triggers_enabled': boolean
|
||||||
|
// redux/settings/sendMessageShortcut
|
||||||
|
'chat.input.send_message_shortcut': PreferenceTypes.SendMessageShortcut
|
||||||
|
// redux/settings/showInputEstimatedTokens
|
||||||
|
'chat.input.show_estimated_tokens': boolean
|
||||||
|
// redux/settings/autoTranslateWithSpace
|
||||||
|
'chat.input.translate.auto_translate_with_space': boolean
|
||||||
|
// redux/settings/showTranslateConfirm
|
||||||
|
'chat.input.translate.show_confirm': boolean
|
||||||
|
// redux/settings/confirmDeleteMessage
|
||||||
|
'chat.message.confirm_delete': boolean
|
||||||
|
// redux/settings/confirmRegenerateMessage
|
||||||
|
'chat.message.confirm_regenerate': boolean
|
||||||
|
// redux/settings/messageFont
|
||||||
|
'chat.message.font': string
|
||||||
|
// redux/settings/fontSize
|
||||||
|
'chat.message.font_size': number
|
||||||
|
// redux/settings/mathEngine
|
||||||
|
'chat.message.math.engine': PreferenceTypes.MathEngine
|
||||||
|
// redux/settings/mathEnableSingleDollar
|
||||||
|
'chat.message.math.single_dollar': boolean
|
||||||
|
// redux/settings/foldDisplayMode
|
||||||
|
'chat.message.multi_model.fold_display_mode': PreferenceTypes.MultiModelFoldDisplayMode
|
||||||
|
// redux/settings/gridColumns
|
||||||
|
'chat.message.multi_model.grid_columns': number
|
||||||
|
// redux/settings/gridPopoverTrigger
|
||||||
|
'chat.message.multi_model.grid_popover_trigger': PreferenceTypes.MultiModelGridPopoverTrigger
|
||||||
|
// redux/settings/multiModelMessageStyle
|
||||||
|
'chat.message.multi_model.style': PreferenceTypes.MultiModelMessageStyle
|
||||||
|
// redux/settings/messageNavigation
|
||||||
|
'chat.message.navigation_mode': PreferenceTypes.ChatMessageNavigationMode
|
||||||
|
// redux/settings/renderInputMessageAsMarkdown
|
||||||
|
'chat.message.render_as_markdown': boolean
|
||||||
|
// redux/settings/showMessageDivider
|
||||||
|
'chat.message.show_divider': boolean
|
||||||
|
// redux/settings/showMessageOutline
|
||||||
|
'chat.message.show_outline': boolean
|
||||||
|
// redux/settings/showPrompt
|
||||||
|
'chat.message.show_prompt': boolean
|
||||||
|
// redux/settings/messageStyle
|
||||||
|
'chat.message.style': PreferenceTypes.ChatMessageStyle
|
||||||
|
// redux/settings/thoughtAutoCollapse
|
||||||
|
'chat.message.thought.auto_collapse': boolean
|
||||||
|
// redux/settings/narrowMode
|
||||||
|
'chat.narrow_mode': boolean
|
||||||
|
// redux/settings/skipBackupFile
|
||||||
|
'data.backup.general.skip_backup_file': boolean
|
||||||
|
// redux/settings/localBackupAutoSync
|
||||||
|
'data.backup.local.auto_sync': boolean
|
||||||
|
// redux/settings/localBackupDir
|
||||||
|
'data.backup.local.dir': string
|
||||||
|
// redux/settings/localBackupMaxBackups
|
||||||
|
'data.backup.local.max_backups': number
|
||||||
|
// redux/settings/localBackupSkipBackupFile
|
||||||
|
'data.backup.local.skip_backup_file': boolean
|
||||||
|
// redux/settings/localBackupSyncInterval
|
||||||
|
'data.backup.local.sync_interval': number
|
||||||
|
// redux/nutstore/nutstoreAutoSync
|
||||||
|
'data.backup.nutstore.auto_sync': boolean
|
||||||
|
// redux/nutstore/nutstoreMaxBackups
|
||||||
|
'data.backup.nutstore.max_backups': number
|
||||||
|
// redux/nutstore/nutstorePath
|
||||||
|
'data.backup.nutstore.path': string
|
||||||
|
// redux/nutstore/nutstoreSkipBackupFile
|
||||||
|
'data.backup.nutstore.skip_backup_file': boolean
|
||||||
|
// redux/nutstore/nutstoreSyncInterval
|
||||||
|
'data.backup.nutstore.sync_interval': number
|
||||||
|
// redux/nutstore/nutstoreToken
|
||||||
|
'data.backup.nutstore.token': string
|
||||||
|
// redux/settings/s3.accessKeyId
|
||||||
|
'data.backup.s3.access_key_id': string
|
||||||
|
// redux/settings/s3.autoSync
|
||||||
|
'data.backup.s3.auto_sync': boolean
|
||||||
|
// redux/settings/s3.bucket
|
||||||
|
'data.backup.s3.bucket': string
|
||||||
|
// redux/settings/s3.endpoint
|
||||||
|
'data.backup.s3.endpoint': string
|
||||||
|
// redux/settings/s3.maxBackups
|
||||||
|
'data.backup.s3.max_backups': number
|
||||||
|
// redux/settings/s3.region
|
||||||
|
'data.backup.s3.region': string
|
||||||
|
// redux/settings/s3.root
|
||||||
|
'data.backup.s3.root': string
|
||||||
|
// redux/settings/s3.secretAccessKey
|
||||||
|
'data.backup.s3.secret_access_key': string
|
||||||
|
// redux/settings/s3.skipBackupFile
|
||||||
|
'data.backup.s3.skip_backup_file': boolean
|
||||||
|
// redux/settings/s3.syncInterval
|
||||||
|
'data.backup.s3.sync_interval': number
|
||||||
|
// redux/settings/webdavAutoSync
|
||||||
|
'data.backup.webdav.auto_sync': boolean
|
||||||
|
// redux/settings/webdavDisableStream
|
||||||
|
'data.backup.webdav.disable_stream': boolean
|
||||||
|
// redux/settings/webdavHost
|
||||||
|
'data.backup.webdav.host': string
|
||||||
|
// redux/settings/webdavMaxBackups
|
||||||
|
'data.backup.webdav.max_backups': number
|
||||||
|
// redux/settings/webdavPass
|
||||||
|
'data.backup.webdav.pass': string
|
||||||
|
// redux/settings/webdavPath
|
||||||
|
'data.backup.webdav.path': string
|
||||||
|
// redux/settings/webdavSkipBackupFile
|
||||||
|
'data.backup.webdav.skip_backup_file': boolean
|
||||||
|
// redux/settings/webdavSyncInterval
|
||||||
|
'data.backup.webdav.sync_interval': number
|
||||||
|
// redux/settings/webdavUser
|
||||||
|
'data.backup.webdav.user': string
|
||||||
|
// redux/settings/excludeCitationsInExport
|
||||||
|
'data.export.markdown.exclude_citations': boolean
|
||||||
|
// redux/settings/forceDollarMathInMarkdown
|
||||||
|
'data.export.markdown.force_dollar_math': boolean
|
||||||
|
// redux/settings/markdownExportPath
|
||||||
|
'data.export.markdown.path': string | null
|
||||||
|
// redux/settings/showModelNameInMarkdown
|
||||||
|
'data.export.markdown.show_model_name': boolean
|
||||||
|
// redux/settings/showModelProviderInMarkdown
|
||||||
|
'data.export.markdown.show_model_provider': boolean
|
||||||
|
// redux/settings/standardizeCitationsInExport
|
||||||
|
'data.export.markdown.standardize_citations': boolean
|
||||||
|
// redux/settings/useTopicNamingForMessageTitle
|
||||||
|
'data.export.markdown.use_topic_naming_for_message_title': boolean
|
||||||
|
// redux/settings/exportMenuOptions.docx
|
||||||
|
'data.export.menus.docx': boolean
|
||||||
|
// redux/settings/exportMenuOptions.image
|
||||||
|
'data.export.menus.image': boolean
|
||||||
|
// redux/settings/exportMenuOptions.joplin
|
||||||
|
'data.export.menus.joplin': boolean
|
||||||
|
// redux/settings/exportMenuOptions.markdown
|
||||||
|
'data.export.menus.markdown': boolean
|
||||||
|
// redux/settings/exportMenuOptions.markdown_reason
|
||||||
|
'data.export.menus.markdown_reason': boolean
|
||||||
|
// redux/settings/exportMenuOptions.notes
|
||||||
|
'data.export.menus.notes': boolean
|
||||||
|
// redux/settings/exportMenuOptions.notion
|
||||||
|
'data.export.menus.notion': boolean
|
||||||
|
// redux/settings/exportMenuOptions.obsidian
|
||||||
|
'data.export.menus.obsidian': boolean
|
||||||
|
// redux/settings/exportMenuOptions.plain_text
|
||||||
|
'data.export.menus.plain_text': boolean
|
||||||
|
// redux/settings/exportMenuOptions.siyuan
|
||||||
|
'data.export.menus.siyuan': boolean
|
||||||
|
// redux/settings/exportMenuOptions.yuque
|
||||||
|
'data.export.menus.yuque': boolean
|
||||||
|
// redux/settings/joplinExportReasoning
|
||||||
|
'data.integration.joplin.export_reasoning': boolean
|
||||||
|
// redux/settings/joplinToken
|
||||||
|
'data.integration.joplin.token': string
|
||||||
|
// redux/settings/joplinUrl
|
||||||
|
'data.integration.joplin.url': string
|
||||||
|
// redux/settings/notionApiKey
|
||||||
|
'data.integration.notion.api_key': string
|
||||||
|
// redux/settings/notionDatabaseID
|
||||||
|
'data.integration.notion.database_id': string
|
||||||
|
// redux/settings/notionExportReasoning
|
||||||
|
'data.integration.notion.export_reasoning': boolean
|
||||||
|
// redux/settings/notionPageNameKey
|
||||||
|
'data.integration.notion.page_name_key': string
|
||||||
|
// redux/settings/defaultObsidianVault
|
||||||
|
'data.integration.obsidian.default_vault': string
|
||||||
|
// redux/settings/siyuanApiUrl
|
||||||
|
'data.integration.siyuan.api_url': string | null
|
||||||
|
// redux/settings/siyuanBoxId
|
||||||
|
'data.integration.siyuan.box_id': string | null
|
||||||
|
// redux/settings/siyuanRootPath
|
||||||
|
'data.integration.siyuan.root_path': string | null
|
||||||
|
// redux/settings/siyuanToken
|
||||||
|
'data.integration.siyuan.token': string | null
|
||||||
|
// redux/settings/yuqueRepoId
|
||||||
|
'data.integration.yuque.repo_id': string
|
||||||
|
// redux/settings/yuqueToken
|
||||||
|
'data.integration.yuque.token': string
|
||||||
|
// redux/settings/yuqueUrl
|
||||||
|
'data.integration.yuque.url': string
|
||||||
|
// redux/settings/apiServer.apiKey
|
||||||
|
'feature.csaas.api_key': string
|
||||||
|
// redux/settings/apiServer.enabled
|
||||||
|
'feature.csaas.enabled': boolean
|
||||||
|
// redux/settings/apiServer.host
|
||||||
|
'feature.csaas.host': string
|
||||||
|
// redux/settings/apiServer.port
|
||||||
|
'feature.csaas.port': number
|
||||||
|
// redux/settings/maxKeepAliveMinapps
|
||||||
|
'feature.minapp.max_keep_alive': number
|
||||||
|
// redux/settings/minappsOpenLinkExternal
|
||||||
|
'feature.minapp.open_link_external': boolean
|
||||||
|
// redux/settings/showOpenedMinappsInSidebar
|
||||||
|
'feature.minapp.show_opened_in_sidebar': boolean
|
||||||
|
// redux/note/settings.defaultEditMode
|
||||||
|
'feature.notes.default_edit_mode': string
|
||||||
|
// redux/note/settings.defaultViewMode
|
||||||
|
'feature.notes.default_view_mode': string
|
||||||
|
// redux/note/settings.fontFamily
|
||||||
|
'feature.notes.font_family': string
|
||||||
|
// redux/note/settings.fontSize
|
||||||
|
'feature.notes.font_size': number
|
||||||
|
// redux/note/settings.isFullWidth
|
||||||
|
'feature.notes.full_width': boolean
|
||||||
|
// redux/note/notesPath
|
||||||
|
'feature.notes.path': string
|
||||||
|
// redux/note/settings.showTabStatus
|
||||||
|
'feature.notes.show_tab_status': boolean
|
||||||
|
// redux/note/settings.showTableOfContents
|
||||||
|
'feature.notes.show_table_of_contents': boolean
|
||||||
|
// redux/note/settings.showWorkspace
|
||||||
|
'feature.notes.show_workspace': boolean
|
||||||
|
// redux/note/sortType
|
||||||
|
'feature.notes.sort_type': string
|
||||||
|
// redux/settings/clickTrayToShowQuickAssistant
|
||||||
|
'feature.quick_assistant.click_tray_to_show': boolean
|
||||||
|
// redux/settings/enableQuickAssistant
|
||||||
|
'feature.quick_assistant.enabled': boolean
|
||||||
|
// redux/settings/readClipboardAtStartup
|
||||||
|
'feature.quick_assistant.read_clipboard_at_startup': boolean
|
||||||
|
// redux/selectionStore/actionItems
|
||||||
|
'feature.selection.action_items': PreferenceTypes.SelectionActionItem[]
|
||||||
|
// redux/selectionStore/actionWindowOpacity
|
||||||
|
'feature.selection.action_window_opacity': number
|
||||||
|
// redux/selectionStore/isAutoClose
|
||||||
|
'feature.selection.auto_close': boolean
|
||||||
|
// redux/selectionStore/isAutoPin
|
||||||
|
'feature.selection.auto_pin': boolean
|
||||||
|
// redux/selectionStore/isCompact
|
||||||
|
'feature.selection.compact': boolean
|
||||||
|
// redux/selectionStore/selectionEnabled
|
||||||
|
'feature.selection.enabled': boolean
|
||||||
|
// redux/selectionStore/filterList
|
||||||
|
'feature.selection.filter_list': string[]
|
||||||
|
// redux/selectionStore/filterMode
|
||||||
|
'feature.selection.filter_mode': PreferenceTypes.SelectionFilterMode
|
||||||
|
// redux/selectionStore/isFollowToolbar
|
||||||
|
'feature.selection.follow_toolbar': boolean
|
||||||
|
// redux/selectionStore/isRemeberWinSize
|
||||||
|
'feature.selection.remember_win_size': boolean
|
||||||
|
// redux/selectionStore/triggerMode
|
||||||
|
'feature.selection.trigger_mode': PreferenceTypes.SelectionTriggerMode
|
||||||
|
// redux/settings/translateModelPrompt
|
||||||
|
'feature.translate.model_prompt': string
|
||||||
|
// redux/settings/targetLanguage
|
||||||
|
'feature.translate.target_language': string
|
||||||
|
// redux/shortcuts/shortcuts.exit_fullscreen
|
||||||
|
'shortcut.app.exit_fullscreen': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.search_message
|
||||||
|
'shortcut.app.search_message': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.show_app
|
||||||
|
'shortcut.app.show_main_window': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.mini_window
|
||||||
|
'shortcut.app.show_mini_window': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.show_settings
|
||||||
|
'shortcut.app.show_settings': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.toggle_show_assistants
|
||||||
|
'shortcut.app.toggle_show_assistants': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.zoom_in
|
||||||
|
'shortcut.app.zoom_in': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.zoom_out
|
||||||
|
'shortcut.app.zoom_out': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.zoom_reset
|
||||||
|
'shortcut.app.zoom_reset': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.clear_topic
|
||||||
|
'shortcut.chat.clear': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.copy_last_message
|
||||||
|
'shortcut.chat.copy_last_message': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.search_message_in_chat
|
||||||
|
'shortcut.chat.search_message': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.toggle_new_context
|
||||||
|
'shortcut.chat.toggle_new_context': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.selection_assistant_select_text
|
||||||
|
'shortcut.selection.get_text': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.selection_assistant_toggle
|
||||||
|
'shortcut.selection.toggle_enabled': Record<string, unknown>
|
||||||
|
// redux/shortcuts/shortcuts.new_topic
|
||||||
|
'shortcut.topic.new': Record<string, unknown>
|
||||||
|
// redux/settings/enableTopicNaming
|
||||||
|
'topic.naming.enabled': boolean
|
||||||
|
// redux/settings/topicNamingPrompt
|
||||||
|
'topic.naming_prompt': string
|
||||||
|
// redux/settings/topicPosition
|
||||||
|
'topic.position': string
|
||||||
|
// redux/settings/pinTopicsToTop
|
||||||
|
'topic.tab.pin_to_top': boolean
|
||||||
|
// redux/settings/showTopics
|
||||||
|
'topic.tab.show': boolean
|
||||||
|
// redux/settings/showTopicTime
|
||||||
|
'topic.tab.show_time': boolean
|
||||||
|
// redux/translate/settings
|
||||||
|
'translate.settings.auto_copy': boolean
|
||||||
|
// indexedDB/translate
|
||||||
|
'translate.settings.auto_detection_method': PreferenceTypes.AutoDetectionMethod
|
||||||
|
'translate.settings.enable_markdown': boolean
|
||||||
|
'translate.settings.scroll_sync': boolean
|
||||||
|
// new preference
|
||||||
|
'translate.settings.target_langs': PreferenceTypes.TargetLangs
|
||||||
|
// redux/settings/customCss
|
||||||
|
'ui.custom_css': string
|
||||||
|
// redux/settings/navbarPosition
|
||||||
|
'ui.navbar.position': 'left' | 'top'
|
||||||
|
// redux/settings/sidebarIcons.disabled
|
||||||
|
'ui.sidebar.icons.invisible': PreferenceTypes.SidebarIcon[]
|
||||||
|
// redux/settings/sidebarIcons.visible
|
||||||
|
'ui.sidebar.icons.visible': PreferenceTypes.SidebarIcon[]
|
||||||
|
// redux/settings/theme
|
||||||
|
'ui.theme_mode': PreferenceTypes.ThemeMode
|
||||||
|
// redux/settings/userTheme.userCodeFontFamily
|
||||||
|
'ui.theme_user.code_font_family': string
|
||||||
|
// redux/settings/userTheme.colorPrimary
|
||||||
|
'ui.theme_user.color_primary': string
|
||||||
|
// redux/settings/userTheme.userFontFamily
|
||||||
|
'ui.theme_user.font_family': string
|
||||||
|
// redux/settings/windowStyle
|
||||||
|
'ui.window_style': PreferenceTypes.WindowStyle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint sort-keys: ["error", "asc", {"caseSensitive": true, "natural": false}] */
|
||||||
|
export const DefaultPreferences: PreferenceSchemas = {
|
||||||
|
default: {
|
||||||
|
'app.developer_mode.enabled': false,
|
||||||
|
'app.disable_hardware_acceleration': false,
|
||||||
|
'app.dist.auto_update.enabled': true,
|
||||||
|
'app.dist.test_plan.channel': PreferenceTypes.UpgradeChannel.LATEST,
|
||||||
|
'app.dist.test_plan.enabled': false,
|
||||||
|
'app.language': null,
|
||||||
|
'app.launch_on_boot': false,
|
||||||
|
'app.notification.assistant.enabled': false,
|
||||||
|
'app.notification.backup.enabled': false,
|
||||||
|
'app.notification.knowledge.enabled': false,
|
||||||
|
'app.privacy.data_collection.enabled': false,
|
||||||
|
'app.proxy.bypass_rules': '',
|
||||||
|
'app.proxy.mode': 'system',
|
||||||
|
'app.proxy.url': '',
|
||||||
|
'app.spell_check.enabled': false,
|
||||||
|
'app.spell_check.languages': [],
|
||||||
|
'app.tray.enabled': true,
|
||||||
|
'app.tray.on_close': true,
|
||||||
|
'app.tray.on_launch': false,
|
||||||
|
'app.user.id': 'uuid()',
|
||||||
|
'app.user.name': '',
|
||||||
|
'app.zoom_factor': 1,
|
||||||
|
'assistant.click_to_show_topic': true,
|
||||||
|
'assistant.icon_type': 'emoji',
|
||||||
|
'assistant.tab.show': true,
|
||||||
|
'assistant.tab.sort_type': 'list',
|
||||||
|
'chat.code.collapsible': false,
|
||||||
|
'chat.code.editor.autocompletion': true,
|
||||||
|
'chat.code.editor.enabled': false,
|
||||||
|
'chat.code.editor.fold_gutter': false,
|
||||||
|
'chat.code.editor.highlight_active_line': false,
|
||||||
|
'chat.code.editor.keymap': false,
|
||||||
|
'chat.code.editor.theme_dark': 'auto',
|
||||||
|
'chat.code.editor.theme_light': 'auto',
|
||||||
|
'chat.code.execution.enabled': false,
|
||||||
|
'chat.code.execution.timeout_minutes': 1,
|
||||||
|
'chat.code.fancy_block': true,
|
||||||
|
'chat.code.image_tools': false,
|
||||||
|
'chat.code.preview.theme_dark': 'auto',
|
||||||
|
'chat.code.preview.theme_light': 'auto',
|
||||||
|
'chat.code.show_line_numbers': false,
|
||||||
|
'chat.code.viewer.theme_dark': 'auto',
|
||||||
|
'chat.code.viewer.theme_light': 'auto',
|
||||||
|
'chat.code.wrappable': false,
|
||||||
|
'chat.input.paste_long_text_as_file': false,
|
||||||
|
'chat.input.paste_long_text_threshold': 1500,
|
||||||
|
'chat.input.quick_panel.triggers_enabled': false,
|
||||||
|
'chat.input.send_message_shortcut': 'Enter',
|
||||||
|
'chat.input.show_estimated_tokens': false,
|
||||||
|
'chat.input.translate.auto_translate_with_space': false,
|
||||||
|
'chat.input.translate.show_confirm': true,
|
||||||
|
'chat.message.confirm_delete': true,
|
||||||
|
'chat.message.confirm_regenerate': true,
|
||||||
|
'chat.message.font': 'system',
|
||||||
|
'chat.message.font_size': 14,
|
||||||
|
'chat.message.math.engine': 'KaTeX',
|
||||||
|
'chat.message.math.single_dollar': true,
|
||||||
|
'chat.message.multi_model.fold_display_mode': 'expanded',
|
||||||
|
'chat.message.multi_model.grid_columns': 2,
|
||||||
|
'chat.message.multi_model.grid_popover_trigger': 'click',
|
||||||
|
'chat.message.multi_model.style': 'horizontal',
|
||||||
|
'chat.message.navigation_mode': 'none',
|
||||||
|
'chat.message.render_as_markdown': false,
|
||||||
|
'chat.message.show_divider': true,
|
||||||
|
'chat.message.show_outline': false,
|
||||||
|
'chat.message.show_prompt': true,
|
||||||
|
'chat.message.style': 'plain',
|
||||||
|
'chat.message.thought.auto_collapse': true,
|
||||||
|
'chat.narrow_mode': false,
|
||||||
|
'data.backup.general.skip_backup_file': false,
|
||||||
|
'data.backup.local.auto_sync': false,
|
||||||
|
'data.backup.local.dir': '',
|
||||||
|
'data.backup.local.max_backups': 0,
|
||||||
|
'data.backup.local.skip_backup_file': false,
|
||||||
|
'data.backup.local.sync_interval': 0,
|
||||||
|
'data.backup.nutstore.auto_sync': false,
|
||||||
|
'data.backup.nutstore.max_backups': 0,
|
||||||
|
'data.backup.nutstore.path': '/cherry-studio',
|
||||||
|
'data.backup.nutstore.skip_backup_file': false,
|
||||||
|
'data.backup.nutstore.sync_interval': 0,
|
||||||
|
'data.backup.nutstore.token': '',
|
||||||
|
'data.backup.s3.access_key_id': '',
|
||||||
|
'data.backup.s3.auto_sync': false,
|
||||||
|
'data.backup.s3.bucket': '',
|
||||||
|
'data.backup.s3.endpoint': '',
|
||||||
|
'data.backup.s3.max_backups': 0,
|
||||||
|
'data.backup.s3.region': '',
|
||||||
|
'data.backup.s3.root': '',
|
||||||
|
'data.backup.s3.secret_access_key': '',
|
||||||
|
'data.backup.s3.skip_backup_file': false,
|
||||||
|
'data.backup.s3.sync_interval': 0,
|
||||||
|
'data.backup.webdav.auto_sync': false,
|
||||||
|
'data.backup.webdav.disable_stream': false,
|
||||||
|
'data.backup.webdav.host': '',
|
||||||
|
'data.backup.webdav.max_backups': 0,
|
||||||
|
'data.backup.webdav.pass': '',
|
||||||
|
'data.backup.webdav.path': '/cherry-studio',
|
||||||
|
'data.backup.webdav.skip_backup_file': false,
|
||||||
|
'data.backup.webdav.sync_interval': 0,
|
||||||
|
'data.backup.webdav.user': '',
|
||||||
|
'data.export.markdown.exclude_citations': false,
|
||||||
|
'data.export.markdown.force_dollar_math': false,
|
||||||
|
'data.export.markdown.path': null,
|
||||||
|
'data.export.markdown.show_model_name': false,
|
||||||
|
'data.export.markdown.show_model_provider': false,
|
||||||
|
'data.export.markdown.standardize_citations': false,
|
||||||
|
'data.export.markdown.use_topic_naming_for_message_title': false,
|
||||||
|
'data.export.menus.docx': true,
|
||||||
|
'data.export.menus.image': true,
|
||||||
|
'data.export.menus.joplin': true,
|
||||||
|
'data.export.menus.markdown': true,
|
||||||
|
'data.export.menus.markdown_reason': true,
|
||||||
|
'data.export.menus.notes': true,
|
||||||
|
'data.export.menus.notion': true,
|
||||||
|
'data.export.menus.obsidian': true,
|
||||||
|
'data.export.menus.plain_text': true,
|
||||||
|
'data.export.menus.siyuan': true,
|
||||||
|
'data.export.menus.yuque': true,
|
||||||
|
'data.integration.joplin.export_reasoning': false,
|
||||||
|
'data.integration.joplin.token': '',
|
||||||
|
'data.integration.joplin.url': '',
|
||||||
|
'data.integration.notion.api_key': '',
|
||||||
|
'data.integration.notion.database_id': '',
|
||||||
|
'data.integration.notion.export_reasoning': false,
|
||||||
|
'data.integration.notion.page_name_key': 'Name',
|
||||||
|
'data.integration.obsidian.default_vault': '',
|
||||||
|
'data.integration.siyuan.api_url': null,
|
||||||
|
'data.integration.siyuan.box_id': null,
|
||||||
|
'data.integration.siyuan.root_path': null,
|
||||||
|
'data.integration.siyuan.token': null,
|
||||||
|
'data.integration.yuque.repo_id': '',
|
||||||
|
'data.integration.yuque.token': '',
|
||||||
|
'data.integration.yuque.url': '',
|
||||||
|
'feature.csaas.api_key': '`cs-sk-${uuid()}`',
|
||||||
|
'feature.csaas.enabled': false,
|
||||||
|
'feature.csaas.host': 'localhost',
|
||||||
|
'feature.csaas.port': 23333,
|
||||||
|
'feature.minapp.max_keep_alive': 3,
|
||||||
|
'feature.minapp.open_link_external': false,
|
||||||
|
'feature.minapp.show_opened_in_sidebar': true,
|
||||||
|
'feature.notes.default_edit_mode': 'preview',
|
||||||
|
'feature.notes.default_view_mode': 'edit',
|
||||||
|
'feature.notes.font_family': 'default',
|
||||||
|
'feature.notes.font_size': 16,
|
||||||
|
'feature.notes.full_width': true,
|
||||||
|
'feature.notes.path': '',
|
||||||
|
'feature.notes.show_tab_status': true,
|
||||||
|
'feature.notes.show_table_of_contents': true,
|
||||||
|
'feature.notes.show_workspace': true,
|
||||||
|
'feature.notes.sort_type': 'sort_a2z',
|
||||||
|
'feature.quick_assistant.click_tray_to_show': false,
|
||||||
|
'feature.quick_assistant.enabled': false,
|
||||||
|
'feature.quick_assistant.read_clipboard_at_startup': true,
|
||||||
|
'feature.selection.action_items': [
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
icon: 'languages',
|
||||||
|
id: 'translate',
|
||||||
|
isBuiltIn: true,
|
||||||
|
name: 'selection.action.builtin.translate'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
icon: 'file-question',
|
||||||
|
id: 'explain',
|
||||||
|
isBuiltIn: true,
|
||||||
|
name: 'selection.action.builtin.explain'
|
||||||
|
},
|
||||||
|
{ enabled: true, icon: 'scan-text', id: 'summary', isBuiltIn: true, name: 'selection.action.builtin.summary' },
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
icon: 'search',
|
||||||
|
id: 'search',
|
||||||
|
isBuiltIn: true,
|
||||||
|
name: 'selection.action.builtin.search',
|
||||||
|
searchEngine: 'Google|https://www.google.com/search?q={{queryString}}'
|
||||||
|
},
|
||||||
|
{ enabled: true, icon: 'clipboard-copy', id: 'copy', isBuiltIn: true, name: 'selection.action.builtin.copy' },
|
||||||
|
{ enabled: false, icon: 'wand-sparkles', id: 'refine', isBuiltIn: true, name: 'selection.action.builtin.refine' },
|
||||||
|
{ enabled: false, icon: 'quote', id: 'quote', isBuiltIn: true, name: 'selection.action.builtin.quote' }
|
||||||
|
],
|
||||||
|
'feature.selection.action_window_opacity': 100,
|
||||||
|
'feature.selection.auto_close': false,
|
||||||
|
'feature.selection.auto_pin': false,
|
||||||
|
'feature.selection.compact': false,
|
||||||
|
'feature.selection.enabled': false,
|
||||||
|
'feature.selection.filter_list': [],
|
||||||
|
'feature.selection.filter_mode': PreferenceTypes.SelectionFilterMode.Default,
|
||||||
|
'feature.selection.follow_toolbar': true,
|
||||||
|
'feature.selection.remember_win_size': false,
|
||||||
|
'feature.selection.trigger_mode': PreferenceTypes.SelectionTriggerMode.Selected,
|
||||||
|
'feature.translate.model_prompt': TRANSLATE_PROMPT,
|
||||||
|
'feature.translate.target_language': 'en-us',
|
||||||
|
'shortcut.app.exit_fullscreen': { editable: false, enabled: true, key: ['Escape'], system: true },
|
||||||
|
'shortcut.app.search_message': {
|
||||||
|
editable: true,
|
||||||
|
enabled: true,
|
||||||
|
key: ['CommandOrControl', 'Shift', 'F'],
|
||||||
|
system: false
|
||||||
|
},
|
||||||
|
'shortcut.app.show_main_window': { editable: true, enabled: true, key: [], system: true },
|
||||||
|
'shortcut.app.show_mini_window': { editable: true, enabled: false, key: ['CommandOrControl', 'E'], system: true },
|
||||||
|
'shortcut.app.show_settings': { editable: false, enabled: true, key: ['CommandOrControl', ','], system: true },
|
||||||
|
'shortcut.app.toggle_show_assistants': {
|
||||||
|
editable: true,
|
||||||
|
enabled: true,
|
||||||
|
key: ['CommandOrControl', '['],
|
||||||
|
system: false
|
||||||
|
},
|
||||||
|
'shortcut.app.zoom_in': { editable: false, enabled: true, key: ['CommandOrControl', '='], system: true },
|
||||||
|
'shortcut.app.zoom_out': { editable: false, enabled: true, key: ['CommandOrControl', '-'], system: true },
|
||||||
|
'shortcut.app.zoom_reset': { editable: false, enabled: true, key: ['CommandOrControl', '0'], system: true },
|
||||||
|
'shortcut.chat.clear': { editable: true, enabled: true, key: ['CommandOrControl', 'L'], system: false },
|
||||||
|
'shortcut.chat.copy_last_message': {
|
||||||
|
editable: true,
|
||||||
|
enabled: false,
|
||||||
|
key: ['CommandOrControl', 'Shift', 'C'],
|
||||||
|
system: false
|
||||||
|
},
|
||||||
|
'shortcut.chat.search_message': { editable: true, enabled: true, key: ['CommandOrControl', 'F'], system: false },
|
||||||
|
'shortcut.chat.toggle_new_context': {
|
||||||
|
editable: true,
|
||||||
|
enabled: true,
|
||||||
|
key: ['CommandOrControl', 'K'],
|
||||||
|
system: false
|
||||||
|
},
|
||||||
|
'shortcut.selection.get_text': { editable: true, enabled: false, key: [], system: true },
|
||||||
|
'shortcut.selection.toggle_enabled': { editable: true, enabled: false, key: [], system: true },
|
||||||
|
'shortcut.topic.new': { editable: true, enabled: true, key: ['CommandOrControl', 'N'], system: false },
|
||||||
|
'topic.naming.enabled': true,
|
||||||
|
'topic.naming_prompt': '',
|
||||||
|
'topic.position': 'left',
|
||||||
|
'topic.tab.pin_to_top': false,
|
||||||
|
'topic.tab.show': true,
|
||||||
|
'topic.tab.show_time': false,
|
||||||
|
'translate.settings.auto_copy': false,
|
||||||
|
'translate.settings.auto_detection_method': 'franc',
|
||||||
|
'translate.settings.enable_markdown': false,
|
||||||
|
'translate.settings.scroll_sync': false,
|
||||||
|
'translate.settings.target_langs': {
|
||||||
|
alter: 'zh-cn',
|
||||||
|
target: 'en-us'
|
||||||
|
},
|
||||||
|
'ui.custom_css': '',
|
||||||
|
'ui.navbar.position': 'top',
|
||||||
|
'ui.sidebar.icons.invisible': [],
|
||||||
|
'ui.sidebar.icons.visible': [
|
||||||
|
'assistants',
|
||||||
|
'store',
|
||||||
|
'paintings',
|
||||||
|
'translate',
|
||||||
|
'minapp',
|
||||||
|
'knowledge',
|
||||||
|
'files',
|
||||||
|
'code_tools',
|
||||||
|
'notes'
|
||||||
|
],
|
||||||
|
'ui.theme_mode': PreferenceTypes.ThemeMode.system,
|
||||||
|
'ui.theme_user.code_font_family': '',
|
||||||
|
'ui.theme_user.color_primary': '#00b96b',
|
||||||
|
'ui.theme_user.font_family': '',
|
||||||
|
'ui.window_style': 'opaque'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === AUTO-GENERATED CONTENT END ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成统计:
|
||||||
|
* - 总配置项: 197
|
||||||
|
* - electronStore项: 1
|
||||||
|
* - redux项: 196
|
||||||
|
* - localStorage项: 0
|
||||||
|
*/
|
||||||
100
packages/shared/data/preference/preferenceTypes.ts
Normal file
100
packages/shared/data/preference/preferenceTypes.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import type { TranslateLanguageCode } from '@types'
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
import type { PreferenceSchemas } from './preferenceSchemas'
|
||||||
|
|
||||||
|
export type PreferenceDefaultScopeType = PreferenceSchemas['default']
|
||||||
|
export type PreferenceKeyType = keyof PreferenceDefaultScopeType
|
||||||
|
|
||||||
|
export type PreferenceUpdateOptions = {
|
||||||
|
optimistic: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PreferenceShortcutType = {
|
||||||
|
key: string[]
|
||||||
|
editable: boolean
|
||||||
|
enabled: boolean
|
||||||
|
system: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SelectionTriggerMode {
|
||||||
|
Selected = 'selected',
|
||||||
|
Ctrlkey = 'ctrlkey',
|
||||||
|
Shortcut = 'shortcut'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SelectionFilterMode {
|
||||||
|
Default = 'default',
|
||||||
|
Whitelist = 'whitelist',
|
||||||
|
Blacklist = 'blacklist'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectionActionItem = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
enabled: boolean
|
||||||
|
isBuiltIn: boolean
|
||||||
|
icon?: string
|
||||||
|
prompt?: string
|
||||||
|
assistantId?: string
|
||||||
|
selectedText?: string
|
||||||
|
searchEngine?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ThemeMode {
|
||||||
|
light = 'light',
|
||||||
|
dark = 'dark',
|
||||||
|
system = 'system'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 有限的UI语言 */
|
||||||
|
export type LanguageVarious = 'zh-CN' | 'zh-TW' | 'el-GR' | 'en-US' | 'es-ES' | 'fr-FR' | 'ja-JP' | 'pt-PT' | 'ru-RU'
|
||||||
|
|
||||||
|
export type WindowStyle = 'transparent' | 'opaque'
|
||||||
|
|
||||||
|
export type SendMessageShortcut = 'Enter' | 'Shift+Enter' | 'Ctrl+Enter' | 'Command+Enter' | 'Alt+Enter'
|
||||||
|
|
||||||
|
export type AssistantTabSortType = 'tags' | 'list'
|
||||||
|
|
||||||
|
export type SidebarIcon =
|
||||||
|
| 'assistants'
|
||||||
|
| 'store'
|
||||||
|
| 'paintings'
|
||||||
|
| 'translate'
|
||||||
|
| 'minapp'
|
||||||
|
| 'knowledge'
|
||||||
|
| 'files'
|
||||||
|
| 'code_tools'
|
||||||
|
| 'notes'
|
||||||
|
|
||||||
|
export type AssistantIconType = 'model' | 'emoji' | 'none'
|
||||||
|
|
||||||
|
export type ProxyMode = 'system' | 'custom' | 'none'
|
||||||
|
|
||||||
|
export type MultiModelFoldDisplayMode = 'expanded' | 'compact'
|
||||||
|
|
||||||
|
export type MathEngine = 'KaTeX' | 'MathJax' | 'none'
|
||||||
|
|
||||||
|
export enum UpgradeChannel {
|
||||||
|
LATEST = 'latest', // 最新稳定版本
|
||||||
|
RC = 'rc', // 公测版本
|
||||||
|
BETA = 'beta' // 预览版本
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChatMessageStyle = 'plain' | 'bubble'
|
||||||
|
|
||||||
|
export type ChatMessageNavigationMode = 'none' | 'buttons' | 'anchor'
|
||||||
|
|
||||||
|
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
||||||
|
|
||||||
|
export type MultiModelGridPopoverTrigger = 'hover' | 'click'
|
||||||
|
|
||||||
|
const AutoDetectionMethodSchema = z.enum(['franc', 'llm', 'auto'])
|
||||||
|
export type AutoDetectionMethod = z.infer<typeof AutoDetectionMethodSchema>
|
||||||
|
export const isAutoDetectionMethod = (method: string): method is AutoDetectionMethod => {
|
||||||
|
return AutoDetectionMethodSchema.safeParse(method).success
|
||||||
|
}
|
||||||
|
export type TargetLangs = {
|
||||||
|
target: TranslateLanguageCode
|
||||||
|
alter: TranslateLanguageCode
|
||||||
|
}
|
||||||
15
packages/ui/.gitignore
vendored
Normal file
15
packages/ui/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Storybook build output
|
||||||
|
storybook-static/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
17
packages/ui/.storybook/main.ts
Normal file
17
packages/ui/.storybook/main.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/react-vite'
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../stories/components/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||||
|
addons: ['@storybook/addon-docs', '@storybook/addon-themes'],
|
||||||
|
framework: '@storybook/react-vite',
|
||||||
|
viteFinal: async (config) => {
|
||||||
|
const { mergeConfig } = await import('vite')
|
||||||
|
// 动态导入 @tailwindcss/vite 以避免 ESM/CJS 兼容性问题
|
||||||
|
const tailwindPlugin = (await import('@tailwindcss/vite')).default
|
||||||
|
return mergeConfig(config, {
|
||||||
|
plugins: [tailwindPlugin()]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
18
packages/ui/.storybook/preview.tsx
Normal file
18
packages/ui/.storybook/preview.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import '../stories/tailwind.css'
|
||||||
|
|
||||||
|
import { withThemeByClassName } from '@storybook/addon-themes'
|
||||||
|
import type { Preview } from '@storybook/react'
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
decorators: [
|
||||||
|
withThemeByClassName({
|
||||||
|
themes: {
|
||||||
|
light: '',
|
||||||
|
dark: 'dark'
|
||||||
|
},
|
||||||
|
defaultTheme: 'light'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default preview
|
||||||
152
packages/ui/MIGRATION_STATUS.md
Normal file
152
packages/ui/MIGRATION_STATUS.md
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
# UI 组件库迁移状态
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 从 @cherrystudio/ui 导入组件
|
||||||
|
import { Spinner, DividerWithText, InfoTooltip, CustomTag } from '@cherrystudio/ui'
|
||||||
|
|
||||||
|
// 在组件中使用
|
||||||
|
function MyComponent() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Spinner size={24} />
|
||||||
|
<DividerWithText text="分隔文本" />
|
||||||
|
<InfoTooltip content="提示信息" />
|
||||||
|
<CustomTag color="var(--color-primary)">标签</CustomTag>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 目录结构说明
|
||||||
|
|
||||||
|
```text
|
||||||
|
@packages/ui/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/ # 组件主目录
|
||||||
|
│ │ ├── base/ # 基础组件(按钮、输入框、标签等)
|
||||||
|
│ │ ├── display/ # 显示组件(卡片、列表、表格等)
|
||||||
|
│ │ ├── layout/ # 布局组件(容器、网格、间距等)
|
||||||
|
│ │ ├── icons/ # 图标组件
|
||||||
|
│ │ ├── interactive/ # 交互组件(弹窗、提示、下拉等)
|
||||||
|
│ │ └── composite/ # 复合组件(多个基础组件组合而成)
|
||||||
|
│ ├── hooks/ # 自定义 React Hooks
|
||||||
|
│ └── types/ # TypeScript 类型定义
|
||||||
|
```
|
||||||
|
|
||||||
|
### 组件分类指南
|
||||||
|
|
||||||
|
提交 PR 时,请根据组件功能将其放入正确的目录:
|
||||||
|
|
||||||
|
- **base**: 最基础的 UI 元素,如按钮、输入框、开关、标签等
|
||||||
|
- **display**: 用于展示内容的组件,如卡片、列表、表格、标签页等
|
||||||
|
- **layout**: 用于页面布局的组件,如容器、网格系统、分隔符等
|
||||||
|
- **icons**: 所有图标相关的组件
|
||||||
|
- **interactive**: 需要用户交互的组件,如模态框、抽屉、提示框、下拉菜单等
|
||||||
|
- **composite**: 复合组件,由多个基础组件组合而成
|
||||||
|
|
||||||
|
## 迁移概览
|
||||||
|
|
||||||
|
- **总组件数**: 236
|
||||||
|
- **已迁移**: 34
|
||||||
|
- **已重构**: 18
|
||||||
|
- **待迁移**: 184
|
||||||
|
|
||||||
|
## 组件状态表
|
||||||
|
|
||||||
|
| Category | Component Name | Migration Status | Refactoring Status | Description |
|
||||||
|
| --------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| **base** | | | | 基础组件 |
|
||||||
|
| | CopyButton | ✅ | ✅ | 复制按钮 |
|
||||||
|
| | CustomTag | ✅ | ✅ | 自定义标签 |
|
||||||
|
| | DividerWithText | ✅ | ✅ | 带文本的分隔线 |
|
||||||
|
| | EmojiIcon | ✅ | ✅ | 表情图标 |
|
||||||
|
| | ErrorBoundary | ✅ | ✅ | 错误边界 (通过 props 解耦) |
|
||||||
|
| | StatusTag | ✅ | ✅ | 统一状态标签(合并了 ErrorTag、SuccessTag、WarnTag、InfoTag) |
|
||||||
|
| | IndicatorLight | ✅ | ✅ | 指示灯 |
|
||||||
|
| | Spinner | ✅ | ✅ | 加载动画 |
|
||||||
|
| | TextBadge | ✅ | ✅ | 文本徽标 |
|
||||||
|
| | CustomCollapse | ✅ | ✅ | 自定义折叠面板 |
|
||||||
|
| **display** | | | | 显示组件 |
|
||||||
|
| | Ellipsis | ✅ | ✅ | 文本省略 |
|
||||||
|
| | ExpandableText | ✅ | ✅ | 可展开文本 |
|
||||||
|
| | ThinkingEffect | ✅ | ✅ | 思考效果动画 |
|
||||||
|
| | EmojiAvatar | ✅ | ✅ | 表情头像 |
|
||||||
|
| | ListItem | ✅ | ✅ | 列表项 |
|
||||||
|
| | MaxContextCount | ✅ | ✅ | 最大上下文数显示 |
|
||||||
|
| | ProviderAvatar | ✅ | ✅ | 提供者头像 |
|
||||||
|
| | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) |
|
||||||
|
| | OGCard | ❌ | ❌ | OG 卡片 |
|
||||||
|
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown 渲染器 |
|
||||||
|
| | Preview/* | ❌ | ❌ | 预览组件 |
|
||||||
|
| **layout** | | | | 布局组件 |
|
||||||
|
| | HorizontalScrollContainer | ✅ | ❌ | 水平滚动容器 |
|
||||||
|
| | Scrollbar | ✅ | ❌ | 滚动条 |
|
||||||
|
| | Layout/* | ✅ | ✅ | 布局组件 |
|
||||||
|
| | Tab/* | ❌ | ❌ | 标签页 (Redux 依赖) |
|
||||||
|
| | TopView | ❌ | ❌ | 顶部视图 (window.api 依赖) |
|
||||||
|
| **icons** | | | | 图标组件 |
|
||||||
|
| | Icon | ✅ | ✅ | 图标工厂函数和预定义图标(合并了 CopyIcon、DeleteIcon、EditIcon、RefreshIcon、ResetIcon、ToolIcon、VisionIcon、WebSearchIcon、WrapIcon、UnWrapIcon、OcrIcon) |
|
||||||
|
| | FileIcons | ✅ | ❌ | 文件图标 (FileSvgIcon、FilePngIcon) |
|
||||||
|
| | ReasoningIcon | ✅ | ❌ | 推理图标 |
|
||||||
|
| | SvgSpinners180Ring | ✅ | ❌ | 旋转加载图标 |
|
||||||
|
| | ToolsCallingIcon | ✅ | ❌ | 工具调用图标 |
|
||||||
|
| **interactive** | | | | 交互组件 |
|
||||||
|
| | InfoTooltip | ✅ | ❌ | 信息提示 |
|
||||||
|
| | HelpTooltip | ✅ | ❌ | 帮助提示 |
|
||||||
|
| | WarnTooltip | ✅ | ❌ | 警告提示 |
|
||||||
|
| | EditableNumber | ✅ | ❌ | 可编辑数字 |
|
||||||
|
| | InfoPopover | ✅ | ❌ | 信息弹出框 |
|
||||||
|
| | CollapsibleSearchBar | ✅ | ❌ | 可折叠搜索栏 |
|
||||||
|
| | ImageToolButton | ✅ | ❌ | 图片工具按钮 |
|
||||||
|
| | DraggableList | ✅ | ❌ | 可拖拽列表 |
|
||||||
|
| | CodeEditor | ✅ | ❌ | 代码编辑器 |
|
||||||
|
| | EmojiPicker | ❌ | ❌ | 表情选择器 (useTheme 依赖) |
|
||||||
|
| | Selector | ✅ | ❌ | 选择器 (i18n 依赖) |
|
||||||
|
| | ModelSelector | ❌ | ❌ | 模型选择器 (Redux 依赖) |
|
||||||
|
| | LanguageSelect | ❌ | ❌ | 语言选择 |
|
||||||
|
| | TranslateButton | ❌ | ❌ | 翻译按钮 (window.api 依赖) |
|
||||||
|
| **composite** | | | | 复合组件 |
|
||||||
|
| | - | - | - | 暂无复合组件 |
|
||||||
|
| **未分类** | | | | 需要分类的组件 |
|
||||||
|
| | Popups/* (16+ 文件) | ❌ | ❌ | 弹窗组件 (业务耦合) |
|
||||||
|
| | RichEditor/* (30+ 文件) | ❌ | ❌ | 富文本编辑器 |
|
||||||
|
| | MarkdownEditor/* | ❌ | ❌ | Markdown 编辑器 |
|
||||||
|
| | MinApp/* | ❌ | ❌ | 迷你应用 (Redux 依赖) |
|
||||||
|
| | Avatar/* | ❌ | ❌ | 头像组件 |
|
||||||
|
| | ActionTools/* | ❌ | ❌ | 操作工具 |
|
||||||
|
| | CodeBlockView/* | ❌ | ❌ | 代码块视图 (window.api 依赖) |
|
||||||
|
| | ContextMenu | ❌ | ❌ | 右键菜单 (Electron API) |
|
||||||
|
| | WindowControls | ❌ | ❌ | 窗口控制 (Electron API) |
|
||||||
|
| | ErrorBoundary | ❌ | ❌ | 错误边界 (window.api 依赖) |
|
||||||
|
|
||||||
|
## 迁移步骤
|
||||||
|
|
||||||
|
### 第一阶段:复制迁移(当前阶段)
|
||||||
|
|
||||||
|
- 将组件原样复制到 @packages/ui
|
||||||
|
- 保留原有依赖(antd、styled-components 等)
|
||||||
|
- 在文件顶部添加原路径注释
|
||||||
|
|
||||||
|
### 第二阶段:重构优化
|
||||||
|
|
||||||
|
- 移除 antd 依赖,替换为 HeroUI
|
||||||
|
- 移除 styled-components,替换为 Tailwind CSS
|
||||||
|
- 优化组件 API 和类型定义
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **不迁移**包含以下依赖的组件(解耦后可迁移):
|
||||||
|
- window.api 调用
|
||||||
|
- Redux(useSelector、useDispatch 等)
|
||||||
|
- 其他外部数据源
|
||||||
|
|
||||||
|
2. **可迁移**但需要后续解耦的组件:
|
||||||
|
- 使用 i18n 的组件(将 i18n 改为 props 传入)
|
||||||
|
- 使用 antd 的组件(后续替换为 HeroUI)
|
||||||
|
|
||||||
|
3. **提交规范**:
|
||||||
|
- 每次 PR 专注于一个类别的组件
|
||||||
|
- 确保所有迁移的组件都有导出
|
||||||
|
- 更新此文档的迁移状态
|
||||||
151
packages/ui/MIGRATION_STATUS_EN.md
Normal file
151
packages/ui/MIGRATION_STATUS_EN.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# UI Component Library Migration Status
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Import components from @cherrystudio/ui
|
||||||
|
import { Spinner, DividerWithText, InfoTooltip } from '@cherrystudio/ui'
|
||||||
|
|
||||||
|
// Use in components
|
||||||
|
function MyComponent() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Spinner size={24} />
|
||||||
|
<DividerWithText text="Divider Text" />
|
||||||
|
<InfoTooltip content="Tooltip message" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
@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)
|
||||||
|
│ ├── hooks/ # Custom React Hooks
|
||||||
|
│ └── types/ # TypeScript type definitions
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
- **icons**: All icon-related components
|
||||||
|
- **interactive**: Components requiring user interaction like modals, drawers, tooltips, dropdowns, etc.
|
||||||
|
- **composite**: Composite components made from multiple base components
|
||||||
|
|
||||||
|
## Migration Overview
|
||||||
|
|
||||||
|
- **Total Components**: 236
|
||||||
|
- **Migrated**: 34
|
||||||
|
- **Refactored**: 18
|
||||||
|
- **Pending Migration**: 184
|
||||||
|
|
||||||
|
## Component Status Table
|
||||||
|
|
||||||
|
| Category | Component Name | Migration Status | Refactoring Status | Description |
|
||||||
|
| ----------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| **base** | | | | Base components |
|
||||||
|
| | CopyButton | ✅ | ✅ | Copy button |
|
||||||
|
| | CustomTag | ✅ | ✅ | Custom tag |
|
||||||
|
| | DividerWithText | ✅ | ✅ | Divider with text |
|
||||||
|
| | EmojiIcon | ✅ | ✅ | Emoji icon |
|
||||||
|
| | ErrorBoundary | ✅ | ✅ | Error boundary (decoupled via props) |
|
||||||
|
| | StatusTag | ✅ | ✅ | Unified status tag (merged ErrorTag, SuccessTag, WarnTag, InfoTag) |
|
||||||
|
| | IndicatorLight | ✅ | ✅ | Indicator light |
|
||||||
|
| | Spinner | ✅ | ✅ | Loading spinner |
|
||||||
|
| | TextBadge | ✅ | ✅ | Text badge |
|
||||||
|
| | CustomCollapse | ✅ | ✅ | Custom collapse panel |
|
||||||
|
| **display** | | | | Display components |
|
||||||
|
| | Ellipsis | ✅ | ✅ | Text ellipsis |
|
||||||
|
| | ExpandableText | ✅ | ✅ | Expandable text |
|
||||||
|
| | ThinkingEffect | ✅ | ✅ | Thinking effect animation |
|
||||||
|
| | EmojiAvatar | ✅ | ✅ | Emoji avatar |
|
||||||
|
| | ListItem | ✅ | ✅ | List item |
|
||||||
|
| | MaxContextCount | ✅ | ✅ | Max context count display |
|
||||||
|
| | ProviderAvatar | ✅ | ✅ | Provider avatar |
|
||||||
|
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
|
||||||
|
| | OGCard | ❌ | ❌ | OG card |
|
||||||
|
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
|
||||||
|
| | Preview/* | ❌ | ❌ | Preview components |
|
||||||
|
| **layout** | | | | Layout components |
|
||||||
|
| | HorizontalScrollContainer | ✅ | ❌ | Horizontal scroll container |
|
||||||
|
| | Scrollbar | ✅ | ❌ | Scrollbar |
|
||||||
|
| | Layout/* | ✅ | ✅ | Layout components |
|
||||||
|
| | Tab/* | ❌ | ❌ | Tab (Redux dependency) |
|
||||||
|
| | TopView | ❌ | ❌ | Top view (window.api dependency) |
|
||||||
|
| **icons** | | | | Icon components |
|
||||||
|
| | Icon | ✅ | ✅ | Icon factory function and predefined icons (merged CopyIcon, DeleteIcon, EditIcon, RefreshIcon, ResetIcon, ToolIcon, VisionIcon, WebSearchIcon, WrapIcon, UnWrapIcon, OcrIcon) |
|
||||||
|
| | FileIcons | ✅ | ❌ | File icons (FileSvgIcon, FilePngIcon) |
|
||||||
|
| | ReasoningIcon | ✅ | ❌ | Reasoning icon |
|
||||||
|
| | SvgSpinners180Ring | ✅ | ❌ | Spinner loading icon |
|
||||||
|
| | ToolsCallingIcon | ✅ | ❌ | Tools calling icon |
|
||||||
|
| **interactive** | | | | Interactive components |
|
||||||
|
| | InfoTooltip | ✅ | ❌ | Info tooltip |
|
||||||
|
| | HelpTooltip | ✅ | ❌ | Help tooltip |
|
||||||
|
| | WarnTooltip | ✅ | ❌ | Warning tooltip |
|
||||||
|
| | EditableNumber | ✅ | ❌ | Editable number |
|
||||||
|
| | InfoPopover | ✅ | ❌ | Info popover |
|
||||||
|
| | CollapsibleSearchBar | ✅ | ❌ | Collapsible search bar |
|
||||||
|
| | ImageToolButton | ✅ | ❌ | Image tool button |
|
||||||
|
| | DraggableList | ✅ | ❌ | Draggable list |
|
||||||
|
| | CodeEditor | ✅ | ❌ | Code editor |
|
||||||
|
| | EmojiPicker | ❌ | ❌ | Emoji picker (useTheme dependency) |
|
||||||
|
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
|
||||||
|
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
|
||||||
|
| | LanguageSelect | ❌ | ❌ | Language select |
|
||||||
|
| | TranslateButton | ❌ | ❌ | Translate button (window.api dependency) |
|
||||||
|
| **composite** | | | | Composite components |
|
||||||
|
| | - | - | - | No composite components yet |
|
||||||
|
| **Uncategorized** | | | | Components needing categorization |
|
||||||
|
| | Popups/* (16+ files) | ❌ | ❌ | Popup components (business coupled) |
|
||||||
|
| | RichEditor/* (30+ files) | ❌ | ❌ | Rich text editor |
|
||||||
|
| | MarkdownEditor/* | ❌ | ❌ | Markdown editor |
|
||||||
|
| | MinApp/* | ❌ | ❌ | Mini app (Redux dependency) |
|
||||||
|
| | Avatar/* | ❌ | ❌ | Avatar components |
|
||||||
|
| | ActionTools/* | ❌ | ❌ | Action tools |
|
||||||
|
| | CodeBlockView/* | ❌ | ❌ | Code block view (window.api dependency) |
|
||||||
|
| | ContextMenu | ❌ | ❌ | Context menu (Electron API) |
|
||||||
|
| | WindowControls | ❌ | ❌ | Window controls (Electron API) |
|
||||||
|
| | ErrorBoundary | ❌ | ❌ | Error boundary (window.api dependency) |
|
||||||
|
|
||||||
|
## Migration Steps
|
||||||
|
|
||||||
|
### Phase 1: Copy Migration (Current Phase)
|
||||||
|
|
||||||
|
- Copy components as-is to @packages/ui
|
||||||
|
- Retain original dependencies (antd, styled-components, etc.)
|
||||||
|
- Add original path comment at file top
|
||||||
|
|
||||||
|
### Phase 2: Refactor and Optimize
|
||||||
|
|
||||||
|
- Remove antd dependencies, replace with HeroUI
|
||||||
|
- Remove styled-components, replace with Tailwind CSS
|
||||||
|
- Optimize component APIs and type definitions
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
1. **Do NOT migrate** components with these dependencies (can be migrated after decoupling):
|
||||||
|
- window.api calls
|
||||||
|
- Redux (useSelector, useDispatch, etc.)
|
||||||
|
- Other external data sources
|
||||||
|
|
||||||
|
2. **Can migrate** but need decoupling later:
|
||||||
|
- Components using i18n (change i18n to props)
|
||||||
|
- Components using antd (replace with HeroUI later)
|
||||||
|
|
||||||
|
3. **Submission Guidelines**:
|
||||||
|
- Each PR should focus on one category of components
|
||||||
|
- Ensure all migrated components are exported
|
||||||
|
- Update migration status in this document
|
||||||
200
packages/ui/README.md
Normal file
200
packages/ui/README.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# @cherrystudio/ui
|
||||||
|
|
||||||
|
Cherry Studio UI 组件库 - 为 Cherry Studio 设计的 React 组件集合
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- 🎨 基于 Tailwind CSS 的现代化设计
|
||||||
|
- 📦 支持 ESM 和 CJS 格式
|
||||||
|
- 🔷 完整的 TypeScript 支持
|
||||||
|
- 🚀 可以作为 npm 包发布
|
||||||
|
- 🔧 开箱即用的常用 hooks 和工具函数
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装组件库
|
||||||
|
npm install @cherrystudio/ui
|
||||||
|
|
||||||
|
# 安装必需的 peer dependencies
|
||||||
|
npm install @heroui/react framer-motion react react-dom tailwindcss
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
### 1. Tailwind CSS v4 配置
|
||||||
|
|
||||||
|
本组件库使用 Tailwind CSS v4,配置方式已改变。在你的主 CSS 文件(如 `src/styles/tailwind.css`)中:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
/* 必须扫描组件库文件以提取类名 */
|
||||||
|
@source '../node_modules/@cherrystudio/ui/dist/**/*.{js,mjs}';
|
||||||
|
|
||||||
|
/* 你的应用源文件 */
|
||||||
|
@source './src/**/*.{js,ts,jsx,tsx}';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 如果你的应用直接使用 HeroUI 组件,需要添加:
|
||||||
|
* @source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||||
|
* @plugin '@heroui/react/plugin';
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 自定义主题配置(可选) */
|
||||||
|
@theme {
|
||||||
|
/* 你的主题扩展 */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:Tailwind CSS v4 不再使用 `tailwind.config.js` 文件,所有配置都在 CSS 中完成。
|
||||||
|
|
||||||
|
### 2. Provider 配置
|
||||||
|
|
||||||
|
在你的 App 根组件中添加 HeroUI Provider:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { HeroUIProvider } from '@heroui/react'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<HeroUIProvider>
|
||||||
|
{/* 你的应用内容 */}
|
||||||
|
</HeroUIProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
### 基础组件
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Button, Input } from '@cherrystudio/ui'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button variant="primary" size="md">
|
||||||
|
点击我
|
||||||
|
</Button>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入内容"
|
||||||
|
onChange={(value) => console.log(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分模块导入
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 只导入组件
|
||||||
|
import { Button } from '@cherrystudio/ui/components'
|
||||||
|
|
||||||
|
// 只导入 hooks
|
||||||
|
import { useDebounce, useLocalStorage } from '@cherrystudio/ui/hooks'
|
||||||
|
|
||||||
|
// 只导入工具函数
|
||||||
|
import { cn, formatFileSize } from '@cherrystudio/ui/utils'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# 开发模式(监听文件变化)
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
# 类型检查
|
||||||
|
yarn type-check
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
yarn test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/
|
||||||
|
├── components/ # React 组件
|
||||||
|
│ ├── Button/ # 按钮组件
|
||||||
|
│ ├── Input/ # 输入框组件
|
||||||
|
│ └── index.ts # 组件导出
|
||||||
|
├── hooks/ # React Hooks
|
||||||
|
├── utils/ # 工具函数
|
||||||
|
├── types/ # 类型定义
|
||||||
|
└── index.ts # 主入口文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 组件列表
|
||||||
|
|
||||||
|
### Button 按钮
|
||||||
|
|
||||||
|
支持多种变体和尺寸的按钮组件。
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
|
||||||
|
- `variant`: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
|
||||||
|
- `size`: 'sm' | 'md' | 'lg'
|
||||||
|
- `loading`: boolean
|
||||||
|
- `fullWidth`: boolean
|
||||||
|
- `leftIcon` / `rightIcon`: React.ReactNode
|
||||||
|
|
||||||
|
### Input 输入框
|
||||||
|
|
||||||
|
带有错误处理和密码显示切换的输入框组件。
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
|
||||||
|
- `type`: 'text' | 'password' | 'email' | 'number'
|
||||||
|
- `error`: boolean
|
||||||
|
- `errorMessage`: string
|
||||||
|
- `onChange`: (value: string) => void
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
### useDebounce
|
||||||
|
|
||||||
|
防抖处理,延迟执行状态更新。
|
||||||
|
|
||||||
|
### useLocalStorage
|
||||||
|
|
||||||
|
本地存储的 React Hook 封装。
|
||||||
|
|
||||||
|
### useClickOutside
|
||||||
|
|
||||||
|
检测点击元素外部区域。
|
||||||
|
|
||||||
|
### useCopyToClipboard
|
||||||
|
|
||||||
|
复制文本到剪贴板。
|
||||||
|
|
||||||
|
## 工具函数
|
||||||
|
|
||||||
|
### cn(...inputs)
|
||||||
|
|
||||||
|
基于 clsx 的类名合并工具,支持条件类名。
|
||||||
|
|
||||||
|
### formatFileSize(bytes)
|
||||||
|
|
||||||
|
格式化文件大小显示。
|
||||||
|
|
||||||
|
### debounce(func, delay)
|
||||||
|
|
||||||
|
防抖函数。
|
||||||
|
|
||||||
|
### throttle(func, delay)
|
||||||
|
|
||||||
|
节流函数。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
21
packages/ui/components.json
Normal file
21
packages/ui/components.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"hooks": "@/hooks",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"utils": "@/utils"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rsc": false,
|
||||||
|
"style": "new-york",
|
||||||
|
"tailwind": {
|
||||||
|
"baseColor": "zinc",
|
||||||
|
"config": "",
|
||||||
|
"css": "src/styles/globals.css",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"tsx": true
|
||||||
|
}
|
||||||
129
packages/ui/package.json
Normal file
129
packages/ui/package.json
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"name": "@cherrystudio/ui",
|
||||||
|
"version": "1.0.0-alpha.1",
|
||||||
|
"description": "Cherry Studio UI Component Library - React Components for Cherry Studio",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"react-native": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsdown",
|
||||||
|
"dev": "tsc -w",
|
||||||
|
"clean": "rm -rf dist",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"lint": "eslint src --ext .ts,.tsx --fix",
|
||||||
|
"type-check": "tsc --noEmit -p tsconfig.json --composite false",
|
||||||
|
"storybook": "storybook dev -p 6006",
|
||||||
|
"build-storybook": "storybook build"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"ui",
|
||||||
|
"components",
|
||||||
|
"react",
|
||||||
|
"tailwindcss",
|
||||||
|
"typescript",
|
||||||
|
"cherry-studio"
|
||||||
|
],
|
||||||
|
"author": "Cherry Studio",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/CherryHQ/cherry-studio.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/CherryHQ/cherry-studio/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/CherryHQ/cherry-studio#readme",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@heroui/react": "^2.8.4",
|
||||||
|
"framer-motion": "^11.0.0 || ^12.0.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"tailwindcss": "^4.1.13"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"lucide-react": "^0.545.0",
|
||||||
|
"react-dropzone": "^14.3.8",
|
||||||
|
"tailwind-merge": "^2.5.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@heroui/react": "^2.8.4",
|
||||||
|
"@storybook/addon-docs": "^9.1.6",
|
||||||
|
"@storybook/addon-themes": "^9.1.6",
|
||||||
|
"@storybook/react-vite": "^9.1.6",
|
||||||
|
"@types/react": "^19.0.12",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@types/styled-components": "^5.1.34",
|
||||||
|
"@uiw/codemirror-extensions-langs": "^4.25.1",
|
||||||
|
"@uiw/codemirror-themes-all": "^4.25.1",
|
||||||
|
"@uiw/react-codemirror": "^4.25.1",
|
||||||
|
"antd": "^5.22.5",
|
||||||
|
"eslint-plugin-storybook": "9.1.6",
|
||||||
|
"framer-motion": "^12.23.12",
|
||||||
|
"linguist-languages": "^9.0.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"storybook": "^9.1.6",
|
||||||
|
"styled-components": "^6.1.15",
|
||||||
|
"tsdown": "^0.15.5",
|
||||||
|
"tsx": "^4.20.5",
|
||||||
|
"typescript": "^5.6.2",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@codemirror/language": "6.11.3",
|
||||||
|
"@codemirror/lint": "6.8.5",
|
||||||
|
"@codemirror/view": "6.38.1"
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"react-native": "./dist/index.js",
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.js",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./components": {
|
||||||
|
"types": "./dist/components/index.d.ts",
|
||||||
|
"react-native": "./dist/components/index.js",
|
||||||
|
"import": "./dist/components/index.mjs",
|
||||||
|
"require": "./dist/components/index.js",
|
||||||
|
"default": "./dist/components/index.js"
|
||||||
|
},
|
||||||
|
"./hooks": {
|
||||||
|
"types": "./dist/hooks/index.d.ts",
|
||||||
|
"react-native": "./dist/hooks/index.js",
|
||||||
|
"import": "./dist/hooks/index.mjs",
|
||||||
|
"require": "./dist/hooks/index.js",
|
||||||
|
"default": "./dist/hooks/index.js"
|
||||||
|
},
|
||||||
|
"./utils": {
|
||||||
|
"types": "./dist/utils/index.d.ts",
|
||||||
|
"react-native": "./dist/utils/index.js",
|
||||||
|
"import": "./dist/utils/index.mjs",
|
||||||
|
"require": "./dist/utils/index.js",
|
||||||
|
"default": "./dist/utils/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@4.9.1"
|
||||||
|
}
|
||||||
36
packages/ui/src/components/base/Avatar/EmojiAvatar.tsx
Normal file
36
packages/ui/src/components/base/Avatar/EmojiAvatar.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { cn } from '@heroui/react'
|
||||||
|
import React, { memo } from 'react'
|
||||||
|
|
||||||
|
interface EmojiAvatarProps {
|
||||||
|
children: string
|
||||||
|
size?: number
|
||||||
|
fontSize?: number
|
||||||
|
onClick?: React.MouseEventHandler<HTMLDivElement>
|
||||||
|
className?: string
|
||||||
|
style?: React.CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmojiAvatar = ({ children, size = 31, fontSize, onClick, className, style }: EmojiAvatarProps) => (
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-center',
|
||||||
|
'bg-background-soft border-border',
|
||||||
|
'rounded-[20%] cursor-pointer',
|
||||||
|
'transition-opacity hover:opacity-80',
|
||||||
|
'border-[0.5px]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
fontSize: fontSize ?? size * 0.5,
|
||||||
|
...style
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
EmojiAvatar.displayName = 'EmojiAvatar'
|
||||||
|
|
||||||
|
export default memo(EmojiAvatar)
|
||||||
26
packages/ui/src/components/base/Avatar/index.tsx
Normal file
26
packages/ui/src/components/base/Avatar/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { AvatarProps as HeroUIAvatarProps } from '@heroui/react'
|
||||||
|
import { Avatar as HeroUIAvatar, AvatarGroup as HeroUIAvatarGroup, cn } from '@heroui/react'
|
||||||
|
|
||||||
|
import EmojiAvatar from './EmojiAvatar'
|
||||||
|
|
||||||
|
export interface AvatarProps extends Omit<HeroUIAvatarProps, 'size'> {
|
||||||
|
size?: 'xs' | 'sm' | 'md' | 'lg'
|
||||||
|
}
|
||||||
|
|
||||||
|
const Avatar = (props: AvatarProps) => {
|
||||||
|
const { size, className = '', ...rest } = props
|
||||||
|
const isExtraSmall = size === 'xs'
|
||||||
|
|
||||||
|
const resolvedSize = isExtraSmall ? undefined : size
|
||||||
|
const mergedClassName = cn(isExtraSmall && 'w-6 h-6 text-tiny', 'shadow-lg', className)
|
||||||
|
|
||||||
|
return <HeroUIAvatar size={resolvedSize} className={mergedClassName} {...rest} />
|
||||||
|
}
|
||||||
|
|
||||||
|
Avatar.displayName = 'Avatar'
|
||||||
|
|
||||||
|
const AvatarGroup = HeroUIAvatarGroup
|
||||||
|
|
||||||
|
AvatarGroup.displayName = 'AvatarGroup'
|
||||||
|
|
||||||
|
export { Avatar, AvatarGroup, EmojiAvatar }
|
||||||
12
packages/ui/src/components/base/Button/index.tsx
Normal file
12
packages/ui/src/components/base/Button/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { ButtonProps as HeroUIButtonProps } from '@heroui/react'
|
||||||
|
import { Button as HeroUIButton } from '@heroui/react'
|
||||||
|
|
||||||
|
export interface ButtonProps extends HeroUIButtonProps {}
|
||||||
|
|
||||||
|
const Button = ({ ...props }: ButtonProps) => {
|
||||||
|
return <HeroUIButton {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
Button.displayName = 'Button'
|
||||||
|
|
||||||
|
export default Button
|
||||||
31
packages/ui/src/components/base/CopyButton/index.tsx
Normal file
31
packages/ui/src/components/base/CopyButton/index.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Original path: src/renderer/src/components/CopyButton.tsx
|
||||||
|
import { Tooltip } from '@heroui/react'
|
||||||
|
import { Copy } from 'lucide-react'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
|
||||||
|
interface CopyButtonProps {
|
||||||
|
tooltip?: string
|
||||||
|
label?: string
|
||||||
|
size?: number
|
||||||
|
className?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const CopyButton: FC<CopyButtonProps> = ({ tooltip, label, size = 14, className = '', ...props }) => {
|
||||||
|
const button = (
|
||||||
|
<div
|
||||||
|
className={`flex flex-row items-center gap-1 cursor-pointer text-gray-600 dark:text-gray-400 transition-colors duration-200 hover:text-blue-600 dark:hover:text-blue-400 ${className}`}
|
||||||
|
{...props}>
|
||||||
|
<Copy size={size} className="transition-colors duration-200" />
|
||||||
|
{label && <span style={{ fontSize: `${size}px` }}>{label}</span>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (tooltip) {
|
||||||
|
return <Tooltip content={tooltip}>{button}</Tooltip>
|
||||||
|
}
|
||||||
|
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CopyButton
|
||||||
46
packages/ui/src/components/base/CustomCollapse/index.tsx
Normal file
46
packages/ui/src/components/base/CustomCollapse/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Accordion, AccordionItem, type AccordionItemProps, type AccordionProps } from '@heroui/react'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import { memo } from 'react'
|
||||||
|
|
||||||
|
// 重新导出 HeroUI 的组件,方便直接使用
|
||||||
|
export { Accordion, AccordionItem } from '@heroui/react'
|
||||||
|
|
||||||
|
interface CustomCollapseProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
accordionProps?: Omit<AccordionProps, 'children'>
|
||||||
|
accordionItemProps?: Omit<AccordionItemProps, 'children'>
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomCollapse: FC<CustomCollapseProps> = ({ children, accordionProps = {}, accordionItemProps = {} }) => {
|
||||||
|
// 解构 Accordion 的 props
|
||||||
|
const {
|
||||||
|
defaultExpandedKeys = ['1'],
|
||||||
|
variant = 'bordered',
|
||||||
|
className = '',
|
||||||
|
isDisabled = false,
|
||||||
|
...restAccordionProps
|
||||||
|
} = accordionProps
|
||||||
|
|
||||||
|
// 解构 AccordionItem 的 props
|
||||||
|
const { title = 'Collapse Panel', ...restAccordionItemProps } = accordionItemProps
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion
|
||||||
|
defaultExpandedKeys={defaultExpandedKeys}
|
||||||
|
variant={variant}
|
||||||
|
className={className}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
selectionMode="multiple"
|
||||||
|
{...restAccordionProps}>
|
||||||
|
<AccordionItem
|
||||||
|
key="1"
|
||||||
|
aria-label={typeof title === 'string' ? title : 'collapse-item'}
|
||||||
|
title={title}
|
||||||
|
{...restAccordionItemProps}>
|
||||||
|
{children}
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(CustomCollapse)
|
||||||
87
packages/ui/src/components/base/CustomTag/index.tsx
Normal file
87
packages/ui/src/components/base/CustomTag/index.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// Original path: src/renderer/src/components/Tags/CustomTag.tsx
|
||||||
|
import { Tooltip } from '@heroui/react'
|
||||||
|
import { X } from 'lucide-react'
|
||||||
|
import type { CSSProperties, FC, MouseEventHandler } from 'react'
|
||||||
|
import { memo, useMemo } from 'react'
|
||||||
|
|
||||||
|
export interface CustomTagProps {
|
||||||
|
icon?: React.ReactNode
|
||||||
|
children?: React.ReactNode | string
|
||||||
|
color: string
|
||||||
|
size?: number
|
||||||
|
style?: CSSProperties
|
||||||
|
tooltip?: string
|
||||||
|
closable?: boolean
|
||||||
|
onClose?: () => void
|
||||||
|
onClick?: MouseEventHandler<HTMLDivElement>
|
||||||
|
disabled?: boolean
|
||||||
|
inactive?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomTag: FC<CustomTagProps> = ({
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
color,
|
||||||
|
size = 12,
|
||||||
|
style,
|
||||||
|
tooltip,
|
||||||
|
closable = false,
|
||||||
|
onClose,
|
||||||
|
onClick,
|
||||||
|
disabled,
|
||||||
|
inactive,
|
||||||
|
className = ''
|
||||||
|
}) => {
|
||||||
|
const actualColor = inactive ? '#aaaaaa' : color
|
||||||
|
|
||||||
|
const tagContent = useMemo(
|
||||||
|
() => (
|
||||||
|
<div
|
||||||
|
className={`inline-flex items-center gap-1 rounded-full whitespace-nowrap relative transition-opacity duration-200 ${
|
||||||
|
!disabled && onClick ? 'cursor-pointer hover:opacity-80' : disabled ? 'cursor-not-allowed' : 'cursor-auto'
|
||||||
|
} ${className}`}
|
||||||
|
style={{
|
||||||
|
padding: `${size / 3}px ${closable ? size * 1.8 : size * 0.8}px ${size / 3}px ${size * 0.8}px`,
|
||||||
|
color: actualColor,
|
||||||
|
backgroundColor: actualColor + '20',
|
||||||
|
fontSize: `${size}px`,
|
||||||
|
lineHeight: 1,
|
||||||
|
...style
|
||||||
|
}}
|
||||||
|
onClick={disabled ? undefined : onClick}>
|
||||||
|
{icon && <span style={{ fontSize: `${size}px`, color: actualColor }}>{icon}</span>}
|
||||||
|
{children}
|
||||||
|
{closable && (
|
||||||
|
<div
|
||||||
|
className="absolute flex items-center justify-center cursor-pointer rounded-full transition-all duration-200 hover:bg-[#da8a8a] hover:text-white"
|
||||||
|
style={{
|
||||||
|
right: `${size * 0.2}px`,
|
||||||
|
top: `${size * 0.2}px`,
|
||||||
|
bottom: `${size * 0.2}px`,
|
||||||
|
fontSize: `${size * 0.8}px`,
|
||||||
|
color: actualColor,
|
||||||
|
aspectRatio: 1
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onClose?.()
|
||||||
|
}}>
|
||||||
|
<X size={size * 0.8} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[actualColor, children, closable, disabled, icon, onClick, onClose, size, style, className]
|
||||||
|
)
|
||||||
|
|
||||||
|
return tooltip ? (
|
||||||
|
<Tooltip content={tooltip} delay={300}>
|
||||||
|
{tagContent}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
tagContent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(CustomTag)
|
||||||
20
packages/ui/src/components/base/DividerWithText/index.tsx
Normal file
20
packages/ui/src/components/base/DividerWithText/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Original: src/renderer/src/components/DividerWithText.tsx
|
||||||
|
import type { CSSProperties } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface DividerWithTextProps {
|
||||||
|
text: string
|
||||||
|
style?: CSSProperties
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DividerWithText: React.FC<DividerWithTextProps> = ({ text, style, className = '' }) => {
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center my-0 ${className}`} style={style}>
|
||||||
|
<span className="text-xs text-gray-600 dark:text-gray-400 mr-2">{text}</span>
|
||||||
|
<div className="flex-1 h-px bg-gray-200 dark:bg-gray-700" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DividerWithText
|
||||||
34
packages/ui/src/components/base/EmojiIcon/index.tsx
Normal file
34
packages/ui/src/components/base/EmojiIcon/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Original path: src/renderer/src/components/EmojiIcon.tsx
|
||||||
|
import type { FC } from 'react'
|
||||||
|
|
||||||
|
interface EmojiIconProps {
|
||||||
|
emoji: string
|
||||||
|
className?: string
|
||||||
|
size?: number
|
||||||
|
fontSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmojiIcon: FC<EmojiIconProps> = ({ emoji, className = '', size = 26, fontSize = 15 }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-center flex-shrink-0 relative overflow-hidden mr-1 rounded-full ${className}`}
|
||||||
|
style={{
|
||||||
|
width: `${size}px`,
|
||||||
|
height: `${size}px`,
|
||||||
|
borderRadius: `${size / 2}px`,
|
||||||
|
fontSize: `${fontSize}px`
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 flex items-center justify-center blur-sm opacity-40"
|
||||||
|
style={{
|
||||||
|
fontSize: '200%',
|
||||||
|
transform: 'scale(1.5)'
|
||||||
|
}}>
|
||||||
|
{emoji || '⭐️'}
|
||||||
|
</div>
|
||||||
|
{emoji}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmojiIcon
|
||||||
94
packages/ui/src/components/base/ErrorBoundary/index.tsx
Normal file
94
packages/ui/src/components/base/ErrorBoundary/index.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// Original path: src/renderer/src/components/ErrorBoundary.tsx
|
||||||
|
import { Button } from '@heroui/react'
|
||||||
|
import { AlertTriangle } from 'lucide-react'
|
||||||
|
import type { ComponentType, ReactNode } from 'react'
|
||||||
|
import type { FallbackProps } from 'react-error-boundary'
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary'
|
||||||
|
|
||||||
|
import { formatErrorMessage } from './utils'
|
||||||
|
|
||||||
|
interface CustomFallbackProps extends FallbackProps {
|
||||||
|
onDebugClick?: () => void | Promise<void>
|
||||||
|
onReloadClick?: () => void | Promise<void>
|
||||||
|
debugButtonText?: string
|
||||||
|
reloadButtonText?: string
|
||||||
|
errorMessage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultFallback: ComponentType<CustomFallbackProps> = (props: CustomFallbackProps): ReactNode => {
|
||||||
|
const {
|
||||||
|
error,
|
||||||
|
onDebugClick,
|
||||||
|
onReloadClick,
|
||||||
|
debugButtonText = 'Open DevTools',
|
||||||
|
reloadButtonText = 'Reload',
|
||||||
|
errorMessage = 'An error occurred'
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center w-full p-2">
|
||||||
|
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 w-full">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<AlertTriangle className="text-red-500 dark:text-red-400 flex-shrink-0 mt-0.5" size={20} />
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-red-800 dark:text-red-200 font-medium text-sm mb-1">{errorMessage}</h3>
|
||||||
|
<p className="text-red-700 dark:text-red-300 text-sm mb-3">{formatErrorMessage(error)}</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{onDebugClick && (
|
||||||
|
<Button size="sm" variant="flat" color="danger" onPress={onDebugClick}>
|
||||||
|
{debugButtonText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onReloadClick && (
|
||||||
|
<Button size="sm" variant="flat" color="danger" onPress={onReloadClick}>
|
||||||
|
{reloadButtonText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorBoundaryCustomizedProps {
|
||||||
|
children: ReactNode
|
||||||
|
fallbackComponent?: ComponentType<CustomFallbackProps>
|
||||||
|
onDebugClick?: () => void | Promise<void>
|
||||||
|
onReloadClick?: () => void | Promise<void>
|
||||||
|
debugButtonText?: string
|
||||||
|
reloadButtonText?: string
|
||||||
|
errorMessage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorBoundaryCustomized = ({
|
||||||
|
children,
|
||||||
|
fallbackComponent,
|
||||||
|
onDebugClick,
|
||||||
|
onReloadClick,
|
||||||
|
debugButtonText,
|
||||||
|
reloadButtonText,
|
||||||
|
errorMessage
|
||||||
|
}: ErrorBoundaryCustomizedProps) => {
|
||||||
|
const FallbackComponent = fallbackComponent ?? DefaultFallback
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary
|
||||||
|
FallbackComponent={(props: FallbackProps) => (
|
||||||
|
<FallbackComponent
|
||||||
|
{...props}
|
||||||
|
onDebugClick={onDebugClick}
|
||||||
|
onReloadClick={onReloadClick}
|
||||||
|
debugButtonText={debugButtonText}
|
||||||
|
reloadButtonText={reloadButtonText}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
/>
|
||||||
|
)}>
|
||||||
|
{children}
|
||||||
|
</ErrorBoundary>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ErrorBoundaryCustomized as ErrorBoundary }
|
||||||
|
export type { CustomFallbackProps, ErrorBoundaryCustomizedProps }
|
||||||
8
packages/ui/src/components/base/ErrorBoundary/utils.ts
Normal file
8
packages/ui/src/components/base/ErrorBoundary/utils.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Utility functions for ErrorBoundary component
|
||||||
|
|
||||||
|
export function formatErrorMessage(error: Error): string {
|
||||||
|
if (error.message) {
|
||||||
|
return error.message
|
||||||
|
}
|
||||||
|
return error.toString()
|
||||||
|
}
|
||||||
37
packages/ui/src/components/base/IndicatorLight/index.tsx
Normal file
37
packages/ui/src/components/base/IndicatorLight/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Original: src/renderer/src/components/IndicatorLight.tsx
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface IndicatorLightProps {
|
||||||
|
color: string
|
||||||
|
size?: number
|
||||||
|
shadow?: boolean
|
||||||
|
style?: React.CSSProperties
|
||||||
|
animation?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const IndicatorLight: React.FC<IndicatorLightProps> = ({
|
||||||
|
color,
|
||||||
|
size = 8,
|
||||||
|
shadow = true,
|
||||||
|
style,
|
||||||
|
animation = true,
|
||||||
|
className = ''
|
||||||
|
}) => {
|
||||||
|
const actualColor = color === 'green' ? '#22c55e' : color
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`rounded-full ${animation ? 'animate-pulse' : ''} ${className}`}
|
||||||
|
style={{
|
||||||
|
width: `${size}px`,
|
||||||
|
height: `${size}px`,
|
||||||
|
backgroundColor: actualColor,
|
||||||
|
boxShadow: shadow ? `0 0 6px ${actualColor}` : 'none',
|
||||||
|
...style
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndicatorLight
|
||||||
333
packages/ui/src/components/base/Selector/README.md
Normal file
333
packages/ui/src/components/base/Selector/README.md
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
# Selector 组件
|
||||||
|
|
||||||
|
基于 HeroUI Select 封装的下拉选择组件,简化了 Set 和 Selection 的转换逻辑。
|
||||||
|
|
||||||
|
## 核心特性
|
||||||
|
|
||||||
|
- ✅ **类型安全**: 单选和多选自动推断回调类型
|
||||||
|
- ✅ **智能转换**: 自动处理 `Set<Key>` 和原始值的转换
|
||||||
|
- ✅ **HeroUI 风格**: 保持与 HeroUI 生态一致的 API
|
||||||
|
- ✅ **支持数字和字符串**: 泛型支持,自动识别值类型
|
||||||
|
|
||||||
|
## 基础用法
|
||||||
|
|
||||||
|
### 单选模式(默认)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Selector } from '@cherrystudio/ui'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
function Example() {
|
||||||
|
const [language, setLanguage] = useState('zh-CN')
|
||||||
|
|
||||||
|
const languageOptions = [
|
||||||
|
{ label: '中文', value: 'zh-CN' },
|
||||||
|
{ label: 'English', value: 'en-US' },
|
||||||
|
{ label: '日本語', value: 'ja-JP' }
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Selector
|
||||||
|
selectedKeys={language}
|
||||||
|
onSelectionChange={(value) => {
|
||||||
|
// value 类型自动推断为 string
|
||||||
|
setLanguage(value)
|
||||||
|
}}
|
||||||
|
items={languageOptions}
|
||||||
|
placeholder="选择语言"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 多选模式
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Selector } from '@cherrystudio/ui'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
function Example() {
|
||||||
|
const [languages, setLanguages] = useState(['zh-CN', 'en-US'])
|
||||||
|
|
||||||
|
const languageOptions = [
|
||||||
|
{ label: '中文', value: 'zh-CN' },
|
||||||
|
{ label: 'English', value: 'en-US' },
|
||||||
|
{ label: '日本語', value: 'ja-JP' },
|
||||||
|
{ label: 'Français', value: 'fr-FR' }
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Selector
|
||||||
|
selectionMode="multiple"
|
||||||
|
selectedKeys={languages}
|
||||||
|
onSelectionChange={(values) => {
|
||||||
|
// values 类型自动推断为 string[]
|
||||||
|
setLanguages(values)
|
||||||
|
}}
|
||||||
|
items={languageOptions}
|
||||||
|
placeholder="选择语言"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数字类型值
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Selector } from '@cherrystudio/ui'
|
||||||
|
|
||||||
|
function Example() {
|
||||||
|
const [priority, setPriority] = useState<number>(1)
|
||||||
|
|
||||||
|
const priorityOptions = [
|
||||||
|
{ label: '低', value: 1 },
|
||||||
|
{ label: '中', value: 2 },
|
||||||
|
{ label: '高', value: 3 }
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Selector<number>
|
||||||
|
selectedKeys={priority}
|
||||||
|
onSelectionChange={(value) => {
|
||||||
|
// value 类型为 number
|
||||||
|
setPriority(value)
|
||||||
|
}}
|
||||||
|
items={priorityOptions}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 禁用选项
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const options = [
|
||||||
|
{ label: '选项 1', value: '1' },
|
||||||
|
{ label: '选项 2 (禁用)', value: '2', disabled: true },
|
||||||
|
{ label: '选项 3', value: '3' }
|
||||||
|
]
|
||||||
|
|
||||||
|
<Selector
|
||||||
|
selectedKeys="1"
|
||||||
|
onSelectionChange={handleChange}
|
||||||
|
items={options}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义 Label
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Flex } from '@cherrystudio/ui'
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<Flex className="items-center gap-2">
|
||||||
|
<span>🇨🇳</span>
|
||||||
|
<span>中文</span>
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
value: 'zh-CN'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<Flex className="items-center gap-2">
|
||||||
|
<span>🇺🇸</span>
|
||||||
|
<span>English</span>
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
value: 'en-US'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
<Selector
|
||||||
|
selectedKeys="zh-CN"
|
||||||
|
onSelectionChange={handleChange}
|
||||||
|
items={options}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### SelectorProps
|
||||||
|
|
||||||
|
| 属性 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `items` | `SelectorItem<V>[]` | - | 必填,选项列表 |
|
||||||
|
| `selectedKeys` | `V` \| `V[]` | - | 受控的选中值(单选为单个值,多选为数组) |
|
||||||
|
| `onSelectionChange` | `(key: V) => void` \| `(keys: V[]) => void` | - | 选择变化回调(类型根据 selectionMode 自动推断) |
|
||||||
|
| `selectionMode` | `'single'` \| `'multiple'` | `'single'` | 选择模式 |
|
||||||
|
| `placeholder` | `string` | - | 占位文本 |
|
||||||
|
| `disabled` | `boolean` | `false` | 是否禁用 |
|
||||||
|
| `isRequired` | `boolean` | `false` | 是否必填 |
|
||||||
|
| `label` | `ReactNode` | - | 标签文本 |
|
||||||
|
| `description` | `ReactNode` | - | 描述文本 |
|
||||||
|
| `errorMessage` | `ReactNode` | - | 错误提示 |
|
||||||
|
| ...rest | `SelectProps` | - | 其他 HeroUI Select 属性 |
|
||||||
|
|
||||||
|
### SelectorItem
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface SelectorItem<V = string | number> {
|
||||||
|
label: string | ReactNode // 显示文本或自定义内容
|
||||||
|
value: V // 选项值
|
||||||
|
disabled?: boolean // 是否禁用
|
||||||
|
[key: string]: any // 其他自定义属性
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 类型安全
|
||||||
|
|
||||||
|
组件使用 TypeScript 条件类型,根据 `selectionMode` 自动推断回调类型:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 单选模式
|
||||||
|
<Selector
|
||||||
|
selectionMode="single" // 或省略(默认单选)
|
||||||
|
selectedKeys={value} // 类型: V
|
||||||
|
onSelectionChange={(v) => ...} // v 类型: V
|
||||||
|
/>
|
||||||
|
|
||||||
|
// 多选模式
|
||||||
|
<Selector
|
||||||
|
selectionMode="multiple"
|
||||||
|
selectedKeys={values} // 类型: V[]
|
||||||
|
onSelectionChange={(vs) => ...} // vs 类型: V[]
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 与 HeroUI Select 的区别
|
||||||
|
|
||||||
|
| 特性 | HeroUI Select | Selector (本组件) |
|
||||||
|
|------|---------------|------------------|
|
||||||
|
| `selectedKeys` | `Set<Key> \| 'all'` | `V` \| `V[]` (自动转换) |
|
||||||
|
| `onSelectionChange` | `(keys: Selection) => void` | `(key: V) => void` \| `(keys: V[]) => void` |
|
||||||
|
| 单选回调 | 返回 `Set` (需手动提取) | 直接返回单个值 |
|
||||||
|
| 多选回调 | 返回 `Set` (需转数组) | 直接返回数组 |
|
||||||
|
| 类型推断 | 无 | 根据 selectionMode 自动推断 |
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 显式声明 selectionMode
|
||||||
|
|
||||||
|
虽然单选是默认模式,但建议显式声明以提高代码可读性:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ✅ 推荐
|
||||||
|
<Selector selectionMode="single" ... />
|
||||||
|
|
||||||
|
// ⚠️ 可以但不够清晰
|
||||||
|
<Selector ... />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用泛型指定值类型
|
||||||
|
|
||||||
|
当值类型为数字或联合类型时,使用泛型获得更好的类型提示:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ✅ 推荐
|
||||||
|
<Selector<number> selectedKeys={priority} ... />
|
||||||
|
|
||||||
|
// ✅ 推荐(联合类型)
|
||||||
|
type Status = 'pending' | 'approved' | 'rejected'
|
||||||
|
<Selector<Status> selectedKeys={status} ... />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 避免在渲染时创建 items
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ❌ 不推荐(每次渲染都创建新数组)
|
||||||
|
<Selector items={[{ label: 'A', value: '1' }]} />
|
||||||
|
|
||||||
|
// ✅ 推荐(在组件外或使用 useMemo)
|
||||||
|
const items = [{ label: 'A', value: '1' }]
|
||||||
|
<Selector items={items} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## 迁移指南
|
||||||
|
|
||||||
|
### 从 antd Select 迁移
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// antd Select
|
||||||
|
import { Select } from 'antd'
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
onChange={(value) => onChange(value)}
|
||||||
|
options={[
|
||||||
|
{ label: 'A', value: '1' },
|
||||||
|
{ label: 'B', value: '2' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
// 迁移到 Selector
|
||||||
|
import { Selector } from '@cherrystudio/ui'
|
||||||
|
|
||||||
|
<Selector
|
||||||
|
selectedKeys={value} // value → selectedKeys
|
||||||
|
onSelectionChange={(value) => onChange(value)} // onChange → onSelectionChange
|
||||||
|
items={[ // options → items
|
||||||
|
{ label: 'A', value: '1' },
|
||||||
|
{ label: 'B', value: '2' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 从旧版 Selector 迁移
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 旧版 Selector (返回数组)
|
||||||
|
<Selector
|
||||||
|
onSelectionChange={(values) => {
|
||||||
|
const value = values[0] // 需要手动提取
|
||||||
|
onChange(value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
// 新版 Selector (直接返回值)
|
||||||
|
<Selector
|
||||||
|
selectionMode="single"
|
||||||
|
onSelectionChange={(value) => {
|
||||||
|
onChange(value) // 直接使用
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 为什么单选模式下还需要 selectedKeys 而不是 selectedKey?
|
||||||
|
|
||||||
|
A: 为了保持与 HeroUI API 命名的一致性,同时简化组件实现。组件内部会自动处理单个值和 Set 的转换。
|
||||||
|
|
||||||
|
### Q: 如何清空选择?
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 单选模式
|
||||||
|
<Selector
|
||||||
|
selectedKeys={value}
|
||||||
|
onSelectionChange={setValue}
|
||||||
|
isClearable // 添加清空按钮
|
||||||
|
/>
|
||||||
|
|
||||||
|
// 或手动设置为 undefined
|
||||||
|
setValue(undefined)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: 支持异步加载选项吗?
|
||||||
|
|
||||||
|
支持,配合 `isLoading` 属性使用:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const [items, setItems] = useState([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchItems().then(data => {
|
||||||
|
setItems(data)
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
<Selector items={items} isLoading={loading} />
|
||||||
|
```
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Autocomplete, AutocompleteItem } from '@heroui/react'
|
||||||
|
import type { Key } from '@react-types/shared'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
import type { SearchableSelectorItem, SearchableSelectorProps } from './types'
|
||||||
|
|
||||||
|
const SearchableSelector = <T extends SearchableSelectorItem>(props: SearchableSelectorProps<T>) => {
|
||||||
|
const { items, onSelectionChange, selectedKeys, selectionMode = 'single', children, ...rest } = props
|
||||||
|
|
||||||
|
// 转换 selectedKeys: V | V[] → Key | undefined (Autocomplete 只支持单选)
|
||||||
|
const autocompleteSelectedKey = useMemo(() => {
|
||||||
|
if (selectedKeys === undefined) return undefined
|
||||||
|
|
||||||
|
if (selectionMode === 'multiple') {
|
||||||
|
// Autocomplete 不支持多选,取第一个
|
||||||
|
const keys = selectedKeys as T['value'][]
|
||||||
|
return keys.length > 0 ? String(keys[0]) : undefined
|
||||||
|
} else {
|
||||||
|
return String(selectedKeys)
|
||||||
|
}
|
||||||
|
}, [selectedKeys, selectionMode])
|
||||||
|
|
||||||
|
// 处理选择变化
|
||||||
|
const handleSelectionChange = (key: Key | null) => {
|
||||||
|
if (!onSelectionChange || key === null) return
|
||||||
|
|
||||||
|
const strKey = String(key)
|
||||||
|
// 尝试转换回数字类型
|
||||||
|
const num = Number(strKey)
|
||||||
|
const value = !isNaN(num) && items.some((item) => item.value === num) ? (num as T['value']) : (strKey as T['value'])
|
||||||
|
|
||||||
|
if (selectionMode === 'multiple') {
|
||||||
|
// 多选模式: 返回数组 (Autocomplete 只支持单选,这里简化处理)
|
||||||
|
;(onSelectionChange as (keys: T['value'][]) => void)([value])
|
||||||
|
} else {
|
||||||
|
// 单选模式: 返回单个值
|
||||||
|
;(onSelectionChange as (key: T['value']) => void)(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认渲染函数
|
||||||
|
const defaultRenderItem = (item: T) => (
|
||||||
|
<AutocompleteItem key={String(item.value)} textValue={item.label ? String(item.label) : String(item.value)}>
|
||||||
|
{item.label ?? item.value}
|
||||||
|
</AutocompleteItem>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
{...rest}
|
||||||
|
items={items}
|
||||||
|
selectedKey={autocompleteSelectedKey}
|
||||||
|
onSelectionChange={handleSelectionChange}
|
||||||
|
allowsCustomValue={false}>
|
||||||
|
{children ?? defaultRenderItem}
|
||||||
|
</Autocomplete>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchableSelector
|
||||||
75
packages/ui/src/components/base/Selector/Selector.tsx
Normal file
75
packages/ui/src/components/base/Selector/Selector.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import type { Selection } from '@heroui/react'
|
||||||
|
import { Select, SelectItem } from '@heroui/react'
|
||||||
|
import type { Key } from '@react-types/shared'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
import type { SelectorItem, SelectorProps } from './types'
|
||||||
|
|
||||||
|
const Selector = <T extends SelectorItem>(props: SelectorProps<T>) => {
|
||||||
|
const { items, onSelectionChange, selectedKeys, selectionMode = 'single', children, ...rest } = props
|
||||||
|
|
||||||
|
// 转换 selectedKeys: V | V[] | undefined → Set<Key> | undefined
|
||||||
|
const heroUISelectedKeys = useMemo(() => {
|
||||||
|
if (selectedKeys === undefined) return undefined
|
||||||
|
|
||||||
|
if (selectionMode === 'multiple') {
|
||||||
|
// 多选模式: V[] → Set<Key>
|
||||||
|
return new Set((selectedKeys as T['value'][]).map((key) => String(key) as Key))
|
||||||
|
} else {
|
||||||
|
// 单选模式: V → Set<Key>
|
||||||
|
return new Set([String(selectedKeys) as Key])
|
||||||
|
}
|
||||||
|
}, [selectedKeys, selectionMode])
|
||||||
|
|
||||||
|
// 处理选择变化,转换 Selection → V | V[]
|
||||||
|
const handleSelectionChange = (keys: Selection) => {
|
||||||
|
if (!onSelectionChange) return
|
||||||
|
|
||||||
|
if (keys === 'all') {
|
||||||
|
// 如果是全选,返回所有非禁用项的值
|
||||||
|
const allValues = items.filter((item) => !item.disabled).map((item) => item.value)
|
||||||
|
if (selectionMode === 'multiple') {
|
||||||
|
;(onSelectionChange as (keys: T['value'][]) => void)(allValues)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换 Set<Key> 为原始类型
|
||||||
|
const keysArray = Array.from(keys).map((key) => {
|
||||||
|
const strKey = String(key)
|
||||||
|
// 尝试转换回数字类型(如果原始值是数字)
|
||||||
|
const num = Number(strKey)
|
||||||
|
return !isNaN(num) && items.some((item) => item.value === num) ? (num as T['value']) : (strKey as T['value'])
|
||||||
|
})
|
||||||
|
|
||||||
|
if (selectionMode === 'multiple') {
|
||||||
|
// 多选模式: 返回数组
|
||||||
|
;(onSelectionChange as (keys: T['value'][]) => void)(keysArray)
|
||||||
|
} else {
|
||||||
|
// 单选模式: 返回单个值
|
||||||
|
if (keysArray.length > 0) {
|
||||||
|
;(onSelectionChange as (key: T['value']) => void)(keysArray[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认渲染函数
|
||||||
|
const defaultRenderItem = (item: T) => (
|
||||||
|
<SelectItem key={String(item.value)} textValue={item.label ? String(item.label) : String(item.value)}>
|
||||||
|
{item.label ?? item.value}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
{...rest}
|
||||||
|
items={items}
|
||||||
|
selectionMode={selectionMode}
|
||||||
|
selectedKeys={heroUISelectedKeys as 'all' | Iterable<Key> | undefined}
|
||||||
|
onSelectionChange={handleSelectionChange}>
|
||||||
|
{children ?? defaultRenderItem}
|
||||||
|
</Select>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Selector
|
||||||
13
packages/ui/src/components/base/Selector/index.tsx
Normal file
13
packages/ui/src/components/base/Selector/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// 统一导出 Selector 相关组件和类型
|
||||||
|
export { default as SearchableSelector } from './SearchableSelector'
|
||||||
|
export { default } from './Selector'
|
||||||
|
export type {
|
||||||
|
MultipleSearchableSelectorProps,
|
||||||
|
MultipleSelectorProps,
|
||||||
|
SearchableSelectorItem,
|
||||||
|
SearchableSelectorProps,
|
||||||
|
SelectorItem,
|
||||||
|
SelectorProps,
|
||||||
|
SingleSearchableSelectorProps,
|
||||||
|
SingleSelectorProps
|
||||||
|
} from './types'
|
||||||
79
packages/ui/src/components/base/Selector/types.ts
Normal file
79
packages/ui/src/components/base/Selector/types.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import type { AutocompleteProps, SelectProps } from '@heroui/react'
|
||||||
|
import type { ReactElement, ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface SelectorItem<V = string | number> {
|
||||||
|
label?: string | ReactNode
|
||||||
|
value: V
|
||||||
|
disabled?: boolean
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义渲染函数类型
|
||||||
|
type SelectorRenderItem<T> = (item: T) => ReactElement
|
||||||
|
|
||||||
|
// 单选模式的 Props
|
||||||
|
interface SingleSelectorProps<T extends SelectorItem = SelectorItem>
|
||||||
|
extends Omit<SelectProps<T>, 'children' | 'onSelectionChange' | 'selectedKeys' | 'selectionMode'> {
|
||||||
|
items: T[]
|
||||||
|
selectionMode?: 'single'
|
||||||
|
selectedKeys?: T['value']
|
||||||
|
onSelectionChange?: (key: T['value']) => void
|
||||||
|
children?: SelectorRenderItem<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多选模式的 Props
|
||||||
|
interface MultipleSelectorProps<T extends SelectorItem = SelectorItem>
|
||||||
|
extends Omit<SelectProps<T>, 'children' | 'onSelectionChange' | 'selectedKeys' | 'selectionMode'> {
|
||||||
|
items: T[]
|
||||||
|
selectionMode: 'multiple'
|
||||||
|
selectedKeys?: T['value'][]
|
||||||
|
onSelectionChange?: (keys: T['value'][]) => void
|
||||||
|
children?: SelectorRenderItem<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectorProps<T extends SelectorItem = SelectorItem> = SingleSelectorProps<T> | MultipleSelectorProps<T>
|
||||||
|
|
||||||
|
interface SearchableSelectorItem<V = string | number> {
|
||||||
|
label?: string | ReactNode
|
||||||
|
value: V
|
||||||
|
disabled?: boolean
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义渲染函数类型
|
||||||
|
type SearchableRenderItem<T> = (item: T) => ReactElement
|
||||||
|
|
||||||
|
// 单选模式的 Props
|
||||||
|
interface SingleSearchableSelectorProps<T extends SearchableSelectorItem = SearchableSelectorItem>
|
||||||
|
extends Omit<AutocompleteProps<T>, 'children' | 'onSelectionChange' | 'selectedKey' | 'selectionMode'> {
|
||||||
|
items: T[]
|
||||||
|
selectionMode?: 'single'
|
||||||
|
selectedKeys?: T['value']
|
||||||
|
onSelectionChange?: (key: T['value']) => void
|
||||||
|
children?: SearchableRenderItem<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多选模式的 Props
|
||||||
|
interface MultipleSearchableSelectorProps<T extends SearchableSelectorItem = SearchableSelectorItem>
|
||||||
|
extends Omit<AutocompleteProps<T>, 'children' | 'onSelectionChange' | 'selectedKey' | 'selectionMode'> {
|
||||||
|
items: T[]
|
||||||
|
selectionMode: 'multiple'
|
||||||
|
selectedKeys?: T['value'][]
|
||||||
|
onSelectionChange?: (keys: T['value'][]) => void
|
||||||
|
children?: SearchableRenderItem<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchableSelectorProps<T extends SearchableSelectorItem = SearchableSelectorItem> =
|
||||||
|
| SingleSearchableSelectorProps<T>
|
||||||
|
| MultipleSearchableSelectorProps<T>
|
||||||
|
|
||||||
|
export type {
|
||||||
|
MultipleSearchableSelectorProps,
|
||||||
|
MultipleSelectorProps,
|
||||||
|
SearchableSelectorItem,
|
||||||
|
SearchableSelectorProps,
|
||||||
|
SelectorItem,
|
||||||
|
SelectorProps,
|
||||||
|
SingleSearchableSelectorProps,
|
||||||
|
SingleSelectorProps
|
||||||
|
}
|
||||||
37
packages/ui/src/components/base/Spinner/index.tsx
Normal file
37
packages/ui/src/components/base/Spinner/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Original: src/renderer/src/components/Spinner.tsx
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Search } from 'lucide-react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define variants for the spinner animation
|
||||||
|
const spinnerVariants = {
|
||||||
|
defaultColor: {
|
||||||
|
color: '#2a2a2a'
|
||||||
|
},
|
||||||
|
dimmed: {
|
||||||
|
color: '#8C9296'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Spinner({ text, className = '' }: Props) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={`flex items-center gap-1 p-0 ${className}`}
|
||||||
|
variants={spinnerVariants}
|
||||||
|
initial="defaultColor"
|
||||||
|
animate={['defaultColor', 'dimmed']}
|
||||||
|
transition={{
|
||||||
|
duration: 0.8,
|
||||||
|
repeat: Infinity,
|
||||||
|
repeatType: 'reverse',
|
||||||
|
ease: 'easeInOut'
|
||||||
|
}}>
|
||||||
|
<Search size={16} style={{ color: 'unset' }} />
|
||||||
|
<span>{text}</span>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
53
packages/ui/src/components/base/StatusTag/index.tsx
Normal file
53
packages/ui/src/components/base/StatusTag/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { LucideIcon } from 'lucide-react'
|
||||||
|
import { AlertTriangleIcon, CheckIcon, CircleXIcon, InfoIcon } from 'lucide-react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import CustomTag from '../CustomTag'
|
||||||
|
|
||||||
|
export type StatusType = 'success' | 'error' | 'warning' | 'info'
|
||||||
|
|
||||||
|
export interface StatusTagProps {
|
||||||
|
type: StatusType
|
||||||
|
message: string
|
||||||
|
iconSize?: number
|
||||||
|
icon?: React.ReactNode
|
||||||
|
color?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusConfig: Record<StatusType, { Icon: LucideIcon; color: string }> = {
|
||||||
|
success: { Icon: CheckIcon, color: '#10B981' }, // green-500
|
||||||
|
error: { Icon: CircleXIcon, color: '#EF4444' }, // red-500
|
||||||
|
warning: { Icon: AlertTriangleIcon, color: '#F59E0B' }, // amber-500
|
||||||
|
info: { Icon: InfoIcon, color: '#3B82F6' } // blue-500
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StatusTag: React.FC<StatusTagProps> = ({ type, message, iconSize = 14, icon, color, className }) => {
|
||||||
|
const config = statusConfig[type]
|
||||||
|
const Icon = config.Icon
|
||||||
|
const finalColor = color || config.color
|
||||||
|
const finalIcon = icon || <Icon size={iconSize} color={finalColor} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomTag icon={finalIcon} color={finalColor} className={className}>
|
||||||
|
{message}
|
||||||
|
</CustomTag>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留原有的导出以保持向后兼容
|
||||||
|
export const SuccessTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
|
||||||
|
<StatusTag type="success" iconSize={iconSize} message={message} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export const ErrorTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
|
||||||
|
<StatusTag type="error" iconSize={iconSize} message={message} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export const WarnTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
|
||||||
|
<StatusTag type="warning" iconSize={iconSize} message={message} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export const InfoTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
|
||||||
|
<StatusTag type="info" iconSize={iconSize} message={message} />
|
||||||
|
)
|
||||||
54
packages/ui/src/components/base/Switch/index.tsx
Normal file
54
packages/ui/src/components/base/Switch/index.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { SwitchProps } from '@heroui/react'
|
||||||
|
import { cn, Spinner, Switch } from '@heroui/react'
|
||||||
|
|
||||||
|
// Enhanced Switch component with loading state support
|
||||||
|
interface CustomSwitchProps extends SwitchProps {
|
||||||
|
isLoading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A customized Switch component based on HeroUI Switch
|
||||||
|
* @see https://www.heroui.com/docs/components/switch#api
|
||||||
|
* @param isLoading When true, displays a loading spinner in the switch thumb
|
||||||
|
*/
|
||||||
|
const CustomizedSwitch = ({ isLoading, children, ref, thumbIcon, ...props }: CustomSwitchProps) => {
|
||||||
|
const finalThumbIcon = isLoading ? <Spinner size="sm" /> : thumbIcon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch ref={ref} {...props} thumbIcon={finalThumbIcon}>
|
||||||
|
{children}
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DescriptionSwitch = ({ children, ...props }: CustomSwitchProps) => {
|
||||||
|
return (
|
||||||
|
<CustomizedSwitch
|
||||||
|
size="sm"
|
||||||
|
classNames={{
|
||||||
|
base: cn(
|
||||||
|
'inline-flex w-full max-w-md flex-row-reverse items-center hover:bg-content2',
|
||||||
|
'cursor-pointer justify-between gap-2 rounded-lg border-2 border-transparent py-2 pr-1',
|
||||||
|
'data-[selected=true]:border-primary'
|
||||||
|
),
|
||||||
|
wrapper: 'p-0 h-4 overflow-visible',
|
||||||
|
thumb: cn(
|
||||||
|
'h-6 w-6 border-2 shadow-lg',
|
||||||
|
'group-data-[hover=true]:border-primary',
|
||||||
|
//selected
|
||||||
|
'group-data-[selected=true]:ms-6',
|
||||||
|
// pressed
|
||||||
|
'group-data-[pressed=true]:w-7',
|
||||||
|
'group-data-pressed:group-data-selected:ms-4'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
{...props}>
|
||||||
|
{children}
|
||||||
|
</CustomizedSwitch>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomizedSwitch.displayName = 'Switch'
|
||||||
|
|
||||||
|
export { DescriptionSwitch, CustomizedSwitch as Switch }
|
||||||
|
export type { CustomSwitchProps as SwitchProps }
|
||||||
20
packages/ui/src/components/base/TextBadge/index.tsx
Normal file
20
packages/ui/src/components/base/TextBadge/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Original: src/renderer/src/components/TextBadge.tsx
|
||||||
|
import type { FC } from 'react'
|
||||||
|
|
||||||
|
interface TextBadgeProps {
|
||||||
|
text: string
|
||||||
|
style?: React.CSSProperties
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextBadge: FC<TextBadgeProps> = ({ text, style, className = '' }) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`text-xs text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/30 px-1.5 py-0.5 rounded font-medium ${className}`}
|
||||||
|
style={style}>
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextBadge
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast'
|
import { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast'
|
||||||
import { RequireSome } from '@renderer/types'
|
|
||||||
|
import type { RequireSome } from '@/types'
|
||||||
|
|
||||||
type AddToastProps = Parameters<typeof addToast>[0]
|
type AddToastProps = Parameters<typeof addToast>[0]
|
||||||
type ToastPropsColored = Omit<AddToastProps, 'color'>
|
type ToastPropsColored = Omit<AddToastProps, 'color'>
|
||||||
@@ -21,35 +22,35 @@ const createToast = (color: 'danger' | 'success' | 'warning' | 'default') => {
|
|||||||
* @param arg - Toast content (string) or toast options object
|
* @param arg - Toast content (string) or toast options object
|
||||||
* @returns Toast ID or null
|
* @returns Toast ID or null
|
||||||
*/
|
*/
|
||||||
export const error = createToast('danger')
|
const error = createToast('danger')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a success toast notification with green color
|
* Display a success toast notification with green color
|
||||||
* @param arg - Toast content (string) or toast options object
|
* @param arg - Toast content (string) or toast options object
|
||||||
* @returns Toast ID or null
|
* @returns Toast ID or null
|
||||||
*/
|
*/
|
||||||
export const success = createToast('success')
|
const success = createToast('success')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a warning toast notification with yellow color
|
* Display a warning toast notification with yellow color
|
||||||
* @param arg - Toast content (string) or toast options object
|
* @param arg - Toast content (string) or toast options object
|
||||||
* @returns Toast ID or null
|
* @returns Toast ID or null
|
||||||
*/
|
*/
|
||||||
export const warning = createToast('warning')
|
const warning = createToast('warning')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display an info toast notification with default color
|
* Display an info toast notification with default color
|
||||||
* @param arg - Toast content (string) or toast options object
|
* @param arg - Toast content (string) or toast options object
|
||||||
* @returns Toast ID or null
|
* @returns Toast ID or null
|
||||||
*/
|
*/
|
||||||
export const info = createToast('default')
|
const info = createToast('default')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a loading toast notification that resolves with a promise
|
* Display a loading toast notification that resolves with a promise
|
||||||
* @param args - Toast options object containing a promise to resolve
|
* @param args - Toast options object containing a promise to resolve
|
||||||
* @returns Toast ID or null
|
* @returns Toast ID or null
|
||||||
*/
|
*/
|
||||||
export const loading = (args: RequireSome<AddToastProps, 'promise'>) => {
|
const loading = (args: RequireSome<AddToastProps, 'promise'>) => {
|
||||||
// Disappear immediately by default
|
// Disappear immediately by default
|
||||||
if (args.timeout === undefined) {
|
if (args.timeout === undefined) {
|
||||||
args.timeout = 1
|
args.timeout = 1
|
||||||
@@ -57,7 +58,20 @@ export const loading = (args: RequireSome<AddToastProps, 'promise'>) => {
|
|||||||
return addToast(args)
|
return addToast(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getToastUtilities = () =>
|
export type ToastUtilities = {
|
||||||
|
getToastQueue: typeof getToastQueue
|
||||||
|
addToast: typeof addToast
|
||||||
|
closeToast: typeof closeToast
|
||||||
|
closeAll: typeof closeAll
|
||||||
|
isToastClosing: typeof isToastClosing
|
||||||
|
error: typeof error
|
||||||
|
success: typeof success
|
||||||
|
warning: typeof warning
|
||||||
|
info: typeof info
|
||||||
|
loading: typeof loading
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getToastUtilities = (): ToastUtilities =>
|
||||||
({
|
({
|
||||||
getToastQueue,
|
getToastQueue,
|
||||||
addToast,
|
addToast,
|
||||||
34
packages/ui/src/components/base/Tooltip/index.tsx
Normal file
34
packages/ui/src/components/base/Tooltip/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { TooltipProps as HeroUITooltipProps } from '@heroui/react'
|
||||||
|
import { cn, Tooltip as HeroUITooltip } from '@heroui/react'
|
||||||
|
|
||||||
|
export interface TooltipProps extends HeroUITooltipProps {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tooltip wrapper that applies consistent styling and arrow display.
|
||||||
|
* Differences from raw HeroUI Tooltip:
|
||||||
|
* 1. Defaults showArrow={true}
|
||||||
|
* 2. Merges a default max-w-60 class into the content slot, capping width at 240px.
|
||||||
|
* All other HeroUI Tooltip props/behaviors remain unchanged.
|
||||||
|
*
|
||||||
|
* @see https://www.heroui.com/docs/components/tooltip
|
||||||
|
*/
|
||||||
|
export const Tooltip = ({
|
||||||
|
children,
|
||||||
|
classNames,
|
||||||
|
showArrow,
|
||||||
|
...rest
|
||||||
|
}: Omit<TooltipProps, 'classNames'> & {
|
||||||
|
classNames?: TooltipProps['classNames'] & { placeholder?: string }
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<HeroUITooltip
|
||||||
|
classNames={{
|
||||||
|
...classNames,
|
||||||
|
content: cn('max-w-60', classNames?.content)
|
||||||
|
}}
|
||||||
|
showArrow={showArrow ?? true}
|
||||||
|
{...rest}>
|
||||||
|
<div className={cn('relative z-10 inline-block', classNames?.placeholder)}>{children}</div>
|
||||||
|
</HeroUITooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
28
packages/ui/src/components/display/Ellipsis/index.tsx
Normal file
28
packages/ui/src/components/display/Ellipsis/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Original: src/renderer/src/components/Ellipsis/index.tsx
|
||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
import { cn } from '../../../utils'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
maxLine?: number
|
||||||
|
className?: string
|
||||||
|
ref?: React.Ref<HTMLDivElement>
|
||||||
|
} & HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
const Ellipsis = (props: Props) => {
|
||||||
|
const { maxLine = 1, children, className, ref, ...rest } = props
|
||||||
|
|
||||||
|
const ellipsisClasses = cn(
|
||||||
|
'overflow-hidden text-ellipsis',
|
||||||
|
maxLine > 1 ? `line-clamp-${maxLine} break-words` : 'block whitespace-nowrap',
|
||||||
|
className
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={ellipsisClasses} {...rest}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Ellipsis
|
||||||
50
packages/ui/src/components/display/ExpandableText/index.tsx
Normal file
50
packages/ui/src/components/display/ExpandableText/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Original: src/renderer/src/components/ExpandableText.tsx
|
||||||
|
import { Button } from '@heroui/react'
|
||||||
|
import { memo, useCallback, useState } from 'react'
|
||||||
|
|
||||||
|
interface ExpandableTextProps {
|
||||||
|
text: string
|
||||||
|
style?: React.CSSProperties
|
||||||
|
className?: string
|
||||||
|
expandText?: string
|
||||||
|
collapseText?: string
|
||||||
|
lineClamp?: number
|
||||||
|
ref?: React.RefObject<HTMLDivElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExpandableText = ({
|
||||||
|
text,
|
||||||
|
style,
|
||||||
|
className = '',
|
||||||
|
expandText = 'Expand',
|
||||||
|
collapseText = 'Collapse',
|
||||||
|
lineClamp = 1,
|
||||||
|
ref
|
||||||
|
}: ExpandableTextProps) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false)
|
||||||
|
|
||||||
|
const toggleExpand = useCallback(() => {
|
||||||
|
setIsExpanded((prev) => !prev)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={`flex ${isExpanded ? 'flex-col' : 'flex-row items-center'} gap-2 ${className}`}
|
||||||
|
style={style}>
|
||||||
|
<div
|
||||||
|
className={`overflow-hidden ${
|
||||||
|
isExpanded ? '' : lineClamp === 1 ? 'text-ellipsis whitespace-nowrap' : `line-clamp-${lineClamp}`
|
||||||
|
} ${isExpanded ? '' : 'flex-1'}`}>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
<Button size="sm" variant="light" color="primary" onClick={toggleExpand} className="min-w-fit px-2">
|
||||||
|
{isExpanded ? collapseText : expandText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpandableText.displayName = 'ExpandableText'
|
||||||
|
|
||||||
|
export default memo(ExpandableText)
|
||||||
61
packages/ui/src/components/display/ListItem/index.tsx
Normal file
61
packages/ui/src/components/display/ListItem/index.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// Original path: src/renderer/src/components/ListItem/index.tsx
|
||||||
|
import { Tooltip } from '@heroui/react'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import { cn } from '../../../utils'
|
||||||
|
|
||||||
|
interface ListItemProps {
|
||||||
|
active?: boolean
|
||||||
|
icon?: ReactNode
|
||||||
|
title: ReactNode
|
||||||
|
subtitle?: string
|
||||||
|
titleStyle?: React.CSSProperties
|
||||||
|
onClick?: () => void
|
||||||
|
rightContent?: ReactNode
|
||||||
|
style?: React.CSSProperties
|
||||||
|
className?: string
|
||||||
|
ref?: React.Ref<HTMLDivElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListItem = ({
|
||||||
|
active,
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
titleStyle,
|
||||||
|
onClick,
|
||||||
|
rightContent,
|
||||||
|
style,
|
||||||
|
className,
|
||||||
|
ref
|
||||||
|
}: ListItemProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'px-3 py-1.5 rounded-md text-xs flex flex-col justify-between relative cursor-pointer border border-transparent',
|
||||||
|
'hover:bg-gray-50 dark:hover:bg-gray-800',
|
||||||
|
active && 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
style={style}>
|
||||||
|
<div className="flex items-center gap-0.5 overflow-hidden text-xs">
|
||||||
|
{icon && <span className="flex items-center justify-center mr-2">{icon}</span>}
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<Tooltip content={title}>
|
||||||
|
<div className="truncate text-gray-900 dark:text-gray-100" style={titleStyle}>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
{subtitle && (
|
||||||
|
<div className="text-[10px] text-gray-500 dark:text-gray-400 mt-0.5 line-clamp-1">{subtitle}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{rightContent && <div className="ml-auto">{rightContent}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListItem
|
||||||
23
packages/ui/src/components/display/MaxContextCount/index.tsx
Normal file
23
packages/ui/src/components/display/MaxContextCount/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Original path: src/renderer/src/components/MaxContextCount.tsx
|
||||||
|
import { Infinity as InfinityIcon } from 'lucide-react'
|
||||||
|
import type { CSSProperties } from 'react'
|
||||||
|
|
||||||
|
const MAX_CONTEXT_COUNT = 100
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
maxContext: number
|
||||||
|
style?: CSSProperties
|
||||||
|
size?: number
|
||||||
|
className?: string
|
||||||
|
ref?: React.Ref<HTMLSpanElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MaxContextCount({ maxContext, style, size = 14, className, ref }: Props) {
|
||||||
|
return maxContext === MAX_CONTEXT_COUNT ? (
|
||||||
|
<InfinityIcon size={size} style={style} className={className} aria-label="infinity" />
|
||||||
|
) : (
|
||||||
|
<span ref={ref} style={style} className={className}>
|
||||||
|
{maxContext.toString()}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { Variants } from 'motion/react'
|
||||||
|
export const lightbulbVariants: Variants = {
|
||||||
|
active: {
|
||||||
|
opacity: [1, 0.2, 1],
|
||||||
|
transition: {
|
||||||
|
duration: 1.2,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
times: [0, 0.5, 1],
|
||||||
|
repeat: Infinity
|
||||||
|
}
|
||||||
|
},
|
||||||
|
idle: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'easeInOut'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const lightbulbSoftVariants: Variants = {
|
||||||
|
active: {
|
||||||
|
opacity: [1, 0.5, 1],
|
||||||
|
transition: {
|
||||||
|
duration: 2,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
times: [0, 0.5, 1],
|
||||||
|
repeat: Infinity
|
||||||
|
}
|
||||||
|
},
|
||||||
|
idle: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'easeInOut'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
128
packages/ui/src/components/display/ThinkingEffect/index.tsx
Normal file
128
packages/ui/src/components/display/ThinkingEffect/index.tsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
// Original path: src/renderer/src/components/ThinkingEffect.tsx
|
||||||
|
import { isEqual } from 'lodash'
|
||||||
|
import { ChevronRight, Lightbulb } from 'lucide-react'
|
||||||
|
import { motion } from 'motion/react'
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
|
import { cn } from '../../../utils'
|
||||||
|
import { lightbulbVariants } from './defaultVariants'
|
||||||
|
|
||||||
|
interface ThinkingEffectProps {
|
||||||
|
isThinking: boolean
|
||||||
|
thinkingTimeText: React.ReactNode
|
||||||
|
content: string
|
||||||
|
expanded: boolean
|
||||||
|
className?: string
|
||||||
|
ref?: React.Ref<HTMLDivElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThinkingEffect: React.FC<ThinkingEffectProps> = ({
|
||||||
|
isThinking,
|
||||||
|
thinkingTimeText,
|
||||||
|
content,
|
||||||
|
expanded,
|
||||||
|
className,
|
||||||
|
ref
|
||||||
|
}) => {
|
||||||
|
const [messages, setMessages] = useState<string[]>([])
|
||||||
|
useEffect(() => {
|
||||||
|
const allLines = (content || '').split('\n')
|
||||||
|
const newMessages = isThinking ? allLines.slice(0, -1) : allLines
|
||||||
|
const validMessages = newMessages.filter((line) => line.trim() !== '')
|
||||||
|
|
||||||
|
if (!isEqual(messages, validMessages)) {
|
||||||
|
setMessages(validMessages)
|
||||||
|
}
|
||||||
|
}, [content, isThinking, messages])
|
||||||
|
|
||||||
|
const showThinking = useMemo(() => {
|
||||||
|
return isThinking && !expanded
|
||||||
|
}, [expanded, isThinking])
|
||||||
|
|
||||||
|
const LINE_HEIGHT = 14
|
||||||
|
|
||||||
|
const containerHeight = useMemo(() => {
|
||||||
|
if (!showThinking || messages.length < 1) return 38
|
||||||
|
return Math.min(75, Math.max(messages.length + 1, 2) * LINE_HEIGHT + 25)
|
||||||
|
}, [showThinking, messages.length])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={{ height: containerHeight }}
|
||||||
|
className={cn(
|
||||||
|
'w-full rounded-xl overflow-hidden relative flex items-center border-0.5 border-gray-200 dark:border-gray-700 transition-all duration-150 pointer-events-none select-none',
|
||||||
|
expanded && 'rounded-b-none',
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
<div className="w-12 flex justify-center items-center h-full flex-shrink-0 relative pl-1.5 transition-all duration-150">
|
||||||
|
<motion.div
|
||||||
|
variants={lightbulbVariants}
|
||||||
|
animate={isThinking ? 'active' : 'idle'}
|
||||||
|
initial="idle"
|
||||||
|
className="flex justify-center items-center">
|
||||||
|
<Lightbulb
|
||||||
|
size={!showThinking || messages.length < 2 ? 20 : 30}
|
||||||
|
style={{ transition: 'width,height, 150ms' }}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 h-full py-1.5 overflow-hidden relative">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute inset-x-0 top-0 text-sm leading-3.5 font-medium py-2.5 z-50 transition-all duration-150',
|
||||||
|
(!showThinking || !messages.length) && 'pt-3'
|
||||||
|
)}>
|
||||||
|
{thinkingTimeText}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showThinking && (
|
||||||
|
<div
|
||||||
|
className="w-full h-full relative"
|
||||||
|
style={{
|
||||||
|
mask: 'linear-gradient(to bottom, rgb(0 0 0 / 0%) 0%, rgb(0 0 0 / 0%) 35%, rgb(0 0 0 / 25%) 40%, rgb(0 0 0 / 100%) 90%, rgb(0 0 0 / 100%) 100%)'
|
||||||
|
}}>
|
||||||
|
<motion.div
|
||||||
|
className="w-full absolute top-full flex flex-col justify-end"
|
||||||
|
style={{
|
||||||
|
height: messages.length * LINE_HEIGHT
|
||||||
|
}}
|
||||||
|
initial={{
|
||||||
|
y: -2
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
y: -messages.length * LINE_HEIGHT - 2
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 0.15,
|
||||||
|
ease: 'linear'
|
||||||
|
}}>
|
||||||
|
{messages.map((message, index) => {
|
||||||
|
if (index < messages.length - 5) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="w-full leading-3.5 text-xs text-gray-600 dark:text-gray-300 whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-10 flex justify-center items-center h-full flex-shrink-0 relative text-gray-400 dark:text-gray-500 transition-transform duration-150',
|
||||||
|
expanded && 'rotate-90'
|
||||||
|
)}>
|
||||||
|
<ChevronRight size={20} strokeWidth={1} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThinkingEffect
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user