From 84998aa217a65aee12c331cf5d0fe5b4100cee64 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 3 Apr 2026 23:40:49 +0800 Subject: [PATCH] feat: differentiate fetch models error messages by failure type Distinguish between missing API key, missing endpoint, auth failure, unsupported provider (404/405), and timeout errors instead of showing a generic failure toast for all cases. --- .../providers/forms/ClaudeFormFields.tsx | 13 +++-- .../providers/forms/CodexFormFields.tsx | 13 +++-- .../providers/forms/GeminiFormFields.tsx | 13 +++-- .../providers/forms/OpenClawFormFields.tsx | 13 +++-- .../providers/forms/OpenCodeFormFields.tsx | 13 +++-- src/i18n/locales/en.json | 8 +++- src/i18n/locales/ja.json | 8 +++- src/i18n/locales/zh.json | 8 +++- src/lib/api/model-fetch.ts | 48 +++++++++++++++++++ 9 files changed, 119 insertions(+), 18 deletions(-) diff --git a/src/components/providers/forms/ClaudeFormFields.tsx b/src/components/providers/forms/ClaudeFormFields.tsx index c916de3ac..033a0874c 100644 --- a/src/components/providers/forms/ClaudeFormFields.tsx +++ b/src/components/providers/forms/ClaudeFormFields.tsx @@ -33,7 +33,11 @@ import { copilotGetModelsForAccount, } from "@/lib/api/copilot"; import type { CopilotModel } from "@/lib/api/copilot"; -import { fetchModelsForConfig, type FetchedModel } from "@/lib/api/model-fetch"; +import { + fetchModelsForConfig, + showFetchModelsError, + type FetchedModel, +} from "@/lib/api/model-fetch"; import type { ProviderCategory, ClaudeApiFormat, @@ -186,7 +190,10 @@ export function ClaudeFormFields({ const handleFetchModels = useCallback(() => { if (!baseUrl || !apiKey) { - toast.error(t("providerForm.fetchModelsFailed")); + showFetchModelsError(null, t, { + hasApiKey: !!apiKey, + hasBaseUrl: !!baseUrl, + }); return; } setIsFetchingModels(true); @@ -203,7 +210,7 @@ export function ClaudeFormFields({ }) .catch((err) => { console.warn("[ModelFetch] Failed:", err); - toast.error(t("providerForm.fetchModelsFailed")); + showFetchModelsError(err, t); }) .finally(() => setIsFetchingModels(false)); }, [baseUrl, apiKey, isFullUrl, t]); diff --git a/src/components/providers/forms/CodexFormFields.tsx b/src/components/providers/forms/CodexFormFields.tsx index dea13df71..55faec6ca 100644 --- a/src/components/providers/forms/CodexFormFields.tsx +++ b/src/components/providers/forms/CodexFormFields.tsx @@ -5,7 +5,11 @@ import { toast } from "sonner"; import { Download, Loader2 } from "lucide-react"; import EndpointSpeedTest from "./EndpointSpeedTest"; import { ApiKeySection, EndpointField, ModelInputWithFetch } from "./shared"; -import { fetchModelsForConfig, type FetchedModel } from "@/lib/api/model-fetch"; +import { + fetchModelsForConfig, + showFetchModelsError, + type FetchedModel, +} from "@/lib/api/model-fetch"; import type { ProviderCategory } from "@/types"; interface EndpointCandidate { @@ -75,7 +79,10 @@ export function CodexFormFields({ const handleFetchModels = useCallback(() => { if (!codexBaseUrl || !codexApiKey) { - toast.error(t("providerForm.fetchModelsFailed")); + showFetchModelsError(null, t, { + hasApiKey: !!codexApiKey, + hasBaseUrl: !!codexBaseUrl, + }); return; } setIsFetchingModels(true); @@ -92,7 +99,7 @@ export function CodexFormFields({ }) .catch((err) => { console.warn("[ModelFetch] Failed:", err); - toast.error(t("providerForm.fetchModelsFailed")); + showFetchModelsError(err, t); }) .finally(() => setIsFetchingModels(false)); }, [codexBaseUrl, codexApiKey, isFullUrl, t]); diff --git a/src/components/providers/forms/GeminiFormFields.tsx b/src/components/providers/forms/GeminiFormFields.tsx index 293b974f8..a01a16c56 100644 --- a/src/components/providers/forms/GeminiFormFields.tsx +++ b/src/components/providers/forms/GeminiFormFields.tsx @@ -6,7 +6,11 @@ import { Button } from "@/components/ui/button"; import { toast } from "sonner"; import EndpointSpeedTest from "./EndpointSpeedTest"; import { ApiKeySection, EndpointField, ModelInputWithFetch } from "./shared"; -import { fetchModelsForConfig, type FetchedModel } from "@/lib/api/model-fetch"; +import { + fetchModelsForConfig, + showFetchModelsError, + type FetchedModel, +} from "@/lib/api/model-fetch"; import type { ProviderCategory } from "@/types"; interface EndpointCandidate { @@ -74,7 +78,10 @@ export function GeminiFormFields({ const handleFetchModels = useCallback(() => { if (!baseUrl || !apiKey) { - toast.error(t("providerForm.fetchModelsFailed")); + showFetchModelsError(null, t, { + hasApiKey: !!apiKey, + hasBaseUrl: !!baseUrl, + }); return; } setIsFetchingModels(true); @@ -91,7 +98,7 @@ export function GeminiFormFields({ }) .catch((err) => { console.warn("[ModelFetch] Failed:", err); - toast.error(t("providerForm.fetchModelsFailed")); + showFetchModelsError(err, t); }) .finally(() => setIsFetchingModels(false)); }, [baseUrl, apiKey, t]); diff --git a/src/components/providers/forms/OpenClawFormFields.tsx b/src/components/providers/forms/OpenClawFormFields.tsx index 4291a73f5..7253c1697 100644 --- a/src/components/providers/forms/OpenClawFormFields.tsx +++ b/src/components/providers/forms/OpenClawFormFields.tsx @@ -35,7 +35,11 @@ import { } from "@/components/ui/dropdown-menu"; import { Checkbox } from "@/components/ui/checkbox"; import { ApiKeySection } from "./shared"; -import { fetchModelsForConfig, type FetchedModel } from "@/lib/api/model-fetch"; +import { + fetchModelsForConfig, + showFetchModelsError, + type FetchedModel, +} from "@/lib/api/model-fetch"; import { openclawApiProtocols } from "@/config/openclawProviderPresets"; import type { ProviderCategory, OpenClawModel } from "@/types"; @@ -129,7 +133,10 @@ export function OpenClawFormFields({ // Fetch models from API const handleFetchModels = useCallback(() => { if (!baseUrl || !apiKey) { - toast.error(t("providerForm.fetchModelsFailed")); + showFetchModelsError(null, t, { + hasApiKey: !!apiKey, + hasBaseUrl: !!baseUrl, + }); return; } setIsFetchingModels(true); @@ -146,7 +153,7 @@ export function OpenClawFormFields({ }) .catch((err) => { console.warn("[ModelFetch] Failed:", err); - toast.error(t("providerForm.fetchModelsFailed")); + showFetchModelsError(err, t); }) .finally(() => setIsFetchingModels(false)); }, [baseUrl, apiKey, t]); diff --git a/src/components/providers/forms/OpenCodeFormFields.tsx b/src/components/providers/forms/OpenCodeFormFields.tsx index 024b7baae..717b166d0 100644 --- a/src/components/providers/forms/OpenCodeFormFields.tsx +++ b/src/components/providers/forms/OpenCodeFormFields.tsx @@ -28,7 +28,11 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { ApiKeySection } from "./shared"; -import { fetchModelsForConfig, type FetchedModel } from "@/lib/api/model-fetch"; +import { + fetchModelsForConfig, + showFetchModelsError, + type FetchedModel, +} from "@/lib/api/model-fetch"; import { opencodeNpmPackages } from "@/config/opencodeProviderPresets"; import { cn } from "@/lib/utils"; import { @@ -247,7 +251,10 @@ export function OpenCodeFormFields({ const handleFetchModels = useCallback(() => { if (!baseUrl || !apiKey) { - toast.error(t("providerForm.fetchModelsFailed")); + showFetchModelsError(null, t, { + hasApiKey: !!apiKey, + hasBaseUrl: !!baseUrl, + }); return; } setIsFetchingModels(true); @@ -264,7 +271,7 @@ export function OpenCodeFormFields({ }) .catch((err) => { console.warn("[ModelFetch] Failed:", err); - toast.error(t("providerForm.fetchModelsFailed")); + showFetchModelsError(err, t); }) .finally(() => setIsFetchingModels(false)); }, [baseUrl, apiKey, t]); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index be231601c..ce540b17a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -795,7 +795,13 @@ "fetchingModels": "Fetching...", "fetchModelsSuccess": "Found {{count}} models", "fetchModelsFailed": "Failed to fetch models", - "fetchModelsEmpty": "No models found" + "fetchModelsEmpty": "No models found", + "fetchModelsNeedApiKey": "Please fill in API Key first", + "fetchModelsNeedEndpoint": "Please fill in API endpoint first", + "fetchModelsNeedConfig": "Please fill in API endpoint and API Key first", + "fetchModelsAuthFailed": "API Key is invalid or lacks permission", + "fetchModelsNotSupported": "This provider does not support fetching model list", + "fetchModelsTimeout": "Request timed out, please check network connection" }, "copilot": { "authSection": "GitHub Copilot Authentication", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index d8aac311f..6ddc13dbc 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -795,7 +795,13 @@ "fetchingModels": "取得中...", "fetchModelsSuccess": "{{count}}件のモデルを取得", "fetchModelsFailed": "モデル一覧の取得に失敗しました", - "fetchModelsEmpty": "モデルが見つかりません" + "fetchModelsEmpty": "モデルが見つかりません", + "fetchModelsNeedApiKey": "先に API Key を入力してください", + "fetchModelsNeedEndpoint": "先に API エンドポイントを入力してください", + "fetchModelsNeedConfig": "先に API エンドポイントと API Key を入力してください", + "fetchModelsAuthFailed": "API Key が無効か、権限がありません", + "fetchModelsNotSupported": "このプロバイダーはモデル一覧の取得に対応していません", + "fetchModelsTimeout": "リクエストがタイムアウトしました。ネットワーク接続を確認してください" }, "copilot": { "authSection": "GitHub Copilot 認証", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index a92fac2c2..61174120b 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -795,7 +795,13 @@ "fetchingModels": "正在获取...", "fetchModelsSuccess": "获取到 {{count}} 个模型", "fetchModelsFailed": "获取模型列表失败", - "fetchModelsEmpty": "未找到可用模型" + "fetchModelsEmpty": "未找到可用模型", + "fetchModelsNeedApiKey": "请先填写 API Key", + "fetchModelsNeedEndpoint": "请先填写 API 端点", + "fetchModelsNeedConfig": "请先填写 API 端点和 API Key", + "fetchModelsAuthFailed": "API Key 无效或无权限", + "fetchModelsNotSupported": "该供应商不支持获取模型列表", + "fetchModelsTimeout": "请求超时,请检查网络连接" }, "copilot": { "authSection": "GitHub Copilot 认证", diff --git a/src/lib/api/model-fetch.ts b/src/lib/api/model-fetch.ts index c63add582..2595587d6 100644 --- a/src/lib/api/model-fetch.ts +++ b/src/lib/api/model-fetch.ts @@ -1,4 +1,6 @@ import { invoke } from "@tauri-apps/api/core"; +import type { TFunction } from "i18next"; +import { toast } from "sonner"; export interface FetchedModel { id: string; @@ -18,3 +20,49 @@ export async function fetchModelsForConfig( ): Promise { return invoke("fetch_models_for_config", { baseUrl, apiKey, isFullUrl }); } + +/** + * 根据错误类型显示对应的 toast 提示 + */ +export function showFetchModelsError( + err: unknown, + t: TFunction, + opts?: { hasApiKey: boolean; hasBaseUrl: boolean }, +): void { + // 前端预检:缺少必填字段 + if (opts && !opts.hasBaseUrl && !opts.hasApiKey) { + toast.error(t("providerForm.fetchModelsNeedConfig")); + return; + } + if (opts && !opts.hasApiKey) { + toast.error(t("providerForm.fetchModelsNeedApiKey")); + return; + } + if (opts && !opts.hasBaseUrl) { + toast.error(t("providerForm.fetchModelsNeedEndpoint")); + return; + } + + // 解析后端错误字符串 + const msg = String(err); + + if (msg.includes("HTTP 401") || msg.includes("HTTP 403")) { + toast.error(t("providerForm.fetchModelsAuthFailed")); + return; + } + if (msg.includes("HTTP 404") || msg.includes("HTTP 405")) { + toast.error(t("providerForm.fetchModelsNotSupported")); + return; + } + if (msg.includes("timeout") || msg.includes("timed out")) { + toast.error(t("providerForm.fetchModelsTimeout")); + return; + } + if (msg.includes("Failed to parse")) { + toast.error(t("providerForm.fetchModelsNotSupported")); + return; + } + + // 通用兜底 + toast.error(t("providerForm.fetchModelsFailed")); +}