fix(providers): preserve custom endpoints in meta during add/edit operations
Fixed two critical data loss bugs where user-added custom endpoints were discarded: 1. **AddProviderDialog**: Form submission ignored values.meta from ProviderForm and re-inferred URLs only from presets/config, causing loss of endpoints added via speed test modal. Now prioritizes form-collected meta and uses fallback inference only when custom_endpoints is missing. 2. **ProviderForm**: Edit mode always returned initialData.meta, discarding any changes made in the speed test modal. Now uses mergeProviderMeta to properly merge customEndpointsMap with existing meta fields. Changes: - Extract mergeProviderMeta utility to handle meta field merging logic - Preserve other meta fields (e.g., usage_script) during endpoint updates - Unify new/edit code paths to use consistent meta handling - Add comprehensive unit tests for meta merging scenarios - Add integration tests for AddProviderDialog submission flow Impact: - Third-party and custom providers can now reliably manage multiple endpoints - Edit operations correctly reflect user modifications - No data loss for existing meta fields like usage_script
This commit is contained in:
@@ -47,84 +47,93 @@ export function AddProviderDialog({
|
||||
websiteUrl: values.websiteUrl?.trim() || undefined,
|
||||
settingsConfig: parsedConfig,
|
||||
...(values.presetCategory ? { category: values.presetCategory } : {}),
|
||||
...(values.meta ? { meta: values.meta } : {}),
|
||||
};
|
||||
|
||||
// 收集端点候选(仅新增供应商时)
|
||||
// 1. 从预设配置中获取 endpointCandidates
|
||||
// 2. 从当前配置中提取 baseUrl (ANTHROPIC_BASE_URL 或 Codex base_url)
|
||||
const urlSet = new Set<string>();
|
||||
const hasCustomEndpoints =
|
||||
providerData.meta?.custom_endpoints &&
|
||||
Object.keys(providerData.meta.custom_endpoints).length > 0;
|
||||
|
||||
const addUrl = (rawUrl?: string) => {
|
||||
const url = (rawUrl || "").trim().replace(/\/+$/, "");
|
||||
if (url && url.startsWith("http")) {
|
||||
urlSet.add(url);
|
||||
}
|
||||
};
|
||||
if (!hasCustomEndpoints) {
|
||||
// 收集端点候选(仅在缺少自定义端点时兜底)
|
||||
// 1. 从预设配置中获取 endpointCandidates
|
||||
// 2. 从当前配置中提取 baseUrl (ANTHROPIC_BASE_URL 或 Codex base_url)
|
||||
const urlSet = new Set<string>();
|
||||
|
||||
// 如果选择了预设,获取预设中的 endpointCandidates
|
||||
if (values.presetId) {
|
||||
if (appType === "claude") {
|
||||
const presets = providerPresets;
|
||||
const presetIndex = parseInt(values.presetId.replace("claude-", ""));
|
||||
if (
|
||||
!isNaN(presetIndex) &&
|
||||
presetIndex >= 0 &&
|
||||
presetIndex < presets.length
|
||||
) {
|
||||
const preset = presets[presetIndex];
|
||||
if (preset?.endpointCandidates) {
|
||||
preset.endpointCandidates.forEach(addUrl);
|
||||
const addUrl = (rawUrl?: string) => {
|
||||
const url = (rawUrl || "").trim().replace(/\/+$/, "");
|
||||
if (url && url.startsWith("http")) {
|
||||
urlSet.add(url);
|
||||
}
|
||||
};
|
||||
|
||||
if (values.presetId) {
|
||||
if (appType === "claude") {
|
||||
const presets = providerPresets;
|
||||
const presetIndex = parseInt(
|
||||
values.presetId.replace("claude-", ""),
|
||||
);
|
||||
if (
|
||||
!isNaN(presetIndex) &&
|
||||
presetIndex >= 0 &&
|
||||
presetIndex < presets.length
|
||||
) {
|
||||
const preset = presets[presetIndex];
|
||||
if (preset?.endpointCandidates) {
|
||||
preset.endpointCandidates.forEach(addUrl);
|
||||
}
|
||||
}
|
||||
} else if (appType === "codex") {
|
||||
const presets = codexProviderPresets;
|
||||
const presetIndex = parseInt(
|
||||
values.presetId.replace("codex-", ""),
|
||||
);
|
||||
if (
|
||||
!isNaN(presetIndex) &&
|
||||
presetIndex >= 0 &&
|
||||
presetIndex < presets.length
|
||||
) {
|
||||
const preset = presets[presetIndex];
|
||||
if (Array.isArray(preset.endpointCandidates)) {
|
||||
preset.endpointCandidates.forEach(addUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (appType === "claude") {
|
||||
const env = parsedConfig.env as Record<string, any> | undefined;
|
||||
if (env?.ANTHROPIC_BASE_URL) {
|
||||
addUrl(env.ANTHROPIC_BASE_URL);
|
||||
}
|
||||
} else if (appType === "codex") {
|
||||
const presets = codexProviderPresets;
|
||||
const presetIndex = parseInt(values.presetId.replace("codex-", ""));
|
||||
if (
|
||||
!isNaN(presetIndex) &&
|
||||
presetIndex >= 0 &&
|
||||
presetIndex < presets.length
|
||||
) {
|
||||
const preset = presets[presetIndex];
|
||||
if ((preset as any).endpointCandidates) {
|
||||
(preset as any).endpointCandidates.forEach(addUrl);
|
||||
const config = parsedConfig.config as string | undefined;
|
||||
if (config) {
|
||||
const baseUrlMatch =
|
||||
config.match(/base_url\s*=\s*["']([^"']+)["']/);
|
||||
if (baseUrlMatch?.[1]) {
|
||||
addUrl(baseUrlMatch[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从当前配置中提取 baseUrl
|
||||
if (appType === "claude") {
|
||||
const env = parsedConfig.env as Record<string, any> | undefined;
|
||||
if (env?.ANTHROPIC_BASE_URL) {
|
||||
addUrl(env.ANTHROPIC_BASE_URL);
|
||||
}
|
||||
} else if (appType === "codex") {
|
||||
// Codex 的 baseUrl 在 config.toml 字符串中
|
||||
const config = parsedConfig.config as string | undefined;
|
||||
if (config) {
|
||||
const baseUrlMatch = config.match(/base_url\s*=\s*["']([^"']+)["']/);
|
||||
if (baseUrlMatch?.[1]) {
|
||||
addUrl(baseUrlMatch[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
const urls = Array.from(urlSet);
|
||||
if (urls.length > 0) {
|
||||
const now = Date.now();
|
||||
const customEndpoints: Record<string, CustomEndpoint> = {};
|
||||
urls.forEach((url) => {
|
||||
customEndpoints[url] = {
|
||||
url,
|
||||
addedAt: now,
|
||||
lastUsed: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
// 如果收集到了端点,添加到 meta.custom_endpoints
|
||||
const urls = Array.from(urlSet);
|
||||
if (urls.length > 0) {
|
||||
const now = Date.now();
|
||||
const customEndpoints: Record<string, CustomEndpoint> = {};
|
||||
urls.forEach((url) => {
|
||||
customEndpoints[url] = {
|
||||
url,
|
||||
addedAt: now,
|
||||
lastUsed: undefined,
|
||||
providerData.meta = {
|
||||
...(providerData.meta ?? {}),
|
||||
custom_endpoints: customEndpoints,
|
||||
};
|
||||
});
|
||||
|
||||
providerData.meta = {
|
||||
custom_endpoints: customEndpoints,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await onSubmit(providerData);
|
||||
|
||||
@@ -6,13 +6,14 @@ import { Button } from "@/components/ui/button";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider";
|
||||
import type { AppType } from "@/lib/api";
|
||||
import type { ProviderCategory, CustomEndpoint, ProviderMeta } from "@/types";
|
||||
import type { ProviderCategory, ProviderMeta } from "@/types";
|
||||
import { providerPresets, type ProviderPreset } from "@/config/providerPresets";
|
||||
import {
|
||||
codexProviderPresets,
|
||||
type CodexProviderPreset,
|
||||
} from "@/config/codexProviderPresets";
|
||||
import { applyTemplateValues } from "@/utils/providerConfigUtils";
|
||||
import { mergeProviderMeta } from "@/utils/providerMetaUtils";
|
||||
import CodexConfigEditor from "./CodexConfigEditor";
|
||||
import { CommonConfigEditor } from "./CommonConfigEditor";
|
||||
import { ProviderPresetSelector } from "./ProviderPresetSelector";
|
||||
@@ -324,12 +325,9 @@ export function ProviderForm({
|
||||
}
|
||||
|
||||
// 处理 meta 字段(新建与编辑使用不同策略)
|
||||
if (initialData?.meta) {
|
||||
// 编辑模式:后端已通过 API 更新 meta,直接使用原有值
|
||||
payload.meta = initialData.meta;
|
||||
} else if (customEndpointsMap) {
|
||||
// 新建模式:从表单收集的自定义端点打包到 meta
|
||||
payload.meta = { custom_endpoints: customEndpointsMap };
|
||||
const mergedMeta = mergeProviderMeta(initialData?.meta, customEndpointsMap);
|
||||
if (mergedMeta) {
|
||||
payload.meta = mergedMeta;
|
||||
}
|
||||
|
||||
onSubmit(payload);
|
||||
@@ -580,7 +578,5 @@ export function ProviderForm({
|
||||
export type ProviderFormValues = ProviderFormData & {
|
||||
presetId?: string;
|
||||
presetCategory?: ProviderCategory;
|
||||
meta?: {
|
||||
custom_endpoints?: Record<string, CustomEndpoint>;
|
||||
};
|
||||
meta?: ProviderMeta;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user