diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx index 1bc4c9ea4..324f83aad 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx @@ -14,6 +14,8 @@ import type { ClientLlmModel, ClientLlmModelCatalog, } from '../../services/clientApi'; +import { ClientAuthRequestError } from '../../services/clientApi'; +import { ClientHttpTimeoutError } from '../../services/clientHttp'; import { cachedLlmModelCatalog, refreshLlmModelCatalog, @@ -27,6 +29,14 @@ export type ConversationModelSelectHandle = { /** 客户端配置读取/写回失败:与「模型目录加载失败」区分,避免误导提示。 */ class ModelSelectionConfigError extends Error {} +function modelCatalogErrorMessage(error: unknown) { + if (error instanceof ClientHttpTimeoutError) + return '模型列表请求超时,请重试'; + if (error instanceof ClientAuthRequestError && error.status) + return `模型列表加载失败(HTTP ${error.status})`; + return '模型列表加载失败'; +} + export function ConversationModelSelect({ className, disabled, @@ -51,6 +61,7 @@ export function ConversationModelSelect({ const [busy, setBusy] = useState(!initialCatalog); const [error, setError] = useState(''); const [notice, setNotice] = useState(''); + const [manualRefreshBusy, setManualRefreshBusy] = useState(false); const [open, setOpen] = useState(false); const containerRef = useRef(null); const appliedRevisionRef = useRef( @@ -180,7 +191,11 @@ export function ConversationModelSelect({ ); const syncCatalog = useCallback( - async (showBusy: boolean) => { + async (showBusy: boolean, manualRefresh = false) => { + if (manualRefresh && mountedRef.current) { + setManualRefreshBusy(true); + setNotice('正在刷新模型列表'); + } const busyToken = showBusy ? ++busyTokenRef.current : busyTokenRef.current; @@ -195,12 +210,17 @@ export function ConversationModelSelect({ try { let catalog: ClientLlmModelCatalog; let usingCachedCatalog = false; + let catalogError: unknown = null; try { catalog = await refreshLlmModelCatalog(); - } catch { + } catch (error) { + catalogError = error; const cached = cachedLlmModelCatalog(); if (!cached) { - if (mountedRef.current) setError('模型列表加载失败'); + if (mountedRef.current) { + setError(modelCatalogErrorMessage(error)); + setNotice(''); + } markReady(false); return false; } @@ -209,10 +229,14 @@ export function ConversationModelSelect({ } const ready = await applyCatalog(catalog, showBusy, epochAtRequest); if (usingCachedCatalog && mountedRef.current) - setError('模型列表加载失败'); + setError(modelCatalogErrorMessage(catalogError)); + if (manualRefresh && mountedRef.current) { + setNotice(usingCachedCatalog ? '' : '模型列表已刷新'); + } return ready; } catch (error) { if (mountedRef.current) { + setNotice(''); setError( error instanceof ModelSelectionConfigError ? error.message @@ -230,6 +254,7 @@ export function ConversationModelSelect({ ) { setBusy(false); } + if (manualRefresh && mountedRef.current) setManualRefreshBusy(false); } }, [applyCatalog, markReady], @@ -375,11 +400,12 @@ export function ConversationModelSelect({ type="button" className="conversation-model-menu-refresh" aria-label="刷新模型列表" - disabled={disabled || busy} - onClick={() => void syncCatalog(true)} + disabled={disabled || busy || manualRefreshBusy} + aria-busy={manualRefreshBusy} + onClick={() => void syncCatalog(true, true)} >