fix: api server status (#10734)

* refactor(apiServer): move api server types to dedicated module

Restructure api server type definitions by moving them from index.ts to a dedicated apiServer.ts file. This improves code organization and maintainability by grouping related types together.

* feat(api-server): add api server management hooks and integration

Extract api server management logic into reusable hook and integrate with settings page

* feat(api-server): improve api server status handling and error messages

- add new error messages for api server status
- optimize initial state and loading in useApiServer hook
- centralize api server enabled check via useApiServer hook
- update components to use new api server status handling

* fix(agents): update error message key for agent server not running

* fix(i18n): update api server status messages across locales

Remove redundant 'notRunning' message in en-us locale
Add consistent 'not_running' error message in all locales
Add missing 'notEnabled' message in several locales

* refactor: update api server type imports to use @types

Move api server related type imports from renderer/src/types to @types package for better code organization and maintainability

* docs(IpcChannel): add comment about unused api-server:get-config

Add TODO comment about data inconsistency in useApiServer hook

* refactor(assistants): pass apiServerEnabled as prop instead of using hook

Move apiServerEnabled from being fetched via useApiServer hook to being passed as a prop through component hierarchy. This improves maintainability by making dependencies more explicit and reducing hook usage in child components.

* style(AssistantsTab): add consistent margin-bottom to alert components

* feat(useAgent): add api server status checks before fetching agent

Ensure api server is enabled and running before attempting to fetch agent data
This commit is contained in:
Phantom 2025-10-16 12:49:31 +08:00 committed by GitHub
parent 2e173631a0
commit 1a972ac0e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 277 additions and 75 deletions

View File

@ -317,6 +317,7 @@ export enum IpcChannel {
ApiServer_Stop = 'api-server:stop',
ApiServer_Restart = 'api-server:restart',
ApiServer_GetStatus = 'api-server:get-status',
// NOTE: This api is not be used.
ApiServer_GetConfig = 'api-server:get-config',
// Anthropic OAuth

View File

@ -1,5 +1,11 @@
import { IpcChannel } from '@shared/IpcChannel'
import { ApiServerConfig } from '@types'
import {
ApiServerConfig,
GetApiServerStatusResult,
RestartApiServerStatusResult,
StartApiServerStatusResult,
StopApiServerStatusResult
} from '@types'
import { ipcMain } from 'electron'
import { apiServer } from '../apiServer'
@ -52,7 +58,7 @@ export class ApiServerService {
registerIpcHandlers(): void {
// API Server
ipcMain.handle(IpcChannel.ApiServer_Start, async () => {
ipcMain.handle(IpcChannel.ApiServer_Start, async (): Promise<StartApiServerStatusResult> => {
try {
await this.start()
return { success: true }
@ -61,7 +67,7 @@ export class ApiServerService {
}
})
ipcMain.handle(IpcChannel.ApiServer_Stop, async () => {
ipcMain.handle(IpcChannel.ApiServer_Stop, async (): Promise<StopApiServerStatusResult> => {
try {
await this.stop()
return { success: true }
@ -70,7 +76,7 @@ export class ApiServerService {
}
})
ipcMain.handle(IpcChannel.ApiServer_Restart, async () => {
ipcMain.handle(IpcChannel.ApiServer_Restart, async (): Promise<RestartApiServerStatusResult> => {
try {
await this.restart()
return { success: true }
@ -79,7 +85,7 @@ export class ApiServerService {
}
})
ipcMain.handle(IpcChannel.ApiServer_GetStatus, async () => {
ipcMain.handle(IpcChannel.ApiServer_GetStatus, async (): Promise<GetApiServerStatusResult> => {
try {
const config = await this.getCurrentConfig()
return {

View File

@ -12,6 +12,7 @@ import {
FileListResponse,
FileMetadata,
FileUploadResponse,
GetApiServerStatusResult,
KnowledgeBaseParams,
KnowledgeItem,
KnowledgeSearchResult,
@ -22,8 +23,11 @@ import {
OcrProvider,
OcrResult,
Provider,
RestartApiServerStatusResult,
S3Config,
Shortcut,
StartApiServerStatusResult,
StopApiServerStatusResult,
SupportedOcrFile,
ThemeMode,
WebDavConfig
@ -496,6 +500,12 @@ const api = {
ipcRenderer.removeListener(channel, listener)
}
}
},
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)
}
}

View File

@ -1,18 +1,28 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { useApiServer } from '../useApiServer'
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 fetcher = useCallback(async () => {
if (!id || id === 'fake') {
return null
}
if (!apiServerConfig.enabled) {
throw new Error(t('apiServer.messages.notEnabled'))
}
if (!apiServerRunning) {
throw new Error(t('agent.server.error.not_running'))
}
const result = await client.getAgent(id)
return result
}, [client, id])
}, [apiServerConfig.enabled, apiServerRunning, client, id, t])
const { data, error, isLoading } = useSWR(key, id ? fetcher : null)
return {

View File

@ -6,6 +6,7 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { useApiServer } from '../useApiServer'
import { useRuntime } from '../useRuntime'
import { useAgentClient } from './useAgentClient'
@ -23,11 +24,18 @@ export const useAgents = () => {
const { t } = useTranslation()
const client = useAgentClient()
const key = client.agentPaths.base
const { apiServerConfig, apiServerRunning } = useApiServer()
const fetcher = useCallback(async () => {
if (!apiServerConfig.enabled) {
throw new Error(t('apiServer.messages.notEnabled'))
}
if (!apiServerRunning) {
throw new Error(t('agent.server.error.not_running'))
}
const result = await client.listAgents()
// NOTE: We only use the array for now. useUpdateAgent depends on this behavior.
return result.data
}, [client])
}, [apiServerConfig.enabled, apiServerRunning, client, t])
const { data, error, isLoading, mutate } = useSWR(key, fetcher)
const { chat } = useRuntime()
const { activeAgentId } = chat

View File

@ -0,0 +1,112 @@
import { loggerService } from '@logger'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setApiServerEnabled as setApiServerEnabledAction } from '@renderer/store/settings'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
const logger = loggerService.withContext('useApiServer')
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()
// Optimistic initial state.
const [apiServerRunning, setApiServerRunning] = useState(apiServerConfig.enabled)
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)
} catch (error: any) {
logger.error('Failed to check API server status:', error)
} finally {
setApiServerLoading(false)
}
}, [])
const startApiServer = useCallback(async () => {
if (apiServerLoading) return
setApiServerLoading(true)
try {
const result = await window.api.apiServer.start()
if (result.success) {
setApiServerRunning(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, t])
const stopApiServer = useCallback(async () => {
if (apiServerLoading) return
setApiServerLoading(true)
try {
const result = await window.api.apiServer.stop()
if (result.success) {
setApiServerRunning(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, t])
const restartApiServer = useCallback(async () => {
if (apiServerLoading) return
setApiServerLoading(true)
try {
const result = await window.api.apiServer.restart()
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, t])
useEffect(() => {
checkApiServerStatus()
}, [checkApiServerStatus])
return {
apiServerConfig,
apiServerRunning,
apiServerLoading,
startApiServer,
stopApiServer,
restartApiServer,
checkApiServerStatus,
setApiServerEnabled
}
}

View File

@ -30,6 +30,11 @@
"failed": "Failed to list agents."
}
},
"server": {
"error": {
"not_running": "The API server is enabled but not running properly."
}
},
"session": {
"accessible_paths": {
"add": "Add directory",
@ -237,6 +242,7 @@
"messages": {
"apiKeyCopied": "API Key copied to clipboard",
"apiKeyRegenerated": "API Key regenerated",
"notEnabled": "The API Server is not enabled.",
"operationFailed": "API Server operation failed: ",
"restartError": "Failed to restart API Server: ",
"restartFailed": "API Server restart failed: ",

View File

@ -30,6 +30,11 @@
"failed": "获取智能体列表失败"
}
},
"server": {
"error": {
"not_running": "API 服务器已启用但未正常运行。"
}
},
"session": {
"accessible_paths": {
"add": "添加目录",
@ -237,6 +242,7 @@
"messages": {
"apiKeyCopied": "API 密钥已复制到剪贴板",
"apiKeyRegenerated": "API 密钥已重新生成",
"notEnabled": "API 服务器未启用。",
"operationFailed": "API 服务器操作失败:",
"restartError": "重启 API 服务器失败:",
"restartFailed": "API 服务器重启失败:",

View File

@ -30,6 +30,11 @@
"failed": "無法列出代理程式。"
}
},
"server": {
"error": {
"not_running": "API 伺服器已啟用,但運行不正常。"
}
},
"session": {
"accessible_paths": {
"add": "新增目錄",
@ -237,6 +242,7 @@
"messages": {
"apiKeyCopied": "API 金鑰已複製到剪貼簿",
"apiKeyRegenerated": "API 金鑰已重新生成",
"notEnabled": "API 伺服器未啟用。",
"operationFailed": "API 伺服器操作失敗:",
"restartError": "重新啟動 API 伺服器失敗:",
"restartFailed": "API 伺服器重新啟動失敗:",

View File

@ -30,6 +30,11 @@
"failed": "Αποτυχία καταχώρησης πρακτόρων."
}
},
"server": {
"error": {
"not_running": "Ο διακομιστής API είναι ενεργοποιημένος αλλά δεν λειτουργεί σωστά."
}
},
"session": {
"accessible_paths": {
"add": "Προσθήκη καταλόγου",
@ -237,6 +242,7 @@
"messages": {
"apiKeyCopied": "Το κλειδί API αντιγράφηκε στο πρόχειρο",
"apiKeyRegenerated": "Το κλειδί API αναδημιουργήθηκε",
"notEnabled": "Ο διακομιστής API δεν είναι ενεργοποιημένος.",
"operationFailed": "Η λειτουργία του Διακομιστή API απέτυχε: ",
"restartError": "Αποτυχία επανεκκίνησης του Διακομιστή API: ",
"restartFailed": "Η επανεκκίνηση του Διακομιστή API απέτυχε: ",

View File

@ -30,6 +30,11 @@
"failed": "Error al listar agentes."
}
},
"server": {
"error": {
"not_running": "El servidor de API está habilitado pero no funciona correctamente."
}
},
"session": {
"accessible_paths": {
"add": "Agregar directorio",
@ -237,6 +242,7 @@
"messages": {
"apiKeyCopied": "Clave API copiada al portapapeles",
"apiKeyRegenerated": "Clave API regenerada",
"notEnabled": "El servidor de API no está habilitado.",
"operationFailed": "Falló la operación del Servidor API: ",
"restartError": "Error al reiniciar el Servidor API: ",
"restartFailed": "Falló el reinicio del Servidor API: ",

View File

@ -30,6 +30,11 @@
"failed": "Échec de la liste des agents."
}
},
"server": {
"error": {
"not_running": "Le serveur API est activé mais ne fonctionne pas correctement."
}
},
"session": {
"accessible_paths": {
"add": "Ajouter un répertoire",
@ -237,6 +242,7 @@
"messages": {
"apiKeyCopied": "Clé API copiée dans le presse-papiers",
"apiKeyRegenerated": "Clé API régénérée",
"notEnabled": "Le serveur API n'est pas activé.",
"operationFailed": "Opération du Serveur API échouée : ",
"restartError": "Échec du redémarrage du Serveur API : ",
"restartFailed": "Redémarrage du Serveur API échoué : ",

View File

@ -30,6 +30,11 @@
"failed": "エージェントの一覧取得に失敗しました。"
}
},
"server": {
"error": {
"not_running": "APIサーバーは有効になっていますが、正常に動作していません。"
}
},
"session": {
"accessible_paths": {
"add": "ディレクトリを追加",
@ -237,6 +242,7 @@
"messages": {
"apiKeyCopied": "API キーがクリップボードにコピーされました",
"apiKeyRegenerated": "API キーが再生成されました",
"notEnabled": "APIサーバーが有効になっていません。",
"operationFailed": "API サーバーの操作に失敗しました:",
"restartError": "API サーバーの再起動に失敗しました:",
"restartFailed": "API サーバーの再起動に失敗しました:",

View File

@ -30,6 +30,11 @@
"failed": "Falha ao listar agentes."
}
},
"server": {
"error": {
"not_running": "O servidor de API está habilitado, mas não está funcionando corretamente."
}
},
"session": {
"accessible_paths": {
"add": "Adicionar diretório",
@ -237,6 +242,7 @@
"messages": {
"apiKeyCopied": "Chave API copiada para a área de transferência",
"apiKeyRegenerated": "Chave API regenerada",
"notEnabled": "O Servidor de API não está habilitado.",
"operationFailed": "Operação do Servidor API falhou: ",
"restartError": "Falha ao reiniciar o Servidor API: ",
"restartFailed": "Reinício do Servidor API falhou: ",

View File

@ -30,6 +30,11 @@
"failed": "Не удалось получить список агентов."
}
},
"server": {
"error": {
"not_running": "API-сервер включен, но работает неправильно."
}
},
"session": {
"accessible_paths": {
"add": "Добавить каталог",
@ -237,6 +242,7 @@
"messages": {
"apiKeyCopied": "API ключ скопирован в буфер обмена",
"apiKeyRegenerated": "API ключ перегенерирован",
"notEnabled": "API-сервер не включен.",
"operationFailed": "Операция API сервера не удалась: ",
"restartError": "Не удалось перезапустить API сервер: ",
"restartFailed": "Перезапуск API сервера не удался: ",

View File

@ -1,15 +1,16 @@
import { Alert, Spinner } from '@heroui/react'
import Scrollbar from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/agents/useAgents'
import { useApiServer } from '@renderer/hooks/useApiServer'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
import { useTags } from '@renderer/hooks/useTags'
import { useAppDispatch } from '@renderer/store'
import { addIknowAction } from '@renderer/store/runtime'
import { Assistant, AssistantsSortType } from '@renderer/types'
import { getErrorMessage } from '@renderer/utils'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -35,7 +36,8 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
const { activeAssistant, setActiveAssistant, onCreateAssistant, onCreateDefaultAssistant } = props
const containerRef = useRef<HTMLDivElement>(null)
const { t } = useTranslation()
const { apiServer } = useSettings()
const { apiServerConfig, apiServerRunning } = useApiServer()
const apiServerEnabled = apiServerConfig.enabled
const { iknow, chat } = useRuntime()
const dispatch = useAppDispatch()
@ -55,7 +57,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
const { unifiedItems, handleUnifiedListReorder } = useUnifiedItems({
agents,
assistants,
apiServerEnabled: apiServer.enabled,
apiServerEnabled,
agentsLoading,
agentsError,
updateAssistants
@ -72,17 +74,17 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
unifiedItems,
assistants,
agents,
apiServerEnabled: apiServer.enabled,
apiServerEnabled,
agentsLoading,
agentsError,
updateAssistants
})
useEffect(() => {
if (!agentsLoading && agents.length > 0 && !activeAgentId && apiServer.enabled) {
if (!agentsLoading && agents.length > 0 && !activeAgentId && apiServerConfig.enabled) {
setActiveAgentId(agents[0].id)
}
}, [agentsLoading, agents, activeAgentId, setActiveAgentId, apiServer.enabled])
}, [agentsLoading, agents, activeAgentId, setActiveAgentId, apiServerConfig.enabled])
const onDeleteAssistant = useCallback(
(assistant: Assistant) => {
@ -105,7 +107,7 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
return (
<Container className="assistants-tab" ref={containerRef}>
{!apiServer.enabled && !iknow[ALERT_KEY] && (
{!apiServerConfig.enabled && !iknow[ALERT_KEY] && (
<Alert
color="warning"
title={t('agent.warning.enable_server')}
@ -113,11 +115,22 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
onClose={() => {
dispatch(addIknowAction(ALERT_KEY))
}}
className="mb-2"
/>
)}
{agentsLoading && <Spinner />}
{apiServer.enabled && agentsError && <Alert color="danger" title={t('agent.list.error.failed')} />}
{apiServerConfig.enabled && !apiServerRunning && (
<Alert color="danger" title={t('agent.server.error.not_running')} isClosable className="mb-2" />
)}
{apiServerConfig.enabled && apiServerRunning && agentsError && (
<Alert
color="danger"
title={t('agent.list.error.failed')}
description={getErrorMessage(agentsError)}
className="mb-2"
/>
)}
{assistantsTabSortType === 'tags' ? (
<UnifiedTagGroups

View File

@ -1,13 +1,12 @@
// TODO: Refactor this component to use HeroUI
import { useTheme } from '@renderer/context/ThemeProvider'
import { loggerService } from '@renderer/services/LoggerService'
import { useApiServer } from '@renderer/hooks/useApiServer'
import { RootState, useAppDispatch } from '@renderer/store'
import { setApiServerApiKey, setApiServerEnabled, setApiServerPort } from '@renderer/store/settings'
import { setApiServerApiKey, setApiServerPort } from '@renderer/store/settings'
import { formatErrorMessage } from '@renderer/utils/error'
import { IpcChannel } from '@shared/IpcChannel'
import { Button, Input, InputNumber, Tooltip, Typography } from 'antd'
import { Copy, ExternalLink, Play, RotateCcw, Square } from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
@ -15,7 +14,6 @@ import { v4 as uuidv4 } from 'uuid'
import { SettingContainer } from '../..'
const logger = loggerService.withContext('ApiServerSettings')
const { Text, Title } = Typography
const ApiServerSettings: FC = () => {
@ -25,67 +23,25 @@ const ApiServerSettings: FC = () => {
// API Server state with proper defaults
const apiServerConfig = useSelector((state: RootState) => state.settings.apiServer)
const [apiServerRunning, setApiServerRunning] = useState(false)
const [apiServerLoading, setApiServerLoading] = useState(false)
// API Server functions
const checkApiServerStatus = async () => {
try {
const status = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_GetStatus)
setApiServerRunning(status.running)
} catch (error: any) {
logger.error('Failed to check API server status:', error)
}
}
useEffect(() => {
checkApiServerStatus()
}, [])
const { apiServerRunning, apiServerLoading, startApiServer, stopApiServer, restartApiServer, setApiServerEnabled } =
useApiServer()
const handleApiServerToggle = async (enabled: boolean) => {
setApiServerLoading(true)
try {
if (enabled) {
const result = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_Start)
if (result.success) {
setApiServerRunning(true)
window.toast.success(t('apiServer.messages.startSuccess'))
} else {
window.toast.error(t('apiServer.messages.startError') + result.error)
}
await startApiServer()
} else {
const result = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_Stop)
if (result.success) {
setApiServerRunning(false)
window.toast.success(t('apiServer.messages.stopSuccess'))
} else {
window.toast.error(t('apiServer.messages.stopError') + result.error)
}
await stopApiServer()
}
} catch (error) {
window.toast.error(t('apiServer.messages.operationFailed') + formatErrorMessage(error))
} finally {
dispatch(setApiServerEnabled(enabled))
setApiServerLoading(false)
setApiServerEnabled(enabled)
}
}
const handleApiServerRestart = async () => {
setApiServerLoading(true)
try {
const result = await window.electron.ipcRenderer.invoke(IpcChannel.ApiServer_Restart)
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)
}
await restartApiServer()
}
const copyApiKey = () => {

View File

@ -0,0 +1,38 @@
export type ApiServerConfig = {
enabled: boolean
host: string
port: number
apiKey: string
}
export type GetApiServerStatusResult = {
running: boolean
config: ApiServerConfig | null
}
export type StartApiServerStatusResult =
| {
success: true
}
| {
success: false
error: string
}
export type RestartApiServerStatusResult =
| {
success: true
}
| {
success: false
error: string
}
export type StopApiServerStatusResult =
| {
success: true
}
| {
success: false
error: string
}

View File

@ -17,6 +17,7 @@ import type { BaseTool, MCPTool } from './tool'
export * from './agent'
export * from './apiModels'
export * from './apiServer'
export * from './knowledge'
export * from './mcp'
export * from './notification'
@ -848,13 +849,6 @@ export type S3Config = {
}
export type { Message } from './newMessage'
export interface ApiServerConfig {
enabled: boolean
host: string
port: number
apiKey: string
}
export * from './tool'
// Memory Service Types