From dc8e03988f66305eca167ff7b5334ecde84481c9 Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 15 Sep 2026 14:41:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DAGC=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=88=B7=E6=96=B0=E6=97=A0=E5=8F=8D=E9=A6=88?= =?UTF-8?q?=E4=B8=8E=E5=93=8D=E5=BA=94=E4=BD=93=E6=97=A0=E9=99=90=E7=AD=89?= =?UTF-8?q?=E5=BE=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConversationModelSelect:手动刷新立即显示「刷新中…」,真实刷新成功后显示「模型列表已刷新」,revision 未变化也有反馈 ConversationModelSelect:刷新失败按 HTTP 状态与超时给出可辨认提示,沿用有效缓存时不报告刷新成功,已选模型与挂载/聚焦语义不变 clientApi:成功与错误响应体统一走 readClientHttpResponseText 的 15 秒上限,响应体卡住时结束等待并释放模型目录在途请求 测试:补齐模型目录响应体超时后保留缓存并可重试、迟到响应不覆盖新目录、手动刷新进行中/同版本成功/失败反馈用例 文档:同步 AGC 模型目录刷新反馈与响应体超时合同,并在共享记忆记录该排障口径 --- .../ConversationModelSelect.tsx | 40 ++++- .../src/services/clientApi.ts | 16 +- .../tests/clientApi.test.ts | 59 +++++++ .../tests/conversationModelSelect.test.tsx | 145 +++++++++++++++++- docs/project-memory/shared-memory/pitfalls.md | 5 + ...方案】AGC后台模型别名与对话选择-2026-09-05.md | 3 + 6 files changed, 253 insertions(+), 15 deletions(-) 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)} >