mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
feat: Support Cherry Studio as a Service (CSaaS) (#8098)
This commit is contained in:
parent
47c909dda4
commit
d0b2f18d9a
105
CLAUDE.md
Normal file
105
CLAUDE.md
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
- **Prerequisites**: Node.js v20.x.x, Yarn 4.6.0
|
||||||
|
- **Setup Yarn**: `corepack enable && corepack prepare yarn@4.6.0 --activate`
|
||||||
|
- **Install Dependencies**: `yarn install`
|
||||||
|
|
||||||
|
### Development
|
||||||
|
- **Start Development**: `yarn dev` - Runs Electron app in development mode
|
||||||
|
- **Debug Mode**: `yarn debug` - Starts with debugging enabled, use chrome://inspect
|
||||||
|
|
||||||
|
### Testing & Quality
|
||||||
|
- **Run Tests**: `yarn test` - Runs all tests (Vitest)
|
||||||
|
- **Run E2E Tests**: `yarn test:e2e` - Playwright end-to-end tests
|
||||||
|
- **Type Check**: `yarn typecheck` - Checks TypeScript for both node and web
|
||||||
|
- **Lint**: `yarn lint` - ESLint with auto-fix
|
||||||
|
- **Format**: `yarn format` - Prettier formatting
|
||||||
|
|
||||||
|
### Build & Release
|
||||||
|
- **Build**: `yarn build` - Builds for production (includes typecheck)
|
||||||
|
- **Platform-specific builds**:
|
||||||
|
- Windows: `yarn build:win`
|
||||||
|
- macOS: `yarn build:mac`
|
||||||
|
- Linux: `yarn build:linux`
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Electron Multi-Process Architecture
|
||||||
|
- **Main Process** (`src/main/`): Node.js backend handling system integration, file operations, and services
|
||||||
|
- **Renderer Process** (`src/renderer/`): React-based UI running in Chromium
|
||||||
|
- **Preload Scripts** (`src/preload/`): Secure bridge between main and renderer processes
|
||||||
|
|
||||||
|
### Key Architectural Components
|
||||||
|
|
||||||
|
#### Main Process Services (`src/main/services/`)
|
||||||
|
- **MCPService**: Model Context Protocol server management
|
||||||
|
- **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
|
||||||
|
|
||||||
|
#### State Management (`src/renderer/src/store/`)
|
||||||
|
- **Redux Toolkit**: Centralized state management
|
||||||
|
- **Persistent Storage**: Redux-persist for data persistence
|
||||||
|
- **Thunks**: Async actions for complex operations
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
- **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
|
||||||
|
|
||||||
|
## Logging Standards
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
```typescript
|
||||||
|
// Main process
|
||||||
|
import { loggerService } from '@logger'
|
||||||
|
const logger = loggerService.withContext('moduleName')
|
||||||
|
|
||||||
|
// Renderer process (set window source first)
|
||||||
|
loggerService.initWindowSource('windowName')
|
||||||
|
const logger = loggerService.withContext('moduleName')
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
logger.info('message', CONTEXT)
|
||||||
|
logger.error('message', new Error('error'), CONTEXT)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Levels (highest to lowest)
|
||||||
|
- `error` - Critical errors causing crash/unusable functionality
|
||||||
|
- `warn` - Potential issues that don't affect core functionality
|
||||||
|
- `info` - Application lifecycle and key user actions
|
||||||
|
- `verbose` - Detailed flow information for feature tracing
|
||||||
|
- `debug` - Development diagnostic info (not for production)
|
||||||
|
- `silly` - Extreme debugging, low-level information
|
||||||
10
package.json
10
package.json
@ -81,6 +81,8 @@
|
|||||||
"os-proxy-config": "^1.1.2",
|
"os-proxy-config": "^1.1.2",
|
||||||
"pdfjs-dist": "4.10.38",
|
"pdfjs-dist": "4.10.38",
|
||||||
"selection-hook": "^1.0.8",
|
"selection-hook": "^1.0.8",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"swagger-ui-express": "^5.0.1",
|
||||||
"turndown": "7.2.0"
|
"turndown": "7.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -120,7 +122,7 @@
|
|||||||
"@langchain/community": "^0.3.36",
|
"@langchain/community": "^0.3.36",
|
||||||
"@langchain/ollama": "^0.2.1",
|
"@langchain/ollama": "^0.2.1",
|
||||||
"@mistralai/mistralai": "^1.7.5",
|
"@mistralai/mistralai": "^1.7.5",
|
||||||
"@modelcontextprotocol/sdk": "^1.12.3",
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
||||||
"@mozilla/readability": "^0.6.0",
|
"@mozilla/readability": "^0.6.0",
|
||||||
"@notionhq/client": "^2.2.15",
|
"@notionhq/client": "^2.2.15",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
@ -141,7 +143,10 @@
|
|||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@tryfabric/martian": "^1.2.4",
|
"@tryfabric/martian": "^1.2.4",
|
||||||
"@types/cli-progress": "^3",
|
"@types/cli-progress": "^3",
|
||||||
|
"@types/content-type": "^1.1.9",
|
||||||
|
"@types/cors": "^2.8.19",
|
||||||
"@types/diff": "^7",
|
"@types/diff": "^7",
|
||||||
|
"@types/express": "^5",
|
||||||
"@types/fs-extra": "^11",
|
"@types/fs-extra": "^11",
|
||||||
"@types/lodash": "^4.17.5",
|
"@types/lodash": "^4.17.5",
|
||||||
"@types/markdown-it": "^14",
|
"@types/markdown-it": "^14",
|
||||||
@ -152,6 +157,8 @@
|
|||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||||
"@types/react-window": "^1",
|
"@types/react-window": "^1",
|
||||||
|
"@types/swagger-jsdoc": "^6",
|
||||||
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"@types/tinycolor2": "^1",
|
"@types/tinycolor2": "^1",
|
||||||
"@types/word-extractor": "^1",
|
"@types/word-extractor": "^1",
|
||||||
"@uiw/codemirror-extensions-langs": "^4.23.14",
|
"@uiw/codemirror-extensions-langs": "^4.23.14",
|
||||||
@ -195,6 +202,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
|
"express": "^5.1.0",
|
||||||
"fast-diff": "^1.3.0",
|
"fast-diff": "^1.3.0",
|
||||||
"fast-xml-parser": "^5.2.0",
|
"fast-xml-parser": "^5.2.0",
|
||||||
"fetch-socks": "1.3.2",
|
"fetch-socks": "1.3.2",
|
||||||
|
|||||||
@ -273,5 +273,11 @@ export enum IpcChannel {
|
|||||||
TRACE_SET_TITLE = 'trace:setTitle',
|
TRACE_SET_TITLE = 'trace:setTitle',
|
||||||
TRACE_ADD_END_MESSAGE = 'trace:addEndMessage',
|
TRACE_ADD_END_MESSAGE = 'trace:addEndMessage',
|
||||||
TRACE_CLEAN_LOCAL_DATA = 'trace:cleanLocalData',
|
TRACE_CLEAN_LOCAL_DATA = 'trace:cleanLocalData',
|
||||||
TRACE_ADD_STREAM_MESSAGE = 'trace:addStreamMessage'
|
TRACE_ADD_STREAM_MESSAGE = 'trace:addStreamMessage',
|
||||||
|
// API Server
|
||||||
|
ApiServer_Start = 'api-server:start',
|
||||||
|
ApiServer_Stop = 'api-server:stop',
|
||||||
|
ApiServer_Restart = 'api-server:restart',
|
||||||
|
ApiServer_GetStatus = 'api-server:get-status',
|
||||||
|
ApiServer_GetConfig = 'api-server:get-config'
|
||||||
}
|
}
|
||||||
|
|||||||
128
src/main/apiServer/app.ts
Normal file
128
src/main/apiServer/app.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { loggerService } from '@main/services/LoggerService'
|
||||||
|
import cors from 'cors'
|
||||||
|
import express from 'express'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
import { authMiddleware } from './middleware/auth'
|
||||||
|
import { errorHandler } from './middleware/error'
|
||||||
|
import { setupOpenAPIDocumentation } from './middleware/openapi'
|
||||||
|
import { chatRoutes } from './routes/chat'
|
||||||
|
import { mcpRoutes } from './routes/mcp'
|
||||||
|
import { modelsRoutes } from './routes/models'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('ApiServer')
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
// Global middleware
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const start = Date.now()
|
||||||
|
res.on('finish', () => {
|
||||||
|
const duration = Date.now() - start
|
||||||
|
logger.info(`${req.method} ${req.path} - ${res.statusCode} - ${duration}ms`)
|
||||||
|
})
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use((_req, res, next) => {
|
||||||
|
res.setHeader('X-Request-ID', uuidv4())
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
cors({
|
||||||
|
origin: '*',
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /health:
|
||||||
|
* get:
|
||||||
|
* summary: Health check endpoint
|
||||||
|
* description: Check server status (no authentication required)
|
||||||
|
* tags: [Health]
|
||||||
|
* security: []
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Server is healthy
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* example: ok
|
||||||
|
* timestamp:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* version:
|
||||||
|
* type: string
|
||||||
|
* example: 1.0.0
|
||||||
|
*/
|
||||||
|
app.get('/health', (_req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
version: process.env.npm_package_version || '1.0.0'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /:
|
||||||
|
* get:
|
||||||
|
* summary: API information
|
||||||
|
* description: Get basic API information and available endpoints
|
||||||
|
* tags: [General]
|
||||||
|
* security: []
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: API information
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* example: Cherry Studio API
|
||||||
|
* version:
|
||||||
|
* type: string
|
||||||
|
* example: 1.0.0
|
||||||
|
* endpoints:
|
||||||
|
* type: object
|
||||||
|
*/
|
||||||
|
app.get('/', (_req, res) => {
|
||||||
|
res.json({
|
||||||
|
name: 'Cherry Studio API',
|
||||||
|
version: '1.0.0',
|
||||||
|
endpoints: {
|
||||||
|
health: 'GET /health',
|
||||||
|
models: 'GET /v1/models',
|
||||||
|
chat: 'POST /v1/chat/completions',
|
||||||
|
mcp: 'GET /v1/mcps'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// API v1 routes with auth
|
||||||
|
const apiRouter = express.Router()
|
||||||
|
apiRouter.use(authMiddleware)
|
||||||
|
apiRouter.use(express.json())
|
||||||
|
// Mount routes
|
||||||
|
apiRouter.use('/chat', chatRoutes)
|
||||||
|
apiRouter.use('/mcps', mcpRoutes)
|
||||||
|
apiRouter.use('/models', modelsRoutes)
|
||||||
|
app.use('/v1', apiRouter)
|
||||||
|
|
||||||
|
// Setup OpenAPI documentation
|
||||||
|
setupOpenAPIDocumentation(app)
|
||||||
|
|
||||||
|
// Error handling (must be last)
|
||||||
|
app.use(errorHandler)
|
||||||
|
|
||||||
|
export { app }
|
||||||
64
src/main/apiServer/config.ts
Normal file
64
src/main/apiServer/config.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { ApiServerConfig } from '@types'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
import { loggerService } from '../services/LoggerService'
|
||||||
|
import { reduxService } from '../services/ReduxService'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('ApiServerConfig')
|
||||||
|
|
||||||
|
class ConfigManager {
|
||||||
|
private _config: ApiServerConfig | null = null
|
||||||
|
|
||||||
|
async load(): Promise<ApiServerConfig> {
|
||||||
|
try {
|
||||||
|
const settings = await reduxService.select('state.settings')
|
||||||
|
|
||||||
|
// Auto-generate API key if not set
|
||||||
|
if (!settings?.apiServer?.apiKey) {
|
||||||
|
const generatedKey = `cs-sk-${uuidv4()}`
|
||||||
|
await reduxService.dispatch({
|
||||||
|
type: 'settings/setApiServerApiKey',
|
||||||
|
payload: generatedKey
|
||||||
|
})
|
||||||
|
|
||||||
|
this._config = {
|
||||||
|
port: settings?.apiServer?.port ?? 23333,
|
||||||
|
host: 'localhost',
|
||||||
|
apiKey: generatedKey
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._config = {
|
||||||
|
port: settings?.apiServer?.port ?? 23333,
|
||||||
|
host: 'localhost',
|
||||||
|
apiKey: settings.apiServer.apiKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._config
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.warn('Failed to load config from Redux, using defaults:', error)
|
||||||
|
this._config = {
|
||||||
|
port: 23333,
|
||||||
|
host: 'localhost',
|
||||||
|
apiKey: `cs-sk-${uuidv4()}`
|
||||||
|
}
|
||||||
|
return this._config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(): Promise<ApiServerConfig> {
|
||||||
|
if (!this._config) {
|
||||||
|
await this.load()
|
||||||
|
}
|
||||||
|
if (!this._config) {
|
||||||
|
throw new Error('Failed to load API server configuration')
|
||||||
|
}
|
||||||
|
return this._config
|
||||||
|
}
|
||||||
|
|
||||||
|
async reload(): Promise<ApiServerConfig> {
|
||||||
|
return await this.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = new ConfigManager()
|
||||||
2
src/main/apiServer/index.ts
Normal file
2
src/main/apiServer/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { config } from './config'
|
||||||
|
export { apiServer } from './server'
|
||||||
25
src/main/apiServer/middleware/auth.ts
Normal file
25
src/main/apiServer/middleware/auth.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { NextFunction, Request, Response } from 'express'
|
||||||
|
|
||||||
|
import { config } from '../config'
|
||||||
|
|
||||||
|
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const auth = req.header('Authorization')
|
||||||
|
|
||||||
|
if (!auth || !auth.startsWith('Bearer ')) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = auth.slice(7) // Remove 'Bearer ' prefix
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized, Bearer token is empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { apiKey } = await config.get()
|
||||||
|
|
||||||
|
if (token !== apiKey) {
|
||||||
|
return res.status(403).json({ error: 'Forbidden' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
21
src/main/apiServer/middleware/error.ts
Normal file
21
src/main/apiServer/middleware/error.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { NextFunction, Request, Response } from 'express'
|
||||||
|
|
||||||
|
import { loggerService } from '../../services/LoggerService'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('ApiServerErrorHandler')
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
export const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||||
|
logger.error('API Server Error:', err)
|
||||||
|
|
||||||
|
// Don't expose internal errors in production
|
||||||
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
error: {
|
||||||
|
message: isDev ? err.message : 'Internal server error',
|
||||||
|
type: 'server_error',
|
||||||
|
...(isDev && { stack: err.stack })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
206
src/main/apiServer/middleware/openapi.ts
Normal file
206
src/main/apiServer/middleware/openapi.ts
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
|
import swaggerJSDoc from 'swagger-jsdoc'
|
||||||
|
import swaggerUi from 'swagger-ui-express'
|
||||||
|
|
||||||
|
import { loggerService } from '../../services/LoggerService'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('OpenAPIMiddleware')
|
||||||
|
|
||||||
|
const swaggerOptions: swaggerJSDoc.Options = {
|
||||||
|
definition: {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
info: {
|
||||||
|
title: 'Cherry Studio API',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'OpenAI-compatible API for Cherry Studio with additional Cherry-specific endpoints',
|
||||||
|
contact: {
|
||||||
|
name: 'Cherry Studio',
|
||||||
|
url: 'https://github.com/CherryHQ/cherry-studio'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: 'http://localhost:23333',
|
||||||
|
description: 'Local development server'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
BearerAuth: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT',
|
||||||
|
description: 'Use the API key from Cherry Studio settings'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
schemas: {
|
||||||
|
Error: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
error: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
message: { type: 'string' },
|
||||||
|
type: { type: 'string' },
|
||||||
|
code: { type: 'string' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ChatMessage: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
role: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['system', 'user', 'assistant', 'tool']
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
oneOf: [
|
||||||
|
{ type: 'string' },
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
type: { type: 'string' },
|
||||||
|
text: { type: 'string' },
|
||||||
|
image_url: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: { type: 'string' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
name: { type: 'string' },
|
||||||
|
tool_calls: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string' },
|
||||||
|
type: { type: 'string' },
|
||||||
|
function: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string' },
|
||||||
|
arguments: { type: 'string' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ChatCompletionRequest: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['model', 'messages'],
|
||||||
|
properties: {
|
||||||
|
model: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The model to use for completion, in format provider:model-id'
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
type: 'array',
|
||||||
|
items: { $ref: '#/components/schemas/ChatMessage' }
|
||||||
|
},
|
||||||
|
temperature: {
|
||||||
|
type: 'number',
|
||||||
|
minimum: 0,
|
||||||
|
maximum: 2,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
max_tokens: {
|
||||||
|
type: 'integer',
|
||||||
|
minimum: 1
|
||||||
|
},
|
||||||
|
stream: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
type: { type: 'string' },
|
||||||
|
function: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string' },
|
||||||
|
description: { type: 'string' },
|
||||||
|
parameters: { type: 'object' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Model: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string' },
|
||||||
|
object: { type: 'string', enum: ['model'] },
|
||||||
|
created: { type: 'integer' },
|
||||||
|
owned_by: { type: 'string' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MCPServer: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string' },
|
||||||
|
name: { type: 'string' },
|
||||||
|
command: { type: 'string' },
|
||||||
|
args: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' }
|
||||||
|
},
|
||||||
|
env: { type: 'object' },
|
||||||
|
disabled: { type: 'boolean' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
BearerAuth: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
apis: ['./src/main/apiServer/routes/*.ts', './src/main/apiServer/app.ts']
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupOpenAPIDocumentation(app: Express) {
|
||||||
|
try {
|
||||||
|
const specs = swaggerJSDoc(swaggerOptions)
|
||||||
|
|
||||||
|
// Serve OpenAPI JSON
|
||||||
|
app.get('/api-docs.json', (_req, res) => {
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
res.send(specs)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Serve Swagger UI
|
||||||
|
app.use(
|
||||||
|
'/api-docs',
|
||||||
|
swaggerUi.serve,
|
||||||
|
swaggerUi.setup(specs, {
|
||||||
|
customCss: `
|
||||||
|
.swagger-ui .topbar { display: none; }
|
||||||
|
.swagger-ui .info .title { color: #1890ff; }
|
||||||
|
`,
|
||||||
|
customSiteTitle: 'Cherry Studio API Documentation'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info('OpenAPI documentation setup complete')
|
||||||
|
logger.info('Documentation available at /api-docs')
|
||||||
|
logger.info('OpenAPI spec available at /api-docs.json')
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to setup OpenAPI documentation:', error as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
225
src/main/apiServer/routes/chat.ts
Normal file
225
src/main/apiServer/routes/chat.ts
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import express, { Request, Response } from 'express'
|
||||||
|
import OpenAI from 'openai'
|
||||||
|
import { ChatCompletionCreateParams } from 'openai/resources'
|
||||||
|
|
||||||
|
import { loggerService } from '../../services/LoggerService'
|
||||||
|
import { chatCompletionService } from '../services/chat-completion'
|
||||||
|
import { getProviderByModel, getRealProviderModel } from '../utils'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('ApiServerChatRoutes')
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /v1/chat/completions:
|
||||||
|
* post:
|
||||||
|
* summary: Create chat completion
|
||||||
|
* description: Create a chat completion response, compatible with OpenAI API
|
||||||
|
* tags: [Chat]
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/ChatCompletionRequest'
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Chat completion response
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: string
|
||||||
|
* object:
|
||||||
|
* type: string
|
||||||
|
* example: chat.completion
|
||||||
|
* created:
|
||||||
|
* type: integer
|
||||||
|
* model:
|
||||||
|
* type: string
|
||||||
|
* choices:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* index:
|
||||||
|
* type: integer
|
||||||
|
* message:
|
||||||
|
* $ref: '#/components/schemas/ChatMessage'
|
||||||
|
* finish_reason:
|
||||||
|
* type: string
|
||||||
|
* usage:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* prompt_tokens:
|
||||||
|
* type: integer
|
||||||
|
* completion_tokens:
|
||||||
|
* type: integer
|
||||||
|
* total_tokens:
|
||||||
|
* type: integer
|
||||||
|
* text/plain:
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Server-sent events stream (when stream=true)
|
||||||
|
* 400:
|
||||||
|
* description: Bad request
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 429:
|
||||||
|
* description: Rate limit exceeded
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
router.post('/completions', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const request: ChatCompletionCreateParams = req.body
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: {
|
||||||
|
message: 'Request body is required',
|
||||||
|
type: 'invalid_request_error',
|
||||||
|
code: 'missing_body'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Chat completion request:', {
|
||||||
|
model: request.model,
|
||||||
|
messageCount: request.messages?.length || 0,
|
||||||
|
stream: request.stream
|
||||||
|
})
|
||||||
|
|
||||||
|
// Validate request
|
||||||
|
const validation = chatCompletionService.validateRequest(request)
|
||||||
|
if (!validation.isValid) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: {
|
||||||
|
message: validation.errors.join('; '),
|
||||||
|
type: 'invalid_request_error',
|
||||||
|
code: 'validation_failed'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get provider
|
||||||
|
const provider = await getProviderByModel(request.model)
|
||||||
|
if (!provider) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: {
|
||||||
|
message: `Model "${request.model}" not found`,
|
||||||
|
type: 'invalid_request_error',
|
||||||
|
code: 'model_not_found'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate model availability
|
||||||
|
const modelId = getRealProviderModel(request.model)
|
||||||
|
const model = provider.models?.find((m) => m.id === modelId)
|
||||||
|
if (!model) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: {
|
||||||
|
message: `Model "${modelId}" not available in provider "${provider.id}"`,
|
||||||
|
type: 'invalid_request_error',
|
||||||
|
code: 'model_not_available'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create OpenAI client
|
||||||
|
const client = new OpenAI({
|
||||||
|
baseURL: provider.apiHost,
|
||||||
|
apiKey: provider.apiKey
|
||||||
|
})
|
||||||
|
request.model = modelId
|
||||||
|
|
||||||
|
// Handle streaming
|
||||||
|
if (request.stream) {
|
||||||
|
const streamResponse = await client.chat.completions.create(request)
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
||||||
|
res.setHeader('Cache-Control', 'no-cache')
|
||||||
|
res.setHeader('Connection', 'keep-alive')
|
||||||
|
|
||||||
|
try {
|
||||||
|
for await (const chunk of streamResponse as any) {
|
||||||
|
res.write(`data: ${JSON.stringify(chunk)}\n\n`)
|
||||||
|
}
|
||||||
|
res.write('data: [DONE]\n\n')
|
||||||
|
res.end()
|
||||||
|
} catch (streamError: any) {
|
||||||
|
logger.error('Stream error:', streamError)
|
||||||
|
res.write(
|
||||||
|
`data: ${JSON.stringify({
|
||||||
|
error: {
|
||||||
|
message: 'Stream processing error',
|
||||||
|
type: 'server_error',
|
||||||
|
code: 'stream_error'
|
||||||
|
}
|
||||||
|
})}\n\n`
|
||||||
|
)
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle non-streaming
|
||||||
|
const response = await client.chat.completions.create(request)
|
||||||
|
return res.json(response)
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Chat completion error:', error)
|
||||||
|
|
||||||
|
let statusCode = 500
|
||||||
|
let errorType = 'server_error'
|
||||||
|
let errorCode = 'internal_error'
|
||||||
|
let errorMessage = 'Internal server error'
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
errorMessage = error.message
|
||||||
|
|
||||||
|
if (error.message.includes('API key') || error.message.includes('authentication')) {
|
||||||
|
statusCode = 401
|
||||||
|
errorType = 'authentication_error'
|
||||||
|
errorCode = 'invalid_api_key'
|
||||||
|
} else if (error.message.includes('rate limit') || error.message.includes('quota')) {
|
||||||
|
statusCode = 429
|
||||||
|
errorType = 'rate_limit_error'
|
||||||
|
errorCode = 'rate_limit_exceeded'
|
||||||
|
} else if (error.message.includes('timeout') || error.message.includes('connection')) {
|
||||||
|
statusCode = 502
|
||||||
|
errorType = 'server_error'
|
||||||
|
errorCode = 'upstream_error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(statusCode).json({
|
||||||
|
error: {
|
||||||
|
message: errorMessage,
|
||||||
|
type: errorType,
|
||||||
|
code: errorCode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export { router as chatRoutes }
|
||||||
153
src/main/apiServer/routes/mcp.ts
Normal file
153
src/main/apiServer/routes/mcp.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import express, { Request, Response } from 'express'
|
||||||
|
|
||||||
|
import { loggerService } from '../../services/LoggerService'
|
||||||
|
import { mcpApiService } from '../services/mcp'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('ApiServerMCPRoutes')
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /v1/mcps:
|
||||||
|
* get:
|
||||||
|
* summary: List MCP servers
|
||||||
|
* description: Get a list of all configured Model Context Protocol servers
|
||||||
|
* tags: [MCP]
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: List of MCP servers
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* data:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/MCPServer'
|
||||||
|
* 503:
|
||||||
|
* description: Service unavailable
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* example: false
|
||||||
|
* error:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
router.get('/', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
logger.info('Get all MCP servers request received')
|
||||||
|
const servers = await mcpApiService.getAllServers(req)
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: servers
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error fetching MCP servers:', error)
|
||||||
|
return res.status(503).json({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
message: `Failed to retrieve MCP servers: ${error.message}`,
|
||||||
|
type: 'service_unavailable',
|
||||||
|
code: 'servers_unavailable'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /v1/mcps/{server_id}:
|
||||||
|
* get:
|
||||||
|
* summary: Get MCP server info
|
||||||
|
* description: Get detailed information about a specific MCP server
|
||||||
|
* tags: [MCP]
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: server_id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: MCP server ID
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: MCP server information
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* data:
|
||||||
|
* $ref: '#/components/schemas/MCPServer'
|
||||||
|
* 404:
|
||||||
|
* description: MCP server not found
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* example: false
|
||||||
|
* error:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
router.get('/:server_id', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
logger.info('Get MCP server info request received')
|
||||||
|
const server = await mcpApiService.getServerInfo(req.params.server_id)
|
||||||
|
if (!server) {
|
||||||
|
logger.warn('MCP server not found')
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
message: 'MCP server not found',
|
||||||
|
type: 'not_found',
|
||||||
|
code: 'server_not_found'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: server
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error fetching MCP server info:', error)
|
||||||
|
return res.status(503).json({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
message: `Failed to retrieve MCP server info: ${error.message}`,
|
||||||
|
type: 'service_unavailable',
|
||||||
|
code: 'server_info_unavailable'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Connect to MCP server
|
||||||
|
router.all('/:server_id/mcp', async (req: Request, res: Response) => {
|
||||||
|
const server = await mcpApiService.getServerById(req.params.server_id)
|
||||||
|
if (!server) {
|
||||||
|
logger.warn('MCP server not found')
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
message: 'MCP server not found',
|
||||||
|
type: 'not_found',
|
||||||
|
code: 'server_not_found'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return await mcpApiService.handleRequest(req, res, server)
|
||||||
|
})
|
||||||
|
|
||||||
|
export { router as mcpRoutes }
|
||||||
66
src/main/apiServer/routes/models.ts
Normal file
66
src/main/apiServer/routes/models.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import express, { Request, Response } from 'express'
|
||||||
|
|
||||||
|
import { loggerService } from '../../services/LoggerService'
|
||||||
|
import { chatCompletionService } from '../services/chat-completion'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('ApiServerModelsRoutes')
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /v1/models:
|
||||||
|
* get:
|
||||||
|
* summary: List available models
|
||||||
|
* description: Returns a list of available AI models from all configured providers
|
||||||
|
* tags: [Models]
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: List of available models
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* object:
|
||||||
|
* type: string
|
||||||
|
* example: list
|
||||||
|
* data:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/Model'
|
||||||
|
* 503:
|
||||||
|
* description: Service unavailable
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
router.get('/', async (_req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
logger.info('Models list request received')
|
||||||
|
|
||||||
|
const models = await chatCompletionService.getModels()
|
||||||
|
|
||||||
|
if (models.length === 0) {
|
||||||
|
logger.warn('No models available from providers')
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Returning ${models.length} models`)
|
||||||
|
return res.json({
|
||||||
|
object: 'list',
|
||||||
|
data: models
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error fetching models:', error)
|
||||||
|
return res.status(503).json({
|
||||||
|
error: {
|
||||||
|
message: 'Failed to retrieve models',
|
||||||
|
type: 'service_unavailable',
|
||||||
|
code: 'models_unavailable'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export { router as modelsRoutes }
|
||||||
65
src/main/apiServer/server.ts
Normal file
65
src/main/apiServer/server.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { createServer } from 'node:http'
|
||||||
|
|
||||||
|
import { loggerService } from '../services/LoggerService'
|
||||||
|
import { app } from './app'
|
||||||
|
import { config } from './config'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('ApiServer')
|
||||||
|
|
||||||
|
export class ApiServer {
|
||||||
|
private server: ReturnType<typeof createServer> | null = null
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
if (this.server) {
|
||||||
|
logger.warn('Server already running')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
const { port, host, apiKey } = await config.load()
|
||||||
|
|
||||||
|
// Create server with Express app
|
||||||
|
this.server = createServer(app)
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.server!.listen(port, host, () => {
|
||||||
|
logger.info(`API Server started at http://${host}:${port}`)
|
||||||
|
logger.info(`API Key: ${apiKey}`)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.server!.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
if (!this.server) return
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.server!.close(() => {
|
||||||
|
logger.info('API Server stopped')
|
||||||
|
this.server = null
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async restart(): Promise<void> {
|
||||||
|
await this.stop()
|
||||||
|
await config.reload()
|
||||||
|
await this.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning(): boolean {
|
||||||
|
const hasServer = this.server !== null
|
||||||
|
const isListening = this.server?.listening || false
|
||||||
|
const result = hasServer && isListening
|
||||||
|
|
||||||
|
logger.debug('isRunning check:', { hasServer, isListening, result })
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiServer = new ApiServer()
|
||||||
222
src/main/apiServer/services/chat-completion.ts
Normal file
222
src/main/apiServer/services/chat-completion.ts
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import OpenAI from 'openai'
|
||||||
|
import { ChatCompletionCreateParams } from 'openai/resources'
|
||||||
|
|
||||||
|
import { loggerService } from '../../services/LoggerService'
|
||||||
|
import {
|
||||||
|
getProviderByModel,
|
||||||
|
getRealProviderModel,
|
||||||
|
listAllAvailableModels,
|
||||||
|
OpenAICompatibleModel,
|
||||||
|
transformModelToOpenAI,
|
||||||
|
validateProvider
|
||||||
|
} from '../utils'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('ChatCompletionService')
|
||||||
|
|
||||||
|
export interface ModelData extends OpenAICompatibleModel {
|
||||||
|
provider_id: string
|
||||||
|
model_id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationResult {
|
||||||
|
isValid: boolean
|
||||||
|
errors: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChatCompletionService {
|
||||||
|
async getModels(): Promise<ModelData[]> {
|
||||||
|
try {
|
||||||
|
logger.info('Getting available models from providers')
|
||||||
|
|
||||||
|
const models = await listAllAvailableModels()
|
||||||
|
|
||||||
|
const modelData: ModelData[] = models.map((model) => {
|
||||||
|
const openAIModel = transformModelToOpenAI(model)
|
||||||
|
return {
|
||||||
|
...openAIModel,
|
||||||
|
provider_id: model.provider,
|
||||||
|
model_id: model.id,
|
||||||
|
name: model.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(`Successfully retrieved ${modelData.length} models`)
|
||||||
|
return modelData
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error getting models:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateRequest(request: ChatCompletionCreateParams): ValidationResult {
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
// Validate model
|
||||||
|
if (!request.model) {
|
||||||
|
errors.push('Model is required')
|
||||||
|
} else if (typeof request.model !== 'string') {
|
||||||
|
errors.push('Model must be a string')
|
||||||
|
} else if (!request.model.includes(':')) {
|
||||||
|
errors.push('Model must be in format "provider:model_id"')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate messages
|
||||||
|
if (!request.messages) {
|
||||||
|
errors.push('Messages array is required')
|
||||||
|
} else if (!Array.isArray(request.messages)) {
|
||||||
|
errors.push('Messages must be an array')
|
||||||
|
} else if (request.messages.length === 0) {
|
||||||
|
errors.push('Messages array cannot be empty')
|
||||||
|
} else {
|
||||||
|
// Validate each message
|
||||||
|
request.messages.forEach((message, index) => {
|
||||||
|
if (!message.role) {
|
||||||
|
errors.push(`Message ${index}: role is required`)
|
||||||
|
}
|
||||||
|
if (!message.content) {
|
||||||
|
errors.push(`Message ${index}: content is required`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate optional parameters
|
||||||
|
if (request.temperature !== undefined) {
|
||||||
|
if (typeof request.temperature !== 'number' || request.temperature < 0 || request.temperature > 2) {
|
||||||
|
errors.push('Temperature must be a number between 0 and 2')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.max_tokens !== undefined) {
|
||||||
|
if (typeof request.max_tokens !== 'number' || request.max_tokens < 1) {
|
||||||
|
errors.push('max_tokens must be a positive number')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: errors.length === 0,
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processCompletion(request: ChatCompletionCreateParams): Promise<OpenAI.Chat.Completions.ChatCompletion> {
|
||||||
|
try {
|
||||||
|
logger.info('Processing chat completion request:', {
|
||||||
|
model: request.model,
|
||||||
|
messageCount: request.messages.length,
|
||||||
|
stream: request.stream
|
||||||
|
})
|
||||||
|
|
||||||
|
// Validate request
|
||||||
|
const validation = this.validateRequest(request)
|
||||||
|
if (!validation.isValid) {
|
||||||
|
throw new Error(`Request validation failed: ${validation.errors.join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get provider for the model
|
||||||
|
const provider = await getProviderByModel(request.model!)
|
||||||
|
if (!provider) {
|
||||||
|
throw new Error(`Provider not found for model: ${request.model}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate provider
|
||||||
|
if (!validateProvider(provider)) {
|
||||||
|
throw new Error(`Provider validation failed for: ${provider.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract model ID from the full model string
|
||||||
|
const modelId = getRealProviderModel(request.model)
|
||||||
|
|
||||||
|
// Create OpenAI client for the provider
|
||||||
|
const client = new OpenAI({
|
||||||
|
baseURL: provider.apiHost,
|
||||||
|
apiKey: provider.apiKey
|
||||||
|
})
|
||||||
|
|
||||||
|
// Prepare request with the actual model ID
|
||||||
|
const providerRequest = {
|
||||||
|
...request,
|
||||||
|
model: modelId,
|
||||||
|
stream: false
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Sending request to provider:', {
|
||||||
|
provider: provider.id,
|
||||||
|
model: modelId,
|
||||||
|
apiHost: provider.apiHost
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = (await client.chat.completions.create(providerRequest)) as OpenAI.Chat.Completions.ChatCompletion
|
||||||
|
|
||||||
|
logger.info('Successfully processed chat completion')
|
||||||
|
return response
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error processing chat completion:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async *processStreamingCompletion(
|
||||||
|
request: ChatCompletionCreateParams
|
||||||
|
): AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk> {
|
||||||
|
try {
|
||||||
|
logger.info('Processing streaming chat completion request:', {
|
||||||
|
model: request.model,
|
||||||
|
messageCount: request.messages.length
|
||||||
|
})
|
||||||
|
|
||||||
|
// Validate request
|
||||||
|
const validation = this.validateRequest(request)
|
||||||
|
if (!validation.isValid) {
|
||||||
|
throw new Error(`Request validation failed: ${validation.errors.join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get provider for the model
|
||||||
|
const provider = await getProviderByModel(request.model!)
|
||||||
|
if (!provider) {
|
||||||
|
throw new Error(`Provider not found for model: ${request.model}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate provider
|
||||||
|
if (!validateProvider(provider)) {
|
||||||
|
throw new Error(`Provider validation failed for: ${provider.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract model ID from the full model string
|
||||||
|
const modelId = getRealProviderModel(request.model)
|
||||||
|
|
||||||
|
// Create OpenAI client for the provider
|
||||||
|
const client = new OpenAI({
|
||||||
|
baseURL: provider.apiHost,
|
||||||
|
apiKey: provider.apiKey
|
||||||
|
})
|
||||||
|
|
||||||
|
// Prepare streaming request
|
||||||
|
const streamingRequest = {
|
||||||
|
...request,
|
||||||
|
model: modelId,
|
||||||
|
stream: true as const
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Sending streaming request to provider:', {
|
||||||
|
provider: provider.id,
|
||||||
|
model: modelId,
|
||||||
|
apiHost: provider.apiHost
|
||||||
|
})
|
||||||
|
|
||||||
|
const stream = await client.chat.completions.create(streamingRequest)
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
yield chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Successfully completed streaming chat completion')
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error processing streaming chat completion:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const chatCompletionService = new ChatCompletionService()
|
||||||
251
src/main/apiServer/services/mcp.ts
Normal file
251
src/main/apiServer/services/mcp.ts
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import mcpService from '@main/services/MCPService'
|
||||||
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp'
|
||||||
|
import {
|
||||||
|
isJSONRPCRequest,
|
||||||
|
JSONRPCMessage,
|
||||||
|
JSONRPCMessageSchema,
|
||||||
|
MessageExtraInfo
|
||||||
|
} from '@modelcontextprotocol/sdk/types'
|
||||||
|
import { MCPServer } from '@types'
|
||||||
|
import { randomUUID } from 'crypto'
|
||||||
|
import { EventEmitter } from 'events'
|
||||||
|
import { Request, Response } from 'express'
|
||||||
|
import { IncomingMessage, ServerResponse } from 'http'
|
||||||
|
|
||||||
|
import { loggerService } from '../../services/LoggerService'
|
||||||
|
import { reduxService } from '../../services/ReduxService'
|
||||||
|
import { getMcpServerById } from '../utils/mcp'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('MCPApiService')
|
||||||
|
const transports: Record<string, StreamableHTTPServerTransport> = {}
|
||||||
|
|
||||||
|
interface McpServerDTO {
|
||||||
|
id: MCPServer['id']
|
||||||
|
name: MCPServer['name']
|
||||||
|
type: MCPServer['type']
|
||||||
|
description: MCPServer['description']
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface McpServersResp {
|
||||||
|
servers: Record<string, McpServerDTO>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCPApiService - API layer for MCP server management
|
||||||
|
*
|
||||||
|
* This service provides a REST API interface for MCP servers while integrating
|
||||||
|
* with the existing application architecture:
|
||||||
|
*
|
||||||
|
* 1. Uses ReduxService to access the renderer's Redux store directly
|
||||||
|
* 2. Syncs changes back to the renderer via Redux actions
|
||||||
|
* 3. Leverages existing MCPService for actual server connections
|
||||||
|
* 4. Provides session management for API clients
|
||||||
|
*/
|
||||||
|
class MCPApiService extends EventEmitter {
|
||||||
|
private transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: () => randomUUID()
|
||||||
|
})
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this.initMcpServer()
|
||||||
|
logger.silly('MCPApiService initialized')
|
||||||
|
}
|
||||||
|
|
||||||
|
private initMcpServer() {
|
||||||
|
this.transport.onmessage = this.onMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get servers directly from Redux store
|
||||||
|
*/
|
||||||
|
private async getServersFromRedux(): Promise<MCPServer[]> {
|
||||||
|
try {
|
||||||
|
logger.silly('Getting servers from Redux store')
|
||||||
|
|
||||||
|
// Try to get from cache first (faster)
|
||||||
|
const cachedServers = reduxService.selectSync<MCPServer[]>('state.mcp.servers')
|
||||||
|
if (cachedServers && Array.isArray(cachedServers)) {
|
||||||
|
logger.silly(`Found ${cachedServers.length} servers in Redux cache`)
|
||||||
|
return cachedServers
|
||||||
|
}
|
||||||
|
|
||||||
|
// If cache is not available, get fresh data
|
||||||
|
const servers = await reduxService.select<MCPServer[]>('state.mcp.servers')
|
||||||
|
logger.silly(`Fetched ${servers?.length || 0} servers from Redux store`)
|
||||||
|
return servers || []
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to get servers from Redux:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all activated servers
|
||||||
|
async getAllServers(req: Request): Promise<McpServersResp> {
|
||||||
|
try {
|
||||||
|
const servers = await this.getServersFromRedux()
|
||||||
|
logger.silly(`Returning ${servers.length} servers`)
|
||||||
|
const resp: McpServersResp = {
|
||||||
|
servers: {}
|
||||||
|
}
|
||||||
|
for (const server of servers) {
|
||||||
|
if (server.isActive) {
|
||||||
|
resp.servers[server.id] = {
|
||||||
|
id: server.id,
|
||||||
|
name: server.name,
|
||||||
|
type: 'streamableHttp',
|
||||||
|
description: server.description,
|
||||||
|
url: `${req.protocol}://${req.host}/v1/mcps/${server.id}/mcp`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to get all servers:', error)
|
||||||
|
throw new Error('Failed to retrieve servers')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get server by id
|
||||||
|
async getServerById(id: string): Promise<MCPServer | null> {
|
||||||
|
try {
|
||||||
|
logger.silly(`getServerById called with id: ${id}`)
|
||||||
|
const servers = await this.getServersFromRedux()
|
||||||
|
const server = servers.find((s) => s.id === id)
|
||||||
|
if (!server) {
|
||||||
|
logger.warn(`Server with id ${id} not found`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
logger.silly(`Returning server with id ${id}`)
|
||||||
|
return server
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(`Failed to get server with id ${id}:`, error)
|
||||||
|
throw new Error('Failed to retrieve server')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getServerInfo(id: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
logger.silly(`getServerInfo called with id: ${id}`)
|
||||||
|
const server = await this.getServerById(id)
|
||||||
|
if (!server) {
|
||||||
|
logger.warn(`Server with id ${id} not found`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
logger.silly(`Returning server info for id ${id}`)
|
||||||
|
|
||||||
|
const client = await mcpService.initClient(server)
|
||||||
|
const tools = await client.listTools()
|
||||||
|
|
||||||
|
logger.info(`Server with id ${id} info:`, { tools: JSON.stringify(tools) })
|
||||||
|
|
||||||
|
// const [version, tools, prompts, resources] = await Promise.all([
|
||||||
|
// () => {
|
||||||
|
// try {
|
||||||
|
// return client.getServerVersion()
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.error(`Failed to get server version for id ${id}:`, { error: error })
|
||||||
|
// return '1.0.0'
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// (() => {
|
||||||
|
// try {
|
||||||
|
// return client.listTools()
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.error(`Failed to list tools for id ${id}:`, { error: error })
|
||||||
|
// return []
|
||||||
|
// }
|
||||||
|
// })(),
|
||||||
|
// (() => {
|
||||||
|
// try {
|
||||||
|
// return client.listPrompts()
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.error(`Failed to list prompts for id ${id}:`, { error: error })
|
||||||
|
// return []
|
||||||
|
// }
|
||||||
|
// })(),
|
||||||
|
// (() => {
|
||||||
|
// try {
|
||||||
|
// return client.listResources()
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.error(`Failed to list resources for id ${id}:`, { error: error })
|
||||||
|
// return []
|
||||||
|
// }
|
||||||
|
// })()
|
||||||
|
// ])
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: server.id,
|
||||||
|
name: server.name,
|
||||||
|
type: server.type,
|
||||||
|
description: server.description,
|
||||||
|
tools
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(`Failed to get server info with id ${id}:`, error)
|
||||||
|
throw new Error('Failed to retrieve server info')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRequest(req: Request, res: Response, server: MCPServer) {
|
||||||
|
const sessionId = req.headers['mcp-session-id'] as string | undefined
|
||||||
|
logger.silly(`Handling request for server with sessionId ${sessionId}`)
|
||||||
|
let transport: StreamableHTTPServerTransport
|
||||||
|
if (sessionId && transports[sessionId]) {
|
||||||
|
transport = transports[sessionId]
|
||||||
|
} else {
|
||||||
|
transport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: () => randomUUID(),
|
||||||
|
onsessioninitialized: (sessionId) => {
|
||||||
|
transports[sessionId] = transport
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
transport.onclose = () => {
|
||||||
|
logger.info(`Transport for sessionId ${sessionId} closed`)
|
||||||
|
if (transport.sessionId) {
|
||||||
|
delete transports[transport.sessionId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mcpServer = await getMcpServerById(server.id)
|
||||||
|
if (mcpServer) {
|
||||||
|
await mcpServer.connect(transport)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const jsonpayload = req.body
|
||||||
|
const messages: JSONRPCMessage[] = []
|
||||||
|
|
||||||
|
if (Array.isArray(jsonpayload)) {
|
||||||
|
for (const payload of jsonpayload) {
|
||||||
|
const message = JSONRPCMessageSchema.parse(payload)
|
||||||
|
messages.push(message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const message = JSONRPCMessageSchema.parse(jsonpayload)
|
||||||
|
messages.push(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
if (isJSONRPCRequest(message)) {
|
||||||
|
if (!message.params) {
|
||||||
|
message.params = {}
|
||||||
|
}
|
||||||
|
if (!message.params._meta) {
|
||||||
|
message.params._meta = {}
|
||||||
|
}
|
||||||
|
message.params._meta.serverId = server.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Request body`, { rawBody: req.body, messages: JSON.stringify(messages) })
|
||||||
|
await transport.handleRequest(req as IncomingMessage, res as ServerResponse, messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMessage(message: JSONRPCMessage, extra?: MessageExtraInfo) {
|
||||||
|
logger.info(`Received message: ${JSON.stringify(message)}`, extra)
|
||||||
|
// Handle message here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mcpApiService = new MCPApiService()
|
||||||
111
src/main/apiServer/utils/index.ts
Normal file
111
src/main/apiServer/utils/index.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { loggerService } from '@main/services/LoggerService'
|
||||||
|
import { reduxService } from '@main/services/ReduxService'
|
||||||
|
import { Model, Provider } from '@types'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('ApiServerUtils')
|
||||||
|
|
||||||
|
// OpenAI compatible model format
|
||||||
|
export interface OpenAICompatibleModel {
|
||||||
|
id: string
|
||||||
|
object: 'model'
|
||||||
|
created: number
|
||||||
|
owned_by: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAvailableProviders(): Promise<Provider[]> {
|
||||||
|
try {
|
||||||
|
// Wait for store to be ready before accessing providers
|
||||||
|
const providers = await reduxService.select('state.llm.providers')
|
||||||
|
if (!providers || !Array.isArray(providers)) {
|
||||||
|
logger.warn('No providers found in Redux store, returning empty array')
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return providers.filter((p: Provider) => p.enabled)
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to get providers from Redux store:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAllAvailableModels(): Promise<Model[]> {
|
||||||
|
try {
|
||||||
|
const providers = await getAvailableProviders()
|
||||||
|
return providers.map((p: Provider) => p.models || []).flat() as Model[]
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to list available models:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProviderByModel(model: string): Promise<Provider | undefined> {
|
||||||
|
try {
|
||||||
|
if (!model || typeof model !== 'string') {
|
||||||
|
logger.warn(`Invalid model parameter: ${model}`)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const providers = await getAvailableProviders()
|
||||||
|
const modelInfo = model.split(':')
|
||||||
|
|
||||||
|
if (modelInfo.length < 2) {
|
||||||
|
logger.warn(`Invalid model format, expected "provider:model": ${model}`)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerId = modelInfo[0]
|
||||||
|
const provider = providers.find((p: Provider) => p.id === providerId)
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
logger.warn(`Provider not found for model: ${model}`)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to get provider by model:', error)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRealProviderModel(modelStr: string): string {
|
||||||
|
return modelStr.split(':').slice(1).join(':')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformModelToOpenAI(model: Model): OpenAICompatibleModel {
|
||||||
|
return {
|
||||||
|
id: `${model.provider}:${model.id}`,
|
||||||
|
object: 'model',
|
||||||
|
created: Math.floor(Date.now() / 1000),
|
||||||
|
owned_by: model.owned_by || model.provider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateProvider(provider: Provider): boolean {
|
||||||
|
try {
|
||||||
|
if (!provider) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check required fields
|
||||||
|
if (!provider.id || !provider.type || !provider.apiKey || !provider.apiHost) {
|
||||||
|
logger.warn('Provider missing required fields:', {
|
||||||
|
id: !!provider.id,
|
||||||
|
type: !!provider.type,
|
||||||
|
apiKey: !!provider.apiKey,
|
||||||
|
apiHost: !!provider.apiHost
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if provider is enabled
|
||||||
|
if (!provider.enabled) {
|
||||||
|
logger.debug(`Provider is disabled: ${provider.id}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error validating provider:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/main/apiServer/utils/mcp.ts
Normal file
76
src/main/apiServer/utils/mcp.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import mcpService from '@main/services/MCPService'
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListToolsResult } from '@modelcontextprotocol/sdk/types.js'
|
||||||
|
import { MCPServer } from '@types'
|
||||||
|
|
||||||
|
import { loggerService } from '../../services/LoggerService'
|
||||||
|
import { reduxService } from '../../services/ReduxService'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('MCPApiService')
|
||||||
|
|
||||||
|
const cachedServers: Record<string, Server> = {}
|
||||||
|
|
||||||
|
async function handleListToolsRequest(request: any, extra: any): Promise<ListToolsResult> {
|
||||||
|
logger.debug('Handling list tools request', { request: request, extra: extra })
|
||||||
|
const serverId: string = request.params._meta.serverId
|
||||||
|
const serverConfig = await getMcpServerConfigById(serverId)
|
||||||
|
if (!serverConfig) {
|
||||||
|
throw new Error(`Server not found: ${serverId}`)
|
||||||
|
}
|
||||||
|
const client = await mcpService.initClient(serverConfig)
|
||||||
|
return await client.listTools()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCallToolRequest(request: any, extra: any): Promise<any> {
|
||||||
|
logger.debug('Handling call tool request', { request: request, extra: extra })
|
||||||
|
const serverId: string = request.params._meta.serverId
|
||||||
|
const serverConfig = await getMcpServerConfigById(serverId)
|
||||||
|
if (!serverConfig) {
|
||||||
|
throw new Error(`Server not found: ${serverId}`)
|
||||||
|
}
|
||||||
|
const client = await mcpService.initClient(serverConfig)
|
||||||
|
return client.callTool(request.params)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMcpServerConfigById(id: string): Promise<MCPServer | undefined> {
|
||||||
|
const servers = await getServersFromRedux()
|
||||||
|
return servers.find((s) => s.id === id || s.name === id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get servers directly from Redux store
|
||||||
|
*/
|
||||||
|
async function getServersFromRedux(): Promise<MCPServer[]> {
|
||||||
|
try {
|
||||||
|
const servers = await reduxService.select<MCPServer[]>('state.mcp.servers')
|
||||||
|
logger.silly(`Fetched ${servers?.length || 0} servers from Redux store`)
|
||||||
|
return servers || []
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to get servers from Redux:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMcpServerById(id: string): Promise<Server> {
|
||||||
|
const server = cachedServers[id]
|
||||||
|
if (!server) {
|
||||||
|
const servers = await getServersFromRedux()
|
||||||
|
const mcpServer = servers.find((s) => s.id === id || s.name === id)
|
||||||
|
if (!mcpServer) {
|
||||||
|
throw new Error(`Server not found: ${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createMcpServer = (name: string, version: string): Server => {
|
||||||
|
const server = new Server({ name: name, version }, { capabilities: { tools: {} } })
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, handleListToolsRequest)
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, handleCallToolRequest)
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
const newServer = createMcpServer(mcpServer.name, '0.1.0')
|
||||||
|
cachedServers[id] = newServer
|
||||||
|
return newServer
|
||||||
|
}
|
||||||
|
logger.silly('getMcpServer ', { server: server })
|
||||||
|
return server
|
||||||
|
}
|
||||||
@ -27,6 +27,7 @@ import { registerShortcuts } from './services/ShortcutService'
|
|||||||
import { TrayService } from './services/TrayService'
|
import { TrayService } from './services/TrayService'
|
||||||
import { windowService } from './services/WindowService'
|
import { windowService } from './services/WindowService'
|
||||||
import process from 'node:process'
|
import process from 'node:process'
|
||||||
|
import { apiServerService } from './services/ApiServerService'
|
||||||
|
|
||||||
const logger = loggerService.withContext('MainEntry')
|
const logger = loggerService.withContext('MainEntry')
|
||||||
|
|
||||||
@ -139,6 +140,13 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
|
|
||||||
//start selection assistant service
|
//start selection assistant service
|
||||||
initSelectionService()
|
initSelectionService()
|
||||||
|
|
||||||
|
// Start API server if enabled
|
||||||
|
try {
|
||||||
|
await apiServerService.start()
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to start API server:', error)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
registerProtocolClient(app)
|
registerProtocolClient(app)
|
||||||
@ -184,6 +192,7 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
// 简单的资源清理,不阻塞退出流程
|
// 简单的资源清理,不阻塞退出流程
|
||||||
try {
|
try {
|
||||||
await mcpService.cleanup()
|
await mcpService.cleanup()
|
||||||
|
await apiServerService.stop()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('Error cleaning up MCP service:', error as Error)
|
logger.warn('Error cleaning up MCP service:', error as Error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types'
|
|||||||
import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron'
|
import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron'
|
||||||
import { Notification } from 'src/renderer/src/types/notification'
|
import { Notification } from 'src/renderer/src/types/notification'
|
||||||
|
|
||||||
|
import { apiServerService } from './services/ApiServerService'
|
||||||
import appService from './services/AppService'
|
import appService from './services/AppService'
|
||||||
import AppUpdater from './services/AppUpdater'
|
import AppUpdater from './services/AppUpdater'
|
||||||
import BackupManager from './services/BackupManager'
|
import BackupManager from './services/BackupManager'
|
||||||
@ -695,4 +696,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
(_, spanId: string, modelName: string, context: string, msg: any) =>
|
(_, spanId: string, modelName: string, context: string, msg: any) =>
|
||||||
addStreamMessage(spanId, modelName, context, msg)
|
addStreamMessage(spanId, modelName, context, msg)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// API Server
|
||||||
|
apiServerService.registerIpcHandlers()
|
||||||
}
|
}
|
||||||
|
|||||||
108
src/main/services/ApiServerService.ts
Normal file
108
src/main/services/ApiServerService.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
|
import { ApiServerConfig } from '@types'
|
||||||
|
import { ipcMain } from 'electron'
|
||||||
|
|
||||||
|
import { apiServer } from '../apiServer'
|
||||||
|
import { config } from '../apiServer/config'
|
||||||
|
import { loggerService } from './LoggerService'
|
||||||
|
const logger = loggerService.withContext('ApiServerService')
|
||||||
|
|
||||||
|
export class ApiServerService {
|
||||||
|
constructor() {
|
||||||
|
// Use the new clean implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await apiServer.start()
|
||||||
|
logger.info('API Server started successfully')
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to start API Server:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await apiServer.stop()
|
||||||
|
logger.info('API Server stopped successfully')
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to stop API Server:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async restart(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await apiServer.restart()
|
||||||
|
logger.info('API Server restarted successfully')
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to restart API Server:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning(): boolean {
|
||||||
|
return apiServer.isRunning()
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentConfig(): Promise<ApiServerConfig> {
|
||||||
|
return await config.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
registerIpcHandlers(): void {
|
||||||
|
// API Server
|
||||||
|
ipcMain.handle(IpcChannel.ApiServer_Start, async () => {
|
||||||
|
try {
|
||||||
|
await this.start()
|
||||||
|
return { success: true }
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle(IpcChannel.ApiServer_Stop, async () => {
|
||||||
|
try {
|
||||||
|
await this.stop()
|
||||||
|
return { success: true }
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle(IpcChannel.ApiServer_Restart, async () => {
|
||||||
|
try {
|
||||||
|
await this.restart()
|
||||||
|
return { success: true }
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle(IpcChannel.ApiServer_GetStatus, async () => {
|
||||||
|
try {
|
||||||
|
const config = await this.getCurrentConfig()
|
||||||
|
return {
|
||||||
|
running: this.isRunning(),
|
||||||
|
config
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
running: this.isRunning(),
|
||||||
|
config: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle(IpcChannel.ApiServer_GetConfig, async () => {
|
||||||
|
try {
|
||||||
|
return await this.getCurrentConfig()
|
||||||
|
} catch (error: any) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const apiServerService = new ApiServerService()
|
||||||
@ -19,6 +19,7 @@ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'
|
|||||||
// Import notification schemas from MCP SDK
|
// Import notification schemas from MCP SDK
|
||||||
import {
|
import {
|
||||||
CancelledNotificationSchema,
|
CancelledNotificationSchema,
|
||||||
|
type GetPromptResult,
|
||||||
LoggingMessageNotificationSchema,
|
LoggingMessageNotificationSchema,
|
||||||
ProgressNotificationSchema,
|
ProgressNotificationSchema,
|
||||||
PromptListChangedNotificationSchema,
|
PromptListChangedNotificationSchema,
|
||||||
@ -27,15 +28,7 @@ import {
|
|||||||
ToolListChangedNotificationSchema
|
ToolListChangedNotificationSchema
|
||||||
} from '@modelcontextprotocol/sdk/types.js'
|
} from '@modelcontextprotocol/sdk/types.js'
|
||||||
import { nanoid } from '@reduxjs/toolkit'
|
import { nanoid } from '@reduxjs/toolkit'
|
||||||
import type {
|
import type { GetResourceResponse, MCPCallToolResponse, MCPPrompt, MCPResource, MCPServer, MCPTool } from '@types'
|
||||||
GetMCPPromptResponse,
|
|
||||||
GetResourceResponse,
|
|
||||||
MCPCallToolResponse,
|
|
||||||
MCPPrompt,
|
|
||||||
MCPResource,
|
|
||||||
MCPServer,
|
|
||||||
MCPTool
|
|
||||||
} from '@types'
|
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
import { memoize } from 'lodash'
|
import { memoize } from 'lodash'
|
||||||
@ -192,6 +185,7 @@ class McpService {
|
|||||||
},
|
},
|
||||||
authProvider
|
authProvider
|
||||||
}
|
}
|
||||||
|
logger.debug(`StreamableHTTPClientTransport options:`, options)
|
||||||
return new StreamableHTTPClientTransport(new URL(server.baseUrl!), options)
|
return new StreamableHTTPClientTransport(new URL(server.baseUrl!), options)
|
||||||
} else if (server.type === 'sse') {
|
} else if (server.type === 'sse') {
|
||||||
const options: SSEClientTransportOptions = {
|
const options: SSEClientTransportOptions = {
|
||||||
@ -568,6 +562,7 @@ class McpService {
|
|||||||
private async listToolsImpl(server: MCPServer): Promise<MCPTool[]> {
|
private async listToolsImpl(server: MCPServer): Promise<MCPTool[]> {
|
||||||
logger.debug(`Listing tools for server: ${server.name}`)
|
logger.debug(`Listing tools for server: ${server.name}`)
|
||||||
const client = await this.initClient(server)
|
const client = await this.initClient(server)
|
||||||
|
logger.debug(`Client for server: ${server.name}`, client)
|
||||||
try {
|
try {
|
||||||
const { tools } = await client.listTools()
|
const { tools } = await client.listTools()
|
||||||
const serverTools: MCPTool[] = []
|
const serverTools: MCPTool[] = []
|
||||||
@ -705,11 +700,7 @@ class McpService {
|
|||||||
/**
|
/**
|
||||||
* Get a specific prompt from an MCP server (implementation)
|
* Get a specific prompt from an MCP server (implementation)
|
||||||
*/
|
*/
|
||||||
private async getPromptImpl(
|
private async getPromptImpl(server: MCPServer, name: string, args?: Record<string, any>): Promise<GetPromptResult> {
|
||||||
server: MCPServer,
|
|
||||||
name: string,
|
|
||||||
args?: Record<string, any>
|
|
||||||
): Promise<GetMCPPromptResponse> {
|
|
||||||
logger.debug(`Getting prompt ${name} from server: ${server.name}`)
|
logger.debug(`Getting prompt ${name} from server: ${server.name}`)
|
||||||
const client = await this.initClient(server)
|
const client = await this.initClient(server)
|
||||||
return await client.getPrompt({ name, arguments: args })
|
return await client.getPrompt({ name, arguments: args })
|
||||||
@ -722,8 +713,8 @@ class McpService {
|
|||||||
public async getPrompt(
|
public async getPrompt(
|
||||||
_: Electron.IpcMainInvokeEvent,
|
_: Electron.IpcMainInvokeEvent,
|
||||||
{ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }
|
{ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }
|
||||||
): Promise<GetMCPPromptResponse> {
|
): Promise<GetPromptResult> {
|
||||||
const cachedGetPrompt = withCache<[MCPServer, string, Record<string, any> | undefined], GetMCPPromptResponse>(
|
const cachedGetPrompt = withCache<[MCPServer, string, Record<string, any> | undefined], GetPromptResult>(
|
||||||
this.getPromptImpl.bind(this),
|
this.getPromptImpl.bind(this),
|
||||||
(server, name, args) => {
|
(server, name, args) => {
|
||||||
const serverKey = this.getServerKey(server)
|
const serverKey = this.getServerKey(server)
|
||||||
|
|||||||
@ -68,7 +68,8 @@ export class ReduxService extends EventEmitter {
|
|||||||
const selectorFn = new Function('state', `return ${selector}`)
|
const selectorFn = new Function('state', `return ${selector}`)
|
||||||
return selectorFn(this.stateCache)
|
return selectorFn(this.stateCache)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to select from cache:', error as Error)
|
// change it to debug level as it not block other operations
|
||||||
|
logger.debug('Failed to select from cache:', error as Error)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,6 +76,59 @@
|
|||||||
},
|
},
|
||||||
"title": "Agents"
|
"title": "Agents"
|
||||||
},
|
},
|
||||||
|
"apiServer": {
|
||||||
|
"actions": {
|
||||||
|
"copy": "Copy",
|
||||||
|
"regenerate": "Regenerate",
|
||||||
|
"restart": {
|
||||||
|
"button": "Restart",
|
||||||
|
"tooltip": "Restart Server"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authHeaderText": "Use in Authorization header:",
|
||||||
|
"configuration": "Configuration",
|
||||||
|
"description": "Expose Cherry Studio's AI capabilities through OpenAI-compatible HTTP APIs",
|
||||||
|
"documentation": {
|
||||||
|
"title": "API Documentation",
|
||||||
|
"unavailable": {
|
||||||
|
"description": "Start the API server to view the interactive documentation",
|
||||||
|
"title": "API Documentation Unavailable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"apiKey": {
|
||||||
|
"copyTooltip": "Copy API Key",
|
||||||
|
"label": "API Key",
|
||||||
|
"placeholder": "API key will be auto-generated"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"helpText": "Stop server to change port",
|
||||||
|
"label": "Port"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"copyTooltip": "Copy URL",
|
||||||
|
"label": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"apiKeyCopied": "API Key copied to clipboard",
|
||||||
|
"apiKeyRegenerated": "API Key regenerated",
|
||||||
|
"operationFailed": "API Server operation failed: ",
|
||||||
|
"restartError": "Failed to restart API Server: ",
|
||||||
|
"restartFailed": "API Server restart failed: ",
|
||||||
|
"restartSuccess": "API Server restarted successfully",
|
||||||
|
"startError": "Failed to start API Server: ",
|
||||||
|
"startSuccess": "API Server started successfully",
|
||||||
|
"stopError": "Failed to stop API Server: ",
|
||||||
|
"stopSuccess": "API Server stopped successfully",
|
||||||
|
"urlCopied": "Server URL copied to clipboard"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"running": "Running",
|
||||||
|
"stopped": "Stopped"
|
||||||
|
},
|
||||||
|
"title": "API Server"
|
||||||
|
},
|
||||||
"assistants": {
|
"assistants": {
|
||||||
"abbr": "Assistants",
|
"abbr": "Assistants",
|
||||||
"clear": {
|
"clear": {
|
||||||
|
|||||||
@ -76,6 +76,59 @@
|
|||||||
},
|
},
|
||||||
"title": "エージェント"
|
"title": "エージェント"
|
||||||
},
|
},
|
||||||
|
"apiServer": {
|
||||||
|
"actions": {
|
||||||
|
"copy": "コピー",
|
||||||
|
"regenerate": "再生成",
|
||||||
|
"restart": {
|
||||||
|
"button": "再起動",
|
||||||
|
"tooltip": "サーバーを再起動"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authHeaderText": "認証ヘッダーで使用:",
|
||||||
|
"configuration": "設定",
|
||||||
|
"description": "OpenAI 互換の HTTP API を通じて Cherry Studio の AI 機能を公開します",
|
||||||
|
"documentation": {
|
||||||
|
"title": "API ドキュメント",
|
||||||
|
"unavailable": {
|
||||||
|
"description": "インタラクティブドキュメントを表示するには API サーバーを開始してください",
|
||||||
|
"title": "API ドキュメントが利用できません"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"apiKey": {
|
||||||
|
"copyTooltip": "API キーをコピー",
|
||||||
|
"label": "API キー",
|
||||||
|
"placeholder": "API キーは自動生成されます"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"helpText": "ポートを変更するにはサーバーを停止してください",
|
||||||
|
"label": "ポート"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"copyTooltip": "URL をコピー",
|
||||||
|
"label": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"apiKeyCopied": "API キーがクリップボードにコピーされました",
|
||||||
|
"apiKeyRegenerated": "API キーが再生成されました",
|
||||||
|
"operationFailed": "API サーバーの操作に失敗しました:",
|
||||||
|
"restartError": "API サーバーの再起動に失敗しました:",
|
||||||
|
"restartFailed": "API サーバーの再起動に失敗しました:",
|
||||||
|
"restartSuccess": "API サーバーが正常に再起動されました",
|
||||||
|
"startError": "API サーバーの開始に失敗しました:",
|
||||||
|
"startSuccess": "API サーバーが正常に開始されました",
|
||||||
|
"stopError": "API サーバーの停止に失敗しました:",
|
||||||
|
"stopSuccess": "API サーバーが正常に停止されました",
|
||||||
|
"urlCopied": "サーバー URL がクリップボードにコピーされました"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"running": "実行中",
|
||||||
|
"stopped": "停止中"
|
||||||
|
},
|
||||||
|
"title": "API サーバー"
|
||||||
|
},
|
||||||
"assistants": {
|
"assistants": {
|
||||||
"abbr": "アシスタント",
|
"abbr": "アシスタント",
|
||||||
"clear": {
|
"clear": {
|
||||||
|
|||||||
@ -76,6 +76,59 @@
|
|||||||
},
|
},
|
||||||
"title": "Агенты"
|
"title": "Агенты"
|
||||||
},
|
},
|
||||||
|
"apiServer": {
|
||||||
|
"actions": {
|
||||||
|
"copy": "Копировать",
|
||||||
|
"regenerate": "Перегенерировать",
|
||||||
|
"restart": {
|
||||||
|
"button": "Перезапустить",
|
||||||
|
"tooltip": "Перезапустить сервер"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authHeaderText": "Использовать в заголовке авторизации:",
|
||||||
|
"configuration": "Конфигурация",
|
||||||
|
"description": "Предоставляет возможности ИИ Cherry Studio через HTTP API, совместимые с OpenAI",
|
||||||
|
"documentation": {
|
||||||
|
"title": "Документация API",
|
||||||
|
"unavailable": {
|
||||||
|
"description": "Запустите API сервер для просмотра интерактивной документации",
|
||||||
|
"title": "Документация API недоступна"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"apiKey": {
|
||||||
|
"copyTooltip": "Копировать API ключ",
|
||||||
|
"label": "API Ключ",
|
||||||
|
"placeholder": "API ключ будет сгенерирован автоматически"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"helpText": "Остановите сервер для изменения порта",
|
||||||
|
"label": "Порт"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"copyTooltip": "Копировать URL",
|
||||||
|
"label": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"apiKeyCopied": "API ключ скопирован в буфер обмена",
|
||||||
|
"apiKeyRegenerated": "API ключ перегенерирован",
|
||||||
|
"operationFailed": "Операция API сервера не удалась: ",
|
||||||
|
"restartError": "Не удалось перезапустить API сервер: ",
|
||||||
|
"restartFailed": "Перезапуск API сервера не удался: ",
|
||||||
|
"restartSuccess": "API сервер успешно перезапущен",
|
||||||
|
"startError": "Не удалось запустить API сервер: ",
|
||||||
|
"startSuccess": "API сервер успешно запущен",
|
||||||
|
"stopError": "Не удалось остановить API сервер: ",
|
||||||
|
"stopSuccess": "API сервер успешно остановлен",
|
||||||
|
"urlCopied": "URL сервера скопирован в буфер обмена"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"running": "Работает",
|
||||||
|
"stopped": "Остановлен"
|
||||||
|
},
|
||||||
|
"title": "API Сервер"
|
||||||
|
},
|
||||||
"assistants": {
|
"assistants": {
|
||||||
"abbr": "Ассистент",
|
"abbr": "Ассистент",
|
||||||
"clear": {
|
"clear": {
|
||||||
|
|||||||
@ -76,6 +76,59 @@
|
|||||||
},
|
},
|
||||||
"title": "智能体"
|
"title": "智能体"
|
||||||
},
|
},
|
||||||
|
"apiServer": {
|
||||||
|
"actions": {
|
||||||
|
"copy": "复制",
|
||||||
|
"regenerate": "重新生成",
|
||||||
|
"restart": {
|
||||||
|
"button": "重启",
|
||||||
|
"tooltip": "重启服务器"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authHeaderText": "在授权标头中使用:",
|
||||||
|
"configuration": "配置",
|
||||||
|
"description": "通过 OpenAI 兼容的 HTTP API 暴露 Cherry Studio 的 AI 功能",
|
||||||
|
"documentation": {
|
||||||
|
"title": "API 文档",
|
||||||
|
"unavailable": {
|
||||||
|
"description": "启动 API 服务器以查看交互式文档",
|
||||||
|
"title": "API 文档不可用"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"apiKey": {
|
||||||
|
"copyTooltip": "复制 API 密钥",
|
||||||
|
"label": "API 密钥",
|
||||||
|
"placeholder": "API 密钥将自动生成"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"helpText": "停止服务器以更改端口",
|
||||||
|
"label": "端口"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"copyTooltip": "复制 URL",
|
||||||
|
"label": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"apiKeyCopied": "API 密钥已复制到剪贴板",
|
||||||
|
"apiKeyRegenerated": "API 密钥已重新生成",
|
||||||
|
"operationFailed": "API 服务器操作失败:",
|
||||||
|
"restartError": "重启 API 服务器失败:",
|
||||||
|
"restartFailed": "API 服务器重启失败:",
|
||||||
|
"restartSuccess": "API 服务器重启成功",
|
||||||
|
"startError": "启动 API 服务器失败:",
|
||||||
|
"startSuccess": "API 服务器启动成功",
|
||||||
|
"stopError": "停止 API 服务器失败:",
|
||||||
|
"stopSuccess": "API 服务器停止成功",
|
||||||
|
"urlCopied": "服务器 URL 已复制到剪贴板"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"running": "运行中",
|
||||||
|
"stopped": "已停止"
|
||||||
|
},
|
||||||
|
"title": "API 服务器"
|
||||||
|
},
|
||||||
"assistants": {
|
"assistants": {
|
||||||
"abbr": "助手",
|
"abbr": "助手",
|
||||||
"clear": {
|
"clear": {
|
||||||
|
|||||||
@ -76,6 +76,59 @@
|
|||||||
},
|
},
|
||||||
"title": "智慧代理人"
|
"title": "智慧代理人"
|
||||||
},
|
},
|
||||||
|
"apiServer": {
|
||||||
|
"actions": {
|
||||||
|
"copy": "複製",
|
||||||
|
"regenerate": "重新生成",
|
||||||
|
"restart": {
|
||||||
|
"button": "重新啟動",
|
||||||
|
"tooltip": "重新啟動伺服器"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authHeaderText": "在授權標頭中使用:",
|
||||||
|
"configuration": "配置",
|
||||||
|
"description": "透過 OpenAI 相容的 HTTP API 公開 Cherry Studio 的 AI 功能",
|
||||||
|
"documentation": {
|
||||||
|
"title": "API 文件",
|
||||||
|
"unavailable": {
|
||||||
|
"description": "啟動 API 伺服器以檢視互動式文件",
|
||||||
|
"title": "API 文件無法使用"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"apiKey": {
|
||||||
|
"copyTooltip": "複製 API 金鑰",
|
||||||
|
"label": "API 金鑰",
|
||||||
|
"placeholder": "API 金鑰將自動生成"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"helpText": "停止伺服器以變更連接埠",
|
||||||
|
"label": "連接埠"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"copyTooltip": "複製 URL",
|
||||||
|
"label": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"apiKeyCopied": "API 金鑰已複製到剪貼簿",
|
||||||
|
"apiKeyRegenerated": "API 金鑰已重新生成",
|
||||||
|
"operationFailed": "API 伺服器操作失敗:",
|
||||||
|
"restartError": "重新啟動 API 伺服器失敗:",
|
||||||
|
"restartFailed": "API 伺服器重新啟動失敗:",
|
||||||
|
"restartSuccess": "API 伺服器重新啟動成功",
|
||||||
|
"startError": "啟動 API 伺服器失敗:",
|
||||||
|
"startSuccess": "API 伺服器啟動成功",
|
||||||
|
"stopError": "停止 API 伺服器失敗:",
|
||||||
|
"stopSuccess": "API 伺服器停止成功",
|
||||||
|
"urlCopied": "伺服器 URL 已複製到剪貼簿"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"running": "執行中",
|
||||||
|
"stopped": "已停止"
|
||||||
|
},
|
||||||
|
"title": "API 伺服器"
|
||||||
|
},
|
||||||
"assistants": {
|
"assistants": {
|
||||||
"abbr": "助手",
|
"abbr": "助手",
|
||||||
"clear": {
|
"clear": {
|
||||||
|
|||||||
@ -76,6 +76,59 @@
|
|||||||
},
|
},
|
||||||
"title": "Ειδικοί"
|
"title": "Ειδικοί"
|
||||||
},
|
},
|
||||||
|
"apiServer": {
|
||||||
|
"actions": {
|
||||||
|
"copy": "Αντιγραφή",
|
||||||
|
"regenerate": "Αναδημιουργία",
|
||||||
|
"restart": {
|
||||||
|
"button": "Επανεκκίνηση",
|
||||||
|
"tooltip": "Επανεκκίνηση Διακομιστή"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authHeaderText": "Χρήση στην κεφαλίδα εξουσιοδότησης:",
|
||||||
|
"configuration": "Διαμόρφωση",
|
||||||
|
"description": "Εκθέτει τις δυνατότητες AI του Cherry Studio μέσω API HTTP συμβατών με OpenAI",
|
||||||
|
"documentation": {
|
||||||
|
"title": "Τεκμηρίωση API",
|
||||||
|
"unavailable": {
|
||||||
|
"description": "Ξεκινήστε τον διακομιστή API για να δείτε την διαδραστική τεκμηρίωση",
|
||||||
|
"title": "Τεκμηρίωση API Μη Διαθέσιμη"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"apiKey": {
|
||||||
|
"copyTooltip": "Αντιγραφή Κλειδιού API",
|
||||||
|
"label": "Κλειδί API",
|
||||||
|
"placeholder": "Το κλειδί API θα δημιουργηθεί αυτόματα"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"helpText": "Σταματήστε τον διακομιστή για να αλλάξετε τη θύρα",
|
||||||
|
"label": "Θύρα"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"copyTooltip": "Αντιγραφή URL",
|
||||||
|
"label": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"apiKeyCopied": "Το κλειδί API αντιγράφηκε στο πρόχειρο",
|
||||||
|
"apiKeyRegenerated": "Το κλειδί API αναδημιουργήθηκε",
|
||||||
|
"operationFailed": "Η λειτουργία του Διακομιστή API απέτυχε: ",
|
||||||
|
"restartError": "Αποτυχία επανεκκίνησης του Διακομιστή API: ",
|
||||||
|
"restartFailed": "Η επανεκκίνηση του Διακομιστή API απέτυχε: ",
|
||||||
|
"restartSuccess": "Ο διακομιστής API επανεκκινήθηκε επιτυχώς",
|
||||||
|
"startError": "Αποτυχία εκκίνησης του Διακομιστή API: ",
|
||||||
|
"startSuccess": "Ο διακομιστής API ξεκίνησε επιτυχώς",
|
||||||
|
"stopError": "Αποτυχία διακοπής του Διακομιστή API: ",
|
||||||
|
"stopSuccess": "Ο διακομιστής API σταμάτησε επιτυχώς",
|
||||||
|
"urlCopied": "Το URL του διακομιστή αντιγράφηκε στο πρόχειρο"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"running": "Εκτελείται",
|
||||||
|
"stopped": "Σταματημένος"
|
||||||
|
},
|
||||||
|
"title": "Διακομιστής API"
|
||||||
|
},
|
||||||
"assistants": {
|
"assistants": {
|
||||||
"abbr": "Βοηθός",
|
"abbr": "Βοηθός",
|
||||||
"clear": {
|
"clear": {
|
||||||
|
|||||||
@ -76,6 +76,59 @@
|
|||||||
},
|
},
|
||||||
"title": "Agente"
|
"title": "Agente"
|
||||||
},
|
},
|
||||||
|
"apiServer": {
|
||||||
|
"actions": {
|
||||||
|
"copy": "Copiar",
|
||||||
|
"regenerate": "Regenerar",
|
||||||
|
"restart": {
|
||||||
|
"button": "Reiniciar",
|
||||||
|
"tooltip": "Reiniciar Servidor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authHeaderText": "Usar en el encabezado de autorización:",
|
||||||
|
"configuration": "Configuración",
|
||||||
|
"description": "Expone las capacidades de IA de Cherry Studio a través de APIs HTTP compatibles con OpenAI",
|
||||||
|
"documentation": {
|
||||||
|
"title": "Documentación API",
|
||||||
|
"unavailable": {
|
||||||
|
"description": "Inicia el servidor API para ver la documentación interactiva",
|
||||||
|
"title": "Documentación API No Disponible"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"apiKey": {
|
||||||
|
"copyTooltip": "Copiar Clave API",
|
||||||
|
"label": "Clave API",
|
||||||
|
"placeholder": "La clave API se generará automáticamente"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"helpText": "Detén el servidor para cambiar el puerto",
|
||||||
|
"label": "Puerto"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"copyTooltip": "Copiar URL",
|
||||||
|
"label": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"apiKeyCopied": "Clave API copiada al portapapeles",
|
||||||
|
"apiKeyRegenerated": "Clave API regenerada",
|
||||||
|
"operationFailed": "Falló la operación del Servidor API: ",
|
||||||
|
"restartError": "Error al reiniciar el Servidor API: ",
|
||||||
|
"restartFailed": "Falló el reinicio del Servidor API: ",
|
||||||
|
"restartSuccess": "Servidor API reiniciado exitosamente",
|
||||||
|
"startError": "Error al iniciar el Servidor API: ",
|
||||||
|
"startSuccess": "Servidor API iniciado exitosamente",
|
||||||
|
"stopError": "Error al detener el Servidor API: ",
|
||||||
|
"stopSuccess": "Servidor API detenido exitosamente",
|
||||||
|
"urlCopied": "URL del servidor copiada al portapapeles"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"running": "Ejecutándose",
|
||||||
|
"stopped": "Detenido"
|
||||||
|
},
|
||||||
|
"title": "Servidor API"
|
||||||
|
},
|
||||||
"assistants": {
|
"assistants": {
|
||||||
"abbr": "Asistente",
|
"abbr": "Asistente",
|
||||||
"clear": {
|
"clear": {
|
||||||
|
|||||||
@ -76,6 +76,59 @@
|
|||||||
},
|
},
|
||||||
"title": "Agent intelligent"
|
"title": "Agent intelligent"
|
||||||
},
|
},
|
||||||
|
"apiServer": {
|
||||||
|
"actions": {
|
||||||
|
"copy": "Copier",
|
||||||
|
"regenerate": "Régénérer",
|
||||||
|
"restart": {
|
||||||
|
"button": "Redémarrer",
|
||||||
|
"tooltip": "Redémarrer le Serveur"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authHeaderText": "Utiliser dans l'en-tête d'autorisation :",
|
||||||
|
"configuration": "Configuration",
|
||||||
|
"description": "Expose les capacités IA de Cherry Studio via des APIs HTTP compatibles OpenAI",
|
||||||
|
"documentation": {
|
||||||
|
"title": "Documentation API",
|
||||||
|
"unavailable": {
|
||||||
|
"description": "Démarrez le serveur API pour voir la documentation interactive",
|
||||||
|
"title": "Documentation API Indisponible"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"apiKey": {
|
||||||
|
"copyTooltip": "Copier la Clé API",
|
||||||
|
"label": "Clé API",
|
||||||
|
"placeholder": "La clé API sera générée automatiquement"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"helpText": "Arrêtez le serveur pour changer le port",
|
||||||
|
"label": "Port"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"copyTooltip": "Copier l'URL",
|
||||||
|
"label": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"apiKeyCopied": "Clé API copiée dans le presse-papiers",
|
||||||
|
"apiKeyRegenerated": "Clé API régénérée",
|
||||||
|
"operationFailed": "Opération du Serveur API échouée : ",
|
||||||
|
"restartError": "Échec du redémarrage du Serveur API : ",
|
||||||
|
"restartFailed": "Redémarrage du Serveur API échoué : ",
|
||||||
|
"restartSuccess": "Serveur API redémarré avec succès",
|
||||||
|
"startError": "Échec du démarrage du Serveur API : ",
|
||||||
|
"startSuccess": "Serveur API démarré avec succès",
|
||||||
|
"stopError": "Échec de l'arrêt du Serveur API : ",
|
||||||
|
"stopSuccess": "Serveur API arrêté avec succès",
|
||||||
|
"urlCopied": "URL du serveur copiée dans le presse-papiers"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"running": "En cours d'exécution",
|
||||||
|
"stopped": "Arrêté"
|
||||||
|
},
|
||||||
|
"title": "Serveur API"
|
||||||
|
},
|
||||||
"assistants": {
|
"assistants": {
|
||||||
"abbr": "Aide",
|
"abbr": "Aide",
|
||||||
"clear": {
|
"clear": {
|
||||||
|
|||||||
@ -76,6 +76,59 @@
|
|||||||
},
|
},
|
||||||
"title": "Agente"
|
"title": "Agente"
|
||||||
},
|
},
|
||||||
|
"apiServer": {
|
||||||
|
"actions": {
|
||||||
|
"copy": "Copiar",
|
||||||
|
"regenerate": "Regenerar",
|
||||||
|
"restart": {
|
||||||
|
"button": "Reiniciar",
|
||||||
|
"tooltip": "Reiniciar Servidor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authHeaderText": "Usar no cabeçalho de autorização:",
|
||||||
|
"configuration": "Configuração",
|
||||||
|
"description": "Expõe as capacidades de IA do Cherry Studio através de APIs HTTP compatíveis com OpenAI",
|
||||||
|
"documentation": {
|
||||||
|
"title": "Documentação API",
|
||||||
|
"unavailable": {
|
||||||
|
"description": "Inicie o servidor API para ver a documentação interativa",
|
||||||
|
"title": "Documentação API Indisponível"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"apiKey": {
|
||||||
|
"copyTooltip": "Copiar Chave API",
|
||||||
|
"label": "Chave API",
|
||||||
|
"placeholder": "A chave API será gerada automaticamente"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"helpText": "Pare o servidor para alterar a porta",
|
||||||
|
"label": "Porta"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"copyTooltip": "Copiar URL",
|
||||||
|
"label": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"apiKeyCopied": "Chave API copiada para a área de transferência",
|
||||||
|
"apiKeyRegenerated": "Chave API regenerada",
|
||||||
|
"operationFailed": "Operação do Servidor API falhou: ",
|
||||||
|
"restartError": "Falha ao reiniciar o Servidor API: ",
|
||||||
|
"restartFailed": "Reinício do Servidor API falhou: ",
|
||||||
|
"restartSuccess": "Servidor API reiniciado com sucesso",
|
||||||
|
"startError": "Falha ao iniciar o Servidor API: ",
|
||||||
|
"startSuccess": "Servidor API iniciado com sucesso",
|
||||||
|
"stopError": "Falha ao parar o Servidor API: ",
|
||||||
|
"stopSuccess": "Servidor API parado com sucesso",
|
||||||
|
"urlCopied": "URL do servidor copiada para a área de transferência"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"running": "A executar",
|
||||||
|
"stopped": "Parado"
|
||||||
|
},
|
||||||
|
"title": "Servidor API"
|
||||||
|
},
|
||||||
"assistants": {
|
"assistants": {
|
||||||
"abbr": "Assistente",
|
"abbr": "Assistente",
|
||||||
"clear": {
|
"clear": {
|
||||||
|
|||||||
@ -0,0 +1,444 @@
|
|||||||
|
import { CopyOutlined, GlobalOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||||
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { loggerService } from '@renderer/services/LoggerService'
|
||||||
|
import { RootState, useAppDispatch } from '@renderer/store'
|
||||||
|
import { setApiServerApiKey, setApiServerPort } from '@renderer/store/settings'
|
||||||
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
|
import { Button, Card, Input, Space, Switch, Tooltip, Typography } from 'antd'
|
||||||
|
import { FC, useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
import { SettingContainer } from '..'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('ApiServerSettings')
|
||||||
|
const { Text, Title } = Typography
|
||||||
|
|
||||||
|
const ConfigCard = styled(Card)`
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
|
||||||
|
.ant-card-head {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const SectionHeader = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const FieldLabel = styled.div`
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ActionButtonGroup = styled(Space)`
|
||||||
|
.ant-btn {
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn-primary {
|
||||||
|
background: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const StyledInput = styled(Input)`
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1.5px solid var(--color-border);
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-within {
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ServerControlPanel = styled.div<{ status: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: ${(props) =>
|
||||||
|
props.status
|
||||||
|
? 'linear-gradient(135deg, #f6ffed 0%, #f0f9ff 100%)'
|
||||||
|
: 'linear-gradient(135deg, #fff2f0 0%, #fafafa 100%)'};
|
||||||
|
border: 1px solid ${(props) => (props.status ? '#d9f7be' : '#ffd6d6')};
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const StatusSection = styled.div<{ status: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
position: relative;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: ${(props) => (props.status ? '#52c41a' : '#ff4d4f')};
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: ${(props) => (props.status ? '#52c41a' : '#ff4d4f')};
|
||||||
|
opacity: 0.2;
|
||||||
|
animation: ${(props) => (props.status ? 'pulse 2s infinite' : 'none')};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${(props) => (props.status ? '#52c41a' : '#ff4d4f')};
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-subtext {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.5);
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ControlSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
|
||||||
|
.restart-btn {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(10px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ApiServerSettings: FC = () => {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
// API Server state with proper defaults
|
||||||
|
const apiServerConfig = useSelector((state: RootState) => {
|
||||||
|
return state.settings.apiServer
|
||||||
|
})
|
||||||
|
|
||||||
|
const [apiServerRunning, setApiServerRunning] = useState(false)
|
||||||
|
const [apiServerLoading, setApiServerLoading] = useState(false)
|
||||||
|
|
||||||
|
// API Server functions
|
||||||
|
const checkApiServerStatus = async () => {
|
||||||
|
try {
|
||||||
|
const status = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_GetStatus)
|
||||||
|
setApiServerRunning(status.running)
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to check API server status:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkApiServerStatus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleApiServerToggle = async (enabled: boolean) => {
|
||||||
|
setApiServerLoading(true)
|
||||||
|
try {
|
||||||
|
if (enabled) {
|
||||||
|
const result = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_Start)
|
||||||
|
if (result.success) {
|
||||||
|
setApiServerRunning(true)
|
||||||
|
window.message.success(t('apiServer.messages.startSuccess'))
|
||||||
|
} else {
|
||||||
|
window.message.error(t('apiServer.messages.startError') + result.error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const result = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_Stop)
|
||||||
|
if (result.success) {
|
||||||
|
setApiServerRunning(false)
|
||||||
|
window.message.success(t('apiServer.messages.stopSuccess'))
|
||||||
|
} else {
|
||||||
|
window.message.error(t('apiServer.messages.stopError') + result.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
window.message.error(t('apiServer.messages.operationFailed') + (error as Error).message)
|
||||||
|
} finally {
|
||||||
|
setApiServerLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleApiServerRestart = async () => {
|
||||||
|
setApiServerLoading(true)
|
||||||
|
try {
|
||||||
|
const result = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_Restart)
|
||||||
|
if (result.success) {
|
||||||
|
await checkApiServerStatus()
|
||||||
|
window.message.success(t('apiServer.messages.restartSuccess'))
|
||||||
|
} else {
|
||||||
|
window.message.error(t('apiServer.messages.restartError') + result.error)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
window.message.error(t('apiServer.messages.restartFailed') + (error as Error).message)
|
||||||
|
} finally {
|
||||||
|
setApiServerLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyApiKey = () => {
|
||||||
|
navigator.clipboard.writeText(apiServerConfig.apiKey)
|
||||||
|
window.message.success(t('apiServer.messages.apiKeyCopied'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const regenerateApiKey = () => {
|
||||||
|
const newApiKey = `cs-sk-${uuidv4()}`
|
||||||
|
dispatch(setApiServerApiKey(newApiKey))
|
||||||
|
window.message.success(t('apiServer.messages.apiKeyRegenerated'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePortChange = (value: string) => {
|
||||||
|
const port = parseInt(value) || 23333
|
||||||
|
if (port >= 1000 && port <= 65535) {
|
||||||
|
dispatch(setApiServerPort(port))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingContainer theme={theme} style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
|
{/* Header Section */}
|
||||||
|
<div style={{ marginBottom: 32 }}>
|
||||||
|
<Title level={3} style={{ margin: 0, marginBottom: 8 }}>
|
||||||
|
{t('apiServer.title')}
|
||||||
|
</Title>
|
||||||
|
<Text type="secondary">{t('apiServer.description')}</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Server Status & Configuration Card */}
|
||||||
|
<ConfigCard
|
||||||
|
title={
|
||||||
|
<SectionHeader>
|
||||||
|
<GlobalOutlined />
|
||||||
|
<h4>{t('apiServer.configuration')}</h4>
|
||||||
|
</SectionHeader>
|
||||||
|
}>
|
||||||
|
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||||
|
{/* Server Control Panel */}
|
||||||
|
<ServerControlPanel status={apiServerRunning}>
|
||||||
|
<StatusSection status={apiServerRunning}>
|
||||||
|
<div className="status-indicator" />
|
||||||
|
<div className="status-content">
|
||||||
|
<div className="status-text">
|
||||||
|
{apiServerRunning ? t('apiServer.status.running') : t('apiServer.status.stopped')}
|
||||||
|
</div>
|
||||||
|
<div className="status-subtext">
|
||||||
|
{apiServerRunning ? `http://localhost:${apiServerConfig.port}` : t('apiServer.fields.port.helpText')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</StatusSection>
|
||||||
|
|
||||||
|
<ControlSection>
|
||||||
|
<Switch
|
||||||
|
checked={apiServerRunning}
|
||||||
|
loading={apiServerLoading}
|
||||||
|
onChange={handleApiServerToggle}
|
||||||
|
size="default"
|
||||||
|
/>
|
||||||
|
<Tooltip title={t('apiServer.actions.restart.tooltip')}>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={handleApiServerRestart}
|
||||||
|
loading={apiServerLoading}
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
className={`restart-btn ${apiServerRunning ? 'visible' : ''}`}>
|
||||||
|
{t('apiServer.actions.restart.button')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</ControlSection>
|
||||||
|
</ServerControlPanel>
|
||||||
|
|
||||||
|
{/* Configuration Fields */}
|
||||||
|
<div style={{ display: 'grid', gap: '12px' }}>
|
||||||
|
{/* Port Configuration */}
|
||||||
|
{!apiServerRunning && (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||||
|
<FieldLabel style={{ minWidth: 50, margin: 0 }}>{t('apiServer.fields.port.label')}</FieldLabel>
|
||||||
|
<StyledInput
|
||||||
|
type="number"
|
||||||
|
value={apiServerConfig.port}
|
||||||
|
onChange={(e) => handlePortChange(e.target.value)}
|
||||||
|
style={{ width: 100 }}
|
||||||
|
min={1000}
|
||||||
|
max={65535}
|
||||||
|
disabled={apiServerRunning}
|
||||||
|
placeholder="23333"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
{apiServerRunning && (
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{t('apiServer.fields.port.helpText')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* API Key Configuration */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||||
|
<FieldLabel style={{ minWidth: 50, margin: 0 }}>{t('apiServer.fields.apiKey.label')}</FieldLabel>
|
||||||
|
<StyledInput
|
||||||
|
value={apiServerConfig.apiKey}
|
||||||
|
readOnly
|
||||||
|
style={{ flex: 1, minWidth: 200, maxWidth: 300 }}
|
||||||
|
placeholder={t('apiServer.fields.apiKey.placeholder')}
|
||||||
|
disabled={apiServerRunning}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<ActionButtonGroup>
|
||||||
|
<Tooltip title={t('apiServer.fields.apiKey.copyTooltip')}>
|
||||||
|
<Button icon={<CopyOutlined />} onClick={copyApiKey} disabled={!apiServerConfig.apiKey} size="small">
|
||||||
|
{t('apiServer.actions.copy')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
{!apiServerRunning && (
|
||||||
|
<Button onClick={regenerateApiKey} disabled={apiServerRunning} size="small">
|
||||||
|
{t('apiServer.actions.regenerate')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ActionButtonGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Authorization header info */}
|
||||||
|
<Text type="secondary" style={{ fontSize: 11, lineHeight: 1.3 }}>
|
||||||
|
{t('apiServer.authHeaderText')}{' '}
|
||||||
|
<Text code style={{ fontSize: 11 }}>
|
||||||
|
Bearer {apiServerConfig.apiKey || 'your-api-key'}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</ConfigCard>
|
||||||
|
|
||||||
|
{/* API Documentation Card */}
|
||||||
|
<ConfigCard
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
marginBottom: 0
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: 0
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<SectionHeader>
|
||||||
|
<h4>{t('apiServer.documentation.title')}</h4>
|
||||||
|
</SectionHeader>
|
||||||
|
}>
|
||||||
|
{apiServerRunning ? (
|
||||||
|
<iframe
|
||||||
|
src={`http://localhost:${apiServerConfig.port}/api-docs`}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
border: 'none',
|
||||||
|
height: 'calc(100vh - 500px)'
|
||||||
|
}}
|
||||||
|
title="API Documentation"
|
||||||
|
sandbox="allow-scripts allow-forms"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '60px 20px',
|
||||||
|
color: 'var(--color-text-2)',
|
||||||
|
background: 'var(--color-bg-2)',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px dashed var(--color-border)',
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: 16,
|
||||||
|
height: '300px'
|
||||||
|
}}>
|
||||||
|
<GlobalOutlined style={{ fontSize: 48, marginBottom: 16, opacity: 0.3 }} />
|
||||||
|
<div style={{ fontSize: 16, fontWeight: 500, marginBottom: 8 }}>
|
||||||
|
{t('apiServer.documentation.unavailable.title')}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 14 }}>{t('apiServer.documentation.unavailable.description')}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ConfigCard>
|
||||||
|
</SettingContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiServerSettings
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default as ApiServerSettings } from './ApiServerSettings'
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
Package,
|
Package,
|
||||||
PencilRuler,
|
PencilRuler,
|
||||||
Rocket,
|
Rocket,
|
||||||
|
Server,
|
||||||
Settings2,
|
Settings2,
|
||||||
SquareTerminal,
|
SquareTerminal,
|
||||||
TextCursorInput,
|
TextCursorInput,
|
||||||
@ -22,6 +23,7 @@ import { Link, Route, Routes, useLocation } from 'react-router-dom'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import AboutSettings from './AboutSettings'
|
import AboutSettings from './AboutSettings'
|
||||||
|
import { ApiServerSettings } from './ApiServerSettings'
|
||||||
import DataSettings from './DataSettings/DataSettings'
|
import DataSettings from './DataSettings/DataSettings'
|
||||||
import DisplaySettings from './DisplaySettings/DisplaySettings'
|
import DisplaySettings from './DisplaySettings/DisplaySettings'
|
||||||
import GeneralSettings from './GeneralSettings'
|
import GeneralSettings from './GeneralSettings'
|
||||||
@ -77,6 +79,18 @@ const SettingsPage: FC = () => {
|
|||||||
{t('settings.mcp.title')}
|
{t('settings.mcp.title')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuItemLink>
|
</MenuItemLink>
|
||||||
|
<MenuItemLink to="/settings/api-server">
|
||||||
|
<MenuItem className={isRoute('/settings/api-server')}>
|
||||||
|
<Server size={18} />
|
||||||
|
{t('apiServer.title')}
|
||||||
|
</MenuItem>
|
||||||
|
</MenuItemLink>
|
||||||
|
<MenuItemLink to="/settings/tool">
|
||||||
|
<MenuItem className={isRoute('/settings/tool')}>
|
||||||
|
<PencilRuler size={18} />
|
||||||
|
{t('settings.tool.title')}
|
||||||
|
</MenuItem>
|
||||||
|
</MenuItemLink>
|
||||||
<MenuItemLink to="/settings/memory">
|
<MenuItemLink to="/settings/memory">
|
||||||
<MenuItem className={isRoute('/settings/memory')}>
|
<MenuItem className={isRoute('/settings/memory')}>
|
||||||
<Brain size={18} />
|
<Brain size={18} />
|
||||||
@ -133,6 +147,7 @@ const SettingsPage: FC = () => {
|
|||||||
<Route path="tool/*" element={<ToolSettings />} />
|
<Route path="tool/*" element={<ToolSettings />} />
|
||||||
<Route path="mcp/*" element={<MCPSettings />} />
|
<Route path="mcp/*" element={<MCPSettings />} />
|
||||||
<Route path="memory" element={<MemorySettings />} />
|
<Route path="memory" element={<MemorySettings />} />
|
||||||
|
<Route path="api-server" element={<ApiServerSettings />} />
|
||||||
<Route path="general/*" element={<GeneralSettings />} />
|
<Route path="general/*" element={<GeneralSettings />} />
|
||||||
<Route path="display" element={<DisplaySettings />} />
|
<Route path="display" element={<DisplaySettings />} />
|
||||||
<Route path="shortcut" element={<ShortcutSettings />} />
|
<Route path="shortcut" element={<ShortcutSettings />} />
|
||||||
|
|||||||
@ -60,7 +60,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 124,
|
version: 125,
|
||||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1919,6 +1919,22 @@ const migrateConfig = {
|
|||||||
logger.error('migrate 124 error', error as Error)
|
logger.error('migrate 124 error', error as Error)
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'125': (state: RootState) => {
|
||||||
|
try {
|
||||||
|
// Initialize API server configuration if not present
|
||||||
|
if (!state.settings.apiServer) {
|
||||||
|
state.settings.apiServer = {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 23333,
|
||||||
|
apiKey: `cs-sk-${uuid()}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('migrate 125 error', error as Error)
|
||||||
|
return state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
|
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
|
||||||
import {
|
import {
|
||||||
|
ApiServerConfig,
|
||||||
AssistantsSortType,
|
AssistantsSortType,
|
||||||
CodeStyleVarious,
|
CodeStyleVarious,
|
||||||
LanguageVarious,
|
LanguageVarious,
|
||||||
@ -204,6 +205,8 @@ export interface SettingsState {
|
|||||||
enableDeveloperMode: boolean
|
enableDeveloperMode: boolean
|
||||||
// UI
|
// UI
|
||||||
navbarPosition: 'left' | 'top'
|
navbarPosition: 'left' | 'top'
|
||||||
|
// API Server
|
||||||
|
apiServer: ApiServerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
||||||
@ -376,7 +379,13 @@ export const initialState: SettingsState = {
|
|||||||
// Developer mode
|
// Developer mode
|
||||||
enableDeveloperMode: false,
|
enableDeveloperMode: false,
|
||||||
// UI
|
// UI
|
||||||
navbarPosition: 'left'
|
navbarPosition: 'left',
|
||||||
|
// API Server
|
||||||
|
apiServer: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 23333,
|
||||||
|
apiKey: `cs-sk-${uuid()}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsSlice = createSlice({
|
const settingsSlice = createSlice({
|
||||||
@ -780,6 +789,19 @@ const settingsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setNavbarPosition: (state, action: PayloadAction<'left' | 'top'>) => {
|
setNavbarPosition: (state, action: PayloadAction<'left' | 'top'>) => {
|
||||||
state.navbarPosition = action.payload
|
state.navbarPosition = action.payload
|
||||||
|
},
|
||||||
|
// API Server actions
|
||||||
|
setApiServerPort: (state, action: PayloadAction<number>) => {
|
||||||
|
state.apiServer = {
|
||||||
|
...state.apiServer,
|
||||||
|
port: action.payload
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setApiServerApiKey: (state, action: PayloadAction<string>) => {
|
||||||
|
state.apiServer = {
|
||||||
|
...state.apiServer,
|
||||||
|
apiKey: action.payload
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -902,7 +924,10 @@ export const {
|
|||||||
setS3,
|
setS3,
|
||||||
setS3Partial,
|
setS3Partial,
|
||||||
setEnableDeveloperMode,
|
setEnableDeveloperMode,
|
||||||
setNavbarPosition
|
setNavbarPosition,
|
||||||
|
// API Server actions
|
||||||
|
setApiServerPort,
|
||||||
|
setApiServerApiKey
|
||||||
} = settingsSlice.actions
|
} = settingsSlice.actions
|
||||||
|
|
||||||
export default settingsSlice.reducer
|
export default settingsSlice.reducer
|
||||||
|
|||||||
@ -841,6 +841,12 @@ export type S3Config = {
|
|||||||
|
|
||||||
export type { Message } from './newMessage'
|
export type { Message } from './newMessage'
|
||||||
|
|
||||||
|
export interface ApiServerConfig {
|
||||||
|
host: string
|
||||||
|
port: number
|
||||||
|
apiKey: string
|
||||||
|
}
|
||||||
|
|
||||||
// Memory Service Types
|
// Memory Service Types
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
export interface MemoryConfig {
|
export interface MemoryConfig {
|
||||||
|
|||||||
344
yarn.lock
344
yarn.lock
@ -244,6 +244,48 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@apidevtools/json-schema-ref-parser@npm:^9.0.6":
|
||||||
|
version: 9.1.2
|
||||||
|
resolution: "@apidevtools/json-schema-ref-parser@npm:9.1.2"
|
||||||
|
dependencies:
|
||||||
|
"@jsdevtools/ono": "npm:^7.1.3"
|
||||||
|
"@types/json-schema": "npm:^7.0.6"
|
||||||
|
call-me-maybe: "npm:^1.0.1"
|
||||||
|
js-yaml: "npm:^4.1.0"
|
||||||
|
checksum: 10c0/ebf952eb2e00bf0919f024e72897e047fd5012f0a9e47ac361873f6de0a733b9334513cdbc73205a6b43ac4a652b8c87f55e489c39b2d60bd0bc1cb2b411e218
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@apidevtools/openapi-schemas@npm:^2.0.4":
|
||||||
|
version: 2.1.0
|
||||||
|
resolution: "@apidevtools/openapi-schemas@npm:2.1.0"
|
||||||
|
checksum: 10c0/f4aa0f9df32e474d166c84ef91bceb18fa1c4f44b5593879529154ef340846811ea57dc2921560f157f692262827d28d988dd6e19fb21f00320e9961964176b4
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@apidevtools/swagger-methods@npm:^3.0.2":
|
||||||
|
version: 3.0.2
|
||||||
|
resolution: "@apidevtools/swagger-methods@npm:3.0.2"
|
||||||
|
checksum: 10c0/8c390e8e50c0be7787ba0ba4c3758488bde7c66c2d995209b4b48c1f8bc988faf393cbb24a4bd1cd2d42ce5167c26538e8adea5c85eb922761b927e4dab9fa1c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@apidevtools/swagger-parser@npm:10.0.3":
|
||||||
|
version: 10.0.3
|
||||||
|
resolution: "@apidevtools/swagger-parser@npm:10.0.3"
|
||||||
|
dependencies:
|
||||||
|
"@apidevtools/json-schema-ref-parser": "npm:^9.0.6"
|
||||||
|
"@apidevtools/openapi-schemas": "npm:^2.0.4"
|
||||||
|
"@apidevtools/swagger-methods": "npm:^3.0.2"
|
||||||
|
"@jsdevtools/ono": "npm:^7.1.3"
|
||||||
|
call-me-maybe: "npm:^1.0.1"
|
||||||
|
z-schema: "npm:^5.0.1"
|
||||||
|
peerDependencies:
|
||||||
|
openapi-types: ">=7"
|
||||||
|
checksum: 10c0/3b43f719c2d647ac8dcf30f132834d413ce21cbf7a8d9c3b35ec91149dd25d608c8fd892358fcd61a8edd8c5140a7fb13676f948e2d87067d081a47b8c7107e9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@asamuzakjp/css-color@npm:^3.1.1":
|
"@asamuzakjp/css-color@npm:^3.1.1":
|
||||||
version: 3.1.2
|
version: 3.1.2
|
||||||
resolution: "@asamuzakjp/css-color@npm:3.1.2"
|
resolution: "@asamuzakjp/css-color@npm:3.1.2"
|
||||||
@ -3121,6 +3163,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@jsdevtools/ono@npm:^7.1.3":
|
||||||
|
version: 7.1.3
|
||||||
|
resolution: "@jsdevtools/ono@npm:7.1.3"
|
||||||
|
checksum: 10c0/a9f7e3e8e3bc315a34959934a5e2f874c423cf4eae64377d3fc9de0400ed9f36cb5fd5ebce3300d2e8f4085f557c4a8b591427a583729a87841fda46e6c216b9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@kangfenmao/keyv-storage@npm:^0.1.0":
|
"@kangfenmao/keyv-storage@npm:^0.1.0":
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
resolution: "@kangfenmao/keyv-storage@npm:0.1.0"
|
resolution: "@kangfenmao/keyv-storage@npm:0.1.0"
|
||||||
@ -3989,22 +4038,23 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk@npm:^1.12.3":
|
"@modelcontextprotocol/sdk@npm:^1.17.0":
|
||||||
version: 1.12.3
|
version: 1.17.0
|
||||||
resolution: "@modelcontextprotocol/sdk@npm:1.12.3"
|
resolution: "@modelcontextprotocol/sdk@npm:1.17.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: "npm:^6.12.6"
|
ajv: "npm:^6.12.6"
|
||||||
content-type: "npm:^1.0.5"
|
content-type: "npm:^1.0.5"
|
||||||
cors: "npm:^2.8.5"
|
cors: "npm:^2.8.5"
|
||||||
cross-spawn: "npm:^7.0.5"
|
cross-spawn: "npm:^7.0.5"
|
||||||
eventsource: "npm:^3.0.2"
|
eventsource: "npm:^3.0.2"
|
||||||
|
eventsource-parser: "npm:^3.0.0"
|
||||||
express: "npm:^5.0.1"
|
express: "npm:^5.0.1"
|
||||||
express-rate-limit: "npm:^7.5.0"
|
express-rate-limit: "npm:^7.5.0"
|
||||||
pkce-challenge: "npm:^5.0.0"
|
pkce-challenge: "npm:^5.0.0"
|
||||||
raw-body: "npm:^3.0.0"
|
raw-body: "npm:^3.0.0"
|
||||||
zod: "npm:^3.23.8"
|
zod: "npm:^3.23.8"
|
||||||
zod-to-json-schema: "npm:^3.24.1"
|
zod-to-json-schema: "npm:^3.24.1"
|
||||||
checksum: 10c0/8bc0b91e596ec886efc64d68ae8474247647405f1a5ae407e02439c74c2a03528b3fbdce8f9352d9c2df54aa4548411e1aa1816ab3b09e045c2ff4202e2fd374
|
checksum: 10c0/ac497edeb05a434bf8092475e4354ec602644b0197735d3bcd809ee1922f2078ab71e7d8d9dbe1c42765978fa3f2f807df01a2a3ad421c986f0b2207c3a40a68
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -5026,6 +5076,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@scarf/scarf@npm:=1.4.0":
|
||||||
|
version: 1.4.0
|
||||||
|
resolution: "@scarf/scarf@npm:1.4.0"
|
||||||
|
checksum: 10c0/332118bb488e7a70eaad068fb1a33f016d30442fb0498b37a80cb425c1e741853a5de1a04dce03526ed6265481ecf744aa6e13f072178d19e6b94b19f623ae1c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@selderee/plugin-htmlparser2@npm:^0.11.0":
|
"@selderee/plugin-htmlparser2@npm:^0.11.0":
|
||||||
version: 0.11.0
|
version: 0.11.0
|
||||||
resolution: "@selderee/plugin-htmlparser2@npm:0.11.0"
|
resolution: "@selderee/plugin-htmlparser2@npm:0.11.0"
|
||||||
@ -6180,6 +6237,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/body-parser@npm:*":
|
||||||
|
version: 1.19.6
|
||||||
|
resolution: "@types/body-parser@npm:1.19.6"
|
||||||
|
dependencies:
|
||||||
|
"@types/connect": "npm:*"
|
||||||
|
"@types/node": "npm:*"
|
||||||
|
checksum: 10c0/542da05c924dce58ee23f50a8b981fee36921850c82222e384931fda3e106f750f7880c47be665217d72dbe445129049db6eb1f44e7a06b09d62af8f3cca8ea7
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/cacheable-request@npm:^6.0.1":
|
"@types/cacheable-request@npm:^6.0.1":
|
||||||
version: 6.0.3
|
version: 6.0.3
|
||||||
resolution: "@types/cacheable-request@npm:6.0.3"
|
resolution: "@types/cacheable-request@npm:6.0.3"
|
||||||
@ -6210,6 +6277,31 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/connect@npm:*":
|
||||||
|
version: 3.4.38
|
||||||
|
resolution: "@types/connect@npm:3.4.38"
|
||||||
|
dependencies:
|
||||||
|
"@types/node": "npm:*"
|
||||||
|
checksum: 10c0/2e1cdba2c410f25649e77856505cd60223250fa12dff7a503e492208dbfdd25f62859918f28aba95315251fd1f5e1ffbfca1e25e73037189ab85dd3f8d0a148c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/content-type@npm:^1.1.9":
|
||||||
|
version: 1.1.9
|
||||||
|
resolution: "@types/content-type@npm:1.1.9"
|
||||||
|
checksum: 10c0/d8b198257862991880d38985ad9871241db18b21ec728bddc78e4c61e0f987cc037dae6c5f9bd2bcc08f41de74ad371180af2fcdefeafe25d0ccae0c3fceb7fd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/cors@npm:^2.8.19":
|
||||||
|
version: 2.8.19
|
||||||
|
resolution: "@types/cors@npm:2.8.19"
|
||||||
|
dependencies:
|
||||||
|
"@types/node": "npm:*"
|
||||||
|
checksum: 10c0/b5dd407040db7d8aa1bd36e79e5f3f32292f6b075abc287529e9f48df1a25fda3e3799ba30b4656667ffb931d3b75690c1d6ca71e39f7337ea6dfda8581916d0
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/d3-array@npm:*":
|
"@types/d3-array@npm:*":
|
||||||
version: 3.2.1
|
version: 3.2.1
|
||||||
resolution: "@types/d3-array@npm:3.2.1"
|
resolution: "@types/d3-array@npm:3.2.1"
|
||||||
@ -6521,6 +6613,29 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/express-serve-static-core@npm:^5.0.0":
|
||||||
|
version: 5.0.7
|
||||||
|
resolution: "@types/express-serve-static-core@npm:5.0.7"
|
||||||
|
dependencies:
|
||||||
|
"@types/node": "npm:*"
|
||||||
|
"@types/qs": "npm:*"
|
||||||
|
"@types/range-parser": "npm:*"
|
||||||
|
"@types/send": "npm:*"
|
||||||
|
checksum: 10c0/28666f6a0743b8678be920a6eed075bc8afc96fc7d8ef59c3c049bd6b51533da3b24daf3b437d061e053fba1475e4f3175cb4972f5e8db41608e817997526430
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/express@npm:*, @types/express@npm:^5":
|
||||||
|
version: 5.0.3
|
||||||
|
resolution: "@types/express@npm:5.0.3"
|
||||||
|
dependencies:
|
||||||
|
"@types/body-parser": "npm:*"
|
||||||
|
"@types/express-serve-static-core": "npm:^5.0.0"
|
||||||
|
"@types/serve-static": "npm:*"
|
||||||
|
checksum: 10c0/f0fbc8daa7f40070b103cf4d020ff1dd08503477d866d1134b87c0390bba71d5d7949cb8b4e719a81ccba89294d8e1573414e6dcbb5bb1d097a7b820928ebdef
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/fs-extra@npm:9.0.13, @types/fs-extra@npm:^9.0.11":
|
"@types/fs-extra@npm:9.0.13, @types/fs-extra@npm:^9.0.11":
|
||||||
version: 9.0.13
|
version: 9.0.13
|
||||||
resolution: "@types/fs-extra@npm:9.0.13"
|
resolution: "@types/fs-extra@npm:9.0.13"
|
||||||
@ -6573,7 +6688,14 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/json-schema@npm:^7.0.15":
|
"@types/http-errors@npm:*":
|
||||||
|
version: 2.0.5
|
||||||
|
resolution: "@types/http-errors@npm:2.0.5"
|
||||||
|
checksum: 10c0/00f8140fbc504f47356512bd88e1910c2f07e04233d99c88c854b3600ce0523c8cd0ba7d1897667243282eb44c59abb9245959e2428b9de004f93937f52f7c15
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.6":
|
||||||
version: 7.0.15
|
version: 7.0.15
|
||||||
resolution: "@types/json-schema@npm:7.0.15"
|
resolution: "@types/json-schema@npm:7.0.15"
|
||||||
checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db
|
checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db
|
||||||
@ -6668,6 +6790,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/mime@npm:^1":
|
||||||
|
version: 1.3.5
|
||||||
|
resolution: "@types/mime@npm:1.3.5"
|
||||||
|
checksum: 10c0/c2ee31cd9b993804df33a694d5aa3fa536511a49f2e06eeab0b484fef59b4483777dbb9e42a4198a0809ffbf698081fdbca1e5c2218b82b91603dfab10a10fbc
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/ms@npm:*":
|
"@types/ms@npm:*":
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
resolution: "@types/ms@npm:2.1.0"
|
resolution: "@types/ms@npm:2.1.0"
|
||||||
@ -6720,6 +6849,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/qs@npm:*":
|
||||||
|
version: 6.14.0
|
||||||
|
resolution: "@types/qs@npm:6.14.0"
|
||||||
|
checksum: 10c0/5b3036df6e507483869cdb3858201b2e0b64b4793dc4974f188caa5b5732f2333ab9db45c08157975054d3b070788b35088b4bc60257ae263885016ee2131310
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/range-parser@npm:*":
|
||||||
|
version: 1.2.7
|
||||||
|
resolution: "@types/range-parser@npm:1.2.7"
|
||||||
|
checksum: 10c0/361bb3e964ec5133fa40644a0b942279ed5df1949f21321d77de79f48b728d39253e5ce0408c9c17e4e0fd95ca7899da36841686393b9f7a1e209916e9381a3c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/react-dom@npm:^19.0.4":
|
"@types/react-dom@npm:^19.0.4":
|
||||||
version: 19.1.2
|
version: 19.1.2
|
||||||
resolution: "@types/react-dom@npm:19.1.2"
|
resolution: "@types/react-dom@npm:19.1.2"
|
||||||
@ -6772,6 +6915,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/send@npm:*":
|
||||||
|
version: 0.17.5
|
||||||
|
resolution: "@types/send@npm:0.17.5"
|
||||||
|
dependencies:
|
||||||
|
"@types/mime": "npm:^1"
|
||||||
|
"@types/node": "npm:*"
|
||||||
|
checksum: 10c0/a86c9b89bb0976ff58c1cdd56360ea98528f4dbb18a5c2287bb8af04815513a576a42b4e0e1e7c4d14f7d6ea54733f6ef935ebff8c65e86d9c222881a71e1f15
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/serve-static@npm:*":
|
||||||
|
version: 1.15.8
|
||||||
|
resolution: "@types/serve-static@npm:1.15.8"
|
||||||
|
dependencies:
|
||||||
|
"@types/http-errors": "npm:*"
|
||||||
|
"@types/node": "npm:*"
|
||||||
|
"@types/send": "npm:*"
|
||||||
|
checksum: 10c0/8ad86a25b87da5276cb1008c43c74667ff7583904d46d5fcaf0355887869d859d453d7dc4f890788ae04705c23720e9b6b6f3215e2d1d2a4278bbd090a9268dd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/stylis@npm:4.2.5":
|
"@types/stylis@npm:4.2.5":
|
||||||
version: 4.2.5
|
version: 4.2.5
|
||||||
resolution: "@types/stylis@npm:4.2.5"
|
resolution: "@types/stylis@npm:4.2.5"
|
||||||
@ -6779,6 +6943,23 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/swagger-jsdoc@npm:^6":
|
||||||
|
version: 6.0.4
|
||||||
|
resolution: "@types/swagger-jsdoc@npm:6.0.4"
|
||||||
|
checksum: 10c0/fbe17d91a12e1e60a255b02e6def6877c81b356c75ffcd0e5167fbaf1476e2d6600cd7eea79e6b3e0ff7929dec33ade345147509ed3b98026f63c782b74514f6
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/swagger-ui-express@npm:^4.1.8":
|
||||||
|
version: 4.1.8
|
||||||
|
resolution: "@types/swagger-ui-express@npm:4.1.8"
|
||||||
|
dependencies:
|
||||||
|
"@types/express": "npm:*"
|
||||||
|
"@types/serve-static": "npm:*"
|
||||||
|
checksum: 10c0/9c9e8327c40376b98b6fbd5dd2d722b7b5473e5c168af809431f16b34c948d2d3d44ce2157d2066355e9926ba86416481a30cd1cbcdbb064dd2fedb28442a85a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/tinycolor2@npm:^1":
|
"@types/tinycolor2@npm:^1":
|
||||||
version: 1.4.6
|
version: 1.4.6
|
||||||
resolution: "@types/tinycolor2@npm:1.4.6"
|
resolution: "@types/tinycolor2@npm:1.4.6"
|
||||||
@ -7781,7 +7962,7 @@ __metadata:
|
|||||||
"@libsql/client": "npm:0.14.0"
|
"@libsql/client": "npm:0.14.0"
|
||||||
"@libsql/win32-x64-msvc": "npm:^0.4.7"
|
"@libsql/win32-x64-msvc": "npm:^0.4.7"
|
||||||
"@mistralai/mistralai": "npm:^1.7.5"
|
"@mistralai/mistralai": "npm:^1.7.5"
|
||||||
"@modelcontextprotocol/sdk": "npm:^1.12.3"
|
"@modelcontextprotocol/sdk": "npm:^1.17.0"
|
||||||
"@mozilla/readability": "npm:^0.6.0"
|
"@mozilla/readability": "npm:^0.6.0"
|
||||||
"@notionhq/client": "npm:^2.2.15"
|
"@notionhq/client": "npm:^2.2.15"
|
||||||
"@opentelemetry/api": "npm:^1.9.0"
|
"@opentelemetry/api": "npm:^1.9.0"
|
||||||
@ -7803,7 +7984,10 @@ __metadata:
|
|||||||
"@testing-library/user-event": "npm:^14.6.1"
|
"@testing-library/user-event": "npm:^14.6.1"
|
||||||
"@tryfabric/martian": "npm:^1.2.4"
|
"@tryfabric/martian": "npm:^1.2.4"
|
||||||
"@types/cli-progress": "npm:^3"
|
"@types/cli-progress": "npm:^3"
|
||||||
|
"@types/content-type": "npm:^1.1.9"
|
||||||
|
"@types/cors": "npm:^2.8.19"
|
||||||
"@types/diff": "npm:^7"
|
"@types/diff": "npm:^7"
|
||||||
|
"@types/express": "npm:^5"
|
||||||
"@types/fs-extra": "npm:^11"
|
"@types/fs-extra": "npm:^11"
|
||||||
"@types/lodash": "npm:^4.17.5"
|
"@types/lodash": "npm:^4.17.5"
|
||||||
"@types/markdown-it": "npm:^14"
|
"@types/markdown-it": "npm:^14"
|
||||||
@ -7814,6 +7998,8 @@ __metadata:
|
|||||||
"@types/react-dom": "npm:^19.0.4"
|
"@types/react-dom": "npm:^19.0.4"
|
||||||
"@types/react-infinite-scroll-component": "npm:^5.0.0"
|
"@types/react-infinite-scroll-component": "npm:^5.0.0"
|
||||||
"@types/react-window": "npm:^1"
|
"@types/react-window": "npm:^1"
|
||||||
|
"@types/swagger-jsdoc": "npm:^6"
|
||||||
|
"@types/swagger-ui-express": "npm:^4.1.8"
|
||||||
"@types/tinycolor2": "npm:^1"
|
"@types/tinycolor2": "npm:^1"
|
||||||
"@types/word-extractor": "npm:^1"
|
"@types/word-extractor": "npm:^1"
|
||||||
"@uiw/codemirror-extensions-langs": "npm:^4.23.14"
|
"@uiw/codemirror-extensions-langs": "npm:^4.23.14"
|
||||||
@ -7857,6 +8043,7 @@ __metadata:
|
|||||||
eslint-plugin-react-hooks: "npm:^5.2.0"
|
eslint-plugin-react-hooks: "npm:^5.2.0"
|
||||||
eslint-plugin-simple-import-sort: "npm:^12.1.1"
|
eslint-plugin-simple-import-sort: "npm:^12.1.1"
|
||||||
eslint-plugin-unused-imports: "npm:^4.1.4"
|
eslint-plugin-unused-imports: "npm:^4.1.4"
|
||||||
|
express: "npm:^5.1.0"
|
||||||
fast-diff: "npm:^1.3.0"
|
fast-diff: "npm:^1.3.0"
|
||||||
fast-xml-parser: "npm:^5.2.0"
|
fast-xml-parser: "npm:^5.2.0"
|
||||||
fetch-socks: "npm:1.3.2"
|
fetch-socks: "npm:1.3.2"
|
||||||
@ -7923,6 +8110,8 @@ __metadata:
|
|||||||
strict-url-sanitise: "npm:^0.0.1"
|
strict-url-sanitise: "npm:^0.0.1"
|
||||||
string-width: "npm:^7.2.0"
|
string-width: "npm:^7.2.0"
|
||||||
styled-components: "npm:^6.1.11"
|
styled-components: "npm:^6.1.11"
|
||||||
|
swagger-jsdoc: "npm:^6.2.8"
|
||||||
|
swagger-ui-express: "npm:^5.0.1"
|
||||||
tar: "npm:^7.4.3"
|
tar: "npm:^7.4.3"
|
||||||
tiny-pinyin: "npm:^1.3.2"
|
tiny-pinyin: "npm:^1.3.2"
|
||||||
tokenx: "npm:^1.1.0"
|
tokenx: "npm:^1.1.0"
|
||||||
@ -8991,6 +9180,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"call-me-maybe@npm:^1.0.1":
|
||||||
|
version: 1.0.2
|
||||||
|
resolution: "call-me-maybe@npm:1.0.2"
|
||||||
|
checksum: 10c0/8eff5dbb61141ebb236ed71b4e9549e488bcb5451c48c11e5667d5c75b0532303788a1101e6978cafa2d0c8c1a727805599c2741e3e0982855c9f1d78cd06c9f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"callsites@npm:^3.0.0":
|
"callsites@npm:^3.0.0":
|
||||||
version: 3.1.0
|
version: 3.1.0
|
||||||
resolution: "callsites@npm:3.1.0"
|
resolution: "callsites@npm:3.1.0"
|
||||||
@ -9518,6 +9714,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"commander@npm:6.2.0":
|
||||||
|
version: 6.2.0
|
||||||
|
resolution: "commander@npm:6.2.0"
|
||||||
|
checksum: 10c0/1b701c6726fc2b6c6a7d9ab017be9465153546a05767cdd0e15e9f9a11c07f88f64d47684b90b07e5fb103d173efb6afdf4a21f6d6c4c25f7376bd027d21062c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"commander@npm:7":
|
"commander@npm:7":
|
||||||
version: 7.2.0
|
version: 7.2.0
|
||||||
resolution: "commander@npm:7.2.0"
|
resolution: "commander@npm:7.2.0"
|
||||||
@ -9546,6 +9749,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"commander@npm:^9.4.1":
|
||||||
|
version: 9.5.0
|
||||||
|
resolution: "commander@npm:9.5.0"
|
||||||
|
checksum: 10c0/5f7784fbda2aaec39e89eb46f06a999e00224b3763dc65976e05929ec486e174fe9aac2655f03ba6a5e83875bd173be5283dc19309b7c65954701c02025b3c1d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"compare-version@npm:^0.1.2":
|
"compare-version@npm:^0.1.2":
|
||||||
version: 0.1.2
|
version: 0.1.2
|
||||||
resolution: "compare-version@npm:0.1.2"
|
resolution: "compare-version@npm:0.1.2"
|
||||||
@ -10681,6 +10891,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"doctrine@npm:3.0.0":
|
||||||
|
version: 3.0.0
|
||||||
|
resolution: "doctrine@npm:3.0.0"
|
||||||
|
dependencies:
|
||||||
|
esutils: "npm:^2.0.2"
|
||||||
|
checksum: 10c0/c96bdccabe9d62ab6fea9399fdff04a66e6563c1d6fb3a3a063e8d53c3bb136ba63e84250bbf63d00086a769ad53aef92d2bd483f03f837fc97b71cbee6b2520
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"docx@npm:^9.0.2":
|
"docx@npm:^9.0.2":
|
||||||
version: 9.3.0
|
version: 9.3.0
|
||||||
resolution: "docx@npm:9.3.0"
|
resolution: "docx@npm:9.3.0"
|
||||||
@ -11737,6 +11956,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"eventsource-parser@npm:^3.0.0":
|
||||||
|
version: 3.0.3
|
||||||
|
resolution: "eventsource-parser@npm:3.0.3"
|
||||||
|
checksum: 10c0/2594011630efba56cafafc8ed6bd9a50db8f6d5dd62089b0950346e7961828c16efe07a588bdea3ba79e568fd9246c8163824a2ffaade767e1fdb2270c1fae0b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"eventsource-parser@npm:^3.0.1":
|
"eventsource-parser@npm:^3.0.1":
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
resolution: "eventsource-parser@npm:3.0.1"
|
resolution: "eventsource-parser@npm:3.0.1"
|
||||||
@ -11814,7 +12040,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"express@npm:^5.0.1":
|
"express@npm:^5.0.1, express@npm:^5.1.0":
|
||||||
version: 5.1.0
|
version: 5.1.0
|
||||||
resolution: "express@npm:5.1.0"
|
resolution: "express@npm:5.1.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -12684,6 +12910,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"glob@npm:7.1.6":
|
||||||
|
version: 7.1.6
|
||||||
|
resolution: "glob@npm:7.1.6"
|
||||||
|
dependencies:
|
||||||
|
fs.realpath: "npm:^1.0.0"
|
||||||
|
inflight: "npm:^1.0.4"
|
||||||
|
inherits: "npm:2"
|
||||||
|
minimatch: "npm:^3.0.4"
|
||||||
|
once: "npm:^1.3.0"
|
||||||
|
path-is-absolute: "npm:^1.0.0"
|
||||||
|
checksum: 10c0/2575cce9306ac534388db751f0aa3e78afedb6af8f3b529ac6b2354f66765545145dba8530abf7bff49fb399a047d3f9b6901c38ee4c9503f592960d9af67763
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.12, glob@npm:^10.3.7, glob@npm:^10.4.1":
|
"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.12, glob@npm:^10.3.7, glob@npm:^10.4.1":
|
||||||
version: 10.4.5
|
version: 10.4.5
|
||||||
resolution: "glob@npm:10.4.5"
|
resolution: "glob@npm:10.4.5"
|
||||||
@ -14622,6 +14862,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.get@npm:^4.4.2":
|
||||||
|
version: 4.4.2
|
||||||
|
resolution: "lodash.get@npm:4.4.2"
|
||||||
|
checksum: 10c0/48f40d471a1654397ed41685495acb31498d5ed696185ac8973daef424a749ca0c7871bf7b665d5c14f5cc479394479e0307e781f61d5573831769593411be6e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash.isequal@npm:^4.5.0":
|
"lodash.isequal@npm:^4.5.0":
|
||||||
version: 4.5.0
|
version: 4.5.0
|
||||||
resolution: "lodash.isequal@npm:4.5.0"
|
resolution: "lodash.isequal@npm:4.5.0"
|
||||||
@ -14636,6 +14883,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.mergewith@npm:^4.6.2":
|
||||||
|
version: 4.6.2
|
||||||
|
resolution: "lodash.mergewith@npm:4.6.2"
|
||||||
|
checksum: 10c0/4adbed65ff96fd65b0b3861f6899f98304f90fd71e7f1eb36c1270e05d500ee7f5ec44c02ef979b5ddbf75c0a0b9b99c35f0ad58f4011934c4d4e99e5200b3b5
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash@npm:^4.17.15, lodash@npm:^4.17.21":
|
"lodash@npm:^4.17.15, lodash@npm:^4.17.21":
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
resolution: "lodash@npm:4.17.21"
|
resolution: "lodash@npm:4.17.21"
|
||||||
@ -20110,6 +20364,51 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"swagger-jsdoc@npm:^6.2.8":
|
||||||
|
version: 6.2.8
|
||||||
|
resolution: "swagger-jsdoc@npm:6.2.8"
|
||||||
|
dependencies:
|
||||||
|
commander: "npm:6.2.0"
|
||||||
|
doctrine: "npm:3.0.0"
|
||||||
|
glob: "npm:7.1.6"
|
||||||
|
lodash.mergewith: "npm:^4.6.2"
|
||||||
|
swagger-parser: "npm:^10.0.3"
|
||||||
|
yaml: "npm:2.0.0-1"
|
||||||
|
bin:
|
||||||
|
swagger-jsdoc: bin/swagger-jsdoc.js
|
||||||
|
checksum: 10c0/7e20f08e8d90cc1e787cd82c096291cf12533359f89c70fbe4295a01f7c4734f2e82a03ba94027127bcd3da04b817abfe979f00d00ef0cd8283e449250a66215
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"swagger-parser@npm:^10.0.3":
|
||||||
|
version: 10.0.3
|
||||||
|
resolution: "swagger-parser@npm:10.0.3"
|
||||||
|
dependencies:
|
||||||
|
"@apidevtools/swagger-parser": "npm:10.0.3"
|
||||||
|
checksum: 10c0/d1a5c05f651f21a23508a36416071630b83e91dfffd52a6d44b06ca2cd1b86304c0dd2f4c04526c999b70062fa89bde3f5d54a1436626f4350590b6c6265a098
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"swagger-ui-dist@npm:>=5.0.0":
|
||||||
|
version: 5.27.0
|
||||||
|
resolution: "swagger-ui-dist@npm:5.27.0"
|
||||||
|
dependencies:
|
||||||
|
"@scarf/scarf": "npm:=1.4.0"
|
||||||
|
checksum: 10c0/8f50e67b9b92a6953f28954cda137af82550ab236813c984a6f71ec314e14121a960187e62fd5db42fc1a5763d7e687ad990f4f921c7e96d3e02b06bf631d8ae
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"swagger-ui-express@npm:^5.0.1":
|
||||||
|
version: 5.0.1
|
||||||
|
resolution: "swagger-ui-express@npm:5.0.1"
|
||||||
|
dependencies:
|
||||||
|
swagger-ui-dist: "npm:>=5.0.0"
|
||||||
|
peerDependencies:
|
||||||
|
express: ">=4.0.0 || >=5.0.0-beta"
|
||||||
|
checksum: 10c0/dbe9830caef7fe455241e44e74958bac62642997e4341c1b0f38a3d684d19a4a81b431217c656792d99f046a1b5f261abf7783ede0afe41098cd4450401f6fd1
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"symbol-tree@npm:^3.2.4":
|
"symbol-tree@npm:^3.2.4":
|
||||||
version: 3.2.4
|
version: 3.2.4
|
||||||
resolution: "symbol-tree@npm:3.2.4"
|
resolution: "symbol-tree@npm:3.2.4"
|
||||||
@ -21104,6 +21403,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"validator@npm:^13.7.0":
|
||||||
|
version: 13.15.15
|
||||||
|
resolution: "validator@npm:13.15.15"
|
||||||
|
checksum: 10c0/f5349d1fbb9cc36f9f6c5dab1880764ddad1d0d2b084e2a71e5964f7de1635d20e406611559df9a3db24828ce775cbee5e3b6dd52f0d555a61939ed7ea5990bd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"vary@npm:^1, vary@npm:^1.1.2":
|
"vary@npm:^1, vary@npm:^1.1.2":
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
resolution: "vary@npm:1.1.2"
|
resolution: "vary@npm:1.1.2"
|
||||||
@ -21820,6 +22126,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"yaml@npm:2.0.0-1":
|
||||||
|
version: 2.0.0-1
|
||||||
|
resolution: "yaml@npm:2.0.0-1"
|
||||||
|
checksum: 10c0/e76eba2fbae37cd3e5bff057184be7cdca849895149d2f5660386871a501d76d2e1ec5906c48269a9fe798f214df31d342675b37bcd9d09af7c12eb6fb46a740
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"yaml@npm:^2.2.1, yaml@npm:^2.7.0":
|
"yaml@npm:^2.2.1, yaml@npm:^2.7.0":
|
||||||
version: 2.7.1
|
version: 2.7.1
|
||||||
resolution: "yaml@npm:2.7.1"
|
resolution: "yaml@npm:2.7.1"
|
||||||
@ -21868,6 +22181,23 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"z-schema@npm:^5.0.1":
|
||||||
|
version: 5.0.5
|
||||||
|
resolution: "z-schema@npm:5.0.5"
|
||||||
|
dependencies:
|
||||||
|
commander: "npm:^9.4.1"
|
||||||
|
lodash.get: "npm:^4.4.2"
|
||||||
|
lodash.isequal: "npm:^4.5.0"
|
||||||
|
validator: "npm:^13.7.0"
|
||||||
|
dependenciesMeta:
|
||||||
|
commander:
|
||||||
|
optional: true
|
||||||
|
bin:
|
||||||
|
z-schema: bin/z-schema
|
||||||
|
checksum: 10c0/e4c812cfe6468c19b2a21d07d4ff8fb70359062d33400b45f89017eaa3efe9d51e85963f2b115eaaa99a16b451782249bf9b1fa8b31d35cc473e7becb3e44264
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"zip-stream@npm:^6.0.1":
|
"zip-stream@npm:^6.0.1":
|
||||||
version: 6.0.1
|
version: 6.0.1
|
||||||
resolution: "zip-stream@npm:6.0.1"
|
resolution: "zip-stream@npm:6.0.1"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user