feat(proxy): add full URL mode and refactor endpoint rewriting

- Add `isFullUrl` provider meta to treat base_url as complete API endpoint
- Remove hardcoded `?beta=true` from Claude adapter, pass through from client
- Refactor forwarder endpoint rewriting with proper query string handling
- Block provider switching when proxy is required but not running
- Add full URL toggle UI in endpoint field with i18n (zh/en/ja)
This commit is contained in:
YoVinchen
2026-03-16 14:22:54 +08:00
parent 36bbdc36f5
commit eeda9adb03
15 changed files with 313 additions and 55 deletions
@@ -76,6 +76,10 @@ interface ClaudeFormFieldsProps {
// Auth Field (ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY)
apiKeyField: ClaudeApiKeyField;
onApiKeyFieldChange: (field: ClaudeApiKeyField) => void;
// Full URL mode
isFullUrl: boolean;
onFullUrlChange: (value: boolean) => void;
}
export function ClaudeFormFields({
@@ -112,6 +116,8 @@ export function ClaudeFormFields({
onApiFormatChange,
apiKeyField,
onApiKeyFieldChange,
isFullUrl,
onFullUrlChange,
}: ClaudeFormFieldsProps) {
const { t } = useTranslation();
@@ -181,6 +187,9 @@ export function ClaudeFormFields({
: t("providerForm.apiHint")
}
onManageClick={() => onEndpointModalToggle(true)}
showFullUrlToggle={true}
isFullUrl={isFullUrl}
onFullUrlChange={onFullUrlChange}
/>
)}
@@ -160,6 +160,10 @@ export function ProviderForm({
const [endpointAutoSelect, setEndpointAutoSelect] = useState<boolean>(
() => initialData?.meta?.endpointAutoSelect ?? true,
);
const [localIsFullUrl, setLocalIsFullUrl] = useState<boolean>(() => {
if (appId !== "claude") return false;
return initialData?.meta?.isFullUrl ?? false;
});
const [testConfig, setTestConfig] = useState<ProviderTestConfig>(
() => initialData?.meta?.testConfig ?? { enabled: false },
@@ -199,6 +203,9 @@ export function ProviderForm({
setDraftCustomEndpoints([]);
}
setEndpointAutoSelect(initialData?.meta?.endpointAutoSelect ?? true);
setLocalIsFullUrl(
appId === "claude" ? (initialData?.meta?.isFullUrl ?? false) : false,
);
setTestConfig(initialData?.meta?.testConfig ?? { enabled: false });
setProxyConfig(initialData?.meta?.proxyConfig ?? { enabled: false });
setPricingConfig({
@@ -897,6 +904,10 @@ export function ProviderForm({
localApiKeyField !== "ANTHROPIC_AUTH_TOKEN"
? localApiKeyField
: undefined,
isFullUrl:
appId === "claude" && category !== "official" && localIsFullUrl
? true
: undefined,
};
await onSubmit(payload);
@@ -1136,6 +1147,7 @@ export function ProviderForm({
}
setLocalApiKeyField(preset.apiKeyField ?? "ANTHROPIC_AUTH_TOKEN");
setLocalIsFullUrl(false);
form.reset({
name: preset.nameKey ? t(preset.nameKey) : preset.name,
@@ -1344,6 +1356,8 @@ export function ProviderForm({
onApiFormatChange={handleApiFormatChange}
apiKeyField={localApiKeyField}
onApiKeyFieldChange={handleApiKeyFieldChange}
isFullUrl={localIsFullUrl}
onFullUrlChange={setLocalIsFullUrl}
/>
)}
@@ -1607,7 +1621,9 @@ export function ProviderForm({
<Button variant="outline" type="button" onClick={onCancel}>
{t("common.cancel")}
</Button>
<Button type="submit" disabled={isSubmitting}>{submitLabel}</Button>
<Button type="submit" disabled={isSubmitting}>
{submitLabel}
</Button>
</div>
)}
</form>
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import { FormLabel } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Zap } from "lucide-react";
import { Link2, Zap } from "lucide-react";
interface EndpointFieldProps {
id: string;
@@ -13,6 +13,9 @@ interface EndpointFieldProps {
showManageButton?: boolean;
onManageClick?: () => void;
manageButtonLabel?: string;
showFullUrlToggle?: boolean;
isFullUrl?: boolean;
onFullUrlChange?: (value: boolean) => void;
}
export function EndpointField({
@@ -25,6 +28,9 @@ export function EndpointField({
showManageButton = true,
onManageClick,
manageButtonLabel,
showFullUrlToggle = false,
isFullUrl = false,
onFullUrlChange,
}: EndpointFieldProps) {
const { t } = useTranslation();
@@ -55,6 +61,35 @@ export function EndpointField({
placeholder={placeholder}
autoComplete="off"
/>
{showFullUrlToggle && onFullUrlChange ? (
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => onFullUrlChange(!isFullUrl)}
className={`flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs transition-colors ${
isFullUrl
? "border-primary bg-primary/10 text-primary"
: "border-border text-muted-foreground hover:border-foreground/30 hover:text-foreground"
}`}
>
<Link2 className="h-3.5 w-3.5" />
{isFullUrl
? t("providerForm.fullUrlEnabled", {
defaultValue: "完整 URL 模式",
})
: t("providerForm.fullUrlDisabled", {
defaultValue: "标记为完整 URL",
})}
</button>
{isFullUrl ? (
<span className="text-xs text-muted-foreground">
{t("providerForm.fullUrlHint", {
defaultValue: "代理将直接使用此 URL,不拼接路径",
})}
</span>
) : null}
</div>
) : null}
{hint ? (
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
<p className="text-xs text-amber-600 dark:text-amber-400">{hint}</p>