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_Stop = 'api-server:stop',
ApiServer_Restart = 'api-server:restart', ApiServer_Restart = 'api-server:restart',
ApiServer_GetStatus = 'api-server:get-status', ApiServer_GetStatus = 'api-server:get-status',
// NOTE: This api is not be used.
ApiServer_GetConfig = 'api-server:get-config', ApiServer_GetConfig = 'api-server:get-config',
// Anthropic OAuth // Anthropic OAuth

View File

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

View File

@ -12,6 +12,7 @@ import {
FileListResponse, FileListResponse,
FileMetadata, FileMetadata,
FileUploadResponse, FileUploadResponse,
GetApiServerStatusResult,
KnowledgeBaseParams, KnowledgeBaseParams,
KnowledgeItem, KnowledgeItem,
KnowledgeSearchResult, KnowledgeSearchResult,
@ -22,8 +23,11 @@ import {
OcrProvider, OcrProvider,
OcrResult, OcrResult,
Provider, Provider,
RestartApiServerStatusResult,
S3Config, S3Config,
Shortcut, Shortcut,
StartApiServerStatusResult,
StopApiServerStatusResult,
SupportedOcrFile, SupportedOcrFile,
ThemeMode, ThemeMode,
WebDavConfig WebDavConfig
@ -496,6 +500,12 @@ const api = {
ipcRenderer.removeListener(channel, listener) 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 { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr' import useSWR from 'swr'
import { useApiServer } from '../useApiServer'
import { useAgentClient } from './useAgentClient' import { useAgentClient } from './useAgentClient'
export const useAgent = (id: string | null) => { export const useAgent = (id: string | null) => {
const { t } = useTranslation()
const client = useAgentClient() const client = useAgentClient()
const key = id ? client.agentPaths.withId(id) : null const key = id ? client.agentPaths.withId(id) : null
const { apiServerConfig, apiServerRunning } = useApiServer()
const fetcher = useCallback(async () => { const fetcher = useCallback(async () => {
if (!id || id === 'fake') { if (!id || id === 'fake') {
return null 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) const result = await client.getAgent(id)
return result return result
}, [client, id]) }, [apiServerConfig.enabled, apiServerRunning, client, id, t])
const { data, error, isLoading } = useSWR(key, id ? fetcher : null) const { data, error, isLoading } = useSWR(key, id ? fetcher : null)
return { return {

View File

@ -6,6 +6,7 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useSWR from 'swr' import useSWR from 'swr'
import { useApiServer } from '../useApiServer'
import { useRuntime } from '../useRuntime' import { useRuntime } from '../useRuntime'
import { useAgentClient } from './useAgentClient' import { useAgentClient } from './useAgentClient'
@ -23,11 +24,18 @@ export const useAgents = () => {
const { t } = useTranslation() const { t } = useTranslation()
const client = useAgentClient() const client = useAgentClient()
const key = client.agentPaths.base const key = client.agentPaths.base
const { apiServerConfig, apiServerRunning } = useApiServer()
const fetcher = useCallback(async () => { 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() const result = await client.listAgents()
// NOTE: We only use the array for now. useUpdateAgent depends on this behavior. // NOTE: We only use the array for now. useUpdateAgent depends on this behavior.
return result.data return result.data
}, [client]) }, [apiServerConfig.enabled, apiServerRunning, client, t])
const { data, error, isLoading, mutate } = useSWR(key, fetcher) const { data, error, isLoading, mutate } = useSWR(key, fetcher)
const { chat } = useRuntime() const { chat } = useRuntime()
const { activeAgentId } = chat 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." "failed": "Failed to list agents."
} }
}, },
"server": {
"error": {
"not_running": "The API server is enabled but not running properly."
}
},
"session": { "session": {
"accessible_paths": { "accessible_paths": {
"add": "Add directory", "add": "Add directory",
@ -237,6 +242,7 @@
"messages": { "messages": {
"apiKeyCopied": "API Key copied to clipboard", "apiKeyCopied": "API Key copied to clipboard",
"apiKeyRegenerated": "API Key regenerated", "apiKeyRegenerated": "API Key regenerated",
"notEnabled": "The API Server is not enabled.",
"operationFailed": "API Server operation failed: ", "operationFailed": "API Server operation failed: ",
"restartError": "Failed to restart API Server: ", "restartError": "Failed to restart API Server: ",
"restartFailed": "API Server restart failed: ", "restartFailed": "API Server restart failed: ",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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