From 85334d8dced436d6dd84c15e18cd4b41d15edb80 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 3 Nov 2025 10:24:59 +0800 Subject: [PATCH] refine(usage): enhance query robustness and error handling Backend improvements: - Add InvalidHttpMethod error enum for better error semantics - Clamp HTTP timeout to 2-30s to prevent config abuse - Strict HTTP method validation instead of silent fallback to GET Frontend improvements: - Add i18n support for usage query errors (en/zh) - Improve error handling with type-safe unknown instead of any - Optimize i18n import (direct import instead of dynamic) - Disable auto-retry for usage queries to avoid API stampede Additional changes: - Apply prettier formatting to affected files Files changed: - src-tauri/src/error.rs (+2) - src-tauri/src/usage_script.rs (+8 -2) - src/i18n/locales/{en,zh}.json (+4 -1 each) - src/lib/api/usage.ts (+21 -4) - src/lib/query/queries.ts (+1) - style: prettier formatting on 6 other files --- src-tauri/src/error.rs | 2 ++ src-tauri/src/usage_script.rs | 10 ++++++-- .../providers/forms/ClaudeFormFields.tsx | 24 +++++++++++++----- .../providers/forms/ProviderForm.tsx | 5 +++- .../providers/forms/hooks/useModelState.ts | 22 +++++++++++----- src/config/claudeProviderPresets.ts | 23 ++++++++--------- src/i18n/locales/en.json | 5 +++- src/i18n/locales/zh.json | 5 +++- src/lib/api/usage.ts | 25 ++++++++++++++++--- src/lib/query/queries.ts | 1 + src/utils/postChangeSync.ts | 1 - src/utils/providerConfigUtils.ts | 7 +++++- 12 files changed, 94 insertions(+), 36 deletions(-) diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index 62d68e6..093605d 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -46,6 +46,8 @@ pub enum AppError { McpValidation(String), #[error("{0}")] Message(String), + #[error("不支持的 HTTP 方法: {0}")] + InvalidHttpMethod(String), #[error("{zh} ({en})")] Localized { key: &'static str, diff --git a/src-tauri/src/usage_script.rs b/src-tauri/src/usage_script.rs index 29e041d..d88633c 100644 --- a/src-tauri/src/usage_script.rs +++ b/src-tauri/src/usage_script.rs @@ -116,12 +116,18 @@ struct RequestConfig { /// 发送 HTTP 请求 async fn send_http_request(config: &RequestConfig, timeout_secs: u64) -> Result { + // 约束超时范围,防止异常配置导致长时间阻塞 + let timeout = timeout_secs.clamp(2, 30); let client = Client::builder() - .timeout(Duration::from_secs(timeout_secs)) + .timeout(Duration::from_secs(timeout)) .build() .map_err(|e| AppError::Message(format!("创建客户端失败: {}", e)))?; - let method = config.method.parse().unwrap_or(reqwest::Method::GET); + // 严格校验 HTTP 方法,非法值不回退为 GET + let method: reqwest::Method = config + .method + .parse() + .map_err(|_| AppError::InvalidHttpMethod(config.method.clone()))?; let mut req = client.request(method.clone(), &config.url); diff --git a/src/components/providers/forms/ClaudeFormFields.tsx b/src/components/providers/forms/ClaudeFormFields.tsx index 22bb850..1506ea3 100644 --- a/src/components/providers/forms/ClaudeFormFields.tsx +++ b/src/components/providers/forms/ClaudeFormFields.tsx @@ -166,7 +166,9 @@ export function ClaudeFormFields({ id="claudeModel" type="text" value={claudeModel} - onChange={(e) => onModelChange("ANTHROPIC_MODEL", e.target.value)} + onChange={(e) => + onModelChange("ANTHROPIC_MODEL", e.target.value) + } placeholder={t("providerForm.modelPlaceholder", { defaultValue: "claude-3-7-sonnet-20250219", })} @@ -177,7 +179,9 @@ export function ClaudeFormFields({ {/* 默认 Haiku */}
- {t("providerForm.anthropicDefaultHaikuModel", { defaultValue: "Haiku 默认模型" })} + {t("providerForm.anthropicDefaultHaikuModel", { + defaultValue: "Haiku 默认模型", + })} - {t("providerForm.anthropicDefaultSonnetModel", { defaultValue: "Sonnet 默认模型" })} + {t("providerForm.anthropicDefaultSonnetModel", { + defaultValue: "Sonnet 默认模型", + })} - onModelChange("ANTHROPIC_DEFAULT_SONNET_MODEL", e.target.value) + onModelChange( + "ANTHROPIC_DEFAULT_SONNET_MODEL", + e.target.value, + ) } placeholder={t("providerForm.modelPlaceholder", { defaultValue: "claude-3-7-sonnet-20250219", @@ -215,7 +224,9 @@ export function ClaudeFormFields({ {/* 默认 Opus */}
- {t("providerForm.anthropicDefaultOpusModel", { defaultValue: "Opus 默认模型" })} + {t("providerForm.anthropicDefaultOpusModel", { + defaultValue: "Opus 默认模型", + })}

{t("providerForm.modelHelper", { - defaultValue: "可选:指定默认使用的 Claude 模型,留空则使用系统默认。", + defaultValue: + "可选:指定默认使用的 Claude 模型,留空则使用系统默认。", })}

diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index ebd369d..431c528 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -7,7 +7,10 @@ import { Form } from "@/components/ui/form"; import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider"; import type { AppId } from "@/lib/api"; import type { ProviderCategory, ProviderMeta } from "@/types"; -import { providerPresets, type ProviderPreset } from "@/config/claudeProviderPresets"; +import { + providerPresets, + type ProviderPreset, +} from "@/config/claudeProviderPresets"; import { codexProviderPresets, type CodexProviderPreset, diff --git a/src/components/providers/forms/hooks/useModelState.ts b/src/components/providers/forms/hooks/useModelState.ts index 8718736..6e2fe4b 100644 --- a/src/components/providers/forms/hooks/useModelState.ts +++ b/src/components/providers/forms/hooks/useModelState.ts @@ -9,7 +9,10 @@ interface UseModelStateProps { * 管理模型选择状态 * 支持 ANTHROPIC_MODEL 和 ANTHROPIC_SMALL_FAST_MODEL */ -export function useModelState({ settingsConfig, onConfigChange }: UseModelStateProps) { +export function useModelState({ + settingsConfig, + onConfigChange, +}: UseModelStateProps) { const [claudeModel, setClaudeModel] = useState(""); const [defaultHaikuModel, setDefaultHaikuModel] = useState(""); const [defaultSonnetModel, setDefaultSonnetModel] = useState(""); @@ -24,9 +27,12 @@ export function useModelState({ settingsConfig, onConfigChange }: UseModelStateP try { const cfg = settingsConfig ? JSON.parse(settingsConfig) : {}; const env = cfg?.env || {}; - const model = typeof env.ANTHROPIC_MODEL === "string" ? env.ANTHROPIC_MODEL : ""; + const model = + typeof env.ANTHROPIC_MODEL === "string" ? env.ANTHROPIC_MODEL : ""; const small = - typeof env.ANTHROPIC_SMALL_FAST_MODEL === "string" ? env.ANTHROPIC_SMALL_FAST_MODEL : ""; + typeof env.ANTHROPIC_SMALL_FAST_MODEL === "string" + ? env.ANTHROPIC_SMALL_FAST_MODEL + : ""; const haiku = typeof env.ANTHROPIC_DEFAULT_HAIKU_MODEL === "string" ? env.ANTHROPIC_DEFAULT_HAIKU_MODEL @@ -59,12 +65,16 @@ export function useModelState({ settingsConfig, onConfigChange }: UseModelStateP value: string, ) => { if (field === "ANTHROPIC_MODEL") setClaudeModel(value); - if (field === "ANTHROPIC_DEFAULT_HAIKU_MODEL") setDefaultHaikuModel(value); - if (field === "ANTHROPIC_DEFAULT_SONNET_MODEL") setDefaultSonnetModel(value); + if (field === "ANTHROPIC_DEFAULT_HAIKU_MODEL") + setDefaultHaikuModel(value); + if (field === "ANTHROPIC_DEFAULT_SONNET_MODEL") + setDefaultSonnetModel(value); if (field === "ANTHROPIC_DEFAULT_OPUS_MODEL") setDefaultOpusModel(value); try { - const currentConfig = settingsConfig ? JSON.parse(settingsConfig) : { env: {} }; + const currentConfig = settingsConfig + ? JSON.parse(settingsConfig) + : { env: {} }; if (!currentConfig.env) currentConfig.env = {}; // 新键仅写入;旧键不再写入 diff --git a/src/config/claudeProviderPresets.ts b/src/config/claudeProviderPresets.ts index 8933077..f62b2b7 100644 --- a/src/config/claudeProviderPresets.ts +++ b/src/config/claudeProviderPresets.ts @@ -73,10 +73,7 @@ export const providerPresets: ProviderPreset[] = [ }, }, // 请求地址候选(用于地址管理/测速),用户可自行选择/覆盖 - endpointCandidates: [ - "https://aihubmix.com", - "https://api.aihubmix.com", - ], + endpointCandidates: ["https://aihubmix.com", "https://api.aihubmix.com"], category: "aggregator", }, { @@ -203,15 +200,15 @@ export const providerPresets: ProviderPreset[] = [ websiteUrl: "https://platform.minimaxi.com", apiKeyUrl: "https://platform.minimaxi.com/user-center/basic-information", settingsConfig: { - "env": { - "ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic", - "ANTHROPIC_AUTH_TOKEN": "", - "API_TIMEOUT_MS": "3000000", - "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": 1, - "ANTHROPIC_MODEL": "MiniMax-M2", - "ANTHROPIC_DEFAULT_SONNET_MODEL": "MiniMax-M2", - "ANTHROPIC_DEFAULT_OPUS_MODEL": "MiniMax-M2", - "ANTHROPIC_DEFAULT_HAIKU_MODEL": "MiniMax-M2" + env: { + ANTHROPIC_BASE_URL: "https://api.minimaxi.com/anthropic", + ANTHROPIC_AUTH_TOKEN: "", + API_TIMEOUT_MS: "3000000", + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1, + ANTHROPIC_MODEL: "MiniMax-M2", + ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMax-M2", + ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMax-M2", + ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMax-M2", }, }, category: "cn_official", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e3ab942..67fceef 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -373,7 +373,10 @@ "tip2": "• Extractor function runs in sandbox environment, supports ES2020+ syntax", "tip3": "• Entire config must be wrapped in () to form object literal expression" }, - + "errors": { + "usage_query_failed": "Usage query failed" + }, + "presetSelector": { "title": "Select Configuration Type", "custom": "Custom", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 66016e3..96bbf69 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -373,7 +373,10 @@ "tip2": "• extractor 函数在沙箱环境中执行,支持 ES2020+ 语法", "tip3": "• 整个配置必须用 () 包裹,形成对象字面量表达式" }, - + "errors": { + "usage_query_failed": "用量查询失败" + }, + "presetSelector": { "title": "选择配置类型", "custom": "自定义", diff --git a/src/lib/api/usage.ts b/src/lib/api/usage.ts index 89cfe30..badc88c 100644 --- a/src/lib/api/usage.ts +++ b/src/lib/api/usage.ts @@ -1,12 +1,29 @@ import { invoke } from "@tauri-apps/api/core"; import type { UsageResult } from "@/types"; import type { AppId } from "./types"; +import i18n from "@/i18n"; export const usageApi = { async query(providerId: string, appId: AppId): Promise { - return await invoke("query_provider_usage", { - provider_id: providerId, - app: appId, - }); + try { + return await invoke("query_provider_usage", { + provider_id: providerId, + app: appId, + }); + } catch (error: unknown) { + // 提取错误消息:优先使用后端返回的错误信息 + const message = + typeof error === "string" + ? error + : error instanceof Error + ? error.message + : ""; + + // 如果没有错误消息,使用国际化的默认提示 + return { + success: false, + error: message || i18n.t("errors.usage_query_failed"), + }; + } }, }; diff --git a/src/lib/query/queries.ts b/src/lib/query/queries.ts index 21f0945..8f03fa0 100644 --- a/src/lib/query/queries.ts +++ b/src/lib/query/queries.ts @@ -93,6 +93,7 @@ export const useUsageQuery = ( queryFn: async () => usageApi.query(providerId, appId), enabled: enabled && !!providerId, refetchOnWindowFocus: false, + retry: false, staleTime: 5 * 60 * 1000, // 5分钟 }); }; diff --git a/src/utils/postChangeSync.ts b/src/utils/postChangeSync.ts index ed8c1d9..c48977a 100644 --- a/src/utils/postChangeSync.ts +++ b/src/utils/postChangeSync.ts @@ -16,4 +16,3 @@ export async function syncCurrentProvidersLiveSafe(): Promise<{ return { ok: false, error }; } } - diff --git a/src/utils/providerConfigUtils.ts b/src/utils/providerConfigUtils.ts index f577c87..db22342 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -170,7 +170,12 @@ export const getApiKeyFromConfig = (jsonString: string): string => { const config = JSON.parse(jsonString); const token = config?.env?.ANTHROPIC_AUTH_TOKEN; const apiKey = config?.env?.ANTHROPIC_API_KEY; - const value = typeof token === "string" ? token : typeof apiKey === "string" ? apiKey : ""; + const value = + typeof token === "string" + ? token + : typeof apiKey === "string" + ? apiKey + : ""; return value; } catch (err) { return "";