feat(provider-form): custom User-Agent presets dropdown in advanced settings

Polish the provider-level User-Agent override UI on the Claude and Codex forms.

- Add a shared CustomUserAgentField (label + input + preset dropdown + live
  validation) so both forms stay in sync.
- Provide curated UA presets (Claude Code / Kilo Code families that pass
  coding-plan UA whitelists per #3671); the first is Claude Code's real
  `claude-cli/x (external, cli)` format. Whitelists gate on the name prefix,
  not the version, so static values stay valid across upgrades.
- Expose presets via a dropdown to the right of the input (z-[200] so it
  renders above the dialog layers) instead of inline chips.
- Move the field into the existing advanced/reasoning collapsibles.
- userAgent.ts mirrors the backend byte rule (reject only control chars;
  non-ASCII is allowed) for a non-blocking inline hint.
- i18n for all four locales (zh/en/ja/zh-TW).
This commit is contained in:
Jason
2026-06-10 16:39:26 +08:00
parent 8b925c2f2f
commit 596019505f
11 changed files with 212 additions and 56 deletions
@@ -47,6 +47,7 @@ import {
showFetchModelsError,
type FetchedModel,
} from "@/lib/api/model-fetch";
import { CustomUserAgentField } from "./CustomUserAgentField";
import type {
ProviderCategory,
ClaudeApiFormat,
@@ -204,7 +205,8 @@ export function ClaudeFormFields({
defaultSonnetModel ||
defaultOpusModel ||
apiFormat !== "anthropic" ||
apiKeyField !== "ANTHROPIC_AUTH_TOKEN"
apiKeyField !== "ANTHROPIC_AUTH_TOKEN" ||
customUserAgent
);
const [advancedExpanded, setAdvancedExpanded] = useState(hasAnyAdvancedValue);
@@ -257,7 +259,7 @@ export function ClaudeFormFields({
const modelsUrl = matchedPreset?.modelsUrl;
setIsFetchingModels(true);
fetchModelsForConfig(baseUrl, apiKey, isFullUrl, modelsUrl)
fetchModelsForConfig(baseUrl, apiKey, isFullUrl, modelsUrl, customUserAgent)
.then((models) => {
setFetchedModels(models);
showModelFetchResult(models.length);
@@ -267,7 +269,7 @@ export function ClaudeFormFields({
showFetchModelsError(err, t);
})
.finally(() => setIsFetchingModels(false));
}, [baseUrl, apiKey, isFullUrl, showModelFetchResult, t]);
}, [baseUrl, apiKey, isFullUrl, customUserAgent, showModelFetchResult, t]);
const handleFetchCopilotModels = useCallback(() => {
if (!isCopilotAuthenticated) {
@@ -671,30 +673,6 @@ export function ClaudeFormFields({
/>
)}
{category !== "official" && (
<div className="space-y-2">
<FormLabel htmlFor="claude-custom-user-agent">
{t("providerForm.customUserAgent", {
defaultValue: "自定义 User-Agent",
})}
</FormLabel>
<Input
id="claude-custom-user-agent"
type="text"
value={customUserAgent}
onChange={(e) => onCustomUserAgentChange(e.target.value)}
placeholder="Mozilla/5.0 ..."
autoComplete="off"
/>
<p className="text-xs text-muted-foreground">
{t("providerForm.customUserAgentHint", {
defaultValue:
"仅在开启本地路由/代理接管后生效,会替换转发到供应商 API 请求中的 User-Agent。",
})}
</p>
</div>
)}
{shouldShowModelSelector && (
<Collapsible open={advancedExpanded} onOpenChange={setAdvancedExpanded}>
<CollapsibleTrigger asChild>
@@ -962,6 +940,12 @@ export function ClaudeFormFields({
})}
</p>
</div>
<CustomUserAgentField
id="claude-custom-user-agent"
value={customUserAgent}
onChange={onCustomUserAgentChange}
/>
</CollapsibleContent>
</Collapsible>
)}