mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-13 21:57:30 +08:00
refactor: rename 'apiServer' to 'apiGateway' (#12352)
This commit is contained in:
parent
9607ac0798
commit
fcbf1a1581
@ -336,13 +336,13 @@ export enum IpcChannel {
|
||||
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_Ready = 'api-server:ready',
|
||||
ApiGateway_Start = 'api-server:start',
|
||||
ApiGateway_Stop = 'api-server:stop',
|
||||
ApiGateway_Restart = 'api-server:restart',
|
||||
ApiGateway_GetStatus = 'api-server:get-status',
|
||||
ApiGateway_Ready = 'api-server:ready',
|
||||
// NOTE: This api is not be used.
|
||||
ApiServer_GetConfig = 'api-server:get-config',
|
||||
ApiGateway_GetConfig = 'api-server:get-config',
|
||||
|
||||
// Anthropic OAuth
|
||||
Anthropic_StartOAuthFlow = 'anthropic:start-oauth-flow',
|
||||
|
||||
@ -7,7 +7,7 @@ export const documentExts = ['.pdf', '.doc', '.docx', '.pptx', '.xlsx', '.odt',
|
||||
export const thirdPartyApplicationExts = ['.draftsExport']
|
||||
export const bookExts = ['.epub']
|
||||
|
||||
export const API_SERVER_DEFAULTS = {
|
||||
export const API_GATEWAY_DEFAULTS = {
|
||||
HOST: '127.0.0.1',
|
||||
PORT: 23333
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ import { messagesProviderRoutes, messagesRoutes } from './routes/messages'
|
||||
import { modelsRoutes } from './routes/models'
|
||||
import { responsesRoutes } from './routes/responses'
|
||||
|
||||
const logger = loggerService.withContext('ApiServer')
|
||||
const logger = loggerService.withContext('ApiGateway')
|
||||
|
||||
const extendMessagesTimeout: express.RequestHandler = (req, res, next) => {
|
||||
req.setTimeout(LONG_POLL_TIMEOUT_MS)
|
||||
@ -1,4 +1,4 @@
|
||||
import { API_SERVER_DEFAULTS } from '@shared/config/constant'
|
||||
import { API_GATEWAY_DEFAULTS } from '@shared/config/constant'
|
||||
import type { ApiGatewayConfig, GatewayEndpoint } from '@types'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
@ -19,19 +19,19 @@ class ConfigManager {
|
||||
async load(): Promise<ApiGatewayConfig> {
|
||||
try {
|
||||
const settings = await reduxService.select('state.settings')
|
||||
const serverSettings = settings?.apiServer
|
||||
const serverSettings = settings?.apiGateway
|
||||
let apiKey = serverSettings?.apiKey
|
||||
if (!apiKey || apiKey.trim() === '') {
|
||||
apiKey = this.generateApiKey()
|
||||
await reduxService.dispatch({
|
||||
type: 'settings/setApiServerApiKey',
|
||||
type: 'settings/setApiGatewayApiKey',
|
||||
payload: apiKey
|
||||
})
|
||||
}
|
||||
this._config = {
|
||||
enabled: serverSettings?.enabled ?? false,
|
||||
port: serverSettings?.port ?? API_SERVER_DEFAULTS.PORT,
|
||||
host: serverSettings?.host ?? API_SERVER_DEFAULTS.HOST,
|
||||
port: serverSettings?.port ?? API_GATEWAY_DEFAULTS.PORT,
|
||||
host: serverSettings?.host ?? API_GATEWAY_DEFAULTS.HOST,
|
||||
apiKey: apiKey,
|
||||
modelGroups: serverSettings?.modelGroups ?? [],
|
||||
enabledEndpoints: serverSettings?.enabledEndpoints ?? DEFAULT_ENABLED_ENDPOINTS,
|
||||
@ -42,8 +42,8 @@ class ConfigManager {
|
||||
logger.warn('Failed to load config from Redux, using defaults', { error })
|
||||
this._config = {
|
||||
enabled: false,
|
||||
port: API_SERVER_DEFAULTS.PORT,
|
||||
host: API_SERVER_DEFAULTS.HOST,
|
||||
port: API_GATEWAY_DEFAULTS.PORT,
|
||||
host: API_GATEWAY_DEFAULTS.HOST,
|
||||
apiKey: this.generateApiKey(),
|
||||
modelGroups: [],
|
||||
enabledEndpoints: DEFAULT_ENABLED_ENDPOINTS,
|
||||
2
src/main/apiGateway/index.ts
Normal file
2
src/main/apiGateway/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { config } from './config'
|
||||
export { apiGateway } from './server'
|
||||
@ -2,7 +2,7 @@ import type { NextFunction, Request, Response } from 'express'
|
||||
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerErrorHandler')
|
||||
const logger = loggerService.withContext('ApiGatewayErrorHandler')
|
||||
|
||||
// oxlint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
@ -182,11 +182,11 @@ const swaggerOptions: swaggerJSDoc.Options = {
|
||||
},
|
||||
// Only include gateway/external API routes (exclude internal APIs like agents)
|
||||
apis: [
|
||||
'./src/main/apiServer/routes/chat.ts',
|
||||
'./src/main/apiServer/routes/messages.ts',
|
||||
'./src/main/apiServer/routes/models.ts',
|
||||
'./src/main/apiServer/routes/mcp.ts',
|
||||
'./src/main/apiServer/app.ts'
|
||||
'./src/main/apiGateway/routes/chat.ts',
|
||||
'./src/main/apiGateway/routes/messages.ts',
|
||||
'./src/main/apiGateway/routes/models.ts',
|
||||
'./src/main/apiGateway/routes/mcp.ts',
|
||||
'./src/main/apiGateway/app.ts'
|
||||
]
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import type { Request, Response } from 'express'
|
||||
|
||||
import type { ValidationRequest } from '../validators/zodValidator'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerAgentsHandlers')
|
||||
const logger = loggerService.withContext('ApiGatewayAgentsHandlers')
|
||||
|
||||
const modelValidationErrorBody = (error: AgentModelValidationError) => ({
|
||||
error: {
|
||||
@ -1,14 +1,14 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { MESSAGE_STREAM_TIMEOUT_MS } from '@main/apiServer/config/timeouts'
|
||||
import { MESSAGE_STREAM_TIMEOUT_MS } from '@main/apiGateway/config/timeouts'
|
||||
import {
|
||||
createStreamAbortController,
|
||||
STREAM_TIMEOUT_REASON,
|
||||
type StreamAbortController
|
||||
} from '@main/apiServer/utils/createStreamAbortController'
|
||||
} from '@main/apiGateway/utils/createStreamAbortController'
|
||||
import { agentService, sessionMessageService, sessionService } from '@main/services/agents'
|
||||
import type { Request, Response } from 'express'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerMessagesHandlers')
|
||||
const logger = loggerService.withContext('ApiGatewayMessagesHandlers')
|
||||
|
||||
// Helper function to verify agent and session exist and belong together
|
||||
const verifyAgentAndSession = async (agentId: string, sessionId: string) => {
|
||||
@ -6,7 +6,7 @@ import type { Request, Response } from 'express'
|
||||
|
||||
import type { ValidationRequest } from '../validators/zodValidator'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerSessionsHandlers')
|
||||
const logger = loggerService.withContext('ApiGatewaySessionsHandlers')
|
||||
|
||||
const modelValidationErrorBody = (error: AgentModelValidationError) => ({
|
||||
error: {
|
||||
@ -3,7 +3,7 @@ import type { Request, Response } from 'express'
|
||||
import { agentService } from '../../../../services/agents'
|
||||
import { loggerService } from '../../../../services/LoggerService'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerMiddleware')
|
||||
const logger = loggerService.withContext('ApiGatewayMiddleware')
|
||||
|
||||
// Since Zod validators handle their own errors, this is now a pass-through
|
||||
export const handleValidationErrors = (_req: Request, _res: Response, next: any): void => {
|
||||
@ -6,7 +6,7 @@ import type { ExtendedChatCompletionCreateParams } from '../adapters'
|
||||
import { processMessage } from '../services/ProxyStreamService'
|
||||
import { validateModelId } from '../utils'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerChatRoutes')
|
||||
const logger = loggerService.withContext('ApiGatewayChatRoutes')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
@ -4,7 +4,7 @@ import express from 'express'
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
import { mcpApiService } from '../services/mcp'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerMCPRoutes')
|
||||
const logger = loggerService.withContext('ApiGatewayMCPRoutes')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
@ -39,7 +39,7 @@ function shouldUseDirectAnthropic(provider: Provider, modelId: string): boolean
|
||||
return isModelAnthropicCompatible(provider, modelId)
|
||||
}
|
||||
|
||||
const logger = loggerService.withContext('ApiServerMessagesRoutes')
|
||||
const logger = loggerService.withContext('ApiGatewayMessagesRoutes')
|
||||
|
||||
const router = express.Router()
|
||||
const providerRouter = express.Router({ mergeParams: true })
|
||||
@ -6,7 +6,7 @@ import express from 'express'
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
import { modelsService } from '../services/models'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerModelsRoutes')
|
||||
const logger = loggerService.withContext('ApiGatewayModelsRoutes')
|
||||
|
||||
const router = express
|
||||
.Router()
|
||||
@ -13,7 +13,7 @@ import { isModelOpenAIResponsesCompatible, validateModelId } from '../utils'
|
||||
// Use SDK namespace types
|
||||
type ResponseCreateParams = OpenAI.Responses.ResponseCreateParams
|
||||
|
||||
const logger = loggerService.withContext('ApiServerResponsesRoutes')
|
||||
const logger = loggerService.withContext('ApiGatewayResponsesRoutes')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
@ -7,13 +7,13 @@ import { windowService } from '../services/WindowService'
|
||||
import { app } from './app'
|
||||
import { config } from './config'
|
||||
|
||||
const logger = loggerService.withContext('ApiServer')
|
||||
const logger = loggerService.withContext('ApiGateway')
|
||||
|
||||
const GLOBAL_REQUEST_TIMEOUT_MS = 5 * 60_000
|
||||
const GLOBAL_HEADERS_TIMEOUT_MS = GLOBAL_REQUEST_TIMEOUT_MS + 5_000
|
||||
const GLOBAL_KEEPALIVE_TIMEOUT_MS = 60_000
|
||||
|
||||
export class ApiServer {
|
||||
export class ApiGateway {
|
||||
private server: ReturnType<typeof createServer> | null = null
|
||||
|
||||
async start(): Promise<void> {
|
||||
@ -43,7 +43,7 @@ export class ApiServer {
|
||||
// Notify renderer that API server is ready
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send(IpcChannel.ApiServer_Ready)
|
||||
mainWindow.webContents.send(IpcChannel.ApiGateway_Ready)
|
||||
}
|
||||
|
||||
resolve()
|
||||
@ -93,4 +93,4 @@ export class ApiServer {
|
||||
}
|
||||
}
|
||||
|
||||
export const apiServer = new ApiServer()
|
||||
export const apiGateway = new ApiGateway()
|
||||
@ -5,7 +5,7 @@
|
||||
* This includes Google Gemini's thought signatures and OpenRouter's reasoning details.
|
||||
*/
|
||||
|
||||
import type { ReasoningDetailUnion } from '@main/apiServer/adapters/openrouter'
|
||||
import type { ReasoningDetailUnion } from '@main/apiGateway/adapters/openrouter'
|
||||
import { CacheService } from '@main/services/CacheService'
|
||||
|
||||
/**
|
||||
@ -5,7 +5,7 @@ import { isOpenAILLMModel } from '@shared/aiCore/config/aihubmix'
|
||||
import { isPpioAnthropicCompatibleModel, isSiliconAnthropicCompatibleModel } from '@shared/config/providers'
|
||||
import type { ApiModel, Model, Provider } from '@types'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerUtils')
|
||||
const logger = loggerService.withContext('ApiGatewayUtils')
|
||||
|
||||
// Cache configuration
|
||||
const PROVIDERS_CACHE_KEY = 'api-server:providers'
|
||||
@ -1,2 +0,0 @@
|
||||
export { config } from './config'
|
||||
export { apiServer } from './server'
|
||||
@ -16,7 +16,7 @@ import process from 'node:process'
|
||||
|
||||
import { registerIpc } from './ipc'
|
||||
import { agentService } from './services/agents'
|
||||
import { apiServerService } from './services/ApiServerService'
|
||||
import { apiGatewayService } from './services/ApiGatewayService'
|
||||
import { appMenuService } from './services/AppMenuService'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import { lanTransferClientService } from './services/lanTransfer'
|
||||
@ -178,7 +178,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
runAsyncFunction(async () => {
|
||||
// Start API server if enabled or if agents exist
|
||||
try {
|
||||
const config = await apiServerService.getCurrentConfig()
|
||||
const config = await apiGatewayService.getCurrentConfig()
|
||||
logger.info('API server config:', config)
|
||||
|
||||
// Check if there are any agents
|
||||
@ -196,7 +196,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
}
|
||||
|
||||
if (shouldStart) {
|
||||
await apiServerService.start()
|
||||
await apiGatewayService.start()
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to check/start API server:', error)
|
||||
@ -259,7 +259,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
|
||||
try {
|
||||
await mcpService.cleanup()
|
||||
await apiServerService.stop()
|
||||
await apiGatewayService.stop()
|
||||
} catch (error) {
|
||||
logger.warn('Error cleaning up MCP service:', error as Error)
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ import fontList from 'font-list'
|
||||
|
||||
import { agentMessageRepository } from './services/agents/database'
|
||||
import { PluginService } from './services/agents/plugins/PluginService'
|
||||
import { apiServerService } from './services/ApiServerService'
|
||||
import { apiGatewayService } from './services/ApiGatewayService'
|
||||
import appService from './services/AppService'
|
||||
import AppUpdater from './services/AppUpdater'
|
||||
import BackupManager from './services/BackupManager'
|
||||
@ -943,7 +943,7 @@ export async function registerIpc(mainWindow: BrowserWindow, app: Electron.App)
|
||||
}
|
||||
})
|
||||
// API Server
|
||||
apiServerService.registerIpcHandlers()
|
||||
apiGatewayService.registerIpcHandlers()
|
||||
|
||||
// Anthropic OAuth
|
||||
ipcMain.handle(IpcChannel.Anthropic_StartOAuthFlow, () => anthropicService.startOAuthFlow())
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import type {
|
||||
ApiServerConfig,
|
||||
GetApiServerStatusResult,
|
||||
RestartApiServerStatusResult,
|
||||
StartApiServerStatusResult,
|
||||
StopApiServerStatusResult
|
||||
ApiGatewayConfig,
|
||||
GetApiGatewayStatusResult,
|
||||
RestartApiGatewayStatusResult,
|
||||
StartApiGatewayStatusResult,
|
||||
StopApiGatewayStatusResult
|
||||
} from '@types'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
import { apiServer } from '../apiServer'
|
||||
import { config } from '../apiServer/config'
|
||||
import { apiGateway } from '../apiGateway'
|
||||
import { config } from '../apiGateway/config'
|
||||
import { loggerService } from './LoggerService'
|
||||
const logger = loggerService.withContext('ApiServerService')
|
||||
const logger = loggerService.withContext('ApiGatewayService')
|
||||
|
||||
export class ApiServerService {
|
||||
export class ApiGatewayService {
|
||||
constructor() {
|
||||
// Use the new clean implementation
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
try {
|
||||
await apiServer.start()
|
||||
await apiGateway.start()
|
||||
logger.info('API Server started successfully')
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to start API Server:', error)
|
||||
@ -30,7 +30,7 @@ export class ApiServerService {
|
||||
|
||||
async stop(): Promise<void> {
|
||||
try {
|
||||
await apiServer.stop()
|
||||
await apiGateway.stop()
|
||||
logger.info('API Server stopped successfully')
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to stop API Server:', error)
|
||||
@ -40,7 +40,7 @@ export class ApiServerService {
|
||||
|
||||
async restart(): Promise<void> {
|
||||
try {
|
||||
await apiServer.restart()
|
||||
await apiGateway.restart()
|
||||
logger.info('API Server restarted successfully')
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to restart API Server:', error)
|
||||
@ -49,16 +49,16 @@ export class ApiServerService {
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return apiServer.isRunning()
|
||||
return apiGateway.isRunning()
|
||||
}
|
||||
|
||||
async getCurrentConfig(): Promise<ApiServerConfig> {
|
||||
async getCurrentConfig(): Promise<ApiGatewayConfig> {
|
||||
return config.get()
|
||||
}
|
||||
|
||||
registerIpcHandlers(): void {
|
||||
// API Server
|
||||
ipcMain.handle(IpcChannel.ApiServer_Start, async (): Promise<StartApiServerStatusResult> => {
|
||||
ipcMain.handle(IpcChannel.ApiGateway_Start, async (): Promise<StartApiGatewayStatusResult> => {
|
||||
try {
|
||||
await this.start()
|
||||
return { success: true }
|
||||
@ -67,7 +67,7 @@ export class ApiServerService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ApiServer_Stop, async (): Promise<StopApiServerStatusResult> => {
|
||||
ipcMain.handle(IpcChannel.ApiGateway_Stop, async (): Promise<StopApiGatewayStatusResult> => {
|
||||
try {
|
||||
await this.stop()
|
||||
return { success: true }
|
||||
@ -76,7 +76,7 @@ export class ApiServerService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ApiServer_Restart, async (): Promise<RestartApiServerStatusResult> => {
|
||||
ipcMain.handle(IpcChannel.ApiGateway_Restart, async (): Promise<RestartApiGatewayStatusResult> => {
|
||||
try {
|
||||
await this.restart()
|
||||
return { success: true }
|
||||
@ -85,7 +85,7 @@ export class ApiServerService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ApiServer_GetStatus, async (): Promise<GetApiServerStatusResult> => {
|
||||
ipcMain.handle(IpcChannel.ApiGateway_GetStatus, async (): Promise<GetApiGatewayStatusResult> => {
|
||||
try {
|
||||
const config = await this.getCurrentConfig()
|
||||
return {
|
||||
@ -100,7 +100,7 @@ export class ApiServerService {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ApiServer_GetConfig, async () => {
|
||||
ipcMain.handle(IpcChannel.ApiGateway_GetConfig, async () => {
|
||||
try {
|
||||
return this.getCurrentConfig()
|
||||
} catch (error: any) {
|
||||
@ -111,4 +111,4 @@ export class ApiServerService {
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const apiServerService = new ApiServerService()
|
||||
export const apiGatewayService = new ApiGatewayService()
|
||||
@ -1,7 +1,7 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { mcpApiService } from '@main/apiServer/services/mcp'
|
||||
import type { ModelValidationError } from '@main/apiServer/utils'
|
||||
import { validateModelId } from '@main/apiServer/utils'
|
||||
import { mcpApiService } from '@main/apiGateway/services/mcp'
|
||||
import type { ModelValidationError } from '@main/apiGateway/utils'
|
||||
import { validateModelId } from '@main/apiGateway/utils'
|
||||
import { buildFunctionCallToolName } from '@main/utils/mcp'
|
||||
import type { AgentType, MCPTool, SlashCommand, Tool } from '@types'
|
||||
import { objectKeys } from '@types'
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ModelValidationError } from '@main/apiServer/utils'
|
||||
import type { ModelValidationError } from '@main/apiGateway/utils'
|
||||
import type { AgentType } from '@types'
|
||||
|
||||
export type AgentModelField = 'model' | 'plan_model' | 'small_model'
|
||||
|
||||
@ -13,8 +13,8 @@ import type {
|
||||
} from '@anthropic-ai/claude-agent-sdk'
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { loggerService } from '@logger'
|
||||
import { config as apiConfigService } from '@main/apiServer/config'
|
||||
import { validateModelId } from '@main/apiServer/utils'
|
||||
import { config as apiConfigService } from '@main/apiGateway/config'
|
||||
import { validateModelId } from '@main/apiGateway/utils'
|
||||
import { isWin } from '@main/constant'
|
||||
import { autoDiscoverGitBash } from '@main/utils/process'
|
||||
import getLoginShellEnvironment from '@main/utils/shell-env'
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import type { Tool } from '@types'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@main/apiServer/services/mcp', () => ({
|
||||
vi.mock('@main/apiGateway/services/mcp', () => ({
|
||||
mcpApiService: {
|
||||
getServerInfo: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@main/apiServer/utils', () => ({
|
||||
vi.mock('@main/apiGateway/utils', () => ({
|
||||
validateModelId: vi.fn()
|
||||
}))
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ import type {
|
||||
FileListResponse,
|
||||
FileMetadata,
|
||||
FileUploadResponse,
|
||||
GetApiServerStatusResult,
|
||||
GetApiGatewayStatusResult,
|
||||
KnowledgeBaseParams,
|
||||
KnowledgeItem,
|
||||
KnowledgeSearchResult,
|
||||
@ -33,11 +33,11 @@ import type {
|
||||
OcrProvider,
|
||||
OcrResult,
|
||||
Provider,
|
||||
RestartApiServerStatusResult,
|
||||
RestartApiGatewayStatusResult,
|
||||
S3Config,
|
||||
Shortcut,
|
||||
StartApiServerStatusResult,
|
||||
StopApiServerStatusResult,
|
||||
StartApiGatewayStatusResult,
|
||||
StopApiGatewayStatusResult,
|
||||
SupportedOcrFile,
|
||||
ThemeMode,
|
||||
WebDavConfig
|
||||
@ -573,18 +573,18 @@ const api = {
|
||||
}
|
||||
}
|
||||
},
|
||||
apiServer: {
|
||||
getStatus: (): Promise<GetApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_GetStatus),
|
||||
start: (): Promise<StartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Start),
|
||||
restart: (): Promise<RestartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Restart),
|
||||
stop: (): Promise<StopApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Stop),
|
||||
apiGateway: {
|
||||
getStatus: (): Promise<GetApiGatewayStatusResult> => ipcRenderer.invoke(IpcChannel.ApiGateway_GetStatus),
|
||||
start: (): Promise<StartApiGatewayStatusResult> => ipcRenderer.invoke(IpcChannel.ApiGateway_Start),
|
||||
restart: (): Promise<RestartApiGatewayStatusResult> => ipcRenderer.invoke(IpcChannel.ApiGateway_Restart),
|
||||
stop: (): Promise<StopApiGatewayStatusResult> => ipcRenderer.invoke(IpcChannel.ApiGateway_Stop),
|
||||
onReady: (callback: () => void): (() => void) => {
|
||||
const listener = () => {
|
||||
callback()
|
||||
}
|
||||
ipcRenderer.on(IpcChannel.ApiServer_Ready, listener)
|
||||
ipcRenderer.on(IpcChannel.ApiGateway_Ready, listener)
|
||||
return () => {
|
||||
ipcRenderer.removeListener(IpcChannel.ApiServer_Ready, listener)
|
||||
ipcRenderer.removeListener(IpcChannel.ApiGateway_Ready, listener)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -2,28 +2,28 @@ import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
|
||||
import { useApiServer } from '../useApiServer'
|
||||
import { useApiGateway } from '../useApiGateway'
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
export const useAgent = (id: string | null) => {
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
const key = id ? client.agentPaths.withId(id) : null
|
||||
const { apiServerConfig, apiServerRunning } = useApiServer()
|
||||
const { apiGatewayConfig, apiGatewayRunning } = useApiGateway()
|
||||
|
||||
// Disable SWR fetching when server is not running by setting key to null
|
||||
const swrKey = apiServerRunning && id ? key : null
|
||||
const swrKey = apiGatewayRunning && id ? key : null
|
||||
|
||||
const fetcher = useCallback(async () => {
|
||||
if (!id) {
|
||||
throw new Error(t('agent.get.error.null_id'))
|
||||
}
|
||||
if (!apiServerConfig.enabled) {
|
||||
throw new Error(t('apiServer.messages.notEnabled'))
|
||||
if (!apiGatewayConfig.enabled) {
|
||||
throw new Error(t('apiGateway.messages.notEnabled'))
|
||||
}
|
||||
const result = await client.getAgent(id)
|
||||
return result
|
||||
}, [apiServerConfig.enabled, client, id, t])
|
||||
}, [apiGatewayConfig.enabled, client, id, t])
|
||||
const { data, error, isLoading } = useSWR(swrKey, fetcher)
|
||||
|
||||
return {
|
||||
|
||||
@ -3,8 +3,8 @@ import { AgentApiClient } from '@renderer/api/agent'
|
||||
import { useSettings } from '../useSettings'
|
||||
|
||||
export const useAgentClient = () => {
|
||||
const { apiServer } = useSettings()
|
||||
const { host, port, apiKey } = apiServer
|
||||
const { apiGateway } = useSettings()
|
||||
const { host, port, apiKey } = apiGateway
|
||||
const client = new AgentApiClient({
|
||||
baseURL: `http://${host}:${port}`,
|
||||
headers: {
|
||||
|
||||
@ -6,7 +6,7 @@ import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
|
||||
import { useApiServer } from '../useApiServer'
|
||||
import { useApiGateway } from '../useApiGateway'
|
||||
import { useRuntime } from '../useRuntime'
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
@ -24,23 +24,23 @@ export const useAgents = () => {
|
||||
const { t } = useTranslation()
|
||||
const client = useAgentClient()
|
||||
const key = client.agentPaths.base
|
||||
const { apiServerConfig, apiServerRunning } = useApiServer()
|
||||
const { apiGatewayConfig, apiGatewayRunning } = useApiGateway()
|
||||
|
||||
// Disable SWR fetching when server is not running by setting key to null
|
||||
const swrKey = apiServerRunning ? key : null
|
||||
const swrKey = apiGatewayRunning ? key : null
|
||||
|
||||
const fetcher = useCallback(async () => {
|
||||
// API server will start on startup if enabled OR there are agents
|
||||
if (!apiServerConfig.enabled && !apiServerRunning) {
|
||||
throw new Error(t('apiServer.messages.notEnabled'))
|
||||
if (!apiGatewayConfig.enabled && !apiGatewayRunning) {
|
||||
throw new Error(t('apiGateway.messages.notEnabled'))
|
||||
}
|
||||
if (!apiServerRunning) {
|
||||
if (!apiGatewayRunning) {
|
||||
throw new Error(t('agent.server.error.not_running'))
|
||||
}
|
||||
const result = await client.listAgents({ sortBy: 'created_at', orderBy: 'desc' })
|
||||
// NOTE: We only use the array for now. useUpdateAgent depends on this behavior.
|
||||
return result.data
|
||||
}, [apiServerConfig.enabled, apiServerRunning, client, t])
|
||||
}, [apiGatewayConfig.enabled, apiGatewayRunning, client, t])
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR(swrKey, fetcher)
|
||||
const { chat } = useRuntime()
|
||||
|
||||
158
src/renderer/src/hooks/useApiGateway.ts
Normal file
158
src/renderer/src/hooks/useApiGateway.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setApiGatewayEnabled as setApiGatewayEnabledAction } from '@renderer/store/settings'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const logger = loggerService.withContext('useApiGateway')
|
||||
|
||||
// Module-level single instance subscription to prevent EventEmitter memory leak
|
||||
// Only one IPC listener will be registered regardless of how many components use this hook
|
||||
const onReadyCallbacks = new Set<() => void>()
|
||||
let removeIpcListener: (() => void) | null = null
|
||||
|
||||
const ensureIpcSubscribed = () => {
|
||||
if (!removeIpcListener) {
|
||||
removeIpcListener = window.api.apiGateway.onReady(() => {
|
||||
onReadyCallbacks.forEach((cb) => cb())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const cleanupIpcIfEmpty = () => {
|
||||
if (onReadyCallbacks.size === 0 && removeIpcListener) {
|
||||
removeIpcListener()
|
||||
removeIpcListener = null
|
||||
}
|
||||
}
|
||||
|
||||
export const useApiGateway = () => {
|
||||
const { t } = useTranslation()
|
||||
// FIXME: We currently store two copies of the config data in both the renderer and the main processes,
|
||||
// which carries the risk of data inconsistency. This should be modified so that the main process stores
|
||||
// the data, and the renderer retrieves it.
|
||||
const apiGatewayConfig = useAppSelector((state) => state.settings.apiGateway)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// Initial state - no longer optimistic, wait for actual status
|
||||
const [apiGatewayRunning, setApiGatewayRunning] = useState(false)
|
||||
const [apiGatewayLoading, setApiGatewayLoading] = useState(true)
|
||||
|
||||
const setApiGatewayEnabled = useCallback(
|
||||
(enabled: boolean) => {
|
||||
dispatch(setApiGatewayEnabledAction(enabled))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
// API Server functions
|
||||
const checkApiGatewayStatus = useCallback(async () => {
|
||||
setApiGatewayLoading(true)
|
||||
try {
|
||||
const status = await window.api.apiGateway.getStatus()
|
||||
setApiGatewayRunning(status.running)
|
||||
if (status.running && !apiGatewayConfig.enabled) {
|
||||
setApiGatewayEnabled(true)
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to check API server status:', error)
|
||||
} finally {
|
||||
setApiGatewayLoading(false)
|
||||
}
|
||||
}, [apiGatewayConfig.enabled, setApiGatewayEnabled])
|
||||
|
||||
const startApiGateway = useCallback(async () => {
|
||||
if (apiGatewayLoading) return
|
||||
setApiGatewayLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiGateway.start()
|
||||
if (result.success) {
|
||||
setApiGatewayRunning(true)
|
||||
setApiGatewayEnabled(true)
|
||||
window.toast.success(t('apiGateway.messages.startSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiGateway.messages.startError') + result.error)
|
||||
}
|
||||
} catch (error: any) {
|
||||
window.toast.error(t('apiGateway.messages.startError') + (error.message || error))
|
||||
} finally {
|
||||
setApiGatewayLoading(false)
|
||||
}
|
||||
}, [apiGatewayLoading, setApiGatewayEnabled, t])
|
||||
|
||||
const stopApiGateway = useCallback(async () => {
|
||||
if (apiGatewayLoading) return
|
||||
setApiGatewayLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiGateway.stop()
|
||||
if (result.success) {
|
||||
setApiGatewayRunning(false)
|
||||
setApiGatewayEnabled(false)
|
||||
window.toast.success(t('apiGateway.messages.stopSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiGateway.messages.stopError') + result.error)
|
||||
}
|
||||
} catch (error: any) {
|
||||
window.toast.error(t('apiGateway.messages.stopError') + (error.message || error))
|
||||
} finally {
|
||||
setApiGatewayLoading(false)
|
||||
}
|
||||
}, [apiGatewayLoading, setApiGatewayEnabled, t])
|
||||
|
||||
const restartApiGateway = useCallback(async () => {
|
||||
if (apiGatewayLoading) return
|
||||
setApiGatewayLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiGateway.restart()
|
||||
setApiGatewayEnabled(result.success)
|
||||
if (result.success) {
|
||||
await checkApiGatewayStatus()
|
||||
window.toast.success(t('apiGateway.messages.restartSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiGateway.messages.restartError') + result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
window.toast.error(t('apiGateway.messages.restartFailed') + (error as Error).message)
|
||||
} finally {
|
||||
setApiGatewayLoading(false)
|
||||
}
|
||||
}, [apiGatewayLoading, checkApiGatewayStatus, setApiGatewayEnabled, t])
|
||||
|
||||
useEffect(() => {
|
||||
checkApiGatewayStatus()
|
||||
}, [checkApiGatewayStatus])
|
||||
|
||||
// Use ref to keep the latest checkApiGatewayStatus without causing re-subscription
|
||||
const checkStatusRef = useRef(checkApiGatewayStatus)
|
||||
useEffect(() => {
|
||||
checkStatusRef.current = checkApiGatewayStatus
|
||||
})
|
||||
|
||||
// Create stable callback for the single instance subscription
|
||||
const handleReady = useCallback(() => {
|
||||
logger.info('API server ready event received, checking status')
|
||||
checkStatusRef.current()
|
||||
}, [])
|
||||
|
||||
// Listen for API server ready event using single instance subscription
|
||||
useEffect(() => {
|
||||
ensureIpcSubscribed()
|
||||
onReadyCallbacks.add(handleReady)
|
||||
|
||||
return () => {
|
||||
onReadyCallbacks.delete(handleReady)
|
||||
cleanupIpcIfEmpty()
|
||||
}
|
||||
}, [handleReady])
|
||||
|
||||
return {
|
||||
apiGatewayConfig,
|
||||
apiGatewayRunning,
|
||||
apiGatewayLoading,
|
||||
startApiGateway,
|
||||
stopApiGateway,
|
||||
restartApiGateway,
|
||||
checkApiGatewayStatus,
|
||||
setApiGatewayEnabled
|
||||
}
|
||||
}
|
||||
@ -1,158 +0,0 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setApiServerEnabled as setApiServerEnabledAction } from '@renderer/store/settings'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const logger = loggerService.withContext('useApiServer')
|
||||
|
||||
// Module-level single instance subscription to prevent EventEmitter memory leak
|
||||
// Only one IPC listener will be registered regardless of how many components use this hook
|
||||
const onReadyCallbacks = new Set<() => void>()
|
||||
let removeIpcListener: (() => void) | null = null
|
||||
|
||||
const ensureIpcSubscribed = () => {
|
||||
if (!removeIpcListener) {
|
||||
removeIpcListener = window.api.apiServer.onReady(() => {
|
||||
onReadyCallbacks.forEach((cb) => cb())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const cleanupIpcIfEmpty = () => {
|
||||
if (onReadyCallbacks.size === 0 && removeIpcListener) {
|
||||
removeIpcListener()
|
||||
removeIpcListener = null
|
||||
}
|
||||
}
|
||||
|
||||
export const useApiServer = () => {
|
||||
const { t } = useTranslation()
|
||||
// FIXME: We currently store two copies of the config data in both the renderer and the main processes,
|
||||
// which carries the risk of data inconsistency. This should be modified so that the main process stores
|
||||
// the data, and the renderer retrieves it.
|
||||
const apiServerConfig = useAppSelector((state) => state.settings.apiServer)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// Initial state - no longer optimistic, wait for actual status
|
||||
const [apiServerRunning, setApiServerRunning] = useState(false)
|
||||
const [apiServerLoading, setApiServerLoading] = useState(true)
|
||||
|
||||
const setApiServerEnabled = useCallback(
|
||||
(enabled: boolean) => {
|
||||
dispatch(setApiServerEnabledAction(enabled))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
// API Server functions
|
||||
const checkApiServerStatus = useCallback(async () => {
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const status = await window.api.apiServer.getStatus()
|
||||
setApiServerRunning(status.running)
|
||||
if (status.running && !apiServerConfig.enabled) {
|
||||
setApiServerEnabled(true)
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to check API server status:', error)
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerConfig.enabled, setApiServerEnabled])
|
||||
|
||||
const startApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.start()
|
||||
if (result.success) {
|
||||
setApiServerRunning(true)
|
||||
setApiServerEnabled(true)
|
||||
window.toast.success(t('apiServer.messages.startSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.startError') + result.error)
|
||||
}
|
||||
} catch (error: any) {
|
||||
window.toast.error(t('apiServer.messages.startError') + (error.message || error))
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, setApiServerEnabled, t])
|
||||
|
||||
const stopApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.stop()
|
||||
if (result.success) {
|
||||
setApiServerRunning(false)
|
||||
setApiServerEnabled(false)
|
||||
window.toast.success(t('apiServer.messages.stopSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.stopError') + result.error)
|
||||
}
|
||||
} catch (error: any) {
|
||||
window.toast.error(t('apiServer.messages.stopError') + (error.message || error))
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, setApiServerEnabled, t])
|
||||
|
||||
const restartApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.restart()
|
||||
setApiServerEnabled(result.success)
|
||||
if (result.success) {
|
||||
await checkApiServerStatus()
|
||||
window.toast.success(t('apiServer.messages.restartSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.restartError') + result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
window.toast.error(t('apiServer.messages.restartFailed') + (error as Error).message)
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, checkApiServerStatus, setApiServerEnabled, t])
|
||||
|
||||
useEffect(() => {
|
||||
checkApiServerStatus()
|
||||
}, [checkApiServerStatus])
|
||||
|
||||
// Use ref to keep the latest checkApiServerStatus without causing re-subscription
|
||||
const checkStatusRef = useRef(checkApiServerStatus)
|
||||
useEffect(() => {
|
||||
checkStatusRef.current = checkApiServerStatus
|
||||
})
|
||||
|
||||
// Create stable callback for the single instance subscription
|
||||
const handleReady = useCallback(() => {
|
||||
logger.info('API server ready event received, checking status')
|
||||
checkStatusRef.current()
|
||||
}, [])
|
||||
|
||||
// Listen for API server ready event using single instance subscription
|
||||
useEffect(() => {
|
||||
ensureIpcSubscribed()
|
||||
onReadyCallbacks.add(handleReady)
|
||||
|
||||
return () => {
|
||||
onReadyCallbacks.delete(handleReady)
|
||||
cleanupIpcIfEmpty()
|
||||
}
|
||||
}, [handleReady])
|
||||
|
||||
return {
|
||||
apiServerConfig,
|
||||
apiServerRunning,
|
||||
apiServerLoading,
|
||||
startApiServer,
|
||||
stopApiServer,
|
||||
restartApiServer,
|
||||
checkApiServerStatus,
|
||||
setApiServerEnabled
|
||||
}
|
||||
}
|
||||
@ -377,7 +377,7 @@
|
||||
},
|
||||
"title": "API Gateway"
|
||||
},
|
||||
"apiServer": {
|
||||
"apiGateway": {
|
||||
"actions": {
|
||||
"copy": "Copy",
|
||||
"regenerate": "Regenerate",
|
||||
|
||||
@ -318,66 +318,6 @@
|
||||
}
|
||||
},
|
||||
"apiGateway": {
|
||||
"actions": {
|
||||
"addGroup": "添加分组",
|
||||
"copyEnvVars": "复制为环境变量"
|
||||
},
|
||||
"description": "通过统一的 API 网关暴露 Cherry Studio 的 AI 功能",
|
||||
"endpoints": {
|
||||
"chatCompletions": "OpenAI 兼容的聊天补全接口",
|
||||
"messages": "Anthropic 兼容的消息接口",
|
||||
"responses": "OpenAI Responses API(新格式)"
|
||||
},
|
||||
"fields": {
|
||||
"baseUrl": {
|
||||
"copyTooltip": "复制 Base URL",
|
||||
"description": "在外部应用程序中使用此 URL 作为 API 基础地址",
|
||||
"label": "基础 URL"
|
||||
},
|
||||
"copyAsEnv": {
|
||||
"description": "复制为环境变量格式,便于集成",
|
||||
"format": "格式",
|
||||
"label": "环境变量"
|
||||
},
|
||||
"defaultModel": {
|
||||
"description": "为外部应用程序选择默认的提供商和模型",
|
||||
"label": "默认模型",
|
||||
"modelPlaceholder": "选择模型",
|
||||
"providerPlaceholder": "选择提供商"
|
||||
},
|
||||
"enabledEndpoints": {
|
||||
"description": "选择每个模型分组要暴露的 API 端点",
|
||||
"label": "启用的端点"
|
||||
},
|
||||
"modelGroups": {
|
||||
"description": "创建具有唯一 URL 的模型分组,用于不同的提供商/模型组合",
|
||||
"empty": "尚未配置模型分组。点击「添加分组」创建一个。",
|
||||
"label": "模型分组",
|
||||
"mode": {
|
||||
"assistant": "助手预设",
|
||||
"assistantHint": "助手预设会覆盖请求参数。",
|
||||
"assistantPlaceholder": "选择助手",
|
||||
"label": "模式",
|
||||
"model": "直接模型"
|
||||
},
|
||||
"namePlaceholder": "分组名称"
|
||||
},
|
||||
"networkAccess": {
|
||||
"description": "允许网络上的其他设备连接",
|
||||
"label": "暴露到网络",
|
||||
"warning": "警告:这将使 API 可从网络上的其他设备访问。仅在您信任网络时启用。"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"baseUrlCopied": "基础 URL 已复制到剪贴板",
|
||||
"envVarsCopied": "环境变量已复制到剪贴板",
|
||||
"nameDuplicate": "已存在相同 URL 路径的分组",
|
||||
"nameRequired": "分组名称不能为空",
|
||||
"nameUpdated": "分组名称已更新"
|
||||
},
|
||||
"title": "API 网关"
|
||||
},
|
||||
"apiServer": {
|
||||
"actions": {
|
||||
"copy": "复制",
|
||||
"regenerate": "重新生成",
|
||||
|
||||
@ -318,66 +318,6 @@
|
||||
}
|
||||
},
|
||||
"apiGateway": {
|
||||
"actions": {
|
||||
"addGroup": "[to be translated]:Add Group",
|
||||
"copyEnvVars": "[to be translated]:Copy as Environment Variables"
|
||||
},
|
||||
"description": "[to be translated]:Expose Cherry Studio's AI capabilities through a unified API Gateway",
|
||||
"endpoints": {
|
||||
"chatCompletions": "[to be translated]:OpenAI-compatible chat completions",
|
||||
"messages": "[to be translated]:Anthropic-compatible messages API",
|
||||
"responses": "[to be translated]:OpenAI Responses API (new format)"
|
||||
},
|
||||
"fields": {
|
||||
"baseUrl": {
|
||||
"copyTooltip": "[to be translated]:Copy Base URL",
|
||||
"description": "[to be translated]:Use this URL as your API base URL in external applications",
|
||||
"label": "[to be translated]:Base URL"
|
||||
},
|
||||
"copyAsEnv": {
|
||||
"description": "[to be translated]:Copy configuration as environment variables for easy integration",
|
||||
"format": "[to be translated]:Format",
|
||||
"label": "[to be translated]:Environment Variables"
|
||||
},
|
||||
"defaultModel": {
|
||||
"description": "[to be translated]:Select the default provider and model for external applications",
|
||||
"label": "[to be translated]:Default Model",
|
||||
"modelPlaceholder": "[to be translated]:Select model",
|
||||
"providerPlaceholder": "[to be translated]:Select provider"
|
||||
},
|
||||
"enabledEndpoints": {
|
||||
"description": "[to be translated]:Choose which API endpoints to expose",
|
||||
"label": "[to be translated]:Enabled Endpoints"
|
||||
},
|
||||
"modelGroups": {
|
||||
"description": "[to be translated]:Create model groups with unique URLs for different provider/model combinations",
|
||||
"empty": "[to be translated]:No model groups configured. Click 'Add Group' to create one.",
|
||||
"label": "[to be translated]:Model Groups",
|
||||
"mode": {
|
||||
"assistant": "[to be translated]:Assistant Preset",
|
||||
"assistantHint": "[to be translated]:Assistant preset overrides request parameters.",
|
||||
"assistantPlaceholder": "[to be translated]:Select assistant",
|
||||
"label": "[to be translated]:Mode",
|
||||
"model": "[to be translated]:Direct Model"
|
||||
},
|
||||
"namePlaceholder": "[to be translated]:Group name"
|
||||
},
|
||||
"networkAccess": {
|
||||
"description": "[to be translated]:Allow connections from other devices on your network",
|
||||
"label": "[to be translated]:Expose to Network",
|
||||
"warning": "[to be translated]:Warning: This will make the API accessible from other devices on your network. Only enable if you trust your network."
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"baseUrlCopied": "[to be translated]:Base URL copied to clipboard",
|
||||
"envVarsCopied": "[to be translated]:Environment variables copied to clipboard",
|
||||
"nameDuplicate": "[to be translated]:A group with the same URL path already exists",
|
||||
"nameRequired": "[to be translated]:Group name cannot be empty",
|
||||
"nameUpdated": "[to be translated]:Group name updated"
|
||||
},
|
||||
"title": "[to be translated]:API Gateway"
|
||||
},
|
||||
"apiServer": {
|
||||
"actions": {
|
||||
"copy": "複製",
|
||||
"regenerate": "重新產生",
|
||||
|
||||
@ -318,66 +318,6 @@
|
||||
}
|
||||
},
|
||||
"apiGateway": {
|
||||
"actions": {
|
||||
"addGroup": "[to be translated]:Add Group",
|
||||
"copyEnvVars": "[to be translated]:Copy as Environment Variables"
|
||||
},
|
||||
"description": "[to be translated]:Expose Cherry Studio's AI capabilities through a unified API Gateway",
|
||||
"endpoints": {
|
||||
"chatCompletions": "[to be translated]:OpenAI-compatible chat completions",
|
||||
"messages": "[to be translated]:Anthropic-compatible messages API",
|
||||
"responses": "[to be translated]:OpenAI Responses API (new format)"
|
||||
},
|
||||
"fields": {
|
||||
"baseUrl": {
|
||||
"copyTooltip": "[to be translated]:Copy Base URL",
|
||||
"description": "[to be translated]:Use this URL as your API base URL in external applications",
|
||||
"label": "[to be translated]:Base URL"
|
||||
},
|
||||
"copyAsEnv": {
|
||||
"description": "[to be translated]:Copy configuration as environment variables for easy integration",
|
||||
"format": "[to be translated]:Format",
|
||||
"label": "[to be translated]:Environment Variables"
|
||||
},
|
||||
"defaultModel": {
|
||||
"description": "[to be translated]:Select the default provider and model for external applications",
|
||||
"label": "[to be translated]:Default Model",
|
||||
"modelPlaceholder": "[to be translated]:Select model",
|
||||
"providerPlaceholder": "[to be translated]:Select provider"
|
||||
},
|
||||
"enabledEndpoints": {
|
||||
"description": "[to be translated]:Choose which API endpoints to expose",
|
||||
"label": "[to be translated]:Enabled Endpoints"
|
||||
},
|
||||
"modelGroups": {
|
||||
"description": "[to be translated]:Create model groups with unique URLs for different provider/model combinations",
|
||||
"empty": "[to be translated]:No model groups configured. Click 'Add Group' to create one.",
|
||||
"label": "[to be translated]:Model Groups",
|
||||
"mode": {
|
||||
"assistant": "[to be translated]:Assistant Preset",
|
||||
"assistantHint": "[to be translated]:Assistant preset overrides request parameters.",
|
||||
"assistantPlaceholder": "[to be translated]:Select assistant",
|
||||
"label": "[to be translated]:Mode",
|
||||
"model": "[to be translated]:Direct Model"
|
||||
},
|
||||
"namePlaceholder": "[to be translated]:Group name"
|
||||
},
|
||||
"networkAccess": {
|
||||
"description": "[to be translated]:Allow connections from other devices on your network",
|
||||
"label": "[to be translated]:Expose to Network",
|
||||
"warning": "[to be translated]:Warning: This will make the API accessible from other devices on your network. Only enable if you trust your network."
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"baseUrlCopied": "[to be translated]:Base URL copied to clipboard",
|
||||
"envVarsCopied": "[to be translated]:Environment variables copied to clipboard",
|
||||
"nameDuplicate": "[to be translated]:A group with the same URL path already exists",
|
||||
"nameRequired": "[to be translated]:Group name cannot be empty",
|
||||
"nameUpdated": "[to be translated]:Group name updated"
|
||||
},
|
||||
"title": "[to be translated]:API Gateway"
|
||||
},
|
||||
"apiServer": {
|
||||
"actions": {
|
||||
"copy": "Kopieren",
|
||||
"regenerate": "Neu generieren",
|
||||
|
||||
@ -318,66 +318,6 @@
|
||||
}
|
||||
},
|
||||
"apiGateway": {
|
||||
"actions": {
|
||||
"addGroup": "[to be translated]:Add Group",
|
||||
"copyEnvVars": "[to be translated]:Copy as Environment Variables"
|
||||
},
|
||||
"description": "[to be translated]:Expose Cherry Studio's AI capabilities through a unified API Gateway",
|
||||
"endpoints": {
|
||||
"chatCompletions": "[to be translated]:OpenAI-compatible chat completions",
|
||||
"messages": "[to be translated]:Anthropic-compatible messages API",
|
||||
"responses": "[to be translated]:OpenAI Responses API (new format)"
|
||||
},
|
||||
"fields": {
|
||||
"baseUrl": {
|
||||
"copyTooltip": "[to be translated]:Copy Base URL",
|
||||
"description": "[to be translated]:Use this URL as your API base URL in external applications",
|
||||
"label": "[to be translated]:Base URL"
|
||||
},
|
||||
"copyAsEnv": {
|
||||
"description": "[to be translated]:Copy configuration as environment variables for easy integration",
|
||||
"format": "[to be translated]:Format",
|
||||
"label": "[to be translated]:Environment Variables"
|
||||
},
|
||||
"defaultModel": {
|
||||
"description": "[to be translated]:Select the default provider and model for external applications",
|
||||
"label": "[to be translated]:Default Model",
|
||||
"modelPlaceholder": "[to be translated]:Select model",
|
||||
"providerPlaceholder": "[to be translated]:Select provider"
|
||||
},
|
||||
"enabledEndpoints": {
|
||||
"description": "[to be translated]:Choose which API endpoints to expose",
|
||||
"label": "[to be translated]:Enabled Endpoints"
|
||||
},
|
||||
"modelGroups": {
|
||||
"description": "[to be translated]:Create model groups with unique URLs for different provider/model combinations",
|
||||
"empty": "[to be translated]:No model groups configured. Click 'Add Group' to create one.",
|
||||
"label": "[to be translated]:Model Groups",
|
||||
"mode": {
|
||||
"assistant": "[to be translated]:Assistant Preset",
|
||||
"assistantHint": "[to be translated]:Assistant preset overrides request parameters.",
|
||||
"assistantPlaceholder": "[to be translated]:Select assistant",
|
||||
"label": "[to be translated]:Mode",
|
||||
"model": "[to be translated]:Direct Model"
|
||||
},
|
||||
"namePlaceholder": "[to be translated]:Group name"
|
||||
},
|
||||
"networkAccess": {
|
||||
"description": "[to be translated]:Allow connections from other devices on your network",
|
||||
"label": "[to be translated]:Expose to Network",
|
||||
"warning": "[to be translated]:Warning: This will make the API accessible from other devices on your network. Only enable if you trust your network."
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"baseUrlCopied": "[to be translated]:Base URL copied to clipboard",
|
||||
"envVarsCopied": "[to be translated]:Environment variables copied to clipboard",
|
||||
"nameDuplicate": "[to be translated]:A group with the same URL path already exists",
|
||||
"nameRequired": "[to be translated]:Group name cannot be empty",
|
||||
"nameUpdated": "[to be translated]:Group name updated"
|
||||
},
|
||||
"title": "[to be translated]:API Gateway"
|
||||
},
|
||||
"apiServer": {
|
||||
"actions": {
|
||||
"copy": "Αντιγραφή",
|
||||
"regenerate": "Αναδημιουργία",
|
||||
|
||||
@ -318,66 +318,6 @@
|
||||
}
|
||||
},
|
||||
"apiGateway": {
|
||||
"actions": {
|
||||
"addGroup": "[to be translated]:Add Group",
|
||||
"copyEnvVars": "[to be translated]:Copy as Environment Variables"
|
||||
},
|
||||
"description": "[to be translated]:Expose Cherry Studio's AI capabilities through a unified API Gateway",
|
||||
"endpoints": {
|
||||
"chatCompletions": "[to be translated]:OpenAI-compatible chat completions",
|
||||
"messages": "[to be translated]:Anthropic-compatible messages API",
|
||||
"responses": "[to be translated]:OpenAI Responses API (new format)"
|
||||
},
|
||||
"fields": {
|
||||
"baseUrl": {
|
||||
"copyTooltip": "[to be translated]:Copy Base URL",
|
||||
"description": "[to be translated]:Use this URL as your API base URL in external applications",
|
||||
"label": "[to be translated]:Base URL"
|
||||
},
|
||||
"copyAsEnv": {
|
||||
"description": "[to be translated]:Copy configuration as environment variables for easy integration",
|
||||
"format": "[to be translated]:Format",
|
||||
"label": "[to be translated]:Environment Variables"
|
||||
},
|
||||
"defaultModel": {
|
||||
"description": "[to be translated]:Select the default provider and model for external applications",
|
||||
"label": "[to be translated]:Default Model",
|
||||
"modelPlaceholder": "[to be translated]:Select model",
|
||||
"providerPlaceholder": "[to be translated]:Select provider"
|
||||
},
|
||||
"enabledEndpoints": {
|
||||
"description": "[to be translated]:Choose which API endpoints to expose",
|
||||
"label": "[to be translated]:Enabled Endpoints"
|
||||
},
|
||||
"modelGroups": {
|
||||
"description": "[to be translated]:Create model groups with unique URLs for different provider/model combinations",
|
||||
"empty": "[to be translated]:No model groups configured. Click 'Add Group' to create one.",
|
||||
"label": "[to be translated]:Model Groups",
|
||||
"mode": {
|
||||
"assistant": "[to be translated]:Assistant Preset",
|
||||
"assistantHint": "[to be translated]:Assistant preset overrides request parameters.",
|
||||
"assistantPlaceholder": "[to be translated]:Select assistant",
|
||||
"label": "[to be translated]:Mode",
|
||||
"model": "[to be translated]:Direct Model"
|
||||
},
|
||||
"namePlaceholder": "[to be translated]:Group name"
|
||||
},
|
||||
"networkAccess": {
|
||||
"description": "[to be translated]:Allow connections from other devices on your network",
|
||||
"label": "[to be translated]:Expose to Network",
|
||||
"warning": "[to be translated]:Warning: This will make the API accessible from other devices on your network. Only enable if you trust your network."
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"baseUrlCopied": "[to be translated]:Base URL copied to clipboard",
|
||||
"envVarsCopied": "[to be translated]:Environment variables copied to clipboard",
|
||||
"nameDuplicate": "[to be translated]:A group with the same URL path already exists",
|
||||
"nameRequired": "[to be translated]:Group name cannot be empty",
|
||||
"nameUpdated": "[to be translated]:Group name updated"
|
||||
},
|
||||
"title": "[to be translated]:API Gateway"
|
||||
},
|
||||
"apiServer": {
|
||||
"actions": {
|
||||
"copy": "Copiar",
|
||||
"regenerate": "Regenerar",
|
||||
|
||||
@ -318,66 +318,6 @@
|
||||
}
|
||||
},
|
||||
"apiGateway": {
|
||||
"actions": {
|
||||
"addGroup": "[to be translated]:Add Group",
|
||||
"copyEnvVars": "[to be translated]:Copy as Environment Variables"
|
||||
},
|
||||
"description": "[to be translated]:Expose Cherry Studio's AI capabilities through a unified API Gateway",
|
||||
"endpoints": {
|
||||
"chatCompletions": "[to be translated]:OpenAI-compatible chat completions",
|
||||
"messages": "[to be translated]:Anthropic-compatible messages API",
|
||||
"responses": "[to be translated]:OpenAI Responses API (new format)"
|
||||
},
|
||||
"fields": {
|
||||
"baseUrl": {
|
||||
"copyTooltip": "[to be translated]:Copy Base URL",
|
||||
"description": "[to be translated]:Use this URL as your API base URL in external applications",
|
||||
"label": "[to be translated]:Base URL"
|
||||
},
|
||||
"copyAsEnv": {
|
||||
"description": "[to be translated]:Copy configuration as environment variables for easy integration",
|
||||
"format": "[to be translated]:Format",
|
||||
"label": "[to be translated]:Environment Variables"
|
||||
},
|
||||
"defaultModel": {
|
||||
"description": "[to be translated]:Select the default provider and model for external applications",
|
||||
"label": "[to be translated]:Default Model",
|
||||
"modelPlaceholder": "[to be translated]:Select model",
|
||||
"providerPlaceholder": "[to be translated]:Select provider"
|
||||
},
|
||||
"enabledEndpoints": {
|
||||
"description": "[to be translated]:Choose which API endpoints to expose",
|
||||
"label": "[to be translated]:Enabled Endpoints"
|
||||
},
|
||||
"modelGroups": {
|
||||
"description": "[to be translated]:Create model groups with unique URLs for different provider/model combinations",
|
||||
"empty": "[to be translated]:No model groups configured. Click 'Add Group' to create one.",
|
||||
"label": "[to be translated]:Model Groups",
|
||||
"mode": {
|
||||
"assistant": "[to be translated]:Assistant Preset",
|
||||
"assistantHint": "[to be translated]:Assistant preset overrides request parameters.",
|
||||
"assistantPlaceholder": "[to be translated]:Select assistant",
|
||||
"label": "[to be translated]:Mode",
|
||||
"model": "[to be translated]:Direct Model"
|
||||
},
|
||||
"namePlaceholder": "[to be translated]:Group name"
|
||||
},
|
||||
"networkAccess": {
|
||||
"description": "[to be translated]:Allow connections from other devices on your network",
|
||||
"label": "[to be translated]:Expose to Network",
|
||||
"warning": "[to be translated]:Warning: This will make the API accessible from other devices on your network. Only enable if you trust your network."
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"baseUrlCopied": "[to be translated]:Base URL copied to clipboard",
|
||||
"envVarsCopied": "[to be translated]:Environment variables copied to clipboard",
|
||||
"nameDuplicate": "[to be translated]:A group with the same URL path already exists",
|
||||
"nameRequired": "[to be translated]:Group name cannot be empty",
|
||||
"nameUpdated": "[to be translated]:Group name updated"
|
||||
},
|
||||
"title": "[to be translated]:API Gateway"
|
||||
},
|
||||
"apiServer": {
|
||||
"actions": {
|
||||
"copy": "Copier",
|
||||
"regenerate": "Régénérer",
|
||||
|
||||
@ -318,66 +318,6 @@
|
||||
}
|
||||
},
|
||||
"apiGateway": {
|
||||
"actions": {
|
||||
"addGroup": "[to be translated]:Add Group",
|
||||
"copyEnvVars": "[to be translated]:Copy as Environment Variables"
|
||||
},
|
||||
"description": "[to be translated]:Expose Cherry Studio's AI capabilities through a unified API Gateway",
|
||||
"endpoints": {
|
||||
"chatCompletions": "[to be translated]:OpenAI-compatible chat completions",
|
||||
"messages": "[to be translated]:Anthropic-compatible messages API",
|
||||
"responses": "[to be translated]:OpenAI Responses API (new format)"
|
||||
},
|
||||
"fields": {
|
||||
"baseUrl": {
|
||||
"copyTooltip": "[to be translated]:Copy Base URL",
|
||||
"description": "[to be translated]:Use this URL as your API base URL in external applications",
|
||||
"label": "[to be translated]:Base URL"
|
||||
},
|
||||
"copyAsEnv": {
|
||||
"description": "[to be translated]:Copy configuration as environment variables for easy integration",
|
||||
"format": "[to be translated]:Format",
|
||||
"label": "[to be translated]:Environment Variables"
|
||||
},
|
||||
"defaultModel": {
|
||||
"description": "[to be translated]:Select the default provider and model for external applications",
|
||||
"label": "[to be translated]:Default Model",
|
||||
"modelPlaceholder": "[to be translated]:Select model",
|
||||
"providerPlaceholder": "[to be translated]:Select provider"
|
||||
},
|
||||
"enabledEndpoints": {
|
||||
"description": "[to be translated]:Choose which API endpoints to expose",
|
||||
"label": "[to be translated]:Enabled Endpoints"
|
||||
},
|
||||
"modelGroups": {
|
||||
"description": "[to be translated]:Create model groups with unique URLs for different provider/model combinations",
|
||||
"empty": "[to be translated]:No model groups configured. Click 'Add Group' to create one.",
|
||||
"label": "[to be translated]:Model Groups",
|
||||
"mode": {
|
||||
"assistant": "[to be translated]:Assistant Preset",
|
||||
"assistantHint": "[to be translated]:Assistant preset overrides request parameters.",
|
||||
"assistantPlaceholder": "[to be translated]:Select assistant",
|
||||
"label": "[to be translated]:Mode",
|
||||
"model": "[to be translated]:Direct Model"
|
||||
},
|
||||
"namePlaceholder": "[to be translated]:Group name"
|
||||
},
|
||||
"networkAccess": {
|
||||
"description": "[to be translated]:Allow connections from other devices on your network",
|
||||
"label": "[to be translated]:Expose to Network",
|
||||
"warning": "[to be translated]:Warning: This will make the API accessible from other devices on your network. Only enable if you trust your network."
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"baseUrlCopied": "[to be translated]:Base URL copied to clipboard",
|
||||
"envVarsCopied": "[to be translated]:Environment variables copied to clipboard",
|
||||
"nameDuplicate": "[to be translated]:A group with the same URL path already exists",
|
||||
"nameRequired": "[to be translated]:Group name cannot be empty",
|
||||
"nameUpdated": "[to be translated]:Group name updated"
|
||||
},
|
||||
"title": "[to be translated]:API Gateway"
|
||||
},
|
||||
"apiServer": {
|
||||
"actions": {
|
||||
"copy": "コピー",
|
||||
"regenerate": "再生成",
|
||||
|
||||
@ -318,66 +318,6 @@
|
||||
}
|
||||
},
|
||||
"apiGateway": {
|
||||
"actions": {
|
||||
"addGroup": "[to be translated]:Add Group",
|
||||
"copyEnvVars": "[to be translated]:Copy as Environment Variables"
|
||||
},
|
||||
"description": "[to be translated]:Expose Cherry Studio's AI capabilities through a unified API Gateway",
|
||||
"endpoints": {
|
||||
"chatCompletions": "[to be translated]:OpenAI-compatible chat completions",
|
||||
"messages": "[to be translated]:Anthropic-compatible messages API",
|
||||
"responses": "[to be translated]:OpenAI Responses API (new format)"
|
||||
},
|
||||
"fields": {
|
||||
"baseUrl": {
|
||||
"copyTooltip": "[to be translated]:Copy Base URL",
|
||||
"description": "[to be translated]:Use this URL as your API base URL in external applications",
|
||||
"label": "[to be translated]:Base URL"
|
||||
},
|
||||
"copyAsEnv": {
|
||||
"description": "[to be translated]:Copy configuration as environment variables for easy integration",
|
||||
"format": "[to be translated]:Format",
|
||||
"label": "[to be translated]:Environment Variables"
|
||||
},
|
||||
"defaultModel": {
|
||||
"description": "[to be translated]:Select the default provider and model for external applications",
|
||||
"label": "[to be translated]:Default Model",
|
||||
"modelPlaceholder": "[to be translated]:Select model",
|
||||
"providerPlaceholder": "[to be translated]:Select provider"
|
||||
},
|
||||
"enabledEndpoints": {
|
||||
"description": "[to be translated]:Choose which API endpoints to expose",
|
||||
"label": "[to be translated]:Enabled Endpoints"
|
||||
},
|
||||
"modelGroups": {
|
||||
"description": "[to be translated]:Create model groups with unique URLs for different provider/model combinations",
|
||||
"empty": "[to be translated]:No model groups configured. Click 'Add Group' to create one.",
|
||||
"label": "[to be translated]:Model Groups",
|
||||
"mode": {
|
||||
"assistant": "[to be translated]:Assistant Preset",
|
||||
"assistantHint": "[to be translated]:Assistant preset overrides request parameters.",
|
||||
"assistantPlaceholder": "[to be translated]:Select assistant",
|
||||
"label": "[to be translated]:Mode",
|
||||
"model": "[to be translated]:Direct Model"
|
||||
},
|
||||
"namePlaceholder": "[to be translated]:Group name"
|
||||
},
|
||||
"networkAccess": {
|
||||
"description": "[to be translated]:Allow connections from other devices on your network",
|
||||
"label": "[to be translated]:Expose to Network",
|
||||
"warning": "[to be translated]:Warning: This will make the API accessible from other devices on your network. Only enable if you trust your network."
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"baseUrlCopied": "[to be translated]:Base URL copied to clipboard",
|
||||
"envVarsCopied": "[to be translated]:Environment variables copied to clipboard",
|
||||
"nameDuplicate": "[to be translated]:A group with the same URL path already exists",
|
||||
"nameRequired": "[to be translated]:Group name cannot be empty",
|
||||
"nameUpdated": "[to be translated]:Group name updated"
|
||||
},
|
||||
"title": "[to be translated]:API Gateway"
|
||||
},
|
||||
"apiServer": {
|
||||
"actions": {
|
||||
"copy": "Copiar",
|
||||
"regenerate": "Regenerar",
|
||||
|
||||
@ -318,66 +318,6 @@
|
||||
}
|
||||
},
|
||||
"apiGateway": {
|
||||
"actions": {
|
||||
"addGroup": "[to be translated]:Add Group",
|
||||
"copyEnvVars": "[to be translated]:Copy as Environment Variables"
|
||||
},
|
||||
"description": "[to be translated]:Expose Cherry Studio's AI capabilities through a unified API Gateway",
|
||||
"endpoints": {
|
||||
"chatCompletions": "[to be translated]:OpenAI-compatible chat completions",
|
||||
"messages": "[to be translated]:Anthropic-compatible messages API",
|
||||
"responses": "[to be translated]:OpenAI Responses API (new format)"
|
||||
},
|
||||
"fields": {
|
||||
"baseUrl": {
|
||||
"copyTooltip": "[to be translated]:Copy Base URL",
|
||||
"description": "[to be translated]:Use this URL as your API base URL in external applications",
|
||||
"label": "[to be translated]:Base URL"
|
||||
},
|
||||
"copyAsEnv": {
|
||||
"description": "[to be translated]:Copy configuration as environment variables for easy integration",
|
||||
"format": "[to be translated]:Format",
|
||||
"label": "[to be translated]:Environment Variables"
|
||||
},
|
||||
"defaultModel": {
|
||||
"description": "[to be translated]:Select the default provider and model for external applications",
|
||||
"label": "[to be translated]:Default Model",
|
||||
"modelPlaceholder": "[to be translated]:Select model",
|
||||
"providerPlaceholder": "[to be translated]:Select provider"
|
||||
},
|
||||
"enabledEndpoints": {
|
||||
"description": "[to be translated]:Choose which API endpoints to expose for each model group",
|
||||
"label": "[to be translated]:Enabled Endpoints"
|
||||
},
|
||||
"modelGroups": {
|
||||
"description": "[to be translated]:Create model groups with unique URLs for different provider/model combinations",
|
||||
"empty": "[to be translated]:No model groups configured. Click 'Add Group' to create one.",
|
||||
"label": "[to be translated]:Model Groups",
|
||||
"mode": {
|
||||
"assistant": "[to be translated]:Assistant Preset",
|
||||
"assistantHint": "[to be translated]:Assistant preset overrides request parameters.",
|
||||
"assistantPlaceholder": "[to be translated]:Select assistant",
|
||||
"label": "[to be translated]:Mode",
|
||||
"model": "[to be translated]:Direct Model"
|
||||
},
|
||||
"namePlaceholder": "[to be translated]:Group name"
|
||||
},
|
||||
"networkAccess": {
|
||||
"description": "[to be translated]:Allow connections from other devices on your network",
|
||||
"label": "[to be translated]:Expose to Network",
|
||||
"warning": "[to be translated]:Warning: This will make the API accessible from other devices on your network. Only enable if you trust your network."
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"baseUrlCopied": "[to be translated]:Base URL copied to clipboard",
|
||||
"envVarsCopied": "[to be translated]:Environment variables copied to clipboard",
|
||||
"nameDuplicate": "[to be translated]:A group with the same URL path already exists",
|
||||
"nameRequired": "[to be translated]:Group name cannot be empty",
|
||||
"nameUpdated": "[to be translated]:Group name updated"
|
||||
},
|
||||
"title": "[to be translated]:API Gateway"
|
||||
},
|
||||
"apiServer": {
|
||||
"actions": {
|
||||
"copy": "Copiază",
|
||||
"regenerate": "Regenerează",
|
||||
|
||||
@ -318,66 +318,6 @@
|
||||
}
|
||||
},
|
||||
"apiGateway": {
|
||||
"actions": {
|
||||
"addGroup": "[to be translated]:Add Group",
|
||||
"copyEnvVars": "[to be translated]:Copy as Environment Variables"
|
||||
},
|
||||
"description": "[to be translated]:Expose Cherry Studio's AI capabilities through a unified API Gateway",
|
||||
"endpoints": {
|
||||
"chatCompletions": "[to be translated]:OpenAI-compatible chat completions",
|
||||
"messages": "[to be translated]:Anthropic-compatible messages API",
|
||||
"responses": "[to be translated]:OpenAI Responses API (new format)"
|
||||
},
|
||||
"fields": {
|
||||
"baseUrl": {
|
||||
"copyTooltip": "[to be translated]:Copy Base URL",
|
||||
"description": "[to be translated]:Use this URL as your API base URL in external applications",
|
||||
"label": "[to be translated]:Base URL"
|
||||
},
|
||||
"copyAsEnv": {
|
||||
"description": "[to be translated]:Copy configuration as environment variables for easy integration",
|
||||
"format": "[to be translated]:Format",
|
||||
"label": "[to be translated]:Environment Variables"
|
||||
},
|
||||
"defaultModel": {
|
||||
"description": "[to be translated]:Select the default provider and model for external applications",
|
||||
"label": "[to be translated]:Default Model",
|
||||
"modelPlaceholder": "[to be translated]:Select model",
|
||||
"providerPlaceholder": "[to be translated]:Select provider"
|
||||
},
|
||||
"enabledEndpoints": {
|
||||
"description": "[to be translated]:Choose which API endpoints to expose",
|
||||
"label": "[to be translated]:Enabled Endpoints"
|
||||
},
|
||||
"modelGroups": {
|
||||
"description": "[to be translated]:Create model groups with unique URLs for different provider/model combinations",
|
||||
"empty": "[to be translated]:No model groups configured. Click 'Add Group' to create one.",
|
||||
"label": "[to be translated]:Model Groups",
|
||||
"mode": {
|
||||
"assistant": "[to be translated]:Assistant Preset",
|
||||
"assistantHint": "[to be translated]:Assistant preset overrides request parameters.",
|
||||
"assistantPlaceholder": "[to be translated]:Select assistant",
|
||||
"label": "[to be translated]:Mode",
|
||||
"model": "[to be translated]:Direct Model"
|
||||
},
|
||||
"namePlaceholder": "[to be translated]:Group name"
|
||||
},
|
||||
"networkAccess": {
|
||||
"description": "[to be translated]:Allow connections from other devices on your network",
|
||||
"label": "[to be translated]:Expose to Network",
|
||||
"warning": "[to be translated]:Warning: This will make the API accessible from other devices on your network. Only enable if you trust your network."
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"baseUrlCopied": "[to be translated]:Base URL copied to clipboard",
|
||||
"envVarsCopied": "[to be translated]:Environment variables copied to clipboard",
|
||||
"nameDuplicate": "[to be translated]:A group with the same URL path already exists",
|
||||
"nameRequired": "[to be translated]:Group name cannot be empty",
|
||||
"nameUpdated": "[to be translated]:Group name updated"
|
||||
},
|
||||
"title": "[to be translated]:API Gateway"
|
||||
},
|
||||
"apiServer": {
|
||||
"actions": {
|
||||
"copy": "Копировать",
|
||||
"regenerate": "Перегенерировать",
|
||||
|
||||
@ -53,7 +53,7 @@ const Chat: FC<Props> = (props) => {
|
||||
const { chat } = useRuntime()
|
||||
const { activeTopicOrSession, activeAgentId, activeSessionIdMap } = chat
|
||||
const activeSessionId = activeAgentId ? activeSessionIdMap[activeAgentId] : null
|
||||
const { apiServer } = useSettings()
|
||||
const { apiGateway } = useSettings()
|
||||
const sessionAgentId = activeTopicOrSession === 'session' ? activeAgentId : null
|
||||
const { createDefaultSession } = useCreateDefaultSession(sessionAgentId)
|
||||
|
||||
@ -228,7 +228,7 @@ const Chat: FC<Props> = (props) => {
|
||||
{activeTopicOrSession === 'session' && activeAgentId && !activeSessionId && <SessionInvalid />}
|
||||
{activeTopicOrSession === 'session' && activeAgentId && activeSessionId && (
|
||||
<>
|
||||
{!apiServer.enabled ? (
|
||||
{!apiGateway.enabled ? (
|
||||
<Alert type="warning" message={t('agent.warning.enable_server')} style={{ margin: '5px 16px' }} />
|
||||
) : (
|
||||
<AgentSessionMessages agentId={activeAgentId} sessionId={activeSessionId} />
|
||||
|
||||
@ -183,7 +183,7 @@ const AgentSessionInputbarInner: FC<InnerProps> = ({ assistant, agentId, session
|
||||
customHeight,
|
||||
setCustomHeight
|
||||
} = useTextareaResize({ maxHeight: 500, minHeight: 30 })
|
||||
const { sendMessageShortcut, apiServer } = useSettings()
|
||||
const { sendMessageShortcut, apiGateway } = useSettings()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const quickPanel = useQuickPanel()
|
||||
@ -343,7 +343,7 @@ const AgentSessionInputbarInner: FC<InnerProps> = ({ assistant, agentId, session
|
||||
}
|
||||
}, [config.enableQuickPanel, toolsRegistry])
|
||||
|
||||
const sendDisabled = (inputEmpty && files.length === 0) || !apiServer.enabled
|
||||
const sendDisabled = (inputEmpty && files.length === 0) || !apiGateway.enabled
|
||||
|
||||
const streamingAskIds = useMemo(() => {
|
||||
if (!topicMessages) {
|
||||
|
||||
@ -18,13 +18,13 @@ const createSessionTool = defineTool({
|
||||
render: function CreateSessionRender(context) {
|
||||
const { t, assistant, session } = context
|
||||
const newTopicShortcut = useShortcutDisplay('new_topic')
|
||||
const { apiServer } = useSettings()
|
||||
const { apiGateway } = useSettings()
|
||||
const sessionAgentId = session?.agentId
|
||||
|
||||
const agentId = sessionAgentId || assistant.id
|
||||
const { createDefaultSession, creatingSession } = useCreateDefaultSession(agentId)
|
||||
|
||||
const createSessionDisabled = creatingSession || !apiServer.enabled
|
||||
const createSessionDisabled = creatingSession || !apiGateway.enabled
|
||||
|
||||
const handleCreateSession = useCallback(async () => {
|
||||
if (createSessionDisabled) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import { useApiGateway } from '@renderer/hooks/useApiGateway'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
@ -30,8 +30,8 @@ interface AssistantsTabProps {
|
||||
const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
const { activeAssistant, setActiveAssistant, onCreateAssistant, onCreateDefaultAssistant } = props
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { apiServerConfig } = useApiServer()
|
||||
const apiServerEnabled = apiServerConfig.enabled
|
||||
const { apiGatewayConfig } = useApiGateway()
|
||||
const apiGatewayEnabled = apiGatewayConfig.enabled
|
||||
const { chat } = useRuntime()
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -51,7 +51,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
const { unifiedItems, handleUnifiedListReorder } = useUnifiedItems({
|
||||
agents,
|
||||
assistants,
|
||||
apiServerEnabled,
|
||||
apiGatewayEnabled,
|
||||
agentsLoading,
|
||||
agentsError,
|
||||
updateAssistants
|
||||
@ -68,7 +68,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
unifiedItems,
|
||||
assistants,
|
||||
agents,
|
||||
apiServerEnabled,
|
||||
apiGatewayEnabled,
|
||||
agentsLoading,
|
||||
agentsError,
|
||||
updateAssistants
|
||||
|
||||
@ -15,9 +15,9 @@ const SessionsTab: FC<SessionsTabProps> = () => {
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId } = chat
|
||||
const { t } = useTranslation()
|
||||
const { apiServer } = useSettings()
|
||||
const { apiGateway } = useSettings()
|
||||
|
||||
if (!apiServer.enabled) {
|
||||
if (!apiGateway.enabled) {
|
||||
return <Alert type="warning" message={t('agent.warning.enable_server')} style={{ margin: 10 }} />
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import AddAssistantOrAgentPopup from '@renderer/components/Popups/AddAssistantOrAgentPopup'
|
||||
import AgentModalPopup from '@renderer/components/Popups/agent/AgentModal'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import { useApiGateway } from '@renderer/hooks/useApiGateway'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import type { AgentEntity, Assistant, Topic } from '@renderer/types'
|
||||
@ -19,7 +19,7 @@ interface UnifiedAddButtonProps {
|
||||
const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant, setActiveAssistant, setActiveAgentId }) => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const { apiServerRunning, startApiServer } = useApiServer()
|
||||
const { apiGatewayRunning, startApiGateway } = useApiGateway()
|
||||
|
||||
const afterCreate = useCallback(
|
||||
(a: AgentEntity) => {
|
||||
@ -54,7 +54,7 @@ const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant, setAct
|
||||
}
|
||||
|
||||
if (type === 'agent') {
|
||||
!apiServerRunning && startApiServer()
|
||||
!apiGatewayRunning && startApiGateway()
|
||||
AgentModalPopup.show({ afterSubmit: afterCreate })
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,14 +12,14 @@ interface UseUnifiedGroupingOptions {
|
||||
unifiedItems: UnifiedItem[]
|
||||
assistants: Assistant[]
|
||||
agents: AgentEntity[]
|
||||
apiServerEnabled: boolean
|
||||
apiGatewayEnabled: boolean
|
||||
agentsLoading: boolean
|
||||
agentsError: Error | null
|
||||
updateAssistants: (assistants: Assistant[]) => void
|
||||
}
|
||||
|
||||
export const useUnifiedGrouping = (options: UseUnifiedGroupingOptions) => {
|
||||
const { unifiedItems, assistants, agents, apiServerEnabled, agentsLoading, agentsError, updateAssistants } = options
|
||||
const { unifiedItems, assistants, agents, apiGatewayEnabled, agentsLoading, agentsError, updateAssistants } = options
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
@ -102,7 +102,7 @@ export const useUnifiedGrouping = (options: UseUnifiedGroupingOptions) => {
|
||||
const availableAgents = new Map<string, AgentEntity>()
|
||||
const availableAssistants = new Map<string, Assistant>()
|
||||
|
||||
if (apiServerEnabled && !agentsLoading && !agentsError) {
|
||||
if (apiGatewayEnabled && !agentsLoading && !agentsError) {
|
||||
agents.forEach((agent) => availableAgents.set(agent.id, agent))
|
||||
}
|
||||
updatedAssistants.forEach((assistant) => availableAssistants.set(assistant.id, assistant))
|
||||
@ -147,7 +147,7 @@ export const useUnifiedGrouping = (options: UseUnifiedGroupingOptions) => {
|
||||
assistants,
|
||||
t,
|
||||
updateAssistants,
|
||||
apiServerEnabled,
|
||||
apiGatewayEnabled,
|
||||
agentsLoading,
|
||||
agentsError,
|
||||
agents,
|
||||
|
||||
@ -8,14 +8,14 @@ export type UnifiedItem = { type: 'agent'; data: AgentEntity } | { type: 'assist
|
||||
interface UseUnifiedItemsOptions {
|
||||
agents: AgentEntity[]
|
||||
assistants: Assistant[]
|
||||
apiServerEnabled: boolean
|
||||
apiGatewayEnabled: boolean
|
||||
agentsLoading: boolean
|
||||
agentsError: Error | null
|
||||
updateAssistants: (assistants: Assistant[]) => void
|
||||
}
|
||||
|
||||
export const useUnifiedItems = (options: UseUnifiedItemsOptions) => {
|
||||
const { agents, assistants, apiServerEnabled, agentsLoading, agentsError, updateAssistants } = options
|
||||
const { agents, assistants, apiGatewayEnabled, agentsLoading, agentsError, updateAssistants } = options
|
||||
const dispatch = useAppDispatch()
|
||||
const unifiedListOrder = useAppSelector((state) => state.assistants.unifiedListOrder || [])
|
||||
|
||||
@ -27,7 +27,7 @@ export const useUnifiedItems = (options: UseUnifiedItemsOptions) => {
|
||||
const availableAgents = new Map<string, AgentEntity>()
|
||||
const availableAssistants = new Map<string, Assistant>()
|
||||
|
||||
if (apiServerEnabled && !agentsLoading && !agentsError) {
|
||||
if (apiGatewayEnabled && !agentsLoading && !agentsError) {
|
||||
agents.forEach((agent) => availableAgents.set(agent.id, agent))
|
||||
}
|
||||
assistants.forEach((assistant) => availableAssistants.set(assistant.id, assistant))
|
||||
@ -50,7 +50,7 @@ export const useUnifiedItems = (options: UseUnifiedItemsOptions) => {
|
||||
items.unshift(...newItems)
|
||||
|
||||
return items
|
||||
}, [agents, assistants, apiServerEnabled, agentsLoading, agentsError, unifiedListOrder])
|
||||
}, [agents, assistants, apiGatewayEnabled, agentsLoading, agentsError, unifiedListOrder])
|
||||
|
||||
const handleUnifiedListReorder = useCallback(
|
||||
(newList: UnifiedItem[]) => {
|
||||
|
||||
@ -36,7 +36,7 @@ import QuickAssistantSettings from './QuickAssistantSettings'
|
||||
import QuickPhraseSettings from './QuickPhraseSettings'
|
||||
import SelectionAssistantSettings from './SelectionAssistantSettings/SelectionAssistantSettings'
|
||||
import ShortcutSettings from './ShortcutSettings'
|
||||
import { ApiServerSettings } from './ToolSettings/ApiServerSettings'
|
||||
import { ApiGatewaySettings } from './ToolSettings/ApiGatewaySettings'
|
||||
import WebSearchSettings from './WebSearchSettings'
|
||||
|
||||
const SettingsPage: FC = () => {
|
||||
@ -152,7 +152,7 @@ const SettingsPage: FC = () => {
|
||||
<Route path="provider" element={<ProviderList />} />
|
||||
<Route path="model" element={<ModelSettings />} />
|
||||
<Route path="websearch/*" element={<WebSearchSettings />} />
|
||||
<Route path="api-server" element={<ApiServerSettings />} />
|
||||
<Route path="api-server" element={<ApiGatewaySettings />} />
|
||||
<Route path="docprocess" element={<DocProcessSettings />} />
|
||||
<Route path="quickphrase" element={<QuickPhraseSettings />} />
|
||||
<Route path="mcp/*" element={<MCPSettings />} />
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
import { useApiGateway } from '@renderer/hooks/useApiGateway'
|
||||
import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
@ -8,15 +8,15 @@ import { useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
addApiGatewayModelGroup,
|
||||
removeApiGatewayModelGroup,
|
||||
setApiGatewayApiKey,
|
||||
setApiGatewayEnabledEndpoints,
|
||||
setApiGatewayExposeToNetwork,
|
||||
setApiServerApiKey,
|
||||
setApiServerPort,
|
||||
setApiGatewayPort,
|
||||
updateApiGatewayModelGroup
|
||||
} from '@renderer/store/settings'
|
||||
import type { GatewayEndpoint, ModelGroup } from '@renderer/types'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { API_SERVER_DEFAULTS } from '@shared/config/constant'
|
||||
import { API_GATEWAY_DEFAULTS } from '@shared/config/constant'
|
||||
import { validators } from '@shared/utils'
|
||||
import { Alert, Button, Checkbox, Input, InputNumber, Segmented, Select, Switch, Tooltip, Typography } from 'antd'
|
||||
import { AlertTriangle, Copy, ExternalLink, Play, Plus, RotateCcw, Square, Trash2 } from 'lucide-react'
|
||||
@ -39,50 +39,56 @@ const GATEWAY_ENDPOINTS: { value: GatewayEndpoint; labelKey: string }[] = [
|
||||
|
||||
type EnvFormat = 'openai' | 'anthropic' | 'responses'
|
||||
|
||||
const ApiServerSettings: FC = () => {
|
||||
const ApiGatewaySettings: FC = () => {
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// API Gateway state with proper defaults
|
||||
const apiServerConfig = useSelector((state: RootState) => state.settings.apiServer)
|
||||
const apiGatewayConfig = useSelector((state: RootState) => state.settings.apiGateway)
|
||||
const assistants = useSelector((state: RootState) => state.assistants.assistants)
|
||||
const { apiServerRunning, apiServerLoading, startApiServer, stopApiServer, restartApiServer, setApiServerEnabled } =
|
||||
useApiServer()
|
||||
const {
|
||||
apiGatewayRunning,
|
||||
apiGatewayLoading,
|
||||
startApiGateway,
|
||||
stopApiGateway,
|
||||
restartApiGateway,
|
||||
setApiGatewayEnabled
|
||||
} = useApiGateway()
|
||||
|
||||
const handleApiServerToggle = async (enabled: boolean) => {
|
||||
const handleApiGatewayToggle = async (enabled: boolean) => {
|
||||
try {
|
||||
if (enabled) {
|
||||
await startApiServer()
|
||||
await startApiGateway()
|
||||
} else {
|
||||
await stopApiServer()
|
||||
await stopApiGateway()
|
||||
}
|
||||
} catch (error) {
|
||||
window.toast.error(t('apiServer.messages.operationFailed') + formatErrorMessage(error))
|
||||
window.toast.error(t('apiGateway.messages.operationFailed') + formatErrorMessage(error))
|
||||
} finally {
|
||||
setApiServerEnabled(enabled)
|
||||
setApiGatewayEnabled(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
const handleApiServerRestart = async () => {
|
||||
await restartApiServer()
|
||||
const handleApiGatewayRestart = async () => {
|
||||
await restartApiGateway()
|
||||
}
|
||||
|
||||
const copyApiKey = () => {
|
||||
navigator.clipboard.writeText(apiServerConfig.apiKey)
|
||||
window.toast.success(t('apiServer.messages.apiKeyCopied'))
|
||||
navigator.clipboard.writeText(apiGatewayConfig.apiKey)
|
||||
window.toast.success(t('apiGateway.messages.apiKeyCopied'))
|
||||
}
|
||||
|
||||
const regenerateApiKey = () => {
|
||||
const newApiKey = `cs-sk-${uuidv4()}`
|
||||
dispatch(setApiServerApiKey(newApiKey))
|
||||
window.toast.success(t('apiServer.messages.apiKeyRegenerated'))
|
||||
dispatch(setApiGatewayApiKey(newApiKey))
|
||||
window.toast.success(t('apiGateway.messages.apiKeyRegenerated'))
|
||||
}
|
||||
|
||||
const handlePortChange = (value: string) => {
|
||||
const port = parseInt(value) || API_SERVER_DEFAULTS.PORT
|
||||
const port = parseInt(value) || API_GATEWAY_DEFAULTS.PORT
|
||||
if (port >= 1000 && port <= 65535) {
|
||||
dispatch(setApiServerPort(port))
|
||||
dispatch(setApiGatewayPort(port))
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,9 +101,9 @@ const ApiServerSettings: FC = () => {
|
||||
}
|
||||
|
||||
const openApiDocs = () => {
|
||||
if (apiServerRunning) {
|
||||
const host = apiServerConfig.host || API_SERVER_DEFAULTS.HOST
|
||||
const port = apiServerConfig.port || API_SERVER_DEFAULTS.PORT
|
||||
if (apiGatewayRunning) {
|
||||
const host = apiGatewayConfig.host || API_GATEWAY_DEFAULTS.HOST
|
||||
const port = apiGatewayConfig.port || API_GATEWAY_DEFAULTS.PORT
|
||||
window.open(`http://${host}:${port}/api-docs`, '_blank')
|
||||
}
|
||||
}
|
||||
@ -106,7 +112,7 @@ const ApiServerSettings: FC = () => {
|
||||
const addModelGroup = () => {
|
||||
const newGroup: ModelGroup = {
|
||||
id: uuidv4().slice(0, 8), // Internal identifier
|
||||
name: `group-${apiServerConfig.modelGroups.length + 1}`, // URL-safe name
|
||||
name: `group-${apiGatewayConfig.modelGroups.length + 1}`, // URL-safe name
|
||||
providerId: '',
|
||||
modelId: '',
|
||||
mode: 'model',
|
||||
@ -134,69 +140,69 @@ const ApiServerSettings: FC = () => {
|
||||
</Title>
|
||||
<Text type="secondary">{t('apiGateway.description')}</Text>
|
||||
</HeaderContent>
|
||||
{apiServerRunning && (
|
||||
{apiGatewayRunning && (
|
||||
<Button type="primary" icon={<ExternalLink size={14} />} onClick={openApiDocs}>
|
||||
{t('apiServer.documentation.title')}
|
||||
{t('apiGateway.documentation.title')}
|
||||
</Button>
|
||||
)}
|
||||
</HeaderSection>
|
||||
|
||||
{!apiServerRunning && (
|
||||
{!apiGatewayRunning && (
|
||||
<Alert type="warning" message={t('agent.warning.enable_server')} style={{ marginBottom: 10 }} showIcon />
|
||||
)}
|
||||
|
||||
{/* Server Control Panel with integrated configuration */}
|
||||
<ServerControlPanel $status={apiServerRunning}>
|
||||
<ServerControlPanel $status={apiGatewayRunning}>
|
||||
<StatusSection>
|
||||
<StatusIndicator $status={apiServerRunning} />
|
||||
<StatusIndicator $status={apiGatewayRunning} />
|
||||
<StatusContent>
|
||||
<StatusText $status={apiServerRunning}>
|
||||
{apiServerRunning ? t('apiServer.status.running') : t('apiServer.status.stopped')}
|
||||
<StatusText $status={apiGatewayRunning}>
|
||||
{apiGatewayRunning ? t('apiGateway.status.running') : t('apiGateway.status.stopped')}
|
||||
</StatusText>
|
||||
<StatusSubtext>
|
||||
{apiServerRunning
|
||||
? `http://${apiServerConfig.host || API_SERVER_DEFAULTS.HOST}:${apiServerConfig.port || API_SERVER_DEFAULTS.PORT}`
|
||||
: t('apiServer.fields.port.description')}
|
||||
{apiGatewayRunning
|
||||
? `http://${apiGatewayConfig.host || API_GATEWAY_DEFAULTS.HOST}:${apiGatewayConfig.port || API_GATEWAY_DEFAULTS.PORT}`
|
||||
: t('apiGateway.fields.port.description')}
|
||||
</StatusSubtext>
|
||||
</StatusContent>
|
||||
</StatusSection>
|
||||
|
||||
<ControlSection>
|
||||
{apiServerRunning && (
|
||||
<Tooltip title={t('apiServer.actions.restart.tooltip')}>
|
||||
{apiGatewayRunning && (
|
||||
<Tooltip title={t('apiGateway.actions.restart.tooltip')}>
|
||||
<RestartButton
|
||||
$loading={apiServerLoading}
|
||||
onClick={apiServerLoading ? undefined : handleApiServerRestart}>
|
||||
$loading={apiGatewayLoading}
|
||||
onClick={apiGatewayLoading ? undefined : handleApiGatewayRestart}>
|
||||
<RotateCcw size={14} />
|
||||
<span>{t('apiServer.actions.restart.button')}</span>
|
||||
<span>{t('apiGateway.actions.restart.button')}</span>
|
||||
</RestartButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Port input when server is stopped */}
|
||||
{!apiServerRunning && (
|
||||
{!apiGatewayRunning && (
|
||||
<StyledInputNumber
|
||||
value={apiServerConfig.port}
|
||||
onChange={(value) => handlePortChange(String(value || API_SERVER_DEFAULTS.PORT))}
|
||||
value={apiGatewayConfig.port}
|
||||
onChange={(value) => handlePortChange(String(value || API_GATEWAY_DEFAULTS.PORT))}
|
||||
min={1000}
|
||||
max={65535}
|
||||
disabled={apiServerRunning}
|
||||
placeholder={String(API_SERVER_DEFAULTS.PORT)}
|
||||
disabled={apiGatewayRunning}
|
||||
placeholder={String(API_GATEWAY_DEFAULTS.PORT)}
|
||||
size="middle"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tooltip title={apiServerRunning ? t('apiServer.actions.stop') : t('apiServer.actions.start')}>
|
||||
{apiServerRunning ? (
|
||||
<Tooltip title={apiGatewayRunning ? t('apiGateway.actions.stop') : t('apiGateway.actions.start')}>
|
||||
{apiGatewayRunning ? (
|
||||
<StopButton
|
||||
$loading={apiServerLoading}
|
||||
onClick={apiServerLoading ? undefined : () => handleApiServerToggle(false)}>
|
||||
$loading={apiGatewayLoading}
|
||||
onClick={apiGatewayLoading ? undefined : () => handleApiGatewayToggle(false)}>
|
||||
<Square size={20} style={{ color: 'var(--color-status-error)' }} />
|
||||
</StopButton>
|
||||
) : (
|
||||
<StartButton
|
||||
$loading={apiServerLoading}
|
||||
onClick={apiServerLoading ? undefined : () => handleApiServerToggle(true)}>
|
||||
$loading={apiGatewayLoading}
|
||||
onClick={apiGatewayLoading ? undefined : () => handleApiGatewayToggle(true)}>
|
||||
<Play size={20} style={{ color: 'var(--color-status-success)' }} />
|
||||
</StartButton>
|
||||
)}
|
||||
@ -206,23 +212,23 @@ const ApiServerSettings: FC = () => {
|
||||
|
||||
{/* API Key Configuration - moved to top */}
|
||||
<ConfigurationField>
|
||||
<FieldLabel>{t('apiServer.fields.apiKey.label')}</FieldLabel>
|
||||
<FieldDescription>{t('apiServer.fields.apiKey.description')}</FieldDescription>
|
||||
<FieldLabel>{t('apiGateway.fields.apiKey.label')}</FieldLabel>
|
||||
<FieldDescription>{t('apiGateway.fields.apiKey.description')}</FieldDescription>
|
||||
|
||||
<StyledInput
|
||||
value={apiServerConfig.apiKey}
|
||||
value={apiGatewayConfig.apiKey}
|
||||
readOnly
|
||||
placeholder={t('apiServer.fields.apiKey.placeholder')}
|
||||
placeholder={t('apiGateway.fields.apiKey.placeholder')}
|
||||
size="middle"
|
||||
suffix={
|
||||
<InputButtonContainer>
|
||||
{!apiServerRunning && (
|
||||
<RegenerateButton onClick={regenerateApiKey} disabled={apiServerRunning} type="link">
|
||||
{t('apiServer.actions.regenerate')}
|
||||
{!apiGatewayRunning && (
|
||||
<RegenerateButton onClick={regenerateApiKey} disabled={apiGatewayRunning} type="link">
|
||||
{t('apiGateway.actions.regenerate')}
|
||||
</RegenerateButton>
|
||||
)}
|
||||
<Tooltip title={t('apiServer.fields.apiKey.copyTooltip')}>
|
||||
<InputButton icon={<Copy size={14} />} onClick={copyApiKey} disabled={!apiServerConfig.apiKey} />
|
||||
<Tooltip title={t('apiGateway.fields.apiKey.copyTooltip')}>
|
||||
<InputButton icon={<Copy size={14} />} onClick={copyApiKey} disabled={!apiGatewayConfig.apiKey} />
|
||||
</Tooltip>
|
||||
</InputButtonContainer>
|
||||
}
|
||||
@ -235,7 +241,7 @@ const ApiServerSettings: FC = () => {
|
||||
<FieldDescription>{t('apiGateway.fields.enabledEndpoints.description')}</FieldDescription>
|
||||
|
||||
<Checkbox.Group
|
||||
value={apiServerConfig.enabledEndpoints}
|
||||
value={apiGatewayConfig.enabledEndpoints}
|
||||
onChange={(values) => handleEndpointsChange(values as GatewayEndpoint[])}>
|
||||
<EndpointList>
|
||||
{GATEWAY_ENDPOINTS.map((endpoint) => (
|
||||
@ -262,13 +268,13 @@ const ApiServerSettings: FC = () => {
|
||||
</Button>
|
||||
</FieldHeader>
|
||||
|
||||
{apiServerConfig.modelGroups.length === 0 ? (
|
||||
{apiGatewayConfig.modelGroups.length === 0 ? (
|
||||
<EmptyState>
|
||||
<Text type="secondary">{t('apiGateway.fields.modelGroups.empty')}</Text>
|
||||
</EmptyState>
|
||||
) : (
|
||||
<ModelGroupList>
|
||||
{apiServerConfig.modelGroups.map((group) => (
|
||||
{apiGatewayConfig.modelGroups.map((group) => (
|
||||
<ModelGroupCard
|
||||
key={group.id}
|
||||
group={group}
|
||||
@ -288,9 +294,9 @@ const ApiServerSettings: FC = () => {
|
||||
<FieldLabel>{t('apiGateway.fields.networkAccess.label')}</FieldLabel>
|
||||
<FieldDescription>{t('apiGateway.fields.networkAccess.description')}</FieldDescription>
|
||||
</div>
|
||||
<Switch checked={apiServerConfig.exposeToNetwork} onChange={handleExposeToNetworkChange} />
|
||||
<Switch checked={apiGatewayConfig.exposeToNetwork} onChange={handleExposeToNetworkChange} />
|
||||
</NetworkAccessRow>
|
||||
{apiServerConfig.exposeToNetwork && (
|
||||
{apiGatewayConfig.exposeToNetwork && (
|
||||
<WarningBox>
|
||||
<AlertTriangle size={16} />
|
||||
<span>{t('apiGateway.fields.networkAccess.warning')}</span>
|
||||
@ -318,29 +324,29 @@ const ENV_FORMAT_TO_ENDPOINT: Record<EnvFormat, GatewayEndpoint> = {
|
||||
const ModelGroupCard: FC<ModelGroupCardProps> = ({ group, assistants, onUpdate, onDelete }) => {
|
||||
const { t } = useTranslation()
|
||||
const { providers } = useProviders()
|
||||
const apiServerConfig = useSelector((state: RootState) => state.settings.apiServer)
|
||||
const apiGatewayConfig = useSelector((state: RootState) => state.settings.apiGateway)
|
||||
const [envFormat, setEnvFormat] = useState<EnvFormat>('openai')
|
||||
const mode = group.mode ?? 'model'
|
||||
|
||||
// Reset envFormat when selected endpoint is disabled
|
||||
useEffect(() => {
|
||||
const isCurrentFormatEnabled = apiServerConfig.enabledEndpoints.includes(ENV_FORMAT_TO_ENDPOINT[envFormat])
|
||||
const isCurrentFormatEnabled = apiGatewayConfig.enabledEndpoints.includes(ENV_FORMAT_TO_ENDPOINT[envFormat])
|
||||
if (!isCurrentFormatEnabled) {
|
||||
// Find first enabled format
|
||||
const firstEnabledFormat = (['openai', 'anthropic', 'responses'] as EnvFormat[]).find((fmt) =>
|
||||
apiServerConfig.enabledEndpoints.includes(ENV_FORMAT_TO_ENDPOINT[fmt])
|
||||
apiGatewayConfig.enabledEndpoints.includes(ENV_FORMAT_TO_ENDPOINT[fmt])
|
||||
)
|
||||
if (firstEnabledFormat) {
|
||||
setEnvFormat(firstEnabledFormat)
|
||||
}
|
||||
}
|
||||
}, [apiServerConfig.enabledEndpoints, envFormat])
|
||||
}, [apiGatewayConfig.enabledEndpoints, envFormat])
|
||||
|
||||
// In-place edit for group name (which is also the URL path)
|
||||
const { isEditing, startEdit, inputProps, validationError } = useInPlaceEdit({
|
||||
onSave: async (name) => {
|
||||
// Check for duplicate name
|
||||
const isDuplicate = apiServerConfig.modelGroups.some((g) => g.name === name && g.id !== group.id)
|
||||
const isDuplicate = apiGatewayConfig.modelGroups.some((g) => g.name === name && g.id !== group.id)
|
||||
if (isDuplicate) {
|
||||
throw new Error(t('apiGateway.messages.nameDuplicate'))
|
||||
}
|
||||
@ -363,8 +369,8 @@ const ModelGroupCard: FC<ModelGroupCardProps> = ({ group, assistants, onUpdate,
|
||||
}, [selectedProvider])
|
||||
|
||||
const getBaseUrl = () => {
|
||||
const host = apiServerConfig.exposeToNetwork ? '0.0.0.0' : apiServerConfig.host || API_SERVER_DEFAULTS.HOST
|
||||
const port = apiServerConfig.port || API_SERVER_DEFAULTS.PORT
|
||||
const host = apiGatewayConfig.exposeToNetwork ? '0.0.0.0' : apiGatewayConfig.host || API_GATEWAY_DEFAULTS.HOST
|
||||
const port = apiGatewayConfig.port || API_GATEWAY_DEFAULTS.PORT
|
||||
return `http://${host}:${port}`
|
||||
}
|
||||
|
||||
@ -384,7 +390,7 @@ const ModelGroupCard: FC<ModelGroupCardProps> = ({ group, assistants, onUpdate,
|
||||
|
||||
const copyGroupEnvVars = () => {
|
||||
const baseUrl = getGroupUrl()
|
||||
const apiKey = apiServerConfig.apiKey
|
||||
const apiKey = apiGatewayConfig.apiKey
|
||||
// Responses API uses OpenAI SDK format
|
||||
const prefix = envFormat === 'anthropic' ? 'ANTHROPIC' : 'OPENAI'
|
||||
// OpenAI SDK expects /v1 in the base URL, Anthropic doesn't
|
||||
@ -545,7 +551,7 @@ const ModelGroupCard: FC<ModelGroupCardProps> = ({ group, assistants, onUpdate,
|
||||
{ label: 'Anthropic', value: 'anthropic' },
|
||||
{ label: 'Responses', value: 'responses' }
|
||||
].filter((opt) =>
|
||||
apiServerConfig.enabledEndpoints.includes(ENV_FORMAT_TO_ENDPOINT[opt.value as EnvFormat])
|
||||
apiGatewayConfig.enabledEndpoints.includes(ENV_FORMAT_TO_ENDPOINT[opt.value as EnvFormat])
|
||||
)}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
@ -943,4 +949,4 @@ const ButtonGroup = styled.div`
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default ApiServerSettings
|
||||
export default ApiGatewaySettings
|
||||
@ -0,0 +1 @@
|
||||
export { default as ApiGatewaySettings } from './ApiGatewaySettings'
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user