mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-11 16:39:15 +08:00
refactor: remove api server
This commit is contained in:
parent
1efefad3ee
commit
c76aa03566
@ -73,15 +73,12 @@
|
||||
"@libsql/client": "0.14.0",
|
||||
"@libsql/win32-x64-msvc": "^0.4.7",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"express": "^5.1.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"jsdom": "26.1.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"officeparser": "^4.2.0",
|
||||
"os-proxy-config": "^1.1.2",
|
||||
"selection-hook": "^1.0.8",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"turndown": "7.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -145,7 +142,6 @@
|
||||
"@types/content-type": "^1.1.9",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/diff": "^7",
|
||||
"@types/express": "^5",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/markdown-it": "^14",
|
||||
@ -156,8 +152,6 @@
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@types/react-window": "^1",
|
||||
"@types/swagger-jsdoc": "^6",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/tinycolor2": "^1",
|
||||
"@types/word-extractor": "^1",
|
||||
"@uiw/codemirror-extensions-langs": "^4.23.14",
|
||||
|
||||
@ -273,11 +273,5 @@ export enum IpcChannel {
|
||||
TRACE_SET_TITLE = 'trace:setTitle',
|
||||
TRACE_ADD_END_MESSAGE = 'trace:addEndMessage',
|
||||
TRACE_CLEAN_LOCAL_DATA = 'trace:cleanLocalData',
|
||||
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'
|
||||
TRACE_ADD_STREAM_MESSAGE = 'trace:addStreamMessage'
|
||||
}
|
||||
|
||||
@ -1,128 +0,0 @@
|
||||
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 }
|
||||
@ -1,67 +0,0 @@
|
||||
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 = {
|
||||
enabled: settings?.apiServer?.enabled ?? false,
|
||||
port: settings?.apiServer?.port ?? 23333,
|
||||
host: 'localhost',
|
||||
apiKey: generatedKey
|
||||
}
|
||||
} else {
|
||||
this._config = {
|
||||
enabled: settings?.apiServer?.enabled ?? false,
|
||||
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 = {
|
||||
enabled: false,
|
||||
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()
|
||||
@ -1,2 +0,0 @@
|
||||
export { config } from './config'
|
||||
export { apiServer } from './server'
|
||||
@ -1,25 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
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 })
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1,206 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,225 +0,0 @@
|
||||
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 }
|
||||
@ -1,153 +0,0 @@
|
||||
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 }
|
||||
@ -1,66 +0,0 @@
|
||||
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 }
|
||||
@ -1,65 +0,0 @@
|
||||
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()
|
||||
@ -1,222 +0,0 @@
|
||||
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()
|
||||
@ -1,251 +0,0 @@
|
||||
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()
|
||||
@ -1,111 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
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,7 +27,6 @@ import { registerShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import process from 'node:process'
|
||||
import { apiServerService } from './services/ApiServerService'
|
||||
|
||||
const logger = loggerService.withContext('MainEntry')
|
||||
|
||||
@ -140,17 +139,6 @@ if (!app.requestSingleInstanceLock()) {
|
||||
|
||||
//start selection assistant service
|
||||
initSelectionService()
|
||||
|
||||
// Start API server if enabled
|
||||
try {
|
||||
const config = await apiServerService.getCurrentConfig()
|
||||
logger.info('API server config:', config)
|
||||
if (config.enabled) {
|
||||
await apiServerService.start()
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to check/start API server:', error)
|
||||
}
|
||||
})
|
||||
|
||||
registerProtocolClient(app)
|
||||
@ -196,7 +184,6 @@ if (!app.requestSingleInstanceLock()) {
|
||||
// 简单的资源清理,不阻塞退出流程
|
||||
try {
|
||||
await mcpService.cleanup()
|
||||
await apiServerService.stop()
|
||||
} catch (error) {
|
||||
logger.warn('Error cleaning up MCP service:', error as Error)
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types'
|
||||
import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron'
|
||||
import { Notification } from 'src/renderer/src/types/notification'
|
||||
|
||||
import { apiServerService } from './services/ApiServerService'
|
||||
import appService from './services/AppService'
|
||||
import AppUpdater from './services/AppUpdater'
|
||||
import BackupManager from './services/BackupManager'
|
||||
@ -696,7 +695,4 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
(_, spanId: string, modelName: string, context: string, msg: any) =>
|
||||
addStreamMessage(spanId, modelName, context, msg)
|
||||
)
|
||||
|
||||
// API Server
|
||||
apiServerService.registerIpcHandlers()
|
||||
}
|
||||
|
||||
@ -1,108 +0,0 @@
|
||||
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()
|
||||
@ -1,423 +0,0 @@
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import { setApiServerApiKey, setApiServerEnabled, setApiServerPort } from '@renderer/store/settings'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { Button, Input, InputNumber, Tooltip, Typography } from 'antd'
|
||||
import { Copy, ExternalLink, Play, RotateCcw, Square } from 'lucide-react'
|
||||
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 ApiServerSettings: FC = () => {
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// API Server state with proper defaults
|
||||
const apiServerConfig = useSelector((state: RootState) => 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 {
|
||||
dispatch(setApiServerEnabled(enabled))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
const openApiDocs = () => {
|
||||
if (apiServerRunning) {
|
||||
window.open(`http://localhost:${apiServerConfig.port}/api-docs`, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container theme={theme}>
|
||||
{/* Header Section */}
|
||||
<HeaderSection>
|
||||
<HeaderContent>
|
||||
<Title level={3} style={{ margin: 0, marginBottom: 8 }}>
|
||||
{t('apiServer.title')}
|
||||
</Title>
|
||||
<Text type="secondary">{t('apiServer.description')}</Text>
|
||||
</HeaderContent>
|
||||
{apiServerRunning && (
|
||||
<Button type="primary" icon={<ExternalLink size={14} />} onClick={openApiDocs}>
|
||||
{t('apiServer.documentation.title')}
|
||||
</Button>
|
||||
)}
|
||||
</HeaderSection>
|
||||
|
||||
{/* Server Control Panel with integrated configuration */}
|
||||
<ServerControlPanel $status={apiServerRunning}>
|
||||
<StatusSection>
|
||||
<StatusIndicator $status={apiServerRunning} />
|
||||
<StatusContent>
|
||||
<StatusText $status={apiServerRunning}>
|
||||
{apiServerRunning ? t('apiServer.status.running') : t('apiServer.status.stopped')}
|
||||
</StatusText>
|
||||
<StatusSubtext>
|
||||
{apiServerRunning ? `http://localhost:${apiServerConfig.port}` : t('apiServer.fields.port.description')}
|
||||
</StatusSubtext>
|
||||
</StatusContent>
|
||||
</StatusSection>
|
||||
|
||||
<ControlSection>
|
||||
{apiServerRunning && (
|
||||
<Tooltip title={t('apiServer.actions.restart.tooltip')}>
|
||||
<RestartButton
|
||||
$loading={apiServerLoading}
|
||||
onClick={apiServerLoading ? undefined : handleApiServerRestart}>
|
||||
<RotateCcw size={14} />
|
||||
<span>{t('apiServer.actions.restart.button')}</span>
|
||||
</RestartButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Port input when server is stopped */}
|
||||
{!apiServerRunning && (
|
||||
<StyledInputNumber
|
||||
value={apiServerConfig.port}
|
||||
onChange={(value) => handlePortChange(String(value || 23333))}
|
||||
min={1000}
|
||||
max={65535}
|
||||
disabled={apiServerRunning}
|
||||
placeholder="23333"
|
||||
size="middle"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tooltip title={apiServerRunning ? t('apiServer.actions.stop') : t('apiServer.actions.start')}>
|
||||
{apiServerRunning ? (
|
||||
<StopButton
|
||||
$loading={apiServerLoading}
|
||||
onClick={apiServerLoading ? undefined : () => handleApiServerToggle(false)}>
|
||||
<Square size={20} style={{ color: 'var(--color-status-error)' }} />
|
||||
</StopButton>
|
||||
) : (
|
||||
<StartButton
|
||||
$loading={apiServerLoading}
|
||||
onClick={apiServerLoading ? undefined : () => handleApiServerToggle(true)}>
|
||||
<Play size={20} style={{ color: 'var(--color-status-success)' }} />
|
||||
</StartButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
</ControlSection>
|
||||
</ServerControlPanel>
|
||||
|
||||
{/* API Key Configuration */}
|
||||
<ConfigurationField>
|
||||
<FieldLabel>{t('apiServer.fields.apiKey.label')}</FieldLabel>
|
||||
<FieldDescription>{t('apiServer.fields.apiKey.description')}</FieldDescription>
|
||||
|
||||
<StyledInput
|
||||
value={apiServerConfig.apiKey}
|
||||
readOnly
|
||||
placeholder={t('apiServer.fields.apiKey.placeholder')}
|
||||
size="middle"
|
||||
suffix={
|
||||
<InputButtonContainer>
|
||||
{!apiServerRunning && (
|
||||
<RegenerateButton onClick={regenerateApiKey} disabled={apiServerRunning} type="link">
|
||||
{t('apiServer.actions.regenerate')}
|
||||
</RegenerateButton>
|
||||
)}
|
||||
<Tooltip title={t('apiServer.fields.apiKey.copyTooltip')}>
|
||||
<InputButton icon={<Copy size={14} />} onClick={copyApiKey} disabled={!apiServerConfig.apiKey} />
|
||||
</Tooltip>
|
||||
</InputButtonContainer>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Authorization header info */}
|
||||
<AuthHeaderSection>
|
||||
<FieldLabel>{t('apiServer.authHeader.title')}</FieldLabel>
|
||||
<StyledInput
|
||||
style={{ height: 38 }}
|
||||
value={`Authorization: Bearer ${apiServerConfig.apiKey || 'your-api-key'}`}
|
||||
readOnly
|
||||
size="middle"
|
||||
/>
|
||||
</AuthHeaderSection>
|
||||
</ConfigurationField>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
// Styled Components
|
||||
const Container = styled(SettingContainer)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
`
|
||||
|
||||
const HeaderSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
`
|
||||
|
||||
const HeaderContent = styled.div`
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const ServerControlPanel = styled.div<{ $status: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-radius: 8px;
|
||||
background: var(--color-background);
|
||||
border: 1px solid ${(props) => (props.$status ? 'var(--color-status-success)' : 'var(--color-border)')};
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 16px;
|
||||
`
|
||||
|
||||
const StatusSection = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
`
|
||||
|
||||
const StatusIndicator = styled.div<{ $status: boolean }>`
|
||||
position: relative;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: ${(props) => (props.$status ? 'var(--color-status-success)' : 'var(--color-status-error)')};
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -3px;
|
||||
border-radius: 50%;
|
||||
background: ${(props) => (props.$status ? 'var(--color-status-success)' : 'var(--color-status-error)')};
|
||||
opacity: 0.2;
|
||||
animation: ${(props) => (props.$status ? 'pulse 2s infinite' : 'none')};
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.2;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.5);
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const StatusContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
`
|
||||
|
||||
const StatusText = styled.div<{ $status: boolean }>`
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: ${(props) => (props.$status ? 'var(--color-status-success)' : 'var(--color-text-1)')};
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const StatusSubtext = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const ControlSection = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
`
|
||||
|
||||
const RestartButton = styled.div<{ $loading: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--color-text-2);
|
||||
cursor: ${(props) => (props.$loading ? 'not-allowed' : 'pointer')};
|
||||
opacity: ${(props) => (props.$loading ? 0.5 : 1)};
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => (props.$loading ? 'var(--color-text-2)' : 'var(--color-primary)')};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledInputNumber = styled(InputNumber)`
|
||||
width: 80px;
|
||||
border-radius: 6px;
|
||||
border: 1.5px solid var(--color-border);
|
||||
margin-right: 5px;
|
||||
`
|
||||
|
||||
const StartButton = styled.div<{ $loading: boolean }>`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: ${(props) => (props.$loading ? 'not-allowed' : 'pointer')};
|
||||
opacity: ${(props) => (props.$loading ? 0.5 : 1)};
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: ${(props) => (props.$loading ? 'scale(1)' : 'scale(1.1)')};
|
||||
}
|
||||
`
|
||||
|
||||
const StopButton = styled.div<{ $loading: boolean }>`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: ${(props) => (props.$loading ? 'not-allowed' : 'pointer')};
|
||||
opacity: ${(props) => (props.$loading ? 0.5 : 1)};
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: ${(props) => (props.$loading ? 'scale(1)' : 'scale(1.1)')};
|
||||
}
|
||||
`
|
||||
|
||||
const ConfigurationField = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
`
|
||||
|
||||
const FieldLabel = styled.div`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const FieldDescription = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const StyledInput = styled(Input)`
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
border: 1.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const InputButtonContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
`
|
||||
|
||||
const InputButton = styled(Button)`
|
||||
border: none;
|
||||
padding: 0 4px;
|
||||
background: transparent;
|
||||
`
|
||||
|
||||
const RegenerateButton = styled(Button)`
|
||||
padding: 0 4px;
|
||||
font-size: 12px;
|
||||
height: auto;
|
||||
line-height: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
`
|
||||
|
||||
const AuthHeaderSection = styled.div`
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
export default ApiServerSettings
|
||||
@ -1 +0,0 @@
|
||||
export { default as ApiServerSettings } from './ApiServerSettings'
|
||||
@ -1,12 +1,11 @@
|
||||
import { GlobalOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import { FileCode, Server } from 'lucide-react'
|
||||
import { FileCode } from 'lucide-react'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ApiServerSettings from './ApiServerSettings/ApiServerSettings'
|
||||
import PreprocessSettings from './PreprocessSettings'
|
||||
import WebSearchSettings from './WebSearchSettings'
|
||||
|
||||
@ -17,8 +16,7 @@ const ToolSettings: FC = () => {
|
||||
const [menu, setMenu] = useState<string>(_menu)
|
||||
const menuItems = [
|
||||
{ key: 'web-search', title: 'settings.tool.websearch.title', icon: <GlobalOutlined style={{ fontSize: 16 }} /> },
|
||||
{ key: 'preprocess', title: 'settings.tool.preprocess.title', icon: <FileCode size={16} /> },
|
||||
{ key: 'api-server', title: 'apiServer.title', icon: <Server size={16} /> }
|
||||
{ key: 'preprocess', title: 'settings.tool.preprocess.title', icon: <FileCode size={16} /> }
|
||||
]
|
||||
|
||||
_menu = menu
|
||||
@ -39,7 +37,6 @@ const ToolSettings: FC = () => {
|
||||
</MenuList>
|
||||
{menu == 'web-search' && <WebSearchSettings />}
|
||||
{menu == 'preprocess' && <PreprocessSettings />}
|
||||
{menu == 'api-server' && <ApiServerSettings />}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
308
yarn.lock
308
yarn.lock
@ -244,48 +244,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 3.1.2
|
||||
resolution: "@asamuzakjp/css-color@npm:3.1.2"
|
||||
@ -3140,13 +3098,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.1.0
|
||||
resolution: "@kangfenmao/keyv-storage@npm:0.1.0"
|
||||
@ -4962,13 +4913,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.11.0
|
||||
resolution: "@selderee/plugin-htmlparser2@npm:0.11.0"
|
||||
@ -6123,16 +6067,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 6.0.3
|
||||
resolution: "@types/cacheable-request@npm:6.0.3"
|
||||
@ -6163,15 +6097,6 @@ __metadata:
|
||||
languageName: node
|
||||
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"
|
||||
@ -6499,29 +6424,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 9.0.13
|
||||
resolution: "@types/fs-extra@npm:9.0.13"
|
||||
@ -6574,14 +6476,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@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":
|
||||
"@types/json-schema@npm:^7.0.15":
|
||||
version: 7.0.15
|
||||
resolution: "@types/json-schema@npm:7.0.15"
|
||||
checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db
|
||||
@ -6676,13 +6571,6 @@ __metadata:
|
||||
languageName: node
|
||||
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:*":
|
||||
version: 2.1.0
|
||||
resolution: "@types/ms@npm:2.1.0"
|
||||
@ -6735,20 +6623,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 19.1.2
|
||||
resolution: "@types/react-dom@npm:19.1.2"
|
||||
@ -6801,27 +6675,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 4.2.5
|
||||
resolution: "@types/stylis@npm:4.2.5"
|
||||
@ -6829,23 +6682,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.4.6
|
||||
resolution: "@types/tinycolor2@npm:1.4.6"
|
||||
@ -7871,7 +7707,6 @@ __metadata:
|
||||
"@types/content-type": "npm:^1.1.9"
|
||||
"@types/cors": "npm:^2.8.19"
|
||||
"@types/diff": "npm:^7"
|
||||
"@types/express": "npm:^5"
|
||||
"@types/fs-extra": "npm:^11"
|
||||
"@types/lodash": "npm:^4.17.5"
|
||||
"@types/markdown-it": "npm:^14"
|
||||
@ -7882,8 +7717,6 @@ __metadata:
|
||||
"@types/react-dom": "npm:^19.0.4"
|
||||
"@types/react-infinite-scroll-component": "npm:^5.0.0"
|
||||
"@types/react-window": "npm:^1"
|
||||
"@types/swagger-jsdoc": "npm:^6"
|
||||
"@types/swagger-ui-express": "npm:^4.1.8"
|
||||
"@types/tinycolor2": "npm:^1"
|
||||
"@types/word-extractor": "npm:^1"
|
||||
"@uiw/codemirror-extensions-langs": "npm:^4.23.14"
|
||||
@ -7927,7 +7760,6 @@ __metadata:
|
||||
eslint-plugin-react-hooks: "npm:^5.2.0"
|
||||
eslint-plugin-simple-import-sort: "npm:^12.1.1"
|
||||
eslint-plugin-unused-imports: "npm:^4.1.4"
|
||||
express: "npm:^5.1.0"
|
||||
fast-diff: "npm:^1.3.0"
|
||||
fast-xml-parser: "npm:^5.2.0"
|
||||
fetch-socks: "npm:1.3.2"
|
||||
@ -7994,8 +7826,6 @@ __metadata:
|
||||
strict-url-sanitise: "npm:^0.0.1"
|
||||
string-width: "npm:^7.2.0"
|
||||
styled-components: "npm:^6.1.11"
|
||||
swagger-jsdoc: "npm:^6.2.8"
|
||||
swagger-ui-express: "npm:^5.0.1"
|
||||
tar: "npm:^7.4.3"
|
||||
tiny-pinyin: "npm:^1.3.2"
|
||||
tokenx: "npm:^1.1.0"
|
||||
@ -9052,13 +8882,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 3.1.0
|
||||
resolution: "callsites@npm:3.1.0"
|
||||
@ -9586,13 +9409,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 7.2.0
|
||||
resolution: "commander@npm:7.2.0"
|
||||
@ -9621,13 +9437,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 0.1.2
|
||||
resolution: "compare-version@npm:0.1.2"
|
||||
@ -10763,15 +10572,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 9.3.0
|
||||
resolution: "docx@npm:9.3.0"
|
||||
@ -11912,7 +11712,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"express@npm:^5.0.1, express@npm:^5.1.0":
|
||||
"express@npm:^5.0.1":
|
||||
version: 5.1.0
|
||||
resolution: "express@npm:5.1.0"
|
||||
dependencies:
|
||||
@ -12775,20 +12575,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 10.4.5
|
||||
resolution: "glob@npm:10.4.5"
|
||||
@ -14727,13 +14513,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 4.5.0
|
||||
resolution: "lodash.isequal@npm:4.5.0"
|
||||
@ -14748,13 +14527,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 4.17.21
|
||||
resolution: "lodash@npm:4.17.21"
|
||||
@ -20212,51 +19984,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 3.2.4
|
||||
resolution: "symbol-tree@npm:3.2.4"
|
||||
@ -21258,13 +20985,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.1.2
|
||||
resolution: "vary@npm:1.1.2"
|
||||
@ -21981,13 +21701,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 2.7.1
|
||||
resolution: "yaml@npm:2.7.1"
|
||||
@ -22036,23 +21749,6 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 6.0.1
|
||||
resolution: "zip-stream@npm:6.0.1"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user