refactor: rename 'apiServer' to 'apiGateway' (#12352)

This commit is contained in:
SuYao 2026-01-07 23:53:11 +08:00 committed by GitHub
parent 9607ac0798
commit fcbf1a1581
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
106 changed files with 445 additions and 1041 deletions

View File

@ -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',

View File

@ -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
}

View File

@ -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)

View File

@ -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,

View File

@ -0,0 +1,2 @@
export { config } from './config'
export { apiGateway } from './server'

View File

@ -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) => {

View File

@ -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'
]
}

View File

@ -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: {

View File

@ -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) => {

View File

@ -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: {

View File

@ -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 => {

View File

@ -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()

View File

@ -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()

View File

@ -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 })

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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'
/**

View File

@ -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'

View File

@ -1,2 +0,0 @@
export { config } from './config'
export { apiServer } from './server'

View File

@ -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)
}

View File

@ -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())

View File

@ -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()

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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()
}))

View File

@ -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)
}
}
},

View File

@ -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 {

View File

@ -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: {

View File

@ -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()

View 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
}
}

View File

@ -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
}
}

View File

@ -377,7 +377,7 @@
},
"title": "API Gateway"
},
"apiServer": {
"apiGateway": {
"actions": {
"copy": "Copy",
"regenerate": "Regenerate",

View File

@ -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": "重新生成",

View File

@ -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": "重新產生",

View File

@ -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",

View File

@ -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": "Αναδημιουργία",

View File

@ -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",

View File

@ -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",

View File

@ -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": "再生成",

View File

@ -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",

View File

@ -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ă",

View File

@ -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": "Перегенерировать",

View File

@ -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} />

View File

@ -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) {

View File

@ -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) {

View File

@ -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

View File

@ -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 }} />
}

View File

@ -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 })
}
}

View File

@ -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,

View File

@ -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[]) => {

View File

@ -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 />} />

View File

@ -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

View File

@ -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