mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
e776160912
- Fold local routing toggle, model mapping, reasoning overrides and custom User-Agent into a single collapsible advanced section, mirroring the Claude form (auto-expands when UA is set or local routing is enabled) - Custom User-Agent becomes configurable for native Responses providers; it was previously reachable only when openai_chat routing was on - Collapsed hint names local routing as the entry point for Chat Completions / non-GPT providers - Backfill all missing codexConfig keys in zh-TW locale
658 lines
24 KiB
TypeScript
658 lines
24 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Button } from "@/components/ui/button";
|
|
import { FormLabel } from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from "@/components/ui/collapsible";
|
|
import { toast } from "sonner";
|
|
import {
|
|
ChevronDown,
|
|
ChevronRight,
|
|
Download,
|
|
Loader2,
|
|
Plus,
|
|
Trash2,
|
|
} from "lucide-react";
|
|
import EndpointSpeedTest from "./EndpointSpeedTest";
|
|
import { ApiKeySection, EndpointField, ModelDropdown } from "./shared";
|
|
import {
|
|
fetchModelsForConfig,
|
|
showFetchModelsError,
|
|
type FetchedModel,
|
|
} from "@/lib/api/model-fetch";
|
|
import { CustomUserAgentField } from "./CustomUserAgentField";
|
|
import { cn } from "@/lib/utils";
|
|
import type {
|
|
CodexApiFormat,
|
|
CodexCatalogModel,
|
|
CodexChatReasoning,
|
|
ProviderCategory,
|
|
} from "@/types";
|
|
|
|
interface EndpointCandidate {
|
|
url: string;
|
|
}
|
|
|
|
interface CodexFormFieldsProps {
|
|
providerId?: string;
|
|
// API Key
|
|
codexApiKey: string;
|
|
onApiKeyChange: (key: string) => void;
|
|
category?: ProviderCategory;
|
|
shouldShowApiKeyLink: boolean;
|
|
websiteUrl: string;
|
|
isPartner?: boolean;
|
|
partnerPromotionKey?: string;
|
|
|
|
// Base URL
|
|
shouldShowSpeedTest: boolean;
|
|
codexBaseUrl: string;
|
|
onBaseUrlChange: (url: string) => void;
|
|
isFullUrl: boolean;
|
|
onFullUrlChange: (value: boolean) => void;
|
|
isEndpointModalOpen: boolean;
|
|
onEndpointModalToggle: (open: boolean) => void;
|
|
onCustomEndpointsChange?: (endpoints: string[]) => void;
|
|
autoSelect: boolean;
|
|
onAutoSelectChange: (checked: boolean) => void;
|
|
|
|
// API Format
|
|
// Note: wire_api is always "responses" for Codex; apiFormat controls proxy-layer conversion
|
|
apiFormat: CodexApiFormat;
|
|
onApiFormatChange: (format: CodexApiFormat) => void;
|
|
codexChatReasoning?: CodexChatReasoning;
|
|
onCodexChatReasoningChange?: (value: CodexChatReasoning) => void;
|
|
|
|
// Model Catalog
|
|
catalogModels?: CodexCatalogModel[];
|
|
onCatalogModelsChange?: (models: CodexCatalogModel[]) => void;
|
|
|
|
// Speed Test Endpoints
|
|
speedTestEndpoints: EndpointCandidate[];
|
|
|
|
// Local proxy User-Agent override
|
|
customUserAgent: string;
|
|
onCustomUserAgentChange: (value: string) => void;
|
|
}
|
|
|
|
type CodexCatalogRow = CodexCatalogModel & { rowId: string };
|
|
|
|
function createCatalogRow(seed?: Partial<CodexCatalogModel>): CodexCatalogRow {
|
|
return {
|
|
rowId: crypto.randomUUID(),
|
|
model: seed?.model ?? "",
|
|
displayName: seed?.displayName ?? "",
|
|
contextWindow: seed?.contextWindow ?? "",
|
|
};
|
|
}
|
|
|
|
// Compares rows (with rowId) to incoming models (without) by data fields only,
|
|
// so both sync effects can use the same equality definition.
|
|
function catalogRowsMatchModels(
|
|
rows: Array<Pick<CodexCatalogRow, "model" | "displayName" | "contextWindow">>,
|
|
models: CodexCatalogModel[],
|
|
): boolean {
|
|
if (rows.length !== models.length) return false;
|
|
return rows.every((row, i) => {
|
|
const incoming = models[i];
|
|
return (
|
|
row.model === (incoming.model ?? "") &&
|
|
(row.displayName ?? "") === (incoming.displayName ?? "") &&
|
|
String(row.contextWindow ?? "") === String(incoming.contextWindow ?? "")
|
|
);
|
|
});
|
|
}
|
|
|
|
export function CodexFormFields({
|
|
providerId,
|
|
codexApiKey,
|
|
onApiKeyChange,
|
|
category,
|
|
shouldShowApiKeyLink,
|
|
websiteUrl,
|
|
isPartner,
|
|
partnerPromotionKey,
|
|
shouldShowSpeedTest,
|
|
codexBaseUrl,
|
|
onBaseUrlChange,
|
|
isFullUrl,
|
|
onFullUrlChange,
|
|
isEndpointModalOpen,
|
|
onEndpointModalToggle,
|
|
onCustomEndpointsChange,
|
|
autoSelect,
|
|
onAutoSelectChange,
|
|
apiFormat,
|
|
onApiFormatChange,
|
|
codexChatReasoning = {},
|
|
onCodexChatReasoningChange,
|
|
catalogModels = [],
|
|
onCatalogModelsChange,
|
|
speedTestEndpoints,
|
|
customUserAgent,
|
|
onCustomUserAgentChange,
|
|
}: CodexFormFieldsProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const [fetchedModels, setFetchedModels] = useState<FetchedModel[]>([]);
|
|
const [isFetchingModels, setIsFetchingModels] = useState(false);
|
|
const needsLocalRouting = apiFormat === "openai_chat";
|
|
const canEditCatalog = Boolean(onCatalogModelsChange);
|
|
const canEditReasoning = Boolean(onCodexChatReasoningChange);
|
|
const supportsThinking =
|
|
codexChatReasoning.supportsThinking === true ||
|
|
codexChatReasoning.supportsEffort === true;
|
|
const supportsEffort = codexChatReasoning.supportsEffort === true;
|
|
|
|
// needsLocalRouting 非默认值说明预设/用户动过路由配置,需要让模型映射保持可见
|
|
const hasAnyAdvancedValue = !!customUserAgent || needsLocalRouting;
|
|
const [advancedExpanded, setAdvancedExpanded] = useState(hasAnyAdvancedValue);
|
|
|
|
// 预设/编辑加载填充高级值后自动展开(仅从折叠→展开,不会自动折叠)
|
|
useEffect(() => {
|
|
if (hasAnyAdvancedValue) {
|
|
setAdvancedExpanded(true);
|
|
}
|
|
}, [hasAnyAdvancedValue]);
|
|
|
|
const [catalogRows, setCatalogRows] = useState<CodexCatalogRow[]>(() =>
|
|
catalogModels.map((m) => createCatalogRow(m)),
|
|
);
|
|
|
|
// 记录上次发送给父组件的数据,避免重复触发
|
|
const lastSentModelsRef = useRef<CodexCatalogModel[]>(catalogModels);
|
|
|
|
// 父 → 子:仅当 prop 数据真的变化(预设切换 / 编辑加载)时才重建 rowId;
|
|
// 同 shape 时保留现有 rowId,避免编辑过程中焦点丢失。
|
|
useEffect(() => {
|
|
setCatalogRows((current) => {
|
|
if (catalogRowsMatchModels(current, catalogModels)) return current;
|
|
return catalogModels.map((m) => createCatalogRow(m));
|
|
});
|
|
// 同步更新 ref,避免父组件传入新数据时子→父 effect 误判为本地修改
|
|
lastSentModelsRef.current = catalogModels;
|
|
}, [catalogModels]);
|
|
|
|
// 子 → 父:rowId 是视图层概念,不应进入持久化数据;剥离后再回传。
|
|
// 注意:依赖数组不包含 catalogModels,避免父→子更新触发子→父回调形成循环。
|
|
useEffect(() => {
|
|
if (!onCatalogModelsChange) return;
|
|
const next: CodexCatalogModel[] = catalogRows.map(
|
|
({ rowId: _rowId, ...rest }) => rest,
|
|
);
|
|
// 只有当数据真的变化时才通知父组件
|
|
if (catalogRowsMatchModels(catalogRows, lastSentModelsRef.current)) return;
|
|
lastSentModelsRef.current = next;
|
|
onCatalogModelsChange(next);
|
|
}, [catalogRows, onCatalogModelsChange]);
|
|
|
|
const handleLocalRoutingChange = useCallback(
|
|
(checked: boolean) => {
|
|
onApiFormatChange(checked ? "openai_chat" : "openai_responses");
|
|
},
|
|
[onApiFormatChange],
|
|
);
|
|
|
|
const handleReasoningThinkingChange = useCallback(
|
|
(checked: boolean) => {
|
|
if (!onCodexChatReasoningChange) return;
|
|
onCodexChatReasoningChange({
|
|
...codexChatReasoning,
|
|
supportsThinking: checked,
|
|
supportsEffort: checked ? codexChatReasoning.supportsEffort : false,
|
|
});
|
|
},
|
|
[codexChatReasoning, onCodexChatReasoningChange],
|
|
);
|
|
|
|
const handleReasoningEffortChange = useCallback(
|
|
(checked: boolean) => {
|
|
if (!onCodexChatReasoningChange) return;
|
|
onCodexChatReasoningChange({
|
|
...codexChatReasoning,
|
|
supportsThinking: checked ? true : codexChatReasoning.supportsThinking,
|
|
supportsEffort: checked,
|
|
effortParam: checked
|
|
? (codexChatReasoning.effortParam ?? "reasoning_effort")
|
|
: "none",
|
|
});
|
|
},
|
|
[codexChatReasoning, onCodexChatReasoningChange],
|
|
);
|
|
|
|
const handleFetchModels = useCallback(() => {
|
|
if (!codexBaseUrl || !codexApiKey) {
|
|
showFetchModelsError(null, t, {
|
|
hasApiKey: !!codexApiKey,
|
|
hasBaseUrl: !!codexBaseUrl,
|
|
});
|
|
return;
|
|
}
|
|
setIsFetchingModels(true);
|
|
fetchModelsForConfig(
|
|
codexBaseUrl,
|
|
codexApiKey,
|
|
isFullUrl,
|
|
undefined,
|
|
customUserAgent,
|
|
)
|
|
.then((models) => {
|
|
setFetchedModels(models);
|
|
if (models.length === 0) {
|
|
toast.info(t("providerForm.fetchModelsEmpty"));
|
|
} else {
|
|
toast.success(
|
|
t("providerForm.fetchModelsSuccess", { count: models.length }),
|
|
);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.warn("[ModelFetch] Failed:", err);
|
|
showFetchModelsError(err, t);
|
|
})
|
|
.finally(() => setIsFetchingModels(false));
|
|
}, [codexBaseUrl, codexApiKey, isFullUrl, customUserAgent, t]);
|
|
|
|
const handleAddCatalogRow = useCallback(() => {
|
|
if (!onCatalogModelsChange) return;
|
|
setCatalogRows((current) => [...current, createCatalogRow()]);
|
|
}, [onCatalogModelsChange]);
|
|
|
|
const handleUpdateCatalogRow = useCallback(
|
|
(index: number, patch: Partial<CodexCatalogModel>) => {
|
|
setCatalogRows((current) =>
|
|
current.map((row, i) => (i === index ? { ...row, ...patch } : row)),
|
|
);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const handleRemoveCatalogRow = useCallback((index: number) => {
|
|
setCatalogRows((current) => current.filter((_, i) => i !== index));
|
|
}, []);
|
|
|
|
const renderCatalogActionButtons = (onAdd: () => void, addLabel: string) => (
|
|
<div className="flex gap-1">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleFetchModels}
|
|
disabled={isFetchingModels}
|
|
className="h-7 gap-1"
|
|
>
|
|
{isFetchingModels ? (
|
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
) : (
|
|
<Download className="h-3.5 w-3.5" />
|
|
)}
|
|
{t("providerForm.fetchModels")}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={onAdd}
|
|
className="h-7 gap-1"
|
|
>
|
|
<Plus className="h-3.5 w-3.5" />
|
|
{addLabel}
|
|
</Button>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{/* Codex API Key 输入框 */}
|
|
<ApiKeySection
|
|
id="codexApiKey"
|
|
label="API Key"
|
|
value={codexApiKey}
|
|
onChange={onApiKeyChange}
|
|
category={category}
|
|
shouldShowLink={shouldShowApiKeyLink}
|
|
websiteUrl={websiteUrl}
|
|
isPartner={isPartner}
|
|
partnerPromotionKey={partnerPromotionKey}
|
|
placeholder={{
|
|
official: t("providerForm.codexOfficialNoApiKey", {
|
|
defaultValue: "官方供应商无需 API Key",
|
|
}),
|
|
thirdParty: t("providerForm.codexApiKeyAutoFill", {
|
|
defaultValue: "输入 API Key,将自动填充到配置",
|
|
}),
|
|
}}
|
|
/>
|
|
|
|
{/* Codex Base URL 输入框 */}
|
|
{shouldShowSpeedTest && (
|
|
<EndpointField
|
|
id="codexBaseUrl"
|
|
label={t("codexConfig.apiUrlLabel")}
|
|
value={codexBaseUrl}
|
|
onChange={onBaseUrlChange}
|
|
placeholder={t("providerForm.codexApiEndpointPlaceholder")}
|
|
hint={t("providerForm.codexApiHint")}
|
|
showFullUrlToggle
|
|
isFullUrl={isFullUrl}
|
|
onFullUrlChange={onFullUrlChange}
|
|
onManageClick={() => onEndpointModalToggle(true)}
|
|
/>
|
|
)}
|
|
|
|
{/* 高级选项 —— 本地路由映射/模型映射/思考能力/自定义 UA;预设供应商通常无需展开 */}
|
|
{category !== "official" && (
|
|
<Collapsible
|
|
open={advancedExpanded}
|
|
onOpenChange={setAdvancedExpanded}
|
|
className="rounded-lg border border-border-default p-4"
|
|
>
|
|
<CollapsibleTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant={null}
|
|
size="sm"
|
|
className="h-8 w-full justify-start gap-1.5 px-0 text-sm font-medium text-foreground hover:opacity-70"
|
|
>
|
|
{advancedExpanded ? (
|
|
<ChevronDown className="h-4 w-4" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4" />
|
|
)}
|
|
{t("providerForm.advancedOptionsToggle", {
|
|
defaultValue: "高级选项",
|
|
})}
|
|
</Button>
|
|
</CollapsibleTrigger>
|
|
{!advancedExpanded && (
|
|
<p className="mt-1 ml-1 text-xs text-muted-foreground">
|
|
{t("codexConfig.advancedSectionHint", {
|
|
defaultValue:
|
|
"包含本地路由映射、模型映射、思考能力与自定义 User-Agent。供应商使用 Chat Completions 协议或非 GPT 模型时,需在此开启本地路由映射。",
|
|
})}
|
|
</p>
|
|
)}
|
|
<CollapsibleContent className="space-y-3 pt-3">
|
|
{/* 本地路由映射开关 —— 沿用 shouldShowSpeedTest 门控,cloud_provider 保持不可切换 */}
|
|
{shouldShowSpeedTest && (
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="space-y-1">
|
|
<FormLabel>
|
|
{t("codexConfig.localRoutingToggle", {
|
|
defaultValue: "需要本地路由映射",
|
|
})}
|
|
</FormLabel>
|
|
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
{needsLocalRouting
|
|
? t("codexConfig.localRoutingOnHint", {
|
|
defaultValue:
|
|
"Codex 目前仅原生支持 OpenAI Responses API 与 GPT 系列模型;如果您的供应商使用 Chat Completions 协议或非 GPT 模型(如 DeepSeek、Kimi),则需要打开本开关,并在使用过程中保持本地路由开启。",
|
|
})
|
|
: t("codexConfig.localRoutingOffHint", {
|
|
defaultValue:
|
|
"如果您的供应商不是原生 OpenAI Responses API,或者模型名不是 Codex 默认的 GPT 系列,请打开此开关。",
|
|
})}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={needsLocalRouting}
|
|
onCheckedChange={handleLocalRoutingChange}
|
|
aria-label={t("codexConfig.localRoutingToggle", {
|
|
defaultValue: "需要本地路由映射",
|
|
})}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{needsLocalRouting && canEditReasoning && (
|
|
<div
|
|
className={cn(
|
|
"space-y-3",
|
|
shouldShowSpeedTest && "border-t border-border-default pt-3",
|
|
)}
|
|
>
|
|
<div className="space-y-1">
|
|
<FormLabel>
|
|
{t("codexConfig.reasoningGroupTitle", {
|
|
defaultValue: "思考能力",
|
|
})}
|
|
</FormLabel>
|
|
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
{t("codexConfig.reasoningSectionHint", {
|
|
defaultValue:
|
|
"预设供应商已自动配置;自定义供应商会按名称/地址自动推断。仅当自动识别不准时才需手动覆盖。",
|
|
})}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="space-y-1">
|
|
<FormLabel>
|
|
{t("codexConfig.reasoningModeToggle", {
|
|
defaultValue: "支持思考模式",
|
|
})}
|
|
</FormLabel>
|
|
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
{t("codexConfig.reasoningModeHint", {
|
|
defaultValue:
|
|
"上游 Chat Completions 接口支持开启或关闭 thinking 时启用。Kimi、GLM、Qwen 等通常属于这一类。",
|
|
})}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={supportsThinking}
|
|
onCheckedChange={handleReasoningThinkingChange}
|
|
aria-label={t("codexConfig.reasoningModeToggle", {
|
|
defaultValue: "支持思考模式",
|
|
})}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between gap-4 border-t border-border-default pt-3">
|
|
<div className="space-y-1">
|
|
<FormLabel>
|
|
{t("codexConfig.reasoningEffortToggle", {
|
|
defaultValue: "支持思考等级",
|
|
})}
|
|
</FormLabel>
|
|
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
{t("codexConfig.reasoningEffortHint", {
|
|
defaultValue:
|
|
"上游支持 low/high/max 等思考深度控制时启用。启用后会自动启用思考模式,并把 Codex 的 reasoning.effort 转成上游 Chat 参数。",
|
|
})}
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={supportsEffort}
|
|
onCheckedChange={handleReasoningEffortChange}
|
|
aria-label={t("codexConfig.reasoningEffortToggle", {
|
|
defaultValue: "支持思考等级",
|
|
})}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div
|
|
className={cn(
|
|
(shouldShowSpeedTest ||
|
|
(needsLocalRouting && canEditReasoning)) &&
|
|
"border-t border-border-default pt-3",
|
|
)}
|
|
>
|
|
<CustomUserAgentField
|
|
id="codex-custom-user-agent"
|
|
value={customUserAgent}
|
|
onChange={onCustomUserAgentChange}
|
|
/>
|
|
</div>
|
|
|
|
{/* 模型映射 —— 仅在本地路由 + 可编辑时显示;上方恒有 UA 字段,分隔线无需条件 */}
|
|
{needsLocalRouting && canEditCatalog && (
|
|
<div className="space-y-4 border-t border-border-default pt-3">
|
|
<div className="space-y-1">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<FormLabel>
|
|
{t("codexConfig.modelMappingTitle", {
|
|
defaultValue: "模型映射",
|
|
})}
|
|
</FormLabel>
|
|
{renderCatalogActionButtons(
|
|
handleAddCatalogRow,
|
|
t("codexConfig.addCatalogModel", {
|
|
defaultValue: "添加模型",
|
|
}),
|
|
)}
|
|
</div>
|
|
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
{t("codexConfig.modelMappingHint", {
|
|
defaultValue:
|
|
"选择模型角色后,CC Switch 会自动生成 Codex 兼容路由;菜单显示名可以填 DeepSeek、Kimi 等品牌模型,实际请求模型按右侧填写内容发送。",
|
|
})}
|
|
</p>
|
|
</div>
|
|
|
|
{catalogRows.length > 0 && (
|
|
<div className="space-y-2">
|
|
{/* 列头:md+ 显示 */}
|
|
<div className="hidden grid-cols-[1fr_1fr_140px_36px] gap-2 px-1 text-xs font-medium text-muted-foreground md:grid">
|
|
<span>
|
|
{t("codexConfig.catalogColumnDisplay", {
|
|
defaultValue: "菜单显示名",
|
|
})}
|
|
</span>
|
|
<span>
|
|
{t("codexConfig.catalogColumnModel", {
|
|
defaultValue: "实际请求模型",
|
|
})}
|
|
</span>
|
|
<span>
|
|
{t("codexConfig.catalogColumnContext", {
|
|
defaultValue: "上下文窗口",
|
|
})}
|
|
</span>
|
|
<span />
|
|
</div>
|
|
|
|
{catalogRows.map((row, index) => (
|
|
<div
|
|
key={row.rowId}
|
|
className="grid grid-cols-1 gap-2 md:grid-cols-[1fr_1fr_140px_36px]"
|
|
>
|
|
<Input
|
|
value={row.displayName ?? ""}
|
|
onChange={(event) =>
|
|
handleUpdateCatalogRow(index, {
|
|
displayName: event.target.value,
|
|
})
|
|
}
|
|
placeholder={t(
|
|
"codexConfig.catalogDisplayNamePlaceholder",
|
|
{
|
|
defaultValue: "例如: DeepSeek V4 Flash",
|
|
},
|
|
)}
|
|
aria-label={t("codexConfig.catalogColumnDisplay", {
|
|
defaultValue: "菜单显示名",
|
|
})}
|
|
/>
|
|
<div className="flex gap-1">
|
|
<Input
|
|
value={row.model}
|
|
onChange={(event) =>
|
|
handleUpdateCatalogRow(index, {
|
|
model: event.target.value,
|
|
})
|
|
}
|
|
placeholder={t(
|
|
"codexConfig.catalogModelPlaceholder",
|
|
{
|
|
defaultValue: "例如: deepseek-v4-flash",
|
|
},
|
|
)}
|
|
aria-label={t("codexConfig.catalogColumnModel", {
|
|
defaultValue: "实际请求模型",
|
|
})}
|
|
className="flex-1"
|
|
/>
|
|
{fetchedModels.length > 0 && (
|
|
<ModelDropdown
|
|
models={fetchedModels}
|
|
onSelect={(id) =>
|
|
handleUpdateCatalogRow(index, {
|
|
model: id,
|
|
displayName: row.displayName?.trim()
|
|
? row.displayName
|
|
: id,
|
|
})
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
<Input
|
|
type="number"
|
|
min={1}
|
|
inputMode="numeric"
|
|
value={row.contextWindow ?? ""}
|
|
onChange={(event) =>
|
|
handleUpdateCatalogRow(index, {
|
|
contextWindow: event.target.value.replace(
|
|
/[^\d]/g,
|
|
"",
|
|
),
|
|
})
|
|
}
|
|
placeholder={t(
|
|
"codexConfig.contextWindowPlaceholder",
|
|
{
|
|
defaultValue: "例如: 128000",
|
|
},
|
|
)}
|
|
aria-label={t("codexConfig.catalogColumnContext", {
|
|
defaultValue: "上下文窗口",
|
|
})}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-9 w-9 text-muted-foreground hover:text-destructive"
|
|
onClick={() => handleRemoveCatalogRow(index)}
|
|
title={t("common.delete", { defaultValue: "删除" })}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
)}
|
|
|
|
{/* 端点测速弹窗 - Codex */}
|
|
{shouldShowSpeedTest && isEndpointModalOpen && (
|
|
<EndpointSpeedTest
|
|
appId="codex"
|
|
providerId={providerId}
|
|
value={codexBaseUrl}
|
|
onChange={onBaseUrlChange}
|
|
initialEndpoints={speedTestEndpoints}
|
|
visible={isEndpointModalOpen}
|
|
onClose={() => onEndpointModalToggle(false)}
|
|
autoSelect={autoSelect}
|
|
onAutoSelectChange={onAutoSelectChange}
|
|
onCustomEndpointsChange={onCustomEndpointsChange}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|