diff --git a/client/src/api/settings.ts b/client/src/api/settings.ts index 13d98eaa42..c2ac163707 100644 --- a/client/src/api/settings.ts +++ b/client/src/api/settings.ts @@ -169,6 +169,18 @@ export interface ModelRouteConnectivityResponse { statuses: ModelRouteConnectivityStatus[]; } +export type ModelRouteConnectivityCheckStatus = "idle" | "running" | "done"; + +export interface ModelRouteConnectivityCheckResponse { + status: ModelRouteConnectivityCheckStatus; + taskId: string | null; + startedAt: string | null; + completedAt: string | null; + fingerprint: string | null; + result: ModelRouteConnectivityResponse | null; + cached?: boolean; +} + export interface StructuredFallbackSettings { enabled: boolean; provider: LLMProvider; @@ -418,8 +430,18 @@ export async function getModelRoutes() { return data; } -export async function testModelRouteConnectivity() { - const { data } = await apiClient.post>("/llm/model-routes/connectivity"); +export async function testModelRouteConnectivity(force = false) { + const { data } = await apiClient.post>( + "/llm/model-routes/connectivity", + { force }, + ); + return data; +} + +export async function getModelRouteConnectivityStatus() { + const { data } = await apiClient.get>( + "/llm/model-routes/connectivity/status", + ); return data; } diff --git a/client/src/hooks/useModelRouteCheck.ts b/client/src/hooks/useModelRouteCheck.ts new file mode 100644 index 0000000000..e38095bcd8 --- /dev/null +++ b/client/src/hooks/useModelRouteCheck.ts @@ -0,0 +1,48 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { getModelRouteConnectivityStatus, testModelRouteConnectivity } from "@/api/settings"; +import { queryKeys } from "@/api/queryKeys"; + +/** + * 模型路由连通性检查统一 Hook。 + * + * - 所有设置页面共用同一状态源(后端 single-flight 状态机 + 指纹缓存) + * - 进入页面只查 GET /status(轻量),绝不自动触发探针 + * - running 时自动轮询(2s),done/idle 停止 + * - triggerCheck(force):手动刷新;checkAfterConfigChange():保存路由后自动检查 + */ +export function useModelRouteCheck() { + const queryClient = useQueryClient(); + const checkKey = queryKeys.settings.modelRouteConnectivity; + + const statusQuery = useQuery({ + queryKey: checkKey, + queryFn: getModelRouteConnectivityStatus, + refetchInterval: (query) => (query.state.data?.data?.status === "running" ? 2000 : false), + refetchOnWindowFocus: false, + }); + + const triggerCheck = useMutation({ + mutationFn: (force: boolean) => testModelRouteConnectivity(force), + onSuccess: (resp) => { + // 统一状态写入,跨页面共享 + queryClient.setQueryData(checkKey, resp); + if (resp.data?.status === "running") { + void statusQuery.refetch(); + } + }, + }); + + return { + /** 当前检查状态(idle/running/done + result),来自 GET /status */ + status: statusQuery.data?.data, + /** 是否正在检查(POST 进行中 或 后端 running) */ + isChecking: triggerCheck.isPending || statusQuery.data?.data?.status === "running", + /** 触发检查:true 强制绕过指纹缓存(仍受 single-flight 约束) */ + triggerCheck, + /** 配置变更后自动触发新检查(指纹变化 → 后端启动新任务) */ + checkAfterConfigChange: () => { + void queryClient.invalidateQueries({ queryKey: queryKeys.settings.modelRoutes }); + triggerCheck.mutate(false); + }, + }; +} diff --git a/client/src/pages/settings/ModelRoutesPage.tsx b/client/src/pages/settings/ModelRoutesPage.tsx index 444239cfb8..82e786ab4a 100644 --- a/client/src/pages/settings/ModelRoutesPage.tsx +++ b/client/src/pages/settings/ModelRoutesPage.tsx @@ -9,8 +9,8 @@ import { getStructuredFallbackConfig, saveModelRoute, saveStructuredFallbackConfig, - testModelRouteConnectivity, } from "@/api/settings"; +import { useModelRouteCheck } from "@/hooks/useModelRouteCheck"; import { queryKeys } from "@/api/queryKeys"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -61,12 +61,12 @@ export default function ModelRoutesPage() { queryFn: getModelRoutes, }); - const modelRouteConnectivityQuery = useQuery({ - queryKey: queryKeys.settings.modelRouteConnectivity, - queryFn: testModelRouteConnectivity, - enabled: modelRoutesQuery.isSuccess, - refetchOnWindowFocus: false, - }); + const { + status: connectivityCheck, + isChecking, + triggerCheck, + checkAfterConfigChange, + } = useModelRouteCheck(); const structuredFallbackQuery = useQuery({ queryKey: queryKeys.settings.structuredFallback, @@ -78,10 +78,8 @@ export default function ModelRoutesPage() { mutationFn: (payload: RouteSavePayload) => saveModelRoute(payload), onSuccess: async () => { setActionResult("保存完成,这个任务会使用新路由。"); - await Promise.all([ - queryClient.invalidateQueries({ queryKey: queryKeys.settings.modelRoutes }), - queryClient.invalidateQueries({ queryKey: queryKeys.settings.modelRouteConnectivity }), - ]); + await queryClient.invalidateQueries({ queryKey: queryKeys.settings.modelRoutes }); + checkAfterConfigChange(); }, }); @@ -92,10 +90,8 @@ export default function ModelRoutesPage() { }, onSuccess: async (count) => { setActionResult(`保存完成,${count} 个任务会使用新路由。`); - await Promise.all([ - queryClient.invalidateQueries({ queryKey: queryKeys.settings.modelRoutes }), - queryClient.invalidateQueries({ queryKey: queryKeys.settings.modelRouteConnectivity }), - ]); + await queryClient.invalidateQueries({ queryKey: queryKeys.settings.modelRoutes }); + checkAfterConfigChange(); }, }); @@ -103,16 +99,14 @@ export default function ModelRoutesPage() { mutationFn: (payload: Partial) => saveStructuredFallbackConfig(payload), onSuccess: async () => { setActionResult("结构化备用模型保存完成。"); - await Promise.all([ - queryClient.invalidateQueries({ queryKey: queryKeys.settings.structuredFallback }), - queryClient.invalidateQueries({ queryKey: queryKeys.settings.modelRouteConnectivity }), - ]); + await queryClient.invalidateQueries({ queryKey: queryKeys.settings.structuredFallback }); + checkAfterConfigChange(); }, }); const providerConfigs = useMemo(() => apiKeySettingsQuery.data?.data ?? [], [apiKeySettingsQuery.data?.data]); const modelRoutes = modelRoutesQuery.data?.data; - const modelRouteConnectivity = modelRouteConnectivityQuery.data?.data; + const modelRouteConnectivity = connectivityCheck?.result; const structuredFallback = structuredFallbackQuery.data?.data; const taskTypes = modelRoutes?.taskTypes ?? []; const providerOptions = useMemo(() => providerConfigs.map((item) => item.provider), [providerConfigs]); @@ -265,7 +259,7 @@ export default function ModelRoutesPage() {
0 ? "failed" @@ -273,7 +267,7 @@ export default function ModelRoutesPage() { ? "healthy" : "idle"} /> - {modelRouteConnectivityQuery.isPending || modelRouteConnectivityQuery.isFetching + {isChecking ? "正在检测生效路由..." : connectivitySummary.total > 0 ? `检测结果:${connectivitySummary.total} 条路由,健康 ${connectivitySummary.healthy},异常 ${connectivitySummary.failed}` @@ -287,11 +281,11 @@ export default function ModelRoutesPage() {