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
256 lines
7.9 KiB
TypeScript
256 lines
7.9 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { FormLabel } from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import EndpointSpeedTest from "./EndpointSpeedTest";
|
|
import { ApiKeySection, EndpointField } from "./shared";
|
|
import type { ProviderCategory } from "@/types";
|
|
import type { TemplateValueConfig } from "@/config/claudeProviderPresets";
|
|
|
|
interface EndpointCandidate {
|
|
url: string;
|
|
}
|
|
|
|
interface ClaudeFormFieldsProps {
|
|
// API Key
|
|
shouldShowApiKey: boolean;
|
|
apiKey: string;
|
|
onApiKeyChange: (key: string) => void;
|
|
category?: ProviderCategory;
|
|
shouldShowApiKeyLink: boolean;
|
|
websiteUrl: string;
|
|
|
|
// Template Values
|
|
templateValueEntries: Array<[string, TemplateValueConfig]>;
|
|
templateValues: Record<string, TemplateValueConfig>;
|
|
templatePresetName: string;
|
|
onTemplateValueChange: (key: string, value: string) => void;
|
|
|
|
// Base URL
|
|
shouldShowSpeedTest: boolean;
|
|
baseUrl: string;
|
|
onBaseUrlChange: (url: string) => void;
|
|
isEndpointModalOpen: boolean;
|
|
onEndpointModalToggle: (open: boolean) => void;
|
|
onCustomEndpointsChange: (endpoints: string[]) => void;
|
|
|
|
// Model Selector
|
|
shouldShowModelSelector: boolean;
|
|
claudeModel: string;
|
|
defaultHaikuModel: string;
|
|
defaultSonnetModel: string;
|
|
defaultOpusModel: string;
|
|
onModelChange: (
|
|
field:
|
|
| "ANTHROPIC_MODEL"
|
|
| "ANTHROPIC_DEFAULT_HAIKU_MODEL"
|
|
| "ANTHROPIC_DEFAULT_SONNET_MODEL"
|
|
| "ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
value: string,
|
|
) => void;
|
|
|
|
// Speed Test Endpoints
|
|
speedTestEndpoints: EndpointCandidate[];
|
|
}
|
|
|
|
export function ClaudeFormFields({
|
|
shouldShowApiKey,
|
|
apiKey,
|
|
onApiKeyChange,
|
|
category,
|
|
shouldShowApiKeyLink,
|
|
websiteUrl,
|
|
templateValueEntries,
|
|
templateValues,
|
|
templatePresetName,
|
|
onTemplateValueChange,
|
|
shouldShowSpeedTest,
|
|
baseUrl,
|
|
onBaseUrlChange,
|
|
isEndpointModalOpen,
|
|
onEndpointModalToggle,
|
|
onCustomEndpointsChange,
|
|
shouldShowModelSelector,
|
|
claudeModel,
|
|
defaultHaikuModel,
|
|
defaultSonnetModel,
|
|
defaultOpusModel,
|
|
onModelChange,
|
|
speedTestEndpoints,
|
|
}: ClaudeFormFieldsProps) {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<>
|
|
{/* API Key 输入框 */}
|
|
{shouldShowApiKey && (
|
|
<ApiKeySection
|
|
value={apiKey}
|
|
onChange={onApiKeyChange}
|
|
category={category}
|
|
shouldShowLink={shouldShowApiKeyLink}
|
|
websiteUrl={websiteUrl}
|
|
/>
|
|
)}
|
|
|
|
{/* 模板变量输入 */}
|
|
{templateValueEntries.length > 0 && (
|
|
<div className="space-y-3">
|
|
<FormLabel>
|
|
{t("providerForm.parameterConfig", {
|
|
name: templatePresetName,
|
|
defaultValue: `${templatePresetName} 参数配置`,
|
|
})}
|
|
</FormLabel>
|
|
<div className="space-y-4">
|
|
{templateValueEntries.map(([key, config]) => (
|
|
<div key={key} className="space-y-2">
|
|
<FormLabel htmlFor={`template-${key}`}>
|
|
{config.label}
|
|
</FormLabel>
|
|
<Input
|
|
id={`template-${key}`}
|
|
type="text"
|
|
required
|
|
value={
|
|
templateValues[key]?.editorValue ??
|
|
config.editorValue ??
|
|
config.defaultValue ??
|
|
""
|
|
}
|
|
onChange={(e) => onTemplateValueChange(key, e.target.value)}
|
|
placeholder={config.placeholder || config.label}
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Base URL 输入框 */}
|
|
{shouldShowSpeedTest && (
|
|
<EndpointField
|
|
id="baseUrl"
|
|
label={t("providerForm.apiEndpoint")}
|
|
value={baseUrl}
|
|
onChange={onBaseUrlChange}
|
|
placeholder={t("providerForm.apiEndpointPlaceholder")}
|
|
hint={t("providerForm.apiHint")}
|
|
onManageClick={() => onEndpointModalToggle(true)}
|
|
/>
|
|
)}
|
|
|
|
{/* 端点测速弹窗 */}
|
|
{shouldShowSpeedTest && isEndpointModalOpen && (
|
|
<EndpointSpeedTest
|
|
appId="claude"
|
|
value={baseUrl}
|
|
onChange={onBaseUrlChange}
|
|
initialEndpoints={speedTestEndpoints}
|
|
visible={isEndpointModalOpen}
|
|
onClose={() => onEndpointModalToggle(false)}
|
|
onCustomEndpointsChange={onCustomEndpointsChange}
|
|
/>
|
|
)}
|
|
|
|
{/* 模型选择器 */}
|
|
{shouldShowModelSelector && (
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{/* 主模型 */}
|
|
<div className="space-y-2">
|
|
<FormLabel htmlFor="claudeModel">
|
|
{t("providerForm.anthropicModel", { defaultValue: "主模型" })}
|
|
</FormLabel>
|
|
<Input
|
|
id="claudeModel"
|
|
type="text"
|
|
value={claudeModel}
|
|
onChange={(e) =>
|
|
onModelChange("ANTHROPIC_MODEL", e.target.value)
|
|
}
|
|
placeholder={t("providerForm.modelPlaceholder", {
|
|
defaultValue: "claude-3-7-sonnet-20250219",
|
|
})}
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
|
|
{/* 默认 Haiku */}
|
|
<div className="space-y-2">
|
|
<FormLabel htmlFor="claudeDefaultHaikuModel">
|
|
{t("providerForm.anthropicDefaultHaikuModel", {
|
|
defaultValue: "Haiku 默认模型",
|
|
})}
|
|
</FormLabel>
|
|
<Input
|
|
id="claudeDefaultHaikuModel"
|
|
type="text"
|
|
value={defaultHaikuModel}
|
|
onChange={(e) =>
|
|
onModelChange("ANTHROPIC_DEFAULT_HAIKU_MODEL", e.target.value)
|
|
}
|
|
placeholder={t("providerForm.haikuModelPlaceholder", {
|
|
defaultValue: "GLM-4.5-Air",
|
|
})}
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
|
|
{/* 默认 Sonnet */}
|
|
<div className="space-y-2">
|
|
<FormLabel htmlFor="claudeDefaultSonnetModel">
|
|
{t("providerForm.anthropicDefaultSonnetModel", {
|
|
defaultValue: "Sonnet 默认模型",
|
|
})}
|
|
</FormLabel>
|
|
<Input
|
|
id="claudeDefaultSonnetModel"
|
|
type="text"
|
|
value={defaultSonnetModel}
|
|
onChange={(e) =>
|
|
onModelChange(
|
|
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
e.target.value,
|
|
)
|
|
}
|
|
placeholder={t("providerForm.modelPlaceholder", {
|
|
defaultValue: "claude-3-7-sonnet-20250219",
|
|
})}
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
|
|
{/* 默认 Opus */}
|
|
<div className="space-y-2">
|
|
<FormLabel htmlFor="claudeDefaultOpusModel">
|
|
{t("providerForm.anthropicDefaultOpusModel", {
|
|
defaultValue: "Opus 默认模型",
|
|
})}
|
|
</FormLabel>
|
|
<Input
|
|
id="claudeDefaultOpusModel"
|
|
type="text"
|
|
value={defaultOpusModel}
|
|
onChange={(e) =>
|
|
onModelChange("ANTHROPIC_DEFAULT_OPUS_MODEL", e.target.value)
|
|
}
|
|
placeholder={t("providerForm.modelPlaceholder", {
|
|
defaultValue: "claude-3-7-opus-20250219",
|
|
})}
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t("providerForm.modelHelper", {
|
|
defaultValue:
|
|
"可选:指定默认使用的 Claude 模型,留空则使用系统默认。",
|
|
})}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|