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
This commit is contained in:
@@ -46,6 +46,8 @@ pub enum AppError {
|
|||||||
McpValidation(String),
|
McpValidation(String),
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Message(String),
|
Message(String),
|
||||||
|
#[error("不支持的 HTTP 方法: {0}")]
|
||||||
|
InvalidHttpMethod(String),
|
||||||
#[error("{zh} ({en})")]
|
#[error("{zh} ({en})")]
|
||||||
Localized {
|
Localized {
|
||||||
key: &'static str,
|
key: &'static str,
|
||||||
|
|||||||
@@ -116,12 +116,18 @@ struct RequestConfig {
|
|||||||
|
|
||||||
/// 发送 HTTP 请求
|
/// 发送 HTTP 请求
|
||||||
async fn send_http_request(config: &RequestConfig, timeout_secs: u64) -> Result<String, AppError> {
|
async fn send_http_request(config: &RequestConfig, timeout_secs: u64) -> Result<String, AppError> {
|
||||||
|
// 约束超时范围,防止异常配置导致长时间阻塞
|
||||||
|
let timeout = timeout_secs.clamp(2, 30);
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
.timeout(Duration::from_secs(timeout_secs))
|
.timeout(Duration::from_secs(timeout))
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| AppError::Message(format!("创建客户端失败: {}", e)))?;
|
.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);
|
let mut req = client.request(method.clone(), &config.url);
|
||||||
|
|
||||||
|
|||||||
@@ -166,7 +166,9 @@ export function ClaudeFormFields({
|
|||||||
id="claudeModel"
|
id="claudeModel"
|
||||||
type="text"
|
type="text"
|
||||||
value={claudeModel}
|
value={claudeModel}
|
||||||
onChange={(e) => onModelChange("ANTHROPIC_MODEL", e.target.value)}
|
onChange={(e) =>
|
||||||
|
onModelChange("ANTHROPIC_MODEL", e.target.value)
|
||||||
|
}
|
||||||
placeholder={t("providerForm.modelPlaceholder", {
|
placeholder={t("providerForm.modelPlaceholder", {
|
||||||
defaultValue: "claude-3-7-sonnet-20250219",
|
defaultValue: "claude-3-7-sonnet-20250219",
|
||||||
})}
|
})}
|
||||||
@@ -177,7 +179,9 @@ export function ClaudeFormFields({
|
|||||||
{/* 默认 Haiku */}
|
{/* 默认 Haiku */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<FormLabel htmlFor="claudeDefaultHaikuModel">
|
<FormLabel htmlFor="claudeDefaultHaikuModel">
|
||||||
{t("providerForm.anthropicDefaultHaikuModel", { defaultValue: "Haiku 默认模型" })}
|
{t("providerForm.anthropicDefaultHaikuModel", {
|
||||||
|
defaultValue: "Haiku 默认模型",
|
||||||
|
})}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
id="claudeDefaultHaikuModel"
|
id="claudeDefaultHaikuModel"
|
||||||
@@ -196,14 +200,19 @@ export function ClaudeFormFields({
|
|||||||
{/* 默认 Sonnet */}
|
{/* 默认 Sonnet */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<FormLabel htmlFor="claudeDefaultSonnetModel">
|
<FormLabel htmlFor="claudeDefaultSonnetModel">
|
||||||
{t("providerForm.anthropicDefaultSonnetModel", { defaultValue: "Sonnet 默认模型" })}
|
{t("providerForm.anthropicDefaultSonnetModel", {
|
||||||
|
defaultValue: "Sonnet 默认模型",
|
||||||
|
})}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
id="claudeDefaultSonnetModel"
|
id="claudeDefaultSonnetModel"
|
||||||
type="text"
|
type="text"
|
||||||
value={defaultSonnetModel}
|
value={defaultSonnetModel}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onModelChange("ANTHROPIC_DEFAULT_SONNET_MODEL", e.target.value)
|
onModelChange(
|
||||||
|
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
placeholder={t("providerForm.modelPlaceholder", {
|
placeholder={t("providerForm.modelPlaceholder", {
|
||||||
defaultValue: "claude-3-7-sonnet-20250219",
|
defaultValue: "claude-3-7-sonnet-20250219",
|
||||||
@@ -215,7 +224,9 @@ export function ClaudeFormFields({
|
|||||||
{/* 默认 Opus */}
|
{/* 默认 Opus */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<FormLabel htmlFor="claudeDefaultOpusModel">
|
<FormLabel htmlFor="claudeDefaultOpusModel">
|
||||||
{t("providerForm.anthropicDefaultOpusModel", { defaultValue: "Opus 默认模型" })}
|
{t("providerForm.anthropicDefaultOpusModel", {
|
||||||
|
defaultValue: "Opus 默认模型",
|
||||||
|
})}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
id="claudeDefaultOpusModel"
|
id="claudeDefaultOpusModel"
|
||||||
@@ -233,7 +244,8 @@ export function ClaudeFormFields({
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{t("providerForm.modelHelper", {
|
{t("providerForm.modelHelper", {
|
||||||
defaultValue: "可选:指定默认使用的 Claude 模型,留空则使用系统默认。",
|
defaultValue:
|
||||||
|
"可选:指定默认使用的 Claude 模型,留空则使用系统默认。",
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import { Form } from "@/components/ui/form";
|
|||||||
import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider";
|
import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider";
|
||||||
import type { AppId } from "@/lib/api";
|
import type { AppId } from "@/lib/api";
|
||||||
import type { ProviderCategory, ProviderMeta } from "@/types";
|
import type { ProviderCategory, ProviderMeta } from "@/types";
|
||||||
import { providerPresets, type ProviderPreset } from "@/config/claudeProviderPresets";
|
import {
|
||||||
|
providerPresets,
|
||||||
|
type ProviderPreset,
|
||||||
|
} from "@/config/claudeProviderPresets";
|
||||||
import {
|
import {
|
||||||
codexProviderPresets,
|
codexProviderPresets,
|
||||||
type CodexProviderPreset,
|
type CodexProviderPreset,
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ interface UseModelStateProps {
|
|||||||
* 管理模型选择状态
|
* 管理模型选择状态
|
||||||
* 支持 ANTHROPIC_MODEL 和 ANTHROPIC_SMALL_FAST_MODEL
|
* 支持 ANTHROPIC_MODEL 和 ANTHROPIC_SMALL_FAST_MODEL
|
||||||
*/
|
*/
|
||||||
export function useModelState({ settingsConfig, onConfigChange }: UseModelStateProps) {
|
export function useModelState({
|
||||||
|
settingsConfig,
|
||||||
|
onConfigChange,
|
||||||
|
}: UseModelStateProps) {
|
||||||
const [claudeModel, setClaudeModel] = useState("");
|
const [claudeModel, setClaudeModel] = useState("");
|
||||||
const [defaultHaikuModel, setDefaultHaikuModel] = useState("");
|
const [defaultHaikuModel, setDefaultHaikuModel] = useState("");
|
||||||
const [defaultSonnetModel, setDefaultSonnetModel] = useState("");
|
const [defaultSonnetModel, setDefaultSonnetModel] = useState("");
|
||||||
@@ -24,9 +27,12 @@ export function useModelState({ settingsConfig, onConfigChange }: UseModelStateP
|
|||||||
try {
|
try {
|
||||||
const cfg = settingsConfig ? JSON.parse(settingsConfig) : {};
|
const cfg = settingsConfig ? JSON.parse(settingsConfig) : {};
|
||||||
const env = cfg?.env || {};
|
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 =
|
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 =
|
const haiku =
|
||||||
typeof env.ANTHROPIC_DEFAULT_HAIKU_MODEL === "string"
|
typeof env.ANTHROPIC_DEFAULT_HAIKU_MODEL === "string"
|
||||||
? env.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
? env.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
||||||
@@ -59,12 +65,16 @@ export function useModelState({ settingsConfig, onConfigChange }: UseModelStateP
|
|||||||
value: string,
|
value: string,
|
||||||
) => {
|
) => {
|
||||||
if (field === "ANTHROPIC_MODEL") setClaudeModel(value);
|
if (field === "ANTHROPIC_MODEL") setClaudeModel(value);
|
||||||
if (field === "ANTHROPIC_DEFAULT_HAIKU_MODEL") setDefaultHaikuModel(value);
|
if (field === "ANTHROPIC_DEFAULT_HAIKU_MODEL")
|
||||||
if (field === "ANTHROPIC_DEFAULT_SONNET_MODEL") setDefaultSonnetModel(value);
|
setDefaultHaikuModel(value);
|
||||||
|
if (field === "ANTHROPIC_DEFAULT_SONNET_MODEL")
|
||||||
|
setDefaultSonnetModel(value);
|
||||||
if (field === "ANTHROPIC_DEFAULT_OPUS_MODEL") setDefaultOpusModel(value);
|
if (field === "ANTHROPIC_DEFAULT_OPUS_MODEL") setDefaultOpusModel(value);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const currentConfig = settingsConfig ? JSON.parse(settingsConfig) : { env: {} };
|
const currentConfig = settingsConfig
|
||||||
|
? JSON.parse(settingsConfig)
|
||||||
|
: { env: {} };
|
||||||
if (!currentConfig.env) currentConfig.env = {};
|
if (!currentConfig.env) currentConfig.env = {};
|
||||||
|
|
||||||
// 新键仅写入;旧键不再写入
|
// 新键仅写入;旧键不再写入
|
||||||
|
|||||||
@@ -73,10 +73,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 请求地址候选(用于地址管理/测速),用户可自行选择/覆盖
|
// 请求地址候选(用于地址管理/测速),用户可自行选择/覆盖
|
||||||
endpointCandidates: [
|
endpointCandidates: ["https://aihubmix.com", "https://api.aihubmix.com"],
|
||||||
"https://aihubmix.com",
|
|
||||||
"https://api.aihubmix.com",
|
|
||||||
],
|
|
||||||
category: "aggregator",
|
category: "aggregator",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -203,15 +200,15 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
websiteUrl: "https://platform.minimaxi.com",
|
websiteUrl: "https://platform.minimaxi.com",
|
||||||
apiKeyUrl: "https://platform.minimaxi.com/user-center/basic-information",
|
apiKeyUrl: "https://platform.minimaxi.com/user-center/basic-information",
|
||||||
settingsConfig: {
|
settingsConfig: {
|
||||||
"env": {
|
env: {
|
||||||
"ANTHROPIC_BASE_URL": "https://api.minimaxi.com/anthropic",
|
ANTHROPIC_BASE_URL: "https://api.minimaxi.com/anthropic",
|
||||||
"ANTHROPIC_AUTH_TOKEN": "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
"API_TIMEOUT_MS": "3000000",
|
API_TIMEOUT_MS: "3000000",
|
||||||
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": 1,
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
|
||||||
"ANTHROPIC_MODEL": "MiniMax-M2",
|
ANTHROPIC_MODEL: "MiniMax-M2",
|
||||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "MiniMax-M2",
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMax-M2",
|
||||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "MiniMax-M2",
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMax-M2",
|
||||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "MiniMax-M2"
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMax-M2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
|
|||||||
@@ -373,7 +373,10 @@
|
|||||||
"tip2": "• Extractor function runs in sandbox environment, supports ES2020+ syntax",
|
"tip2": "• Extractor function runs in sandbox environment, supports ES2020+ syntax",
|
||||||
"tip3": "• Entire config must be wrapped in () to form object literal expression"
|
"tip3": "• Entire config must be wrapped in () to form object literal expression"
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"usage_query_failed": "Usage query failed"
|
||||||
|
},
|
||||||
|
|
||||||
"presetSelector": {
|
"presetSelector": {
|
||||||
"title": "Select Configuration Type",
|
"title": "Select Configuration Type",
|
||||||
"custom": "Custom",
|
"custom": "Custom",
|
||||||
|
|||||||
@@ -373,7 +373,10 @@
|
|||||||
"tip2": "• extractor 函数在沙箱环境中执行,支持 ES2020+ 语法",
|
"tip2": "• extractor 函数在沙箱环境中执行,支持 ES2020+ 语法",
|
||||||
"tip3": "• 整个配置必须用 () 包裹,形成对象字面量表达式"
|
"tip3": "• 整个配置必须用 () 包裹,形成对象字面量表达式"
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"usage_query_failed": "用量查询失败"
|
||||||
|
},
|
||||||
|
|
||||||
"presetSelector": {
|
"presetSelector": {
|
||||||
"title": "选择配置类型",
|
"title": "选择配置类型",
|
||||||
"custom": "自定义",
|
"custom": "自定义",
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import type { UsageResult } from "@/types";
|
import type { UsageResult } from "@/types";
|
||||||
import type { AppId } from "./types";
|
import type { AppId } from "./types";
|
||||||
|
import i18n from "@/i18n";
|
||||||
|
|
||||||
export const usageApi = {
|
export const usageApi = {
|
||||||
async query(providerId: string, appId: AppId): Promise<UsageResult> {
|
async query(providerId: string, appId: AppId): Promise<UsageResult> {
|
||||||
return await invoke("query_provider_usage", {
|
try {
|
||||||
provider_id: providerId,
|
return await invoke("query_provider_usage", {
|
||||||
app: appId,
|
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"),
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export const useUsageQuery = (
|
|||||||
queryFn: async () => usageApi.query(providerId, appId),
|
queryFn: async () => usageApi.query(providerId, appId),
|
||||||
enabled: enabled && !!providerId,
|
enabled: enabled && !!providerId,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
retry: false,
|
||||||
staleTime: 5 * 60 * 1000, // 5分钟
|
staleTime: 5 * 60 * 1000, // 5分钟
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,4 +16,3 @@ export async function syncCurrentProvidersLiveSafe(): Promise<{
|
|||||||
return { ok: false, error };
|
return { ok: false, error };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,7 +170,12 @@ export const getApiKeyFromConfig = (jsonString: string): string => {
|
|||||||
const config = JSON.parse(jsonString);
|
const config = JSON.parse(jsonString);
|
||||||
const token = config?.env?.ANTHROPIC_AUTH_TOKEN;
|
const token = config?.env?.ANTHROPIC_AUTH_TOKEN;
|
||||||
const apiKey = config?.env?.ANTHROPIC_API_KEY;
|
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;
|
return value;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
Reference in New Issue
Block a user