refactor(endpoint): separate edit and create mode endpoint management (#192)
Optimize custom endpoint management logic to distinguish between edit and create modes: - Edit mode: endpoints are read/written directly to backend via API - Create mode: use draftCustomEndpoints to stage, save on submit - Remove duplicate endpoint loading in useSpeedTestEndpoints - Add isSaving state and initialCustomUrls tracking
This commit is contained in:
@@ -34,7 +34,7 @@ interface ClaudeFormFieldsProps {
|
|||||||
onBaseUrlChange: (url: string) => void;
|
onBaseUrlChange: (url: string) => void;
|
||||||
isEndpointModalOpen: boolean;
|
isEndpointModalOpen: boolean;
|
||||||
onEndpointModalToggle: (open: boolean) => void;
|
onEndpointModalToggle: (open: boolean) => void;
|
||||||
onCustomEndpointsChange: (endpoints: string[]) => void;
|
onCustomEndpointsChange?: (endpoints: string[]) => void;
|
||||||
|
|
||||||
// Model Selector
|
// Model Selector
|
||||||
shouldShowModelSelector: boolean;
|
shouldShowModelSelector: boolean;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ interface CodexFormFieldsProps {
|
|||||||
onBaseUrlChange: (url: string) => void;
|
onBaseUrlChange: (url: string) => void;
|
||||||
isEndpointModalOpen: boolean;
|
isEndpointModalOpen: boolean;
|
||||||
onEndpointModalToggle: (open: boolean) => void;
|
onEndpointModalToggle: (open: boolean) => void;
|
||||||
onCustomEndpointsChange: (endpoints: string[]) => void;
|
onCustomEndpointsChange?: (endpoints: string[]) => void;
|
||||||
|
|
||||||
// Speed Test Endpoints
|
// Speed Test Endpoints
|
||||||
speedTestEndpoints: EndpointCandidate[];
|
speedTestEndpoints: EndpointCandidate[];
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ interface EndpointSpeedTestProps {
|
|||||||
initialEndpoints: EndpointCandidate[];
|
initialEndpoints: EndpointCandidate[];
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
// 当自定义端点列表变化时回传(仅包含 isCustom 的条目)
|
// 新建模式:当自定义端点列表变化时回传(仅包含 isCustom 的条目)
|
||||||
|
// 编辑模式:不使用此回调,端点直接保存到后端
|
||||||
onCustomEndpointsChange?: (urls: string[]) => void;
|
onCustomEndpointsChange?: (urls: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,25 +102,31 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
const [autoSelect, setAutoSelect] = useState(true);
|
const [autoSelect, setAutoSelect] = useState(true);
|
||||||
const [isTesting, setIsTesting] = useState(false);
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
const [lastError, setLastError] = useState<string | null>(null);
|
const [lastError, setLastError] = useState<string | null>(null);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
// 记录初始的自定义端点,用于对比变化
|
||||||
|
const [initialCustomUrls, setInitialCustomUrls] = useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
|
||||||
const normalizedSelected = normalizeEndpointUrl(value);
|
const normalizedSelected = normalizeEndpointUrl(value);
|
||||||
|
|
||||||
const hasEndpoints = entries.length > 0;
|
const hasEndpoints = entries.length > 0;
|
||||||
|
const isEditMode = Boolean(providerId); // 编辑模式有 providerId
|
||||||
|
|
||||||
// 加载保存的自定义端点(按正在编辑的供应商)
|
// 编辑模式:加载已保存的自定义端点
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
const loadCustomEndpoints = async () => {
|
const loadCustomEndpoints = async () => {
|
||||||
try {
|
try {
|
||||||
if (!providerId) return;
|
if (!providerId) return; // 新建模式不加载
|
||||||
|
|
||||||
const customEndpoints = await vscodeApi.getCustomEndpoints(
|
const customEndpoints = await vscodeApi.getCustomEndpoints(
|
||||||
appId,
|
appId,
|
||||||
providerId,
|
providerId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 检查是否已取消
|
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
const candidates: EndpointCandidate[] = customEndpoints.map(
|
const candidates: EndpointCandidate[] = customEndpoints.map(
|
||||||
@@ -129,6 +136,13 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 记录初始的自定义端点
|
||||||
|
const customUrls = new Set(
|
||||||
|
customEndpoints.map((ep) => normalizeEndpointUrl(ep.url)),
|
||||||
|
);
|
||||||
|
setInitialCustomUrls(customUrls);
|
||||||
|
|
||||||
|
// 合并自定义端点与初始端点
|
||||||
setEntries((prev) => {
|
setEntries((prev) => {
|
||||||
const map = new Map<string, EndpointEntry>();
|
const map = new Map<string, EndpointEntry>();
|
||||||
|
|
||||||
@@ -137,7 +151,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
map.set(entry.url, entry);
|
map.set(entry.url, entry);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 合并自定义端点
|
// 添加从后端加载的自定义端点
|
||||||
candidates.forEach((candidate) => {
|
candidates.forEach((candidate) => {
|
||||||
const sanitized = normalizeEndpointUrl(candidate.url);
|
const sanitized = normalizeEndpointUrl(candidate.url);
|
||||||
if (sanitized && !map.has(sanitized)) {
|
if (sanitized && !map.has(sanitized)) {
|
||||||
@@ -161,60 +175,20 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (visible) {
|
// 只在编辑模式下加载
|
||||||
|
if (providerId) {
|
||||||
loadCustomEndpoints();
|
loadCustomEndpoints();
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [appId, visible, providerId, t]);
|
}, [appId, providerId, t, initialEndpoints]);
|
||||||
|
|
||||||
|
// 新建模式:将自定义端点变化透传给父组件(仅限 isCustom)
|
||||||
|
// 编辑模式:不使用此回调,端点已通过 API 直接保存
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEntries((prev) => {
|
if (!onCustomEndpointsChange || isEditMode) return; // 编辑模式不使用回调
|
||||||
const map = new Map<string, EndpointEntry>();
|
|
||||||
prev.forEach((entry) => {
|
|
||||||
map.set(entry.url, entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
let changed = false;
|
|
||||||
|
|
||||||
const mergeCandidate = (candidate: EndpointCandidate) => {
|
|
||||||
const sanitized = candidate.url
|
|
||||||
? normalizeEndpointUrl(candidate.url)
|
|
||||||
: "";
|
|
||||||
if (!sanitized) return;
|
|
||||||
const existing = map.get(sanitized);
|
|
||||||
if (existing) return;
|
|
||||||
|
|
||||||
map.set(sanitized, {
|
|
||||||
id: candidate.id ?? randomId(),
|
|
||||||
url: sanitized,
|
|
||||||
isCustom: candidate.isCustom ?? false,
|
|
||||||
latency: null,
|
|
||||||
status: undefined,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
changed = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
initialEndpoints.forEach(mergeCandidate);
|
|
||||||
|
|
||||||
if (normalizedSelected && !map.has(normalizedSelected)) {
|
|
||||||
mergeCandidate({ url: normalizedSelected, isCustom: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!changed) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(map.values());
|
|
||||||
});
|
|
||||||
}, [initialEndpoints, normalizedSelected]);
|
|
||||||
|
|
||||||
// 将自定义端点变化透传给父组件(仅限 isCustom)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!onCustomEndpointsChange) return;
|
|
||||||
try {
|
try {
|
||||||
const customUrls = Array.from(
|
const customUrls = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
@@ -228,8 +202,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
// 仅在 entries 变化时同步
|
}, [entries, onCustomEndpointsChange, isEditMode]);
|
||||||
}, [entries, onCustomEndpointsChange]);
|
|
||||||
|
|
||||||
const sortedEntries = useMemo(() => {
|
const sortedEntries = useMemo(() => {
|
||||||
return entries.slice().sort((a: TestResult, b: TestResult) => {
|
return entries.slice().sort((a: TestResult, b: TestResult) => {
|
||||||
@@ -268,7 +241,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
let sanitized = "";
|
let sanitized = "";
|
||||||
if (!errorMsg && parsed) {
|
if (!errorMsg && parsed) {
|
||||||
sanitized = normalizeEndpointUrl(parsed.toString());
|
sanitized = normalizeEndpointUrl(parsed.toString());
|
||||||
// 使用当前 entries 做去重校验,避免依赖可能过期的 addError
|
// 使用当前 entries 做去重校验
|
||||||
const isDuplicate = entries.some((entry) => entry.url === sanitized);
|
const isDuplicate = entries.some((entry) => entry.url === sanitized);
|
||||||
if (isDuplicate) {
|
if (isDuplicate) {
|
||||||
errorMsg = t("endpointTest.urlExists");
|
errorMsg = t("endpointTest.urlExists");
|
||||||
@@ -281,8 +254,9 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setAddError(null);
|
setAddError(null);
|
||||||
|
setLastError(null);
|
||||||
|
|
||||||
// 更新本地状态(延迟提交,不立即保存到后端)
|
// 更新本地状态(延迟保存,点击保存按钮时统一处理)
|
||||||
setEntries((prev) => {
|
setEntries((prev) => {
|
||||||
if (prev.some((e) => e.url === sanitized)) return prev;
|
if (prev.some((e) => e.url === sanitized)) return prev;
|
||||||
return [
|
return [
|
||||||
@@ -303,14 +277,14 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setCustomUrl("");
|
setCustomUrl("");
|
||||||
}, [customUrl, entries, normalizedSelected, onChange]);
|
}, [customUrl, entries, normalizedSelected, onChange, t]);
|
||||||
|
|
||||||
const handleRemoveEndpoint = useCallback(
|
const handleRemoveEndpoint = useCallback(
|
||||||
(entry: EndpointEntry) => {
|
(entry: EndpointEntry) => {
|
||||||
// 清空之前的错误提示
|
// 清空之前的错误提示
|
||||||
setLastError(null);
|
setLastError(null);
|
||||||
|
|
||||||
// 更新本地状态(延迟提交,不立即从后端删除)
|
// 更新本地状态(延迟保存,点击保存按钮时统一处理)
|
||||||
setEntries((prev) => {
|
setEntries((prev) => {
|
||||||
const next = prev.filter((item) => item.id !== entry.id);
|
const next = prev.filter((item) => item.id !== entry.id);
|
||||||
if (entry.url === normalizedSelected) {
|
if (entry.url === normalizedSelected) {
|
||||||
@@ -405,6 +379,58 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
[normalizedSelected, onChange],
|
[normalizedSelected, onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 保存端点变更
|
||||||
|
const handleSave = useCallback(async () => {
|
||||||
|
// 编辑模式:对比初始端点和当前端点,批量保存变更
|
||||||
|
if (isEditMode && providerId) {
|
||||||
|
setIsSaving(true);
|
||||||
|
setLastError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取当前的自定义端点
|
||||||
|
const currentCustomUrls = new Set(
|
||||||
|
entries
|
||||||
|
.filter((e) => e.isCustom)
|
||||||
|
.map((e) => normalizeEndpointUrl(e.url)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 找出新增的端点
|
||||||
|
const toAdd = Array.from(currentCustomUrls).filter(
|
||||||
|
(url) => !initialCustomUrls.has(url),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 找出删除的端点
|
||||||
|
const toRemove = Array.from(initialCustomUrls).filter(
|
||||||
|
(url) => !currentCustomUrls.has(url),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 批量添加
|
||||||
|
for (const url of toAdd) {
|
||||||
|
await vscodeApi.addCustomEndpoint(appId, providerId, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除
|
||||||
|
for (const url of toRemove) {
|
||||||
|
await vscodeApi.removeCustomEndpoint(appId, providerId, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新初始端点列表
|
||||||
|
setInitialCustomUrls(currentCustomUrls);
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : t("endpointTest.saveFailed");
|
||||||
|
setLastError(message);
|
||||||
|
setIsSaving(false);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
onClose();
|
||||||
|
}, [isEditMode, providerId, entries, initialCustomUrls, appId, onClose, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={visible} onOpenChange={(open) => !open && onClose()}>
|
<Dialog open={visible} onOpenChange={(open) => !open && onClose()}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
@@ -580,10 +606,32 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter className="gap-2">
|
||||||
<Button type="button" onClick={onClose} className="gap-2">
|
<Button
|
||||||
<Save className="w-4 h-4" />
|
type="button"
|
||||||
{t("common.save")}
|
variant="outline"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
{t("common.saving")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Save className="w-4 h-4" />
|
||||||
|
{t("common.save")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -100,16 +100,16 @@ export function ProviderForm({
|
|||||||
partnerPromotionKey?: string;
|
partnerPromotionKey?: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
|
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
|
||||||
|
const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
// 新建供应商:收集端点测速弹窗中的"自定义端点",提交时一次性落盘到 meta.custom_endpoints
|
// 新建供应商:收集端点测速弹窗中的"自定义端点",提交时一次性落盘到 meta.custom_endpoints
|
||||||
// 编辑供应商:从 initialData.meta.custom_endpoints 恢复端点列表
|
// 编辑供应商:端点已通过 API 直接保存,不再需要此状态
|
||||||
const [draftCustomEndpoints, setDraftCustomEndpoints] = useState<string[]>(
|
const [draftCustomEndpoints, setDraftCustomEndpoints] = useState<string[]>(
|
||||||
() => {
|
() => {
|
||||||
if (!initialData?.meta?.custom_endpoints) {
|
// 仅在新建模式下使用
|
||||||
return [];
|
if (initialData) return [];
|
||||||
}
|
return [];
|
||||||
// 从 Record<string, CustomEndpoint> 中提取 URL 列表
|
|
||||||
return Object.keys(initialData.meta.custom_endpoints);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -125,10 +125,8 @@ export function ProviderForm({
|
|||||||
setSelectedPresetId(initialData ? null : "custom");
|
setSelectedPresetId(initialData ? null : "custom");
|
||||||
setActivePreset(null);
|
setActivePreset(null);
|
||||||
|
|
||||||
// 重新初始化 draftCustomEndpoints(编辑模式时从 meta 恢复)
|
// 编辑模式不需要恢复 draftCustomEndpoints,端点已通过 API 管理
|
||||||
if (initialData?.meta?.custom_endpoints) {
|
if (!initialData) {
|
||||||
setDraftCustomEndpoints(Object.keys(initialData.meta.custom_endpoints));
|
|
||||||
} else {
|
|
||||||
setDraftCustomEndpoints([]);
|
setDraftCustomEndpoints([]);
|
||||||
}
|
}
|
||||||
}, [appId, initialData]);
|
}, [appId, initialData]);
|
||||||
@@ -220,8 +218,6 @@ export function ProviderForm({
|
|||||||
[originalHandleCodexConfigChange, debouncedValidate],
|
[originalHandleCodexConfigChange, debouncedValidate],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] =
|
|
||||||
useState(false);
|
|
||||||
const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] =
|
const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
@@ -361,60 +357,51 @@ export function ProviderForm({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 meta 字段:基于 draftCustomEndpoints 生成 custom_endpoints
|
// 处理 meta 字段:仅在新建模式下从 draftCustomEndpoints 生成 custom_endpoints
|
||||||
// 注意:不使用 customEndpointsMap,因为它包含了候选端点(预设、Base URL 等)
|
// 编辑模式:端点已通过 API 直接保存,不在此处理
|
||||||
// 而我们只需要保存用户真正添加的自定义端点
|
if (!isEditMode && draftCustomEndpoints.length > 0) {
|
||||||
const customEndpointsToSave: Record<
|
const customEndpointsToSave: Record<
|
||||||
string,
|
string,
|
||||||
import("@/types").CustomEndpoint
|
import("@/types").CustomEndpoint
|
||||||
> | null =
|
> = draftCustomEndpoints.reduce(
|
||||||
draftCustomEndpoints.length > 0
|
(acc, url) => {
|
||||||
? draftCustomEndpoints.reduce(
|
const now = Date.now();
|
||||||
(acc, url) => {
|
acc[url] = { url, addedAt: now, lastUsed: undefined };
|
||||||
// 尝试从 initialData.meta 中获取原有的端点元数据(保留 addedAt 和 lastUsed)
|
return acc;
|
||||||
const existing = initialData?.meta?.custom_endpoints?.[url];
|
},
|
||||||
if (existing) {
|
{} as Record<string, import("@/types").CustomEndpoint>,
|
||||||
acc[url] = existing;
|
);
|
||||||
} else {
|
|
||||||
// 新端点:使用当前时间戳
|
|
||||||
const now = Date.now();
|
|
||||||
acc[url] = { url, addedAt: now, lastUsed: undefined };
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, import("@/types").CustomEndpoint>,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// 检测是否需要清空端点(重要:区分"用户清空端点"和"用户没有修改端点")
|
// 检测是否需要清空端点(重要:区分"用户清空端点"和"用户没有修改端点")
|
||||||
const hadEndpoints =
|
const hadEndpoints =
|
||||||
initialData?.meta?.custom_endpoints &&
|
initialData?.meta?.custom_endpoints &&
|
||||||
Object.keys(initialData.meta.custom_endpoints).length > 0;
|
Object.keys(initialData.meta.custom_endpoints).length > 0;
|
||||||
const needsClearEndpoints =
|
const needsClearEndpoints =
|
||||||
hadEndpoints && draftCustomEndpoints.length === 0;
|
hadEndpoints && draftCustomEndpoints.length === 0;
|
||||||
|
|
||||||
// 如果用户明确清空了端点,传递空对象(而不是 null)让后端知道要删除
|
// 如果用户明确清空了端点,传递空对象(而不是 null)让后端知道要删除
|
||||||
let mergedMeta = needsClearEndpoints
|
let mergedMeta = needsClearEndpoints
|
||||||
? mergeProviderMeta(initialData?.meta, {})
|
? mergeProviderMeta(initialData?.meta, {})
|
||||||
: mergeProviderMeta(initialData?.meta, customEndpointsToSave);
|
: mergeProviderMeta(initialData?.meta, customEndpointsToSave);
|
||||||
|
|
||||||
// 添加合作伙伴标识与促销 key
|
// 添加合作伙伴标识与促销 key
|
||||||
if (activePreset?.isPartner) {
|
if (activePreset?.isPartner) {
|
||||||
mergedMeta = {
|
mergedMeta = {
|
||||||
...(mergedMeta ?? {}),
|
...(mergedMeta ?? {}),
|
||||||
isPartner: true,
|
isPartner: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activePreset?.partnerPromotionKey) {
|
if (activePreset?.partnerPromotionKey) {
|
||||||
mergedMeta = {
|
mergedMeta = {
|
||||||
...(mergedMeta ?? {}),
|
...(mergedMeta ?? {}),
|
||||||
partnerPromotionKey: activePreset.partnerPromotionKey,
|
partnerPromotionKey: activePreset.partnerPromotionKey,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mergedMeta !== undefined) {
|
if (mergedMeta !== undefined) {
|
||||||
payload.meta = mergedMeta;
|
payload.meta = mergedMeta;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(payload);
|
onSubmit(payload);
|
||||||
@@ -609,7 +596,9 @@ export function ProviderForm({
|
|||||||
onBaseUrlChange={handleClaudeBaseUrlChange}
|
onBaseUrlChange={handleClaudeBaseUrlChange}
|
||||||
isEndpointModalOpen={isEndpointModalOpen}
|
isEndpointModalOpen={isEndpointModalOpen}
|
||||||
onEndpointModalToggle={setIsEndpointModalOpen}
|
onEndpointModalToggle={setIsEndpointModalOpen}
|
||||||
onCustomEndpointsChange={setDraftCustomEndpoints}
|
onCustomEndpointsChange={
|
||||||
|
isEditMode ? undefined : setDraftCustomEndpoints
|
||||||
|
}
|
||||||
shouldShowModelSelector={category !== "official"}
|
shouldShowModelSelector={category !== "official"}
|
||||||
claudeModel={claudeModel}
|
claudeModel={claudeModel}
|
||||||
defaultHaikuModel={defaultHaikuModel}
|
defaultHaikuModel={defaultHaikuModel}
|
||||||
@@ -636,7 +625,9 @@ export function ProviderForm({
|
|||||||
onBaseUrlChange={handleCodexBaseUrlChange}
|
onBaseUrlChange={handleCodexBaseUrlChange}
|
||||||
isEndpointModalOpen={isCodexEndpointModalOpen}
|
isEndpointModalOpen={isCodexEndpointModalOpen}
|
||||||
onEndpointModalToggle={setIsCodexEndpointModalOpen}
|
onEndpointModalToggle={setIsCodexEndpointModalOpen}
|
||||||
onCustomEndpointsChange={setDraftCustomEndpoints}
|
onCustomEndpointsChange={
|
||||||
|
isEditMode ? undefined : setDraftCustomEndpoints
|
||||||
|
}
|
||||||
speedTestEndpoints={speedTestEndpoints}
|
speedTestEndpoints={speedTestEndpoints}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -25,10 +25,12 @@ interface UseSpeedTestEndpointsProps {
|
|||||||
* 收集端点测速弹窗的初始端点列表
|
* 收集端点测速弹窗的初始端点列表
|
||||||
*
|
*
|
||||||
* 收集来源:
|
* 收集来源:
|
||||||
* 1. 编辑模式:从 meta.custom_endpoints 读取已保存的端点(优先)
|
* 1. 当前选中的 Base URL
|
||||||
* 2. 当前选中的 Base URL
|
* 2. 编辑模式下的初始数据 URL
|
||||||
* 3. 编辑模式下的初始数据 URL
|
* 3. 预设中的 endpointCandidates
|
||||||
* 4. 预设中的 endpointCandidates
|
*
|
||||||
|
* 注意:已保存的自定义端点通过 getCustomEndpoints API 在 EndpointSpeedTest 组件中加载,
|
||||||
|
* 不在此处读取,避免重复导入。
|
||||||
*/
|
*/
|
||||||
export function useSpeedTestEndpoints({
|
export function useSpeedTestEndpoints({
|
||||||
appId,
|
appId,
|
||||||
@@ -43,28 +45,21 @@ export function useSpeedTestEndpoints({
|
|||||||
if (appId !== "claude" && appId !== "gemini") return [];
|
if (appId !== "claude" && appId !== "gemini") return [];
|
||||||
|
|
||||||
const map = new Map<string, EndpointCandidate>();
|
const map = new Map<string, EndpointCandidate>();
|
||||||
// 所有端点都标记为 isCustom: true,给用户完全的管理自由
|
// 候选端点标记为 isCustom: false,表示来自预设或配置
|
||||||
const add = (url?: string) => {
|
// 已保存的自定义端点会在 EndpointSpeedTest 组件中通过 API 加载
|
||||||
|
const add = (url?: string, isCustom = false) => {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
const sanitized = url.trim().replace(/\/+$/, "");
|
const sanitized = url.trim().replace(/\/+$/, "");
|
||||||
if (!sanitized || map.has(sanitized)) return;
|
if (!sanitized || map.has(sanitized)) return;
|
||||||
map.set(sanitized, { url: sanitized, isCustom: true });
|
map.set(sanitized, { url: sanitized, isCustom });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. 编辑模式:从 meta.custom_endpoints 读取已保存的端点(优先)
|
// 1. 当前 Base URL
|
||||||
if (initialData?.meta?.custom_endpoints) {
|
|
||||||
const customEndpoints = initialData.meta.custom_endpoints;
|
|
||||||
for (const url of Object.keys(customEndpoints)) {
|
|
||||||
add(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 当前 Base URL
|
|
||||||
if (baseUrl) {
|
if (baseUrl) {
|
||||||
add(baseUrl);
|
add(baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 编辑模式:初始数据中的 URL
|
// 2. 编辑模式:初始数据中的 URL
|
||||||
if (initialData && typeof initialData.settingsConfig === "object") {
|
if (initialData && typeof initialData.settingsConfig === "object") {
|
||||||
const configEnv = initialData.settingsConfig as {
|
const configEnv = initialData.settingsConfig as {
|
||||||
env?: { ANTHROPIC_BASE_URL?: string; GOOGLE_GEMINI_BASE_URL?: string };
|
env?: { ANTHROPIC_BASE_URL?: string; GOOGLE_GEMINI_BASE_URL?: string };
|
||||||
@@ -78,7 +73,7 @@ export function useSpeedTestEndpoints({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 预设中的 endpointCandidates(也允许用户删除)
|
// 3. 预设中的 endpointCandidates
|
||||||
if (selectedPresetId && selectedPresetId !== "custom") {
|
if (selectedPresetId && selectedPresetId !== "custom") {
|
||||||
const entry = presetEntries.find((item) => item.id === selectedPresetId);
|
const entry = presetEntries.find((item) => item.id === selectedPresetId);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
@@ -112,28 +107,21 @@ export function useSpeedTestEndpoints({
|
|||||||
if (appId !== "codex") return [];
|
if (appId !== "codex") return [];
|
||||||
|
|
||||||
const map = new Map<string, EndpointCandidate>();
|
const map = new Map<string, EndpointCandidate>();
|
||||||
// 所有端点都标记为 isCustom: true,给用户完全的管理自由
|
// 候选端点标记为 isCustom: false,表示来自预设或配置
|
||||||
const add = (url?: string) => {
|
// 已保存的自定义端点会在 EndpointSpeedTest 组件中通过 API 加载
|
||||||
|
const add = (url?: string, isCustom = false) => {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
const sanitized = url.trim().replace(/\/+$/, "");
|
const sanitized = url.trim().replace(/\/+$/, "");
|
||||||
if (!sanitized || map.has(sanitized)) return;
|
if (!sanitized || map.has(sanitized)) return;
|
||||||
map.set(sanitized, { url: sanitized, isCustom: true });
|
map.set(sanitized, { url: sanitized, isCustom });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. 编辑模式:从 meta.custom_endpoints 读取已保存的端点(优先)
|
// 1. 当前 Codex Base URL
|
||||||
if (initialData?.meta?.custom_endpoints) {
|
|
||||||
const customEndpoints = initialData.meta.custom_endpoints;
|
|
||||||
for (const url of Object.keys(customEndpoints)) {
|
|
||||||
add(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 当前 Codex Base URL
|
|
||||||
if (codexBaseUrl) {
|
if (codexBaseUrl) {
|
||||||
add(codexBaseUrl);
|
add(codexBaseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 编辑模式:初始数据中的 URL
|
// 2. 编辑模式:初始数据中的 URL
|
||||||
const initialCodexConfig = initialData?.settingsConfig as
|
const initialCodexConfig = initialData?.settingsConfig as
|
||||||
| {
|
| {
|
||||||
config?: string;
|
config?: string;
|
||||||
@@ -146,7 +134,7 @@ export function useSpeedTestEndpoints({
|
|||||||
add(match[1]);
|
add(match[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 预设中的 endpointCandidates(也允许用户删除)
|
// 3. 预设中的 endpointCandidates
|
||||||
if (selectedPresetId && selectedPresetId !== "custom") {
|
if (selectedPresetId && selectedPresetId !== "custom") {
|
||||||
const entry = presetEntries.find((item) => item.id === selectedPresetId);
|
const entry = presetEntries.find((item) => item.id === selectedPresetId);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
|
|||||||
Reference in New Issue
Block a user